test(i18n): every locale key must be read by the app; 75 dead keys removed

A contract test scans the source (tests excluded) for each key as a whole literal, or under a
dynamic prefix like `plex.sort.${k}`, with plural forms counted through their base key. It found 75
keys that had outlived the code showing them (the old sidebar sort vocabulary, the old login page, a
pager the DataTable replaced, …); they are gone from EN and HU. A key read in a way the scan cannot
see goes in an explicit ALLOWED map with its reason.
This commit is contained in:
2026-09-19 17:15:18 +02:00
parent 4f72b193aa
commit cedcbb1434
33 changed files with 76 additions and 170 deletions
@@ -0,0 +1,70 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
// Every locale key is used by the app. The parity test proves EN and HU agree; nothing proved either
// one is still READ — 75 keys had outlived the code that showed them (a whole sidebar sort vocabulary,
// the old login page, a pager the DataTable replaced), each a string a translator would keep in step
// for nobody, and each one making "is this key live?" a grep instead of a fact.
//
// A key counts as used when the source (tests excluded — a test naming a key does not show it to
// anyone) holds it as a whole string literal, or holds a string that ends in `.` right before an
// interpolation or a concatenation — a DYNAMIC prefix like `plex.sort.${k}` — which then covers every
// key under it. Plural forms count through their base key. A key read in a way this cannot see goes
// in ALLOWED with the reason, rather than the check being loosened for everyone.
const ALLOWED: Record<string, string> = {};
const SRC = fileURLToPath(new URL("../", import.meta.url));
function flatten(obj: Record<string, unknown>, prefix: string, out: string[]): string[] {
for (const [k, v] of Object.entries(obj)) {
const key = `${prefix}.${k}`;
if (v && typeof v === "object" && !Array.isArray(v))
flatten(v as Record<string, unknown>, key, out);
else out.push(key);
}
return out;
}
function sources(dir: string, out: string[] = []): string[] {
for (const f of readdirSync(dir)) {
const p = join(dir, f);
if (statSync(p).isDirectory()) sources(p, out);
else if (/\.(ts|tsx)$/.test(f) && !/\.test\./.test(f)) out.push(readFileSync(p, "utf-8"));
}
return out;
}
const locales = join(SRC, "i18n/locales/en");
const keys = readdirSync(locales).flatMap((f) =>
flatten(JSON.parse(readFileSync(join(locales, f), "utf-8")), f.replace(/\.json$/, ""), []),
);
const code = sources(SRC).join("\n");
const literals = new Set([...code.matchAll(/["'`]([\w.-]+)["'`]/g)].map((m) => m[1]!));
const prefixes = [...code.matchAll(/["'`]([\w.]+\.)(?:\$\{|["'`]\s*\+)/g)].map((m) => m[1]!);
const base = (k: string) => k.replace(/_(zero|one|two|few|many|other)$/, "");
const used = (k: string) =>
literals.has(k) || literals.has(base(k)) || prefixes.some((p) => k.startsWith(p));
describe("i18n keys", () => {
it("finds keys and code to check them against", () => {
expect(keys.length).toBeGreaterThan(1000);
expect(literals.has("home.hero.reroll")).toBe(true);
expect(prefixes).toContain("plex.sort.");
});
it("are all read somewhere in the app", () => {
expect(keys.filter((k) => !used(k) && !(k in ALLOWED))).toEqual([]);
});
it("allow only keys that exist and the scan really cannot see", () => {
for (const k of Object.keys(ALLOWED)) {
expect({ k, exists: keys.includes(k), scanSees: used(k) }).toEqual({
k,
exists: true,
scanSees: false,
});
}
});
});