lambder 3.2.3 → 3.2.4
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/dist/LambderI18n.d.ts +26 -13
- package/dist/LambderI18n.js +53 -17
- package/package.json +1 -1
package/dist/LambderI18n.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type LambderI18nExtractParams<S extends string> = S extends `${string}{${
|
|
|
25
25
|
* `{tokens}`, a params object with exactly those tokens is required.
|
|
26
26
|
*/
|
|
27
27
|
export type LambderI18nTranslator<TContract extends Record<string, string>> = <K extends keyof TContract & string>(...args: LambderI18nExtractParams<TContract[K]> extends never ? [key: K] : [key: K, params: Record<LambderI18nExtractParams<TContract[K]>, string | number>]) => string;
|
|
28
|
-
export interface LambderI18nConfig<TLanguages extends Record<string, LambderLanguageMeta>, TDefault extends keyof TLanguages & string, TEnforced extends readonly (keyof TLanguages & string)[],
|
|
28
|
+
export interface LambderI18nConfig<TLanguages extends Record<string, LambderLanguageMeta>, TDefault extends keyof TLanguages & string, TEnforced extends readonly (keyof TLanguages & string)[], TBase extends Record<TDefault, Record<string, string>>> {
|
|
29
29
|
/** Master registry of every supported language and its metadata. */
|
|
30
30
|
languages: TLanguages;
|
|
31
31
|
/** Final fallback language. Must be included in `enforced`. */
|
|
@@ -39,10 +39,8 @@ export interface LambderI18nConfig<TLanguages extends Record<string, LambderLang
|
|
|
39
39
|
* App-wide base dictionary. Strict: every language in `languages` must
|
|
40
40
|
* provide every key (the `defaultLanguage` block is the typed contract).
|
|
41
41
|
*/
|
|
42
|
-
base: {
|
|
43
|
-
[L in keyof TLanguages]: Record<keyof
|
|
44
|
-
} & {
|
|
45
|
-
[D in TDefault]: TContract;
|
|
42
|
+
base: TBase & {
|
|
43
|
+
[L in keyof TLanguages]: Record<keyof TBase[TDefault], string>;
|
|
46
44
|
};
|
|
47
45
|
/**
|
|
48
46
|
* Optional language detector, tried before browser detection. Return a
|
|
@@ -61,18 +59,22 @@ export interface LambderI18nInstance<TLanguages extends Record<string, LambderLa
|
|
|
61
59
|
/** Translator bound to an explicit language (per-request backend use). */
|
|
62
60
|
forLanguage(code: keyof TLanguages & string): LambderI18nTranslator<TContract>;
|
|
63
61
|
/**
|
|
64
|
-
* Strict extension: every language must provide every new key.
|
|
62
|
+
* Strict extension: every language must provide every new key. Keys must
|
|
63
|
+
* be new — redeclaring a parent key is a compile-time and runtime error.
|
|
65
64
|
* Returns a new instance whose key space = parent keys + new keys.
|
|
66
65
|
*/
|
|
67
66
|
extend<const TExt extends {
|
|
68
67
|
[D in TDefault]: Record<string, string>;
|
|
69
68
|
}>(dict: {
|
|
70
69
|
[L in keyof TLanguages]: Record<keyof TExt[TDefault], string>;
|
|
70
|
+
} & {
|
|
71
|
+
[D in TDefault]: Partial<Record<keyof TContract, never>>;
|
|
71
72
|
} & TExt): LambderI18nInstance<TLanguages, TDefault, TEnforced, TContract & TExt[TDefault]>;
|
|
72
73
|
/**
|
|
73
74
|
* Partial extension: only the `enforced` languages are required; all other
|
|
74
75
|
* languages are optional (and may provide a subset of keys) — missing
|
|
75
|
-
* translations fall back to the default language.
|
|
76
|
+
* translations fall back to the default language. Keys must be new —
|
|
77
|
+
* redeclaring a parent key is a compile-time and runtime error.
|
|
76
78
|
*/
|
|
77
79
|
extendPartial<const TExt extends {
|
|
78
80
|
[D in TDefault]: Record<string, string>;
|
|
@@ -80,8 +82,10 @@ export interface LambderI18nInstance<TLanguages extends Record<string, LambderLa
|
|
|
80
82
|
[E in TEnforced[number]]: Record<keyof TExt[TDefault], string>;
|
|
81
83
|
} & {
|
|
82
84
|
[L in Exclude<keyof TLanguages & string, TEnforced[number]>]?: Partial<Record<keyof TExt[TDefault], string>>;
|
|
85
|
+
} & {
|
|
86
|
+
[D in TDefault]: Partial<Record<keyof TContract, never>>;
|
|
83
87
|
} & TExt): LambderI18nInstance<TLanguages, TDefault, TEnforced, TContract & TExt[TDefault]>;
|
|
84
|
-
/** Merge additional translations at runtime (e.g. fetched from an API). */
|
|
88
|
+
/** Merge additional translations at runtime (e.g. fetched from an API). Notifies change listeners. */
|
|
85
89
|
registerDictionary(code: keyof TLanguages & string, dict: Record<string, string>): void;
|
|
86
90
|
/** Override the active language (shared with all extended instances). */
|
|
87
91
|
setLanguage(code: keyof TLanguages & string): void;
|
|
@@ -97,7 +101,10 @@ export interface LambderI18nInstance<TLanguages extends Record<string, LambderLa
|
|
|
97
101
|
readonly currentDir: "ltr" | "rtl";
|
|
98
102
|
/** BCP-47 locale of the active language for Intl APIs (defaults to the code). */
|
|
99
103
|
readonly currentIntlLocale: string;
|
|
100
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Subscribe to changes (language switched, or runtime dictionaries
|
|
106
|
+
* registered). Returns an unsubscribe function.
|
|
107
|
+
*/
|
|
101
108
|
onLanguageChange(listener: (code: keyof TLanguages & string) => void): () => void;
|
|
102
109
|
/**
|
|
103
110
|
* Apply the active language to `<html lang>` and `<html dir>` (RTL support).
|
|
@@ -117,9 +124,15 @@ export interface LambderI18nInstance<TLanguages extends Record<string, LambderLa
|
|
|
117
124
|
readonly enforced: TEnforced;
|
|
118
125
|
}
|
|
119
126
|
/** Language codes of an instance: `LambderI18nCodes<typeof i18n>`. */
|
|
120
|
-
export type LambderI18nCodes<T
|
|
127
|
+
export type LambderI18nCodes<T extends {
|
|
128
|
+
languageList: readonly string[];
|
|
129
|
+
}> = T["languageList"][number];
|
|
121
130
|
/** Translation keys of an instance: `LambderI18nKeys<typeof i18n>`. */
|
|
122
|
-
export type LambderI18nKeys<T
|
|
131
|
+
export type LambderI18nKeys<T extends {
|
|
132
|
+
t: (...args: never[]) => string;
|
|
133
|
+
}> = Parameters<T["t"]>[0];
|
|
123
134
|
/** Translator type of an instance: `LambderI18nTranslatorFor<typeof i18n>`. */
|
|
124
|
-
export type LambderI18nTranslatorFor<T
|
|
125
|
-
|
|
135
|
+
export type LambderI18nTranslatorFor<T extends {
|
|
136
|
+
t: unknown;
|
|
137
|
+
}> = T["t"];
|
|
138
|
+
export declare const createLambderI18n: <const TLanguages extends Record<string, LambderLanguageMeta>, const TDefault extends keyof TLanguages & string, const TEnforced extends readonly (keyof TLanguages & string)[], const TBase extends Record<TDefault, Record<string, string>>>(config: LambderI18nConfig<TLanguages, TDefault, TEnforced, TBase>) => LambderI18nInstance<TLanguages, TDefault, TEnforced, TBase[TDefault]>;
|
package/dist/LambderI18n.js
CHANGED
|
@@ -42,7 +42,14 @@ class LanguageState {
|
|
|
42
42
|
return this.override;
|
|
43
43
|
if (this.detected)
|
|
44
44
|
return this.detected;
|
|
45
|
-
|
|
45
|
+
// Fail-open: a broken app detector must not take down every t() call.
|
|
46
|
+
let custom = null;
|
|
47
|
+
try {
|
|
48
|
+
custom = this.customDetect?.();
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.error("LambderI18n: detectLanguage threw; continuing detection chain.", err);
|
|
52
|
+
}
|
|
46
53
|
if (custom && this.isCode(custom)) {
|
|
47
54
|
this.detected = custom;
|
|
48
55
|
return custom;
|
|
@@ -64,13 +71,24 @@ class LanguageState {
|
|
|
64
71
|
this.detected = null;
|
|
65
72
|
this.notify(this.resolve());
|
|
66
73
|
}
|
|
74
|
+
/** Re-notify listeners without a language change (e.g. dictionaries changed). */
|
|
75
|
+
emitChange() {
|
|
76
|
+
this.notify(this.resolve());
|
|
77
|
+
}
|
|
67
78
|
subscribe(listener) {
|
|
68
79
|
this.listeners.add(listener);
|
|
69
80
|
return () => { this.listeners.delete(listener); };
|
|
70
81
|
}
|
|
71
82
|
notify(code) {
|
|
72
|
-
for (const listener of this.listeners)
|
|
73
|
-
|
|
83
|
+
for (const listener of this.listeners) {
|
|
84
|
+
// Isolate listeners: one bad subscriber must not block the others.
|
|
85
|
+
try {
|
|
86
|
+
listener(code);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
console.error("LambderI18n: onLanguageChange listener threw.", err);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
74
92
|
}
|
|
75
93
|
}
|
|
76
94
|
const interpolate = (text, params) => {
|
|
@@ -98,6 +116,8 @@ const buildInstance = (core, layer) => {
|
|
|
98
116
|
return interpolate(text, params);
|
|
99
117
|
};
|
|
100
118
|
const t = (key, params) => translateIn(core.state.resolve(), key, params);
|
|
119
|
+
// forLanguage sits on the hot path of reactive T() bridges: cache per code.
|
|
120
|
+
const translatorCache = new Map();
|
|
101
121
|
const validateExtension = (dict, requiredLanguages, label) => {
|
|
102
122
|
for (const lang of Object.keys(dict)) {
|
|
103
123
|
if (!core.isCode(lang))
|
|
@@ -107,40 +127,50 @@ const buildInstance = (core, layer) => {
|
|
|
107
127
|
if (!dict[lang])
|
|
108
128
|
throw new Error(`LambderI18n: ${label} is missing required language "${lang}".`);
|
|
109
129
|
}
|
|
130
|
+
for (const block of Object.values(dict)) {
|
|
131
|
+
for (const key of Object.keys(block ?? {})) {
|
|
132
|
+
if (layerLookup(layer, core.defaultLanguage, key) !== undefined) {
|
|
133
|
+
throw new Error(`LambderI18n: ${label} redeclares existing key "${key}".`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
110
137
|
};
|
|
111
138
|
const instance = {
|
|
112
|
-
t
|
|
139
|
+
t,
|
|
113
140
|
forLanguage(code) {
|
|
141
|
+
const cached = translatorCache.get(code);
|
|
142
|
+
if (cached)
|
|
143
|
+
return cached;
|
|
114
144
|
if (!core.isCode(code))
|
|
115
145
|
throw new Error(`LambderI18n: unsupported language code "${code}".`);
|
|
116
|
-
|
|
146
|
+
const translator = (key, params) => translateIn(code, key, params);
|
|
147
|
+
translatorCache.set(code, translator);
|
|
148
|
+
return translator;
|
|
117
149
|
},
|
|
118
150
|
extend(dict) {
|
|
119
151
|
validateExtension(dict, core.languageList, "extend() dictionary");
|
|
120
|
-
return buildInstance(core, { dicts: dict, parent: layer });
|
|
152
|
+
return buildInstance(core, { dicts: { ...dict }, parent: layer });
|
|
121
153
|
},
|
|
122
154
|
extendPartial(dict) {
|
|
123
155
|
validateExtension(dict, core.enforced, "extendPartial() dictionary");
|
|
124
|
-
return buildInstance(core, { dicts: dict, parent: layer });
|
|
156
|
+
return buildInstance(core, { dicts: { ...dict }, parent: layer });
|
|
125
157
|
},
|
|
126
158
|
registerDictionary(code, dict) {
|
|
127
159
|
if (!core.isCode(code))
|
|
128
160
|
throw new Error(`LambderI18n: unsupported language code "${code}".`);
|
|
129
161
|
layer.dicts[code] = { ...layer.dicts[code], ...dict };
|
|
162
|
+
core.state.emitChange();
|
|
130
163
|
},
|
|
131
164
|
setLanguage(code) { core.state.set(code); },
|
|
132
165
|
resetLanguage() { core.state.reset(); },
|
|
133
166
|
get currentLanguage() { return core.state.resolve(); },
|
|
134
|
-
get currentLanguageMeta() {
|
|
135
|
-
const code = core.state.resolve();
|
|
136
|
-
return { code, ...core.languages[code] };
|
|
137
|
-
},
|
|
167
|
+
get currentLanguageMeta() { return core.metaByCode.get(core.state.resolve()); },
|
|
138
168
|
get currentDir() {
|
|
139
|
-
return core.
|
|
169
|
+
return core.metaByCode.get(core.state.resolve())?.dir ?? "ltr";
|
|
140
170
|
},
|
|
141
171
|
get currentIntlLocale() {
|
|
142
172
|
const code = core.state.resolve();
|
|
143
|
-
return core.
|
|
173
|
+
return core.metaByCode.get(code)?.intlLocale ?? code;
|
|
144
174
|
},
|
|
145
175
|
onLanguageChange(listener) { return core.state.subscribe(listener); },
|
|
146
176
|
applyToDocument() {
|
|
@@ -149,14 +179,12 @@ const buildInstance = (core, layer) => {
|
|
|
149
179
|
return;
|
|
150
180
|
const code = core.state.resolve();
|
|
151
181
|
doc.documentElement.lang = code;
|
|
152
|
-
doc.documentElement.dir = core.
|
|
182
|
+
doc.documentElement.dir = core.metaByCode.get(code)?.dir ?? "ltr";
|
|
153
183
|
},
|
|
154
184
|
isLanguageCode: core.isCode,
|
|
155
185
|
languages: core.languages,
|
|
156
186
|
languageList: core.languageList,
|
|
157
|
-
|
|
158
|
-
return core.languageList.map((code) => ({ code, ...core.languages[code] }));
|
|
159
|
-
},
|
|
187
|
+
languageMetaList: core.metaList,
|
|
160
188
|
defaultLanguage: core.defaultLanguage,
|
|
161
189
|
enforced: core.enforced,
|
|
162
190
|
};
|
|
@@ -180,6 +208,11 @@ export const createLambderI18n = (config) => {
|
|
|
180
208
|
throw new Error(`LambderI18n: base dictionary is missing language "${lang}".`);
|
|
181
209
|
}
|
|
182
210
|
}
|
|
211
|
+
for (const lang of Object.keys(config.base)) {
|
|
212
|
+
if (!isCode(lang)) {
|
|
213
|
+
throw new Error(`LambderI18n: base dictionary contains unsupported language "${lang}".`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
183
216
|
const customDetect = config.detectLanguage
|
|
184
217
|
? () => config.detectLanguage({
|
|
185
218
|
isLanguageCode: isCode,
|
|
@@ -187,9 +220,12 @@ export const createLambderI18n = (config) => {
|
|
|
187
220
|
defaultLanguage: config.defaultLanguage,
|
|
188
221
|
})
|
|
189
222
|
: null;
|
|
223
|
+
const metaByCode = new Map(languageList.map((code) => [code, { code, ...config.languages[code] }]));
|
|
190
224
|
const core = {
|
|
191
225
|
languages: config.languages,
|
|
192
226
|
languageList,
|
|
227
|
+
metaByCode,
|
|
228
|
+
metaList: [...metaByCode.values()],
|
|
193
229
|
defaultLanguage: config.defaultLanguage,
|
|
194
230
|
enforced: config.enforced,
|
|
195
231
|
state: new LanguageState(isCode, config.defaultLanguage, customDetect),
|