From f3197ffc5362296e14907a2232fb6d9f4c5f4d76 Mon Sep 17 00:00:00 2001 From: iamdoubz <> Date: Thu, 16 Jul 2026 22:58:50 -0500 Subject: [PATCH] chore(i18n): key-parity + placeholder check script, catches untranslated keys --- scripts/check-i18n.mjs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 scripts/check-i18n.mjs diff --git a/scripts/check-i18n.mjs b/scripts/check-i18n.mjs new file mode 100644 index 0000000..edca7c9 --- /dev/null +++ b/scripts/check-i18n.mjs @@ -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");