88 lines
2.4 KiB
JavaScript
88 lines
2.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Validates that all locale translation files have identical key structures.
|
|
* Compares all locale folders (except en) against the en baseline for every namespace.
|
|
*
|
|
* Usage: node scripts/i18n-check.mjs
|
|
*/
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const LOCALES_DIR = path.resolve("src/i18n/locales");
|
|
const BASE_LOCALE = "en";
|
|
|
|
function getKeys(obj, prefix = "") {
|
|
const keys = [];
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
keys.push(...getKeys(value, fullKey));
|
|
} else {
|
|
keys.push(fullKey);
|
|
}
|
|
}
|
|
return keys.sort();
|
|
}
|
|
|
|
let hasErrors = false;
|
|
|
|
const baseDir = path.join(LOCALES_DIR, BASE_LOCALE);
|
|
const namespaces = fs
|
|
.readdirSync(baseDir)
|
|
.filter((f) => f.endsWith(".json"))
|
|
.map((f) => f.replace(".json", ""));
|
|
|
|
const compareLocales = fs
|
|
.readdirSync(LOCALES_DIR, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.filter((locale) => locale !== BASE_LOCALE)
|
|
.sort((a, b) => a.localeCompare(b));
|
|
|
|
for (const namespace of namespaces) {
|
|
const basePath = path.join(baseDir, `${namespace}.json`);
|
|
const baseData = JSON.parse(fs.readFileSync(basePath, "utf-8"));
|
|
const baseKeys = getKeys(baseData);
|
|
|
|
for (const locale of compareLocales) {
|
|
const localePath = path.join(LOCALES_DIR, locale, `${namespace}.json`);
|
|
|
|
if (!fs.existsSync(localePath)) {
|
|
console.error(`MISSING: ${locale}/${namespace}.json does not exist`);
|
|
hasErrors = true;
|
|
continue;
|
|
}
|
|
|
|
const localeData = JSON.parse(fs.readFileSync(localePath, "utf-8"));
|
|
const localeKeys = getKeys(localeData);
|
|
|
|
const missing = baseKeys.filter((k) => !localeKeys.includes(k));
|
|
const extra = localeKeys.filter((k) => !baseKeys.includes(k));
|
|
|
|
if (missing.length > 0) {
|
|
console.error(`MISSING in ${locale}/${namespace}.json:`);
|
|
for (const key of missing) {
|
|
console.error(` - ${key}`);
|
|
}
|
|
hasErrors = true;
|
|
}
|
|
|
|
if (extra.length > 0) {
|
|
console.error(`EXTRA in ${locale}/${namespace}.json:`);
|
|
for (const key of extra) {
|
|
console.error(` + ${key}`);
|
|
}
|
|
hasErrors = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasErrors) {
|
|
console.error("\ni18n check FAILED — translation files are out of sync.");
|
|
process.exit(1);
|
|
} else {
|
|
console.log(
|
|
`i18n check PASSED — all ${compareLocales.length} locales match ${BASE_LOCALE} across ${namespaces.length} namespaces.`,
|
|
);
|
|
}
|