i18ntk 2.0.4 → 2.2.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 +38 -60
- package/main/i18ntk-analyze.js +49 -44
- package/main/i18ntk-complete.js +75 -74
- package/main/i18ntk-fixer.js +3 -3
- package/main/i18ntk-init.js +5 -5
- package/main/i18ntk-scanner.js +2 -2
- package/main/i18ntk-sizing.js +35 -35
- package/main/i18ntk-summary.js +4 -4
- package/main/i18ntk-ui.js +54 -8
- package/main/i18ntk-usage.js +14 -14
- package/main/i18ntk-validate.js +6 -5
- package/main/manage/commands/AnalyzeCommand.js +40 -35
- package/main/manage/commands/FixerCommand.js +2 -2
- package/main/manage/commands/ScannerCommand.js +2 -2
- package/main/manage/commands/ValidateCommand.js +9 -9
- package/main/manage/index.js +147 -75
- package/main/manage/managers/LanguageMenu.js +7 -2
- package/main/manage/services/UsageService.js +7 -7
- package/package.json +269 -290
- package/settings/settings-cli.js +3 -3
- package/ui-locales/de.json +161 -166
- package/ui-locales/en.json +13 -18
- package/ui-locales/es.json +171 -184
- package/ui-locales/fr.json +155 -161
- package/ui-locales/ja.json +192 -243
- package/ui-locales/ru.json +145 -196
- package/ui-locales/zh.json +179 -185
- package/utils/cli-helper.js +26 -98
- package/utils/extractors/regex.js +39 -12
- package/utils/i18n-helper.js +88 -40
- package/{scripts → utils}/locale-optimizer.js +61 -60
- package/utils/security-check-improved.js +16 -13
- package/utils/security.js +6 -4
- package/main/i18ntk-go.js +0 -283
- package/main/i18ntk-java.js +0 -380
- package/main/i18ntk-js.js +0 -512
- package/main/i18ntk-manage.js +0 -1694
- package/main/i18ntk-php.js +0 -462
- package/main/i18ntk-py.js +0 -379
- package/main/i18ntk-settings.js +0 -23
- package/main/manage/index-fixed.js +0 -1447
- package/main/manage/services/ConfigurationService-fixed.js +0 -449
- package/scripts/build-lite.js +0 -279
- package/scripts/deprecate-versions.js +0 -317
- package/scripts/export-translations.js +0 -84
- package/scripts/fix-all-i18n.js +0 -215
- package/scripts/fix-and-purify-i18n.js +0 -213
- package/scripts/fix-locale-control-chars.js +0 -110
- package/scripts/lint-locales.js +0 -80
- package/scripts/prepublish.js +0 -348
- package/scripts/security-check.js +0 -117
- package/scripts/sync-translations.js +0 -151
- package/scripts/sync-ui-locales.js +0 -20
- package/scripts/validate-all-translations.js +0 -139
- package/scripts/verify-deprecations.js +0 -157
- package/scripts/verify-translations.js +0 -63
- package/utils/security-fixed.js +0 -607
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
|
@@ -11,23 +11,24 @@ function getSecurityUtils() {
|
|
|
11
11
|
} catch (error) {
|
|
12
12
|
// Fallback: use basic fs operations if SecurityUtils is not available
|
|
13
13
|
return {
|
|
14
|
-
safeExistsSync: (
|
|
14
|
+
safeExistsSync: (targetPath) => {
|
|
15
15
|
try {
|
|
16
|
-
|
|
16
|
+
require('fs').accessSync(targetPath);
|
|
17
|
+
return true;
|
|
17
18
|
} catch {
|
|
18
19
|
return false;
|
|
19
20
|
}
|
|
20
21
|
},
|
|
21
|
-
safeWriteFileSync: (
|
|
22
|
+
safeWriteFileSync: (targetPath, data, _basePath, encoding = 'utf8') => {
|
|
22
23
|
try {
|
|
23
|
-
return require('fs').writeFileSync(
|
|
24
|
+
return require('fs').writeFileSync(targetPath, data, encoding);
|
|
24
25
|
} catch {
|
|
25
26
|
return null;
|
|
26
27
|
}
|
|
27
28
|
},
|
|
28
|
-
safeReadFileSync: (
|
|
29
|
+
safeReadFileSync: (targetPath, _basePath, encoding = 'utf8') => {
|
|
29
30
|
try {
|
|
30
|
-
return require('fs').readFileSync(
|
|
31
|
+
return require('fs').readFileSync(targetPath, encoding);
|
|
31
32
|
} catch {
|
|
32
33
|
return null;
|
|
33
34
|
}
|
|
@@ -56,9 +57,42 @@ function stripBOMAndComments(s) {
|
|
|
56
57
|
return s;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function
|
|
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') {
|
|
60
87
|
const SecurityUtils = getSecurityUtils();
|
|
61
|
-
|
|
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
|
+
}
|
|
62
96
|
return JSON.parse(stripBOMAndComments(raw));
|
|
63
97
|
}
|
|
64
98
|
|
|
@@ -67,21 +101,30 @@ function pkgUiLocalesDirViaThisFile() {
|
|
|
67
101
|
}
|
|
68
102
|
|
|
69
103
|
function pkgUiLocalesDirViaResolve() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
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
|
+
// Removed legacyResourcesUiLocalesDir as resources/i18n/ui-locales is deprecated
|
|
75
118
|
|
|
76
|
-
function resolveLocalesDirs() {
|
|
119
|
+
function resolveLocalesDirs(preferredDir) {
|
|
120
|
+
const SecurityUtils = getSecurityUtils();
|
|
77
121
|
const dirs = [];
|
|
78
122
|
const addDir = (dir) => {
|
|
79
123
|
if (typeof dir === 'string' && dir.trim()) {
|
|
80
124
|
try {
|
|
81
125
|
const normalized = path.normalize(path.resolve(dir.trim()));
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (SecurityUtils.safeExistsSync(normalized) && fs.statSync(normalized).isDirectory()) {
|
|
126
|
+
const stats = SecurityUtils.safeStatSync(normalized, getValidationBase(normalized));
|
|
127
|
+
if (stats && stats.isDirectory()) {
|
|
85
128
|
dirs.push(normalized);
|
|
86
129
|
}
|
|
87
130
|
} catch {
|
|
@@ -90,6 +133,8 @@ function resolveLocalesDirs() {
|
|
|
90
133
|
}
|
|
91
134
|
};
|
|
92
135
|
|
|
136
|
+
addDir(preferredDir);
|
|
137
|
+
|
|
93
138
|
const pkgA = pkgUiLocalesDirViaThisFile();
|
|
94
139
|
addDir(pkgA);
|
|
95
140
|
|
|
@@ -98,6 +143,8 @@ function resolveLocalesDirs() {
|
|
|
98
143
|
addDir(pkgB);
|
|
99
144
|
}
|
|
100
145
|
|
|
146
|
+
// Removed legacy directory fallback
|
|
147
|
+
|
|
101
148
|
// Deduplicate while preserving order
|
|
102
149
|
const seen = new Set();
|
|
103
150
|
return dirs.filter(dir => {
|
|
@@ -114,8 +161,9 @@ function candidatesForLang(dir, lang) {
|
|
|
114
161
|
];
|
|
115
162
|
}
|
|
116
163
|
|
|
117
|
-
function findLocaleFilesAllDirs(lang) {
|
|
118
|
-
const
|
|
164
|
+
function findLocaleFilesAllDirs(lang, preferredDir) {
|
|
165
|
+
const SecurityUtils = getSecurityUtils();
|
|
166
|
+
const dirs = resolveLocalesDirs(preferredDir);
|
|
119
167
|
|
|
120
168
|
if (process.env.I18NTK_DEBUG_LOCALES === '1') {
|
|
121
169
|
console.log('🔎 i18ntk locale search dirs:', dirs);
|
|
@@ -127,14 +175,12 @@ function findLocaleFilesAllDirs(lang) {
|
|
|
127
175
|
for (const dir of dirs) {
|
|
128
176
|
for (const candidate of candidatesForLang(dir, lang)) {
|
|
129
177
|
try {
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
const stats = fs.statSync(candidate);
|
|
133
|
-
if (stats.isFile() && stats.size > 0) {
|
|
178
|
+
const stats = SecurityUtils.safeStatSync(candidate, getValidationBase(candidate));
|
|
179
|
+
if (stats && stats.isFile() && stats.size > 0) {
|
|
134
180
|
// Validate file is readable and parseable
|
|
135
181
|
fs.accessSync(candidate, fs.constants.R_OK);
|
|
136
182
|
// Quick JSON validation
|
|
137
|
-
const content =
|
|
183
|
+
const content = safeReadFile(candidate, 'utf8');
|
|
138
184
|
if (content) {
|
|
139
185
|
if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
|
|
140
186
|
files.push(candidate);
|
|
@@ -144,7 +190,6 @@ function findLocaleFilesAllDirs(lang) {
|
|
|
144
190
|
} else {
|
|
145
191
|
errors.push({ file: candidate, error: 'Empty or unreadable file' });
|
|
146
192
|
}
|
|
147
|
-
}
|
|
148
193
|
}
|
|
149
194
|
} catch (error) {
|
|
150
195
|
errors.push({ file: candidate, error: error.message });
|
|
@@ -164,20 +209,22 @@ let currentLanguage = 'en';
|
|
|
164
209
|
let isInitialized = false;
|
|
165
210
|
const missingWarned = new Set();
|
|
166
211
|
|
|
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();
|
|
212
|
+
function loadTranslations(language) {
|
|
213
|
+
const cfg = safeRequireConfig();
|
|
214
|
+
const settings = cfg?.getConfig?.() || {};
|
|
215
|
+
const configuredLanguage = settings.uiLanguage || settings.language;
|
|
216
|
+
|
|
217
|
+
// Prioritize CLI argument, then UI language, then language fallback
|
|
218
|
+
const requested = (language || configuredLanguage || 'en').toString();
|
|
174
219
|
const short = requested.split('-')[0].toLowerCase();
|
|
175
220
|
const tryOrder = [requested, short, 'en'];
|
|
176
221
|
|
|
177
222
|
const loadErrors = [];
|
|
178
223
|
|
|
224
|
+
const preferredDir = arguments.length > 1 ? arguments[1] : null;
|
|
225
|
+
|
|
179
226
|
for (const lang of tryOrder) {
|
|
180
|
-
const files = findLocaleFilesAllDirs(lang);
|
|
227
|
+
const files = findLocaleFilesAllDirs(lang, preferredDir);
|
|
181
228
|
|
|
182
229
|
// Prioritize bundled locales over project ones
|
|
183
230
|
const prioritizedFiles = files.sort((a, b) => Number(isBundledPath(b)) - Number(isBundledPath(a)));
|
|
@@ -353,12 +400,13 @@ function getAvailableLanguages() {
|
|
|
353
400
|
for (const d of dirs) {
|
|
354
401
|
try {
|
|
355
402
|
const SecurityUtils = getSecurityUtils();
|
|
356
|
-
if (!SecurityUtils.safeExistsSync(d)) continue;
|
|
403
|
+
if (!SecurityUtils.safeExistsSync(d, getValidationBase(d))) continue;
|
|
357
404
|
for (const f of fs.readdirSync(d)) {
|
|
358
405
|
if (f.endsWith('.json')) langs.add(path.basename(f, '.json'));
|
|
359
406
|
}
|
|
360
407
|
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
361
|
-
|
|
408
|
+
const nestedPath = path.join(d, f.name, `${f.name}.json`);
|
|
409
|
+
if (f.isDirectory() && SecurityUtils.safeExistsSync(nestedPath, getValidationBase(nestedPath))) {
|
|
362
410
|
langs.add(f.name);
|
|
363
411
|
}
|
|
364
412
|
}
|
|
@@ -391,10 +439,10 @@ function deepMerge(target, source) {
|
|
|
391
439
|
* Refresh language from settings manager
|
|
392
440
|
* This ensures translations stay in sync with settings changes
|
|
393
441
|
*/
|
|
394
|
-
function refreshLanguageFromSettings() {
|
|
395
|
-
const cfg = safeRequireConfig();
|
|
396
|
-
const settings = cfg?.getConfig?.() || {};
|
|
397
|
-
const configuredLanguage = settings.uiLanguage || settings.language || 'en';
|
|
442
|
+
function refreshLanguageFromSettings() {
|
|
443
|
+
const cfg = safeRequireConfig();
|
|
444
|
+
const settings = cfg?.getConfig?.() || {};
|
|
445
|
+
const configuredLanguage = settings.uiLanguage || settings.language || 'en';
|
|
398
446
|
|
|
399
447
|
if (configuredLanguage !== currentLanguage) {
|
|
400
448
|
isInitialized = false;
|
|
@@ -417,4 +465,4 @@ module.exports = {
|
|
|
417
465
|
deepMerge,
|
|
418
466
|
refreshTranslations,
|
|
419
467
|
refreshLanguageFromSettings
|
|
420
|
-
};
|
|
468
|
+
};
|