101 lines
2.9 KiB
JavaScript
Executable File
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));
|