#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "filesystem/Filesystem.h" #include "i18n/I18n.h" #include #include using namespace eve::i18n; TEST_CASE("i18n.loadAndLookup") { I18n* i18n = I18n::create(); REQUIRE(i18n != nullptr); i18n->clear(); CHECK(i18n->loadFromJson("en", R"({ "menu": { "start": "Start", "quit": "Quit" }, "greeting": "Hello, {name}!", "items": { "one": "{n} item", "other": "{n} items" } })")); CHECK(i18n->loadFromJson("zh", R"({ "menu": { "start": "\u5f00\u59cb", "quit": "\u9000\u51fa" }, "greeting": "\u4f60\u597d\uff0c{name}\uff01", "items": { "one": "{n} \u4e2a", "other": "{n} \u4e2a" } })")); CHECK_EQ(i18n->getLanguageCount(), 2); CHECK(i18n->hasLanguage("en")); CHECK(i18n->hasLanguage("zh")); CHECK(!i18n->hasLanguage("fr")); CHECK(i18n->setLanguage("zh")); CHECK_EQ(i18n->getLanguage(), std::string("zh")); CHECK_EQ(i18n->get("menu.start"), std::string("\xe5\xbc\x80\xe5\xa7\x8b")); // 开始 CHECK_EQ(i18n->get("menu.quit"), std::string("\xe9\x80\x80\xe5\x87\xba")); // 退出 CHECK(i18n->has("menu.start")); CHECK(!i18n->has("missing.key")); // Unknown key returns the key itself. CHECK_EQ(i18n->get("menu.nope"), std::string("menu.nope")); } TEST_CASE("i18n.defaultLanguageFallback") { I18n* i18n = I18n::create(); i18n->clear(); i18n->loadFromJson("en", R"({"only_en": "English"})"); i18n->loadFromJson("fr", R"({"menu.start": "D\u00e9marrer"})"); i18n->setDefaultLanguage("en"); i18n->setLanguage("fr"); CHECK_EQ(i18n->get("menu.start"), std::string("D\xc3\xa9marrer")); // Key missing in current language falls back to default language. CHECK_EQ(i18n->get("only_en"), std::string("English")); // Switching to an unloaded language keeps the previous one. CHECK(!i18n->setLanguage("de")); CHECK_EQ(i18n->getLanguage(), std::string("fr")); } TEST_CASE("i18n.paramsInterpolation") { I18n* i18n = I18n::create(); i18n->clear(); i18n->loadFromJson("en", R"({ "greeting": "Hello, {name}! You have {count} new messages.", "plain": "No placeholders here." })"); std::unordered_map params = {{"name", "World"}, {"count", "3"}}; CHECK_EQ(i18n->getWithParams("greeting", params), std::string("Hello, World! You have 3 new messages.")); CHECK_EQ(i18n->getWithParams("plain", params), std::string("No placeholders here.")); // Missing params leave the placeholder untouched. CHECK_EQ(i18n->getWithParams("greeting", {}), std::string("Hello, {name}! You have {count} new messages.")); } TEST_CASE("i18n.plurals") { I18n* i18n = I18n::create(); i18n->clear(); i18n->loadFromJson("en", R"({ "items": { "one": "{n} item", "other": "{n} items" }, "apples": { "one": "{n} apple", "other": "{n} apples" } })"); CHECK_EQ(i18n->getPlural("items", 1), std::string("1 item")); CHECK_EQ(i18n->getPlural("items", 0), std::string("0 items")); CHECK_EQ(i18n->getPlural("items", 5), std::string("5 items")); CHECK_EQ(i18n->getPlural("apples", 1), std::string("1 apple")); CHECK_EQ(i18n->getPlural("apples", 21), std::string("21 apples")); // Plural with extra params. std::unordered_map params = {{"kind", "red"}}; CHECK_EQ(i18n->getPluralWithParams("items", 2, params), std::string("2 items")); } TEST_CASE("i18n.pluralRulesByLanguage") { I18n* i18n = I18n::create(); i18n->clear(); // Russian three-form plural rules. i18n->loadFromJson("ru", R"({ "m": { "one": "{n} \u043e\u043a\u043d\u043e", "few": "{n} \u043e\u043a\u043d\u0430", "many": "{n} \u043e\u043a\u043e\u043d" } })"); i18n->setLanguage("ru"); CHECK_EQ(i18n->getPlural("m", 1), std::string("1 \xd0\xbe\xd0\xba\xd0\xbd\xd0\xbe")); CHECK_EQ(i18n->getPlural("m", 2), std::string("2 \xd0\xbe\xd0\xba\xd0\xbd\xd0\xb0")); CHECK_EQ(i18n->getPlural("m", 5), std::string("5 \xd0\xbe\xd0\xba\xd0\xbe\xd0\xbd")); CHECK_EQ(i18n->getPlural("m", 11), std::string("11 \xd0\xbe\xd0\xba\xd0\xbe\xd0\xbd")); CHECK_EQ(i18n->getPlural("m", 21), std::string("21 \xd0\xbe\xd0\xba\xd0\xbd\xd0\xbe")); CHECK_EQ(i18n->getPlural("m", 22), std::string("22 \xd0\xbe\xd0\xba\xd0\xbd\xd0\xb0")); // Chinese has no plural distinction -> "other". i18n->loadFromJson("zh", R"({"m": { "one": "{n}\u4e2a", "other": "{n}\u4e2a" }})"); i18n->setLanguage("zh"); CHECK_EQ(i18n->getPlural("m", 0), std::string("0\xe4\xb8\xaa")); CHECK_EQ(i18n->getPlural("m", 1), std::string("1\xe4\xb8\xaa")); CHECK_EQ(i18n->getPlural("m", 100), std::string("100\xe4\xb8\xaa")); // A locale that only defines "one"/"other" and defaults to "other" fallback. i18n->loadFromJson("en", R"({"m": { "one": "singular", "other": "plural" }})"); i18n->setLanguage("en"); CHECK_EQ(i18n->getPlural("m", 1), std::string("singular")); CHECK_EQ(i18n->getPlural("m", 2), std::string("plural")); } TEST_CASE("i18n.jsonNumbersAndEscapes") { I18n* i18n = I18n::create(); i18n->clear(); i18n->loadFromJson("en", R"({ "pi": 3.14, "enabled": true, "line": "first\nsecond", "quote": "say \"hi\"", "unicode": "\u00e9" })"); CHECK_EQ(i18n->get("pi"), std::string("3.14")); CHECK_EQ(i18n->get("enabled"), std::string("true")); CHECK_EQ(i18n->get("line"), std::string("first\nsecond")); CHECK_EQ(i18n->get("quote"), std::string("say \"hi\"")); CHECK_EQ(i18n->get("unicode"), std::string("\xc3\xa9")); } TEST_CASE("i18n.invalidJson") { I18n* i18n = I18n::create(); i18n->clear(); CHECK(!i18n->loadFromJson("en", "{ not valid json")); CHECK(!i18n->loadFromJson("en", R"(["root", "must", "be", "object"])")); CHECK(!i18n->loadFromJson("", R"({"a": "b"})")); CHECK_EQ(i18n->getLanguageCount(), 0); } TEST_CASE("i18n.strictBundleIsCompleteAtomicAndPlaceholderSafe") { I18n* i18n = I18n::create(); REQUIRE(i18n != nullptr); i18n->clear(); auto admitted = i18n->replaceBundleFromJson(R"({ "schema":"eve.i18n.bundle", "version":1, "defaultLocale":"en", "locales":{ "en":{ "dialogue":{"welcome":"Welcome, {name}!"}, "items":{"one":"{n} item","other":"{n} items"} }, "zh-CN":{ "dialogue":{"welcome":"欢迎,{name}!"}, "items":{"other":"{n} 件物品"} } } })"); REQUIRE(admitted.ok()); CHECK_EQ(admitted.value(), 2); CHECK_EQ(i18n->getDefaultLanguage(), std::string("en")); CHECK_EQ(i18n->getLanguage(), std::string("en")); CHECK_EQ(i18n->getKeyCount("en"), 2); CHECK(i18n->hasInLanguage("zh-CN", "dialogue.welcome")); auto covered = i18n->validateKeyCoverage("dialogue.welcome"); REQUIRE(covered.ok()); CHECK_EQ(covered.value(), 2); CHECK(!i18n->validateKeyCoverage("dialogue.missing").ok()); CHECK(!i18n->validateKeyCoverage("dialogue..welcome").ok()); CHECK(!i18n->validateKeyCoverage("dialogue.").ok()); REQUIRE(i18n->setLanguage("zh-CN")); CHECK_EQ(i18n->getWithParams("dialogue.welcome", {{"name", "Eve"}}), std::string("欢迎,Eve!")); auto incomplete = i18n->replaceBundleFromJson(R"({ "schema":"eve.i18n.bundle","version":1,"defaultLocale":"en", "locales":{"en":{"message":"Hello {name}"},"fr":{"message":"Bonjour"}} })"); CHECK(!incomplete.ok()); CHECK_EQ(i18n->getLanguage(), std::string("zh-CN")); CHECK_EQ(i18n->getLanguageCount(), 2); CHECK(i18n->hasInLanguage("en", "dialogue.welcome")); auto unknown = i18n->replaceBundleFromJson(R"({ "schema":"eve.i18n.bundle","version":1,"defaultLocale":"en","extra":true, "locales":{"en":{"message":"Hello"}} })"); CHECK(!unknown.ok()); CHECK_EQ(i18n->getLanguage(), std::string("zh-CN")); auto malformedPlural = i18n->replaceBundleFromJson(R"({ "schema":"eve.i18n.bundle","version":1,"defaultLocale":"en", "locales":{"en":{"items":{"one":"one item","label":"items"}}} })"); CHECK(!malformedPlural.ok()); CHECK_EQ(i18n->getLanguageCount(), 2); } TEST_CASE("i18n.structuredLocaleOperationsPreservePublishedStateOnFailure") { I18n* i18n = I18n::create(); REQUIRE(i18n != nullptr); i18n->clear(); auto admitted = i18n->replaceLocaleFromJson("en", R"({"greeting":"Hello"})"); REQUIRE(admitted.ok()); auto selected = i18n->selectLanguage("en"); REQUIRE(selected.ok()); auto malformed = i18n->replaceLocaleFromJson("en", "{ broken"); CHECK(!malformed.ok()); CHECK_EQ(i18n->get("greeting"), std::string("Hello")); auto missing = i18n->selectLanguage("fr"); CHECK(!missing.ok()); CHECK_EQ(i18n->getLanguage(), std::string("en")); } TEST_CASE("i18n.unloadAndClear") { I18n* i18n = I18n::create(); i18n->clear(); i18n->loadFromJson("en", R"({"a": "A"})"); i18n->loadFromJson("de", R"({"a": "B"})"); CHECK_EQ(i18n->getLanguageCount(), 2); i18n->unload("de"); CHECK(!i18n->hasLanguage("de")); CHECK_EQ(i18n->getLanguageCount(), 1); i18n->clear(); CHECK_EQ(i18n->getLanguageCount(), 0); } TEST_CASE("i18n.languageEnumeration") { I18n* i18n = I18n::create(); i18n->clear(); i18n->loadFromJson("zh", R"({"a": "1"})"); i18n->loadFromJson("en", R"({"a": "2"})"); i18n->loadFromJson("ja", R"({"a": "3"})"); CHECK_EQ(i18n->getLanguageCount(), 3); CHECK_EQ(i18n->getLanguageAt(0), std::string("en")); CHECK_EQ(i18n->getLanguageAt(1), std::string("ja")); CHECK_EQ(i18n->getLanguageAt(2), std::string("zh")); CHECK_EQ(i18n->getLanguageAt(3), std::string("")); } TEST_CASE("i18n.pluralMoreLanguages") { I18n* i18n = I18n::create(); i18n->clear(); i18n->loadFromJson("fr", R"({"m": { "one": "un", "other": "plusieurs" }})"); i18n->loadFromJson("pl", R"({"m": { "one": "jeden", "few": "kilka", "many": "duzo" }})"); i18n->loadFromJson("cs", R"({"m": { "one": "jedna", "few": "par", "other": "hodne" }})"); i18n->loadFromJson("zh-CN", R"({"m": { "one": "yi", "other": "duo" }})"); i18n->setLanguage("fr"); CHECK_EQ(i18n->getPlural("m", 0), std::string("un")); CHECK_EQ(i18n->getPlural("m", 2), std::string("plusieurs")); i18n->setLanguage("pl"); CHECK_EQ(i18n->getPlural("m", 1), std::string("jeden")); CHECK_EQ(i18n->getPlural("m", 3), std::string("kilka")); CHECK_EQ(i18n->getPlural("m", 15), std::string("duzo")); i18n->setLanguage("cs"); CHECK_EQ(i18n->getPlural("m", 1), std::string("jedna")); CHECK_EQ(i18n->getPlural("m", 3), std::string("par")); CHECK_EQ(i18n->getPlural("m", 7), std::string("hodne")); i18n->setLanguage("zh-CN"); CHECK_EQ(i18n->getPlural("m", 1), std::string("duo")); } TEST_CASE("i18n.autoReloadAndUpdateNoop") { I18n* i18n = I18n::create(); i18n->clear(); CHECK(i18n->isAutoReload()); // module default is on i18n->setAutoReload(false); CHECK(!i18n->isAutoReload()); i18n->setAutoReload(true); CHECK(i18n->isAutoReload()); i18n->loadFromJson("en", R"({"a": "A"})"); CHECK_EQ(i18n->update(0.0f), 0); // JSON-loaded locales have no file path to watch i18n->setAutoReload(false); CHECK(!i18n->isAutoReload()); } TEST_CASE("i18n.loadFromFileRoundTripAndMissing") { I18n* i18n = I18n::create(); i18n->clear(); CHECK(!i18n->loadFromFile("en", "")); CHECK(!i18n->loadFromFile("en", "nope.json")); CHECK(!i18n->loadFromFile("", "nope.json")); auto* fs = eve::filesystem::Filesystem::create(); REQUIRE(fs->setIdentity("ev_ut_i18n", true)); REQUIRE(fs->setupWriteDirectory()); const char* json = R"({"hello": "world", "n": 42})"; fs->write("i18n_locale.json", json, std::strlen(json)); CHECK(i18n->loadFromFile("en", "i18n_locale.json")); CHECK(i18n->hasLanguage("en")); CHECK_EQ(i18n->get("hello"), std::string("world")); CHECK_EQ(i18n->get("n"), std::string("42")); }