i18ntk 3.2.0 → 4.0.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/CHANGELOG.md +62 -2
- package/README.md +177 -22
- package/SECURITY.md +28 -8
- package/main/i18ntk-backup.js +305 -62
- package/main/i18ntk-complete.js +120 -49
- package/main/i18ntk-scanner.js +188 -49
- package/main/i18ntk-sizing.js +223 -29
- package/main/i18ntk-translate.js +25 -1
- package/main/i18ntk-usage.js +203 -3
- package/main/i18ntk-validate.js +107 -3
- package/main/manage/commands/FixerCommand.js +23 -21
- package/main/manage/index.js +13 -7
- package/main/manage/services/FileManagementService.js +12 -6
- package/package.json +3 -3
- package/runtime/i18ntk.d.ts +22 -16
- package/runtime/index.d.ts +9 -7
- package/runtime/index.js +240 -50
- package/ui-locales/de.json +1389 -1359
- package/ui-locales/en.json +1 -1
- package/ui-locales/es.json +1503 -1473
- package/ui-locales/fr.json +1626 -1596
- package/ui-locales/ja.json +1595 -1565
- package/ui-locales/ru.json +1638 -1608
- package/ui-locales/zh.json +1613 -1583
- package/utils/translate/api.js +164 -41
- package/utils/translate/protection.js +147 -6
- package/utils/translate/safe-network.js +280 -0
- package/utils/watch-locales.js +183 -36
package/runtime/index.js
CHANGED
|
@@ -11,13 +11,32 @@ const { envManager } = require('../utils/env-manager');
|
|
|
11
11
|
let configManager = null;
|
|
12
12
|
try { configManager = require('../utils/config-manager'); } catch (_) { /* optional */ }
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
function createState(options = {}) {
|
|
15
|
+
return {
|
|
16
|
+
baseDir: resolveBaseDir(options.baseDir),
|
|
17
|
+
language: options.language || 'en',
|
|
18
|
+
fallbackLanguage: options.fallbackLanguage || 'en',
|
|
19
|
+
keySeparator: options.keySeparator || '.',
|
|
20
|
+
cache: new Map(),
|
|
21
|
+
lazy: options.lazy === true,
|
|
22
|
+
keyManifest: new Map(),
|
|
23
|
+
loadedFiles: new Set(),
|
|
24
|
+
eagerLoadedLanguages: new Set(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const singletonState = {
|
|
15
29
|
baseDir: null, // absolute path to locales dir (e.g., ./locales)
|
|
16
30
|
language: 'en',
|
|
17
31
|
fallbackLanguage: 'en',
|
|
18
|
-
keySeparator: '.',
|
|
19
|
-
cache: new Map(), // lang -> merged translations object
|
|
20
|
-
|
|
32
|
+
keySeparator: '.',
|
|
33
|
+
cache: new Map(), // lang -> merged translations object
|
|
34
|
+
lazy: false,
|
|
35
|
+
keyManifest: new Map(), // lang -> Map: keyName -> filePath
|
|
36
|
+
loadedFiles: new Set(), // tracks loaded files in lazy mode
|
|
37
|
+
eagerLoadedLanguages: new Set(),
|
|
38
|
+
};
|
|
39
|
+
let singletonInitialized = false;
|
|
21
40
|
|
|
22
41
|
// --- Utilities ---
|
|
23
42
|
function stripBOMAndComments(s) {
|
|
@@ -98,12 +117,12 @@ function resolveBaseDir(explicitBaseDir) {
|
|
|
98
117
|
}
|
|
99
118
|
}
|
|
100
119
|
|
|
101
|
-
function listJsonFilesRecursively(dir) {
|
|
120
|
+
function listJsonFilesRecursively(dir, baseDir = dir) {
|
|
102
121
|
const results = [];
|
|
103
122
|
const stack = [dir];
|
|
104
123
|
while (stack.length) {
|
|
105
124
|
const d = stack.pop();
|
|
106
|
-
if (!SecurityUtils.safeExistsSync(d)) continue;
|
|
125
|
+
if (!SecurityUtils.safeExistsSync(d, baseDir)) continue;
|
|
107
126
|
try {
|
|
108
127
|
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
109
128
|
const full = path.join(d, entry.name);
|
|
@@ -120,6 +139,86 @@ function listJsonFilesRecursively(dir) {
|
|
|
120
139
|
return results;
|
|
121
140
|
}
|
|
122
141
|
|
|
142
|
+
function loadKeyManifestFromDir(baseDir) {
|
|
143
|
+
const validatedBase = SecurityUtils.validatePath(baseDir, path.dirname(baseDir));
|
|
144
|
+
const baseStat = SecurityUtils.safeStatSync(validatedBase, path.dirname(validatedBase));
|
|
145
|
+
const baseRoot = baseStat && baseStat.isFile() ? path.dirname(validatedBase) : validatedBase;
|
|
146
|
+
const files = baseStat && baseStat.isFile() ? [validatedBase] : listJsonFilesRecursively(validatedBase, validatedBase);
|
|
147
|
+
const manifest = new Map();
|
|
148
|
+
const MAX_SIZE = 100 * 1024;
|
|
149
|
+
let currentSize = 0;
|
|
150
|
+
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
let validated;
|
|
153
|
+
try { validated = SecurityUtils.validatePath(file, baseRoot); } catch (_) { continue; }
|
|
154
|
+
const rel = path.relative(baseRoot, validated);
|
|
155
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) continue;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const data = readJsonSafe(validated);
|
|
159
|
+
if (!data || typeof data !== 'object') continue;
|
|
160
|
+
|
|
161
|
+
for (const key of Object.keys(data)) {
|
|
162
|
+
if (manifest.has(key)) continue;
|
|
163
|
+
|
|
164
|
+
const entrySize = JSON.stringify(key).length + JSON.stringify(validated).length + 5;
|
|
165
|
+
if (currentSize + entrySize > MAX_SIZE) break;
|
|
166
|
+
|
|
167
|
+
manifest.set(key, validated);
|
|
168
|
+
currentSize += entrySize;
|
|
169
|
+
}
|
|
170
|
+
} catch (_) { continue; }
|
|
171
|
+
|
|
172
|
+
if (currentSize >= MAX_SIZE) break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return manifest;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getLanguagePath(baseDir, lang) {
|
|
179
|
+
const langDir = path.join(baseDir, lang);
|
|
180
|
+
const langFile = path.join(baseDir, `${lang}.json`);
|
|
181
|
+
const langDirStat = SecurityUtils.safeStatSync(langDir, path.dirname(langDir));
|
|
182
|
+
if (langDirStat && langDirStat.isDirectory()) return langDir;
|
|
183
|
+
const langFileStat = SecurityUtils.safeStatSync(langFile, path.dirname(langFile));
|
|
184
|
+
if (langFileStat && langFileStat.isFile()) return langFile;
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getManifestForLanguage(runtimeState, lang) {
|
|
189
|
+
if (!runtimeState.keyManifest) runtimeState.keyManifest = new Map();
|
|
190
|
+
if (runtimeState.keyManifest.has(lang)) return runtimeState.keyManifest.get(lang);
|
|
191
|
+
|
|
192
|
+
const languagePath = getLanguagePath(runtimeState.baseDir, lang);
|
|
193
|
+
const manifest = languagePath ? loadKeyManifestFromDir(languagePath) : new Map();
|
|
194
|
+
runtimeState.keyManifest.set(lang, manifest);
|
|
195
|
+
return manifest;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function loadFileLazy(runtimeState, filePath, lang) {
|
|
199
|
+
const baseDir = runtimeState.baseDir;
|
|
200
|
+
if (!baseDir) throw new Error('baseDir not initialized');
|
|
201
|
+
|
|
202
|
+
let validatedPath;
|
|
203
|
+
try { validatedPath = SecurityUtils.validatePath(filePath, baseDir); } catch (e) {
|
|
204
|
+
throw new Error(`Invalid file path for lazy load: ${e.message}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const rel = path.relative(baseDir, validatedPath);
|
|
208
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
209
|
+
throw new Error(`File outside base directory: ${filePath}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const data = readJsonSafe(validatedPath);
|
|
213
|
+
if (data && typeof data === 'object') {
|
|
214
|
+
let cacheEntry = runtimeState.cache.get(lang);
|
|
215
|
+
if (!cacheEntry) cacheEntry = {};
|
|
216
|
+
deepMerge(cacheEntry, data);
|
|
217
|
+
runtimeState.cache.set(lang, cacheEntry);
|
|
218
|
+
}
|
|
219
|
+
return data;
|
|
220
|
+
}
|
|
221
|
+
|
|
123
222
|
function readLanguageFromBase(baseDir, lang) {
|
|
124
223
|
const merged = {};
|
|
125
224
|
const langFile = path.join(baseDir, `${lang}.json`);
|
|
@@ -128,7 +227,7 @@ function readLanguageFromBase(baseDir, lang) {
|
|
|
128
227
|
// Prefer folder if exists, otherwise single file
|
|
129
228
|
const langDirStat = SecurityUtils.safeStatSync(langDir, path.dirname(langDir));
|
|
130
229
|
if (langDirStat && langDirStat.isDirectory()) {
|
|
131
|
-
const files = listJsonFilesRecursively(langDir);
|
|
230
|
+
const files = listJsonFilesRecursively(langDir, langDir);
|
|
132
231
|
for (const file of files) {
|
|
133
232
|
try {
|
|
134
233
|
const data = readJsonSafe(file);
|
|
@@ -150,10 +249,19 @@ function readLanguageFromBase(baseDir, lang) {
|
|
|
150
249
|
return merged;
|
|
151
250
|
}
|
|
152
251
|
|
|
153
|
-
function
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
252
|
+
function getTranslationsForState(runtimeState, lang) {
|
|
253
|
+
if (!runtimeState.baseDir) runtimeState.baseDir = resolveBaseDir();
|
|
254
|
+
|
|
255
|
+
if (runtimeState.lazy) {
|
|
256
|
+
if (runtimeState.cache.has(lang)) return runtimeState.cache.get(lang);
|
|
257
|
+
runtimeState.cache.set(lang, {});
|
|
258
|
+
getManifestForLanguage(runtimeState, lang);
|
|
259
|
+
return runtimeState.cache.get(lang);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (runtimeState.cache.has(lang)) return runtimeState.cache.get(lang);
|
|
263
|
+
const data = readLanguageFromBase(runtimeState.baseDir, lang);
|
|
264
|
+
runtimeState.cache.set(lang, data);
|
|
157
265
|
return data;
|
|
158
266
|
}
|
|
159
267
|
|
|
@@ -165,15 +273,38 @@ function interpolate(template, params) {
|
|
|
165
273
|
}
|
|
166
274
|
|
|
167
275
|
// Resolve a dotted key path from an object
|
|
168
|
-
function resolveKey(obj, key, sep = '.') {
|
|
169
|
-
if (!obj || typeof obj !== 'object') return undefined;
|
|
170
|
-
if (!key || typeof key !== 'string') return undefined;
|
|
276
|
+
function resolveKey(obj, key, sep = '.', runtimeState = null, lang = null) {
|
|
277
|
+
if (!obj || typeof obj !== 'object') return undefined;
|
|
278
|
+
if (!key || typeof key !== 'string') return undefined;
|
|
171
279
|
const parts = key.split(sep);
|
|
172
280
|
let cur = obj;
|
|
173
281
|
for (const p of parts) {
|
|
174
282
|
if (cur && Object.prototype.hasOwnProperty.call(cur, p)) {
|
|
175
283
|
cur = cur[p];
|
|
176
284
|
} else {
|
|
285
|
+
if (runtimeState && lang && runtimeState.lazy) {
|
|
286
|
+
const manifest = getManifestForLanguage(runtimeState, lang);
|
|
287
|
+
for (let i = parts.length; i > 0; i--) {
|
|
288
|
+
const prefix = parts.slice(0, i).join(sep);
|
|
289
|
+
const filePath = manifest.get(prefix);
|
|
290
|
+
const loadedFileKey = `${lang}\0${filePath}`;
|
|
291
|
+
if (filePath && !runtimeState.loadedFiles.has(loadedFileKey)) {
|
|
292
|
+
loadFileLazy(runtimeState, filePath, lang);
|
|
293
|
+
runtimeState.loadedFiles.add(loadedFileKey);
|
|
294
|
+
const langData = runtimeState.cache.get(lang);
|
|
295
|
+
return resolveKey(langData, key, sep, runtimeState, lang);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!runtimeState.eagerLoadedLanguages.has(lang)) {
|
|
299
|
+
const fullData = readLanguageFromBase(runtimeState.baseDir, lang);
|
|
300
|
+
const langData = runtimeState.cache.get(lang) || {};
|
|
301
|
+
deepMerge(langData, fullData);
|
|
302
|
+
runtimeState.cache.set(lang, langData);
|
|
303
|
+
runtimeState.eagerLoadedLanguages.add(lang);
|
|
304
|
+
return resolveKey(langData, key, sep, runtimeState, lang);
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
177
308
|
return undefined;
|
|
178
309
|
}
|
|
179
310
|
}
|
|
@@ -182,62 +313,110 @@ function resolveKey(obj, key, sep = '.') {
|
|
|
182
313
|
|
|
183
314
|
// --- Public API ---
|
|
184
315
|
function initRuntime(options = {}) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
316
|
+
const runtimeState = createState(options);
|
|
317
|
+
preload(runtimeState, options.preload);
|
|
318
|
+
|
|
319
|
+
if (!singletonInitialized) {
|
|
320
|
+
singletonState.baseDir = runtimeState.baseDir;
|
|
321
|
+
singletonState.language = runtimeState.language;
|
|
322
|
+
singletonState.fallbackLanguage = runtimeState.fallbackLanguage;
|
|
323
|
+
singletonState.keySeparator = runtimeState.keySeparator;
|
|
324
|
+
singletonState.lazy = runtimeState.lazy;
|
|
325
|
+
singletonState.keyManifest = new Map();
|
|
326
|
+
singletonState.loadedFiles = new Set();
|
|
327
|
+
singletonState.eagerLoadedLanguages = new Set();
|
|
328
|
+
singletonState.cache.clear();
|
|
329
|
+
preload(singletonState, options.preload);
|
|
330
|
+
singletonInitialized = true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return createRuntime(runtimeState);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function preload(runtimeState, shouldPreload) {
|
|
337
|
+
if (shouldPreload === true) {
|
|
338
|
+
if (runtimeState.lazy) {
|
|
339
|
+
getManifestForLanguage(runtimeState, runtimeState.language);
|
|
340
|
+
if (!runtimeState.cache.has(runtimeState.language)) {
|
|
341
|
+
runtimeState.cache.set(runtimeState.language, {});
|
|
342
|
+
}
|
|
343
|
+
if (runtimeState.fallbackLanguage && runtimeState.fallbackLanguage !== runtimeState.language) {
|
|
344
|
+
getManifestForLanguage(runtimeState, runtimeState.fallbackLanguage);
|
|
345
|
+
if (!runtimeState.cache.has(runtimeState.fallbackLanguage)) {
|
|
346
|
+
runtimeState.cache.set(runtimeState.fallbackLanguage, {});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
getTranslationsForState(runtimeState, runtimeState.language);
|
|
351
|
+
if (runtimeState.fallbackLanguage && runtimeState.fallbackLanguage !== runtimeState.language) {
|
|
352
|
+
getTranslationsForState(runtimeState, runtimeState.fallbackLanguage);
|
|
353
|
+
}
|
|
195
354
|
}
|
|
196
355
|
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function createRuntime(runtimeState) {
|
|
359
|
+
const runtimeTranslate = (key, params) => translateWithState(runtimeState, key, params);
|
|
197
360
|
return {
|
|
198
|
-
t:
|
|
199
|
-
translate,
|
|
200
|
-
setLanguage,
|
|
201
|
-
getLanguage,
|
|
202
|
-
getAvailableLanguages,
|
|
203
|
-
refresh,
|
|
361
|
+
t: runtimeTranslate,
|
|
362
|
+
translate: runtimeTranslate,
|
|
363
|
+
setLanguage: (lang) => setLanguageForState(runtimeState, lang),
|
|
364
|
+
getLanguage: () => getLanguageForState(runtimeState),
|
|
365
|
+
getAvailableLanguages: () => getAvailableLanguagesForState(runtimeState),
|
|
366
|
+
refresh: (lang) => refreshForState(runtimeState, lang),
|
|
204
367
|
};
|
|
205
368
|
}
|
|
206
369
|
|
|
207
370
|
function translate(key, params = {}) {
|
|
208
|
-
|
|
209
|
-
|
|
371
|
+
return translateWithState(singletonState, key, params);
|
|
372
|
+
}
|
|
210
373
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
374
|
+
function translateWithState(runtimeState, key, params = {}) {
|
|
375
|
+
const langData = getTranslationsForState(runtimeState, runtimeState.language);
|
|
376
|
+
let value = resolveKey(langData, key, runtimeState.keySeparator, runtimeState, runtimeState.language);
|
|
377
|
+
|
|
378
|
+
if (typeof value === 'undefined' && runtimeState.fallbackLanguage) {
|
|
379
|
+
const fbData = getTranslationsForState(runtimeState, runtimeState.fallbackLanguage);
|
|
380
|
+
value = resolveKey(fbData, key, runtimeState.keySeparator, runtimeState, runtimeState.fallbackLanguage);
|
|
381
|
+
}
|
|
215
382
|
|
|
216
383
|
if (typeof value === 'string') return interpolate(value, params);
|
|
217
384
|
return typeof value === 'undefined' ? key : value;
|
|
218
385
|
}
|
|
219
386
|
|
|
220
387
|
function setLanguage(lang) {
|
|
388
|
+
setLanguageForState(singletonState, lang);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function setLanguageForState(runtimeState, lang) {
|
|
221
392
|
if (!lang || typeof lang !== 'string') return;
|
|
222
|
-
|
|
393
|
+
runtimeState.language = lang;
|
|
223
394
|
}
|
|
224
395
|
|
|
225
396
|
function getLanguage() {
|
|
226
|
-
return
|
|
397
|
+
return getLanguageForState(singletonState);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function getLanguageForState(runtimeState) {
|
|
401
|
+
return runtimeState.language;
|
|
227
402
|
}
|
|
228
403
|
|
|
229
404
|
function getAvailableLanguages() {
|
|
405
|
+
return getAvailableLanguagesForState(singletonState);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function getAvailableLanguagesForState(runtimeState) {
|
|
230
409
|
const langs = new Set();
|
|
231
|
-
if (!
|
|
232
|
-
if (!SecurityUtils.safeExistsSync(
|
|
410
|
+
if (!runtimeState.baseDir) runtimeState.baseDir = resolveBaseDir();
|
|
411
|
+
if (!SecurityUtils.safeExistsSync(runtimeState.baseDir, path.dirname(runtimeState.baseDir))) return ['en'];
|
|
233
412
|
try {
|
|
234
|
-
for (const entry of fs.readdirSync(
|
|
413
|
+
for (const entry of fs.readdirSync(runtimeState.baseDir, { withFileTypes: true })) {
|
|
235
414
|
if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
|
|
236
415
|
langs.add(entry.name.replace(/\.json$/i, ''));
|
|
237
416
|
} else if (entry.isDirectory()) {
|
|
238
417
|
const lang = entry.name;
|
|
239
|
-
const idx = path.join(
|
|
240
|
-
if (SecurityUtils.safeExistsSync(idx)) langs.add(lang);
|
|
418
|
+
const idx = path.join(runtimeState.baseDir, lang, `${lang}.json`);
|
|
419
|
+
if (SecurityUtils.safeExistsSync(idx, path.dirname(idx))) langs.add(lang);
|
|
241
420
|
else langs.add(lang); // be permissive
|
|
242
421
|
}
|
|
243
422
|
}
|
|
@@ -248,13 +427,23 @@ function getAvailableLanguages() {
|
|
|
248
427
|
return Array.from(langs.size ? langs : new Set(['en']));
|
|
249
428
|
}
|
|
250
429
|
|
|
251
|
-
function refresh(lang
|
|
252
|
-
|
|
253
|
-
if (lang !== state.fallbackLanguage && state.cache.has(state.fallbackLanguage)) {
|
|
254
|
-
// do nothing; keep or clear on demand
|
|
255
|
-
}
|
|
430
|
+
function refresh(lang) {
|
|
431
|
+
refreshForState(singletonState, lang);
|
|
256
432
|
}
|
|
257
433
|
|
|
434
|
+
function refreshForState(runtimeState, lang = runtimeState.language) {
|
|
435
|
+
if (runtimeState.cache.has(lang)) runtimeState.cache.delete(lang);
|
|
436
|
+
if (runtimeState.eagerLoadedLanguages) runtimeState.eagerLoadedLanguages.delete(lang);
|
|
437
|
+
if (runtimeState.loadedFiles) {
|
|
438
|
+
for (const fileKey of Array.from(runtimeState.loadedFiles)) {
|
|
439
|
+
if (fileKey.startsWith(`${lang}\0`)) runtimeState.loadedFiles.delete(fileKey);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (lang !== runtimeState.fallbackLanguage && runtimeState.cache.has(runtimeState.fallbackLanguage)) {
|
|
443
|
+
// do nothing; keep or clear on demand
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
258
447
|
module.exports = {
|
|
259
448
|
initRuntime,
|
|
260
449
|
translate,
|
|
@@ -262,5 +451,6 @@ module.exports = {
|
|
|
262
451
|
setLanguage,
|
|
263
452
|
getLanguage,
|
|
264
453
|
getAvailableLanguages,
|
|
265
|
-
refresh,
|
|
266
|
-
|
|
454
|
+
refresh,
|
|
455
|
+
loadKeyManifest: (baseDir) => loadKeyManifestFromDir(baseDir || singletonState.baseDir),
|
|
456
|
+
};
|