/** * Erzeugt fr/falukant.json neu aus de/falukant.json (google-translate-api-x). * Stellt sicher, dass benannte Platzhalter {name} exakt wie im Deutschen bleiben. * * Aus frontend/: node scripts/smooth-fr-falukant.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 DE_PATH = path.join(__dirname, '../src/i18n/locales/de/falukant.json'); const FR_PATH = path.join(__dirname, '../src/i18n/locales/fr/falukant.json'); const CACHE_PATH = path.join(__dirname, '.falukant-fr-smooth-cache.json'); const CONCURRENCY = Math.max(1, parseInt(process.env.I18N_MT_CONCURRENCY || '3', 10)); const DELAY_MS = Math.max(0, parseInt(process.env.I18N_MT_DELAY_MS || '400', 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; if (/^([dDMYHhms\.\/:,\[\]'\s-]|yyyy|yy)+$/i.test(str.trim()) && str.length < 96) return true; return false; } /** Ersetzt {…}-Tokens in der Übersetzung durch die Namen aus dem Deutschen (gleiche Reihenfolge). */ function alignPlaceholders(deStr, frStr) { const deNames = [...deStr.matchAll(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g)].map((m) => m[1]); if (deNames.length === 0) return frStr; let i = 0; return frStr.replace(/\{[^}]+\}/g, () => { const name = deNames[i]; i += 1; return name !== undefined ? `{${name}}` : '{}'; }); } 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 aligned = alignPlaceholders(text, res.text); cache[text] = aligned; return aligned; } catch { await new Promise((r) => setTimeout(r, 800 * (attempt + 1))); } } console.warn('Übersetzung fehlgeschlagen:', text.slice(0, 72)); cache[text] = text; return text; } function collectStrings(node, set) { if (typeof node === 'string') { set.add(node); return; } if (Array.isArray(node)) { for (const x of node) collectStrings(x, set); return; } if (node && typeof node === 'object') { for (const k of Object.keys(node)) collectStrings(node[k], set); } } function applyMap(node, cache) { if (typeof node === 'string') { if (shouldSkipTranslation(node)) return node; return cache[node] !== undefined ? cache[node] : node; } if (Array.isArray(node)) return node.map((x) => applyMap(x, cache)); if (node && typeof node === 'object') { const out = {}; for (const k of Object.keys(node)) { out[k] = applyMap(node[k], cache); } return out; } return node; } async function main() { const deRoot = JSON.parse(fs.readFileSync(DE_PATH, 'utf8')); const cache = loadCache(); const all = new Set(); collectStrings(deRoot, all); const unique = [...all].filter((s) => !shouldSkipTranslation(s)); const missing = unique.filter((s) => cache[s] === undefined); console.log(`Falukant: ${unique.length} übersetzbare Strings, ${missing.length} ohne Cache.`); let next = 0; let done = 0; async function worker() { for (;;) { const i = next++; if (i >= missing.length) return; const s = missing[i]; await translateOne(s, cache); done++; if (done % 40 === 0) { saveCache(cache); console.log(`… ${done}/${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); const out = applyMap(deRoot, cache); fs.writeFileSync(FR_PATH, `${JSON.stringify(out, null, 4)}\n`, 'utf8'); console.log('Geschrieben:', FR_PATH); } main().catch((e) => { console.error(e); process.exit(1); });