CRITICAL FIX: Section 9 incorrectly stated data "may be transferred to and processed in New Zealand" - this is factually incorrect. Data is hosted in the EU (OVHCloud France, MongoDB Atlas Frankfurt) and NEVER transferred to NZ. Changes: - Section 9: Complete rewrite for accuracy * Clarified: NZ administration vs. EU hosting * Added explicit hosting providers and regions * Confirmed NO data transfer to New Zealand * Expanded GDPR compliance details (Articles 5, 6, 15-22, 25, 32) - Section 4: Updated retention periods to match GDPR page (7 years donations, 14 months analytics anonymization) - Section 7: Enhanced security specifications (TLS 1.3, AES-256, bcrypt) - Section 3: Clarified MongoDB hosting is in EU Translations: - German (DE): 99/99 professional translations via DeepL ✓ - French (FR): 99/99 professional translations via DeepL ✓ This correction strengthens GDPR compliance messaging and provides accurate transparency about data residency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
205 lines
6.3 KiB
JavaScript
Executable file
205 lines
6.3 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Translate privacy.json from EN to DE and FR using DeepL API
|
|
*
|
|
* Usage: node scripts/translate-privacy-deepl.js [--force]
|
|
*
|
|
* Options:
|
|
* --force Overwrite existing translations
|
|
*
|
|
* Requires: DEEPL_API_KEY environment variable
|
|
*/
|
|
|
|
require('dotenv').config();
|
|
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
|
|
|
|
const FORCE = process.argv.includes('--force');
|
|
|
|
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/privacy.json');
|
|
const DE_FILE = path.join(__dirname, '../public/locales/de/privacy.json');
|
|
const FR_FILE = path.join(__dirname, '../public/locales/fr/privacy.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',
|
|
tag_handling: 'html' // Preserve HTML tags
|
|
}).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));
|
|
} else if (Array.isArray(value)) {
|
|
// Handle arrays of strings
|
|
value.forEach((item, index) => {
|
|
if (typeof item === 'string') {
|
|
strings.push(`${currentPath}.${index}`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
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) unless --force flag
|
|
if (!FORCE && existingValue && existingValue.trim().length > 0 && existingValue !== enValue) {
|
|
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: privacy.json (EN → DE, FR)');
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
if (FORCE) {
|
|
console.log('⚠️ --force flag enabled: Will overwrite existing translations\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/privacy.json and fr/privacy.json');
|
|
console.log(' 2. Test on local server: npm start');
|
|
console.log(' 3. Visit http://localhost:9000/privacy.html and switch languages');
|
|
|
|
} catch (error) {
|
|
console.error('\n❌ Fatal error:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|