lambder 3.2.2 → 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 +41 -12
- package/dist/LambderI18n.js +58 -9
- package/dist/index.d.ts +1 -1
- 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;
|
|
@@ -89,9 +93,18 @@ export interface LambderI18nInstance<TLanguages extends Record<string, LambderLa
|
|
|
89
93
|
resetLanguage(): void;
|
|
90
94
|
/** The currently active language code. */
|
|
91
95
|
readonly currentLanguage: keyof TLanguages & string;
|
|
92
|
-
/** Metadata of the currently active language. */
|
|
93
|
-
readonly currentLanguageMeta: TLanguages[keyof TLanguages]
|
|
94
|
-
|
|
96
|
+
/** Metadata of the currently active language, with `code` injected. */
|
|
97
|
+
readonly currentLanguageMeta: TLanguages[keyof TLanguages] & {
|
|
98
|
+
code: keyof TLanguages & string;
|
|
99
|
+
};
|
|
100
|
+
/** Text direction of the active language (defaults to "ltr"). */
|
|
101
|
+
readonly currentDir: "ltr" | "rtl";
|
|
102
|
+
/** BCP-47 locale of the active language for Intl APIs (defaults to the code). */
|
|
103
|
+
readonly currentIntlLocale: string;
|
|
104
|
+
/**
|
|
105
|
+
* Subscribe to changes (language switched, or runtime dictionaries
|
|
106
|
+
* registered). Returns an unsubscribe function.
|
|
107
|
+
*/
|
|
95
108
|
onLanguageChange(listener: (code: keyof TLanguages & string) => void): () => void;
|
|
96
109
|
/**
|
|
97
110
|
* Apply the active language to `<html lang>` and `<html dir>` (RTL support).
|
|
@@ -103,7 +116,23 @@ export interface LambderI18nInstance<TLanguages extends Record<string, LambderLa
|
|
|
103
116
|
isLanguageCode(value: string): value is keyof TLanguages & string;
|
|
104
117
|
readonly languages: TLanguages;
|
|
105
118
|
readonly languageList: (keyof TLanguages & string)[];
|
|
119
|
+
/** Ordered language metadata (declaration order), with `code` injected — ready for switcher menus. */
|
|
120
|
+
readonly languageMetaList: (TLanguages[keyof TLanguages] & {
|
|
121
|
+
code: keyof TLanguages & string;
|
|
122
|
+
})[];
|
|
106
123
|
readonly defaultLanguage: TDefault;
|
|
107
124
|
readonly enforced: TEnforced;
|
|
108
125
|
}
|
|
109
|
-
|
|
126
|
+
/** Language codes of an instance: `LambderI18nCodes<typeof i18n>`. */
|
|
127
|
+
export type LambderI18nCodes<T extends {
|
|
128
|
+
languageList: readonly string[];
|
|
129
|
+
}> = T["languageList"][number];
|
|
130
|
+
/** Translation keys of an instance: `LambderI18nKeys<typeof i18n>`. */
|
|
131
|
+
export type LambderI18nKeys<T extends {
|
|
132
|
+
t: (...args: never[]) => string;
|
|
133
|
+
}> = Parameters<T["t"]>[0];
|
|
134
|
+
/** Translator type of an instance: `LambderI18nTranslatorFor<typeof i18n>`. */
|
|
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,31 +127,51 @@ 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() { return core.
|
|
167
|
+
get currentLanguageMeta() { return core.metaByCode.get(core.state.resolve()); },
|
|
168
|
+
get currentDir() {
|
|
169
|
+
return core.metaByCode.get(core.state.resolve())?.dir ?? "ltr";
|
|
170
|
+
},
|
|
171
|
+
get currentIntlLocale() {
|
|
172
|
+
const code = core.state.resolve();
|
|
173
|
+
return core.metaByCode.get(code)?.intlLocale ?? code;
|
|
174
|
+
},
|
|
135
175
|
onLanguageChange(listener) { return core.state.subscribe(listener); },
|
|
136
176
|
applyToDocument() {
|
|
137
177
|
const doc = globalThis.document;
|
|
@@ -139,11 +179,12 @@ const buildInstance = (core, layer) => {
|
|
|
139
179
|
return;
|
|
140
180
|
const code = core.state.resolve();
|
|
141
181
|
doc.documentElement.lang = code;
|
|
142
|
-
doc.documentElement.dir = core.
|
|
182
|
+
doc.documentElement.dir = core.metaByCode.get(code)?.dir ?? "ltr";
|
|
143
183
|
},
|
|
144
184
|
isLanguageCode: core.isCode,
|
|
145
185
|
languages: core.languages,
|
|
146
186
|
languageList: core.languageList,
|
|
187
|
+
languageMetaList: core.metaList,
|
|
147
188
|
defaultLanguage: core.defaultLanguage,
|
|
148
189
|
enforced: core.enforced,
|
|
149
190
|
};
|
|
@@ -167,6 +208,11 @@ export const createLambderI18n = (config) => {
|
|
|
167
208
|
throw new Error(`LambderI18n: base dictionary is missing language "${lang}".`);
|
|
168
209
|
}
|
|
169
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
|
+
}
|
|
170
216
|
const customDetect = config.detectLanguage
|
|
171
217
|
? () => config.detectLanguage({
|
|
172
218
|
isLanguageCode: isCode,
|
|
@@ -174,9 +220,12 @@ export const createLambderI18n = (config) => {
|
|
|
174
220
|
defaultLanguage: config.defaultLanguage,
|
|
175
221
|
})
|
|
176
222
|
: null;
|
|
223
|
+
const metaByCode = new Map(languageList.map((code) => [code, { code, ...config.languages[code] }]));
|
|
177
224
|
const core = {
|
|
178
225
|
languages: config.languages,
|
|
179
226
|
languageList,
|
|
227
|
+
metaByCode,
|
|
228
|
+
metaList: [...metaByCode.values()],
|
|
180
229
|
defaultLanguage: config.defaultLanguage,
|
|
181
230
|
enforced: config.enforced,
|
|
182
231
|
state: new LanguageState(isCode, config.defaultLanguage, customDetect),
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type { LambderSessionContext } from "./LambderSessionManager.js";
|
|
|
20
20
|
export { LambderDdbCache } from "./LambderDdbCache.js";
|
|
21
21
|
export type { LambderDdbCacheOptions, LambderDdbCacheSetOptions, LambderDdbCacheGetOrSetOptions, } from "./LambderDdbCache.js";
|
|
22
22
|
export { createLambderI18n } from "./LambderI18n.js";
|
|
23
|
-
export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, } from "./LambderI18n.js";
|
|
23
|
+
export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./LambderI18n.js";
|
|
24
24
|
export { type ApiContractShape, } from "./LambderApiContract.js";
|
|
25
25
|
export type { LambderRenderContext, LambderSessionRenderContext, LambderHttpEvent } from "./LambderContext.js";
|
|
26
26
|
export { createContext, isV2HttpEvent } from "./LambderContext.js";
|