#!/usr/bin/env bun /** * `check-locale-completeness.ts` — assert every key in * `/src/locales/en-GB.json` exists in every other locale * bundle shipped by the package. Pure file walk; no transitive deps on * the package source. * * Usage: * bun packages/react/react-ui/scripts/check-locale-completeness.ts * * Exit codes: * 0 — every key parity-complete * 1 — at least one key missing * 2 — internal failure */ import { readdir, readFile } from "node:fs/promises"; import { join, resolve } from "node:path"; interface BundleDiff { readonly locale: string; readonly missing: ReadonlyArray; readonly extra: ReadonlyArray; } export async function checkLocaleCompleteness( packageRoot: string ): Promise { const localesDir = join(resolve(packageRoot), "src", "locales"); const enPath = join(localesDir, "en-GB.json"); const enKeys = new Set( Object.keys(JSON.parse(await readFile(enPath, "utf8"))) ); const entries = await readdir(localesDir); const diffs: BundleDiff[] = []; for (const entry of entries) { if (entry === "en-GB.json") continue; if (!entry.endsWith(".json")) continue; const otherPath = join(localesDir, entry); const other = JSON.parse(await readFile(otherPath, "utf8")) as Record< string, string >; const otherKeys = new Set(Object.keys(other)); const missing: string[] = []; for (const k of enKeys) { if (!otherKeys.has(k)) missing.push(k); } const extra: string[] = []; for (const k of otherKeys) { if (!enKeys.has(k)) extra.push(k); } if (missing.length > 0 || extra.length > 0) { diffs.push({ locale: entry.replace(/\.json$/, ""), missing, extra, }); } } return diffs; } function format(diffs: ReadonlyArray): string { if (diffs.length === 0) { return "check-locale-completeness: every locale parity-complete."; } const lines: string[] = [ `check-locale-completeness: ${diffs.length} locale(s) failed parity.`, ]; for (const d of diffs) { lines.push(` ${d.locale}:`); if (d.missing.length > 0) { lines.push(" missing keys:"); for (const k of d.missing) lines.push(` - ${k}`); } if (d.extra.length > 0) { lines.push(" extra keys (not in en-GB):"); for (const k of d.extra) lines.push(` - ${k}`); } } return lines.join("\n"); } async function main(): Promise { const argv = process.argv.slice(2); if (argv.length === 0) { process.stderr.write( "Usage: check-locale-completeness.ts \n" ); process.exit(2); } const root = argv[0] as string; const diffs = await checkLocaleCompleteness(root); const message = format(diffs); if (diffs.length === 0) { process.stdout.write(`${message}\n`); process.exit(0); } process.stderr.write(`${message}\n`); process.exit(1); } if (import.meta.main) { await main().catch((err: unknown) => { process.stderr.write(`internal error: ${(err as Error).message}\n`); process.exit(2); }); }