#!/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 };