import assert from "node:assert/strict";
import { test } from "node:test";
import type { Catalog } from "./catalog.ts";
import { createTranslator } from "./translate.ts";
const core: Catalog = {
"greeting": "Hello, {{name}}!",
"shell.signOut": "Sign out",
"shifts.count": { one: "{{count}} shift", other: "{{count}} shifts" },
};
const coreSv: Catalog = {
"greeting": "Hej, {{name}}!",
"shell.signOut": "Logga ut",
"shifts.count": { one: "{{count}} pass", other: "{{count}} pass" },
};
test("a key resolves from the first catalog that has it", () => {
const t = createTranslator({ catalogs: [coreSv, core], locale: "sv-SE" });
assert.equal(t("shell.signOut"), "Logga ut");
});
test("a key missing from the active locale falls back down the chain", () => {
const t = createTranslator({ catalogs: [{ "shell.signOut": "Logga ut" }, core], locale: "sv-SE" });
assert.equal(t("greeting", { name: "Li" }), "Hello, Li!");
});
test("a plugin catalog wins over the core one", () => {
const plugin: Catalog = { "shell.signOut": "Leave" };
const t = createTranslator({ catalogs: [plugin, core], locale: "en-US" });
assert.equal(t("shell.signOut"), "Leave");
});
test("a key missing everywhere renders as itself", () => {
const t = createTranslator({ catalogs: [core], locale: "en-US" });
assert.equal(t("nope.at.all"), "nope.at.all");
assert.equal(t("Shifts"), "Shifts"); // the nav-label contract: a plain label is its own fallback
});
test("{{vars}} interpolate; an unsupplied one stays visible", () => {
const t = createTranslator({ catalogs: [{ both: "{{a}} and {{b}}", n: "n={{n}}" }], locale: "en-US" });
assert.equal(t("both", { a: "x", b: "y" }), "x and y");
assert.equal(t("both", { a: "x" }), "x and {{b}}");
assert.equal(t("n", { n: 3 }), "n=3");
});
test("t returns raw text — escaping is the view's job", () => {
const t = createTranslator({ catalogs: [{ hi: "Hi {{name}}" }], locale: "en-US" });
assert.equal(t("hi", { name: "ok" }), "Hi ok");
});
test("plural messages select on count via Intl.PluralRules", () => {
const t = createTranslator({ catalogs: [core], locale: "en-US" });
assert.equal(t("shifts.count", { count: 1 }), "1 shift");
assert.equal(t("shifts.count", { count: 0 }), "0 shifts");
assert.equal(t("shifts.count", { count: 7 }), "7 shifts");
});
test("plural selection follows the active locale's own categories", () => {
const cs: Catalog = { files: { few: "{{count}} soubory", many: "{{count}} souboru", one: "{{count}} soubor", other: "{{count}} souborů" } };
const t = createTranslator({ catalogs: [cs], locale: "cs-CZ" });
assert.equal(t("files", { count: 1 }), "1 soubor");
assert.equal(t("files", { count: 3 }), "3 soubory");
assert.equal(t("files", { count: 10 }), "10 souborů");
});
test("a plural message without a count, or without the selected category, falls back to other", () => {
const t = createTranslator({ catalogs: [core], locale: "en-US" });
assert.equal(t("shifts.count"), "{{count}} shifts");
const partial = createTranslator({ catalogs: [{ x: { other: "many" } }], locale: "en-US" });
assert.equal(partial("x", { count: 1 }), "many");
});