tractatus/scripts/translate-researcher-deepl.js
TheFlow 5e7b3ef21f feat(i18n): add complete internationalization for researcher page
Implemented full translation infrastructure for researcher.html:
- Added 148 data-i18n attributes across all content sections
- Created 142 translation keys in nested JSON structure
- Translated all keys to German (DE) and French (FR) via DeepL Pro API
- Zero translation errors, all keys validated across 3 languages

Content translated includes:
- Research Context & Scope (4 major paragraphs)
- Theoretical Foundations (Organizational Theory + Values Pluralism accordions)
- Empirical Observations (3 documented failure modes with labels)
- Six-Component Architecture (all services with descriptions)
- Interactive Demonstrations, Resources, Bibliography, Limitations

New scripts:
- translate-researcher-deepl.js: Automated DeepL translation with rate limiting
- validate-researcher-i18n.js: i18n completeness validation tool

Translation quality verified with sample checks. Page ready for multilingual deployment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 00:18:45 +13:00

187 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Translate researcher.json from EN to DE and FR using DeepL API
*
* Usage: node scripts/translate-researcher-deepl.js
*
* Requires: DEEPL_API_KEY environment variable
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const DEEPL_API_KEY = process.env.DEEPL_API_KEY;
const API_URL = 'api.deepl.com'; // Pro API endpoint
if (!DEEPL_API_KEY) {
console.error('❌ ERROR: DEEPL_API_KEY environment variable not set');
console.error(' Set it with: export DEEPL_API_KEY="your-key-here"');
process.exit(1);
}
const EN_FILE = path.join(__dirname, '../public/locales/en/researcher.json');
const DE_FILE = path.join(__dirname, '../public/locales/de/researcher.json');
const FR_FILE = path.join(__dirname, '../public/locales/fr/researcher.json');
// Load JSON files
const enData = JSON.parse(fs.readFileSync(EN_FILE, 'utf8'));
const deData = JSON.parse(fs.readFileSync(DE_FILE, 'utf8'));
const frData = JSON.parse(fs.readFileSync(FR_FILE, 'utf8'));
// DeepL API request function
function translateText(text, targetLang) {
return new Promise((resolve, reject) => {
const postData = new URLSearchParams({
auth_key: DEEPL_API_KEY,
text: text,
target_lang: targetLang,
source_lang: 'EN',
formality: 'default',
preserve_formatting: '1'
}).toString();
const options = {
hostname: API_URL,
port: 443,
path: '/v2/translate',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
if (res.statusCode === 200) {
try {
const response = JSON.parse(data);
resolve(response.translations[0].text);
} catch (err) {
reject(new Error(`Failed to parse response: ${err.message}`));
}
} else {
reject(new Error(`DeepL API error: ${res.statusCode} - ${data}`));
}
});
});
req.on('error', reject);
req.write(postData);
req.end();
});
}
// Helper to get nested value
function getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
// Helper to set nested value
function setNestedValue(obj, path, value) {
const keys = path.split('.');
const lastKey = keys.pop();
const target = keys.reduce((current, key) => {
if (!current[key]) current[key] = {};
return current[key];
}, obj);
target[lastKey] = value;
}
// Recursively find all string values and their paths
function findAllStrings(obj, prefix = '') {
const strings = [];
for (const [key, value] of Object.entries(obj)) {
const currentPath = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'string') {
strings.push(currentPath);
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
strings.push(...findAllStrings(value, currentPath));
}
}
return strings;
}
// Main translation function
async function translateFile(targetLang, targetData, targetFile) {
console.log(`\n🌐 Translating to ${targetLang}...`);
const allPaths = findAllStrings(enData);
let translatedCount = 0;
let skippedCount = 0;
let errorCount = 0;
for (const keyPath of allPaths) {
const enValue = getNestedValue(enData, keyPath);
const existingValue = getNestedValue(targetData, keyPath);
// Skip if already translated (not empty)
if (existingValue && existingValue.trim().length > 0) {
skippedCount++;
process.stdout.write('.');
continue;
}
try {
// Translate
const translated = await translateText(enValue, targetLang);
setNestedValue(targetData, keyPath, translated);
translatedCount++;
process.stdout.write('✓');
// Rate limiting: wait 500ms between requests to avoid 429 errors
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
console.error(`\n❌ Error translating ${keyPath}:`, error.message);
errorCount++;
process.stdout.write('✗');
}
}
console.log(`\n\n📊 Translation Summary for ${targetLang}:`);
console.log(` ✓ Translated: ${translatedCount}`);
console.log(` . Skipped (already exists): ${skippedCount}`);
console.log(` ✗ Errors: ${errorCount}`);
// Save updated file
fs.writeFileSync(targetFile, JSON.stringify(targetData, null, 2) + '\n', 'utf8');
console.log(` 💾 Saved: ${targetFile}`);
}
// Run translations
async function main() {
console.log('═══════════════════════════════════════════════════════════');
console.log(' DeepL Translation: researcher.json (EN → DE, FR)');
console.log('═══════════════════════════════════════════════════════════\n');
const totalStrings = findAllStrings(enData).length;
console.log(`📝 Total translation keys in EN file: ${totalStrings}`);
try {
// Translate to German
await translateFile('DE', deData, DE_FILE);
// Translate to French
await translateFile('FR', frData, FR_FILE);
console.log('\n✅ Translation complete!');
console.log('\n💡 Next steps:');
console.log(' 1. Review translations in de/researcher.json and fr/researcher.json');
console.log(' 2. Test on local server: npm start');
console.log(' 3. Visit http://localhost:9000/researcher.html and switch languages');
} catch (error) {
console.error('\n❌ Fatal error:', error);
process.exit(1);
}
}
main();