UNPKG

i18ntk

Version:

🚀 The fastest i18n toolkit with 97% performance boost! Zero-dependency, enterprise-grade internationalization for React, Vue, Angular, Python, Java, PHP & more. Features PIN protection, auto framework detection, 7+ UI languages, and comprehensive transla

1,244 lines (1,053 loc) 51.2 kB
#!/usr/bin/env node /** * I18NTK 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 configManager = require('../utils/config-manager'); const SecurityUtils = require('../utils/security'); const AdminAuth = require('../utils/admin-auth'); const { loadTranslations, t } = require('../utils/i18n-helper'); const { detectFramework } = require('../utils/framework-detector'); const { getFormatAdapter } = require('../utils/format-manager'); // Ensure UIi18n is available for this initializer class const UIi18n = require('./i18ntk-ui'); loadTranslations(process.env.I18NTK_LANG); const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/config-helper'); const { showFrameworkWarningOnce } = require('../utils/cli-helper'); const { createPrompt, isInteractive } = require('../utils/prompt-helper'); // 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 = { sourceLanguage: 'en', excludeFiles: ['.DS_Store', 'Thumbs.db'], supportedExtensions: ['.json'], // Default structure: modular (folder per language) structure: 'modular', // one of: 'single' | 'modular' | 'existing' perLanguageStructure: {}, // optional map lang -> 'single' | 'modular' noPrompt: false, ...config }; this.format = getFormatAdapter(this.config.format); this.config.supportedExtensions = [this.format.extension]; this.detectedFramework = detectFramework(process.cwd()); if (this.detectedFramework && !this.config.translationPatterns) { this.config.translationPatterns = this.detectedFramework.patterns; } this.sourceDir = this.config.sourceDir || './locales'; // Source language directory depends on structure this.sourceLanguageDir = this.config.structure === 'single' ? this.sourceDir : path.join(this.sourceDir, this.config.sourceLanguage); // Ensure defaultLanguages is properly initialized from config this.config.defaultLanguages = this.config.defaultLanguages || ['de', 'es', 'fr', 'ru']; // No longer create readline interface here - use CLI helpers this.rl = null; this.shouldCloseRL = false; this.announcedExistingDir = false; this.promptInstance = null; } // Updated checkI18nDependencies method that uses configuration async checkI18nDependencies(noPrompt = false) { const packageJsonPath = path.resolve('./package.json'); if (!fs.existsSync(packageJsonPath)) { console.log(t('errors.noPackageJson')); return true; // Allow to continue without framework } 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(t('init.detectedI18nFrameworks', { frameworks: installedFrameworks.join(', ') })); const cfg = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); cfg.framework = cfg.framework || {}; cfg.framework.detected = true; cfg.framework.installed = installedFrameworks; if (configManager.saveSettings) { configManager.saveSettings(cfg); } else if (configManager.saveConfig) { configManager.saveConfig(cfg); } return true; } else { const cfg = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); if (cfg.framework) { cfg.framework.detected = false; if (configManager.saveSettings) { configManager.saveSettings(cfg); } else if (configManager.saveConfig) { configManager.saveConfig(cfg); } } // Framework detection is now handled by maybePromptFramework in i18ntk-manage.js // Skip prompting here to avoid double prompts return true; } } catch (error) { console.log(t('init.errors.packageJsonRead')); return true; // Allow to continue on error } } // Add the missing promptContinueWithoutI18n method async promptContinueWithoutI18n(noPrompt = false) { if (!isInteractive({ noPrompt: noPrompt || this.config.noPrompt })) { return true; } const answer = await this.prompt('\n' + t('init.continueWithoutI18nPrompt')); return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'; } // Add the missing prompt method async prompt(question) { if (!this.promptInstance) { this.promptInstance = createPrompt({ noPrompt: this.config.noPrompt }); } if (!isInteractive({ noPrompt: this.config.noPrompt })) { return ''; } return this.promptInstance.question(question); } // 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; } else if (key === 'yes') { parsed.yes = true; } } }); if (!process.stdin.isTTY) { parsed.yes = true; } if (parsed.yes) { parsed.noPrompt = true; } return parsed; } // Detect existing translation directories and allow user selection async detectAndSelectDirectory(skipPrompt = false) { 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 englishFormats = ['en', 'en-US', 'en-GB', 'english']; // Check for English directories first for (const format of englishFormats) { const englishPath = path.join(location, format); if (fs.existsSync(englishPath) && fs.statSync(englishPath).isDirectory()) { const englishFiles = fs.readdirSync(englishPath).filter(file => file.endsWith(this.format.extension)); if (englishFiles.length > 0) { // Found English files, prioritize this existingLocations.unshift(location); break; } } } // Also check for any language directories or format files const hasLanguageDirs = items.some(item => { const itemPath = path.join(location, item); if (fs.statSync(itemPath).isDirectory()) { return ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh', 'en-US', 'en-GB'].includes(item); } return item.endsWith(this.format.extension); }); if (hasLanguageDirs && !existingLocations.includes(location)) { existingLocations.push(location); } } catch (error) { // Continue checking other locations } } } if (existingLocations.length > 0) { if (skipPrompt || !process.stdin.isTTY) { const selectedDir = existingLocations[0]; this.config.sourceDir = selectedDir; this.sourceDir = path.resolve(selectedDir); this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage); const rel = configManager.toRelative(selectedDir); await configManager.updateConfig({ sourceDir: rel, i18nDir: rel }); return selectedDir; } console.log('\n' + t('init.existingDirectoriesFound')); console.log(t('common.separator')); // List existing locations existingLocations.forEach((location, index) => { console.log(` ${index + 1}. ${location}`); }); // Add options for new directory and exit console.log(` ${existingLocations.length + 1}. Create new directory`); console.log(` 0. Exit`); let answer; let selectedIndex; // Keep asking until we get a valid number while (true) { answer = await this.prompt('\n' + t('init.selectDirectoryPrompt') + ' (0-' + (existingLocations.length + 1) + '):'); // Check for exit (0) if (answer === '0') { console.log(t('init.initializationCancelled')); process.exit(0); } // Parse the selection selectedIndex = parseInt(answer) - 1; // Validate the selection if (!isNaN(selectedIndex) && selectedIndex >= 0 && selectedIndex <= existingLocations.length) { break; } console.log(t('errors.invalidOption', { option: answer })); } if (selectedIndex >= 0 && selectedIndex < existingLocations.length) { const selectedDir = existingLocations[selectedIndex]; if (!this.announcedExistingDir) { console.log(t('init.usingExistingDirectory', { dir: selectedDir })); this.announcedExistingDir = true; } this.config.sourceDir = selectedDir; this.sourceDir = path.resolve(selectedDir); this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage); const rel = configManager.toRelative(selectedDir); await configManager.updateConfig({ sourceDir: rel, i18nDir: rel }); return selectedDir; } else if (selectedIndex === existingLocations.length) { const newDirName = await this.prompt('\n' + t('init.enterNewDirectoryName') + ': '); if (newDirName && newDirName.trim()) { const newDirPath = path.resolve(newDirName.trim()); if (!fs.existsSync(newDirPath)) { fs.mkdirSync(newDirPath, { recursive: true }); console.log(t('init.createdNewDirectory', { dir: newDirPath })); } else { console.log(t('init.directoryAlreadyExists', { dir: newDirPath })); } const sourceLangDir = path.join(newDirPath, this.config.sourceLanguage); if (!fs.existsSync(sourceLangDir)) { fs.mkdirSync(sourceLangDir, { recursive: true }); console.log(t('init.createdSourceLanguageDirectory', { dir: sourceLangDir })); await this.createSampleTranslationFile(sourceLangDir); } this.config.sourceDir = newDirPath; this.sourceDir = newDirPath; this.sourceLanguageDir = sourceLangDir; const rel = configManager.toRelative(newDirPath); await configManager.updateConfig({ sourceDir: rel, i18nDir: rel }); return newDirPath; } else { console.log(t('init.invalidDirectoryName')); return null; } } } if (skipPrompt || !process.stdin.isTTY) { return null; } return null; } // Setup initial directory structure if needed async setupInitialStructure(skipPrompt = false) { // First, detect if there are existing translation directories with English files const usedExisting = await this.detectAndSelectDirectory(skipPrompt); if (usedExisting) { console.log(t('init.usingExistingDirectory', { dir: this.sourceDir })); // When using existing, sourceLanguageDir might already be set to English directory return; } // Validate paths const validatedSourceDir = SecurityUtils.validatePath(this.sourceDir, process.cwd()); if (!validatedSourceDir) { throw new Error(t('validate.invalidSourceDirectory', { sourceDir: this.sourceDir }) || `Invalid source directory: ${this.sourceDir}`); } // For modular structure, ensure per-language subdirectory exists let validatedSourceLanguageDir = this.sourceLanguageDir; if (this.config.structure !== 'single') { validatedSourceLanguageDir = SecurityUtils.validatePath(this.sourceLanguageDir, process.cwd()); if (!validatedSourceLanguageDir) { throw new Error(t('validate.invalidSourceLanguageDirectory', { sourceDir: this.sourceLanguageDir }) || `Invalid source language directory: ${this.sourceLanguageDir}`); } } // Create directories if they do not exist if (!fs.existsSync(validatedSourceDir)) { fs.mkdirSync(validatedSourceDir, { recursive: true }); } if (this.config.structure !== 'single' && !fs.existsSync(validatedSourceLanguageDir)) { fs.mkdirSync(validatedSourceLanguageDir, { recursive: true }); } // Create sample translation file if none exist const englishFiles = (this.config.structure === 'single' ? fs.readdirSync(validatedSourceDir) : fs.readdirSync(validatedSourceLanguageDir)) .filter(file => file.endsWith(this.format.extension)); if (englishFiles.length === 0) { await this.createSampleTranslationFile(this.config.structure === 'single' ? validatedSourceDir : validatedSourceLanguageDir); } else { // Directory exists, check if we need to create a sample file const existingFiles = (this.config.structure === 'single' ? fs.readdirSync(validatedSourceDir) : fs.readdirSync(validatedSourceLanguageDir)) .filter(file => file.endsWith(this.format.extension)); if (existingFiles.length === 0) { // No format files exist, create sample file await this.createSampleTranslationFile(this.config.structure === 'single' ? validatedSourceDir : validatedSourceLanguageDir); } } const rel = configManager.toRelative(this.sourceDir); await configManager.updateConfig({ sourceDir: rel, i18nDir: rel }); } // 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${this.format.extension}`); const i18ntkCommonFilePath = path.join(validatedSourceLanguageDir, `i18ntk-common${this.format.extension}`); 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(t('validate.invalidSampleFilePath') || 'Invalid sample file path'); } const success = await SecurityUtils.safeWriteFile(validatedSampleFilePath, this.format.serialize(sampleTranslations), process.cwd()); if (success) { console.log(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(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(t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceDir }) || `Source directory not found: ${this.sourceDir}`); } if (!fs.existsSync(this.sourceLanguageDir)) { throw new Error(t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceLanguageDir }) || `Source language directory not found: ${this.sourceLanguageDir}`); } return true; } // Get all JSON files from source language directory (supports single/modular) getSourceFiles() { try { if (this.config.structure === 'single') { if (!fs.existsSync(this.sourceDir)) { throw new Error(t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceDir }) || `Source directory not found: ${this.sourceDir}`); } const files = fs.readdirSync(this.sourceDir) .filter(file => file.endsWith(this.format.extension) && !this.config.excludeFiles.includes(file)); if (files.length === 0) { throw new Error(t('validate.noJsonFilesFound', { sourceDir: this.sourceDir }) || `No JSON files found in source directory: ${this.sourceDir}`); } return files; } if (!fs.existsSync(this.sourceLanguageDir)) { // Try to find English files in parent directory or subdirectories const parentDir = path.dirname(this.sourceLanguageDir); if (fs.existsSync(parentDir)) { const subdirs = fs.readdirSync(parentDir).filter(item => { const fullPath = path.join(parentDir, item); return fs.statSync(fullPath).isDirectory(); }); // Look for English files in any subdirectory for (const subdir of subdirs) { const englishDir = path.join(parentDir, subdir); if (fs.existsSync(englishDir)) { const files = fs.readdirSync(englishDir); const formatFiles = files.filter(file => file.endsWith(this.format.extension) && !this.config.excludeFiles.includes(file) ); if (formatFiles.length > 0) { // Found English files, use this directory this.sourceLanguageDir = englishDir; return formatFiles; } } } } throw new Error(t('validate.noJsonFilesFound', { sourceDir: this.sourceLanguageDir }) || `No JSON files found in source directory: ${this.sourceLanguageDir}`); } const files = fs.readdirSync(this.sourceLanguageDir) .filter(file => { return file.endsWith(this.format.extension) && !this.config.excludeFiles.includes(file); }); if (files.length === 0) { throw new Error(t('validate.noJsonFilesFound', { sourceDir: this.sourceLanguageDir }) || `No JSON files found in source directory: ${this.sourceLanguageDir}`); } return files; } catch (error) { console.warn(t('init.warningCannotReadSourceDir', { dir: this.sourceLanguageDir, error: error.message })); throw error; } } // Recursively mark all string values with country code markers markWithCountryCode(obj, countryCode) { if (typeof obj === 'string') { return `[${countryCode.toUpperCase()}] ${obj}`; } if (Array.isArray(obj)) { return obj.map(item => this.markWithCountryCode(item, countryCode)); } if (obj && typeof obj === 'object') { const result = {}; for (const [key, value] of Object.entries(obj)) { result[key] = this.markWithCountryCode(value, countryCode); } return result; } return obj; } // Get the structure type for a specific language getLanguageStructure(language) { // Check for per-language structure first if (this.config.perLanguageStructure && this.config.perLanguageStructure[language]) { return this.config.perLanguageStructure[language]; } // Fall back to the global structure return this.config.structure || 'modular'; } // Create or update a language file securely (supports single/modular) async createLanguageFile(sourceFile, targetLanguage, sourceContent) { try { const sourceFilePath = path.join(this.sourceLanguageDir, sourceFile); let targetFilePath; if (this.getLanguageStructure(targetLanguage) === 'single') { // Single-file per language, write to <lang>.json in sourceDir const baseName = `${targetLanguage}${this.format.extension}`; targetFilePath = path.join(this.sourceDir, baseName); } else { // Modular: folder per language mirroring source file const targetDir = path.join(this.sourceDir, targetLanguage); if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true }); targetFilePath = path.join(targetDir, sourceFile); } // Validate source and target paths const validatedSourcePath = SecurityUtils.validatePath(sourceFilePath, process.cwd()); const validatedTargetPath = SecurityUtils.validatePath(targetFilePath, process.cwd()); const targetDir = path.dirname(validatedTargetPath); if (!validatedSourcePath || !validatedTargetPath) { SecurityUtils.logSecurityEvent('Invalid path detected in createLanguageFile', 'error', { sourcePath: sourceFilePath, targetPath: targetFilePath }); throw new Error(t('validate.invalidFilePathDetected') || 'Invalid file path detected'); } // Create target directory if it doesn't exist if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } let targetContent; // If target file exists, preserve existing translations if (fs.existsSync(validatedTargetPath)) { try { const existingContent = await SecurityUtils.safeReadFile(validatedTargetPath, process.cwd()); if (existingContent) { targetContent = this.mergeTranslations(sourceContent, this.format.read(existingContent), targetLanguage); } else { targetContent = this.markWithCountryCode(sourceContent, targetLanguage); } } catch (error) { console.warn(`⚠️ Warning: Could not parse existing file ${validatedTargetPath}, creating new one`); SecurityUtils.logSecurityEvent('File parse error', 'warn', { file: validatedTargetPath, error: error.message }); targetContent = this.markWithCountryCode(sourceContent, targetLanguage); } } else { targetContent = this.markWithCountryCode(sourceContent, targetLanguage); } // Write the file securely const success = await SecurityUtils.safeWriteFile(validatedTargetPath, this.format.serialize(targetContent), process.cwd()); if (!success) { SecurityUtils.logSecurityEvent('Failed to write language file', 'error', { file: validatedTargetPath }); throw new Error(t('validate.failedToWriteFile', { filePath: validatedTargetPath }) || `Failed to write file: ${validatedTargetPath}`); } SecurityUtils.logSecurityEvent('Language file created/updated', 'info', { file: validatedTargetPath, language: targetLanguage }); return validatedTargetPath; } catch (error) { console.error(t('init.errors.initializationFailed', { error: error.message })); return false; } } // Merge existing translations with new structure using country code markers mergeTranslations(sourceObj, existingObj, countryCode) { if (typeof sourceObj === 'string') { // If existing translation exists and doesn't contain country code marker, keep it if (typeof existingObj === 'string' && !existingObj.startsWith(`[${countryCode.toUpperCase()}]`) && existingObj.trim() !== '') { return existingObj; } return this.markWithCountryCode(sourceObj, countryCode); } if (Array.isArray(sourceObj)) { return sourceObj.map((item, index) => { const existingItem = Array.isArray(existingObj) ? existingObj[index] : undefined; return this.mergeTranslations(item, existingItem, countryCode); }); } 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, countryCode); } return result; } return sourceObj; } // Get translation statistics getTranslationStats(obj) { let total = 0; let translated = 0; let missing = 0; const count = (item) => { if (typeof item === 'string') { total++; const isCountryCodeMarker = /^\[([A-Z]{2})\]/.test(item); if (isCountryCodeMarker) { missing++; } else if (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 }; } // Interactive admin PIN setup async promptAdminPinSetup() { const { ask, askHidden, flushStdout } = require('../utils/cli'); console.log('\n' + t('init.adminPinSetupOptional')); console.log(t('init.adminPinSeparator')); console.log(t('init.adminPinDescription1')); console.log(t('init.adminPinDescription2')); console.log(t('init.adminPinDescription3')); console.log(t('init.adminPinDescription4')); await flushStdout(); const enableProtection = await ask('\n' + t('adminPin.setup_prompt')); if (enableProtection.toLowerCase() === 'y' || enableProtection.toLowerCase() === 'yes') { try { const adminAuth = new AdminAuth(); await adminAuth.initialize(); let pin = null; do { pin = await askHidden(t('init.enterAdminPin')); if (!/^\d{4}$/.test(pin)) { console.log(t('init.pinMustBe4Digits')); pin = null; continue; } const confirm = await askHidden(t('init.confirmAdminPin')); if (pin !== confirm) { console.log(t('init.pinMismatch')); pin = null; } } while (!pin); const saved = await adminAuth.setupPin(pin); if (saved) { await configManager.updateConfig({ security: { adminPinEnabled: true, adminPinPromptOnInit: true, pinProtection: { enabled: true } } }); console.log(t('init.adminPinSetupSuccess')); } else { console.error(t('init.errorSettingUpAdminPin', { error: 'Failed to save PIN' })); } } catch (error) { console.error(t('init.errorSettingUpAdminPin', { error: error.message })); console.log(t('init.continuingWithoutAdminPin')); } } else { console.log(t('init.skippingAdminPinSetup')); } } // Interactive language selection async selectLanguages(skipPrompt = false) { if (skipPrompt || !process.stdin.isTTY) { return this.config.defaultLanguages; } const { ask } = require('../utils/cli'); console.log('\n' + t('init.languageSelectionTitle')); console.log(t('common.separator')); console.log(t('init.available')); Object.entries(LANGUAGE_CONFIG).forEach(([code, config], index) => { console.log(` ${(index + 1).toString().padStart(2)}. ${code} - ${config.name} (${config.nativeName})`); }); console.log('\n' + t('init.defaultLanguages', { languages: this.config.defaultLanguages.join(', ') })); const answer = await ask('\n' + t('init.enterLanguageCodes')); 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(t('init.warningInvalidLanguageCodes', { languages: invalidLanguages.join(', ') })); } return validLanguages.length > 0 ? validLanguages : this.config.defaultLanguages; } // Interactive setup configuration (internationalized) async promptSetupConfiguration(skipPrompt = false) { if (skipPrompt || !process.stdin.isTTY) { return { structure: 'modular', duplicateStructure: true }; } const { ask } = require('../utils/cli'); console.log('\n' + t('init.setup.title')); console.log(t('common.separator')); // Determine recommended option const recommended = ' (recommended)'; console.log(t('init.setup.question')); console.log(' 1. ' + t('init.setup.opt_single') + (this.config.structure === 'single' ? recommended : '')); console.log(' 2. ' + t('init.setup.opt_modular') + (this.config.structure !== 'single' ? recommended : '')); console.log(' 3. ' + t('init.setup.opt_existing')); const structureChoice = await ask('\n' + t('init.setup.choice_prompt')); let structure = 'modular'; if (structureChoice === '1') structure = 'single'; else if (structureChoice === '2') structure = 'modular'; else structure = 'existing'; let duplicateStructure = true; let perLanguage = []; if (structure !== 'existing') { const duplicateChoice = await ask('\n' + t('init.setup.apply_all_prompt')); duplicateStructure = duplicateChoice.toLowerCase() === 'y' || duplicateChoice.toLowerCase() === 'yes'; if (!duplicateStructure) { // Prompt for languages to include/exclude console.log(t('init.setup.per_language_intro')); const available = Object.keys(LANGUAGE_CONFIG).join(', '); console.log(t('init.setup.available_languages', { languages: available })); const includeAns = await ask(t('init.setup.include_prompt')); perLanguage = includeAns.split(',').map(l => l.trim().toLowerCase()).filter(Boolean); } } return { structure, duplicateStructure, perLanguage }; } // Enhanced initialization with dependency checking async initialize(hasI18n = true, args = {}) { console.log(t('init.initializingProject')); if (!hasI18n) { console.log(t('init.warningProceedingWithoutFramework')); console.log(t('init.translationFilesCreatedWarning')); } // Get setup configuration const setupConfig = await this.promptSetupConfiguration(args.noPrompt); // Handle directory selection and structure setup const selectedDir = await this.detectAndSelectDirectory(args.noPrompt); if (selectedDir) { this.config.sourceDir = selectedDir; this.sourceDir = path.resolve(selectedDir); this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage); if (!this.announcedExistingDir) { console.log(t('init.usingExistingDirectory', { dir: selectedDir })); this.announcedExistingDir = true; } } else { await this.setupInitialStructure(args.noPrompt); } // Validate source this.validateSource(); // Prompt for admin PIN setup if not already configured const securitySettings = configManager.getConfig().security || {}; if (!securitySettings.adminPinEnabled && securitySettings.adminPinPromptOnInit !== false && !args.noPrompt) { const { flushStdout } = require('../utils/cli'); await flushStdout(); await this.promptAdminPinSetup(); } // Get target languages - use args.languages if provided let targetLanguages = args.languages || await this.selectLanguages(args.noPrompt); // Ensure targetLanguages is always an array targetLanguages = Array.isArray(targetLanguages) ? targetLanguages : []; if (targetLanguages.length === 0) { console.log(t('init.noTargetLanguagesSpecified')); return; } console.log('\n' + t('init.targetLanguages', { languages: targetLanguages.map(lang => `${lang} (${LANGUAGE_CONFIG[lang]?.name || 'Unknown'})`).join(', ') })); // Get source files const sourceFiles = this.getSourceFiles(); console.log('\n' + t('init.foundSourceFiles', { count: sourceFiles.length })); // Process each language const results = {}; for (const targetLanguage of targetLanguages) { console.log('\n' + 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 = this.format.read(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 = this.format.read(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(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(t('init.overallProgress', { translated: languageResults.totalStats.translated, total: languageResults.totalStats.total, percentage: languageResults.totalStats.percentage })); } // Generate and display completion summary await this.generateCompletionSummary(results, targetLanguages); console.log('\n' + t('init.initializationCompletedSuccessfully')); console.log('\n' + t('init.nextStepsTitle')); console.log(t('init.nextStep1')); console.log(t('init.nextStep2')); console.log(t('init.nextStep3')); } // Generate completion summary with proper error handling async generateCompletionSummary(results, targetLanguages) { try { console.log('\n' + '='.repeat(50)); console.log(t('init.initializationSummaryTitle')); console.log(t('common.separator')); let totalChanges = 0; let languagesProcessed = 0; let missingKeysAdded = 0; Object.entries(results || {}).forEach(([lang, data]) => { if (!data || typeof data !== 'object') return; const langName = LANGUAGE_CONFIG[lang]?.name || 'Unknown'; const stats = data.totalStats || { total: 0, translated: 0, percentage: 0, missing: 0 }; const statusIcon = stats.percentage === 100 ? '✅' : stats.percentage >= 80 ? '🟡' : '🔴'; console.log( t('init.languageSummary', { icon: statusIcon, name: langName, code: lang, percentage: stats.percentage || 0, }) ); if (data.files && Array.isArray(data.files)) { console.log(t('init.languageFiles', { count: data.files.length })); } console.log( t('init.languageKeys', { translated: stats.translated || 0, total: stats.total || 0, }) ); console.log(t('init.languageMissing', { count: stats.missing || 0 })); totalChanges += (stats.translated || 0) + (stats.missing || 0); languagesProcessed += 1; missingKeysAdded += stats.missing || 0; }); console.log('\n📊 COMPLETION SUMMARY'); console.log(t('common.separator')); console.log(`📝 Total changes: ${totalChanges}`); console.log(`🌍 Languages processed: ${languagesProcessed}`); console.log(`➕ Missing keys added: ${missingKeysAdded}`); if (process.stdin.isTTY && !this.config?.noPrompt) { const { ask } = require('../utils/cli'); const generateReport = await ask('\n🤖 Would you like a report generated? (Y/N): '); if (generateReport.toLowerCase() === 'y' || generateReport.toLowerCase() === 'yes') { await this.generateDetailedReport(results, targetLanguages); } } } catch (error) { console.error('\n❌ Error during completion:', error.message); console.log('📊 COMPLETION SUMMARY (Basic)'); console.log(t('common.separator')); console.log(`🌍 Languages processed: ${Object.keys(results || {}).length}`); } } // Generate detailed report async generateDetailedReport(results, targetLanguages) { try { const outputDir = this.config.outputDir || path.join(process.cwd(), 'i18ntk-reports'); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const reportPath = path.join(outputDir, 'init-report.json'); const report = { timestamp: new Date().toISOString(), languages: targetLanguages, results: results, summary: { languagesProcessed: targetLanguages.length, totalFiles: Object.values(results).reduce((sum, data) => sum + (data.files?.length || 0), 0), totalKeys: Object.values(results).reduce((sum, data) => sum + (data.totalStats?.total || 0), 0), totalMissing: Object.values(results).reduce((sum, data) => sum + (data.totalStats?.missing || 0), 0) } }; await fs.promises.writeFile(reportPath, JSON.stringify(report, null, 2)); console.log(`✅ Report generated: ${reportPath}`); } catch (error) { console.error('❌ Failed to generate report:', error.message); } } // Offer interactive locale optimization after initialization async offerLocaleOptimization() { try { console.log('\n' + '='.repeat(60)); console.log('🎯 **PACKAGE SIZE OPTIMIZATION**'); console.log('='.repeat(60)); try { // Import locale optimizer directly const LocaleOptimizer = require('../scripts/locale-optimizer'); // First run dry run to show current state console.log('\n🔍 Running locale optimization preview...'); const optimizer = new LocaleOptimizer(); await optimizer.run({ dryRun: true }); console.log('\n💡 You can reduce package size by selecting only the languages you need'); const answer = await this.prompt('\n🤖 Would you like to run interactive optimization now? (y/n): '); if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { console.log('\n🚀 Starting interactive locale optimization...'); await optimizer.run({ interactive: true }); console.log('\n✅ Package optimization completed!'); } else { console.log('\n💡 You can run locale optimization later with:'); console.log(' node scripts/locale-optimizer.js --interactive'); } } catch (error) { console.log('\n⚠️ Could not offer locale optimization:', error.message); console.log('\n💡 You can run locale optimization later with:'); console.log(' node scripts/locale-optimizer.js --interactive'); } } catch (error) { console.log('\n⚠️ Could not offer locale optimization:', error.message); } } // Run the initialization process with admin authentication async run(options = {}) { const fromMenu = options.fromMenu || false; try { // Parse command line arguments const args = this.parseArgs(); // On first run, prompt user for preferred UI language if (!fs.existsSync(configManager.CONFIG_PATH)) { const { getGlobalReadline } = require('../utils/cli'); getGlobalReadline(); const selectedLang = await this.ui.selectLanguage(); loadTranslations(selectedLang); } // Initialize configuration properly when called from menu if (fromMenu && !this.sourceDir) { const baseConfig = await getUnifiedConfig('init', args); this.config = { ...baseConfig, ...this.config }; this.sourceDir = this.config.sourceDir; this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage); // Load translations for UI messages const uiLanguage = this.config.uiLanguage || 'en'; const { loadTranslations } = require('../utils/i18n-helper'); loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales')); } // Setup is now handled centrally by config manager // 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 && !fromMenu) { console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'initialize i18n project' })); const cliHelper = require('../utils/cli-helper'); const pin = await cliHelper.promptPin(t('adminCli.enterPin')); const isValid = await this.adminAuth.verifyPin(pin); if (!isValid) { console.log(t('adminCli.invalidPin')); if (!fromMenu) process.exit(1); return; } console.log(t('adminCli.authenticationSuccess')); } // Check i18n dependencies first and exit if user chooses not to continue const hasI18n = await this.checkI18nDependencies(args.noPrompt); if (!hasI18n) { console.log(t('init.errors.noFramework')); console.log(t('init.suggestions.installFramework')); if (this.shouldCloseRL) { this.rl.close(); global.activeReadlineInterface = null; } if (!fromMenu) process.exit(0); return; } // Call the enhanced initialize method with args await this.initialize(hasI18n, args); } catch (error) { console.error(t('init.errors.initializationFailed', { error: error.message })); if (!fromMenu && require.main === module) { process.exit(1); } } finally { // No explicit readline cleanup needed; CLI helpers manage a shared interface } } // Run non-interactive mode for workflow automation async runNonInteractive() { try { const args = { noPrompt: true, ...this.parseArgs() }; // Initialize configuration const baseConfig = await getUnifiedConfig('init', args); this.config = { ...baseConfig, ...this.config }; this.sourceDir = this.config.sourceDir; this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage); // Load translations for UI messages const uiLanguage = this.config.uiLanguage || 'en'; const { loadTranslations } = require('../utils/i18n-helper'); loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales')); // Skip i18n framework check in non-interactive mode console.log('Running initialization in non-interactive mode...'); // Call the enhanced initialize method await this.initialize(true, args); } catch (error) { console.error('Error in non-interactive mode:', error.message); throw error; } finally { // No explicit readline cleanup needed; CLI helpers manage a shared interface } } } module.exports = I18nInitializer; // Run if called directly if (require.main === module) { async function main() { const args = parseCommonArgs(process.argv.slice(2)); const prompt = createPrompt({ noPrompt: args.noPrompt }); try { if (args.help) { displayHelp('i18ntk-init', { 'languages': 'Comma-separated list of target languages', 'source-dir': 'Directory for translation files', 'source-language': 'Source language code', 'no-pro