lambder 7.2.1 → 7.2.2
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 +28 -0
- package/README.md +2 -2
- package/dist/client.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/shared/LambderI18n.d.ts +45 -5
- package/dist/shared/LambderI18n.js +117 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,34 @@ sit on its first published patch, and later patches list only what they changed.
|
|
|
9
9
|
Releases up to 3.2.6 carry git tags; the ones after it were published without
|
|
10
10
|
one, so versions are not cross-linked to tag comparisons here.
|
|
11
11
|
|
|
12
|
+
## [7.2.2] - 2026-09-18
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Languages loaded on demand in `createLambderI18n`.** Any language block
|
|
17
|
+
except the default one, in `base`, `extend` and `extendPartial`, can be a
|
|
18
|
+
loader instead of the dictionary: a function resolving to the dictionary or
|
|
19
|
+
to a module whose default export is one, so `tr: () => import("./tr")` is
|
|
20
|
+
the whole thing and a bundler gives each language its own file. Until now
|
|
21
|
+
every declared language had to be written inline, so every visitor
|
|
22
|
+
downloaded every language. A loader is checked against the same contract as
|
|
23
|
+
an inline block, and a missing key is a compile error that names it. The
|
|
24
|
+
default block stays inline: it is the contract and the fallback every
|
|
25
|
+
lookup ends in, and creation refuses a loader there.
|
|
26
|
+
- **`i18n.loadLanguage(code?)`** runs a language's loaders (the active
|
|
27
|
+
language by default) across every instance sharing the root, and resolves
|
|
28
|
+
once their dictionaries are merged. Until then `t` falls back per key to
|
|
29
|
+
the default language, as it does for any missing translation. Concurrent
|
|
30
|
+
calls share each load, and change listeners fire once per load. A loader
|
|
31
|
+
that answered never runs again; one that rejected rejects the call and runs
|
|
32
|
+
again on the next. Creating an extension loads nothing, so one created
|
|
33
|
+
after its language was loaded awaits its own `loadLanguage()`, which runs
|
|
34
|
+
only what is still missing. `setLanguage` and `resetLanguage` start the
|
|
35
|
+
loaders of the language they switch to, and listeners fire again when they
|
|
36
|
+
land; loading first switches without a flash of the default language.
|
|
37
|
+
Configs without loaders behave exactly as before. The
|
|
38
|
+
`LambderI18nDictionaryLoader` type is exported from both entries.
|
|
39
|
+
|
|
12
40
|
## [7.2.1] - 2026-09-15
|
|
13
41
|
|
|
14
42
|
### Added
|
package/README.md
CHANGED
|
@@ -146,7 +146,7 @@ guide that matches what you are building. The full index lives in
|
|
|
146
146
|
| [Frontend client](./docs/client.md) | `LambderCaller`: typed calls, failure outcomes, timeouts, guard inputs, request compression, transports |
|
|
147
147
|
| [Frontend hosting](./docs/frontend-hosting.md) | File sources, `servePublicFiles`, `serveIndexHtml`, `res.templateFile` |
|
|
148
148
|
| [Templating](./docs/templating.md) | `html`/`xml` tagged templates and `LambderTemplatingEngine` |
|
|
149
|
-
| [Translations](./docs/i18n.md) | `createLambderI18n`: typed keys, extension, detection, runtime dictionaries |
|
|
149
|
+
| [Translations](./docs/i18n.md) | `createLambderI18n`: typed keys, extension, detection, on-demand languages, runtime dictionaries |
|
|
150
150
|
| [The mock runtime](./docs/mock.md) | `LambderMockApp`: the typed contract served from mock handlers over the real pipeline, in the browser and in tests |
|
|
151
151
|
| [DynamoDB tables](./docs/dynamodb-tables.md) | Table shapes, TTL and IAM for sessions, cache, rate limits and idempotency |
|
|
152
152
|
| [Exports reference](./docs/exports.md) | Every name the three entry points export, grouped by purpose |
|
|
@@ -159,7 +159,7 @@ framework:
|
|
|
159
159
|
| Module | Guide | Description |
|
|
160
160
|
| --- | --- | --- |
|
|
161
161
|
| `html` / `xml` tags + `LambderTemplatingEngine` | [Templating](./docs/templating.md) | Type-safe tagged templates and a comment-only HTML template engine (build-pipeline-safe) |
|
|
162
|
-
| `createLambderI18n` | [Translations](./docs/i18n.md) | Typed translations with enforced/optional languages, component-level extension
|
|
162
|
+
| `createLambderI18n` | [Translations](./docs/i18n.md) | Typed translations with enforced/optional languages, component-level extension, auto language detection and on-demand language loading (isomorphic) |
|
|
163
163
|
| `LambderDdbCache` | [DynamoDB cache](./docs/ddb-cache.md) | DynamoDB-backed compressed JSON cache with lease-based single-fill and grouped keys (server-only) |
|
|
164
164
|
| `LambderDdbRateLimiter` / `LambderMemoryRateLimiter` | [Rate limiter](./docs/ddb-rate-limiter.md) | Fixed-window rate limiter, atomic per window, in DynamoDB (server-only) or in memory |
|
|
165
165
|
| `LambderDdbIdempotencyStore` / `LambderMemoryIdempotencyStore` | [Idempotency store](./docs/ddb-idempotency.md) | Idempotency records with owner-checked claims, in DynamoDB (compressed replays, server-only) or in memory |
|
package/dist/client.d.ts
CHANGED
|
@@ -32,5 +32,5 @@ export { resolveCompressionOption } from "./shared/wire/LambderCompressionOption
|
|
|
32
32
|
export type { LambderCompressionOption, LambderCompressionSettingsBase } from "./shared/wire/LambderCompressionOption.js";
|
|
33
33
|
export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml, type LambderHtmlValue } from "./shared/LambderHtml.js";
|
|
34
34
|
export { createLambderI18n } from "./shared/LambderI18n.js";
|
|
35
|
-
export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
|
|
35
|
+
export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
|
|
36
36
|
export type { LambderHttpStatusCode } from "./shared/wire/LambderHttpStatus.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export type { LambderRateLimitKeyFn, LambderRateLimitPer, LambderRateLimitBudget
|
|
|
103
103
|
export type { LambderApiIdempotencyConfig } from "./api/LambderApiIdempotency.js";
|
|
104
104
|
export type { LambderGuardsOptionValue, LambderRateLimitOverride, LambderRateLimitOptionValue, LambderApiIdempotencyOption, } from "./shared/wire/LambderApiOptionValues.js";
|
|
105
105
|
export { createLambderI18n } from "./shared/LambderI18n.js";
|
|
106
|
-
export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
|
|
106
|
+
export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
|
|
107
107
|
export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderApiNullAnswerConfig, LambderContractEntry, LambderMergeContract, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
|
|
108
108
|
export type { LambderRenderContext, LambderSessionRenderContext, LambderHttpEvent, LambderHttpEventFormat } from "./core/LambderContext.js";
|
|
109
109
|
export type { LambderApiAnswerOutcome, LambderApiSuccessOutcome, LambderApiCallFailure, LambderApiValidationFailure, LambderApiEnvelopeFailure, LambderApiHttpAnswer, } from "./shared/wire/LambderApiOutcome.js";
|
|
@@ -25,6 +25,21 @@ 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
|
+
/**
|
|
29
|
+
* A language block fetched on demand instead of bundled: a function that
|
|
30
|
+
* resolves to the dictionary, or to a module whose default export is the
|
|
31
|
+
* dictionary, so `() => import("./tr")` is a loader. It runs when
|
|
32
|
+
* `loadLanguage` asks for its language, never before.
|
|
33
|
+
*/
|
|
34
|
+
export type LambderI18nDictionaryLoader<TDict> = () => Promise<TDict | {
|
|
35
|
+
default: TDict;
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* What a non-default language block is checked against: a loader when one was
|
|
39
|
+
* given, the dictionary otherwise. Checking against the matching side alone,
|
|
40
|
+
* rather than the union, is what lets a compile error name the missing key.
|
|
41
|
+
*/
|
|
42
|
+
type LanguageBlockFor<TBlocks, L, TDict> = L extends keyof TBlocks ? TBlocks[L] extends (...args: never[]) => unknown ? LambderI18nDictionaryLoader<TDict> : TDict : TDict;
|
|
28
43
|
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
44
|
/** Master registry of every supported language and its metadata. */
|
|
30
45
|
languages: TLanguages;
|
|
@@ -38,9 +53,12 @@ export interface LambderI18nConfig<TLanguages extends Record<string, LambderLang
|
|
|
38
53
|
/**
|
|
39
54
|
* App-wide base dictionary. Strict: every language in `languages` must
|
|
40
55
|
* provide every key (the `defaultLanguage` block is the typed contract).
|
|
56
|
+
* Any language but the default may be a loader instead
|
|
57
|
+
* (`tr: () => import("./tr")`), fetched by `loadLanguage`. The default
|
|
58
|
+
* block stays inline, because every lookup falls back to it.
|
|
41
59
|
*/
|
|
42
60
|
base: TBase & {
|
|
43
|
-
[L in keyof TLanguages]: Record<keyof TBase[TDefault], string
|
|
61
|
+
[L in keyof TLanguages]: L extends TDefault ? Record<keyof TBase[TDefault], string> : LanguageBlockFor<TBase, L, Record<keyof TBase[TDefault], string>>;
|
|
44
62
|
};
|
|
45
63
|
/**
|
|
46
64
|
* Optional language detector, tried before browser detection. Return a
|
|
@@ -61,12 +79,13 @@ export interface LambderI18nInstance<TLanguages extends Record<string, LambderLa
|
|
|
61
79
|
/**
|
|
62
80
|
* Strict extension: every language must provide every new key. Keys must
|
|
63
81
|
* be new: redeclaring a parent key is a compile-time and runtime error.
|
|
82
|
+
* Any language but the default may be a loader, as in `base`.
|
|
64
83
|
* Returns a new instance whose key space = parent keys + new keys.
|
|
65
84
|
*/
|
|
66
85
|
extend<const TExt extends {
|
|
67
86
|
[D in TDefault]: Record<string, string>;
|
|
68
87
|
}>(dict: {
|
|
69
|
-
[L in keyof TLanguages]: Record<keyof TExt[TDefault], string
|
|
88
|
+
[L in keyof TLanguages]: L extends TDefault ? Record<keyof TExt[TDefault], string> : LanguageBlockFor<TExt, L, Record<keyof TExt[TDefault], string>>;
|
|
70
89
|
} & {
|
|
71
90
|
[D in TDefault]: Partial<Record<keyof TContract, never>>;
|
|
72
91
|
} & TExt): LambderI18nInstance<TLanguages, TDefault, TEnforced, TContract & TExt[TDefault]>;
|
|
@@ -75,19 +94,39 @@ export interface LambderI18nInstance<TLanguages extends Record<string, LambderLa
|
|
|
75
94
|
* languages are optional (and may provide a subset of keys), and missing
|
|
76
95
|
* translations fall back to the default language. Keys must be new:
|
|
77
96
|
* redeclaring a parent key is a compile-time and runtime error.
|
|
97
|
+
* Any language but the default may be a loader, as in `base`.
|
|
78
98
|
*/
|
|
79
99
|
extendPartial<const TExt extends {
|
|
80
100
|
[D in TDefault]: Record<string, string>;
|
|
81
101
|
}>(dict: {
|
|
82
|
-
[E in TEnforced[number]]: Record<keyof TExt[TDefault], string
|
|
102
|
+
[E in TEnforced[number]]: E extends TDefault ? Record<keyof TExt[TDefault], string> : LanguageBlockFor<TExt, E, Record<keyof TExt[TDefault], string>>;
|
|
83
103
|
} & {
|
|
84
|
-
[L in Exclude<keyof TLanguages & string, TEnforced[number]>]?: Partial<Record<keyof TExt[TDefault], string
|
|
104
|
+
[L in Exclude<keyof TLanguages & string, TEnforced[number]>]?: LanguageBlockFor<TExt, L, Partial<Record<keyof TExt[TDefault], string>>>;
|
|
85
105
|
} & {
|
|
86
106
|
[D in TDefault]: Partial<Record<keyof TContract, never>>;
|
|
87
107
|
} & TExt): LambderI18nInstance<TLanguages, TDefault, TEnforced, TContract & TExt[TDefault]>;
|
|
108
|
+
/**
|
|
109
|
+
* Run the loaders a language has in this instance and in every instance
|
|
110
|
+
* sharing its root, and resolve once their dictionaries are merged.
|
|
111
|
+
* Defaults to the active language; resolves at once when nothing is left
|
|
112
|
+
* to load. Until then `t` falls back per key to the default language, so
|
|
113
|
+
* await it before the first render, and before `setLanguage` to switch
|
|
114
|
+
* without a flash of the default language. Change listeners fire once
|
|
115
|
+
* per load, however many calls share it. A loader that answered never
|
|
116
|
+
* runs again; one that rejected rejects this call and runs again on the
|
|
117
|
+
* next. Creating an extension loads nothing: one created after its
|
|
118
|
+
* language was loaded awaits its own `loadLanguage()`, which runs only
|
|
119
|
+
* what is still missing.
|
|
120
|
+
*/
|
|
121
|
+
loadLanguage(code?: keyof TLanguages & string): Promise<void>;
|
|
88
122
|
/** Merge additional translations at runtime (e.g. fetched from an API). Notifies change listeners. */
|
|
89
123
|
registerDictionary(code: keyof TLanguages & string, dict: Record<string, string>): void;
|
|
90
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Override the active language (shared with all extended instances), and
|
|
126
|
+
* start its loaders; change listeners fire again when they land. Await
|
|
127
|
+
* `loadLanguage(code)` first to switch without a flash of the default
|
|
128
|
+
* language.
|
|
129
|
+
*/
|
|
91
130
|
setLanguage(code: keyof TLanguages & string): void;
|
|
92
131
|
/** Clear the override and re-run detection. */
|
|
93
132
|
resetLanguage(): void;
|
|
@@ -136,3 +175,4 @@ export type LambderI18nTranslatorFor<T extends {
|
|
|
136
175
|
t: unknown;
|
|
137
176
|
}> = T["t"];
|
|
138
177
|
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]>;
|
|
178
|
+
export {};
|
|
@@ -108,6 +108,93 @@ const layerLookup = (layer, lang, key) => {
|
|
|
108
108
|
}
|
|
109
109
|
return undefined;
|
|
110
110
|
};
|
|
111
|
+
const assertNoRedeclaredKeys = (parent, defaultLanguage, block, label) => {
|
|
112
|
+
for (const key of Object.keys(block)) {
|
|
113
|
+
if (layerLookup(parent, defaultLanguage, key) !== undefined) {
|
|
114
|
+
throw new Error(`LambderI18n: ${label} redeclares existing key "${key}".`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
/** The default block is what every lookup falls back to, so it cannot wait for a loader. */
|
|
119
|
+
const assertInlineDefaultBlock = (dict, defaultLanguage, label) => {
|
|
120
|
+
if (typeof dict[defaultLanguage] === "function") {
|
|
121
|
+
throw new Error(`LambderI18n: ${label} must give the default language "${defaultLanguage}" inline, not as a loader.`);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const createLayer = (core, sources, parent) => {
|
|
125
|
+
const layer = { dicts: {}, loaders: new Map(), inFlight: new Map(), parent };
|
|
126
|
+
for (const [lang, source] of Object.entries(sources)) {
|
|
127
|
+
if (typeof source === "function")
|
|
128
|
+
layer.loaders.set(lang, source);
|
|
129
|
+
else if (source)
|
|
130
|
+
layer.dicts[lang] = source;
|
|
131
|
+
}
|
|
132
|
+
if (layer.loaders.size > 0)
|
|
133
|
+
core.lazyLayers.add(layer);
|
|
134
|
+
return layer;
|
|
135
|
+
};
|
|
136
|
+
const isObjectValue = (value) => typeof value === "object" && value !== null;
|
|
137
|
+
/**
|
|
138
|
+
* A loader resolves to the dictionary itself or to a module whose default
|
|
139
|
+
* export is the dictionary. The two cannot be confused: a dictionary's values
|
|
140
|
+
* are strings, so a `default` holding an object can only be a module's.
|
|
141
|
+
*/
|
|
142
|
+
const dictionaryFromLoaded = (loaded, lang) => {
|
|
143
|
+
const dict = isObjectValue(loaded) && isObjectValue(loaded.default) ? loaded.default : loaded;
|
|
144
|
+
if (!isObjectValue(dict)) {
|
|
145
|
+
throw new Error(`LambderI18n: the "${lang}" loader resolved to neither a dictionary nor a module whose default export is one.`);
|
|
146
|
+
}
|
|
147
|
+
return dict;
|
|
148
|
+
};
|
|
149
|
+
/** Run a layer's loader for a language and merge what it brings, registered as the layer's load under way. */
|
|
150
|
+
const startLayerLoad = (core, layer, lang, loader) => {
|
|
151
|
+
const load = Promise.resolve()
|
|
152
|
+
.then(loader)
|
|
153
|
+
.then((loaded) => {
|
|
154
|
+
// A loader that answered is spent even when its answer is refused:
|
|
155
|
+
// running it again would fetch the same file and fail the same
|
|
156
|
+
// way. Only a loader that rejected stays, to be retried.
|
|
157
|
+
layer.loaders.delete(lang);
|
|
158
|
+
if (layer.loaders.size === 0)
|
|
159
|
+
core.lazyLayers.delete(layer);
|
|
160
|
+
const dict = dictionaryFromLoaded(loaded, lang);
|
|
161
|
+
if (layer.parent)
|
|
162
|
+
assertNoRedeclaredKeys(layer.parent, core.defaultLanguage, dict, `the "${lang}" loader`);
|
|
163
|
+
// Translations registered while the loader ran override what it brought.
|
|
164
|
+
layer.dicts[lang] = { ...dict, ...layer.dicts[lang] };
|
|
165
|
+
})
|
|
166
|
+
.finally(() => { layer.inFlight.delete(lang); });
|
|
167
|
+
layer.inFlight.set(lang, load);
|
|
168
|
+
return load;
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* loadLanguage for a whole root: every layer still holding a loader for the
|
|
172
|
+
* language. Concurrent calls share each layer's load, and only the call that
|
|
173
|
+
* started loads announces them, so one load is one change event.
|
|
174
|
+
*/
|
|
175
|
+
const loadLanguageAcrossRoot = async (core, lang) => {
|
|
176
|
+
const started = [];
|
|
177
|
+
const joined = [];
|
|
178
|
+
for (const layer of core.lazyLayers) {
|
|
179
|
+
const loader = layer.loaders.get(lang);
|
|
180
|
+
if (!loader)
|
|
181
|
+
continue;
|
|
182
|
+
const running = layer.inFlight.get(lang);
|
|
183
|
+
if (running)
|
|
184
|
+
joined.push(running);
|
|
185
|
+
else
|
|
186
|
+
started.push(startLayerLoad(core, layer, lang, loader));
|
|
187
|
+
}
|
|
188
|
+
if (started.length === 0 && joined.length === 0)
|
|
189
|
+
return;
|
|
190
|
+
const [startedResults, joinedResults] = await Promise.all([Promise.allSettled(started), Promise.allSettled(joined)]);
|
|
191
|
+
if (startedResults.some((result) => result.status === "fulfilled"))
|
|
192
|
+
core.state.emitChange();
|
|
193
|
+
const failure = [...startedResults, ...joinedResults]
|
|
194
|
+
.find((result) => result.status === "rejected");
|
|
195
|
+
if (failure)
|
|
196
|
+
throw failure.reason;
|
|
197
|
+
};
|
|
111
198
|
const buildInstance = (core, layer) => {
|
|
112
199
|
const translateIn = (lang, key, params) => {
|
|
113
200
|
const text = layerLookup(layer, lang, key)
|
|
@@ -127,14 +214,20 @@ const buildInstance = (core, layer) => {
|
|
|
127
214
|
if (!dict[lang])
|
|
128
215
|
throw new Error(`LambderI18n: ${label} is missing required language "${lang}".`);
|
|
129
216
|
}
|
|
217
|
+
assertInlineDefaultBlock(dict, core.defaultLanguage, label);
|
|
218
|
+
// A loader's keys are checked the same way once it has run.
|
|
130
219
|
for (const block of Object.values(dict)) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
throw new Error(`LambderI18n: ${label} redeclares existing key "${key}".`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
220
|
+
if (isObjectValue(block))
|
|
221
|
+
assertNoRedeclaredKeys(layer, core.defaultLanguage, block, label);
|
|
136
222
|
}
|
|
137
223
|
};
|
|
224
|
+
// setLanguage and resetLanguage cannot hand a failure back, so it is logged
|
|
225
|
+
// and the language keeps falling back to the default one.
|
|
226
|
+
const loadSwitchedLanguage = (lang) => {
|
|
227
|
+
loadLanguageAcrossRoot(core, lang).catch((err) => {
|
|
228
|
+
console.error(`LambderI18n: loading "${lang}" after switching to it failed; it falls back to the default language.`, err);
|
|
229
|
+
});
|
|
230
|
+
};
|
|
138
231
|
const instance = {
|
|
139
232
|
t,
|
|
140
233
|
forLanguage(code) {
|
|
@@ -149,11 +242,17 @@ const buildInstance = (core, layer) => {
|
|
|
149
242
|
},
|
|
150
243
|
extend(dict) {
|
|
151
244
|
validateExtension(dict, core.languageList, "extend() dictionary");
|
|
152
|
-
return buildInstance(core,
|
|
245
|
+
return buildInstance(core, createLayer(core, dict, layer));
|
|
153
246
|
},
|
|
154
247
|
extendPartial(dict) {
|
|
155
248
|
validateExtension(dict, core.enforced, "extendPartial() dictionary");
|
|
156
|
-
return buildInstance(core,
|
|
249
|
+
return buildInstance(core, createLayer(core, dict, layer));
|
|
250
|
+
},
|
|
251
|
+
loadLanguage(code) {
|
|
252
|
+
const lang = code ?? core.state.resolve();
|
|
253
|
+
if (!core.isCode(lang))
|
|
254
|
+
return Promise.reject(new Error(`LambderI18n: unsupported language code "${lang}".`));
|
|
255
|
+
return loadLanguageAcrossRoot(core, lang);
|
|
157
256
|
},
|
|
158
257
|
registerDictionary(code, dict) {
|
|
159
258
|
if (!core.isCode(code))
|
|
@@ -161,8 +260,14 @@ const buildInstance = (core, layer) => {
|
|
|
161
260
|
layer.dicts[code] = { ...layer.dicts[code], ...dict };
|
|
162
261
|
core.state.emitChange();
|
|
163
262
|
},
|
|
164
|
-
setLanguage(code) {
|
|
165
|
-
|
|
263
|
+
setLanguage(code) {
|
|
264
|
+
core.state.set(code);
|
|
265
|
+
loadSwitchedLanguage(code);
|
|
266
|
+
},
|
|
267
|
+
resetLanguage() {
|
|
268
|
+
core.state.reset();
|
|
269
|
+
loadSwitchedLanguage(core.state.resolve());
|
|
270
|
+
},
|
|
166
271
|
get currentLanguage() { return core.state.resolve(); },
|
|
167
272
|
get currentLanguageMeta() { return core.metaByCode.get(core.state.resolve()); },
|
|
168
273
|
get currentDir() {
|
|
@@ -213,6 +318,7 @@ export const createLambderI18n = (config) => {
|
|
|
213
318
|
throw new Error(`LambderI18n: base dictionary contains unsupported language "${lang}".`);
|
|
214
319
|
}
|
|
215
320
|
}
|
|
321
|
+
assertInlineDefaultBlock(config.base, config.defaultLanguage, "base dictionary");
|
|
216
322
|
const customDetect = config.detectLanguage
|
|
217
323
|
? () => config.detectLanguage({
|
|
218
324
|
isLanguageCode: isCode,
|
|
@@ -230,6 +336,7 @@ export const createLambderI18n = (config) => {
|
|
|
230
336
|
enforced: config.enforced,
|
|
231
337
|
state: new LanguageState(isCode, config.defaultLanguage, customDetect),
|
|
232
338
|
isCode,
|
|
339
|
+
lazyLayers: new Set(),
|
|
233
340
|
};
|
|
234
|
-
return buildInstance(core,
|
|
341
|
+
return buildInstance(core, createLayer(core, config.base, null));
|
|
235
342
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.2",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
|
|
6
6
|
"keywords": [
|