Files
reactShop/translate-i18n.js
sebseb7 4f5bc96c9b upd
2025-07-16 09:57:45 +02:00

522 lines
17 KiB
JavaScript
Executable File

#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import OpenAI from 'openai';
// Configuration
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const LOCALES_DIR = './src/i18n/locales';
const GERMAN_DIR = path.join(LOCALES_DIR, 'de');
const ENGLISH_DIR = path.join(LOCALES_DIR, 'en');
// Translation file groups
const TRANSLATION_FILES = [
'locale.js',
'navigation.js',
'auth.js',
'cart.js',
'product.js',
'search.js',
'sorting.js',
'chat.js',
'delivery.js',
'checkout.js',
'payment.js',
'filters.js',
'tax.js',
'footer.js',
'titles.js',
'sections.js',
'pages.js',
'orders.js',
'settings.js',
'common.js'
];
// Model configuration
const GERMAN_TO_ENGLISH_MODEL = 'gpt-4.1'; // High-quality model for German -> English (critical step)
const ENGLISH_TO_OTHER_MODEL = 'gpt-4.1-mini'; // Faster/cheaper model for English -> Other languages
// Supported languages for translation
const TARGET_LANGUAGES = {
'bg': 'Bulgarian',
'cs': 'Czech',
'es': 'Spanish',
'fr': 'French',
'el': 'Greek',
'hr': 'Croatian',
'hu': 'Hungarian',
'it': 'Italian',
'pl': 'Polish',
'ro': 'Romanian',
'ru': 'Russian',
'sk': 'Slovak',
'sl': 'Slovenian',
'sr': 'Serbian',
'sv': 'Swedish',
'tr': 'Turkish',
'uk': 'Ukrainian',
'ar': 'Arabic (Egyptian)',
'zh': 'Chinese (Simplified)'
};
// Initialize OpenAI client
const openai = new OpenAI({
apiKey: OPENAI_API_KEY,
});
// System prompt for German to English translation
const GERMAN_TO_ENGLISH_SYSTEM_PROMPT = `
You MUST translate German strings to English AND add the original German text as a comment after EVERY translated string.
CRITICAL REQUIREMENT: Every translated string must have the original German text as a comment.
Rules:
1. Translate all German strings to English
2. MANDATORY: Add the original German text as a comment after EVERY translated string using // format
3. Preserve all existing comments from the German version
4. Maintain the exact JavaScript object structure and formatting
5. Keep all interpolation variables like {{count}}, {{vat}}, etc. unchanged
6. Keep locale codes appropriate for English
7. For the locale section, use "en-US" as code
8. Do not translate technical terms that are already in English
9. Preserve any special formatting or HTML entities
10. Return a valid JavaScript object (not JSON) that can be exported
MANDATORY FORMAT for every string:
"englishTranslation": "English Translation", // Original German Text
Examples:
"login": "Login", // Anmelden
"email": "Email", // E-Mail
"password": "Password", // Passwort
"home": "Home", // Startseite
DO NOT output any string without its German comment. Every single translated string needs the German original as a comment.
`;
// System prompt template for English to other languages (file content will be inserted)
const ENGLISH_TO_OTHER_SYSTEM_PROMPT_TEMPLATE = `
Translate the English strings in the following file to {{targetLanguage}}, preserving the German comments.
Rules:
1. Translate only the English strings to {{targetLanguage}}
2. Drop the comments in output
3. Maintain the exact JavaScript object structure and formatting
4. Keep all interpolation variables like {{count}}, {{vat}}, etc. unchanged
5. Update locale code appropriately for the target language
6. Do not translate technical terms, API keys, or code-related strings
7. Preserve any special formatting or HTML entities
8. Return a valid JavaScript object (not JSON) that can be exported
Here is the English translation file to translate:
{{englishFileContent}}
`;
// Function to check if source file is newer than target file
function isSourceNewer(sourcePath, targetPath) {
try {
// If target doesn't exist, source is considered newer
if (!fs.existsSync(targetPath)) {
return true;
}
const sourceStats = fs.statSync(sourcePath);
const targetStats = fs.statSync(targetPath);
return sourceStats.mtime > targetStats.mtime;
} catch (error) {
console.error(`Error checking file timestamps for ${sourcePath} -> ${targetPath}:`, error.message);
return true; // Default to translating if we can't check
}
}
// Function to get files that need translation from German to English
function getFilesNeedingEnglishTranslation() {
const filesToTranslate = [];
for (const fileName of TRANSLATION_FILES) {
const germanFile = path.join(GERMAN_DIR, fileName);
const englishFile = path.join(ENGLISH_DIR, fileName);
if (!fs.existsSync(germanFile)) {
console.log(`⚠️ German file not found: ${fileName}`);
continue;
}
if (isSourceNewer(germanFile, englishFile)) {
filesToTranslate.push(fileName);
console.log(`📝 ${fileName} needs German → English translation`);
} else {
console.log(`⏭️ ${fileName} is up to date (German → English)`);
}
}
return filesToTranslate;
}
// Function to get files that need translation from English to target language
function getFilesNeedingTargetTranslation(langCode) {
const filesToTranslate = [];
const targetDir = path.join(LOCALES_DIR, langCode);
for (const fileName of TRANSLATION_FILES) {
const englishFile = path.join(ENGLISH_DIR, fileName);
const targetFile = path.join(targetDir, fileName);
if (!fs.existsSync(englishFile)) {
console.log(`⚠️ English file not found: ${fileName}`);
continue;
}
if (isSourceNewer(englishFile, targetFile)) {
filesToTranslate.push(fileName);
console.log(`📝 ${fileName} needs English → ${langCode} translation`);
} else {
console.log(`⏭️ ${fileName} is up to date (English → ${langCode})`);
}
}
return filesToTranslate;
}
// Function to read and parse JavaScript export file
function readTranslationFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
// Remove the export default and evaluate the object
const objectContent = content.replace(/^export default\s*/, '').replace(/;\s*$/, '');
return eval(`(${objectContent})`);
} catch (error) {
console.error(`Error reading ${filePath}:`, error.message);
return null;
}
}
// Function to write translation file (preserving comments as string)
function writeTranslationFile(filePath, translationString) {
try {
// Ensure directory exists
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Ensure the string has proper export format
const content = translationString.startsWith('export default')
? translationString
: `export default ${translationString}`;
// Ensure it ends with semicolon and newline
const finalContent = content.endsWith(';\n') ? content : content.replace(/;?\s*$/, ';\n');
fs.writeFileSync(filePath, finalContent, 'utf8');
console.log(`✅ Successfully wrote ${filePath}`);
} catch (error) {
console.error(`Error writing ${filePath}:`, error.message);
}
}
// Function to translate content using OpenAI (for German to English)
async function translateContent(content, systemPrompt, targetLanguage = null, model = 'gpt-4') {
try {
const prompt = targetLanguage
? systemPrompt.replace(/{{targetLanguage}}/g, targetLanguage)
: systemPrompt;
const response = await openai.chat.completions.create({
model: model,
messages: [
{ role: 'system', content: prompt },
{ role: 'user', content: `Please translate this translation file content:\n\n${content}` }
],
temperature: 0.1,
max_tokens: 4000
});
return response.choices[0].message.content;
} catch (error) {
console.error('OpenAI API error:', error.message);
throw error;
}
}
// Function to translate English to other languages (optimized for caching)
async function translateToTargetLanguage(englishContent, targetLanguage, model = 'gpt-4o-mini') {
try {
// Create system prompt with file content (cacheable)
const systemPrompt = ENGLISH_TO_OTHER_SYSTEM_PROMPT_TEMPLATE
.replace(/{{targetLanguage}}/g, targetLanguage)
.replace(/{{englishFileContent}}/g, englishContent);
const response = await openai.chat.completions.create({
model: model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: `Please translate to ${targetLanguage}` }
],
temperature: 0.1,
max_tokens: 4000
});
return response.choices[0].message.content;
} catch (error) {
console.error('OpenAI API error:', error.message);
throw error;
}
}
// Function to extract JavaScript object string from OpenAI response (preserving comments)
function extractJSObjectString(response) {
try {
// Remove code block markers if present
let cleaned = response.replace(/```javascript|```json|```/g, '').trim();
// Try to find the object in the response
const objectMatch = cleaned.match(/\{[\s\S]*\}/);
if (objectMatch) {
return objectMatch[0];
}
// If no object found, return the cleaned response
return cleaned;
} catch (error) {
console.error('Error parsing OpenAI response:', error.message);
console.log('Response was:', response);
return null;
}
}
// Main translation function for multiple files
async function translateToEnglish() {
console.log(`🔄 Step 1: Checking which files need German → English translation...`);
const filesToTranslate = getFilesNeedingEnglishTranslation();
if (filesToTranslate.length === 0) {
console.log('✅ All German → English translations are up to date');
return TRANSLATION_FILES.filter(fileName => fs.existsSync(path.join(ENGLISH_DIR, fileName)));
}
console.log(`🔄 Translating ${filesToTranslate.length} files from German to English using ${GERMAN_TO_ENGLISH_MODEL}...`);
const translatedFiles = [];
for (const fileName of filesToTranslate) {
const germanFile = path.join(GERMAN_DIR, fileName);
const englishFile = path.join(ENGLISH_DIR, fileName);
console.log(`🔄 Translating ${fileName}...`);
try {
// Read German translation file
const germanContent = fs.readFileSync(germanFile, 'utf8');
const translatedContent = await translateContent(germanContent, GERMAN_TO_ENGLISH_SYSTEM_PROMPT, null, GERMAN_TO_ENGLISH_MODEL);
const englishObjectString = extractJSObjectString(translatedContent);
if (englishObjectString) {
writeTranslationFile(englishFile, englishObjectString);
translatedFiles.push(fileName);
console.log(`${fileName} translated successfully`);
} else {
throw new Error(`Failed to parse English translation for ${fileName}`);
}
} catch (error) {
console.error(`❌ Error translating ${fileName}:`, error.message);
}
// Add delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 1000));
}
console.log(`✅ German to English translation completed for ${translatedFiles.length} files`);
// Return all English files that exist (both newly translated and existing)
return TRANSLATION_FILES.filter(fileName => fs.existsSync(path.join(ENGLISH_DIR, fileName)));
}
// Function to translate English to other languages (multiple files)
async function translateToOtherLanguages(availableEnglishFiles) {
console.log(`🔄 Step 2: Translating English to other languages using ${ENGLISH_TO_OTHER_MODEL}...`);
for (const [langCode, langName] of Object.entries(TARGET_LANGUAGES)) {
console.log(`🔄 Checking ${langName} (${langCode}) translations...`);
// Create target language directory if it doesn't exist
const targetDir = path.join(LOCALES_DIR, langCode);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const filesToTranslate = getFilesNeedingTargetTranslation(langCode);
if (filesToTranslate.length === 0) {
console.log(`✅ All English → ${langName} translations are up to date`);
continue;
}
console.log(`🔄 Translating ${filesToTranslate.length} files to ${langName}...`);
for (const fileName of filesToTranslate) {
try {
const englishFile = path.join(ENGLISH_DIR, fileName);
const targetFile = path.join(targetDir, fileName);
console.log(`🔄 Translating ${fileName} to ${langName}...`);
// Read English file
const englishContent = fs.readFileSync(englishFile, 'utf8');
const translatedContent = await translateToTargetLanguage(
englishContent,
langName,
ENGLISH_TO_OTHER_MODEL
);
const translatedObjectString = extractJSObjectString(translatedContent);
if (translatedObjectString) {
// Special handling for locale.js file
let updatedString = translatedObjectString;
if (fileName === 'locale.js') {
updatedString = translatedObjectString.replace(
/"code":\s*"[^"]*"/,
`"code": "${getLocaleCode(langCode)}"`
);
}
writeTranslationFile(targetFile, updatedString);
console.log(`${fileName} translated to ${langName}`);
} else {
console.error(`❌ Failed to parse ${fileName} translation for ${langName}`);
}
// Add delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
console.error(`❌ Error translating ${fileName} to ${langName}:`, error.message);
}
}
// Add longer delay between languages
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
// Helper function to get locale codes
function getLocaleCode(langCode) {
const localeCodes = {
'bg': 'bg-BG',
'cs': 'cs-CZ',
'es': 'es-ES',
'fr': 'fr-FR',
'el': 'el-GR',
'hr': 'hr-HR',
'hu': 'hu-HU',
'it': 'it-IT',
'pl': 'pl-PL',
'ro': 'ro-RO',
'ru': 'ru-RU',
'sk': 'sk-SK',
'sl': 'sl-SI',
'sr': 'sr-RS',
'sv': 'sv-SE',
'tr': 'tr-TR',
'uk': 'uk-UA',
'ar': 'ar-EG',
'zh': 'zh-CN'
};
return localeCodes[langCode] || `${langCode}-${langCode.toUpperCase()}`;
}
// Main execution
async function main() {
// Parse command line arguments
const args = process.argv.slice(2);
const skipEnglish = args.includes('--skip-english') || args.includes('-s');
const onlyEnglish = args.includes('--only-english') || args.includes('-e');
if (skipEnglish && onlyEnglish) {
console.error('❌ Cannot use both --skip-english and --only-english flags');
process.exit(1);
}
console.log('🚀 Starting translation process...');
if (skipEnglish) {
console.log('⏭️ Skipping German → English translation (using existing English file)');
} else if (onlyEnglish) {
console.log('🎯 Only translating German → English (skipping other languages)');
}
// Check if OpenAI API key is set (only if we're doing actual translation)
if (!skipEnglish && !OPENAI_API_KEY) {
console.error('❌ OPENAI_API_KEY environment variable is not set');
console.log('Please set your OpenAI API key: export OPENAI_API_KEY="your-api-key-here"');
process.exit(1);
}
// Check if German directory exists (only if we're translating from German)
if (!skipEnglish && !fs.existsSync(GERMAN_DIR)) {
console.error(`❌ German translation directory not found: ${GERMAN_DIR}`);
process.exit(1);
}
try {
let translatedFiles;
if (skipEnglish) {
// Skip German → English, read existing English files
if (!fs.existsSync(ENGLISH_DIR)) {
console.error(`❌ English translation directory not found: ${ENGLISH_DIR}`);
console.log('💡 Run without --skip-english first to generate the English files');
process.exit(1);
}
console.log('📖 Reading existing English translation files...');
translatedFiles = TRANSLATION_FILES.filter(fileName => {
const englishFile = path.join(ENGLISH_DIR, fileName);
return fs.existsSync(englishFile);
});
console.log(`✅ Found ${translatedFiles.length} English files`);
} else {
// Step 1: Translate German to English
translatedFiles = await translateToEnglish();
if (!translatedFiles || translatedFiles.length === 0) {
console.error('❌ Failed to create English translations, stopping process');
process.exit(1);
}
}
if (onlyEnglish) {
console.log('🎉 English translation completed! Skipping other languages.');
} else {
// Step 2: Translate English to other languages
await translateToOtherLanguages(translatedFiles);
console.log('🎉 All translations completed successfully!');
}
} catch (error) {
console.error('❌ Translation process failed:', error.message);
process.exit(1);
}
}
// Run the script
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export {
translateToEnglish,
translateToOtherLanguages,
readTranslationFile,
writeTranslationFile
};