chore(i18n): key-parity + placeholder check script, catches untranslated keys

This commit is contained in:
iamdoubz committed 2026-07-16 22:58:50 -05:00
1 parent ddc3dd4262
commit f3197ffc53
1 file changed
+32
+32
View File
@@ -0,0 +1,32 @@
// i18n key-parity check: every locale file must have exactly en.json's keys,
// and every translation must use the same {placeholders} as its English
// source. Runs as part of `npm run lint` — English fallback otherwise hides
// missing translations silently (they shipped untranslated for months once).
import { readFileSync, readdirSync } from "node:fs";
const DIR = new URL("../src/lib/i18n/", import.meta.url);
const en = JSON.parse(readFileSync(new URL("en.json", DIR), "utf8"));
const placeholders = (s) =>
[...s.matchAll(/\{(\w+)\}/g)]
.map((m) => m[1])
.sort()
.join(",");
let failed = false;
for (const file of readdirSync(DIR).filter((f) => f.endsWith(".json") && f !== "en.json")) {
const dict = JSON.parse(readFileSync(new URL(file, DIR), "utf8"));
const missing = Object.keys(en).filter((k) => !(k in dict));
const extra = Object.keys(dict).filter((k) => !(k in en));
const badVars = Object.keys(dict).filter(
(k) => k in en && placeholders(dict[k]) !== placeholders(en[k]),
);
for (const k of missing) console.error(`${file}: missing "${k}"`);
for (const k of extra) console.error(`${file}: extra "${k}" (not in en.json)`);
for (const k of badVars) console.error(`${file}: "${k}" placeholders differ from en.json`);
if (missing.length || extra.length || badVars.length) failed = true;
}
if (failed) {
console.error("\ni18n parity check FAILED — keep every locale in lockstep with en.json.");
process.exit(1);
}
console.log("i18n parity OK");