Files
EVEngine/test/i18n.cpp
T

317 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "filesystem/Filesystem.h"
#include "i18n/I18n.h"
#include <cstring>
#include <string>
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<std::string, std::string> 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<std::string, std::string> 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"));
}