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/utils/cli-helper.js
CHANGED
|
@@ -124,106 +124,34 @@ class CliHelper {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
// Utility: Show framework suggestion warning only once per process
|
|
127
|
-
let frameworkWarningShown = false;
|
|
128
|
-
function showFrameworkWarningOnce(ui) {
|
|
129
|
-
if (frameworkWarningShown) return;
|
|
130
|
-
frameworkWarningShown = true;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const keys = key.split('.');
|
|
153
|
-
let result = translations;
|
|
154
|
-
for (const k of keys) {
|
|
155
|
-
result = result && result[k];
|
|
156
|
-
}
|
|
157
|
-
return result || key;
|
|
158
|
-
};
|
|
159
|
-
} else {
|
|
160
|
-
throw new Error('Locale file not found');
|
|
161
|
-
}
|
|
162
|
-
} catch (fallbackError) {
|
|
163
|
-
// Final fallback: load from current UI locale or English
|
|
164
|
-
try {
|
|
165
|
-
const path = require('path');
|
|
166
|
-
const fs = require('fs');
|
|
167
|
-
|
|
168
|
-
// Try to determine current language from settings
|
|
169
|
-
const settingsManager = require('../settings/settings-manager');
|
|
170
|
-
const settings = settingsManager.loadSettings();
|
|
171
|
-
const currentLang = settings.uiLanguage || 'en';
|
|
172
|
-
|
|
173
|
-
const localePath = path.join(__dirname, '..', 'resources', 'i18n', 'ui-locales', `${currentLang}.json`);
|
|
174
|
-
if (SecurityUtils.safeExistsSync(localePath)) {
|
|
175
|
-
const translations = JSON.parse(SecurityUtils.safeReadFileSync(localePath, path.dirname(localePath), 'utf8'));
|
|
176
|
-
t = (key) => {
|
|
177
|
-
const keys = key.split('.');
|
|
178
|
-
let result = translations;
|
|
179
|
-
for (const k of keys) {
|
|
180
|
-
result = result && result[k];
|
|
181
|
-
}
|
|
182
|
-
return result || key;
|
|
183
|
-
};
|
|
184
|
-
} else {
|
|
185
|
-
// Fallback to English
|
|
186
|
-
const enLocalePath = path.join(__dirname, '..', 'resources', 'i18n', 'ui-locales', 'en.json');
|
|
187
|
-
if (SecurityUtils.safeExistsSync(enLocalePath)) {
|
|
188
|
-
const translations = JSON.parse(SecurityUtils.safeReadFileSync(enLocalePath, path.dirname(enLocalePath), 'utf8'));
|
|
189
|
-
t = (key) => {
|
|
190
|
-
const keys = key.split('.');
|
|
191
|
-
let result = translations;
|
|
192
|
-
for (const k of keys) {
|
|
193
|
-
result = result && result[k];
|
|
194
|
-
}
|
|
195
|
-
return result || key;
|
|
196
|
-
};
|
|
197
|
-
} else {
|
|
198
|
-
throw new Error('No locale files found');
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
} catch (finalError) {
|
|
202
|
-
// Absolute last resort: basic hardcoded fallback
|
|
203
|
-
const messages = {
|
|
204
|
-
'init.suggestions.noFramework': 'No i18n framework detected. Consider using one of the following:',
|
|
205
|
-
'init.frameworks.react': ' - React i18next (react-i18next)',
|
|
206
|
-
'init.frameworks.vue': ' - Vue i18n (vue-i18next)',
|
|
207
|
-
'init.frameworks.i18next': ' - i18next (i18next)',
|
|
208
|
-
'init.frameworks.nuxt': ' - Nuxt i18n (@nuxtjs/i18next)',
|
|
209
|
-
'init.frameworks.svelte': ' - Svelte i18n (svelte-i18next)'
|
|
210
|
-
};
|
|
211
|
-
t = (key) => messages[key] || key;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
console.log(t('init.suggestions.noFramework'));
|
|
218
|
-
console.log(t('init.frameworks.react'));
|
|
219
|
-
console.log(t('init.frameworks.vue'));
|
|
220
|
-
console.log(t('init.frameworks.i18next'));
|
|
221
|
-
console.log(t('init.frameworks.nuxt'));
|
|
222
|
-
console.log(t('init.frameworks.svelte'));
|
|
223
|
-
}
|
|
127
|
+
let frameworkWarningShown = false;
|
|
128
|
+
function showFrameworkWarningOnce(ui) {
|
|
129
|
+
if (frameworkWarningShown) return;
|
|
130
|
+
frameworkWarningShown = true;
|
|
131
|
+
|
|
132
|
+
const translate = ui && typeof ui.t === 'function'
|
|
133
|
+
? ui.t.bind(ui)
|
|
134
|
+
: require('./i18n-helper').t;
|
|
135
|
+
|
|
136
|
+
const fallback = {
|
|
137
|
+
noFramework: 'No i18n framework detected. Consider using one of the following:',
|
|
138
|
+
react: ' - React i18next (react-i18next)',
|
|
139
|
+
vue: ' - Vue i18n (vue-i18next)',
|
|
140
|
+
i18next: ' - i18next (i18next)',
|
|
141
|
+
nuxt: ' - Nuxt i18n (@nuxtjs/i18n)',
|
|
142
|
+
svelte: ' - Svelte i18n (svelte-i18n)'
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
console.log(translate('init.suggestions.noFramework') || fallback.noFramework);
|
|
146
|
+
console.log(translate('init.frameworks.react') || fallback.react);
|
|
147
|
+
console.log(translate('init.frameworks.vue') || fallback.vue);
|
|
148
|
+
console.log(translate('init.frameworks.i18next') || fallback.i18next);
|
|
149
|
+
console.log(translate('init.frameworks.nuxt') || fallback.nuxt);
|
|
150
|
+
console.log(translate('init.frameworks.svelte') || fallback.svelte);
|
|
151
|
+
}
|
|
224
152
|
|
|
225
153
|
// Export a singleton instance and attach utility for backward compatibility
|
|
226
154
|
const cliHelper = new CliHelper();
|
|
227
155
|
cliHelper.showFrameworkWarningOnce = showFrameworkWarningOnce;
|
|
228
156
|
|
|
229
|
-
module.exports = cliHelper;
|
|
157
|
+
module.exports = cliHelper;
|
|
@@ -1,21 +1,48 @@
|
|
|
1
|
-
function
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
function normalizeKeyCandidate(rawKey) {
|
|
2
|
+
if (rawKey === null || rawKey === undefined) return null;
|
|
3
|
+
|
|
4
|
+
let key = String(rawKey).trim();
|
|
5
|
+
if (!key) return null;
|
|
6
|
+
|
|
7
|
+
// Normalize dynamic template segments to wildcard form.
|
|
8
|
+
key = key.replace(/\$\{[^}]+\}/g, '*');
|
|
9
|
+
|
|
10
|
+
// Reject multiline keys and obvious code fragments.
|
|
11
|
+
if (/[\r\n\t]/.test(key)) return null;
|
|
12
|
+
if (/\s/.test(key)) return null;
|
|
13
|
+
if (/(=>|\|\||&&|function\b|return\b|includes\()/i.test(key)) return null;
|
|
14
|
+
|
|
15
|
+
// Allow typical i18n key character sets only.
|
|
16
|
+
if (!/^[A-Za-z0-9_.:*-]+$/.test(key)) return null;
|
|
17
|
+
|
|
18
|
+
// Reject malformed key shapes.
|
|
19
|
+
if (key.startsWith('.') || key.endsWith('.')) return null;
|
|
20
|
+
if (key.includes('..')) return null;
|
|
21
|
+
|
|
22
|
+
return key;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extract(content, patterns = []) {
|
|
26
|
+
const keys = new Set();
|
|
27
|
+
if (!Array.isArray(patterns)) return [];
|
|
28
|
+
if (content === null || content === undefined) return [];
|
|
5
29
|
const contentStr = String(content);
|
|
6
30
|
for (const pattern of patterns) {
|
|
7
31
|
try {
|
|
8
|
-
let regex = pattern instanceof RegExp ? new RegExp(pattern.source, 'g') : new RegExp(pattern, 'g');
|
|
9
|
-
let match;
|
|
10
|
-
while ((match = regex.exec(contentStr)) !== null) {
|
|
11
|
-
if (match[1])
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
let regex = pattern instanceof RegExp ? new RegExp(pattern.source, 'g') : new RegExp(pattern, 'g');
|
|
33
|
+
let match;
|
|
34
|
+
while ((match = regex.exec(contentStr)) !== null) {
|
|
35
|
+
if (match[1]) {
|
|
36
|
+
const normalized = normalizeKeyCandidate(match[1]);
|
|
37
|
+
if (normalized) keys.add(normalized);
|
|
38
|
+
}
|
|
39
|
+
if (regex.lastIndex === 0) break;
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
15
42
|
// skip invalid patterns
|
|
16
43
|
}
|
|
17
44
|
}
|
|
18
45
|
return Array.from(keys);
|
|
19
46
|
}
|
|
20
47
|
|
|
21
|
-
module.exports = { extract };
|
|
48
|
+
module.exports = { extract };
|
package/utils/i18n-helper.js
CHANGED
|
@@ -1,38 +1,39 @@
|
|
|
1
1
|
// utils/i18n-helper.js
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
4
|
|
|
5
5
|
// Lazy load SecurityUtils to prevent circular dependencies
|
|
6
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: (
|
|
15
|
-
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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: (targetPath) => {
|
|
15
|
+
try {
|
|
16
|
+
require('fs').accessSync(targetPath);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
safeWriteFileSync: (targetPath, data, _basePath, encoding = 'utf8') => {
|
|
23
|
+
try {
|
|
24
|
+
return require('fs').writeFileSync(targetPath, data, encoding);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
safeReadFileSync: (targetPath, _basePath, encoding = 'utf8') => {
|
|
30
|
+
try {
|
|
31
|
+
return require('fs').readFileSync(targetPath, encoding);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
return securityUtils;
|
|
@@ -49,54 +50,105 @@ function safeRequireConfig() {
|
|
|
49
50
|
try { return require('./config-manager'); } catch { return null; }
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
function stripBOMAndComments(s) {
|
|
53
|
-
if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
|
|
54
|
-
s = s.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
55
|
-
s = s.replace(/^\s*\/\/.*$/mg, '');
|
|
56
|
-
return s;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
53
|
+
function stripBOMAndComments(s) {
|
|
54
|
+
if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
|
|
55
|
+
s = s.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
56
|
+
s = s.replace(/^\s*\/\/.*$/mg, '');
|
|
57
|
+
return s;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getValidationBase(targetPath) {
|
|
61
|
+
const fallbackBase = path.resolve(__dirname, '..');
|
|
62
|
+
if (!targetPath || typeof targetPath !== 'string') {
|
|
63
|
+
return fallbackBase;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let current = path.resolve(path.dirname(targetPath));
|
|
67
|
+
while (true) {
|
|
68
|
+
try {
|
|
69
|
+
if (fs.statSync(current).isDirectory()) {
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Continue upward until we find an existing directory.
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const parent = path.dirname(current);
|
|
77
|
+
if (parent === current) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
current = parent;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return fallbackBase;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function safeReadFile(targetPath, encoding = 'utf8') {
|
|
87
|
+
const SecurityUtils = getSecurityUtils();
|
|
88
|
+
return SecurityUtils.safeReadFileSync(targetPath, getValidationBase(targetPath), encoding);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readJsonSafe(file) {
|
|
92
|
+
const raw = safeReadFile(file, 'utf8');
|
|
93
|
+
if (raw === null) {
|
|
94
|
+
throw new Error(`Unable to read JSON file: ${file}`);
|
|
95
|
+
}
|
|
96
|
+
return JSON.parse(stripBOMAndComments(raw));
|
|
97
|
+
}
|
|
64
98
|
|
|
65
99
|
function pkgUiLocalesDirViaThisFile() {
|
|
66
100
|
return path.resolve(__dirname, '..', 'ui-locales');
|
|
67
101
|
}
|
|
68
102
|
|
|
69
|
-
function pkgUiLocalesDirViaResolve() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
function pkgUiLocalesDirViaResolve() {
|
|
104
|
+
try {
|
|
105
|
+
// Try the new correct path first (ui-locales/en.json)
|
|
106
|
+
const enJson = require.resolve('i18ntk/ui-locales/en.json');
|
|
107
|
+
return path.dirname(enJson);
|
|
108
|
+
} catch {
|
|
109
|
+
try {
|
|
110
|
+
// Fallback to the old incorrect path for backward compatibility
|
|
111
|
+
const enJson = require.resolve('i18ntk/resources/i18n/ui-locales/en.json');
|
|
112
|
+
return path.dirname(enJson);
|
|
113
|
+
} catch { return null; }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function legacyResourcesUiLocalesDir() {
|
|
118
|
+
return path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveLocalesDirs(preferredDir) {
|
|
122
|
+
const SecurityUtils = getSecurityUtils();
|
|
123
|
+
const dirs = [];
|
|
124
|
+
const addDir = (dir) => {
|
|
125
|
+
if (typeof dir === 'string' && dir.trim()) {
|
|
126
|
+
try {
|
|
127
|
+
const normalized = path.normalize(path.resolve(dir.trim()));
|
|
128
|
+
const stats = SecurityUtils.safeStatSync(normalized, getValidationBase(normalized));
|
|
129
|
+
if (stats && stats.isDirectory()) {
|
|
130
|
+
dirs.push(normalized);
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Silently ignore invalid paths
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
addDir(preferredDir);
|
|
139
|
+
|
|
140
|
+
const pkgA = pkgUiLocalesDirViaThisFile();
|
|
141
|
+
addDir(pkgA);
|
|
142
|
+
|
|
143
|
+
const pkgB = pkgUiLocalesDirViaResolve();
|
|
144
|
+
if (pkgB && pkgB !== pkgA) {
|
|
145
|
+
addDir(pkgB);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const legacyDir = legacyResourcesUiLocalesDir();
|
|
149
|
+
if (legacyDir !== pkgA && legacyDir !== pkgB) {
|
|
150
|
+
addDir(legacyDir);
|
|
151
|
+
}
|
|
100
152
|
|
|
101
153
|
// Deduplicate while preserving order
|
|
102
154
|
const seen = new Set();
|
|
@@ -114,8 +166,9 @@ function candidatesForLang(dir, lang) {
|
|
|
114
166
|
];
|
|
115
167
|
}
|
|
116
168
|
|
|
117
|
-
function findLocaleFilesAllDirs(lang) {
|
|
118
|
-
const
|
|
169
|
+
function findLocaleFilesAllDirs(lang, preferredDir) {
|
|
170
|
+
const SecurityUtils = getSecurityUtils();
|
|
171
|
+
const dirs = resolveLocalesDirs(preferredDir);
|
|
119
172
|
|
|
120
173
|
if (process.env.I18NTK_DEBUG_LOCALES === '1') {
|
|
121
174
|
console.log('🔎 i18ntk locale search dirs:', dirs);
|
|
@@ -124,32 +177,29 @@ function findLocaleFilesAllDirs(lang) {
|
|
|
124
177
|
const files = [];
|
|
125
178
|
const errors = [];
|
|
126
179
|
|
|
127
|
-
for (const dir of dirs) {
|
|
128
|
-
for (const candidate of candidatesForLang(dir, lang)) {
|
|
129
|
-
try {
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
errors.push({ file: candidate, error: error.message });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
180
|
+
for (const dir of dirs) {
|
|
181
|
+
for (const candidate of candidatesForLang(dir, lang)) {
|
|
182
|
+
try {
|
|
183
|
+
const stats = SecurityUtils.safeStatSync(candidate, getValidationBase(candidate));
|
|
184
|
+
if (stats && stats.isFile() && stats.size > 0) {
|
|
185
|
+
// Validate file is readable and parseable
|
|
186
|
+
fs.accessSync(candidate, fs.constants.R_OK);
|
|
187
|
+
// Quick JSON validation
|
|
188
|
+
const content = safeReadFile(candidate, 'utf8');
|
|
189
|
+
if (content) {
|
|
190
|
+
if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
|
|
191
|
+
files.push(candidate);
|
|
192
|
+
} else {
|
|
193
|
+
errors.push({ file: candidate, error: 'Invalid JSON format' });
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
errors.push({ file: candidate, error: 'Empty or unreadable file' });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
errors.push({ file: candidate, error: error.message });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
153
203
|
}
|
|
154
204
|
|
|
155
205
|
if (process.env.I18NTK_DEBUG_LOCALES === '1' && errors.length > 0) {
|
|
@@ -165,19 +215,21 @@ let isInitialized = false;
|
|
|
165
215
|
const missingWarned = new Set();
|
|
166
216
|
|
|
167
217
|
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();
|
|
218
|
+
const cfg = safeRequireConfig();
|
|
219
|
+
const settings = cfg?.getConfig?.() || {};
|
|
220
|
+
const configuredLanguage = settings.uiLanguage || settings.language;
|
|
221
|
+
|
|
222
|
+
// Prioritize CLI argument, then UI language, then language fallback
|
|
223
|
+
const requested = (language || configuredLanguage || 'en').toString();
|
|
174
224
|
const short = requested.split('-')[0].toLowerCase();
|
|
175
225
|
const tryOrder = [requested, short, 'en'];
|
|
176
226
|
|
|
177
227
|
const loadErrors = [];
|
|
178
228
|
|
|
179
|
-
|
|
180
|
-
|
|
229
|
+
const preferredDir = arguments.length > 1 ? arguments[1] : null;
|
|
230
|
+
|
|
231
|
+
for (const lang of tryOrder) {
|
|
232
|
+
const files = findLocaleFilesAllDirs(lang, preferredDir);
|
|
181
233
|
|
|
182
234
|
// Prioritize bundled locales over project ones
|
|
183
235
|
const prioritizedFiles = files.sort((a, b) => Number(isBundledPath(b)) - Number(isBundledPath(a)));
|
|
@@ -350,19 +402,20 @@ function getCurrentLanguage() {
|
|
|
350
402
|
function getAvailableLanguages() {
|
|
351
403
|
const dirs = resolveLocalesDirs();
|
|
352
404
|
const langs = new Set();
|
|
353
|
-
for (const d of dirs) {
|
|
354
|
-
try {
|
|
355
|
-
const SecurityUtils = getSecurityUtils();
|
|
356
|
-
if (!SecurityUtils.safeExistsSync(d)) continue;
|
|
357
|
-
for (const f of fs.readdirSync(d)) {
|
|
358
|
-
if (f.endsWith('.json')) langs.add(path.basename(f, '.json'));
|
|
359
|
-
}
|
|
360
|
-
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
405
|
+
for (const d of dirs) {
|
|
406
|
+
try {
|
|
407
|
+
const SecurityUtils = getSecurityUtils();
|
|
408
|
+
if (!SecurityUtils.safeExistsSync(d, getValidationBase(d))) continue;
|
|
409
|
+
for (const f of fs.readdirSync(d)) {
|
|
410
|
+
if (f.endsWith('.json')) langs.add(path.basename(f, '.json'));
|
|
411
|
+
}
|
|
412
|
+
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
413
|
+
const nestedPath = path.join(d, f.name, `${f.name}.json`);
|
|
414
|
+
if (f.isDirectory() && SecurityUtils.safeExistsSync(nestedPath, getValidationBase(nestedPath))) {
|
|
415
|
+
langs.add(f.name);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} catch {}
|
|
366
419
|
}
|
|
367
420
|
return Array.from(langs.size ? langs : new Set(['en']));
|
|
368
421
|
}
|
|
@@ -391,10 +444,10 @@ function deepMerge(target, source) {
|
|
|
391
444
|
* Refresh language from settings manager
|
|
392
445
|
* This ensures translations stay in sync with settings changes
|
|
393
446
|
*/
|
|
394
|
-
function refreshLanguageFromSettings() {
|
|
395
|
-
const cfg = safeRequireConfig();
|
|
396
|
-
const settings = cfg?.getConfig?.() || {};
|
|
397
|
-
const configuredLanguage = settings.uiLanguage || settings.language || 'en';
|
|
447
|
+
function refreshLanguageFromSettings() {
|
|
448
|
+
const cfg = safeRequireConfig();
|
|
449
|
+
const settings = cfg?.getConfig?.() || {};
|
|
450
|
+
const configuredLanguage = settings.uiLanguage || settings.language || 'en';
|
|
398
451
|
|
|
399
452
|
if (configuredLanguage !== currentLanguage) {
|
|
400
453
|
isInitialized = false;
|
|
@@ -417,4 +470,4 @@ module.exports = {
|
|
|
417
470
|
deepMerge,
|
|
418
471
|
refreshTranslations,
|
|
419
472
|
refreshLanguageFromSettings
|
|
420
|
-
};
|
|
473
|
+
};
|
|
@@ -125,14 +125,12 @@ class SecurityChecker {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
// Check for dangerous
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
/
|
|
132
|
-
/
|
|
133
|
-
|
|
134
|
-
/Function\s*\(/g
|
|
135
|
-
];
|
|
128
|
+
// Check for dangerous code execution patterns in SecurityUtils itself.
|
|
129
|
+
// Direct fs usage is expected here because this module provides vetted wrappers.
|
|
130
|
+
const dangerousPatterns = [
|
|
131
|
+
/eval\s*\(/g,
|
|
132
|
+
/Function\s*\(/g
|
|
133
|
+
];
|
|
136
134
|
|
|
137
135
|
for (const pattern of dangerousPatterns) {
|
|
138
136
|
const matches = content.match(pattern);
|
|
@@ -251,11 +249,16 @@ class SecurityChecker {
|
|
|
251
249
|
// No action needed for legitimate package requires
|
|
252
250
|
}
|
|
253
251
|
|
|
254
|
-
async checkFilePermissions() {
|
|
255
|
-
this.log('Checking file permissions...');
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
async checkFilePermissions() {
|
|
253
|
+
this.log('Checking file permissions...');
|
|
254
|
+
|
|
255
|
+
// POSIX permission checks are noisy/non-actionable on Windows.
|
|
256
|
+
if (process.platform === 'win32') {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const criticalFiles = [
|
|
261
|
+
'utils/security.js',
|
|
259
262
|
'tests/security.test.js',
|
|
260
263
|
'package.json'
|
|
261
264
|
];
|
package/utils/security-fixed.js
CHANGED
|
@@ -464,10 +464,12 @@ class SecurityUtils {
|
|
|
464
464
|
'theme', 'ui', 'setup', 'reports', 'display', 'interface',
|
|
465
465
|
// Security and settings
|
|
466
466
|
'security', 'settings', 'preferences', 'config', 'configuration',
|
|
467
|
-
// Additional common properties
|
|
468
|
-
'autoSave', 'autoBackup', 'validateOnSave', 'showWarnings', 'verbose',
|
|
469
|
-
'timeout', 'retries', 'batchSize', 'maxConcurrency', 'cacheEnabled'
|
|
470
|
-
|
|
467
|
+
// Additional common properties
|
|
468
|
+
'autoSave', 'autoBackup', 'validateOnSave', 'showWarnings', 'verbose',
|
|
469
|
+
'timeout', 'retries', 'batchSize', 'maxConcurrency', 'cacheEnabled',
|
|
470
|
+
// Date and reporting options used by existing settings
|
|
471
|
+
'dateFormat', 'timeFormat', 'timezone', 'reportLanguage', 'dateTime'
|
|
472
|
+
]);
|
|
471
473
|
|
|
472
474
|
// Remove unknown properties
|
|
473
475
|
Object.keys(sanitized).forEach(key => {
|
package/utils/security.js
CHANGED
|
@@ -531,10 +531,12 @@ class SecurityUtils {
|
|
|
531
531
|
'theme', 'ui', 'setup', 'reports', 'display', 'interface',
|
|
532
532
|
// Security and settings
|
|
533
533
|
'security', 'settings', 'preferences', 'config', 'configuration',
|
|
534
|
-
// Additional common properties
|
|
535
|
-
'autoSave', 'autoBackup', 'validateOnSave', 'showWarnings', 'verbose',
|
|
536
|
-
'timeout', 'retries', 'batchSize', 'maxConcurrency', 'cacheEnabled'
|
|
537
|
-
|
|
534
|
+
// Additional common properties
|
|
535
|
+
'autoSave', 'autoBackup', 'validateOnSave', 'showWarnings', 'verbose',
|
|
536
|
+
'timeout', 'retries', 'batchSize', 'maxConcurrency', 'cacheEnabled',
|
|
537
|
+
// Date and reporting options used by existing settings
|
|
538
|
+
'dateFormat', 'timeFormat', 'timezone', 'reportLanguage', 'dateTime'
|
|
539
|
+
]);
|
|
538
540
|
|
|
539
541
|
// Remove unknown properties
|
|
540
542
|
Object.keys(sanitized).forEach(key => {
|