feat(i18n): add French language support and enhance localization
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Introduced French as a supported language across the application, updating locale files and adding translations for various components. - Enhanced language handling logic to accommodate French, ensuring proper detection and fallback mechanisms. - Updated UI elements to include French language options, improving accessibility for French-speaking users. - Refactored SEO handling to include French in hreflang links, enhancing search engine indexing for multilingual content. - Added new scripts for managing French translations and ensuring consistency across language files.
This commit is contained in:
137
frontend/scripts/smooth-fr-falukant.mjs
Normal file
137
frontend/scripts/smooth-fr-falukant.mjs
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user