Add i18n support: per-locale catalogs, URL-driven locale, translated core and examples
CI / full-gate (push) Successful in 2m37s

This commit is contained in:
2026-08-03 22:37:27 +02:00
parent c30cd95ebd
commit 245d1ad5b5
93 changed files with 2480 additions and 464 deletions
+57
View File
@@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { type Catalog, checkCatalog, isCatalog } from "./catalog.ts";
const baseline: Catalog = { greeting: "Hello", "shifts.count": { one: "{{count}} shift", other: "{{count}} shifts" } };
const parity = (locale: string, catalog: Catalog): string[] =>
checkCatalog({ baseline, baselineLocale: "en-US", catalog, locale });
test("a complete translation reports nothing", () => {
assert.deepEqual(parity("sv-SE", { greeting: "Hej", "shifts.count": { one: "{{count}} pass", other: "{{count}} pass" } }), []);
});
test("a missing or unknown key is reported", () => {
const missing = parity("sv-SE", { "shifts.count": { one: "a", other: "b" } });
assert.equal(missing.length, 1);
assert.match(missing[0] ?? "", /missing key "greeting"/);
const extra = parity("sv-SE", { ...baseline, stray: "x" });
assert.equal(extra.length, 1);
assert.match(extra[0] ?? "", /unknown key "stray".*en-US/);
});
test("a key must stay the same kind as in the baseline", () => {
const flat = parity("sv-SE", { greeting: "Hej", "shifts.count": "pass" });
assert.equal(flat.length, 1);
assert.match(flat[0] ?? "", /"shifts.count" must be a plural message/);
const plural = parity("sv-SE", { greeting: { one: "Hej", other: "Hej" }, "shifts.count": { one: "a", other: "b" } });
assert.equal(plural.length, 1);
assert.match(plural[0] ?? "", /"greeting" must be a string/);
});
test("a plural message must cover exactly its own locale's categories", () => {
const short = parity("cs-CZ", { greeting: "Ahoj", "shifts.count": { one: "a", other: "b" } });
assert.equal(short.length, 1);
assert.match(short[0] ?? "", /"shifts\.count".*cs-CZ.*few, many/);
const long = parity("sv-SE", { greeting: "Hej", "shifts.count": { few: "x", one: "a", other: "b" } });
assert.equal(long.length, 1);
assert.match(long[0] ?? "", /"shifts\.count".*few/);
});
test("the baseline is checked against itself, so an incomplete plural fails at home too", () => {
assert.deepEqual(checkCatalog({ baseline, baselineLocale: "en-US", catalog: baseline, locale: "en-US" }), []);
const bad: Catalog = { greeting: "Hello", "shifts.count": { one: "{{count}} shift" } };
assert.match(checkCatalog({ baseline: bad, baselineLocale: "en-US", catalog: bad, locale: "en-US" })[0] ?? "", /other/);
});
test("isCatalog accepts strings and plural objects, rejects anything else", () => {
assert.equal(isCatalog({ a: "x", b: { other: "y" } }), true);
assert.equal(isCatalog({ a: 1 }), false);
assert.equal(isCatalog({ a: { other: 1 } }), false);
assert.equal(isCatalog({ a: {} }), false); // an empty plural message says nothing
assert.equal(isCatalog({ a: { bogus: "x" } }), false); // not a plural category
assert.equal(isCatalog(null), false);
assert.equal(isCatalog([]), false);
});