i18ntk 1.10.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +141 -1185
- package/main/i18ntk-analyze.js +149 -133
- package/main/i18ntk-backup-class.js +420 -0
- package/main/i18ntk-backup.js +4 -4
- package/main/i18ntk-complete.js +90 -65
- package/main/i18ntk-doctor.js +123 -103
- package/main/i18ntk-fixer.js +61 -725
- package/main/i18ntk-go.js +14 -15
- package/main/i18ntk-init.js +76 -25
- package/main/i18ntk-java.js +27 -32
- package/main/i18ntk-js.js +70 -68
- package/main/i18ntk-manage.js +128 -29
- package/main/i18ntk-php.js +75 -75
- package/main/i18ntk-py.js +55 -56
- package/main/i18ntk-scanner.js +59 -57
- package/main/i18ntk-setup.js +10 -396
- package/main/i18ntk-sizing.js +46 -40
- package/main/i18ntk-summary.js +21 -18
- package/main/i18ntk-ui.js +11 -10
- package/main/i18ntk-usage.js +55 -19
- package/main/i18ntk-validate.js +13 -13
- package/main/manage/commands/AnalyzeCommand.js +1124 -0
- package/main/manage/commands/BackupCommand.js +62 -0
- package/main/manage/commands/CommandRouter.js +295 -0
- package/main/manage/commands/CompleteCommand.js +61 -0
- package/main/manage/commands/DoctorCommand.js +60 -0
- package/main/manage/commands/FixerCommand.js +624 -0
- package/main/manage/commands/InitCommand.js +62 -0
- package/main/manage/commands/ScannerCommand.js +654 -0
- package/main/manage/commands/SizingCommand.js +60 -0
- package/main/manage/commands/SummaryCommand.js +61 -0
- package/main/manage/commands/UsageCommand.js +60 -0
- package/main/manage/commands/ValidateCommand.js +978 -0
- package/main/manage/index-fixed.js +1447 -0
- package/main/manage/index.js +1462 -0
- package/main/manage/managers/DebugMenu.js +140 -0
- package/main/manage/managers/InteractiveMenu.js +177 -0
- package/main/manage/managers/LanguageMenu.js +62 -0
- package/main/manage/managers/SettingsMenu.js +53 -0
- package/main/manage/services/AuthenticationService.js +263 -0
- package/main/manage/services/ConfigurationService-fixed.js +449 -0
- package/main/manage/services/ConfigurationService.js +449 -0
- package/main/manage/services/FileManagementService.js +368 -0
- package/main/manage/services/FrameworkDetectionService.js +458 -0
- package/main/manage/services/InitService.js +1051 -0
- package/main/manage/services/SetupService.js +462 -0
- package/main/manage/services/SummaryService.js +450 -0
- package/main/manage/services/UsageService.js +1502 -0
- package/package.json +32 -30
- package/runtime/enhanced.d.ts +221 -221
- package/runtime/index.d.ts +29 -29
- package/runtime/index.full.d.ts +331 -331
- package/runtime/index.js +7 -6
- package/scripts/build-lite.js +17 -17
- package/scripts/deprecate-versions.js +23 -6
- package/scripts/export-translations.js +5 -5
- package/scripts/fix-all-i18n.js +3 -3
- package/scripts/fix-and-purify-i18n.js +3 -2
- package/scripts/fix-locale-control-chars.js +110 -0
- package/scripts/lint-locales.js +80 -0
- package/scripts/locale-optimizer.js +8 -8
- package/scripts/prepublish.js +21 -21
- package/scripts/security-check.js +13 -5
- package/scripts/sync-translations.js +4 -4
- package/scripts/sync-ui-locales.js +9 -8
- package/scripts/validate-all-translations.js +8 -7
- package/scripts/verify-deprecations.js +23 -15
- package/scripts/verify-translations.js +6 -5
- package/settings/i18ntk-config.json +282 -282
- package/settings/language-config.json +5 -5
- package/settings/settings-cli.js +9 -9
- package/settings/settings-manager.js +23 -20
- package/ui-locales/de.json +2417 -2348
- package/ui-locales/en.json +2415 -2352
- package/ui-locales/es.json +2425 -2353
- package/ui-locales/fr.json +2418 -2348
- package/ui-locales/ja.json +2463 -2361
- package/ui-locales/ru.json +2463 -2359
- package/ui-locales/zh.json +2418 -2351
- package/utils/admin-auth.js +2 -2
- package/utils/admin-cli.js +297 -297
- package/utils/admin-pin.js +9 -9
- package/utils/cli-helper.js +9 -9
- package/utils/config-helper.js +152 -103
- package/utils/config-manager.js +204 -164
- package/utils/config.js +5 -4
- package/utils/env-manager.js +256 -0
- package/utils/framework-detector.js +27 -24
- package/utils/i18n-helper.js +85 -41
- package/utils/init-helper.js +152 -94
- package/utils/json-output.js +98 -98
- package/utils/logger.js +6 -2
- package/utils/mini-commander.js +179 -0
- package/utils/missing-key-validator.js +5 -5
- package/utils/plugin-loader.js +29 -11
- package/utils/prompt.js +14 -44
- package/utils/safe-json.js +40 -0
- package/utils/secure-errors.js +3 -3
- package/utils/security-check-improved.js +390 -0
- package/utils/security-config.js +5 -5
- package/utils/security-fixed.js +607 -0
- package/utils/security.js +462 -248
- package/utils/setup-enforcer.js +136 -44
- package/utils/setup-validator.js +33 -32
- package/utils/terminal-icons.js +1 -1
- package/utils/ultra-performance-optimizer.js +11 -9
- package/utils/watch-locales.js +2 -1
- package/utils/prompt-fixed.js +0 -55
- package/utils/security-check.js +0 -450
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized Environment Variable Manager
|
|
3
|
+
*
|
|
4
|
+
* This module provides secure access to a fixed allowlist of environment variables.
|
|
5
|
+
* Only explicitly defined environment variables are accessible, all others are ignored.
|
|
6
|
+
* No secrets or sensitive data should ever be stored in environment variables.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const ALLOWED_ENV_VARS = {
|
|
10
|
+
// Logging and output
|
|
11
|
+
'I18NTK_LOG_LEVEL': {
|
|
12
|
+
default: 'error',
|
|
13
|
+
validate: (value) => ['error', 'warn', 'info', 'debug', 'silent'].includes(value.toLowerCase()),
|
|
14
|
+
transform: (value) => value.toLowerCase()
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
'I18NTK_OUTDIR': {
|
|
18
|
+
default: './i18ntk-reports',
|
|
19
|
+
validate: (value) => typeof value === 'string' && value.length > 0,
|
|
20
|
+
transform: (value) => value.trim()
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
// UI and interaction
|
|
24
|
+
'I18NTK_UI_LANGUAGE': {
|
|
25
|
+
default: 'en',
|
|
26
|
+
validate: (value) => ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(value.toLowerCase()),
|
|
27
|
+
transform: (value) => value.toLowerCase()
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
'I18NTK_SILENT': {
|
|
31
|
+
default: 'false',
|
|
32
|
+
validate: (value) => ['true', 'false', '1', '0', 'yes', 'no'].includes(value.toLowerCase()),
|
|
33
|
+
transform: (value) => {
|
|
34
|
+
const lower = value.toLowerCase();
|
|
35
|
+
return lower === 'true' || lower === '1' || lower === 'yes' ? 'true' : 'false';
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Debug and development
|
|
40
|
+
'I18NTK_DEBUG_LOCALES': {
|
|
41
|
+
default: '0',
|
|
42
|
+
validate: (value) => ['0', '1', 'true', 'false'].includes(value.toLowerCase()),
|
|
43
|
+
transform: (value) => {
|
|
44
|
+
const lower = value.toLowerCase();
|
|
45
|
+
return lower === '1' || lower === 'true' ? '1' : '0';
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Runtime configuration
|
|
50
|
+
'I18NTK_RUNTIME_DIR': {
|
|
51
|
+
default: null,
|
|
52
|
+
validate: (value) => typeof value === 'string',
|
|
53
|
+
transform: (value) => value.trim() || null
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
'I18NTK_I18N_DIR': {
|
|
57
|
+
default: './locales',
|
|
58
|
+
validate: (value) => typeof value === 'string' && value.length > 0,
|
|
59
|
+
transform: (value) => value.trim()
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
'I18NTK_SOURCE_DIR': {
|
|
63
|
+
default: './locales',
|
|
64
|
+
validate: (value) => typeof value === 'string' && value.length > 0,
|
|
65
|
+
transform: (value) => value.trim()
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
'I18NTK_PROJECT_ROOT': {
|
|
69
|
+
default: '.',
|
|
70
|
+
validate: (value) => typeof value === 'string' && value.length > 0,
|
|
71
|
+
transform: (value) => value.trim()
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Framework detection
|
|
75
|
+
'I18NTK_FRAMEWORK_PREFERENCE': {
|
|
76
|
+
default: 'auto',
|
|
77
|
+
validate: (value) => ['auto', 'vanilla', 'react', 'vue', 'angular', 'svelte', 'i18next', 'nuxt', 'next', 'django', 'flask', 'fastapi', 'spring-boot', 'laravel'].includes(value.toLowerCase()),
|
|
78
|
+
transform: (value) => value.toLowerCase()
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
'I18NTK_FRAMEWORK_FALLBACK': {
|
|
82
|
+
default: 'vanilla',
|
|
83
|
+
validate: (value) => ['vanilla', 'react', 'vue', 'angular', 'svelte', 'i18next', 'nuxt', 'next', 'django', 'flask', 'fastapi', 'spring-boot', 'laravel'].includes(value.toLowerCase()),
|
|
84
|
+
transform: (value) => value.toLowerCase()
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
'I18NTK_FRAMEWORK_DETECT': {
|
|
88
|
+
default: 'true',
|
|
89
|
+
validate: (value) => ['true', 'false', '1', '0', 'yes', 'no'].includes(value.toLowerCase()),
|
|
90
|
+
transform: (value) => {
|
|
91
|
+
const lower = value.toLowerCase();
|
|
92
|
+
return lower === 'true' || lower === '1' || lower === 'yes' ? 'true' : 'false';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Security: Block access to sensitive environment variables
|
|
98
|
+
const BLOCKED_PATTERNS = [
|
|
99
|
+
/^SECRET/i,
|
|
100
|
+
/^PASSWORD/i,
|
|
101
|
+
/^KEY/i,
|
|
102
|
+
/^TOKEN/i,
|
|
103
|
+
/^API_KEY/i,
|
|
104
|
+
/^PRIVATE/i,
|
|
105
|
+
/^AUTH/i,
|
|
106
|
+
/^CREDENTIAL/i,
|
|
107
|
+
/^AWS_/i,
|
|
108
|
+
/^GITHUB_/i,
|
|
109
|
+
/^NPM_/i,
|
|
110
|
+
/^NODE_/i,
|
|
111
|
+
/^PATH$/,
|
|
112
|
+
/^HOME$/,
|
|
113
|
+
/^USER$/,
|
|
114
|
+
/^USERNAME$/,
|
|
115
|
+
/^SHELL$/,
|
|
116
|
+
/^TERM$/,
|
|
117
|
+
/^DISPLAY$/,
|
|
118
|
+
/^LANG$/,
|
|
119
|
+
/^LC_/i
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
class EnvironmentManager {
|
|
123
|
+
constructor() {
|
|
124
|
+
this._cache = new Map();
|
|
125
|
+
this._validated = new Set();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get a validated environment variable value
|
|
130
|
+
* @param {string} name - Environment variable name
|
|
131
|
+
* @returns {string|null} - Validated value or null if not allowed
|
|
132
|
+
*/
|
|
133
|
+
get(name) {
|
|
134
|
+
// Only allow explicitly defined variables
|
|
135
|
+
if (!ALLOWED_ENV_VARS[name]) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check cache first
|
|
140
|
+
if (this._cache.has(name)) {
|
|
141
|
+
return this._cache.get(name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const definition = ALLOWED_ENV_VARS[name];
|
|
145
|
+
const rawValue = process.env[name];
|
|
146
|
+
|
|
147
|
+
// Use default if not set
|
|
148
|
+
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
|
149
|
+
this._cache.set(name, definition.default);
|
|
150
|
+
return definition.default;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Validate and transform
|
|
154
|
+
try {
|
|
155
|
+
const transformed = definition.transform(rawValue);
|
|
156
|
+
|
|
157
|
+
if (!definition.validate(transformed)) {
|
|
158
|
+
console.warn(`[i18ntk] Invalid value for ${name}: "${rawValue}". Using default: ${definition.default}`);
|
|
159
|
+
this._cache.set(name, definition.default);
|
|
160
|
+
return definition.default;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this._cache.set(name, transformed);
|
|
164
|
+
this._validated.add(name);
|
|
165
|
+
return transformed;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.warn(`[i18ntk] Error processing ${name}: ${error.message}. Using default: ${definition.default}`);
|
|
168
|
+
this._cache.set(name, definition.default);
|
|
169
|
+
return definition.default;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if an environment variable is allowed
|
|
175
|
+
* @param {string} name - Environment variable name
|
|
176
|
+
* @returns {boolean} - True if allowed
|
|
177
|
+
*/
|
|
178
|
+
isAllowed(name) {
|
|
179
|
+
return !!ALLOWED_ENV_VARS[name];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get all allowed environment variables with their current values
|
|
184
|
+
* @returns {Object} - Object with variable names as keys and values
|
|
185
|
+
*/
|
|
186
|
+
getAll() {
|
|
187
|
+
const result = {};
|
|
188
|
+
for (const name of Object.keys(ALLOWED_ENV_VARS)) {
|
|
189
|
+
result[name] = this.get(name);
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get documentation for all allowed environment variables
|
|
196
|
+
* @returns {Array} - Array of documentation objects
|
|
197
|
+
*/
|
|
198
|
+
getDocumentation() {
|
|
199
|
+
return Object.entries(ALLOWED_ENV_VARS).map(([name, definition]) => ({
|
|
200
|
+
name,
|
|
201
|
+
default: definition.default,
|
|
202
|
+
description: this._getDescription(name)
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Clear the cache (for testing)
|
|
208
|
+
*/
|
|
209
|
+
clearCache() {
|
|
210
|
+
this._cache.clear();
|
|
211
|
+
this._validated.clear();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if a variable name matches blocked patterns
|
|
216
|
+
* @param {string} name - Variable name to check
|
|
217
|
+
* @returns {boolean} - True if blocked
|
|
218
|
+
*/
|
|
219
|
+
isBlocked(name) {
|
|
220
|
+
return BLOCKED_PATTERNS.some(pattern => pattern.test(name));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get human-readable description for an environment variable
|
|
225
|
+
* @param {string} name - Environment variable name
|
|
226
|
+
* @returns {string} - Description
|
|
227
|
+
*/
|
|
228
|
+
_getDescription(name) {
|
|
229
|
+
const descriptions = {
|
|
230
|
+
'I18NTK_LOG_LEVEL': 'Logging level (error, warn, info, debug, silent)',
|
|
231
|
+
'I18NTK_OUTDIR': 'Output directory for reports and generated files',
|
|
232
|
+
'I18NTK_UI_LANGUAGE': 'UI language (en, de, es, fr, ru, ja, zh)',
|
|
233
|
+
'I18NTK_SILENT': 'Run in silent mode without interactive prompts',
|
|
234
|
+
'I18NTK_DEBUG_LOCALES': 'Enable debug logging for locale loading',
|
|
235
|
+
'I18NTK_RUNTIME_DIR': 'Custom runtime directory path',
|
|
236
|
+
'I18NTK_I18N_DIR': 'Directory containing i18n/locale files',
|
|
237
|
+
'I18NTK_SOURCE_DIR': 'Source directory for scanning',
|
|
238
|
+
'I18NTK_PROJECT_ROOT': 'Project root directory',
|
|
239
|
+
'I18NTK_FRAMEWORK_PREFERENCE': 'Preferred framework (auto, react, vue, etc.)',
|
|
240
|
+
'I18NTK_FRAMEWORK_FALLBACK': 'Fallback framework when auto-detection fails',
|
|
241
|
+
'I18NTK_FRAMEWORK_DETECT': 'Enable automatic framework detection'
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return descriptions[name] || 'Configuration option';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Create singleton instance
|
|
249
|
+
const envManager = new EnvironmentManager();
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
EnvironmentManager,
|
|
253
|
+
envManager,
|
|
254
|
+
ALLOWED_ENV_VARS,
|
|
255
|
+
BLOCKED_PATTERNS
|
|
256
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
1
|
const path = require('path');
|
|
3
2
|
const { gte } = require('./version-utils');
|
|
3
|
+
const SecurityUtils = require('./security');
|
|
4
4
|
|
|
5
5
|
// Framework compatibility information
|
|
6
6
|
const FRAMEWORK_COMPATIBILITY = {
|
|
@@ -38,9 +38,9 @@ const FRAMEWORKS = {
|
|
|
38
38
|
configFilePatterns: [
|
|
39
39
|
/i18n\.(js|ts)$/,
|
|
40
40
|
/i18ntk\.config\.(js|ts)$/,
|
|
41
|
-
/\.i18nrc(\.(js|json))
|
|
41
|
+
/\.i18nrc(\.(js|json))?$/,
|
|
42
42
|
],
|
|
43
|
-
setupGuide: '
|
|
43
|
+
setupGuide: 'Refer to the official i18ntk documentation on GitHub for setup instructions.',
|
|
44
44
|
priority: 100, // Higher priority to detect before other frameworks
|
|
45
45
|
ignore: [
|
|
46
46
|
'node_modules/**',
|
|
@@ -52,7 +52,7 @@ const FRAMEWORKS = {
|
|
|
52
52
|
'**/*.test.{js,jsx,ts,tsx}'
|
|
53
53
|
]
|
|
54
54
|
},
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
// Vue i18n has highest specificity due to its unique syntax
|
|
57
57
|
'vue-i18n': {
|
|
58
58
|
name: 'vue-i18n',
|
|
@@ -66,7 +66,7 @@ const FRAMEWORKS = {
|
|
|
66
66
|
],
|
|
67
67
|
ignore: ['node_modules/**']
|
|
68
68
|
},
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
// React i18next has medium specificity
|
|
71
71
|
'react-i18next': {
|
|
72
72
|
name: 'React i18next',
|
|
@@ -76,9 +76,9 @@ const FRAMEWORKS = {
|
|
|
76
76
|
regex: /\b(?:useTranslation|withTranslation|Trans|I18n|i18n\.t|t\(?=\s*[`'"])/,
|
|
77
77
|
configFile: 'i18n.js',
|
|
78
78
|
configFilePatterns: [/i18n\.(js|ts)$/, /i18next\.config\.(js|ts)$/],
|
|
79
|
-
setupGuide: '
|
|
79
|
+
setupGuide: 'Refer to the official react-i18next documentation for setup instructions.'
|
|
80
80
|
},
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
// Base i18next has lowest specificity
|
|
83
83
|
'i18next': {
|
|
84
84
|
name: 'i18next',
|
|
@@ -246,10 +246,10 @@ const FRAMEWORKS = {
|
|
|
246
246
|
'test/tmp/**'
|
|
247
247
|
]
|
|
248
248
|
},
|
|
249
|
-
go: {
|
|
250
|
-
name: 'go',
|
|
251
|
-
deps: ['
|
|
252
|
-
globs: ['**/*.go'],
|
|
249
|
+
go: {
|
|
250
|
+
name: 'go',
|
|
251
|
+
deps: ['go-i18n', 'x-text'],
|
|
252
|
+
globs: ['**/*.go'],
|
|
253
253
|
patterns: [
|
|
254
254
|
/i18n\.NewMessage\([^,]+,\s*["`]([^"`]+)["`]/g,
|
|
255
255
|
/i18n\.NewLocalizer\([^)]+\)\.MustLocalize\([^,]+,\s*["`]([^"`]+)["`]/g,
|
|
@@ -277,11 +277,6 @@ const FRAMEWORKS = {
|
|
|
277
277
|
}
|
|
278
278
|
};
|
|
279
279
|
|
|
280
|
-
/**
|
|
281
|
-
* Detect the i18n framework being used in the project
|
|
282
|
-
* @param {string} projectRoot - Path to the project root
|
|
283
|
-
* @returns {Promise<Object>} Object containing framework info and detection confidence
|
|
284
|
-
*/
|
|
285
280
|
/**
|
|
286
281
|
* Detects the i18n framework being used in the project
|
|
287
282
|
* @param {string} projectRoot - Path to the project root
|
|
@@ -294,23 +289,31 @@ async function detectFramework(projectRoot) {
|
|
|
294
289
|
|
|
295
290
|
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
296
291
|
const detectedFrameworks = [];
|
|
297
|
-
|
|
292
|
+
|
|
298
293
|
// Only proceed if package.json exists
|
|
299
|
-
if (!
|
|
294
|
+
if (!SecurityUtils.safeExistsSync(packageJsonPath, projectRoot)) {
|
|
300
295
|
return null;
|
|
301
296
|
}
|
|
302
297
|
|
|
303
298
|
try {
|
|
304
299
|
// Read and parse package.json
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
300
|
+
const packageJsonContent = SecurityUtils.safeReadFileSync(packageJsonPath, projectRoot, 'utf8');
|
|
301
|
+
if (!packageJsonContent) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
|
|
305
|
+
if (!packageJson) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const deps = {
|
|
310
|
+
...(packageJson.dependencies || {}),
|
|
308
311
|
...(packageJson.devDependencies || {}),
|
|
309
312
|
...(packageJson.peerDependencies || {})
|
|
310
313
|
};
|
|
311
314
|
|
|
312
315
|
// Sort frameworks by priority (highest first)
|
|
313
|
-
const sortedFrameworks = Object.entries(FRAMEWORKS).sort((a, b) =>
|
|
316
|
+
const sortedFrameworks = Object.entries(FRAMEWORKS).sort((a, b) =>
|
|
314
317
|
(b[1].priority || 0) - (a[1].priority || 0)
|
|
315
318
|
);
|
|
316
319
|
|
|
@@ -350,7 +353,7 @@ async function detectFramework(projectRoot) {
|
|
|
350
353
|
// First sort by confidence
|
|
351
354
|
const confidenceDiff = b.confidence - a.confidence;
|
|
352
355
|
if (confidenceDiff !== 0) return confidenceDiff;
|
|
353
|
-
|
|
356
|
+
|
|
354
357
|
// If confidence is equal, sort by priority
|
|
355
358
|
return (b.priority || 0) - (a.priority || 0);
|
|
356
359
|
})[0];
|
|
@@ -363,4 +366,4 @@ async function detectFramework(projectRoot) {
|
|
|
363
366
|
}
|
|
364
367
|
}
|
|
365
368
|
|
|
366
|
-
module.exports = { detectFramework, FRAMEWORKS };
|
|
369
|
+
module.exports = { detectFramework, FRAMEWORKS };
|
package/utils/i18n-helper.js
CHANGED
|
@@ -2,11 +2,47 @@
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
|
|
5
|
+
// Lazy load SecurityUtils to prevent circular dependencies
|
|
6
|
+
let securityUtils;
|
|
7
|
+
function getSecurityUtils() {
|
|
8
|
+
if (!securityUtils) {
|
|
9
|
+
try {
|
|
10
|
+
securityUtils = require('./security');
|
|
11
|
+
} catch (error) {
|
|
12
|
+
// Fallback: use basic fs operations if SecurityUtils is not available
|
|
13
|
+
return {
|
|
14
|
+
safeExistsSync: (path) => {
|
|
15
|
+
try {
|
|
16
|
+
return require('fs').existsSync(path);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
safeWriteFileSync: (path, data, encoding) => {
|
|
22
|
+
try {
|
|
23
|
+
return require('fs').writeFileSync(path, data, encoding);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
safeReadFileSync: (path, encoding) => {
|
|
29
|
+
try {
|
|
30
|
+
return require('fs').readFileSync(path, encoding);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return securityUtils;
|
|
39
|
+
}
|
|
40
|
+
|
|
5
41
|
// Helper functions for OS-agnostic path handling
|
|
6
42
|
function toPosix(p) { return String(p).replace(/\\/g, '/'); }
|
|
7
|
-
function isBundledPath(p) {
|
|
8
|
-
const s = toPosix(p);
|
|
9
|
-
return s.includes('/node_modules/i18ntk/') || s.includes('/i18ntk/ui-locales/');
|
|
43
|
+
function isBundledPath(p) {
|
|
44
|
+
const s = toPosix(p);
|
|
45
|
+
return s.includes('/node_modules/i18ntk/') || s.includes('/i18ntk/ui-locales/');
|
|
10
46
|
}
|
|
11
47
|
|
|
12
48
|
function safeRequireConfig() {
|
|
@@ -21,7 +57,8 @@ function stripBOMAndComments(s) {
|
|
|
21
57
|
}
|
|
22
58
|
|
|
23
59
|
function readJsonSafe(file) {
|
|
24
|
-
const
|
|
60
|
+
const SecurityUtils = getSecurityUtils();
|
|
61
|
+
const raw = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8');
|
|
25
62
|
return JSON.parse(stripBOMAndComments(raw));
|
|
26
63
|
}
|
|
27
64
|
|
|
@@ -43,7 +80,8 @@ function resolveLocalesDirs() {
|
|
|
43
80
|
try {
|
|
44
81
|
const normalized = path.normalize(path.resolve(dir.trim()));
|
|
45
82
|
|
|
46
|
-
|
|
83
|
+
const SecurityUtils = getSecurityUtils();
|
|
84
|
+
if (SecurityUtils.safeExistsSync(normalized) && fs.statSync(normalized).isDirectory()) {
|
|
47
85
|
dirs.push(normalized);
|
|
48
86
|
}
|
|
49
87
|
} catch {
|
|
@@ -54,7 +92,7 @@ function resolveLocalesDirs() {
|
|
|
54
92
|
|
|
55
93
|
const pkgA = pkgUiLocalesDirViaThisFile();
|
|
56
94
|
addDir(pkgA);
|
|
57
|
-
|
|
95
|
+
|
|
58
96
|
const pkgB = pkgUiLocalesDirViaResolve();
|
|
59
97
|
if (pkgB && pkgB !== pkgA) {
|
|
60
98
|
addDir(pkgB);
|
|
@@ -78,28 +116,33 @@ function candidatesForLang(dir, lang) {
|
|
|
78
116
|
|
|
79
117
|
function findLocaleFilesAllDirs(lang) {
|
|
80
118
|
const dirs = resolveLocalesDirs();
|
|
81
|
-
|
|
119
|
+
|
|
82
120
|
if (process.env.I18NTK_DEBUG_LOCALES === '1') {
|
|
83
121
|
console.log('🔎 i18ntk locale search dirs:', dirs);
|
|
84
122
|
}
|
|
85
|
-
|
|
123
|
+
|
|
86
124
|
const files = [];
|
|
87
125
|
const errors = [];
|
|
88
|
-
|
|
126
|
+
|
|
89
127
|
for (const dir of dirs) {
|
|
90
128
|
for (const candidate of candidatesForLang(dir, lang)) {
|
|
91
129
|
try {
|
|
92
|
-
|
|
130
|
+
const SecurityUtils = getSecurityUtils();
|
|
131
|
+
if (SecurityUtils.safeExistsSync(candidate)) {
|
|
93
132
|
const stats = fs.statSync(candidate);
|
|
94
133
|
if (stats.isFile() && stats.size > 0) {
|
|
95
134
|
// Validate file is readable and parseable
|
|
96
135
|
fs.accessSync(candidate, fs.constants.R_OK);
|
|
97
136
|
// Quick JSON validation
|
|
98
|
-
const content =
|
|
99
|
-
if (content
|
|
100
|
-
|
|
137
|
+
const content = SecurityUtils.safeReadFileSync(candidate, path.dirname(candidate), 'utf8');
|
|
138
|
+
if (content) {
|
|
139
|
+
if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
|
|
140
|
+
files.push(candidate);
|
|
141
|
+
} else {
|
|
142
|
+
errors.push({ file: candidate, error: 'Invalid JSON format' });
|
|
143
|
+
}
|
|
101
144
|
} else {
|
|
102
|
-
errors.push({ file: candidate, error: '
|
|
145
|
+
errors.push({ file: candidate, error: 'Empty or unreadable file' });
|
|
103
146
|
}
|
|
104
147
|
}
|
|
105
148
|
}
|
|
@@ -108,11 +151,11 @@ function findLocaleFilesAllDirs(lang) {
|
|
|
108
151
|
}
|
|
109
152
|
}
|
|
110
153
|
}
|
|
111
|
-
|
|
154
|
+
|
|
112
155
|
if (process.env.I18NTK_DEBUG_LOCALES === '1' && errors.length > 0) {
|
|
113
156
|
console.warn(`⚠️ Locale resolution errors for ${lang}:`, errors);
|
|
114
157
|
}
|
|
115
|
-
|
|
158
|
+
|
|
116
159
|
return files;
|
|
117
160
|
}
|
|
118
161
|
|
|
@@ -121,34 +164,34 @@ let currentLanguage = 'en';
|
|
|
121
164
|
let isInitialized = false;
|
|
122
165
|
const missingWarned = new Set();
|
|
123
166
|
|
|
124
|
-
function loadTranslations(language) {
|
|
125
|
-
const cfg = safeRequireConfig();
|
|
126
|
-
const settings = cfg?.getConfig?.() || {};
|
|
127
|
-
const configuredLanguage = settings.uiLanguage || settings.language
|
|
128
|
-
|
|
129
|
-
// Prioritize
|
|
130
|
-
const requested = (
|
|
167
|
+
function loadTranslations(language) {
|
|
168
|
+
const cfg = safeRequireConfig();
|
|
169
|
+
const settings = cfg?.getConfig?.() || {};
|
|
170
|
+
const configuredLanguage = settings.uiLanguage || settings.language;
|
|
171
|
+
|
|
172
|
+
// Prioritize CLI argument, then UI language, then language fallback
|
|
173
|
+
const requested = (language || configuredLanguage || 'en').toString();
|
|
131
174
|
const short = requested.split('-')[0].toLowerCase();
|
|
132
175
|
const tryOrder = [requested, short, 'en'];
|
|
133
176
|
|
|
134
177
|
const loadErrors = [];
|
|
135
|
-
|
|
178
|
+
|
|
136
179
|
for (const lang of tryOrder) {
|
|
137
180
|
const files = findLocaleFilesAllDirs(lang);
|
|
138
|
-
|
|
181
|
+
|
|
139
182
|
// Prioritize bundled locales over project ones
|
|
140
183
|
const prioritizedFiles = files.sort((a, b) => Number(isBundledPath(b)) - Number(isBundledPath(a)));
|
|
141
|
-
|
|
184
|
+
|
|
142
185
|
for (const file of prioritizedFiles) {
|
|
143
186
|
try {
|
|
144
187
|
translations = readJsonSafe(file);
|
|
145
188
|
currentLanguage = lang;
|
|
146
189
|
isInitialized = true;
|
|
147
|
-
|
|
190
|
+
|
|
148
191
|
if (process.env.I18NTK_DEBUG_LOCALES === '1') {
|
|
149
192
|
console.log(`🗂 Loaded UI locale → ${file} (${lang})`);
|
|
150
193
|
}
|
|
151
|
-
|
|
194
|
+
|
|
152
195
|
// Validate translations object
|
|
153
196
|
if (typeof translations === 'object' && translations !== null) {
|
|
154
197
|
return translations;
|
|
@@ -199,11 +242,11 @@ function loadTranslations(language) {
|
|
|
199
242
|
};
|
|
200
243
|
currentLanguage = 'en';
|
|
201
244
|
isInitialized = true;
|
|
202
|
-
|
|
245
|
+
|
|
203
246
|
if (loadErrors.length > 0) {
|
|
204
247
|
console.warn(`⚠️ No valid UI locale files found. Using built-in English strings.`);
|
|
205
248
|
}
|
|
206
|
-
|
|
249
|
+
|
|
207
250
|
return translations;
|
|
208
251
|
}
|
|
209
252
|
|
|
@@ -219,7 +262,7 @@ function t(key, params = {}) {
|
|
|
219
262
|
loadTranslations();
|
|
220
263
|
isInitialized = true;
|
|
221
264
|
}
|
|
222
|
-
|
|
265
|
+
|
|
223
266
|
// Split the key into parts (e.g., 'module.subkey' -> ['module', 'subkey'])
|
|
224
267
|
const keyParts = key.split('.');
|
|
225
268
|
let value = translations;
|
|
@@ -258,12 +301,12 @@ function t(key, params = {}) {
|
|
|
258
301
|
}
|
|
259
302
|
return key;
|
|
260
303
|
}
|
|
261
|
-
|
|
304
|
+
|
|
262
305
|
// If we found a string, interpolate parameters
|
|
263
306
|
if (typeof value === 'string') {
|
|
264
307
|
return interpolateParams(value, params);
|
|
265
308
|
}
|
|
266
|
-
|
|
309
|
+
|
|
267
310
|
// Return the key if the final value is not a string
|
|
268
311
|
console.warn(`Translation key does not resolve to a string: ${key}`);
|
|
269
312
|
return key;
|
|
@@ -309,12 +352,13 @@ function getAvailableLanguages() {
|
|
|
309
352
|
const langs = new Set();
|
|
310
353
|
for (const d of dirs) {
|
|
311
354
|
try {
|
|
312
|
-
|
|
355
|
+
const SecurityUtils = getSecurityUtils();
|
|
356
|
+
if (!SecurityUtils.safeExistsSync(d)) continue;
|
|
313
357
|
for (const f of fs.readdirSync(d)) {
|
|
314
358
|
if (f.endsWith('.json')) langs.add(path.basename(f, '.json'));
|
|
315
359
|
}
|
|
316
360
|
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
317
|
-
if (f.isDirectory() &&
|
|
361
|
+
if (f.isDirectory() && SecurityUtils.safeExistsSync(path.join(d, f.name, `${f.name}.json`))) {
|
|
318
362
|
langs.add(f.name);
|
|
319
363
|
}
|
|
320
364
|
}
|
|
@@ -347,11 +391,11 @@ function deepMerge(target, source) {
|
|
|
347
391
|
* Refresh language from settings manager
|
|
348
392
|
* This ensures translations stay in sync with settings changes
|
|
349
393
|
*/
|
|
350
|
-
function refreshLanguageFromSettings() {
|
|
351
|
-
const cfg = safeRequireConfig();
|
|
352
|
-
const settings = cfg?.getConfig?.() || {};
|
|
353
|
-
const configuredLanguage = settings.
|
|
354
|
-
|
|
394
|
+
function refreshLanguageFromSettings() {
|
|
395
|
+
const cfg = safeRequireConfig();
|
|
396
|
+
const settings = cfg?.getConfig?.() || {};
|
|
397
|
+
const configuredLanguage = settings.uiLanguage || settings.language || 'en';
|
|
398
|
+
|
|
355
399
|
if (configuredLanguage !== currentLanguage) {
|
|
356
400
|
isInitialized = false;
|
|
357
401
|
loadTranslations(configuredLanguage);
|
|
@@ -373,4 +417,4 @@ module.exports = {
|
|
|
373
417
|
deepMerge,
|
|
374
418
|
refreshTranslations,
|
|
375
419
|
refreshLanguageFromSettings
|
|
376
|
-
};
|
|
420
|
+
};
|