i18ntk 4.0.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 (50) hide show
  1. package/CHANGELOG.md +116 -29
  2. package/README.md +83 -18
  3. package/SECURITY.md +13 -5
  4. package/main/i18ntk-analyze.js +10 -20
  5. package/main/i18ntk-backup.js +227 -111
  6. package/main/i18ntk-init.js +153 -157
  7. package/main/i18ntk-scanner.js +9 -7
  8. package/main/i18ntk-setup.js +36 -13
  9. package/main/i18ntk-sizing.js +18 -50
  10. package/main/i18ntk-translate.js +169 -21
  11. package/main/i18ntk-usage.js +298 -154
  12. package/main/i18ntk-validate.js +49 -37
  13. package/main/manage/commands/AnalyzeCommand.js +7 -17
  14. package/main/manage/commands/CommandRouter.js +6 -6
  15. package/main/manage/commands/TranslateCommand.js +65 -56
  16. package/main/manage/commands/ValidateCommand.js +34 -26
  17. package/main/manage/index.js +11 -42
  18. package/main/manage/managers/InteractiveMenu.js +11 -40
  19. package/main/manage/services/InitService.js +114 -118
  20. package/main/manage/services/UsageService.js +244 -85
  21. package/package.json +55 -4
  22. package/runtime/enhanced.d.ts +5 -5
  23. package/runtime/enhanced.js +49 -25
  24. package/runtime/i18ntk.d.ts +30 -7
  25. package/runtime/index.d.ts +48 -19
  26. package/runtime/index.js +188 -97
  27. package/settings/settings-cli.js +115 -38
  28. package/settings/settings-manager.js +24 -6
  29. package/ui-locales/de.json +192 -11
  30. package/ui-locales/en.json +182 -8
  31. package/ui-locales/es.json +193 -12
  32. package/ui-locales/fr.json +189 -8
  33. package/ui-locales/ja.json +190 -8
  34. package/ui-locales/ru.json +191 -9
  35. package/ui-locales/zh.json +194 -9
  36. package/utils/cli-helper.js +8 -12
  37. package/utils/config-helper.js +1 -1
  38. package/utils/config-manager.js +8 -6
  39. package/utils/localized-confirm.js +55 -0
  40. package/utils/menu-layout.js +41 -0
  41. package/utils/report-writer.js +110 -0
  42. package/utils/security.js +15 -22
  43. package/utils/translate/api.js +31 -3
  44. package/utils/translate/placeholder.js +42 -1
  45. package/utils/translate/protection.js +17 -12
  46. package/utils/translate/report.js +3 -2
  47. package/utils/translate/safe-network.js +24 -4
  48. package/utils/usage-insights.js +435 -0
  49. package/utils/usage-source.js +50 -0
  50. package/utils/watch-locales.js +13 -9
@@ -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 () => {
@@ -61,6 +63,10 @@ async function getConfig() {
61
63
  return await getUnifiedConfig('usage');
62
64
  }
63
65
 
66
+ function toBool(v) {
67
+ return v === true || v === 'true' || v === '1';
68
+ }
69
+
64
70
  class I18nUsageAnalyzer {
65
71
  constructor(config = {}) {
66
72
  this.config = config;
@@ -70,9 +76,14 @@ class I18nUsageAnalyzer {
70
76
 
71
77
  // Initialize class properties
72
78
  this.availableKeys = new Set();
73
- this.usedKeys = new Set();
74
- this.fileUsage = new Map();
75
- 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
76
87
  this.translationStats = new Map(); // Track translation completeness
77
88
  this.extractor = getExtractor(config.extractor);
78
89
  this.placeholderKeys = new Set();
@@ -88,6 +99,7 @@ class I18nUsageAnalyzer {
88
99
  this.deadKeys = new Map();
89
100
  this.cleanupMode = false;
90
101
  this.dryRunDelete = false;
102
+ this._sourceCommentsSet = null;
91
103
 
92
104
  // Use global translation function
93
105
  this.rl = null;
@@ -383,10 +395,10 @@ class I18nUsageAnalyzer {
383
395
  console.log('🔍 Debug mode enabled');
384
396
  }
385
397
 
386
- if (args.cleanup) {
398
+ if (toBool(args.cleanup)) {
387
399
  this.cleanupMode = true;
388
400
  }
389
- if (args.dryRunDelete) {
401
+ if (toBool(args.dryRunDelete)) {
390
402
  this.dryRunDelete = true;
391
403
  }
392
404
 
@@ -488,30 +500,25 @@ class I18nUsageAnalyzer {
488
500
  });
489
501
  }
490
502
 
491
- // Ensure sourceDir points to source code, not locales
492
- if (!args.sourceDir && this.config.sourceDir === this.config.i18nDir) {
493
- // Default to common source directories if not explicitly provided
494
- const possibleSourceDirs = ['src', 'lib', 'app', 'source'];
495
- const projectRoot = this.config.projectRoot || '.';
496
-
497
- for (const dir of possibleSourceDirs) {
498
- const testPath = path.resolve(projectRoot, dir);
499
- if (SecurityUtils.safeExistsSync(testPath)) {
500
- this.config.sourceDir = testPath;
501
- this.sourceDir = testPath;
502
- break;
503
- }
504
- }
505
-
506
- // If no common source directory found, use current directory
507
- if (this.config.sourceDir === this.config.i18nDir) {
508
- this.config.sourceDir = projectRoot;
509
- this.sourceDir = projectRoot;
510
- }
511
- }
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
+ }
512
519
 
513
520
  // 🚧 prevent scanning locales as source
514
- if (path.resolve(this.sourceDir) === path.resolve(this.i18nDir)) {
521
+ if (this.sourceDir && !args.sourceDir && path.resolve(this.sourceDir) === path.resolve(this.i18nDir)) {
515
522
  const fallback = path.resolve(this.config.projectRoot || '.', 'src');
516
523
  console.warn(t('usage.sourceEqualsI18nWarn') ||
517
524
  `⚠️ sourceDir equals i18nDir (${this.sourceDir}). Falling back to ${fallback} for source scanning.`);
@@ -527,15 +534,12 @@ class I18nUsageAnalyzer {
527
534
  });
528
535
  }
529
536
 
530
- console.log(t('usage.detectedSourceDirectory', { sourceDir: this.sourceDir }));
537
+ console.log(t('usage.detectedSourceDirectory', { sourceDir: this.sourceDir || t('usage.noSourceDirectoryConfigured') || '(none)' }));
531
538
  console.log(t('usage.detectedI18nDirectory', { i18nDir: this.i18nDir }));
532
539
 
533
540
  // Load available translation keys first
534
541
  await this.loadAvailableKeys();
535
542
 
536
- // NEW: Detect framework patterns before analysis
537
- await this.detectFrameworkPatterns();
538
-
539
543
  // Perform usage analysis with enhanced features
540
544
  await this.analyzeUsage();
541
545
 
@@ -614,6 +618,7 @@ class I18nUsageAnalyzer {
614
618
  }
615
619
 
616
620
  if (this.cleanupMode) {
621
+ this._buildSourceCommentsSet();
617
622
  const deadKeys = this.findDeadKeys();
618
623
  console.log('\n' + t('usage.deadKeysDetectionTitle'));
619
624
  console.log(t('usage.deadKeysCount', { count: deadKeys.length }));
@@ -703,7 +708,10 @@ Analysis Features (v1.10.1):
703
708
  • Framework-specific pattern recognition (React, Vue, Angular)
704
709
  • Advanced translation completeness scoring
705
710
  • Performance metrics and optimization tracking
706
- • 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
707
715
  • Security-enhanced path validation
708
716
  • Detailed reporting with validation errors
709
717
  • Dead key detection with confidence scoring
@@ -817,11 +825,17 @@ Analysis Features (v1.10.1):
817
825
 
818
826
  if (value && typeof value === 'object' && !Array.isArray(value)) {
819
827
  keys.push(...this.extractKeysFromObject(value, fullKey, namespace));
820
- } else {
821
- // Add dot notation key (e.g., "pagination.showing")
822
- keys.push(fullKey);
823
- }
824
- }
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
+ }
825
839
  } catch (error) {
826
840
  // Handle any unexpected errors during key extraction
827
841
  console.warn(`⚠️ Error during key extraction: ${error.message}`);
@@ -858,32 +872,85 @@ Analysis Features (v1.10.1):
858
872
  }
859
873
  }
860
874
 
861
- // Extract translation keys from source code with enhanced patterns
862
- extractKeysFromFile(filePath) {
863
- try {
864
- const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
865
- if (!content) return [];
866
-
867
- // Skip JSON files entirely to prevent scanning translation files
868
- if (filePath.endsWith('.json')) return [];
869
- const rawPatterns = Array.isArray(this.config.translationPatterns) ? this.config.translationPatterns : [];
870
- if (rawPatterns.length === 0) return [];
871
-
872
- return this.extractor.extract(content, rawPatterns);
873
-
874
- // Null-safe translation patterns handling
875
- } catch (error) {
876
- console.warn(`${t('usage.failedToExtractKeys')} ${filePath}: ${error.message}`);
877
- return [];
878
- }
879
- }
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
+ }
880
943
 
881
944
  // Analyze usage in source files
882
- async analyzeUsage() {
883
- try {
884
- console.log(t('usage.checkUsage.analyzing_source_files'));
885
-
886
- // 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
887
954
  if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
888
955
  throw new Error(this.t('usage.sourceDirectoryDoesNotExist', { dir: this.sourceDir }) || `Source directory not found: ${this.sourceDir}`);
889
956
  }
@@ -900,21 +967,25 @@ Analysis Features (v1.10.1):
900
967
  let totalKeysFound = 0;
901
968
  let processedFiles = 0;
902
969
 
903
- for (const filePath of sourceFiles) {
904
- try {
905
- const keys = this.extractKeysFromFile(filePath);
906
-
907
- if (keys.length > 0) {
908
- const relativePath = path.relative(this.sourceDir, filePath);
909
- this.fileUsage.set(relativePath, keys);
910
-
911
- keys.forEach(key => {
912
- this.usedKeys.add(key);
913
- totalKeysFound++;
914
- });
915
- }
916
-
917
- 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++;
918
989
 
919
990
  // Progress indicator for large numbers of files
920
991
  if (sourceFiles.length > 10 && processedFiles % Math.ceil(sourceFiles.length / 10) === 0) {
@@ -926,10 +997,22 @@ Analysis Features (v1.10.1):
926
997
  }
927
998
  }
928
999
 
929
- console.log(t("usage.checkUsage.found_thisusedkeyssize_unique_", { usedKeysSize: this.usedKeys.size }));
930
- console.log(t("usage.checkUsage.total_key_usages_totalkeysfoun", { totalKeysFound }));
931
-
932
- } 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) {
933
1016
  console.error(t('usage.failedToAnalyzeUsage', { error: error.message }));
934
1017
  throw error;
935
1018
  }
@@ -1242,11 +1325,13 @@ Analysis Features (v1.10.1):
1242
1325
  return false;
1243
1326
  }
1244
1327
 
1245
- _keyInSourceComments(key) {
1328
+ _buildSourceCommentsSet() {
1329
+ if (this._sourceCommentsSet !== null) return;
1330
+ this._sourceCommentsSet = new Set();
1331
+
1246
1332
  const commentPatterns = [
1247
1333
  /\/\/[^\n]*/g,
1248
- /\/\*[\s\S]*?\*\//g,
1249
- /\/\*\*[\s\S]*?\*\//g
1334
+ /\/\*[\s\S]*?\*\//g
1250
1335
  ];
1251
1336
 
1252
1337
  try {
@@ -1262,13 +1347,22 @@ Analysis Features (v1.10.1):
1262
1347
  const comments = content.match(pattern);
1263
1348
  if (comments) {
1264
1349
  for (const comment of comments) {
1265
- if (comment.includes(key)) {
1266
- return true;
1267
- }
1350
+ this._sourceCommentsSet.add(comment);
1268
1351
  }
1269
1352
  }
1270
1353
  }
1271
1354
  }
1355
+ } catch (e) {
1356
+ this._sourceCommentsSet = new Set();
1357
+ }
1358
+ }
1359
+
1360
+ _keyInSourceComments(key) {
1361
+ try {
1362
+ if (!this._sourceCommentsSet || this._sourceCommentsSet.size === 0) return false;
1363
+ for (const comment of this._sourceCommentsSet) {
1364
+ if (comment.includes(key)) return true;
1365
+ }
1272
1366
  } catch (e) {
1273
1367
  // Silently fail - comment detection is best-effort
1274
1368
  }
@@ -1333,9 +1427,19 @@ Analysis Features (v1.10.1):
1333
1427
  return filepath;
1334
1428
  }
1335
1429
 
1336
- // Find files that use specific keys
1337
- findKeyUsage(searchKey) {
1338
- 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 = [];
1339
1443
 
1340
1444
  for (const [filePath, keys] of this.fileUsage) {
1341
1445
  const matchingKeys = keys.filter(key => {
@@ -1406,13 +1510,14 @@ Analysis Features (v1.10.1):
1406
1510
  // Translation completeness with advanced scoring
1407
1511
  report += `${t('summary.usageReportTranslationCompleteness')}\n`;
1408
1512
  report += `${'='.repeat(50)}\n`;
1409
- for (const [language, stats] of this.translationStats) {
1410
- const translations = this.translationsByLanguage[language] || {};
1411
- const score = this.calculateTranslationScore ? this.calculateTranslationScore(language, translations) : {
1412
- completeness: ((stats.translated / stats.total) * 100).toFixed(1),
1413
- quality: ((stats.translated / stats.total) * 100).toFixed(1),
1414
- placeholderAccuracy: 100
1415
- };
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
+ };
1416
1521
 
1417
1522
  report += `${t('summary.usageReportLanguageCompleteness', { language: language.toUpperCase(), completeness: score.completeness, translated: stats.translated, total: stats.total })}\n`;
1418
1523
  report += ` Quality: ${score.quality}%\n`;
@@ -1433,9 +1538,9 @@ Analysis Features (v1.10.1):
1433
1538
  }
1434
1539
  report += `\n`;
1435
1540
 
1436
- // Key complexity analysis
1437
- if (this.keyComplexity && this.keyComplexity.size > 0) {
1438
- report += `🔍 Key Complexity Analysis:\n`;
1541
+ // Key complexity analysis
1542
+ if (this.keyComplexity && this.keyComplexity.size > 0) {
1543
+ report += `🔍 Key Complexity Analysis:\n`;
1439
1544
  const complexityStats = { simple: 0, moderate: 0, complex: 0 };
1440
1545
  this.keyComplexity.forEach((data, key) => {
1441
1546
  complexityStats[data.level]++;
@@ -1443,10 +1548,81 @@ Analysis Features (v1.10.1):
1443
1548
 
1444
1549
  report += ` Simple keys: ${complexityStats.simple}\n`;
1445
1550
  report += ` Moderate keys: ${complexityStats.moderate}\n`;
1446
- report += ` Complex keys: ${complexityStats.complex}\n\n`;
1447
- }
1448
-
1449
- // 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
1450
1626
  if (unusedKeys.length > 0) {
1451
1627
  report += `${t('summary.usageReportUnusedTranslationKeys')}\n`;
1452
1628
  report += `${'='.repeat(50)}\n`;
@@ -1936,9 +2112,6 @@ Analysis Features (v1.10.1):
1936
2112
  // Close readline interface to prevent hanging
1937
2113
  this.closeReadline();
1938
2114
 
1939
- // Return instead of force exit to allow proper cleanup
1940
- return;
1941
-
1942
2115
  return {
1943
2116
  success: true,
1944
2117
  stats: {
@@ -1947,15 +2120,22 @@ Analysis Features (v1.10.1):
1947
2120
  dynamicKeys: dynamicKeys.length,
1948
2121
  unusedKeys: unusedKeys.length,
1949
2122
  missingKeys: missingKeys.length,
1950
- filesScanned: this.fileUsage.size,
1951
- notTranslatedKeys: notTranslatedStats.total,
1952
- translationCompleteness: Object.fromEntries(this.translationStats)
1953
- },
1954
- unusedKeys,
1955
- missingKeys,
1956
- dynamicKeys,
1957
- notTranslatedStats
1958
- };
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
+ };
1959
2139
 
1960
2140
  } catch (error) {
1961
2141
  console.error(t("checkUsage.usage_analysis_failed"));
@@ -2013,40 +2193,4 @@ if (require.main === module) {
2013
2193
  }
2014
2194
  }
2015
2195
 
2016
- module.exports = I18nUsageAnalyzer;
2017
-
2018
- // Run if called directly
2019
- if (require.main === module) {
2020
- async function main() {
2021
- try {
2022
- const cliArgs = parseCommonArgs(process.argv.slice(2));
2023
-
2024
- if (cliArgs.help) {
2025
- displayHelp('usage');
2026
- process.exit(0);
2027
- }
2028
-
2029
- // Let run() handle full initialization to avoid duplicate setup output
2030
- const analyzer = new I18nUsageAnalyzer();
2031
- await analyzer.run();
2032
- } catch (error) {
2033
- console.error('Error:', error.message);
2034
- process.exit(1);
2035
- }
2036
- }
2037
-
2038
- // Check if we're being called from the menu system (stdin has data)
2039
- const hasStdinData = !process.stdin.isTTY;
2040
-
2041
- if (hasStdinData) {
2042
- // When called from menu, consume stdin data and run with defaults
2043
- process.stdin.resume();
2044
- process.stdin.on('data', () => {});
2045
- process.stdin.on('end', () => {
2046
- main();
2047
- });
2048
- } else {
2049
- // Normal direct execution
2050
- main();
2051
- }
2052
- }
2196
+ module.exports = I18nUsageAnalyzer;