UNPKG

@akson/cortex-shopify-translations

Version:

Unified Shopify translations management client with product extraction, translation sync, and CLI tools

300 lines (241 loc) β€’ 8.24 kB
#!/usr/bin/env node import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import OpenAI from 'openai'; import chalk from 'chalk'; import dotenv from 'dotenv'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Load environment variables dotenv.config(); // Configuration const CONFIG = { batchSize: 10, targetLanguages: ['de', 'it', 'en'], sourceLanguage: 'fr' }; // Initialize OpenAI function initializeOpenAI() { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { console.error(chalk.red('❌ OpenAI API key not found.')); console.log(chalk.yellow('Please set OPENAI_API_KEY environment variable')); process.exit(1); } return new OpenAI({ apiKey }); } // Parse command line arguments function parseArgs() { const args = process.argv.slice(2); // First argument is the file (default: translations.json) let file = 'translations.json'; if (args.length > 0 && !args[0].startsWith('-')) { file = args[0]; args.shift(); } const options = { file, force: args.includes('--force'), verbose: args.includes('--verbose'), help: args.includes('--help') || args.includes('-h') }; return options; } // Show help function printHelp() { console.log(` πŸ€– AI Translation Tool for Shopify Usage: npx @akson/cortex-shopify-translations translate [file] [options] Arguments: file Translation file (default: translations.json) Options: --force Retranslate all items --verbose Show detailed progress -h, --help Show this help Examples: npx @akson/cortex-shopify-translations translate npx @akson/cortex-shopify-translations translate my-translations.json npx @akson/cortex-shopify-translations translate --force `); } // Initialize status fields for tracking function initializeStatusFields(data) { data.translations.forEach(item => { CONFIG.targetLanguages.forEach(lang => { const statusField = `${lang}_status`; if (!item[statusField]) { item[statusField] = 'pending'; } }); }); } // Load translation file function loadTranslationFile(filePath) { if (!fs.existsSync(filePath)) { console.error(chalk.red(`❌ File not found: ${filePath}`)); process.exit(1); } const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); initializeStatusFields(data); // Count progress let totalCompleted = 0; CONFIG.targetLanguages.forEach(lang => { const completed = data.translations.filter(item => item[`${lang}_status`] === 'completed' ).length; totalCompleted += completed; }); const totalPossible = data.translations.length * CONFIG.targetLanguages.length; const percentComplete = Math.round((totalCompleted / totalPossible) * 100); if (totalCompleted > 0) { console.log(chalk.yellow(`πŸ“„ Resuming ${path.basename(filePath)} (${percentComplete}% complete)`)); } else { console.log(chalk.blue(`πŸ“„ Starting ${path.basename(filePath)} (${data.translations.length} items)`)); } return data; } // Save progress function saveProgress(filePath, data) { data.metadata = data.metadata || {}; data.metadata.lastUpdated = new Date().toISOString(); fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); } // Check if translation needed function needsTranslation(item, lang, force) { if (force) return true; const fixedField = `${lang}_fixed`; const sourceField = 'fr_original'; // CHECK EMPTY FIELDS FIRST (higher priority than status) // If field is missing, empty, or matches original, it needs translation if (!item[fixedField] || item[fixedField] === '' || item[fixedField] === item[sourceField]) { return true; } // Then check status - only skip if it's completed/verified AND has content const status = item[`${lang}_status`]; if (status === 'completed' || status === 'verified') { return false; } // Default: needs translation return true; } // Translate batch with OpenAI async function translateBatch(openai, items, targetLang) { const langNames = { de: 'Swiss German', it: 'Swiss Italian', en: 'English' }; const prompt = `Translate for MyArmy.ch (Swiss military merchandise). Rules: Use "badge" not "Γ©cusson", formal tone, preserve {{variables}}. To ${langNames[targetLang]}: ${items.map((item, i) => `${i + 1}. "${item.fr_original}"`).join('\n')} Return JSON array: [{"translation": "..."}, ...]`; try { const response = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [ { role: 'system', content: 'You are a translator for Swiss military e-commerce.' }, { role: 'user', content: prompt } ], temperature: 0.3, response_format: { type: "json_object" } }); const result = JSON.parse(response.choices[0].message.content); const translations = Array.isArray(result) ? result : result.translations; return items.map((item, i) => ({ ...item, translation: translations[i]?.translation || item.fr_original })); } catch (error) { console.error(chalk.red(`Translation error: ${error.message}`)); return items.map(item => ({ ...item, translation: item.fr_original })); } } // Process all translations async function processTranslations(filePath, options) { const openai = initializeOpenAI(); const data = loadTranslationFile(filePath); for (const lang of CONFIG.targetLanguages) { console.log(chalk.cyan(`\n🌍 Processing ${lang.toUpperCase()}...`)); // Find items needing translation const itemsToTranslate = data.translations.filter(item => needsTranslation(item, lang, options.force) ); if (itemsToTranslate.length === 0) { console.log(chalk.green(`βœ“ All ${lang.toUpperCase()} translations complete`)); continue; } console.log(chalk.yellow(`πŸ“ Translating ${itemsToTranslate.length} items...`)); // Process in batches for (let i = 0; i < itemsToTranslate.length; i += CONFIG.batchSize) { const batch = itemsToTranslate.slice(i, i + CONFIG.batchSize); if (options.verbose) { console.log(chalk.gray(` Batch ${Math.floor(i/CONFIG.batchSize) + 1}/${Math.ceil(itemsToTranslate.length/CONFIG.batchSize)}`)); } else if (i % 50 === 0) { process.stdout.write('.'); } const translated = await translateBatch(openai, batch, lang); // Update translations translated.forEach(item => { const index = data.translations.findIndex(t => t.key === item.key); if (index !== -1) { data.translations[index][`${lang}_fixed`] = item.translation; data.translations[index][`${lang}_status`] = 'completed'; } }); // Save after each batch saveProgress(filePath, data); // Rate limiting await new Promise(resolve => setTimeout(resolve, 500)); } if (!options.verbose) console.log(''); } console.log(chalk.green('\nβœ… Translation complete!')); // Summary const stats = {}; CONFIG.targetLanguages.forEach(lang => { stats[lang] = data.translations.filter(item => item[`${lang}_status`] === 'completed' ).length; }); console.log(chalk.blue('\nπŸ“Š Summary:')); Object.entries(stats).forEach(([lang, count]) => { console.log(` ${lang.toUpperCase()}: ${count}/${data.translations.length} translated`); }); } // Main async function main() { const options = parseArgs(); if (options.help) { printHelp(); process.exit(0); } const filePath = path.resolve(process.cwd(), options.file); console.log(chalk.blue('πŸ€– Cortex Shopify Translator')); console.log(chalk.gray('Powered by OpenAI GPT-4o-mini\n')); try { await processTranslations(filePath, options); process.exit(0); } catch (error) { console.error(chalk.red(`\n❌ Error: ${error.message}`)); if (options.verbose) { console.error(error.stack); } process.exit(1); } } // Run if executed directly if (import.meta.url === `file://${process.argv[1]}`) { main().catch(console.error); } export { processTranslations, initializeOpenAI };