yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
language_manager.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdint>
5#include <filesystem>
6#include <fstream>
7#include <sstream>
8#include <string>
9
11#include "util/log.h"
12#include "util/platform_paths.h"
13
14namespace yaze {
15namespace i18n {
16namespace {
17
18constexpr char kDefaultLocale[] = "en";
19
20// Appends the UTF-8 encoding of a Unicode code point to out.
21void AppendUtf8(uint32_t cp, std::string* out) {
22 if (cp <= 0x7F) {
23 out->push_back(static_cast<char>(cp));
24 } else if (cp <= 0x7FF) {
25 out->push_back(static_cast<char>(0xC0 | (cp >> 6)));
26 out->push_back(static_cast<char>(0x80 | (cp & 0x3F)));
27 } else if (cp <= 0xFFFF) {
28 out->push_back(static_cast<char>(0xE0 | (cp >> 12)));
29 out->push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
30 out->push_back(static_cast<char>(0x80 | (cp & 0x3F)));
31 } else {
32 out->push_back(static_cast<char>(0xF0 | (cp >> 18)));
33 out->push_back(static_cast<char>(0x80 | ((cp >> 12) & 0x3F)));
34 out->push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
35 out->push_back(static_cast<char>(0x80 | (cp & 0x3F)));
36 }
37}
38
39int HexVal(char c) {
40 if (c >= '0' && c <= '9')
41 return c - '0';
42 if (c >= 'a' && c <= 'f')
43 return c - 'a' + 10;
44 if (c >= 'A' && c <= 'F')
45 return c - 'A' + 10;
46 return -1;
47}
48
49// Minimal, self-contained parser for a FLAT JSON object of the form
50// {"source":"translation", ...}. Values and keys are UTF-8; standard escapes
51// (\" \\ \/ \b \f \n \r \t \uXXXX, incl. surrogate pairs) are decoded. Anything
52// that is not a flat string->string object is rejected (returns false). This
53// avoids coupling the foundation util library to a JSON dependency.
55 public:
56 explicit FlatJsonParser(const std::string& text) : s_(text) {}
57
58 bool Parse(std::unordered_map<std::string, std::string>* out) {
59 SkipWs();
60 if (!Consume('{'))
61 return false;
62 SkipWs();
63 if (Peek() == '}') {
64 ++i_;
65 return true;
66 }
67 while (true) {
68 SkipWs();
69 std::string key;
70 if (!ParseString(&key))
71 return false;
72 SkipWs();
73 if (!Consume(':'))
74 return false;
75 SkipWs();
76 std::string value;
77 if (!ParseString(&value))
78 return false;
79 (*out)[key] = value;
80 SkipWs();
81 const char c = Peek();
82 if (c == ',') {
83 ++i_;
84 continue;
85 }
86 if (c == '}') {
87 ++i_;
88 return true;
89 }
90 return false;
91 }
92 }
93
94 private:
95 char Peek() const { return i_ < s_.size() ? s_[i_] : '\0'; }
96 bool Consume(char c) {
97 if (Peek() != c)
98 return false;
99 ++i_;
100 return true;
101 }
102 void SkipWs() {
103 while (i_ < s_.size()) {
104 const char c = s_[i_];
105 if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
106 ++i_;
107 } else {
108 break;
109 }
110 }
111 }
112
113 bool ParseString(std::string* out) {
114 if (!Consume('"'))
115 return false;
116 while (i_ < s_.size()) {
117 const char c = s_[i_++];
118 if (c == '"')
119 return true;
120 if (c != '\\') {
121 out->push_back(c);
122 continue;
123 }
124 if (i_ >= s_.size())
125 return false;
126 const char e = s_[i_++];
127 switch (e) {
128 case '"':
129 out->push_back('"');
130 break;
131 case '\\':
132 out->push_back('\\');
133 break;
134 case '/':
135 out->push_back('/');
136 break;
137 case 'b':
138 out->push_back('\b');
139 break;
140 case 'f':
141 out->push_back('\f');
142 break;
143 case 'n':
144 out->push_back('\n');
145 break;
146 case 'r':
147 out->push_back('\r');
148 break;
149 case 't':
150 out->push_back('\t');
151 break;
152 case 'u': {
153 uint32_t cp = 0;
154 if (!ParseHex4(&cp))
155 return false;
156 if (cp >= 0xD800 && cp <= 0xDBFF) { // high surrogate
157 if (i_ + 1 < s_.size() && s_[i_] == '\\' && s_[i_ + 1] == 'u') {
158 i_ += 2;
159 uint32_t lo = 0;
160 if (!ParseHex4(&lo))
161 return false;
162 cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
163 }
164 }
165 AppendUtf8(cp, out);
166 break;
167 }
168 default:
169 return false;
170 }
171 }
172 return false; // unterminated string
173 }
174
175 bool ParseHex4(uint32_t* out) {
176 if (i_ + 4 > s_.size())
177 return false;
178 uint32_t v = 0;
179 for (int k = 0; k < 4; ++k) {
180 const int h = HexVal(s_[i_++]);
181 if (h < 0)
182 return false;
183 v = (v << 4) | static_cast<uint32_t>(h);
184 }
185 *out = v;
186 return true;
187 }
188
189 const std::string& s_;
190 std::string::size_type i_ = 0;
191};
192
193// Resolves the assets/i18n directory (via a known sentinel file) so we can
194// enumerate available locale catalogs. Returns empty on failure.
195std::filesystem::path ResolveI18nDir() {
196 auto path = util::PlatformPaths::FindAsset("i18n/en.json");
197 if (path.ok()) {
198 return path->parent_path();
199 }
200 // en.json is optional; try fr.json to still locate the directory.
201 path = util::PlatformPaths::FindAsset("i18n/fr.json");
202 if (path.ok()) {
203 return path->parent_path();
204 }
205 return {};
206}
207
208} // namespace
209
211 static LanguageManager instance;
212 return instance;
213}
214
216 // "en" is the identity locale: an empty catalog means every lookup misses and
217 // tr() falls back to the (English) source string.
218 catalogs_[kDefaultLocale] = {};
219}
220
221std::vector<std::string> LanguageManager::GetAvailableLocales() const {
222 std::vector<std::string> locales;
223 locales.push_back(kDefaultLocale);
224 const std::filesystem::path dir = ResolveI18nDir();
225 if (!dir.empty()) {
226 std::error_code ec;
227 for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) {
228 if (ec)
229 break;
230 if (!entry.is_regular_file())
231 continue;
232 const std::filesystem::path& p = entry.path();
233 if (p.extension() != ".json")
234 continue;
235 std::string stem = p.stem().string();
236 if (stem == kDefaultLocale)
237 continue;
238 if (std::find(locales.begin(), locales.end(), stem) == locales.end()) {
239 locales.push_back(stem);
240 }
241 }
242 }
243 std::sort(locales.begin(), locales.end());
244 return locales;
245}
246
247bool LanguageManager::LoadCatalog(const std::string& locale) {
248 if (catalogs_.count(locale))
249 return true;
250 if (locale == kDefaultLocale) {
251 catalogs_[locale] = {};
252 return true;
253 }
254 auto asset = util::PlatformPaths::FindAsset("i18n/" + locale + ".json");
255 if (!asset.ok()) {
256 util::logf("i18n: catalog for '%s' not found", locale.c_str());
257 return false;
258 }
259 std::ifstream file(asset->string(), std::ios::binary);
260 if (!file) {
261 util::logf("i18n: cannot open catalog %s", asset->string().c_str());
262 return false;
263 }
264 std::ostringstream ss;
265 ss << file.rdbuf();
266 const std::string text = ss.str();
267 std::unordered_map<std::string, std::string> catalog;
268 FlatJsonParser parser(text);
269 if (!parser.Parse(&catalog)) {
270 util::logf("i18n: failed to parse catalog %s", asset->string().c_str());
271 return false;
272 }
273 util::logf("i18n: loaded '%s' catalog (%d entries)", locale.c_str(),
274 static_cast<int>(catalog.size()));
275 catalogs_[locale] = std::move(catalog);
276 return true;
277}
278
279void LanguageManager::SetLanguage(const std::string& locale) {
280 std::string target = locale.empty() ? std::string(kDefaultLocale) : locale;
281 if (!LoadCatalog(target)) {
282 target = kDefaultLocale;
283 LoadCatalog(target);
284 }
285 current_locale_ = target;
287 if (on_changed_) {
289 }
290}
291
293 const std::string& locale, const std::string& json_text) {
294 std::unordered_map<std::string, std::string> catalog;
295 FlatJsonParser parser(json_text);
296 if (!parser.Parse(&catalog)) {
297 return false;
298 }
299 catalogs_[locale] = std::move(catalog);
300 return true;
301}
302
303const std::string* LanguageManager::Find(const std::string& visible_key) const {
304 auto cat_it = catalogs_.find(current_locale_);
305 if (cat_it == catalogs_.end())
306 return nullptr;
307 auto it = cat_it->second.find(visible_key);
308 if (it == cat_it->second.end())
309 return nullptr;
310 return &it->second;
311}
312
313} // namespace i18n
314} // namespace yaze
std::vector< std::string > GetAvailableLocales() const
const std::string * Find(const std::string &visible_key) const
void SetLanguage(const std::string &locale)
static LanguageManager & Get()
LanguageChangedCallback on_changed_
bool LoadCatalog(const std::string &locale)
std::map< std::string, std::unordered_map< std::string, std::string > > catalogs_
bool LoadCatalogFromStringForTesting(const std::string &locale, const std::string &json_text)
bool Parse(std::unordered_map< std::string, std::string > *out)
static absl::StatusOr< std::filesystem::path > FindAsset(const std::string &relative_path)
Find an asset file in multiple standard locations.
void ClearTranslationCache()
Definition translator.cc:64
void logf(const absl::FormatSpec< Args... > &format, Args &&... args)
Definition log.h:115