i18ntk 4.5.3 → 4.6.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 +63 -3
- package/README.md +127 -530
- package/main/i18ntk-analyze.js +77 -42
- package/main/i18ntk-complete.js +56 -37
- package/main/i18ntk-report.js +22 -9
- package/main/i18ntk-scanner.js +151 -29
- package/main/i18ntk-sizing.js +9 -6
- package/main/i18ntk-summary.js +61 -39
- package/main/i18ntk-usage.js +29 -16
- package/main/i18ntk-validate.js +38 -24
- package/main/manage/commands/AnalyzeCommand.js +79 -44
- package/main/manage/commands/ScannerCommand.js +158 -35
- package/main/manage/commands/ValidateCommand.js +37 -23
- package/main/manage/index.js +13 -9
- package/main/manage/services/FrameworkDetectionService.js +67 -9
- package/package.json +27 -36
- package/utils/config-helper.js +42 -32
- package/utils/exit-codes.js +8 -6
- package/utils/framework-detector.js +317 -6
- package/utils/prompt-helper.js +1 -1
- package/utils/report-model.js +17 -4
- package/utils/usage-source.js +2 -1
package/main/i18ntk-analyze.js
CHANGED
|
@@ -71,9 +71,16 @@ class I18nAnalyzer {
|
|
|
71
71
|
const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
|
|
72
72
|
loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
|
|
73
73
|
|
|
74
|
-
this.sourceDir = this.config.sourceDir;
|
|
75
|
-
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
76
|
-
this.outputDir = this.config.outputDir;
|
|
74
|
+
this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
|
|
75
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
76
|
+
this.outputDir = this.config.outputDir;
|
|
77
|
+
|
|
78
|
+
const explicitLocaleDir = args.i18nDir || (!args.i18nDir && args.sourceDir ? args.sourceDir : null);
|
|
79
|
+
if (explicitLocaleDir && !SecurityUtils.safeExistsSync(this.sourceDir, process.cwd())) {
|
|
80
|
+
const err = new Error(`Locale directory not found: ${this.sourceDir}`);
|
|
81
|
+
err.exitCode = 2;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
77
84
|
|
|
78
85
|
// Validate source directory exists
|
|
79
86
|
const { validateSourceDir } = require('../utils/config-helper');
|
|
@@ -710,10 +717,32 @@ try {
|
|
|
710
717
|
}
|
|
711
718
|
}
|
|
712
719
|
|
|
713
|
-
// Show help message
|
|
714
|
-
showHelp() {
|
|
715
|
-
console.log(t('analyze.help_message'));
|
|
716
|
-
}
|
|
720
|
+
// Show help message
|
|
721
|
+
showHelp() {
|
|
722
|
+
console.log(t('analyze.help_message'));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
provideSetupGuidance() {
|
|
726
|
+
return [
|
|
727
|
+
'Setup guidance:',
|
|
728
|
+
`- Ensure the locale root exists: ${this.sourceDir}`,
|
|
729
|
+
`- Ensure the source locale exists: ${path.join(this.sourceDir, this.config.sourceLanguage)}`,
|
|
730
|
+
'- Add at least one target locale, for example de/common.json or de.json.',
|
|
731
|
+
'- Prefer --locales-dir for locale files. Use --code-dir for application source files.'
|
|
732
|
+
].join('\n');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
detectStructureType() {
|
|
736
|
+
const sourceLocaleDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
737
|
+
const sourceLocaleFile = path.join(this.sourceDir, `${this.config.sourceLanguage}.json`);
|
|
738
|
+
if (SecurityUtils.safeExistsSync(sourceLocaleFile, this.sourceDir)) {
|
|
739
|
+
return { type: 'monolith' };
|
|
740
|
+
}
|
|
741
|
+
if (SecurityUtils.safeExistsSync(sourceLocaleDir, this.sourceDir)) {
|
|
742
|
+
return { type: 'directory' };
|
|
743
|
+
}
|
|
744
|
+
return { type: 'unknown' };
|
|
745
|
+
}
|
|
717
746
|
|
|
718
747
|
// Main analyze method
|
|
719
748
|
async analyze() {
|
|
@@ -737,9 +766,9 @@ try {
|
|
|
737
766
|
|
|
738
767
|
const languages = this.getAvailableLanguages();
|
|
739
768
|
|
|
740
|
-
if (languages.length === 0) {
|
|
741
|
-
const error = t('analyze.noLanguages') || '⚠️ No target languages found.';
|
|
742
|
-
const guidance = this.provideSetupGuidance();
|
|
769
|
+
if (languages.length === 0) {
|
|
770
|
+
const error = t('analyze.noLanguages') || '⚠️ No target languages found.';
|
|
771
|
+
const guidance = this.provideSetupGuidance();
|
|
743
772
|
|
|
744
773
|
if (args.json) {
|
|
745
774
|
jsonOutput.setStatus('error', error);
|
|
@@ -751,10 +780,10 @@ try {
|
|
|
751
780
|
console.log(JSON.stringify(jsonOutput.data, null, args.indent || 2));
|
|
752
781
|
return;
|
|
753
782
|
}
|
|
754
|
-
console.log(error);
|
|
755
|
-
console.log('\n' + guidance);
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
783
|
+
console.log(error);
|
|
784
|
+
console.log('\n' + guidance);
|
|
785
|
+
return { success: true, warnings: 1, message: error };
|
|
786
|
+
}
|
|
758
787
|
|
|
759
788
|
if (!args.json) {
|
|
760
789
|
console.log(t('analyze.foundLanguages', { count: languages.length, languages: languages.join(', ') }) || `📋 Found ${languages.length} languages to analyze: ${languages.join(', ')}`);
|
|
@@ -773,8 +802,11 @@ try {
|
|
|
773
802
|
const report = this.generateLanguageReport(analysis);
|
|
774
803
|
|
|
775
804
|
// Save report
|
|
776
|
-
const reportPath = await this.saveReport(language, report);
|
|
777
|
-
|
|
805
|
+
const reportPath = await this.saveReport(language, report);
|
|
806
|
+
if (!reportPath) {
|
|
807
|
+
throw new Error(`Failed to save analysis report for ${language}`);
|
|
808
|
+
}
|
|
809
|
+
const processedCount = results.length + 1;
|
|
778
810
|
|
|
779
811
|
if (!args.json) {
|
|
780
812
|
console.log(t('analyze.completed', { language }) || `✅ Analysis completed for ${language}`);
|
|
@@ -842,9 +874,9 @@ try {
|
|
|
842
874
|
console.log(t('analyze.finished') || '\n✅ Analysis completed successfully!');
|
|
843
875
|
|
|
844
876
|
// Only prompt for input if running standalone and not in no-prompt mode
|
|
845
|
-
if (require.main === module && !this.noPrompt) {
|
|
846
|
-
await this.prompt('\nPress Enter to continue...');
|
|
847
|
-
}
|
|
877
|
+
if (require.main === module && !this.noPrompt && process.env.CI !== 'true' && process.stdin.isTTY && process.stdout.isTTY) {
|
|
878
|
+
await this.prompt('\nPress Enter to continue...');
|
|
879
|
+
}
|
|
848
880
|
this.closeReadline();
|
|
849
881
|
|
|
850
882
|
return results;
|
|
@@ -881,8 +913,8 @@ try {
|
|
|
881
913
|
const uiLanguage = this.config.uiLanguage || 'en';
|
|
882
914
|
loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
|
|
883
915
|
|
|
884
|
-
this.sourceDir = this.config.sourceDir;
|
|
885
|
-
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
916
|
+
this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
|
|
917
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
886
918
|
this.outputDir = this.config.outputDir;
|
|
887
919
|
}
|
|
888
920
|
|
|
@@ -921,19 +953,20 @@ try {
|
|
|
921
953
|
loadTranslations(args.uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));}
|
|
922
954
|
|
|
923
955
|
// Update config if source directory is provided
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
this.
|
|
927
|
-
this.
|
|
928
|
-
|
|
956
|
+
const localeDirArg = args.i18nDir || (!args.i18nDir && args.sourceDir ? args.sourceDir : null);
|
|
957
|
+
if (localeDirArg) {
|
|
958
|
+
this.config.i18nDir = localeDirArg;
|
|
959
|
+
this.sourceDir = path.resolve(PROJECT_ROOT, localeDirArg);
|
|
960
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
961
|
+
}
|
|
929
962
|
|
|
930
963
|
if (args.outputDir) {
|
|
931
964
|
this.config.outputDir = args.outputDir;
|
|
932
965
|
this.outputDir = path.resolve(this.config.outputDir);
|
|
933
966
|
}
|
|
934
|
-
const execute = async () => {
|
|
935
|
-
await this.analyze();
|
|
936
|
-
};
|
|
967
|
+
const execute = async () => {
|
|
968
|
+
return await this.analyze();
|
|
969
|
+
};
|
|
937
970
|
|
|
938
971
|
if (args.watch) {
|
|
939
972
|
await execute();
|
|
@@ -949,19 +982,21 @@ try {
|
|
|
949
982
|
});
|
|
950
983
|
console.log('��� Watching for translation changes. Press Ctrl+C to exit.');
|
|
951
984
|
} else {
|
|
952
|
-
await execute();
|
|
953
|
-
if (!fromMenu && require.main === module) {
|
|
954
|
-
process.exit(0);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
985
|
+
const result = await execute();
|
|
986
|
+
if (!fromMenu && require.main === module) {
|
|
987
|
+
process.exit(0);
|
|
988
|
+
}
|
|
989
|
+
return result;
|
|
990
|
+
}
|
|
991
|
+
} catch (error) {
|
|
992
|
+
console.error(t('analyze.error') || '❌ Analysis failed:', error.message);
|
|
993
|
+
this.closeReadline();
|
|
994
|
+
if (!fromMenu && require.main === module) {
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
997
|
+
throw error;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
965
1000
|
|
|
966
1001
|
async runSetupWizard() {
|
|
967
1002
|
console.log('Translation Analysis Setup Wizard');
|
package/main/i18ntk-complete.js
CHANGED
|
@@ -16,9 +16,10 @@ const path = require('path');
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const SecurityUtils = require('../utils/security');
|
|
18
18
|
const { loadTranslations, t } = require('../utils/i18n-helper');
|
|
19
|
-
const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli');
|
|
20
|
-
const SetupEnforcer = require('../utils/setup-enforcer');
|
|
21
|
-
const { getUnifiedConfig } = require('../utils/config-helper');
|
|
19
|
+
const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli');
|
|
20
|
+
const SetupEnforcer = require('../utils/setup-enforcer');
|
|
21
|
+
const { getUnifiedConfig, displayHelp } = require('../utils/config-helper');
|
|
22
|
+
const { isInteractive } = require('../utils/prompt-helper');
|
|
22
23
|
|
|
23
24
|
// Ensure setup is complete before running, except for help output.
|
|
24
25
|
(async () => {
|
|
@@ -64,7 +65,7 @@ class I18nCompletionTool {
|
|
|
64
65
|
|
|
65
66
|
const baseConfig = await getUnifiedConfig('complete', args);
|
|
66
67
|
this.config = { ...baseConfig, ...(this.config || {}) };
|
|
67
|
-
this.sourceDir = this.config.sourceDir;
|
|
68
|
+
this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
|
|
68
69
|
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
69
70
|
|
|
70
71
|
// Validate source directory exists
|
|
@@ -99,24 +100,31 @@ class I18nCompletionTool {
|
|
|
99
100
|
const args = process.argv.slice(2);
|
|
100
101
|
const parsed = {};
|
|
101
102
|
|
|
102
|
-
args.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
for (let i = 0; i < args.length; i++) {
|
|
104
|
+
const arg = args[i];
|
|
105
|
+
if (arg.startsWith('--')) {
|
|
106
|
+
const [key, rawValue] = arg.substring(2).split('=');
|
|
107
|
+
const value = rawValue !== undefined
|
|
108
|
+
? rawValue
|
|
109
|
+
: (args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true);
|
|
110
|
+
if (key === 'source-dir' || key === 'locales-dir' || key === 'i18n-dir') {
|
|
111
|
+
parsed.sourceDir = value;
|
|
112
|
+
parsed.i18nDir = value;
|
|
113
|
+
} else if (key === 'source-language' || key === 'source-locale') {
|
|
114
|
+
parsed.sourceLanguage = value;
|
|
115
|
+
} else if (key === 'code-dir' || key === 'source-code-dir') {
|
|
116
|
+
parsed.codeDir = value;
|
|
117
|
+
} else if (key === 'auto-translate') {
|
|
110
118
|
parsed.autoTranslate = true;
|
|
111
119
|
} else if (key === 'dry-run') {
|
|
112
120
|
parsed.dryRun = true;
|
|
113
121
|
} else if (key === 'no-prompt') {
|
|
114
122
|
parsed.noPrompt = true;
|
|
115
123
|
} else if (key === 'help' || key === 'h') {
|
|
116
|
-
parsed.help = true;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
124
|
+
parsed.help = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
120
128
|
|
|
121
129
|
return parsed;
|
|
122
130
|
}
|
|
@@ -567,22 +575,24 @@ class I18nCompletionTool {
|
|
|
567
575
|
|
|
568
576
|
const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
|
|
569
577
|
loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
|
|
570
|
-
this.sourceDir = this.config.sourceDir;
|
|
578
|
+
this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
|
|
571
579
|
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
572
580
|
} else {
|
|
573
581
|
await this.initialize();
|
|
574
582
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
console.error(
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
this.sourceDir =
|
|
585
|
-
|
|
583
|
+
const localesDirArg = args.i18nDir || args.sourceDir;
|
|
584
|
+
if (localesDirArg) {
|
|
585
|
+
const resolvedSourceDir = path.resolve(localesDirArg);
|
|
586
|
+
const validatedSourceDir = SecurityUtils.validatePath(resolvedSourceDir, process.cwd());
|
|
587
|
+
if (!validatedSourceDir) {
|
|
588
|
+
console.error(t("complete.invalidSourceDir", { sourceDir: localesDirArg }));
|
|
589
|
+
console.error(`Path validation failed — source directory is outside the allowed project boundary.`);
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
this.config.sourceDir = resolvedSourceDir;
|
|
593
|
+
this.config.i18nDir = resolvedSourceDir;
|
|
594
|
+
this.sourceDir = validatedSourceDir;
|
|
595
|
+
}
|
|
586
596
|
|
|
587
597
|
if (args.sourceLanguage) {
|
|
588
598
|
this.config.sourceLanguage = SecurityUtils.sanitizeInput(args.sourceLanguage, {
|
|
@@ -643,15 +653,24 @@ class I18nCompletionTool {
|
|
|
643
653
|
}
|
|
644
654
|
|
|
645
655
|
console.log('\n');
|
|
646
|
-
console.log(t("complete.summaryTitle"));
|
|
647
|
-
console.log(t("complete.separator"));
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
656
|
+
console.log(t("complete.summaryTitle"));
|
|
657
|
+
console.log(t("complete.separator"));
|
|
658
|
+
const targetLocalesChanged = allChanges.length;
|
|
659
|
+
const filesModified = new Set(allChanges.flatMap(lang => lang.changes.map(change => `${lang.language}/${change.file}`))).size;
|
|
660
|
+
const filesSkipped = Math.max(0, targetLanguages.length - targetLocalesChanged);
|
|
661
|
+
console.log(`Locales scanned: ${languages.length}`);
|
|
662
|
+
console.log(`Target locales changed: ${targetLocalesChanged}`);
|
|
663
|
+
console.log(`Unique source keys added: ${missingKeys.length}`);
|
|
664
|
+
console.log(`Total key insertions: ${totalChanges}`);
|
|
665
|
+
console.log(`Files modified: ${args.dryRun ? 0 : filesModified}`);
|
|
666
|
+
console.log(`Files skipped: ${filesSkipped}`);
|
|
667
|
+
if (args.dryRun) {
|
|
668
|
+
console.log('Dry run only. No files were modified.');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (!args.dryRun && allChanges.length > 0 && isInteractive(args)) {
|
|
672
|
+
const rl = this.rl || this.initReadline();
|
|
673
|
+
const answer = await this.prompt('\n' + t('complete.generateReportPrompt') + ' (Y/N): ');
|
|
655
674
|
|
|
656
675
|
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
657
676
|
await this.generateReport(allChanges, languages);
|
package/main/i18ntk-report.js
CHANGED
|
@@ -22,23 +22,32 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
22
22
|
help: false,
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
for (
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
const raw = argv[i];
|
|
26
27
|
const arg = raw === 'report' ? '' : raw;
|
|
27
28
|
if (!arg) continue;
|
|
29
|
+
const nextValue = () => {
|
|
30
|
+
if (i + 1 < argv.length && !String(argv[i + 1]).startsWith('--')) {
|
|
31
|
+
return argv[++i];
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
28
35
|
if (arg === '--help' || arg === '-h') args.help = true;
|
|
29
36
|
else if (arg === '--json') args.json = true;
|
|
30
37
|
else if (arg === '--markdown' || arg === '--md') args.markdown = true;
|
|
31
38
|
else if (arg === '--html') args.html = true;
|
|
32
39
|
else if (arg.startsWith('--out=')) args.out = arg.slice('--out='.length);
|
|
33
|
-
else if (arg === '--out') args.
|
|
34
|
-
else if (args.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
else if (arg === '--out') args.out = nextValue();
|
|
41
|
+
else if (arg.startsWith('--source-dir=')) args.sourceDir = arg.slice('--source-dir='.length);
|
|
42
|
+
else if (arg.startsWith('--code-dir=')) args.sourceDir = arg.slice('--code-dir='.length);
|
|
43
|
+
else if (arg.startsWith('--source-code-dir=')) args.sourceDir = arg.slice('--source-code-dir='.length);
|
|
44
|
+
else if (arg === '--source-dir' || arg === '--code-dir' || arg === '--source-code-dir') args.sourceDir = nextValue();
|
|
38
45
|
else if (arg.startsWith('--i18n-dir=')) args.i18nDir = arg.slice('--i18n-dir='.length);
|
|
39
46
|
else if (arg.startsWith('--locales-dir=')) args.i18nDir = arg.slice('--locales-dir='.length);
|
|
47
|
+
else if (arg === '--i18n-dir' || arg === '--locales-dir') args.i18nDir = nextValue();
|
|
40
48
|
else if (arg.startsWith('--source-language=')) args.sourceLocale = arg.slice('--source-language='.length);
|
|
41
49
|
else if (arg.startsWith('--source-locale=')) args.sourceLocale = arg.slice('--source-locale='.length);
|
|
50
|
+
else if (arg === '--source-language' || arg === '--source-locale') args.sourceLocale = nextValue();
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
if (!args.json && !args.markdown && !args.html) {
|
|
@@ -107,9 +116,13 @@ Options:
|
|
|
107
116
|
--markdown Print/write Markdown report output
|
|
108
117
|
--html Print/write HTML report output
|
|
109
118
|
--out <dir> Write selected report formats into a directory
|
|
110
|
-
--
|
|
111
|
-
--
|
|
112
|
-
--source-
|
|
119
|
+
--code-dir=<dir> Source code directory to scan
|
|
120
|
+
--source-code-dir=<dir> Alias for --code-dir
|
|
121
|
+
--source-dir=<dir> Legacy alias for --code-dir
|
|
122
|
+
--locales-dir=<dir> Locale directory
|
|
123
|
+
--i18n-dir=<dir> Alias for --locales-dir
|
|
124
|
+
--source-locale=<code> Source locale code (default: en)
|
|
125
|
+
--source-language=<code> Legacy alias for --source-locale
|
|
113
126
|
`;
|
|
114
127
|
}
|
|
115
128
|
|