feat(i18n): add scripts for locale translation and patching
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 45s
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.
This commit is contained in:
12542
scripts/.i18n-translate-cache.json
Normal file
12542
scripts/.i18n-translate-cache.json
Normal file
File diff suppressed because it is too large
Load Diff
124
scripts/apply-cognate-overrides.js
Normal file
124
scripts/apply-cognate-overrides.js
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Setzt bewusste Cognates/Produktnamen explizit in allen Locales (Wert = en-US oder de).
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const LOCALES_DIR = path.join(ROOT, 'frontend', 'src', 'i18n', 'locales');
|
||||
|
||||
const LOCALES = [
|
||||
'fr', 'es', 'it', 'pl', 'ja', 'zh', 'th', 'tl', 'fil',
|
||||
'en-US', 'en-GB', 'en-AU',
|
||||
];
|
||||
|
||||
// Keys, die in Nicht-DE-Locales explizit gesetzt werden (Wert aus en-US-Merge)
|
||||
const COGNATE_KEYS = [
|
||||
'common.filter', 'common.details', 'common.name', 'common.status',
|
||||
'common.optional', 'common.in', 'common.ok',
|
||||
'navigation.clickTtBrowser',
|
||||
'members.rowTooltipSeparator', 'members.ttrQttr', 'members.status',
|
||||
'members.clickTtRequestShort',
|
||||
'diary.standardDurationShort', 'diary.min',
|
||||
'trainingStats.name', 'trainingStats.ttr', 'trainingStats.qttr',
|
||||
'tournaments.knockoutLabel', 'tournaments.genderMixed', 'tournaments.optional',
|
||||
'tournaments.index', 'tournaments.diff',
|
||||
'billing.iban',
|
||||
'schedule.code', 'schedule.team', 'schedule.teams', 'schedule.vs',
|
||||
'teamManagement.team', 'teamManagement.teams',
|
||||
'courtDrawingTool.strokeTypeTopspin', 'courtDrawingTool.strokeTypeFlip',
|
||||
'courtDrawingTool.strokeTypeBlock',
|
||||
'matchReportApi.vs', 'courtDrawing.ok',
|
||||
'pdfGenerator.diff', 'pdfGenerator.lineupQttr',
|
||||
'pendingApprovals.email', 'permissions.email',
|
||||
'logs.details', 'logs.logDetailsLabels.ip',
|
||||
'dialogs.info.ok', 'dialogs.confirm.ok',
|
||||
'memberTransferDialog.server', 'memberTransferDialog.endpoint', 'memberTransferDialog.format',
|
||||
'myTischtennis.title', 'myTischtennisAccount.title', 'myTischtennisAccount.teams',
|
||||
'memberNotes.tags', 'nuscoreAnalyzer.title',
|
||||
'predefinedActivities.min',
|
||||
];
|
||||
|
||||
const EXTRA = {
|
||||
fr: { 'members.address': 'Adresse' },
|
||||
es: { 'members.address': 'Dirección' },
|
||||
it: { 'members.address': 'Indirizzo' },
|
||||
pl: { 'members.address': 'Adres' },
|
||||
ja: { 'members.address': '住所' },
|
||||
zh: { 'members.address': '地址' },
|
||||
th: { 'members.address': 'ที่อยู่' },
|
||||
tl: { 'members.address': 'Address', 'languages.th': 'Thai (ไทย)' },
|
||||
fil: { 'members.address': 'Address', 'languages.th': 'Thai (ไทย)' },
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function deepMerge(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] = deepMerge(result[key], value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setByPath(obj, dotPath, value) {
|
||||
const parts = dotPath.split('.');
|
||||
let cur = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};
|
||||
cur = cur[parts[i]];
|
||||
}
|
||||
cur[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
function buildOverrides(deFlat, targetFlat) {
|
||||
const out = {};
|
||||
for (const [key, value] of Object.entries(targetFlat)) {
|
||||
if (value !== deFlat[key]) setByPath(out, key, value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const de = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, 'de.json'), 'utf8'));
|
||||
const enUs = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, 'en-US.json'), 'utf8'));
|
||||
const deFlat = flatten(de);
|
||||
const enMerged = flatten(deepMerge(JSON.parse(JSON.stringify(de)), enUs));
|
||||
|
||||
for (const locale of LOCALES) {
|
||||
const localePath = path.join(LOCALES_DIR, `${locale}.json`);
|
||||
if (!fs.existsSync(localePath)) continue;
|
||||
const localeJson = JSON.parse(fs.readFileSync(localePath, 'utf8'));
|
||||
const merged = flatten(deepMerge(JSON.parse(JSON.stringify(de)), localeJson));
|
||||
|
||||
for (const key of COGNATE_KEYS) {
|
||||
const val = enMerged[key] ?? deFlat[key];
|
||||
if (val !== undefined) merged[key] = val;
|
||||
}
|
||||
for (const [key, val] of Object.entries(EXTRA[locale] || {})) {
|
||||
merged[key] = val;
|
||||
}
|
||||
|
||||
const overrides = buildOverrides(deFlat, merged);
|
||||
fs.writeFileSync(localePath, `${JSON.stringify(overrides, null, 2)}\n`, 'utf8');
|
||||
const stillDe = Object.keys(deFlat).filter((k) => merged[k] === deFlat[k]).length;
|
||||
console.log(`${locale}: overrides=${Object.keys(flatten(overrides)).length}, stillDe=${stillDe}`);
|
||||
}
|
||||
120
scripts/apply-i18n-cache.py
Normal file
120
scripts/apply-i18n-cache.py
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Schreibt Locale-Overrides aus Cache + EN-Fallback – ohne API-Aufrufe."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LOCALES_DIR = ROOT / "frontend" / "src" / "i18n" / "locales"
|
||||
CACHE_FILE = Path(__file__).resolve().parent / ".i18n-translate-cache.json"
|
||||
|
||||
TARGETS = {
|
||||
"fr": "fr",
|
||||
"es": "es",
|
||||
"it": "it",
|
||||
"pl": "pl",
|
||||
"ja": "ja",
|
||||
"zh": "zh-CN",
|
||||
"th": "th",
|
||||
"tl": "tl",
|
||||
"fil": "tl",
|
||||
}
|
||||
|
||||
|
||||
def deep_merge(base, override):
|
||||
if not isinstance(base, dict) or isinstance(base, list):
|
||||
return override if override is not None else base
|
||||
result = dict(base)
|
||||
for key, value in (override or {}).items():
|
||||
if (
|
||||
isinstance(value, dict)
|
||||
and not isinstance(value, list)
|
||||
and isinstance(result.get(key), dict)
|
||||
and not isinstance(result.get(key), list)
|
||||
):
|
||||
result[key] = deep_merge(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def flatten(obj, prefix=""):
|
||||
out = {}
|
||||
for key, value in (obj or {}).items():
|
||||
next_key = f"{prefix}.{key}" if prefix else key
|
||||
if isinstance(value, dict) and not isinstance(value, list):
|
||||
out.update(flatten(value, next_key))
|
||||
elif isinstance(value, str):
|
||||
out[next_key] = value
|
||||
return out
|
||||
|
||||
|
||||
def set_by_path(obj, dot_path, value):
|
||||
parts = dot_path.split(".")
|
||||
cur = obj
|
||||
for part in parts[:-1]:
|
||||
if part not in cur or not isinstance(cur[part], dict):
|
||||
cur[part] = {}
|
||||
cur = cur[part]
|
||||
cur[parts[-1]] = value
|
||||
|
||||
|
||||
def build_overrides(de_flat, target_flat):
|
||||
out = {}
|
||||
for key, value in target_flat.items():
|
||||
if value != de_flat.get(key):
|
||||
set_by_path(out, key, value)
|
||||
return out
|
||||
|
||||
|
||||
def apply_locale(code: str, cache: dict, en_flat: dict, de_flat: dict) -> None:
|
||||
target = TARGETS[code]
|
||||
locale_path = LOCALES_DIR / f"{code}.json"
|
||||
locale_json = json.loads(locale_path.read_text(encoding="utf-8"))
|
||||
de = json.loads((LOCALES_DIR / "de.json").read_text(encoding="utf-8"))
|
||||
merged = flatten(deep_merge(json.loads(json.dumps(de)), locale_json))
|
||||
|
||||
from_cache = from_en = still = 0
|
||||
for key, de_val in de_flat.items():
|
||||
if merged.get(key) != de_val:
|
||||
continue
|
||||
from_lang = "en" if en_flat.get(key) and en_flat[key] != de_val else "de"
|
||||
text = en_flat[key] if from_lang == "en" else de_val
|
||||
cache_key = f"{from_lang}|{target}|{text}"
|
||||
if cache_key in cache:
|
||||
merged[key] = cache[cache_key]
|
||||
from_cache += 1
|
||||
elif from_lang == "en":
|
||||
merged[key] = text
|
||||
from_en += 1
|
||||
else:
|
||||
still += 1
|
||||
|
||||
overrides = build_overrides(de_flat, merged)
|
||||
locale_path.write_text(json.dumps(overrides, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(
|
||||
f"[{code}] cache={from_cache} enFallback={from_en} stillDe={still} overrides={len(flatten(overrides))}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
codes = sys.argv[1:] or list(TARGETS.keys())
|
||||
cache = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
||||
de = json.loads((LOCALES_DIR / "de.json").read_text(encoding="utf-8"))
|
||||
en_us = json.loads((LOCALES_DIR / "en-US.json").read_text(encoding="utf-8"))
|
||||
de_flat = flatten(de)
|
||||
en_flat = flatten(deep_merge(de, en_us))
|
||||
|
||||
for code in codes:
|
||||
if code not in TARGETS:
|
||||
print(f"skip unknown: {code}", file=sys.stderr)
|
||||
continue
|
||||
apply_locale(code, cache, en_flat, de_flat)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
192
scripts/check-i18n-completeness.js
Normal file
192
scripts/check-i18n-completeness.js
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/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();
|
||||
104
scripts/fill-de-ch-from-de.js
Normal file
104
scripts/fill-de-ch-from-de.js
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* de-CH: Alle ererbten de-Strings als Override mit CH-Orthografie (ß→ss, …).
|
||||
* Bestehende de-CH-Overrides bleiben erhalten.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const LOCALES_DIR = path.join(ROOT, 'frontend', 'src', 'i18n', 'locales');
|
||||
|
||||
const CH_REPLACEMENTS = [
|
||||
[/ß/g, 'ss'],
|
||||
[/Straße/g, 'Strasse'],
|
||||
[/straße/g, 'strasse'],
|
||||
[/Grüße/g, 'Grüsse'],
|
||||
[/grüße/g, 'grüsse'],
|
||||
[/Gruß/g, 'Gruss'],
|
||||
[/gruß/g, 'gruss'],
|
||||
];
|
||||
|
||||
function toSwiss(text) {
|
||||
let out = text;
|
||||
for (const [re, rep] of CH_REPLACEMENTS) {
|
||||
out = out.replace(re, rep);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function deepMerge(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] = deepMerge(result[key], value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setByPath(obj, dotPath, value) {
|
||||
const parts = dotPath.split('.');
|
||||
let cur = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};
|
||||
cur = cur[parts[i]];
|
||||
}
|
||||
cur[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
function buildOverrides(deFlat, targetFlat) {
|
||||
const out = {};
|
||||
for (const [key, value] of Object.entries(targetFlat)) {
|
||||
if (value !== deFlat[key]) setByPath(out, key, value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const de = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, 'de.json'), 'utf8'));
|
||||
const ch = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, 'de-CH.json'), 'utf8'));
|
||||
const deFlat = flatten(de);
|
||||
const merged = flatten(deepMerge(JSON.parse(JSON.stringify(de)), ch));
|
||||
|
||||
let patched = 0;
|
||||
for (const key of Object.keys(deFlat)) {
|
||||
if (merged[key] === deFlat[key]) {
|
||||
const swiss = toSwiss(deFlat[key]);
|
||||
if (swiss !== deFlat[key] || !Object.prototype.hasOwnProperty.call(flatten(ch), key)) {
|
||||
merged[key] = swiss;
|
||||
if (swiss !== deFlat[key]) patched++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alle Keys: CH-Text als Override wenn abweichend von de
|
||||
for (const key of Object.keys(deFlat)) {
|
||||
const swiss = toSwiss(deFlat[key]);
|
||||
if (merged[key] !== deFlat[key]) continue;
|
||||
merged[key] = swiss;
|
||||
}
|
||||
|
||||
const overrides = buildOverrides(deFlat, merged);
|
||||
const outPath = path.join(LOCALES_DIR, 'de-CH.json');
|
||||
fs.writeFileSync(outPath, `${JSON.stringify(overrides, null, 2)}\n`, 'utf8');
|
||||
|
||||
const stillDe = Object.keys(deFlat).filter((k) => merged[k] === deFlat[k]).length;
|
||||
console.log(`de-CH: overrides=${Object.keys(flatten(overrides)).length}, ss-patched=${patched}, stillDe=${stillDe}`);
|
||||
65
scripts/fill-de-extended-gaps.js
Normal file
65
scripts/fill-de-extended-gaps.js
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env node
|
||||
/** Ergänzt fehlende billing/orders-Keys in de-extended aus de. */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const LOCALES_DIR = path.join(__dirname, '../frontend/src/i18n/locales');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function deepMerge(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] = deepMerge(result[key], value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setByPath(obj, dotPath, value) {
|
||||
const parts = dotPath.split('.');
|
||||
let cur = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};
|
||||
cur = cur[parts[i]];
|
||||
}
|
||||
cur[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
const de = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, 'de.json'), 'utf8'));
|
||||
const ext = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, 'de-extended.json'), 'utf8'));
|
||||
const deFlat = flatten(de);
|
||||
const merged = flatten(deepMerge(JSON.parse(JSON.stringify(de)), ext));
|
||||
|
||||
const NAMESPACES = ['billing', 'orders'];
|
||||
let added = 0;
|
||||
for (const key of Object.keys(deFlat)) {
|
||||
if (!NAMESPACES.some((ns) => key.startsWith(`${ns}.`))) continue;
|
||||
if (!(key in flatten(ext)) || merged[key] === deFlat[key]) {
|
||||
setByPath(ext, key, deFlat[key]);
|
||||
merged[key] = deFlat[key];
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(LOCALES_DIR, 'de-extended.json'),
|
||||
`${JSON.stringify(ext, null, 2)}\n`,
|
||||
'utf8'
|
||||
);
|
||||
const stillDe = Object.keys(deFlat).filter((k) => merged[k] === deFlat[k]).length;
|
||||
console.log(`de-extended: added ${added} keys, stillDe=${stillDe}`);
|
||||
237
scripts/fill-i18n-deep.py
Executable file
237
scripts/fill-i18n-deep.py
Executable file
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Füllt Locale-JSONs via deep-translator (Fallback wenn MyMemory/Google limitiert)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
|
||||
from pathlib import Path
|
||||
|
||||
from deep_translator import GoogleTranslator
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LOCALES_DIR = ROOT / "frontend" / "src" / "i18n" / "locales"
|
||||
CACHE_FILE = Path(__file__).resolve().parent / ".i18n-translate-cache.json"
|
||||
|
||||
LOCALE_TARGETS = {
|
||||
"fr": "fr",
|
||||
"es": "es",
|
||||
"it": "it",
|
||||
"pl": "pl",
|
||||
"ja": "ja",
|
||||
"zh": "zh-CN",
|
||||
"th": "th",
|
||||
"tl": "tl",
|
||||
"fil": "tl",
|
||||
}
|
||||
|
||||
SKIP = {"de", "de-extended", "de-CH", "en-US", "en-GB", "en-AU"}
|
||||
FILL_ORDER = ["fr", "es", "it", "pl", "ja", "zh", "th", "tl", "fil"]
|
||||
|
||||
PLACEHOLDER_RE = re.compile(r"\{[^}]+\}")
|
||||
|
||||
|
||||
def deep_merge(base, override):
|
||||
if not isinstance(base, dict) or isinstance(base, list):
|
||||
return override if override is not None else base
|
||||
result = dict(base)
|
||||
for key, value in (override or {}).items():
|
||||
if (
|
||||
isinstance(value, dict)
|
||||
and not isinstance(value, list)
|
||||
and isinstance(result.get(key), dict)
|
||||
and not isinstance(result.get(key), list)
|
||||
):
|
||||
result[key] = deep_merge(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def flatten(obj, prefix=""):
|
||||
out = {}
|
||||
for key, value in (obj or {}).items():
|
||||
next_key = f"{prefix}.{key}" if prefix else key
|
||||
if isinstance(value, dict) and not isinstance(value, list):
|
||||
out.update(flatten(value, next_key))
|
||||
elif isinstance(value, str):
|
||||
out[next_key] = value
|
||||
return out
|
||||
|
||||
|
||||
def set_by_path(obj, dot_path, value):
|
||||
parts = dot_path.split(".")
|
||||
cur = obj
|
||||
for part in parts[:-1]:
|
||||
if part not in cur or not isinstance(cur[part], dict):
|
||||
cur[part] = {}
|
||||
cur = cur[part]
|
||||
cur[parts[-1]] = value
|
||||
|
||||
|
||||
def build_overrides(de_flat, target_flat):
|
||||
out = {}
|
||||
for key, value in target_flat.items():
|
||||
if value != de_flat.get(key):
|
||||
set_by_path(out, key, value)
|
||||
return out
|
||||
|
||||
|
||||
def protect_placeholders(text):
|
||||
tokens = []
|
||||
|
||||
def repl(m):
|
||||
token = f"__PH{len(tokens)}__"
|
||||
tokens.append(m.group(0))
|
||||
return token
|
||||
|
||||
return PLACEHOLDER_RE.sub(repl, text), tokens
|
||||
|
||||
|
||||
def restore_placeholders(text, tokens):
|
||||
out = text
|
||||
for i, token in enumerate(tokens):
|
||||
out = out.replace(f"__PH{i}__", token)
|
||||
return out
|
||||
|
||||
|
||||
def load_cache():
|
||||
if CACHE_FILE.exists():
|
||||
return json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
||||
return {}
|
||||
|
||||
|
||||
def save_cache(cache):
|
||||
CACHE_FILE.write_text(json.dumps(cache, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def fill_de_ch(de_flat, merged_flat):
|
||||
for key in de_flat:
|
||||
if merged_flat.get(key) == de_flat[key]:
|
||||
merged_flat[key] = de_flat[key].replace("ß", "ss")
|
||||
|
||||
|
||||
def translate_text(translator, cache, text, from_lang, to_lang, delay, timeout=20):
|
||||
cache_key = f"{from_lang}|{to_lang}|{text}"
|
||||
if cache_key in cache:
|
||||
return cache[cache_key]
|
||||
safe, tokens = protect_placeholders(text)
|
||||
|
||||
def _call():
|
||||
return translator.translate(safe)
|
||||
|
||||
last_err = None
|
||||
for attempt in range(3):
|
||||
try:
|
||||
with ThreadPoolExecutor(max_workers=1) as pool:
|
||||
raw = pool.submit(_call).result(timeout=timeout)
|
||||
out = restore_placeholders(raw, tokens)
|
||||
cache[cache_key] = out
|
||||
time.sleep(delay)
|
||||
return out
|
||||
except (FuturesTimeout, Exception) as e:
|
||||
last_err = e
|
||||
time.sleep(2 + attempt * 2)
|
||||
raise last_err
|
||||
|
||||
|
||||
def fill_locale(code, en_flat, cache, delay, dry_run):
|
||||
target = LOCALE_TARGETS[code]
|
||||
de = json.loads((LOCALES_DIR / "de.json").read_text(encoding="utf-8"))
|
||||
de_flat = flatten(de)
|
||||
locale_path = LOCALES_DIR / f"{code}.json"
|
||||
locale_json = json.loads(locale_path.read_text(encoding="utf-8"))
|
||||
merged_flat = flatten(deep_merge(json.loads(json.dumps(de)), locale_json))
|
||||
|
||||
if code == "de-CH":
|
||||
fill_de_ch(de_flat, merged_flat)
|
||||
else:
|
||||
keys_to_fix = [k for k in de_flat if merged_flat.get(k) == de_flat[k]]
|
||||
unique = {}
|
||||
for k in keys_to_fix:
|
||||
from_lang = "en" if en_flat.get(k) and en_flat[k] != de_flat[k] else "de"
|
||||
text = en_flat[k] if from_lang == "en" else de_flat[k]
|
||||
unique.setdefault((from_lang, text), []).append(k)
|
||||
|
||||
print(f"[{code}] {len(keys_to_fix)} keys, {len(unique)} unique → {target}", flush=True)
|
||||
|
||||
by_source = {"en": [], "de": []}
|
||||
for (from_lang, text), keys in unique.items():
|
||||
by_source[from_lang].append((text, keys))
|
||||
|
||||
done = 0
|
||||
for from_lang in ("en", "de"):
|
||||
items = by_source[from_lang]
|
||||
if not items:
|
||||
continue
|
||||
translator = GoogleTranslator(source=from_lang, target=target)
|
||||
for text, keys in items:
|
||||
cache_key = f"{from_lang}|{target}|{text}"
|
||||
try:
|
||||
if dry_run:
|
||||
translated = f"[{target}] {text[:30]}"
|
||||
elif cache_key in cache:
|
||||
translated = cache[cache_key]
|
||||
else:
|
||||
translated = translate_text(translator, cache, text, from_lang, target, delay)
|
||||
for k in keys:
|
||||
merged_flat[k] = translated
|
||||
done += 1
|
||||
if done % 50 == 0:
|
||||
print(f"[{code}] {done}/{len(unique)}", flush=True)
|
||||
save_cache(cache)
|
||||
except Exception as e:
|
||||
print(f"[{code}] skip: {text[:40]}… ({e})", file=sys.stderr, flush=True)
|
||||
if from_lang == "en" and en_flat:
|
||||
for k in keys:
|
||||
merged_flat[k] = text
|
||||
|
||||
save_cache(cache)
|
||||
|
||||
overrides = build_overrides(de_flat, merged_flat)
|
||||
if not dry_run:
|
||||
locale_path.write_text(
|
||||
json.dumps(overrides, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
still_de = sum(1 for k in de_flat if merged_flat.get(k) == de_flat[k])
|
||||
print(f"[{code}] overrides={len(flatten(overrides))}, stillDe={still_de}", flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--locale")
|
||||
parser.add_argument("--all", action="store_true")
|
||||
parser.add_argument("--from-locale", dest="from_locale", help="Bei --all: ab dieser Locale fortsetzen")
|
||||
parser.add_argument("--delay", type=float, default=0.35)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
codes = FILL_ORDER if args.all else [args.locale] if args.locale else None
|
||||
if codes and args.from_locale:
|
||||
if args.from_locale not in FILL_ORDER:
|
||||
parser.error(f"Unknown locale: {args.from_locale}")
|
||||
codes = FILL_ORDER[FILL_ORDER.index(args.from_locale) :]
|
||||
if not codes:
|
||||
parser.error("Usage: --locale <code> | --all")
|
||||
|
||||
de = json.loads((LOCALES_DIR / "de.json").read_text(encoding="utf-8"))
|
||||
en_us = json.loads((LOCALES_DIR / "en-US.json").read_text(encoding="utf-8"))
|
||||
en_flat = flatten(deep_merge(de, en_us))
|
||||
cache = load_cache()
|
||||
|
||||
for code in codes:
|
||||
if code in SKIP or code not in LOCALE_TARGETS:
|
||||
continue
|
||||
fill_locale(code, en_flat, cache, args.delay, args.dry_run)
|
||||
time.sleep(5)
|
||||
|
||||
save_cache(cache)
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
385
scripts/fill-i18n-locales.js
Normal file
385
scripts/fill-i18n-locales.js
Normal file
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Füllt Locale-JSONs: Blätter, die nach deepMerge(de, locale) noch Deutsch sind,
|
||||
* werden übersetzt (MyMemory, Fallback Google) und als Override geschrieben.
|
||||
*
|
||||
* Cache: scripts/.i18n-translate-cache.json
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/fill-i18n-locales.js --locale en-US
|
||||
* node scripts/fill-i18n-locales.js --all --delay 450
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const LOCALES_DIR = path.join(ROOT, 'frontend', 'src', 'i18n', 'locales');
|
||||
const CACHE_FILE = path.join(__dirname, '.i18n-translate-cache.json');
|
||||
const TRANSLATE_PKG = path.join(ROOT, 'frontend', 'node_modules', '@vitalets/google-translate-api');
|
||||
|
||||
const PLACEHOLDER_RE = /\{[^}]+\}/g;
|
||||
|
||||
const LOCALE_TARGETS = {
|
||||
'en-US': 'en',
|
||||
'en-GB': 'en',
|
||||
'en-AU': 'en',
|
||||
es: 'es',
|
||||
fr: 'fr',
|
||||
it: 'it',
|
||||
pl: 'pl',
|
||||
ja: 'ja',
|
||||
zh: 'zh-CN',
|
||||
th: 'th',
|
||||
tl: 'tl',
|
||||
fil: 'tl',
|
||||
'de-CH': 'de',
|
||||
};
|
||||
|
||||
const SKIP_LOCALES = new Set(['de', 'de-extended']);
|
||||
|
||||
const ALL_FILL_ORDER = [
|
||||
'en-US',
|
||||
'en-GB',
|
||||
'en-AU',
|
||||
'fr',
|
||||
'es',
|
||||
'it',
|
||||
'pl',
|
||||
'ja',
|
||||
'zh',
|
||||
'th',
|
||||
'tl',
|
||||
'fil',
|
||||
'de-CH',
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function setByPath(obj, dotPath, value) {
|
||||
const parts = dotPath.split('.');
|
||||
let cur = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object') {
|
||||
cur[parts[i]] = {};
|
||||
}
|
||||
cur = cur[parts[i]];
|
||||
}
|
||||
cur[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
function buildOverrides(deFlat, targetFlat) {
|
||||
const out = {};
|
||||
for (const [key, value] of Object.entries(targetFlat)) {
|
||||
if (value !== deFlat[key]) {
|
||||
setByPath(out, key, value);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function protectPlaceholders(text) {
|
||||
const tokens = [];
|
||||
const safe = text.replace(PLACEHOLDER_RE, (m) => {
|
||||
const token = `__PH${tokens.length}__`;
|
||||
tokens.push(m);
|
||||
return token;
|
||||
});
|
||||
return { safe, tokens };
|
||||
}
|
||||
|
||||
function restorePlaceholders(text, tokens) {
|
||||
let out = text;
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
out = out.replace(new RegExp(`__\\s*PH\\s*${i}\\s*__`, 'gi'), tokens[i]);
|
||||
out = out.replace(`__PH${i}__`, tokens[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function loadCache() {
|
||||
if (!fs.existsSync(CACHE_FILE)) return {};
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveCache(cache) {
|
||||
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
let locale = null;
|
||||
let all = false;
|
||||
let delay = 450;
|
||||
let dryRun = false;
|
||||
let noGoogle = false;
|
||||
let enFallback = false;
|
||||
let skipEn = false;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--locale' && args[i + 1]) locale = args[++i];
|
||||
else if (args[i] === '--all') all = true;
|
||||
else if (args[i] === '--delay' && args[i + 1]) delay = parseInt(args[++i], 10) || 450;
|
||||
else if (args[i] === '--dry-run') dryRun = true;
|
||||
else if (args[i] === '--no-google') noGoogle = true;
|
||||
else if (args[i] === '--en-fallback') enFallback = true;
|
||||
else if (args[i] === '--skip-en') skipEn = true;
|
||||
}
|
||||
return { locale, all, delay, dryRun, noGoogle, enFallback, skipEn };
|
||||
}
|
||||
|
||||
async function mymemoryTranslate(text, from, to) {
|
||||
const url = new URL('https://api.mymemory.translated.net/get');
|
||||
url.searchParams.set('q', text.slice(0, 450));
|
||||
url.searchParams.set('langpair', `${from}|${to}`);
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.quotaFinished) {
|
||||
throw new Error('MyMemory quota finished');
|
||||
}
|
||||
if (data.responseStatus !== 200) {
|
||||
throw new Error(data.responseDetails || `MyMemory status ${data.responseStatus}`);
|
||||
}
|
||||
return data.responseData.translatedText;
|
||||
}
|
||||
|
||||
async function googleTranslate(translateFn, text, from, to) {
|
||||
const res = await translateFn(text, { from, to });
|
||||
return res.text;
|
||||
}
|
||||
|
||||
async function translateText(providers, text, from, to, cache) {
|
||||
const cacheKey = `${from}|${to}|${text}`;
|
||||
if (cache[cacheKey]) return cache[cacheKey];
|
||||
|
||||
const { safe, tokens } = protectPlaceholders(text);
|
||||
let out;
|
||||
let lastErr;
|
||||
for (const provider of providers) {
|
||||
try {
|
||||
const raw = await provider(safe, from, to);
|
||||
out = restorePlaceholders(raw, tokens);
|
||||
out = out.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
cache[cacheKey] = out;
|
||||
return out;
|
||||
} catch (e) {
|
||||
lastErr = e;
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
function bootstrapEnFromGb(deFlat, mergedFlat) {
|
||||
const gbJson = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, 'en-GB.json'), 'utf8'));
|
||||
const de = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, 'de.json'), 'utf8'));
|
||||
const gbFlat = flatten(deepMergeMessages(de, gbJson));
|
||||
let n = 0;
|
||||
for (const k of Object.keys(deFlat)) {
|
||||
if (mergedFlat[k] === deFlat[k] && gbFlat[k] && gbFlat[k] !== deFlat[k]) {
|
||||
mergedFlat[k] = gbFlat[k];
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function bootstrapEnFromUs(deFlat, mergedFlat, enFlat) {
|
||||
if (!enFlat) return 0;
|
||||
let n = 0;
|
||||
for (const k of Object.keys(deFlat)) {
|
||||
if (mergedFlat[k] === deFlat[k] && enFlat[k] && enFlat[k] !== deFlat[k]) {
|
||||
mergedFlat[k] = enFlat[k];
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
async function fillLocale(code, opts) {
|
||||
const { providers, cache, delay, dryRun, enFlat, enFallback } = opts;
|
||||
const targetLang = LOCALE_TARGETS[code];
|
||||
const de = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, 'de.json'), 'utf8'));
|
||||
const deFlat = flatten(de);
|
||||
const localePath = path.join(LOCALES_DIR, `${code}.json`);
|
||||
const localeJson = JSON.parse(fs.readFileSync(localePath, 'utf8'));
|
||||
const mergedFlat = flatten(deepMergeMessages(JSON.parse(JSON.stringify(de)), localeJson));
|
||||
|
||||
if (code === 'en-US' || code === 'en-AU') {
|
||||
const copied = bootstrapEnFromGb(deFlat, mergedFlat);
|
||||
if (copied) console.log(`[${code}] bootstrapped ${copied} strings from en-GB`);
|
||||
}
|
||||
if ((code === 'en-GB' || code === 'en-AU') && enFlat) {
|
||||
const copied = bootstrapEnFromUs(deFlat, mergedFlat, enFlat);
|
||||
if (copied) console.log(`[${code}] bootstrapped ${copied} strings from en-US`);
|
||||
}
|
||||
|
||||
const keysToFix = Object.keys(deFlat).filter((k) => mergedFlat[k] === deFlat[k]);
|
||||
if (!keysToFix.length) {
|
||||
console.log(`[${code}] nothing to fill`);
|
||||
if (!dryRun) {
|
||||
fs.writeFileSync(localePath, `${JSON.stringify(buildOverrides(deFlat, mergedFlat), null, 2)}\n`, 'utf8');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const uniqueTexts = new Map();
|
||||
for (const k of keysToFix) {
|
||||
const from = enFlat && enFlat[k] && enFlat[k] !== deFlat[k] ? 'en' : 'de';
|
||||
const text = from === 'en' ? enFlat[k] : deFlat[k];
|
||||
const mapKey = `${from}\0${text}`;
|
||||
if (!uniqueTexts.has(mapKey)) uniqueTexts.set(mapKey, { from, text, keys: [] });
|
||||
uniqueTexts.get(mapKey).keys.push(k);
|
||||
}
|
||||
|
||||
console.log(`[${code}] ${keysToFix.length} keys, ${uniqueTexts.size} unique, target=${targetLang}`);
|
||||
|
||||
if (code === 'de-CH') {
|
||||
for (const k of keysToFix) {
|
||||
const base = deFlat[k];
|
||||
mergedFlat[k] = base.replace(/ß/g, 'ss');
|
||||
}
|
||||
} else {
|
||||
let done = 0;
|
||||
const failed = [];
|
||||
for (const entry of uniqueTexts.values()) {
|
||||
const { from, text, keys } = entry;
|
||||
const cacheKey = `${from}|${targetLang}|${text}`;
|
||||
let translated;
|
||||
if (from === 'en' && targetLang === 'en') {
|
||||
translated = text;
|
||||
} else if (cache[cacheKey]) {
|
||||
translated = cache[cacheKey];
|
||||
} else if (dryRun) {
|
||||
translated = `[${targetLang}] ${text.slice(0, 30)}`;
|
||||
} else {
|
||||
try {
|
||||
translated = await translateText(providers, text, from, targetLang, cache);
|
||||
await sleep(delay);
|
||||
} catch (e) {
|
||||
console.error(`[${code}] skip: ${text.slice(0, 50)}… (${e.message})`);
|
||||
failed.push(entry);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (const k of keys) mergedFlat[k] = translated;
|
||||
done++;
|
||||
if (done % 50 === 0) {
|
||||
console.log(`[${code}] ${done}/${uniqueTexts.size} (failed ${failed.length})`);
|
||||
saveCache(cache);
|
||||
}
|
||||
}
|
||||
saveCache(cache);
|
||||
|
||||
if (failed.length && !dryRun) {
|
||||
console.log(`[${code}] retrying ${failed.length} failed strings…`);
|
||||
await sleep(5000);
|
||||
for (const { from, text, keys } of failed) {
|
||||
const cacheKey = `${from}|${targetLang}|${text}`;
|
||||
try {
|
||||
const translated = cache[cacheKey] || (await translateText(providers, text, from, targetLang, cache));
|
||||
for (const k of keys) mergedFlat[k] = translated;
|
||||
await sleep(delay);
|
||||
} catch (e) {
|
||||
if (enFallback && from === 'en') {
|
||||
for (const k of keys) mergedFlat[k] = text;
|
||||
} else {
|
||||
console.error(`[${code}] final skip: ${text.slice(0, 40)}…`);
|
||||
}
|
||||
}
|
||||
}
|
||||
saveCache(cache);
|
||||
}
|
||||
}
|
||||
|
||||
const overrides = buildOverrides(deFlat, mergedFlat);
|
||||
if (!dryRun) {
|
||||
fs.writeFileSync(localePath, `${JSON.stringify(overrides, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
const stillDe = Object.keys(deFlat).filter((k) => mergedFlat[k] === deFlat[k]).length;
|
||||
console.log(`[${code}] overrides=${Object.keys(flatten(overrides)).length}, stillDe=${stillDe}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { locale, all, delay, dryRun, noGoogle, enFallback, skipEn } = parseArgs();
|
||||
const providers = [
|
||||
(text, from, to) => mymemoryTranslate(text, from, to),
|
||||
];
|
||||
if (!noGoogle && fs.existsSync(TRANSLATE_PKG)) {
|
||||
const { translate: translateFn } = require(TRANSLATE_PKG);
|
||||
providers.push((text, from, to) => googleTranslate(translateFn, text, from, to));
|
||||
}
|
||||
|
||||
const cache = loadCache();
|
||||
let codes = all ? ALL_FILL_ORDER : locale ? [locale] : null;
|
||||
if (skipEn && codes) {
|
||||
codes = codes.filter((c) => !['en-US', 'en-GB', 'en-AU'].includes(c));
|
||||
}
|
||||
if (!codes) {
|
||||
console.error('Usage: --locale <code> | --all [--delay ms]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let enFlat = null;
|
||||
const de = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, 'de.json'), 'utf8'));
|
||||
const enUsPath = path.join(LOCALES_DIR, 'en-US.json');
|
||||
if (fs.existsSync(enUsPath)) {
|
||||
enFlat = flatten(deepMergeMessages(de, JSON.parse(fs.readFileSync(enUsPath, 'utf8'))));
|
||||
}
|
||||
|
||||
for (const code of codes) {
|
||||
if (SKIP_LOCALES.has(code)) continue;
|
||||
if (!(code in LOCALE_TARGETS)) continue;
|
||||
await fillLocale(code, { providers, cache, delay, dryRun, enFlat, enFallback });
|
||||
if (all && !dryRun) await sleep(8000);
|
||||
if (code === 'en-US' && !dryRun) {
|
||||
enFlat = flatten(deepMergeMessages(de, JSON.parse(fs.readFileSync(enUsPath, 'utf8'))));
|
||||
}
|
||||
}
|
||||
saveCache(cache);
|
||||
console.log('Done.');
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
196
scripts/fix-en-leaks.py
Normal file
196
scripts/fix-en-leaks.py
Normal file
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Übersetzt Keys, deren Locale-Wert noch dem en-US-Merge entspricht (EN-Leak)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
|
||||
from pathlib import Path
|
||||
|
||||
from deep_translator import GoogleTranslator
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LOCALES_DIR = ROOT / "frontend" / "src" / "i18n" / "locales"
|
||||
CACHE_FILE = Path(__file__).resolve().parent / ".i18n-translate-cache.json"
|
||||
|
||||
TARGETS = {
|
||||
"fr": "fr", "es": "es", "it": "it", "pl": "pl",
|
||||
"ja": "ja", "zh": "zh-CN", "th": "th", "tl": "tl", "fil": "tl",
|
||||
}
|
||||
|
||||
PLACEHOLDER_RE = re.compile(r"\{[^}]+\}")
|
||||
|
||||
|
||||
def deep_merge(base, override):
|
||||
if not isinstance(base, dict) or isinstance(base, list):
|
||||
return override if override is not None else base
|
||||
result = dict(base)
|
||||
for key, value in (override or {}).items():
|
||||
if (
|
||||
isinstance(value, dict) and not isinstance(value, list)
|
||||
and isinstance(result.get(key), dict) and not isinstance(result.get(key), list)
|
||||
):
|
||||
result[key] = deep_merge(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def flatten(obj, prefix=""):
|
||||
out = {}
|
||||
for key, value in (obj or {}).items():
|
||||
next_key = f"{prefix}.{key}" if prefix else key
|
||||
if isinstance(value, dict) and not isinstance(value, list):
|
||||
out.update(flatten(value, next_key))
|
||||
elif isinstance(value, str):
|
||||
out[next_key] = value
|
||||
return out
|
||||
|
||||
|
||||
def set_by_path(obj, dot_path, value):
|
||||
parts = dot_path.split(".")
|
||||
cur = obj
|
||||
for part in parts[:-1]:
|
||||
if part not in cur or not isinstance(cur[part], dict):
|
||||
cur[part] = {}
|
||||
cur = cur[part]
|
||||
cur[parts[-1]] = value
|
||||
|
||||
|
||||
def build_overrides(de_flat, target_flat):
|
||||
out = {}
|
||||
for key, value in target_flat.items():
|
||||
if value != de_flat.get(key):
|
||||
set_by_path(out, key, value)
|
||||
return out
|
||||
|
||||
|
||||
def protect_placeholders(text):
|
||||
tokens = []
|
||||
def repl(m):
|
||||
token = f"__PH{len(tokens)}__"
|
||||
tokens.append(m.group(0))
|
||||
return token
|
||||
return PLACEHOLDER_RE.sub(repl, text), tokens
|
||||
|
||||
|
||||
def restore_placeholders(text, tokens):
|
||||
out = text
|
||||
for i, token in enumerate(tokens):
|
||||
out = out.replace(f"__PH{i}__", token)
|
||||
return out
|
||||
|
||||
|
||||
def load_cache():
|
||||
if CACHE_FILE.exists():
|
||||
return json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
||||
return {}
|
||||
|
||||
|
||||
def save_cache(cache):
|
||||
CACHE_FILE.write_text(json.dumps(cache, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def translate_en(translator, cache, text, target, delay, timeout=25):
|
||||
cache_key = f"en|{target}|{text}"
|
||||
if cache_key in cache:
|
||||
return cache[cache_key]
|
||||
safe, tokens = protect_placeholders(text)
|
||||
|
||||
def _call():
|
||||
return translator.translate(safe)
|
||||
|
||||
last_err = None
|
||||
for attempt in range(3):
|
||||
try:
|
||||
with ThreadPoolExecutor(max_workers=1) as pool:
|
||||
raw = pool.submit(_call).result(timeout=timeout)
|
||||
out = restore_placeholders(raw, tokens)
|
||||
cache[cache_key] = out
|
||||
time.sleep(delay)
|
||||
return out
|
||||
except (FuturesTimeout, Exception) as e:
|
||||
last_err = e
|
||||
time.sleep(2 + attempt * 2)
|
||||
raise last_err
|
||||
|
||||
|
||||
def fix_locale(code, de_flat, en_flat, cache, delay, dry_run):
|
||||
target = TARGETS[code]
|
||||
de = json.loads((LOCALES_DIR / "de.json").read_text(encoding="utf-8"))
|
||||
locale_path = LOCALES_DIR / f"{code}.json"
|
||||
locale_json = json.loads(locale_path.read_text(encoding="utf-8"))
|
||||
merged = flatten(deep_merge(json.loads(json.dumps(de)), locale_json))
|
||||
|
||||
leaks = [
|
||||
k for k in de_flat
|
||||
if merged.get(k) == en_flat.get(k) and en_flat.get(k) != de_flat.get(k)
|
||||
]
|
||||
unique_texts = {}
|
||||
for k in leaks:
|
||||
text = en_flat[k]
|
||||
unique_texts.setdefault(text, []).append(k)
|
||||
|
||||
print(f"[{code}] {len(leaks)} EN-leaks, {len(unique_texts)} unique → {target}", flush=True)
|
||||
if not unique_texts:
|
||||
return
|
||||
|
||||
translator = GoogleTranslator(source="en", target=target)
|
||||
done = 0
|
||||
for text, keys in unique_texts.items():
|
||||
try:
|
||||
if dry_run:
|
||||
translated = f"[{target}] {text[:25]}"
|
||||
else:
|
||||
translated = translate_en(translator, cache, text, target, delay)
|
||||
for k in keys:
|
||||
merged[k] = translated
|
||||
done += 1
|
||||
if done % 50 == 0:
|
||||
print(f"[{code}] {done}/{len(unique_texts)}", flush=True)
|
||||
save_cache(cache)
|
||||
except Exception as e:
|
||||
print(f"[{code}] skip: {text[:40]}… ({e})", file=sys.stderr, flush=True)
|
||||
|
||||
save_cache(cache)
|
||||
overrides = build_overrides(de_flat, merged)
|
||||
if not dry_run:
|
||||
locale_path.write_text(json.dumps(overrides, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
en_leaks_left = sum(
|
||||
1 for k in de_flat
|
||||
if merged.get(k) == en_flat.get(k) and en_flat.get(k) != de_flat.get(k)
|
||||
)
|
||||
print(f"[{code}] overrides={len(flatten(overrides))}, enLeaksLeft={en_leaks_left}", flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("locales", nargs="*", default=list(TARGETS.keys()))
|
||||
parser.add_argument("--delay", type=float, default=0.2)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
de = json.loads((LOCALES_DIR / "de.json").read_text(encoding="utf-8"))
|
||||
en_us = json.loads((LOCALES_DIR / "en-US.json").read_text(encoding="utf-8"))
|
||||
de_flat = flatten(de)
|
||||
en_flat = flatten(deep_merge(json.loads(json.dumps(de)), en_us))
|
||||
cache = load_cache()
|
||||
|
||||
for code in args.locales:
|
||||
if code not in TARGETS:
|
||||
print(f"skip {code}", file=sys.stderr)
|
||||
continue
|
||||
fix_locale(code, de_flat, en_flat, cache, args.delay, args.dry_run)
|
||||
time.sleep(3)
|
||||
|
||||
save_cache(cache)
|
||||
print("Done.", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
49
scripts/patch-de-ch-swiss.js
Normal file
49
scripts/patch-de-ch-swiss.js
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
/** Setzt nur ß→ss Overrides in de-CH.json, ohne bestehende Einträge zu löschen. */
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const LOCALES_DIR = path.join(ROOT, 'frontend', 'src', 'i18n', 'locales');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function setByPath(obj, dotPath, value) {
|
||||
const parts = dotPath.split('.');
|
||||
let cur = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};
|
||||
cur = cur[parts[i]];
|
||||
}
|
||||
cur[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
const de = JSON.parse(fs.readFileSync(path.join(LOCALES_DIR, 'de.json'), 'utf8'));
|
||||
const deFlat = flatten(de);
|
||||
const chPath = path.join(LOCALES_DIR, 'de-CH.json');
|
||||
const ch = JSON.parse(fs.readFileSync(chPath, 'utf8'));
|
||||
const chFlat = flatten(ch);
|
||||
|
||||
let n = 0;
|
||||
for (const [key, deVal] of Object.entries(deFlat)) {
|
||||
if (!deVal.includes('ß')) continue;
|
||||
const swiss = deVal.replace(/ß/g, 'ss');
|
||||
if (chFlat[key] !== swiss) {
|
||||
setByPath(ch, key, swiss);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(chPath, `${JSON.stringify(ch, null, 2)}\n`, 'utf8');
|
||||
console.log(`de-CH: ${n} ß→ss patches applied`);
|
||||
47
scripts/patch-en-gb-au.js
Normal file
47
scripts/patch-en-gb-au.js
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
/** UK/AU-Orthografie in en-GB / en-AU (gegenüber en-US). */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const LOCALES_DIR = path.join(__dirname, '../frontend/src/i18n/locales');
|
||||
|
||||
const UK_MAP = [
|
||||
[/\bcolor\b/gi, (m) => (m[0] === 'C' ? 'Colour' : 'colour')],
|
||||
[/\bcolors\b/gi, (m) => (m[0] === 'C' ? 'Colours' : 'colours')],
|
||||
[/\borganize\b/gi, (m) => (m[0] === 'O' ? 'Organise' : 'organise')],
|
||||
[/\borganized\b/gi, (m) => (m[0] === 'O' ? 'Organised' : 'organised')],
|
||||
[/\bcenter\b/gi, (m) => (m[0] === 'C' ? 'Centre' : 'centre')],
|
||||
[/\bfavorite\b/gi, (m) => (m[0] === 'F' ? 'Favourite' : 'favourite')],
|
||||
[/\bfavorites\b/gi, (m) => (m[0] === 'F' ? 'Favourites' : 'favourites')],
|
||||
[/\blabor\b/gi, (m) => (m[0] === 'L' ? 'Labour' : 'labour')],
|
||||
[/\bdefense\b/gi, (m) => (m[0] === 'D' ? 'Defence' : 'defence')],
|
||||
];
|
||||
|
||||
function applyUk(text) {
|
||||
let out = text;
|
||||
for (const [re, repl] of UK_MAP) {
|
||||
out = out.replace(re, repl);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function walk(obj, fn) {
|
||||
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return;
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (typeof value === 'string') obj[key] = fn(value);
|
||||
else if (value && typeof value === 'object') walk(value, fn);
|
||||
}
|
||||
}
|
||||
|
||||
for (const locale of ['en-GB', 'en-AU']) {
|
||||
const p = path.join(LOCALES_DIR, `${locale}.json`);
|
||||
const data = JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||
let n = 0;
|
||||
walk(data, (s) => {
|
||||
const t = applyUk(s);
|
||||
if (t !== s) n++;
|
||||
return t;
|
||||
});
|
||||
fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
||||
console.log(`${locale}: ${n} UK spelling patches`);
|
||||
}
|
||||
19
scripts/run-fix-en-leaks.sh
Executable file
19
scripts/run-fix-en-leaks.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PY="$ROOT/scripts/.venv-i18n/bin/python"
|
||||
LOG="/tmp/fix-en-leaks.log"
|
||||
LOCALES=(th tl fil fr es it pl ja zh)
|
||||
|
||||
: >"$LOG"
|
||||
echo "[$(date -Iseconds)] start" | tee -a "$LOG"
|
||||
|
||||
for loc in "${LOCALES[@]}"; do
|
||||
echo "[$(date -Iseconds)] === $loc ===" | tee -a "$LOG"
|
||||
PYTHONUNBUFFERED=1 "$PY" -u "$ROOT/scripts/fix-en-leaks.py" "$loc" --delay 0.15 2>&1 | tee -a "$LOG" || {
|
||||
echo "[$(date -Iseconds)] FAILED $loc" | tee -a "$LOG"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
echo "[$(date -Iseconds)] ALL DONE" | tee -a "$LOG"
|
||||
21
scripts/run-i18n-deep-fill.sh
Executable file
21
scripts/run-i18n-deep-fill.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sequentieller Deep-Fill – eine Locale nach der anderen, mit Fortschritts-Log.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PY="$ROOT/scripts/.venv-i18n/bin/python"
|
||||
LOG="${TMPDIR:-/tmp}/i18n-deep-fill.log"
|
||||
# Nur noch offene Locales (fr/es/it bereits gefüllt)
|
||||
LOCALES=(pl ja zh th tl fil)
|
||||
|
||||
: >"$LOG"
|
||||
echo "[$(date -Iseconds)] start" | tee -a "$LOG"
|
||||
|
||||
for loc in "${LOCALES[@]}"; do
|
||||
echo "[$(date -Iseconds)] === $loc ===" | tee -a "$LOG"
|
||||
if ! PYTHONUNBUFFERED=1 "$PY" -u "$ROOT/scripts/fill-i18n-deep.py" --locale "$loc" --delay 0.2 2>&1 | tee -a "$LOG"; then
|
||||
echo "[$(date -Iseconds)] FAILED $loc" | tee -a "$LOG"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "[$(date -Iseconds)] ALL DONE" | tee -a "$LOG"
|
||||
80
scripts/update-i18n-todo-stats.js
Normal file
80
scripts/update-i18n-todo-stats.js
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Aktualisiert Kennzahlen-Tabellen in frontend/I18N_TODO.md aus check-i18n-completeness.
|
||||
* Usage: node scripts/update-i18n-todo-stats.js
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const TODO = path.join(ROOT, 'frontend', 'I18N_TODO.md');
|
||||
|
||||
const LOCALES = [
|
||||
'de-CH',
|
||||
'de-extended',
|
||||
'en-US',
|
||||
'en-GB',
|
||||
'en-AU',
|
||||
'es',
|
||||
'fr',
|
||||
'it',
|
||||
'pl',
|
||||
'ja',
|
||||
'zh',
|
||||
'th',
|
||||
'tl',
|
||||
'fil',
|
||||
];
|
||||
|
||||
function parseStats() {
|
||||
const out = execSync(`node "${path.join(__dirname, 'check-i18n-completeness.js')}"`, {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const stats = {};
|
||||
for (const line of out.split('\n')) {
|
||||
const m = line.match(/^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
|
||||
if (m && LOCALES.includes(m[1])) {
|
||||
stats[m[1]] = {
|
||||
explicit: m[2],
|
||||
erbtsDE: m[3],
|
||||
deExpl: m[4],
|
||||
neDe: m[5],
|
||||
enUs: m[6],
|
||||
};
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
function replaceTable(md, locale, row) {
|
||||
const header = `## \`${locale}\``;
|
||||
const idx = md.indexOf(header);
|
||||
if (idx === -1) return md;
|
||||
const tableStart = md.indexOf('| explicit |', idx);
|
||||
const tableEnd = md.indexOf('\n\n### Aufgaben', tableStart);
|
||||
if (tableStart === -1 || tableEnd === -1) return md;
|
||||
const newTable = `| explicit | erbtsDE | =de expl | ≠de | ≈en-US |
|
||||
|----------|---------|----------|-----|--------|
|
||||
| ${row.explicit} | ${row.erbtsDE} | ${row.deExpl} | ${row.neDe} | ${row.enUs} |`;
|
||||
return md.slice(0, tableStart) + newTable + md.slice(tableEnd);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const stats = parseStats();
|
||||
let md = fs.readFileSync(TODO, 'utf8');
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
md = md.replace(
|
||||
/Stand der Kennzahlen: \*\*[\d-]+\*\*/,
|
||||
`Stand der Kennzahlen: **${date}**`
|
||||
);
|
||||
for (const locale of LOCALES) {
|
||||
if (stats[locale]) md = replaceTable(md, locale, stats[locale]);
|
||||
}
|
||||
fs.writeFileSync(TODO, md, 'utf8');
|
||||
console.log('Updated', TODO, 'for', Object.keys(stats).length, 'locales');
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user