Files
descrybe/apps/web/src/lib/i18n/i18n.test.ts
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

309 lines
10 KiB
TypeScript

/**
* UI i18n unit tests (node:test).
* Pure modules only — no $app / i18n.svelte.ts ($state) imports.
*
* Run from apps/web:
* node --experimental-strip-types --test src/lib/i18n/*.test.ts
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
DEFAULT_UI_LOCALE,
UI_LOCALE_KEY,
UI_LOCALES,
buildUILocaleCookie,
isUILocale,
normalizeUILocale,
persistUILocale,
preferredAcceptLanguage,
preferredAcceptLanguageFor,
readPreferredUILocale,
resolvePreferredUILocale
} from "./locales.ts";
import {
MESSAGE_CATALOG,
isRegisteredCatalogLocale,
loadAllMessages,
loadMessages,
messagesFor
} from "./messages/catalog.ts";
import { en } from "./messages/en.ts";
import {
baseMessageKeys,
coverageSummary,
missingKeysForLocale
} from "./coverage.ts";
import { resolveMessage } from "./resolve.ts";
/** Mirrors setLocale's in-memory switch (normalize + active locale). */
function setLocale(next: string): string {
return normalizeUILocale(next);
}
describe("setLocale / locale switching", () => {
it("normalizes supported codes and rejects unknown to English", () => {
assert.equal(setLocale("es"), "es");
assert.equal(setLocale("FR"), "fr");
assert.equal(setLocale(" de "), "de");
assert.equal(setLocale("xx"), DEFAULT_UI_LOCALE);
assert.equal(setLocale(""), DEFAULT_UI_LOCALE);
assert.equal(setLocale(null as unknown as string), DEFAULT_UI_LOCALE);
});
it("isUILocale matches every UI_LOCALES entry", () => {
for (const { code } of UI_LOCALES) {
assert.equal(isUILocale(code), true, code);
}
assert.equal(isUILocale("xx"), false);
});
it("switching locale changes resolved pack strings", async () => {
await loadMessages("es");
const key = "common.cancel";
const enText = resolveMessage("en", key);
const esText = resolveMessage(setLocale("es"), key);
assert.equal(enText, en[key]);
assert.notEqual(esText, enText);
assert.equal(esText, messagesFor("es")[key]);
});
});
describe("English fallback for missing keys", () => {
it("falls back to English when the active pack omits a key", () => {
const key = "common.cancel";
const english = en[key];
assert.ok(english);
const missingLocale = "zz";
assert.deepEqual(messagesFor(missingLocale), {});
assert.equal(resolveMessage(missingLocale, key), english);
});
it("falls back to the key itself when English also lacks it", () => {
assert.equal(resolveMessage("en", "totally.missing.key"), "totally.missing.key");
assert.equal(resolveMessage("es", "totally.missing.key"), "totally.missing.key");
});
it("HTML-escapes interpolation vars", () => {
const sampleKey = Object.keys(en).find((k) => en[k]?.includes("{name}"));
if (!sampleKey) {
assert.equal(resolveMessage("en", "totally.missing.key"), "totally.missing.key");
return;
}
const out = resolveMessage("en", sampleKey, { name: "<script>x</script>" });
assert.match(out, /&lt;script&gt;/);
assert.doesNotMatch(out, /<script>/);
});
});
describe("pack key parity across UI_LOCALES vs en", () => {
it("MESSAGE_CATALOG registers every UI_LOCALES code", () => {
for (const { code } of UI_LOCALES) {
assert.equal(
isRegisteredCatalogLocale(code),
true,
`missing catalog entry for ${code}`
);
}
assert.ok(MESSAGE_CATALOG.en);
});
it("every UI locale has the same keys as English (no blanks)", async () => {
await loadAllMessages();
const baseKeys = baseMessageKeys();
assert.ok(baseKeys.length >= 350, `expected a full en catalog, got ${baseKeys.length}`);
const summary = coverageSummary();
assert.equal(summary.length, UI_LOCALES.length);
for (const row of summary) {
assert.equal(row.total, baseKeys.length, row.code);
assert.equal(
row.missing,
0,
`${row.code} missing: ${row.missing_keys.slice(0, 5).join(", ")}`
);
assert.equal(row.translated, baseKeys.length, row.code);
assert.equal(row.registered, true, row.code);
assert.deepEqual(missingKeysForLocale(row.code, baseKeys), []);
}
});
it("feed schedule keys distinguish auto interval from manual Sync", () => {
const interval = en["feeds.field.interval"];
const hint = en["feeds.field.intervalHint"];
const manual = en["feeds.syncManualTitle"];
assert.ok(interval && /auto/i.test(interval), `interval label should say auto: ${interval}`);
assert.ok(hint && /sync/i.test(hint) && /interval|schedule/i.test(hint), `hint should contrast Sync vs schedule: ${hint}`);
assert.ok(manual && /one-off|manual|now/i.test(manual), `manual title should clarify one-off: ${manual}`);
assert.notEqual(interval, "Sync interval (minutes)");
});
it("English key count is positive and matches Object.keys(en)", () => {
const keys = baseMessageKeys();
assert.equal(keys.length, Object.keys(en).length);
assert.ok(keys.length >= 350, `expected a full catalog, got ${keys.length}`);
});
});
type LocaleWindow = Window & { __UI_LOCALE__?: string };
function installLocaleStorageMocks(protocol: "http:" | "https:" = "http:") {
const store = new Map<string, string>();
let cookieJar = "";
let lastCookieAssignment = "";
const localStorageMock = {
getItem(key: string) {
return store.has(key) ? store.get(key)! : null;
},
setItem(key: string, value: string) {
store.set(key, String(value));
},
removeItem(key: string) {
store.delete(key);
},
clear() {
store.clear();
}
};
const documentMock = {
get cookie() {
return cookieJar;
},
set cookie(value: string) {
lastCookieAssignment = String(value);
const pair = String(value).split(";")[0] ?? "";
const eq = pair.indexOf("=");
if (eq <= 0) return;
const name = pair.slice(0, eq);
const rest = cookieJar
.split(";")
.map((p) => p.trim())
.filter((p) => p && !p.startsWith(`${name}=`));
rest.push(pair);
cookieJar = rest.join("; ");
}
};
const win = { __UI_LOCALE__: undefined as string | undefined } as LocaleWindow;
const g = globalThis as typeof globalThis & {
window?: LocaleWindow;
localStorage?: typeof localStorageMock;
document?: typeof documentMock;
location?: { protocol: string };
};
const prev = {
window: g.window,
localStorage: g.localStorage,
document: g.document,
location: g.location
};
g.window = win as typeof g.window;
g.localStorage = localStorageMock as typeof g.localStorage;
g.document = documentMock as typeof g.document;
g.location = { protocol } as typeof g.location;
return {
store,
getCookie: () => cookieJar,
getLastCookieAssignment: () => lastCookieAssignment,
win,
restore() {
if (prev.window === undefined) (g as { window?: unknown }).window = undefined;
else g.window = prev.window;
if (prev.localStorage === undefined) (g as { localStorage?: unknown }).localStorage = undefined;
else g.localStorage = prev.localStorage;
if (prev.document === undefined) (g as { document?: unknown }).document = undefined;
else g.document = prev.document;
if (prev.location === undefined) (g as { location?: unknown }).location = undefined;
else g.location = prev.location;
}
};
}
describe("persistUILocale / cookie+localStorage round-trip", () => {
it("buildUILocaleCookie uses UI_LOCALE_KEY and optional Secure", () => {
assert.equal(UI_LOCALE_KEY, "descrybe-ui-locale");
assert.equal(
buildUILocaleCookie("es"),
`${UI_LOCALE_KEY}=es; Path=/; SameSite=Lax; Max-Age=31536000`
);
assert.equal(
buildUILocaleCookie("FR", true),
`${UI_LOCALE_KEY}=fr; Path=/; SameSite=Lax; Max-Age=31536000; Secure`
);
});
it("set locale → descrybe-ui-locale → read back → normalize", () => {
const mock = installLocaleStorageMocks("http:");
try {
const written = persistUILocale(" ES ");
assert.equal(written, "es");
assert.equal(mock.store.get(UI_LOCALE_KEY), "es");
assert.equal(mock.win.__UI_LOCALE__, "es");
assert.match(mock.getCookie(), new RegExp(`(?:^|;\\s*)${UI_LOCALE_KEY}=es(?:;|$)`));
assert.equal(readPreferredUILocale(), "es");
// Prefer localStorage when window marker is cleared (reload / FOUC path).
delete mock.win.__UI_LOCALE__;
assert.equal(readPreferredUILocale(), "es");
assert.equal(persistUILocale("FR"), "fr");
assert.equal(mock.store.get(UI_LOCALE_KEY), "fr");
assert.equal(readPreferredUILocale(), "fr");
assert.equal(persistUILocale("xx"), DEFAULT_UI_LOCALE);
assert.equal(mock.store.get(UI_LOCALE_KEY), DEFAULT_UI_LOCALE);
assert.equal(readPreferredUILocale(), DEFAULT_UI_LOCALE);
} finally {
mock.restore();
}
});
it("writes Secure cookie when location is https", () => {
const mock = installLocaleStorageMocks("https:");
try {
persistUILocale("de");
assert.equal(mock.store.get(UI_LOCALE_KEY), "de");
assert.equal(
mock.getLastCookieAssignment(),
`${UI_LOCALE_KEY}=de; Path=/; SameSite=Lax; Max-Age=31536000; Secure`
);
} finally {
mock.restore();
}
});
it("SSR / no window: normalize only, no throw", () => {
assert.equal(typeof window, "undefined");
assert.equal(persistUILocale("ja"), "ja");
assert.equal(readPreferredUILocale(), DEFAULT_UI_LOCALE);
});
});
describe("preferredAcceptLanguage / readPreferredUILocale → header tag", () => {
it("resolvePreferredUILocale prefers window override over stored", () => {
assert.equal(resolvePreferredUILocale("nl", "en"), "nl");
assert.equal(resolvePreferredUILocale("XX", "fr"), "fr");
assert.equal(resolvePreferredUILocale(null, "de"), "de");
assert.equal(resolvePreferredUILocale(undefined, undefined), DEFAULT_UI_LOCALE);
assert.equal(resolvePreferredUILocale("", " ES "), "es");
});
it("preferredAcceptLanguageFor maps UI locale to BCP 47 Accept-Language tag", () => {
for (const { code, htmlLang } of UI_LOCALES) {
assert.equal(preferredAcceptLanguageFor(code), htmlLang, code);
}
assert.equal(preferredAcceptLanguageFor("NL"), "nl");
assert.equal(preferredAcceptLanguageFor("xx"), "en");
});
it("preferredAcceptLanguage without window defaults to English tag", () => {
assert.equal(typeof window, "undefined");
assert.equal(readPreferredUILocale(), DEFAULT_UI_LOCALE);
assert.equal(preferredAcceptLanguage(), "en");
assert.equal(
preferredAcceptLanguageFor(resolvePreferredUILocale("nl", "en")),
"nl"
);
});
});