/** * Übersetzt String-Blätter in locales/fr/*.json von Deutsch nach Französisch * (google-translate-api-x, inoffiziell), mit Cache unter scripts/.i18n-de-fr-cache.json. * * Ausführen aus frontend/: node scripts/generate-fr-locale-from-de.mjs */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { translate } from 'google-translate-api-x'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const LOCALES_FR = path.join(__dirname, '../src/i18n/locales/fr'); const CACHE_PATH = path.join(__dirname, '.i18n-de-fr-cache.json'); const CONCURRENCY = Math.max(1, parseInt(process.env.I18N_MT_CONCURRENCY || '4', 10)); const DELAY_MS = Math.max(0, parseInt(process.env.I18N_MT_DELAY_MS || '350', 10)); function loadCache() { try { return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8')); } catch { return {}; } } function saveCache(cache) { fs.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 0), 'utf8'); } function shouldSkipTranslation(str) { if (typeof str !== 'string' || str.length === 0) return true; if (/^[\d\s./:\\\-–—:]+$/u.test(str) && str.length < 96) return true; return false; } function collectStrings(node, set) { if (node === null || node === undefined) return; if (typeof node === 'string') { set.add(node); return; } if (Array.isArray(node)) { for (const x of node) collectStrings(x, set); return; } if (typeof node === 'object') { for (const k of Object.keys(node)) collectStrings(node[k], set); } } function applyTranslations(node, cache) { if (node === null || node === undefined) return node; if (typeof node === 'string') { if (shouldSkipTranslation(node)) return node; return cache[node] !== undefined ? cache[node] : node; } if (Array.isArray(node)) return node.map((x) => applyTranslations(x, cache)); if (typeof node === 'object') { const out = {}; for (const k of Object.keys(node)) { out[k] = applyTranslations(node[k], cache); } return out; } return node; } async function translateOne(text, cache) { if (shouldSkipTranslation(text)) return text; if (cache[text]) return cache[text]; for (let attempt = 0; attempt < 4; attempt++) { try { const res = await translate(text, { from: 'de', to: 'fr' }); const out = res.text; cache[text] = out; return out; } catch (e) { await new Promise((r) => setTimeout(r, 900 * (attempt + 1))); } } console.warn('Übersetzung fehlgeschlagen, Original behalten:', text.slice(0, 80)); cache[text] = text; return text; } async function main() { const files = fs.readdirSync(LOCALES_FR).filter((f) => f.endsWith('.json')); const all = new Set(); for (const f of files) { const raw = JSON.parse(fs.readFileSync(path.join(LOCALES_FR, f), 'utf8')); collectStrings(raw, all); } const unique = [...all].filter((s) => !shouldSkipTranslation(s)); const cache = loadCache(); const missing = unique.filter((s) => cache[s] === undefined); console.log(`Unique (MT): ${unique.length}, ohne Cache-Eintrag: ${missing.length}`); let next = 0; let saved = 0; async function worker() { for (;;) { const i = next++; if (i >= missing.length) return; const s = missing[i]; await translateOne(s, cache); saved++; if (saved % 30 === 0) { saveCache(cache); console.log(`… ${saved}/${missing.length}`); } if (DELAY_MS > 0) { await new Promise((r) => setTimeout(r, DELAY_MS)); } } } const workers = Array.from({ length: Math.min(CONCURRENCY, Math.max(1, missing.length)) }, () => worker()); await Promise.all(workers); saveCache(cache); for (const f of files) { const p = path.join(LOCALES_FR, f); const raw = JSON.parse(fs.readFileSync(p, 'utf8')); const out = applyTranslations(raw, cache); fs.writeFileSync(p, `${JSON.stringify(out, null, 4)}\n`, 'utf8'); } console.log('Fertig: fr/*.json aktualisiert.'); } main().catch((e) => { console.error(e); process.exit(1); });