@prosopo/locale 3.5.0 → 3.6.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/.turbo/turbo-build$colon$cjs.log +15 -13
- package/.turbo/turbo-build$colon$tsc.log +8 -5
- package/.turbo/turbo-build.log +16 -14
- package/CHANGELOG.md +47 -0
- package/dist/cjs/i18SharedOptions.cjs +2 -2
- package/dist/cjs/i18nBackend.cjs +1 -1
- package/dist/cjs/i18nFrontend.cjs +169 -51
- package/dist/cjs/i18nMiddleware.cjs +5 -2
- package/dist/cjs/index.cjs +3 -2
- package/dist/cjs/translationKey.cjs +2 -3
- package/dist/cjs/translations.cjs +4 -5
- package/dist/cjs/translator.cjs +2 -3
- package/dist/i18SharedOptions.d.ts +2 -2
- package/dist/i18SharedOptions.js +3 -3
- package/dist/i18SharedOptions.js.map +1 -1
- package/dist/i18nBackend.js +1 -1
- package/dist/i18nFrontend.d.ts +2 -2
- package/dist/i18nFrontend.d.ts.map +1 -1
- package/dist/i18nFrontend.js +169 -45
- package/dist/i18nFrontend.js.map +1 -1
- package/dist/i18nMiddleware.d.ts.map +1 -1
- package/dist/i18nMiddleware.js +3 -1
- package/dist/i18nMiddleware.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/loadI18next.d.ts +2 -3
- package/dist/loadI18next.d.ts.map +1 -1
- package/dist/loadI18next.js.map +1 -1
- package/dist/tests/i18nFrontend.unit.test.js +218 -170
- package/dist/tests/i18nFrontend.unit.test.js.map +1 -1
- package/dist/tests/i18nMiddleware.unit.test.js +11 -2
- package/dist/tests/i18nMiddleware.unit.test.js.map +1 -1
- package/dist/tests/loadI18next.unit.test.js.map +1 -1
- package/dist/tests/locale.test-d.js +15 -11
- package/dist/tests/locale.test-d.js.map +1 -1
- package/dist/tests/translationKey.unit.test.js +11 -33
- package/dist/tests/translationKey.unit.test.js.map +1 -1
- package/dist/tests/translations.unit.test.js +12 -22
- package/dist/tests/translations.unit.test.js.map +1 -1
- package/dist/translationKey.d.ts +2 -3
- package/dist/translationKey.d.ts.map +1 -1
- package/dist/translationKey.js +2 -3
- package/dist/translationKey.js.map +1 -1
- package/dist/translations.d.ts +3 -2
- package/dist/translations.d.ts.map +1 -1
- package/dist/translations.js +3 -3
- package/dist/translations.js.map +1 -1
- package/dist/translator.d.ts +2 -3
- package/dist/translator.d.ts.map +1 -1
- package/dist/translator.js +2 -3
- package/dist/translator.js.map +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +3 -7
- package/src/i18SharedOptions.ts +3 -3
- package/src/i18nFrontend.ts +240 -73
- package/src/i18nMiddleware.ts +6 -1
- package/src/index.ts +9 -3
- package/src/loadI18next.ts +7 -8
- package/src/tests/i18nFrontend.unit.test.ts +310 -216
- package/src/tests/i18nMiddleware.unit.test.ts +21 -2
- package/src/tests/loadI18next.unit.test.ts +11 -11
- package/src/tests/locale.test-d.ts +24 -23
- package/src/tests/translationKey.unit.test.ts +12 -38
- package/src/tests/translations.unit.test.ts +18 -31
- package/src/translationKey.ts +10 -9
- package/src/translations.ts +6 -5
- package/src/translator.ts +6 -7
- package/src/types.ts +42 -0
- package/tsconfig.cjs.json +3 -0
- package/tsconfig.json +3 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -12,288 +12,382 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
16
|
+
import type { Ti18n } from "../types.js";
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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.
|
|
20
21
|
|
|
21
|
-
type
|
|
22
|
-
type ResourceLoader = (language: string, namespace: string) => Promise<unknown>;
|
|
23
|
-
|
|
24
|
-
interface FakeI18n {
|
|
25
|
-
isInitialized: boolean;
|
|
26
|
-
use: (plugin: unknown) => FakeI18n;
|
|
27
|
-
init: (options: InitOptions) => FakeI18n;
|
|
28
|
-
on: (event: string, listener: Listener) => void;
|
|
29
|
-
}
|
|
22
|
+
type Catalogues = Record<string, Record<string, unknown>>;
|
|
30
23
|
|
|
31
24
|
interface Harness {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
detectorOptions: unknown[];
|
|
38
|
-
emit: (event: string) => void;
|
|
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>;
|
|
39
30
|
}
|
|
40
31
|
|
|
41
|
-
const load = async (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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> => {
|
|
47
40
|
vi.resetModules();
|
|
48
41
|
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
init: (options: InitOptions): FakeI18n => {
|
|
62
|
-
initOptions.push(options);
|
|
63
|
-
return fake;
|
|
64
|
-
},
|
|
65
|
-
on: (event: string, listener: Listener): void => {
|
|
66
|
-
listeners.set(event, [...(listeners.get(event) ?? []), listener]);
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const harness: Harness = {
|
|
71
|
-
fake,
|
|
72
|
-
plugins,
|
|
73
|
-
initOptions,
|
|
74
|
-
resourceLoaders,
|
|
75
|
-
detectorOptions,
|
|
76
|
-
emit: (event: string): void => {
|
|
77
|
-
for (const listener of listeners.get(event) ?? []) {
|
|
78
|
-
listener();
|
|
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);
|
|
79
54
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
vi.doMock("i18next", () => ({ default: fake }));
|
|
84
|
-
vi.doMock("i18next-chained-backend", () => ({ default: "ChainedBackend" }));
|
|
85
|
-
vi.doMock("i18next-http-backend", () => ({ default: "HttpBackend" }));
|
|
86
|
-
vi.doMock("i18next-browser-languagedetector", () => ({
|
|
87
|
-
default: class {
|
|
88
|
-
constructor(_services: unknown, opts: unknown) {
|
|
89
|
-
detectorOptions.push(opts);
|
|
55
|
+
if (options.fetchFails) {
|
|
56
|
+
throw new Error("network down");
|
|
90
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;
|
|
91
76
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
default: (loader: ResourceLoader): string => {
|
|
95
|
-
resourceLoaders.push(loader);
|
|
96
|
-
return "InMemoryBackend";
|
|
77
|
+
set cookie(value: string) {
|
|
78
|
+
cookie = value;
|
|
97
79
|
},
|
|
98
|
-
})
|
|
80
|
+
});
|
|
99
81
|
|
|
100
82
|
const imported = await import("../i18nFrontend.js");
|
|
101
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
initializeI18n: imported.default,
|
|
85
|
+
requested,
|
|
86
|
+
cookie: (): string => cookie,
|
|
87
|
+
stored: (): Record<string, string> => store,
|
|
88
|
+
};
|
|
102
89
|
};
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
backendOptions?: { loadPath?: string }[];
|
|
110
|
-
};
|
|
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
|
+
});
|
|
111
96
|
|
|
112
97
|
afterEach(() => {
|
|
98
|
+
vi.unstubAllGlobals();
|
|
113
99
|
vi.resetModules();
|
|
114
|
-
for (const module of [
|
|
115
|
-
"i18next",
|
|
116
|
-
"i18next-chained-backend",
|
|
117
|
-
"i18next-http-backend",
|
|
118
|
-
"i18next-browser-languagedetector",
|
|
119
|
-
"i18next-resources-to-backend",
|
|
120
|
-
]) {
|
|
121
|
-
vi.doUnmock(module);
|
|
122
|
-
}
|
|
123
100
|
});
|
|
124
101
|
|
|
125
|
-
describe("
|
|
126
|
-
test("
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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");
|
|
130
114
|
});
|
|
131
115
|
|
|
132
|
-
test("
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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");
|
|
137
125
|
});
|
|
138
126
|
|
|
139
|
-
test("
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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");
|
|
146
135
|
});
|
|
147
136
|
|
|
148
|
-
test("
|
|
149
|
-
const
|
|
150
|
-
|
|
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");
|
|
151
172
|
});
|
|
152
173
|
});
|
|
153
174
|
|
|
154
|
-
describe("
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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);
|
|
162
186
|
});
|
|
163
187
|
|
|
164
|
-
test("
|
|
165
|
-
const {
|
|
166
|
-
|
|
167
|
-
|
|
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);
|
|
168
199
|
});
|
|
169
200
|
|
|
170
|
-
test("
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
expect(harness.
|
|
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);
|
|
176
207
|
});
|
|
208
|
+
});
|
|
177
209
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
order: ["cookie", "localStorage", "navigator"],
|
|
183
|
-
lookupQuerystring: "lng",
|
|
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" } } },
|
|
184
214
|
});
|
|
215
|
+
|
|
216
|
+
expect((await initialised(harness)).t("WIDGET.I_AM_HUMAN")).toBe(
|
|
217
|
+
"I am human",
|
|
218
|
+
);
|
|
185
219
|
});
|
|
186
220
|
|
|
187
|
-
test("
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
expect(harness.detectorOptions[0]).toMatchObject({
|
|
191
|
-
caches: ["localStorage", "cookie"],
|
|
221
|
+
test("substitutes interpolation values", async () => {
|
|
222
|
+
const harness = await load({
|
|
223
|
+
catalogues: { en: { POSITION: "{{x}} across, {{y}} down" } },
|
|
192
224
|
});
|
|
225
|
+
|
|
226
|
+
expect((await initialised(harness)).t("POSITION", { x: 10, y: 20 })).toBe(
|
|
227
|
+
"10 across, 20 down",
|
|
228
|
+
);
|
|
193
229
|
});
|
|
194
|
-
});
|
|
195
230
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
expect(
|
|
231
|
+
test("falls back to English when the translation lacks the key", async () => {
|
|
232
|
+
const harness = await load({
|
|
233
|
+
catalogues: { fr: { KEPT: "gardé" }, en: { KEPT: "kept", ONLY: "only" } },
|
|
234
|
+
});
|
|
235
|
+
const i18n = await initialised(harness, "fr");
|
|
236
|
+
|
|
237
|
+
expect(i18n.t("KEPT")).toBe("gardé");
|
|
238
|
+
expect(i18n.t("ONLY")).toBe("only");
|
|
204
239
|
});
|
|
205
240
|
|
|
206
|
-
test("
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
241
|
+
test("falls back to the caller's default when no catalogue has the key", async () => {
|
|
242
|
+
const harness = await load({ catalogues: { en: {} } });
|
|
243
|
+
|
|
244
|
+
expect(
|
|
245
|
+
(await initialised(harness)).t("WIDGET.CHECKING", {
|
|
246
|
+
defaultValue: "Checking that you are human",
|
|
247
|
+
}),
|
|
248
|
+
).toBe("Checking that you are human");
|
|
212
249
|
});
|
|
213
250
|
|
|
214
|
-
test("
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
expect(loadPath).toMatch(/^[a-z]+:\/\//);
|
|
224
|
-
// The module's own filename must have been stripped before the locales
|
|
225
|
-
// segment is appended, or the path would nest under the script itself.
|
|
226
|
-
expect(loadPath).not.toContain("i18nFrontend");
|
|
251
|
+
test("interpolates the default too", async () => {
|
|
252
|
+
const harness = await load({ catalogues: { en: {} } });
|
|
253
|
+
|
|
254
|
+
expect(
|
|
255
|
+
(await initialised(harness)).t("POSITION", {
|
|
256
|
+
defaultValue: "{{x}} across",
|
|
257
|
+
x: 7,
|
|
258
|
+
}),
|
|
259
|
+
).toBe("7 across");
|
|
227
260
|
});
|
|
228
261
|
|
|
229
|
-
test("
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
262
|
+
test("falls back to the key when there is no default either", async () => {
|
|
263
|
+
const harness = await load({ catalogues: { en: {} } });
|
|
264
|
+
|
|
265
|
+
expect((await initialised(harness)).t("WIDGET.MISSING")).toBe(
|
|
266
|
+
"WIDGET.MISSING",
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("leaves a placeholder alone when no value was supplied for it", async () => {
|
|
271
|
+
const harness = await load({
|
|
272
|
+
catalogues: { en: { GREET: "hello {{name}}" } },
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect((await initialised(harness)).t("GREET", { other: 1 })).toBe(
|
|
276
|
+
"hello {{name}}",
|
|
277
|
+
);
|
|
239
278
|
});
|
|
240
279
|
});
|
|
241
280
|
|
|
242
|
-
describe("
|
|
243
|
-
test("
|
|
244
|
-
|
|
245
|
-
|
|
281
|
+
describe("when the catalogue cannot be fetched", () => {
|
|
282
|
+
test("still initialises, so the widget renders untranslated", async () => {
|
|
283
|
+
// Untranslated beats absent: a widget whose label never arrives is a
|
|
284
|
+
// widget the visitor cannot use.
|
|
285
|
+
const harness = await load({ catalogues: {}, fetchFails: true });
|
|
246
286
|
|
|
247
|
-
|
|
248
|
-
expect(callback).not.toHaveBeenCalled();
|
|
287
|
+
const i18n = await initialised(harness);
|
|
249
288
|
|
|
250
|
-
|
|
251
|
-
expect(
|
|
289
|
+
expect(i18n.isInitialized).toBe(true);
|
|
290
|
+
expect(i18n.t("WIDGET.CHECKING", { defaultValue: "Checking" })).toBe(
|
|
291
|
+
"Checking",
|
|
292
|
+
);
|
|
252
293
|
});
|
|
253
294
|
|
|
254
|
-
test("
|
|
255
|
-
|
|
256
|
-
// so an unguarded listener would repeat whatever side effect the caller
|
|
257
|
-
// attached to it.
|
|
258
|
-
const callback = vi.fn();
|
|
259
|
-
const { initializeI18n, harness } = await load();
|
|
295
|
+
test("reports the namespace as loaded so callers stop waiting", async () => {
|
|
296
|
+
const harness = await load({ catalogues: {}, fetchFails: true });
|
|
260
297
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
298
|
+
expect((await initialised(harness)).hasLoadedNamespace("translation")).toBe(
|
|
299
|
+
true,
|
|
300
|
+
);
|
|
265
301
|
});
|
|
302
|
+
});
|
|
266
303
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
304
|
+
describe("switching language", () => {
|
|
305
|
+
test("loads the new catalogue and translates against it", async () => {
|
|
306
|
+
const harness = await load({
|
|
307
|
+
catalogues: { en: { YES: "yes" }, de: { YES: "ja" } },
|
|
308
|
+
});
|
|
309
|
+
const i18n = await initialised(harness, "en");
|
|
310
|
+
|
|
311
|
+
await i18n.changeLanguage("de");
|
|
312
|
+
|
|
313
|
+
expect(i18n.language).toBe("de");
|
|
314
|
+
expect(i18n.t("YES")).toBe("ja");
|
|
271
315
|
});
|
|
272
316
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
317
|
+
test("notifies subscribers", async () => {
|
|
318
|
+
const harness = await load({ catalogues: { en: {}, de: {} } });
|
|
319
|
+
const i18n = await initialised(harness, "en");
|
|
320
|
+
const listener = vi.fn();
|
|
321
|
+
i18n.on("languageChanged", listener);
|
|
322
|
+
|
|
323
|
+
await i18n.changeLanguage("de");
|
|
279
324
|
|
|
280
|
-
|
|
325
|
+
expect(listener).toHaveBeenCalled();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("does nothing when the language is already active", async () => {
|
|
329
|
+
const harness = await load({ catalogues: { en: {} } });
|
|
330
|
+
const i18n = await initialised(harness, "en");
|
|
331
|
+
const before = harness.requested.length;
|
|
332
|
+
|
|
333
|
+
await i18n.changeLanguage("en");
|
|
334
|
+
|
|
335
|
+
expect(harness.requested).toHaveLength(before);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("unsubscribed listeners stop being called", async () => {
|
|
339
|
+
const harness = await load({ catalogues: { en: {}, de: {} } });
|
|
340
|
+
const i18n = await initialised(harness, "en");
|
|
341
|
+
const listener = vi.fn();
|
|
342
|
+
i18n.on("languageChanged", listener);
|
|
343
|
+
i18n.off("languageChanged", listener);
|
|
344
|
+
|
|
345
|
+
await i18n.changeLanguage("de");
|
|
346
|
+
|
|
347
|
+
expect(listener).not.toHaveBeenCalled();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe("the singleton", () => {
|
|
352
|
+
test("a second call reuses the instance rather than refetching", async () => {
|
|
353
|
+
// Every widget on the page shares one catalogue fetch.
|
|
354
|
+
const harness = await load({ catalogues: { en: {} } });
|
|
355
|
+
const first = await initialised(harness);
|
|
356
|
+
const before = harness.requested.length;
|
|
357
|
+
|
|
358
|
+
const second = harness.initializeI18n();
|
|
359
|
+
|
|
360
|
+
expect(second).toBe(first);
|
|
361
|
+
expect(harness.requested).toHaveLength(before);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("a second call still fires its callback", async () => {
|
|
365
|
+
// The one-shot callback is only armed on first init, so without this
|
|
366
|
+
// loadI18next() would never settle for the second widget.
|
|
367
|
+
const harness = await load({ catalogues: { en: {} } });
|
|
368
|
+
await initialised(harness);
|
|
369
|
+
|
|
370
|
+
const callback = vi.fn();
|
|
371
|
+
harness.initializeI18n(callback);
|
|
281
372
|
|
|
282
|
-
expect(harness.initOptions).toHaveLength(0);
|
|
283
373
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
284
|
-
|
|
285
|
-
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe("before the catalogue arrives", () => {
|
|
378
|
+
let harness: Harness;
|
|
379
|
+
|
|
380
|
+
beforeEach(async () => {
|
|
381
|
+
harness = await load({ never: true });
|
|
286
382
|
});
|
|
287
383
|
|
|
288
|
-
test("
|
|
289
|
-
|
|
290
|
-
initializeI18n();
|
|
291
|
-
expect(harness.plugins).toHaveLength(0);
|
|
384
|
+
test("reports itself uninitialised", () => {
|
|
385
|
+
expect(harness.initializeI18n().isInitialized).toBe(false);
|
|
292
386
|
});
|
|
293
387
|
|
|
294
|
-
test("
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
388
|
+
test("reports the namespace as not loaded", () => {
|
|
389
|
+
expect(harness.initializeI18n().hasLoadedNamespace("translation")).toBe(
|
|
390
|
+
false,
|
|
391
|
+
);
|
|
298
392
|
});
|
|
299
393
|
});
|