载入中...
搜索中...
未找到
I18n.cpp
浏览该文件的文档.
1#include "i18n/I18n.h"
2
3#include "common/Json.h"
4#include "common/Module.h"
8
9#include <squirrel.h>
10#include <simplesquirrel/simplesquirrel.hpp>
11
12#include <algorithm>
13#include <cmath>
14#include <cstdint>
15#include <set>
16#include <sstream>
17#include <unordered_set>
18#include <vector>
19
20namespace eve::i18n {
21
23
24namespace {
25
26bool isPluralCategory(const std::string& key) {
27 static const char* cats[] = {"zero", "one", "two", "few", "many", "other"};
28 for (const char* c : cats)
29 if (key == c) return true;
30 return false;
31}
32
34std::string numberToString(double v) {
35 std::ostringstream os;
36 os << v;
37 return os.str();
38}
39
40bool validIdentifier(const std::string& value) {
41 if (value.empty() || value.size() > 64) return false;
42 for (const unsigned char ch : value)
43 if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' ||
44 ch == '_'))
45 return false;
46 return true;
47}
48
49bool collectPlaceholders(const std::string& text, std::set<std::string>& result, std::string& error) {
50 result.clear();
51 for (size_t offset = 0; offset < text.size();) {
52 if (text[offset] == '}') {
53 error = "unmatched closing placeholder brace";
54 return false;
55 }
56 if (text[offset] != '{') {
57 ++offset;
58 continue;
59 }
60 const size_t close = text.find('}', offset + 1);
61 if (close == std::string::npos) {
62 error = "unterminated placeholder";
63 return false;
64 }
65 const std::string name = text.substr(offset + 1, close - offset - 1);
66 if (!validIdentifier(name)) {
67 error = "placeholder names must be stable identifiers";
68 return false;
69 }
70 result.insert(name);
71 offset = close + 1;
72 }
73 return true;
74}
75
76bool flattenStrict(eve::json::Value node, const std::string& prefix, int depth,
77 std::unordered_map<std::string, std::string>& strings,
78 std::unordered_map<std::string, std::unordered_map<std::string, std::string>>& plurals,
79 std::string& error, std::string& errorPath) {
80 if (depth > 16) {
81 error = "locale nesting exceeds 16 levels";
82 errorPath = prefix;
83 return false;
84 }
85 if (!node.isObject() || node.size() == 0) {
86 error = "translation namespaces must be non-empty objects";
87 errorPath = prefix;
88 return false;
89 }
90 const std::vector<std::string> names = node.keys();
91 bool allPlural = true;
92 bool anyPlural = false;
93 for (const auto& name : names) {
94 if (isPluralCategory(name))
95 anyPlural = true;
96 else
97 allPlural = false;
98 if (!validIdentifier(name)) {
99 error = "translation key segments must be stable identifiers";
100 errorPath = prefix + "." + name;
101 return false;
102 }
103 }
104 if (anyPlural && !allPlural) {
105 error = "plural categories cannot be mixed with namespace keys";
106 errorPath = prefix;
107 return false;
108 }
109 if (allPlural) {
110 if (prefix.empty() || !node.has("other")) {
111 error = "plural tables require a non-empty key and an other form";
112 errorPath = prefix;
113 return false;
114 }
115 std::unordered_map<std::string, std::string> forms;
116 for (const auto& name : names) {
117 const auto value = node.get(name.c_str());
118 if (!value.isString() || value.asString().empty() || value.asString().size() > 16384) {
119 error = "plural forms must be non-empty strings of at most 16384 bytes";
120 errorPath = prefix + "." + name;
121 return false;
122 }
123 forms.emplace(name, value.asString());
124 }
125 plurals.emplace(prefix, std::move(forms));
126 return true;
127 }
128 for (const auto& name : names) {
129 const auto value = node.get(name.c_str());
130 const std::string path = prefix.empty() ? name : prefix + "." + name;
131 if (value.isString()) {
132 if (value.asString().empty() || value.asString().size() > 16384) {
133 error = "translations must be non-empty strings of at most 16384 bytes";
134 errorPath = path;
135 return false;
136 }
137 strings.emplace(path, value.asString());
138 } else if (value.isObject()) {
139 if (!flattenStrict(value, path, depth + 1, strings, plurals, error, errorPath)) return false;
140 } else {
141 error = "translations must be strings, namespaces, or plural tables";
142 errorPath = path;
143 return false;
144 }
145 if (strings.size() + plurals.size() > 10000) {
146 error = "a locale may contain at most 10000 translation keys";
147 errorPath = path;
148 return false;
149 }
150 }
151 return true;
152}
153
154void flatten(eve::json::Value node, const std::string& prefix, std::unordered_map<std::string, std::string>& strings,
155 std::unordered_map<std::string, std::unordered_map<std::string, std::string>>& plurals) {
156 if (node.isObject()) {
157 const std::vector<std::string> names = node.keys();
158 // A plain object of plural categories is a plural form table.
159 bool allCats = !names.empty();
160 bool allStrings = true;
161 for (const auto& k : names) {
162 if (!isPluralCategory(k)) {
163 allCats = false;
164 break;
165 }
166 if (!node.get(k.c_str()).isString()) allStrings = false;
167 }
168 if (allCats && allStrings && !prefix.empty()) {
169 std::unordered_map<std::string, std::string> forms;
170 for (const auto& k : names) forms[k] = node.getString(k.c_str());
171 plurals[prefix] = std::move(forms);
172 return;
173 }
174 for (const auto& k : names) {
175 const std::string path = prefix.empty() ? k : prefix + "." + k;
176 flatten(node.get(k.c_str()), path, strings, plurals);
177 }
178 return;
179 }
180 if (prefix.empty()) return;
181 // Scalars stringify; arrays and nulls are not translatable and are skipped.
182 if (node.isString() || node.isNumber() || node.isBool()) strings[prefix] = node.asString();
183}
184
185bool parseLocale(const std::string& text, std::unordered_map<std::string, std::string>& strings,
186 std::unordered_map<std::string, std::unordered_map<std::string, std::string>>& plurals,
187 std::string* error) {
189 if (!doc.valid()) return false;
190 if (!doc.root().isObject()) {
191 if (error) *error = "locale root must be a JSON object";
192 return false;
193 }
194 strings.clear();
195 plurals.clear();
196 flatten(doc.root(), "", strings, plurals);
197 return true;
198}
199
200int64_t fileModtime(const std::string& path) {
201 auto* fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>("Filesystem");
202 if (!fs) fs = eve::filesystem::Filesystem::create();
204 if (!fs->getInfo(path, info)) return -1;
205 return info.modtime;
206}
207
208// Convert a Squirrel table (or null) into a string params map.
209std::unordered_map<std::string, std::string> readParams(ssq::Object params) {
210 std::unordered_map<std::string, std::string> out;
211 HSQUIRRELVM vm = params.getHandle();
212 if (!vm) return out;
213 const HSQOBJECT raw = params.getRaw();
214 if (raw._type != OT_TABLE) return out;
215
216 const SQInteger top = sq_gettop(vm);
217 sq_pushobject(vm, raw);
218 sq_pushnull(vm); // iterator
219 while (SQ_SUCCEEDED(sq_next(vm, -2))) {
220 // key at -2, value at -1
221 const SQChar* k = nullptr;
222 if (sq_gettype(vm, -2) == OT_STRING && SQ_SUCCEEDED(sq_getstring(vm, -2, &k)) && k) {
223 std::string v;
224 const SQObjectType vt = sq_gettype(vm, -1);
225 if (vt == OT_STRING) {
226 const SQChar* sv = nullptr;
227 if (SQ_SUCCEEDED(sq_getstring(vm, -1, &sv)) && sv) v = sv;
228 } else if (vt == OT_INTEGER) {
229 SQInteger iv = 0;
230 if (SQ_SUCCEEDED(sq_getinteger(vm, -1, &iv))) v = std::to_string(iv);
231 } else if (vt == OT_FLOAT) {
232 SQFloat fv = 0;
233 if (SQ_SUCCEEDED(sq_getfloat(vm, -1, &fv))) v = numberToString(fv);
234 } else if (vt == OT_BOOL) {
235 SQBool bv = SQFalse;
236 if (SQ_SUCCEEDED(sq_getbool(vm, -1, &bv))) v = bv ? "true" : "false";
237 }
238 out[k] = std::move(v);
239 }
240 sq_pop(vm, 2);
241 }
242 sq_settop(vm, top);
243 return out;
244}
245
246} // namespace
247
248// ---------------------------------------------------------------------------
249// Module implementation
250// ---------------------------------------------------------------------------
251
252I18n::Locale* I18n::findLocale(const std::string& lang) {
253 auto it = locales_.find(lang);
254 return it == locales_.end() ? nullptr : &it->second;
255}
256
257const I18n::Locale* I18n::findLocale(const std::string& lang) const {
258 auto it = locales_.find(lang);
259 return it == locales_.end() ? nullptr : &it->second;
260}
261
262eve::Result<void> I18n::replaceLocaleFromJson(const std::string& lang, const std::string& json) {
263 if (lang.empty())
265 eve::DiagnosticCode::InvalidArgument, "locale identifier must not be empty", "locale", {}, "i18n.bundle"));
266 Locale loc;
267 std::string error;
268 if (!parseLocale(json, loc.strings, loc.plurals, &error))
270 error.empty() ? "invalid locale JSON" : error,
271 "locale." + lang, {}, "i18n.bundle"));
272 loc.path = "";
273 loc.modtime = -1;
274 locales_[lang] = std::move(loc);
276}
277
278bool I18n::loadFromJson(const std::string& lang, const std::string& json) {
279 return replaceLocaleFromJson(lang, json).ok();
280}
281
283 std::string parseError;
284 const auto document = eve::json::Document::parse(json, &parseError);
285 if (!document.valid())
287 eve::DiagnosticCode::ParseError, parseError.empty() ? "invalid localization bundle JSON" : parseError, "$",
288 {}, "i18n.bundle"));
289 const auto root = document.root();
290 if (!root.isObject())
292 "localization bundle root must be an object", "$", {},
293 "i18n.bundle"));
294 const std::unordered_set<std::string> rootFields = {"schema", "version", "defaultLocale", "locales"};
295 for (const auto& field : root.keys())
296 if (!rootFields.contains(field))
298 "localization bundle contains an unknown field",
299 "$." + field, {}, "i18n.bundle"));
300 const auto schema = root.get("schema");
301 const auto version = root.get("version");
302 const auto defaultLocale = root.get("defaultLocale");
303 const auto locales = root.get("locales");
304 if (!schema.isString() || schema.asString() != "eve.i18n.bundle")
306 "localization bundle schema id is invalid", "$.schema",
307 {}, "i18n.bundle"));
308 if (!version.isInt64() || version.asInt64() != 1)
310 "unsupported localization bundle schema version",
311 "$.version", {}, "i18n.bundle"));
312 if (!defaultLocale.isString() || !validIdentifier(defaultLocale.asString()))
314 "defaultLocale must be a stable locale identifier",
315 "$.defaultLocale", {}, "i18n.bundle"));
316 if (!locales.isObject() || locales.size() == 0 || locales.size() > 32)
318 "locales must contain between 1 and 32 locale objects",
319 "$.locales", {}, "i18n.bundle"));
320 if (!locales.has(defaultLocale.asString().c_str()))
322 "defaultLocale is not present in locales",
323 "$.defaultLocale", {}, "i18n.bundle"));
324
325 std::unordered_map<std::string, Locale> proposed;
326 for (const auto& lang : locales.keys()) {
327 if (!validIdentifier(lang))
329 "locale names must be stable identifiers",
330 "$.locales." + lang, {}, "i18n.bundle"));
331 Locale candidate;
332 std::string error;
333 std::string errorPath;
334 if (!flattenStrict(locales.get(lang.c_str()), "", 0, candidate.strings, candidate.plurals, error, errorPath))
337 "$.locales." + lang + (errorPath.empty() ? "" : "." + errorPath), {}, "i18n.bundle"));
338 proposed.emplace(lang, std::move(candidate));
339 }
340
341 const Locale& source = proposed.at(defaultLocale.asString());
342 for (const auto& [lang, locale] : proposed) {
343 for (const auto& [key, sourceText] : source.strings) {
344 const auto translated = locale.strings.find(key);
345 if (translated == locale.strings.end())
347 eve::DiagnosticCode::NotFound, "locale is missing a required singular translation",
348 "$.locales." + lang + "." + key, {}, "i18n.bundle"));
349 std::set<std::string> expected;
350 std::set<std::string> actual;
351 std::string placeholderError;
352 if (!collectPlaceholders(sourceText, expected, placeholderError))
355 "$.locales." + defaultLocale.asString() + "." + key, {}, "i18n.bundle"));
356 if (!collectPlaceholders(translated->second, actual, placeholderError))
359 "$.locales." + lang + "." + key, {}, "i18n.bundle"));
360 if (expected != actual)
362 eve::DiagnosticCode::InvalidArgument, "translation placeholders do not match the default locale",
363 "$.locales." + lang + "." + key, {}, "i18n.bundle"));
364 }
365 for (const auto& [key, translated] : locale.strings)
366 if (!source.strings.contains(key))
369 "locale contains a singular key absent from the default locale",
370 "$.locales." + lang + "." + key, {}, "i18n.bundle"));
371 for (const auto& [key, sourceForms] : source.plurals) {
372 const auto translated = locale.plurals.find(key);
373 if (translated == locale.plurals.end())
375 eve::DiagnosticCode::NotFound, "locale is missing a required plural translation",
376 "$.locales." + lang + "." + key, {}, "i18n.bundle"));
377 for (const auto& [form, translatedText] : translated->second) {
378 const auto exactSource = sourceForms.find(form);
379 const auto fallbackSource = sourceForms.find("other");
380 const std::string& sourceText =
381 exactSource != sourceForms.end() ? exactSource->second : fallbackSource->second;
382 std::set<std::string> expected;
383 std::set<std::string> actual;
384 std::string placeholderError;
385 if (!collectPlaceholders(sourceText, expected, placeholderError))
387 eve::DiagnosticCode::InvalidArgument, placeholderError,
388 "$.locales." + defaultLocale.asString() + "." + key + "." + form, {}, "i18n.bundle"));
389 if (!collectPlaceholders(translatedText, actual, placeholderError))
392 "$.locales." + lang + "." + key + "." + form, {}, "i18n.bundle"));
393 if (expected != actual)
395 eve::DiagnosticCode::InvalidArgument, "plural placeholders do not match the default locale",
396 "$.locales." + lang + "." + key + "." + form, {}, "i18n.bundle"));
397 }
398 }
399 for (const auto& [key, forms] : locale.plurals) {
400 (void)forms;
401 if (!source.plurals.contains(key))
403 eve::DiagnosticCode::InvalidArgument, "locale contains a plural key absent from the default locale",
404 "$.locales." + lang + "." + key, {}, "i18n.bundle"));
405 }
406 }
407
408 const std::string nextDefault = defaultLocale.asString();
409 const std::string nextLanguage = proposed.contains(language_) ? language_ : nextDefault;
410 locales_ = std::move(proposed);
411 defaultLanguage_ = nextDefault;
412 language_ = nextLanguage;
413 return eve::Result<int>::success(static_cast<int>(locales_.size()), eve::Status::success(eve::StatusCode::Applied));
414}
415
416eve::Result<void> I18n::replaceLocaleFromFile(const std::string& lang, const std::string& path) {
417 if (lang.empty() || path.empty())
419 "locale identifier and VFS path must not be empty",
420 "locale.file", {}, "i18n.bundle"));
421
422 auto* fs = ModuleManager::getInstance<filesystem::Filesystem>("Filesystem");
423 if (!fs) fs = filesystem::Filesystem::create();
424
425 filesystem::FileData* fd = nullptr;
426 try {
427 fd = fs->read(path);
428 } catch (...) {
429 delete fd;
431 eve::DiagnosticCode::NotFound, "locale file could not be read", path, {}, "i18n.bundle"));
432 }
433 if (fd == nullptr || fd->getData() == nullptr || fd->getSize() == 0) {
434 delete fd;
436 eve::DiagnosticCode::NotFound, "locale file is missing or empty", path, {}, "i18n.bundle"));
437 }
438
439 std::string text(static_cast<const char*>(fd->getData()), fd->getSize());
440 delete fd;
441
442 Locale loc;
443 std::string error;
444 if (!parseLocale(text, loc.strings, loc.plurals, &error))
446 eve::DiagnosticCode::ParseError, error.empty() ? "invalid locale JSON" : error, path, {}, "i18n.bundle"));
447
448 loc.path = path;
449 loc.modtime = fileModtime(path);
450 fs->watch(path);
451 locales_[lang] = std::move(loc);
453}
454
455bool I18n::loadFromFile(const std::string& lang, const std::string& path) {
456 return replaceLocaleFromFile(lang, path).ok();
457}
458
459void I18n::unload(const std::string& lang) { locales_.erase(lang); }
460
461void I18n::clear() { locales_.clear(); }
462
463eve::Result<void> I18n::selectLanguage(const std::string& lang) {
464 if (!hasLanguage(lang))
466 eve::DiagnosticCode::NotFound, "language has not been admitted", "language." + lang, {}, "i18n.bundle"));
467 language_ = lang;
469}
470
471bool I18n::setLanguage(const std::string& lang) { return selectLanguage(lang).ok(); }
472
473std::string I18n::getLanguageAt(int index) const {
474 if (index < 0 || size_t(index) >= locales_.size()) return {};
475 std::vector<std::string> langs;
476 langs.reserve(locales_.size());
477 for (const auto& [lang, loc] : locales_) langs.push_back(lang);
478 std::sort(langs.begin(), langs.end());
479 return langs[size_t(index)];
480}
481
482bool I18n::hasLanguage(const std::string& lang) const { return findLocale(lang) != nullptr; }
483
484bool I18n::hasInLanguage(const std::string& lang, const std::string& key) const {
485 const Locale* locale = findLocale(lang);
486 return locale && (locale->strings.contains(key) || locale->plurals.contains(key));
487}
488
490 if (key.empty() || key.back() == '.')
492 "translation key must not be empty", "requiredKey", {},
493 "i18n.bundle"));
494 size_t offset = 0;
495 while (offset < key.size()) {
496 const size_t separator = key.find('.', offset);
497 const std::string segment = key.substr(offset, separator == std::string::npos ? std::string::npos
498 : separator - offset);
499 if (!validIdentifier(segment))
501 eve::DiagnosticCode::InvalidArgument, "translation key segments must be stable identifiers",
502 "requiredKey." + key, {}, "i18n.bundle"));
503 if (separator == std::string::npos) break;
504 offset = separator + 1;
505 }
506 if (locales_.empty())
508 eve::DiagnosticCode::NotFound, "no locales have been admitted", "locales", {}, "i18n.bundle"));
509 std::vector<std::string> languages;
510 languages.reserve(locales_.size());
511 for (const auto& [language, locale] : locales_) languages.push_back(language);
512 std::sort(languages.begin(), languages.end());
513 for (const auto& language : languages)
514 if (!hasInLanguage(language, key))
516 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "required translation key is missing",
517 "locales." + language + "." + key, {}, "i18n.bundle"));
518 return eve::Result<int>::success(static_cast<int>(languages.size()));
519}
520
521int I18n::getKeyCount(const std::string& lang) const {
522 const Locale* locale = findLocale(lang);
523 return locale ? static_cast<int>(locale->strings.size() + locale->plurals.size()) : 0;
524}
525
526bool I18n::has(const std::string& key) const {
527 const Locale* cur = findLocale(language_);
528 if (cur && (cur->strings.find(key) != cur->strings.end() || cur->plurals.find(key) != cur->plurals.end()))
529 return true;
530 if (defaultLanguage_ != language_) {
531 if (const Locale* def = findLocale(defaultLanguage_))
532 if (def->strings.find(key) != def->strings.end() || def->plurals.find(key) != def->plurals.end())
533 return true;
534 }
535 return false;
536}
537
538std::string I18n::get(const std::string& key) const {
539 if (const Locale* cur = findLocale(language_)) {
540 auto it = cur->strings.find(key);
541 if (it != cur->strings.end()) return it->second;
542 }
543 if (defaultLanguage_ != language_) {
544 if (const Locale* def = findLocale(defaultLanguage_)) {
545 auto it = def->strings.find(key);
546 if (it != def->strings.end()) return it->second;
547 }
548 }
549 return key;
550}
551
552std::string I18n::formatString(const std::string& tpl,
553 const std::unordered_map<std::string, std::string>& params) const {
554 std::string out;
555 out.reserve(tpl.size());
556 for (size_t i = 0; i < tpl.size();) {
557 if (tpl[i] == '{') {
558 const size_t close = tpl.find('}', i + 1);
559 if (close != std::string::npos) {
560 const std::string key = tpl.substr(i + 1, close - i - 1);
561 const auto it = params.find(key);
562 if (it != params.end()) {
563 out += it->second;
564 i = close + 1;
565 continue;
566 }
567 }
568 }
569 out += tpl[i++];
570 }
571 return out;
572}
573
574std::string I18n::getWithParams(const std::string& key,
575 const std::unordered_map<std::string, std::string>& params) const {
576 return formatString(get(key), params);
577}
578
579std::string I18n::getPlural(const std::string& key, int n) const { return getPluralWithParams(key, n, {}); }
580
581std::string I18n::getPluralWithParams(const std::string& key, int n,
582 const std::unordered_map<std::string, std::string>& params) const {
583 std::unordered_map<std::string, std::string> merged = params;
584 merged["n"] = std::to_string(n);
585
586 const Locale* cur = findLocale(language_);
587 const Locale* def = (defaultLanguage_ != language_) ? findLocale(defaultLanguage_) : nullptr;
588 if (!cur && !def) return key;
589
590 // Look up in the current language first, then fall back to the default.
591 std::string current;
592 if (const Locale* loc = cur) {
593 const auto pit = loc->plurals.find(key);
594 if (pit != loc->plurals.end()) {
595 const std::string form = pluralForm(language_, n);
596 auto it = pit->second.find(form);
597 if (it == pit->second.end()) it = pit->second.find("other");
598 if (it == pit->second.end()) it = pit->second.find("one");
599 if (it != pit->second.end()) return formatString(it->second, merged);
600 } else {
601 const auto sit = loc->strings.find(key);
602 if (sit != loc->strings.end()) return formatString(sit->second, merged);
603 }
604 }
605 if (const Locale* loc = def) {
606 const auto pit = loc->plurals.find(key);
607 if (pit != loc->plurals.end()) {
608 const std::string form = pluralForm(defaultLanguage_, n);
609 auto it = pit->second.find(form);
610 if (it == pit->second.end()) it = pit->second.find("other");
611 if (it == pit->second.end()) it = pit->second.find("one");
612 if (it != pit->second.end()) return formatString(it->second, merged);
613 } else {
614 const auto sit = loc->strings.find(key);
615 if (sit != loc->strings.end()) return formatString(sit->second, merged);
616 }
617 }
618 return key;
619}
620
621std::string I18n::pluralForm(const std::string& lang, int n) const {
622 std::string base = lang;
623 if (const size_t dash = base.find('-'); dash != std::string::npos) base = base.substr(0, dash);
624 if (const size_t under = base.find('_'); under != std::string::npos) base = base.substr(0, under);
625
626 if (base == "zh" || base == "ja" || base == "ko" || base == "th" || base == "vi" || base == "id" || base == "ms" ||
627 base == "tr" || base == "my")
628 return "other";
629
630 if (base == "fr" || base == "pt") return (n == 0 || n == 1) ? "one" : "other";
631
632 if (base == "ru" || base == "uk" || base == "be") {
633 const int mod10 = n % 10;
634 const int mod100 = n % 100;
635 if (mod10 == 1 && mod100 != 11) return "one";
636 if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return "few";
637 return "many";
638 }
639
640 if (base == "pl") {
641 const int mod10 = n % 10;
642 const int mod100 = n % 100;
643 if (n == 1) return "one";
644 if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return "few";
645 return "many";
646 }
647
648 if (base == "cs" || base == "sk") {
649 if (n == 1) return "one";
650 if (n >= 2 && n <= 4) return "few";
651 return "other";
652 }
653
654 return (n == 1) ? "one" : "other";
655}
656
657int I18n::update(float dt) {
658 (void)dt;
659 if (!autoReload_) return 0;
660
661 // Collect dirty entries first so reloading cannot invalidate the iterator.
662 std::vector<std::string> dirty;
663 for (const auto& [lang, loc] : locales_) {
664 if (loc.path.empty()) continue;
665 const int64_t mt = fileModtime(loc.path);
666 if (mt >= 0 && mt != loc.modtime) dirty.push_back(lang);
667 }
668
669 int reloaded = 0;
670 for (const std::string& lang : dirty) {
671 if (replaceLocaleFromFile(lang, locales_[lang].path).ok()) ++reloaded;
672 }
673 return reloaded;
674}
675
676// ---------------------------------------------------------------------------
677// Script binding
678// ---------------------------------------------------------------------------
679
680void I18n::expose(ssq::Table& table) {
681 auto cls = table.addClass(name, I18n::create, false);
682 expose(cls);
683}
684
685void I18n::expose(ssq::Class& cls) {
686 cls.addFunc("getName", &I18n::getName);
687 cls.addFunc("loadFromJson", &I18n::loadFromJson);
688 cls.addFunc("loadFromFile", &I18n::loadFromFile);
689 cls.addFunc("replaceLocaleFromJson", [vm = cls.getHandle()](I18n* self, const std::string& lang,
690 const std::string& json) {
691 if (!self)
692 return eve::script::projectResult(vm, eve::Result<void>::failure(eve::Diagnostic::error(
693 eve::DiagnosticCode::InvalidArgument,
694 "I18n receiver must not be null", "i18n", {}, "i18n.squirrel")));
695 return eve::script::projectResult(vm, self->replaceLocaleFromJson(lang, json));
696 });
697 cls.addFunc("replaceLocaleFromFile", [vm = cls.getHandle()](I18n* self, const std::string& lang,
698 const std::string& path) {
699 if (!self)
702 "I18n receiver must not be null", "i18n", {}, "i18n.squirrel")));
703 return eve::script::projectResult(vm, self->replaceLocaleFromFile(lang, path));
704 });
705 cls.addFunc("replaceBundleFromJson", [vm = cls.getHandle()](I18n* self, const std::string& json) {
706 if (!self)
710 "I18n receiver must not be null", "i18n", {}, "i18n.squirrel")),
711 [](int count) { return eve::Value(count); });
712 return eve::script::projectResult(vm, self->replaceBundleFromJson(json),
713 [](int count) { return eve::Value(count); });
714 });
715 cls.addFunc("unload", &I18n::unload);
716 cls.addFunc("clear", &I18n::clear);
717 cls.addFunc("setLanguage", &I18n::setLanguage);
718 cls.addFunc("selectLanguage", [vm = cls.getHandle()](I18n* self, const std::string& lang) {
719 if (!self)
722 "I18n receiver must not be null", "i18n", {}, "i18n.squirrel")));
723 return eve::script::projectResult(vm, self->selectLanguage(lang));
724 });
725 cls.addFunc("getLanguage", &I18n::getLanguage);
726 cls.addFunc("setDefaultLanguage", &I18n::setDefaultLanguage);
727 cls.addFunc("getDefaultLanguage", &I18n::getDefaultLanguage);
728 cls.addFunc("getLanguageCount", &I18n::getLanguageCount);
729 cls.addFunc("getLanguageAt", &I18n::getLanguageAt);
730 cls.addFunc("hasLanguage", &I18n::hasLanguage);
731 cls.addFunc("hasInLanguage", &I18n::hasInLanguage);
732 cls.addFunc("validateKeyCoverage", [vm = cls.getHandle()](I18n* self, const std::string& key) {
733 if (!self)
737 "I18n receiver must not be null", "i18n", {}, "i18n.squirrel")),
738 [](int count) { return eve::Value(count); });
739 return eve::script::projectResult(vm, self->validateKeyCoverage(key),
740 [](int count) { return eve::Value(count); });
741 });
742 cls.addFunc("getKeyCount", &I18n::getKeyCount);
743 cls.addFunc("has", &I18n::has);
744 cls.addFunc("get", &I18n::get);
745 cls.addFunc("getWithParams", [](I18n* self, const std::string& key, ssq::Object params) -> std::string {
746 return self->getWithParams(key, readParams(params));
747 });
748 cls.addFunc("getPlural", &I18n::getPlural);
749 cls.addFunc("getPluralWithParams",
750 [](I18n* self, const std::string& key, int n, ssq::Object params) -> std::string {
751 return self->getPluralWithParams(key, n, readParams(params));
752 });
753 cls.addFunc("setAutoReload", &I18n::setAutoReload);
754 cls.addFunc("isAutoReload", &I18n::isAutoReload);
755 cls.addFunc("update", &I18n::update);
756}
757
758} // namespace eve::i18n
double value
SQInteger top
int root
Definition AnimSmr.cpp:119
struct SQVM * HSQUIRRELVM
HSQUIRRELVM vm
Definition ECS.cpp:20
HSQOBJECT cls
Definition ECS.cpp:21
std::uint32_t key
glm::vec3 n
Definition Grass.cpp:63
float v
std::int32_t c
std::string text
size_t offset
std::string name
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:26
std::string error
Definition Package.cpp:60
bool dirty
std::string path
Definition PlayHost.cpp:110
const RoadNode * node
double current
std::uint32_t count
The single Squirrel projection for common Result, Status and Value.
uint32_t index
const UnitySourceAsset & source
std::uint32_t depth
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
virtual std::string getName() const =0
Returns the name.
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:480
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
Data buffer paired with a filename (used for type identification).
Definition FileData.h:14
size_t getSize() const
Byte length of the owned buffer.
Definition FileData.h:29
void * getData() const
Pointer to the owned byte buffer.
Definition FileData.h:27
Game localization (i18n) module.
Definition I18n.h:38
bool loadFromJson(const std::string &lang, const std::string &json)
Compatibility-only bool projection of replaceLocaleFromJson.
Definition I18n.cpp:278
std::string getDefaultLanguage() const
Returns the default language.
Definition I18n.h:106
std::string getPluralWithParams(const std::string &key, int n, const std::unordered_map< std::string, std::string > &params) const
Returns the plural with params.
Definition I18n.cpp:581
bool hasLanguage(const std::string &lang) const
True when language.
Definition I18n.cpp:482
int update(float dt)
Re-read changed locale files; returns the number of reloads performed.
Definition I18n.cpp:657
void setAutoReload(bool enable)
Sets the auto reload.
Definition I18n.h:141
bool isAutoReload() const
True when auto reload.
Definition I18n.h:143
eve::Result< void > selectLanguage(const std::string &lang)
Select an admitted language with a structured missing-locale diagnostic.
Definition I18n.cpp:463
bool loadFromFile(const std::string &lang, const std::string &path)
Compatibility-only bool projection of replaceLocaleFromFile.
Definition I18n.cpp:455
std::string getWithParams(const std::string &key, const std::unordered_map< std::string, std::string > &params) const
Returns the with params.
Definition I18n.cpp:574
void unload(const std::string &lang)
卸载 / 清空语言表。
Definition I18n.cpp:459
bool hasInLanguage(const std::string &lang, const std::string &key) const
Return whether one exact locale owns key, without default-language substitution.
Definition I18n.cpp:484
std::string getLanguage() const
Returns the language.
Definition I18n.h:102
bool setLanguage(const std::string &lang)
Compatibility-only bool projection of selectLanguage.
Definition I18n.cpp:471
void clear()
Clears .
Definition I18n.cpp:461
bool has(const std::string &key) const
翻译查找:按键(点号命名空间)取字符串。
Definition I18n.cpp:526
I18n()=default
I 18 n.
std::string getLanguageAt(int index) const
Returns the language at.
Definition I18n.cpp:473
eve::Result< void > replaceLocaleFromJson(const std::string &lang, const std::string &json)
Atomically replace one locale from JSON with structured failure details.
Definition I18n.cpp:262
int getKeyCount(const std::string &lang) const
Return the number of singular and plural keys owned by one locale, or zero if absent.
Definition I18n.cpp:521
eve::Result< int > validateKeyCoverage(const std::string &key) const
Validate that every admitted locale owns one exact product key.
Definition I18n.cpp:489
std::string getPlural(const std::string &key, int n) const
Returns the plural.
Definition I18n.cpp:579
eve::Result< void > replaceLocaleFromFile(const std::string &lang, const std::string &path)
Atomically replace one locale from a VFS file with structured failure details.
Definition I18n.cpp:416
std::string get(const std::string &key) const
Returns the value.
Definition I18n.cpp:538
void setDefaultLanguage(const std::string &lang)
Sets the default language.
Definition I18n.h:104
int getLanguageCount() const
Returns the language count.
Definition I18n.h:108
eve::Result< int > replaceBundleFromJson(const std::string &json)
Atomically replace every locale from one strictly validated product bundle.
Definition I18n.cpp:282
EVENGINE_API_FOUNDATION public API.
Definition Json.h:136
bool valid() const
Valid.
Definition Json.h:157
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
Definition Json.cpp:581
Value root() const
Root.
Definition Json.h:159
EVENGINE_API_FOUNDATION public API.
Definition Json.h:38
bool isObject() const
True when object.
Definition Json.cpp:331
std::vector< ParamSpec > params
WGPUStringView sv(const char *s)
Build a WGPUStringView from a C string (null-safe, length auto-computed).
Definition wgpu_types.h:20
ssq::Table projectResult(HSQUIRRELVM vm, Result< void > &&result)
Consume and project a void native Result using the common schema.
glm::uvec4 info