i18ntk 2.0.3 ā 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 -32
- 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,44 +1,59 @@
|
|
|
1
|
-
# i18ntk v2.0
|
|
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
|
-
[](https://www.npmjs.com/package/i18ntk)
|
|
7
|
+
[](https://www.npmjs.com/package/i18ntk)
|
|
8
8
|
[](https://www.npmjs.com/package/i18ntk)
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](https://www.npmjs.com/package/i18ntk)
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[](https://socket.dev/npm/package/i18ntk/overview/2.1.0)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## Why i18ntk
|
|
15
15
|
|
|
16
|
-
|
|
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`
|
|
17
21
|
|
|
18
22
|
## Install
|
|
19
23
|
|
|
20
24
|
```bash
|
|
25
|
+
# global (recommended for CLI use)
|
|
21
26
|
npm install -g i18ntk
|
|
27
|
+
|
|
28
|
+
# local
|
|
29
|
+
npm install --save-dev i18ntk
|
|
30
|
+
|
|
31
|
+
# one-off
|
|
32
|
+
npx i18ntk --help
|
|
22
33
|
```
|
|
23
34
|
|
|
24
35
|
## Quick Start
|
|
25
36
|
|
|
26
37
|
```bash
|
|
27
|
-
#
|
|
28
|
-
i18ntk
|
|
29
|
-
|
|
30
|
-
# direct workflow
|
|
38
|
+
# initialize locales/project settings
|
|
31
39
|
i18ntk --command=init
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
i18ntk --command=
|
|
40
|
+
|
|
41
|
+
# analyze translation completeness
|
|
42
|
+
i18ntk --command=analyze
|
|
43
|
+
|
|
44
|
+
# validate translation structure/content
|
|
45
|
+
i18ntk --command=validate
|
|
46
|
+
|
|
47
|
+
# complete missing keys
|
|
48
|
+
i18ntk --command=complete
|
|
35
49
|
```
|
|
36
50
|
|
|
37
|
-
##
|
|
51
|
+
## Command Model (v2)
|
|
38
52
|
|
|
39
|
-
Primary CLI
|
|
53
|
+
Primary CLI:
|
|
40
54
|
|
|
41
55
|
```bash
|
|
56
|
+
i18ntk
|
|
42
57
|
i18ntk --command=init
|
|
43
58
|
i18ntk --command=analyze
|
|
44
59
|
i18ntk --command=validate
|
|
@@ -50,7 +65,7 @@ i18ntk --command=summary
|
|
|
50
65
|
i18ntk --command=debug
|
|
51
66
|
```
|
|
52
67
|
|
|
53
|
-
Standalone
|
|
68
|
+
Standalone executables:
|
|
54
69
|
|
|
55
70
|
```bash
|
|
56
71
|
i18ntk-init
|
|
@@ -66,19 +81,8 @@ i18ntk-fixer
|
|
|
66
81
|
i18ntk-backup
|
|
67
82
|
```
|
|
68
83
|
|
|
69
|
-
Backup helper:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
i18ntk-backup --help
|
|
73
|
-
i18ntk-backup create ./locales
|
|
74
|
-
i18ntk-backup list
|
|
75
|
-
i18ntk-backup restore <backup-file>
|
|
76
|
-
```
|
|
77
|
-
|
|
78
84
|
## Common Flags
|
|
79
85
|
|
|
80
|
-
Most commands support:
|
|
81
|
-
|
|
82
86
|
- `--source-dir <path>`
|
|
83
87
|
- `--i18n-dir <path>`
|
|
84
88
|
- `--output-dir <path>`
|
|
@@ -90,13 +94,13 @@ Most commands support:
|
|
|
90
94
|
|
|
91
95
|
## Configuration
|
|
92
96
|
|
|
93
|
-
i18ntk reads project settings from `.i18ntk-config` in
|
|
97
|
+
i18ntk reads project settings from `.i18ntk-config` in your project root.
|
|
94
98
|
|
|
95
99
|
Example:
|
|
96
100
|
|
|
97
101
|
```json
|
|
98
102
|
{
|
|
99
|
-
"version": "2.
|
|
103
|
+
"version": "2.1.0",
|
|
100
104
|
"sourceDir": "./locales",
|
|
101
105
|
"i18nDir": "./locales",
|
|
102
106
|
"outputDir": "./i18ntk-reports",
|
|
@@ -125,7 +129,7 @@ setLanguage('fr');
|
|
|
125
129
|
console.log(getLanguage());
|
|
126
130
|
```
|
|
127
131
|
|
|
128
|
-
##
|
|
132
|
+
## Documentation
|
|
129
133
|
|
|
130
134
|
- [Documentation Index](docs/README.md)
|
|
131
135
|
- [API Reference](docs/api/API_REFERENCE.md)
|
|
@@ -133,9 +137,9 @@ console.log(getLanguage());
|
|
|
133
137
|
- [Runtime API Guide](docs/runtime.md)
|
|
134
138
|
- [Scanner Guide](docs/scanner-guide.md)
|
|
135
139
|
- [Environment Variables](docs/environment-variables.md)
|
|
136
|
-
- [Migration Guide v2.
|
|
140
|
+
- [Migration Guide v2.1.0](docs/migration-guide-v2.1.0.md)
|
|
137
141
|
- [Release Runbook](DEVUPDATE.md)
|
|
138
142
|
|
|
139
143
|
## License
|
|
140
144
|
|
|
141
|
-
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,
|