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,294 lines (1,135 loc) • 60.6 kB
#!/usr/bin/env node /** * I18NTK MANAGEMENT TOOLKIT - MAIN MANAGER * * This is the main entry point for all i18n operations. * It provides an interactive interface to manage translations. * * Usage: * npm run i18ntk:manage * npm run i18ntk:manage -- --command=init * npm run i18ntk:manage -- --command=analyze * npm run i18ntk:manage -- --command=validate * npm run i18ntk:manage -- --command=usage * npm run i18ntk:manage -- --help * * Alternative direct usage: * node i18ntk-manage.js * node i18ntk-manage.js --command=init */ const fs = require('fs'); const path = require('path'); const UIi18n = require('./i18ntk-ui'); const AdminAuth = require('../utils/admin-auth'); const SecurityUtils = require('../utils/security'); const AdminCLI = require('../utils/admin-cli'); const configManager = require('../settings/settings-manager'); const { showFrameworkWarningOnce } = require('../utils/cli-helper'); const I18nInitializer = require('./i18ntk-init'); const { I18nAnalyzer } = require('./i18ntk-analyze'); const I18nValidator = require('./i18ntk-validate'); const I18nUsageAnalyzer = require('./i18ntk-usage'); const I18nSizingAnalyzer = require('./i18ntk-sizing'); const I18nFixer = require('./i18ntk-fixer'); const SettingsCLI = require('../settings/settings-cli'); // const I18nDebugger = require('../scripts/debug/debugger'); const { createPrompt, isInteractive } = require('../utils/prompt-helper'); const { loadTranslations, t, refreshLanguageFromSettings} = require('../utils/i18n-helper'); const cliHelper = require('../utils/cli-helper'); const { loadConfig, saveConfig, ensureConfigDefaults } = require('../utils/config'); const pkg = require('../package.json'); const SetupEnforcer = require('../utils/setup-enforcer'); // Setup check will be handled in the I18nManager.run() method async function runInitFlow() { const initializer = new I18nInitializer(); await initializer.run({ fromMenu: true }); const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); return { i18nDir: settings.i18nDir, sourceDir: settings.sourceDir }; } /** * Ensures the project is properly initialized or exits the process * @param {Object} prompt - Prompt interface for user interaction * @returns {Promise<Object>} Configuration object if initialized */ async function ensureInitializedOrExit(prompt) { const { checkInitialized } = require('../utils/init-helper'); const cliHelper = require('../utils/cli-helper'); const pkg = require('../package.json'); const { initialized, config } = await checkInitialized(); if (!initialized) { console.log('\nThis project is not yet initialized with i18ntk.'); const shouldInitialize = await cliHelper.confirm('Would you like to initialize it now?'); if (!shouldInitialize) { console.log('Exiting. Please initialize the project first.'); process.exit(1); } // The initialization will be handled by the init command return config; } // Check if we need to prompt for framework detection const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); // Ensure framework configuration exists with all required fields if (!settings.framework) { settings.framework = { detected: false, preference: null, prompt: 'always', lastPromptedVersion: null, installed: [], version: '1.0' // Schema version for future compatibility }; } // Check if we need to prompt for framework detection if (!settings.framework.detected && settings.framework.prompt !== 'suppress' && settings.framework.lastPromptedVersion !== pkg.version) { console.log('\nWe noticed you haven\'t set up an i18n framework yet.'); console.log('Would you like to detect your i18n framework automatically?'); console.log('1. Detect automatically'); console.log('2. I\'ll set it up manually'); console.log('3. Don\'t show this again'); const answer = await prompt.question('\nSelect an option (1-3): '); const choice = answer.trim(); let action; if (choice === '1') action = 'detect'; else if (choice === '2') action = 'manual'; else if (choice === '3') action = 'dont-show'; else action = 'manual'; // default fallback if (action === 'dont-show') { // Update settings to suppress future prompts for this version settings.framework.prompt = 'suppress'; settings.framework.lastPromptedVersion = pkg.version; if (configManager.saveSettings) { await configManager.saveSettings(settings); } else if (configManager.saveConfig) { await configManager.saveConfig(settings); } console.log('Framework detection prompt will be suppressed for this version.'); } else if (action === 'detect') { // Run framework detection const { detectedLanguage, detectedFramework } = detectEnvironmentAndFramework(); if (detectedFramework && detectedFramework !== 'generic') { console.log(`\nDetected framework: ${detectedFramework}`); // Update settings with detected framework settings.framework.detected = true; settings.framework.preference = detectedFramework; settings.framework.lastDetected = new Date().toISOString(); if (configManager.saveSettings) { await configManager.saveSettings(settings); } else if (configManager.saveConfig) { await configManager.saveConfig(settings); } console.log(`Framework set to: ${detectedFramework}`); } else { console.log('\nCould not detect a specific i18n framework.'); console.log('Please set up your i18n framework manually.'); } } } return { ...config, ...settings }; } async function detectEnvironmentAndFramework() { const packageJsonPath = path.join(process.cwd(), 'package.json'); const pyprojectPath = path.join(process.cwd(), 'pyproject.toml'); const requirementsPath = path.join(process.cwd(), 'requirements.txt'); const goModPath = path.join(process.cwd(), 'go.mod'); const pomPath = path.join(process.cwd(), 'pom.xml'); const composerPath = path.join(process.cwd(), 'composer.json'); let detectedLanguage = 'generic'; let detectedFramework = 'generic'; if (fs.existsSync(packageJsonPath)) { detectedLanguage = 'javascript'; try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const deps = { ...(packageJson.dependencies || {}), ...(packageJson.devDependencies || {}), ...(packageJson.peerDependencies || {}) }; // Check for i18ntk-runtime first (check both package names) const hasI18nTkRuntime = deps['i18ntk-runtime'] || deps['i18ntk/runtime']; // Check for common i18n patterns in source code if not found in package.json if (!hasI18nTkRuntime) { const i18nPatterns = [ /i18n\.t\(['\"`]/, /useI18n\(/, /from ['\"]i18ntk[\/\\]runtime['\"]/, /require\(['\"]i18ntk[\/\\]runtime['\"]\)/ ]; const sourceFiles = await glob(['src/**/*.{js,jsx,ts,tsx}'], { cwd: process.cwd(), ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'] }); for (const file of sourceFiles) { try { const content = await fs.promises.readFile(path.join(process.cwd(), file), 'utf8'); if (i18nPatterns.some(pattern => pattern.test(content))) { detectedFramework = 'i18ntk-runtime'; break; } } catch (e) { // Skip files we can't read continue; } } } else { detectedFramework = 'i18ntk-runtime'; } // Only check other frameworks if i18ntk-runtime wasn't detected if (detectedFramework !== 'i18ntk-runtime') { if (deps.react || deps['react-dom']) detectedFramework = 'react'; else if (deps.vue || deps['vue-router']) detectedFramework = 'vue'; else if (deps['@angular/core']) detectedFramework = 'angular'; else if (deps.next) detectedFramework = 'nextjs'; else if (deps.nuxt) detectedFramework = 'nuxt'; else if (deps.svelte) detectedFramework = 'svelte'; else detectedFramework = 'generic'; } } catch (error) { detectedFramework = 'generic'; } } else if (fs.existsSync(pyprojectPath) || fs.existsSync(requirementsPath)) { detectedLanguage = 'python'; try { if (fs.existsSync(requirementsPath)) { const requirements = fs.readFileSync(requirementsPath, 'utf8'); if (requirements.includes('django')) detectedFramework = 'django'; else if (requirements.includes('flask')) detectedFramework = 'flask'; else if (requirements.includes('fastapi')) detectedFramework = 'fastapi'; else detectedFramework = 'generic'; } } catch (error) { detectedFramework = 'generic'; } } else if (fs.existsSync(goModPath)) { detectedLanguage = 'go'; detectedFramework = 'generic'; } else if (fs.existsSync(pomPath)) { detectedLanguage = 'java'; try { const pomContent = fs.readFileSync(pomPath, 'utf8'); if (pomContent.includes('spring-boot')) detectedFramework = 'spring-boot'; else if (pomContent.includes('spring')) detectedFramework = 'spring'; else if (pomContent.includes('quarkus')) detectedFramework = 'quarkus'; else detectedFramework = 'generic'; } catch (error) { detectedFramework = 'generic'; } } else if (fs.existsSync(composerPath)) { detectedLanguage = 'php'; try { const composer = JSON.parse(fs.readFileSync(composerPath, 'utf8')); const deps = composer.require || {}; if (deps['laravel/framework']) detectedFramework = 'laravel'; else if (deps['symfony/framework-bundle']) detectedFramework = 'symfony'; else if (deps['wordpress']) detectedFramework = 'wordpress'; else detectedFramework = 'generic'; } catch (error) { detectedFramework = 'generic'; } } return { detectedLanguage, detectedFramework }; } function getFrameworkSuggestions(language) { const suggestions = { javascript: [ { name: 'i18next', description: 'Feature-rich i18n framework for JavaScript' }, { name: 'react-i18next', description: 'React integration for i18next' }, { name: 'vue-i18n', description: 'Vue.js i18n plugin' }, { name: 'Angular i18n', description: 'Built-in Angular i18n' } ], typescript: [ { name: 'i18next', description: 'TypeScript-first i18n framework' }, { name: 'react-i18next', description: 'React + TypeScript integration' }, { name: 'vue-i18n', description: 'Vue.js i18n with TypeScript support' } ], python: [ { name: 'Django i18n', description: 'Built-in Django internationalization' }, { name: 'Flask-Babel', description: 'Babel integration for Flask' }, { name: 'FastAPI i18n', description: 'i18n middleware for FastAPI' } ], java: [ { name: 'Spring i18n', description: 'Spring Framework internationalization' }, { name: 'Spring Boot i18n', description: 'Spring Boot auto-configuration' }, { name: 'Quarkus i18n', description: 'Quarkus internationalization support' } ], go: [ { name: 'go-i18n', description: 'Go i18n library with pluralization' }, { name: 'nicksnyder/go-i18n', description: 'Feature-rich Go i18n' } ], php: [ { name: 'Laravel i18n', description: 'Built-in Laravel localization' }, { name: 'Symfony Translation', description: 'Symfony translation component' }, { name: 'WordPress i18n', description: 'WordPress localization functions' } ] }; return suggestions[language] || suggestions.javascript; } /** * Handles framework detection and prompting logic * @param {Object} prompt - Prompt interface for user interaction * @param {Object} cfg - Configuration object * @param {string} currentVersion - Current version of the tool * @returns {Promise<Object>} Updated configuration */ async function maybePromptFramework(prompt, cfg, currentVersion) { // Load current settings to check framework configuration let settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); // Ensure framework configuration exists with all required fields if (!settings.framework) { settings.framework = { detected: false, preference: null, prompt: 'always', lastPromptedVersion: null, installed: [], version: '1.0' // Schema version for future compatibility }; // Save the updated settings if (configManager.saveSettings) { await configManager.saveSettings(settings); } else if (configManager.saveConfig) { await configManager.saveConfig(settings); } } // Reload settings to ensure we have the latest framework detection results const freshSettings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); if (freshSettings.framework) { settings.framework = { ...settings.framework, ...freshSettings.framework }; } // Check if framework is already detected or preference is explicitly set to none if (settings.framework.detected || settings.framework.preference === 'none') { return cfg; } // Check if DNR (Do Not Remind) is active for this version if (settings.framework.prompt === 'suppress' && settings.framework.lastPromptedVersion === currentVersion) { return cfg; } // Reset DNR if version changed if (settings.framework.prompt === 'suppress' && settings.framework.lastPromptedVersion !== currentVersion) { settings.framework.prompt = 'always'; settings.framework.lastPromptedVersion = null; // Save the updated settings if (configManager.saveSettings) { await configManager.saveSettings(settings); } else if (configManager.saveConfig) { await configManager.saveConfig(settings); } } // This function is now handled by ensureInitializedOrExit for better flow control return cfg; } // Use unified configuration system const { getUnifiedConfig, ensureInitialized, validateSourceDir } = require('../utils/config-helper'); class I18nManager { constructor(config = {}) { this.config = config; this.rl = null; this.isReadlineClosed = false; this.isAuthenticated = false; this.ui = null; this.adminAuth = new AdminAuth(); // No longer create readline interface here - use CLI helpers } initializeReadline() { // Use centralized CLI helper instead of direct readline this.rl = null; this.isReadlineClosed = false; } // Initialize configuration using unified system async initialize() { try { // Parse args here for other initialization needs (but language is already loaded) const args = this.parseArgs(); if (args.help) { this.showHelp(); process.exit(0); } // Ensure UI is initialized (it should already be loaded in run()) if (!this.ui) { const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); const uiLanguage = args.uiLanguage || settings.uiLanguage || settings.language || this.config.uiLanguage || 'en'; this.ui.loadLanguage(uiLanguage); loadTranslations(uiLanguage); } // Validate source directory exists const {validateSourceDir, displayPaths} = require('../utils/config-helper'); try { validateSourceDir(this.config.sourceDir, 'i18ntk-manage'); } catch (err) { console.log(this.ui.t('init.requiredTitle')); console.log(this.ui.t('init.requiredBody')); const answer = await cliHelper.prompt(this.ui.t('init.promptRunNow')); if (answer.trim().toLowerCase() === 'y') { const initializer = new I18nInitializer(this.config); await initializer.run({ fromMenu: true }); } else { throw err; } } } catch (error) { throw error; } } // Auto-detect i18n directory from common locations only if not configured in settings detectI18nDirectory() { const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); const projectRoot = path.resolve(settings.projectRoot || this.config.projectRoot || '.'); // Use per-script directory configuration if available, fallback to global sourceDir const sourceDir = settings.scriptDirectories?.manage || settings.sourceDir; if (sourceDir) { this.config.sourceDir = path.resolve(projectRoot, sourceDir); return; } // Define possible i18n paths for auto-detection const possibleI18nPaths = [ './locales', './src/locales', './src/i18n', './src/i18n/locales', './app/locales', './app/i18n', './public/locales', './assets/locales', './translations', './lang' ]; // Only auto-detect if no settings are configured for (const possiblePath of possibleI18nPaths) { const resolvedPath = path.resolve(projectRoot, possiblePath); if (fs.existsSync(resolvedPath)) { // Check if it contains language directories try { const items = fs.readdirSync(resolvedPath); const hasLanguageDirs = items.some(item => { const itemPath = path.join(resolvedPath, item); return fs.statSync(itemPath).isDirectory() && ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(item); }); if (hasLanguageDirs) { this.config.sourceDir = possiblePath; t('init.autoDetectedI18nDirectory', { path: possiblePath }); break; } } catch (error) { // Continue checking other paths } } } } // Check if i18n framework is installed - configuration-based check without prompts async checkI18nDependencies() { const packageJsonPath = path.resolve('./package.json'); if (!fs.existsSync(packageJsonPath)) { console.log(this.ui ? this.ui.t('errors.noPackageJson') : 'No package.json found'); return false; // Treat as no framework detected } 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', 'i18ntk-runtime' ]; const installedFrameworks = i18nFrameworks.filter(framework => dependencies[framework]); if (installedFrameworks.length > 0) { if (this.ui && this.ui.t) { console.log(this.ui.t('init.detectedFrameworks', { frameworks: installedFrameworks.join(', ') })); } else { console.log(`Detected 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); } } // Always signal that frameworks were not detected return false; } } catch (error) { console.log(t('init.errors.packageJsonRead')); return false; // Treat as no framework detected on error } } /** * Prompt user to continue without i18n framework */ async promptContinueWithoutI18n() { const promptText = this.ui && this.ui.t ? this.ui.t('init.continueWithoutI18nPrompt') : 'Do you want to continue without one? (y/N)'; const answer = await this.prompt('\n' + promptText); return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'; } // 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('='); const sanitizedKey = key?.trim(); const sanitizedValue = value !== undefined ? value.trim() : true; switch (sanitizedKey) { case 'source-dir': parsed.sourceDir = sanitizedValue; break; case 'i18n-dir': parsed.i18nDir = sanitizedValue; break; case 'output-dir': parsed.outputDir = sanitizedValue; break; case 'source-language': parsed.sourceLanguage = sanitizedValue; break; case 'ui-language': parsed.uiLanguage = sanitizedValue; break; case 'no-prompt': parsed.noPrompt = true; break; case 'admin-pin': parsed.adminPin = sanitizedValue || ''; break; case 'help': case 'h': parsed.help = true; break; default: // Handle language shorthand flags like --de, --fr if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(sanitizedKey)) { parsed.uiLanguage = sanitizedKey; } break; } } }); return parsed; } // Add this run method after the checkI18nDependencies method async run() { let prompt; try { // Ensure setup is complete before running any operations await SetupEnforcer.checkSetupCompleteAsync(); const args = this.parseArgs(); prompt = createPrompt({ noPrompt: args.noPrompt || Boolean(args.adminPin) }); const interactive = isInteractive({ noPrompt: args.noPrompt || Boolean(args.adminPin) }); // Load settings and UI language const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); this.ui = new UIi18n(); const uiLanguage = args.uiLanguage || settings.uiLanguage || settings.language || this.config.uiLanguage || 'en'; this.ui.loadLanguage(uiLanguage); if (args.adminPin) { this.adminAuth.verifyPin = async () => true; this.prompt = async () => ''; } if (args.help) { this.showHelp(); return; } let cfgAfterInitCheck = {}; if (interactive) { cfgAfterInitCheck = await ensureInitializedOrExit(prompt); const frameworksDetected = await this.checkI18nDependencies(); if (!frameworksDetected) { await maybePromptFramework(prompt, cfgAfterInitCheck, pkg.version); } } this.config = { ...this.config, ...cfgAfterInitCheck }; await this.initialize(); const rawArgs = process.argv.slice(2); // Preserve original CLI args array for positional checks let commandToExecute = null; // Define valid direct commands const directCommands = [ 'init', 'analyze', 'validate', 'usage', 'scanner', 'sizing', 'complete', 'fix', 'summary', 'debug', 'workflow' ]; // Handle help immediately without dependency checks if (args.help) { this.showHelp(); this.safeClose(); process.exit(0); } // Handle debug flag if (args.debug) { // Enable debug mode for this session const { blue } = require('../utils/colors-new'); console.log(blue('Debug mode enabled')); } // Check for --command= argument first const commandFlagArg = rawArgs.find(arg => arg.startsWith('--command=')); if (commandFlagArg) { commandToExecute = commandFlagArg.split('=')[1]; } else if (rawArgs.length > 0 && directCommands.includes(rawArgs[0])) { // If no --command=, check if the first argument is a direct command commandToExecute = rawArgs[0]; } if (commandToExecute) { console.log(t('ui.executingCommand', { command: commandToExecute })); await this.executeCommand(commandToExecute); this.safeClose(); return; } // If no command provided and --no-prompt is set, exit gracefully if (args.noPrompt) { this.safeClose(); process.exit(0); } // Framework detection is now handled by maybePromptFramework above // Skip the redundant checkI18nDependencies prompt // Interactive mode - showInteractiveMenu will handle the title await this.showInteractiveMenu(); } catch (error) { if (this.ui && this.ui.t) { console.error(t('common.genericError', { error: error.message })); } else { console.error(`Error: ${error.message}`); } process.exit(1); } finally { if (prompt && typeof prompt.close === 'function') { prompt.close(); } this.safeClose(); } } showHelp() { const localT = this.ui && this.ui.t ? this.ui.t.bind(this.ui) : (key) => { // Fallback help text when UI is not initialized const helpTexts = { 'help.usage': 'Usage: npm run i18ntk [command] [options]', 'help.interactiveMode': 'Run without arguments for interactive mode', 'help.initProject': ' init - Initialize i18n project structure', 'help.analyzeTranslations': ' analyze - Analyze translation files', 'help.validateTranslations': ' validate - Validate translations for errors', 'help.checkUsage': ' usage - Check translation usage in code', 'help.showHelp': ' help - Show this help message', 'help.availableCommands': '\nAvailable commands:', 'help.initCommand': ' --command=init Initialize i18n project', 'help.analyzeCommand': ' --command=analyze Analyze translations', 'help.validateCommand': ' --command=validate Validate translations', 'help.usageCommand': ' --command=usage Check translation usage', 'help.sizingCommand': ' --command=sizing Analyze translation sizing', 'help.completeCommand': ' --command=complete Run complete analysis', 'help.summaryCommand': ' --command=summary Generate summary report', 'help.debugCommand': ' --command=debug Run debug utilities', 'help.scannerCommand': ' --command=scanner Scan for translation keys' }; return helpTexts[key] || key; }; console.log(t('help.usage')); console.log(t('help.interactiveMode')); console.log(t('help.initProject')); console.log(t('help.analyzeTranslations')); console.log(t('help.validateTranslations')); console.log(t('help.checkUsage')); console.log(t('help.showHelp')); console.log(t('help.availableCommands')); console.log(t('help.initCommand')); console.log(t('help.analyzeCommand')); console.log(t('help.validateCommand')); console.log(t('help.usageCommand')); console.log(t('help.sizingCommand')); console.log(t('help.completeCommand')); console.log(t('help.summaryCommand')); console.log(t('help.debugCommand')); console.log(t('help.scannerCommand')); // Ensure proper exit for direct command execution if (process.argv.includes('--help') || process.argv.includes('-h')) { this.safeClose(); process.exit(0); } } /** * Determine execution context based on options and environment */ getExecutionContext(options = {}) { // Check if called from interactive menu if (options.fromMenu === true) { return { type: 'manager', source: 'interactive_menu' }; } // Check if called from workflow/autorun if (options.fromWorkflow === true) { return { type: 'workflow', source: 'autorun_script' }; } // Check if this is a direct command line execution if (process.argv.some(arg => arg.startsWith('--command='))) { return { type: 'direct', source: 'command_line' }; } // Default to direct execution return { type: 'direct', source: 'unknown' }; } async executeCommand(command, options = {}) { console.log(t('menu.executingCommand', { command })); // Enhanced context detection const executionContext = this.getExecutionContext(options); const isDirectCommand = executionContext.type === 'direct'; const isWorkflowExecution = executionContext.type === 'workflow'; const isManagerExecution = executionContext.type === 'manager'; // Ensure UI language is refreshed from settings for workflow and direct execution if (isWorkflowExecution || isDirectCommand) { this.ui.refreshLanguageFromSettings(); } // Check admin authentication for all commands when PIN protection is enabled const authRequiredCommands = ['init', 'analyze', 'validate', 'usage', 'scanner', 'complete', 'fix', 'sizing', 'workflow', 'status', 'delete', 'settings', 'debug']; if (authRequiredCommands.includes(command)) { const authPassed = await this.checkAdminAuth(); if (!authPassed) { if (!this.isNonInteractiveMode() && !isDirectCommand) { await this.prompt(t('menu.pressEnterToContinue')); await this.showInteractiveMenu(); } return; } } try { switch (command) { case 'init': const initializer = new I18nInitializer(this.config); await initializer.run({fromMenu: isManagerExecution}); break; case 'analyze': const analyzer = new I18nAnalyzer(); await analyzer.run({fromMenu: isManagerExecution}); break; case 'validate': const validator = new I18nValidator(); await validator.run({fromMenu: isManagerExecution}); break; case 'usage': const usageAnalyzer = new I18nUsageAnalyzer(); await usageAnalyzer.run({fromMenu: isManagerExecution}); break; case 'sizing': const sizingAnalyzer = new I18nSizingAnalyzer(); await sizingAnalyzer.run({fromMenu: isManagerExecution}); break; case 'complete': const completeTool = require('./i18ntk-complete'); const tool = new completeTool(); await tool.run({fromMenu: isManagerExecution}); break; case 'fix': const fixerTool = new I18nFixer(); await fixerTool.run({fromMenu: isManagerExecution}); break; case 'scanner': const Scanner = require('./i18ntk-scanner'); const scanner = new Scanner(); await scanner.initialize(); await scanner.run(); break; case 'debug': console.log('Debug functionality is not available in this version.'); break; case 'help': this.showHelp(); if (isManagerExecution && !this.isNonInteractiveMode()) { await this.prompt(t('menu.pressEnterToContinue')); await this.showInteractiveMenu(); } else { console.log(t('workflow.exitingCompleted')); this.safeClose(); process.exit(0); } return; break; default: console.log(t('menu.unknownCommand', { command })); this.showHelp(); // No return here, let the completion logic handle the exit/menu return break; } // Handle command completion based on execution context console.log(t('operations.completed')); if (isManagerExecution && !this.isNonInteractiveMode()) { // Interactive menu execution - return to menu await this.prompt(t('menu.returnToMainMenu')); await this.showInteractiveMenu(); } else { // Direct commands, non-interactive mode, or workflow execution - exit immediately console.log(t('workflow.exitingCompleted')); this.safeClose(); process.exit(0); } } catch (error) { console.error(t('common.errorExecutingCommand', { error: error.message })); if (isManagerExecution && !this.isNonInteractiveMode()) { // Interactive menu execution - show error and return to menu await this.prompt(t('menu.pressEnterToContinue')); await this.showInteractiveMenu(); } else if (isDirectCommand && !this.isNonInteractiveMode()) { // Direct command execution - show "enter to continue" and exit with error await this.prompt(t('menu.pressEnterToContinue')); this.safeClose(); process.exit(1); } else { // Non-interactive mode or workflow execution - exit immediately with error this.safeClose(); process.exit(1); } } } // Add admin authentication check async checkAdminAuth() { const isRequired = await this.adminAuth.isAuthRequired(); if (!isRequired) { return true; } // Check if admin PIN was provided via command line const args = this.parseArgs(); if (args.adminPin) { const isValid = await this.adminAuth.verifyPin(args.adminPin); if (isValid) { console.log(t('adminCli.authSuccess')); return true; } else { console.log(t('adminCli.invalidPin')); return false; } } console.log(t('adminCli.authRequired')); 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')); return false; } console.log(t('adminCli.authSuccess')); return true; } async showInteractiveMenu() { // Check if we're in non-interactive mode (like echo 0 | node script) if (this.isNonInteractiveMode()) { console.log(`\n${t('menu.title')}`); console.log(t('menu.separator')); console.log(`1. ${t('menu.options.init')}`); console.log(`2. ${t('menu.options.analyze')}`); console.log(`3. ${t('menu.options.validate')}`); console.log(`4. ${t('menu.options.usage')}`); console.log(`5. ${t('menu.options.complete')}`); console.log(`6. ${t('menu.options.sizing')}`); console.log(`7. ${t('menu.options.fix')}`); console.log(`8. ${t('menu.options.status')}`); console.log(`9. ${t('menu.options.delete')}`); console.log(`10. ${t('menu.options.settings')}`); console.log(`11. ${t('menu.options.help')}`); console.log(`12. ${t('menu.options.language')}`); console.log(`13. ${t('menu.options.scanner')}`); console.log(`0. ${t('menu.options.exit')}`); console.log('\n' + t('menu.nonInteractiveModeWarning')); console.log(t('menu.useDirectExecution')); console.log(t('menu.useHelpForCommands')); this.safeClose(); process.exit(0); return; } console.log(`\n${t('menu.title')}`); console.log(t('menu.separator')); console.log(`1. ${t('menu.options.init')}`); console.log(`2. ${t('menu.options.analyze')}`); console.log(`3. ${t('menu.options.validate')}`); console.log(`4. ${t('menu.options.usage')}`); console.log(`5. ${t('menu.options.complete')}`); console.log(`6. ${t('menu.options.sizing')}`); console.log(`7. ${t('menu.options.fix')}`); console.log(`8. ${t('menu.options.status')}`); console.log(`9. ${t('menu.options.delete')}`); console.log(`10. ${t('menu.options.settings')}`); console.log(`11. ${t('menu.options.help')}`); console.log(`12. ${t('menu.options.language')}`); console.log(`13. ${t('menu.options.scanner')}`); console.log(`0. ${t('menu.options.exit')}`); const choice = await this.prompt('\n' + t('menu.selectOptionPrompt')); switch (choice.trim()) { case '1': await this.executeCommand('init', {fromMenu: true}); break; case '2': await this.executeCommand('analyze', {fromMenu: true}); break; case '3': await this.executeCommand('validate', {fromMenu: true}); break; case '4': await this.executeCommand('usage', {fromMenu: true}); break; case '5': await this.executeCommand('complete', {fromMenu: true}); break; case '6': await this.executeCommand('sizing', {fromMenu: true}); break; case '7': await this.executeCommand('fix', {fromMenu: true}); break; case '8': // Check for PIN protection const authRequired = await this.adminAuth.isAuthRequiredForScript('summaryReports'); if (authRequired) { console.log(`\n${t('adminCli.protectedAccess')}`); 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')); await this.prompt(t('menu.pressEnterToContinue')); await this.showInteractiveMenu(); return; } console.log(t('adminCli.accessGranted')); } console.log(t('summary.status.generating')); try { const summaryTool = require('./i18ntk-summary'); const summary = new summaryTool(); await summary.run({ fromMenu: true }); console.log(t('summary.status.completed')); // Check if we're in interactive mode before prompting if (!this.isNonInteractiveMode()) { try { await this.prompt('\n' + t('debug.pressEnterToContinue')); await this.showInteractiveMenu(); } catch (error) { console.log(t('menu.returning')); process.exit(0); } } else { console.log(t('status.exitingCompleted')); process.exit(0); } } catch (error) { console.error(t('common.errorGeneratingStatusSummary', { error: error.message })); // Check if we're in interactive mode before prompting if (!this.isNonInteractiveMode()) { try { await this.prompt('\n' + t('debug.pressEnterToContinue')); await this.showInteractiveMenu(); } catch (error) { console.log(t('menu.returning')); process.exit(0); } } else { console.log(t('common.errorExiting')); process.exit(1); } } break; case '9': await this.deleteReports(); break; case '10': await this.showSettingsMenu(); break; case '11': this.showHelp(); await this.prompt(t('menu.returnToMainMenu')); await this.showInteractiveMenu(); break; case '12': await this.showLanguageMenu(); break; case '13': await this.executeCommand('scanner', {fromMenu: true}); break; case '0': console.log(t('menu.goodbye')); this.safeClose(); process.exit(0); default: console.log(t('menu.invalidChoice')); await this.showInteractiveMenu(); } } // Language Menu async showLanguageMenu() { console.log(`\n${t('language.title')}`); console.log(t('language.separator')); console.log(t('language.current', { language: this.ui.getLanguageDisplayName(this.ui.getCurrentLanguage()) })); console.log('\n' + t('language.available')); this.ui.availableLanguages.forEach((lang, index) => { const displayName = this.ui.getLanguageDisplayName(lang); const current = lang === this.ui.getCurrentLanguage() ? ' ✓' : ''; console.log(t('language.languageOption', { index: index + 1, displayName, current })); }); console.log(`0. ${t('language.backToMainMenu')}`); const choice = await this.prompt('\n' + t('language.prompt')); const choiceNum = parseInt(choice); if (choiceNum === 0) { await this.showInteractiveMenu(); return; } else if (choiceNum >= 1 && choiceNum <= this.ui.availableLanguages.length) { const selectedLang = this.ui.availableLanguages[choiceNum - 1]; await this.ui.changeLanguage(selectedLang); console.log(t('language.changed', { language: this.ui.getLanguageDisplayName(selectedLang) })); // Force reload translations for the entire system const { loadTranslations } = require('../utils/i18n-helper'); loadTranslations(selectedLang); // Return to main menu with new language await this.prompt('\n' + t('language.pressEnterToContinue')); await this.showInteractiveMenu(); } else { console.log(t('language.invalid')); await this.prompt('\n' + t('language.pressEnterToContinue')); await this.showLanguageMenu(); } } // Debug Tools Menu async showDebugMenu() { // Check for PIN protection const authRequired = await this.adminAuth.isAuthRequiredForScript('debugMenu'); if (authRequired) { console.log(`\n${t('adminPin.protectedAccess')}`); const cliHelper = require('../utils/cli-helper'); const pin = await cliHelper.promptPin(t('adminPin.enterPin') + ': '); const isValid = await this.adminAuth.verifyPin(pin); if (!isValid) { console.log(t('adminPin.invalidPin')); await this.prompt(t('menu.pressEnterToContinue')); await this.showInteractiveMenu(); return; } console.log(t('adminPin.accessGranted')); } console.log(`\n${t('debug.title')}`); console.log(t('debug.separator')); console.log(t('debug.mainDebuggerSystemDiagnostics')); console.log(t('debug.debugLogs')); console.log(t('debug.backToMainMenu')); const choice = await this.prompt('\n' + t('debug.selectOption')); switch (choice.trim()) { case '1': await this.runDebugTool('debugger.js', 'Main Debugger'); break; case '2': await this.viewDebugLogs(); break; case '0': await this.showInteractiveMenu(); return; default: console.log(t('debug.invalidChoiceSelectRange')); await this.showDebugMenu(); } } // Run a debug tool async runDebugTool(toolName, displayName) { console.log(t('debug.runningDebugTool', { displayName })); try { const toolPath = path.join(__dirname, '..', 'scripts', 'debug', toolName); if (fs.existsSync(toolPath)) { console.log(`Debug tool available: ${toolName}`); console.log(`To run this tool manually: node "${toolPath}"`); console.log(`Working directory: ${path.join(__dirname, '..')}`); } else { console.log(t('debug.debugToolNotFound', { toolName })); } } catch (error) { console.error(t('debug.errorRunningDebugTool', { displayName, error: error.message })); } await this.prompt('\n' + t('menu.pressEnterToContinue')); await this.showDebugMenu(); } // View debug logs async viewDebugLogs() { console.log(`\n${t('debug.recentDebugLogs')}`); console.log('============================================================'); try { const logsDir = path.join(__dirname, '..', 'scripts', 'debug', 'logs'); if (fs.existsSync(logsDir)) { const files = fs.readdirSync(logsDir) .filter(file => file.endsWith('.log') || file.endsWith('.txt')) .sort((a, b) => { const statA = fs.statSync(path.join(logsDir, a)); const statB = fs.statSync(path.join(logsDir, b)); return statB.mtime - statA.mtime; }) .slice(0, 5); if (files.length > 0) { files.forEach((file, index) => { const filePath = path.join(logsDir, file); const stats = fs.statSync(filePath); console.log(`${index + 1}. ${file} (${stats.mtime.toLocaleString()})`); }); const choice = await this.prompt('\n' + t('debug.selectLogPrompt', { count: files.length })); const fileIndex = parseInt(choice) - 1; if (fileIndex >= 0 && fileIndex < files.length) { const logContent = fs.readFileSync(path.join(logsDir, files[fileIndex]), 'utf8'); console.log(`\n${t('debug.contentOf', { filename: files[fileIndex] })}:`); console.log('============================================================'); console.log(logContent.slice(-2000)); // Show last 2000 characters console.log('============================================================'); } } else { console.log(t('debug.noDebugLogsFound')); } } else { console.log(t('debug.debugLogsDirectoryNotFound')); } } catch (error) { console.error(t('errors.errorReadingDebugLogs', { error: error.message })); } await this.prompt('\n' + t('menu.pressEnterToContinue')); await this.showInteractiveMenu(); } // Enhanced delete reports and logs functionality async deleteReports() { // Check for PIN protection const authRequired = await this.adminAuth.isAuthRequiredForScript('deleteReports'); if (authRequired) { console.log(`\n${t('adminPin.protectedAccess')}`); const cliHelper = require('../utils/cli-helper'); const pin = await cliHelper.promptPin(t('adminPin.enterPin') + ': '); const isValid = await this.adminAuth.verifyPin(pin); if (!isValid) { console.log(t('adminPin.invalidPin')); await this.prompt(t('menu.pressEnterToContinue')); await this.showInteractiveMenu(); return; } console.log(t('adminPin.accessGranted')); } console.log(`\n${t('operations.deleteReportsTitle')}`); console.log('============================================================'); const targetDirs = [ { path: path.join(process.cwd(), 'i18ntk-reports'), name: 'Reports', type: 'reports' }, { path: path.join(process.cwd(), 'reports'), name: 'Legacy Reports', type: 'reports' }, { path: path.join(process.cwd(), 'reports', 'backups'), name: 'Reports Backups', type: 'backups' }, { path: path.join(process.cwd(), 'scripts', 'debug', 'logs'), name: 'Debug Logs', type: 'logs' }, { path: path.join(process.cwd(), 'scripts', 'debug', 'reports'), name: 'Debug Reports', type: 'reports' }, { path: path.join(process.cwd(), 'settings', 'backups'), name: 'Settings Backups', type: 'backups' }, { path: path.join(process.cwd(), 'utils', 'i18ntk-reports'), name: 'Utils Reports', type: 'reports' } ].filter(dir => dir.path && typeof dir.path === 'string'); try { console.log(t('operations.scanningForFiles')); let availableDirs = []; // Check which directories exist and have files for (const dir of targetDirs) { if (fs.existsSync(dir.path)) { con