i18ntk 4.1.0 → 4.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +51 -5
  2. package/README.md +79 -17
  3. package/SECURITY.md +10 -4
  4. package/main/i18ntk-analyze.js +10 -20
  5. package/main/i18ntk-backup.js +106 -44
  6. package/main/i18ntk-init.js +153 -157
  7. package/main/i18ntk-setup.js +36 -13
  8. package/main/i18ntk-translate.js +169 -21
  9. package/main/i18ntk-usage.js +272 -103
  10. package/main/i18ntk-validate.js +38 -31
  11. package/main/manage/commands/AnalyzeCommand.js +7 -17
  12. package/main/manage/commands/CommandRouter.js +6 -6
  13. package/main/manage/commands/TranslateCommand.js +65 -56
  14. package/main/manage/commands/ValidateCommand.js +34 -26
  15. package/main/manage/index.js +11 -42
  16. package/main/manage/managers/InteractiveMenu.js +11 -40
  17. package/main/manage/services/InitService.js +114 -118
  18. package/main/manage/services/UsageService.js +244 -85
  19. package/package.json +21 -14
  20. package/runtime/enhanced.d.ts +5 -5
  21. package/runtime/enhanced.js +49 -25
  22. package/runtime/i18ntk.d.ts +30 -7
  23. package/runtime/index.d.ts +48 -19
  24. package/runtime/index.js +175 -90
  25. package/settings/settings-cli.js +115 -38
  26. package/settings/settings-manager.js +24 -6
  27. package/ui-locales/de.json +192 -11
  28. package/ui-locales/en.json +182 -8
  29. package/ui-locales/es.json +193 -12
  30. package/ui-locales/fr.json +189 -8
  31. package/ui-locales/ja.json +190 -8
  32. package/ui-locales/ru.json +191 -9
  33. package/ui-locales/zh.json +194 -9
  34. package/utils/cli-helper.js +8 -12
  35. package/utils/config-helper.js +1 -1
  36. package/utils/config-manager.js +8 -6
  37. package/utils/localized-confirm.js +55 -0
  38. package/utils/menu-layout.js +41 -0
  39. package/utils/report-writer.js +110 -0
  40. package/utils/security.js +15 -22
  41. package/utils/translate/api.js +31 -3
  42. package/utils/translate/placeholder.js +42 -1
  43. package/utils/translate/report.js +3 -2
  44. package/utils/translate/safe-network.js +24 -4
  45. package/utils/usage-insights.js +435 -0
  46. package/utils/usage-source.js +50 -0
  47. package/utils/watch-locales.js +1 -8
@@ -72,19 +72,19 @@ export interface LanguageConfig {
72
72
  }
73
73
 
74
74
  // Enhanced TypeScript-compatible translation functions
75
- export function translate<T = string>(key: string, params?: TranslationParams, options?: TranslationOptions): T;
76
- export function translate<T = string>(key: TranslationKey<T>, params?: TranslationParams, options?: TranslationOptions): T;
75
+ export function translate<T = string>(key: string, params?: TranslationParams, options?: TranslationOptions): Promise<T>;
76
+ export function translate<T = string>(key: TranslationKey<T>, params?: TranslationParams, options?: TranslationOptions): Promise<T>;
77
77
 
78
78
  export const t: typeof translate;
79
79
 
80
80
  // Type-safe translation with strict typing
81
- export function tTyped<T = string>(key: string, params?: TranslationParams, options?: TranslationOptions): T;
81
+ export function tTyped<T = string>(key: string, params?: TranslationParams, options?: TranslationOptions): Promise<T>;
82
82
 
83
83
  // Translation with encryption support
84
84
  export function translateEncrypted<T = string>(key: string, params?: TranslationParams, options?: TranslationOptions): Promise<T>;
85
85
 
86
86
  // Batch translation operations
87
- export function translateBatch<T = string>(keys: string[], params?: TranslationParams[], options?: TranslationOptions): T[];
87
+ export function translateBatch<T = string>(keys: string[], params?: TranslationParams[], options?: TranslationOptions): Promise<T[]>;
88
88
  export function translateBatchEncrypted<T = string>(keys: string[], params?: TranslationParams[], options?: TranslationOptions): Promise<T[]>;
89
89
 
90
90
  // Configuration and initialization
@@ -219,4 +219,4 @@ export interface MigrationResult {
219
219
  }
220
220
 
221
221
  // Export compatibility with existing runtime
222
- export * from './i18ntk.d.ts';
222
+ export * from './i18ntk.d.ts';
@@ -884,26 +884,46 @@ async function initI18nRuntime(options = {}) {
884
884
  }
885
885
 
886
886
  // Backward compatibility
887
- function translate(key, params, options) {
888
- if (!runtimeInstance) {
889
- runtimeInstance = new I18nEnhancedRuntime();
890
- }
891
- return runtimeInstance.translate(key, params, options);
892
- }
893
-
894
- const t = translate;
895
-
896
- // Export for both CommonJS and ES modules
897
- module.exports = {
898
- initI18nRuntime,
899
- translate,
900
- t,
901
- translateEncrypted: async (key, params, options) => {
902
- if (!runtimeInstance) {
903
- runtimeInstance = new I18nEnhancedRuntime();
904
- }
905
- return runtimeInstance.translateEncrypted(key, params, options);
906
- },
887
+ function translate(key, params, options) {
888
+ if (!runtimeInstance) {
889
+ runtimeInstance = new I18nEnhancedRuntime();
890
+ }
891
+ return runtimeInstance.translate(key, params, options);
892
+ }
893
+
894
+ const t = translate;
895
+ const tTyped = translate;
896
+
897
+ async function translateBatch(keys, paramsArray, options) {
898
+ if (!runtimeInstance) {
899
+ runtimeInstance = new I18nEnhancedRuntime();
900
+ }
901
+ return runtimeInstance.translateBatch(keys, paramsArray, options);
902
+ }
903
+
904
+ async function translateBatchEncrypted(keys, paramsArray, options) {
905
+ if (!runtimeInstance) {
906
+ runtimeInstance = new I18nEnhancedRuntime();
907
+ }
908
+ return runtimeInstance.translateBatchEncrypted(keys, paramsArray, options);
909
+ }
910
+
911
+ async function translateEncrypted(key, params, options) {
912
+ if (!runtimeInstance) {
913
+ runtimeInstance = new I18nEnhancedRuntime();
914
+ }
915
+ return runtimeInstance.translateEncrypted(key, params, options);
916
+ }
917
+
918
+ // Export for both CommonJS and ES modules
919
+ module.exports = {
920
+ initI18nRuntime,
921
+ translate,
922
+ t,
923
+ tTyped,
924
+ translateEncrypted,
925
+ translateBatch,
926
+ translateBatchEncrypted,
907
927
 
908
928
  // TypeScript compatibility exports
909
929
  I18nEnhancedRuntime,
@@ -922,8 +942,12 @@ module.exports = {
922
942
  };
923
943
 
924
944
  // ES module support
925
- module.exports.default = {
926
- initI18nRuntime,
927
- translate,
928
- t,
929
- };
945
+ module.exports.default = {
946
+ initI18nRuntime,
947
+ translate,
948
+ t,
949
+ tTyped,
950
+ translateEncrypted,
951
+ translateBatch,
952
+ translateBatchEncrypted,
953
+ };
@@ -1,6 +1,6 @@
1
1
  // runtime/i18ntk.d.ts
2
- // Complete TypeScript definitions for i18ntk internationalization framework
3
- // Version 3.2.0 - Full TypeScript support with AES-256-GCM encryption
2
+ // Complete TypeScript definitions for i18ntk internationalization framework
3
+ // Version 4.2.0 - Enhanced API types plus lightweight runtime compatibility
4
4
 
5
5
  /**
6
6
  * Core translation parameters interface
@@ -445,16 +445,21 @@ export interface I18nRuntime {
445
445
  /**
446
446
  * Basic runtime interface (for backward compatibility)
447
447
  */
448
- export interface BasicI18nRuntime {
448
+ export interface BasicI18nRuntime {
449
449
  /**
450
450
  * Translate a key with parameters (synchronous)
451
451
  */
452
- translate(key: string, params?: TranslationParams): string;
452
+ translate(key: string, params?: TranslationParams, options?: Pick<TranslationOptions, 'language' | 'fallbackLanguage'>): string | number | boolean | null | object | unknown[];
453
453
 
454
454
  /**
455
455
  * Alias for translate function (synchronous)
456
456
  */
457
- t(key: string, params?: TranslationParams): string;
457
+ t(key: string, params?: TranslationParams, options?: Pick<TranslationOptions, 'language' | 'fallbackLanguage'>): string | number | boolean | null | object | unknown[];
458
+
459
+ /**
460
+ * Translate multiple keys synchronously.
461
+ */
462
+ translateBatch(keys: string[], params?: TranslationParams | TranslationParams[], options?: Pick<TranslationOptions, 'language' | 'fallbackLanguage'>): Array<string | number | boolean | null | object | unknown[]>;
458
463
 
459
464
  /**
460
465
  * Set language
@@ -469,7 +474,25 @@ export interface BasicI18nRuntime {
469
474
  /**
470
475
  * Get available languages (synchronous)
471
476
  */
472
- getAvailableLanguages(): string[];
477
+ getAvailableLanguages(): string[];
478
+
479
+ /**
480
+ * Clear all cached runtime data, or one language when provided.
481
+ */
482
+ clearCache(language?: string): void;
483
+
484
+ /**
485
+ * Return lightweight cache diagnostics for production observability.
486
+ */
487
+ getCacheInfo(): {
488
+ language: string;
489
+ fallbackLanguage: string;
490
+ lazy: boolean;
491
+ cachedLanguages: string[];
492
+ manifestLanguages: string[];
493
+ loadedFileCount: number;
494
+ eagerLoadedLanguages: string[];
495
+ };
473
496
 
474
497
  /**
475
498
  * Refresh translations
@@ -487,7 +510,7 @@ export declare function initI18nRuntime(config: I18nConfig): Promise<I18nRuntime
487
510
  * This is the default export from 'i18ntk/runtime'
488
511
  */
489
512
  export declare function initRuntime(options: {
490
- baseDir: string;
513
+ baseDir?: string;
491
514
  language?: string;
492
515
  fallbackLanguage?: string;
493
516
  keySeparator?: string;
@@ -1,31 +1,60 @@
1
1
  // runtime/index.d.ts
2
2
  // Public runtime API types for i18ntk
3
3
 
4
- export interface InitOptions {
5
- baseDir?: string;
6
- language?: string;
4
+ export interface InitOptions {
5
+ /** Locale base directory. Defaults to config/env resolution when omitted. */
6
+ baseDir?: string;
7
+ language?: string;
7
8
  fallbackLanguage?: string;
8
9
  keySeparator?: string;
10
+ /** Eagerly cache the active and fallback languages during initialization. */
9
11
  preload?: boolean;
12
+ /** Build a key manifest and load matching JSON files on first key access. */
10
13
  lazy?: boolean;
11
14
  }
12
-
13
- export type TranslateParams = Record<string, unknown>;
14
-
15
- export function translate(key: string, params?: TranslateParams): string;
16
- export const t: typeof translate;
17
-
18
- export function initRuntime(options?: InitOptions): {
19
- t: typeof translate;
20
- translate: typeof translate;
21
- setLanguage: typeof setLanguage;
22
- getLanguage: typeof getLanguage;
23
- getAvailableLanguages: typeof getAvailableLanguages;
24
- refresh: typeof refresh;
25
- };
26
-
27
- export function setLanguage(lang: string): void;
15
+
16
+ export type TranslateParams = Record<string, unknown>;
17
+ export interface TranslationOptions {
18
+ /** Translate this call with a different language without mutating runtime state. */
19
+ language?: string;
20
+ /** Fallback language for this call. */
21
+ fallbackLanguage?: string;
22
+ }
23
+ export type TranslationValue = string | number | boolean | null | object | unknown[];
24
+ export type TranslateBatchParams = TranslateParams | TranslateParams[];
25
+
26
+ export interface RuntimeCacheInfo {
27
+ language: string;
28
+ fallbackLanguage: string;
29
+ lazy: boolean;
30
+ cachedLanguages: string[];
31
+ manifestLanguages: string[];
32
+ loadedFileCount: number;
33
+ eagerLoadedLanguages: string[];
34
+ }
35
+
36
+ export interface RuntimeInstance {
37
+ t(key: string, params?: TranslateParams, options?: TranslationOptions): TranslationValue;
38
+ translate(key: string, params?: TranslateParams, options?: TranslationOptions): TranslationValue;
39
+ translateBatch(keys: string[], params?: TranslateBatchParams, options?: TranslationOptions): TranslationValue[];
40
+ setLanguage(lang: string): void;
41
+ getLanguage(): string;
42
+ getAvailableLanguages(): string[];
43
+ clearCache(lang?: string): void;
44
+ getCacheInfo(): RuntimeCacheInfo;
45
+ refresh(lang?: string): void;
46
+ }
47
+
48
+ export function translate(key: string, params?: TranslateParams, options?: TranslationOptions): TranslationValue;
49
+ export const t: typeof translate;
50
+ export function translateBatch(keys: string[], params?: TranslateBatchParams, options?: TranslationOptions): TranslationValue[];
51
+
52
+ export function initRuntime(options?: InitOptions): RuntimeInstance;
53
+
54
+ export function setLanguage(lang: string): void;
28
55
  export function getLanguage(): string;
29
56
  export function getAvailableLanguages(): string[];
57
+ export function clearCache(lang?: string): void;
58
+ export function getCacheInfo(): RuntimeCacheInfo;
30
59
  export function refresh(lang?: string): void;
31
60
  export function loadKeyManifest(baseDir?: string): Map<string, string>;
package/runtime/index.js CHANGED
@@ -8,14 +8,22 @@ const path = require('path');
8
8
  const SecurityUtils = require('../utils/security');
9
9
  const { envManager } = require('../utils/env-manager');
10
10
 
11
- let configManager = null;
12
- try { configManager = require('../utils/config-manager'); } catch (_) { /* optional */ }
13
-
14
- function createState(options = {}) {
15
- return {
16
- baseDir: resolveBaseDir(options.baseDir),
17
- language: options.language || 'en',
18
- fallbackLanguage: options.fallbackLanguage || 'en',
11
+ let configManager = null;
12
+ try { configManager = require('../utils/config-manager'); } catch (_) { /* optional */ }
13
+
14
+ const SAFE_LANGUAGE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,35}$/;
15
+
16
+ function normalizeLanguageCode(value, fallback = 'en') {
17
+ if (typeof value !== 'string') return fallback;
18
+ const trimmed = value.trim();
19
+ return SAFE_LANGUAGE_PATTERN.test(trimmed) ? trimmed : fallback;
20
+ }
21
+
22
+ function createState(options = {}) {
23
+ return {
24
+ baseDir: resolveBaseDir(options.baseDir),
25
+ language: normalizeLanguageCode(options.language, 'en'),
26
+ fallbackLanguage: normalizeLanguageCode(options.fallbackLanguage, 'en'),
19
27
  keySeparator: options.keySeparator || '.',
20
28
  cache: new Map(),
21
29
  lazy: options.lazy === true,
@@ -47,21 +55,29 @@ function stripBOMAndComments(s) {
47
55
  return s;
48
56
  }
49
57
 
50
- function readJsonSafe(file) {
51
- const raw = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8');
52
- if (raw === null || raw === undefined) {
53
- throw new Error(`Unable to read JSON file: ${file}`);
54
- }
55
- const cleaned = stripBOMAndComments(raw);
56
- if (!cleaned) {
57
- throw new Error(`Empty JSON file: ${file}`);
58
- }
59
- try {
60
- return JSON.parse(cleaned);
61
- } catch (parseError) {
62
- throw new Error(`Invalid JSON in file ${file}: ${parseError.message}`);
63
- }
64
- }
58
+ function readJsonSafe(file) {
59
+ const raw = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8');
60
+ if (raw === null || raw === undefined) {
61
+ throw new Error(`Unable to read JSON file: ${file}`);
62
+ }
63
+ const withoutBOM = raw.charCodeAt && raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw;
64
+ if (!withoutBOM) {
65
+ throw new Error(`Empty JSON file: ${file}`);
66
+ }
67
+ try {
68
+ return JSON.parse(withoutBOM);
69
+ } catch (parseError) {
70
+ const cleaned = stripBOMAndComments(raw);
71
+ if (!cleaned) {
72
+ throw new Error(`Empty JSON file: ${file}`);
73
+ }
74
+ try {
75
+ return JSON.parse(cleaned);
76
+ } catch (_) {
77
+ throw new Error(`Invalid JSON in file ${file}: ${parseError.message}`);
78
+ }
79
+ }
80
+ }
65
81
 
66
82
  function deepMerge(target, source) {
67
83
  if (!target || typeof target !== 'object') target = {};
@@ -177,11 +193,13 @@ function loadKeyManifestFromDir(baseDir) {
177
193
  }
178
194
 
179
195
  function getLanguagePath(baseDir, lang) {
196
+ lang = normalizeLanguageCode(lang, '');
197
+ if (!lang) return null;
180
198
  const langDir = path.join(baseDir, lang);
181
199
  const langFile = path.join(baseDir, `${lang}.json`);
182
- const langDirStat = SecurityUtils.safeStatSync(langDir, path.dirname(langDir));
200
+ const langDirStat = SecurityUtils.safeStatSync(langDir, baseDir);
183
201
  if (langDirStat && langDirStat.isDirectory()) return langDir;
184
- const langFileStat = SecurityUtils.safeStatSync(langFile, path.dirname(langFile));
202
+ const langFileStat = SecurityUtils.safeStatSync(langFile, baseDir);
185
203
  if (langFileStat && langFileStat.isFile()) return langFile;
186
204
  return null;
187
205
  }
@@ -220,13 +238,15 @@ function loadFileLazy(runtimeState, filePath, lang) {
220
238
  return data;
221
239
  }
222
240
 
223
- function readLanguageFromBase(baseDir, lang) {
224
- const merged = {};
225
- const langFile = path.join(baseDir, `${lang}.json`);
226
- const langDir = path.join(baseDir, lang);
227
-
228
- // Prefer folder if exists, otherwise single file
229
- const langDirStat = SecurityUtils.safeStatSync(langDir, path.dirname(langDir));
241
+ function readLanguageFromBase(baseDir, lang) {
242
+ lang = normalizeLanguageCode(lang, '');
243
+ if (!lang) return {};
244
+ const merged = {};
245
+ const langFile = path.join(baseDir, `${lang}.json`);
246
+ const langDir = path.join(baseDir, lang);
247
+
248
+ // Prefer folder if exists, otherwise single file
249
+ const langDirStat = SecurityUtils.safeStatSync(langDir, baseDir);
230
250
  if (langDirStat && langDirStat.isDirectory()) {
231
251
  const files = listJsonFilesRecursively(langDir, langDir);
232
252
  for (const file of files) {
@@ -237,8 +257,8 @@ function readLanguageFromBase(baseDir, lang) {
237
257
  // Skip unreadable/invalid files
238
258
  }
239
259
  }
240
- } else {
241
- const langFileStat = SecurityUtils.safeStatSync(langFile, path.dirname(langFile));
260
+ } else {
261
+ const langFileStat = SecurityUtils.safeStatSync(langFile, baseDir);
242
262
  if (langFileStat && langFileStat.isFile()) {
243
263
  try {
244
264
  const data = readJsonSafe(langFile);
@@ -250,9 +270,10 @@ function readLanguageFromBase(baseDir, lang) {
250
270
  return merged;
251
271
  }
252
272
 
253
- function getTranslationsForState(runtimeState, lang) {
254
- if (!runtimeState.baseDir) runtimeState.baseDir = resolveBaseDir();
255
-
273
+ function getTranslationsForState(runtimeState, lang) {
274
+ if (!runtimeState.baseDir) runtimeState.baseDir = resolveBaseDir();
275
+ lang = normalizeLanguageCode(lang, runtimeState.fallbackLanguage || 'en');
276
+
256
277
  if (runtimeState.lazy) {
257
278
  if (runtimeState.cache.has(lang)) return runtimeState.cache.get(lang);
258
279
  runtimeState.cache.set(lang, {});
@@ -266,12 +287,13 @@ function getTranslationsForState(runtimeState, lang) {
266
287
  return data;
267
288
  }
268
289
 
269
- function interpolate(template, params) {
270
- if (typeof template !== 'string') return template;
271
- return template
272
- .replace(/\{\{(\w+)\}\}/g, (m, p1) => (p1 in params ? String(params[p1]) : m))
273
- .replace(/\{(\w+)\}/g, (m, p1) => (p1 in params ? String(params[p1]) : m));
274
- }
290
+ function interpolate(template, params) {
291
+ if (typeof template !== 'string') return template;
292
+ params = params && typeof params === 'object' ? params : {};
293
+ return template
294
+ .replace(/\{\{(\w+)\}\}/g, (m, p1) => (p1 in params ? String(params[p1]) : m))
295
+ .replace(/\{(\w+)\}/g, (m, p1) => (p1 in params ? String(params[p1]) : m));
296
+ }
275
297
 
276
298
  // Resolve a dotted key path from an object
277
299
  function resolveKey(obj, key, sep = '.', runtimeState = null, lang = null) {
@@ -360,43 +382,67 @@ function preload(runtimeState, shouldPreload) {
360
382
  }
361
383
  }
362
384
 
363
- function createRuntime(runtimeState) {
364
- const runtimeTranslate = (key, params) => translateWithState(runtimeState, key, params);
365
- return {
366
- t: runtimeTranslate,
367
- translate: runtimeTranslate,
368
- setLanguage: (lang) => setLanguageForState(runtimeState, lang),
369
- getLanguage: () => getLanguageForState(runtimeState),
370
- getAvailableLanguages: () => getAvailableLanguagesForState(runtimeState),
371
- refresh: (lang) => refreshForState(runtimeState, lang),
372
- };
373
- }
374
-
375
- function translate(key, params = {}) {
376
- return translateWithState(singletonState, key, params);
377
- }
378
-
379
- function translateWithState(runtimeState, key, params = {}) {
380
- const langData = getTranslationsForState(runtimeState, runtimeState.language);
381
- let value = resolveKey(langData, key, runtimeState.keySeparator, runtimeState, runtimeState.language);
385
+ function createRuntime(runtimeState) {
386
+ const runtimeTranslate = (key, params, options) => translateWithState(runtimeState, key, params, options);
387
+ return {
388
+ t: runtimeTranslate,
389
+ translate: runtimeTranslate,
390
+ translateBatch: (keys, params, options) => translateBatchWithState(runtimeState, keys, params, options),
391
+ setLanguage: (lang) => setLanguageForState(runtimeState, lang),
392
+ getLanguage: () => getLanguageForState(runtimeState),
393
+ getAvailableLanguages: () => getAvailableLanguagesForState(runtimeState),
394
+ clearCache: (lang) => clearCacheForState(runtimeState, lang),
395
+ getCacheInfo: () => getCacheInfoForState(runtimeState),
396
+ refresh: (lang) => refreshForState(runtimeState, lang),
397
+ };
398
+ }
382
399
 
383
- if (typeof value === 'undefined' && runtimeState.fallbackLanguage) {
384
- const fbData = getTranslationsForState(runtimeState, runtimeState.fallbackLanguage);
385
- value = resolveKey(fbData, key, runtimeState.keySeparator, runtimeState, runtimeState.fallbackLanguage);
400
+ function translate(key, params = {}, options = {}) {
401
+ return translateWithState(singletonState, key, params, options);
402
+ }
403
+
404
+ function translateWithState(runtimeState, key, params = {}, options = {}) {
405
+ params = params && typeof params === 'object' ? params : {};
406
+ options = options && typeof options === 'object' ? options : {};
407
+
408
+ const activeLanguage = normalizeLanguageCode(options.language, runtimeState.language);
409
+ const fallbackLanguage = typeof options.fallbackLanguage === 'string'
410
+ ? normalizeLanguageCode(options.fallbackLanguage, '')
411
+ : runtimeState.fallbackLanguage;
412
+
413
+ const langData = getTranslationsForState(runtimeState, activeLanguage);
414
+ let value = resolveKey(langData, key, runtimeState.keySeparator, runtimeState, activeLanguage);
415
+
416
+ if (typeof value === 'undefined' && fallbackLanguage && fallbackLanguage !== activeLanguage) {
417
+ const fbData = getTranslationsForState(runtimeState, fallbackLanguage);
418
+ value = resolveKey(fbData, key, runtimeState.keySeparator, runtimeState, fallbackLanguage);
386
419
  }
387
-
388
- if (typeof value === 'string') return interpolate(value, params);
389
- return typeof value === 'undefined' ? key : value;
390
- }
420
+
421
+ if (typeof value === 'string') return interpolate(value, params);
422
+ return typeof value === 'undefined' ? key : value;
423
+ }
424
+
425
+ function translateBatch(keys, params = {}, options = {}) {
426
+ return translateBatchWithState(singletonState, keys, params, options);
427
+ }
428
+
429
+ function translateBatchWithState(runtimeState, keys, params = {}, options = {}) {
430
+ if (!Array.isArray(keys)) return [];
431
+ return keys.map((key, index) => {
432
+ const perKeyParams = Array.isArray(params) ? (params[index] || {}) : params;
433
+ return translateWithState(runtimeState, key, perKeyParams, options);
434
+ });
435
+ }
391
436
 
392
437
  function setLanguage(lang) {
393
438
  setLanguageForState(singletonState, lang);
394
439
  }
395
440
 
396
- function setLanguageForState(runtimeState, lang) {
397
- if (!lang || typeof lang !== 'string') return;
398
- runtimeState.language = lang;
399
- }
441
+ function setLanguageForState(runtimeState, lang) {
442
+ const safeLang = normalizeLanguageCode(lang, '');
443
+ if (!safeLang) return;
444
+ runtimeState.language = safeLang;
445
+ }
400
446
 
401
447
  function getLanguage() {
402
448
  return getLanguageForState(singletonState);
@@ -410,33 +456,69 @@ function getAvailableLanguages() {
410
456
  return getAvailableLanguagesForState(singletonState);
411
457
  }
412
458
 
413
- function getAvailableLanguagesForState(runtimeState) {
459
+ function getAvailableLanguagesForState(runtimeState) {
414
460
  const langs = new Set();
415
461
  if (!runtimeState.baseDir) runtimeState.baseDir = resolveBaseDir();
416
462
  if (!SecurityUtils.safeExistsSync(runtimeState.baseDir, path.dirname(runtimeState.baseDir))) return ['en'];
417
463
  try {
418
464
  for (const entry of fs.readdirSync(runtimeState.baseDir, { withFileTypes: true })) {
419
- if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
420
- langs.add(entry.name.replace(/\.json$/i, ''));
421
- } else if (entry.isDirectory()) {
422
- const lang = entry.name;
423
- const idx = path.join(runtimeState.baseDir, lang, `${lang}.json`);
424
- if (SecurityUtils.safeExistsSync(idx, path.dirname(idx))) langs.add(lang);
425
- else langs.add(lang); // be permissive
426
- }
465
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
466
+ const lang = normalizeLanguageCode(entry.name.replace(/\.json$/i, ''), '');
467
+ if (lang) langs.add(lang);
468
+ } else if (entry.isDirectory()) {
469
+ const lang = normalizeLanguageCode(entry.name, '');
470
+ if (!lang) continue;
471
+ const idx = path.join(runtimeState.baseDir, lang, `${lang}.json`);
472
+ if (SecurityUtils.safeExistsSync(idx, runtimeState.baseDir)) langs.add(lang);
473
+ else langs.add(lang); // be permissive
474
+ }
427
475
  }
428
476
  } catch (_) {
429
477
  // Unreadable directory
430
478
  return ['en'];
431
479
  }
432
- return Array.from(langs.size ? langs : new Set(['en']));
433
- }
480
+ return Array.from(langs.size ? langs : new Set(['en']));
481
+ }
482
+
483
+ function clearCache(lang) {
484
+ clearCacheForState(singletonState, lang);
485
+ }
486
+
487
+ function clearCacheForState(runtimeState, lang) {
488
+ if (typeof lang === 'string') {
489
+ refreshForState(runtimeState, lang);
490
+ return;
491
+ }
492
+
493
+ runtimeState.cache.clear();
494
+ if (runtimeState.keyManifest) runtimeState.keyManifest.clear();
495
+ if (runtimeState.loadedFiles) runtimeState.loadedFiles.clear();
496
+ if (runtimeState.eagerLoadedLanguages) runtimeState.eagerLoadedLanguages.clear();
497
+ }
498
+
499
+ function getCacheInfo() {
500
+ return getCacheInfoForState(singletonState);
501
+ }
502
+
503
+ function getCacheInfoForState(runtimeState) {
504
+ return {
505
+ language: runtimeState.language,
506
+ fallbackLanguage: runtimeState.fallbackLanguage,
507
+ lazy: runtimeState.lazy === true,
508
+ cachedLanguages: Array.from(runtimeState.cache.keys()).sort(),
509
+ manifestLanguages: runtimeState.keyManifest ? Array.from(runtimeState.keyManifest.keys()).sort() : [],
510
+ loadedFileCount: runtimeState.loadedFiles ? runtimeState.loadedFiles.size : 0,
511
+ eagerLoadedLanguages: runtimeState.eagerLoadedLanguages ? Array.from(runtimeState.eagerLoadedLanguages).sort() : [],
512
+ };
513
+ }
434
514
 
435
515
  function refresh(lang) {
436
516
  refreshForState(singletonState, lang);
437
517
  }
438
518
 
439
519
  function refreshForState(runtimeState, lang = runtimeState.language) {
520
+ lang = normalizeLanguageCode(lang, '');
521
+ if (!lang) return;
440
522
  if (runtimeState.cache.has(lang)) runtimeState.cache.delete(lang);
441
523
  if (runtimeState.eagerLoadedLanguages) runtimeState.eagerLoadedLanguages.delete(lang);
442
524
  if (runtimeState.keyManifest) runtimeState.keyManifest.delete(lang);
@@ -451,12 +533,15 @@ function refreshForState(runtimeState, lang = runtimeState.language) {
451
533
  }
452
534
 
453
535
  module.exports = {
454
- initRuntime,
455
- translate,
456
- t: translate,
457
- setLanguage,
458
- getLanguage,
459
- getAvailableLanguages,
536
+ initRuntime,
537
+ translate,
538
+ t: translate,
539
+ translateBatch,
540
+ setLanguage,
541
+ getLanguage,
542
+ getAvailableLanguages,
543
+ clearCache,
544
+ getCacheInfo,
460
545
  refresh,
461
546
  loadKeyManifest: (baseDir) => loadKeyManifestFromDir(baseDir || singletonState.baseDir),
462
547
  };