@prosopo/locale 3.6.2 → 3.8.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.
Files changed (106) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +3 -6
  2. package/.turbo/turbo-build$colon$tsc.log +6 -9
  3. package/.turbo/turbo-build.log +5 -11
  4. package/CHANGELOG.md +43 -0
  5. package/LICENSE +201 -0
  6. package/dist/cjs/locales/en/translation.cjs +24 -2
  7. package/dist/locales/ar/translation.json +24 -2
  8. package/dist/locales/az/translation.json +24 -2
  9. package/dist/locales/cs/translation.json +24 -2
  10. package/dist/locales/de/translation.json +24 -2
  11. package/dist/locales/el/translation.json +24 -2
  12. package/dist/locales/en/translation.js +24 -2
  13. package/dist/locales/en/translation.json +24 -2
  14. package/dist/locales/es/translation.json +24 -2
  15. package/dist/locales/fi/translation.json +24 -2
  16. package/dist/locales/fr/translation.json +24 -2
  17. package/dist/locales/hi/translation.json +24 -2
  18. package/dist/locales/hu/translation.json +24 -2
  19. package/dist/locales/id/translation.json +24 -2
  20. package/dist/locales/it/translation.json +24 -2
  21. package/dist/locales/ja/translation.json +24 -2
  22. package/dist/locales/jv/translation.json +24 -2
  23. package/dist/locales/ko/translation.json +24 -2
  24. package/dist/locales/ml/translation.json +24 -2
  25. package/dist/locales/ms/translation.json +24 -2
  26. package/dist/locales/nl/translation.json +24 -2
  27. package/dist/locales/no/translation.json +24 -2
  28. package/dist/locales/pl/translation.json +24 -2
  29. package/dist/locales/pt/translation.json +24 -2
  30. package/dist/locales/pt-BR/translation.json +24 -2
  31. package/dist/locales/ro/translation.json +24 -2
  32. package/dist/locales/ru/translation.json +24 -2
  33. package/dist/locales/sr/translation.json +24 -2
  34. package/dist/locales/sv/translation.json +24 -2
  35. package/dist/locales/th/translation.json +24 -2
  36. package/dist/locales/tr/translation.json +24 -2
  37. package/dist/locales/uk/translation.json +24 -2
  38. package/dist/locales/vi/translation.json +24 -2
  39. package/dist/locales/zh-CN/translation.json +24 -2
  40. package/package.json +65 -66
  41. package/src/direction.ts +0 -36
  42. package/src/errorMessage.ts +0 -37
  43. package/src/i18SharedOptions.ts +0 -29
  44. package/src/i18nBackend.ts +0 -69
  45. package/src/i18nFrontend.ts +0 -267
  46. package/src/i18nMiddleware.ts +0 -35
  47. package/src/index.ts +0 -33
  48. package/src/loadI18next.ts +0 -136
  49. package/src/locales/ar/translation.json +0 -493
  50. package/src/locales/az/translation.json +0 -493
  51. package/src/locales/cs/translation.json +0 -493
  52. package/src/locales/de/translation.json +0 -493
  53. package/src/locales/el/translation.json +0 -493
  54. package/src/locales/en/translation.json +0 -493
  55. package/src/locales/es/translation.json +0 -493
  56. package/src/locales/fi/translation.json +0 -493
  57. package/src/locales/fr/translation.json +0 -493
  58. package/src/locales/hi/translation.json +0 -493
  59. package/src/locales/hu/translation.json +0 -493
  60. package/src/locales/id/translation.json +0 -493
  61. package/src/locales/it/translation.json +0 -493
  62. package/src/locales/ja/translation.json +0 -493
  63. package/src/locales/jv/translation.json +0 -493
  64. package/src/locales/ko/translation.json +0 -493
  65. package/src/locales/ml/translation.json +0 -493
  66. package/src/locales/ms/translation.json +0 -493
  67. package/src/locales/nl/translation.json +0 -493
  68. package/src/locales/no/translation.json +0 -493
  69. package/src/locales/pl/translation.json +0 -493
  70. package/src/locales/pt/translation.json +0 -493
  71. package/src/locales/pt-BR/translation.json +0 -493
  72. package/src/locales/ro/translation.json +0 -493
  73. package/src/locales/ru/translation.json +0 -493
  74. package/src/locales/sr/translation.json +0 -493
  75. package/src/locales/sv/translation.json +0 -493
  76. package/src/locales/th/translation.json +0 -493
  77. package/src/locales/tr/translation.json +0 -493
  78. package/src/locales/uk/translation.json +0 -493
  79. package/src/locales/vi/translation.json +0 -493
  80. package/src/locales/zh-CN/translation.json +0 -493
  81. package/src/process.ts +0 -29
  82. package/src/tests/direction.unit.test.ts +0 -52
  83. package/src/tests/errorMessage.unit.test.ts +0 -84
  84. package/src/tests/i18SharedOptions.unit.test.ts +0 -139
  85. package/src/tests/i18nBackend.unit.test.ts +0 -241
  86. package/src/tests/i18nFrontend.unit.test.ts +0 -404
  87. package/src/tests/i18nMiddleware.unit.test.ts +0 -159
  88. package/src/tests/loadI18next.unit.test.ts +0 -443
  89. package/src/tests/locale.test-d.ts +0 -184
  90. package/src/tests/localePlaceholders.unit.test.ts +0 -81
  91. package/src/tests/locales.unit.test.ts +0 -79
  92. package/src/tests/process.unit.test.ts +0 -33
  93. package/src/tests/thrownErrorKeys.unit.test.ts +0 -91
  94. package/src/tests/translationKey.unit.test.ts +0 -155
  95. package/src/tests/translations.unit.test.ts +0 -97
  96. package/src/tests/translator.unit.test.ts +0 -213
  97. package/src/tests/util.unit.test.ts +0 -102
  98. package/src/translationKey.ts +0 -73
  99. package/src/translations.ts +0 -55
  100. package/src/translator.ts +0 -69
  101. package/src/types.ts +0 -47
  102. package/src/util.ts +0 -25
  103. package/tsconfig.cjs.json +0 -21
  104. package/tsconfig.json +0 -22
  105. package/tsconfig.tsbuildinfo +0 -1
  106. package/tsconfig.types.json +0 -9
@@ -1,404 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
16
- import type { Ti18n } from "../types.js";
17
-
18
- // The module keeps a singleton, so every test re-imports it under a fresh
19
- // registry. `fetch`, `localStorage`, `navigator.languages` and `document.cookie`
20
- // are the module's four inputs and are all stubbed per test.
21
-
22
- type Catalogues = Record<string, Record<string, unknown>>;
23
-
24
- interface Harness {
25
- initializeI18n: (callback?: (value: Ti18n) => void, lng?: string) => Ti18n;
26
- /** Every URL passed to fetch, in call order. */
27
- requested: string[];
28
- cookie: () => string;
29
- stored: () => Record<string, string>;
30
- }
31
-
32
- const load = async (options: {
33
- catalogues?: Catalogues;
34
- languages?: string[];
35
- cookie?: string;
36
- stored?: string;
37
- fetchFails?: boolean;
38
- never?: boolean;
39
- }): Promise<Harness> => {
40
- vi.resetModules();
41
-
42
- const requested: string[] = [];
43
- const store: Record<string, string> = options.stored
44
- ? { i18nextLng: options.stored }
45
- : {};
46
- let cookie = options.cookie ? `i18next=${options.cookie}` : "";
47
-
48
- vi.stubGlobal(
49
- "fetch",
50
- vi.fn(async (url: string): Promise<Response> => {
51
- requested.push(url);
52
- if (options.never) {
53
- return new Promise<Response>(() => undefined);
54
- }
55
- if (options.fetchFails) {
56
- throw new Error("network down");
57
- }
58
- const language = url.split("/").at(-2) ?? "";
59
- const body = options.catalogues?.[language];
60
- return {
61
- ok: body !== undefined,
62
- json: async (): Promise<unknown> => body,
63
- } as Response;
64
- }),
65
- );
66
- vi.stubGlobal("localStorage", {
67
- getItem: (key: string): string | null => store[key] ?? null,
68
- setItem: (key: string, value: string): void => {
69
- store[key] = value;
70
- },
71
- });
72
- vi.stubGlobal("navigator", { languages: options.languages ?? ["en"] });
73
- vi.stubGlobal("document", {
74
- get cookie(): string {
75
- return cookie;
76
- },
77
- set cookie(value: string) {
78
- cookie = value;
79
- },
80
- });
81
-
82
- const imported = await import("../i18nFrontend.js");
83
- return {
84
- initializeI18n: imported.default,
85
- requested,
86
- cookie: (): string => cookie,
87
- stored: (): Record<string, string> => store,
88
- };
89
- };
90
-
91
- /** Resolves with the instance the init callback hands back. */
92
- const initialised = async (harness: Harness, lng?: string): Promise<Ti18n> =>
93
- new Promise<Ti18n>((resolve) => {
94
- harness.initializeI18n(resolve, lng);
95
- });
96
-
97
- afterEach(() => {
98
- vi.unstubAllGlobals();
99
- vi.resetModules();
100
- });
101
-
102
- describe("choosing a language", () => {
103
- test("uses the language the caller asked for", async () => {
104
- // The site owner's `data-language` is the single source of truth: reading
105
- // the browser first would render a flash of the wrong language.
106
- const harness = await load({
107
- catalogues: { fr: {}, en: {} },
108
- languages: ["de"],
109
- });
110
-
111
- const i18n = await initialised(harness, "fr");
112
-
113
- expect(i18n.language).toBe("fr");
114
- });
115
-
116
- test("prefers a cookie over the browser", async () => {
117
- const harness = await load({
118
- catalogues: { es: {}, en: {} },
119
- cookie: "es",
120
- stored: "de",
121
- languages: ["it"],
122
- });
123
-
124
- expect((await initialised(harness)).language).toBe("es");
125
- });
126
-
127
- test("prefers stored over the browser", async () => {
128
- const harness = await load({
129
- catalogues: { de: {}, en: {} },
130
- stored: "de",
131
- languages: ["it"],
132
- });
133
-
134
- expect((await initialised(harness)).language).toBe("de");
135
- });
136
-
137
- test("falls back to the browser when nothing is remembered", async () => {
138
- const harness = await load({
139
- catalogues: { it: {}, en: {} },
140
- languages: ["it"],
141
- });
142
-
143
- expect((await initialised(harness)).language).toBe("it");
144
- });
145
-
146
- test("resolves a regional tag to its base language", async () => {
147
- // Browsers report en-GB, pt-PT and so on; only the base languages (plus
148
- // the handful of regional catalogues) are shipped.
149
- const harness = await load({
150
- catalogues: { en: {} },
151
- languages: ["de-AT"],
152
- });
153
-
154
- expect((await initialised(harness)).language).toBe("de");
155
- });
156
-
157
- test("ignores a language with no catalogue", async () => {
158
- const harness = await load({ catalogues: { en: {} }, languages: ["xx"] });
159
-
160
- expect((await initialised(harness)).language).toBe("en");
161
- });
162
-
163
- test("remembers the language in both cookie and storage", async () => {
164
- // Kept from the i18next language detector: a visitor who has picked a
165
- // language keeps it across pages that embed the widget.
166
- const harness = await load({ catalogues: { fr: {}, en: {} } });
167
-
168
- await initialised(harness, "fr");
169
-
170
- expect(harness.stored().i18nextLng).toBe("fr");
171
- expect(harness.cookie()).toContain("i18next=fr");
172
- });
173
- });
174
-
175
- describe("fetching catalogues", () => {
176
- test("loads the active language from the directory the module came from", async () => {
177
- const harness = await load({ catalogues: { fr: {}, en: {} } });
178
-
179
- await initialised(harness, "fr");
180
-
181
- expect(
182
- harness.requested.some((url) =>
183
- url.endsWith("/locales/fr/translation.json"),
184
- ),
185
- ).toBe(true);
186
- });
187
-
188
- test("loads English alongside it, for keys a translation is missing", async () => {
189
- const harness = await load({ catalogues: { fr: {}, en: {} } });
190
-
191
- await initialised(harness, "fr");
192
-
193
- expect(harness.requested).toHaveLength(2);
194
- expect(
195
- harness.requested.some((url) =>
196
- url.endsWith("/locales/en/translation.json"),
197
- ),
198
- ).toBe(true);
199
- });
200
-
201
- test("fetches once when the active language is already English", async () => {
202
- const harness = await load({ catalogues: { en: {} } });
203
-
204
- await initialised(harness, "en");
205
-
206
- expect(harness.requested).toHaveLength(1);
207
- });
208
- });
209
-
210
- describe("translating", () => {
211
- test("reads a nested key by its dotted path", async () => {
212
- const harness = await load({
213
- catalogues: { en: { WIDGET: { I_AM_HUMAN: "I am human" } } },
214
- });
215
-
216
- expect((await initialised(harness)).t("WIDGET.I_AM_HUMAN")).toBe(
217
- "I am human",
218
- );
219
- });
220
-
221
- test("substitutes interpolation values", async () => {
222
- const harness = await load({
223
- catalogues: {
224
- en: { WIDGET: { PUZZLE: { POSITION: "{{x}} across, {{y}} down" } } },
225
- },
226
- });
227
-
228
- expect(
229
- (await initialised(harness)).t("WIDGET.PUZZLE.POSITION", {
230
- x: 10,
231
- y: 20,
232
- }),
233
- ).toBe("10 across, 20 down");
234
- });
235
-
236
- test("falls back to English when the translation lacks the key", async () => {
237
- const harness = await load({
238
- catalogues: {
239
- fr: { WIDGET: { SUBMIT: "gardé" } },
240
- en: { WIDGET: { SUBMIT: "kept", CANCEL: "only" } },
241
- },
242
- });
243
- const i18n = await initialised(harness, "fr");
244
-
245
- expect(i18n.t("WIDGET.SUBMIT")).toBe("gardé");
246
- expect(i18n.t("WIDGET.CANCEL")).toBe("only");
247
- });
248
-
249
- test("falls back to the caller's default when no catalogue has the key", async () => {
250
- const harness = await load({ catalogues: { en: {} } });
251
-
252
- expect(
253
- (await initialised(harness)).t("WIDGET.CHECKING", {
254
- defaultValue: "Checking that you are human",
255
- }),
256
- ).toBe("Checking that you are human");
257
- });
258
-
259
- test("interpolates the default too", async () => {
260
- const harness = await load({ catalogues: { en: {} } });
261
-
262
- expect(
263
- (await initialised(harness)).t("WIDGET.PUZZLE.POSITION", {
264
- defaultValue: "{{x}} across",
265
- x: 7,
266
- }),
267
- ).toBe("7 across");
268
- });
269
-
270
- test("falls back to the key when there is no default either", async () => {
271
- const harness = await load({ catalogues: { en: {} } });
272
-
273
- expect((await initialised(harness)).t("WIDGET.SUBMIT")).toBe(
274
- "WIDGET.SUBMIT",
275
- );
276
- });
277
-
278
- test("leaves a placeholder alone when no value was supplied for it", async () => {
279
- const harness = await load({
280
- catalogues: { en: { WIDGET: { SUBMIT: "hello {{name}}" } } },
281
- });
282
-
283
- expect((await initialised(harness)).t("WIDGET.SUBMIT", { other: 1 })).toBe(
284
- "hello {{name}}",
285
- );
286
- });
287
- });
288
-
289
- describe("when the catalogue cannot be fetched", () => {
290
- test("still initialises, so the widget renders untranslated", async () => {
291
- // Untranslated beats absent: a widget whose label never arrives is a
292
- // widget the visitor cannot use.
293
- const harness = await load({ catalogues: {}, fetchFails: true });
294
-
295
- const i18n = await initialised(harness);
296
-
297
- expect(i18n.isInitialized).toBe(true);
298
- expect(i18n.t("WIDGET.CHECKING", { defaultValue: "Checking" })).toBe(
299
- "Checking",
300
- );
301
- });
302
-
303
- test("reports the namespace as loaded so callers stop waiting", async () => {
304
- const harness = await load({ catalogues: {}, fetchFails: true });
305
-
306
- expect((await initialised(harness)).hasLoadedNamespace("translation")).toBe(
307
- true,
308
- );
309
- });
310
- });
311
-
312
- describe("switching language", () => {
313
- test("loads the new catalogue and translates against it", async () => {
314
- const harness = await load({
315
- catalogues: {
316
- en: { WIDGET: { SUBMIT: "yes" } },
317
- de: { WIDGET: { SUBMIT: "ja" } },
318
- },
319
- });
320
- const i18n = await initialised(harness, "en");
321
-
322
- await i18n.changeLanguage("de");
323
-
324
- expect(i18n.language).toBe("de");
325
- expect(i18n.t("WIDGET.SUBMIT")).toBe("ja");
326
- });
327
-
328
- test("notifies subscribers", async () => {
329
- const harness = await load({ catalogues: { en: {}, de: {} } });
330
- const i18n = await initialised(harness, "en");
331
- const listener = vi.fn();
332
- i18n.on("languageChanged", listener);
333
-
334
- await i18n.changeLanguage("de");
335
-
336
- expect(listener).toHaveBeenCalled();
337
- });
338
-
339
- test("does nothing when the language is already active", async () => {
340
- const harness = await load({ catalogues: { en: {} } });
341
- const i18n = await initialised(harness, "en");
342
- const before = harness.requested.length;
343
-
344
- await i18n.changeLanguage("en");
345
-
346
- expect(harness.requested).toHaveLength(before);
347
- });
348
-
349
- test("unsubscribed listeners stop being called", async () => {
350
- const harness = await load({ catalogues: { en: {}, de: {} } });
351
- const i18n = await initialised(harness, "en");
352
- const listener = vi.fn();
353
- i18n.on("languageChanged", listener);
354
- i18n.off("languageChanged", listener);
355
-
356
- await i18n.changeLanguage("de");
357
-
358
- expect(listener).not.toHaveBeenCalled();
359
- });
360
- });
361
-
362
- describe("the singleton", () => {
363
- test("a second call reuses the instance rather than refetching", async () => {
364
- // Every widget on the page shares one catalogue fetch.
365
- const harness = await load({ catalogues: { en: {} } });
366
- const first = await initialised(harness);
367
- const before = harness.requested.length;
368
-
369
- const second = harness.initializeI18n();
370
-
371
- expect(second).toBe(first);
372
- expect(harness.requested).toHaveLength(before);
373
- });
374
-
375
- test("a second call still fires its callback", async () => {
376
- // The one-shot callback is only armed on first init, so without this
377
- // loadI18next() would never settle for the second widget.
378
- const harness = await load({ catalogues: { en: {} } });
379
- await initialised(harness);
380
-
381
- const callback = vi.fn();
382
- harness.initializeI18n(callback);
383
-
384
- expect(callback).toHaveBeenCalledTimes(1);
385
- });
386
- });
387
-
388
- describe("before the catalogue arrives", () => {
389
- let harness: Harness;
390
-
391
- beforeEach(async () => {
392
- harness = await load({ never: true });
393
- });
394
-
395
- test("reports itself uninitialised", () => {
396
- expect(harness.initializeI18n().isInitialized).toBe(false);
397
- });
398
-
399
- test("reports the namespace as not loaded", () => {
400
- expect(harness.initializeI18n().hasLoadedNamespace("translation")).toBe(
401
- false,
402
- );
403
- });
404
- });
@@ -1,159 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { i18n } from "i18next";
16
- import type { handle } from "i18next-http-middleware";
17
- import { afterEach, describe, expect, test, vi } from "vitest";
18
-
19
- type HandleOptions = NonNullable<Parameters<typeof handle>[1]>;
20
- type Handler = ReturnType<typeof handle>;
21
-
22
- const asI18n = (value: object): i18n => value as unknown as i18n;
23
-
24
- interface Harness {
25
- instance: i18n;
26
- handleCalls: [i18n, HandleOptions | undefined][];
27
- handler: Handler;
28
- /** True once loadI18next has resolved — handle() must not run before then. */
29
- loaded: () => boolean;
30
- }
31
-
32
- const load = async (options: {
33
- loadRejects?: boolean;
34
- }): Promise<{
35
- i18nMiddleware: (opts: HandleOptions) => Promise<Handler>;
36
- harness: Harness;
37
- }> => {
38
- vi.resetModules();
39
-
40
- const instance = asI18n({ name: "the singleton" });
41
- const handleCalls: [i18n, HandleOptions | undefined][] = [];
42
- const handler = ((): void => {}) as unknown as Handler;
43
- let loaded = false;
44
-
45
- // The middleware takes the i18next singleton directly: handle() needs the
46
- // real instance, not the narrow Ti18n loadI18next hands back.
47
- vi.doMock("i18next", () => ({ default: instance }));
48
-
49
- vi.doMock("i18next-http-middleware", () => ({
50
- handle: (i18next: i18n, opts?: HandleOptions): Handler => {
51
- handleCalls.push([i18next, opts]);
52
- return handler;
53
- },
54
- }));
55
- vi.doMock("../loadI18next.js", () => ({
56
- default: async (): Promise<i18n> => {
57
- if (options.loadRejects) {
58
- throw new Error("i18n failed to load");
59
- }
60
- loaded = true;
61
- return instance;
62
- },
63
- }));
64
-
65
- const imported = await import("../i18nMiddleware.js");
66
- return {
67
- i18nMiddleware: imported.default,
68
- harness: { instance, handleCalls, handler, loaded: (): boolean => loaded },
69
- };
70
- };
71
-
72
- afterEach(() => {
73
- vi.resetModules();
74
- vi.doUnmock("i18next-http-middleware");
75
- vi.doUnmock("../loadI18next.js");
76
- vi.doUnmock("i18next");
77
- });
78
-
79
- describe("i18nMiddleware", () => {
80
- test("builds the handler from the i18next singleton", async () => {
81
- const { i18nMiddleware, harness } = await load({});
82
-
83
- const handler = await i18nMiddleware({});
84
-
85
- expect(harness.handleCalls).toHaveLength(1);
86
- expect(harness.handleCalls[0]?.[0]).toBe(harness.instance);
87
- expect(handler).toBe(harness.handler);
88
- });
89
-
90
- test("waits for the resources before handing the instance over", async () => {
91
- // handle() reads the instance's language list when it is built, so an
92
- // uninitialised singleton would produce a middleware that detects nothing.
93
- const { i18nMiddleware, harness } = await load({});
94
-
95
- await i18nMiddleware({});
96
-
97
- expect(harness.loaded()).toBe(true);
98
- });
99
-
100
- test("forwards the caller's options", async () => {
101
- const { i18nMiddleware, harness } = await load({});
102
- const options: HandleOptions = { ignoreRoutes: ["/healthz"] };
103
-
104
- await i18nMiddleware(options);
105
-
106
- expect(harness.handleCalls[0]?.[1]).toEqual({ ignoreRoutes: ["/healthz"] });
107
- });
108
-
109
- test("copies the options rather than passing the caller's object through", async () => {
110
- // handle() keeps the object for the lifetime of the process, so sharing
111
- // it would let a later mutation by the caller silently change routing.
112
- const { i18nMiddleware, harness } = await load({});
113
- const options: HandleOptions = { ignoreRoutes: ["/healthz"] };
114
-
115
- await i18nMiddleware(options);
116
-
117
- expect(harness.handleCalls[0]?.[1]).not.toBe(options);
118
- });
119
-
120
- test("accepts an empty options object", async () => {
121
- const { i18nMiddleware, harness } = await load({});
122
- await i18nMiddleware({});
123
- expect(harness.handleCalls[0]?.[1]).toEqual({});
124
- });
125
-
126
- test("always requests the backend instance", async () => {
127
- // Middleware only ever runs on the server; asking for the frontend
128
- // instance would pull in the browser detector and http backend.
129
- const loadSpy = vi.fn(async (): Promise<i18n> => asI18n({}));
130
- vi.resetModules();
131
- vi.doMock("i18next-http-middleware", () => ({
132
- handle: (): Handler => ((): void => {}) as unknown as Handler,
133
- }));
134
- vi.doMock("../loadI18next.js", () => ({ default: loadSpy }));
135
-
136
- const imported = await import("../i18nMiddleware.js");
137
- await imported.default({});
138
-
139
- expect(loadSpy).toHaveBeenCalledWith(true);
140
- });
141
-
142
- test("propagates a failure to load i18next instead of returning a broken handler", async () => {
143
- // The caller mounts whatever this resolves to. Swallowing the error
144
- // would mount a handler over an uninitialised i18next and fail on the
145
- // first request instead of at boot.
146
- const { i18nMiddleware } = await load({ loadRejects: true });
147
-
148
- await expect(i18nMiddleware({})).rejects.toThrow("i18n failed to load");
149
- });
150
-
151
- test("returns a fresh handler per call", async () => {
152
- const { i18nMiddleware, harness } = await load({});
153
-
154
- await i18nMiddleware({});
155
- await i18nMiddleware({ ignoreRoutes: ["/x"] });
156
-
157
- expect(harness.handleCalls).toHaveLength(2);
158
- });
159
- });