@akson/cortex-shopify-translations
Version:
Unified Shopify translations management client with product extraction, translation sync, and CLI tools
228 lines (191 loc) ⢠8 kB
JavaScript
/**
* 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();