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;
31std::string numberToString(
double v) {
32 std::ostringstream
os;
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();
43 bool allCats = !names.empty();
44 bool allStrings =
true;
45 for (
const auto &k : names) {
46 if (!isPluralCategory(k)) {
50 if (!
node.get(k.c_str()).isString()) allStrings =
false;
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);
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);
64 if (prefix.empty())
return;
66 if (
node.isString() ||
node.isNumber() ||
node.isBool()) strings[prefix] =
node.asString();
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,
74 if (!doc.
valid())
return false;
76 if (
error) *
error =
"locale root must be a JSON object";
81 flatten(doc.
root(),
"", strings, plurals);
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;
94std::unordered_map<std::string, std::string> readParams(ssq::Object params) {
95 std::unordered_map<std::string, std::string> out;
98 const HSQOBJECT raw = params.getRaw();
99 if (raw._type != OT_TABLE)
return out;
101 const SQInteger top = sq_gettop(
vm);
102 sq_pushobject(
vm, raw);
104 while (SQ_SUCCEEDED(sq_next(
vm, -2))) {
106 const SQChar *k =
nullptr;
107 if (sq_gettype(
vm, -2) == OT_STRING && SQ_SUCCEEDED(sq_getstring(
vm, -2, &k)) && k) {
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) {
115 if (SQ_SUCCEEDED(sq_getinteger(
vm, -1, &iv)))
v = std::to_string(iv);
116 }
else if (vt == OT_FLOAT) {
118 if (SQ_SUCCEEDED(sq_getfloat(
vm, -1, &fv)))
v = numberToString(fv);
119 }
else if (vt == OT_BOOL) {
121 if (SQ_SUCCEEDED(sq_getbool(
vm, -1, &bv)))
v = bv ?
"true" :
"false";
123 out[k] = std::move(
v);
137I18n::Locale *I18n::findLocale(
const std::string &lang) {
138 auto it = locales_.find(lang);
139 return it == locales_.end() ? nullptr : &it->second;
142const I18n::Locale *I18n::findLocale(
const std::string &lang)
const {
143 auto it = locales_.find(lang);
144 return it == locales_.end() ? nullptr : &it->second;
148 if (lang.empty())
return false;
151 if (!parseLocale(json, loc.strings, loc.plurals, &
error))
return false;
154 locales_[lang] = std::move(loc);
159 if (lang.empty() || path.empty())
return false;
161 auto *fs = ModuleManager::getInstance<filesystem::Filesystem>(
"Filesystem");
162 if (!fs) fs = filesystem::Filesystem::create();
171 if (fd ==
nullptr || fd->
getData() ==
nullptr || fd->
getSize() == 0) {
176 std::string text(
static_cast<const char *
>(fd->
getData()), fd->
getSize());
181 if (!parseLocale(text, loc.strings, loc.plurals, &
error))
return false;
184 loc.modtime = fileModtime(path);
186 locales_[lang] = std::move(loc);
191 locales_.erase(lang);
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)];
214 return findLocale(lang) !=
nullptr;
218 const Locale *cur = findLocale(language_);
219 if (cur && (cur->strings.find(key) != cur->strings.end() || cur->plurals.find(key) != cur->plurals.end()))
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())
230 if (
const Locale *cur = findLocale(language_)) {
231 auto it = cur->strings.find(key);
232 if (it != cur->strings.end())
return it->second;
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;
243std::string I18n::formatString(
const std::string &tpl,
244 const std::unordered_map<std::string, std::string> ¶ms)
const {
246 out.reserve(tpl.size());
247 for (
size_t i = 0; i < tpl.size();) {
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()) {
266 const std::unordered_map<std::string, std::string> ¶ms)
const {
267 return formatString(
get(key), params);
275 const std::unordered_map<std::string, std::string> ¶ms)
const {
276 std::unordered_map<std::string, std::string> merged = params;
277 merged[
"n"] = std::to_string(
n);
279 const Locale *cur = findLocale(language_);
280 const Locale *def = (defaultLanguage_ != language_) ? findLocale(defaultLanguage_) :
nullptr;
281 if (!cur && !def)
return key;
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);
294 const auto sit = loc->strings.find(key);
295 if (sit != loc->strings.end())
return formatString(sit->second, merged);
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);
307 const auto sit = loc->strings.find(key);
308 if (sit != loc->strings.end())
return formatString(sit->second, merged);
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);
319 if (base ==
"zh" || base ==
"ja" || base ==
"ko" || base ==
"th" || base ==
"vi" ||
320 base ==
"id" || base ==
"ms" || base ==
"tr" || base ==
"my")
323 if (base ==
"fr" || base ==
"pt")
return (
n == 0 ||
n == 1) ?
"one" :
"other";
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";
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";
341 if (base ==
"cs" || base ==
"sk") {
342 if (
n == 1)
return "one";
343 if (
n >= 2 &&
n <= 4)
return "few";
347 return (
n == 1) ?
"one" :
"other";
352 if (!autoReload_)
return 0;
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);
363 for (
const std::string &lang :
dirty) {
364 if (
loadFromFile(lang, locales_[lang].path)) ++reloaded;
373void I18n::expose(ssq::Table &table) {
374 auto cls = table.addClass(
name, I18n::create,
false);
378void I18n::expose(ssq::Class &
cls) {
393 cls.addFunc(
"getWithParams", [](
I18n *self,
const std::string &key,
394 ssq::Object params) -> std::string {
395 return self->getWithParams(key, readParams(params));
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));