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.
- package/CHANGELOG.md +116 -29
- package/README.md +83 -18
- package/SECURITY.md +13 -5
- package/main/i18ntk-analyze.js +10 -20
- package/main/i18ntk-backup.js +227 -111
- package/main/i18ntk-init.js +153 -157
- package/main/i18ntk-scanner.js +9 -7
- package/main/i18ntk-setup.js +36 -13
- package/main/i18ntk-sizing.js +18 -50
- package/main/i18ntk-translate.js +169 -21
- package/main/i18ntk-usage.js +298 -154
- package/main/i18ntk-validate.js +49 -37
- package/main/manage/commands/AnalyzeCommand.js +7 -17
- package/main/manage/commands/CommandRouter.js +6 -6
- package/main/manage/commands/TranslateCommand.js +65 -56
- package/main/manage/commands/ValidateCommand.js +34 -26
- package/main/manage/index.js +11 -42
- package/main/manage/managers/InteractiveMenu.js +11 -40
- package/main/manage/services/InitService.js +114 -118
- package/main/manage/services/UsageService.js +244 -85
- package/package.json +55 -4
- package/runtime/enhanced.d.ts +5 -5
- package/runtime/enhanced.js +49 -25
- package/runtime/i18ntk.d.ts +30 -7
- package/runtime/index.d.ts +48 -19
- package/runtime/index.js +188 -97
- package/settings/settings-cli.js +115 -38
- package/settings/settings-manager.js +24 -6
- package/ui-locales/de.json +192 -11
- package/ui-locales/en.json +182 -8
- package/ui-locales/es.json +193 -12
- package/ui-locales/fr.json +189 -8
- package/ui-locales/ja.json +190 -8
- package/ui-locales/ru.json +191 -9
- package/ui-locales/zh.json +194 -9
- package/utils/cli-helper.js +8 -12
- package/utils/config-helper.js +1 -1
- package/utils/config-manager.js +8 -6
- package/utils/localized-confirm.js +55 -0
- package/utils/menu-layout.js +41 -0
- package/utils/report-writer.js +110 -0
- package/utils/security.js +15 -22
- package/utils/translate/api.js +31 -3
- package/utils/translate/placeholder.js +42 -1
- package/utils/translate/protection.js +17 -12
- package/utils/translate/report.js +3 -2
- package/utils/translate/safe-network.js +24 -4
- package/utils/usage-insights.js +435 -0
- package/utils/usage-source.js +50 -0
- package/utils/watch-locales.js +13 -9
package/runtime/enhanced.d.ts
CHANGED
|
@@ -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';
|
package/runtime/enhanced.js
CHANGED
|
@@ -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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
+
};
|
package/runtime/i18ntk.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// runtime/i18ntk.d.ts
|
|
2
|
-
// Complete TypeScript definitions for i18ntk internationalization framework
|
|
3
|
-
// Version
|
|
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
|
|
513
|
+
baseDir?: string;
|
|
491
514
|
language?: string;
|
|
492
515
|
fallbackLanguage?: string;
|
|
493
516
|
keySeparator?: string;
|
package/runtime/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
56
|
-
if (!
|
|
57
|
-
throw new Error(`Empty JSON file: ${file}`);
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
return JSON.parse(
|
|
61
|
-
} catch (parseError) {
|
|
62
|
-
|
|
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,
|
|
200
|
+
const langDirStat = SecurityUtils.safeStatSync(langDir, baseDir);
|
|
182
201
|
if (langDirStat && langDirStat.isDirectory()) return langDir;
|
|
183
|
-
const langFileStat = SecurityUtils.safeStatSync(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
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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,
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
393
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
if (
|
|
420
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
};
|