i18ntk 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,7 +33,6 @@
33
33
 
34
34
  const fs = require('fs');
35
35
  const path = require('path');
36
- const { performance } = require('perf_hooks');
37
36
  const { loadTranslations, t } = require('../utils/i18n-helper');
38
37
  const configManager = require('../settings/settings-manager');
39
38
  const SecurityUtils = require('../utils/security');
@@ -675,7 +674,7 @@ Generated: ${new Date().toISOString()}
675
674
 
676
675
  // Main analysis method
677
676
  async analyze() {
678
- const startTime = performance.now();
677
+ const startTime = Date.now();
679
678
 
680
679
  try {
681
680
  logger.info(t("sizing.starting_i18n_sizing_analysis"));
@@ -702,8 +701,8 @@ Generated: ${new Date().toISOString()}
702
701
 
703
702
  await this.generateHumanReadableReport();
704
703
 
705
- const endTime = performance.now();
706
- logger.info(t("sizing.analysis_completed", { duration: (endTime - startTime).toFixed(2) }));
704
+ const endTime = Date.now();
705
+ logger.info(t("sizing.analysis_completed", { duration: ((endTime - startTime) / 1000).toFixed(2) }));
707
706
 
708
707
  } catch (error) {
709
708
  logger.error(t("sizing.analysis_failed", { errorMessage: error.message }));
@@ -910,7 +909,7 @@ Options:
910
909
  logger.info(t("sizing.starting_analysis"));
911
910
  logger.info(t("sizing.source_directory", { sourceDir: this.sourceDir }));
912
911
 
913
- const startTime = performance.now();
912
+ const startTime = Date.now();
914
913
 
915
914
  // Get language files
916
915
  const files = this.getLanguageFiles();
@@ -936,8 +935,8 @@ Options:
936
935
  // Generate reports if requested
937
936
  await this.generateHumanReadableReport();
938
937
 
939
- const endTime = performance.now();
940
- const duration = ((endTime - startTime) / 1000).toFixed(2);
938
+ const endTime = Date.now();
939
+ const duration = ((endTime - startTime) / 1000).toFixed(2);
941
940
 
942
941
  logger.info(t("sizing.analysis_completed", { duration }));
943
942
 
@@ -961,4 +960,4 @@ if (require.main === module) {
961
960
  });
962
961
  }
963
962
 
964
- module.exports = I18nSizingAnalyzer;
963
+ module.exports = I18nSizingAnalyzer;
@@ -786,10 +786,22 @@ Analysis Features (v1.8.3):
786
786
  return keys;
787
787
  }
788
788
 
789
- collectPlaceholderKeys(obj, prefix = '', language) {
790
- const patterns = this.placeholderStyles[language] || [];
791
- const regexes = patterns.map(p => new RegExp(p));
792
- if (typeof obj !== 'object' || obj === null) return;
789
+ collectPlaceholderKeys(obj, prefix = '', language) {
790
+ const patterns = this.placeholderStyles[language] || [];
791
+ const regexes = patterns.reduce((compiled, pattern) => {
792
+ try {
793
+ compiled.push(new RegExp(pattern));
794
+ } catch (error) {
795
+ SecurityUtils.logSecurityEvent('Invalid placeholder regex pattern skipped', 'warn', {
796
+ component: 'i18ntk-usage',
797
+ language,
798
+ pattern,
799
+ error: error.message
800
+ });
801
+ }
802
+ return compiled;
803
+ }, []);
804
+ if (typeof obj !== 'object' || obj === null) return;
793
805
 
794
806
  for (const [key, value] of Object.entries(obj)) {
795
807
  const fullKey = prefix ? `${prefix}.${key}` : key;
@@ -1837,4 +1849,4 @@ if (require.main === module) {
1837
1849
  // Normal direct execution
1838
1850
  main();
1839
1851
  }
1840
- }
1852
+ }
@@ -186,18 +186,31 @@ class I18nValidator {
186
186
  this.warnings.push({ message, details, type: 'warning' });
187
187
  }
188
188
 
189
- // Get all available languages
190
- getAvailableLanguages() {
191
- try {
192
- if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
193
- throw new Error(`Source directory not found: ${this.sourceDir}`);
194
- }
195
-
196
- const languages = fs.readdirSync(this.sourceDir)
197
- .filter(item => {
198
- const itemPath = path.join(this.sourceDir, item);
199
- return fs.statSync(itemPath).isDirectory() && item !== this.config.sourceLanguage;
200
- });
189
+ // Get all available languages
190
+ isExcludedLanguageDirectory(name) {
191
+ if (!name || typeof name !== 'string') return true;
192
+ const lowered = name.toLowerCase();
193
+ return lowered.startsWith('backup-') ||
194
+ lowered === 'backup' ||
195
+ lowered === 'backups' ||
196
+ lowered === 'i18ntk-backups' ||
197
+ lowered === 'reports' ||
198
+ lowered === 'i18ntk-reports';
199
+ }
200
+
201
+ getAvailableLanguages() {
202
+ try {
203
+ if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
204
+ throw new Error(`Source directory not found: ${this.sourceDir}`);
205
+ }
206
+
207
+ const languages = fs.readdirSync(this.sourceDir)
208
+ .filter(item => {
209
+ const itemPath = path.join(this.sourceDir, item);
210
+ return fs.statSync(itemPath).isDirectory() &&
211
+ item !== this.config.sourceLanguage &&
212
+ !this.isExcludedLanguageDirectory(item);
213
+ });
201
214
 
202
215
  return languages;
203
216
  } catch (error) {
@@ -620,10 +633,42 @@ class I18nValidator {
620
633
  return warnings;
621
634
  }
622
635
 
623
- // Show help message
624
- showHelp() {
625
- console.log(t('validate.help_message'));
626
- }
636
+ // Show help message
637
+ showHelp() {
638
+ console.log(t('validate.help_message'));
639
+ }
640
+
641
+ saveValidationSummaryReport(results = {}, success = true) {
642
+ try {
643
+ const outputDir = path.resolve(this.config.outputDir || './i18ntk-reports');
644
+ SecurityUtils.safeMkdirSync(outputDir, process.cwd(), { recursive: true });
645
+
646
+ const timestamp = new Date().toISOString();
647
+ const safeTimestamp = timestamp.replace(/[:.]/g, '-');
648
+ const reportPath = path.join(outputDir, `validation-summary-${safeTimestamp}.txt`);
649
+
650
+ const lines = [];
651
+ lines.push('I18NTK Validation Summary');
652
+ lines.push('========================');
653
+ lines.push(`Generated: ${timestamp}`);
654
+ lines.push(`Result: ${success ? 'PASS' : 'FAIL'}`);
655
+ lines.push(`Errors: ${this.errors.length}`);
656
+ lines.push(`Warnings: ${this.warnings.length}`);
657
+ lines.push('');
658
+ lines.push('Language Results');
659
+ lines.push('----------------');
660
+
661
+ Object.entries(results).forEach(([language, validation]) => {
662
+ const summary = validation?.summary || {};
663
+ lines.push(`${language}: ${summary.percentage || 0}% (${summary.translatedKeys || 0}/${summary.totalKeys || 0})`);
664
+ });
665
+
666
+ SecurityUtils.safeWriteFileSync(reportPath, lines.join('\n') + '\n', process.cwd(), 'utf8');
667
+ return reportPath;
668
+ } catch (error) {
669
+ return null;
670
+ }
671
+ }
627
672
 
628
673
  // Main validation process
629
674
  async validate() {
@@ -854,11 +899,16 @@ class I18nValidator {
854
899
  console.log(t('validate.considerRunningUsageAnalysis'));
855
900
  }
856
901
 
857
- // Exit with appropriate code
858
- const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
859
-
860
- return {
861
- success,
902
+ // Exit with appropriate code
903
+ const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
904
+ const summaryReportPath = this.saveValidationSummaryReport(results, success);
905
+ if (summaryReportPath) {
906
+ console.log('');
907
+ console.log(`📄 Validation summary report saved: ${summaryReportPath}`);
908
+ }
909
+
910
+ return {
911
+ success,
862
912
  errors: this.errors.length,
863
913
  warnings: this.warnings.length,
864
914
  results
@@ -1051,4 +1101,4 @@ if (require.main === module) {
1051
1101
  process.exit(ExitCodes.CONFIG_ERROR);
1052
1102
  }
1053
1103
  })();
1054
- }
1104
+ }
@@ -7,14 +7,15 @@
7
7
  * Contains embedded business logic from I18nAnalyzer.
8
8
  */
9
9
 
10
- const path = require('path');
11
- const cliHelper = require('../../../utils/cli-helper');
12
- const { loadTranslations, t } = require('../../../utils/i18n-helper');
13
- const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../../../utils/config-helper');
14
- const SecurityUtils = require('../../../utils/security');
15
- const AdminCLI = require('../../../utils/admin-cli');
16
- const watchLocales = require('../../../utils/watch-locales');
17
- const JsonOutput = require('../../../utils/json-output');
10
+ const path = require('path');
11
+ const cliHelper = require('../../../utils/cli-helper');
12
+ const { loadTranslations, t } = require('../../../utils/i18n-helper');
13
+ const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../../../utils/config-helper');
14
+ const SecurityUtils = require('../../../utils/security');
15
+ const AdminCLI = require('../../../utils/admin-cli');
16
+ const AdminAuth = require('../../../utils/admin-auth');
17
+ const watchLocales = require('../../../utils/watch-locales');
18
+ const JsonOutput = require('../../../utils/json-output');
18
19
 
19
20
  loadTranslations('en', path.resolve(__dirname, '../../../ui-locales'));
20
21
 
@@ -81,8 +82,7 @@ class AnalyzeCommand {
81
82
  this.outputDir = this.config.outputDir;
82
83
 
83
84
  // Validate source directory exists
84
- const { validateSourceDir } = require('../../../utils/config-helper');
85
- validateSourceDir(this.sourceDir, 'i18ntk-analyze');
85
+ validateSourceDir(this.sourceDir, 'i18ntk-analyze');
86
86
 
87
87
  } catch (error) {
88
88
  console.error(`Fatal analysis error: ${error.message}`);
@@ -908,15 +908,13 @@ class AnalyzeCommand {
908
908
  const isCalledDirectly = require.main === module;
909
909
  if (isCalledDirectly && !args.noPrompt && !fromMenu) {
910
910
  // Only check admin authentication when running directly and not in no-prompt mode
911
- const AdminAuth = require('../../../utils/admin-auth');
912
- const adminAuth = new AdminAuth();
911
+ const adminAuth = new AdminAuth();
913
912
  await adminAuth.initialize();
914
913
 
915
914
  const isRequired = await adminAuth.isAuthRequired();
916
915
  if (isRequired) {
917
916
  console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'analyze translations' }));
918
- const cliHelper = require('../../../utils/cli-helper');
919
- const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
917
+ const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
920
918
  const isValid = await this.adminAuth.verifyPin(pin);
921
919
 
922
920
  if (!isValid) {
@@ -7,8 +7,9 @@
7
7
  * Handles command execution, authentication, and completion management.
8
8
  */
9
9
 
10
- const path = require('path');
11
- const { t } = require('../../../utils/i18n-helper');
10
+ const path = require('path');
11
+ const { t } = require('../../../utils/i18n-helper');
12
+ const cliHelper = require('../../../utils/cli-helper');
12
13
 
13
14
  // Import command handlers
14
15
  const InitCommand = require('./InitCommand');
@@ -90,20 +91,22 @@ class CommandRouter {
90
91
  /**
91
92
  * Check admin authentication for protected commands
92
93
  */
93
- async checkAdminAuth() {
94
- if (!this.adminAuth) {
95
- return true; // No auth service available, allow execution
96
- }
94
+ async checkAdminAuth(adminPin = null) {
95
+ if (!this.adminAuth) {
96
+ return true; // No auth service available, allow execution
97
+ }
97
98
 
98
99
  const isRequired = await this.adminAuth.isAuthRequired();
99
100
  if (!isRequired) {
100
101
  return true;
101
102
  }
102
103
 
103
- console.log(t('adminCli.authRequired'));
104
- const cliHelper = require('../../../utils/cli-helper');
105
- const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
106
- const isValid = await this.adminAuth.verifyPin(pin);
104
+ let pin = adminPin;
105
+ if (!pin) {
106
+ console.log(t('adminCli.authRequired'));
107
+ pin = await cliHelper.promptPin(t('adminCli.enterPin'));
108
+ }
109
+ const isValid = await this.adminAuth.verifyPin(pin);
107
110
 
108
111
  if (!isValid) {
109
112
  console.log(t('adminCli.invalidPin'));
@@ -141,7 +144,7 @@ class CommandRouter {
141
144
  ];
142
145
 
143
146
  if (authRequiredCommands.includes(command)) {
144
- const authPassed = await this.checkAdminAuth();
147
+ const authPassed = await this.checkAdminAuth(options.adminPin || null);
145
148
  if (!authPassed) {
146
149
  if (!this.isNonInteractiveMode && !isDirectCommand && this.prompt) {
147
150
  await this.prompt(t('menu.pressEnterToContinue'));
@@ -292,4 +295,4 @@ class CommandRouter {
292
295
  }
293
296
  }
294
297
 
295
- module.exports = CommandRouter;
298
+ module.exports = CommandRouter;
@@ -27,10 +27,21 @@ class FixerCommand {
27
27
  // Initialize fixer properties
28
28
  this.sourceDir = null;
29
29
  this.outputDir = null;
30
- this.backupDir = null;
31
- this.dryRun = false;
32
- this.force = false;
33
- }
30
+ this.backupDir = null;
31
+ this.dryRun = false;
32
+ this.force = false;
33
+ }
34
+
35
+ isExcludedLanguageDirectory(name) {
36
+ if (!name || typeof name !== 'string') return true;
37
+ const lowered = name.toLowerCase();
38
+ return lowered.startsWith('backup-') ||
39
+ lowered === 'backup' ||
40
+ lowered === 'backups' ||
41
+ lowered === 'i18ntk-backups' ||
42
+ lowered === 'reports' ||
43
+ lowered === 'i18ntk-reports';
44
+ }
34
45
 
35
46
  /**
36
47
  * Set runtime dependencies for interactive operations
@@ -68,7 +79,7 @@ class FixerCommand {
68
79
 
69
80
  this.sourceDir = this.config.sourceDir;
70
81
  this.outputDir = this.config.outputDir;
71
- this.backupDir = path.join(this.sourceDir, 'backup');
82
+ this.backupDir = path.resolve(this.config.backup?.location || './i18ntk-backups', 'fixer');
72
83
 
73
84
  // Validate source directory exists
74
85
  const { validateSourceDir } = require('../../../utils/config-helper');
@@ -127,10 +138,15 @@ class FixerCommand {
127
138
  const languages = [];
128
139
 
129
140
  // Check for directory-based structure
130
- const directories = items
131
- .filter(item => item.isDirectory())
132
- .map(item => item.name)
133
- .filter(name => name !== 'node_modules' && !name.startsWith('.') && name !== this.config.sourceLanguage);
141
+ const directories = items
142
+ .filter(item => item.isDirectory())
143
+ .map(item => item.name)
144
+ .filter(name =>
145
+ name !== 'node_modules' &&
146
+ !name.startsWith('.') &&
147
+ name !== this.config.sourceLanguage &&
148
+ !this.isExcludedLanguageDirectory(name)
149
+ );
134
150
 
135
151
  // Check for monolith files (language.json files)
136
152
  const files = items
@@ -142,9 +158,13 @@ class FixerCommand {
142
158
 
143
159
  // Add monolith files as languages (without .json extension)
144
160
  const monolithLanguages = files
145
- .map(file => file.replace('.json', ''))
146
- .filter(lang => !languages.includes(lang) && lang !== this.config.sourceLanguage);
147
- languages.push(...monolithLanguages);
161
+ .map(file => file.replace('.json', ''))
162
+ .filter(lang =>
163
+ !languages.includes(lang) &&
164
+ lang !== this.config.sourceLanguage &&
165
+ !this.isExcludedLanguageDirectory(lang)
166
+ );
167
+ languages.push(...monolithLanguages);
148
168
 
149
169
  return [...new Set(languages)].sort();
150
170
  } catch (error) {
@@ -279,14 +299,21 @@ class FixerCommand {
279
299
  }
280
300
 
281
301
  // Create backup of translation files
282
- async createBackup() {
283
- if (this.dryRun) return;
284
-
285
- try {
286
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
287
- this.backupDir = path.join(this.sourceDir, `backup-${timestamp}`);
288
-
289
- console.log(t('fixer.creatingBackup', { dir: this.backupDir }));
302
+ async createBackup() {
303
+ if (this.dryRun) return;
304
+
305
+ try {
306
+ const backupEnabled = this.config?.backup?.enabled === true;
307
+ if (!backupEnabled) {
308
+ this.backupDir = null;
309
+ return;
310
+ }
311
+
312
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
313
+ const backupRoot = path.resolve(this.config?.backup?.location || './i18ntk-backups');
314
+ this.backupDir = path.join(backupRoot, 'fixer', `backup-${timestamp}`);
315
+
316
+ console.log(t('fixer.creatingBackup', { dir: this.backupDir }));
290
317
 
291
318
  // Ensure backup directory exists
292
319
  const dirCreated = SecurityUtils.safeMkdirSync(this.backupDir, process.cwd(), { recursive: true });
@@ -299,8 +326,8 @@ class FixerCommand {
299
326
  const languages = this.getAvailableLanguages();
300
327
  languages.push(this.config.sourceLanguage); // Include source language
301
328
 
302
- for (const language of languages) {
303
- const languageFiles = this.getLanguageFiles(language);
329
+ for (const language of languages) {
330
+ const languageFiles = this.getLanguageFiles(language);
304
331
 
305
332
  for (const fileName of languageFiles) {
306
333
  const sourcePath = path.join(this.sourceDir, language, fileName);
@@ -316,13 +343,42 @@ class FixerCommand {
316
343
  SecurityUtils.safeWriteFileSync(backupPath, content, process.cwd(), 'utf8');
317
344
  }
318
345
  }
319
- }
320
-
321
- console.log(t('fixer.backupCreated'));
322
- } catch (error) {
323
- console.warn(`Failed to create backup: ${error.message}`);
324
- }
325
- }
346
+ }
347
+
348
+ this.cleanupOldBackups();
349
+
350
+ console.log(t('fixer.backupCreated'));
351
+ } catch (error) {
352
+ console.warn(`Failed to create backup: ${error.message}`);
353
+ }
354
+ }
355
+
356
+ cleanupOldBackups() {
357
+ try {
358
+ const fixerBackupRoot = path.resolve(this.config?.backup?.location || './i18ntk-backups', 'fixer');
359
+ if (!SecurityUtils.safeExistsSync(fixerBackupRoot, process.cwd())) return;
360
+
361
+ const configuredKeep = parseInt(this.config?.backup?.maxBackups, 10);
362
+ const keepCount = Number.isInteger(configuredKeep) ? Math.min(Math.max(configuredKeep, 1), 3) : 1;
363
+
364
+ const backupDirs = SecurityUtils.safeReaddirSync(fixerBackupRoot, process.cwd(), { withFileTypes: true })
365
+ .filter(entry => entry.isDirectory() && entry.name.startsWith('backup-'))
366
+ .map(entry => {
367
+ const dirPath = path.join(fixerBackupRoot, entry.name);
368
+ const stat = SecurityUtils.safeStatSync(dirPath, process.cwd());
369
+ return { name: entry.name, path: dirPath, mtimeMs: stat ? stat.mtimeMs : 0 };
370
+ })
371
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
372
+
373
+ if (backupDirs.length <= keepCount) return;
374
+
375
+ for (const staleDir of backupDirs.slice(keepCount)) {
376
+ fs.rmSync(staleDir.path, { recursive: true, force: true });
377
+ }
378
+ } catch (error) {
379
+ console.warn(`Failed to clean old backups: ${error.message}`);
380
+ }
381
+ }
326
382
 
327
383
  // Analyze translation issues for fixing
328
384
  analyzeIssues(language, fileName) {
@@ -471,9 +527,9 @@ class FixerCommand {
471
527
  }
472
528
 
473
529
  // Create backup unless disabled
474
- if (!args.noBackup && !this.dryRun) {
475
- await this.createBackup();
476
- }
530
+ if (!args.noBackup && !this.dryRun && this.config?.backup?.enabled === true) {
531
+ await this.createBackup();
532
+ }
477
533
 
478
534
  const languages = this.getAvailableLanguages();
479
535
 
@@ -535,9 +591,9 @@ class FixerCommand {
535
591
  console.log(t('fixer.totalIssues', { count: totalIssues }));
536
592
  console.log(t('fixer.totalFixed', { count: totalFixed }));
537
593
 
538
- if (this.backupDir && !args.noBackup) {
539
- console.log(t('fixer.backupLocation', { dir: this.backupDir }));
540
- }
594
+ if (this.backupDir && !args.noBackup && this.config?.backup?.enabled === true) {
595
+ console.log(t('fixer.backupLocation', { dir: this.backupDir }));
596
+ }
541
597
 
542
598
  console.log(t('fixer.completed'));
543
599
 
@@ -621,4 +677,4 @@ class FixerCommand {
621
677
  }
622
678
  }
623
679
 
624
- module.exports = FixerCommand;
680
+ module.exports = FixerCommand;
@@ -11,9 +11,10 @@ const fs = require('fs');
11
11
  const path = require('path');
12
12
  const { loadTranslations, t } = require('../../../utils/i18n-helper');
13
13
  const configManager = require('../../../utils/config-manager');
14
- const SecurityUtils = require('../../../utils/security');
15
- const AdminCLI = require('../../../utils/admin-cli');
16
- const watchLocales = require('../../../utils/watch-locales');
14
+ const SecurityUtils = require('../../../utils/security');
15
+ const AdminCLI = require('../../../utils/admin-cli');
16
+ const AdminAuth = require('../../../utils/admin-auth');
17
+ const watchLocales = require('../../../utils/watch-locales');
17
18
  const { getGlobalReadline, closeGlobalReadline } = require('../../../utils/cli');
18
19
  const { getUnifiedConfig, parseCommonArgs, displayHelp, validateSourceDir, displayPaths } = require('../../../utils/config-helper');
19
20
  const I18nInitializer = require('../../i18ntk-init');
@@ -166,18 +167,31 @@ class ValidateCommand {
166
167
  this.warnings.push({ message, details, type: 'warning' });
167
168
  }
168
169
 
169
- // Get all available languages
170
- getAvailableLanguages() {
171
- try {
172
- if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
173
- throw new Error(`Source directory not found: ${this.sourceDir}`);
174
- }
175
-
176
- const languages = fs.readdirSync(this.sourceDir)
177
- .filter(item => {
178
- const itemPath = path.join(this.sourceDir, item);
179
- return fs.statSync(itemPath).isDirectory() && item !== this.config.sourceLanguage;
180
- });
170
+ // Get all available languages
171
+ isExcludedLanguageDirectory(name) {
172
+ if (!name || typeof name !== 'string') return true;
173
+ const lowered = name.toLowerCase();
174
+ return lowered.startsWith('backup-') ||
175
+ lowered === 'backup' ||
176
+ lowered === 'backups' ||
177
+ lowered === 'i18ntk-backups' ||
178
+ lowered === 'reports' ||
179
+ lowered === 'i18ntk-reports';
180
+ }
181
+
182
+ getAvailableLanguages() {
183
+ try {
184
+ if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
185
+ throw new Error(`Source directory not found: ${this.sourceDir}`);
186
+ }
187
+
188
+ const languages = fs.readdirSync(this.sourceDir)
189
+ .filter(item => {
190
+ const itemPath = path.join(this.sourceDir, item);
191
+ return fs.statSync(itemPath).isDirectory() &&
192
+ item !== this.config.sourceLanguage &&
193
+ !this.isExcludedLanguageDirectory(item);
194
+ });
181
195
 
182
196
  return languages;
183
197
  } catch (error) {
@@ -599,9 +613,43 @@ class ValidateCommand {
599
613
  }
600
614
 
601
615
  // Show help message
602
- showHelp() {
603
- console.log(t('validate.help_message'));
604
- }
616
+ showHelp() {
617
+ console.log(t('validate.help_message'));
618
+ }
619
+
620
+ saveValidationSummaryReport(results = {}, success = true) {
621
+ try {
622
+ const outputDir = path.resolve(this.config.outputDir || './i18ntk-reports');
623
+ SecurityUtils.safeMkdirSync(outputDir, process.cwd(), { recursive: true });
624
+
625
+ const timestamp = new Date().toISOString();
626
+ const safeTimestamp = timestamp.replace(/[:.]/g, '-');
627
+ const reportPath = path.join(outputDir, `validation-summary-${safeTimestamp}.txt`);
628
+
629
+ const lines = [];
630
+ lines.push('I18NTK Validation Summary');
631
+ lines.push('========================');
632
+ lines.push(`Generated: ${timestamp}`);
633
+ lines.push(`Result: ${success ? 'PASS' : 'FAIL'}`);
634
+ lines.push(`Errors: ${this.errors.length}`);
635
+ lines.push(`Warnings: ${this.warnings.length}`);
636
+ lines.push('');
637
+ lines.push('Language Results');
638
+ lines.push('----------------');
639
+
640
+ Object.entries(results).forEach(([language, validation]) => {
641
+ const summary = validation?.summary || {};
642
+ lines.push(
643
+ `${language}: ${summary.percentage || 0}% (${summary.translatedKeys || 0}/${summary.totalKeys || 0})`
644
+ );
645
+ });
646
+
647
+ SecurityUtils.safeWriteFileSync(reportPath, lines.join('\n') + '\n', process.cwd(), 'utf8');
648
+ return reportPath;
649
+ } catch (error) {
650
+ return null;
651
+ }
652
+ }
605
653
 
606
654
  // Main validation process
607
655
  async validate() {
@@ -832,11 +880,16 @@ class ValidateCommand {
832
880
  console.log(t('validate.considerRunningUsageAnalysis'));
833
881
  }
834
882
 
835
- // Exit with appropriate code
836
- const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
837
-
838
- return {
839
- success,
883
+ // Exit with appropriate code
884
+ const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
885
+ const summaryReportPath = this.saveValidationSummaryReport(results, success);
886
+ if (summaryReportPath) {
887
+ console.log('');
888
+ console.log(`📄 Validation summary report saved: ${summaryReportPath}`);
889
+ }
890
+
891
+ return {
892
+ success,
840
893
  errors: this.errors.length,
841
894
  warnings: this.warnings.length,
842
895
  results
@@ -879,8 +932,7 @@ class ValidateCommand {
879
932
  // Skip admin authentication when called from menu
880
933
  if (!fromMenu) {
881
934
  // Check admin authentication for sensitive operations (only when called directly and not in no-prompt mode)
882
- const AdminAuth = require('../../../utils/admin-auth');
883
- const adminAuth = new AdminAuth();
935
+ const adminAuth = new AdminAuth();
884
936
  await adminAuth.initialize();
885
937
 
886
938
  const isCalledDirectly = require.main === module;
@@ -888,8 +940,7 @@ class ValidateCommand {
888
940
  if (isRequired && isCalledDirectly && !args.noPrompt) {
889
941
  console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'validate translations' }));
890
942
 
891
- const cliHelper = require('../../../utils/cli-helper');
892
- const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
943
+ const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
893
944
 
894
945
  const isValid = await adminAuth.verifyPin(pin);
895
946
  this.closeReadline();