i18n-keyless-react 2.3.0 → 2.3.2

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.
@@ -7,23 +7,45 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
7
7
  return path;
8
8
  };
9
9
  // A single AsyncLocalStorage instance, created lazily on the server only. Each
10
- // `run()` creates an isolated store, so the instance is correctly a module singleton.
11
- let als;
12
- let alsInit;
10
+ // `run()` creates an isolated store, so the instance is correctly a process singleton.
11
+ //
12
+ // CRITICAL — two layered problems, one solution:
13
+ //
14
+ // 1. A module-local `als` is duplicated when a bundler instantiates this package more than
15
+ // once (e.g. TanStack Start / Vite SSR build a separate server-entry and SSR-render
16
+ // environment). `runWithI18nKeyless` (server entry) would set `als` on copy A while
17
+ // `getRequestScope`/`recordUsedKey` (React render) read copy B's `als` — `undefined` —
18
+ // so SSR silently falls back to the primary language with a hydration mismatch.
19
+ //
20
+ // 2. Storing the instance on `globalThis` fixes (1) ONLY if the key is realm-independent.
21
+ // `Symbol.for()`'s registry is PER-REALM: TanStack Start / Vite SSR run the server entry
22
+ // and the React render in two different V8 realms that SHARE the same `globalThis` object
23
+ // but each resolve `Symbol.for("x")` to a DIFFERENT symbol — so the write-side and
24
+ // read-side miss each other even through one globalThis. (Confirmed empirically: same
25
+ // globalThis on both sides, yet getAls() returned the ALS on the server entry and
26
+ // `undefined` in the render.) A plain STRING key is realm-independent and bridges them.
27
+ //
28
+ // So: route every read and write through one `globalThis` slot keyed by a plain string.
29
+ const ALS_KEY = "__i18n_keyless_als__";
30
+ const ALS_INIT_KEY = "__i18n_keyless_alsInit__";
31
+ const registry = globalThis;
32
+ function getAls() {
33
+ return registry[ALS_KEY];
34
+ }
13
35
  async function ensureALS() {
14
36
  // Browser: no request scoping (single user → the store is correct). Also avoids
15
37
  // pulling a Node builtin into client bundles.
16
- if (als || typeof window !== "undefined") {
38
+ if (registry[ALS_KEY] || typeof window !== "undefined") {
17
39
  return;
18
40
  }
19
- if (!alsInit) {
20
- alsInit = (async () => {
41
+ if (!registry[ALS_INIT_KEY]) {
42
+ registry[ALS_INIT_KEY] = (async () => {
21
43
  try {
22
44
  // Variable specifier + ignore hints keep bundlers from trying to resolve a Node
23
45
  // builtin into the browser graph. tsc keeps these comments (removeComments=false).
24
46
  const specifier = "node:async_hooks";
25
47
  const mod = (await import(__rewriteRelativeImportExtension(/* @vite-ignore */ /* webpackIgnore: true */ specifier)));
26
- als = new mod.AsyncLocalStorage();
48
+ registry[ALS_KEY] = new mod.AsyncLocalStorage();
27
49
  }
28
50
  catch {
29
51
  // AsyncLocalStorage unavailable (e.g. some edge runtimes). Scoping degrades to a
@@ -31,7 +53,7 @@ async function ensureALS() {
31
53
  }
32
54
  })();
33
55
  }
34
- await alsInit;
56
+ await registry[ALS_INIT_KEY];
35
57
  }
36
58
  /**
37
59
  * Runs `fn` with a per-request translation scope active. Every `getTranslation(...)`
@@ -51,6 +73,7 @@ async function ensureALS() {
51
73
  */
52
74
  export async function runWithI18nKeyless(scope, fn) {
53
75
  await ensureALS();
76
+ const als = getAls();
54
77
  if (!als) {
55
78
  return fn();
56
79
  }
@@ -64,7 +87,7 @@ export async function runWithI18nKeyless(scope, fn) {
64
87
  * exported for advanced/server use.
65
88
  */
66
89
  export function getRequestScope() {
67
- const s = als?.getStore();
90
+ const s = getAls()?.getStore();
68
91
  return s ? { lang: s.lang, translations: s.translations } : undefined;
69
92
  }
70
93
  /**
@@ -74,7 +97,7 @@ export function getRequestScope() {
74
97
  * `<I18nKeylessText>`.
75
98
  */
76
99
  export function recordUsedKey(key) {
77
- als?.getStore()?.used.add(key);
100
+ getAls()?.getStore()?.used.add(key);
78
101
  }
79
102
  /**
80
103
  * Returns a snapshot containing ONLY the keys used during the current render (∩ the keys
@@ -87,7 +110,7 @@ export function recordUsedKey(key) {
87
110
  * client-side navigation has every key. Returns `undefined` outside a scope. See docs/SSR.md.
88
111
  */
89
112
  export function getUsedTranslationsSnapshot() {
90
- const s = als?.getStore();
113
+ const s = getAls()?.getStore();
91
114
  if (!s) {
92
115
  return undefined;
93
116
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "i18n-keyless-react",
3
3
  "private": false,
4
- "version": "2.3.0",
4
+ "version": "2.3.2",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -24,7 +24,7 @@
24
24
  "postpublish": "rm -rf ./dist && rm *.tgz"
25
25
  },
26
26
  "dependencies": {
27
- "i18n-keyless-core": "2.3.0",
27
+ "i18n-keyless-core": "2.3.2",
28
28
  "zustand": "^5.0.3"
29
29
  },
30
30
  "peerDependencies": {