@xndrjs/i18n 0.6.0 → 0.7.0-alpha.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/README.md +154 -134
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +276 -61
- package/dist/index.js +687 -126
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +23 -7
- package/dist/validation/index.js +196 -89
- package/dist/validation/index.js.map +1 -1
- package/package.json +1 -1
- package/src/IcuTranslationProviderMulti.test.ts +115 -215
- package/src/IcuTranslationProviderMulti.ts +107 -96
- package/src/IcuTranslationProviderSingle.test.ts +36 -294
- package/src/IcuTranslationProviderSingle.ts +55 -72
- package/src/builder-load-registry.ts +18 -0
- package/src/builder-loaders.ts +18 -0
- package/src/builder-multi.ts +481 -0
- package/src/builder-types.test.ts +80 -0
- package/src/builder-types.ts +24 -0
- package/src/builder.test.ts +421 -0
- package/src/builder.ts +112 -0
- package/src/codegen/delivery-artifacts.test.ts +2 -1
- package/src/codegen/delivery-artifacts.ts +2 -1
- package/src/codegen/emit/dictionary-file.test.ts +0 -7
- package/src/codegen/emit/dictionary-schema-file.ts +55 -42
- package/src/codegen/emit/instance-file.test.ts +88 -0
- package/src/codegen/emit/instance-file.ts +164 -17
- package/src/codegen/emit/namespace-loaders-file.test.ts +35 -10
- package/src/codegen/emit/namespace-loaders-file.ts +11 -61
- package/src/codegen/emit/types-file.test.ts +0 -2
- package/src/codegen/generate-i18n-types.test.ts +8 -303
- package/src/codegen/generate-i18n-types.ts +16 -1
- package/src/codegen/project-locales-set-namespace.test.ts +25 -32
- package/src/engine.ts +71 -0
- package/src/index.ts +47 -7
- package/src/patch-key.test.ts +186 -0
- package/src/patch-key.ts +140 -0
- package/src/project-locales.test.ts +76 -0
- package/src/project-locales.ts +58 -1
- package/src/scope-multi.ts +124 -0
- package/src/scope-single.ts +85 -0
- package/src/scope-types.ts +27 -0
- package/src/scope.test.ts +481 -0
- package/src/single-builder.ts +153 -0
- package/src/types.ts +16 -0
- package/src/validation/create-normalized-schema.ts +1 -1
- package/src/validation/errors.ts +2 -0
- package/src/validation/index.ts +135 -27
- package/src/validation/normalize.ts +176 -46
- package/src/validation/types.ts +4 -0
- package/src/validation/validate-normalized.ts +43 -1
- package/src/validation/validation.test.ts +200 -7
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
|
|
3
|
-
import type { LocaleOfMulti } from "./types.js";
|
|
4
3
|
|
|
5
4
|
type TestSchema = {
|
|
6
5
|
default: {
|
|
@@ -69,269 +68,170 @@ const dictionary: TestSchema = {
|
|
|
69
68
|
};
|
|
70
69
|
|
|
71
70
|
describe("IcuTranslationProviderMulti", () => {
|
|
72
|
-
const
|
|
71
|
+
const engine = new IcuTranslationProviderMulti<TestSchema, TestParams>(dictionary);
|
|
73
72
|
|
|
74
|
-
it("
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
it("tracks namespaces via dictionary keys", () => {
|
|
74
|
+
const partial = new IcuTranslationProviderMulti<TestSchema, TestParams>({
|
|
75
|
+
default: dictionary.default,
|
|
76
|
+
});
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
expect(
|
|
81
|
-
"You have 1 invoice for Ada"
|
|
82
|
-
);
|
|
83
|
-
expect(provider.get("billing", "invoice_summary", "en", { count: 3, name: "Ada" })).toBe(
|
|
84
|
-
"You have 3 invoices for Ada"
|
|
85
|
-
);
|
|
86
|
-
expect(provider.get("billing", "invoice_summary", "it", { count: 1, name: "Ada" })).toBe(
|
|
87
|
-
"Hai 1 fattura per Ada"
|
|
88
|
-
);
|
|
89
|
-
expect(provider.get("billing", "invoice_summary", "it", { count: 3, name: "Ada" })).toBe(
|
|
90
|
-
"Hai 3 fatture per Ada"
|
|
91
|
-
);
|
|
78
|
+
expect("default" in partial.getAll()).toBe(true);
|
|
79
|
+
expect("billing" in partial.getAll()).toBe(false);
|
|
92
80
|
});
|
|
93
81
|
|
|
94
|
-
it("
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
);
|
|
98
|
-
expect(provider.get("default", "dashboard_status", "en", { msgCount: 3, chatCount: 2 })).toBe(
|
|
99
|
-
"You have 3 messages in 2 chats"
|
|
100
|
-
);
|
|
101
|
-
expect(provider.get("default", "dashboard_status", "it", { msgCount: 1, chatCount: 1 })).toBe(
|
|
102
|
-
"Hai 1 messaggio in una chat"
|
|
103
|
-
);
|
|
104
|
-
expect(provider.get("default", "dashboard_status", "it", { msgCount: 3, chatCount: 2 })).toBe(
|
|
105
|
-
"Hai 3 messaggi in 2 chat"
|
|
106
|
-
);
|
|
107
|
-
});
|
|
82
|
+
it("supports partial initialization", () => {
|
|
83
|
+
const partial = new IcuTranslationProviderMulti<TestSchema, TestParams>({
|
|
84
|
+
default: dictionary.default,
|
|
85
|
+
});
|
|
108
86
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"Linus owns his inbox"
|
|
112
|
-
);
|
|
113
|
-
expect(provider.get("default", "ranking_position", "en", { position: 3 })).toBe(
|
|
114
|
-
"You finished 3rd"
|
|
87
|
+
expect(partial.toScope({ namespaces: ["default"] }).t("default", "login_button", "en")).toBe(
|
|
88
|
+
"Login"
|
|
115
89
|
);
|
|
116
90
|
});
|
|
117
91
|
|
|
118
|
-
it("
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const expectedAmount = new Intl.NumberFormat("en", {
|
|
122
|
-
style: "currency",
|
|
123
|
-
currency: "EUR",
|
|
124
|
-
}).format(amount);
|
|
125
|
-
const expectedDate = new Intl.DateTimeFormat("en", {
|
|
126
|
-
dateStyle: "short",
|
|
127
|
-
}).format(when);
|
|
128
|
-
const expectedTime = new Intl.DateTimeFormat("en", {
|
|
129
|
-
timeStyle: "short",
|
|
130
|
-
}).format(when);
|
|
92
|
+
it("patches a preloaded key and invalidates its cache", () => {
|
|
93
|
+
const local = new IcuTranslationProviderMulti<TestSchema, TestParams>(dictionary);
|
|
94
|
+
const view = () => local.toScope({ namespaces: ["default", "billing"] });
|
|
131
95
|
|
|
132
|
-
expect(
|
|
133
|
-
|
|
96
|
+
expect(view().t("billing", "invoice_summary", "en", { count: 2, name: "Bob" })).toBe(
|
|
97
|
+
"You have 2 invoices for Bob"
|
|
134
98
|
);
|
|
135
|
-
expect(
|
|
136
|
-
provider.get("billing", "appointment_summary", "en", {
|
|
137
|
-
dueDate: when,
|
|
138
|
-
startTime: when,
|
|
139
|
-
})
|
|
140
|
-
).toBe(`Due ${expectedDate} at ${expectedTime}`);
|
|
141
|
-
});
|
|
142
99
|
|
|
143
|
-
|
|
144
|
-
|
|
100
|
+
local.patchKeyMulti(
|
|
101
|
+
"billing",
|
|
102
|
+
"invoice_summary",
|
|
103
|
+
"en",
|
|
104
|
+
"{count, plural, one {1 bill} other {{count} bills}} for {name}"
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(view().t("billing", "invoice_summary", "en", { count: 2, name: "Bob" })).toBe(
|
|
108
|
+
"2 bills for Bob"
|
|
109
|
+
);
|
|
110
|
+
expect(view().t("default", "login_button", "en")).toBe("Login");
|
|
145
111
|
});
|
|
146
112
|
|
|
147
|
-
it("
|
|
113
|
+
it("adds namespace to dictionary with applyLoadMergeNamespace", () => {
|
|
148
114
|
const partial = new IcuTranslationProviderMulti<TestSchema, TestParams>({
|
|
149
115
|
default: dictionary.default,
|
|
150
116
|
});
|
|
117
|
+
expect("billing" in partial.getAll()).toBe(false);
|
|
151
118
|
|
|
152
|
-
|
|
153
|
-
expect(partial.
|
|
119
|
+
partial.applyLoadMergeNamespace("billing", dictionary.billing);
|
|
120
|
+
expect("billing" in partial.getAll()).toBe(true);
|
|
154
121
|
});
|
|
155
122
|
|
|
156
|
-
it("
|
|
157
|
-
const partial = new IcuTranslationProviderMulti<TestSchema, TestParams>({
|
|
158
|
-
|
|
159
|
-
});
|
|
123
|
+
it("adds namespace to dictionary on first applyLoadMergeNamespace", () => {
|
|
124
|
+
const partial = new IcuTranslationProviderMulti<TestSchema, TestParams>({});
|
|
125
|
+
expect("billing" in partial.getAll()).toBe(false);
|
|
160
126
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
).toThrow(
|
|
164
|
-
'[i18n] Namespace not loaded: "billing". Register it with setNamespace() before calling .get().'
|
|
165
|
-
);
|
|
127
|
+
partial.applyLoadMergeNamespace("billing", dictionary.billing);
|
|
128
|
+
expect("billing" in partial.getAll()).toBe(true);
|
|
166
129
|
});
|
|
167
130
|
|
|
168
|
-
it("
|
|
169
|
-
const
|
|
170
|
-
|
|
131
|
+
it("accumulates a locale via applyLoadMergeNamespace when the namespace already exists", () => {
|
|
132
|
+
const local = new IcuTranslationProviderMulti<TestSchema, TestParams>({
|
|
133
|
+
billing: {
|
|
134
|
+
invoice_summary: {
|
|
135
|
+
en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
171
138
|
});
|
|
139
|
+
const view = () => local.toScope({ namespaces: ["billing"] });
|
|
172
140
|
|
|
173
|
-
expect(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
it("throws when namespace, key, or locale is missing", () => {
|
|
177
|
-
expect(() =>
|
|
178
|
-
provider.get("default", "login_button", "fr" as unknown as LocaleOfMulti<TestSchema>)
|
|
179
|
-
).toThrow(
|
|
180
|
-
'[i18n] Missing key or locale: namespace "default", key "login_button" [fr] (fallback chain: fr)'
|
|
181
|
-
);
|
|
182
|
-
expect(() => provider.get("billing", "login_button" as "empty_label", "en")).toThrow(
|
|
183
|
-
'[i18n] Missing key or locale: namespace "billing", key "login_button" [en] (fallback chain: en)'
|
|
141
|
+
expect("billing" in local.getAll()).toBe(true);
|
|
142
|
+
expect(view().t("billing", "invoice_summary", "en", { count: 1, name: "Ada" })).toBe(
|
|
143
|
+
"You have 1 invoice for Ada"
|
|
184
144
|
);
|
|
185
|
-
});
|
|
186
145
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
146
|
+
local.applyLoadMergeNamespace("billing", {
|
|
147
|
+
invoice_summary: {
|
|
148
|
+
it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
|
|
149
|
+
},
|
|
150
|
+
});
|
|
192
151
|
|
|
193
|
-
|
|
194
|
-
const local = new IcuTranslationProviderMulti<TestSchema, TestParams>(dictionary);
|
|
195
|
-
expect(local.get("billing", "invoice_summary", "en", { count: 2, name: "Bob" })).toBe(
|
|
152
|
+
expect(view().t("billing", "invoice_summary", "en", { count: 2, name: "Bob" })).toBe(
|
|
196
153
|
"You have 2 invoices for Bob"
|
|
197
154
|
);
|
|
155
|
+
expect(view().t("billing", "invoice_summary", "it", { count: 2, name: "Bob" })).toBe(
|
|
156
|
+
"Hai 2 fatture per Bob"
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("merges locales per key with applyLoadMergeNamespace without dropping existing locales", () => {
|
|
161
|
+
const local = new IcuTranslationProviderMulti<TestSchema, TestParams>({
|
|
162
|
+
billing: {
|
|
163
|
+
invoice_summary: {
|
|
164
|
+
en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const view = () => local.toScope({ namespaces: ["billing"] });
|
|
198
169
|
|
|
199
|
-
local.
|
|
200
|
-
...dictionary.billing,
|
|
170
|
+
local.applyLoadMergeNamespace("billing", {
|
|
201
171
|
invoice_summary: {
|
|
202
|
-
|
|
203
|
-
it: dictionary.billing.invoice_summary.it,
|
|
172
|
+
it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
|
|
204
173
|
},
|
|
205
174
|
});
|
|
206
175
|
|
|
207
|
-
expect(
|
|
208
|
-
"2
|
|
176
|
+
expect(view().t("billing", "invoice_summary", "en", { count: 2, name: "Bob" })).toBe(
|
|
177
|
+
"You have 2 invoices for Bob"
|
|
178
|
+
);
|
|
179
|
+
expect(view().t("billing", "invoice_summary", "it", { count: 2, name: "Bob" })).toBe(
|
|
180
|
+
"Hai 2 fatture per Bob"
|
|
209
181
|
);
|
|
210
|
-
expect(local.get("default", "login_button", "en")).toBe("Login");
|
|
211
182
|
});
|
|
212
183
|
|
|
213
|
-
it("
|
|
184
|
+
it("patches a preloaded default namespace key", () => {
|
|
214
185
|
const local = new IcuTranslationProviderMulti<TestSchema, TestParams>(dictionary);
|
|
215
186
|
|
|
216
|
-
local.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
187
|
+
local.patchKeyMulti("default", "login_button", "en", "Sign in");
|
|
188
|
+
|
|
189
|
+
expect(local.toScope({ namespaces: ["default"] }).t("default", "login_button", "en")).toBe(
|
|
190
|
+
"Sign in"
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("merges namespaces with applyLoadMergeAll without dropping existing locales", () => {
|
|
195
|
+
const local = new IcuTranslationProviderMulti<TestSchema, TestParams>({
|
|
196
|
+
billing: {
|
|
197
|
+
invoice_summary: {
|
|
198
|
+
en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const view = () => local.toScope({ namespaces: ["default", "billing"] });
|
|
203
|
+
|
|
204
|
+
local.applyLoadMergeAll({
|
|
205
|
+
billing: {
|
|
206
|
+
invoice_summary: {
|
|
207
|
+
it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
|
|
208
|
+
},
|
|
221
209
|
},
|
|
210
|
+
default: dictionary.default,
|
|
222
211
|
});
|
|
223
212
|
|
|
224
|
-
expect(
|
|
213
|
+
expect(view().t("billing", "invoice_summary", "en", { count: 2, name: "Bob" })).toBe(
|
|
214
|
+
"You have 2 invoices for Bob"
|
|
215
|
+
);
|
|
216
|
+
expect(view().t("billing", "invoice_summary", "it", { count: 2, name: "Bob" })).toBe(
|
|
217
|
+
"Hai 2 fatture per Bob"
|
|
218
|
+
);
|
|
219
|
+
expect(view().t("default", "login_button", "en")).toBe("Login");
|
|
225
220
|
});
|
|
226
221
|
|
|
227
222
|
it("returns a deep-frozen snapshot from getAll", () => {
|
|
228
|
-
expect(
|
|
229
|
-
expect(
|
|
230
|
-
expect(Object.isFrozen(
|
|
223
|
+
expect(engine.getAll()).toEqual(dictionary);
|
|
224
|
+
expect(engine.getAll()).not.toBe(dictionary);
|
|
225
|
+
expect(Object.isFrozen(engine.getAll().default)).toBe(true);
|
|
231
226
|
});
|
|
232
227
|
|
|
233
|
-
it("does not mutate the
|
|
234
|
-
const snapshot =
|
|
228
|
+
it("does not mutate the engine when getAll snapshot is modified", () => {
|
|
229
|
+
const snapshot = engine.getAll();
|
|
235
230
|
expect(() => {
|
|
236
231
|
snapshot.default.login_button.en = "Hacked";
|
|
237
232
|
}).toThrow(TypeError);
|
|
238
|
-
expect(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
describe("forLocale", () => {
|
|
242
|
-
it("binds a locale so get() no longer requires it", () => {
|
|
243
|
-
const it = provider.forLocale("it");
|
|
244
|
-
|
|
245
|
-
expect(it.locale).toBe("it");
|
|
246
|
-
expect(it.get("default", "login_button")).toBe("Accedi");
|
|
247
|
-
expect(it.get("billing", "invoice_summary", { count: 2, name: "Ada" })).toBe(
|
|
248
|
-
"Hai 2 fatture per Ada"
|
|
249
|
-
);
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
describe("locale fallback", () => {
|
|
254
|
-
const fallbackMap = {
|
|
255
|
-
en: null,
|
|
256
|
-
"de-DE": "en",
|
|
257
|
-
"de-CH": "de-DE",
|
|
258
|
-
it: "en",
|
|
259
|
-
} as const;
|
|
260
|
-
|
|
261
|
-
const fallbackProvider = new IcuTranslationProviderMulti<
|
|
262
|
-
TestSchema,
|
|
263
|
-
TestParams,
|
|
264
|
-
keyof typeof fallbackMap | LocaleOfMulti<TestSchema>,
|
|
265
|
-
typeof fallbackMap
|
|
266
|
-
>(dictionary, { localeFallback: fallbackMap });
|
|
267
|
-
|
|
268
|
-
it("resolves a locale through the fallback chain", () => {
|
|
269
|
-
expect(fallbackProvider.get("default", "login_button", "de-CH")).toBe("Login");
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("throws when the fallback chain cannot resolve a template", () => {
|
|
273
|
-
const unresolvedFallback = {
|
|
274
|
-
en: null,
|
|
275
|
-
"de-CH": "fr",
|
|
276
|
-
fr: null,
|
|
277
|
-
} as const;
|
|
278
|
-
const unresolvedProvider = new IcuTranslationProviderMulti<
|
|
279
|
-
TestSchema,
|
|
280
|
-
TestParams,
|
|
281
|
-
keyof typeof unresolvedFallback | LocaleOfMulti<TestSchema>,
|
|
282
|
-
typeof unresolvedFallback
|
|
283
|
-
>(dictionary, { localeFallback: unresolvedFallback });
|
|
284
|
-
|
|
285
|
-
expect(() => unresolvedProvider.get("default", "login_button", "de-CH")).toThrow(
|
|
286
|
-
'[i18n] Missing key or locale: namespace "default", key "login_button" [de-CH] (fallback chain: de-CH → fr)'
|
|
287
|
-
);
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
describe("onMissing", () => {
|
|
292
|
-
it('returns "namespace.key" with onMissing: "key"', () => {
|
|
293
|
-
const local = new IcuTranslationProviderMulti<TestSchema, TestParams>(dictionary, {
|
|
294
|
-
onMissing: "key",
|
|
295
|
-
});
|
|
296
|
-
expect(local.get("default", "missing_key" as "login_button", "en")).toBe(
|
|
297
|
-
"default.missing_key"
|
|
298
|
-
);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it("calls a custom handler with namespace, key, locale, and fallback chain", () => {
|
|
302
|
-
const contexts: unknown[] = [];
|
|
303
|
-
const local = new IcuTranslationProviderMulti<TestSchema, TestParams>(dictionary, {
|
|
304
|
-
onMissing: (context) => {
|
|
305
|
-
contexts.push(context);
|
|
306
|
-
return `<missing:${context.namespace}/${context.key}>`;
|
|
307
|
-
},
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
expect(local.get("default", "missing_key" as "login_button", "en")).toBe(
|
|
311
|
-
"<missing:default/missing_key>"
|
|
312
|
-
);
|
|
313
|
-
expect(contexts).toEqual([
|
|
314
|
-
{ namespace: "default", key: "missing_key", locale: "en", fallbackChain: "en" },
|
|
315
|
-
]);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it("still throws for unloaded namespaces with a lenient onMissing", () => {
|
|
319
|
-
const local = new IcuTranslationProviderMulti<TestSchema, TestParams>(
|
|
320
|
-
{ default: dictionary.default },
|
|
321
|
-
{ onMissing: "key" }
|
|
322
|
-
);
|
|
323
|
-
expect(() => local.get("billing", "account_balance", "en", { amount: 1 })).toThrow(
|
|
324
|
-
'[i18n] Namespace not loaded: "billing"'
|
|
325
|
-
);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it("applies onMissing through forLocale wrappers", () => {
|
|
329
|
-
const local = new IcuTranslationProviderMulti<TestSchema, TestParams>(dictionary, {
|
|
330
|
-
onMissing: "key",
|
|
331
|
-
});
|
|
332
|
-
expect(local.forLocale("en").get("default", "missing_key" as "login_button")).toBe(
|
|
333
|
-
"default.missing_key"
|
|
334
|
-
);
|
|
335
|
-
});
|
|
233
|
+
expect(engine.toScope({ namespaces: ["default"] }).t("default", "login_button", "en")).toBe(
|
|
234
|
+
"Login"
|
|
235
|
+
);
|
|
336
236
|
});
|
|
337
237
|
});
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "./deep-freeze.js";
|
|
2
|
+
import { BuilderLoadRegistry } from "./builder-load-registry.js";
|
|
3
|
+
import type { I18nEngineMulti } from "./engine.js";
|
|
2
4
|
import { resolveAndFormat } from "./format-core.js";
|
|
5
|
+
import {
|
|
6
|
+
assertPatchKeyMulti,
|
|
7
|
+
recordPreloadedKeysMulti,
|
|
8
|
+
seedPreloadedKeysMulti,
|
|
9
|
+
} from "./patch-key.js";
|
|
10
|
+
import { mergeNamespaceLocalesCore } from "./project-locales.js";
|
|
3
11
|
import { validateLocaleFallback } from "./resolve-locale.js";
|
|
4
12
|
import type {
|
|
5
13
|
IcuTranslationProviderOptions,
|
|
@@ -8,83 +16,34 @@ import type {
|
|
|
8
16
|
MultiCompiledCache,
|
|
9
17
|
MultiDictionary,
|
|
10
18
|
OnMissingTranslation,
|
|
19
|
+
PartialKeyDictionary,
|
|
20
|
+
PartialMultiDictionary,
|
|
11
21
|
} from "./types.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Params extends { [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown } },
|
|
16
|
-
Locale extends string,
|
|
17
|
-
> {
|
|
18
|
-
readonly locale: Locale;
|
|
19
|
-
get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(
|
|
20
|
-
namespace: NS,
|
|
21
|
-
key: K,
|
|
22
|
-
...params: Params[NS][K] extends never ? [] : [params: Params[NS][K]]
|
|
23
|
-
): string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface TranslationProviderMulti<
|
|
27
|
-
Schema extends MultiDictionary,
|
|
28
|
-
Params extends { [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown } },
|
|
29
|
-
RequestLocales extends string = LocaleOfMulti<Schema>,
|
|
30
|
-
> {
|
|
31
|
-
get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(
|
|
32
|
-
namespace: NS,
|
|
33
|
-
key: K,
|
|
34
|
-
locale: RequestLocales,
|
|
35
|
-
...params: Params[NS][K] extends never ? [] : [params: Params[NS][K]]
|
|
36
|
-
): string;
|
|
37
|
-
forLocale<Locale extends RequestLocales>(
|
|
38
|
-
locale: Locale
|
|
39
|
-
): TranslationProviderMultiForLocale<Schema, Params, Locale>;
|
|
40
|
-
getAll(): Schema;
|
|
41
|
-
hasNamespace<NS extends keyof Schema & string>(namespace: NS): boolean;
|
|
42
|
-
setAll(values: Schema): void;
|
|
43
|
-
setNamespace<NS extends keyof Schema>(namespace: NS, values: Schema[NS]): void;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export class IcuTranslationProviderMultiForLocale<
|
|
47
|
-
Schema extends MultiDictionary,
|
|
48
|
-
Params extends { [NS in keyof Schema]: { [K in keyof Schema[NS]]: unknown } },
|
|
49
|
-
RequestLocales extends string,
|
|
50
|
-
Locale extends RequestLocales,
|
|
51
|
-
Fallback extends LocaleFallbackMap | undefined,
|
|
52
|
-
> implements TranslationProviderMultiForLocale<Schema, Params, Locale> {
|
|
53
|
-
constructor(
|
|
54
|
-
private readonly provider: IcuTranslationProviderMulti<
|
|
55
|
-
Schema,
|
|
56
|
-
Params,
|
|
57
|
-
RequestLocales,
|
|
58
|
-
Fallback
|
|
59
|
-
>,
|
|
60
|
-
readonly locale: Locale
|
|
61
|
-
) {}
|
|
62
|
-
|
|
63
|
-
get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(
|
|
64
|
-
namespace: NS,
|
|
65
|
-
key: K,
|
|
66
|
-
...args: Params[NS][K] extends never ? [] : [params: Params[NS][K]]
|
|
67
|
-
): string {
|
|
68
|
-
const params = args[0] as Record<string, unknown> | undefined;
|
|
69
|
-
return this.provider.getWithLocale(String(namespace), String(key), this.locale, params);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
22
|
+
import { I18nScopeMultiForLocaleImpl, I18nScopeMultiImpl } from "./scope-multi.js";
|
|
23
|
+
import type { I18nScopeMulti, I18nScopeMultiForLocale } from "./scope-multi.js";
|
|
24
|
+
import type { MultiParams, ParamsForNamespaces, SchemaForNamespaces } from "./scope-types.js";
|
|
72
25
|
|
|
73
26
|
export class IcuTranslationProviderMulti<
|
|
74
27
|
Schema extends MultiDictionary,
|
|
75
|
-
Params extends
|
|
28
|
+
Params extends MultiParams<Schema>,
|
|
76
29
|
RequestLocales extends string = LocaleOfMulti<Schema>,
|
|
77
30
|
Fallback extends LocaleFallbackMap | undefined = undefined,
|
|
78
|
-
> implements
|
|
31
|
+
> implements I18nEngineMulti<Schema, Params, RequestLocales> {
|
|
32
|
+
readonly __i18nEngineMode = "multi" as const;
|
|
33
|
+
|
|
79
34
|
private dictionary: Schema;
|
|
80
35
|
private compiledCache: MultiCompiledCache = {};
|
|
36
|
+
private readonly preloadedKeys = new Set<string>();
|
|
37
|
+
private readonly builderLoadRegistry = new BuilderLoadRegistry();
|
|
81
38
|
private readonly localeFallback?: Fallback;
|
|
82
39
|
private readonly onMissing: OnMissingTranslation;
|
|
83
|
-
private loadedNamespaces: Set<string>;
|
|
84
40
|
|
|
85
|
-
constructor(
|
|
41
|
+
constructor(
|
|
42
|
+
dictionary: PartialMultiDictionary<Schema, RequestLocales>,
|
|
43
|
+
options?: IcuTranslationProviderOptions<Fallback>
|
|
44
|
+
) {
|
|
86
45
|
this.dictionary = structuredClone(dictionary) as Schema;
|
|
87
|
-
this.
|
|
46
|
+
seedPreloadedKeysMulti(this.dictionary, this.preloadedKeys);
|
|
88
47
|
if (options?.localeFallback) {
|
|
89
48
|
validateLocaleFallback(options.localeFallback);
|
|
90
49
|
this.localeFallback = options.localeFallback;
|
|
@@ -92,28 +51,12 @@ export class IcuTranslationProviderMulti<
|
|
|
92
51
|
this.onMissing = options?.onMissing ?? "throw";
|
|
93
52
|
}
|
|
94
53
|
|
|
95
|
-
get<NS extends keyof Schema, K extends keyof Schema[NS] & keyof Params[NS] & string>(
|
|
96
|
-
namespace: NS,
|
|
97
|
-
key: K,
|
|
98
|
-
locale: RequestLocales,
|
|
99
|
-
...args: Params[NS][K] extends never ? [] : [params: Params[NS][K]]
|
|
100
|
-
): string {
|
|
101
|
-
const params = args[0] as Record<string, unknown> | undefined;
|
|
102
|
-
return this.getWithLocale(String(namespace), String(key), locale, params);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
54
|
getWithLocale(
|
|
106
55
|
namespace: string,
|
|
107
56
|
key: string,
|
|
108
57
|
locale: string,
|
|
109
58
|
params?: Record<string, unknown>
|
|
110
59
|
): string {
|
|
111
|
-
if (!this.loadedNamespaces.has(namespace)) {
|
|
112
|
-
throw new Error(
|
|
113
|
-
`[i18n] Namespace not loaded: "${namespace}". Register it with setNamespace() before calling .get().`
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
60
|
const localeByKey = (
|
|
118
61
|
this.dictionary[namespace] as Record<string, Record<string, string>> | undefined
|
|
119
62
|
)?.[key];
|
|
@@ -141,35 +84,103 @@ export class IcuTranslationProviderMulti<
|
|
|
141
84
|
});
|
|
142
85
|
}
|
|
143
86
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
87
|
+
toScope<
|
|
88
|
+
NsList extends readonly (keyof Schema & string)[],
|
|
89
|
+
Locale extends RequestLocales,
|
|
90
|
+
>(options: {
|
|
91
|
+
namespaces: NsList;
|
|
92
|
+
locale: Locale;
|
|
93
|
+
}): I18nScopeMultiForLocale<
|
|
94
|
+
SchemaForNamespaces<Schema, NsList>,
|
|
95
|
+
ParamsForNamespaces<Schema, Params, NsList>,
|
|
96
|
+
RequestLocales,
|
|
97
|
+
Locale
|
|
98
|
+
>;
|
|
99
|
+
toScope<NsList extends readonly (keyof Schema & string)[]>(options: {
|
|
100
|
+
namespaces: NsList;
|
|
101
|
+
}): I18nScopeMulti<
|
|
102
|
+
SchemaForNamespaces<Schema, NsList>,
|
|
103
|
+
ParamsForNamespaces<Schema, Params, NsList>,
|
|
104
|
+
RequestLocales
|
|
105
|
+
>;
|
|
106
|
+
toScope<
|
|
107
|
+
NsList extends readonly (keyof Schema & string)[],
|
|
108
|
+
Locale extends RequestLocales,
|
|
109
|
+
>(options: { namespaces: NsList; locale?: Locale }) {
|
|
110
|
+
if (options.locale !== undefined) {
|
|
111
|
+
return new I18nScopeMultiForLocaleImpl(this, options.locale) as I18nScopeMultiForLocale<
|
|
112
|
+
SchemaForNamespaces<Schema, NsList>,
|
|
113
|
+
ParamsForNamespaces<Schema, Params, NsList>,
|
|
114
|
+
RequestLocales,
|
|
115
|
+
Locale
|
|
116
|
+
>;
|
|
117
|
+
}
|
|
118
|
+
return new I18nScopeMultiImpl(this) as I18nScopeMulti<
|
|
119
|
+
SchemaForNamespaces<Schema, NsList>,
|
|
120
|
+
ParamsForNamespaces<Schema, Params, NsList>,
|
|
121
|
+
RequestLocales
|
|
122
|
+
>;
|
|
148
123
|
}
|
|
149
124
|
|
|
150
125
|
getAll(): Schema {
|
|
151
126
|
return cloneAndFreeze(this.dictionary);
|
|
152
127
|
}
|
|
153
128
|
|
|
154
|
-
|
|
155
|
-
return this.
|
|
129
|
+
hasBuilderResourceLoaded(namespace: string, partition: string | undefined): boolean {
|
|
130
|
+
return this.builderLoadRegistry.has(namespace, partition);
|
|
156
131
|
}
|
|
157
132
|
|
|
158
|
-
|
|
159
|
-
this.
|
|
160
|
-
this.compiledCache = {};
|
|
161
|
-
this.loadedNamespaces = new Set(Object.keys(values));
|
|
133
|
+
markBuilderResourceLoaded(namespace: string, partition: string | undefined): void {
|
|
134
|
+
this.builderLoadRegistry.mark(namespace, partition);
|
|
162
135
|
}
|
|
163
136
|
|
|
164
|
-
|
|
137
|
+
applyLoadMergeNamespace<NS extends keyof Schema>(
|
|
138
|
+
namespace: NS,
|
|
139
|
+
values: PartialKeyDictionary<Schema[NS], RequestLocales>
|
|
140
|
+
): void {
|
|
141
|
+
const existing = this.dictionary[namespace];
|
|
142
|
+
const merged = mergeNamespaceLocalesCore((existing ?? {}) as Schema[NS], values);
|
|
143
|
+
|
|
165
144
|
this.dictionary = {
|
|
166
145
|
...this.dictionary,
|
|
167
|
-
[namespace]:
|
|
146
|
+
[namespace]: merged,
|
|
168
147
|
};
|
|
169
|
-
|
|
148
|
+
recordPreloadedKeysMulti(namespace as string, values, this.preloadedKeys);
|
|
149
|
+
|
|
150
|
+
for (const [key, incomingLocales] of Object.entries(values)) {
|
|
151
|
+
if (incomingLocales === undefined || typeof incomingLocales !== "object") {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const locale of Object.keys(incomingLocales)) {
|
|
156
|
+
delete this.compiledCache[locale]?.[namespace as string]?.[key];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
170
160
|
|
|
171
|
-
|
|
172
|
-
|
|
161
|
+
applyLoadMergeAll(values: PartialMultiDictionary<Schema, RequestLocales>): void {
|
|
162
|
+
for (const namespace of Object.keys(values) as (keyof Schema & string)[]) {
|
|
163
|
+
const incoming = values[namespace];
|
|
164
|
+
if (incoming !== undefined) {
|
|
165
|
+
this.applyLoadMergeNamespace(namespace, incoming);
|
|
166
|
+
}
|
|
173
167
|
}
|
|
174
168
|
}
|
|
169
|
+
|
|
170
|
+
patchKeyMulti(namespace: string, key: string, locale: string, template: string): void {
|
|
171
|
+
const namespaceDictionary = this.dictionary[namespace] as
|
|
172
|
+
| Record<string, Record<string, string>>
|
|
173
|
+
| undefined;
|
|
174
|
+
const localeByKey = namespaceDictionary?.[key];
|
|
175
|
+
|
|
176
|
+
assertPatchKeyMulti(namespace, key, locale, template, this.preloadedKeys, localeByKey);
|
|
177
|
+
|
|
178
|
+
this.applyLoadMergeNamespace(
|
|
179
|
+
namespace as keyof Schema,
|
|
180
|
+
{ [key]: { [locale]: template } } as PartialKeyDictionary<
|
|
181
|
+
Schema[keyof Schema],
|
|
182
|
+
RequestLocales
|
|
183
|
+
>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
175
186
|
}
|