@prosopo/locale 3.4.3 → 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 +17 -15
- package/CHANGELOG.md +74 -0
- package/dist/cjs/i18SharedOptions.cjs +2 -2
- package/dist/cjs/i18nBackend.cjs +1 -1
- package/dist/cjs/i18nFrontend.cjs +167 -53
- package/dist/cjs/i18nMiddleware.cjs +5 -2
- package/dist/cjs/index.cjs +5 -4
- package/dist/cjs/locales/en/translation.cjs +1 -0
- package/dist/cjs/translationKey.cjs +2 -3
- package/dist/cjs/translations.cjs +4 -5
- package/dist/cjs/translator.cjs +29 -0
- 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 +167 -47
- 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 +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- 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/locales/ar/translation.json +1 -0
- package/dist/locales/az/translation.json +1 -0
- package/dist/locales/cs/translation.json +1 -0
- package/dist/locales/de/translation.json +1 -0
- package/dist/locales/el/translation.json +1 -0
- package/dist/locales/en/translation.js +1 -0
- package/dist/locales/en/translation.json +1 -0
- package/dist/locales/es/translation.json +1 -0
- package/dist/locales/fi/translation.json +1 -0
- package/dist/locales/fr/translation.json +1 -0
- package/dist/locales/hi/translation.json +1 -0
- package/dist/locales/hu/translation.json +1 -0
- package/dist/locales/id/translation.json +1 -0
- package/dist/locales/it/translation.json +1 -0
- package/dist/locales/ja/translation.json +1 -0
- package/dist/locales/jv/translation.json +1 -0
- package/dist/locales/ko/translation.json +1 -0
- package/dist/locales/ml/translation.json +1 -0
- package/dist/locales/ms/translation.json +1 -0
- package/dist/locales/nl/translation.json +1 -0
- package/dist/locales/no/translation.json +1 -0
- package/dist/locales/pl/translation.json +1 -0
- package/dist/locales/pt/translation.json +1 -0
- package/dist/locales/pt-BR/translation.json +1 -0
- package/dist/locales/ro/translation.json +1 -0
- package/dist/locales/ru/translation.json +1 -0
- package/dist/locales/sr/translation.json +1 -0
- package/dist/locales/sv/translation.json +1 -0
- package/dist/locales/th/translation.json +1 -0
- package/dist/locales/tr/translation.json +1 -0
- package/dist/locales/uk/translation.json +1 -0
- package/dist/locales/vi/translation.json +1 -0
- package/dist/locales/zh-CN/translation.json +1 -0
- package/dist/tests/i18nFrontend.unit.test.js +218 -173
- 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 +27 -20
- 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/tests/translator.unit.test.d.ts +2 -0
- package/dist/tests/translator.unit.test.d.ts.map +1 -0
- package/dist/tests/translator.unit.test.js +134 -0
- package/dist/tests/translator.unit.test.js.map +1 -0
- 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 +9 -0
- package/dist/translator.d.ts.map +1 -0
- package/dist/translator.js +29 -0
- package/dist/translator.js.map +1 -0
- 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 +4 -9
- package/src/i18SharedOptions.ts +3 -3
- package/src/i18nFrontend.ts +240 -78
- package/src/i18nMiddleware.ts +6 -1
- package/src/index.ts +11 -4
- package/src/loadI18next.ts +8 -9
- package/src/locales/ar/translation.json +1 -0
- package/src/locales/az/translation.json +1 -0
- package/src/locales/cs/translation.json +1 -0
- package/src/locales/de/translation.json +1 -0
- package/src/locales/el/translation.json +1 -0
- package/src/locales/en/translation.json +1 -0
- package/src/locales/es/translation.json +1 -0
- package/src/locales/fi/translation.json +1 -0
- package/src/locales/fr/translation.json +1 -0
- package/src/locales/hi/translation.json +1 -0
- package/src/locales/hu/translation.json +1 -0
- package/src/locales/id/translation.json +1 -0
- package/src/locales/it/translation.json +1 -0
- package/src/locales/ja/translation.json +1 -0
- package/src/locales/jv/translation.json +1 -0
- package/src/locales/ko/translation.json +1 -0
- package/src/locales/ml/translation.json +1 -0
- package/src/locales/ms/translation.json +1 -0
- package/src/locales/nl/translation.json +1 -0
- package/src/locales/no/translation.json +1 -0
- package/src/locales/pl/translation.json +1 -0
- package/src/locales/pt/translation.json +1 -0
- package/src/locales/pt-BR/translation.json +1 -0
- package/src/locales/ro/translation.json +1 -0
- package/src/locales/ru/translation.json +1 -0
- package/src/locales/sr/translation.json +1 -0
- package/src/locales/sv/translation.json +1 -0
- package/src/locales/th/translation.json +1 -0
- package/src/locales/tr/translation.json +1 -0
- package/src/locales/uk/translation.json +1 -0
- package/src/locales/vi/translation.json +1 -0
- package/src/locales/zh-CN/translation.json +1 -0
- package/src/tests/i18nFrontend.unit.test.ts +310 -219
- 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 +36 -32
- package/src/tests/translationKey.unit.test.ts +12 -38
- package/src/tests/translations.unit.test.ts +18 -31
- package/src/tests/translator.unit.test.ts +213 -0
- package/src/translationKey.ts +10 -9
- package/src/translations.ts +6 -5
- package/src/translator.ts +68 -0
- package/src/types.ts +42 -0
- package/tsconfig.cjs.json +3 -0
- package/tsconfig.json +3 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/useTranslation.cjs +0 -12
- package/dist/tests/useTranslation.unit.test.d.ts +0 -2
- package/dist/tests/useTranslation.unit.test.d.ts.map +0 -1
- package/dist/tests/useTranslation.unit.test.js +0 -81
- package/dist/tests/useTranslation.unit.test.js.map +0 -1
- package/dist/useTranslation.d.ts +0 -7
- package/dist/useTranslation.d.ts.map +0 -1
- package/dist/useTranslation.js +0 -11
- package/dist/useTranslation.js.map +0 -1
- package/src/tests/useTranslation.unit.test.ts +0 -134
- package/src/useTranslation.ts +0 -31
|
@@ -12,291 +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
|
-
})
|
|
99
|
-
vi.doMock("react-i18next", () => ({ initReactI18next: "initReactI18next" }));
|
|
80
|
+
});
|
|
100
81
|
|
|
101
82
|
const imported = await import("../i18nFrontend.js");
|
|
102
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
initializeI18n: imported.default,
|
|
85
|
+
requested,
|
|
86
|
+
cookie: (): string => cookie,
|
|
87
|
+
stored: (): Record<string, string> => store,
|
|
88
|
+
};
|
|
103
89
|
};
|
|
104
90
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
backendOptions?: { loadPath?: string }[];
|
|
111
|
-
};
|
|
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
|
+
});
|
|
112
96
|
|
|
113
97
|
afterEach(() => {
|
|
98
|
+
vi.unstubAllGlobals();
|
|
114
99
|
vi.resetModules();
|
|
115
|
-
for (const module of [
|
|
116
|
-
"i18next",
|
|
117
|
-
"i18next-chained-backend",
|
|
118
|
-
"i18next-http-backend",
|
|
119
|
-
"i18next-browser-languagedetector",
|
|
120
|
-
"i18next-resources-to-backend",
|
|
121
|
-
"react-i18next",
|
|
122
|
-
]) {
|
|
123
|
-
vi.doUnmock(module);
|
|
124
|
-
}
|
|
125
100
|
});
|
|
126
101
|
|
|
127
|
-
describe("
|
|
128
|
-
test("
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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");
|
|
132
114
|
});
|
|
133
115
|
|
|
134
|
-
test("
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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");
|
|
140
125
|
});
|
|
141
126
|
|
|
142
|
-
test("
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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");
|
|
149
135
|
});
|
|
150
136
|
|
|
151
|
-
test("
|
|
152
|
-
const
|
|
153
|
-
|
|
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");
|
|
154
172
|
});
|
|
155
173
|
});
|
|
156
174
|
|
|
157
|
-
describe("
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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);
|
|
165
186
|
});
|
|
166
187
|
|
|
167
|
-
test("
|
|
168
|
-
const {
|
|
169
|
-
|
|
170
|
-
|
|
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);
|
|
171
199
|
});
|
|
172
200
|
|
|
173
|
-
test("
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
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);
|
|
179
207
|
});
|
|
208
|
+
});
|
|
180
209
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
order: ["cookie", "localStorage", "navigator"],
|
|
186
|
-
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" } } },
|
|
187
214
|
});
|
|
215
|
+
|
|
216
|
+
expect((await initialised(harness)).t("WIDGET.I_AM_HUMAN")).toBe(
|
|
217
|
+
"I am human",
|
|
218
|
+
);
|
|
188
219
|
});
|
|
189
220
|
|
|
190
|
-
test("
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
expect(harness.detectorOptions[0]).toMatchObject({
|
|
194
|
-
caches: ["localStorage", "cookie"],
|
|
221
|
+
test("substitutes interpolation values", async () => {
|
|
222
|
+
const harness = await load({
|
|
223
|
+
catalogues: { en: { POSITION: "{{x}} across, {{y}} down" } },
|
|
195
224
|
});
|
|
225
|
+
|
|
226
|
+
expect((await initialised(harness)).t("POSITION", { x: 10, y: 20 })).toBe(
|
|
227
|
+
"10 across, 20 down",
|
|
228
|
+
);
|
|
196
229
|
});
|
|
197
|
-
});
|
|
198
230
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
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");
|
|
207
239
|
});
|
|
208
240
|
|
|
209
|
-
test("
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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");
|
|
215
249
|
});
|
|
216
250
|
|
|
217
|
-
test("
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
expect(loadPath).toMatch(/^[a-z]+:\/\//);
|
|
227
|
-
// The module's own filename must have been stripped before the locales
|
|
228
|
-
// segment is appended, or the path would nest under the script itself.
|
|
229
|
-
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");
|
|
230
260
|
});
|
|
231
261
|
|
|
232
|
-
test("
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
);
|
|
242
278
|
});
|
|
243
279
|
});
|
|
244
280
|
|
|
245
|
-
describe("
|
|
246
|
-
test("
|
|
247
|
-
|
|
248
|
-
|
|
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 });
|
|
249
286
|
|
|
250
|
-
|
|
251
|
-
expect(callback).not.toHaveBeenCalled();
|
|
287
|
+
const i18n = await initialised(harness);
|
|
252
288
|
|
|
253
|
-
|
|
254
|
-
expect(
|
|
289
|
+
expect(i18n.isInitialized).toBe(true);
|
|
290
|
+
expect(i18n.t("WIDGET.CHECKING", { defaultValue: "Checking" })).toBe(
|
|
291
|
+
"Checking",
|
|
292
|
+
);
|
|
255
293
|
});
|
|
256
294
|
|
|
257
|
-
test("
|
|
258
|
-
|
|
259
|
-
// so an unguarded listener would repeat whatever side effect the caller
|
|
260
|
-
// attached to it.
|
|
261
|
-
const callback = vi.fn();
|
|
262
|
-
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 });
|
|
263
297
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
298
|
+
expect((await initialised(harness)).hasLoadedNamespace("translation")).toBe(
|
|
299
|
+
true,
|
|
300
|
+
);
|
|
268
301
|
});
|
|
302
|
+
});
|
|
269
303
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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");
|
|
274
315
|
});
|
|
275
316
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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");
|
|
282
324
|
|
|
283
|
-
|
|
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);
|
|
284
372
|
|
|
285
|
-
expect(harness.initOptions).toHaveLength(0);
|
|
286
373
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
287
|
-
|
|
288
|
-
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe("before the catalogue arrives", () => {
|
|
378
|
+
let harness: Harness;
|
|
379
|
+
|
|
380
|
+
beforeEach(async () => {
|
|
381
|
+
harness = await load({ never: true });
|
|
289
382
|
});
|
|
290
383
|
|
|
291
|
-
test("
|
|
292
|
-
|
|
293
|
-
initializeI18n();
|
|
294
|
-
expect(harness.plugins).toHaveLength(0);
|
|
384
|
+
test("reports itself uninitialised", () => {
|
|
385
|
+
expect(harness.initializeI18n().isInitialized).toBe(false);
|
|
295
386
|
});
|
|
296
387
|
|
|
297
|
-
test("
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
388
|
+
test("reports the namespace as not loaded", () => {
|
|
389
|
+
expect(harness.initializeI18n().hasLoadedNamespace("translation")).toBe(
|
|
390
|
+
false,
|
|
391
|
+
);
|
|
301
392
|
});
|
|
302
393
|
});
|