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.
- package/README.md +100 -4
- package/main/i18ntk-analyze.js +78 -15
- package/main/i18ntk-complete.js +0 -1
- package/main/i18ntk-doctor.js +137 -3
- package/main/i18ntk-fixer.js +4 -0
- package/main/i18ntk-init.js +71 -61
- package/main/i18ntk-manage.js +65 -30
- package/main/i18ntk-sizing.js +175 -63
- package/main/i18ntk-ui.js +1 -1
- package/main/i18ntk-usage.js +20 -54
- package/main/i18ntk-validate.js +197 -54
- package/package.json +11 -5
- package/scripts/build-lite.js +0 -1
- package/scripts/locale-optimizer.js +201 -27
- package/scripts/security-check.js +109 -0
- package/scripts/sync-ui-locales.js +19 -0
- package/settings/.i18n-admin-config.json +2 -2
- package/settings/i18ntk-config.json +127 -1
- package/settings/settings-cli.js +5 -11
- package/settings/settings-manager.js +314 -574
- package/ui-locales/de.json +7 -1
- package/ui-locales/en.json +8 -1
- package/ui-locales/es.json +8 -2
- package/ui-locales/fr.json +7 -1
- package/ui-locales/ja.json +7 -1
- package/ui-locales/ru.json +7 -1
- package/ui-locales/zh.json +7 -1
- package/utils/admin-auth.js +3 -2
- package/utils/cli.js +14 -0
- package/utils/config-helper.js +109 -41
- package/utils/config-manager.js +2 -2
- package/utils/exit-codes.js +6 -0
- package/utils/extractor-manager.js +14 -0
- package/utils/extractors/regex.js +19 -0
- package/utils/format-manager.js +35 -0
- package/utils/formats/json.js +11 -0
- package/utils/framework-detector.js +51 -0
- package/utils/i18n-helper.js +5 -1
- package/utils/json-output.js +99 -0
- package/utils/plugin-loader.js +31 -0
- package/utils/prompt-helper.js +41 -0
- package/utils/security.js +6 -2
- package/scripts/update-checker.js +0 -225
package/README.md
CHANGED
|
@@ -2,17 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
**Version:** 1.8.
|
|
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
|
-
[](https://www.npmjs.com/package/i18ntk) [](https://badge.fury.io/js/i18ntk) [](https://nodejs.org/) [](https://www.npmjs.com/package/i18ntk) [](https://www.npmjs.com/package/i18ntk) [](https://badge.fury.io/js/i18ntk) [](https://nodejs.org/) [](https://www.npmjs.com/package/i18ntk) [](https://github.com/vladnoskv/i18ntk) [](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
|
-
|
|
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.
|
|
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
|
|
package/main/i18ntk-analyze.js
CHANGED
|
@@ -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 = [];
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
package/main/i18ntk-complete.js
CHANGED
|
@@ -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');
|
package/main/i18ntk-doctor.js
CHANGED
|
@@ -1,19 +1,153 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const fs = require('fs');
|
|
3
|
-
const
|
|
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
|
|
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
|
+
})();
|
package/main/i18ntk-fixer.js
CHANGED