110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* `check-locale-completeness.ts` — assert every key in
|
|
* `<package>/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 <package-root>
|
|
*
|
|
* 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<string>;
|
|
readonly extra: ReadonlyArray<string>;
|
|
}
|
|
|
|
export async function checkLocaleCompleteness(
|
|
packageRoot: string
|
|
): Promise<BundleDiff[]> {
|
|
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<BundleDiff>): 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<void> {
|
|
const argv = process.argv.slice(2);
|
|
if (argv.length === 0) {
|
|
process.stderr.write(
|
|
"Usage: check-locale-completeness.ts <package-root>\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);
|
|
});
|
|
}
|