i18ntk 2.0.4 ā 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -62
- package/main/i18ntk-analyze.js +59 -54
- package/main/i18ntk-ui.js +96 -49
- package/main/manage/commands/AnalyzeCommand.js +59 -54
- package/main/manage/index.js +140 -71
- package/package.json +290 -290
- package/scripts/fix-all-i18n.js +41 -20
- package/scripts/fix-and-purify-i18n.js +43 -23
- package/scripts/prepublish-dev.js +221 -0
- package/scripts/prepublish.js +155 -141
- package/scripts/validate-all-translations.js +190 -134
- package/ui-locales/de.json +149 -155
- package/ui-locales/en.json +1 -7
- package/ui-locales/es.json +159 -173
- package/ui-locales/fr.json +143 -150
- package/ui-locales/ja.json +181 -233
- package/ui-locales/ru.json +133 -185
- package/ui-locales/zh.json +168 -175
- package/utils/cli-helper.js +26 -98
- package/utils/extractors/regex.js +39 -12
- package/utils/i18n-helper.js +181 -128
- package/utils/security-check-improved.js +16 -13
- package/utils/security-fixed.js +6 -4
- package/utils/security.js +6 -4
- package/main/manage/services/ConfigurationService-fixed.js +0 -449
package/README.md
CHANGED
|
@@ -1,74 +1,59 @@
|
|
|
1
|
-
#
|
|
1
|
+
# i18ntk v2.1.0
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Zero-dependency i18n toolkit for initialization, scanning, analysis, validation, usage tracking, and translation completion.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
**The fastest and most comprehensive i18n toolkit ever built.**
|
|
8
|
-
|
|
9
7
|
[](https://www.npmjs.com/package/i18ntk)
|
|
10
|
-
[](https://opensource.org/licenses/MIT)
|
|
11
|
-
[](https://github.com/vladnoskv/i18ntk#performance)
|
|
12
|
-
[](https://github.com/vladnoskv/i18ntk#features)
|
|
13
8
|
[](https://www.npmjs.com/package/i18ntk)
|
|
14
|
-
[](https://nodejs.org)
|
|
10
|
+
[](https://www.npmjs.com/package/i18ntk)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](https://socket.dev/npm/package/i18ntk/overview/2.1.0)
|
|
24
13
|
|
|
25
|
-
|
|
14
|
+
## Why i18ntk
|
|
26
15
|
|
|
27
|
-
|
|
16
|
+
- Zero runtime dependencies
|
|
17
|
+
- Works across JS/TS, React, Vue, Angular, and generic projects
|
|
18
|
+
- Supports non-interactive CI runs (`--no-prompt`)
|
|
19
|
+
- Includes usage/coverage validation and missing-key completion
|
|
20
|
+
- Ships with runtime translation helpers via `i18ntk/runtime`
|
|
28
21
|
|
|
29
|
-
##
|
|
22
|
+
## Install
|
|
30
23
|
|
|
31
24
|
```bash
|
|
32
|
-
#
|
|
25
|
+
# global (recommended for CLI use)
|
|
33
26
|
npm install -g i18ntk
|
|
34
27
|
|
|
35
|
-
#
|
|
36
|
-
|
|
28
|
+
# local
|
|
29
|
+
npm install --save-dev i18ntk
|
|
37
30
|
|
|
38
|
-
#
|
|
39
|
-
|
|
31
|
+
# one-off
|
|
32
|
+
npx i18ntk --help
|
|
40
33
|
```
|
|
41
34
|
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
Get your i18n project up and running in **60 seconds**:
|
|
35
|
+
## Quick Start
|
|
45
36
|
|
|
46
37
|
```bash
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# 2. Initialize your project
|
|
51
|
-
i18ntk init
|
|
52
|
-
|
|
53
|
-
# 3. Analyze your translations
|
|
54
|
-
i18ntk analyze
|
|
38
|
+
# initialize locales/project settings
|
|
39
|
+
i18ntk --command=init
|
|
55
40
|
|
|
56
|
-
#
|
|
57
|
-
i18ntk
|
|
41
|
+
# analyze translation completeness
|
|
42
|
+
i18ntk --command=analyze
|
|
58
43
|
|
|
59
|
-
#
|
|
60
|
-
i18ntk validate
|
|
44
|
+
# validate translation structure/content
|
|
45
|
+
i18ntk --command=validate
|
|
61
46
|
|
|
62
|
-
#
|
|
47
|
+
# complete missing keys
|
|
48
|
+
i18ntk --command=complete
|
|
63
49
|
```
|
|
64
50
|
|
|
65
|
-
|
|
51
|
+
## Command Model (v2)
|
|
66
52
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Primary CLI commands:
|
|
53
|
+
Primary CLI:
|
|
70
54
|
|
|
71
55
|
```bash
|
|
56
|
+
i18ntk
|
|
72
57
|
i18ntk --command=init
|
|
73
58
|
i18ntk --command=analyze
|
|
74
59
|
i18ntk --command=validate
|
|
@@ -80,7 +65,7 @@ i18ntk --command=summary
|
|
|
80
65
|
i18ntk --command=debug
|
|
81
66
|
```
|
|
82
67
|
|
|
83
|
-
Standalone
|
|
68
|
+
Standalone executables:
|
|
84
69
|
|
|
85
70
|
```bash
|
|
86
71
|
i18ntk-init
|
|
@@ -96,19 +81,8 @@ i18ntk-fixer
|
|
|
96
81
|
i18ntk-backup
|
|
97
82
|
```
|
|
98
83
|
|
|
99
|
-
Backup helper:
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
i18ntk-backup --help
|
|
103
|
-
i18ntk-backup create ./locales
|
|
104
|
-
i18ntk-backup list
|
|
105
|
-
i18ntk-backup restore <backup-file>
|
|
106
|
-
```
|
|
107
|
-
|
|
108
84
|
## Common Flags
|
|
109
85
|
|
|
110
|
-
Most commands support:
|
|
111
|
-
|
|
112
86
|
- `--source-dir <path>`
|
|
113
87
|
- `--i18n-dir <path>`
|
|
114
88
|
- `--output-dir <path>`
|
|
@@ -120,13 +94,13 @@ Most commands support:
|
|
|
120
94
|
|
|
121
95
|
## Configuration
|
|
122
96
|
|
|
123
|
-
i18ntk reads project settings from `.i18ntk-config` in
|
|
97
|
+
i18ntk reads project settings from `.i18ntk-config` in your project root.
|
|
124
98
|
|
|
125
99
|
Example:
|
|
126
100
|
|
|
127
101
|
```json
|
|
128
102
|
{
|
|
129
|
-
"version": "2.
|
|
103
|
+
"version": "2.1.0",
|
|
130
104
|
"sourceDir": "./locales",
|
|
131
105
|
"i18nDir": "./locales",
|
|
132
106
|
"outputDir": "./i18ntk-reports",
|
|
@@ -155,7 +129,7 @@ setLanguage('fr');
|
|
|
155
129
|
console.log(getLanguage());
|
|
156
130
|
```
|
|
157
131
|
|
|
158
|
-
##
|
|
132
|
+
## Documentation
|
|
159
133
|
|
|
160
134
|
- [Documentation Index](docs/README.md)
|
|
161
135
|
- [API Reference](docs/api/API_REFERENCE.md)
|
|
@@ -163,9 +137,9 @@ console.log(getLanguage());
|
|
|
163
137
|
- [Runtime API Guide](docs/runtime.md)
|
|
164
138
|
- [Scanner Guide](docs/scanner-guide.md)
|
|
165
139
|
- [Environment Variables](docs/environment-variables.md)
|
|
166
|
-
- [Migration Guide v2.
|
|
140
|
+
- [Migration Guide v2.1.0](docs/migration-guide-v2.1.0.md)
|
|
167
141
|
- [Release Runbook](DEVUPDATE.md)
|
|
168
142
|
|
|
169
143
|
## License
|
|
170
144
|
|
|
171
|
-
MIT
|
|
145
|
+
MIT
|
package/main/i18ntk-analyze.js
CHANGED
|
@@ -142,22 +142,44 @@ class I18nAnalyzer {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Get all available languages
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
// Get all available languages
|
|
146
|
+
isValidLanguageCode(code) {
|
|
147
|
+
if (!code || typeof code !== 'string') return false;
|
|
148
|
+
return /^[a-z]{2}(?:-[A-Za-z0-9]{2,8})*$/i.test(code.trim());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
isExcludedLanguageDirectory(name) {
|
|
152
|
+
if (!name || typeof name !== 'string') return true;
|
|
153
|
+
const lowered = name.toLowerCase();
|
|
154
|
+
return lowered.startsWith('backup-') || lowered === 'backup' || lowered === 'reports' || lowered === 'i18ntk-reports';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getAvailableLanguages() {
|
|
158
|
+
try {
|
|
159
|
+
const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
|
|
160
|
+
if (!items) {
|
|
161
|
+
console.error('Error reading source directory: Unable to access directory');
|
|
151
162
|
return [];
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
const languages = [];
|
|
155
166
|
|
|
156
167
|
// Check for directory-based structure
|
|
157
|
-
const directories = items
|
|
158
|
-
.filter(item => item.isDirectory())
|
|
159
|
-
.map(item => item.name)
|
|
160
|
-
.filter(name =>
|
|
168
|
+
const directories = items
|
|
169
|
+
.filter(item => item.isDirectory())
|
|
170
|
+
.map(item => item.name)
|
|
171
|
+
.filter(name =>
|
|
172
|
+
name !== 'node_modules' &&
|
|
173
|
+
!name.startsWith('.') &&
|
|
174
|
+
name !== this.config.sourceLanguage &&
|
|
175
|
+
!this.isExcludedLanguageDirectory(name) &&
|
|
176
|
+
this.isValidLanguageCode(name)
|
|
177
|
+
)
|
|
178
|
+
.filter(name => {
|
|
179
|
+
const dirPath = path.join(this.sourceDir, name);
|
|
180
|
+
const dirItems = SecurityUtils.safeReaddirSync(dirPath, process.cwd(), { withFileTypes: true }) || [];
|
|
181
|
+
return dirItems.some(item => item.isFile() && item.name.endsWith('.json'));
|
|
182
|
+
});
|
|
161
183
|
|
|
162
184
|
// Check for monolith files (language.json files)
|
|
163
185
|
const files = items
|
|
@@ -168,34 +190,17 @@ class I18nAnalyzer {
|
|
|
168
190
|
languages.push(...directories);
|
|
169
191
|
|
|
170
192
|
// Add monolith files as languages (without .json extension)
|
|
171
|
-
const monolithLanguages = files
|
|
172
|
-
.map(file => file.replace('.json', ''))
|
|
173
|
-
.filter(lang =>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const jsonFiles = dirItems
|
|
183
|
-
.filter(item => item.isFile() && item.name.endsWith('.json'))
|
|
184
|
-
.map(item => item.name.replace('.json', ''));
|
|
185
|
-
|
|
186
|
-
// If directory contains JSON files, it's likely a language directory
|
|
187
|
-
if (jsonFiles.length > 0) {
|
|
188
|
-
if (!languages.includes(dir)) {
|
|
189
|
-
languages.push(dir);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} catch (error) {
|
|
194
|
-
// Skip directories we can't read
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return [...new Set(languages)].sort();
|
|
193
|
+
const monolithLanguages = files
|
|
194
|
+
.map(file => file.replace('.json', ''))
|
|
195
|
+
.filter(lang =>
|
|
196
|
+
!languages.includes(lang) &&
|
|
197
|
+
lang !== this.config.sourceLanguage &&
|
|
198
|
+
!this.isExcludedLanguageDirectory(lang) &&
|
|
199
|
+
this.isValidLanguageCode(lang)
|
|
200
|
+
);
|
|
201
|
+
languages.push(...monolithLanguages);
|
|
202
|
+
|
|
203
|
+
return [...new Set(languages)].sort();
|
|
199
204
|
} catch (error) {
|
|
200
205
|
console.error('Error reading source directory:', error.message);
|
|
201
206
|
return [];
|
|
@@ -707,8 +712,7 @@ try {
|
|
|
707
712
|
throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
|
|
708
713
|
}
|
|
709
714
|
|
|
710
|
-
|
|
711
|
-
return reportPath;
|
|
715
|
+
return reportPath;
|
|
712
716
|
|
|
713
717
|
} catch (error) {
|
|
714
718
|
console.error(`Failed to save report for ${language}:`, error.message);
|
|
@@ -775,20 +779,21 @@ try {
|
|
|
775
779
|
console.log(t('analyze.analyzing', { language }) || `\nš Analyzing ${language}...`);
|
|
776
780
|
}
|
|
777
781
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
782
|
+
const analysis = this.analyzeLanguage(language);
|
|
783
|
+
const report = this.generateLanguageReport(analysis);
|
|
784
|
+
|
|
785
|
+
// Save report
|
|
786
|
+
const reportPath = await this.saveReport(language, report);
|
|
787
|
+
const processedCount = results.length + 1;
|
|
788
|
+
|
|
789
|
+
if (!args.json) {
|
|
790
|
+
console.log(t('analyze.completed', { language }) || `ā
Analysis completed for ${language}`);
|
|
791
|
+
console.log(t('analyze.progress', {
|
|
792
|
+
translated: processedCount,
|
|
793
|
+
total: languages.length
|
|
794
|
+
}) || ` Progress: ${processedCount}/${languages.length} languages processed`);
|
|
795
|
+
console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
|
|
796
|
+
}
|
|
792
797
|
|
|
793
798
|
results.push({
|
|
794
799
|
language,
|
package/main/i18ntk-ui.js
CHANGED
|
@@ -11,13 +11,13 @@ const legacyConfigManager = require('../utils/config-manager');
|
|
|
11
11
|
const { getIcon, isUnicodeSupported } = require('../utils/terminal-icons');
|
|
12
12
|
const configManager = new SettingsManager();
|
|
13
13
|
|
|
14
|
-
class UIi18n {
|
|
15
|
-
constructor() {
|
|
16
|
-
this.currentLanguage = 'en';
|
|
17
|
-
this.translations = {};
|
|
18
|
-
this.uiLocalesDir =
|
|
19
|
-
this.availableLanguages = [];
|
|
20
|
-
this.configFile = path.resolve(configManager.configFile);
|
|
14
|
+
class UIi18n {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.currentLanguage = 'en';
|
|
17
|
+
this.translations = {};
|
|
18
|
+
this.uiLocalesDir = this.resolveUiLocalesDir();
|
|
19
|
+
this.availableLanguages = [];
|
|
20
|
+
this.configFile = path.resolve(configManager.configFile);
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
// Initialize with safe defaults
|
|
@@ -25,13 +25,13 @@ this.translations = {};
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
initialize() {
|
|
29
|
-
try {
|
|
30
|
-
const config = configManager.loadSettings ? configManager.loadSettings() : configManager.getConfig ? configManager.getConfig() : {};
|
|
31
|
-
|
|
32
|
-
// Use safe defaults if config is not available
|
|
33
|
-
this.uiLocalesDir =
|
|
34
|
-
this.availableLanguages = this.detectAvailableLanguages();
|
|
28
|
+
initialize() {
|
|
29
|
+
try {
|
|
30
|
+
const config = configManager.loadSettings ? configManager.loadSettings() : configManager.getConfig ? configManager.getConfig() : {};
|
|
31
|
+
|
|
32
|
+
// Use safe defaults if config is not available
|
|
33
|
+
this.uiLocalesDir = this.resolveUiLocalesDir();
|
|
34
|
+
this.availableLanguages = this.detectAvailableLanguages();
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
const configuredLanguage = config?.language || config?.uiLanguage || 'en';
|
|
@@ -42,25 +42,71 @@ this.translations = {};
|
|
|
42
42
|
} else {
|
|
43
43
|
this.loadLanguage('en');
|
|
44
44
|
}
|
|
45
|
-
} catch (error) {
|
|
46
|
-
console.warn('UIi18n: Failed to initialize with config, using defaults:', error.message);
|
|
47
|
-
this.uiLocalesDir =
|
|
48
|
-
this.availableLanguages = this.detectAvailableLanguages();
|
|
49
|
-
this.loadLanguage('en');
|
|
50
|
-
}
|
|
51
|
-
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn('UIi18n: Failed to initialize with config, using defaults:', error.message);
|
|
47
|
+
this.uiLocalesDir = this.resolveUiLocalesDir();
|
|
48
|
+
this.availableLanguages = this.detectAvailableLanguages();
|
|
49
|
+
this.loadLanguage('en');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getValidationBase(targetPath) {
|
|
54
|
+
const fallbackBase = path.resolve(__dirname, '..');
|
|
55
|
+
if (!targetPath || typeof targetPath !== 'string') {
|
|
56
|
+
return fallbackBase;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let current = path.resolve(path.dirname(targetPath));
|
|
60
|
+
while (true) {
|
|
61
|
+
try {
|
|
62
|
+
if (fs.statSync(current).isDirectory()) {
|
|
63
|
+
return current;
|
|
64
|
+
}
|
|
65
|
+
} catch (_) {
|
|
66
|
+
// Continue walking up until an existing directory is found.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const parent = path.dirname(current);
|
|
70
|
+
if (parent === current) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
current = parent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return fallbackBase;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
resolveUiLocalesDir() {
|
|
80
|
+
const candidates = [
|
|
81
|
+
path.resolve(__dirname, '..', 'ui-locales'),
|
|
82
|
+
path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales')
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
for (const candidate of candidates) {
|
|
86
|
+
try {
|
|
87
|
+
const stats = SecurityUtils.safeStatSync(candidate, this.getValidationBase(candidate));
|
|
88
|
+
if (stats && stats.isDirectory()) {
|
|
89
|
+
return candidate;
|
|
90
|
+
}
|
|
91
|
+
} catch (_) {
|
|
92
|
+
// Try next candidate.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return candidates[0];
|
|
97
|
+
}
|
|
52
98
|
/**
|
|
53
99
|
/**
|
|
54
100
|
* Detect which UI locales are currently installed
|
|
55
101
|
* @returns {string[]} Array of available language codes
|
|
56
102
|
*/
|
|
57
|
-
detectAvailableLanguages() {
|
|
58
|
-
const all = ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'];
|
|
59
|
-
return all.filter(lang => {
|
|
60
|
-
const filePath = path.join(this.uiLocalesDir, `${lang}.json`);
|
|
61
|
-
return SecurityUtils.safeExistsSync(filePath);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
103
|
+
detectAvailableLanguages() {
|
|
104
|
+
const all = ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'];
|
|
105
|
+
return all.filter(lang => {
|
|
106
|
+
const filePath = path.join(this.uiLocalesDir, `${lang}.json`);
|
|
107
|
+
return SecurityUtils.safeExistsSync(filePath, this.getValidationBase(filePath));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
64
110
|
|
|
65
111
|
/**
|
|
66
112
|
* Refresh the list of available languages and ensure current language is valid
|
|
@@ -91,13 +137,13 @@ this.translations = {};
|
|
|
91
137
|
}
|
|
92
138
|
|
|
93
139
|
try {
|
|
94
|
-
// Primary: Use monolith JSON file (en.json, de.json, etc.)
|
|
95
|
-
const monolithTranslationFile = path.join(this.uiLocalesDir, `${language}.json`);
|
|
96
|
-
|
|
97
|
-
if (SecurityUtils.safeExistsSync(monolithTranslationFile)) {
|
|
98
|
-
try {
|
|
99
|
-
const content = SecurityUtils.safeReadFileSync(monolithTranslationFile, path.dirname(monolithTranslationFile), 'utf8');
|
|
100
|
-
const fullTranslations = JSON.parse(content);
|
|
140
|
+
// Primary: Use monolith JSON file (en.json, de.json, etc.)
|
|
141
|
+
const monolithTranslationFile = path.join(this.uiLocalesDir, `${language}.json`);
|
|
142
|
+
|
|
143
|
+
if (SecurityUtils.safeExistsSync(monolithTranslationFile, this.getValidationBase(monolithTranslationFile))) {
|
|
144
|
+
try {
|
|
145
|
+
const content = SecurityUtils.safeReadFileSync(monolithTranslationFile, path.dirname(monolithTranslationFile), 'utf8');
|
|
146
|
+
const fullTranslations = JSON.parse(content);
|
|
101
147
|
|
|
102
148
|
// Flatten the nested structure for easier key access
|
|
103
149
|
this.translations = this.flattenTranslations(fullTranslations);
|
|
@@ -111,13 +157,14 @@ this.translations = {};
|
|
|
111
157
|
this.translations = {};
|
|
112
158
|
}
|
|
113
159
|
} else {
|
|
114
|
-
// Fallback: Use folder-based structure if monolith file doesn't exist
|
|
115
|
-
const langDir = path.join(this.uiLocalesDir, language);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
160
|
+
// Fallback: Use folder-based structure if monolith file doesn't exist
|
|
161
|
+
const langDir = path.join(this.uiLocalesDir, language);
|
|
162
|
+
|
|
163
|
+
const langDirStats = SecurityUtils.safeStatSync(langDir, this.getValidationBase(langDir));
|
|
164
|
+
if (langDirStats && langDirStats.isDirectory()) {
|
|
165
|
+
const files = fs.readdirSync(langDir).filter(file => file.endsWith('.json'));
|
|
166
|
+
if (debugEnabled) {
|
|
167
|
+
console.log(`UI: Found files in ${langDir}: ${files.join(', ')}`);
|
|
121
168
|
}
|
|
122
169
|
|
|
123
170
|
for (const file of files) {
|
|
@@ -381,12 +428,12 @@ this.translations = {};
|
|
|
381
428
|
* @param {object} replacements - Object with replacement values
|
|
382
429
|
* @returns {string|null} English translation or null if not found
|
|
383
430
|
*/
|
|
384
|
-
getEnglishFallback(keyPath, replacements = {}) {
|
|
385
|
-
try {
|
|
386
|
-
const englishFile = path.join(this.uiLocalesDir, 'en.json');
|
|
387
|
-
if (SecurityUtils.safeExistsSync(englishFile)) {
|
|
388
|
-
const englishContent = SecurityUtils.safeReadFileSync(englishFile, path.dirname(englishFile), 'utf8');
|
|
389
|
-
const englishTranslations = JSON.parse(englishContent);
|
|
431
|
+
getEnglishFallback(keyPath, replacements = {}) {
|
|
432
|
+
try {
|
|
433
|
+
const englishFile = path.join(this.uiLocalesDir, 'en.json');
|
|
434
|
+
if (SecurityUtils.safeExistsSync(englishFile, this.getValidationBase(englishFile))) {
|
|
435
|
+
const englishContent = SecurityUtils.safeReadFileSync(englishFile, path.dirname(englishFile), 'utf8');
|
|
436
|
+
const englishTranslations = JSON.parse(englishContent);
|
|
390
437
|
|
|
391
438
|
// Use the same flattening approach for consistency
|
|
392
439
|
const flattenedEnglish = this.flattenTranslations(englishTranslations);
|
|
@@ -523,4 +570,4 @@ this.translations = {};
|
|
|
523
570
|
}
|
|
524
571
|
|
|
525
572
|
// Export the class, not a singleton instance
|
|
526
|
-
module.exports = UIi18n;
|
|
573
|
+
module.exports = UIi18n;
|
|
@@ -149,22 +149,44 @@ class AnalyzeCommand {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
// Get all available languages
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
152
|
+
// Get all available languages
|
|
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
|
+
}
|
|
163
|
+
|
|
164
|
+
getAvailableLanguages() {
|
|
165
|
+
try {
|
|
166
|
+
const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
|
|
167
|
+
if (!items) {
|
|
168
|
+
console.error('Error reading source directory: Unable to access directory');
|
|
158
169
|
return [];
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
const languages = [];
|
|
162
173
|
|
|
163
174
|
// Check for directory-based structure
|
|
164
|
-
const directories = items
|
|
165
|
-
.filter(item => item.isDirectory())
|
|
166
|
-
.map(item => item.name)
|
|
167
|
-
.filter(name =>
|
|
175
|
+
const directories = items
|
|
176
|
+
.filter(item => item.isDirectory())
|
|
177
|
+
.map(item => item.name)
|
|
178
|
+
.filter(name =>
|
|
179
|
+
name !== 'node_modules' &&
|
|
180
|
+
!name.startsWith('.') &&
|
|
181
|
+
name !== this.config.sourceLanguage &&
|
|
182
|
+
!this.isExcludedLanguageDirectory(name) &&
|
|
183
|
+
this.isValidLanguageCode(name)
|
|
184
|
+
)
|
|
185
|
+
.filter(name => {
|
|
186
|
+
const dirPath = path.join(this.sourceDir, name);
|
|
187
|
+
const dirItems = SecurityUtils.safeReaddirSync(dirPath, process.cwd(), { withFileTypes: true }) || [];
|
|
188
|
+
return dirItems.some(item => item.isFile() && item.name.endsWith('.json'));
|
|
189
|
+
});
|
|
168
190
|
|
|
169
191
|
// Check for monolith files (language.json files)
|
|
170
192
|
const files = items
|
|
@@ -175,34 +197,17 @@ class AnalyzeCommand {
|
|
|
175
197
|
languages.push(...directories);
|
|
176
198
|
|
|
177
199
|
// Add monolith files as languages (without .json extension)
|
|
178
|
-
const monolithLanguages = files
|
|
179
|
-
.map(file => file.replace('.json', ''))
|
|
180
|
-
.filter(lang =>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const jsonFiles = dirItems
|
|
190
|
-
.filter(item => item.isFile() && item.name.endsWith('.json'))
|
|
191
|
-
.map(item => item.name.replace('.json', ''));
|
|
192
|
-
|
|
193
|
-
// If directory contains JSON files, it's likely a language directory
|
|
194
|
-
if (jsonFiles.length > 0) {
|
|
195
|
-
if (!languages.includes(dir)) {
|
|
196
|
-
languages.push(dir);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} catch (error) {
|
|
201
|
-
// Skip directories we can't read
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return [...new Set(languages)].sort();
|
|
200
|
+
const monolithLanguages = files
|
|
201
|
+
.map(file => file.replace('.json', ''))
|
|
202
|
+
.filter(lang =>
|
|
203
|
+
!languages.includes(lang) &&
|
|
204
|
+
lang !== this.config.sourceLanguage &&
|
|
205
|
+
!this.isExcludedLanguageDirectory(lang) &&
|
|
206
|
+
this.isValidLanguageCode(lang)
|
|
207
|
+
);
|
|
208
|
+
languages.push(...monolithLanguages);
|
|
209
|
+
|
|
210
|
+
return [...new Set(languages)].sort();
|
|
206
211
|
} catch (error) {
|
|
207
212
|
console.error('Error reading source directory:', error.message);
|
|
208
213
|
return [];
|
|
@@ -714,8 +719,7 @@ class AnalyzeCommand {
|
|
|
714
719
|
throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
|
|
715
720
|
}
|
|
716
721
|
|
|
717
|
-
|
|
718
|
-
return reportPath;
|
|
722
|
+
return reportPath;
|
|
719
723
|
|
|
720
724
|
} catch (error) {
|
|
721
725
|
console.error(`Failed to save report for ${language}:`, error.message);
|
|
@@ -782,20 +786,21 @@ class AnalyzeCommand {
|
|
|
782
786
|
console.log(t('analyze.analyzing', { language }) || `\nš Analyzing ${language}...`);
|
|
783
787
|
}
|
|
784
788
|
|
|
785
|
-
const analysis = this.analyzeLanguage(language);
|
|
786
|
-
const report = this.generateLanguageReport(analysis);
|
|
787
|
-
|
|
788
|
-
// Save report
|
|
789
|
-
const reportPath = await this.saveReport(language, report);
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
console.log(t('analyze.
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
789
|
+
const analysis = this.analyzeLanguage(language);
|
|
790
|
+
const report = this.generateLanguageReport(analysis);
|
|
791
|
+
|
|
792
|
+
// Save report
|
|
793
|
+
const reportPath = await this.saveReport(language, report);
|
|
794
|
+
const processedCount = results.length + 1;
|
|
795
|
+
|
|
796
|
+
if (!args.json) {
|
|
797
|
+
console.log(t('analyze.completed', { language }) || `ā
Analysis completed for ${language}`);
|
|
798
|
+
console.log(t('analyze.progress', {
|
|
799
|
+
translated: processedCount,
|
|
800
|
+
total: languages.length
|
|
801
|
+
}) || ` Progress: ${processedCount}/${languages.length} languages processed`);
|
|
802
|
+
console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
|
|
803
|
+
}
|
|
799
804
|
|
|
800
805
|
results.push({
|
|
801
806
|
language,
|