i18ntk 4.1.0 → 4.2.1
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 +64 -5
- package/README.md +73 -17
- package/SECURITY.md +10 -4
- package/main/i18ntk-analyze.js +10 -20
- package/main/i18ntk-backup.js +106 -44
- package/main/i18ntk-init.js +153 -157
- package/main/i18ntk-setup.js +36 -13
- package/main/i18ntk-sizing.js +44 -27
- package/main/i18ntk-translate.js +311 -41
- package/main/i18ntk-usage.js +272 -103
- package/main/i18ntk-validate.js +38 -31
- package/main/manage/commands/AnalyzeCommand.js +7 -17
- package/main/manage/commands/CommandRouter.js +6 -6
- package/main/manage/commands/SizingCommand.js +5 -2
- package/main/manage/commands/TranslateCommand.js +73 -56
- package/main/manage/commands/ValidateCommand.js +58 -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 +247 -96
- package/package.json +19 -14
- 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 +175 -90
- 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/report.js +32 -4
- 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 +1 -8
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 () => {
|
|
@@ -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.
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
return
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
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
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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"));
|
package/main/i18ntk-validate.js
CHANGED
|
@@ -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(
|
|
785
|
-
console.log(t('validate.
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
-
|
|
756
|
-
const
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
@@ -30,8 +30,11 @@ class SizingCommand {
|
|
|
30
30
|
try {
|
|
31
31
|
const I18nSizingAnalyzer = require('../../i18ntk-sizing');
|
|
32
32
|
const sizingAnalyzer = new I18nSizingAnalyzer();
|
|
33
|
-
await sizingAnalyzer.run(options);
|
|
34
|
-
|
|
33
|
+
const result = await sizingAnalyzer.run(options);
|
|
34
|
+
if (result && result.success === false) {
|
|
35
|
+
throw new Error(result.error || 'Sizing analysis failed');
|
|
36
|
+
}
|
|
37
|
+
return { success: true, command: 'sizing', result };
|
|
35
38
|
} catch (error) {
|
|
36
39
|
console.error(`Sizing command failed: ${error.message}`);
|
|
37
40
|
throw error;
|