115 lines
3.0 KiB
JavaScript
115 lines
3.0 KiB
JavaScript
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();
|