i18ntk 2.2.0 → 2.3.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/README.md +83 -50
- package/main/i18ntk-backup-class.js +37 -41
- package/main/i18ntk-backup.js +28 -30
- package/main/i18ntk-doctor.js +7 -6
- package/main/i18ntk-init.js +44 -8
- package/main/i18ntk-sizing.js +7 -8
- package/main/i18ntk-usage.js +17 -5
- package/main/i18ntk-validate.js +72 -22
- package/main/manage/commands/AnalyzeCommand.js +12 -14
- package/main/manage/commands/CommandRouter.js +15 -12
- package/main/manage/commands/FixerCommand.js +92 -36
- package/main/manage/commands/ValidateCommand.js +78 -27
- package/main/manage/index.js +158 -148
- package/main/manage/managers/DebugMenu.js +6 -6
- package/main/manage/managers/InteractiveMenu.js +6 -6
- package/main/manage/managers/LanguageMenu.js +5 -4
- package/main/manage/managers/SettingsMenu.js +6 -6
- package/main/manage/services/AuthenticationService.js +5 -6
- package/main/manage/services/ConfigurationService.js +22 -34
- package/main/manage/services/FileManagementService.js +6 -6
- package/main/manage/services/InitService.js +44 -8
- package/main/manage/services/UsageService.js +17 -5
- package/package.json +6 -6
- package/settings/settings-cli.js +2 -2
- package/settings/settings-manager.js +984 -968
- package/utils/config-helper.js +27 -16
- package/utils/config-manager.js +8 -7
- package/utils/init-helper.js +3 -2
- package/utils/json-output.js +11 -10
- package/utils/logger.js +4 -4
- package/utils/safe-json.js +3 -3
- package/utils/secure-backup.js +8 -7
- package/utils/setup-enforcer.js +63 -98
package/main/i18ntk-sizing.js
CHANGED
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
|
|
34
34
|
const fs = require('fs');
|
|
35
35
|
const path = require('path');
|
|
36
|
-
const { performance } = require('perf_hooks');
|
|
37
36
|
const { loadTranslations, t } = require('../utils/i18n-helper');
|
|
38
37
|
const configManager = require('../settings/settings-manager');
|
|
39
38
|
const SecurityUtils = require('../utils/security');
|
|
@@ -675,7 +674,7 @@ Generated: ${new Date().toISOString()}
|
|
|
675
674
|
|
|
676
675
|
// Main analysis method
|
|
677
676
|
async analyze() {
|
|
678
|
-
const startTime =
|
|
677
|
+
const startTime = Date.now();
|
|
679
678
|
|
|
680
679
|
try {
|
|
681
680
|
logger.info(t("sizing.starting_i18n_sizing_analysis"));
|
|
@@ -702,8 +701,8 @@ Generated: ${new Date().toISOString()}
|
|
|
702
701
|
|
|
703
702
|
await this.generateHumanReadableReport();
|
|
704
703
|
|
|
705
|
-
const endTime =
|
|
706
|
-
logger.info(t("sizing.analysis_completed", { duration: (endTime - startTime).toFixed(2) }));
|
|
704
|
+
const endTime = Date.now();
|
|
705
|
+
logger.info(t("sizing.analysis_completed", { duration: ((endTime - startTime) / 1000).toFixed(2) }));
|
|
707
706
|
|
|
708
707
|
} catch (error) {
|
|
709
708
|
logger.error(t("sizing.analysis_failed", { errorMessage: error.message }));
|
|
@@ -910,7 +909,7 @@ Options:
|
|
|
910
909
|
logger.info(t("sizing.starting_analysis"));
|
|
911
910
|
logger.info(t("sizing.source_directory", { sourceDir: this.sourceDir }));
|
|
912
911
|
|
|
913
|
-
const startTime =
|
|
912
|
+
const startTime = Date.now();
|
|
914
913
|
|
|
915
914
|
// Get language files
|
|
916
915
|
const files = this.getLanguageFiles();
|
|
@@ -936,8 +935,8 @@ Options:
|
|
|
936
935
|
// Generate reports if requested
|
|
937
936
|
await this.generateHumanReadableReport();
|
|
938
937
|
|
|
939
|
-
const endTime =
|
|
940
|
-
const duration = ((endTime - startTime) / 1000).toFixed(2);
|
|
938
|
+
const endTime = Date.now();
|
|
939
|
+
const duration = ((endTime - startTime) / 1000).toFixed(2);
|
|
941
940
|
|
|
942
941
|
logger.info(t("sizing.analysis_completed", { duration }));
|
|
943
942
|
|
|
@@ -961,4 +960,4 @@ if (require.main === module) {
|
|
|
961
960
|
});
|
|
962
961
|
}
|
|
963
962
|
|
|
964
|
-
module.exports = I18nSizingAnalyzer;
|
|
963
|
+
module.exports = I18nSizingAnalyzer;
|
package/main/i18ntk-usage.js
CHANGED
|
@@ -786,10 +786,22 @@ Analysis Features (v1.8.3):
|
|
|
786
786
|
return keys;
|
|
787
787
|
}
|
|
788
788
|
|
|
789
|
-
collectPlaceholderKeys(obj, prefix = '', language) {
|
|
790
|
-
const patterns = this.placeholderStyles[language] || [];
|
|
791
|
-
const regexes = patterns.
|
|
792
|
-
|
|
789
|
+
collectPlaceholderKeys(obj, prefix = '', language) {
|
|
790
|
+
const patterns = this.placeholderStyles[language] || [];
|
|
791
|
+
const regexes = patterns.reduce((compiled, pattern) => {
|
|
792
|
+
try {
|
|
793
|
+
compiled.push(new RegExp(pattern));
|
|
794
|
+
} catch (error) {
|
|
795
|
+
SecurityUtils.logSecurityEvent('Invalid placeholder regex pattern skipped', 'warn', {
|
|
796
|
+
component: 'i18ntk-usage',
|
|
797
|
+
language,
|
|
798
|
+
pattern,
|
|
799
|
+
error: error.message
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
return compiled;
|
|
803
|
+
}, []);
|
|
804
|
+
if (typeof obj !== 'object' || obj === null) return;
|
|
793
805
|
|
|
794
806
|
for (const [key, value] of Object.entries(obj)) {
|
|
795
807
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
@@ -1837,4 +1849,4 @@ if (require.main === module) {
|
|
|
1837
1849
|
// Normal direct execution
|
|
1838
1850
|
main();
|
|
1839
1851
|
}
|
|
1840
|
-
}
|
|
1852
|
+
}
|
package/main/i18ntk-validate.js
CHANGED
|
@@ -186,18 +186,31 @@ class I18nValidator {
|
|
|
186
186
|
this.warnings.push({ message, details, type: 'warning' });
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
// Get all available languages
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
189
|
+
// Get all available languages
|
|
190
|
+
isExcludedLanguageDirectory(name) {
|
|
191
|
+
if (!name || typeof name !== 'string') return true;
|
|
192
|
+
const lowered = name.toLowerCase();
|
|
193
|
+
return lowered.startsWith('backup-') ||
|
|
194
|
+
lowered === 'backup' ||
|
|
195
|
+
lowered === 'backups' ||
|
|
196
|
+
lowered === 'i18ntk-backups' ||
|
|
197
|
+
lowered === 'reports' ||
|
|
198
|
+
lowered === 'i18ntk-reports';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getAvailableLanguages() {
|
|
202
|
+
try {
|
|
203
|
+
if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
|
|
204
|
+
throw new Error(`Source directory not found: ${this.sourceDir}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const languages = fs.readdirSync(this.sourceDir)
|
|
208
|
+
.filter(item => {
|
|
209
|
+
const itemPath = path.join(this.sourceDir, item);
|
|
210
|
+
return fs.statSync(itemPath).isDirectory() &&
|
|
211
|
+
item !== this.config.sourceLanguage &&
|
|
212
|
+
!this.isExcludedLanguageDirectory(item);
|
|
213
|
+
});
|
|
201
214
|
|
|
202
215
|
return languages;
|
|
203
216
|
} catch (error) {
|
|
@@ -620,10 +633,42 @@ class I18nValidator {
|
|
|
620
633
|
return warnings;
|
|
621
634
|
}
|
|
622
635
|
|
|
623
|
-
// Show help message
|
|
624
|
-
showHelp() {
|
|
625
|
-
console.log(t('validate.help_message'));
|
|
626
|
-
}
|
|
636
|
+
// Show help message
|
|
637
|
+
showHelp() {
|
|
638
|
+
console.log(t('validate.help_message'));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
saveValidationSummaryReport(results = {}, success = true) {
|
|
642
|
+
try {
|
|
643
|
+
const outputDir = path.resolve(this.config.outputDir || './i18ntk-reports');
|
|
644
|
+
SecurityUtils.safeMkdirSync(outputDir, process.cwd(), { recursive: true });
|
|
645
|
+
|
|
646
|
+
const timestamp = new Date().toISOString();
|
|
647
|
+
const safeTimestamp = timestamp.replace(/[:.]/g, '-');
|
|
648
|
+
const reportPath = path.join(outputDir, `validation-summary-${safeTimestamp}.txt`);
|
|
649
|
+
|
|
650
|
+
const lines = [];
|
|
651
|
+
lines.push('I18NTK Validation Summary');
|
|
652
|
+
lines.push('========================');
|
|
653
|
+
lines.push(`Generated: ${timestamp}`);
|
|
654
|
+
lines.push(`Result: ${success ? 'PASS' : 'FAIL'}`);
|
|
655
|
+
lines.push(`Errors: ${this.errors.length}`);
|
|
656
|
+
lines.push(`Warnings: ${this.warnings.length}`);
|
|
657
|
+
lines.push('');
|
|
658
|
+
lines.push('Language Results');
|
|
659
|
+
lines.push('----------------');
|
|
660
|
+
|
|
661
|
+
Object.entries(results).forEach(([language, validation]) => {
|
|
662
|
+
const summary = validation?.summary || {};
|
|
663
|
+
lines.push(`${language}: ${summary.percentage || 0}% (${summary.translatedKeys || 0}/${summary.totalKeys || 0})`);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
SecurityUtils.safeWriteFileSync(reportPath, lines.join('\n') + '\n', process.cwd(), 'utf8');
|
|
667
|
+
return reportPath;
|
|
668
|
+
} catch (error) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
627
672
|
|
|
628
673
|
// Main validation process
|
|
629
674
|
async validate() {
|
|
@@ -854,11 +899,16 @@ class I18nValidator {
|
|
|
854
899
|
console.log(t('validate.considerRunningUsageAnalysis'));
|
|
855
900
|
}
|
|
856
901
|
|
|
857
|
-
// Exit with appropriate code
|
|
858
|
-
const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
902
|
+
// Exit with appropriate code
|
|
903
|
+
const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
|
|
904
|
+
const summaryReportPath = this.saveValidationSummaryReport(results, success);
|
|
905
|
+
if (summaryReportPath) {
|
|
906
|
+
console.log('');
|
|
907
|
+
console.log(`📄 Validation summary report saved: ${summaryReportPath}`);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return {
|
|
911
|
+
success,
|
|
862
912
|
errors: this.errors.length,
|
|
863
913
|
warnings: this.warnings.length,
|
|
864
914
|
results
|
|
@@ -1051,4 +1101,4 @@ if (require.main === module) {
|
|
|
1051
1101
|
process.exit(ExitCodes.CONFIG_ERROR);
|
|
1052
1102
|
}
|
|
1053
1103
|
})();
|
|
1054
|
-
}
|
|
1104
|
+
}
|
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
* Contains embedded business logic from I18nAnalyzer.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const cliHelper = require('../../../utils/cli-helper');
|
|
12
|
-
const { loadTranslations, t } = require('../../../utils/i18n-helper');
|
|
13
|
-
const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../../../utils/config-helper');
|
|
14
|
-
const SecurityUtils = require('../../../utils/security');
|
|
15
|
-
const AdminCLI = require('../../../utils/admin-cli');
|
|
16
|
-
const
|
|
17
|
-
const
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const cliHelper = require('../../../utils/cli-helper');
|
|
12
|
+
const { loadTranslations, t } = require('../../../utils/i18n-helper');
|
|
13
|
+
const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../../../utils/config-helper');
|
|
14
|
+
const SecurityUtils = require('../../../utils/security');
|
|
15
|
+
const AdminCLI = require('../../../utils/admin-cli');
|
|
16
|
+
const AdminAuth = require('../../../utils/admin-auth');
|
|
17
|
+
const watchLocales = require('../../../utils/watch-locales');
|
|
18
|
+
const JsonOutput = require('../../../utils/json-output');
|
|
18
19
|
|
|
19
20
|
loadTranslations('en', path.resolve(__dirname, '../../../ui-locales'));
|
|
20
21
|
|
|
@@ -81,8 +82,7 @@ class AnalyzeCommand {
|
|
|
81
82
|
this.outputDir = this.config.outputDir;
|
|
82
83
|
|
|
83
84
|
// Validate source directory exists
|
|
84
|
-
|
|
85
|
-
validateSourceDir(this.sourceDir, 'i18ntk-analyze');
|
|
85
|
+
validateSourceDir(this.sourceDir, 'i18ntk-analyze');
|
|
86
86
|
|
|
87
87
|
} catch (error) {
|
|
88
88
|
console.error(`Fatal analysis error: ${error.message}`);
|
|
@@ -908,15 +908,13 @@ class AnalyzeCommand {
|
|
|
908
908
|
const isCalledDirectly = require.main === module;
|
|
909
909
|
if (isCalledDirectly && !args.noPrompt && !fromMenu) {
|
|
910
910
|
// Only check admin authentication when running directly and not in no-prompt mode
|
|
911
|
-
const
|
|
912
|
-
const adminAuth = new AdminAuth();
|
|
911
|
+
const adminAuth = new AdminAuth();
|
|
913
912
|
await adminAuth.initialize();
|
|
914
913
|
|
|
915
914
|
const isRequired = await adminAuth.isAuthRequired();
|
|
916
915
|
if (isRequired) {
|
|
917
916
|
console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'analyze translations' }));
|
|
918
|
-
const
|
|
919
|
-
const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
|
|
917
|
+
const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
|
|
920
918
|
const isValid = await this.adminAuth.verifyPin(pin);
|
|
921
919
|
|
|
922
920
|
if (!isValid) {
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* Handles command execution, authentication, and completion management.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const { t } = require('../../../utils/i18n-helper');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { t } = require('../../../utils/i18n-helper');
|
|
12
|
+
const cliHelper = require('../../../utils/cli-helper');
|
|
12
13
|
|
|
13
14
|
// Import command handlers
|
|
14
15
|
const InitCommand = require('./InitCommand');
|
|
@@ -90,20 +91,22 @@ class CommandRouter {
|
|
|
90
91
|
/**
|
|
91
92
|
* Check admin authentication for protected commands
|
|
92
93
|
*/
|
|
93
|
-
async checkAdminAuth() {
|
|
94
|
-
if (!this.adminAuth) {
|
|
95
|
-
return true; // No auth service available, allow execution
|
|
96
|
-
}
|
|
94
|
+
async checkAdminAuth(adminPin = null) {
|
|
95
|
+
if (!this.adminAuth) {
|
|
96
|
+
return true; // No auth service available, allow execution
|
|
97
|
+
}
|
|
97
98
|
|
|
98
99
|
const isRequired = await this.adminAuth.isAuthRequired();
|
|
99
100
|
if (!isRequired) {
|
|
100
101
|
return true;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
let pin = adminPin;
|
|
105
|
+
if (!pin) {
|
|
106
|
+
console.log(t('adminCli.authRequired'));
|
|
107
|
+
pin = await cliHelper.promptPin(t('adminCli.enterPin'));
|
|
108
|
+
}
|
|
109
|
+
const isValid = await this.adminAuth.verifyPin(pin);
|
|
107
110
|
|
|
108
111
|
if (!isValid) {
|
|
109
112
|
console.log(t('adminCli.invalidPin'));
|
|
@@ -141,7 +144,7 @@ class CommandRouter {
|
|
|
141
144
|
];
|
|
142
145
|
|
|
143
146
|
if (authRequiredCommands.includes(command)) {
|
|
144
|
-
const authPassed = await this.checkAdminAuth();
|
|
147
|
+
const authPassed = await this.checkAdminAuth(options.adminPin || null);
|
|
145
148
|
if (!authPassed) {
|
|
146
149
|
if (!this.isNonInteractiveMode && !isDirectCommand && this.prompt) {
|
|
147
150
|
await this.prompt(t('menu.pressEnterToContinue'));
|
|
@@ -292,4 +295,4 @@ class CommandRouter {
|
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
297
|
|
|
295
|
-
module.exports = CommandRouter;
|
|
298
|
+
module.exports = CommandRouter;
|
|
@@ -27,10 +27,21 @@ class FixerCommand {
|
|
|
27
27
|
// Initialize fixer properties
|
|
28
28
|
this.sourceDir = null;
|
|
29
29
|
this.outputDir = null;
|
|
30
|
-
this.backupDir = null;
|
|
31
|
-
this.dryRun = false;
|
|
32
|
-
this.force = false;
|
|
33
|
-
}
|
|
30
|
+
this.backupDir = null;
|
|
31
|
+
this.dryRun = false;
|
|
32
|
+
this.force = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isExcludedLanguageDirectory(name) {
|
|
36
|
+
if (!name || typeof name !== 'string') return true;
|
|
37
|
+
const lowered = name.toLowerCase();
|
|
38
|
+
return lowered.startsWith('backup-') ||
|
|
39
|
+
lowered === 'backup' ||
|
|
40
|
+
lowered === 'backups' ||
|
|
41
|
+
lowered === 'i18ntk-backups' ||
|
|
42
|
+
lowered === 'reports' ||
|
|
43
|
+
lowered === 'i18ntk-reports';
|
|
44
|
+
}
|
|
34
45
|
|
|
35
46
|
/**
|
|
36
47
|
* Set runtime dependencies for interactive operations
|
|
@@ -68,7 +79,7 @@ class FixerCommand {
|
|
|
68
79
|
|
|
69
80
|
this.sourceDir = this.config.sourceDir;
|
|
70
81
|
this.outputDir = this.config.outputDir;
|
|
71
|
-
this.backupDir = path.
|
|
82
|
+
this.backupDir = path.resolve(this.config.backup?.location || './i18ntk-backups', 'fixer');
|
|
72
83
|
|
|
73
84
|
// Validate source directory exists
|
|
74
85
|
const { validateSourceDir } = require('../../../utils/config-helper');
|
|
@@ -127,10 +138,15 @@ class FixerCommand {
|
|
|
127
138
|
const languages = [];
|
|
128
139
|
|
|
129
140
|
// Check for directory-based structure
|
|
130
|
-
const directories = items
|
|
131
|
-
.filter(item => item.isDirectory())
|
|
132
|
-
.map(item => item.name)
|
|
133
|
-
.filter(name =>
|
|
141
|
+
const directories = items
|
|
142
|
+
.filter(item => item.isDirectory())
|
|
143
|
+
.map(item => item.name)
|
|
144
|
+
.filter(name =>
|
|
145
|
+
name !== 'node_modules' &&
|
|
146
|
+
!name.startsWith('.') &&
|
|
147
|
+
name !== this.config.sourceLanguage &&
|
|
148
|
+
!this.isExcludedLanguageDirectory(name)
|
|
149
|
+
);
|
|
134
150
|
|
|
135
151
|
// Check for monolith files (language.json files)
|
|
136
152
|
const files = items
|
|
@@ -142,9 +158,13 @@ class FixerCommand {
|
|
|
142
158
|
|
|
143
159
|
// Add monolith files as languages (without .json extension)
|
|
144
160
|
const monolithLanguages = files
|
|
145
|
-
.map(file => file.replace('.json', ''))
|
|
146
|
-
.filter(lang =>
|
|
147
|
-
|
|
161
|
+
.map(file => file.replace('.json', ''))
|
|
162
|
+
.filter(lang =>
|
|
163
|
+
!languages.includes(lang) &&
|
|
164
|
+
lang !== this.config.sourceLanguage &&
|
|
165
|
+
!this.isExcludedLanguageDirectory(lang)
|
|
166
|
+
);
|
|
167
|
+
languages.push(...monolithLanguages);
|
|
148
168
|
|
|
149
169
|
return [...new Set(languages)].sort();
|
|
150
170
|
} catch (error) {
|
|
@@ -279,14 +299,21 @@ class FixerCommand {
|
|
|
279
299
|
}
|
|
280
300
|
|
|
281
301
|
// Create backup of translation files
|
|
282
|
-
async createBackup() {
|
|
283
|
-
if (this.dryRun) return;
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
302
|
+
async createBackup() {
|
|
303
|
+
if (this.dryRun) return;
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const backupEnabled = this.config?.backup?.enabled === true;
|
|
307
|
+
if (!backupEnabled) {
|
|
308
|
+
this.backupDir = null;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
313
|
+
const backupRoot = path.resolve(this.config?.backup?.location || './i18ntk-backups');
|
|
314
|
+
this.backupDir = path.join(backupRoot, 'fixer', `backup-${timestamp}`);
|
|
315
|
+
|
|
316
|
+
console.log(t('fixer.creatingBackup', { dir: this.backupDir }));
|
|
290
317
|
|
|
291
318
|
// Ensure backup directory exists
|
|
292
319
|
const dirCreated = SecurityUtils.safeMkdirSync(this.backupDir, process.cwd(), { recursive: true });
|
|
@@ -299,8 +326,8 @@ class FixerCommand {
|
|
|
299
326
|
const languages = this.getAvailableLanguages();
|
|
300
327
|
languages.push(this.config.sourceLanguage); // Include source language
|
|
301
328
|
|
|
302
|
-
for (const language of languages) {
|
|
303
|
-
const languageFiles = this.getLanguageFiles(language);
|
|
329
|
+
for (const language of languages) {
|
|
330
|
+
const languageFiles = this.getLanguageFiles(language);
|
|
304
331
|
|
|
305
332
|
for (const fileName of languageFiles) {
|
|
306
333
|
const sourcePath = path.join(this.sourceDir, language, fileName);
|
|
@@ -316,13 +343,42 @@ class FixerCommand {
|
|
|
316
343
|
SecurityUtils.safeWriteFileSync(backupPath, content, process.cwd(), 'utf8');
|
|
317
344
|
}
|
|
318
345
|
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
console.
|
|
324
|
-
}
|
|
325
|
-
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this.cleanupOldBackups();
|
|
349
|
+
|
|
350
|
+
console.log(t('fixer.backupCreated'));
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.warn(`Failed to create backup: ${error.message}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
cleanupOldBackups() {
|
|
357
|
+
try {
|
|
358
|
+
const fixerBackupRoot = path.resolve(this.config?.backup?.location || './i18ntk-backups', 'fixer');
|
|
359
|
+
if (!SecurityUtils.safeExistsSync(fixerBackupRoot, process.cwd())) return;
|
|
360
|
+
|
|
361
|
+
const configuredKeep = parseInt(this.config?.backup?.maxBackups, 10);
|
|
362
|
+
const keepCount = Number.isInteger(configuredKeep) ? Math.min(Math.max(configuredKeep, 1), 3) : 1;
|
|
363
|
+
|
|
364
|
+
const backupDirs = SecurityUtils.safeReaddirSync(fixerBackupRoot, process.cwd(), { withFileTypes: true })
|
|
365
|
+
.filter(entry => entry.isDirectory() && entry.name.startsWith('backup-'))
|
|
366
|
+
.map(entry => {
|
|
367
|
+
const dirPath = path.join(fixerBackupRoot, entry.name);
|
|
368
|
+
const stat = SecurityUtils.safeStatSync(dirPath, process.cwd());
|
|
369
|
+
return { name: entry.name, path: dirPath, mtimeMs: stat ? stat.mtimeMs : 0 };
|
|
370
|
+
})
|
|
371
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
372
|
+
|
|
373
|
+
if (backupDirs.length <= keepCount) return;
|
|
374
|
+
|
|
375
|
+
for (const staleDir of backupDirs.slice(keepCount)) {
|
|
376
|
+
fs.rmSync(staleDir.path, { recursive: true, force: true });
|
|
377
|
+
}
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.warn(`Failed to clean old backups: ${error.message}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
326
382
|
|
|
327
383
|
// Analyze translation issues for fixing
|
|
328
384
|
analyzeIssues(language, fileName) {
|
|
@@ -471,9 +527,9 @@ class FixerCommand {
|
|
|
471
527
|
}
|
|
472
528
|
|
|
473
529
|
// Create backup unless disabled
|
|
474
|
-
if (!args.noBackup && !this.dryRun) {
|
|
475
|
-
await this.createBackup();
|
|
476
|
-
}
|
|
530
|
+
if (!args.noBackup && !this.dryRun && this.config?.backup?.enabled === true) {
|
|
531
|
+
await this.createBackup();
|
|
532
|
+
}
|
|
477
533
|
|
|
478
534
|
const languages = this.getAvailableLanguages();
|
|
479
535
|
|
|
@@ -535,9 +591,9 @@ class FixerCommand {
|
|
|
535
591
|
console.log(t('fixer.totalIssues', { count: totalIssues }));
|
|
536
592
|
console.log(t('fixer.totalFixed', { count: totalFixed }));
|
|
537
593
|
|
|
538
|
-
if (this.backupDir && !args.noBackup) {
|
|
539
|
-
console.log(t('fixer.backupLocation', { dir: this.backupDir }));
|
|
540
|
-
}
|
|
594
|
+
if (this.backupDir && !args.noBackup && this.config?.backup?.enabled === true) {
|
|
595
|
+
console.log(t('fixer.backupLocation', { dir: this.backupDir }));
|
|
596
|
+
}
|
|
541
597
|
|
|
542
598
|
console.log(t('fixer.completed'));
|
|
543
599
|
|
|
@@ -621,4 +677,4 @@ class FixerCommand {
|
|
|
621
677
|
}
|
|
622
678
|
}
|
|
623
679
|
|
|
624
|
-
module.exports = FixerCommand;
|
|
680
|
+
module.exports = FixerCommand;
|
|
@@ -11,9 +11,10 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { loadTranslations, t } = require('../../../utils/i18n-helper');
|
|
13
13
|
const configManager = require('../../../utils/config-manager');
|
|
14
|
-
const SecurityUtils = require('../../../utils/security');
|
|
15
|
-
const AdminCLI = require('../../../utils/admin-cli');
|
|
16
|
-
const
|
|
14
|
+
const SecurityUtils = require('../../../utils/security');
|
|
15
|
+
const AdminCLI = require('../../../utils/admin-cli');
|
|
16
|
+
const AdminAuth = require('../../../utils/admin-auth');
|
|
17
|
+
const watchLocales = require('../../../utils/watch-locales');
|
|
17
18
|
const { getGlobalReadline, closeGlobalReadline } = require('../../../utils/cli');
|
|
18
19
|
const { getUnifiedConfig, parseCommonArgs, displayHelp, validateSourceDir, displayPaths } = require('../../../utils/config-helper');
|
|
19
20
|
const I18nInitializer = require('../../i18ntk-init');
|
|
@@ -166,18 +167,31 @@ class ValidateCommand {
|
|
|
166
167
|
this.warnings.push({ message, details, type: 'warning' });
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
// Get all available languages
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
170
|
+
// Get all available languages
|
|
171
|
+
isExcludedLanguageDirectory(name) {
|
|
172
|
+
if (!name || typeof name !== 'string') return true;
|
|
173
|
+
const lowered = name.toLowerCase();
|
|
174
|
+
return lowered.startsWith('backup-') ||
|
|
175
|
+
lowered === 'backup' ||
|
|
176
|
+
lowered === 'backups' ||
|
|
177
|
+
lowered === 'i18ntk-backups' ||
|
|
178
|
+
lowered === 'reports' ||
|
|
179
|
+
lowered === 'i18ntk-reports';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getAvailableLanguages() {
|
|
183
|
+
try {
|
|
184
|
+
if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
|
|
185
|
+
throw new Error(`Source directory not found: ${this.sourceDir}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const languages = fs.readdirSync(this.sourceDir)
|
|
189
|
+
.filter(item => {
|
|
190
|
+
const itemPath = path.join(this.sourceDir, item);
|
|
191
|
+
return fs.statSync(itemPath).isDirectory() &&
|
|
192
|
+
item !== this.config.sourceLanguage &&
|
|
193
|
+
!this.isExcludedLanguageDirectory(item);
|
|
194
|
+
});
|
|
181
195
|
|
|
182
196
|
return languages;
|
|
183
197
|
} catch (error) {
|
|
@@ -599,9 +613,43 @@ class ValidateCommand {
|
|
|
599
613
|
}
|
|
600
614
|
|
|
601
615
|
// Show help message
|
|
602
|
-
showHelp() {
|
|
603
|
-
console.log(t('validate.help_message'));
|
|
604
|
-
}
|
|
616
|
+
showHelp() {
|
|
617
|
+
console.log(t('validate.help_message'));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
saveValidationSummaryReport(results = {}, success = true) {
|
|
621
|
+
try {
|
|
622
|
+
const outputDir = path.resolve(this.config.outputDir || './i18ntk-reports');
|
|
623
|
+
SecurityUtils.safeMkdirSync(outputDir, process.cwd(), { recursive: true });
|
|
624
|
+
|
|
625
|
+
const timestamp = new Date().toISOString();
|
|
626
|
+
const safeTimestamp = timestamp.replace(/[:.]/g, '-');
|
|
627
|
+
const reportPath = path.join(outputDir, `validation-summary-${safeTimestamp}.txt`);
|
|
628
|
+
|
|
629
|
+
const lines = [];
|
|
630
|
+
lines.push('I18NTK Validation Summary');
|
|
631
|
+
lines.push('========================');
|
|
632
|
+
lines.push(`Generated: ${timestamp}`);
|
|
633
|
+
lines.push(`Result: ${success ? 'PASS' : 'FAIL'}`);
|
|
634
|
+
lines.push(`Errors: ${this.errors.length}`);
|
|
635
|
+
lines.push(`Warnings: ${this.warnings.length}`);
|
|
636
|
+
lines.push('');
|
|
637
|
+
lines.push('Language Results');
|
|
638
|
+
lines.push('----------------');
|
|
639
|
+
|
|
640
|
+
Object.entries(results).forEach(([language, validation]) => {
|
|
641
|
+
const summary = validation?.summary || {};
|
|
642
|
+
lines.push(
|
|
643
|
+
`${language}: ${summary.percentage || 0}% (${summary.translatedKeys || 0}/${summary.totalKeys || 0})`
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
SecurityUtils.safeWriteFileSync(reportPath, lines.join('\n') + '\n', process.cwd(), 'utf8');
|
|
648
|
+
return reportPath;
|
|
649
|
+
} catch (error) {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
605
653
|
|
|
606
654
|
// Main validation process
|
|
607
655
|
async validate() {
|
|
@@ -832,11 +880,16 @@ class ValidateCommand {
|
|
|
832
880
|
console.log(t('validate.considerRunningUsageAnalysis'));
|
|
833
881
|
}
|
|
834
882
|
|
|
835
|
-
// Exit with appropriate code
|
|
836
|
-
const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
883
|
+
// Exit with appropriate code
|
|
884
|
+
const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
|
|
885
|
+
const summaryReportPath = this.saveValidationSummaryReport(results, success);
|
|
886
|
+
if (summaryReportPath) {
|
|
887
|
+
console.log('');
|
|
888
|
+
console.log(`📄 Validation summary report saved: ${summaryReportPath}`);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return {
|
|
892
|
+
success,
|
|
840
893
|
errors: this.errors.length,
|
|
841
894
|
warnings: this.warnings.length,
|
|
842
895
|
results
|
|
@@ -879,8 +932,7 @@ class ValidateCommand {
|
|
|
879
932
|
// Skip admin authentication when called from menu
|
|
880
933
|
if (!fromMenu) {
|
|
881
934
|
// Check admin authentication for sensitive operations (only when called directly and not in no-prompt mode)
|
|
882
|
-
const
|
|
883
|
-
const adminAuth = new AdminAuth();
|
|
935
|
+
const adminAuth = new AdminAuth();
|
|
884
936
|
await adminAuth.initialize();
|
|
885
937
|
|
|
886
938
|
const isCalledDirectly = require.main === module;
|
|
@@ -888,8 +940,7 @@ class ValidateCommand {
|
|
|
888
940
|
if (isRequired && isCalledDirectly && !args.noPrompt) {
|
|
889
941
|
console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'validate translations' }));
|
|
890
942
|
|
|
891
|
-
const
|
|
892
|
-
const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
|
|
943
|
+
const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
|
|
893
944
|
|
|
894
945
|
const isValid = await adminAuth.verifyPin(pin);
|
|
895
946
|
this.closeReadline();
|