import { readFile, writeFile } from 'node:fs/promises' const source = JSON.parse(await readFile('src/i18n/locales/zh-CN.json', 'utf8')) const localeConfigs = { 'zh-TW': { title: 'Spine粒子編輯器-Yeomai' }, ja: { title: 'Spineパーティクルエディター-Yeomai' }, ko: { title: 'Spine 파티클 에디터-Yeomai' }, fr: { title: 'Spine Particle Editor-Yeomai' }, de: { title: 'Spine Particle Editor-Yeomai' }, 'pt-BR': { title: 'Spine Particle Editor-Yeomai' }, es: { title: 'Spine Particle Editor-Yeomai' }, } const nativeLanguageLabels = { zhCN: '中文', zhTW: '繁體中文', en: 'English', ja: '日本語', ko: '한국어', fr: 'Français', de: 'Deutsch', ptBR: 'Português', es: 'Español', } const protectedTerms = [ 'SpineParticle', 'Spine', 'Yeomai', 'JSON', 'FPS', 'ZIP', 'PNG', 'JPEG', 'WebP', 'WebGL', 'Bézier', 'Bezier', 'Mesh', 'Atlas', 'Ctrl', 'macOS', 'Windows', 'Universal', 'A/B', 'AB', ] function flatten(value, path = '', result = []) { for (const [key, child] of Object.entries(value)) { const nextPath = path ? `${path}.${key}` : key if (child && typeof child === 'object' && !Array.isArray(child)) flatten(child, nextPath, result) else result.push({ path: nextPath, value: child }) } return result } function setPath(target, path, value) { const parts = path.split('.') let current = target for (let index = 0; index < parts.length - 1; index++) { current = current[parts[index]] ||= {} } current[parts.at(-1)] = value } function protectText(text) { const values = [] const escapedTerms = protectedTerms .sort((a, b) => b.length - a.length) .map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) const pattern = new RegExp(`\\{[a-zA-Z0-9_]+\\}|\\b(?:${escapedTerms.join('|')})\\b`, 'g') const protectedText = String(text).replace(pattern, (match) => { const token = `ZXQV${values.length}QXZ` values.push(match) return token }) return { protectedText, values } } function restoreText(text, values) { return String(text).replace(/ZXQV(\d+)QXZ/g, (_match, index) => values[Number(index)] ?? '') } async function translateBatch(texts, target) { const params = new URLSearchParams({ client: 'dict-chrome-ex', sl: 'zh-CN', tl: target, }) for (const text of texts) params.append('q', text) const response = await fetch(`https://clients5.google.com/translate_a/t?${params}`, { signal: AbortSignal.timeout(30000), }) if (!response.ok) throw new Error(`Translation HTTP ${response.status}`) const body = await response.json() if (!Array.isArray(body)) throw new Error('Unexpected translation response') if (body.length !== texts.length) { throw new Error(`Translation count mismatch: expected ${texts.length}, got ${body.length}`) } return body.map((value) => typeof value === 'string' ? value : JSON.stringify(value)) } async function translateLocale(code, config) { const entries = flatten(source).filter(({ path }) => !path.startsWith('language.') && path !== 'app.title') const translated = {} const batches = [] let batch = [] let encodedLength = 0 for (const entry of entries) { const protectedEntry = protectText(entry.value) const item = { ...entry, ...protectedEntry } const itemLength = encodeURIComponent(item.protectedText).length + 3 if (batch.length && encodedLength + itemLength > 7000) { batches.push(batch) batch = [] encodedLength = 0 } batch.push(item) encodedLength += itemLength } if (batch.length) batches.push(batch) for (let index = 0; index < batches.length; index++) { const current = batches[index] let values for (let attempt = 0; attempt < 4; attempt++) { try { values = await translateBatch(current.map((entry) => entry.protectedText), code) break } catch (error) { if (attempt === 3) throw error await new Promise((resolve) => setTimeout(resolve, 1800 * (attempt + 1))) } } current.forEach((entry, itemIndex) => { setPath(translated, entry.path, restoreText(values[itemIndex], entry.values)) }) process.stdout.write(`${code}: ${index + 1}/${batches.length}\n`) await new Promise((resolve) => setTimeout(resolve, 500)) } translated.app.title = config.title translated.language = nativeLanguageLabels await writeFile(`src/i18n/locales/${code}.json`, `${JSON.stringify(translated, null, 2)}\n`) } for (const [code, config] of Object.entries(localeConfigs)) { await translateLocale(code, config) } console.log('Generated locale drafts. Review and override key terminology before release.')