MetaImGUI 1.0.0
ImGui Application Template for C++20
Loading...
Searching...
No Matches
Localization.cpp
Go to the documentation of this file.
1/*
2 MetaImGUI
3 Copyright (C) 2026 A P Nicholson
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>.
17*/
18
19#include "Localization.h"
20
21#include "Logger.h"
22
23#include <nlohmann/json.hpp>
24
25#include <fstream>
26
27using json = nlohmann::json;
28
29namespace MetaImGUI {
30
31// ============================================================================
32// WARNING: DO NOT ADD BUILT-IN TRANSLATIONS HERE!
33// ============================================================================
34// All translations MUST come from resources/translations/translations.json
35// This is the SINGLE SOURCE OF TRUTH for all translations.
36//
37// Having two sources of translations (built-in + JSON) causes:
38// - Maintenance nightmares (keeping them in sync)
39// - Bugs when one is updated but not the other
40// - Confusion about which translation is actually being used
41//
42// If translations.json fails to load, the application should:
43// 1. Log a clear ERROR message
44// 2. Return translation keys as-is (they serve as English fallback)
45// 3. Be obvious to developers/users that something is wrong
46// ============================================================================
47
48Localization::Localization() : m_currentLanguage("en") {
49 // Translations are loaded via LoadTranslations() - see warning above
50}
51
53 static Localization instance;
54 return instance;
55}
56
57void Localization::SetLanguage(const std::string& languageCode) {
58 if (m_translations.contains(languageCode)) {
59 m_currentLanguage = languageCode;
60 m_trCache.clear(); // Stale: cached entries belong to the previous language.
61 LOG_INFO("Language set to: {}", languageCode);
62 } else {
63 LOG_ERROR("Language not available: {}", languageCode);
64 }
65}
66
68 return m_currentLanguage;
69}
70
71std::vector<std::string> Localization::GetAvailableLanguages() const {
72 std::vector<std::string> languages;
73 languages.reserve(m_translations.size());
74 for (const auto& pair : m_translations) {
75 languages.push_back(pair.first);
76 }
77 return languages;
78}
79
80std::string Localization::Tr(const std::string& key) const {
81 if (auto cacheIt = m_trCache.find(key); cacheIt != m_trCache.end()) {
82 return cacheIt->second;
83 }
84
85 // Cache miss — walk the lookup tiers.
86 auto resolve = [this, &key]() -> std::string {
87 if (auto langIt = m_translations.find(m_currentLanguage); langIt != m_translations.end()) {
88 if (auto keyIt = langIt->second.find(key); keyIt != langIt->second.end()) {
89 return keyIt->second;
90 }
91 }
92 if (m_currentLanguage != "en") {
93 if (auto enIt = m_translations.find("en"); enIt != m_translations.end()) {
94 if (auto keyIt = enIt->second.find(key); keyIt != enIt->second.end()) {
95 return keyIt->second;
96 }
97 }
98 }
99 return key; // Key not found — surface the key name to make the gap visible.
100 };
101
102 std::string resolved = resolve();
103 // Memoize. We don't bound the cache size: the set of distinct UI keys is
104 // small (dozens) and bounded by the JSON file, not by user input.
105 m_trCache.emplace(key, resolved);
106 return resolved;
107}
108
109void Localization::AddTranslation(const std::string& languageCode, const std::string& key, const std::string& value) {
110 m_translations[languageCode][key] = value;
111 // Invalidate just the affected cache entry rather than the whole cache,
112 // so a streaming load doesn't blow away the cache repeatedly.
113 if (languageCode == m_currentLanguage || languageCode == "en") {
114 m_trCache.erase(key);
115 }
116}
117
118bool Localization::LoadTranslations(const std::string& filepath) {
119 try {
120 std::ifstream file(filepath);
121 if (!file.is_open()) {
122 // Application probes several candidate paths in order; a miss here
123 // is the expected case until the right one is found, so log at
124 // DEBUG only. The aggregated final-failure error is the caller's
125 // responsibility.
126 LOG_DEBUG("Translations file not found at: {}", filepath);
127 return false;
128 }
129
130 json j = json::parse(file);
131
132 // Wipe any previously-loaded translations on successful re-load so a
133 // language whose key set shrank between loads doesn't leak stale
134 // entries.
135 m_translations.clear();
136 m_trCache.clear();
137
138 for (const auto& [languageCode, translations] : j.items()) {
139 for (const auto& [key, value] : translations.items()) {
140 AddTranslation(languageCode, key, value.get<std::string>());
141 }
142 }
143
144 LOG_INFO("Loaded translations from: {}", filepath);
145 return true;
146 } catch (const std::exception& e) {
147 // Parse error is genuinely a problem (file existed but was malformed),
148 // so this stays ERROR.
149 LOG_ERROR("Failed to load translations from {}: {}", filepath, e.what());
150 return false;
151 }
152}
153
154} // namespace MetaImGUI
nlohmann::json json
#define LOG_INFO(...)
Definition Logger.h:166
#define LOG_DEBUG(...)
Definition Logger.h:165
#define LOG_ERROR(...)
Definition Logger.h:168
Simple localization/internationalization system.
static Localization & Instance()
Get singleton instance.
void SetLanguage(const std::string &languageCode)
Set current language.
std::vector< std::string > GetAvailableLanguages() const
Get list of available languages.
std::string Tr(const std::string &key) const
Get translated string.
bool LoadTranslations(const std::string &filepath)
Load translations from JSON file.
std::string GetCurrentLanguage() const
Get current language code.
void AddTranslation(const std::string &languageCode, const std::string &key, const std::string &value)
Add translation for a language.