GDPR Compliance Update - Added complete section on privacy-first analytics Changes: - Updated Section 6 of privacy policy with detailed Umami Analytics information - Documented what data is collected (page views, referrers, browser, device, country) - Documented what is NOT collected (IP addresses, personal info, cookies, precise location) - Added Do Not Track (DNT) support documentation - Provided opt-out instructions (browser console method, DNT setting) - Explained cookie-free tracking and EU data storage - Updated last modified date to October 29, 2025 - Created DeepL translation script for privacy.json - Translated all new content to German (DE) and French (FR) Rationale: - GDPR requires disclosure of all data collection practices - Umami was deployed in previous session but privacy policy not updated - This is a mandatory compliance requirement before further work Testing: - Verified English HTML updates render correctly - Confirmed German translation quality (Analytik und Rückverfolgung) - Validated French translations via DeepL Pro API - All i18n keys properly mapped Files Modified: - public/privacy.html (Section 6 expanded from 13 to 84 lines) - public/locales/en/privacy.json (added comprehensive section_6 object) - public/locales/de/privacy.json (DeepL translated section_6) - public/locales/fr/privacy.json (DeepL translated section_6) - scripts/translate-privacy-deepl.js (new translation automation script) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
137 lines
4.1 KiB
JavaScript
Executable file
137 lines
4.1 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
|
|
*
|
|
* 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/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'
|
|
}).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) {
|
|
const response = JSON.parse(data);
|
|
resolve(response.translations[0].text);
|
|
} else {
|
|
reject(new Error(`DeepL API error: ${res.statusCode} - ${data}`));
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', reject);
|
|
req.write(postData);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// Recursively translate object
|
|
async function translateObject(obj, targetLang) {
|
|
const result = {};
|
|
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
if (Array.isArray(value)) {
|
|
result[key] = [];
|
|
for (const item of value) {
|
|
if (typeof item === 'string') {
|
|
console.log(` Translating array item: ${item.substring(0, 50)}...`);
|
|
result[key].push(await translateText(item, targetLang));
|
|
await new Promise(resolve => setTimeout(resolve, 200)); // Rate limit
|
|
} else {
|
|
result[key].push(item);
|
|
}
|
|
}
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
result[key] = await translateObject(value, targetLang);
|
|
} else if (typeof value === 'string') {
|
|
console.log(` Translating: ${key}`);
|
|
result[key] = await translateText(value, targetLang);
|
|
await new Promise(resolve => setTimeout(resolve, 200)); // Rate limit
|
|
} else {
|
|
result[key] = value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
console.log('🌍 Starting privacy.json translation with DeepL\n');
|
|
|
|
// Translate Section 6 only (the new Umami content)
|
|
console.log('📝 Translating Section 6 to German (DE)...');
|
|
deData.section_6 = await translateObject(enData.section_6, 'DE');
|
|
deData.header.last_updated = await translateText(enData.header.last_updated, 'DE');
|
|
|
|
console.log('\n📝 Translating Section 6 to French (FR)...');
|
|
frData.section_6 = await translateObject(enData.section_6, 'FR');
|
|
frData.header.last_updated = await translateText(enData.header.last_updated, 'FR');
|
|
|
|
// Save files
|
|
fs.writeFileSync(DE_FILE, JSON.stringify(deData, null, 2) + '\n', 'utf8');
|
|
fs.writeFileSync(FR_FILE, JSON.stringify(frData, null, 2) + '\n', 'utf8');
|
|
|
|
console.log('\n✅ Translation complete!');
|
|
console.log(` DE: ${DE_FILE}`);
|
|
console.log(` FR: ${FR_FILE}`);
|
|
|
|
} catch (error) {
|
|
console.error('\n❌ Translation failed:', error.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|