i18ntk 4.0.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 (50) hide show
  1. package/CHANGELOG.md +116 -29
  2. package/README.md +83 -18
  3. package/SECURITY.md +13 -5
  4. package/main/i18ntk-analyze.js +10 -20
  5. package/main/i18ntk-backup.js +227 -111
  6. package/main/i18ntk-init.js +153 -157
  7. package/main/i18ntk-scanner.js +9 -7
  8. package/main/i18ntk-setup.js +36 -13
  9. package/main/i18ntk-sizing.js +18 -50
  10. package/main/i18ntk-translate.js +169 -21
  11. package/main/i18ntk-usage.js +298 -154
  12. package/main/i18ntk-validate.js +49 -37
  13. package/main/manage/commands/AnalyzeCommand.js +7 -17
  14. package/main/manage/commands/CommandRouter.js +6 -6
  15. package/main/manage/commands/TranslateCommand.js +65 -56
  16. package/main/manage/commands/ValidateCommand.js +34 -26
  17. package/main/manage/index.js +11 -42
  18. package/main/manage/managers/InteractiveMenu.js +11 -40
  19. package/main/manage/services/InitService.js +114 -118
  20. package/main/manage/services/UsageService.js +244 -85
  21. package/package.json +55 -4
  22. package/runtime/enhanced.d.ts +5 -5
  23. package/runtime/enhanced.js +49 -25
  24. package/runtime/i18ntk.d.ts +30 -7
  25. package/runtime/index.d.ts +48 -19
  26. package/runtime/index.js +188 -97
  27. package/settings/settings-cli.js +115 -38
  28. package/settings/settings-manager.js +24 -6
  29. package/ui-locales/de.json +192 -11
  30. package/ui-locales/en.json +182 -8
  31. package/ui-locales/es.json +193 -12
  32. package/ui-locales/fr.json +189 -8
  33. package/ui-locales/ja.json +190 -8
  34. package/ui-locales/ru.json +191 -9
  35. package/ui-locales/zh.json +194 -9
  36. package/utils/cli-helper.js +8 -12
  37. package/utils/config-helper.js +1 -1
  38. package/utils/config-manager.js +8 -6
  39. package/utils/localized-confirm.js +55 -0
  40. package/utils/menu-layout.js +41 -0
  41. package/utils/report-writer.js +110 -0
  42. package/utils/security.js +15 -22
  43. package/utils/translate/api.js +31 -3
  44. package/utils/translate/placeholder.js +42 -1
  45. package/utils/translate/protection.js +17 -12
  46. package/utils/translate/report.js +3 -2
  47. package/utils/translate/safe-network.js +24 -4
  48. package/utils/usage-insights.js +435 -0
  49. package/utils/usage-source.js +50 -0
  50. package/utils/watch-locales.js +13 -9
@@ -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 = {};
@@ -139,7 +155,8 @@ function listJsonFilesRecursively(dir, baseDir = dir) {
139
155
  return results;
140
156
  }
141
157
 
142
- function loadKeyManifestFromDir(baseDir) {
158
+ function loadKeyManifestFromDir(baseDir) {
159
+ if (!baseDir || typeof baseDir !== 'string') return new Map();
143
160
  const validatedBase = SecurityUtils.validatePath(baseDir, path.dirname(baseDir));
144
161
  const baseStat = SecurityUtils.safeStatSync(validatedBase, path.dirname(validatedBase));
145
162
  const baseRoot = baseStat && baseStat.isFile() ? path.dirname(validatedBase) : validatedBase;
@@ -176,11 +193,13 @@ function loadKeyManifestFromDir(baseDir) {
176
193
  }
177
194
 
178
195
  function getLanguagePath(baseDir, lang) {
196
+ lang = normalizeLanguageCode(lang, '');
197
+ if (!lang) return null;
179
198
  const langDir = path.join(baseDir, lang);
180
199
  const langFile = path.join(baseDir, `${lang}.json`);
181
- const langDirStat = SecurityUtils.safeStatSync(langDir, path.dirname(langDir));
200
+ const langDirStat = SecurityUtils.safeStatSync(langDir, baseDir);
182
201
  if (langDirStat && langDirStat.isDirectory()) return langDir;
183
- const langFileStat = SecurityUtils.safeStatSync(langFile, path.dirname(langFile));
202
+ const langFileStat = SecurityUtils.safeStatSync(langFile, baseDir);
184
203
  if (langFileStat && langFileStat.isFile()) return langFile;
185
204
  return null;
186
205
  }
@@ -219,13 +238,15 @@ function loadFileLazy(runtimeState, filePath, lang) {
219
238
  return data;
220
239
  }
221
240
 
222
- function readLanguageFromBase(baseDir, lang) {
223
- const merged = {};
224
- const langFile = path.join(baseDir, `${lang}.json`);
225
- const langDir = path.join(baseDir, lang);
226
-
227
- // Prefer folder if exists, otherwise single file
228
- 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);
229
250
  if (langDirStat && langDirStat.isDirectory()) {
230
251
  const files = listJsonFilesRecursively(langDir, langDir);
231
252
  for (const file of files) {
@@ -236,8 +257,8 @@ function readLanguageFromBase(baseDir, lang) {
236
257
  // Skip unreadable/invalid files
237
258
  }
238
259
  }
239
- } else {
240
- const langFileStat = SecurityUtils.safeStatSync(langFile, path.dirname(langFile));
260
+ } else {
261
+ const langFileStat = SecurityUtils.safeStatSync(langFile, baseDir);
241
262
  if (langFileStat && langFileStat.isFile()) {
242
263
  try {
243
264
  const data = readJsonSafe(langFile);
@@ -249,9 +270,10 @@ function readLanguageFromBase(baseDir, lang) {
249
270
  return merged;
250
271
  }
251
272
 
252
- function getTranslationsForState(runtimeState, lang) {
253
- if (!runtimeState.baseDir) runtimeState.baseDir = resolveBaseDir();
254
-
273
+ function getTranslationsForState(runtimeState, lang) {
274
+ if (!runtimeState.baseDir) runtimeState.baseDir = resolveBaseDir();
275
+ lang = normalizeLanguageCode(lang, runtimeState.fallbackLanguage || 'en');
276
+
255
277
  if (runtimeState.lazy) {
256
278
  if (runtimeState.cache.has(lang)) return runtimeState.cache.get(lang);
257
279
  runtimeState.cache.set(lang, {});
@@ -265,12 +287,13 @@ function getTranslationsForState(runtimeState, lang) {
265
287
  return data;
266
288
  }
267
289
 
268
- function interpolate(template, params) {
269
- if (typeof template !== 'string') return template;
270
- return template
271
- .replace(/\{\{(\w+)\}\}/g, (m, p1) => (p1 in params ? String(params[p1]) : m))
272
- .replace(/\{(\w+)\}/g, (m, p1) => (p1 in params ? String(params[p1]) : m));
273
- }
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
+ }
274
297
 
275
298
  // Resolve a dotted key path from an object
276
299
  function resolveKey(obj, key, sep = '.', runtimeState = null, lang = null) {
@@ -288,11 +311,15 @@ function resolveKey(obj, key, sep = '.', runtimeState = null, lang = null) {
288
311
  const prefix = parts.slice(0, i).join(sep);
289
312
  const filePath = manifest.get(prefix);
290
313
  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);
314
+ if (filePath && !runtimeState.loadedFiles.has(loadedFileKey)) {
315
+ runtimeState.loadedFiles.add(loadedFileKey);
316
+ try {
317
+ loadFileLazy(runtimeState, filePath, lang);
318
+ } catch (_) {
319
+ // stale manifest entry — already marked as loaded to prevent retry
320
+ }
321
+ const langData = runtimeState.cache.get(lang);
322
+ return resolveKey(langData, key, sep, runtimeState, lang);
296
323
  }
297
324
  }
298
325
  if (!runtimeState.eagerLoadedLanguages.has(lang)) {
@@ -355,43 +382,67 @@ function preload(runtimeState, shouldPreload) {
355
382
  }
356
383
  }
357
384
 
358
- function createRuntime(runtimeState) {
359
- const runtimeTranslate = (key, params) => translateWithState(runtimeState, key, params);
360
- return {
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),
367
- };
368
- }
369
-
370
- function translate(key, params = {}) {
371
- return translateWithState(singletonState, key, params);
372
- }
373
-
374
- function translateWithState(runtimeState, key, params = {}) {
375
- const langData = getTranslationsForState(runtimeState, runtimeState.language);
376
- 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
+ }
377
399
 
378
- if (typeof value === 'undefined' && runtimeState.fallbackLanguage) {
379
- const fbData = getTranslationsForState(runtimeState, runtimeState.fallbackLanguage);
380
- 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);
381
419
  }
382
-
383
- if (typeof value === 'string') return interpolate(value, params);
384
- return typeof value === 'undefined' ? key : value;
385
- }
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
+ }
386
436
 
387
437
  function setLanguage(lang) {
388
438
  setLanguageForState(singletonState, lang);
389
439
  }
390
440
 
391
- function setLanguageForState(runtimeState, lang) {
392
- if (!lang || typeof lang !== 'string') return;
393
- runtimeState.language = lang;
394
- }
441
+ function setLanguageForState(runtimeState, lang) {
442
+ const safeLang = normalizeLanguageCode(lang, '');
443
+ if (!safeLang) return;
444
+ runtimeState.language = safeLang;
445
+ }
395
446
 
396
447
  function getLanguage() {
397
448
  return getLanguageForState(singletonState);
@@ -405,35 +456,72 @@ function getAvailableLanguages() {
405
456
  return getAvailableLanguagesForState(singletonState);
406
457
  }
407
458
 
408
- function getAvailableLanguagesForState(runtimeState) {
459
+ function getAvailableLanguagesForState(runtimeState) {
409
460
  const langs = new Set();
410
461
  if (!runtimeState.baseDir) runtimeState.baseDir = resolveBaseDir();
411
462
  if (!SecurityUtils.safeExistsSync(runtimeState.baseDir, path.dirname(runtimeState.baseDir))) return ['en'];
412
463
  try {
413
464
  for (const entry of fs.readdirSync(runtimeState.baseDir, { withFileTypes: true })) {
414
- if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
415
- langs.add(entry.name.replace(/\.json$/i, ''));
416
- } else if (entry.isDirectory()) {
417
- const lang = entry.name;
418
- const idx = path.join(runtimeState.baseDir, lang, `${lang}.json`);
419
- if (SecurityUtils.safeExistsSync(idx, path.dirname(idx))) langs.add(lang);
420
- else langs.add(lang); // be permissive
421
- }
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
+ }
422
475
  }
423
476
  } catch (_) {
424
477
  // Unreadable directory
425
478
  return ['en'];
426
479
  }
427
- return Array.from(langs.size ? langs : new Set(['en']));
428
- }
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
+ }
429
514
 
430
515
  function refresh(lang) {
431
516
  refreshForState(singletonState, lang);
432
517
  }
433
518
 
434
519
  function refreshForState(runtimeState, lang = runtimeState.language) {
520
+ lang = normalizeLanguageCode(lang, '');
521
+ if (!lang) return;
435
522
  if (runtimeState.cache.has(lang)) runtimeState.cache.delete(lang);
436
- if (runtimeState.eagerLoadedLanguages) runtimeState.eagerLoadedLanguages.delete(lang);
523
+ if (runtimeState.eagerLoadedLanguages) runtimeState.eagerLoadedLanguages.delete(lang);
524
+ if (runtimeState.keyManifest) runtimeState.keyManifest.delete(lang);
437
525
  if (runtimeState.loadedFiles) {
438
526
  for (const fileKey of Array.from(runtimeState.loadedFiles)) {
439
527
  if (fileKey.startsWith(`${lang}\0`)) runtimeState.loadedFiles.delete(fileKey);
@@ -445,12 +533,15 @@ function refreshForState(runtimeState, lang = runtimeState.language) {
445
533
  }
446
534
 
447
535
  module.exports = {
448
- initRuntime,
449
- translate,
450
- t: translate,
451
- setLanguage,
452
- getLanguage,
453
- getAvailableLanguages,
536
+ initRuntime,
537
+ translate,
538
+ t: translate,
539
+ translateBatch,
540
+ setLanguage,
541
+ getLanguage,
542
+ getAvailableLanguages,
543
+ clearCache,
544
+ getCacheInfo,
454
545
  refresh,
455
546
  loadKeyManifest: (baseDir) => loadKeyManifestFromDir(baseDir || singletonState.baseDir),
456
547
  };