All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 54s
- Changed file permissions from 644 to 755 for various files in the mobile-app and mobile-app_legacy_rn_expo directories, ensuring executable permissions where necessary. - Added a new SQL migration script to the backend to introduce missing columns for club billing and invoice settings in the clubs table, maintaining synchronization with the current Sequelize model.
193 lines
5.8 KiB
JavaScript
Executable File
193 lines
5.8 KiB
JavaScript
Executable File
#!/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();
|