Files
iptvnator/tools/i18n/check-drift.mjs
T
4gray 2d5c4fa4f9 chore: tighten validation and runtime logging
* chore: tighten validation and runtime logging

* fix(i18n): localize new settings labels
2026-05-15 17:43:42 +02:00

123 lines
3.6 KiB
JavaScript

#!/usr/bin/env node
import { readdirSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, '..', '..');
const I18N_DIR = resolve(REPO_ROOT, 'apps/web/src/assets/i18n');
const EN_LOCALE = 'en.json';
const MAX_EXAMPLES = 5;
const FAIL_ON_IDENTICAL = process.argv.includes('--fail-on-identical');
function readJson(filePath) {
return JSON.parse(readFileSync(filePath, 'utf8'));
}
function collectLeaves(value, prefix = '', leaves = new Map()) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`Expected object at ${prefix || '<root>'}`);
}
for (const [key, entry] of Object.entries(value)) {
const path = prefix ? `${prefix}.${key}` : key;
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
collectLeaves(entry, path, leaves);
} else {
leaves.set(path, entry);
}
}
return leaves;
}
function listLocaleFiles() {
return readdirSync(I18N_DIR)
.filter((file) => file.endsWith('.json'))
.sort();
}
function formatExamples(values) {
if (values.length === 0) {
return '';
}
const suffix = values.length > MAX_EXAMPLES ? ', ...' : '';
return ` (${values.slice(0, MAX_EXAMPLES).join(', ')}${suffix})`;
}
function formatError(error) {
return error instanceof Error ? error.message : String(error);
}
let enLeaves;
try {
enLeaves = collectLeaves(readJson(resolve(I18N_DIR, EN_LOCALE)));
} catch (error) {
console.error(`FAIL ${EN_LOCALE}: ${formatError(error)}`);
process.exit(1);
}
const enKeys = [...enLeaves.keys()];
let failed = false;
let identicalTotal = 0;
for (const localeFile of listLocaleFiles()) {
if (localeFile === EN_LOCALE) {
continue;
}
try {
const localeLeaves = collectLeaves(
readJson(resolve(I18N_DIR, localeFile))
);
const localeKeys = [...localeLeaves.keys()];
const missing = enKeys.filter((key) => !localeLeaves.has(key));
const extra = localeKeys.filter((key) => !enLeaves.has(key));
const identical = enKeys.filter((key) => {
if (!localeLeaves.has(key)) {
return false;
}
const localeValue = localeLeaves.get(key);
const enValue = enLeaves.get(key);
return (
typeof localeValue === 'string' &&
typeof enValue === 'string' &&
localeValue.trim().length > 0 &&
localeValue === enValue
);
});
identicalTotal += identical.length;
const localeFailed =
missing.length > 0 ||
extra.length > 0 ||
(FAIL_ON_IDENTICAL && identical.length > 0);
failed = failed || localeFailed;
const status = localeFailed ? 'FAIL' : 'ok';
console.log(
`${status} ${localeFile}: missing=${missing.length}${formatExamples(
missing
)} extra=${extra.length}${formatExamples(extra)} identical_en=${
identical.length
}${formatExamples(identical)}`
);
} catch (error) {
failed = true;
console.log(`FAIL ${localeFile}: ${formatError(error)}`);
}
}
if (identicalTotal > 0 && !FAIL_ON_IDENTICAL) {
console.log(
`warn identical_en=${identicalTotal}; pass --fail-on-identical to make English fallback values fatal.`
);
}
if (failed) {
process.exitCode = 1;
}