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 = {}; const SRC = fileURLToPath(new URL("../", import.meta.url)); function flatten(obj: Record, 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, 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, }); } }); });