UNPKG

i18ntk

Version:

i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.

1,046 lines (890 loc) 39 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { loadTranslations, t } = require('../utils/i18n-helper'); const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/config-helper'); const SecurityUtils = require('../utils/security'); const AdminCLI = require('../utils/admin-cli'); 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('en', path.resolve(__dirname, '..', 'ui-locales')); class I18nSummaryReporter { constructor() { this.config = null; // Will be set from getUnifiedConfig this.stats = { languages: [], totalFiles: 0, totalKeys: 0, keysByLanguage: {}, filesByLanguage: {}, fileSizes: {}, folderSizes: {}, missingFiles: [], inconsistentKeys: [], emptyFiles: [], malformedFiles: [], duplicateKeys: [] }; this.rl = null; } async initialize() { try { const cliArgs = parseCommonArgs(process.argv.slice(2)); if (cliArgs.help) { this.showHelp(); process.exit(0); } const baseConfig = await getUnifiedConfig('summary', cliArgs); this.config = baseConfig; const uiLanguage = this.config.uiLanguage || 'en'; loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales')); this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir; this.config.sourceDir = this.sourceDir; if (!SecurityUtils.safeExistsSync(this.sourceDir, process.cwd())) { const err = new Error(`Locale directory not found: ${this.sourceDir}`); err.exitCode = 2; throw err; } } catch (error) { console.error(`Error initializing summary reporter: ${error.message}`); throw error; } } // 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() { const { closeGlobalReadline } = require('../utils/cli'); closeGlobalReadline(); } // Show help information showHelp() { console.log(`\n${t('summary.helpTitle')}\n`); console.log(t('summary.helpDescription') + '\n'); console.log(t('summary.helpUsage') + '\n'); console.log(t('summary.helpOptions')); console.log(t('summary.helpSourceDir')); console.log(t('summary.helpOutput')); console.log(t('summary.helpVerbose')); console.log(t('summary.helpKeepReports')); console.log(t('summary.helpDeleteReports')); console.log(t('summary.helpHelp') + '\n'); console.log(t('summary.helpExamples')); console.log(t('summary.helpExample1')); console.log(t('summary.helpExample2')); console.log(t('summary.helpExample3')); console.log(t('summary.helpExample4')); console.log(t('summary.helpExample5') + '\n'); } // Get all available languages getAvailableLanguages() { if (!SecurityUtils.safeExistsSync(this.config.sourceDir, this.config.sourceDir)) { return []; } // Check for monolith JSON files (en.json, es.json, etc.) const files = SecurityUtils.safeReaddirSync(this.config.sourceDir, this.config.sourceDir) || []; const languages = files .filter(file => file.endsWith('.json')) .map(file => path.basename(file, '.json')); // Also check for directory-based structure for backward compatibility const directories = fs.readdirSync(this.config.sourceDir) .filter(item => { const itemPath = path.join(this.config.sourceDir, item); const stats = SecurityUtils.safeStatSync(itemPath, this.config.sourceDir); return stats && stats.isDirectory() && !item.startsWith('.') && item !== 'node_modules'; }); return [...new Set([...languages, ...directories])].sort(); } // Get all translation files for a language getLanguageFiles(language) { const languageDir = path.join(this.config.sourceDir, language); if (!SecurityUtils.safeExistsSync(languageDir, this.config.sourceDir)) { return []; } return SecurityUtils.safeReaddirSync(languageDir, this.config.sourceDir) || [] .filter(file => { return this.config.supportedExtensions.some(ext => file.endsWith(ext)) && !this.config.excludeFiles.includes(file); }) .sort(); } // Get file size information getFileSize(filePath) { try { const stats = SecurityUtils.safeStatSync(filePath, this.config.sourceDir); return { size: stats.size, sizeFormatted: this.formatFileSize(stats.size), lastModified: stats.mtime }; } catch (error) { return { size: 0, sizeFormatted: '0 B', lastModified: null }; } } // Format file size in human readable format formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // Calculate folder size calculateFolderSize(folderPath) { let totalSize = 0; try { const items = SecurityUtils.safeReaddirSync(folderPath, this.config.sourceDir) || []; for (const item of items) { const itemPath = path.join(folderPath, item); const stats = SecurityUtils.safeStatSync(itemPath, this.config.sourceDir); if (stats.isDirectory()) { totalSize += this.calculateFolderSize(itemPath); } else { totalSize += stats.size; } } } catch (error) { // Ignore errors for inaccessible files } return totalSize; } // Extract keys from a translation file async extractKeysFromFile(filePath) { try { const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir); if (!content) { console.warn(t('summary.couldNotReadFile', { filePath })); return []; } if (filePath.endsWith('.json')) { const data = JSON.parse(content); return this.extractKeysFromObject(data); } else if (filePath.endsWith('.js') || filePath.endsWith('.ts') || filePath.endsWith('.py')) { // Basic extraction for JS/TS files (assumes export default or module.exports) if (!content) return []; const contentStr = String(content); const match = contentStr.match(/(?:export\s+default|module\.exports\s*=)\s*({[\s\S]*})/);; if (match) { const objStr = match[1]; // Use safe JSON parsing instead of eval for security try { // Convert JS object literal to valid JSON by replacing single quotes and removing trailing commas const jsonStr = objStr .replace(/'/g, '"') .replace(/,\s*}/g, '}') .replace(/,\s*]/g, ']') .replace(/([{,]\s*)(\w+):/g, '$1"$2":'); const data = JSON.parse(jsonStr); return this.extractKeysFromObject(data); } catch (e) { console.warn(t('summary.couldNotParseJSFile', { filePath })); return []; } } } return []; } catch (error) { console.warn(t('summary.errorReadingFile', { filePath, error: error.message })); return []; } } // Extract keys recursively from an object extractKeysFromObject(obj, prefix = '') { const keys = []; for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; if (value && typeof value === 'object' && !Array.isArray(value)) { keys.push(...this.extractKeysFromObject(value, fullKey)); } else { keys.push(fullKey); } } return keys; } // Check if file is empty or has no meaningful content async isFileEmpty(filePath) { try { const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir); if (!content) return true; const trimmedContent = content.trim(); if (!trimmedContent) return true; if (filePath.endsWith('.json')) { const data = SecurityUtils.safeParseJSON(trimmedContent); if (!data) return true; return Object.keys(data).length === 0; } return false; } catch (error) { return false; // If we can't parse it, it's not empty, just malformed } } // Check if file is malformed async isFileMalformed(filePath) { try { const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir); if (!content) return true; if (filePath.endsWith('.json')) { const parsed = SecurityUtils.safeParseJSON(content); if (!parsed) return true; } return false; } catch (error) { return true; } } // Find duplicate keys within a file async findDuplicateKeys(filePath) { try { const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir); if (!content) return []; if (filePath.endsWith('.json')) { // Parse the JSON and check for actual duplicate keys using full path const data = SecurityUtils.safeParseJSON(content); if (!data) return []; // Get all keys with full path const allKeys = this.extractKeysFromObject(data); // Find actual duplicates (same full path) const seen = new Set(); const duplicates = new Set(); for (const key of allKeys) { if (seen.has(key)) { duplicates.add(key); } seen.add(key); } return [...duplicates]; } return []; } catch (error) { return []; } } // Analyze folder structure and collect statistics async analyzeStructure() { console.log(t('summary.analyzingFolder')); this.stats.languages = this.getAvailableLanguages(); if (this.stats.languages.length === 0) { console.log(t('summary.noLanguageDirectoriesFound')); return; } console.log(t('summary.foundLanguages', {count: this.stats.languages.length, languages: this.stats.languages.join(', ')})); // Calculate folder sizes for each language for (const language of this.stats.languages) { const languageDir = path.join(this.config.sourceDir, language); if (SecurityUtils.safeExistsSync(languageDir, this.config.sourceDir)) { this.stats.folderSizes[language] = this.calculateFolderSize(languageDir); } } // Use first language as reference for file structure const referenceLanguage = this.stats.languages[0]; const referenceFiles = this.getLanguageFiles(referenceLanguage); console.log(t('summary.referenceLanguageFiles', {language: referenceLanguage, count: referenceFiles.length})); // Analyze each language for (const language of this.stats.languages) { console.log(t('summary.analyzingLanguage', {language})); const files = this.getLanguageFiles(language); this.stats.filesByLanguage[language] = files; this.stats.keysByLanguage[language] = {}; this.stats.fileSizes[language] = {}; let totalKeysForLanguage = 0; let totalSizeForLanguage = 0; // Check for missing files compared to reference const missingFiles = referenceFiles.filter(file => !files.includes(file)); if (missingFiles.length > 0) { this.stats.missingFiles.push({ language, files: missingFiles }); } // Analyze each file for (const file of files) { const filePath = path.join(this.config.sourceDir, language, file); // Get file size information const fileInfo = this.getFileSize(filePath); this.stats.fileSizes[language][file] = fileInfo; totalSizeForLanguage += fileInfo.size; // Check if file is empty if (await this.isFileEmpty(filePath)) { this.stats.emptyFiles.push({ language, file }); } // Check if file is malformed if (await this.isFileMalformed(filePath)) { this.stats.malformedFiles.push({ language, file }); continue; } // Find duplicate keys const duplicates = await this.findDuplicateKeys(filePath); if (duplicates.length > 0) { this.stats.duplicateKeys.push({ language, file, duplicates }); } // Extract keys const keys = await this.extractKeysFromFile(filePath); this.stats.keysByLanguage[language][file] = keys; totalKeysForLanguage += keys.length; this.stats.totalFiles++; } this.stats.totalKeys += totalKeysForLanguage; console.log(t('summary.keysInFiles', {keys: totalKeysForLanguage, files: files.length})); } // Find inconsistent keys across languages this.findInconsistentKeys(); } // Find keys that are inconsistent across languages findInconsistentKeys() { console.log(t('summary.checkingInconsistentKeys')); const referenceLanguage = this.stats.languages[0]; const referenceKeys = this.stats.keysByLanguage[referenceLanguage]; for (const file of Object.keys(referenceKeys)) { const refKeys = new Set(referenceKeys[file]); for (const language of this.stats.languages.slice(1)) { const langKeys = this.stats.keysByLanguage[language][file] || []; const langKeySet = new Set(langKeys); // Find missing keys in this language const missingInLang = [...refKeys].filter(key => !langKeySet.has(key)); // Find extra keys in this language const extraInLang = [...langKeySet].filter(key => !refKeys.has(key)); if (missingInLang.length > 0 || extraInLang.length > 0) { this.stats.inconsistentKeys.push({ file, language, missing: missingInLang, extra: extraInLang }); } } } } // Generate formatted table row generateTableRow(cells, widths) { const row = cells.map((cell, index) => { const width = widths[index] || 15; return String(cell).padEnd(width); }).join(' | '); return row; } // Generate table separator generateTableSeparator(widths) { return widths.map(width => '-'.repeat(width)).join('-+-'); } // Generate summary report generateDetailedReport() { const report = []; report.push('I18NTK DETAILED DUPLICATE KEYS REPORT'); report.push('='.repeat(40)); report.push(`Generated: ${new Date().toISOString()}`); report.push(`Source directory: ${this.config.sourceDir}`); report.push(''); if (this.stats.duplicateKeys.length > 0) { report.push('🔑 ACTUAL DUPLICATE KEYS (Same Full Path)'); report.push('='.repeat(35)); let grandTotal = 0; for (const item of this.stats.duplicateKeys) { if (item.duplicates.length > 0) { report.push(`\n${item.language.toUpperCase()}/${item.file}:`); report.push(`Found ${item.duplicates.length} duplicate keys:`); for (const key of item.duplicates) { report.push(` - "${key}"`); } grandTotal += item.duplicates.length; } } report.push(''); report.push(`TOTAL DUPLICATE KEYS ACROSS ALL FILES: ${grandTotal}`); report.push(''); report.push('📋 RECOMMENDATIONS FOR FIXING DUPLICATES'); report.push('='.repeat(35)); report.push('1. Review each file listed above'); report.push('2. Remove duplicate key definitions'); report.push('3. Ensure each key is defined only once per file'); report.push('4. Check for actual duplicate key definitions at the same level'); report.push('5. Note: Keys with same name but different paths (e.g., "hero.title" vs "stats.title") are NOT duplicates'); } else { report.push('✅ No actual duplicate keys found in any files.'); report.push('Note: Keys with the same name but in different nested structures are not considered duplicates.'); } return report.join('\n'); } generateReport() { const report = []; const timestamp = new Date().toISOString(); report.push(t('summary.reportTitle')); report.push('='.repeat(50)); report.push(t('summary.generated', {timestamp})); report.push(t('summary.sourceDirectory', {dir: this.config.sourceDir})); report.push(''); // Overview with sizing report.push(t('summary.overview')); report.push('='.repeat(30)); report.push(t('summary.languagesCount', {count: this.stats.languages.length})); report.push(t('summary.totalFiles', {count: this.stats.totalFiles})); report.push(t('summary.totalKeys', {count: this.stats.totalKeys})); const averageKeysPerLanguage = this.stats.languages.length > 0 ? Math.round(this.stats.totalKeys / this.stats.languages.length) : 0; report.push(t('summary.avgKeysPerLanguage', {count: averageKeysPerLanguage})); // Calculate total size const totalSize = Object.values(this.stats.folderSizes).reduce((sum, size) => sum + size, 0); report.push(`Total Size: ${this.formatFileSize(totalSize)}`); report.push(''); // Languages breakdown with sizing report.push(t('summary.languagesBreakdown')); report.push('='.repeat(30)); const langTableWidths = [12, 8, 8, 12, 12]; report.push(this.generateTableRow(['Language', 'Files', 'Keys', 'Folder Size', 'Avg Keys/File'], langTableWidths)); report.push(this.generateTableSeparator(langTableWidths)); for (const language of this.stats.languages) { const files = this.stats.filesByLanguage[language]; const totalKeys = Object.values(this.stats.keysByLanguage[language]) .reduce((sum, keys) => sum + keys.length, 0); const folderSize = this.stats.folderSizes[language] || 0; const avgKeys = files.length > 0 ? Math.round(totalKeys / files.length) : 0; report.push(this.generateTableRow([ language.toUpperCase(), files.length, totalKeys, this.formatFileSize(folderSize), avgKeys ], langTableWidths)); } report.push(''); // File structure with sizing report.push(t('summary.fileStructure')); report.push('='.repeat(30)); const fileTableWidths = [20, 8, 12, 15, 15]; report.push(this.generateTableRow(['File', 'Keys', 'Size', 'Languages', 'Missing In'], fileTableWidths)); report.push(this.generateTableSeparator(fileTableWidths)); const referenceLanguage = this.stats.languages[0]; const referenceFiles = this.stats.filesByLanguage[referenceLanguage] || []; for (const file of referenceFiles) { const keysInFile = this.stats.keysByLanguage[referenceLanguage][file]?.length || 0; // Calculate total size across all languages for this file let totalFileSize = 0; let languagesWithFile = []; let missingIn = []; for (const language of this.stats.languages) { if (this.stats.filesByLanguage[language].includes(file)) { languagesWithFile.push(language.toUpperCase()); const fileSize = this.stats.fileSizes[language][file]?.size || 0; totalFileSize += fileSize; } else { missingIn.push(language.toUpperCase()); } } report.push(this.generateTableRow([ file, keysInFile, this.formatFileSize(totalFileSize), languagesWithFile.join(', '), missingIn.join(', ') ], fileTableWidths)); } report.push(''); // Issues if (this.stats.missingFiles.length > 0 || this.stats.emptyFiles.length > 0 || this.stats.malformedFiles.length > 0 || this.stats.duplicateKeys.length > 0 || this.stats.inconsistentKeys.length > 0) { report.push(t('summary.issuesFound')); report.push('='.repeat(30)); if (this.stats.missingFiles.length > 0) { report.push(t('summary.missingFiles')); for (const item of this.stats.missingFiles) { report.push(` ${item.language}: ${item.files.join(', ')}`); } report.push(''); } if (this.stats.emptyFiles.length > 0) { report.push(t('summary.emptyFiles')); for (const item of this.stats.emptyFiles) { report.push(` ${item.language}/${item.file}`); } report.push(''); } if (this.stats.malformedFiles.length > 0) { report.push(t('summary.malformedFiles')); for (const item of this.stats.malformedFiles) { report.push(` ${item.language}/${item.file}`); } report.push(''); } // Count total duplicate keys const totalDuplicateKeys = this.stats.duplicateKeys.reduce((sum, item) => sum + item.duplicates.length, 0); if (totalDuplicateKeys > 100) { // Auto-generate detailed report for large number of duplicates report.push('🔑 Duplicate keys detected - generating detailed report...'); report.push(' A comprehensive duplicate keys report has been created in i18ntk-reports/'); report.push(' See the detailed report for complete list of duplicates to fix.'); report.push(' Note: Only keys with identical full paths are considered duplicates.'); report.push(''); } else if (this.stats.duplicateKeys.length > 0) { // Show limited display for small number of duplicates report.push(t('summary.duplicateKeys')); let totalDuplicates = 0; for (const item of this.stats.duplicateKeys) { const displayKeys = item.duplicates.slice(0, 25); const remaining = item.duplicates.length - 25; report.push(` ${item.language}/${item.file}: ${displayKeys.join(', ')}${remaining > 0 ? ` ... (+${remaining} more)` : ''}`); totalDuplicates += item.duplicates.length; } report.push(` (Total actual duplicate keys: ${totalDuplicates})`); report.push(' Note: Keys with same name but different paths (e.g., "hero.title" vs "stats.title") are NOT duplicates'); report.push(''); } if (this.stats.inconsistentKeys.length > 0) { report.push(t('summary.inconsistentKeys')); for (const item of this.stats.inconsistentKeys) { report.push(` ${item.language}/${item.file}:`); if (item.missing.length > 0) { report.push(t('summary.missingKeys', {keys: item.missing.slice(0, 5).join(', '), more: item.missing.length > 5 ? '...' : ''})); } if (item.extra.length > 0) { report.push(t('summary.extraKeys', {keys: item.extra.slice(0, 5).join(', '), more: item.extra.length > 5 ? '...' : ''})); } } report.push(''); } } else if (this.stats.languages.length === 0) { report.push('⚠️ NO LOCALE LANGUAGES FOUND'); report.push('='.repeat(30)); report.push('No locale language files or directories were found in the selected locale root.'); report.push(''); } else { report.push(t('summary.noIssuesFound')); report.push('='.repeat(30)); report.push(t('summary.allFilesConsistent')); report.push(''); report.push('Note: Keys with the same name but in different nested structures are not considered duplicates.'); report.push(''); } // Sizing Analysis report.push('Sizing Analysis'); report.push('='.repeat(30)); const totalTranslationSize = Object.values(this.stats.folderSizes).reduce((sum, size) => sum + size, 0); report.push(`Total Translation Size: ${this.formatFileSize(totalTranslationSize)}`); if (totalTranslationSize > 1024 * 1024) { // > 1MB report.push('⚠️ Large translation bundle detected - consider optimization'); } if (totalTranslationSize > 5 * 1024 * 1024) { // > 5MB report.push('🔴 Very large translation bundle - immediate optimization needed'); } // Largest languages const sortedLanguages = this.stats.languages .map(lang => ({ language: lang, size: this.stats.folderSizes[lang] || 0, files: this.stats.filesByLanguage[lang].length, keys: Object.values(this.stats.keysByLanguage[lang]) .reduce((sum, keys) => sum + keys.length, 0) })) .sort((a, b) => b.size - a.size); if (sortedLanguages.length > 0) { report.push('\nLargest Languages:'); const sizeTableWidths = [12, 12, 8, 8]; report.push(this.generateTableRow(['Language', 'Size', 'Files', 'Keys'], sizeTableWidths)); report.push(this.generateTableSeparator(sizeTableWidths)); sortedLanguages.slice(0, 5).forEach(lang => { report.push(this.generateTableRow([ lang.language.toUpperCase(), this.formatFileSize(lang.size), lang.files, lang.keys ], sizeTableWidths)); }); } // File size distribution const allFiles = []; for (const language of this.stats.languages) { for (const file of this.stats.filesByLanguage[language]) { const fileSize = this.stats.fileSizes[language][file]?.size || 0; allFiles.push({ language: language, file: file, size: fileSize, keys: this.stats.keysByLanguage[language][file]?.length || 0 }); } } const largeFiles = allFiles.filter(f => f.size > 50 * 1024); // > 50KB if (largeFiles.length > 0) { report.push('\nLarge Files (>50KB):'); const largeFileWidths = [15, 15, 10, 8]; report.push(this.generateTableRow(['File', 'Language', 'Size', 'Keys'], largeFileWidths)); report.push(this.generateTableSeparator(largeFileWidths)); largeFiles.slice(0, 10).forEach(f => { report.push(this.generateTableRow([ f.file, f.language.toUpperCase(), this.formatFileSize(f.size), f.keys ], largeFileWidths)); }); } report.push(''); // Recommendations report.push(t('summary.recommendations')); report.push('='.repeat(30)); if (this.stats.missingFiles.length > 0) { report.push(t('summary.createMissingFiles')); } if (this.stats.emptyFiles.length > 0) { report.push(t('summary.addContentToEmptyFiles')); } if (this.stats.malformedFiles.length > 0) { report.push(t('summary.fixMalformedFiles')); } if (this.stats.duplicateKeys.length > 0) { report.push(t('summary.removeDuplicateKeys')); } if (this.stats.inconsistentKeys.length > 0) { report.push(t('summary.synchronizeKeys')); } if (this.stats.totalKeys > 10000) { report.push(t('summary.splitLargeFiles')); } if (this.stats.languages.length === 1) { report.push(t('summary.addMoreLanguages')); } // Sizing-specific recommendations if (totalTranslationSize > 1024 * 1024) { report.push('• Consider lazy loading translations for specific languages'); report.push('• Implement translation splitting for large files'); report.push('• Use compression for production builds'); } report.push(''); report.push(t('summary.nextSteps')); report.push(t('summary.nextStep1')); report.push(t('summary.nextStep2')); report.push(t('summary.nextStep3')); report.push(t('summary.nextStep4')); return report.join('\n'); } // Parse command line arguments parseArgs() { const args = process.argv.slice(2); const parsed = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg.startsWith('--')) { const [key, rawValue] = arg.substring(2).split('='); const value = rawValue !== undefined ? rawValue : (args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true); if (key === 'source-dir' || key === 'locales-dir' || key === 'i18n-dir') { parsed.sourceDir = value; parsed.i18nDir = value; } else if (key === 'source-language' || key === 'source-locale') { parsed.sourceLanguage = value; } else if (key === 'code-dir' || key === 'source-code-dir') { parsed.codeDir = value; } else if (key === 'output-file') { parsed.outputFile = value; } else if (key === 'verbose') { parsed.verbose = true; } else if (key === 'delete-reports') { parsed.deleteReports = true; } else if (key === 'keep-reports') { parsed.keepReports = true; } else if (key === 'no-prompt') { parsed.noPrompt = true; } else if (key === 'help') { parsed.help = true; } } } return parsed; } // Run the analysis and generate report async run(options = {}) { const args = this.parseArgs(); const { fromMenu = false, noPrompt = false } = options; // Always use unified configuration to respect settings const baseConfig = await getUnifiedConfig('summary', args); this.config = { ...this.config, ...baseConfig }; const uiLanguage = this.config.uiLanguage || 'en'; loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales')); if (!this.config.sourceDir) { this.config.sourceDir = this.detectI18nDirectory(); } this.config.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir; this.sourceDir = this.config.sourceDir; if (!fromMenu) { // Check admin authentication for sensitive operations (only when called directly) 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 && !noPrompt && !fromMenu && !args.noPrompt) { console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'generate summary' })); const cliHelper = require('../utils/cli-helper'); const pin = await cliHelper.promptPin(t('adminCli.enterPin')); const isValid = await adminAuth.verifyPin(pin); if (!isValid) { console.log(t('adminCli.invalidPin')); process.exit(1); } console.log(t('adminCli.authenticationSuccess')); } if (args.help) { this.showHelp(); return; } // Validate source directory exists if (!SecurityUtils.safeExistsSync(this.config.sourceDir, process.cwd())) { console.error(t('summary.sourceDirectoryDoesNotExist', { sourceDir: this.config.sourceDir })); process.exit(2); } } console.log(t('summary.i18nSummaryReportGenerator')); console.log(t('summary.separator')); console.log(t('summary.sourceDirectory', {dir: this.config.sourceDir})); if (args.verbose) { console.log(t('summary.configurationTitle')); console.log(t('summary.sourceLanguage', { sourceLanguage: this.config.sourceLanguage })); console.log(t('summary.supportedExtensions', { extensions: this.config.supportedExtensions.join(', ') })); console.log(t('summary.excludedFiles', { files: this.config.excludeFiles.join(', ') })); } try { // Analyze structure await this.analyzeStructure(); // Generate report console.log(t('summary.generatingSummaryReport')); const report = this.generateReport(); // Always generate detailed report when duplicates exist const totalDuplicateKeys = this.stats.duplicateKeys.reduce((sum, item) => sum + item.duplicates.length, 0); if (totalDuplicateKeys > 0) { console.log('📋 Generating detailed duplicate keys report...'); const detailedReport = this.generateDetailedReport(); // Create output directory if it doesn't exist const outputDir = path.resolve(process.cwd(), 'i18ntk-reports'); if (!SecurityUtils.safeExistsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const detailedReportName = totalDuplicateKeys > 100 ? `duplicate-keys-detailed-${new Date().toISOString().slice(0,10)}.txt` : `duplicate-keys-summary-${new Date().toISOString().slice(0,10)}.txt`; const detailedReportPath = path.join(outputDir, detailedReportName); const detailedSuccess = await SecurityUtils.safeWriteFile(detailedReportPath, detailedReport, outputDir); if (detailedSuccess) { if (totalDuplicateKeys > 100) { console.log(`✅ Detailed duplicate keys report saved: ${detailedReportPath}`); } else { console.log(`✅ Duplicate keys summary saved: ${detailedReportPath}`); } } else { console.log(`⚠️ Could not save detailed report: ${detailedReportPath}`); } } // Output report if (args.outputFile) { // Always save summary reports to i18ntk-reports const reportsDir = path.resolve(process.cwd(), 'i18ntk-reports'); if (!SecurityUtils.safeExistsSync(reportsDir)) { fs.mkdirSync(reportsDir, { recursive: true }); } const outputFileName = args.outputFile ? path.basename(args.outputFile) : `summary-report-${new Date().toISOString().slice(0,10)}.txt`; const outputPath = path.join(reportsDir, outputFileName); const success = await SecurityUtils.safeWriteFile(outputPath, report, reportsDir); if (success) { console.log(t('summary.reportSaved', { reportPath: outputPath })); } else { console.log(t('summary.reportSaveFailed', { reportPath: outputPath })); } } else { console.log(t('summary.reportContent', { report })); } // Handle report file management if (args.deleteReports && !args.keepReports) { console.log(t('summary.cleaningUpReportFiles')); try { const reportsDir = path.join(this.config.sourceDir, 'scripts', 'i18n', 'reports'); if (SecurityUtils.safeExistsSync(reportsDir)) { const files = SecurityUtils.safeReaddirSync(reportsDir, this.config.sourceDir) || []; const reportFiles = files.filter(file => (file.endsWith('.txt') || file.endsWith('.json') || file.endsWith('.log')) && file !== path.basename(args.outputFile || '') ); let deletedCount = 0; for (const file of reportFiles) { try { // Note: Using direct fs.unlinkSync as SecurityUtils.safeUnlinkSync doesn't exist yet // This should be replaced with a secure version when available fs.unlinkSync(path.join(reportsDir, file)); deletedCount++; } catch (error) { console.log(t('summary.couldNotDelete', { file, error: error.message })); } } if (deletedCount > 0) { console.log(t('summary.deletedOldReportFiles', { count: deletedCount })); } else { console.log(t('summary.noOldReportFilesToDelete')); } } } catch (error) { console.log(t('summary.errorCleaningUpReports', { error: error.message })); } } else if (args.keepReports) { console.log(t('summary.reportFilesPreserved')); } // Summary console.log(t('summary.separator')); console.log(t('summary.analysisComplete')); console.log(t('summary.separator')); console.log(t('summary.analyzedLanguages', { count: this.stats.languages.length })); console.log(t('summary.processedFiles', { count: this.stats.totalFiles })); console.log(t('summary.foundTranslationKeys', { count: this.stats.totalKeys })); const totalIssues = this.stats.missingFiles.length + this.stats.emptyFiles.length + this.stats.malformedFiles.length + this.stats.duplicateKeys.length + this.stats.inconsistentKeys.length; if (totalIssues > 0) { console.log(t('summary.foundIssues', {count: totalIssues})); } else if (this.stats.languages.length === 0) { console.log('⚠️ No locale languages found in the selected locale root.'); } else { console.log(t('summary.noIssuesConsole')); } } catch (error) { console.error(t('summary.errorDuringAnalysis', { error: error.message })); if (args.verbose) { console.error(error.stack); } process.exit(error.exitCode || 1); } process.exit(0); } } // Run if called directly if (require.main === module) { async function main() { const reporter = new I18nSummaryReporter(); await reporter.initialize(); await reporter.run(); } main().catch(error => { console.error('Error in i18ntk-summary:', error.message); process.exit(error.exitCode || 1); }); } module.exports = I18nSummaryReporter;