diff --git a/CMakeLists.txt b/CMakeLists.txt index a30e9a4f..dde22adf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -711,6 +711,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_server_config.cpp src/pipeline/wowee_anniversary_events.cpp src/pipeline/wowee_pvp_ranks.cpp + src/pipeline/wowee_localization.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1585,6 +1586,7 @@ add_executable(wowee_editor tools/editor/cli_server_config_catalog.cpp tools/editor/cli_anniversary_events_catalog.cpp tools/editor/cli_pvp_ranks_catalog.cpp + tools/editor/cli_localization_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_catalog_by_name.cpp @@ -1778,6 +1780,7 @@ add_executable(wowee_editor src/pipeline/wowee_server_config.cpp src/pipeline/wowee_anniversary_events.cpp src/pipeline/wowee_pvp_ranks.cpp + src/pipeline/wowee_localization.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_localization.hpp b/include/pipeline/wowee_localization.hpp new file mode 100644 index 00000000..dcd07352 --- /dev/null +++ b/include/pipeline/wowee_localization.hpp @@ -0,0 +1,141 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Localization catalog (.wlan) — novel +// replacement for the per-language overlay tables that +// vanilla WoW carried as Locale_*.MPQ patches plus the +// Spell.dbc / Item.dbc trailing 16-locale string +// columns. Each entry binds one (originalKey, +// languageCode, namespace) triple to its localized +// translation. +// +// Cross-references with previously-added formats: +// No catalog cross-references — WLAN is a pure +// string-table overlay applied AFTER any per-format +// catalog has resolved its primary text. The lookup +// path is: format-default text -> WLAN override (if +// client locale matches a WLAN entry) -> rendered +// text. +// +// Binary layout (little-endian): +// magic[4] = "WLAN" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// stringId (uint32) +// nameLen + name (English / catalog label) +// descLen + description (translator notes) +// languageCode (uint8) — enUS / enGB / deDE / +// esES / frFR / itIT / +// koKR / ptBR / ruRU / +// zhCN / zhTW +// namespace_ (uint8) — UI / Quest / Item / +// Spell / Creature / +// Tooltip / Gossip / +// System +// pad0 (uint8) / pad1 (uint8) +// keyLen + originalKey — lookup key (canonical +// form — usually the +// English source text +// or a dotted ID like +// "QUEST.123.title") +// locLen + localizedText — translation in the +// target language +// iconColorRGBA (uint32) +struct WoweeLocalization { + enum LanguageCode : uint8_t { + enUS = 0, // US English + enGB = 1, // UK English (variant of enUS for + // colour / armour / etc.) + deDE = 2, // German + esES = 3, // European Spanish + frFR = 4, // French + itIT = 5, // Italian + koKR = 6, // Korean + ptBR = 7, // Brazilian Portuguese + ruRU = 8, // Russian + zhCN = 9, // Simplified Chinese + zhTW = 10, // Traditional Chinese + Unknown = 255, + }; + + enum Namespace : uint8_t { + UI = 0, // button labels, menus + Quest = 1, // quest titles + objective + // text + Item = 2, // item names + descriptions + Spell = 3, // spell names + tooltips + Creature = 4, // creature display names + Tooltip = 5, // shared tooltip strings + Gossip = 6, // NPC gossip dialog + System = 7, // system messages / + // notifications + }; + + struct Entry { + uint32_t stringId = 0; + std::string name; + std::string description; + uint8_t languageCode = enUS; + uint8_t namespace_ = UI; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + std::string originalKey; + std::string localizedText; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t stringId) const; + + // Returns the localized override (if any) for a + // given (key, language, namespace) lookup. Used at + // every render-call by the locale-aware text layer. + const Entry* findOverride(const std::string& originalKey, + uint8_t languageCode, + uint8_t namespaceKind) const; + + // Returns all entries in one language. Used by the + // per-language asset bundling step to package only + // the strings the client needs. + std::vector findByLanguage(uint8_t languageCode) const; +}; + +class WoweeLocalizationLoader { +public: + static bool save(const WoweeLocalization& cat, + const std::string& basePath); + static WoweeLocalization load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-lan* variants. + // + // makeUIBasics — 5 UI-button strings translated + // to deDE/frFR/esES/koKR/zhCN + // (5 entries × 1 key — the + // "Cancel" button across 5 + // languages). + // makeQuestSample — 3 entries — one quest title + // translated into deDE / frFR / + // koKR. + // makeTooltipSet — 4 item tooltip strings in + // deDE + frFR (high-volume use + // case for client localization). + static WoweeLocalization makeUIBasics(const std::string& catalogName); + static WoweeLocalization makeQuestSample(const std::string& catalogName); + static WoweeLocalization makeTooltipSet(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_localization.cpp b/src/pipeline/wowee_localization.cpp new file mode 100644 index 00000000..2e94b8a9 --- /dev/null +++ b/src/pipeline/wowee_localization.cpp @@ -0,0 +1,283 @@ +#include "pipeline/wowee_localization.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'L', 'A', 'N'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(s.size()); + writePOD(os, n); + if (n > 0) os.write(s.data(), n); +} + +bool readStr(std::ifstream& is, std::string& s) { + uint32_t n = 0; + if (!readPOD(is, n)) return false; + if (n > (1u << 20)) return false; + s.resize(n); + if (n > 0) { + is.read(s.data(), n); + if (is.gcount() != static_cast(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".wlan") { + base += ".wlan"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweeLocalization::Entry* +WoweeLocalization::findById(uint32_t stringId) const { + for (const auto& e : entries) + if (e.stringId == stringId) return &e; + return nullptr; +} + +const WoweeLocalization::Entry* +WoweeLocalization::findOverride(const std::string& originalKey, + uint8_t languageCode, + uint8_t namespaceKind) const { + for (const auto& e : entries) { + if (e.languageCode == languageCode && + e.namespace_ == namespaceKind && + e.originalKey == originalKey) { + return &e; + } + } + return nullptr; +} + +std::vector +WoweeLocalization::findByLanguage(uint8_t languageCode) const { + std::vector out; + for (const auto& e : entries) + if (e.languageCode == languageCode) out.push_back(&e); + return out; +} + +bool WoweeLocalizationLoader::save(const WoweeLocalization& cat, + const std::string& basePath) { + std::ofstream os(normalizePath(basePath), std::ios::binary); + if (!os) return false; + os.write(kMagic, 4); + writePOD(os, kVersion); + writeStr(os, cat.name); + uint32_t entryCount = static_cast(cat.entries.size()); + writePOD(os, entryCount); + for (const auto& e : cat.entries) { + writePOD(os, e.stringId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.languageCode); + writePOD(os, e.namespace_); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writeStr(os, e.originalKey); + writeStr(os, e.localizedText); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeLocalization WoweeLocalizationLoader::load( + const std::string& basePath) { + WoweeLocalization out; + std::ifstream is(normalizePath(basePath), std::ios::binary); + if (!is) return out; + char magic[4]; + is.read(magic, 4); + if (std::memcmp(magic, kMagic, 4) != 0) return out; + uint32_t version = 0; + if (!readPOD(is, version) || version != kVersion) return out; + if (!readStr(is, out.name)) return out; + uint32_t entryCount = 0; + if (!readPOD(is, entryCount)) return out; + if (entryCount > (1u << 20)) return out; + out.entries.resize(entryCount); + for (auto& e : out.entries) { + if (!readPOD(is, e.stringId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.languageCode) || + !readPOD(is, e.namespace_) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.originalKey) || + !readStr(is, e.localizedText)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeLocalizationLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeLocalization WoweeLocalizationLoader::makeUIBasics( + const std::string& catalogName) { + using L = WoweeLocalization; + WoweeLocalization c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t lang, const char* key, + const char* localized, + const char* desc) { + L::Entry e; + e.stringId = id; e.name = name; e.description = desc; + e.languageCode = lang; + e.namespace_ = L::UI; + e.originalKey = key; + e.localizedText = localized; + e.iconColorRGBA = packRgba(140, 200, 255); // ui blue + c.entries.push_back(e); + }; + // Translations of the "Cancel" button across 5 + // languages — common UI string used in every dialog + // box. + add(1, "Cancel_deDE", L::deDE, + "Cancel", "Abbrechen", + "German UI: 'Cancel' button."); + add(2, "Cancel_frFR", L::frFR, + "Cancel", "Annuler", + "French UI: 'Cancel' button."); + add(3, "Cancel_esES", L::esES, + "Cancel", "Cancelar", + "Spanish UI: 'Cancel' button."); + add(4, "Cancel_koKR", L::koKR, + "Cancel", "취소", + "Korean UI: 'Cancel' button. Multibyte UTF-8 " + "round-trip preserved."); + add(5, "Cancel_zhCN", L::zhCN, + "Cancel", "取消", + "Simplified Chinese UI: 'Cancel' button."); + return c; +} + +WoweeLocalization WoweeLocalizationLoader::makeQuestSample( + const std::string& catalogName) { + using L = WoweeLocalization; + WoweeLocalization c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t lang, const char* key, + const char* localized, + const char* desc) { + L::Entry e; + e.stringId = id; e.name = name; e.description = desc; + e.languageCode = lang; + e.namespace_ = L::Quest; + e.originalKey = key; + e.localizedText = localized; + e.iconColorRGBA = packRgba(220, 220, 100); // quest gold + c.entries.push_back(e); + }; + // One quest title in 3 languages — illustrates the + // dotted-key convention "QUEST.123.title". + add(100, "Quest123Title_deDE", L::deDE, + "QUEST.123.title", + "Die Verwüsteten Lande", + "Quest 123 title in German — placeholder " + "translation of 'The Blasted Lands'."); + add(101, "Quest123Title_frFR", L::frFR, + "QUEST.123.title", + "Les Terres foudroyees", + "Quest 123 title in French — note: ASCII-only " + "approximation of 'foudroyées' to keep this " + "source file ASCII-clean."); + add(102, "Quest123Title_koKR", L::koKR, + "QUEST.123.title", + "황폐의 땅", + "Quest 123 title in Korean."); + return c; +} + +WoweeLocalization WoweeLocalizationLoader::makeTooltipSet( + const std::string& catalogName) { + using L = WoweeLocalization; + WoweeLocalization c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t lang, const char* key, + const char* localized, + const char* desc) { + L::Entry e; + e.stringId = id; e.name = name; e.description = desc; + e.languageCode = lang; + e.namespace_ = L::Tooltip; + e.originalKey = key; + e.localizedText = localized; + e.iconColorRGBA = packRgba(180, 220, 180); // tooltip green + c.entries.push_back(e); + }; + // Item tooltip strings — high-volume client + // localization use case. 4 strings × 2 languages + // = 4 entries (deDE: 2, frFR: 2). + add(200, "BindOnPickup_deDE", L::deDE, + "TOOLTIP.BindOnPickup", + "Bei Aufnahme gebunden", + "Tooltip line: 'Bind on Pickup' in German. " + "Common to most epic items."); + add(201, "BindOnPickup_frFR", L::frFR, + "TOOLTIP.BindOnPickup", + "Lie quand ramasse", + "Tooltip line: 'Bind on Pickup' in French. " + "ASCII approximation."); + add(202, "Unique_deDE", L::deDE, + "TOOLTIP.Unique", + "Einzigartig", + "Tooltip line: 'Unique' (cannot equip more " + "than one) in German."); + add(203, "Unique_frFR", L::frFR, + "TOOLTIP.Unique", + "Unique", + "Tooltip line: 'Unique' in French (same word, " + "different pronunciation)."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index bc1c19d4..9580190d 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -377,6 +377,8 @@ const char* const kArgRequired[] = { "--gen-prg", "--gen-prg-horde", "--gen-prg-high", "--info-wprg", "--validate-wprg", "--export-wprg-json", "--import-wprg-json", + "--gen-lan", "--gen-lan-quest", "--gen-lan-tooltip", + "--info-wlan", "--validate-wlan", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 7de43d3d..979d2b04 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -167,6 +167,7 @@ #include "cli_server_config_catalog.hpp" #include "cli_anniversary_events_catalog.hpp" #include "cli_pvp_ranks_catalog.hpp" +#include "cli_localization_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_catalog_by_name.hpp" @@ -379,6 +380,7 @@ constexpr DispatchFn kDispatchTable[] = { handleServerConfigCatalog, handleAnniversaryEventsCatalog, handlePvPRanksCatalog, + handleLocalizationCatalog, handleCatalogPluck, handleCatalogFind, handleCatalogByName, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 75849832..2e0603d6 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -125,6 +125,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','C','F','G'}, ".wcfg", "server", "--info-wcfg", "Server config catalog"}, {{'W','A','N','V'}, ".wanv", "events", "--info-wanv", "Anniversary & recurring event catalog"}, {{'W','P','R','G'}, ".wprg", "pvp", "--info-wprg", "PvP ranking grades catalog"}, + {{'W','L','A','N'}, ".wlan", "i18n", "--info-wlan", "Localization catalog"}, {{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"}, {{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"}, {{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index d69b49f6..a26f0148 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2475,6 +2475,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wprg to a human-editable JSON sidecar (defaults to .wprg.json; emits factionFilter as both int AND name string)\n"); std::printf(" --import-wprg-json [out-base]\n"); std::printf(" Import a .wprg.json sidecar back into binary .wprg (factionFilter int OR \"alliance\"/\"horde\"; titlePrefix as plain string)\n"); + std::printf(" --gen-lan [name]\n"); + std::printf(" Emit .wlan 5 UI-button localizations (the 'Cancel' button translated to deDE/frFR/esES/koKR/zhCN — multibyte UTF-8 round-trip preserved)\n"); + std::printf(" --gen-lan-quest [name]\n"); + std::printf(" Emit .wlan 3 quest-title localizations (one quest title in deDE/frFR/koKR — illustrates dotted-key convention 'QUEST.123.title')\n"); + std::printf(" --gen-lan-tooltip [name]\n"); + std::printf(" Emit .wlan 4 item-tooltip strings (BindOnPickup + Unique in deDE+frFR — high-volume client localization use case)\n"); + std::printf(" --info-wlan [--json]\n"); + std::printf(" Print WLAN entries (id / language / namespace / original key / localized text)\n"); + std::printf(" --validate-wlan [--json]\n"); + std::printf(" Static checks: id+name+originalKey required, languageCode 0..10 OR 255 Unknown, namespace 0..7, no duplicate stringIds, no two entries with same (originalKey, languageCode, namespace) triple (locale lookup tie); warns on empty localizedText (override would render blank, possibly worse than fallback)\n"); std::printf(" --catalog-pluck [--json]\n"); std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n"); std::printf(" --catalog-find [--magic ] [--json]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index e06820f2..31a2b5f5 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -147,6 +147,7 @@ constexpr FormatRow kFormats[] = { {"WCFG", ".wcfg", "server", "worldserver.conf flat-text config", "Server config catalog (polymorphic Float/Int/Bool/String values)"}, {"WANV", ".wanv", "events", "GameEvent SQL + per-holiday script", "Anniversary & recurring event catalog (cron-like scheduling)"}, {"WPRG", ".wprg", "pvp", "vanilla 14-rank PvP ladder ladder", "PvP ranking grades catalog (faction + tier + honor thresholds)"}, + {"WLAN", ".wlan", "i18n", "Locale_*.MPQ + DBC trailing strings","Localization catalog (per-language string overlay)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_localization_catalog.cpp b/tools/editor/cli_localization_catalog.cpp new file mode 100644 index 00000000..1fed8f74 --- /dev/null +++ b/tools/editor/cli_localization_catalog.cpp @@ -0,0 +1,297 @@ +#include "cli_localization_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_localization.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWlanExt(std::string base) { + stripExt(base, ".wlan"); + return base; +} + +const char* languageCodeName(uint8_t l) { + using L = wowee::pipeline::WoweeLocalization; + switch (l) { + case L::enUS: return "enUS"; + case L::enGB: return "enGB"; + case L::deDE: return "deDE"; + case L::esES: return "esES"; + case L::frFR: return "frFR"; + case L::itIT: return "itIT"; + case L::koKR: return "koKR"; + case L::ptBR: return "ptBR"; + case L::ruRU: return "ruRU"; + case L::zhCN: return "zhCN"; + case L::zhTW: return "zhTW"; + case L::Unknown: return "Unknown"; + default: return "?"; + } +} + +const char* namespaceName(uint8_t n) { + using L = wowee::pipeline::WoweeLocalization; + switch (n) { + case L::UI: return "ui"; + case L::Quest: return "quest"; + case L::Item: return "item"; + case L::Spell: return "spell"; + case L::Creature: return "creature"; + case L::Tooltip: return "tooltip"; + case L::Gossip: return "gossip"; + case L::System: return "system"; + default: return "unknown"; + } +} + +bool saveOrError(const wowee::pipeline::WoweeLocalization& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeLocalizationLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wlan\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeLocalization& c, + const std::string& base) { + std::printf("Wrote %s.wlan\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" strings : %zu\n", c.entries.size()); +} + +int handleGenUI(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "UIBasicsLocalization"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWlanExt(base); + auto c = wowee::pipeline::WoweeLocalizationLoader::makeUIBasics(name); + if (!saveOrError(c, base, "gen-lan")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenQuest(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "QuestSampleLocalization"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWlanExt(base); + auto c = wowee::pipeline::WoweeLocalizationLoader::makeQuestSample(name); + if (!saveOrError(c, base, "gen-lan-quest")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenTooltip(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "TooltipSetLocalization"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWlanExt(base); + auto c = wowee::pipeline::WoweeLocalizationLoader::makeTooltipSet(name); + if (!saveOrError(c, base, "gen-lan-tooltip")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWlanExt(base); + if (!wowee::pipeline::WoweeLocalizationLoader::exists(base)) { + std::fprintf(stderr, "WLAN not found: %s.wlan\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeLocalizationLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wlan"] = base + ".wlan"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"stringId", e.stringId}, + {"name", e.name}, + {"description", e.description}, + {"languageCode", e.languageCode}, + {"languageCodeName", + languageCodeName(e.languageCode)}, + {"namespace", e.namespace_}, + {"namespaceName", namespaceName(e.namespace_)}, + {"originalKey", e.originalKey}, + {"localizedText", e.localizedText}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WLAN: %s.wlan\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" strings : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id lang ns key text\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-4s %-9s %-25s %s\n", + e.stringId, + languageCodeName(e.languageCode), + namespaceName(e.namespace_), + e.originalKey.c_str(), + e.localizedText.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWlanExt(base); + if (!wowee::pipeline::WoweeLocalizationLoader::exists(base)) { + std::fprintf(stderr, + "validate-wlan: WLAN not found: %s.wlan\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeLocalizationLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::set idsSeen; + // Per-(originalKey, languageCode, namespace_) triple + // uniqueness — two entries with all three matching + // would tie at runtime when the locale-aware text + // layer looks up an override. + std::set tripleSeen; + auto tripleKey = [](const std::string& key, uint8_t lang, + uint8_t ns) { + return std::to_string(lang) + "|" + + std::to_string(ns) + "|" + key; + }; + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (id=" + std::to_string(e.stringId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.stringId == 0) + errors.push_back(ctx + ": stringId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.languageCode > 10 && e.languageCode != 255) { + errors.push_back(ctx + ": languageCode " + + std::to_string(e.languageCode) + + " out of range (must be 0..10 or 255 " + "Unknown)"); + } + if (e.namespace_ > 7) { + errors.push_back(ctx + ": namespace " + + std::to_string(e.namespace_) + + " out of range (must be 0..7)"); + } + if (e.originalKey.empty()) { + errors.push_back(ctx + + ": originalKey is empty — locale-aware " + "text layer has nothing to look up"); + } + if (e.localizedText.empty()) { + warnings.push_back(ctx + + ": localizedText is empty — override " + "would render blank, possibly worse than " + "falling through to default"); + } + // Triple uniqueness check. + if (!e.originalKey.empty()) { + std::string key = tripleKey(e.originalKey, + e.languageCode, + e.namespace_); + if (!tripleSeen.insert(key).second) { + errors.push_back(ctx + + ": (originalKey='" + e.originalKey + + "', languageCode=" + + std::string(languageCodeName(e.languageCode)) + + ", namespace=" + + std::string(namespaceName(e.namespace_)) + + ") triple already bound by another " + "entry — locale lookup would tie " + "non-deterministically"); + } + } + if (!idsSeen.insert(e.stringId).second) { + errors.push_back(ctx + ": duplicate stringId"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wlan"] = base + ".wlan"; + j["ok"] = ok; + j["errors"] = errors; + j["warnings"] = warnings; + std::printf("%s\n", j.dump(2).c_str()); + return ok ? 0 : 1; + } + std::printf("validate-wlan: %s.wlan\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu strings, all stringIds + " + "(key,lang,ns) triples unique\n", + c.entries.size()); + return 0; + } + if (!warnings.empty()) { + std::printf(" warnings (%zu):\n", warnings.size()); + for (const auto& w : warnings) + std::printf(" - %s\n", w.c_str()); + } + if (!errors.empty()) { + std::printf(" ERRORS (%zu):\n", errors.size()); + for (const auto& e : errors) + std::printf(" - %s\n", e.c_str()); + } + return ok ? 0 : 1; +} + +} // namespace + +bool handleLocalizationCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-lan") == 0 && i + 1 < argc) { + outRc = handleGenUI(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-lan-quest") == 0 && + i + 1 < argc) { + outRc = handleGenQuest(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-lan-tooltip") == 0 && + i + 1 < argc) { + outRc = handleGenTooltip(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wlan") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wlan") == 0 && i + 1 < argc) { + outRc = handleValidate(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_localization_catalog.hpp b/tools/editor/cli_localization_catalog.hpp new file mode 100644 index 00000000..512e29a4 --- /dev/null +++ b/tools/editor/cli_localization_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleLocalizationCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee