i18ntk 4.1.0 → 4.2.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +51 -5
  2. package/README.md +79 -17
  3. package/SECURITY.md +10 -4
  4. package/main/i18ntk-analyze.js +10 -20
  5. package/main/i18ntk-backup.js +106 -44
  6. package/main/i18ntk-init.js +153 -157
  7. package/main/i18ntk-setup.js +36 -13
  8. package/main/i18ntk-translate.js +169 -21
  9. package/main/i18ntk-usage.js +272 -103
  10. package/main/i18ntk-validate.js +38 -31
  11. package/main/manage/commands/AnalyzeCommand.js +7 -17
  12. package/main/manage/commands/CommandRouter.js +6 -6
  13. package/main/manage/commands/TranslateCommand.js +65 -56
  14. package/main/manage/commands/ValidateCommand.js +34 -26
  15. package/main/manage/index.js +11 -42
  16. package/main/manage/managers/InteractiveMenu.js +11 -40
  17. package/main/manage/services/InitService.js +114 -118
  18. package/main/manage/services/UsageService.js +244 -85
  19. package/package.json +21 -14
  20. package/runtime/enhanced.d.ts +5 -5
  21. package/runtime/enhanced.js +49 -25
  22. package/runtime/i18ntk.d.ts +30 -7
  23. package/runtime/index.d.ts +48 -19
  24. package/runtime/index.js +175 -90
  25. package/settings/settings-cli.js +115 -38
  26. package/settings/settings-manager.js +24 -6
  27. package/ui-locales/de.json +192 -11
  28. package/ui-locales/en.json +182 -8
  29. package/ui-locales/es.json +193 -12
  30. package/ui-locales/fr.json +189 -8
  31. package/ui-locales/ja.json +190 -8
  32. package/ui-locales/ru.json +191 -9
  33. package/ui-locales/zh.json +194 -9
  34. package/utils/cli-helper.js +8 -12
  35. package/utils/config-helper.js +1 -1
  36. package/utils/config-manager.js +8 -6
  37. package/utils/localized-confirm.js +55 -0
  38. package/utils/menu-layout.js +41 -0
  39. package/utils/report-writer.js +110 -0
  40. package/utils/security.js +15 -22
  41. package/utils/translate/api.js +31 -3
  42. package/utils/translate/placeholder.js +42 -1
  43. package/utils/translate/report.js +3 -2
  44. package/utils/translate/safe-network.js +24 -4
  45. package/utils/usage-insights.js +435 -0
  46. package/utils/usage-source.js +50 -0
  47. package/utils/watch-locales.js +1 -8
@@ -42,8 +42,10 @@ const SettingsManager = require('../settings/settings-manager');
42
42
  const settingsManager = new SettingsManager();
43
43
  const { getUnifiedConfig, parseCommonArgs, displayHelp, validateSourceDir, displayPaths } = require('../utils/config-helper');
44
44
  const I18nInitializer = require('./i18ntk-init');
45
- const JsonOutput = require('../utils/json-output');
46
- const SetupEnforcer = require('../utils/setup-enforcer');
45
+ const JsonOutput = require('../utils/json-output');
46
+ const SetupEnforcer = require('../utils/setup-enforcer');
47
+ const { resolveUsageSourceDir } = require('../utils/usage-source');
48
+ const { analyzeSourceForUsageInsights } = require('../utils/usage-insights');
47
49
 
48
50
  // Ensure setup is complete before running
49
51
  (async () => {
@@ -74,9 +76,14 @@ class I18nUsageAnalyzer {
74
76
 
75
77
  // Initialize class properties
76
78
  this.availableKeys = new Set();
77
- this.usedKeys = new Set();
78
- this.fileUsage = new Map();
79
- this.translationFiles = new Map(); // Track all translation files
79
+ this.usedKeys = new Set();
80
+ this.fileUsage = new Map();
81
+ this.keyUsageLocations = new Map();
82
+ this.hardcodedTextCandidates = [];
83
+ this.namespaceRecommendations = [];
84
+ this.unresolvedDynamicReferences = [];
85
+ this.translationValueIndex = new Map();
86
+ this.translationFiles = new Map(); // Track all translation files
80
87
  this.translationStats = new Map(); // Track translation completeness
81
88
  this.extractor = getExtractor(config.extractor);
82
89
  this.placeholderKeys = new Set();
@@ -493,30 +500,25 @@ class I18nUsageAnalyzer {
493
500
  });
494
501
  }
495
502
 
496
- // Ensure sourceDir points to source code, not locales
497
- if (!args.sourceDir && this.config.sourceDir === this.config.i18nDir) {
498
- // Default to common source directories if not explicitly provided
499
- const possibleSourceDirs = ['src', 'lib', 'app', 'source'];
500
- const projectRoot = this.config.projectRoot || '.';
501
-
502
- for (const dir of possibleSourceDirs) {
503
- const testPath = path.resolve(projectRoot, dir);
504
- if (SecurityUtils.safeExistsSync(testPath)) {
505
- this.config.sourceDir = testPath;
506
- this.sourceDir = testPath;
507
- break;
508
- }
509
- }
510
-
511
- // If no common source directory found, use current directory
512
- if (this.config.sourceDir === this.config.i18nDir) {
513
- this.config.sourceDir = projectRoot;
514
- this.sourceDir = projectRoot;
515
- }
516
- }
503
+ const usageSource = resolveUsageSourceDir({
504
+ sourceDir: this.sourceDir || this.config.sourceDir,
505
+ i18nDir: this.i18nDir || this.config.i18nDir,
506
+ projectRoot: this.config.projectRoot || process.cwd(),
507
+ explicitSourceDir: Boolean(args.sourceDir),
508
+ });
509
+ if (usageSource.reason) {
510
+ console.warn(t('usage.sourceEqualsI18nWarn', { reason: usageSource.reason }) || `Warning: ${usageSource.reason}`);
511
+ }
512
+ this.sourceDir = usageSource.sourceDir;
513
+ this.config.sourceDir = usageSource.sourceDir;
514
+ if (this.sourceDir) {
515
+ await configManager.updateConfig({
516
+ sourceDir: configManager.toRelative(this.sourceDir)
517
+ });
518
+ }
517
519
 
518
520
  // 🚧 prevent scanning locales as source
519
- if (path.resolve(this.sourceDir) === path.resolve(this.i18nDir)) {
521
+ if (this.sourceDir && !args.sourceDir && path.resolve(this.sourceDir) === path.resolve(this.i18nDir)) {
520
522
  const fallback = path.resolve(this.config.projectRoot || '.', 'src');
521
523
  console.warn(t('usage.sourceEqualsI18nWarn') ||
522
524
  `⚠️ sourceDir equals i18nDir (${this.sourceDir}). Falling back to ${fallback} for source scanning.`);
@@ -532,7 +534,7 @@ class I18nUsageAnalyzer {
532
534
  });
533
535
  }
534
536
 
535
- console.log(t('usage.detectedSourceDirectory', { sourceDir: this.sourceDir }));
537
+ console.log(t('usage.detectedSourceDirectory', { sourceDir: this.sourceDir || t('usage.noSourceDirectoryConfigured') || '(none)' }));
536
538
  console.log(t('usage.detectedI18nDirectory', { i18nDir: this.i18nDir }));
537
539
 
538
540
  // Load available translation keys first
@@ -706,7 +708,10 @@ Analysis Features (v1.10.1):
706
708
  • Framework-specific pattern recognition (React, Vue, Angular)
707
709
  • Advanced translation completeness scoring
708
710
  • Performance metrics and optimization tracking
709
- • Key complexity analysis
711
+ • Key complexity analysis
712
+ • Known-key literal matching with source file locations
713
+ • Namespace/file naming recommendations (for example app/shop -> shop.json)
714
+ • Hardcoded user-facing text candidates with suggested translation keys
710
715
  • Security-enhanced path validation
711
716
  • Detailed reporting with validation errors
712
717
  • Dead key detection with confidence scoring
@@ -820,11 +825,17 @@ Analysis Features (v1.10.1):
820
825
 
821
826
  if (value && typeof value === 'object' && !Array.isArray(value)) {
822
827
  keys.push(...this.extractKeysFromObject(value, fullKey, namespace));
823
- } else {
824
- // Add dot notation key (e.g., "pagination.showing")
825
- keys.push(fullKey);
826
- }
827
- }
828
+ } else {
829
+ // Add dot notation key (e.g., "pagination.showing")
830
+ keys.push(fullKey);
831
+ if (typeof value === 'string') {
832
+ const normalizedValue = value.replace(/\s+/g, ' ').trim();
833
+ if (normalizedValue && !this.translationValueIndex.has(normalizedValue)) {
834
+ this.translationValueIndex.set(normalizedValue, fullKey);
835
+ }
836
+ }
837
+ }
838
+ }
828
839
  } catch (error) {
829
840
  // Handle any unexpected errors during key extraction
830
841
  console.warn(`⚠️ Error during key extraction: ${error.message}`);
@@ -861,32 +872,85 @@ Analysis Features (v1.10.1):
861
872
  }
862
873
  }
863
874
 
864
- // Extract translation keys from source code with enhanced patterns
865
- extractKeysFromFile(filePath) {
866
- try {
867
- const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
868
- if (!content) return [];
869
-
870
- // Skip JSON files entirely to prevent scanning translation files
871
- if (filePath.endsWith('.json')) return [];
872
- const rawPatterns = Array.isArray(this.config.translationPatterns) ? this.config.translationPatterns : [];
873
- if (rawPatterns.length === 0) return [];
874
-
875
- return this.extractor.extract(content, rawPatterns);
876
-
877
- // Null-safe translation patterns handling
878
- } catch (error) {
879
- console.warn(`${t('usage.failedToExtractKeys')} ${filePath}: ${error.message}`);
880
- return [];
881
- }
882
- }
875
+ // Extract translation keys from source code with enhanced patterns
876
+ extractKeysFromFile(filePath) {
877
+ try {
878
+ const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
879
+ if (!content) return [];
880
+
881
+ return this.extractKeysFromContent(content, filePath);
882
+
883
+ // Null-safe translation patterns handling
884
+ } catch (error) {
885
+ console.warn(`${t('usage.failedToExtractKeys')} ${filePath}: ${error.message}`);
886
+ return [];
887
+ }
888
+ }
889
+
890
+ extractKeysFromContent(content, filePath = '') {
891
+ if (!content) return [];
892
+ if (filePath && filePath.endsWith('.json')) return [];
893
+ const rawPatterns = Array.isArray(this.config.translationPatterns) ? this.config.translationPatterns : [];
894
+ if (rawPatterns.length === 0) return [];
895
+ return this.extractor.extract(content, rawPatterns);
896
+ }
897
+
898
+ recordUsageInsights(relativePath, insights) {
899
+ const keys = [];
900
+ const seen = new Set();
901
+
902
+ for (const ref of insights.keyReferences || []) {
903
+ if (!ref.key || seen.has(ref.key)) continue;
904
+ seen.add(ref.key);
905
+ keys.push(ref.key);
906
+ this.usedKeys.add(ref.key);
907
+
908
+ if (!this.keyUsageLocations.has(ref.key)) {
909
+ this.keyUsageLocations.set(ref.key, []);
910
+ }
911
+ this.keyUsageLocations.get(ref.key).push({
912
+ filePath: relativePath,
913
+ line: ref.line,
914
+ column: ref.column,
915
+ matchType: ref.matchType,
916
+ });
917
+ }
918
+
919
+ if (keys.length > 0) {
920
+ this.fileUsage.set(relativePath, keys);
921
+ }
922
+
923
+ if (insights.namespaceRecommendation) {
924
+ this.namespaceRecommendations.push({
925
+ filePath: relativePath,
926
+ ...insights.namespaceRecommendation,
927
+ });
928
+ }
929
+
930
+ if (Array.isArray(insights.hardcodedTexts) && insights.hardcodedTexts.length > 0) {
931
+ this.hardcodedTextCandidates.push(...insights.hardcodedTexts);
932
+ }
933
+
934
+ if (Array.isArray(insights.unresolvedDynamicReferences) && insights.unresolvedDynamicReferences.length > 0) {
935
+ this.unresolvedDynamicReferences.push(...insights.unresolvedDynamicReferences.map(ref => ({
936
+ filePath: relativePath,
937
+ ...ref,
938
+ })));
939
+ }
940
+
941
+ return keys.length;
942
+ }
883
943
 
884
944
  // Analyze usage in source files
885
- async analyzeUsage() {
886
- try {
887
- console.log(t('usage.checkUsage.analyzing_source_files'));
888
-
889
- // Check if source directory exists
945
+ async analyzeUsage() {
946
+ try {
947
+ console.log(t('usage.checkUsage.analyzing_source_files'));
948
+ if (!this.sourceDir) {
949
+ console.warn(t('usage.noSourceFilesFound'));
950
+ return;
951
+ }
952
+
953
+ // Check if source directory exists
890
954
  if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
891
955
  throw new Error(this.t('usage.sourceDirectoryDoesNotExist', { dir: this.sourceDir }) || `Source directory not found: ${this.sourceDir}`);
892
956
  }
@@ -903,21 +967,25 @@ Analysis Features (v1.10.1):
903
967
  let totalKeysFound = 0;
904
968
  let processedFiles = 0;
905
969
 
906
- for (const filePath of sourceFiles) {
907
- try {
908
- const keys = this.extractKeysFromFile(filePath);
909
-
910
- if (keys.length > 0) {
911
- const relativePath = path.relative(this.sourceDir, filePath);
912
- this.fileUsage.set(relativePath, keys);
913
-
914
- keys.forEach(key => {
915
- this.usedKeys.add(key);
916
- totalKeysFound++;
917
- });
918
- }
919
-
920
- processedFiles++;
970
+ for (const filePath of sourceFiles) {
971
+ try {
972
+ const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
973
+ if (!content) continue;
974
+
975
+ const relativePath = path.relative(this.sourceDir, filePath);
976
+ const directKeys = this.extractKeysFromContent(content, filePath);
977
+ const insights = analyzeSourceForUsageInsights({
978
+ content,
979
+ relativePath,
980
+ availableKeys: this.availableKeys,
981
+ directKeys,
982
+ translationValueIndex: this.translationValueIndex,
983
+ });
984
+
985
+ this.detectFrameworkPatterns(content, relativePath);
986
+ totalKeysFound += this.recordUsageInsights(relativePath, insights);
987
+
988
+ processedFiles++;
921
989
 
922
990
  // Progress indicator for large numbers of files
923
991
  if (sourceFiles.length > 10 && processedFiles % Math.ceil(sourceFiles.length / 10) === 0) {
@@ -929,10 +997,22 @@ Analysis Features (v1.10.1):
929
997
  }
930
998
  }
931
999
 
932
- console.log(t("usage.checkUsage.found_thisusedkeyssize_unique_", { usedKeysSize: this.usedKeys.size }));
933
- console.log(t("usage.checkUsage.total_key_usages_totalkeysfoun", { totalKeysFound }));
934
-
935
- } catch (error) {
1000
+ console.log(t("usage.checkUsage.found_thisusedkeyssize_unique_", { usedKeysSize: this.usedKeys.size }));
1001
+ console.log(t("usage.checkUsage.total_key_usages_totalkeysfoun", { totalKeysFound }));
1002
+ if (this.keyUsageLocations.size > 0) {
1003
+ console.log(`🔎 Indexed ${this.keyUsageLocations.size} keys with source file locations`);
1004
+ }
1005
+ if (this.namespaceRecommendations.length > 0) {
1006
+ console.log(`🧭 Namespace recommendations: ${this.namespaceRecommendations.length}`);
1007
+ }
1008
+ if (this.hardcodedTextCandidates.length > 0) {
1009
+ console.log(`📝 Hardcoded text candidates: ${this.hardcodedTextCandidates.length}`);
1010
+ }
1011
+ if (this.unresolvedDynamicReferences.length > 0) {
1012
+ console.log(`🧩 Unresolved dynamic key expressions: ${this.unresolvedDynamicReferences.length}`);
1013
+ }
1014
+
1015
+ } catch (error) {
936
1016
  console.error(t('usage.failedToAnalyzeUsage', { error: error.message }));
937
1017
  throw error;
938
1018
  }
@@ -1347,9 +1427,19 @@ Analysis Features (v1.10.1):
1347
1427
  return filepath;
1348
1428
  }
1349
1429
 
1350
- // Find files that use specific keys
1351
- findKeyUsage(searchKey) {
1352
- const usage = [];
1430
+ // Find files that use specific keys
1431
+ findKeyUsage(searchKey) {
1432
+ if (this.keyUsageLocations && this.keyUsageLocations.has(searchKey)) {
1433
+ return this.keyUsageLocations.get(searchKey).map(location => ({
1434
+ filePath: location.filePath,
1435
+ keys: [searchKey],
1436
+ line: location.line,
1437
+ column: location.column,
1438
+ matchType: location.matchType,
1439
+ }));
1440
+ }
1441
+
1442
+ const usage = [];
1353
1443
 
1354
1444
  for (const [filePath, keys] of this.fileUsage) {
1355
1445
  const matchingKeys = keys.filter(key => {
@@ -1420,13 +1510,14 @@ Analysis Features (v1.10.1):
1420
1510
  // Translation completeness with advanced scoring
1421
1511
  report += `${t('summary.usageReportTranslationCompleteness')}\n`;
1422
1512
  report += `${'='.repeat(50)}\n`;
1423
- for (const [language, stats] of this.translationStats) {
1424
- const translations = this.translationsByLanguage[language] || {};
1425
- const score = this.calculateTranslationScore ? this.calculateTranslationScore(language, translations) : {
1426
- completeness: ((stats.translated / stats.total) * 100).toFixed(1),
1427
- quality: ((stats.translated / stats.total) * 100).toFixed(1),
1428
- placeholderAccuracy: 100
1429
- };
1513
+ for (const [language, stats] of this.translationStats) {
1514
+ const translations = this.translationsByLanguage ? this.translationsByLanguage[language] : null;
1515
+ const completeness = stats.total > 0 ? ((stats.translated / stats.total) * 100).toFixed(1) : '0.0';
1516
+ const score = translations && this.calculateTranslationScore ? this.calculateTranslationScore(language, translations) : {
1517
+ completeness,
1518
+ quality: completeness,
1519
+ placeholderAccuracy: 100
1520
+ };
1430
1521
 
1431
1522
  report += `${t('summary.usageReportLanguageCompleteness', { language: language.toUpperCase(), completeness: score.completeness, translated: stats.translated, total: stats.total })}\n`;
1432
1523
  report += ` Quality: ${score.quality}%\n`;
@@ -1447,9 +1538,9 @@ Analysis Features (v1.10.1):
1447
1538
  }
1448
1539
  report += `\n`;
1449
1540
 
1450
- // Key complexity analysis
1451
- if (this.keyComplexity && this.keyComplexity.size > 0) {
1452
- report += `🔍 Key Complexity Analysis:\n`;
1541
+ // Key complexity analysis
1542
+ if (this.keyComplexity && this.keyComplexity.size > 0) {
1543
+ report += `🔍 Key Complexity Analysis:\n`;
1453
1544
  const complexityStats = { simple: 0, moderate: 0, complex: 0 };
1454
1545
  this.keyComplexity.forEach((data, key) => {
1455
1546
  complexityStats[data.level]++;
@@ -1457,10 +1548,81 @@ Analysis Features (v1.10.1):
1457
1548
 
1458
1549
  report += ` Simple keys: ${complexityStats.simple}\n`;
1459
1550
  report += ` Moderate keys: ${complexityStats.moderate}\n`;
1460
- report += ` Complex keys: ${complexityStats.complex}\n\n`;
1461
- }
1462
-
1463
- // Unused keys with complexity
1551
+ report += ` Complex keys: ${complexityStats.complex}\n\n`;
1552
+ }
1553
+
1554
+ const matchCounts = { direct: 0, literal: 0 };
1555
+ for (const locations of this.keyUsageLocations.values()) {
1556
+ for (const location of locations) {
1557
+ matchCounts[location.matchType] = (matchCounts[location.matchType] || 0) + 1;
1558
+ }
1559
+ }
1560
+
1561
+ report += `🔎 Usage Match Index\n`;
1562
+ report += `${'='.repeat(50)}\n`;
1563
+ report += `Direct i18n calls: ${matchCounts.direct || 0}\n`;
1564
+ report += `Known-key literal matches: ${matchCounts.literal || 0}\n`;
1565
+ report += `Resolved dynamic expressions: ${(matchCounts['dynamic-template'] || 0) + (matchCounts['dynamic-variable'] || 0)}\n`;
1566
+ report += `Unresolved dynamic expressions: ${this.unresolvedDynamicReferences.length}\n`;
1567
+ report += `Indexed keys with file locations: ${this.keyUsageLocations.size}\n\n`;
1568
+
1569
+ const indexedKeys = Array.from(this.keyUsageLocations.entries()).slice(0, 100);
1570
+ indexedKeys.forEach(([key, locations]) => {
1571
+ report += `- ${key}\n`;
1572
+ locations.slice(0, 5).forEach(location => {
1573
+ const line = location.line ? `:${location.line}` : '';
1574
+ report += ` - ${location.filePath}${line} (${location.matchType})\n`;
1575
+ });
1576
+ if (locations.length > 5) {
1577
+ report += ` - ... ${locations.length - 5} more locations\n`;
1578
+ }
1579
+ });
1580
+ if (this.keyUsageLocations.size > 100) {
1581
+ report += `... ${this.keyUsageLocations.size - 100} more indexed keys\n`;
1582
+ }
1583
+ report += `\n`;
1584
+
1585
+ if (this.namespaceRecommendations.length > 0) {
1586
+ report += `🧭 Namespace Recommendations\n`;
1587
+ report += `${'='.repeat(50)}\n`;
1588
+ this.namespaceRecommendations.slice(0, 30).forEach(item => {
1589
+ report += `- ${item.filePath}: ${item.message}\n`;
1590
+ });
1591
+ if (this.namespaceRecommendations.length > 30) {
1592
+ report += `... ${this.namespaceRecommendations.length - 30} more recommendations\n`;
1593
+ }
1594
+ report += `\n`;
1595
+ }
1596
+
1597
+ if (this.unresolvedDynamicReferences.length > 0) {
1598
+ report += `🧩 Unresolved Dynamic Key Expressions\n`;
1599
+ report += `${'='.repeat(50)}\n`;
1600
+ report += `These calls could not be resolved to exact keys without executing code. Review them manually or prefer bounded literal maps/arrays for analyzable dynamic keys.\n\n`;
1601
+ this.unresolvedDynamicReferences.slice(0, 50).forEach(item => {
1602
+ const prefix = item.prefix ? ` prefix: ${item.prefix}` : ' no static prefix';
1603
+ report += `- ${item.filePath}:${item.line} ${item.expression} (${prefix})\n`;
1604
+ });
1605
+ if (this.unresolvedDynamicReferences.length > 50) {
1606
+ report += `... ${this.unresolvedDynamicReferences.length - 50} more unresolved expressions\n`;
1607
+ }
1608
+ report += `\n`;
1609
+ }
1610
+
1611
+ if (this.hardcodedTextCandidates.length > 0) {
1612
+ report += `📝 Hardcoded Text Candidates\n`;
1613
+ report += `${'='.repeat(50)}\n`;
1614
+ report += `Inline user-facing text that may be moved into locale files.\n\n`;
1615
+ this.hardcodedTextCandidates.slice(0, 50).forEach(item => {
1616
+ const existing = item.existingKey ? ` existing key: ${item.existingKey}` : ` suggested key: ${item.suggestedKey}`;
1617
+ report += `- ${item.filePath}:${item.line} "${item.text}" (${existing})\n`;
1618
+ });
1619
+ if (this.hardcodedTextCandidates.length > 50) {
1620
+ report += `... ${this.hardcodedTextCandidates.length - 50} more candidates\n`;
1621
+ }
1622
+ report += `\n`;
1623
+ }
1624
+
1625
+ // Unused keys with complexity
1464
1626
  if (unusedKeys.length > 0) {
1465
1627
  report += `${t('summary.usageReportUnusedTranslationKeys')}\n`;
1466
1628
  report += `${'='.repeat(50)}\n`;
@@ -1958,15 +2120,22 @@ Analysis Features (v1.10.1):
1958
2120
  dynamicKeys: dynamicKeys.length,
1959
2121
  unusedKeys: unusedKeys.length,
1960
2122
  missingKeys: missingKeys.length,
1961
- filesScanned: this.fileUsage.size,
1962
- notTranslatedKeys: notTranslatedStats.total,
1963
- translationCompleteness: Object.fromEntries(this.translationStats)
1964
- },
1965
- unusedKeys,
1966
- missingKeys,
1967
- dynamicKeys,
1968
- notTranslatedStats
1969
- };
2123
+ filesScanned: this.fileUsage.size,
2124
+ notTranslatedKeys: notTranslatedStats.total,
2125
+ indexedKeys: this.keyUsageLocations.size,
2126
+ namespaceRecommendations: this.namespaceRecommendations.length,
2127
+ hardcodedTextCandidates: this.hardcodedTextCandidates.length,
2128
+ unresolvedDynamicReferences: this.unresolvedDynamicReferences.length,
2129
+ translationCompleteness: Object.fromEntries(this.translationStats)
2130
+ },
2131
+ unusedKeys,
2132
+ missingKeys,
2133
+ dynamicKeys,
2134
+ notTranslatedStats,
2135
+ namespaceRecommendations: this.namespaceRecommendations,
2136
+ hardcodedTextCandidates: this.hardcodedTextCandidates,
2137
+ unresolvedDynamicReferences: this.unresolvedDynamicReferences
2138
+ };
1970
2139
 
1971
2140
  } catch (error) {
1972
2141
  console.error(t("checkUsage.usage_analysis_failed"));
@@ -64,16 +64,23 @@ const { detectTranslationContentRisks } = require('../utils/validation-risk');
64
64
  loadTranslations('en', path.resolve(__dirname, '..', 'ui-locales'));
65
65
 
66
66
  class I18nValidator {
67
- constructor(config = {}) {
68
- this.config = config;
69
- this.errors = [];
70
- this.warnings = [];
71
- this.keyNamingViolations = [];
72
- this.rl = null;
73
- }
67
+ constructor(config = {}) {
68
+ this.config = config;
69
+ this.errors = [];
70
+ this.warnings = [];
71
+ this.keyNamingViolations = [];
72
+ this.rl = null;
73
+ this.initialized = false;
74
+ this.pathsDisplayed = false;
75
+ this.validationBannerDisplayed = false;
76
+ }
74
77
 
75
- async initialize() {
76
- try {
78
+ async initialize() {
79
+ try {
80
+ if (this.initialized) {
81
+ return;
82
+ }
83
+
77
84
  // Initialize i18n with UI language first
78
85
  const args = this.parseArgs();
79
86
  if (args.help) {
@@ -120,7 +127,9 @@ class I18nValidator {
120
127
  }
121
128
  }
122
129
 
123
- displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
130
+ displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
131
+ this.pathsDisplayed = true;
132
+ this.initialized = true;
124
133
 
125
134
  SecurityUtils.logSecurityEvent(
126
135
  'I18n validator initialized successfully',
@@ -780,9 +789,12 @@ class I18nValidator {
780
789
  const args = this.parseArgs();
781
790
  const jsonOutput = new JsonOutput('validate');
782
791
 
783
- if (!args.json) {
784
- console.log(t('validate.title'));
785
- console.log(t('validate.message'));
792
+ if (!args.json) {
793
+ console.log('');
794
+ console.log(t('validate.title'));
795
+ if (!this.validationBannerDisplayed) {
796
+ console.log(t('validate.message'));
797
+ }
786
798
 
787
799
  // Delete old validation report if it exists
788
800
  const reportPath = path.join(process.cwd(), 'validation-report.txt');
@@ -812,11 +824,14 @@ class I18nValidator {
812
824
  this.config.strictMode = true;
813
825
  }
814
826
 
815
- if (!args.json) {
816
- console.log(t('validate.sourceDirectory', { dir: this.sourceDir }));
817
- console.log(t('validate.sourceLanguage', { sourceLanguage: this.config.sourceLanguage }));
818
- console.log(t('validate.strictMode', { mode: this.config.strictMode ? 'ON' : 'OFF' }));
819
- }
827
+ if (!args.json) {
828
+ if (!this.pathsDisplayed) {
829
+ console.log(t('validate.sourceDirectory', { dir: this.sourceDir }));
830
+ }
831
+ console.log(t('validate.sourceLanguage', { sourceLanguage: this.config.sourceLanguage }));
832
+ console.log(t('validate.strictMode', { mode: this.config.strictMode ? 'ON' : 'OFF' }));
833
+ console.log('');
834
+ }
820
835
 
821
836
  // Validate source language directory exists
822
837
  SecurityUtils.validatePath(this.sourceLanguageDir);
@@ -1062,18 +1077,9 @@ class I18nValidator {
1062
1077
  this.config = {};
1063
1078
  }
1064
1079
 
1065
- // Initialize configuration properly when called from menu
1066
- if (fromMenu && !this.sourceDir) {
1067
- const baseConfig = await getUnifiedConfig('validate', args);
1068
- this.config = { ...baseConfig, ...(this.config || {}) };
1069
-
1070
- const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
1071
- loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
1072
- this.sourceDir = this.config.sourceDir;
1073
- this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
1074
- } else {
1075
- await this.initialize();
1076
- }
1080
+ if (!this.initialized) {
1081
+ await this.initialize();
1082
+ }
1077
1083
  this.config.enforceKeyStyle = args.enforceKeyStyle !== undefined ? args.enforceKeyStyle : this.config.enforceKeyStyle;
1078
1084
 
1079
1085
  // Skip admin authentication when called from menu
@@ -1105,7 +1111,8 @@ class I18nValidator {
1105
1111
  }
1106
1112
  const execute = async () => {
1107
1113
 
1108
- console.log(t('validate.startingValidationProcess'));
1114
+ console.log('\n' + t('validate.startingValidationProcess'));
1115
+ this.validationBannerDisplayed = true;
1109
1116
  SecurityUtils.logSecurityEvent(
1110
1117
  t('validate.runStarted'),
1111
1118
  'info',
@@ -17,6 +17,8 @@ const AdminCLI = require('../../../utils/admin-cli');
17
17
  const AdminAuth = require('../../../utils/admin-auth');
18
18
  const watchLocales = require('../../../utils/watch-locales');
19
19
  const JsonOutput = require('../../../utils/json-output');
20
+ const configManager = require('../../../utils/config-manager');
21
+ const { normalizeReportFormat, writeReportFile } = require('../../../utils/report-writer');
20
22
 
21
23
  loadTranslations('en', path.resolve(__dirname, '../../../ui-locales'));
22
24
 
@@ -752,23 +754,11 @@ class AnalyzeCommand {
752
754
  return null;
753
755
  }
754
756
 
755
- // Create a safe filename
756
- const safeLanguage = language.replace(/[^\w-]/g, '_');
757
- const reportPath = path.resolve(validatedOutputDir, `translation-report-${safeLanguage}.json`);
758
-
759
- // Ensure the final path is still within the output directory
760
- if (!reportPath.startsWith(validatedOutputDir)) {
761
- console.error('Invalid report path detected, potential directory traversal attack');
762
- return null;
763
- }
764
-
765
- // Use safeWriteFile for secure file writing
766
- const success = await SecurityUtils.safeWriteFile(reportPath, JSON.stringify(report, null, 2), process.cwd(), 'utf8');
767
- if (!success) {
768
- throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
769
- }
770
-
771
- return reportPath;
757
+ const safeLanguage = language.replace(/[^\w-]/g, '_');
758
+ const settings = configManager.getConfig ? configManager.getConfig() : {};
759
+ const format = normalizeReportFormat(this.config?.reports?.format || settings.reports?.format || this.config?.reportFormat || 'markdown');
760
+ const reportPath = await writeReportFile(validatedOutputDir, `translation-report-${safeLanguage}`, report, { format, title: `Translation Report ${safeLanguage.toUpperCase()}` });
761
+ return reportPath;
772
762
 
773
763
  } catch (error) {
774
764
  console.error(`Failed to save report for ${language}:`, error.message);
@@ -119,11 +119,11 @@ class CommandRouter {
119
119
  return true;
120
120
  }
121
121
 
122
- /**
123
- * Execute a command with proper routing and error handling
124
- */
125
- async executeCommand(command, options = {}) {
126
- console.log(t('menu.executingCommand', { command }));
122
+ /**
123
+ * Execute a command with proper routing and error handling
124
+ */
125
+ async executeCommand(command, options = {}) {
126
+ console.log('\n' + t('menu.executingCommand', { command }));
127
127
 
128
128
  // Enhanced context detection
129
129
  const executionContext = this.getExecutionContext(options);
@@ -161,7 +161,7 @@ class CommandRouter {
161
161
  const result = await this.routeCommand(command, options, executionContext);
162
162
 
163
163
  // Handle command completion based on execution context
164
- console.log(t('operations.completed'));
164
+ console.log('\n' + t('operations.completed'));
165
165
 
166
166
  if (isManagerExecution && !this.isNonInteractiveMode && this.prompt) {
167
167
  // Interactive menu execution - return to menu