@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
JavaScript
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 };