i18ntk 1.10.2 → 2.0.2
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/LICENSE +1 -1
- package/README.md +141 -1191
- package/main/i18ntk-analyze.js +65 -84
- package/main/i18ntk-backup-class.js +420 -0
- package/main/i18ntk-backup.js +3 -3
- package/main/i18ntk-complete.js +90 -65
- package/main/i18ntk-doctor.js +123 -103
- package/main/i18ntk-fixer.js +61 -725
- package/main/i18ntk-go.js +14 -15
- package/main/i18ntk-init.js +77 -26
- package/main/i18ntk-java.js +27 -32
- package/main/i18ntk-js.js +70 -68
- package/main/i18ntk-manage.js +129 -30
- package/main/i18ntk-php.js +75 -75
- package/main/i18ntk-py.js +55 -56
- package/main/i18ntk-scanner.js +59 -57
- package/main/i18ntk-setup.js +9 -404
- package/main/i18ntk-sizing.js +6 -6
- package/main/i18ntk-summary.js +21 -18
- package/main/i18ntk-ui.js +11 -10
- package/main/i18ntk-usage.js +54 -18
- package/main/i18ntk-validate.js +13 -13
- package/main/manage/commands/AnalyzeCommand.js +1124 -0
- package/main/manage/commands/BackupCommand.js +62 -0
- package/main/manage/commands/CommandRouter.js +295 -0
- package/main/manage/commands/CompleteCommand.js +61 -0
- package/main/manage/commands/DoctorCommand.js +60 -0
- package/main/manage/commands/FixerCommand.js +624 -0
- package/main/manage/commands/InitCommand.js +62 -0
- package/main/manage/commands/ScannerCommand.js +654 -0
- package/main/manage/commands/SizingCommand.js +60 -0
- package/main/manage/commands/SummaryCommand.js +61 -0
- package/main/manage/commands/UsageCommand.js +60 -0
- package/main/manage/commands/ValidateCommand.js +978 -0
- package/main/manage/index-fixed.js +1447 -0
- package/main/manage/index.js +1462 -0
- package/main/manage/managers/DebugMenu.js +140 -0
- package/main/manage/managers/InteractiveMenu.js +177 -0
- package/main/manage/managers/LanguageMenu.js +62 -0
- package/main/manage/managers/SettingsMenu.js +53 -0
- package/main/manage/services/AuthenticationService.js +263 -0
- package/main/manage/services/ConfigurationService-fixed.js +449 -0
- package/main/manage/services/ConfigurationService.js +449 -0
- package/main/manage/services/FileManagementService.js +368 -0
- package/main/manage/services/FrameworkDetectionService.js +458 -0
- package/main/manage/services/InitService.js +1051 -0
- package/main/manage/services/SetupService.js +462 -0
- package/main/manage/services/SummaryService.js +450 -0
- package/main/manage/services/UsageService.js +1502 -0
- package/package.json +32 -29
- package/runtime/enhanced.d.ts +221 -221
- package/runtime/index.d.ts +29 -29
- package/runtime/index.full.d.ts +331 -331
- package/runtime/index.js +7 -6
- package/scripts/build-lite.js +17 -17
- package/scripts/deprecate-versions.js +23 -6
- package/scripts/export-translations.js +5 -5
- package/scripts/fix-all-i18n.js +3 -3
- package/scripts/fix-and-purify-i18n.js +3 -2
- package/scripts/fix-locale-control-chars.js +110 -0
- package/scripts/lint-locales.js +80 -0
- package/scripts/locale-optimizer.js +8 -8
- package/scripts/prepublish.js +21 -21
- package/scripts/security-check.js +117 -117
- package/scripts/sync-translations.js +4 -4
- package/scripts/sync-ui-locales.js +9 -8
- package/scripts/validate-all-translations.js +8 -7
- package/scripts/verify-deprecations.js +157 -161
- package/scripts/verify-translations.js +6 -5
- package/settings/i18ntk-config.json +282 -282
- package/settings/language-config.json +5 -5
- package/settings/settings-cli.js +9 -9
- package/settings/settings-manager.js +18 -18
- package/ui-locales/de.json +2417 -2348
- package/ui-locales/en.json +2415 -2352
- package/ui-locales/es.json +2425 -2353
- package/ui-locales/fr.json +2418 -2348
- package/ui-locales/ja.json +2463 -2361
- package/ui-locales/ru.json +2463 -2359
- package/ui-locales/zh.json +2418 -2351
- package/utils/admin-auth.js +2 -2
- package/utils/admin-cli.js +297 -297
- package/utils/admin-pin.js +9 -9
- package/utils/cli-helper.js +9 -9
- package/utils/config-helper.js +73 -104
- package/utils/config-manager.js +204 -171
- package/utils/config.js +5 -4
- package/utils/env-manager.js +249 -263
- package/utils/framework-detector.js +27 -24
- package/utils/i18n-helper.js +85 -41
- package/utils/init-helper.js +152 -94
- package/utils/json-output.js +98 -98
- package/utils/mini-commander.js +179 -0
- package/utils/missing-key-validator.js +5 -5
- package/utils/plugin-loader.js +40 -29
- package/utils/prompt.js +14 -44
- package/utils/safe-json.js +40 -0
- package/utils/secure-errors.js +3 -3
- package/utils/security-check-improved.js +390 -0
- package/utils/security-config.js +5 -5
- package/utils/security-fixed.js +607 -0
- package/utils/security.js +652 -602
- package/utils/setup-enforcer.js +136 -44
- package/utils/setup-validator.js +33 -32
- package/utils/ultra-performance-optimizer.js +11 -9
- package/utils/watch-locales.js +2 -1
- package/utils/prompt-fixed.js +0 -55
- package/utils/security-check.js +0 -454
package/main/i18ntk-complete.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* node i18ntk-complete.js --source-dir=./src/i18n/locales
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const SecurityUtils = require('../utils/security');
|
|
17
17
|
const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/config-helper');
|
|
@@ -19,21 +19,25 @@ const { loadTranslations, t } = require('../utils/i18n-helper');
|
|
|
19
19
|
const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli');
|
|
20
20
|
const SetupEnforcer = require('../utils/setup-enforcer');
|
|
21
21
|
|
|
22
|
-
// Ensure setup is complete before running
|
|
23
|
-
(async () => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
// Ensure setup is complete before running, except for help output.
|
|
23
|
+
(async () => {
|
|
24
|
+
const isHelpRequest = process.argv.slice(2).some(arg => arg === '--help' || arg === '-h');
|
|
25
|
+
if (isHelpRequest) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
await SetupEnforcer.checkSetupCompleteAsync();
|
|
30
|
+
} catch (error) {
|
|
27
31
|
console.error('Setup check failed:', error.message);
|
|
28
32
|
process.exit(1);
|
|
29
33
|
}
|
|
30
34
|
})();
|
|
31
35
|
|
|
32
|
-
loadTranslations(
|
|
36
|
+
loadTranslations();
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
|
|
36
|
-
class I18nCompletionTool {
|
|
40
|
+
class I18nCompletionTool {
|
|
37
41
|
constructor(config = {}) {
|
|
38
42
|
this.config = config;
|
|
39
43
|
this.sourceDir = null;
|
|
@@ -105,46 +109,67 @@ class I18nCompletionTool {
|
|
|
105
109
|
parsed.autoTranslate = true;
|
|
106
110
|
} else if (key === 'dry-run') {
|
|
107
111
|
parsed.dryRun = true;
|
|
108
|
-
} else if (key === 'no-prompt') {
|
|
109
|
-
parsed.noPrompt = true;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
} else if (key === 'no-prompt') {
|
|
113
|
+
parsed.noPrompt = true;
|
|
114
|
+
} else if (key === 'help' || key === 'h') {
|
|
115
|
+
parsed.help = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
113
119
|
|
|
114
120
|
return parsed;
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
// Get all available languages
|
|
118
|
-
getAvailableLanguages() {
|
|
119
|
-
if (!
|
|
123
|
+
// Get all available languages
|
|
124
|
+
getAvailableLanguages() {
|
|
125
|
+
if (!SecurityUtils.safeExistsSync(this.sourceDir, this.config.projectRoot)) {
|
|
120
126
|
throw new Error(`Source directory not found: ${this.sourceDir}`);
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
// Check for monolith JSON files (en.json, es.json, etc.)
|
|
124
|
-
const files =
|
|
125
|
-
const
|
|
126
|
-
.filter(file => file.endsWith('.json'))
|
|
127
|
-
.map(file => path.basename(file, '.json'))
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
const files = SecurityUtils.safeReaddirSync(this.sourceDir, this.config.projectRoot);
|
|
131
|
+
const languagesFromFiles = files
|
|
132
|
+
.filter(file => file.endsWith('.json'))
|
|
133
|
+
.map(file => path.basename(file, '.json'))
|
|
134
|
+
.filter(code => this.isValidLanguageCode(code));
|
|
135
|
+
|
|
136
|
+
// Also check for directory-based structure for backward compatibility
|
|
137
|
+
const directories = SecurityUtils.safeReaddirSync(this.sourceDir, this.config.projectRoot)
|
|
138
|
+
.filter(item => {
|
|
139
|
+
if (this.isExcludedLanguageDirectory(item)) return false;
|
|
140
|
+
if (!this.isValidLanguageCode(item)) return false;
|
|
141
|
+
const itemPath = path.join(this.sourceDir, item);
|
|
142
|
+
const stat = SecurityUtils.safeStatSync(itemPath, this.config.projectRoot);
|
|
143
|
+
if (!stat || !stat.isDirectory()) return false;
|
|
144
|
+
|
|
145
|
+
const localeFiles = SecurityUtils.safeReaddirSync(itemPath, this.config.projectRoot)
|
|
146
|
+
.filter(name => name.endsWith('.json'));
|
|
147
|
+
return localeFiles.length > 0;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return [...new Set([...languagesFromFiles, ...directories])];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
isValidLanguageCode(code) {
|
|
154
|
+
if (!code || typeof code !== 'string') return false;
|
|
155
|
+
return /^[a-z]{2}(?:-[A-Za-z0-9]{2,8})*$/i.test(code.trim());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
isExcludedLanguageDirectory(name) {
|
|
159
|
+
if (!name || typeof name !== 'string') return true;
|
|
160
|
+
const lowered = name.toLowerCase();
|
|
161
|
+
return lowered.startsWith('backup-') || lowered === 'backup' || lowered === 'reports' || lowered === 'i18ntk-reports';
|
|
162
|
+
}
|
|
138
163
|
|
|
139
164
|
// Get all JSON files from a language directory
|
|
140
165
|
getLanguageFiles(language) {
|
|
141
166
|
const languageDir = path.join(this.sourceDir, language);
|
|
142
167
|
|
|
143
|
-
if (!
|
|
168
|
+
if (!SecurityUtils.safeExistsSync(languageDir, this.config.projectRoot)) {
|
|
144
169
|
return [];
|
|
145
170
|
}
|
|
146
171
|
|
|
147
|
-
return
|
|
172
|
+
return SecurityUtils.safeReaddirSync(languageDir, this.config.projectRoot)
|
|
148
173
|
.filter(file => {
|
|
149
174
|
return file.endsWith('.json') &&
|
|
150
175
|
!this.config.excludeFiles.includes(file);
|
|
@@ -234,18 +259,18 @@ class I18nCompletionTool {
|
|
|
234
259
|
let fileContent = {};
|
|
235
260
|
|
|
236
261
|
// Load existing file or create new
|
|
237
|
-
if (
|
|
262
|
+
if (SecurityUtils.safeExistsSync(filePath, this.config.projectRoot)) {
|
|
238
263
|
try {
|
|
239
|
-
fileContent = JSON.parse(
|
|
264
|
+
fileContent = JSON.parse(SecurityUtils.safeReadFileSync(filePath, this.config.projectRoot, 'utf8'));
|
|
240
265
|
} catch (error) {
|
|
241
266
|
console.warn(t("completeTranslations.warning_could_not_parse_filepa", { filePath })); ;
|
|
242
267
|
fileContent = {};
|
|
243
268
|
}
|
|
244
269
|
} else {
|
|
245
270
|
// Create directory if it doesn't exist
|
|
246
|
-
if (!
|
|
271
|
+
if (!SecurityUtils.safeExistsSync(languageDir, this.config.projectRoot)) {
|
|
247
272
|
if (!dryRun) {
|
|
248
|
-
|
|
273
|
+
SecurityUtils.safeMkdirSync(languageDir, this.config.projectRoot, { recursive: true });
|
|
249
274
|
}
|
|
250
275
|
}
|
|
251
276
|
}
|
|
@@ -271,7 +296,7 @@ class I18nCompletionTool {
|
|
|
271
296
|
|
|
272
297
|
// Save file
|
|
273
298
|
if (fileChanged && !dryRun) {
|
|
274
|
-
|
|
299
|
+
SecurityUtils.safeWriteFileSync(filePath, JSON.stringify(fileContent, null, 2), this.config.projectRoot, 'utf8');
|
|
275
300
|
}
|
|
276
301
|
}
|
|
277
302
|
|
|
@@ -330,7 +355,7 @@ class I18nCompletionTool {
|
|
|
330
355
|
const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
|
|
331
356
|
const missingKeys = [];
|
|
332
357
|
|
|
333
|
-
if (!
|
|
358
|
+
if (!SecurityUtils.safeExistsSync(this.sourceLanguageDir, this.config.projectRoot)) {
|
|
334
359
|
console.log(t("complete.sourceLanguageNotFound", { sourceLanguage: this.config.sourceLanguage }));
|
|
335
360
|
return [];
|
|
336
361
|
}
|
|
@@ -340,7 +365,7 @@ class I18nCompletionTool {
|
|
|
340
365
|
const sourceFilePath = path.join(this.sourceLanguageDir, fileName);
|
|
341
366
|
|
|
342
367
|
try {
|
|
343
|
-
const sourceContent =
|
|
368
|
+
const sourceContent = SecurityUtils.safeParseJSON(SecurityUtils.safeReadFileSync(sourceFilePath, this.config.projectRoot, 'utf8'));
|
|
344
369
|
const sourceKeys = this.getAllKeys(sourceContent);
|
|
345
370
|
|
|
346
371
|
// Check all other languages
|
|
@@ -351,9 +376,9 @@ class I18nCompletionTool {
|
|
|
351
376
|
const targetFilePath = path.join(this.sourceDir, language, fileName);
|
|
352
377
|
let targetKeys = [];
|
|
353
378
|
|
|
354
|
-
if (
|
|
379
|
+
if (SecurityUtils.safeExistsSync(targetFilePath, this.config.projectRoot)) {
|
|
355
380
|
try {
|
|
356
|
-
const targetContent =
|
|
381
|
+
const targetContent = SecurityUtils.safeParseJSON(SecurityUtils.safeReadFileSync(targetFilePath, this.config.projectRoot, 'utf8'));
|
|
357
382
|
targetKeys = this.getAllKeys(targetContent);
|
|
358
383
|
} catch (error) {
|
|
359
384
|
console.warn(t("complete.couldNotParseTarget", { file: targetFilePath }));
|
|
@@ -379,8 +404,8 @@ class I18nCompletionTool {
|
|
|
379
404
|
async generateReport(changes, languages) {
|
|
380
405
|
const projectRoot = this.config.projectRoot || process.cwd();
|
|
381
406
|
const reportsDir = path.join(projectRoot, 'i18ntk-reports');
|
|
382
|
-
if (!
|
|
383
|
-
|
|
407
|
+
if (!SecurityUtils.safeExistsSync(reportsDir, projectRoot)) {
|
|
408
|
+
SecurityUtils.safeMkdirSync(reportsDir, projectRoot, { recursive: true });
|
|
384
409
|
}
|
|
385
410
|
|
|
386
411
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -407,7 +432,7 @@ class I18nCompletionTool {
|
|
|
407
432
|
}))
|
|
408
433
|
};
|
|
409
434
|
|
|
410
|
-
|
|
435
|
+
SecurityUtils.safeWriteFileSync(reportPath, JSON.stringify(report, null, 2), projectRoot, 'utf8');
|
|
411
436
|
console.log(t("complete.reportGenerated", { path: reportPath }));
|
|
412
437
|
return reportPath;
|
|
413
438
|
}
|
|
@@ -456,7 +481,7 @@ class I18nCompletionTool {
|
|
|
456
481
|
this.config = { ...baseConfig, ...(this.config || {}) };
|
|
457
482
|
|
|
458
483
|
const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
|
|
459
|
-
loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));this.sourceDir = this.config.sourceDir;
|
|
484
|
+
loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales'));this.sourceDir = this.config.sourceDir;
|
|
460
485
|
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
461
486
|
} else {
|
|
462
487
|
await this.initialize();
|
|
@@ -527,28 +552,28 @@ class I18nCompletionTool {
|
|
|
527
552
|
console.log(t("complete.languagesProcessed", { languagesProcessed: languages.length }));
|
|
528
553
|
console.log(t("complete.missingKeysAdded", { missingKeysAdded: missingKeys.length }));
|
|
529
554
|
|
|
530
|
-
if (!args.dryRun && allChanges.length > 0) {
|
|
531
|
-
const rl = this.rl || this.initReadline();
|
|
532
|
-
const answer = await this.prompt('\n' + t('complete.generateReportPrompt') + ' (Y/N): ');
|
|
533
|
-
|
|
534
|
-
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
535
|
-
await this.generateReport(allChanges, languages);
|
|
555
|
+
if (!args.dryRun && allChanges.length > 0 && !args.noPrompt) {
|
|
556
|
+
const rl = this.rl || this.initReadline();
|
|
557
|
+
const answer = await this.prompt('\n' + t('complete.generateReportPrompt') + ' (Y/N): ');
|
|
558
|
+
|
|
559
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
560
|
+
await this.generateReport(allChanges, languages);
|
|
536
561
|
}
|
|
537
562
|
}
|
|
538
563
|
|
|
539
|
-
if (!args.dryRun) {
|
|
540
|
-
console.log('\n' + t("complete.nextStepsTitle"));
|
|
541
|
-
console.log(t("complete.separator"));
|
|
542
|
-
console.log(
|
|
543
|
-
console.log('
|
|
544
|
-
console.log(
|
|
545
|
-
console.log('
|
|
546
|
-
console.log(
|
|
547
|
-
console.log('
|
|
548
|
-
console.log('\n' + t("complete.allKeysAvailable"));
|
|
549
|
-
} else {
|
|
550
|
-
console.log('\n' + t("complete.runWithoutDryRun"));
|
|
551
|
-
}
|
|
564
|
+
if (!args.dryRun) {
|
|
565
|
+
console.log('\n' + t("complete.nextStepsTitle"));
|
|
566
|
+
console.log(t("complete.separator"));
|
|
567
|
+
console.log('1. Run usage analysis:');
|
|
568
|
+
console.log(' i18ntk --command=usage');
|
|
569
|
+
console.log('2. Validate translations:');
|
|
570
|
+
console.log(' i18ntk --command=validate');
|
|
571
|
+
console.log('3. Analyze translation status:');
|
|
572
|
+
console.log(' i18ntk --command=analyze');
|
|
573
|
+
console.log('\n' + t("complete.allKeysAvailable"));
|
|
574
|
+
} else {
|
|
575
|
+
console.log('\n' + t("complete.runWithoutDryRun"));
|
|
576
|
+
}
|
|
552
577
|
|
|
553
578
|
// Only prompt when run from the menu (i.e., when a callback or menu context is present)
|
|
554
579
|
if (typeof this.prompt === "function" && args.fromMenu) {
|
|
@@ -580,4 +605,4 @@ if (require.main === module) {
|
|
|
580
605
|
});
|
|
581
606
|
}
|
|
582
607
|
|
|
583
|
-
module.exports = I18nCompletionTool;
|
|
608
|
+
module.exports = I18nCompletionTool;
|
package/main/i18ntk-doctor.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
const SecurityUtils = require('../utils/security');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/config-helper');
|
|
5
6
|
const SetupEnforcer = require('../utils/setup-enforcer');
|
|
6
7
|
|
|
7
|
-
// Ensure setup is complete before running
|
|
8
|
-
(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
8
|
+
// Ensure setup is complete before running (only for standalone execution)
|
|
9
|
+
if (require.main === module) {
|
|
10
|
+
(async () => {
|
|
11
|
+
try {
|
|
12
|
+
await SetupEnforcer.checkSetupCompleteAsync();
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error('Setup check failed:', error.message);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
18
|
+
}
|
|
16
19
|
|
|
17
20
|
const ExitCodes = require('../utils/exit-codes');
|
|
18
21
|
|
|
@@ -50,115 +53,132 @@ function compareTypes(src, tgt, prefix = '', issues = []) {
|
|
|
50
53
|
return issues;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const dirs = {
|
|
61
|
-
projectRoot: config.projectRoot,
|
|
62
|
-
sourceDir: config.sourceDir,
|
|
63
|
-
i18nDir: config.i18nDir,
|
|
64
|
-
outputDir: config.outputDir,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
let exitCode = ExitCodes.SUCCESS;
|
|
68
|
-
const issues = [];
|
|
69
|
-
|
|
70
|
-
console.log('i18ntk doctor');
|
|
71
|
-
for (const [name, dir] of Object.entries(dirs)) {
|
|
72
|
-
const rel = path.relative(config.projectRoot, dir);
|
|
73
|
-
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
74
|
-
issues.push(`path traversal detected: ${dir}`);
|
|
75
|
-
exitCode = Math.max(exitCode, ExitCodes.SECURITY_VIOLATION);
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
const exists = fs.existsSync(dir);
|
|
79
|
-
console.log(`${name}: ${dir} ${exists ? '✅' : '❌'}`);
|
|
80
|
-
if (!exists) {
|
|
81
|
-
if (name !== 'outputDir') {
|
|
82
|
-
issues.push(`Missing directory: ${dir}`);
|
|
83
|
-
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
84
|
-
}
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
try {
|
|
88
|
-
fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
|
|
89
|
-
} catch (e) {
|
|
90
|
-
issues.push(`Permission issue: ${dir}`);
|
|
91
|
-
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
56
|
+
// Export the I18nDoctor class for module usage
|
|
57
|
+
class I18nDoctor {
|
|
58
|
+
async run(options = {}) {
|
|
59
|
+
const args = parseCommonArgs(process.argv.slice(2));
|
|
60
|
+
if (args.help) {
|
|
61
|
+
displayHelp('i18ntk-doctor');
|
|
62
|
+
process.exit(0);
|
|
92
63
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
64
|
+
const config = await getUnifiedConfig('doctor', args);
|
|
65
|
+
const dirs = {
|
|
66
|
+
projectRoot: config.projectRoot,
|
|
67
|
+
sourceDir: config.sourceDir,
|
|
68
|
+
i18nDir: config.i18nDir,
|
|
69
|
+
outputDir: config.outputDir,
|
|
70
|
+
};
|
|
100
71
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const srcDir = path.join(config.i18nDir, sourceLang);
|
|
104
|
-
const srcFiles = fs.existsSync(srcDir) ? fs.readdirSync(srcDir).filter(f => f.endsWith('.json')) : [];
|
|
72
|
+
let exitCode = ExitCodes.SUCCESS;
|
|
73
|
+
const issues = [];
|
|
105
74
|
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const files = fs.readdirSync(langDir).filter(f => f.endsWith('.json'));
|
|
114
|
-
for (const file of files) {
|
|
115
|
-
if (!srcFiles.includes(file)) {
|
|
116
|
-
issues.push(`Dangling namespace file: ${lang}/${file}`);
|
|
117
|
-
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
75
|
+
console.log('i18ntk doctor');
|
|
76
|
+
for (const [name, dir] of Object.entries(dirs)) {
|
|
77
|
+
const rel = path.relative(config.projectRoot, dir);
|
|
78
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
79
|
+
issues.push(`path traversal detected: ${dir}`);
|
|
80
|
+
exitCode = Math.max(exitCode, ExitCodes.SECURITY_VIOLATION);
|
|
81
|
+
continue;
|
|
118
82
|
}
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
if (!
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
83
|
+
const exists = SecurityUtils.safeExistsSync(dir);
|
|
84
|
+
console.log(`${name}: ${dir} ${exists ? '✅' : '❌'}`);
|
|
85
|
+
if (!exists) {
|
|
86
|
+
if (name !== 'outputDir') {
|
|
87
|
+
issues.push(`Missing directory: ${dir}`);
|
|
88
|
+
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
127
91
|
}
|
|
128
|
-
let srcJson, tgtJson;
|
|
129
92
|
try {
|
|
130
|
-
|
|
93
|
+
fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
|
|
131
94
|
} catch (e) {
|
|
132
|
-
issues.push(`
|
|
95
|
+
issues.push(`Permission issue: ${dir}`);
|
|
133
96
|
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
134
|
-
continue;
|
|
135
97
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const pkgVersion = require('../package.json').version;
|
|
101
|
+
if (config.version && config.version !== pkgVersion) {
|
|
102
|
+
issues.push(`Config version mismatch: ${config.version} != ${pkgVersion}`);
|
|
103
|
+
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const sourceLang = config.sourceLanguage || 'en';
|
|
107
|
+
const languages = config.defaultLanguages || [];
|
|
108
|
+
const srcDir = path.join(config.i18nDir, sourceLang);
|
|
109
|
+
const srcFiles = SecurityUtils.safeExistsSync(srcDir) ? fs.readdirSync(srcDir).filter(f => f.endsWith('.json')) : [];
|
|
110
|
+
|
|
111
|
+
for (const lang of languages) {
|
|
112
|
+
const langDir = path.join(config.i18nDir, lang);
|
|
113
|
+
if (!SecurityUtils.safeExistsSync(langDir)) {
|
|
114
|
+
issues.push(`Missing locale directory: ${lang}`);
|
|
140
115
|
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
141
116
|
continue;
|
|
142
117
|
}
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
118
|
+
const files = fs.readdirSync(langDir).filter(f => f.endsWith('.json'));
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
if (!srcFiles.includes(file)) {
|
|
121
|
+
issues.push(`Dangling namespace file: ${lang}/${file}`);
|
|
122
|
+
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
123
|
+
}
|
|
124
|
+
const srcPath = path.join(srcDir, file);
|
|
125
|
+
const tgtPath = path.join(langDir, file);
|
|
126
|
+
if (!SecurityUtils.safeExistsSync(srcPath) || !SecurityUtils.safeExistsSync(tgtPath)) continue;
|
|
127
|
+
const srcContent = SecurityUtils.safeReadFileSync(srcPath, path.dirname(srcPath), 'utf8');
|
|
128
|
+
const tgtContent = SecurityUtils.safeReadFileSync(tgtPath, path.dirname(tgtPath), 'utf8');
|
|
129
|
+
if (hasBOM(srcContent) || hasBOM(tgtContent)) {
|
|
130
|
+
issues.push(`BOM detected in ${lang}/${file}`);
|
|
131
|
+
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
132
|
+
}
|
|
133
|
+
let srcJson, tgtJson;
|
|
134
|
+
try {
|
|
135
|
+
srcJson = JSON.parse(srcContent.replace(/^\uFEFF/, ''));
|
|
136
|
+
} catch (e) {
|
|
137
|
+
issues.push(`Invalid JSON in source ${file}: ${e.message}`);
|
|
148
138
|
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
139
|
+
continue;
|
|
149
140
|
}
|
|
141
|
+
try {
|
|
142
|
+
tgtJson = JSON.parse(tgtContent.replace(/^\uFEFF/, ''));
|
|
143
|
+
} catch (e) {
|
|
144
|
+
issues.push(`Invalid JSON in ${lang}/${file}: ${e.message}`);
|
|
145
|
+
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const srcPlurals = collectPluralKeys(srcJson);
|
|
149
|
+
const tgtPlurals = collectPluralKeys(tgtJson);
|
|
150
|
+
for (const key of srcPlurals) {
|
|
151
|
+
if (!tgtPlurals.has(key)) {
|
|
152
|
+
issues.push(`Inconsistent plural forms in ${lang}/${file}: missing ${key}`);
|
|
153
|
+
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const typeMismatches = compareTypes(srcJson, tgtJson);
|
|
157
|
+
typeMismatches.forEach(k => {
|
|
158
|
+
issues.push(`Type mismatch for key ${k} in ${lang}/${file}`);
|
|
159
|
+
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
160
|
+
});
|
|
150
161
|
}
|
|
151
|
-
const typeMismatches = compareTypes(srcJson, tgtJson);
|
|
152
|
-
typeMismatches.forEach(k => {
|
|
153
|
-
issues.push(`Type mismatch for key ${k} in ${lang}/${file}`);
|
|
154
|
-
exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
|
|
155
|
-
});
|
|
156
162
|
}
|
|
157
|
-
}
|
|
158
163
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
if (issues.length > 0) {
|
|
165
|
+
console.log('\nIssues found:');
|
|
166
|
+
issues.forEach(i => console.log(` - ${i}`));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Return the result instead of exiting for modular usage
|
|
170
|
+
if (require.main === module) {
|
|
171
|
+
process.exit(exitCode);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { success: exitCode === ExitCodes.SUCCESS, issues, exitCode };
|
|
162
175
|
}
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Only run as standalone script if called directly
|
|
179
|
+
if (require.main === module) {
|
|
180
|
+
const doctor = new I18nDoctor();
|
|
181
|
+
doctor.run();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = I18nDoctor;
|