@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
JavaScript
/**
* 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();