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 matches the shape of a DYNAMIC key — a template // or concatenation that opens with a key head, each interpolation any segment: `plex.sort.${k}` // covers `plex.sort.*`, `editor.${m}.hint` only `editor.*.hint`. A shape that is nothing but the // namespace and one interpolation (`card.${status}`) would cover the whole file — it hid five dead // keys — so it covers nothing; a key it reads that nothing else names is listed below. 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 LOG_FAILURE = "AdminLogs reads `adminLogs.${failure}`, a word its query hook returns"; const ALLOWED: Record = { "adminLogs.olderFailed": LOG_FAILURE, "adminLogs.stale": LOG_FAILURE, "adminLogs.countsFailed": LOG_FAILURE, }; // Namespaces that are one backend enum each, and held key-for-value to it by backendIds.contract. const BACKEND_ENUMS = ["auditActions.", "quotaActions."]; 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]!)); // Each dynamic key as a SHAPE, every interpolation a `*`: `plex.sort.*`, `editor.*.hint`, // `config.choices.*.*` — from a template literal that opens with a key head, or a head concatenated. const shapes = [ ...[...code.matchAll(/`([\w.]+\.(?:\$\{[^}`]*\}|[\w.-])*)`/g)].map((m) => m[1]!), ...[...code.matchAll(/["'`]([\w.]+\.)["'`]\s*\+/g)].map((m) => `${m[1]!}\${}`), ] .filter((s) => s.includes("${")) .map((s) => s.replace(/\$\{[^}]*\}/g, "*")); // A shape that is one namespace and one `*` covers nothing (see above). const namespaceWide = (shape: string) => /^[\w-]+\.\*$/.test(shape); const patterns = shapes .filter((s) => !namespaceWide(s)) .map( (s) => new RegExp( `^${s .split("*") .map((part) => part.replace(/[.$]/g, "\\$&")) .join("[\\w.-]+")}$`, ), ); const base = (k: string) => k.replace(/_(zero|one|two|few|many|other)$/, ""); const used = (k: string) => literals.has(k) || literals.has(base(k)) || patterns.some((p) => p.test(k) || p.test(base(k))) || BACKEND_ENUMS.some((ns) => k.startsWith(ns)); 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(used("plex.sort.newest")).toBe(true); // `plex.sort.${k}` expect(shapes).toContain("editor.*.hint"); // `editor.${m}.hint` expect(used("editor.accurate.hint")).toBe(true); expect(used("editor.accurate.nope")).toBe(false); // A bare namespace head covers nothing — the hole that hid five dead keys. expect(shapes).toContain("card.*"); expect(used("card.anythingAtAll")).toBe(false); }); 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, }); } }); });