/ scripts / retranslate-broken.js
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.');