From 40d93647705cbec415ebdd7a7a45427f28263055 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 20:42:01 -0500 Subject: [PATCH] feat(locale): load every locale in locale.xml on request Locale::LoadFromFile can keep the other locales' phrases too, and GetPhrase(id, locale) answers in one of them, falling back to the default. Co-Authored-By: Claude Opus 5.5 --- dCommon/Locale.cpp | 51 ++++++++++++++++++++++++++++++++++++++-------- dCommon/Locale.h | 11 +++++++++- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/dCommon/Locale.cpp b/dCommon/Locale.cpp index 6ab2c16d9..3de50340a 100644 --- a/dCommon/Locale.cpp +++ b/dCommon/Locale.cpp @@ -3,12 +3,20 @@ #include "Logger.h" namespace { - std::unordered_map g_Phrases; + std::unordered_map g_Phrases; // the default locale's + // The other locales' phrases (only loaded with allLocales), by locale + std::unordered_map> g_Others; + std::vector g_Locales; bool g_Loaded = false; const std::string g_Empty; } -void Locale::LoadFromFile(const std::string& filepath, const std::string& locale) { +void Locale::LoadFromFile(const std::string& filepath, const std::string& locale, bool allLocales) { + g_Phrases.clear(); + g_Others.clear(); + g_Locales.clear(); + g_Loaded = false; + tinyxml2::XMLDocument doc; if (doc.LoadFile(filepath.c_str()) != tinyxml2::XML_SUCCESS) { LOG("Failed to load locale file: %s", filepath.c_str()); @@ -18,6 +26,18 @@ void Locale::LoadFromFile(const std::string& filepath, const std::string& locale auto* root = doc.FirstChildElement("localization"); if (!root) return; + g_Locales.push_back(locale); + if (allLocales) { + if (auto* locales = root->FirstChildElement("locales")) { + for (auto* entry = locales->FirstChildElement("locale"); entry; entry = entry->NextSiblingElement("locale")) { + const char* name = entry->GetText(); + if (!name || locale == name || g_Others.contains(name)) continue; + g_Locales.push_back(name); + g_Others[name]; + } + } + } + auto* phrases = root->FirstChildElement("phrases"); if (!phrases) return; @@ -27,18 +47,20 @@ void Locale::LoadFromFile(const std::string& filepath, const std::string& locale for (auto* trans = phrase->FirstChildElement("translation"); trans; trans = trans->NextSiblingElement("translation")) { const char* loc = trans->Attribute("locale"); - if (loc && locale == loc) { - const char* text = trans->GetText(); - if (text) { - g_Phrases[id] = text; - } - break; + const char* text = trans->GetText(); + if (!loc || !text) continue; + if (locale == loc) { + g_Phrases[id] = text; + if (!allLocales) break; + } else if (allLocales) { + const auto other = g_Others.find(loc); + if (other != g_Others.end()) other->second[id] = text; } } } g_Loaded = true; - LOG("Loaded %zu locale phrases from %s (locale: %s)", g_Phrases.size(), filepath.c_str(), locale.c_str()); + LOG("Loaded %zu locale phrases from %s (locale: %s, %zu other locale(s))", g_Phrases.size(), filepath.c_str(), locale.c_str(), g_Others.size()); } const std::string& Locale::GetPhrase(const std::string& phraseId) { @@ -47,6 +69,17 @@ const std::string& Locale::GetPhrase(const std::string& phraseId) { return g_Empty; } +const std::string& Locale::GetPhrase(const std::string& phraseId, const std::string& locale) { + if (const auto other = g_Others.find(locale); other != g_Others.end()) { + if (const auto it = other->second.find(phraseId); it != other->second.end()) return it->second; + } + return GetPhrase(phraseId); +} + +const std::vector& Locale::GetLocales() { + return g_Locales; +} + bool Locale::IsLoaded() { return g_Loaded; } diff --git a/dCommon/Locale.h b/dCommon/Locale.h index c4cda9654..9e25b57f5 100644 --- a/dCommon/Locale.h +++ b/dCommon/Locale.h @@ -5,8 +5,17 @@ #include namespace Locale { - void LoadFromFile(const std::string& filepath, const std::string& locale = "en_US"); + /** + * Load the client's locale.xml: the phrases of `locale` (the default, which GetPhrase(id) answers from) and, with + * `allLocales`, those of every other locale the file lists too. Replaces what an earlier load read. Call before + * other threads read phrases; lookups only read afterwards. + */ + void LoadFromFile(const std::string& filepath, const std::string& locale = "en_US", bool allLocales = false); const std::string& GetPhrase(const std::string& phraseId); + // The phrase in `locale`, else in the default locale; empty when neither has it + const std::string& GetPhrase(const std::string& phraseId, const std::string& locale); + // The locales loaded ("en_US", "de_DE", ...), the default first; empty before a load + const std::vector& GetLocales(); bool IsLoaded(); std::vector GetPhraseIdsWithPrefix(const std::string& prefix); };