TheNoiseClock/scripts/fill-missing-translations.mjs

101 lines
2.9 KiB
JavaScript
Executable File

#!/usr/bin/env node
import fs from 'fs/promises';
const filePath = process.argv[2] ?? 'TheNoiseClock/Localizable.xcstrings';
const dryRun = process.argv.includes('--dry-run');
const raw = await fs.readFile(filePath, 'utf8');
const catalog = JSON.parse(raw);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const cache = new Map();
async function translate(text, targetLang) {
const key = `${targetLang}::${text}`;
if (cache.has(key)) return cache.get(key);
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${encodeURIComponent(targetLang)}&dt=t&q=${encodeURIComponent(text)}`;
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
const res = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json, text/plain, */*'
}
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = await res.json();
const translated = Array.isArray(body?.[0])
? body[0].map((part) => part?.[0] ?? '').join('')
: '';
if (!translated || translated.trim().length === 0) {
throw new Error('Empty translation payload');
}
cache.set(key, translated);
await sleep(80);
return translated;
} catch (error) {
if (attempt === 3) throw error;
await sleep(300 * attempt);
}
}
return text;
}
let totalUpdated = 0;
let esUpdated = 0;
let frUpdated = 0;
for (const [k, entry] of Object.entries(catalog.strings ?? {})) {
const localizations = entry.localizations ?? {};
const en = localizations.en?.stringUnit?.value ?? '';
if (!en) continue;
const es = localizations['es-MX']?.stringUnit?.value ?? '';
const fr = localizations['fr-CA']?.stringUnit?.value ?? '';
let touched = false;
if (!es || es === en) {
const translated = await translate(en, 'es');
entry.localizations ??= {};
entry.localizations['es-MX'] ??= { stringUnit: { state: 'translated', value: '' } };
entry.localizations['es-MX'].stringUnit ??= { state: 'translated', value: '' };
entry.localizations['es-MX'].stringUnit.state = 'translated';
entry.localizations['es-MX'].stringUnit.value = translated;
esUpdated += 1;
touched = true;
}
if (!fr || fr === en) {
const translated = await translate(en, 'fr');
entry.localizations ??= {};
entry.localizations['fr-CA'] ??= { stringUnit: { state: 'translated', value: '' } };
entry.localizations['fr-CA'].stringUnit ??= { state: 'translated', value: '' };
entry.localizations['fr-CA'].stringUnit.state = 'translated';
entry.localizations['fr-CA'].stringUnit.value = translated;
frUpdated += 1;
touched = true;
}
if (touched) totalUpdated += 1;
}
if (!dryRun) {
await fs.writeFile(filePath, `${JSON.stringify(catalog, null, 2)}\n`, 'utf8');
}
console.log(JSON.stringify({
filePath,
totalKeysTouched: totalUpdated,
esUpdated,
frUpdated,
dryRun
}, null, 2));