UNPKG

i18ntk

Version:

i18ntk (i18n Toolkit) - A comprehensive, enterprise-grade internationalization (i18n) management toolkit for JavaScript/TypeScript projects with advanced analysis, validation, and automation features

843 lines (698 loc) 31.8 kB
#!/usr/bin/env node /** * I18N INITIALIZATION SCRIPT * * This script initializes a new i18n project or adds new languages to an existing one. * It uses the English (en) locale as the source of truth and generates translation files * for specified languages with proper structure and __NOT_TRANSLATED__ markers. * * Usage: * node scripts/i18n/01-init-i18n.js * node scripts/i18n/01-init-i18n.js --languages=de,es,fr,ru * node scripts/i18n/01-init-i18n.js --source-dir=./src/i18n/locales --target-languages=de,es */ const fs = require('fs'); const path = require('path'); const readline = require('readline'); const settingsManager = require('../settings/settings-manager'); const SecurityUtils = require('../utils/security'); const AdminAuth = require('../utils/admin-auth'); const UIi18n = require('./i18ntk-ui'); // Get configuration from settings manager function getConfig() { const settings = settingsManager.getSettings(); // Check for per-script directory override, fallback to global sourceDir const sourceDir = settings.scriptDirectories?.init || settings.sourceDir || './locales'; return { sourceDir: sourceDir, sourceLanguage: settings.sourceLanguage || 'en', defaultLanguages: settings.defaultLanguages || settings.processing?.defaultLanguages || ['de', 'es', 'fr', 'ru'], notTranslatedMarker: settings.notTranslatedMarker || settings.processing?.notTranslatedMarker || 'NOT_TRANSLATED', excludeFiles: settings.excludeFiles || settings.processing?.excludeFiles || ['.DS_Store', 'Thumbs.db'], uiLanguage: settings.language || 'en' }; } // Language configurations with native names const LANGUAGE_CONFIG = { 'de': { name: 'German', nativeName: 'Deutsch' }, 'es': { name: 'Spanish', nativeName: 'Español' }, 'fr': { name: 'French', nativeName: 'Français' }, 'ru': { name: 'Russian', nativeName: 'Русский' }, 'it': { name: 'Italian', nativeName: 'Italiano' }, 'ja': { name: 'Japanese', nativeName: '日本語' }, 'ko': { name: 'Korean', nativeName: '한국어' }, 'zh': { name: 'Chinese', nativeName: '中文' }, 'ar': { name: 'Arabic', nativeName: 'العربية' }, 'hi': { name: 'Hindi', nativeName: 'हिन्दी' }, 'nl': { name: 'Dutch', nativeName: 'Nederlands' }, 'sv': { name: 'Swedish', nativeName: 'Svenska' }, 'da': { name: 'Danish', nativeName: 'Dansk' }, 'no': { name: 'Norwegian', nativeName: 'Norsk' }, 'fi': { name: 'Finnish', nativeName: 'Suomi' }, 'pl': { name: 'Polish', nativeName: 'Polski' }, 'cs': { name: 'Czech', nativeName: 'Čeština' }, 'hu': { name: 'Hungarian', nativeName: 'Magyar' }, 'tr': { name: 'Turkish', nativeName: 'Türkçe' } }; class I18nInitializer { constructor(config = {}) { this.ui = new UIi18n(); this.config = { ...getConfig(), ...config }; this.sourceDir = path.resolve(this.config.sourceDir); this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage); // Use global readline interface to prevent doubling if (global.activeReadlineInterface) { this.rl = global.activeReadlineInterface; this.shouldCloseRL = false; } else { this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true, historySize: 0 }); global.activeReadlineInterface = this.rl; this.shouldCloseRL = true; } } // Add the missing checkI18nDependencies method async checkI18nDependencies() { const packageJsonPath = path.resolve('./package.json'); if (!fs.existsSync(packageJsonPath)) { console.log(this.ui.t('init.warnings.noPackageJson')); return await this.promptContinueWithoutI18n(); } try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); // Include peerDependencies in the check const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies, ...packageJson.peerDependencies }; const i18nFrameworks = [ 'react-i18next', 'vue-i18n', 'angular-i18n', 'i18next', 'next-i18next', 'svelte-i18n', '@nuxtjs/i18n' ]; const installedFrameworks = i18nFrameworks.filter(framework => dependencies[framework]); if (installedFrameworks.length > 0) { console.log(this.ui.t('init.detectedI18nFrameworks', { frameworks: installedFrameworks.join(', ') })); return true; } else { console.log(this.ui.t('init.suggestions.noFramework')); console.log(this.ui.t('init.frameworks.react')); console.log(this.ui.t('init.frameworks.vue')); console.log(this.ui.t('init.frameworks.i18next')); console.log(this.ui.t('init.frameworks.nuxt')); console.log(this.ui.t('init.frameworks.svelte')); return await this.promptContinueWithoutI18n(); } } catch (error) { console.log(this.ui.t('init.errors.packageJsonRead')); return await this.promptContinueWithoutI18n(); } } // Add the missing promptContinueWithoutI18n method async promptContinueWithoutI18n() { const answer = await this.prompt('\n' + this.ui.t('init.continueWithoutI18nPrompt')); return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'; } // Add the missing prompt method async prompt(question) { return new Promise((resolve) => { this.rl.question(question, (answer) => { resolve(answer.trim()); }); }); } // Parse command line arguments parseArgs() { const args = process.argv.slice(2); const parsed = {}; args.forEach(arg => { if (arg.startsWith('--')) { const [key, value] = arg.substring(2).split('='); if (key === 'languages' || key === 'target-languages') { parsed.languages = value ? value.split(',').map(l => l.trim()) : []; } else if (key === 'source-dir') { parsed.sourceDir = value; } else if (key === 'source-language') { parsed.sourceLanguage = value; } else if (key === 'no-prompt') { parsed.noPrompt = true; } } }); return parsed; } // Detect existing translation directories and prompt user async detectExistingDirectories() { const possibleLocations = [ './locales', './src/locales', './src/i18n/locales', './app/locales', './public/locales', './translations', './lang', './i18n/locales', './assets/locales', './client/locales', './frontend/locales' ]; const existingLocations = []; // Check for existing translation directories for (const location of possibleLocations) { if (fs.existsSync(location)) { try { const items = fs.readdirSync(location); const hasLanguageDirs = items.some(item => { const itemPath = path.join(location, item); if (fs.statSync(itemPath).isDirectory()) { return ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(item); } return item.endsWith('.json'); }); if (hasLanguageDirs) { existingLocations.push(location); } } catch (error) { // Continue checking other locations } } } if (existingLocations.length > 0) { console.log('\n' + this.ui.t('init.existingDirectoriesFound')); console.log(this.ui.t('common.separator')); existingLocations.forEach((location, index) => { console.log(` ${index + 1}. ${location}`); }); const answer = await this.prompt('\n' + this.ui.t('init.useExistingDirectoryPrompt')); const selectedIndex = parseInt(answer) - 1; if (selectedIndex >= 0 && selectedIndex < existingLocations.length) { const selectedDir = existingLocations[selectedIndex]; console.log(this.ui.t('init.usingExistingDirectory', { dir: selectedDir })); // Update settings to use the existing directory this.config.sourceDir = selectedDir; this.sourceDir = path.resolve(selectedDir); this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage); // Save to settings const currentSettings = settingsManager.getSettings(); currentSettings.sourceDir = selectedDir; settingsManager.saveSettings(currentSettings); return true; } } return false; } // Setup initial directory structure if needed async setupInitialStructure() { // First, detect if there are existing translation directories const usedExisting = await this.detectExistingDirectories(); if (usedExisting) { console.log(this.ui.t('init.usingExistingStructure', { dir: this.sourceDir })); return; } // Validate paths const validatedSourceDir = SecurityUtils.validatePath(this.sourceDir, process.cwd()); const validatedSourceLanguageDir = SecurityUtils.validatePath(this.sourceLanguageDir, process.cwd()); if (!validatedSourceDir || !validatedSourceLanguageDir) { SecurityUtils.logSecurityEvent('Invalid directory paths in setupInitialStructure', 'error', { sourceDir: this.sourceDir, sourceLanguageDir: this.sourceLanguageDir }); throw new Error(this.t('validate.invalidDirectoryPaths') || 'Invalid directory paths detected'); } // Create source directory only if it doesn't exist and no existing was detected if (!fs.existsSync(validatedSourceDir)) { console.log(this.ui.t('init.creatingSourceDirectory', { dir: validatedSourceDir })); fs.mkdirSync(validatedSourceDir, { recursive: true }); SecurityUtils.logSecurityEvent('Source directory created', 'info', { dir: validatedSourceDir }); } // Create source language directory only if it doesn't exist if (!fs.existsSync(validatedSourceLanguageDir)) { console.log(this.ui.t('init.creatingSourceLanguageDirectory', { dir: validatedSourceLanguageDir })); fs.mkdirSync(validatedSourceLanguageDir, { recursive: true }); // Create a sample translation file only if no files exist this.createSampleTranslationFile(validatedSourceLanguageDir); } else { // Directory exists, check if we need to create a sample file const existingFiles = fs.readdirSync(validatedSourceLanguageDir) .filter(file => file.endsWith('.json')); if (existingFiles.length === 0) { // No JSON files exist, create sample file this.createSampleTranslationFile(validatedSourceLanguageDir); } } } // Create sample translation file with smart naming async createSampleTranslationFile(validatedSourceLanguageDir) { const sampleTranslations = { "common": { "welcome": "Welcome", "hello": "Hello", "goodbye": "Goodbye", "yes": "Yes", "no": "No", "save": "Save", "cancel": "Cancel", "delete": "Delete", "edit": "Edit", "loading": "Loading..." }, "navigation": { "home": "Home", "about": "About", "contact": "Contact", "settings": "Settings" } }; // Determine filename: use common.json if it doesn't exist, otherwise i18ntk-common.json const commonFilePath = path.join(validatedSourceLanguageDir, 'common.json'); const i18ntkCommonFilePath = path.join(validatedSourceLanguageDir, 'i18ntk-common.json'); let sampleFilePath; if (!fs.existsSync(commonFilePath)) { sampleFilePath = commonFilePath; } else { sampleFilePath = i18ntkCommonFilePath; } const validatedSampleFilePath = SecurityUtils.validatePath(sampleFilePath, process.cwd()); if (!validatedSampleFilePath) { SecurityUtils.logSecurityEvent('Invalid sample file path', 'error', { path: sampleFilePath }); throw new Error(this.t('validate.invalidSampleFilePath') || 'Invalid sample file path'); } const success = await SecurityUtils.safeWriteFile(validatedSampleFilePath, JSON.stringify(sampleTranslations, null, 2), process.cwd()); if (success) { console.log(this.ui.t('init.createdSampleTranslationFile', { file: validatedSampleFilePath })); SecurityUtils.logSecurityEvent('Sample translation file created', 'info', { file: validatedSampleFilePath }); } else { SecurityUtils.logSecurityEvent('Failed to create sample translation file', 'error', { file: validatedSampleFilePath }); throw new Error(this.t('validate.failedToCreateSampleTranslationFile') || 'Failed to create sample translation file'); } } // Check if source directory and language exist validateSource() { if (!fs.existsSync(this.sourceDir)) { throw new Error(this.t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceDir }) || `Source directory not found: ${this.sourceDir}`); } if (!fs.existsSync(this.sourceLanguageDir)) { throw new Error(this.t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceLanguageDir }) || `Source language directory not found: ${this.sourceLanguageDir}`); } return true; } // Get all JSON files from source language directory getSourceFiles() { const files = fs.readdirSync(this.sourceLanguageDir) .filter(file => { return file.endsWith('.json') && !this.config.excludeFiles.includes(file); }); if (files.length === 0) { throw new Error(this.t('validate.noJsonFilesFound', { sourceDir: this.sourceLanguageDir }) || `No JSON files found in source directory: ${this.sourceLanguageDir}`); } return files; } // Recursively mark all string values as not translated markAsNotTranslated(obj) { if (typeof obj === 'string') { return this.config.notTranslatedMarker; } if (Array.isArray(obj)) { return obj.map(item => this.markAsNotTranslated(item)); } if (obj && typeof obj === 'object') { const result = {}; for (const [key, value] of Object.entries(obj)) { result[key] = this.markAsNotTranslated(value); } return result; } return obj; } // Create or update a language file securely async createLanguageFile(sourceFile, targetLanguage, sourceContent) { const targetDir = path.join(this.sourceDir, targetLanguage); const targetFile = path.join(targetDir, sourceFile); // Validate paths const validatedTargetDir = SecurityUtils.validatePath(targetDir, this.sourceDir); const validatedTargetFile = SecurityUtils.validatePath(targetFile, this.sourceDir); if (!validatedTargetDir || !validatedTargetFile) { SecurityUtils.logSecurityEvent('Invalid path detected in createLanguageFile', 'error', { targetDir, targetFile }); throw new Error(this.t('validate.invalidFilePathDetected') || 'Invalid file path detected'); } // Create target directory if it doesn't exist if (!fs.existsSync(validatedTargetDir)) { fs.mkdirSync(validatedTargetDir, { recursive: true }); } let targetContent; // If target file exists, preserve existing translations if (fs.existsSync(validatedTargetFile)) { try { const existingContent = await SecurityUtils.safeReadFile(validatedTargetFile, this.sourceDir); if (existingContent) { targetContent = this.mergeTranslations(sourceContent, JSON.parse(existingContent)); } else { targetContent = this.markAsNotTranslated(sourceContent); } } catch (error) { console.warn(`⚠️ Warning: Could not parse existing file ${validatedTargetFile}, creating new one`); SecurityUtils.logSecurityEvent('File parse error', 'warn', { file: validatedTargetFile, error: error.message }); targetContent = this.markAsNotTranslated(sourceContent); } } else { targetContent = this.markAsNotTranslated(sourceContent); } // Write the file securely const success = await SecurityUtils.safeWriteFile(validatedTargetFile, JSON.stringify(targetContent, null, 2), this.sourceDir); if (!success) { SecurityUtils.logSecurityEvent('Failed to write language file', 'error', { file: validatedTargetFile }); throw new Error(this.t('validate.failedToWriteFile', { filePath: validatedTargetFile }) || `Failed to write file: ${validatedTargetFile}`); } SecurityUtils.logSecurityEvent('Language file created/updated', 'info', { file: validatedTargetFile, language: targetLanguage }); return validatedTargetFile; } // Merge existing translations with new structure mergeTranslations(sourceObj, existingObj) { if (typeof sourceObj === 'string') { // If existing translation exists and is not the marker, keep it if (typeof existingObj === 'string' && existingObj !== this.config.notTranslatedMarker && existingObj.trim() !== '') { return existingObj; } return this.config.notTranslatedMarker; } if (Array.isArray(sourceObj)) { return sourceObj.map((item, index) => { const existingItem = Array.isArray(existingObj) ? existingObj[index] : undefined; return this.mergeTranslations(item, existingItem); }); } if (sourceObj && typeof sourceObj === 'object') { const result = {}; for (const [key, value] of Object.entries(sourceObj)) { const existingValue = existingObj && typeof existingObj === 'object' ? existingObj[key] : undefined; result[key] = this.mergeTranslations(value, existingValue); } return result; } return sourceObj; } // Get translation statistics getTranslationStats(obj) { let total = 0; let translated = 0; const count = (item) => { if (typeof item === 'string') { total++; if (item !== this.config.notTranslatedMarker && item.trim() !== '') { translated++; } } else if (Array.isArray(item)) { item.forEach(count); } else if (item && typeof item === 'object') { Object.values(item).forEach(count); } }; count(obj); return { total, translated, percentage: total > 0 ? Math.round((translated / total) * 100) : 0, missing: total - translated }; } // Interactive admin PIN setup async promptAdminPinSetup() { const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true, historySize: 0 }); const question = (query) => new Promise(resolve => rl.question(query, resolve)); console.log('\n' + this.ui.t('init.adminPinSetupOptional')); console.log(this.ui.t('init.adminPinSeparator')); console.log(this.ui.t('init.adminPinDescription1')); console.log(this.ui.t('init.adminPinDescription2')); console.log(this.ui.t('init.adminPinDescription3')); console.log(this.ui.t('init.adminPinDescription4')); const setupPin = await question('\n' + this.ui.t('init.adminPinSetupPrompt')); if (setupPin.toLowerCase() === 'y' || setupPin.toLowerCase() === 'yes') { try { const adminAuth = new AdminAuth(); // Enable admin PIN in settings settingsManager.setSecurity({ adminPinEnabled: true, adminPinPromptOnInit: true }); console.log('\n' + this.ui.t('init.settingUpAdminPin')); let pin1, pin2; do { pin1 = await question(this.ui.t('init.enterAdminPin')); if (!/^\d{4,8}$/.test(pin1)) { console.log(this.ui.t('init.pinMustBe4To8Digits')); continue; } pin2 = await question(this.ui.t('init.confirmAdminPin')); if (pin1 !== pin2) { console.log(this.ui.t('init.pinsDoNotMatch')); } } while (pin1 !== pin2 || !/^\d{4,8}$/.test(pin1)); await adminAuth.setupPin(pin1); console.log(this.ui.t('init.adminPinSetupSuccess')); console.log(this.ui.t('init.adminProtectionEnabled')); } catch (error) { console.error(this.ui.t('init.errorSettingUpAdminPin', { error: error.message })); console.log(this.ui.t('init.continuingWithoutAdminPin')); } } else { console.log(this.ui.t('init.skippingAdminPinSetup')); } rl.close(); } // Interactive language selection async selectLanguages() { // Use the global readline interface if available, otherwise create one let rl = this.rl; let shouldClose = false; if (!rl) { const readline = require('readline'); rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true, historySize: 0 }); shouldClose = true; } const question = (query) => new Promise(resolve => rl.question(query, resolve)); console.log('\n' + this.ui.t('init.languageSelectionTitle')); console.log(this.ui.t('common.separator')); console.log(this.ui.t('language.available')); Object.entries(LANGUAGE_CONFIG).forEach(([code, config], index) => { console.log(` ${(index + 1).toString().padStart(2)}. ${code} - ${config.name} (${config.nativeName})`); }); console.log('\n' + this.ui.t('init.defaultLanguages', { languages: this.config.defaultLanguages.join(', ') })); const answer = await question('\n' + this.ui.t('init.enterLanguageCodes')); // Only close if we created our own readline interface if (shouldClose) { rl.close(); } if (answer.trim() === '') { return this.config.defaultLanguages; } const selectedLanguages = answer.split(',').map(lang => lang.trim().toLowerCase()); const validLanguages = selectedLanguages.filter(lang => LANGUAGE_CONFIG[lang]); const invalidLanguages = selectedLanguages.filter(lang => !LANGUAGE_CONFIG[lang]); if (invalidLanguages.length > 0) { console.warn(this.ui.t('init.warningInvalidLanguageCodes', { languages: invalidLanguages.join(', ') })); } return validLanguages.length > 0 ? validLanguages : this.config.defaultLanguages; } // Main initialization process async init() { try { console.log(this.ui.t('init.initializationTitle')); console.log(this.ui.t('common.separator')); // Parse command line arguments const args = this.parseArgs(); if (args.sourceDir) this.config.sourceDir = args.sourceDir; if (args.sourceLanguage) this.config.sourceLanguage = args.sourceLanguage; // Update paths this.sourceDir = path.resolve(this.config.sourceDir); this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage); console.log(this.ui.t('init.sourceDirectoryLabel', { dir: this.sourceDir })); console.log(this.ui.t('init.sourceLanguageLabel', { language: this.config.sourceLanguage })); // Check i18n dependencies first and exit if user chooses not to continue const hasI18n = await this.checkI18nDependencies(); if (!hasI18n) { console.log(this.ui.t('init.errors.noFramework')); console.log(this.ui.t('init.suggestions.installFramework')); if (this.shouldCloseRL) { this.rl.close(); global.activeReadlineInterface = null; } process.exit(0); } // Call the enhanced initialize method with args await this.initialize(hasI18n, args); } catch (error) { console.error(this.ui.t('init.errors.initializationFailed', { error: error.message })); throw error; } finally { if (this.shouldCloseRL && this.rl) { this.rl.close(); global.activeReadlineInterface = null; } } } // Enhanced initialization with dependency checking async initialize(hasI18n = true, args = {}) { console.log(this.ui.t('init.initializingProject')); if (!hasI18n) { console.log(this.ui.t('init.warningProceedingWithoutFramework')); console.log(this.ui.t('init.translationFilesCreatedWarning')); } // Continue with existing initialization logic await this.setupInitialStructure(); // Validate source this.validateSource(); // Prompt for admin PIN setup if not already configured const securitySettings = settingsManager.getSecurity(); if (!securitySettings.adminPinEnabled && securitySettings.adminPinPromptOnInit !== false) { await this.promptAdminPinSetup(); } // Get target languages - use args.languages if provided const targetLanguages = args.languages || await this.selectLanguages(); if (targetLanguages.length === 0) { console.log(this.ui.t('init.noTargetLanguagesSpecified')); return; } console.log('\n' + this.ui.t('init.targetLanguages', { languages: targetLanguages.map(lang => `${lang} (${LANGUAGE_CONFIG[lang]?.name || 'Unknown'})`).join(', ') })); // Get source files const sourceFiles = this.getSourceFiles(); console.log('\n' + this.ui.t('init.foundSourceFiles', { count: sourceFiles.length })); // Process each language const results = {}; for (const targetLanguage of targetLanguages) { console.log('\n' + this.ui.t('init.processingLanguage', { language: targetLanguage, name: LANGUAGE_CONFIG[targetLanguage]?.name || 'Unknown' })); const languageResults = { files: [], totalStats: { total: 0, translated: 0, missing: 0 } }; for (const sourceFile of sourceFiles) { const sourceFilePath = path.join(this.sourceLanguageDir, sourceFile); const validatedSourceFilePath = SecurityUtils.validatePath(sourceFilePath, process.cwd()); if (!validatedSourceFilePath) { SecurityUtils.logSecurityEvent('Invalid source file path', 'error', { path: sourceFilePath }); continue; } const sourceContentRaw = await SecurityUtils.safeReadFile(validatedSourceFilePath, process.cwd()); if (!sourceContentRaw) { SecurityUtils.logSecurityEvent('Failed to read source file', 'error', { file: validatedSourceFilePath }); continue; } const sourceContent = JSON.parse(sourceContentRaw); const targetFilePath = await this.createLanguageFile(sourceFile, targetLanguage, sourceContent); // Get stats for this file const targetContentRaw = await SecurityUtils.safeReadFile(targetFilePath, process.cwd()); if (!targetContentRaw) { SecurityUtils.logSecurityEvent('Failed to read target file for stats', 'error', { file: targetFilePath }); continue; } const targetContent = JSON.parse(targetContentRaw); const stats = this.getTranslationStats(targetContent); languageResults.files.push({ name: sourceFile, path: targetFilePath, stats }); // Add to total stats languageResults.totalStats.total += stats.total; languageResults.totalStats.translated += stats.translated; languageResults.totalStats.missing += stats.missing; console.log(this.ui.t('init.fileProcessingResult', { file: sourceFile, translated: stats.translated, total: stats.total, percentage: stats.percentage })); } // Calculate overall percentage languageResults.totalStats.percentage = languageResults.totalStats.total > 0 ? Math.round((languageResults.totalStats.translated / languageResults.totalStats.total) * 100) : 0; results[targetLanguage] = languageResults; console.log(this.ui.t('init.overallProgress', { translated: languageResults.totalStats.translated, total: languageResults.totalStats.total, percentage: languageResults.totalStats.percentage })); } // Summary report console.log('\n' + '=' .repeat(50)); console.log(this.ui.t('init.initializationSummaryTitle')); console.log(this.ui.t('common.separator')); Object.entries(results).forEach(([lang, data]) => { const langName = LANGUAGE_CONFIG[lang]?.name || 'Unknown'; const statusIcon = data.totalStats.percentage === 100 ? '✅' : data.totalStats.percentage >= 80 ? '🟡' : '🔴'; console.log(this.ui.t('init.languageSummary', { icon: statusIcon, name: langName, code: lang, percentage: data.totalStats.percentage })); console.log(this.ui.t('init.languageFiles', { count: data.files.length })); console.log(this.ui.t('init.languageKeys', { translated: data.totalStats.translated, total: data.totalStats.total })); console.log(this.ui.t('init.languageMissing', { count: data.totalStats.missing })); }); console.log('\n' + this.ui.t('init.initializationCompletedSuccessfully')); console.log('\n' + this.ui.t('init.nextStepsTitle')); console.log(this.ui.t('init.nextStep1')); console.log(this.ui.t('init.nextStep2')); console.log(this.ui.t('init.nextStep3')); } // Run the initialization process with admin authentication async run() { try { // Parse command line arguments const args = this.parseArgs(); // Override config with command line arguments if (args.languages) { this.config.defaultLanguages = args.languages; } if (args.sourceDir) { this.config.sourceDir = args.sourceDir; this.sourceDir = path.resolve(this.config.sourceDir); this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage); } if (args.sourceLanguage) { this.config.sourceLanguage = args.sourceLanguage; this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage); } // Check admin authentication for sensitive operations (only when called directly and not in no-prompt mode) const AdminAuth = require('../utils/admin-auth'); const adminAuth = new AdminAuth(); await adminAuth.initialize(); const isCalledDirectly = require.main === module; const isRequired = await adminAuth.isAuthRequired(); if (isRequired && isCalledDirectly && !args.noPrompt) { console.log('\n' + this.ui.t('adminCli.authRequiredForOperation', { operation: 'initialize i18n project' })); const pin = await this.prompt(this.t('adminCli.enterPin')); const isValid = await adminAuth.verifyPin(pin); if (!isValid) { console.log(this.ui.t('adminCli.invalidPin')); if (this.shouldCloseRL) { this.rl.close(); global.activeReadlineInterface = null; } process.exit(1); } console.log(this.ui.t('adminCli.authenticationSuccess')); } return await this.init(); } catch (error) { console.error(this.ui.t('common.initializationFailed', { error: error.message })); throw error; } } } module.exports = I18nInitializer; // Run if called directly if (require.main === module) { const initializer = new I18nInitializer(); initializer.init().then(() => { process.exit(0); }).catch(error => { console.error(this.ui.t('common.initializationFailed', { error: error.message })); console.error(this.ui.t('common.stackTrace', { stack: error.stack })); process.exit(1); }); }