@prosopo/locale 3.2.9 → 3.3.1
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 +10 -9
- package/.turbo/turbo-build$colon$tsc.log +5 -5
- package/.turbo/turbo-build.log +17 -16
- package/CHANGELOG.md +29 -0
- package/dist/cjs/i18SharedOptions.cjs +1 -1
- package/dist/cjs/i18nBackend.cjs +4 -1
- package/dist/cjs/i18nFrontend.cjs +4 -1
- package/dist/cjs/loadI18next.cjs +41 -14
- package/dist/cjs/process.cjs +17 -0
- package/dist/cjs/translationKey.cjs +2 -0
- package/dist/i18SharedOptions.js +2 -1
- package/dist/i18SharedOptions.js.map +1 -1
- package/dist/i18nBackend.d.ts.map +1 -1
- package/dist/i18nBackend.js +4 -1
- package/dist/i18nBackend.js.map +1 -1
- package/dist/i18nFrontend.d.ts.map +1 -1
- package/dist/i18nFrontend.js +4 -1
- package/dist/i18nFrontend.js.map +1 -1
- package/dist/loadI18next.d.ts +1 -0
- package/dist/loadI18next.d.ts.map +1 -1
- package/dist/loadI18next.js +41 -15
- package/dist/loadI18next.js.map +1 -1
- package/dist/process.d.ts +2 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +17 -0
- package/dist/process.js.map +1 -0
- package/dist/tests/i18SharedOptions.unit.test.d.ts +2 -0
- package/dist/tests/i18SharedOptions.unit.test.d.ts.map +1 -0
- package/dist/tests/i18SharedOptions.unit.test.js +91 -0
- package/dist/tests/i18SharedOptions.unit.test.js.map +1 -0
- package/dist/tests/i18nBackend.unit.test.d.ts +2 -0
- package/dist/tests/i18nBackend.unit.test.d.ts.map +1 -0
- package/dist/tests/i18nBackend.unit.test.js +153 -0
- package/dist/tests/i18nBackend.unit.test.js.map +1 -0
- package/dist/tests/i18nFrontend.unit.test.d.ts +2 -0
- package/dist/tests/i18nFrontend.unit.test.d.ts.map +1 -0
- package/dist/tests/i18nFrontend.unit.test.js +199 -0
- package/dist/tests/i18nFrontend.unit.test.js.map +1 -0
- package/dist/tests/i18nMiddleware.unit.test.d.ts +2 -0
- package/dist/tests/i18nMiddleware.unit.test.d.ts.map +1 -0
- package/dist/tests/i18nMiddleware.unit.test.js +80 -0
- package/dist/tests/i18nMiddleware.unit.test.js.map +1 -0
- package/dist/tests/loadI18next.unit.test.d.ts +2 -0
- package/dist/tests/loadI18next.unit.test.d.ts.map +1 -0
- package/dist/tests/loadI18next.unit.test.js +270 -0
- package/dist/tests/loadI18next.unit.test.js.map +1 -0
- package/dist/tests/locale.test-d.d.ts +2 -0
- package/dist/tests/locale.test-d.d.ts.map +1 -0
- package/dist/tests/locale.test-d.js +88 -0
- package/dist/tests/locale.test-d.js.map +1 -0
- package/dist/tests/process.unit.test.d.ts +2 -0
- package/dist/tests/process.unit.test.d.ts.map +1 -0
- package/dist/tests/process.unit.test.js +14 -0
- package/dist/tests/process.unit.test.js.map +1 -0
- package/dist/tests/translationKey.unit.test.d.ts +2 -0
- package/dist/tests/translationKey.unit.test.d.ts.map +1 -0
- package/dist/tests/translationKey.unit.test.js +108 -0
- package/dist/tests/translationKey.unit.test.js.map +1 -0
- package/dist/tests/translations.unit.test.d.ts +2 -0
- package/dist/tests/translations.unit.test.d.ts.map +1 -0
- package/dist/tests/translations.unit.test.js +74 -0
- package/dist/tests/translations.unit.test.js.map +1 -0
- package/dist/tests/useTranslation.unit.test.d.ts +2 -0
- package/dist/tests/useTranslation.unit.test.d.ts.map +1 -0
- package/dist/tests/useTranslation.unit.test.js +81 -0
- package/dist/tests/useTranslation.unit.test.js.map +1 -0
- package/dist/tests/util.unit.test.d.ts +2 -0
- package/dist/tests/util.unit.test.d.ts.map +1 -0
- package/dist/tests/util.unit.test.js +66 -0
- package/dist/tests/util.unit.test.js.map +1 -0
- package/dist/translationKey.d.ts +4 -0
- package/dist/translationKey.d.ts.map +1 -1
- package/dist/translationKey.js +2 -1
- package/dist/translationKey.js.map +1 -1
- package/package.json +6 -6
- package/src/i18SharedOptions.ts +4 -4
- package/src/i18nBackend.ts +16 -0
- package/src/i18nFrontend.ts +16 -0
- package/src/loadI18next.ts +83 -15
- package/src/process.ts +29 -0
- package/src/tests/i18SharedOptions.unit.test.ts +139 -0
- package/src/tests/i18nBackend.unit.test.ts +241 -0
- package/src/tests/i18nFrontend.unit.test.ts +302 -0
- package/src/tests/i18nMiddleware.unit.test.ts +140 -0
- package/src/tests/loadI18next.unit.test.ts +443 -0
- package/src/tests/locale.test-d.ts +156 -0
- package/src/tests/process.unit.test.ts +33 -0
- package/src/tests/translationKey.unit.test.ts +166 -0
- package/src/tests/translations.unit.test.ts +110 -0
- package/src/tests/useTranslation.unit.test.ts +134 -0
- package/src/tests/util.unit.test.ts +102 -0
- package/src/translationKey.ts +18 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTranslation.unit.test.d.ts","sourceRoot":"","sources":["../../src/tests/useTranslation.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
const asI18n = (value) => value;
|
|
3
|
+
const load = async () => {
|
|
4
|
+
vi.resetModules();
|
|
5
|
+
const instance = asI18n({ name: "package instance" });
|
|
6
|
+
const calls = [];
|
|
7
|
+
let initCalls = 0;
|
|
8
|
+
vi.doMock("../i18nFrontend.js", () => ({
|
|
9
|
+
default: () => {
|
|
10
|
+
initCalls += 1;
|
|
11
|
+
return instance;
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
vi.doMock("react-i18next", () => ({
|
|
15
|
+
useTranslation: (namespace, options) => {
|
|
16
|
+
calls.push([namespace, options]);
|
|
17
|
+
return { t: (key) => key, i18n: options.i18n };
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
const imported = await import("../useTranslation.js");
|
|
21
|
+
return {
|
|
22
|
+
useTranslation: imported.default,
|
|
23
|
+
instance,
|
|
24
|
+
calls,
|
|
25
|
+
initCount: () => initCalls,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.resetModules();
|
|
30
|
+
vi.doUnmock("../i18nFrontend.js");
|
|
31
|
+
vi.doUnmock("react-i18next");
|
|
32
|
+
});
|
|
33
|
+
describe("useTranslation", () => {
|
|
34
|
+
test("always asks for the translation namespace", async () => {
|
|
35
|
+
const { useTranslation, calls } = await load();
|
|
36
|
+
useTranslation();
|
|
37
|
+
expect(calls[0]?.[0]).toBe("translation");
|
|
38
|
+
});
|
|
39
|
+
test("injects the package's own i18next instance", async () => {
|
|
40
|
+
const { useTranslation, calls, instance } = await load();
|
|
41
|
+
useTranslation();
|
|
42
|
+
expect(calls[0]?.[1].i18n).toBe(instance);
|
|
43
|
+
});
|
|
44
|
+
test("initialises the frontend instance on use", async () => {
|
|
45
|
+
const { useTranslation, initCount } = await load();
|
|
46
|
+
useTranslation();
|
|
47
|
+
expect(initCount()).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
test("works with no options at all", async () => {
|
|
50
|
+
const { useTranslation, calls } = await load();
|
|
51
|
+
useTranslation();
|
|
52
|
+
expect(calls[0]?.[1]).toEqual({ i18n: expect.anything() });
|
|
53
|
+
});
|
|
54
|
+
test("passes caller options through", async () => {
|
|
55
|
+
const { useTranslation, calls } = await load();
|
|
56
|
+
useTranslation({ nsMode: "fallback", useSuspense: false });
|
|
57
|
+
expect(calls[0]?.[1]).toMatchObject({
|
|
58
|
+
nsMode: "fallback",
|
|
59
|
+
useSuspense: false,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
test("lets the caller override the instance, since options spread last", async () => {
|
|
63
|
+
const { useTranslation, calls, instance } = await load();
|
|
64
|
+
const other = asI18n({ name: "caller instance" });
|
|
65
|
+
useTranslation({ i18n: other });
|
|
66
|
+
expect(calls[0]?.[1].i18n).toBe(other);
|
|
67
|
+
expect(calls[0]?.[1].i18n).not.toBe(instance);
|
|
68
|
+
});
|
|
69
|
+
test("returns whatever react-i18next returns", async () => {
|
|
70
|
+
const { useTranslation } = await load();
|
|
71
|
+
const result = useTranslation();
|
|
72
|
+
expect(result.t("WIDGET.SUBMIT")).toBe("WIDGET.SUBMIT");
|
|
73
|
+
});
|
|
74
|
+
test("re-initialises on every call, matching hook semantics", async () => {
|
|
75
|
+
const { useTranslation, initCount } = await load();
|
|
76
|
+
useTranslation();
|
|
77
|
+
useTranslation();
|
|
78
|
+
expect(initCount()).toBe(2);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
//# sourceMappingURL=useTranslation.unit.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTranslation.unit.test.js","sourceRoot":"","sources":["../../src/tests/useTranslation.unit.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.unit.test.d.ts","sourceRoot":"","sources":["../../src/tests/util.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { isClientSide, isServerSide } from "../util.js";
|
|
3
|
+
const setWindow = (value) => {
|
|
4
|
+
if (value === undefined) {
|
|
5
|
+
Reflect.deleteProperty(globalThis, "window");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(globalThis, "window", {
|
|
9
|
+
value,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
setWindow(undefined);
|
|
16
|
+
});
|
|
17
|
+
describe("isClientSide", () => {
|
|
18
|
+
test("is false under plain node, where there is no window at all", () => {
|
|
19
|
+
expect(isClientSide()).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
test("is true when window exposes document.createElement", () => {
|
|
22
|
+
setWindow({ document: { createElement: () => { } } });
|
|
23
|
+
expect(isClientSide()).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
test("is false when window exists but has no document", () => {
|
|
26
|
+
setWindow({});
|
|
27
|
+
expect(isClientSide()).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
test("is false when document exists but has no createElement", () => {
|
|
30
|
+
setWindow({ document: {} });
|
|
31
|
+
expect(isClientSide()).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
test("is false when createElement is present but undefined", () => {
|
|
34
|
+
setWindow({ document: { createElement: undefined } });
|
|
35
|
+
expect(isClientSide()).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
test("returns a real boolean, not the truthy function itself", () => {
|
|
38
|
+
const createElement = () => { };
|
|
39
|
+
setWindow({ document: { createElement } });
|
|
40
|
+
const result = isClientSide();
|
|
41
|
+
expect(result).toBe(true);
|
|
42
|
+
expect(typeof result).toBe("boolean");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("isServerSide", () => {
|
|
46
|
+
test("is true under plain node", () => {
|
|
47
|
+
expect(isServerSide()).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
test("is false in a browser-like environment", () => {
|
|
50
|
+
setWindow({ document: { createElement: () => { } } });
|
|
51
|
+
expect(isServerSide()).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
test("is always the exact negation of isClientSide", () => {
|
|
54
|
+
const environments = [
|
|
55
|
+
undefined,
|
|
56
|
+
{},
|
|
57
|
+
{ document: {} },
|
|
58
|
+
{ document: { createElement: () => { } } },
|
|
59
|
+
];
|
|
60
|
+
for (const environment of environments) {
|
|
61
|
+
setWindow(environment);
|
|
62
|
+
expect(isServerSide()).toBe(!isClientSide());
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=util.unit.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.unit.test.js","sourceRoot":"","sources":["../../src/tests/util.unit.test.ts"],"names":[],"mappings":"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"}
|
package/dist/translationKey.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
export type TranslationNode = {
|
|
3
|
+
[key: string]: TranslationNode | string;
|
|
4
|
+
} | string;
|
|
5
|
+
export declare function getLeafFieldPath(obj: TranslationNode): string[];
|
|
2
6
|
export declare const TranslationKeysSchema: z.ZodEnum<[string, ...string[]]>;
|
|
3
7
|
export type TranslationKey = z.infer<typeof TranslationKeysSchema>;
|
|
4
8
|
//# sourceMappingURL=translationKey.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translationKey.d.ts","sourceRoot":"","sources":["../src/translationKey.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"translationKey.d.ts","sourceRoot":"","sources":["../src/translationKey.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,MAAM,eAAe,GACxB;IACA,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,MAAM,CAAC;CACvC,GACD,MAAM,CAAC;AAKV,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,EAAE,CA8B/D;AAED,eAAO,MAAM,qBAAqB,kCAEjC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
|
package/dist/translationKey.js
CHANGED
|
@@ -6,6 +6,7 @@ function getLeafFieldPath(obj) {
|
|
|
6
6
|
return Object.keys(obj).reduce((arr, key) => {
|
|
7
7
|
const value = obj[key];
|
|
8
8
|
if (value === void 0) throw new Error(`Undefined value for key ${key}`);
|
|
9
|
+
if (typeof value === "string") return arr.concat(key);
|
|
9
10
|
const children = getLeafFieldPath(value);
|
|
10
11
|
return arr.concat(children.map((child) => {
|
|
11
12
|
return `${key}.${child}`;
|
|
@@ -14,4 +15,4 @@ function getLeafFieldPath(obj) {
|
|
|
14
15
|
}
|
|
15
16
|
var TranslationKeysSchema = z$1.enum(getLeafFieldPath(translation_default));
|
|
16
17
|
//#endregion
|
|
17
|
-
export { TranslationKeysSchema };
|
|
18
|
+
export { TranslationKeysSchema, getLeafFieldPath };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translationKey.js","sourceRoot":"","sources":["../src/translationKey.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,aAAa,MAAM,+BAA+B,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"translationKey.js","sourceRoot":"","sources":["../src/translationKey.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,aAAa,MAAM,+BAA+B,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAWhF,MAAM,UAAU,gBAAgB,CAAC,GAAoB;IACpD,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,EAAE,CAAC;IACX,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC3C,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,EAAE,CAAC,CAAC;QACnD,CAAC;QASD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC/B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;QAED,MAAM,QAAQ,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAEzC,OAAO,GAAG,CAAC,MAAM,CAChB,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YACtB,OAAO,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,CAAC,CAAC,CACF,CAAC;IACH,CAAC,EAAE,EAAc,CAAC,CAAC;AACpB,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,IAAI,CAC1C,gBAAgB,CAAC,aAAa,CAA0B,CACxD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prosopo/locale",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Prosopo locale library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -33,17 +33,17 @@
|
|
|
33
33
|
"license": "Apache-2.0",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"i18next": "24.2.2",
|
|
36
|
-
"i18next-browser-languagedetector": "8.
|
|
36
|
+
"i18next-browser-languagedetector": "8.2.1",
|
|
37
37
|
"i18next-chained-backend": "4.6.2",
|
|
38
|
-
"i18next-fs-backend": "2.6.
|
|
38
|
+
"i18next-fs-backend": "2.6.7",
|
|
39
39
|
"i18next-http-backend": "3.0.2",
|
|
40
|
-
"i18next-http-middleware": "3.9.
|
|
41
|
-
"i18next-resources-to-backend": "1.2.
|
|
40
|
+
"i18next-http-middleware": "3.9.8",
|
|
41
|
+
"i18next-resources-to-backend": "1.2.3",
|
|
42
42
|
"react-i18next": "15.4.0",
|
|
43
43
|
"zod": "3.23.8"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@prosopo/config": "3.3.
|
|
46
|
+
"@prosopo/config": "3.3.12",
|
|
47
47
|
"@types/node": "22.10.2",
|
|
48
48
|
"@vitest/coverage-v8": "4.1.10",
|
|
49
49
|
"concurrently": "9.0.1",
|
package/src/i18SharedOptions.ts
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
|
+
import { getProcess } from "./process.js";
|
|
14
15
|
import { LanguageSchema, Languages } from "./translations.js";
|
|
15
16
|
|
|
16
|
-
//
|
|
17
|
-
// runtime where `process` is undefined. Without
|
|
17
|
+
// Read through getProcess() so the module stays loadable in a plain browser
|
|
18
|
+
// runtime where `process` is undefined. Without that guard, any side-effectful
|
|
18
19
|
// runtime import of `@prosopo/types` (which transitively reaches here via
|
|
19
20
|
// `LanguageSchema`) crashes the page in Vite's dev/preview mode.
|
|
20
|
-
const logLevel =
|
|
21
|
-
typeof process !== "undefined" ? process.env?.PROSOPO_LOG_LEVEL : undefined;
|
|
21
|
+
const logLevel = getProcess()?.env?.PROSOPO_LOG_LEVEL;
|
|
22
22
|
|
|
23
23
|
export const i18nSharedOptions = {
|
|
24
24
|
debug: logLevel === "debug",
|
package/src/i18nBackend.ts
CHANGED
|
@@ -43,9 +43,25 @@ export function initializeI18n(
|
|
|
43
43
|
loadPath,
|
|
44
44
|
},
|
|
45
45
|
});
|
|
46
|
+
// "loaded" fires once per namespace, and again after changeLanguage(),
|
|
47
|
+
// so the raw event is not the one-shot signal the callback expects.
|
|
48
|
+
// Latch it here as well as in loadI18next: this callback is public and
|
|
49
|
+
// a caller could reasonably use it to run a side effect.
|
|
50
|
+
let notified = false;
|
|
46
51
|
i18n.on("loaded", () => {
|
|
52
|
+
if (notified) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
notified = true;
|
|
47
56
|
i18nLoadedCallback?.(i18n);
|
|
48
57
|
});
|
|
58
|
+
} else {
|
|
59
|
+
// Nothing to initialise — either i18next is already up (a frontend
|
|
60
|
+
// consumer got there first) or we are not on the server. The `loaded`
|
|
61
|
+
// handler above is only registered inside the init branch, so without
|
|
62
|
+
// this the callback would never fire and loadI18next() would stay
|
|
63
|
+
// pending forever instead of handing back the instance we already have.
|
|
64
|
+
i18nLoadedCallback?.(i18n);
|
|
49
65
|
}
|
|
50
66
|
return i18n;
|
|
51
67
|
}
|
package/src/i18nFrontend.ts
CHANGED
|
@@ -79,9 +79,25 @@ export function initializeI18n(
|
|
|
79
79
|
},
|
|
80
80
|
...reactOptions,
|
|
81
81
|
} as InitOptions);
|
|
82
|
+
// "loaded" fires once per namespace, and again after changeLanguage(),
|
|
83
|
+
// so the raw event is not the one-shot signal the callback expects.
|
|
84
|
+
// Latch it here as well as in loadI18next: this callback is public and
|
|
85
|
+
// a caller could reasonably use it to run a side effect.
|
|
86
|
+
let notified = false;
|
|
82
87
|
i18n.on("loaded", () => {
|
|
88
|
+
if (notified) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
notified = true;
|
|
83
92
|
i18nLoadedCallback?.(i18n);
|
|
84
93
|
});
|
|
94
|
+
} else {
|
|
95
|
+
// Already initialised (e.g. useTranslation() ran first, or a second
|
|
96
|
+
// widget mounted). The `loaded` handler is only registered in the init
|
|
97
|
+
// branch, so the callback has to fire here or loadI18next() never
|
|
98
|
+
// settles. Resources are already present, so resolving immediately is
|
|
99
|
+
// correct; the caller reconciles the language afterwards.
|
|
100
|
+
i18nLoadedCallback?.(i18n);
|
|
85
101
|
}
|
|
86
102
|
return i18n;
|
|
87
103
|
}
|
package/src/loadI18next.ts
CHANGED
|
@@ -15,6 +15,18 @@
|
|
|
15
15
|
import type { i18n } from "i18next";
|
|
16
16
|
let i18nInstance: i18n;
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* How long to wait for i18next to report `loaded` before giving up on it.
|
|
20
|
+
*
|
|
21
|
+
* Resolution is driven by an event, and events are not guaranteed: a backend
|
|
22
|
+
* that never answers (http backend against a dead CDN, fs backend against a
|
|
23
|
+
* missing locales dir) leaves the listener armed and the promise pending for
|
|
24
|
+
* the lifetime of the process. `cli.ts` calls loadI18next(true).then(...) with
|
|
25
|
+
* nothing to time it out, so a pending promise there is a silent boot hang with
|
|
26
|
+
* no log line to explain it.
|
|
27
|
+
*/
|
|
28
|
+
export const I18N_LOAD_TIMEOUT_MS = 10_000;
|
|
29
|
+
|
|
18
30
|
const reconcileLanguage = async (
|
|
19
31
|
instance: i18n,
|
|
20
32
|
lng: string | undefined,
|
|
@@ -27,26 +39,82 @@ const reconcileLanguage = async (
|
|
|
27
39
|
|
|
28
40
|
async function loadI18next(backend: boolean, lng?: string): Promise<i18n> {
|
|
29
41
|
return new Promise((resolve, reject) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
// On timeout we resolve with the (possibly not fully loaded) instance
|
|
43
|
+
// rather than rejecting. i18next falls back to the key itself when a
|
|
44
|
+
// resource is missing, so a degraded instance renders untranslated text;
|
|
45
|
+
// a rejection instead takes down the caller, and neither caller handles
|
|
46
|
+
// one — cli.ts has no .catch, and widgetFactory awaits it inside the
|
|
47
|
+
// render path, so the widget would not appear at all. Untranslated beats
|
|
48
|
+
// absent. Genuine errors still reject: this arm only fires on silence.
|
|
49
|
+
let settled = false;
|
|
50
|
+
// No `settled` guard needed here: both latches clear this timer, so it
|
|
51
|
+
// can only fire while nothing has settled.
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
settled = true;
|
|
54
|
+
if (i18nInstance) {
|
|
55
|
+
resolve(i18nInstance);
|
|
56
|
+
} else {
|
|
57
|
+
reject(
|
|
58
|
+
new Error(
|
|
59
|
+
`i18next did not load within ${I18N_LOAD_TIMEOUT_MS}ms and no instance was created`,
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}, I18N_LOAD_TIMEOUT_MS);
|
|
64
|
+
// Node keeps the event loop alive for a pending timer, which would hold
|
|
65
|
+
// a short-lived CLI process open for the full timeout after it had
|
|
66
|
+
// otherwise finished.
|
|
67
|
+
timer.unref?.();
|
|
68
|
+
|
|
69
|
+
// Both latches are one-shot: i18next can emit `loaded` more than once
|
|
70
|
+
// (per namespace, and again after changeLanguage), and a late event
|
|
71
|
+
// arriving after the timeout must not re-settle or leak a second timer.
|
|
72
|
+
const done = (instance: i18n): void => {
|
|
73
|
+
if (settled) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
settled = true;
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
resolve(instance);
|
|
79
|
+
};
|
|
80
|
+
const failWith = (error: unknown): void => {
|
|
81
|
+
if (settled) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
settled = true;
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
reject(error);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Every asynchronous branch below terminates in `.catch(failWith)`. A
|
|
90
|
+
// synchronous try/catch cannot see a rejected dynamic import or a
|
|
91
|
+
// rejected changeLanguage(), so without these the executor would
|
|
92
|
+
// simply return and the promise would stay pending forever — callers
|
|
93
|
+
// (i18nMiddleware, and through it the whole server bootstrap) would
|
|
94
|
+
// hang rather than fail.
|
|
95
|
+
if (backend) {
|
|
96
|
+
import("./i18nBackend.js")
|
|
97
|
+
.then(({ default: initializeI18n }) => {
|
|
33
98
|
if (!i18nInstance) {
|
|
34
|
-
// pass the resolver into the i18n init fn which
|
|
35
|
-
|
|
99
|
+
// pass the resolver into the i18n init fn, which fires it on the first
|
|
100
|
+
// "loaded" event
|
|
101
|
+
i18nInstance = initializeI18n(done);
|
|
36
102
|
} else {
|
|
37
103
|
// we've already initialised i18n so just return it
|
|
38
|
-
|
|
104
|
+
done(i18nInstance);
|
|
39
105
|
}
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
|
|
106
|
+
})
|
|
107
|
+
.catch(failWith);
|
|
108
|
+
} else {
|
|
109
|
+
import("./i18nFrontend.js")
|
|
110
|
+
.then(({ default: initializeI18n }) => {
|
|
43
111
|
if (!i18nInstance) {
|
|
44
112
|
// Pass `lng` in on first init so browser detection is skipped
|
|
45
113
|
// entirely when the site owner has supplied a language. The
|
|
46
114
|
// resolver only fires on the `loaded` event, so at resolve
|
|
47
115
|
// time the target-language resources are guaranteed present.
|
|
48
116
|
i18nInstance = initializeI18n((instance) => {
|
|
49
|
-
void reconcileLanguage(instance, lng).then(
|
|
117
|
+
void reconcileLanguage(instance, lng).then(done).catch(failWith);
|
|
50
118
|
}, lng);
|
|
51
119
|
} else {
|
|
52
120
|
// Singleton already exists (e.g. a prior widget mounted with a
|
|
@@ -54,12 +122,12 @@ async function loadI18next(backend: boolean, lng?: string): Promise<i18n> {
|
|
|
54
122
|
// Reconcile before resolving so callers can render synchronously
|
|
55
123
|
// against the requested language instead of seeing a flash of
|
|
56
124
|
// the previous one.
|
|
57
|
-
void reconcileLanguage(i18nInstance, lng)
|
|
125
|
+
void reconcileLanguage(i18nInstance, lng)
|
|
126
|
+
.then(done)
|
|
127
|
+
.catch(failWith);
|
|
58
128
|
}
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
} catch (e) {
|
|
62
|
-
reject(e);
|
|
129
|
+
})
|
|
130
|
+
.catch(failWith);
|
|
63
131
|
}
|
|
64
132
|
});
|
|
65
133
|
}
|
package/src/process.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
/**
|
|
16
|
+
* The `process` global, or undefined where there is none.
|
|
17
|
+
*
|
|
18
|
+
* This package is reached transitively from browser bundles, where `process`
|
|
19
|
+
* may be missing entirely or present but stripped down. A bare `process.env`
|
|
20
|
+
* read throws a ReferenceError in that case and takes the whole page down.
|
|
21
|
+
*
|
|
22
|
+
* It is a function, and it lives in its own module, so callers can be tested
|
|
23
|
+
* against a runtime that has no `process` at all. Inlining the `typeof` check
|
|
24
|
+
* at the point of use makes that branch untestable: vitest calls
|
|
25
|
+
* `process.memoryUsage()` between suites, so stubbing the real global away
|
|
26
|
+
* fails the run rather than the module under test.
|
|
27
|
+
*/
|
|
28
|
+
export const getProcess = (): NodeJS.Process | undefined =>
|
|
29
|
+
typeof process !== "undefined" ? process : undefined;
|
|
@@ -0,0 +1,139 @@
|
|
|
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 { Languages } from "../translations.js";
|
|
17
|
+
|
|
18
|
+
// The module reads process.env at import time, so every expectation about the
|
|
19
|
+
// debug flag has to re-import it under a fresh module registry.
|
|
20
|
+
type SharedOptions =
|
|
21
|
+
typeof import("../i18SharedOptions.js")["i18nSharedOptions"];
|
|
22
|
+
|
|
23
|
+
const loadOptions = async (): Promise<SharedOptions> => {
|
|
24
|
+
vi.resetModules();
|
|
25
|
+
const { i18nSharedOptions } = await import("../i18SharedOptions.js");
|
|
26
|
+
return i18nSharedOptions;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const originalLogLevel = process.env.PROSOPO_LOG_LEVEL;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
Reflect.deleteProperty(process.env, "PROSOPO_LOG_LEVEL");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.doUnmock("../process.js");
|
|
37
|
+
vi.resetModules();
|
|
38
|
+
if (originalLogLevel === undefined) {
|
|
39
|
+
Reflect.deleteProperty(process.env, "PROSOPO_LOG_LEVEL");
|
|
40
|
+
} else {
|
|
41
|
+
process.env.PROSOPO_LOG_LEVEL = originalLogLevel;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("i18nSharedOptions", () => {
|
|
46
|
+
test("falls back to English", async () => {
|
|
47
|
+
const options = await loadOptions();
|
|
48
|
+
expect(options.fallbackLng).toBe("en");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("uses the single translation namespace", async () => {
|
|
52
|
+
const options = await loadOptions();
|
|
53
|
+
expect(options.namespace).toBe("translation");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("supports exactly the declared language codes", async () => {
|
|
57
|
+
const options = await loadOptions();
|
|
58
|
+
expect(new Set(options.supportedLngs)).toEqual(
|
|
59
|
+
new Set(Object.values(Languages)),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// With this false, i18next will not silently serve `en` resources for a
|
|
64
|
+
// request for `en-GB`. Flipping it would quietly change which file a
|
|
65
|
+
// regional locale resolves to.
|
|
66
|
+
test("does not accept non-explicit regional variants", async () => {
|
|
67
|
+
const options = await loadOptions();
|
|
68
|
+
expect(options.nonExplicitSupportedLngs).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("debug flag", () => {
|
|
72
|
+
test("is off when PROSOPO_LOG_LEVEL is unset", async () => {
|
|
73
|
+
const options = await loadOptions();
|
|
74
|
+
expect(options.debug).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("is on only for the exact value 'debug'", async () => {
|
|
78
|
+
process.env.PROSOPO_LOG_LEVEL = "debug";
|
|
79
|
+
expect((await loadOptions()).debug).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("is off for any other log level", async () => {
|
|
83
|
+
for (const level of ["info", "warn", "error", "trace", ""]) {
|
|
84
|
+
process.env.PROSOPO_LOG_LEVEL = level;
|
|
85
|
+
expect((await loadOptions()).debug, level).toBe(false);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("is case sensitive — 'DEBUG' does not enable it", async () => {
|
|
90
|
+
process.env.PROSOPO_LOG_LEVEL = "DEBUG";
|
|
91
|
+
expect((await loadOptions()).debug).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// This module is reached transitively from browser bundles, where `process`
|
|
96
|
+
// is absent or partial, so both degraded shapes have to load cleanly.
|
|
97
|
+
//
|
|
98
|
+
// The `process` read goes through getProcess() precisely so it can be mocked
|
|
99
|
+
// here. Stubbing the global instead is not viable: vitest itself calls
|
|
100
|
+
// process.memoryUsage() between suites, so removing the real global fails
|
|
101
|
+
// the run rather than the module under test.
|
|
102
|
+
describe("browser runtime, where process may be absent or partial", () => {
|
|
103
|
+
const mockProcess = (value: NodeJS.Process | undefined): void => {
|
|
104
|
+
vi.resetModules();
|
|
105
|
+
vi.doMock("../process.js", () => ({
|
|
106
|
+
getProcess: (): NodeJS.Process | undefined => value,
|
|
107
|
+
}));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
test("loads when there is no process at all", async () => {
|
|
111
|
+
mockProcess(undefined);
|
|
112
|
+
const { i18nSharedOptions } = await import("../i18SharedOptions.js");
|
|
113
|
+
expect(i18nSharedOptions.debug).toBe(false);
|
|
114
|
+
expect(i18nSharedOptions.fallbackLng).toBe("en");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("loads when process exists but exposes no env", async () => {
|
|
118
|
+
// Prototype-chained off the real process so the shape stays honest;
|
|
119
|
+
// only `env` is shadowed away.
|
|
120
|
+
const partial: NodeJS.Process = Object.create(process);
|
|
121
|
+
Reflect.defineProperty(partial, "env", {
|
|
122
|
+
value: undefined,
|
|
123
|
+
configurable: true,
|
|
124
|
+
});
|
|
125
|
+
mockProcess(partial);
|
|
126
|
+
|
|
127
|
+
const { i18nSharedOptions } = await import("../i18SharedOptions.js");
|
|
128
|
+
expect(i18nSharedOptions.debug).toBe(false);
|
|
129
|
+
expect(i18nSharedOptions.fallbackLng).toBe("en");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("still reads the log level when process is present", async () => {
|
|
133
|
+
process.env.PROSOPO_LOG_LEVEL = "debug";
|
|
134
|
+
mockProcess(process);
|
|
135
|
+
const { i18nSharedOptions } = await import("../i18SharedOptions.js");
|
|
136
|
+
expect(i18nSharedOptions.debug).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|