File size: 3,062 Bytes
a0ebf39 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | import fs from 'node:fs';
import path from 'node:path';
const LOCALES_DIR = path.join(process.cwd(), 'lib', 'i18n', 'locales');
const SOURCE_LOCALE = 'en-US.json';
function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function formatPath(keyPath) {
return keyPath || '<root>';
}
function collectLeafKeys(value, fileName, keyPath = '', keys = new Set()) {
if (Array.isArray(value)) {
throw new Error(
`${fileName} has an array at "${formatPath(keyPath)}". Locale values must not be arrays.`,
);
}
if (isPlainObject(value)) {
const entries = Object.entries(value);
if (entries.length === 0) {
throw new Error(
`${fileName} has an empty object at "${formatPath(keyPath)}". Locale objects must not be empty.`,
);
}
for (const [key, child] of entries) {
const nextPath = keyPath ? `${keyPath}.${key}` : key;
collectLeafKeys(child, fileName, nextPath, keys);
}
return keys;
}
if (!keyPath) {
throw new Error(`${fileName} must contain a JSON object at the root.`);
}
keys.add(keyPath);
return keys;
}
function readLocaleKeys(filePath) {
const raw = fs.readFileSync(filePath, 'utf8');
const parsed = JSON.parse(raw);
const fileName = path.basename(filePath);
if (!isPlainObject(parsed)) {
throw new Error(`${fileName} must contain a JSON object at the root.`);
}
return [...collectLeafKeys(parsed, fileName)].sort();
}
function main() {
const localeFiles = fs
.readdirSync(LOCALES_DIR)
.filter((name) => name.endsWith('.json'))
.sort();
if (!localeFiles.includes(SOURCE_LOCALE)) {
throw new Error(`Missing source locale: ${SOURCE_LOCALE}`);
}
const sourceKeys = new Set(readLocaleKeys(path.join(LOCALES_DIR, SOURCE_LOCALE)));
const reports = [];
for (const localeFile of localeFiles) {
if (localeFile === SOURCE_LOCALE) continue;
const localeKeys = new Set(readLocaleKeys(path.join(LOCALES_DIR, localeFile)));
const missing = [...sourceKeys].filter((key) => !localeKeys.has(key)).sort();
const extra = [...localeKeys].filter((key) => !sourceKeys.has(key)).sort();
if (missing.length > 0 || extra.length > 0) {
reports.push({ file: localeFile, missing, extra });
}
}
if (reports.length === 0) {
console.log(
`i18n key alignment check passed (${localeFiles.length} locale files, source: ${SOURCE_LOCALE}).`,
);
return;
}
console.error(`i18n key alignment check failed against ${SOURCE_LOCALE}:`);
for (const report of reports) {
console.error(`\n- ${report.file}`);
if (report.missing.length > 0) {
console.error(` Missing keys (${report.missing.length}):`);
for (const key of report.missing) {
console.error(` - ${key}`);
}
}
if (report.extra.length > 0) {
console.error(` Extra keys (${report.extra.length}):`);
for (const key of report.extra) {
console.error(` - ${key}`);
}
}
}
process.exit(1);
}
main();
|