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

964 lines (819 loc) 35.6 kB
#!/usr/bin/env node /** * I18n Sizing Analyzer * * Analyzes translation file sizes, character counts, and provides sizing statistics * for different languages to help with UI layout planning and optimization. * * Features: * - File size analysis for all translation files * - Character count statistics per language * - Key-level size comparison across languages * - UI layout impact assessment * - Size optimization recommendations * * Usage: * i18ntk sizing [options] * * Options: * --source-dir <dir> Source directory containing translation files (default: ./locales) * --languages <langs> Comma-separated list of languages to analyze (default: all) * --output-report Generate detailed sizing report * --format <format> Output format: json, csv, table (default: table) * --threshold <number> Size difference threshold for warnings (default: 50%) * --detailed Generate detailed report with more information * --help Show this help message * * Examples: * i18ntk sizing --output-report * i18ntk sizing --languages=en,de,fr --format=json * i18ntk sizing --threshold=30 --output-report */ const fs = require('fs'); const path = require('path'); const { loadTranslations, t } = require('../utils/i18n-helper'); const configManager = require('../settings/settings-manager'); const SecurityUtils = require('../utils/security'); const { getUnifiedConfig } = require('../utils/config-helper'); const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli'); const SetupEnforcer = require('../utils/setup-enforcer'); // Ensure setup is complete before running (async () => { try { await SetupEnforcer.checkSetupCompleteAsync(); } catch (error) { console.error('Setup check failed:', error.message); process.exit(1); } })(); loadTranslations(process.env.I18NTK_LANG); // Get configuration from settings manager function getConfig() { const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); // Check for per-script directory override, fallback to global sourceDir const sourceDir = settings.scriptDirectories?.sizing || settings.sourceDir || './locales'; return { projectRoot: settings.projectRoot || '.', sourceDir: sourceDir, i18nDir: settings.i18nDir || settings.sourceDir || './locales', outputDir: settings.outputDir || './i18ntk-reports', threshold: settings.processing?.sizingThreshold || 50, uiLanguage: settings.language || 'en' }; } class I18nSizingAnalyzer { constructor(options = {}) { const config = getConfig(); const projectRoot = path.resolve(config.projectRoot || '.'); this.sourceDir = path.resolve(projectRoot, options.sourceDir || config.sourceDir); this.outputDir = path.resolve(projectRoot, options.outputDir || config.outputDir); this.languages = options.languages || []; this.threshold = options.threshold || config.threshold; // Size difference threshold in percentage this.format = options.format || 'table'; this.outputReport = options.outputReport || false; this.rl = null; // Initialize i18n with UI language from config const uiLanguage = options.uiLanguage || config.uiLanguage || 'en'; loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales')); this.stats = { files: {}, languages: {}, keys: {}, summary: {} }; } // Initialize readline interface initReadline() { return getGlobalReadline(); } // Prompt for user input async prompt(question) { const rl = getGlobalReadline(); return new Promise(resolve => rl.question(question, resolve)); } // Close readline interface closeReadline() { closeGlobalReadline(); } // Get available language files getLanguageFiles() { const validatedSourceDir = SecurityUtils.validatePath(this.sourceDir, process.cwd()); if (!validatedSourceDir) { throw new Error(t("sizing.invalidSourceDirectoryError", { sourceDir: this.sourceDir })); } if (!SecurityUtils.safeExistsSync(validatedSourceDir)) { throw new Error(t("sizing.sourceDirectoryNotFoundError", { sourceDir: validatedSourceDir })); } const files = []; const items = SecurityUtils.safeReaddirSync(validatedSourceDir); // Check for nested language directories for (const item of items) { const itemPath = SecurityUtils.validatePath(path.join(validatedSourceDir, item), process.cwd()); if (!itemPath) continue; const stat = SecurityUtils.safeStatSync(itemPath); if (!stat) continue; if (stat.isDirectory()) { // This is a language directory, combine all JSON files const langFiles = SecurityUtils.safeReaddirSync(itemPath) .filter(file => file.endsWith('.json')) .map(file => SecurityUtils.validatePath(path.join(itemPath, file), process.cwd())) .filter(file => file !== null); if (langFiles.length > 0) { files.push({ language: item, file: `${item}/*.json`, path: itemPath, files: langFiles }); } } else if (item.endsWith('.json')) { // Direct JSON file in root const lang = path.basename(item, '.json'); files.push({ language: lang, file: item, path: itemPath }); } } if (this.languages.length > 0) { return files.filter(f => this.languages.includes(f.language)); } return files; } // Analyze file sizes analyzeFileSizes(files) { SecurityUtils.debugLog('info', t("sizing.analyzing_file_sizes")); files.forEach(({ language, file, path: filePath, files: langFiles }) => { if (langFiles) { // Handle nested directory structure let totalSize = 0; let totalLines = 0; let totalCharacters = 0; let lastModified = new Date(0); langFiles.forEach(langFile => { const stats = SecurityUtils.safeStatSync(langFile, process.cwd()); if (!stats) return; let content = SecurityUtils.safeReadFileSync(langFile, process.cwd()); if (typeof content !== "string") content = ""; totalSize += stats.size; totalLines += content.split('\n').length; totalCharacters += content.length; if (stats.mtime > lastModified) { lastModified = stats.mtime; } }); this.stats.files[language] = { file, size: totalSize, sizeKB: (totalSize / 1024).toFixed(2), lines: totalLines, characters: totalCharacters, lastModified: lastModified, fileCount: langFiles.length }; } else { // Handle single file structure const stats = SecurityUtils.safeStatSync(filePath, process.cwd()); if (!stats) return; let content = SecurityUtils.safeReadFileSync(filePath, process.cwd()); if (typeof content !== "string") content = ""; this.stats.files[language] = { file, size: stats.size, sizeKB: (stats.size / 1024).toFixed(2), lines: content.split('\n').length, characters: content.length, lastModified: stats.mtime, fileCount: 1 }; } }); } // Analyze translation content analyzeTranslationContent(files) { SecurityUtils.debugLog('info', t("sizing.analyzing_translation_content")); files.forEach(({ language, path: filePath, files: langFiles }) => { try { let combinedContent = {}; if (langFiles) { // Handle nested directory structure - combine all JSON files langFiles.forEach(langFile => { const rawContent = SecurityUtils.safeReadFileSync(langFile, process.cwd()); const fileContent = SecurityUtils.safeParseJSON(rawContent); if (fileContent) { const fileName = path.basename(langFile, '.json'); combinedContent[fileName] = fileContent; } }); } else { // Handle single file structure const rawContent = SecurityUtils.safeReadFileSync(filePath, process.cwd()); combinedContent = SecurityUtils.safeParseJSON(rawContent) || {}; } const analysis = this.analyzeTranslationObject(combinedContent, ''); this.stats.languages[language] = { totalKeys: analysis.keyCount, totalCharacters: analysis.charCount, averageKeyLength: analysis.keyCount > 0 ? analysis.charCount / analysis.keyCount : 0, maxKeyLength: analysis.maxLength, minKeyLength: analysis.minLength, emptyKeys: analysis.emptyKeys, longKeys: analysis.longKeys }; // Store individual key sizes for comparison Object.entries(analysis.keys).forEach(([key, value]) => { if (!this.stats.keys[key]) { this.stats.keys[key] = {}; } this.stats.keys[key][language] = { length: value.length, characters: value.length }; }); } catch (error) { SecurityUtils.debugLog('error', t("sizing.failed_to_parse_language_error", { language, errorMessage: error.message })); } }); } // Recursively analyze translation object analyzeTranslationObject(obj, prefix = '') { let keyCount = 0; let charCount = 0; let maxLength = 0; let minLength = Infinity; let emptyKeys = 0; let longKeys = 0; const keys = {}; const traverse = (current, currentPrefix) => { Object.entries(current).forEach(([key, value]) => { const fullKey = currentPrefix ? `${currentPrefix}.${key}` : key; if (typeof value === 'string') { keyCount++; charCount += value.length; maxLength = Math.max(maxLength, value.length); minLength = Math.min(minLength, value.length); if (value.length === 0) emptyKeys++; if (value.length > 100) longKeys++; keys[fullKey] = value; } else if (typeof value === 'object' && value !== null) { traverse(value, fullKey); } }); }; traverse(obj, prefix); return { keyCount, charCount, maxLength: maxLength === 0 ? 0 : maxLength, minLength: minLength === Infinity ? 0 : minLength, emptyKeys, longKeys, keys }; } // Generate size comparison analysis generateSizeComparison() { SecurityUtils.debugLog('info', t("sizing.generating_size_comparisons")); const languages = Object.keys(this.stats.languages); const baseLanguage = languages[0]; // Use first language as baseline if (!baseLanguage) { SecurityUtils.debugLog('warn', t("sizing.no_languages_found_for_comparison")); return; } this.stats.summary = { baseLanguage, totalLanguages: languages.length, sizeVariations: {}, problematicKeys: [], recommendations: [] }; // Compare each language to base language languages.forEach(lang => { if (lang === baseLanguage) return; const baseStats = this.stats.languages[baseLanguage]; const langStats = this.stats.languages[lang]; const sizeDiff = ((langStats.totalCharacters - baseStats.totalCharacters) / baseStats.totalCharacters) * 100; this.stats.summary.sizeVariations[lang] = { characterDifference: langStats.totalCharacters - baseStats.totalCharacters, percentageDifference: sizeDiff.toFixed(2), isProblematic: Math.abs(sizeDiff) > this.threshold }; }); // Find problematic keys (significant size differences) Object.entries(this.stats.keys).forEach(([key, langData]) => { const baseLang = langData[baseLanguage]; if (!baseLang) return; const variations = []; Object.entries(langData).forEach(([lang, data]) => { if (lang === baseLanguage) return; const diff = ((data.length - baseLang.length) / baseLang.length) * 100; if (Math.abs(diff) > this.threshold) { variations.push({ language: lang, characterDifference: data.length - baseLang.length, percentageDifference: diff.toFixed(2), baseLength: baseLang.length, currentLength: data.length }); } }); if (variations.length > 0) { this.stats.summary.problematicKeys.push({ key, variations }); } }); // Generate recommendations this.generateRecommendations(); } // Generate optimization recommendations generateRecommendations() { const recommendations = []; // Check for large size variations Object.entries(this.stats.summary.sizeVariations).forEach(([lang, data]) => { if (data.isProblematic) { const comparison = data.percentageDifference > 0 ? t("sizing.longer") : t("sizing.shorter"); const absPercentage = Math.abs(data.percentageDifference); recommendations.push(t("sizing.review_translations", { lang, absPercentage, comparison })); } }); // Check for problematic keys if (this.stats.summary.problematicKeys.length > 0) { const problematicKeyNames = this.stats.summary.problematicKeys.map(item => item.key).join(', '); recommendations.push(t("sizing.problematic_keys", { count: this.stats.summary.problematicKeys.length, problematicKeys: problematicKeyNames })); } // Check for very long translations Object.entries(this.stats.languages).forEach(([lang, data]) => { if (data.longKeys > 0) { recommendations.push(t("sizing.long_translations", { lang, count: data.longKeys })); } }); this.stats.summary.recommendations = recommendations; } // Display concise folder-level results displayFolderResults() { console.log("\n" + t("sizing.sizing_analysis_results")); console.log(t("sizing.lineSeparator")); // Folder-level summary table console.log("\n" + t("sizing.folder_summary_title")); console.log(t("sizing.folder_summary_table_header")); console.log(t("sizing.lineSeparator")); Object.entries(this.stats.files).forEach(([lang, data]) => { const langData = this.stats.languages[lang]; const totalChars = Math.round(langData.totalKeys * langData.averageKeyLength); console.log(t("sizing.folder_summary_row", { lang, sizeKB: data.sizeKB, totalKeys: langData.totalKeys, avgLength: langData.averageKeyLength.toFixed(1), totalChars: totalChars })); }); // Language comparison summary console.log("\n" + t("sizing.language_comparison_title")); const baseLang = this.languages[0]; if (this.languages.length > 1 && this.stats.languages[baseLang]) { const baseChars = Math.round(this.stats.languages[baseLang].totalKeys * this.stats.languages[baseLang].averageKeyLength); this.languages.slice(1).forEach(lang => { if (this.stats.languages[lang]) { const langChars = Math.round(this.stats.languages[lang].totalKeys * this.stats.languages[lang].averageKeyLength); const diff = langChars - baseChars; const percent = baseChars > 0 ? ((diff / baseChars) * 100).toFixed(1) : 0; const status = Math.abs(diff) > this.threshold ? "⚠️" : "✅"; console.log(`${lang}: ${diff > 0 ? '+' : ''}${diff} chars (${percent}%) ${status}`); } }); } // Summary stats console.log("\n" + t("sizing.summary_stats", { totalLanguages: Object.keys(this.stats.languages).length, totalKeys: Object.keys(this.stats.keys).length, reportPath: this.outputDir })); if (this.detailedKeys) { this.displayDetailedKeys(); } } // Display detailed key analysis (only when explicitly requested) displayDetailedKeys() { console.log("\n" + t("sizing.detailed_key_analysis_title")); console.log(t("sizing.lineSeparator")); let counter = 0; Object.entries(this.stats.keys).forEach(([key, data]) => { if (counter >= 50) { console.log(t("sizing.too_many_keys_warning")); return; } console.log(t("sizing.key_analysis_header", { key })); Object.entries(data.translations).forEach(([lang, translation]) => { const length = translation.length; const isEmpty = length === 0; const isLong = length > this.threshold; const status = isEmpty ? t("sizing.status_empty") : isLong ? t("sizing.status_long") : t("sizing.status_ok"); console.log(t("sizing.key_analysis_detail", { lang, length, status, translation: translation.substring(0, 50) + (translation.length > 50 ? "..." : "") })); }); console.log(""); counter++; }); } // Generate human-readable report async generateHumanReadableReport() { if (!this.outputReport) return; SecurityUtils.debugLog('info', t("sizing.generating_detailed_report")); const validatedOutputDir = SecurityUtils.validatePath(this.outputDir, process.cwd()); if (!validatedOutputDir) { throw new Error(t("sizing.invalidOutputDirectoryError", { outputDir: this.outputDir })); } // Ensure output directory exists if (!SecurityUtils.safeExistsSync(validatedOutputDir)) { SecurityUtils.safeMkdirSync(validatedOutputDir, { recursive: true }); } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); // Generate human-readable text report const textReportPath = SecurityUtils.validatePath(path.join(validatedOutputDir, `sizing-analysis-${timestamp}.txt`), process.cwd()); if (!textReportPath) { throw new Error(t("sizing.invalidReportFileError")); } let textReport = this.generateTextReport(timestamp); const textSuccess = await SecurityUtils.safeWriteFile(textReportPath, textReport, process.cwd()); if (textSuccess) { SecurityUtils.debugLog('info', t("sizing.human_report_saved", { reportPath: textReportPath })); } // Generate JSON for programmatic access const jsonReportPath = SecurityUtils.validatePath(path.join(validatedOutputDir, `sizing-analysis-${timestamp}.json`), process.cwd()); if (!jsonReportPath) { throw new Error(t("sizing.invalidReportFileError")); } const report = { timestamp: new Date().toISOString(), configuration: { sourceDir: this.sourceDir, languages: this.languages, threshold: this.threshold }, analysis: this.stats, metadata: { totalFiles: Object.keys(this.stats.files).length, totalLanguages: Object.keys(this.stats.languages).length, totalKeys: Object.keys(this.stats.keys).length } }; const jsonSuccess = await SecurityUtils.safeWriteFile(jsonReportPath, JSON.stringify(report, null, 2), process.cwd()); if (jsonSuccess) { SecurityUtils.logSecurityEvent('Sizing report saved', 'info', { jsonReportPath }); } // Generate CSV if requested if (this.format === 'csv') { await this.generateCSVReport(timestamp); } } // Generate human-readable text report generateTextReport(timestamp) { let report = `# i18n Sizing Analysis Report Generated: ${new Date().toISOString()} ## Configuration - Source Directory: ${this.sourceDir} - Languages: ${this.languages.join(', ')} - Threshold: ${this.threshold}% ## Summary Statistics - Total Languages: ${Object.keys(this.stats.languages).length} - Total Translation Keys: ${Object.keys(this.stats.keys).length} - Total Files: ${Object.keys(this.stats.files).length} ## Language Overview `; Object.entries(this.stats.languages).forEach(([lang, data]) => { const fileData = this.stats.files[lang] || { sizeKB: 0, lines: 0, characters: 0 }; report += ` ### ${lang.toUpperCase()} - File Size: ${fileData.sizeKB} KB - Lines: ${fileData.lines} - Total Characters: ${fileData.characters} - Translation Keys: ${data.totalKeys} - Average Key Length: ${data.averageKeyLength.toFixed(1)} characters - Empty Translations: ${data.emptyKeys} - Long Keys (> ${this.threshold} chars): ${data.longKeys} `; }); // Size variations if (this.stats.summary.sizeVariations && Object.keys(this.stats.summary.sizeVariations).length > 0) { report += ` ## Size Variations (vs ${this.languages[0]}) `; Object.entries(this.stats.summary.sizeVariations).forEach(([lang, data]) => { report += `- ${lang}: ${data.characterDifference > 0 ? '+' : ''}${data.characterDifference} chars (${data.percentageDifference > 0 ? '+' : ''}${data.percentageDifference}%) ${data.isProblematic ? '⚠️ PROBLEMATIC' : '✅ OK'}\n`; }); } // Problematic keys if (this.stats.summary.problematicKeys.length > 0) { report += ` ## Problematic Keys (${this.stats.summary.problematicKeys.length}) `; this.stats.summary.problematicKeys.forEach((item, index) => { report += `${index + 1}. ${item.key}\n`; item.variations.forEach(v => { report += ` ${v.language}: ${v.characterDifference > 0 ? '+' : ''}${v.characterDifference} chars (${v.percentageDifference > 0 ? '+' : ''}${v.percentageDifference}%)\n`; }); report += '\n'; }); } // Recommendations if (this.stats.summary.recommendations.length > 0) { report += ` ## Recommendations `; this.stats.summary.recommendations.forEach((rec, index) => { report += `${index + 1}. ${rec}\n`; }); } // Detailed key analysis if (this.detailedKeys) { report += ` ## Detailed Key Analysis `; Object.entries(this.stats.keys).forEach(([key, data]) => { report += ` ### ${key} `; Object.entries(data.translations).forEach(([lang, translation]) => { const length = translation.length; const isEmpty = length === 0; const isLong = length > this.threshold; const status = isEmpty ? 'EMPTY' : isLong ? 'LONG' : 'OK'; report += `- ${lang}: ${length} chars [${status}] "${translation.substring(0, 100)}${translation.length > 100 ? '...' : ''}"\n`; }); }); } return report; } // Generate CSV report async generateCSVReport(timestamp) { const validatedOutputDir = SecurityUtils.validatePath(this.outputDir, process.cwd()); if (!validatedOutputDir) { throw new Error(t("sizing.invalidOutputDirectoryError", { outputDir: this.outputDir })); } const csvPath = SecurityUtils.validatePath(path.join(validatedOutputDir, `sizing-analysis-${timestamp}.csv`), process.cwd()); if (!csvPath) { throw new Error(t("sizing.invalidCsvFileError")); } let csvContent = 'Language,File Size (KB),Lines,Characters,Total Keys,Avg Key Length,Max Key Length,Empty Keys,Long Keys\n'; Object.entries(this.stats.files).forEach(([lang]) => { const fileData = this.stats.files[lang]; const langData = this.stats.languages[lang]; csvContent += `${lang},${fileData.sizeKB},${fileData.lines},${fileData.characters},${langData.totalKeys},${langData.averageKeyLength.toFixed(1)},${langData.maxKeyLength},${langData.emptyKeys},${langData.longKeys}\n`; }); const success = await SecurityUtils.safeWriteFile(csvPath, csvContent, process.cwd()); if (success) { SecurityUtils.debugLog('info', t("sizing.csv_report_saved_to", { csvPath })); SecurityUtils.logSecurityEvent('CSV report saved', 'info', { csvPath }); } else { throw new Error(t("sizing.failedToSaveCsvError")); } } // Main analysis method async analyze() { const perfTimer = SecurityUtils.getPerformanceTimer(); const startTime = perfTimer.now(); try { SecurityUtils.debugLog('info', t("sizing.starting_i18n_sizing_analysis")); SecurityUtils.debugLog('info', t("sizing.source_directory", { sourceDir: this.sourceDir })); const files = this.getLanguageFiles(); if (files.length === 0) { SecurityUtils.debugLog('warn', t("sizing.no_translation_files_found")); return; } SecurityUtils.debugLog('info', t("sizing.found_languages", { languages: files.map(f => f.language).join(', ') })); this.analyzeFileSizes(files); this.analyzeTranslationContent(files); this.generateSizeComparison(); if (this.format === 'table') { this.displayFolderResults(); } else if (this.format === 'json') { SecurityUtils.debugLog('info', t("sizing.analysisStats", { stats: JSON.stringify(this.stats, null, 2) })); } await this.generateHumanReadableReport(); const endTime = perfTimer.now(); SecurityUtils.debugLog('info', t("sizing.analysis_completed", { duration: (endTime - startTime).toFixed(2) })); } catch (error) { SecurityUtils.debugLog('error', t("sizing.analysis_failed", { errorMessage: error.message })); process.exit(1); } } // Parse command line arguments without yargs parseArgs() { const args = process.argv.slice(2); const options = { 'source-dir': './locales', 'languages': '', 'output-report': true, 'format': 'table', 'threshold': 50, 'detailed': false, 'detailed-keys': false, 'output-dir': './i18ntk-reports', 'help': false, 's': './locales', 'l': '', 'o': true, 'f': 'table', 't': 50, 'd': false }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--help' || arg === '-h') { options.help = true; continue; } // Handle --key=value format const keyValueMatch = arg.match(/^--?([^=]+)=(.+)$/); if (keyValueMatch) { const key = keyValueMatch[1]; const value = keyValueMatch[2]; if (key === 'source-dir' || key === 's') { options['source-dir'] = value; options.s = value; } else if (key === 'languages' || key === 'l') { options.languages = value; options.l = value; } else if (key === 'output-report' || key === 'o') { options['output-report'] = value.toLowerCase() !== 'false'; options.o = options['output-report']; } else if (key === 'format' || key === 'f') { if (['json', 'csv', 'table'].includes(value)) { options.format = value; options.f = value; } } else if (key === 'threshold' || key === 't') { const numValue = parseInt(value); if (!isNaN(numValue)) { options.threshold = numValue; options.t = numValue; } } else if (key === 'detailed' || key === 'd') { options.detailed = value.toLowerCase() !== 'false'; options.d = options.detailed; } else if (key === 'detailed-keys') { options['detailed-keys'] = value.toLowerCase() !== 'false'; } else if (key === 'output-dir') { options['output-dir'] = value; } continue; } // Handle --key value format const match = arg.match(/^--?(.+)/); if (match) { const key = match[1]; const nextArg = args[i + 1]; if (key === 'source-dir' || key === 's') { options['source-dir'] = nextArg || options['source-dir']; options.s = options['source-dir']; if (nextArg && !nextArg.startsWith('-')) i++; } else if (key === 'languages' || key === 'l') { options.languages = nextArg || options.languages; options.l = options.languages; if (nextArg && !nextArg.startsWith('-')) i++; } else if (key === 'output-report' || key === 'o') { if (nextArg && !nextArg.startsWith('-') && ['true', 'false'].includes(nextArg.toLowerCase())) { options['output-report'] = nextArg.toLowerCase() !== 'false'; options.o = options['output-report']; i++; } else { options['output-report'] = true; options.o = true; } } else if (key === 'format' || key === 'f') { const value = nextArg || options.format; if (['json', 'csv', 'table'].includes(value)) { options.format = value; options.f = value; } if (nextArg && !nextArg.startsWith('-') && ['json', 'csv', 'table'].includes(nextArg)) i++; } else if (key === 'threshold' || key === 't') { const value = parseInt(nextArg); if (!isNaN(value)) { options.threshold = value; options.t = value; } if (nextArg && !nextArg.startsWith('-') && !isNaN(parseInt(nextArg))) i++; } else if (key === 'detailed' || key === 'd') { if (nextArg && !nextArg.startsWith('-') && ['true', 'false'].includes(nextArg.toLowerCase())) { options.detailed = nextArg.toLowerCase() !== 'false'; options.d = options.detailed; i++; } else { options.detailed = true; options.d = true; } } else if (key === 'detailed-keys') { if (nextArg && !nextArg.startsWith('-') && ['true', 'false'].includes(nextArg.toLowerCase())) { options['detailed-keys'] = nextArg.toLowerCase() !== 'false'; i++; } else { options['detailed-keys'] = true; } } else if (key === 'output-dir') { options['output-dir'] = nextArg || options['output-dir']; if (nextArg && !nextArg.startsWith('-')) i++; } } } if (options.help) { console.log(` I18NTK Sizing Analysis Tool Usage: i18ntk-sizing [options] Options: -s, --source-dir <dir> Source directory containing translation files (default: ./locales) -l, --languages <langs> Comma-separated list of languages to analyze -o, --output-report Generate detailed sizing report (default: true) -f, --format <format> Output format: json, csv, table (default: table) -t, --threshold <number> Size difference threshold for warnings (%) (default: 50) -d, --detailed Generate detailed report with more information --detailed-keys Show detailed key-level analysis --output-dir <dir> Output directory for reports (default: ./i18ntk-reports) --help Show this help message `); process.exit(0); } return options; } // Add run method for compatibility with manager async run(options = {}) { const { fromMenu = false } = options; const args = this.parseArgs(); const config = await getUnifiedConfig('sizing', args); this.sourceDir = path.resolve(config.projectRoot || '.', config.sourceDir || './locales'); this.outputDir = path.resolve(config.projectRoot || '.', config.outputDir || './i18ntk-reports'); this.threshold = args.threshold ?? config.processing?.sizingThreshold ?? 50; this.languages = args.languages ? args.languages.split(',').map(l => l.trim()) : []; this.outputReport = args['output-report'] !== undefined ? args['output-report'] : false; this.format = args.format || 'table'; this.detailed = args.detailed; this.detailedKeys = args['detailed-keys']; if (!fromMenu) { 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' + t('adminCli.authRequiredForOperation', { operation: 'analyze sizing' })); 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 { success: false, error: 'Authentication failed' }; } console.log(t('adminCli.authenticationSuccess')); } } return await this.analyze(); } // Main analysis method async analyze() { try { SecurityUtils.debugLog('info', t("sizing.starting_analysis")); SecurityUtils.debugLog('info', t("sizing.source_directory", { sourceDir: this.sourceDir })); const perfTimer = SecurityUtils.getPerformanceTimer(); const startTime = perfTimer.now(); // Get language files const files = this.getLanguageFiles(); if (files.length === 0) { SecurityUtils.debugLog('warn', t("sizing.no_translation_files_found")); return { success: false, error: "No translation files found" }; } SecurityUtils.debugLog('info', t("sizing.found_files", { count: files.length })); // Analyze file sizes this.analyzeFileSizes(files); // Analyze translation content this.analyzeTranslationContent(files); // Generate size comparison this.generateSizeComparison(); // Display results this.displayFolderResults(); // Generate reports if requested await this.generateHumanReadableReport(); const endTime = perfTimer.now(); const duration = ((endTime - startTime) / 1000).toFixed(2); SecurityUtils.debugLog('info', t("sizing.analysis_completed", { duration })); return { success: true, stats: this.stats }; } catch (error) { SecurityUtils.debugLog('error', t("sizing.analysis_failed", { errorMessage: error.message })); return { success: false, error: error.message }; } } } // Update the execution block at the end if (require.main === module) { const analyzer = new I18nSizingAnalyzer(); analyzer.run().then(() => { process.exit(0); }).catch(error => { console.error(t('sizing.fatalError', { error: error.message })); process.exit(1); }); } module.exports = I18nSizingAnalyzer;