@gusnips/react 0.4.3 → 0.4.5
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/dist/hydrate.d.ts +11 -1
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +17 -1
- package/dist/hydrate.js.map +1 -1
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +7 -5
- package/dist/i18n.js.map +1 -1
- package/package.json +1 -1
- package/src/hydrate.test.ts +65 -0
- package/src/hydrate.ts +19 -2
- package/src/i18n.test.ts +29 -11
- package/src/i18n.ts +7 -5
package/dist/hydrate.d.ts
CHANGED
|
@@ -30,6 +30,16 @@ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.ts";
|
|
|
30
30
|
* serves `/pt/pricing` from a file rendered for `/pricing`, with the locale carried in the
|
|
31
31
|
* router's `basename` — so the address and the route are different strings, and only the caller
|
|
32
32
|
* knows which one the build wrote.
|
|
33
|
+
*
|
|
34
|
+
* **A trailing slash is not a different page.** The router renders `/precos/` and `/precos` alike,
|
|
35
|
+
* and which one is in the address bar is the host's call: a host serving `precos/index.html`
|
|
36
|
+
* redirects `/precos` to `/precos/` by default (Firebase Hosting does). Compared exactly, that was
|
|
37
|
+
* false on every page of one adopter: every prerendered body was thrown away and drawn again, on a
|
|
38
|
+
* page that looks the same either way.
|
|
39
|
+
*
|
|
40
|
+
* **`null` means nothing the build wrote can be this reader's page.** The case is a site that
|
|
41
|
+
* prerenders one language and detects the reader's at runtime: Portuguese markup hydrated with
|
|
42
|
+
* Spanish strings is the wrong-page mismatch again, spread across every string on the page.
|
|
33
43
|
*/
|
|
34
|
-
export declare function hydrateOrMount(rootEl: Element, tree: ReactNode, route: string): void;
|
|
44
|
+
export declare function hydrateOrMount(rootEl: Element, tree: ReactNode, route: string | null): void;
|
|
35
45
|
//# sourceMappingURL=hydrate.d.ts.map
|
package/dist/hydrate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E
|
|
1
|
+
{"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAI3F"}
|
package/dist/hydrate.js
CHANGED
|
@@ -15,11 +15,27 @@ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.js";
|
|
|
15
15
|
* serves `/pt/pricing` from a file rendered for `/pricing`, with the locale carried in the
|
|
16
16
|
* router's `basename` — so the address and the route are different strings, and only the caller
|
|
17
17
|
* knows which one the build wrote.
|
|
18
|
+
*
|
|
19
|
+
* **A trailing slash is not a different page.** The router renders `/precos/` and `/precos` alike,
|
|
20
|
+
* and which one is in the address bar is the host's call: a host serving `precos/index.html`
|
|
21
|
+
* redirects `/precos` to `/precos/` by default (Firebase Hosting does). Compared exactly, that was
|
|
22
|
+
* false on every page of one adopter: every prerendered body was thrown away and drawn again, on a
|
|
23
|
+
* page that looks the same either way.
|
|
24
|
+
*
|
|
25
|
+
* **`null` means nothing the build wrote can be this reader's page.** The case is a site that
|
|
26
|
+
* prerenders one language and detects the reader's at runtime: Portuguese markup hydrated with
|
|
27
|
+
* Spanish strings is the wrong-page mismatch again, spread across every string on the page.
|
|
18
28
|
*/
|
|
19
29
|
export function hydrateOrMount(rootEl, tree, route) {
|
|
20
|
-
|
|
30
|
+
const rendered = rootEl.getAttribute(PRERENDERED_ROUTE_ATTR);
|
|
31
|
+
if (route !== null && rendered !== null && samePath(rendered, route))
|
|
21
32
|
hydrateRoot(rootEl, tree);
|
|
22
33
|
else
|
|
23
34
|
createRoot(rootEl).render(tree);
|
|
24
35
|
}
|
|
36
|
+
/** `/precos/` and `/precos` are one page; `/` stays `/`. */
|
|
37
|
+
function samePath(a, b) {
|
|
38
|
+
const trim = (path) => path.replace(/(.)\/+$/, "$1");
|
|
39
|
+
return trim(a) === trim(b);
|
|
40
|
+
}
|
|
25
41
|
//# sourceMappingURL=hydrate.js.map
|
package/dist/hydrate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hydrate.js","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E
|
|
1
|
+
{"version":3,"file":"hydrate.js","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,cAAc,CAAC,MAAe,EAAE,IAAe,EAAE,KAAoB;IACnF,MAAM,QAAQ,GAAG,MAAM,CAAC,YAAY,CAAC,sBAAsB,CAAC,CAAC;IAC7D,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;QAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;;QAC3F,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,4DAA4D;AAC5D,SAAS,QAAQ,CAAC,CAAS,EAAE,CAAS;IACpC,MAAM,IAAI,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACrE,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC;AAC7B,CAAC"}
|
package/dist/i18n.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,aAAa,EACb,UAAU,EACV,QAAQ,GACT,EAAE,eAAe;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,aAAa,EACb,UAAU,EACV,QAAQ,GACT,EAAE,eAAe;;;;;;;;;;;;;;;;EAuBjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAmB/E"}
|
package/dist/i18n.js
CHANGED
|
@@ -26,11 +26,13 @@ export function i18nInitOptions({ fallbackLng, supportedLngs, storageKey, queryK
|
|
|
26
26
|
lookupLocalStorage: storageKey,
|
|
27
27
|
},
|
|
28
28
|
react: { useSuspense: false },
|
|
29
|
-
// `nonExplicitSupportedLngs` is omitted deliberately
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
29
|
+
// `nonExplicitSupportedLngs` is omitted deliberately. It breaks both shapes a catalog comes in.
|
|
30
|
+
// Region-coded: it validates the BASE subtag against `supportedLngs`, "pt" is not in a list
|
|
31
|
+
// that says "pt-BR", and the whole app silently renders English — both donors say so in the
|
|
32
|
+
// same words. Base codes: an "en-US" browser keeps "en-US" as `i18n.language`. Beside
|
|
33
|
+
// `currentOnly` no catalog matches it, so the reader gets the fallback language; beside
|
|
34
|
+
// `languageOnly` the copy is right and every map an app keys by the tag misses — the third
|
|
35
|
+
// migration shipped English copy as `lang="pt-BR"`. Without the flag "en-US" settles on "en".
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
/**
|
package/dist/i18n.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA4BH,MAAM,UAAU,eAAe,CAAC,EAC9B,WAAW,EACX,aAAa,EACb,UAAU,EACV,QAAQ,GACQ;IAChB,OAAO;QACL,WAAW;QACX,aAAa,EAAE,CAAC,GAAG,aAAa,CAAC;QACjC,IAAI,EAAE,aAAsB;QAC5B,aAAa,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,yBAAyB;QAChE,SAAS,EAAE;YACT,KAAK,EAAE,QAAQ;gBACb,CAAC,CAAC,CAAC,aAAa,EAAE,cAAc,EAAE,WAAW,CAAC;gBAC9C,CAAC,CAAC,CAAC,cAAc,EAAE,WAAW,CAAC;YACjC,MAAM,EAAE,CAAC,cAAc,CAAC;YACxB,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACpD,kBAAkB,EAAE,UAAU;SAC/B;QACD,KAAK,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE;QAC7B,
|
|
1
|
+
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA4BH,MAAM,UAAU,eAAe,CAAC,EAC9B,WAAW,EACX,aAAa,EACb,UAAU,EACV,QAAQ,GACQ;IAChB,OAAO;QACL,WAAW;QACX,aAAa,EAAE,CAAC,GAAG,aAAa,CAAC;QACjC,IAAI,EAAE,aAAsB;QAC5B,aAAa,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,yBAAyB;QAChE,SAAS,EAAE;YACT,KAAK,EAAE,QAAQ;gBACb,CAAC,CAAC,CAAC,aAAa,EAAE,cAAc,EAAE,WAAW,CAAC;gBAC9C,CAAC,CAAC,CAAC,cAAc,EAAE,WAAW,CAAC;YACjC,MAAM,EAAE,CAAC,cAAc,CAAC;YACxB,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACpD,kBAAkB,EAAE,UAAU;SAC/B;QACD,KAAK,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE;QAC7B,gGAAgG;QAChG,4FAA4F;QAC5F,4FAA4F;QAC5F,sFAAsF;QACtF,wFAAwF;QACxF,2FAA2F;QAC3F,8FAA8F;KAC/F,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAI,SAAY,EAAE,IAA4B;IAC1E,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,MAAM,CACxB,UAAU,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EACvF,GAAG,CACJ,CAAC;IAEF,MAAM,IAAI,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAE9F,MAAM,IAAI,GAAG,CAAC,KAAc,EAAW,EAAE;QACvC,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;QAClD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;YACpC,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACjF,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,OAAO,IAAI,CAAC,SAAS,CAAM,CAAC;AAC9B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gusnips/react",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "The headless runtime under a Vite + React SPA: typed fetch client with single-flight refresh, auth store, query defaults, route guards, error boundary, chunk-reload recovery, i18n bootstrap.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { hydrateOrMount, SHELL_ROUTE } from "./hydrate.ts";
|
|
3
|
+
|
|
4
|
+
// Both roots are mocked: the question is only WHICH one the entry picks, and either choice is
|
|
5
|
+
// silent in a real browser — a wrong mount looks like a hydrate and a wrong hydrate recovers.
|
|
6
|
+
const roots = vi.hoisted(() => ({
|
|
7
|
+
hydrateRoot: vi.fn(),
|
|
8
|
+
createRoot: vi.fn(() => ({ render: vi.fn() })),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("react-dom/client", () => roots);
|
|
11
|
+
|
|
12
|
+
/** A root whose file names `route` — or names nothing, for `null`. */
|
|
13
|
+
const rootFor = (route: string | null): Element =>
|
|
14
|
+
({ getAttribute: () => route }) as unknown as Element; // Only `getAttribute` is read.
|
|
15
|
+
|
|
16
|
+
const picked = (): "hydrate" | "mount" => {
|
|
17
|
+
const hydrated = roots.hydrateRoot.mock.calls.length > 0;
|
|
18
|
+
const mounted = roots.createRoot.mock.calls.length > 0;
|
|
19
|
+
expect(hydrated !== mounted).toBe(true);
|
|
20
|
+
return hydrated ? "hydrate" : "mount";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("hydrateOrMount", () => {
|
|
28
|
+
it("hydrates the render this file is", () => {
|
|
29
|
+
hydrateOrMount(rootFor("/precos"), null, "/precos");
|
|
30
|
+
expect(picked()).toBe("hydrate");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.each([
|
|
34
|
+
["the address has one", "/precos", "/precos/"],
|
|
35
|
+
["the file has one", "/precos/", "/precos"],
|
|
36
|
+
])("ignores a trailing slash when %s", (_case, rendered, route) => {
|
|
37
|
+
hydrateOrMount(rootFor(rendered), null, route);
|
|
38
|
+
expect(picked()).toBe("hydrate");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("keeps the front page's slash — it is the whole path", () => {
|
|
42
|
+
hydrateOrMount(rootFor("/"), null, "/");
|
|
43
|
+
expect(picked()).toBe("hydrate");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("mounts fresh over a different page", () => {
|
|
47
|
+
hydrateOrMount(rootFor("/precos"), null, "/faq");
|
|
48
|
+
expect(picked()).toBe("mount");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("mounts fresh over a shell, whatever the address", () => {
|
|
52
|
+
hydrateOrMount(rootFor(SHELL_ROUTE), null, "/nao-existe");
|
|
53
|
+
expect(picked()).toBe("mount");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("mounts fresh over a file that names no route", () => {
|
|
57
|
+
hydrateOrMount(rootFor(null), null, "/precos");
|
|
58
|
+
expect(picked()).toBe("mount");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("mounts fresh when the reader cannot be shown this file at all", () => {
|
|
62
|
+
hydrateOrMount(rootFor("/precos"), null, null);
|
|
63
|
+
expect(picked()).toBe("mount");
|
|
64
|
+
});
|
|
65
|
+
});
|
package/src/hydrate.ts
CHANGED
|
@@ -34,8 +34,25 @@ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.ts";
|
|
|
34
34
|
* serves `/pt/pricing` from a file rendered for `/pricing`, with the locale carried in the
|
|
35
35
|
* router's `basename` — so the address and the route are different strings, and only the caller
|
|
36
36
|
* knows which one the build wrote.
|
|
37
|
+
*
|
|
38
|
+
* **A trailing slash is not a different page.** The router renders `/precos/` and `/precos` alike,
|
|
39
|
+
* and which one is in the address bar is the host's call: a host serving `precos/index.html`
|
|
40
|
+
* redirects `/precos` to `/precos/` by default (Firebase Hosting does). Compared exactly, that was
|
|
41
|
+
* false on every page of one adopter: every prerendered body was thrown away and drawn again, on a
|
|
42
|
+
* page that looks the same either way.
|
|
43
|
+
*
|
|
44
|
+
* **`null` means nothing the build wrote can be this reader's page.** The case is a site that
|
|
45
|
+
* prerenders one language and detects the reader's at runtime: Portuguese markup hydrated with
|
|
46
|
+
* Spanish strings is the wrong-page mismatch again, spread across every string on the page.
|
|
37
47
|
*/
|
|
38
|
-
export function hydrateOrMount(rootEl: Element, tree: ReactNode, route: string): void {
|
|
39
|
-
|
|
48
|
+
export function hydrateOrMount(rootEl: Element, tree: ReactNode, route: string | null): void {
|
|
49
|
+
const rendered = rootEl.getAttribute(PRERENDERED_ROUTE_ATTR);
|
|
50
|
+
if (route !== null && rendered !== null && samePath(rendered, route)) hydrateRoot(rootEl, tree);
|
|
40
51
|
else createRoot(rootEl).render(tree);
|
|
41
52
|
}
|
|
53
|
+
|
|
54
|
+
/** `/precos/` and `/precos` are one page; `/` stays `/`. */
|
|
55
|
+
function samePath(a: string, b: string): boolean {
|
|
56
|
+
const trim = (path: string): string => path.replace(/(.)\/+$/, "$1");
|
|
57
|
+
return trim(a) === trim(b);
|
|
58
|
+
}
|
package/src/i18n.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { createInstance } from "i18next";
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { applyBrandVars, i18nInitOptions } from "./i18n.ts";
|
|
3
|
+
import { applyBrandVars, i18nInitOptions, type I18nInitOptions } from "./i18n.ts";
|
|
3
4
|
|
|
4
5
|
describe("applyBrandVars", () => {
|
|
5
6
|
it("fills placeholders anywhere in the tree and leaves the input alone", () => {
|
|
@@ -27,17 +28,34 @@ describe("applyBrandVars", () => {
|
|
|
27
28
|
});
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
/** For a browser saying `lng`: the app's `i18n.language`, and the catalog that answers. */
|
|
32
|
+
async function settle(
|
|
33
|
+
options: Omit<I18nInitOptions, "storageKey">,
|
|
34
|
+
lng: string,
|
|
35
|
+
): Promise<[language: string, catalog: string]> {
|
|
36
|
+
const i18n = createInstance();
|
|
37
|
+
const resources = Object.fromEntries(
|
|
38
|
+
options.supportedLngs.map((tag) => [tag, { translation: { tag } }]),
|
|
39
|
+
);
|
|
40
|
+
await i18n.init({ ...i18nInitOptions({ ...options, storageKey: "app.locale" }), resources, lng });
|
|
41
|
+
return [i18n.language, i18n.t("tag")];
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
describe("i18nInitOptions", () => {
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expect(
|
|
45
|
+
// `nonExplicitSupportedLngs` breaks both catalog shapes, and this pins both. Region-coded, the
|
|
46
|
+
// app renders English for a pt-BR reader — the bug both donors' comments name. Base codes, an
|
|
47
|
+
// "en-US" browser keeps "en-US" as `i18n.language`, which is what an app keys `<html lang>` and
|
|
48
|
+
// `og:locale` by: the third migration's site missed its lookup and shipped English copy as
|
|
49
|
+
// `lang="pt-BR"`.
|
|
50
|
+
it("settles a browser's regional tag on a language the catalogs have", async () => {
|
|
51
|
+
const baseCodes = { fallbackLng: "pt", supportedLngs: ["pt", "es", "en"] };
|
|
52
|
+
expect(await settle(baseCodes, "en-US")).toEqual(["en", "en"]);
|
|
53
|
+
expect(await settle(baseCodes, "es-419")).toEqual(["es", "es"]);
|
|
54
|
+
expect(await settle(baseCodes, "fr-FR")).toEqual(["pt", "pt"]);
|
|
55
|
+
|
|
56
|
+
const regionCoded = { fallbackLng: "en", supportedLngs: ["en", "pt-BR"] };
|
|
57
|
+
expect(await settle(regionCoded, "pt-BR")).toEqual(["pt-BR", "pt-BR"]);
|
|
58
|
+
expect(await settle(regionCoded, "en-GB")).toEqual(["en", "en"]);
|
|
41
59
|
});
|
|
42
60
|
|
|
43
61
|
// Only one donor had the query parameter, and it is the only detection source somebody chose
|
package/src/i18n.ts
CHANGED
|
@@ -58,11 +58,13 @@ export function i18nInitOptions({
|
|
|
58
58
|
lookupLocalStorage: storageKey,
|
|
59
59
|
},
|
|
60
60
|
react: { useSuspense: false },
|
|
61
|
-
// `nonExplicitSupportedLngs` is omitted deliberately
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
61
|
+
// `nonExplicitSupportedLngs` is omitted deliberately. It breaks both shapes a catalog comes in.
|
|
62
|
+
// Region-coded: it validates the BASE subtag against `supportedLngs`, "pt" is not in a list
|
|
63
|
+
// that says "pt-BR", and the whole app silently renders English — both donors say so in the
|
|
64
|
+
// same words. Base codes: an "en-US" browser keeps "en-US" as `i18n.language`. Beside
|
|
65
|
+
// `currentOnly` no catalog matches it, so the reader gets the fallback language; beside
|
|
66
|
+
// `languageOnly` the copy is right and every map an app keys by the tag misses — the third
|
|
67
|
+
// migration shipped English copy as `lang="pt-BR"`. Without the flag "en-US" settles on "en".
|
|
66
68
|
};
|
|
67
69
|
}
|
|
68
70
|
|