@prosopo/locale 3.2.9 → 3.3.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 (93) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +10 -9
  2. package/.turbo/turbo-build$colon$tsc.log +5 -5
  3. package/.turbo/turbo-build.log +17 -16
  4. package/CHANGELOG.md +24 -0
  5. package/dist/cjs/i18SharedOptions.cjs +1 -1
  6. package/dist/cjs/i18nBackend.cjs +4 -1
  7. package/dist/cjs/i18nFrontend.cjs +4 -1
  8. package/dist/cjs/loadI18next.cjs +41 -14
  9. package/dist/cjs/process.cjs +17 -0
  10. package/dist/cjs/translationKey.cjs +2 -0
  11. package/dist/i18SharedOptions.js +2 -1
  12. package/dist/i18SharedOptions.js.map +1 -1
  13. package/dist/i18nBackend.d.ts.map +1 -1
  14. package/dist/i18nBackend.js +4 -1
  15. package/dist/i18nBackend.js.map +1 -1
  16. package/dist/i18nFrontend.d.ts.map +1 -1
  17. package/dist/i18nFrontend.js +4 -1
  18. package/dist/i18nFrontend.js.map +1 -1
  19. package/dist/loadI18next.d.ts +1 -0
  20. package/dist/loadI18next.d.ts.map +1 -1
  21. package/dist/loadI18next.js +41 -15
  22. package/dist/loadI18next.js.map +1 -1
  23. package/dist/process.d.ts +2 -0
  24. package/dist/process.d.ts.map +1 -0
  25. package/dist/process.js +17 -0
  26. package/dist/process.js.map +1 -0
  27. package/dist/tests/i18SharedOptions.unit.test.d.ts +2 -0
  28. package/dist/tests/i18SharedOptions.unit.test.d.ts.map +1 -0
  29. package/dist/tests/i18SharedOptions.unit.test.js +91 -0
  30. package/dist/tests/i18SharedOptions.unit.test.js.map +1 -0
  31. package/dist/tests/i18nBackend.unit.test.d.ts +2 -0
  32. package/dist/tests/i18nBackend.unit.test.d.ts.map +1 -0
  33. package/dist/tests/i18nBackend.unit.test.js +153 -0
  34. package/dist/tests/i18nBackend.unit.test.js.map +1 -0
  35. package/dist/tests/i18nFrontend.unit.test.d.ts +2 -0
  36. package/dist/tests/i18nFrontend.unit.test.d.ts.map +1 -0
  37. package/dist/tests/i18nFrontend.unit.test.js +199 -0
  38. package/dist/tests/i18nFrontend.unit.test.js.map +1 -0
  39. package/dist/tests/i18nMiddleware.unit.test.d.ts +2 -0
  40. package/dist/tests/i18nMiddleware.unit.test.d.ts.map +1 -0
  41. package/dist/tests/i18nMiddleware.unit.test.js +80 -0
  42. package/dist/tests/i18nMiddleware.unit.test.js.map +1 -0
  43. package/dist/tests/loadI18next.unit.test.d.ts +2 -0
  44. package/dist/tests/loadI18next.unit.test.d.ts.map +1 -0
  45. package/dist/tests/loadI18next.unit.test.js +270 -0
  46. package/dist/tests/loadI18next.unit.test.js.map +1 -0
  47. package/dist/tests/locale.test-d.d.ts +2 -0
  48. package/dist/tests/locale.test-d.d.ts.map +1 -0
  49. package/dist/tests/locale.test-d.js +88 -0
  50. package/dist/tests/locale.test-d.js.map +1 -0
  51. package/dist/tests/process.unit.test.d.ts +2 -0
  52. package/dist/tests/process.unit.test.d.ts.map +1 -0
  53. package/dist/tests/process.unit.test.js +14 -0
  54. package/dist/tests/process.unit.test.js.map +1 -0
  55. package/dist/tests/translationKey.unit.test.d.ts +2 -0
  56. package/dist/tests/translationKey.unit.test.d.ts.map +1 -0
  57. package/dist/tests/translationKey.unit.test.js +108 -0
  58. package/dist/tests/translationKey.unit.test.js.map +1 -0
  59. package/dist/tests/translations.unit.test.d.ts +2 -0
  60. package/dist/tests/translations.unit.test.d.ts.map +1 -0
  61. package/dist/tests/translations.unit.test.js +74 -0
  62. package/dist/tests/translations.unit.test.js.map +1 -0
  63. package/dist/tests/useTranslation.unit.test.d.ts +2 -0
  64. package/dist/tests/useTranslation.unit.test.d.ts.map +1 -0
  65. package/dist/tests/useTranslation.unit.test.js +81 -0
  66. package/dist/tests/useTranslation.unit.test.js.map +1 -0
  67. package/dist/tests/util.unit.test.d.ts +2 -0
  68. package/dist/tests/util.unit.test.d.ts.map +1 -0
  69. package/dist/tests/util.unit.test.js +66 -0
  70. package/dist/tests/util.unit.test.js.map +1 -0
  71. package/dist/translationKey.d.ts +4 -0
  72. package/dist/translationKey.d.ts.map +1 -1
  73. package/dist/translationKey.js +2 -1
  74. package/dist/translationKey.js.map +1 -1
  75. package/package.json +1 -1
  76. package/src/i18SharedOptions.ts +4 -4
  77. package/src/i18nBackend.ts +16 -0
  78. package/src/i18nFrontend.ts +16 -0
  79. package/src/loadI18next.ts +83 -15
  80. package/src/process.ts +29 -0
  81. package/src/tests/i18SharedOptions.unit.test.ts +139 -0
  82. package/src/tests/i18nBackend.unit.test.ts +241 -0
  83. package/src/tests/i18nFrontend.unit.test.ts +302 -0
  84. package/src/tests/i18nMiddleware.unit.test.ts +140 -0
  85. package/src/tests/loadI18next.unit.test.ts +443 -0
  86. package/src/tests/locale.test-d.ts +156 -0
  87. package/src/tests/process.unit.test.ts +33 -0
  88. package/src/tests/translationKey.unit.test.ts +166 -0
  89. package/src/tests/translations.unit.test.ts +110 -0
  90. package/src/tests/useTranslation.unit.test.ts +134 -0
  91. package/src/tests/util.unit.test.ts +102 -0
  92. package/src/translationKey.ts +18 -3
  93. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useTranslation.unit.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=util.unit.test.d.ts.map
@@ -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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"}
@@ -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;AA6BxB,eAAO,MAAM,qBAAqB,kCAEjC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,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"}
@@ -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;AAQhF,SAAS,gBAAgB,CAAC,GAAS;IAClC,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;QACD,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"}
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.2.9",
3
+ "version": "3.3.0",
4
4
  "description": "Prosopo locale library",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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
- // Guard the `process.env` read so the module is loadable in a plain browser
17
- // runtime where `process` is undefined. Without the guard, any side-effectful
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",
@@ -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
  }
@@ -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
  }
@@ -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
- try {
31
- if (backend) {
32
- import("./i18nBackend.js").then(({ default: initializeI18n }) => {
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 will resolve after i18n connected fires
35
- i18nInstance = initializeI18n(resolve);
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
- resolve(i18nInstance);
104
+ done(i18nInstance);
39
105
  }
40
- });
41
- } else {
42
- import("./i18nFrontend.js").then(({ default: initializeI18n }) => {
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(resolve);
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).then(resolve);
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
+ });