From 3e58377ec92aaceae15b8c0471c852a6bec7bbd8 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 4 Nov 2025 17:48:46 +0200 Subject: [PATCH] Install package (#484) --- .gitignore | 1 + linguaisync.config.js | 10 + package-lock.json | 29 +- package.json | 6 +- scripts/sync-translations-README.md | 231 ---------- scripts/sync-translations.js | 684 ---------------------------- 6 files changed, 39 insertions(+), 922 deletions(-) create mode 100644 linguaisync.config.js delete mode 100644 scripts/sync-translations-README.md delete mode 100755 scripts/sync-translations.js diff --git a/.gitignore b/.gitignore index 4eadd2a..7da89b2 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ test-results/ # Temporary files tmp/ +*.bak diff --git a/linguaisync.config.js b/linguaisync.config.js new file mode 100644 index 0000000..b624389 --- /dev/null +++ b/linguaisync.config.js @@ -0,0 +1,10 @@ +const path = require('path'); + +module.exports = { + localesDir: path.join(__dirname, 'public/locales'), + baseLanguage: 'en', + translationFiles: ['translation.json', 'quotes.json'], + batchSize: 20, + model: 'gpt-4o-mini', + temperature: 0.3, +}; diff --git a/package-lock.json b/package-lock.json index 3f92b10..a90a741 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tududi", - "version": "v0.85.1", + "version": "v0.86-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tududi", - "version": "v0.85.1", + "version": "v0.86-beta.1", "license": "ISC", "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -31,6 +31,7 @@ "i18next-browser-languagedetector": "^8.0.4", "i18next-http-backend": "^3.0.2", "js-yaml": "~4.1.0", + "linguaisync": "^0.1.2", "lodash": "~4.17.21", "moment-timezone": "~0.6.0", "morgan": "~1.10.0", @@ -12484,6 +12485,30 @@ "dev": true, "license": "MIT" }, + "node_modules/linguaisync": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/linguaisync/-/linguaisync-0.1.2.tgz", + "integrity": "sha512-7iH/O6Nonm5aaLorqBtgBA715BihTbBR4d4LKei4prETiOxWxAluFdyBXVkyIml5wg3NgQ7TyOwqV5X5Nj1EdQ==", + "license": "MIT", + "dependencies": { + "commander": "^12.0.0" + }, + "bin": { + "linguaisync": "bin/sync-translations.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/linguaisync/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", diff --git a/package.json b/package.json index 55d5e5c..6e30d0b 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,6 @@ "migration:undo": "cd backend && npx sequelize-cli db:migrate:undo", "migration:undo:all": "cd backend && npx sequelize-cli db:migrate:undo:all", "migration:status": "cd backend && npx sequelize-cli db:migrate:status", - "translations:sync": "cd scripts && ./sync-translations.js", - "translations:sync-all": "cd scripts && ./sync-translations.js --all", - "translations:dry-run": "cd scripts && ./sync-translations.js --all --dry-run", - "translations:check": "cd scripts && ./sync-translations.js --all --dry-run --verbose", - "translations:export": "cd scripts && ./sync-translations.js --all --dry-run --output missing-translations.json", "clean": "rimraf dist", "lint": "npm run frontend:lint && npm run backend:lint", "lint:fix": "npm run frontend:lint:fix && npm run backend:lint:fix", @@ -135,6 +130,7 @@ "i18next-browser-languagedetector": "^8.0.4", "i18next-http-backend": "^3.0.2", "js-yaml": "~4.1.0", + "linguaisync": "^0.1.2", "lodash": "~4.17.21", "moment-timezone": "~0.6.0", "morgan": "~1.10.0", diff --git a/scripts/sync-translations-README.md b/scripts/sync-translations-README.md deleted file mode 100644 index 22abd70..0000000 --- a/scripts/sync-translations-README.md +++ /dev/null @@ -1,231 +0,0 @@ -# Translation Synchronization Script - -This script helps maintain consistent translations across all language files by comparing them with the English base translation and using OpenAI to fill in missing translations. - -## Setup - -### Prerequisites - -1. **Node.js** (version 14 or higher) -2. **OpenAI API Key** - Get one from [OpenAI Platform](https://platform.openai.com/api-keys) - -### Installation - -```bash -cd scripts -npm install -``` - -### Environment Setup - -Set your OpenAI API key as an environment variable: - -```bash -# Linux/macOS -export OPENAI_API_KEY="your-openai-api-key-here" - -# Windows -set OPENAI_API_KEY=your-openai-api-key-here -``` - -## Usage - -### Check All Languages - -```bash -./sync-translations.js --all -``` - -### Check Specific Languages - -```bash -./sync-translations.js --lang=jp,el,de -./sync-translations.js --lang=es -``` - -### Dry Run (Preview Changes) - -```bash -./sync-translations.js --all --dry-run -./sync-translations.js --lang=it --dry-run -``` - -### Detailed Analysis - -```bash -# Show detailed missing translations with English values -./sync-translations.js --lang=el --dry-run --verbose - -# Export missing translations to JSON file -./sync-translations.js --all --dry-run --output missing-translations.json -``` - -## How It Works - -1. **Base Comparison**: Uses English (`en`) as the base language -2. **Multi-File Support**: Processes both `translation.json` and `quotes.json` files -3. **Key Analysis**: Recursively compares all keys and nested objects -4. **Missing Detection**: Identifies missing translations in target languages -5. **Batch Processing**: Groups missing translations into batches of 20 -6. **AI Translation**: Uses OpenAI GPT-4 to translate missing content -7. **File Updates**: Updates translation files with new translations - -## Features - -- ✅ **Multi-File Support**: Handles both `translation.json` and `quotes.json` files -- ✅ **Recursive Analysis**: Handles nested translation objects -- ✅ **Batch Processing**: Efficient API usage with configurable batch sizes -- ✅ **Placeholder Preservation**: Maintains `{{variables}}` and formatting -- ✅ **Context Awareness**: Provides context for better translations -- ✅ **Error Handling**: Graceful error handling and reporting -- ✅ **Dry Run Mode**: Preview changes before applying (no API key required) -- ✅ **Progress Tracking**: Clear progress indicators -- ✅ **File Backup**: Maintains original file structure - -## Supported Languages - -- `ar` - Arabic (العربية) 🇸🇦 -- `bg` - Bulgarian (Български) 🇧🇬 -- `da` - Danish (Dansk) 🇩🇰 -- `de` - German (Deutsch) 🇩🇪 -- `el` - Greek (Ελληνικά) 🇬🇷 -- `es` - Spanish (Español) 🇪🇸 -- `fi` - Finnish (Suomi) 🇫🇮 -- `fr` - French (Français) 🇫🇷 -- `id` - Indonesian (Bahasa Indonesia) 🇮🇩 -- `it` - Italian (Italiano) 🇮🇹 -- `jp` - Japanese (日本語) 🇯🇵 -- `ko` - Korean (한국어) 🇰🇷 -- `nl` - Dutch (Nederlands) 🇳🇱 -- `no` - Norwegian (Norsk) 🇳🇴 -- `pl` - Polish (Polski) 🇵🇱 -- `pt` - Portuguese (Português) 🇵🇹 -- `ro` - Romanian (Română) 🇷🇴 -- `ru` - Russian (Русский) 🇷🇺 -- `sl` - Slovenian (Slovenščina) 🇸🇮 -- `sv` - Swedish (Svenska) 🇸🇪 -- `tr` - Turkish (Türkçe) 🇹🇷 -- `ua` - Ukrainian (Українська) 🇺🇦 -- `vi` - Vietnamese (Tiếng Việt) 🇻🇳 -- `zh` - Chinese (中文) 🇨🇳 - -## Examples - -### Using NPM Scripts (Recommended) - -```bash -# Quick dry run of all languages -npm run translations:dry-run - -# Detailed analysis with all missing translations -npm run translations:check - -# Export missing translations to JSON file -npm run translations:export - -# Update all languages (requires OPENAI_API_KEY) -npm run translations:sync-all -``` - -### Full Synchronization -```bash -# Update all languages with missing translations -./sync-translations.js --all -``` - -### Targeted Updates -```bash -# Update only Japanese and Italian -./sync-translations.js --lang=jp,it - -# Preview changes for German -./sync-translations.js --lang=de --dry-run - -# Detailed analysis of missing Spanish translations -./sync-translations.js --lang=es --dry-run --verbose - -# Export all missing translations to review -./sync-translations.js --all --dry-run --output review.json -``` - -### Sample Output -``` -🌍 Translation Synchronization Tool - -📁 Locales directory: /path/to/public/locales -🏴󠁧󠁢󠁥󠁮󠁧󠁿 Base language: en -🔄 Processing all available languages: es, de, el, jp, ua, it - -📊 Analyzing es (Spanish)... -✅ es: All translations are up to date! - -📊 Analyzing de (German)... -📝 Found 5 missing translation(s) for de -📦 Processing 1 batch(es) of translations... - Batch 1/1 (5 items)... -✅ de: Applied 5/5 translations - -🎉 Completed! Successfully updated 2/6 languages -``` - -## Configuration - -### Batch Size -Modify `BATCH_SIZE` in the script to change how many translations are sent per API request (default: 50). - -### API Model -The script uses GPT-4 by default. You can change this in the `requestTranslations` function. - -### Rate Limiting -The script includes a 1-second delay between API requests to respect rate limits. - -## Troubleshooting - -### Common Issues - -1. **API Key Not Set** - ``` - ❌ Error: OPENAI_API_KEY environment variable is required - ``` - Solution: Set the environment variable with your OpenAI API key. - -2. **Invalid Language Code** - ``` - ❌ Invalid language codes: xx - ``` - Solution: Use valid language codes from the supported list. - -3. **API Rate Limits** - If you hit rate limits, the script will show an error. Wait a moment and try again. - -### File Structure - -The script expects the following directory structure: -``` -public/ -├── locales/ -│ ├── en/ -│ │ ├── translation.json -│ │ └── quotes.json -│ ├── es/ -│ │ ├── translation.json -│ │ └── quotes.json -│ ├── de/ -│ │ ├── translation.json -│ │ └── quotes.json -│ └── ... -``` - -## Contributing - -To add support for new languages: - -1. Add the language code and name to `LANGUAGE_NAMES` object -2. Create the language directory in `public/locales/` -3. Run the script to generate initial translations - -## Security - -- API keys are passed securely through environment variables -- No sensitive data is logged or stored -- Translation requests use HTTPS \ No newline at end of file diff --git a/scripts/sync-translations.js b/scripts/sync-translations.js deleted file mode 100755 index 3647112..0000000 --- a/scripts/sync-translations.js +++ /dev/null @@ -1,684 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs'); -const path = require('path'); -const { program } = require('commander'); - -// Configuration -const LOCALES_DIR = path.join(__dirname, '../public/locales'); -const BASE_LANGUAGE = 'en'; -const BATCH_SIZE = 20; -const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions'; -// Base translation files to check -const BASE_TRANSLATION_FILES = ['translation.json', 'quotes.json']; - -// Language mappings for OpenAI -const LANGUAGE_NAMES = { - es: 'Spanish', - de: 'German', - el: 'Greek', - jp: 'Japanese', - ua: 'Ukrainian', - it: 'Italian', - fr: 'French', - ru: 'Russian', - tr: 'Turkish', - ko: 'Korean', - vi: 'Vietnamese', - ar: 'Arabic', - nl: 'Dutch', - ro: 'Romanian', - zh: 'Mandarin Chinese', - pt: 'Portuguese', - id: 'Indonesian', - no: 'Norwegian', - fi: 'Finnish', - da: 'Danish', - sv: 'Swedish', - pl: 'Polish', - bg: 'Bulgarian', - sl: 'Slovenian', -}; - -// Get OpenAI API key from environment -const OPENAI_API_KEY = process.env.OPENAI_API_KEY; - -/** - * Load and parse a JSON translation file - */ -function loadTranslationFile(language, filename = 'translation.json') { - const filePath = path.join(LOCALES_DIR, language, filename); - try { - const content = fs.readFileSync(filePath, 'utf8'); - return JSON.parse(content); - } catch (error) { - console.warn( - `⚠️ Warning: Could not load ${filePath}: ${error.message}` - ); - return {}; - } -} - -/** - * Save translation file with proper formatting - */ -function saveTranslationFile(language, data, filename = 'translation.json') { - const filePath = path.join(LOCALES_DIR, language, filename); - const dirPath = path.dirname(filePath); - - // Ensure directory exists - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } - - const jsonString = JSON.stringify(data, null, 2); - fs.writeFileSync(filePath, jsonString + '\n', 'utf8'); -} - -/** - * Get all available language codes - */ -function getAvailableLanguages() { - try { - return fs.readdirSync(LOCALES_DIR).filter((dir) => { - const stat = fs.statSync(path.join(LOCALES_DIR, dir)); - return stat.isDirectory() && dir !== BASE_LANGUAGE; - }); - } catch (error) { - console.error(`❌ Error reading locales directory: ${error.message}`); - return []; - } -} - -/** - * Get available translation files for the base language - */ -function getAvailableTranslationFiles() { - const availableFiles = []; - - for (const filename of BASE_TRANSLATION_FILES) { - const filePath = path.join(LOCALES_DIR, BASE_LANGUAGE, filename); - if (fs.existsSync(filePath)) { - availableFiles.push(filename); - } - } - - return availableFiles; -} - -/** - * Recursively compare objects and find missing keys - */ -function findMissingKeys(baseObj, targetObj, currentPath = '') { - const missing = []; - - for (const [key, value] of Object.entries(baseObj)) { - const fullPath = currentPath ? `${currentPath}.${key}` : key; - - if (!(key in targetObj)) { - // If the missing key is an object, recursively add all its nested keys - if (typeof value === 'object' && value !== null) { - missing.push(...findMissingKeys(value, {}, fullPath)); - } else { - missing.push({ - path: fullPath, - value: value, - isNested: false, - }); - } - } else if ( - typeof value === 'object' && - value !== null && - typeof targetObj[key] === 'object' && - targetObj[key] !== null - ) { - // Recursively check nested objects - missing.push(...findMissingKeys(value, targetObj[key], fullPath)); - } - } - - return missing; -} - -/** - * Set nested object property using dot notation - */ -function setNestedProperty(obj, path, value) { - const keys = path.split('.'); - let current = obj; - - for (let i = 0; i < keys.length - 1; i++) { - const key = keys[i]; - if (!(key in current) || typeof current[key] !== 'object') { - current[key] = {}; - } - current = current[key]; - } - - current[keys[keys.length - 1]] = value; -} - -/** - * Create translation batches for OpenAI API - */ -function createTranslationBatches(missingKeys, batchSize = BATCH_SIZE) { - const batches = []; - - for (let i = 0; i < missingKeys.length; i += batchSize) { - const batch = missingKeys.slice(i, i + batchSize); - batches.push(batch); - } - - return batches; -} - -/** - * Request translations from OpenAI API - */ -async function requestTranslations(batch, targetLanguage) { - const languageName = LANGUAGE_NAMES[targetLanguage] || targetLanguage; - - // Prepare the translation request - const translationItems = batch.map((item) => ({ - key: item.path, - english: - typeof item.value === 'string' - ? item.value - : JSON.stringify(item.value), - })); - - const prompt = `You are a professional translator. Translate the following English text to ${languageName}. - -IMPORTANT INSTRUCTIONS: -1. Maintain the exact same structure and formatting -2. Preserve all placeholders like {{variable}}, {{count}}, etc. -3. Keep HTML tags intact if present -4. For technical terms, use appropriate ${languageName} equivalents -5. Maintain the tone and context appropriate for a task management application -6. If the English value is an array, return an array in the translation (NOT an object with numbered keys) -7. If the English value is an object, return an object in the translation -8. Return ONLY a JSON object with the translations - -Translate these English texts: -${JSON.stringify(translationItems, null, 2)} - -Return format: -{ - "translations": [ - { - "key": "path.to.key", - "translation": "translated text in ${languageName}" - } - ] -}`; - - // Debug: Log request details for troubleshooting - console.log(` 🔍 Debug: Translating ${batch.length} items to ${languageName}`); - console.log(` 📊 Prompt length: ${prompt.length} characters`); - - // Check payload size - const requestPayload = { - model: 'gpt-4o-mini', - messages: [ - { - role: 'system', - content: `You are a professional translator specializing in software localization. Always return valid JSON in the exact format requested.`, - }, - { - role: 'user', - content: prompt, - }, - ], - temperature: 0.3, - max_tokens: 2000, - }; - const payloadSize = JSON.stringify(requestPayload).length; - console.log(` 📦 Request payload size: ${payloadSize} bytes`); - - try { - const response = await fetch(OPENAI_API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${OPENAI_API_KEY}`, - }, - body: JSON.stringify(requestPayload), - }); - - if (!response.ok) { - let errorDetails = ''; - try { - const errorBody = await response.json(); - errorDetails = ` - ${JSON.stringify(errorBody)}`; - } catch (e) { - // If response body isn't JSON, try to get text - try { - errorDetails = ` - ${await response.text()}`; - } catch (e2) { - errorDetails = ' - Unable to read error response'; - } - } - throw new Error( - `OpenAI API error: ${response.status} ${response.statusText}${errorDetails}` - ); - } - - const data = await response.json(); - const content = data.choices[0].message.content; - - // Parse the JSON response - const translationResponse = JSON.parse(content); - return translationResponse.translations || []; - } catch (error) { - console.error(`❌ Error requesting translations: ${error.message}`); - return []; - } -} - -/** - * Process a single language - */ -async function processLanguage(language) { - console.log( - `\n📊 Analyzing ${language} (${LANGUAGE_NAMES[language] || language})...` - ); - - let totalMissingKeys = 0; - let totalAppliedCount = 0; - let processedFiles = 0; - - // Get available translation files from base language - const availableFiles = getAvailableTranslationFiles(); - - // Process each translation file - for (const filename of availableFiles) { - console.log(`\n 📄 Processing ${filename}...`); - - // Load base and target translation files - const baseTranslations = loadTranslationFile(BASE_LANGUAGE, filename); - const targetTranslations = loadTranslationFile(language, filename); - - if (Object.keys(baseTranslations).length === 0) { - console.warn( - `⚠️ Warning: Could not load base ${filename} for ${BASE_LANGUAGE}, skipping...` - ); - continue; - } - - // Find missing keys - const missingKeys = findMissingKeys(baseTranslations, targetTranslations); - - if (missingKeys.length === 0) { - console.log(` ✅ ${filename}: All translations are up to date!`); - processedFiles++; - continue; - } - - console.log( - ` 📝 Found ${missingKeys.length} missing translation(s) in ${filename}` - ); - totalMissingKeys += missingKeys.length; - - // Create batches for API requests - const batches = createTranslationBatches(missingKeys); - console.log(` 📦 Processing ${batches.length} batch(es) of translations...`); - - let allTranslations = []; - - // Process each batch - for (let i = 0; i < batches.length; i++) { - const batch = batches[i]; - console.log( - ` Batch ${i + 1}/${batches.length} (${batch.length} items)...` - ); - - try { - const translations = await requestTranslations(batch, language); - allTranslations.push(...translations); - - // Add a small delay between requests to be respectful to the API - if (i < batches.length - 1) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - } catch (error) { - console.error( - ` ❌ Error processing batch ${i + 1}: ${error.message}` - ); - } - } - - if (allTranslations.length === 0) { - console.error(` ❌ No translations received for ${filename}`); - continue; - } - - // Apply translations to the target object - const updatedTranslations = { ...targetTranslations }; - let appliedCount = 0; - - for (const translation of allTranslations) { - try { - // Try to parse as JSON if it looks like an object - let translatedValue = translation.translation; - if ( - translatedValue.startsWith('{') || - translatedValue.startsWith('[') - ) { - try { - translatedValue = JSON.parse(translatedValue); - - // Check if this should be an array but was translated as a numbered object - if (typeof translatedValue === 'object' && translatedValue !== null && !Array.isArray(translatedValue)) { - const keys = Object.keys(translatedValue); - const isNumberedObject = keys.every((key, index) => key === index.toString()); - - if (isNumberedObject && keys.length > 0) { - // Convert numbered object back to array - translatedValue = keys.map(key => translatedValue[key]); - console.log(` 🔧 Fixed numbered object to array for ${translation.key}`); - } - } - } catch { - // Keep as string if JSON parsing fails - } - } - - setNestedProperty( - updatedTranslations, - translation.key, - translatedValue - ); - appliedCount++; - } catch (error) { - console.warn( - ` ⚠️ Warning: Could not apply translation for ${translation.key}: ${error.message}` - ); - } - } - - // Save the updated translations - saveTranslationFile(language, updatedTranslations, filename); - - console.log( - ` ✅ ${filename}: Applied ${appliedCount}/${missingKeys.length} translations` - ); - - totalAppliedCount += appliedCount; - processedFiles++; - } - - if (processedFiles === 0) { - console.error(`❌ ${language}: No files could be processed`); - return false; - } - - if (totalMissingKeys === 0) { - console.log(`✅ ${language}: All translation files are up to date!`); - return true; - } - - console.log( - `✅ ${language}: Applied ${totalAppliedCount}/${totalMissingKeys} total translations across ${processedFiles} files` - ); - - return true; -} - -/** - * Main execution function - */ -async function main() { - program - .name('sync-translations') - .description( - 'Synchronize translation files with English base and fill missing translations using OpenAI' - ) - .option('--all', 'Update all available languages') - .option( - '--lang ', - 'Comma-separated list of language codes to update (e.g., jp,el,de)' - ) - .option( - '--dry-run', - 'Show what would be updated without making changes' - ) - .option( - '--verbose', - 'Show detailed missing translations in dry run mode' - ) - .option('--output ', 'Save missing translations to a JSON file') - .parse(); - - const options = program.opts(); - - console.log('🌍 Translation Synchronization Tool\n'); - console.log(`📁 Locales directory: ${LOCALES_DIR}`); - console.log(`🏴󠁧󠁢󠁥󠁮󠁧󠁿 Base language: ${BASE_LANGUAGE}`); - - // Show available translation files - const availableFiles = getAvailableTranslationFiles(); - console.log(`📄 Available translation files: ${availableFiles.join(', ')}`); - - // Determine which languages to process - let languagesToProcess = []; - - if (options.all) { - languagesToProcess = getAvailableLanguages(); - console.log( - `🔄 Processing all available languages: ${languagesToProcess.join(', ')}` - ); - } else if (options.lang) { - languagesToProcess = options.lang.split(',').map((lang) => lang.trim()); - console.log( - `🎯 Processing specified languages: ${languagesToProcess.join(', ')}` - ); - } else { - console.log('❓ No languages specified. Use --all or --lang='); - console.log( - '\nAvailable languages:', - getAvailableLanguages().join(', ') - ); - process.exit(1); - } - - if (languagesToProcess.length === 0) { - console.log('❌ No valid languages to process'); - process.exit(1); - } - - // Validate that all specified languages exist - const availableLanguages = getAvailableLanguages(); - const invalidLanguages = languagesToProcess.filter( - (lang) => !availableLanguages.includes(lang) - ); - - if (invalidLanguages.length > 0) { - console.error( - `❌ Invalid language codes: ${invalidLanguages.join(', ')}` - ); - console.error(`Available languages: ${availableLanguages.join(', ')}`); - process.exit(1); - } - - if (options.dryRun) { - console.log('\n🧪 DRY RUN MODE - No files will be modified\n'); - } - - // Process each language - let successCount = 0; - const allMissingTranslations = {}; - - for (const language of languagesToProcess) { - try { - if (options.dryRun) { - // In dry run, just analyze without making API calls - console.log( - `\n📊 Analyzing ${language} (${LANGUAGE_NAMES[language] || language})...` - ); - - let totalMissingKeys = 0; - let allMissingForLanguage = []; - - // Get available translation files from base language - const availableFiles = getAvailableTranslationFiles(); - - // Process each translation file - for (const filename of availableFiles) { - console.log(`\n 📄 Checking ${filename}...`); - - const baseTranslations = loadTranslationFile(BASE_LANGUAGE, filename); - const targetTranslations = loadTranslationFile(language, filename); - - if (Object.keys(baseTranslations).length === 0) { - console.warn( - ` ⚠️ Warning: Could not load base ${filename} for ${BASE_LANGUAGE}, skipping...` - ); - continue; - } - - const missingKeys = findMissingKeys( - baseTranslations, - targetTranslations - ); - - if (missingKeys.length === 0) { - console.log(` ✅ ${filename}: All translations are up to date!`); - continue; - } - - console.log( - ` 📝 Found ${missingKeys.length} missing translation(s) in ${filename}` - ); - - totalMissingKeys += missingKeys.length; - - // Add filename prefix to paths for clarity - const prefixedMissingKeys = missingKeys.map(item => ({ - ...item, - path: `${filename}:${item.path}`, - filename: filename - })); - - allMissingForLanguage.push(...prefixedMissingKeys); - } - - console.log( - `\n📊 ${language}: Would update ${totalMissingKeys} missing translation(s) across ${availableFiles.length} files` - ); - - // Store missing translations for potential output - if (totalMissingKeys > 0) { - allMissingTranslations[language] = { - languageName: LANGUAGE_NAMES[language] || language, - missingCount: totalMissingKeys, - missing: allMissingForLanguage.map((item) => ({ - path: item.path, - englishValue: item.value, - isNested: item.isNested, - filename: item.filename, - })), - }; - } - - if (totalMissingKeys > 0) { - if (options.verbose) { - console.log( - `\n🔍 Detailed missing translations for ${language}:` - ); - console.log('─'.repeat(60)); - - allMissingForLanguage.forEach((item, index) => { - const displayValue = - typeof item.value === 'string' - ? item.value.length > 100 - ? item.value.substring(0, 97) + '...' - : item.value - : JSON.stringify(item.value); - - console.log( - `${(index + 1).toString().padStart(3)}. ${item.path}` - ); - console.log(` 📝 EN: "${displayValue}"`); - console.log( - ` 🎯 ${LANGUAGE_NAMES[language] || language}: [MISSING]` - ); - - if (index < allMissingForLanguage.length - 1) { - console.log(''); - } - }); - console.log('─'.repeat(60)); - } - } - } else { - // Check API key for actual processing - if (!OPENAI_API_KEY) { - console.error('❌ Error: OPENAI_API_KEY environment variable is required for actual translation updates'); - console.error('Please set it with: export OPENAI_API_KEY="your-api-key"'); - process.exit(1); - } - - const success = await processLanguage(language); - if (success) successCount++; - } - } catch (error) { - console.error(`❌ Error processing ${language}: ${error.message}`); - } - } - - if (!options.dryRun) { - console.log( - `\n🎉 Completed! Successfully updated ${successCount}/${languagesToProcess.length} languages` - ); - } else { - // Show summary for dry run - const totalMissing = Object.values(allMissingTranslations).reduce((sum, lang) => sum + lang.missingCount, 0); - if (totalMissing > 0) { - console.log(`\n📋 Summary: ${totalMissing} total missing translations across ${Object.keys(allMissingTranslations).length} languages`); - } - - // Handle output file for dry run - if (options.output && Object.keys(allMissingTranslations).length > 0) { - const outputData = { - generatedAt: new Date().toISOString(), - baseLanguage: BASE_LANGUAGE, - languages: allMissingTranslations, - summary: { - totalLanguages: Object.keys(allMissingTranslations).length, - totalMissingTranslations: totalMissing, - }, - }; - - try { - fs.writeFileSync( - options.output, - JSON.stringify(outputData, null, 2) - ); - console.log(`💾 Missing translations saved to: ${options.output}`); - } catch (error) { - console.error(`❌ Error saving output file: ${error.message}`); - } - } - } -} - -// Handle uncaught errors -process.on('unhandledRejection', (error) => { - console.error('❌ Unhandled error:', error); - process.exit(1); -}); - -// Run the script -if (require.main === module) { - main().catch((error) => { - console.error('❌ Script failed:', error); - process.exit(1); - }); -} - -module.exports = { - loadTranslationFile, - findMissingKeys, - createTranslationBatches, - processLanguage, -};