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.
- package/CHANGELOG.md +116 -29
- package/README.md +83 -18
- package/SECURITY.md +13 -5
- package/main/i18ntk-analyze.js +10 -20
- package/main/i18ntk-backup.js +227 -111
- package/main/i18ntk-init.js +153 -157
- package/main/i18ntk-scanner.js +9 -7
- package/main/i18ntk-setup.js +36 -13
- package/main/i18ntk-sizing.js +18 -50
- package/main/i18ntk-translate.js +169 -21
- package/main/i18ntk-usage.js +298 -154
- package/main/i18ntk-validate.js +49 -37
- package/main/manage/commands/AnalyzeCommand.js +7 -17
- package/main/manage/commands/CommandRouter.js +6 -6
- package/main/manage/commands/TranslateCommand.js +65 -56
- package/main/manage/commands/ValidateCommand.js +34 -26
- package/main/manage/index.js +11 -42
- package/main/manage/managers/InteractiveMenu.js +11 -40
- package/main/manage/services/InitService.js +114 -118
- package/main/manage/services/UsageService.js +244 -85
- package/package.json +55 -4
- package/runtime/enhanced.d.ts +5 -5
- package/runtime/enhanced.js +49 -25
- package/runtime/i18ntk.d.ts +30 -7
- package/runtime/index.d.ts +48 -19
- package/runtime/index.js +188 -97
- package/settings/settings-cli.js +115 -38
- package/settings/settings-manager.js +24 -6
- package/ui-locales/de.json +192 -11
- package/ui-locales/en.json +182 -8
- package/ui-locales/es.json +193 -12
- package/ui-locales/fr.json +189 -8
- package/ui-locales/ja.json +190 -8
- package/ui-locales/ru.json +191 -9
- package/ui-locales/zh.json +194 -9
- package/utils/cli-helper.js +8 -12
- package/utils/config-helper.js +1 -1
- package/utils/config-manager.js +8 -6
- package/utils/localized-confirm.js +55 -0
- package/utils/menu-layout.js +41 -0
- package/utils/report-writer.js +110 -0
- package/utils/security.js +15 -22
- package/utils/translate/api.js +31 -3
- package/utils/translate/placeholder.js +42 -1
- package/utils/translate/protection.js +17 -12
- package/utils/translate/report.js +3 -2
- package/utils/translate/safe-network.js +24 -4
- package/utils/usage-insights.js +435 -0
- package/utils/usage-source.js +50 -0
- package/utils/watch-locales.js +13 -9
package/main/i18ntk-usage.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
return
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
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
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
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;
|