All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 45s
- 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.
193 lines
5.8 KiB
JavaScript
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();
|