UNPKG

@akson/cortex-shopify-translations

Version:

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

228 lines (191 loc) • 8 kB
/** * Publish corrected translations back to Shopify * Supports multiple resource types */ import fs from 'fs/promises'; import { config } from 'dotenv'; import { createGraphQLClient } from './dist/index.js'; // Load environment variables config({ path: '.env.local' }); // Parse command line arguments const args = process.argv.slice(2); const resourceType = args.find(arg => !arg.startsWith('-'))?.toUpperCase() || 'ONLINE_STORE_THEME'; const forceMode = args.includes('--force'); const showHelp = args.includes('--help') || args.includes('-h'); if (showHelp) { console.log(`\n🧠 Shopify Translation Publisher\n`); console.log(`Usage: node publish-translations.mjs [RESOURCE_TYPE] [OPTIONS]\n`); console.log(`Options:`); console.log(` --force Skip confirmation prompt`); console.log(` --help, -h Show this help\n`); console.log(`Examples:`); console.log(` node publish-translations.mjs --force # Publish theme translations`); console.log(` node publish-translations.mjs product --force # Publish product translations`); process.exit(0); } async function publishCorrections() { console.log('🧠 Publishing corrected translations to Shopify...'); try { const client = createGraphQLClient(); const targetLanguages = client.targetLanguages; const sourceLanguage = client.sourceLanguage; // Determine input file const filePrefix = resourceType === 'ONLINE_STORE_THEME' ? 'translations' : `${resourceType.toLowerCase()}-translations`; const jsonFile = `${filePrefix}-to-edit.json`; // Read JSON format let correctedTranslations = []; let metadata = {}; try { console.log(`šŸ“„ Loading translations from ${jsonFile}...`); const jsonContent = await fs.readFile(jsonFile, 'utf-8'); const jsonData = JSON.parse(jsonContent); correctedTranslations = jsonData.translations; metadata = jsonData.metadata; console.log(`āœ… Loaded ${correctedTranslations.length} translations from ${jsonFile}`); console.log(`šŸ“Š Resource Type: ${metadata.resourceType}`); } catch (error) { throw new Error(`Could not find ${jsonFile} file. Run extract first.`); } // Find translations that have been changed const changedTranslations = []; console.log('šŸ” Identifying changed translations...'); for (const translation of correctedTranslations) { const changes = []; // Check changes for all target languages dynamically targetLanguages.forEach(locale => { const currentField = `${locale}_current`; const fixedField = `${locale}_fixed`; if (translation[currentField] !== translation[fixedField] && translation[fixedField]?.trim()) { changes.push({ locale, key: translation.key, oldValue: translation[currentField], newValue: translation[fixedField], digest: translation.digest }); } }); if (changes.length > 0) { const sourceOriginalField = `${sourceLanguage}_original`; changedTranslations.push({ key: translation.key, sourceOriginal: translation[sourceOriginalField], resourceId: translation.resourceId, changes: changes.map(change => ({ ...change, resourceId: translation.resourceId })) }); } } console.log(`šŸ“ Found ${changedTranslations.length} translations with changes`); if (changedTranslations.length === 0) { console.log('āœ… No changes detected. Nothing to publish.'); return; } // Show preview of changes console.log('\nšŸ” Preview of changes to be published:'); changedTranslations.slice(0, 5).forEach((item, i) => { console.log(`\n${i + 1}. Key: ...${item.key.split('.').slice(-2).join('.')}`); console.log(` ${getFlagEmoji(sourceLanguage)} ${sourceLanguage.toUpperCase()} Original: "${item.sourceOriginal}"`); item.changes.forEach(change => { console.log(` ${getFlagEmoji(change.locale)} ${change.locale.toUpperCase()}: "${change.oldValue}" → "${change.newValue}"`); }); }); if (changedTranslations.length > 5) { console.log(`\n ... and ${changedTranslations.length - 5} more changes`); } // Ask for confirmation if not in --force mode if (!forceMode) { console.log('\nā“ Do you want to proceed with these changes?'); console.log(` Add --force flag to skip this confirmation: node publish-translations.mjs ${resourceType.toLowerCase()} --force`); console.log(' Press Ctrl+C to cancel, or remove this check and run again to proceed.'); return; } // Apply changes in batches let successCount = 0; let errorCount = 0; const errors = []; const processedResources = new Set(); console.log('\nšŸš€ Publishing changes...'); for (let i = 0; i < changedTranslations.length; i++) { const item = changedTranslations[i]; console.log(`\nšŸ“¤ [${i + 1}/${changedTranslations.length}] Processing: ...${item.key.split('.').slice(-2).join('.')}`); for (const change of item.changes) { try { const translations = [{ resourceId: change.resourceId, translations: [{ key: change.key, value: change.newValue, locale: change.locale, translatableContentDigest: change.digest }] }]; await client.registerTranslations(translations); processedResources.add(change.resourceId); console.log(` āœ… ${change.locale.toUpperCase()}: Updated successfully`); successCount++; // Rate limiting delay await new Promise(resolve => setTimeout(resolve, 500)); } catch (error) { console.log(` āŒ ${change.locale.toUpperCase()}: Failed - ${error.message}`); errorCount++; errors.push({ key: change.key, locale: change.locale, resourceId: change.resourceId, error: error.message, attemptedValue: change.newValue }); } } } // Generate report const report = { publishedAt: new Date().toISOString(), resourceType: metadata.resourceType, resourcesProcessed: Array.from(processedResources), totalChanges: changedTranslations.reduce((sum, item) => sum + item.changes.length, 0), successful: successCount, failed: errorCount, errors: errors, summary: { translationsProcessed: changedTranslations.length, resourceCount: processedResources.size, successRate: Math.round((successCount / (successCount + errorCount)) * 100) } }; const reportFile = `${filePrefix}-publish-report.json`; await fs.writeFile(reportFile, JSON.stringify(report, null, 2), 'utf-8'); console.log('\nšŸŽ‰ Publishing completed!'); console.log(`šŸ“Š Resource Type: ${metadata.resourceType}`); console.log(`šŸ“Š Resources Processed: ${processedResources.size}`); console.log(`āœ… Successful updates: ${successCount}`); console.log(`āŒ Failed updates: ${errorCount}`); console.log(`šŸ“Š Success rate: ${report.summary.successRate}%`); console.log(`šŸ“‹ Detailed report saved: ${reportFile}`); if (errors.length > 0) { console.log('\nāš ļø Errors encountered:'); errors.slice(0, 3).forEach(error => { console.log(` - ${error.key} (${error.locale}): ${error.error}`); }); } } catch (error) { console.error('āŒ Publishing failed:', error.message); console.error(error.stack); } } function getFlagEmoji(locale) { const flags = { 'de': 'šŸ‡©šŸ‡Ŗ', 'it': 'šŸ‡®šŸ‡¹', 'en': 'šŸ‡¬šŸ‡§', 'fr': 'šŸ‡«šŸ‡·', 'es': 'šŸ‡ŖšŸ‡ø', 'pt': 'šŸ‡µšŸ‡¹', 'nl': 'šŸ‡³šŸ‡±' }; return flags[locale] || 'šŸ³ļø'; } publishCorrections();