retranslate-broken.js
1 #!/usr/bin/env node 2 /** 3 * Re-translate specific broken templates using the improved prompt. 4 * Reads AU source → translates via Claude → writes to target file. 5 */ 6 7 import { readFileSync, writeFileSync } from 'fs'; 8 import { join, dirname } from 'path'; 9 import { fileURLToPath } from 'url'; 10 import dotenv from 'dotenv'; 11 12 dotenv.config(); 13 const __dirname = dirname(fileURLToPath(import.meta.url)); 14 const projectRoot = join(__dirname, '..'); 15 16 const { OPENROUTER_API_KEY } = process.env; 17 const MODEL = 'anthropic/claude-sonnet-4-6'; 18 19 async function callClaude(prompt) { 20 const res = await fetch('https://openrouter.ai/api/v1/chat/completions', { 21 method: 'POST', 22 headers: { 23 Authorization: `Bearer ${OPENROUTER_API_KEY}`, 24 'Content-Type': 'application/json', 25 }, 26 body: JSON.stringify({ 27 model: MODEL, 28 messages: [{ role: 'user', content: prompt }], 29 temperature: 0.1, 30 max_tokens: 2048, 31 }), 32 }); 33 const json = await res.json(); 34 return json.choices?.[0]?.message?.content?.trim() || ''; 35 } 36 37 function braceNet(text) { 38 let n = 0; 39 for (const ch of text) { 40 if (ch === '{') n++; 41 else if (ch === '}') n--; 42 } 43 return n; 44 } 45 46 const LANG_NAMES = { 47 hi: 'Hindi', 48 de: 'German', 49 fr: 'French', 50 it: 'Italian', 51 ja: 'Japanese', 52 ko: 'Korean', 53 es: 'Spanish (Latin American)', 54 nl: 'Dutch', 55 pl: 'Polish', 56 sv: 'Swedish', 57 da: 'Danish', 58 id: 'Indonesian', 59 }; 60 61 const GREETINGS = { 62 hi: 'नमस्ते', 63 de: '{Hallo|Guten Tag}', 64 fr: '{Bonjour|Salut}', 65 it: '{Ciao|Buongiorno}', 66 ja: 'こんにちは', 67 ko: '안녕하세요', 68 es: 'Hola', 69 nl: '{Hallo|Goedendag}', 70 pl: '{Cześć|Dzień dobry}', 71 sv: 'Hej', 72 da: 'Hej', 73 id: 'Halo', 74 }; 75 76 // Templates to re-translate: [auId, lang, country, targetDir] 77 const BROKEN = [ 78 ['email_003_au', 'hi', 'IN', 'IN/hi'], 79 ['email_005_au', 'hi', 'IN', 'IN/hi'], 80 ['email_dad_02_au', 'it', 'IT', 'IT/it'], 81 ['email_dad_04_au', 'it', 'IT', 'IT/it'], 82 ['email_005_au', 'it', 'IT', 'IT/it'], 83 ['email_dad_01_au', 'ja', 'JP', 'JP/ja'], 84 ['email_dad_02_au', 'es', 'MX', 'MX/es'], 85 ['email_dad_04_au', 'nl', 'NL', 'NL/nl'], 86 ]; 87 88 const auEmail = JSON.parse( 89 readFileSync(join(projectRoot, 'data/templates/AU/email.json'), 'utf-8') 90 ); 91 92 for (const [auId, lang, country, targetDir] of BROKEN) { 93 const auTemplate = auEmail.templates.find(t => t.id === auId); 94 if (!auTemplate) { 95 console.log('Not found in AU:', auId); 96 continue; 97 } 98 99 const targetFile = join(projectRoot, 'data/templates', targetDir, 'email.json'); 100 const targetData = JSON.parse(readFileSync(targetFile, 'utf-8')); 101 const targetId = auId.replace('_au', `_${country.toLowerCase()}`); 102 const targetTemplate = targetData.templates.find(t => t.id === targetId); 103 if (!targetTemplate) { 104 console.log('Not found in target:', targetId); 105 continue; 106 } 107 108 const langName = LANG_NAMES[lang]; 109 const greeting = GREETINGS[lang] || ''; 110 const formality = 111 lang === 'hi' 112 ? 'formal आप register' 113 : lang === 'ja' 114 ? 'polite です/ます form' 115 : lang === 'ko' 116 ? '합쇼체 (formal polite)' 117 : lang === 'de' || lang === 'nl' 118 ? 'informal du/jij register (NOT formal Sie/u)' 119 : 'polite but informal register'; 120 121 console.log(`Re-translating ${auId} → ${targetDir}...`); 122 123 const prompt = `Translate this marketing email spintax template from English to ${langName}. 124 125 CRITICAL RULES: 126 1. Preserve the {option1|option2} spintax structure EXACTLY — same number of options, same positions 127 2. Keep ALL [placeholder] tokens unchanged: [firstname], [business_name], [domain], [grade], [score], [primary_weakness], [secondary_weakness], [impact], [keyword], [industry] 128 3. Keep \\n\\n paragraph breaks unchanged 129 4. The word STOP must remain in Latin characters — translate surrounding text only 130 5. Use ${formality} 131 6. Greeting should use "${greeting}" (replace G'day and Hi/Hello/Hey greetings with this; if localGreeting itself contains {|} spintax, keep it as-is) 132 7. Adapt marketing idioms naturally — don't translate literally if a natural ${langName} equivalent exists 133 8. Return ONLY the translated spintax, no explanation, no self-corrections, no "let me redo" statements 134 9. NEVER add opt-out, unsubscribe, or STOP instructions — email unsubscribe is handled separately 135 10. BRACE BALANCE: Count { and } in your input. Your output MUST have the EXACT SAME COUNT. The pattern {{greeting} [firstname|there]{,|!}\\n\\n|} is an outer optional-greeting wrapper — your output must start with { when input starts with {. 136 137 English body_spintax: 138 ${auTemplate.body_spintax} 139 140 ${langName} translation:`; 141 142 try { 143 const translated = await callClaude(prompt); 144 const net = braceNet(translated); 145 if (net !== 0) { 146 console.log(` WARNING: ${targetId} still unbalanced (net ${net > 0 ? '+' : ''}${net})`); 147 } else { 148 console.log(` ✓ ${targetId} balanced OK`); 149 } 150 targetTemplate.body_spintax = translated; 151 } catch (err) { 152 console.log(` ERROR: ${err.message}`); 153 continue; 154 } 155 156 writeFileSync(targetFile, `${JSON.stringify(targetData, null, 2)}\n`); 157 console.log(` Wrote ${targetFile}`); 158 } 159 160 console.log('\nDone. Run check-all-brackets.cjs to verify.');