UNPKG

@akson/cortex-shopify-translations

Version:

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

254 lines (217 loc) • 10.3 kB
/** * Create editable translation file with configurable source/target languages * Supports multiple Shopify resource types: ONLINE_STORE_THEME, PRODUCT, COLLECTION, etc. */ import fs from 'fs/promises'; import { config } from 'dotenv'; import { createGraphQLClient } from './dist/index.js'; import { spawn } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(spawn); // 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 showHelp = args.includes('--help') || args.includes('-h'); const limitArg = args.find(arg => arg.startsWith('--limit=')); const resourceLimit = limitArg ? parseInt(limitArg.split('=')[1]) : (resourceType === 'ONLINE_STORE_THEME' ? 1 : 10); // Default to split for theme translations, use --no-split to disable const shouldSplit = resourceType === 'ONLINE_STORE_THEME' ? !args.includes('--no-split') : args.includes('--split'); // Available resource types const AVAILABLE_TYPES = [ 'ONLINE_STORE_THEME', 'PRODUCT', 'COLLECTION', 'ARTICLE', 'BLOG', 'PAGE', 'SHOP', 'SHOP_POLICY', 'PRODUCT_OPTION', 'DELIVERY_METHOD_DEFINITION', 'METAFIELD', 'SELLING_PLAN', 'SELLING_PLAN_GROUP', 'FILTER', 'EMAIL_TEMPLATE' ]; function showHelpText() { console.log(`\n🧠 Shopify Translation Extractor\n`); console.log(`Usage: node extract-translations.mjs [RESOURCE_TYPE] [OPTIONS]\n`); console.log(`Resource Types:`); AVAILABLE_TYPES.forEach(type => { const isDefault = type === 'ONLINE_STORE_THEME'; const defaultLimit = type === 'ONLINE_STORE_THEME' ? '1' : '10'; console.log(` ${type.toLowerCase().padEnd(25)} ${isDefault ? '(default, limit=' + defaultLimit + ')' : '(limit=' + defaultLimit + ')'}`); }); console.log(`\nOptions:`); console.log(` --limit=N Max resources to extract (default: theme=1, others=10)`); console.log(` --no-split Skip auto-splitting for theme translations (default: split enabled)`); console.log(` --split Force splitting for non-theme resources`); console.log(` --help, -h Show this help\n`); console.log(`Examples:`); console.log(` node extract-translations.mjs # Extract 1 theme`); console.log(` node extract-translations.mjs product --limit=5 # Extract 5 products`); console.log(` node extract-translations.mjs collection # Extract 10 collections`); console.log(` node extract-translations.mjs page --limit=50 # Extract 50 pages\n`); console.log(`Configuration:`); console.log(` Set SHOPIFY_SOURCE_LANGUAGE and SHOPIFY_TARGET_LANGUAGES in .env.local\n`); } if (showHelp) { showHelpText(); process.exit(0); } if (!AVAILABLE_TYPES.includes(resourceType)) { console.error(`āŒ Invalid resource type: ${resourceType}`); console.error(`Available types: ${AVAILABLE_TYPES.join(', ')}`); console.error(`Use --help for more information`); process.exit(1); } async function createTranslationEditor() { console.log(`🧠 Creating translation editor for ${resourceType}...`); try { // Get fresh resource data with correct language structure const client = createGraphQLClient(); const sourceLanguage = client.sourceLanguage; const targetLanguages = client.targetLanguages; console.log(`šŸ” Fetching ${resourceType.toLowerCase()} translations with ${sourceLanguage} as base...`); console.log(`šŸ“Š Resource limit: ${resourceLimit}`); const resources = await client.getTranslatableResources(resourceType, 50); if (resources.edges.length === 0) { throw new Error(`No ${resourceType.toLowerCase()} resources found`); } console.log(`šŸ“¦ Found ${resources.edges.length} ${resourceType.toLowerCase()} resource(s)`); // Apply resource limit const resourcesToProcess = resources.edges .slice(0, resourceLimit) .map(edge => edge.node); if (resources.edges.length > resourceLimit) { console.log(`āš ļø Limited to ${resourceLimit} resources (found ${resources.edges.length} total)`); console.log(` Use --limit=${resources.edges.length} to extract all`); } // Create editing structure with dynamic structure const editableTranslations = []; let totalContent = 0; let totalTranslations = 0; // Process all resources for (const resource of resourcesToProcess) { console.log(`šŸ“Š Processing resource: ${resource.resourceId}`); console.log(`šŸ“ Translatable content keys: ${resource.translatableContent.length}`); console.log(`🌐 Available translations: ${resource.translations.length}`); totalContent += resource.translatableContent.length; totalTranslations += resource.translations.length; // Process all translatable content for source language for (const content of resource.translatableContent) { if (content.locale !== sourceLanguage) { continue; // Skip non-source language content } const key = content.key; const sourceOriginal = content.value; const digest = content.digest; // Find existing translations for this key const existingTranslations = resource.translations.filter(t => t.key === key); // Create dynamic translations object for target languages const translations = {}; targetLanguages.forEach(locale => { const translation = existingTranslations.find(t => t.locale === locale); translations[locale] = translation?.value || ''; }); // Create editable entry with dynamic structure const editableEntry = { key, resourceId: resource.resourceId, [`${sourceLanguage}_original`]: sourceOriginal, // Source language content digest: digest }; // Add target language fields dynamically targetLanguages.forEach(locale => { editableEntry[`${locale}_current`] = translations[locale]; editableEntry[`${locale}_fixed`] = translations[locale]; // Will be edited manually }); editableTranslations.push(editableEntry); } } console.log(`šŸ“ Created ${editableTranslations.length} editable translation entries from ${resourcesToProcess.length} resource(s)`); console.log(`šŸ“Š Total translatable content: ${totalContent}`); console.log(`šŸ“Š Total available translations: ${totalTranslations}`); // Generate filenames with resource type const filePrefix = resourceType === 'ONLINE_STORE_THEME' ? 'translations' : `${resourceType.toLowerCase()}-translations`; const jsonFile = `${filePrefix}-to-edit.json`; // Remove old files try { await fs.unlink(jsonFile); } catch (e) { // Ignore if files don't exist } // Create JSON format const jsonData = { metadata: { createdAt: new Date().toISOString(), resourceType, resourceCount: resourcesToProcess.length, totalTranslations: editableTranslations.length, sourceLanguage, targetLanguages, note: `${sourceLanguage}_original contains the source language content for translations`, instructions: { howToEdit: `Edit the '*_fixed' fields with proper translations FROM the ${sourceLanguage}_original field.`, workflow: [ `1. Look at ${sourceLanguage}_original (this is the source text)`, `2. Fix the *_fixed fields for target languages: ${targetLanguages.join(', ')}`, "3. Save your changes", `4. Run: node publish-translations.mjs ${resourceType.toLowerCase()} --force` ] } }, translations: editableTranslations }; await fs.writeFile(jsonFile, JSON.stringify(jsonData, null, 2), 'utf-8'); console.log(`āœ… JSON file saved: ${jsonFile}`); // Run split script if requested for theme translations if (shouldSplit && resourceType === 'ONLINE_STORE_THEME') { console.log(`\nšŸ”„ Running split script to create category files...`); return new Promise((resolve, reject) => { const splitProcess = spawn('node', ['translations/scripts/split-translations.mjs'], { stdio: 'inherit', cwd: process.cwd() }); splitProcess.on('exit', (code) => { if (code === 0) { console.log(`\nāœ… Split completed successfully!`); console.log('šŸ“ Category files created in: translations/content/'); console.log('\nšŸ“ Next steps:'); console.log(' 1. Edit individual category files in translations/content/'); console.log(' 2. Run: node translations/scripts/merge-back.mjs'); console.log(' 3. Run: node publish-translations.mjs --force'); resolve(); } else { console.error(`āŒ Split script failed with code ${code}`); reject(new Error(`Split script failed with code ${code}`)); } }); splitProcess.on('error', (error) => { console.error('āŒ Failed to run split script:', error); reject(error); }); }); } console.log(`\nšŸŽ‰ ${resourceType} translation editor file created successfully!`); console.log('\nšŸ“ Structure:'); console.log(` - ${sourceLanguage}_original = Source language content`); console.log(' - *_current = Current translations'); console.log(' - *_fixed = Your corrected translations'); console.log(` - resourceId = Shopify resource identifier`); console.log(`\nšŸ“ Resource: ${resourceType} (${resourcesToProcess.length} resource${resourcesToProcess.length > 1 ? 's' : ''})`); console.log(`šŸ“ Languages: ${sourceLanguage} → ${targetLanguages.join(', ')}`); if (!shouldSplit) { console.log('\nšŸ“ Next steps:'); console.log(` 1. Edit ${jsonFile} with your corrections`); console.log(` 2. Run: node publish-translations.mjs ${resourceType.toLowerCase()} --force`); } } catch (error) { console.error('āŒ Failed to create translation editor:', error.message); console.error(error.stack); } } createTranslationEditor();