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.
Files changed (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1191
  3. package/main/i18ntk-analyze.js +65 -84
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +3 -3
  6. package/main/i18ntk-complete.js +90 -65
  7. package/main/i18ntk-doctor.js +123 -103
  8. package/main/i18ntk-fixer.js +61 -725
  9. package/main/i18ntk-go.js +14 -15
  10. package/main/i18ntk-init.js +77 -26
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +129 -30
  14. package/main/i18ntk-php.js +75 -75
  15. package/main/i18ntk-py.js +55 -56
  16. package/main/i18ntk-scanner.js +59 -57
  17. package/main/i18ntk-setup.js +9 -404
  18. package/main/i18ntk-sizing.js +6 -6
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +54 -18
  22. package/main/i18ntk-validate.js +13 -13
  23. package/main/manage/commands/AnalyzeCommand.js +1124 -0
  24. package/main/manage/commands/BackupCommand.js +62 -0
  25. package/main/manage/commands/CommandRouter.js +295 -0
  26. package/main/manage/commands/CompleteCommand.js +61 -0
  27. package/main/manage/commands/DoctorCommand.js +60 -0
  28. package/main/manage/commands/FixerCommand.js +624 -0
  29. package/main/manage/commands/InitCommand.js +62 -0
  30. package/main/manage/commands/ScannerCommand.js +654 -0
  31. package/main/manage/commands/SizingCommand.js +60 -0
  32. package/main/manage/commands/SummaryCommand.js +61 -0
  33. package/main/manage/commands/UsageCommand.js +60 -0
  34. package/main/manage/commands/ValidateCommand.js +978 -0
  35. package/main/manage/index-fixed.js +1447 -0
  36. package/main/manage/index.js +1462 -0
  37. package/main/manage/managers/DebugMenu.js +140 -0
  38. package/main/manage/managers/InteractiveMenu.js +177 -0
  39. package/main/manage/managers/LanguageMenu.js +62 -0
  40. package/main/manage/managers/SettingsMenu.js +53 -0
  41. package/main/manage/services/AuthenticationService.js +263 -0
  42. package/main/manage/services/ConfigurationService-fixed.js +449 -0
  43. package/main/manage/services/ConfigurationService.js +449 -0
  44. package/main/manage/services/FileManagementService.js +368 -0
  45. package/main/manage/services/FrameworkDetectionService.js +458 -0
  46. package/main/manage/services/InitService.js +1051 -0
  47. package/main/manage/services/SetupService.js +462 -0
  48. package/main/manage/services/SummaryService.js +450 -0
  49. package/main/manage/services/UsageService.js +1502 -0
  50. package/package.json +32 -29
  51. package/runtime/enhanced.d.ts +221 -221
  52. package/runtime/index.d.ts +29 -29
  53. package/runtime/index.full.d.ts +331 -331
  54. package/runtime/index.js +7 -6
  55. package/scripts/build-lite.js +17 -17
  56. package/scripts/deprecate-versions.js +23 -6
  57. package/scripts/export-translations.js +5 -5
  58. package/scripts/fix-all-i18n.js +3 -3
  59. package/scripts/fix-and-purify-i18n.js +3 -2
  60. package/scripts/fix-locale-control-chars.js +110 -0
  61. package/scripts/lint-locales.js +80 -0
  62. package/scripts/locale-optimizer.js +8 -8
  63. package/scripts/prepublish.js +21 -21
  64. package/scripts/security-check.js +117 -117
  65. package/scripts/sync-translations.js +4 -4
  66. package/scripts/sync-ui-locales.js +9 -8
  67. package/scripts/validate-all-translations.js +8 -7
  68. package/scripts/verify-deprecations.js +157 -161
  69. package/scripts/verify-translations.js +6 -5
  70. package/settings/i18ntk-config.json +282 -282
  71. package/settings/language-config.json +5 -5
  72. package/settings/settings-cli.js +9 -9
  73. package/settings/settings-manager.js +18 -18
  74. package/ui-locales/de.json +2417 -2348
  75. package/ui-locales/en.json +2415 -2352
  76. package/ui-locales/es.json +2425 -2353
  77. package/ui-locales/fr.json +2418 -2348
  78. package/ui-locales/ja.json +2463 -2361
  79. package/ui-locales/ru.json +2463 -2359
  80. package/ui-locales/zh.json +2418 -2351
  81. package/utils/admin-auth.js +2 -2
  82. package/utils/admin-cli.js +297 -297
  83. package/utils/admin-pin.js +9 -9
  84. package/utils/cli-helper.js +9 -9
  85. package/utils/config-helper.js +73 -104
  86. package/utils/config-manager.js +204 -171
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +249 -263
  89. package/utils/framework-detector.js +27 -24
  90. package/utils/i18n-helper.js +85 -41
  91. package/utils/init-helper.js +152 -94
  92. package/utils/json-output.js +98 -98
  93. package/utils/mini-commander.js +179 -0
  94. package/utils/missing-key-validator.js +5 -5
  95. package/utils/plugin-loader.js +40 -29
  96. package/utils/prompt.js +14 -44
  97. package/utils/safe-json.js +40 -0
  98. package/utils/secure-errors.js +3 -3
  99. package/utils/security-check-improved.js +390 -0
  100. package/utils/security-config.js +5 -5
  101. package/utils/security-fixed.js +607 -0
  102. package/utils/security.js +652 -602
  103. package/utils/setup-enforcer.js +136 -44
  104. package/utils/setup-validator.js +33 -32
  105. package/utils/ultra-performance-optimizer.js +11 -9
  106. package/utils/watch-locales.js +2 -1
  107. package/utils/prompt-fixed.js +0 -55
  108. package/utils/security-check.js +0 -454
@@ -11,7 +11,7 @@
11
11
  * node i18ntk-complete.js --source-dir=./src/i18n/locales
12
12
  */
13
13
 
14
- const fs = require('fs');
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
- try {
25
- await SetupEnforcer.checkSetupCompleteAsync();
26
- } catch (error) {
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(process.env.I18NTK_LANG);
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 (!fs.existsSync(this.sourceDir)) {
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 = fs.readdirSync(this.sourceDir);
125
- const languages = files
126
- .filter(file => file.endsWith('.json'))
127
- .map(file => path.basename(file, '.json'));
128
-
129
- // Also check for directory-based structure for backward compatibility
130
- const directories = fs.readdirSync(this.sourceDir)
131
- .filter(item => {
132
- const itemPath = path.join(this.sourceDir, item);
133
- return fs.statSync(itemPath).isDirectory();
134
- });
135
-
136
- return [...new Set([...languages, ...directories])];
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 (!fs.existsSync(languageDir)) {
168
+ if (!SecurityUtils.safeExistsSync(languageDir, this.config.projectRoot)) {
144
169
  return [];
145
170
  }
146
171
 
147
- return fs.readdirSync(languageDir)
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 (fs.existsSync(filePath)) {
262
+ if (SecurityUtils.safeExistsSync(filePath, this.config.projectRoot)) {
238
263
  try {
239
- fileContent = JSON.parse(fs.readFileSync(filePath, 'utf8'));
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 (!fs.existsSync(languageDir)) {
271
+ if (!SecurityUtils.safeExistsSync(languageDir, this.config.projectRoot)) {
247
272
  if (!dryRun) {
248
- fs.mkdirSync(languageDir, { recursive: true });
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
- fs.writeFileSync(filePath, JSON.stringify(fileContent, null, 2), 'utf8');
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 (!fs.existsSync(this.sourceLanguageDir)) {
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 = JSON.parse(fs.readFileSync(sourceFilePath, 'utf8'));
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 (fs.existsSync(targetFilePath)) {
379
+ if (SecurityUtils.safeExistsSync(targetFilePath, this.config.projectRoot)) {
355
380
  try {
356
- const targetContent = JSON.parse(fs.readFileSync(targetFilePath, 'utf8'));
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 (!fs.existsSync(reportsDir)) {
383
- fs.mkdirSync(reportsDir, { recursive: true });
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
- fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
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(t("complete.nextStep1"));
543
- console.log(' node i18ntk-usage.js --output-report');
544
- console.log(t("complete.nextStep2"));
545
- console.log(' node i18ntk-validate.js');
546
- console.log(t("complete.nextStep3"));
547
- console.log(' node i18ntk-analyze.js');
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;
@@ -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
- (async () => {
9
- try {
10
- await SetupEnforcer.checkSetupCompleteAsync();
11
- } catch (error) {
12
- console.error('Setup check failed:', error.message);
13
- process.exit(1);
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
- (async () => {
54
- const args = parseCommonArgs(process.argv.slice(2));
55
- if (args.help) {
56
- displayHelp('i18ntk-doctor');
57
- process.exit(0);
58
- }
59
- const config = await getUnifiedConfig('doctor', args);
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
- const pkgVersion = require('../package.json').version;
96
- if (config.version && config.version !== pkgVersion) {
97
- issues.push(`Config version mismatch: ${config.version} != ${pkgVersion}`);
98
- exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
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
- const sourceLang = config.sourceLanguage || 'en';
102
- const languages = config.defaultLanguages || [];
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
- for (const lang of languages) {
107
- const langDir = path.join(config.i18nDir, lang);
108
- if (!fs.existsSync(langDir)) {
109
- issues.push(`Missing locale directory: ${lang}`);
110
- exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
111
- continue;
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 srcPath = path.join(srcDir, file);
120
- const tgtPath = path.join(langDir, file);
121
- if (!fs.existsSync(srcPath) || !fs.existsSync(tgtPath)) continue;
122
- const srcContent = fs.readFileSync(srcPath, 'utf8');
123
- const tgtContent = fs.readFileSync(tgtPath, 'utf8');
124
- if (hasBOM(srcContent) || hasBOM(tgtContent)) {
125
- issues.push(`BOM detected in ${lang}/${file}`);
126
- exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
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
- srcJson = JSON.parse(srcContent.replace(/^\uFEFF/, ''));
93
+ fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
131
94
  } catch (e) {
132
- issues.push(`Invalid JSON in source ${file}: ${e.message}`);
95
+ issues.push(`Permission issue: ${dir}`);
133
96
  exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
134
- continue;
135
97
  }
136
- try {
137
- tgtJson = JSON.parse(tgtContent.replace(/^\uFEFF/, ''));
138
- } catch (e) {
139
- issues.push(`Invalid JSON in ${lang}/${file}: ${e.message}`);
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 srcPlurals = collectPluralKeys(srcJson);
144
- const tgtPlurals = collectPluralKeys(tgtJson);
145
- for (const key of srcPlurals) {
146
- if (!tgtPlurals.has(key)) {
147
- issues.push(`Inconsistent plural forms in ${lang}/${file}: missing ${key}`);
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
- if (issues.length > 0) {
160
- console.log('\nIssues found:');
161
- issues.forEach(i => console.log(` - ${i}`));
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
- process.exit(exitCode);
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;