Files
trainingstagebuch/scripts/check-i18n-completeness.js
Torsten Schulz (local) eb54b4f7cf
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 45s
feat(i18n): add scripts for locale translation and patching
- Implemented `fill-de-extended-gaps.js` to fill missing billing/orders keys in de-extended from de.
- Created `fill-i18n-deep.py` for deep translation of locale JSONs using deep-translator with fallback options.
- Added `fill-i18n-locales.js` to translate locale JSONs and write overrides for untranslated keys.
- Introduced `fix-en-leaks.py` to translate keys that still match the en-US merge, addressing English leaks.
- Developed `patch-de-ch-swiss.js` to replace 'ß' with 'ss' in de-CH.json without deleting existing entries.
- Created `patch-en-gb-au.js` to apply UK/AU spelling corrections in en-GB and en-AU locales.
- Added shell scripts `run-fix-en-leaks.sh` and `run-i18n-deep-fill.sh` for sequential execution of translation tasks.
- Implemented `update-i18n-todo-stats.js` to update statistics in the I18N_TODO.md file based on translation completeness.
2026-05-15 15:52:54 +02:00

193 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Vergleicht alle frontend/src/i18n/locales/*.json mit de.json (wie deepMerge in frontend/src/i18n/index.js).
*
* Metriken pro Locale (Blatt-Strings, flatten):
* - inheritedFromDe: Key fehlt in der Locale-Datei → Merge ergibt deutschen Text
* - explicitStillDe: Key ist in der Locale gesetzt, Wert ist identisch zu de (Copy-Paste / vergessen)
* - differsFromDe: abweichend von Deutsch (Zielsprache oder Englisch-Vorlage)
* - enUsLeak: unter differsFromDe: Wert identisch zum en-US-Merge (typisch EN-Vorlage statt Zielsprache)
*
* Usage:
* node scripts/check-i18n-completeness.js
* node scripts/check-i18n-completeness.js --top 25 --locale fil
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const SOURCE_DIR = path.join(ROOT, 'frontend', 'src', 'i18n', 'locales');
function deepMergeMessages(base, override) {
if (!base || typeof base !== 'object' || Array.isArray(base)) {
return override ?? base;
}
const result = { ...base };
for (const [key, value] of Object.entries(override || {})) {
if (
value &&
typeof value === 'object' &&
!Array.isArray(value) &&
result[key] &&
typeof result[key] === 'object' &&
!Array.isArray(result[key])
) {
result[key] = deepMergeMessages(result[key], value);
} else {
result[key] = value;
}
}
return result;
}
function flatten(obj, prefix = '', out = {}) {
for (const [key, value] of Object.entries(obj || {})) {
const nextKey = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
flatten(value, nextKey, out);
} else if (typeof value === 'string') {
out[nextKey] = value;
}
}
return out;
}
/** Blatt-Keys, die in `override` explizit als String gesetzt sind */
function overriddenLeafKeys(override, prefix = '', out = new Set()) {
if (!override || typeof override !== 'object' || Array.isArray(override)) return out;
for (const [key, value] of Object.entries(override)) {
const p = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
overriddenLeafKeys(value, p, out);
} else if (typeof value === 'string') {
out.add(p);
}
}
return out;
}
function topPrefixes(keys, n = 12) {
const counts = {};
for (const k of keys) {
const root = k.split('.')[0];
counts[root] = (counts[root] || 0) + 1;
}
return Object.entries(counts)
.sort((a, b) => b[1] - a[1])
.slice(0, n);
}
function parseArgs(argv) {
let top = 15;
let locale = null;
for (let i = 2; i < argv.length; i++) {
if (argv[i] === '--top' && argv[i + 1]) {
top = parseInt(argv[++i], 10) || 15;
} else if (argv[i] === '--locale' && argv[i + 1]) {
locale = argv[++i];
}
}
return { top, locale };
}
function analyzeLocale(de, deFlat, deKeys, enUSMergedFlat, code) {
const loc = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, `${code}.json`), 'utf8'));
const merged = flatten(deepMergeMessages(JSON.parse(JSON.stringify(de)), loc));
const explicit = overriddenLeafKeys(loc);
const inheritedKeys = [];
const explicitDeKeys = [];
const enLeakKeys = [];
for (const k of deKeys) {
if (merged[k] === deFlat[k]) {
if (!explicit.has(k)) inheritedKeys.push(k);
else explicitDeKeys.push(k);
} else if (merged[k] === enUSMergedFlat[k]) {
enLeakKeys.push(k);
}
}
return {
code,
totalDe: deKeys.length,
explicitLeaves: explicit.size,
inheritedFromDe: inheritedKeys.length,
explicitStillDe: explicitDeKeys.length,
differsFromDe: deKeys.length - inheritedKeys.length - explicitDeKeys.length,
enUsLeak: enLeakKeys.length,
inheritedKeys,
explicitDeKeys,
enLeakKeys,
};
}
function main() {
const { top, locale: onlyLocale } = parseArgs(process.argv);
const de = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, 'de.json'), 'utf8'));
const deFlat = flatten(de);
const deKeys = Object.keys(deFlat).sort();
const enUS = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, 'en-US.json'), 'utf8'));
const enUSMergedFlat = flatten(deepMergeMessages(JSON.parse(JSON.stringify(de)), enUS));
const files = fs
.readdirSync(SOURCE_DIR)
.filter((f) => f.endsWith('.json') && !f.endsWith('.backup'))
.sort();
const rows = [];
for (const file of files) {
if (file === 'de.json') continue;
const code = path.basename(file, '.json');
if (onlyLocale && code !== onlyLocale) continue;
rows.push(analyzeLocale(de, deFlat, deKeys, enUSMergedFlat, code));
}
console.log(`Basis: de.json → ${deKeys.length} Blatt-Strings (flatten)\n`);
console.log(
[
'Locale'.padEnd(14),
'explicit'.padStart(8),
'erbtsDE'.padStart(8),
'=de expl'.padStart(10),
'≠de'.padStart(6),
'≈en-US'.padStart(8),
].join(' '),
);
console.log('-'.repeat(58));
for (const r of rows.sort((a, b) => b.inheritedFromDe - a.inheritedFromDe)) {
console.log(
[
r.code.padEnd(14),
String(r.explicitLeaves).padStart(8),
String(r.inheritedFromDe).padStart(8),
String(r.explicitStillDe).padStart(10),
String(r.differsFromDe).padStart(6),
String(r.enUsLeak).padStart(8),
].join(' '),
);
}
if (onlyLocale && rows.length === 1) {
const r = rows[0];
console.log(`\n--- Top-Namespace (erbte deutsche Strings) für ${r.code} ---`);
for (const [p, c] of topPrefixes(r.inheritedKeys, top)) {
console.log(` ${p}: ${c}`);
}
if (r.enLeakKeys.length) {
console.log(`\n--- Beispiel en-US-Leaks (erste ${Math.min(12, r.enLeakKeys.length)}) ---`);
for (const k of r.enLeakKeys.slice(0, 12)) {
console.log(` ${k}`);
}
}
}
console.log('\nLegende: erbtsDE = kein Blatt-Override in Locale-JSON (= deutsch); ≈en-US = Wert wie en-US-Merge');
}
main();