feat: add multilingual interface support

This commit is contained in:
2026-07-24 16:00:12 +01:00
parent d2567c59ce
commit 16383c2a3f
10 changed files with 788 additions and 84 deletions
+85
View File
@@ -0,0 +1,85 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const {
DEFAULT_LANGUAGE,
TRANSLATIONS,
createI18n,
detectLanguage,
normalizeLanguage,
} = require("../i18n.js");
function createStorage(initialValue) {
let value = initialValue;
return {
getItem() {
return value;
},
setItem(key, nextValue) {
value = nextValue;
},
removeItem() {
value = undefined;
},
};
}
test("normalizes exact and base language tags", () => {
assert.equal(normalizeLanguage("pt-PT"), "pt-PT");
assert.equal(normalizeLanguage("pt-BR"), "pt-PT");
assert.equal(normalizeLanguage("en-US"), "en");
assert.equal(normalizeLanguage("fr"), null);
});
test("prefers a stored language over browser preferences", () => {
const storage = createStorage("en");
assert.equal(detectLanguage({ storage, languages: ["pt-PT"] }), "en");
});
test("uses the first supported browser language and otherwise falls back", () => {
assert.equal(detectLanguage({ storage: createStorage(), languages: ["fr", "pt-BR"] }), "pt-PT");
assert.equal(detectLanguage({ storage: createStorage(), languages: ["es-MX"] }), "es");
assert.equal(detectLanguage({ storage: createStorage(), languages: ["fr"] }), DEFAULT_LANGUAGE);
});
test("persists language changes and interpolates translated values", () => {
const storage = createStorage();
const i18n = createI18n({ storage, languages: ["en"] });
assert.equal(i18n.setLanguage("pt"), "pt-PT");
assert.equal(storage.getItem(), "pt-PT");
assert.equal(
i18n.t("noParcelFound", { parcelNumber: "57" }),
"Não foi encontrada a parcela com o número “57”.",
);
});
test("browser-default mode removes the preference and follows the browser language", () => {
const storage = createStorage("en");
const i18n = createI18n({ storage, languages: ["pt-PT"] });
assert.equal(i18n.setLanguage(null), "pt-PT");
assert.equal(i18n.preference, null);
assert.equal(storage.getItem(), undefined);
});
test("uses English for missing translations and the key for unknown messages", () => {
const storage = createStorage("pt-PT");
const originalTranslation = TRANSLATIONS["pt-PT"].zoomIn;
delete TRANSLATIONS["pt-PT"].zoomIn;
try {
const i18n = createI18n({ storage, languages: [] });
assert.equal(i18n.t("zoomIn"), TRANSLATIONS.en.zoomIn);
assert.equal(i18n.t("notARealMessage"), "notARealMessage");
} finally {
TRANSLATIONS["pt-PT"].zoomIn = originalTranslation;
}
});
test("all shipped languages contain the default translation keys", () => {
const defaultKeys = Object.keys(TRANSLATIONS[DEFAULT_LANGUAGE]).sort();
Object.entries(TRANSLATIONS).forEach(([language, translations]) => {
assert.deepEqual(Object.keys(translations).sort(), defaultKeys, language);
});
});