i18ntk 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +100 -4
  2. package/main/i18ntk-analyze.js +78 -15
  3. package/main/i18ntk-complete.js +0 -1
  4. package/main/i18ntk-doctor.js +137 -3
  5. package/main/i18ntk-fixer.js +4 -0
  6. package/main/i18ntk-init.js +71 -61
  7. package/main/i18ntk-manage.js +65 -30
  8. package/main/i18ntk-sizing.js +175 -63
  9. package/main/i18ntk-ui.js +1 -1
  10. package/main/i18ntk-usage.js +20 -54
  11. package/main/i18ntk-validate.js +197 -54
  12. package/package.json +11 -5
  13. package/scripts/build-lite.js +0 -1
  14. package/scripts/locale-optimizer.js +201 -27
  15. package/scripts/security-check.js +109 -0
  16. package/scripts/sync-ui-locales.js +19 -0
  17. package/settings/.i18n-admin-config.json +2 -2
  18. package/settings/i18ntk-config.json +127 -1
  19. package/settings/settings-cli.js +5 -11
  20. package/settings/settings-manager.js +314 -574
  21. package/ui-locales/de.json +7 -1
  22. package/ui-locales/en.json +8 -1
  23. package/ui-locales/es.json +8 -2
  24. package/ui-locales/fr.json +7 -1
  25. package/ui-locales/ja.json +7 -1
  26. package/ui-locales/ru.json +7 -1
  27. package/ui-locales/zh.json +7 -1
  28. package/utils/admin-auth.js +3 -2
  29. package/utils/cli.js +14 -0
  30. package/utils/config-helper.js +109 -41
  31. package/utils/config-manager.js +2 -2
  32. package/utils/exit-codes.js +6 -0
  33. package/utils/extractor-manager.js +14 -0
  34. package/utils/extractors/regex.js +19 -0
  35. package/utils/format-manager.js +35 -0
  36. package/utils/formats/json.js +11 -0
  37. package/utils/framework-detector.js +51 -0
  38. package/utils/i18n-helper.js +5 -1
  39. package/utils/json-output.js +99 -0
  40. package/utils/plugin-loader.js +31 -0
  41. package/utils/prompt-helper.js +41 -0
  42. package/utils/security.js +6 -2
  43. package/scripts/update-checker.js +0 -225
package/README.md CHANGED
@@ -2,17 +2,25 @@
2
2
 
3
3
  ![i18ntk Logo](docs/screenshots/i18ntk-logo-public.PNG)
4
4
 
5
- **Version:** 1.8.0
5
+ **Version:** 1.8.1
6
6
  **Last Updated:** 2025-08-11
7
7
  **GitHub Repository:** [vladnoskv/i18ntk](https://github.com/vladnoskv/i18ntk)
8
8
 
9
- [![npm](https://img.shields.io/npm/dt/i18ntk.svg)](https://www.npmjs.com/package/i18ntk) [![npm version](https://badge.fury.io/js/i18ntk.svg)](https://badge.fury.io/js/i18ntk) [![Node.js Version](https://img.shields.io/badge/node-%3E%3D16.0.0-brightgreen.svg)](https://nodejs.org/) [![Downloads](https://img.shields.io/npm/dm/i18ntk.svg)](https://www.npmjs.com/package/i18ntk) [![Socket Badge](https://socket.dev/api/badge/npm/package/i18ntk/1.8.0)](https://socket.dev/npm/package/i18ntk/overview/1.8.0) [![GitHub stars](https://img.shields.io/github/stars/vladnoskv/i18ntk?style=social)](https://github.com/vladnoskv/i18ntk)
9
+ [![npm](https://img.shields.io/npm/dt/i18ntk.svg)](https://www.npmjs.com/package/i18ntk) [![npm version](https://badge.fury.io/js/i18ntk.svg)](https://badge.fury.io/js/i18ntk) [![Node.js Version](https://img.shields.io/badge/node-%3E%3D16.0.0-brightgreen.svg)](https://nodejs.org/) [![Downloads](https://img.shields.io/npm/dm/i18ntk.svg)](https://www.npmjs.com/package/i18ntk) [![GitHub stars](https://img.shields.io/github/stars/vladnoskv/i18ntk?style=social)](https://github.com/vladnoskv/i18ntk) [![Socket Badge](https://socket.dev/api/badge/npm/package/i18ntk/1.8.1)](https://socket.dev/npm/package/i18ntk/overview/1.8.1)
10
10
 
11
11
  **🚀 The fastest way to manage translations across any framework or vanilla JavaScript projects**
12
12
 
13
13
  **Framework Support:** Auto-detects popular libraries (React i18next, Vue i18n, i18next, Nuxt i18n, Svelte i18n) or works without a framework. i18ntk manages translation files and validation—it does NOT implement translation logic like i18next or Vue i18n.
14
14
 
15
- > **v1.8.0** – **SAFER WORKFLOW** - Autorun workflow removed for enhanced safety. Enhanced Interactive Translation Fixer Tool with improved automatic detection, selective language/file fixing, mass fix capabilities, and 7-language UI support; enhanced security logging, flexible 4-6 digit PIN authentication, configuration stability improvements, and CI/CD silent mode support; maintains 97% speed improvement.
15
+
16
+ With over **2,000 downloads**, we thank you for your patience. I am proud to release version **1.8.1** with **1.8.0** marking our first fully stable release. Expect fewer updates as the core toolkit has matured. Future efforts may explore optional translation runtime features and a companion web UI for AI-assisted translations while keeping this package dependency-free.
17
+
18
+ > **v1.8.1** – A major release delivering:
19
+ > - 🔍 **Smarter Framework Detection**: Automatically identifies and configures for popular i18n frameworks
20
+ > - 🔌 **Extensible Plugin System**: Powerful architecture for custom extractors and formats
21
+ > - 🔒 **Enhanced Security**: Advanced protection with PIN authentication and AES encryption
22
+ > - ⚡ **Performance Boost**: Up to 97% faster processing with optimized algorithms
23
+ > - 🌐 **Multi-Framework Support**: Seamless integration with React, Vue, Svelte, and more
16
24
 
17
25
  ## 🚀 Quick Start
18
26
 
@@ -27,6 +35,12 @@ npx i18ntk
27
35
  i18ntk analyze --source ./src
28
36
  i18ntk complete --source ./src
29
37
  i18ntk validate --source ./locales
38
+
39
+ # Framework detection
40
+ i18ntk analyze --detect-framework
41
+
42
+ # JSON output for CI/CD
43
+ i18ntk validate --format json
30
44
  ```
31
45
 
32
46
  ---
@@ -56,6 +70,58 @@ i18ntk validate --source ./locales
56
70
  - **Framework‑agnostic:** Works with React, Vue, Svelte, Nuxt, i18next, or plain JSON.
57
71
  - **Scale:** Linear scaling up to 5M keys/second with ultra‑extreme settings.
58
72
  - **Script-by-Script Safety:** Manual execution ensures proper setup before each operation.
73
+ - **Framework fingerprints:** Auto-detects i18next, Lingui, and FormatJS projects to apply sensible defaults.
74
+ - **Plugin architecture:** Optional extractor and format adapters enable AST parsing or YAML/ICU support without extra deps.
75
+
76
+ ## 🎯 Features
77
+
78
+ ### 🔍 **Smart Analysis**
79
+ - **Multi-format Support**: JSON, YAML, and JavaScript translation files
80
+ - **Key Usage Tracking**: Identify unused and missing translation keys
81
+ - **Placeholder Validation**: Ensure consistent placeholder usage
82
+ - **Cross-reference Checking**: Verify translation completeness across languages
83
+
84
+ ### 🎯 **Enhanced Framework Detection (NEW in 1.8.1)**
85
+ - **Smart Framework Detection**: Automatically detects i18next, Lingui, and FormatJS
86
+ - **Package.json Analysis**: Quick detection via dependency analysis
87
+ - **Framework-specific Rules**: Tailored validation for each framework
88
+ - **Enhanced Doctor Tool**: Framework-aware analysis and recommendations
89
+
90
+ ### 🔌 **Plugin System (NEW in 1.8.1)**
91
+ - **Plugin Loader Architecture**: Extensible plugin system with PluginLoader and FormatManager
92
+ - **Custom Extractors**: Support for custom translation extractors
93
+ - **Format Managers**: Unified handling of different translation formats
94
+ - **Easy Extension**: Simple API for adding new plugins and formats
95
+
96
+ ### ⚡ **Performance Optimized**
97
+ - **Ultra-fast Processing**: Handle 200,000+ translation keys in milliseconds
98
+ - **87% Performance Boost**: Extreme mode achieves 38.90ms for 200k keys
99
+ - **Memory Efficient**: <1MB memory usage for any operation
100
+ - **Caching System**: Intelligent caching for repeated operations
101
+ - **Streaming Processing**: Handle large files without memory issues
102
+ - **No Child Processes**: Removed child_process usage for better performance
103
+
104
+ ### 🔒 **Security First (Enhanced in 1.8.1)**
105
+ - **Admin PIN Protection**: Secure sensitive operations with PIN authentication
106
+ - **Command-line PIN**: Support for `--admin-pin` argument in non-interactive mode
107
+ - **Standardized Exit Codes**: Consistent exit codes across all CLI commands
108
+ - **Path Validation**: Prevent directory traversal attacks
109
+ - **Input Sanitization**: Enhanced input validation and sanitization
110
+ - **Security Feature Tests**: Comprehensive security testing suite
111
+
112
+ ### 🌍 **Multi-language Support**
113
+ - **7 Languages**: English, Spanish, French, German, Japanese, Russian, Chinese
114
+ - **UI Localization**: Full interface localization
115
+ - **Context-aware**: Smart language detection and switching
116
+ - **RTL Support**: Right-to-left language support
117
+
118
+ ### 🛠️ **Developer Tools**
119
+ - **Interactive CLI**: Beautiful, user-friendly command-line interface
120
+ - **Auto-completion**: Smart suggestions for commands and keys
121
+ - **Progress Tracking**: Real-time progress bars for long operations
122
+ - **Export Tools**: Generate reports in multiple formats
123
+ - **JSON Output**: Machine-readable JSON output for CI/CD integration
124
+ - **Config Directory**: Support for `--config-dir` standalone configurations
59
125
 
60
126
  ---
61
127
 
@@ -116,6 +182,17 @@ i18ntk validate --source ./locales
116
182
 
117
183
  ---
118
184
 
185
+ ## Exit Codes
186
+
187
+ | Code | Meaning |
188
+ | ---- | ------- |
189
+ | 0 | Success |
190
+ | 1 | Handled configuration error |
191
+ | 2 | Validation failed |
192
+ | 3 | Security violation |
193
+
194
+ ---
195
+
119
196
  ## 🔒 Safer Workflow (NEW in v1.8.0)
120
197
 
121
198
  **Enhanced security through manual script execution:**
@@ -166,6 +243,16 @@ Create `settings/i18ntk-config.json` (auto‑generated by `init`):
166
243
  }
167
244
  ```
168
245
 
246
+ Define per-language placeholder patterns with `placeholderStyles` to ensure placeholder parity across translations:
247
+
248
+ ```json
249
+ "placeholderStyles": {
250
+ "en": ["\\{\\{[^}]+\\}\\}"],
251
+ "de": ["%s"],
252
+ "fr": ["{\\d+}"]
253
+ }
254
+ ```
255
+
169
256
  ### Environment Variables
170
257
 
171
258
  You can override paths with environment variables:
@@ -294,7 +381,16 @@ your-project/
294
381
  - Locale files are **auto‑backed up** before optimization.
295
382
  - Prefer the **interactive optimizer** for safe locale management.
296
383
  - Versions **prior to 1.7.1** are deprecated.
297
- - Upgrades apply improvements automatically; no migration steps required for 1.7.5.
384
+ - Upgrades apply improvements automatically; no migration steps required for 1.8.1.
385
+
386
+ ---
387
+
388
+ ## 🧭 Future Plans
389
+
390
+ - Investigate adding optional translation runtime logic to make i18ntk an all-in-one solution for any framework.
391
+ - Explore a lightweight web-based companion for AI-assisted translations with a clean UI.
392
+ - Maintain a zero-dependency core package while keeping future extensions optional.
393
+ - Fewer, stability-focused releases as we refine long-term direction.
298
394
 
299
395
  ---
300
396
 
@@ -16,6 +16,7 @@ const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/con
16
16
  const SecurityUtils = require('../utils/security');
17
17
  const AdminCLI = require('../utils/admin-cli');
18
18
  const watchLocales = require('../utils/watch-locales');
19
+ const JsonOutput = require('../utils/json-output');
19
20
 
20
21
  const PROJECT_ROOT = process.cwd();
21
22
 
@@ -100,6 +101,14 @@ class I18nAnalyzer {
100
101
  parsed.disableAdmin = true;
101
102
  } else if (sanitizedKey === 'admin-status') {
102
103
  parsed.adminStatus = true;
104
+ } else if (sanitizedKey === 'json') {
105
+ parsed.json = true;
106
+ } else if (sanitizedKey === 'sort-keys') {
107
+ parsed.sortKeys = true;
108
+ } else if (sanitizedKey === 'indent') {
109
+ parsed.indent = parseInt(value) || 2;
110
+ } else if (sanitizedKey === 'newline') {
111
+ parsed.newline = value || 'lf';
103
112
  }
104
113
  }
105
114
  });
@@ -500,12 +509,16 @@ try {
500
509
  // Main analyze method
501
510
  async analyze() {
502
511
  try {
503
- const results = []; // Add this line to declare the results array
504
-
505
- console.log(t('analyze.starting') || '🔍 Starting translation analysis...');
506
- console.log(t('analyze.sourceDirectoryLabel', { sourceDir: path.resolve(this.sourceDir) }));
507
- console.log(t('analyze.sourceLanguageLabel', { sourceLanguage: this.config.sourceLanguage }));
508
- console.log(t('analyze.strictModeLabel', { mode: this.config.processing?.strictMode || this.config.strictMode ? 'ON' : 'OFF' }));
512
+ const results = [];
513
+ const args = this.parseArgs();
514
+ const jsonOutput = new JsonOutput('analyze');
515
+
516
+ if (!args.json) {
517
+ console.log(t('analyze.starting') || '🔍 Starting translation analysis...');
518
+ console.log(t('analyze.sourceDirectoryLabel', { sourceDir: path.resolve(this.sourceDir) }));
519
+ console.log(t('analyze.sourceLanguageLabel', { sourceLanguage: this.config.sourceLanguage }));
520
+ console.log(t('analyze.strictModeLabel', { mode: this.config.processing?.strictMode || this.config.strictMode ? 'ON' : 'OFF' }));
521
+ }
509
522
 
510
523
  // Ensure output directory exists
511
524
  if (!fs.existsSync(this.outputDir)) {
@@ -515,14 +528,28 @@ try {
515
528
  const languages = this.getAvailableLanguages();
516
529
 
517
530
  if (languages.length === 0) {
518
- console.log(t('analyze.noLanguages') || '⚠️ No target languages found.');
531
+ const error = t('analyze.noLanguages') || '⚠️ No target languages found.';
532
+ if (args.json) {
533
+ jsonOutput.setStatus('error', error);
534
+ console.log(JSON.stringify(jsonOutput.getOutput(), null, args.indent || 2));
535
+ return;
536
+ }
537
+ console.log(error);
519
538
  return;
520
539
  }
521
540
 
522
- console.log(t('analyze.foundLanguages', { count: languages.length, languages: languages.join(', ') }) || `📋 Found ${languages.length} languages to analyze: ${languages.join(', ')}`);
541
+ if (!args.json) {
542
+ console.log(t('analyze.foundLanguages', { count: languages.length, languages: languages.join(', ') }) || `📋 Found ${languages.length} languages to analyze: ${languages.join(', ')}`);
543
+ }
544
+
545
+ let totalMissing = 0;
546
+ let totalExtra = 0;
547
+ let totalFiles = 0;
523
548
 
524
549
  for (const language of languages) {
525
- console.log(t('analyze.analyzing', { language }) || `\n🔄 Analyzing ${language}...`);
550
+ if (!args.json) {
551
+ console.log(t('analyze.analyzing', { language }) || `\n🔄 Analyzing ${language}...`);
552
+ }
526
553
 
527
554
  const analysis = this.analyzeLanguage(language);
528
555
  const report = this.generateLanguageReport(analysis);
@@ -530,18 +557,54 @@ try {
530
557
  // Save report
531
558
  const reportPath = await this.saveReport(language, report);
532
559
 
533
- console.log(t('analyze.completed', { language }) || `✅ Analysis completed for ${language}`);
534
- console.log(t('analyze.progress', {
535
- translated: results.length,
536
- total: languages.length
537
- }) || ` Progress: ${results.length}/${languages.length} languages processed`);
538
- console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
560
+ if (!args.json) {
561
+ console.log(t('analyze.completed', { language }) || `✅ Analysis completed for ${language}`);
562
+ console.log(t('analyze.progress', {
563
+ translated: results.length,
564
+ total: languages.length
565
+ }) || ` Progress: ${results.length}/${languages.length} languages processed`);
566
+ console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
567
+ }
539
568
 
540
569
  results.push({
541
570
  language,
542
571
  analysis,
543
572
  reportPath
544
573
  });
574
+
575
+ // Add issues to JSON output
576
+ Object.values(analysis.files).forEach(fileData => {
577
+ if (fileData.structural) {
578
+ fileData.structural.missingKeys?.forEach(key => {
579
+ jsonOutput.addIssue('missing', key, language);
580
+ totalMissing++;
581
+ });
582
+ fileData.structural.extraKeys?.forEach(key => {
583
+ jsonOutput.addIssue('extra', key, language);
584
+ totalExtra++;
585
+ });
586
+ }
587
+ });
588
+ totalFiles += analysis.summary.analyzedFiles;
589
+ }
590
+
591
+ // Set JSON output
592
+ jsonOutput.setStats({
593
+ missing: totalMissing,
594
+ extra: totalExtra,
595
+ files: totalFiles,
596
+ languages: languages.length
597
+ });
598
+
599
+ if (totalMissing > 0 || totalExtra > 0) {
600
+ jsonOutput.setStatus('warn');
601
+ } else {
602
+ jsonOutput.setStatus('ok');
603
+ }
604
+
605
+ if (args.json) {
606
+ console.log(JSON.stringify(jsonOutput.getOutput(), null, args.indent || 2));
607
+ return results;
545
608
  }
546
609
 
547
610
  // Summary
@@ -13,7 +13,6 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
- const { execSync } = require('child_process');
17
16
  const SecurityUtils = require('../utils/security');
18
17
  const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/config-helper');
19
18
  const { loadTranslations, t } = require('../utils/i18n-helper');
@@ -1,19 +1,153 @@
1
1
  #!/usr/bin/env node
2
2
  const fs = require('fs');
3
- const { getUnifiedConfig } = require('../utils/config-helper');
3
+ const path = require('path');
4
+ const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/config-helper');
5
+
6
+ const ExitCodes = require('../utils/exit-codes');
7
+
8
+ function hasBOM(content) {
9
+ return content.charCodeAt(0) === 0xFEFF;
10
+ }
11
+
12
+ function collectPluralKeys(obj, prefix = '', set = new Set()) {
13
+ for (const [key, value] of Object.entries(obj || {})) {
14
+ const fullKey = prefix ? `${prefix}.${key}` : key;
15
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
16
+ const pluralForms = ['zero', 'one', 'two', 'few', 'many', 'other'];
17
+ const keys = Object.keys(value);
18
+ if (keys.some(k => pluralForms.includes(k))) {
19
+ set.add(fullKey);
20
+ }
21
+ collectPluralKeys(value, fullKey, set);
22
+ }
23
+ }
24
+ return set;
25
+ }
26
+
27
+ function compareTypes(src, tgt, prefix = '', issues = []) {
28
+ for (const key of Object.keys(src)) {
29
+ const fullKey = prefix ? `${prefix}.${key}` : key;
30
+ if (!(key in tgt)) continue;
31
+ const sVal = src[key];
32
+ const tVal = tgt[key];
33
+ if (sVal && typeof sVal === 'object' && !Array.isArray(sVal) && tVal && typeof tVal === 'object' && !Array.isArray(tVal)) {
34
+ compareTypes(sVal, tVal, fullKey, issues);
35
+ } else if (typeof sVal !== typeof tVal) {
36
+ issues.push(fullKey);
37
+ }
38
+ }
39
+ return issues;
40
+ }
4
41
 
5
42
  (async () => {
6
- const config = await getUnifiedConfig('doctor');
43
+ const args = parseCommonArgs(process.argv.slice(2));
44
+ if (args.help) {
45
+ displayHelp('i18ntk-doctor');
46
+ process.exit(0);
47
+ }
48
+ const config = await getUnifiedConfig('doctor', args);
7
49
  const dirs = {
8
50
  projectRoot: config.projectRoot,
9
51
  sourceDir: config.sourceDir,
10
52
  i18nDir: config.i18nDir,
11
53
  outputDir: config.outputDir,
12
54
  };
55
+
56
+ let exitCode = ExitCodes.SUCCESS;
57
+ const issues = [];
13
58
 
14
59
  console.log('i18ntk doctor');
15
60
  for (const [name, dir] of Object.entries(dirs)) {
61
+ const rel = path.relative(config.projectRoot, dir);
62
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
63
+ issues.push(`path traversal detected: ${dir}`);
64
+ exitCode = Math.max(exitCode, ExitCodes.SECURITY_VIOLATION);
65
+ continue;
66
+ }
16
67
  const exists = fs.existsSync(dir);
17
68
  console.log(`${name}: ${dir} ${exists ? '✅' : '❌'}`);
69
+ if (!exists) {
70
+ if (name !== 'outputDir') {
71
+ issues.push(`Missing directory: ${dir}`);
72
+ exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
73
+ }
74
+ continue;
75
+ }
76
+ try {
77
+ fs.accessSync(dir, fs.constants.R_OK | fs.constants.W_OK);
78
+ } catch (e) {
79
+ issues.push(`Permission issue: ${dir}`);
80
+ exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
81
+ }
82
+ }
83
+
84
+ const pkgVersion = require('../package.json').version;
85
+ if (config.version && config.version !== pkgVersion) {
86
+ issues.push(`Config version mismatch: ${config.version} != ${pkgVersion}`);
87
+ exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
88
+ }
89
+
90
+ const sourceLang = config.sourceLanguage || 'en';
91
+ const languages = config.defaultLanguages || [];
92
+ const srcDir = path.join(config.i18nDir, sourceLang);
93
+ const srcFiles = fs.existsSync(srcDir) ? fs.readdirSync(srcDir).filter(f => f.endsWith('.json')) : [];
94
+
95
+ for (const lang of languages) {
96
+ const langDir = path.join(config.i18nDir, lang);
97
+ if (!fs.existsSync(langDir)) {
98
+ issues.push(`Missing locale directory: ${lang}`);
99
+ exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
100
+ continue;
101
+ }
102
+ const files = fs.readdirSync(langDir).filter(f => f.endsWith('.json'));
103
+ for (const file of files) {
104
+ if (!srcFiles.includes(file)) {
105
+ issues.push(`Dangling namespace file: ${lang}/${file}`);
106
+ exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
107
+ }
108
+ const srcPath = path.join(srcDir, file);
109
+ const tgtPath = path.join(langDir, file);
110
+ if (!fs.existsSync(srcPath) || !fs.existsSync(tgtPath)) continue;
111
+ const srcContent = fs.readFileSync(srcPath, 'utf8');
112
+ const tgtContent = fs.readFileSync(tgtPath, 'utf8');
113
+ if (hasBOM(srcContent) || hasBOM(tgtContent)) {
114
+ issues.push(`BOM detected in ${lang}/${file}`);
115
+ exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
116
+ }
117
+ let srcJson, tgtJson;
118
+ try {
119
+ srcJson = JSON.parse(srcContent.replace(/^\uFEFF/, ''));
120
+ } catch (e) {
121
+ issues.push(`Invalid JSON in source ${file}: ${e.message}`);
122
+ exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
123
+ continue;
124
+ }
125
+ try {
126
+ tgtJson = JSON.parse(tgtContent.replace(/^\uFEFF/, ''));
127
+ } catch (e) {
128
+ issues.push(`Invalid JSON in ${lang}/${file}: ${e.message}`);
129
+ exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
130
+ continue;
131
+ }
132
+ const srcPlurals = collectPluralKeys(srcJson);
133
+ const tgtPlurals = collectPluralKeys(tgtJson);
134
+ for (const key of srcPlurals) {
135
+ if (!tgtPlurals.has(key)) {
136
+ issues.push(`Inconsistent plural forms in ${lang}/${file}: missing ${key}`);
137
+ exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
138
+ }
139
+ }
140
+ const typeMismatches = compareTypes(srcJson, tgtJson);
141
+ typeMismatches.forEach(k => {
142
+ issues.push(`Type mismatch for key ${k} in ${lang}/${file}`);
143
+ exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
144
+ });
145
+ }
146
+ }
147
+
148
+ if (issues.length > 0) {
149
+ console.log('\nIssues found:');
150
+ issues.forEach(i => console.log(` - ${i}`));
18
151
  }
19
- })();
152
+ process.exit(exitCode);
153
+ })();
@@ -631,6 +631,10 @@ class I18nFixer {
631
631
  } finally {
632
632
  // Ensure readline is properly closed to prevent hanging
633
633
  closeGlobalReadline();
634
+ // Ensure process exits cleanly
635
+ if (require.main === module) {
636
+ process.exit(0);
637
+ }
634
638
  }
635
639
  }
636
640
  }