#!/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));