435 lines
14 KiB
JavaScript
Executable File
435 lines
14 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 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: Translating German to English using ${GERMAN_TO_ENGLISH_MODEL}...`);
|
|
|
|
const translatedFiles = [];
|
|
|
|
for (const fileName of TRANSLATION_FILES) {
|
|
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 translatedFiles;
|
|
}
|
|
|
|
// Function to translate English to other languages (multiple files)
|
|
async function translateToOtherLanguages(translatedFiles) {
|
|
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(`🔄 Translating to ${langName} (${langCode})...`);
|
|
|
|
// 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 });
|
|
}
|
|
|
|
for (const fileName of TRANSLATION_FILES) {
|
|
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
|
|
};
|