载入中...
搜索中...
未找到
I18n.cpp
浏览该文件的文档.
1#include "i18n/I18n.h"
2
5#include "common/Json.h"
6#include "common/Module.h"
7
8#include <simplesquirrel/simplesquirrel.hpp>
9#include <squirrel.h>
10
11#include <algorithm>
12#include <cmath>
13#include <cstdint>
14#include <sstream>
15#include <vector>
16
17namespace eve::i18n {
18
20
21namespace {
22
23bool isPluralCategory(const std::string &key) {
24 static const char *cats[] = {"zero", "one", "two", "few", "many", "other"};
25 for (const char *c : cats)
26 if (key == c) return true;
27 return false;
28}
29
31std::string numberToString(double v) {
32 std::ostringstream os;
33 os << v;
34 return os.str();
35}
36
37void flatten(eve::json::Value node, const std::string &prefix,
38 std::unordered_map<std::string, std::string> &strings,
39 std::unordered_map<std::string, std::unordered_map<std::string, std::string>> &plurals) {
40 if (node.isObject()) {
41 const std::vector<std::string> names = node.keys();
42 // A plain object of plural categories is a plural form table.
43 bool allCats = !names.empty();
44 bool allStrings = true;
45 for (const auto &k : names) {
46 if (!isPluralCategory(k)) {
47 allCats = false;
48 break;
49 }
50 if (!node.get(k.c_str()).isString()) allStrings = false;
51 }
52 if (allCats && allStrings && !prefix.empty()) {
53 std::unordered_map<std::string, std::string> forms;
54 for (const auto &k : names) forms[k] = node.getString(k.c_str());
55 plurals[prefix] = std::move(forms);
56 return;
57 }
58 for (const auto &k : names) {
59 const std::string path = prefix.empty() ? k : prefix + "." + k;
60 flatten(node.get(k.c_str()), path, strings, plurals);
61 }
62 return;
63 }
64 if (prefix.empty()) return;
65 // Scalars stringify; arrays and nulls are not translatable and are skipped.
66 if (node.isString() || node.isNumber() || node.isBool()) strings[prefix] = node.asString();
67}
68
69bool parseLocale(const std::string &text,
70 std::unordered_map<std::string, std::string> &strings,
71 std::unordered_map<std::string, std::unordered_map<std::string, std::string>> &plurals,
72 std::string *error) {
74 if (!doc.valid()) return false;
75 if (!doc.root().isObject()) {
76 if (error) *error = "locale root must be a JSON object";
77 return false;
78 }
79 strings.clear();
80 plurals.clear();
81 flatten(doc.root(), "", strings, plurals);
82 return true;
83}
84
85int64_t fileModtime(const std::string &path) {
86 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>("Filesystem");
87 if (!fs) fs = eve::filesystem::Filesystem::create();
89 if (!fs->getInfo(path, info)) return -1;
90 return info.modtime;
91}
92
93// Convert a Squirrel table (or null) into a string params map.
94std::unordered_map<std::string, std::string> readParams(ssq::Object params) {
95 std::unordered_map<std::string, std::string> out;
96 HSQUIRRELVM vm = params.getHandle();
97 if (!vm) return out;
98 const HSQOBJECT raw = params.getRaw();
99 if (raw._type != OT_TABLE) return out;
100
101 const SQInteger top = sq_gettop(vm);
102 sq_pushobject(vm, raw);
103 sq_pushnull(vm); // iterator
104 while (SQ_SUCCEEDED(sq_next(vm, -2))) {
105 // key at -2, value at -1
106 const SQChar *k = nullptr;
107 if (sq_gettype(vm, -2) == OT_STRING && SQ_SUCCEEDED(sq_getstring(vm, -2, &k)) && k) {
108 std::string v;
109 const SQObjectType vt = sq_gettype(vm, -1);
110 if (vt == OT_STRING) {
111 const SQChar *sv = nullptr;
112 if (SQ_SUCCEEDED(sq_getstring(vm, -1, &sv)) && sv) v = sv;
113 } else if (vt == OT_INTEGER) {
114 SQInteger iv = 0;
115 if (SQ_SUCCEEDED(sq_getinteger(vm, -1, &iv))) v = std::to_string(iv);
116 } else if (vt == OT_FLOAT) {
117 SQFloat fv = 0;
118 if (SQ_SUCCEEDED(sq_getfloat(vm, -1, &fv))) v = numberToString(fv);
119 } else if (vt == OT_BOOL) {
120 SQBool bv = SQFalse;
121 if (SQ_SUCCEEDED(sq_getbool(vm, -1, &bv))) v = bv ? "true" : "false";
122 }
123 out[k] = std::move(v);
124 }
125 sq_pop(vm, 2);
126 }
127 sq_settop(vm, top);
128 return out;
129}
130
131} // namespace
132
133// ---------------------------------------------------------------------------
134// Module implementation
135// ---------------------------------------------------------------------------
136
137I18n::Locale *I18n::findLocale(const std::string &lang) {
138 auto it = locales_.find(lang);
139 return it == locales_.end() ? nullptr : &it->second;
140}
141
142const I18n::Locale *I18n::findLocale(const std::string &lang) const {
143 auto it = locales_.find(lang);
144 return it == locales_.end() ? nullptr : &it->second;
145}
146
147bool I18n::loadFromJson(const std::string &lang, const std::string &json) {
148 if (lang.empty()) return false;
149 Locale loc;
150 std::string error;
151 if (!parseLocale(json, loc.strings, loc.plurals, &error)) return false;
152 loc.path = "";
153 loc.modtime = -1;
154 locales_[lang] = std::move(loc);
155 return true;
156}
157
158bool I18n::loadFromFile(const std::string &lang, const std::string &path) {
159 if (lang.empty() || path.empty()) return false;
160
161 auto *fs = ModuleManager::getInstance<filesystem::Filesystem>("Filesystem");
162 if (!fs) fs = filesystem::Filesystem::create();
163
164 filesystem::FileData *fd = nullptr;
165 try {
166 fd = fs->read(path);
167 } catch (...) {
168 delete fd;
169 return false;
170 }
171 if (fd == nullptr || fd->getData() == nullptr || fd->getSize() == 0) {
172 delete fd;
173 return false;
174 }
175
176 std::string text(static_cast<const char *>(fd->getData()), fd->getSize());
177 delete fd;
178
179 Locale loc;
180 std::string error;
181 if (!parseLocale(text, loc.strings, loc.plurals, &error)) return false;
182
183 loc.path = path;
184 loc.modtime = fileModtime(path);
185 fs->watch(path);
186 locales_[lang] = std::move(loc);
187 return true;
188}
189
190void I18n::unload(const std::string &lang) {
191 locales_.erase(lang);
192}
193
195 locales_.clear();
196}
197
198bool I18n::setLanguage(const std::string &lang) {
199 if (!hasLanguage(lang)) return false;
200 language_ = lang;
201 return true;
202}
203
204std::string I18n::getLanguageAt(int index) const {
205 if (index < 0 || size_t(index) >= locales_.size()) return {};
206 std::vector<std::string> langs;
207 langs.reserve(locales_.size());
208 for (const auto &[lang, loc] : locales_) langs.push_back(lang);
209 std::sort(langs.begin(), langs.end());
210 return langs[size_t(index)];
211}
212
213bool I18n::hasLanguage(const std::string &lang) const {
214 return findLocale(lang) != nullptr;
215}
216
217bool I18n::has(const std::string &key) const {
218 const Locale *cur = findLocale(language_);
219 if (cur && (cur->strings.find(key) != cur->strings.end() || cur->plurals.find(key) != cur->plurals.end()))
220 return true;
221 if (defaultLanguage_ != language_) {
222 if (const Locale *def = findLocale(defaultLanguage_))
223 if (def->strings.find(key) != def->strings.end() || def->plurals.find(key) != def->plurals.end())
224 return true;
225 }
226 return false;
227}
228
229std::string I18n::get(const std::string &key) const {
230 if (const Locale *cur = findLocale(language_)) {
231 auto it = cur->strings.find(key);
232 if (it != cur->strings.end()) return it->second;
233 }
234 if (defaultLanguage_ != language_) {
235 if (const Locale *def = findLocale(defaultLanguage_)) {
236 auto it = def->strings.find(key);
237 if (it != def->strings.end()) return it->second;
238 }
239 }
240 return key;
241}
242
243std::string I18n::formatString(const std::string &tpl,
244 const std::unordered_map<std::string, std::string> &params) const {
245 std::string out;
246 out.reserve(tpl.size());
247 for (size_t i = 0; i < tpl.size();) {
248 if (tpl[i] == '{') {
249 const size_t close = tpl.find('}', i + 1);
250 if (close != std::string::npos) {
251 const std::string name = tpl.substr(i + 1, close - i - 1);
252 const auto it = params.find(name);
253 if (it != params.end()) {
254 out += it->second;
255 i = close + 1;
256 continue;
257 }
258 }
259 }
260 out += tpl[i++];
261 }
262 return out;
263}
264
265std::string I18n::getWithParams(const std::string &key,
266 const std::unordered_map<std::string, std::string> &params) const {
267 return formatString(get(key), params);
268}
269
270std::string I18n::getPlural(const std::string &key, int n) const {
271 return getPluralWithParams(key, n, {});
272}
273
274std::string I18n::getPluralWithParams(const std::string &key, int n,
275 const std::unordered_map<std::string, std::string> &params) const {
276 std::unordered_map<std::string, std::string> merged = params;
277 merged["n"] = std::to_string(n);
278
279 const Locale *cur = findLocale(language_);
280 const Locale *def = (defaultLanguage_ != language_) ? findLocale(defaultLanguage_) : nullptr;
281 if (!cur && !def) return key;
282
283 // Look up in the current language first, then fall back to the default.
284 std::string current;
285 if (const Locale *loc = cur) {
286 const auto pit = loc->plurals.find(key);
287 if (pit != loc->plurals.end()) {
288 const std::string form = pluralForm(language_, n);
289 auto it = pit->second.find(form);
290 if (it == pit->second.end()) it = pit->second.find("other");
291 if (it == pit->second.end()) it = pit->second.find("one");
292 if (it != pit->second.end()) return formatString(it->second, merged);
293 } else {
294 const auto sit = loc->strings.find(key);
295 if (sit != loc->strings.end()) return formatString(sit->second, merged);
296 }
297 }
298 if (const Locale *loc = def) {
299 const auto pit = loc->plurals.find(key);
300 if (pit != loc->plurals.end()) {
301 const std::string form = pluralForm(defaultLanguage_, n);
302 auto it = pit->second.find(form);
303 if (it == pit->second.end()) it = pit->second.find("other");
304 if (it == pit->second.end()) it = pit->second.find("one");
305 if (it != pit->second.end()) return formatString(it->second, merged);
306 } else {
307 const auto sit = loc->strings.find(key);
308 if (sit != loc->strings.end()) return formatString(sit->second, merged);
309 }
310 }
311 return key;
312}
313
314std::string I18n::pluralForm(const std::string &lang, int n) const {
315 std::string base = lang;
316 if (const size_t dash = base.find('-'); dash != std::string::npos) base = base.substr(0, dash);
317 if (const size_t under = base.find('_'); under != std::string::npos) base = base.substr(0, under);
318
319 if (base == "zh" || base == "ja" || base == "ko" || base == "th" || base == "vi" ||
320 base == "id" || base == "ms" || base == "tr" || base == "my")
321 return "other";
322
323 if (base == "fr" || base == "pt") return (n == 0 || n == 1) ? "one" : "other";
324
325 if (base == "ru" || base == "uk" || base == "be") {
326 const int mod10 = n % 10;
327 const int mod100 = n % 100;
328 if (mod10 == 1 && mod100 != 11) return "one";
329 if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return "few";
330 return "many";
331 }
332
333 if (base == "pl") {
334 const int mod10 = n % 10;
335 const int mod100 = n % 100;
336 if (n == 1) return "one";
337 if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return "few";
338 return "many";
339 }
340
341 if (base == "cs" || base == "sk") {
342 if (n == 1) return "one";
343 if (n >= 2 && n <= 4) return "few";
344 return "other";
345 }
346
347 return (n == 1) ? "one" : "other";
348}
349
350int I18n::update(float dt) {
351 (void)dt;
352 if (!autoReload_) return 0;
353
354 // Collect dirty entries first so reloading cannot invalidate the iterator.
355 std::vector<std::string> dirty;
356 for (const auto &[lang, loc] : locales_) {
357 if (loc.path.empty()) continue;
358 const int64_t mt = fileModtime(loc.path);
359 if (mt >= 0 && mt != loc.modtime) dirty.push_back(lang);
360 }
361
362 int reloaded = 0;
363 for (const std::string &lang : dirty) {
364 if (loadFromFile(lang, locales_[lang].path)) ++reloaded;
365 }
366 return reloaded;
367}
368
369// ---------------------------------------------------------------------------
370// Script binding
371// ---------------------------------------------------------------------------
372
373void I18n::expose(ssq::Table &table) {
374 auto cls = table.addClass(name, I18n::create, false);
375 expose(cls);
376}
377
378void I18n::expose(ssq::Class &cls) {
379 cls.addFunc("getName", &I18n::getName);
380 cls.addFunc("loadFromJson", &I18n::loadFromJson);
381 cls.addFunc("loadFromFile", &I18n::loadFromFile);
382 cls.addFunc("unload", &I18n::unload);
383 cls.addFunc("clear", &I18n::clear);
384 cls.addFunc("setLanguage", &I18n::setLanguage);
385 cls.addFunc("getLanguage", &I18n::getLanguage);
386 cls.addFunc("setDefaultLanguage", &I18n::setDefaultLanguage);
387 cls.addFunc("getDefaultLanguage", &I18n::getDefaultLanguage);
388 cls.addFunc("getLanguageCount", &I18n::getLanguageCount);
389 cls.addFunc("getLanguageAt", &I18n::getLanguageAt);
390 cls.addFunc("hasLanguage", &I18n::hasLanguage);
391 cls.addFunc("has", &I18n::has);
392 cls.addFunc("get", &I18n::get);
393 cls.addFunc("getWithParams", [](I18n *self, const std::string &key,
394 ssq::Object params) -> std::string {
395 return self->getWithParams(key, readParams(params));
396 });
397 cls.addFunc("getPlural", &I18n::getPlural);
398 cls.addFunc("getPluralWithParams", [](I18n *self, const std::string &key, int n,
399 ssq::Object params) -> std::string {
400 return self->getPluralWithParams(key, n, readParams(params));
401 });
402 cls.addFunc("setAutoReload", &I18n::setAutoReload);
403 cls.addFunc("isAutoReload", &I18n::isAutoReload);
404 cls.addFunc("update", &I18n::update);
405}
406
407} // namespace eve::i18n
struct SQVM * HSQUIRRELVM
HSQUIRRELVM vm
Definition ECS.cpp:20
HSQOBJECT cls
Definition ECS.cpp:21
glm::vec3 n
Definition Grass.cpp:64
std::string error
std::ostringstream & os
uint32_t c
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:24
bool dirty
const char * name
Definition RockMesh.cpp:21
int v
virtual std::string getName() const =0
Data buffer paired with a filename (used for type identification).
Definition FileData.h:12
size_t getSize() const
Gets the size of the Data in bytes.
Definition FileData.h:24
void * getData() const
Gets a pointer to the data. This pointer will obviously not be valid if the Data object is destroyed.
Definition FileData.h:23
Game localization (i18n) module.
Definition I18n.h:35
bool loadFromJson(const std::string &lang, const std::string &json)
加载语言表(JSON 文本 / 文件)。
Definition I18n.cpp:147
std::string getDefaultLanguage() const
Definition I18n.h:52
std::string getPluralWithParams(const std::string &key, int n, const std::unordered_map< std::string, std::string > &params) const
Definition I18n.cpp:274
bool hasLanguage(const std::string &lang) const
Definition I18n.cpp:213
int update(float dt)
Re-read changed locale files; returns the number of reloads performed.
Definition I18n.cpp:350
void setAutoReload(bool enable)
Definition I18n.h:67
bool isAutoReload() const
Definition I18n.h:68
bool loadFromFile(const std::string &lang, const std::string &path)
Definition I18n.cpp:158
std::string getWithParams(const std::string &key, const std::unordered_map< std::string, std::string > &params) const
Definition I18n.cpp:265
void unload(const std::string &lang)
卸载 / 清空语言表。
Definition I18n.cpp:190
std::string getLanguage() const
Definition I18n.h:50
bool setLanguage(const std::string &lang)
语言管理:当前语言 / 默认语言回退。
Definition I18n.cpp:198
void clear()
Definition I18n.cpp:194
bool has(const std::string &key) const
翻译查找:按键(点号命名空间)取字符串。
Definition I18n.cpp:217
I18n()=default
std::string getLanguageAt(int index) const
Definition I18n.cpp:204
std::string getPlural(const std::string &key, int n) const
Definition I18n.cpp:270
std::string get(const std::string &key) const
Definition I18n.cpp:229
void setDefaultLanguage(const std::string &lang)
Definition I18n.h:51
int getLanguageCount() const
Definition I18n.h:53
bool valid() const
Definition Json.h:111
static Document parse(const std::string &text, std::string *error=nullptr)
Definition Json.cpp:449
Value root() const
Definition Json.h:112
bool isObject() const
Definition Json.cpp:296
NodeDesc node(std::string id, std::vector< NodeDesc > children, std::string name)
Definition NodeDesc.cpp:214