@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.102 → 3.5.0-ultramodern.103

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.
@@ -32,7 +32,6 @@ __webpack_require__.d(__webpack_exports__, {
32
32
  });
33
33
  const runtime_namespaceObject = require("@modern-js/runtime");
34
34
  const context_namespaceObject = require("@modern-js/runtime/context");
35
- const router_namespaceObject = require("@modern-js/runtime/router");
36
35
  const external_react_namespaceObject = require("react");
37
36
  const normalizeUrlPart = (value, prefix)=>{
38
37
  if ('string' != typeof value || !value) return '';
@@ -85,34 +84,30 @@ const getRouterParams = (internalContext, contextRouter)=>{
85
84
  const getRouterSnapshot = (internalContext, contextRouter)=>{
86
85
  const location = getRouterStateLocation(internalContext, contextRouter);
87
86
  const params = getRouterParams(internalContext, contextRouter);
88
- const serializedParams = Object.entries(params).sort(([left], [right])=>left.localeCompare(right)).map(([key, value])=>`${key}=${value}`).join('&');
89
- return `${location?.pathname ?? ''}\u0000${location?.search ?? ''}\u0000${location?.hash ?? ''}\u0000${serializedParams}`;
87
+ return JSON.stringify([
88
+ location?.pathname ?? '',
89
+ location?.search ?? '',
90
+ location?.hash ?? '',
91
+ Object.entries(params).sort(([left], [right])=>left.localeCompare(right))
92
+ ]);
90
93
  };
91
94
  const useI18nRouterAdapter = ()=>{
92
95
  const runtimeContext = (0, external_react_namespaceObject.useContext)(runtime_namespaceObject.RuntimeContext);
93
96
  const internalContext = (0, external_react_namespaceObject.useContext)(context_namespaceObject.InternalRuntimeContext);
94
- const inReactRouter = (0, router_namespaceObject.useInRouterContext)();
95
- const reactRouterNavigate = inReactRouter ? (0, router_namespaceObject.useNavigate)() : null;
96
- const reactRouterLocation = inReactRouter ? (0, router_namespaceObject.useLocation)() : null;
97
- const reactRouterParams = inReactRouter ? (0, router_namespaceObject.useParams)() : {};
97
+ const routerApi = internalContext.router || runtimeContext.router;
98
+ const inReactRouter = routerApi?.useInRouterContext?.() ?? false;
99
+ const reactRouterNavigate = inReactRouter ? routerApi?.useNavigate?.() ?? null : null;
100
+ const reactRouterLocation = inReactRouter ? routerApi?.useLocation?.() ?? null : null;
101
+ const reactRouterParams = inReactRouter ? routerApi?.useParams?.() ?? {} : {};
98
102
  const framework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
99
103
  const contextUseRouter = inReactRouter || 'tanstack' !== framework ? void 0 : internalContext.router?.useRouter || runtimeContext.router?.useRouter;
100
104
  const contextRouter = contextUseRouter ? contextUseRouter({
101
105
  warn: false
102
106
  }) : null;
103
- const [, setRouterVersion] = (0, external_react_namespaceObject.useState)(0);
104
- const routerSnapshotRef = (0, external_react_namespaceObject.useRef)(getRouterSnapshot(internalContext, contextRouter));
105
- routerSnapshotRef.current = getRouterSnapshot(internalContext, contextRouter);
106
107
  const hasRouter = 'tanstack' === framework || 'react-router' === framework || Boolean(reactRouterNavigate);
107
- (0, external_react_namespaceObject.useEffect)(()=>{
108
+ const subscribeToRouter = (0, external_react_namespaceObject.useCallback)((update)=>{
108
109
  const router = getRouterInstance(internalContext, contextRouter);
109
- if (!router) return;
110
- const update = ()=>{
111
- const nextSnapshot = getRouterSnapshot(internalContext, contextRouter);
112
- if (nextSnapshot === routerSnapshotRef.current) return;
113
- routerSnapshotRef.current = nextSnapshot;
114
- setRouterVersion((version)=>version + 1);
115
- };
110
+ if (!router) return ()=>void 0;
116
111
  const unsubscribers = [];
117
112
  if ('react-router' === framework && !inReactRouter && 'function' == typeof router.subscribe) {
118
113
  const subscribe = router.subscribe;
@@ -142,6 +137,11 @@ const useI18nRouterAdapter = ()=>{
142
137
  inReactRouter,
143
138
  internalContext
144
139
  ]);
140
+ const getSnapshot = (0, external_react_namespaceObject.useCallback)(()=>getRouterSnapshot(internalContext, contextRouter), [
141
+ contextRouter,
142
+ internalContext
143
+ ]);
144
+ (0, external_react_namespaceObject.useSyncExternalStore)(subscribeToRouter, getSnapshot, getSnapshot);
145
145
  const navigate = (0, external_react_namespaceObject.useCallback)((href, options)=>{
146
146
  const router = getRouterInstance(internalContext, contextRouter);
147
147
  const activeFramework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
@@ -169,7 +169,7 @@ const useI18nRouterAdapter = ()=>{
169
169
  ]);
170
170
  const location = (reactRouterLocation ? normalizeLocation(reactRouterLocation) : getRouterStateLocation(internalContext, contextRouter)) || getWindowLocation();
171
171
  const params = inReactRouter ? reactRouterParams : getRouterParams(internalContext, contextRouter);
172
- const Link = 'tanstack' === framework ? internalContext.router?.Link || runtimeContext.router?.Link || null : 'react-router' === framework || inReactRouter ? router_namespaceObject.Link : null;
172
+ const Link = 'tanstack' === framework ? internalContext.router?.Link || runtimeContext.router?.Link || null : 'react-router' === framework || inReactRouter ? routerApi?.Link ?? null : null;
173
173
  return {
174
174
  framework,
175
175
  hasRouter,
@@ -1,7 +1,6 @@
1
1
  import { RuntimeContext, isBrowser } from "@modern-js/runtime";
2
2
  import { InternalRuntimeContext, getRouterRuntimeState } from "@modern-js/runtime/context";
3
- import { Link as router_Link, useInRouterContext, useLocation, useNavigate, useParams } from "@modern-js/runtime/router";
4
- import { useCallback, useContext, useEffect, useRef, useState } from "react";
3
+ import { useCallback, useContext, useSyncExternalStore } from "react";
5
4
  const normalizeUrlPart = (value, prefix)=>{
6
5
  if ('string' != typeof value || !value) return '';
7
6
  return value.startsWith(prefix) ? value : `${prefix}${value}`;
@@ -53,34 +52,30 @@ const getRouterParams = (internalContext, contextRouter)=>{
53
52
  const getRouterSnapshot = (internalContext, contextRouter)=>{
54
53
  const location = getRouterStateLocation(internalContext, contextRouter);
55
54
  const params = getRouterParams(internalContext, contextRouter);
56
- const serializedParams = Object.entries(params).sort(([left], [right])=>left.localeCompare(right)).map(([key, value])=>`${key}=${value}`).join('&');
57
- return `${location?.pathname ?? ''}\u0000${location?.search ?? ''}\u0000${location?.hash ?? ''}\u0000${serializedParams}`;
55
+ return JSON.stringify([
56
+ location?.pathname ?? '',
57
+ location?.search ?? '',
58
+ location?.hash ?? '',
59
+ Object.entries(params).sort(([left], [right])=>left.localeCompare(right))
60
+ ]);
58
61
  };
59
62
  const useI18nRouterAdapter = ()=>{
60
63
  const runtimeContext = useContext(RuntimeContext);
61
64
  const internalContext = useContext(InternalRuntimeContext);
62
- const inReactRouter = useInRouterContext();
63
- const reactRouterNavigate = inReactRouter ? useNavigate() : null;
64
- const reactRouterLocation = inReactRouter ? useLocation() : null;
65
- const reactRouterParams = inReactRouter ? useParams() : {};
65
+ const routerApi = internalContext.router || runtimeContext.router;
66
+ const inReactRouter = routerApi?.useInRouterContext?.() ?? false;
67
+ const reactRouterNavigate = inReactRouter ? routerApi?.useNavigate?.() ?? null : null;
68
+ const reactRouterLocation = inReactRouter ? routerApi?.useLocation?.() ?? null : null;
69
+ const reactRouterParams = inReactRouter ? routerApi?.useParams?.() ?? {} : {};
66
70
  const framework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
67
71
  const contextUseRouter = inReactRouter || 'tanstack' !== framework ? void 0 : internalContext.router?.useRouter || runtimeContext.router?.useRouter;
68
72
  const contextRouter = contextUseRouter ? contextUseRouter({
69
73
  warn: false
70
74
  }) : null;
71
- const [, setRouterVersion] = useState(0);
72
- const routerSnapshotRef = useRef(getRouterSnapshot(internalContext, contextRouter));
73
- routerSnapshotRef.current = getRouterSnapshot(internalContext, contextRouter);
74
75
  const hasRouter = 'tanstack' === framework || 'react-router' === framework || Boolean(reactRouterNavigate);
75
- useEffect(()=>{
76
+ const subscribeToRouter = useCallback((update)=>{
76
77
  const router = getRouterInstance(internalContext, contextRouter);
77
- if (!router) return;
78
- const update = ()=>{
79
- const nextSnapshot = getRouterSnapshot(internalContext, contextRouter);
80
- if (nextSnapshot === routerSnapshotRef.current) return;
81
- routerSnapshotRef.current = nextSnapshot;
82
- setRouterVersion((version)=>version + 1);
83
- };
78
+ if (!router) return ()=>void 0;
84
79
  const unsubscribers = [];
85
80
  if ('react-router' === framework && !inReactRouter && 'function' == typeof router.subscribe) {
86
81
  const subscribe = router.subscribe;
@@ -110,6 +105,11 @@ const useI18nRouterAdapter = ()=>{
110
105
  inReactRouter,
111
106
  internalContext
112
107
  ]);
108
+ const getSnapshot = useCallback(()=>getRouterSnapshot(internalContext, contextRouter), [
109
+ contextRouter,
110
+ internalContext
111
+ ]);
112
+ useSyncExternalStore(subscribeToRouter, getSnapshot, getSnapshot);
113
113
  const navigate = useCallback((href, options)=>{
114
114
  const router = getRouterInstance(internalContext, contextRouter);
115
115
  const activeFramework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
@@ -137,7 +137,7 @@ const useI18nRouterAdapter = ()=>{
137
137
  ]);
138
138
  const location = (reactRouterLocation ? normalizeLocation(reactRouterLocation) : getRouterStateLocation(internalContext, contextRouter)) || getWindowLocation();
139
139
  const params = inReactRouter ? reactRouterParams : getRouterParams(internalContext, contextRouter);
140
- const Link = 'tanstack' === framework ? internalContext.router?.Link || runtimeContext.router?.Link || null : 'react-router' === framework || inReactRouter ? router_Link : null;
140
+ const Link = 'tanstack' === framework ? internalContext.router?.Link || runtimeContext.router?.Link || null : 'react-router' === framework || inReactRouter ? routerApi?.Link ?? null : null;
141
141
  return {
142
142
  framework,
143
143
  hasRouter,
@@ -1,8 +1,7 @@
1
1
  import "node:module";
2
2
  import { RuntimeContext, isBrowser } from "@modern-js/runtime";
3
3
  import { InternalRuntimeContext, getRouterRuntimeState } from "@modern-js/runtime/context";
4
- import { Link as router_Link, useInRouterContext, useLocation, useNavigate, useParams } from "@modern-js/runtime/router";
5
- import { useCallback, useContext, useEffect, useRef, useState } from "react";
4
+ import { useCallback, useContext, useSyncExternalStore } from "react";
6
5
  const normalizeUrlPart = (value, prefix)=>{
7
6
  if ('string' != typeof value || !value) return '';
8
7
  return value.startsWith(prefix) ? value : `${prefix}${value}`;
@@ -54,34 +53,30 @@ const getRouterParams = (internalContext, contextRouter)=>{
54
53
  const getRouterSnapshot = (internalContext, contextRouter)=>{
55
54
  const location = getRouterStateLocation(internalContext, contextRouter);
56
55
  const params = getRouterParams(internalContext, contextRouter);
57
- const serializedParams = Object.entries(params).sort(([left], [right])=>left.localeCompare(right)).map(([key, value])=>`${key}=${value}`).join('&');
58
- return `${location?.pathname ?? ''}\u0000${location?.search ?? ''}\u0000${location?.hash ?? ''}\u0000${serializedParams}`;
56
+ return JSON.stringify([
57
+ location?.pathname ?? '',
58
+ location?.search ?? '',
59
+ location?.hash ?? '',
60
+ Object.entries(params).sort(([left], [right])=>left.localeCompare(right))
61
+ ]);
59
62
  };
60
63
  const useI18nRouterAdapter = ()=>{
61
64
  const runtimeContext = useContext(RuntimeContext);
62
65
  const internalContext = useContext(InternalRuntimeContext);
63
- const inReactRouter = useInRouterContext();
64
- const reactRouterNavigate = inReactRouter ? useNavigate() : null;
65
- const reactRouterLocation = inReactRouter ? useLocation() : null;
66
- const reactRouterParams = inReactRouter ? useParams() : {};
66
+ const routerApi = internalContext.router || runtimeContext.router;
67
+ const inReactRouter = routerApi?.useInRouterContext?.() ?? false;
68
+ const reactRouterNavigate = inReactRouter ? routerApi?.useNavigate?.() ?? null : null;
69
+ const reactRouterLocation = inReactRouter ? routerApi?.useLocation?.() ?? null : null;
70
+ const reactRouterParams = inReactRouter ? routerApi?.useParams?.() ?? {} : {};
67
71
  const framework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
68
72
  const contextUseRouter = inReactRouter || 'tanstack' !== framework ? void 0 : internalContext.router?.useRouter || runtimeContext.router?.useRouter;
69
73
  const contextRouter = contextUseRouter ? contextUseRouter({
70
74
  warn: false
71
75
  }) : null;
72
- const [, setRouterVersion] = useState(0);
73
- const routerSnapshotRef = useRef(getRouterSnapshot(internalContext, contextRouter));
74
- routerSnapshotRef.current = getRouterSnapshot(internalContext, contextRouter);
75
76
  const hasRouter = 'tanstack' === framework || 'react-router' === framework || Boolean(reactRouterNavigate);
76
- useEffect(()=>{
77
+ const subscribeToRouter = useCallback((update)=>{
77
78
  const router = getRouterInstance(internalContext, contextRouter);
78
- if (!router) return;
79
- const update = ()=>{
80
- const nextSnapshot = getRouterSnapshot(internalContext, contextRouter);
81
- if (nextSnapshot === routerSnapshotRef.current) return;
82
- routerSnapshotRef.current = nextSnapshot;
83
- setRouterVersion((version)=>version + 1);
84
- };
79
+ if (!router) return ()=>void 0;
85
80
  const unsubscribers = [];
86
81
  if ('react-router' === framework && !inReactRouter && 'function' == typeof router.subscribe) {
87
82
  const subscribe = router.subscribe;
@@ -111,6 +106,11 @@ const useI18nRouterAdapter = ()=>{
111
106
  inReactRouter,
112
107
  internalContext
113
108
  ]);
109
+ const getSnapshot = useCallback(()=>getRouterSnapshot(internalContext, contextRouter), [
110
+ contextRouter,
111
+ internalContext
112
+ ]);
113
+ useSyncExternalStore(subscribeToRouter, getSnapshot, getSnapshot);
114
114
  const navigate = useCallback((href, options)=>{
115
115
  const router = getRouterInstance(internalContext, contextRouter);
116
116
  const activeFramework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
@@ -138,7 +138,7 @@ const useI18nRouterAdapter = ()=>{
138
138
  ]);
139
139
  const location = (reactRouterLocation ? normalizeLocation(reactRouterLocation) : getRouterStateLocation(internalContext, contextRouter)) || getWindowLocation();
140
140
  const params = inReactRouter ? reactRouterParams : getRouterParams(internalContext, contextRouter);
141
- const Link = 'tanstack' === framework ? internalContext.router?.Link || runtimeContext.router?.Link || null : 'react-router' === framework || inReactRouter ? router_Link : null;
141
+ const Link = 'tanstack' === framework ? internalContext.router?.Link || runtimeContext.router?.Link || null : 'react-router' === framework || inReactRouter ? routerApi?.Link ?? null : null;
142
142
  return {
143
143
  framework,
144
144
  hasRouter,
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.5.0-ultramodern.102",
20
+ "version": "3.5.0-ultramodern.103",
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
@@ -91,12 +91,12 @@
91
91
  }
92
92
  },
93
93
  "dependencies": {
94
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.102",
95
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.102",
96
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.102",
97
- "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.5.0-ultramodern.102",
98
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.102",
99
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.102",
94
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.103",
95
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.103",
96
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.103",
97
+ "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.5.0-ultramodern.103",
98
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.103",
99
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.103",
100
100
  "@swc/helpers": "^0.5.23",
101
101
  "i18next-browser-languagedetector": "^8.2.1",
102
102
  "i18next-chained-backend": "^5.0.5",
@@ -105,10 +105,10 @@
105
105
  "i18next-http-middleware": "^3.9.7"
106
106
  },
107
107
  "peerDependencies": {
108
- "@modern-js/runtime": "3.5.0-ultramodern.102",
108
+ "@modern-js/runtime": "3.5.0-ultramodern.103",
109
109
  "i18next": ">=26.3.1",
110
- "react": "^19.2.7",
111
- "react-dom": "^19.2.7",
110
+ "react": "^19.2.8",
111
+ "react-dom": "^19.2.8",
112
112
  "react-i18next": "^17.0.9"
113
113
  },
114
114
  "peerDependenciesMeta": {
@@ -120,15 +120,15 @@
120
120
  }
121
121
  },
122
122
  "devDependencies": {
123
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.102",
124
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.102",
123
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.103",
124
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.103",
125
125
  "@rslib/core": "0.23.2",
126
- "@types/node": "^26.1.1",
126
+ "@types/node": "^26.2.0",
127
127
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
128
128
  "esbuild": "^0.28.1",
129
129
  "i18next": "26.3.6",
130
- "react": "^19.2.7",
131
- "react-dom": "^19.2.7",
130
+ "react": "^19.2.8",
131
+ "react-dom": "^19.2.8",
132
132
  "react-i18next": "17.0.9",
133
133
  "ts-node": "^10.9.2",
134
134
  "typescript": "^7.0.2"
@@ -5,15 +5,8 @@ import {
5
5
  type TInternalRuntimeContext,
6
6
  type TRuntimeContext,
7
7
  } from '@modern-js/runtime/context';
8
- import {
9
- Link as ReactRouterLink,
10
- useInRouterContext,
11
- useLocation as useReactRouterLocation,
12
- useNavigate as useReactRouterNavigate,
13
- useParams as useReactRouterParams,
14
- } from '@modern-js/runtime/router';
15
8
  import type React from 'react';
16
- import { useCallback, useContext, useEffect, useRef, useState } from 'react';
9
+ import { useCallback, useContext, useSyncExternalStore } from 'react';
17
10
 
18
11
  type I18nRouterFramework = 'react-router' | 'tanstack' | string;
19
12
 
@@ -50,8 +43,11 @@ interface I18nRouterAdapter {
50
43
 
51
44
  type RuntimeContextWithRouter = TRuntimeContext & {
52
45
  router?: {
46
+ useInRouterContext?: () => boolean;
53
47
  useRouter?: (options?: { warn?: boolean }) => unknown;
54
48
  useLocation?: () => unknown;
49
+ useNavigate?: () => I18nRouterNavigate;
50
+ useParams?: () => Record<string, string>;
55
51
  useHref?: () => unknown;
56
52
  Link?: I18nRouterLink;
57
53
  };
@@ -211,13 +207,12 @@ const getRouterSnapshot = (
211
207
  ) => {
212
208
  const location = getRouterStateLocation(internalContext, contextRouter);
213
209
  const params = getRouterParams(internalContext, contextRouter);
214
- const serializedParams = Object.entries(params)
215
- .sort(([left], [right]) => left.localeCompare(right))
216
- .map(([key, value]) => `${key}=${value}`)
217
- .join('&');
218
- return `${location?.pathname ?? ''}\u0000${location?.search ?? ''}\u0000${
219
- location?.hash ?? ''
220
- }\u0000${serializedParams}`;
210
+ return JSON.stringify([
211
+ location?.pathname ?? '',
212
+ location?.search ?? '',
213
+ location?.hash ?? '',
214
+ Object.entries(params).sort(([left], [right]) => left.localeCompare(right)),
215
+ ]);
221
216
  };
222
217
 
223
218
  export const useI18nRouterAdapter = (): I18nRouterAdapter => {
@@ -225,10 +220,17 @@ export const useI18nRouterAdapter = (): I18nRouterAdapter => {
225
220
  const internalContext = useContext(
226
221
  InternalRuntimeContext,
227
222
  ) as InternalRuntimeContextWithRouter;
228
- const inReactRouter = useInRouterContext();
229
- const reactRouterNavigate = inReactRouter ? useReactRouterNavigate() : null;
230
- const reactRouterLocation = inReactRouter ? useReactRouterLocation() : null;
231
- const reactRouterParams = inReactRouter ? useReactRouterParams() : {};
223
+ const routerApi = internalContext.router || runtimeContext.router;
224
+ const inReactRouter = routerApi?.useInRouterContext?.() ?? false;
225
+ const reactRouterNavigate = inReactRouter
226
+ ? (routerApi?.useNavigate?.() ?? null)
227
+ : null;
228
+ const reactRouterLocation = inReactRouter
229
+ ? (routerApi?.useLocation?.() ?? null)
230
+ : null;
231
+ const reactRouterParams = inReactRouter
232
+ ? (routerApi?.useParams?.() ?? {})
233
+ : {};
232
234
  const framework = getRouterFramework(
233
235
  runtimeContext,
234
236
  internalContext,
@@ -241,77 +243,72 @@ export const useI18nRouterAdapter = (): I18nRouterAdapter => {
241
243
  const contextRouter = contextUseRouter
242
244
  ? (contextUseRouter({ warn: false }) as RouterInstance | null)
243
245
  : null;
244
- const [, setRouterVersion] = useState(0);
245
- const routerSnapshotRef = useRef(
246
- getRouterSnapshot(internalContext, contextRouter),
247
- );
248
- routerSnapshotRef.current = getRouterSnapshot(internalContext, contextRouter);
249
246
  const hasRouter =
250
247
  framework === 'tanstack' ||
251
248
  framework === 'react-router' ||
252
249
  Boolean(reactRouterNavigate);
253
250
 
254
- useEffect(() => {
255
- const router = getRouterInstance(internalContext, contextRouter);
256
- if (!router) {
257
- return;
258
- }
259
-
260
- const update = () => {
261
- const nextSnapshot = getRouterSnapshot(internalContext, contextRouter);
262
- if (nextSnapshot === routerSnapshotRef.current) {
263
- return;
264
- }
265
- routerSnapshotRef.current = nextSnapshot;
266
- setRouterVersion(version => version + 1);
267
- };
268
- const unsubscribers: Array<() => void> = [];
269
-
270
- if (
271
- framework === 'react-router' &&
272
- !inReactRouter &&
273
- typeof router.subscribe === 'function'
274
- ) {
275
- const subscribe = router.subscribe as (
276
- this: RouterInstance,
277
- listener: () => void,
278
- ) => () => void;
279
- const unsubscribe = subscribe.call(router, update);
280
- if (typeof unsubscribe === 'function') {
281
- unsubscribers.push(unsubscribe);
251
+ const subscribeToRouter = useCallback(
252
+ (update: () => void) => {
253
+ const router = getRouterInstance(internalContext, contextRouter);
254
+ if (!router) {
255
+ return () => undefined;
282
256
  }
283
- }
284
257
 
285
- if (
286
- framework === 'tanstack' &&
287
- typeof router.stores?.location?.subscribe === 'function'
288
- ) {
289
- const unsubscribe = router.stores.location.subscribe(update);
290
- if (typeof unsubscribe === 'function') {
291
- unsubscribers.push(unsubscribe);
258
+ const unsubscribers: Array<() => void> = [];
259
+
260
+ if (
261
+ framework === 'react-router' &&
262
+ !inReactRouter &&
263
+ typeof router.subscribe === 'function'
264
+ ) {
265
+ const subscribe = router.subscribe as (
266
+ this: RouterInstance,
267
+ listener: () => void,
268
+ ) => () => void;
269
+ const unsubscribe = subscribe.call(router, update);
270
+ if (typeof unsubscribe === 'function') {
271
+ unsubscribers.push(unsubscribe);
272
+ }
292
273
  }
293
- }
294
274
 
295
- if (framework === 'tanstack' && typeof router.subscribe === 'function') {
296
- const subscribe = router.subscribe as (
297
- this: RouterInstance,
298
- eventType: string,
299
- listener: () => void,
300
- ) => () => void;
301
- for (const eventType of ['onBeforeNavigate', 'onBeforeLoad']) {
302
- const unsubscribe = subscribe.call(router, eventType, update);
275
+ if (
276
+ framework === 'tanstack' &&
277
+ typeof router.stores?.location?.subscribe === 'function'
278
+ ) {
279
+ const unsubscribe = router.stores.location.subscribe(update);
303
280
  if (typeof unsubscribe === 'function') {
304
281
  unsubscribers.push(unsubscribe);
305
282
  }
306
283
  }
307
- }
308
284
 
309
- return () => {
310
- for (const unsubscribe of unsubscribers) {
311
- unsubscribe();
285
+ if (framework === 'tanstack' && typeof router.subscribe === 'function') {
286
+ const subscribe = router.subscribe as (
287
+ this: RouterInstance,
288
+ eventType: string,
289
+ listener: () => void,
290
+ ) => () => void;
291
+ for (const eventType of ['onBeforeNavigate', 'onBeforeLoad']) {
292
+ const unsubscribe = subscribe.call(router, eventType, update);
293
+ if (typeof unsubscribe === 'function') {
294
+ unsubscribers.push(unsubscribe);
295
+ }
296
+ }
312
297
  }
313
- };
314
- }, [contextRouter, framework, inReactRouter, internalContext]);
298
+
299
+ return () => {
300
+ for (const unsubscribe of unsubscribers) {
301
+ unsubscribe();
302
+ }
303
+ };
304
+ },
305
+ [contextRouter, framework, inReactRouter, internalContext],
306
+ );
307
+ const getSnapshot = useCallback(
308
+ () => getRouterSnapshot(internalContext, contextRouter),
309
+ [contextRouter, internalContext],
310
+ );
311
+ useSyncExternalStore(subscribeToRouter, getSnapshot, getSnapshot);
315
312
 
316
313
  const navigate = useCallback<I18nRouterNavigate>(
317
314
  (href, options) => {
@@ -365,7 +362,7 @@ export const useI18nRouterAdapter = (): I18nRouterAdapter => {
365
362
  framework === 'tanstack'
366
363
  ? internalContext.router?.Link || runtimeContext.router?.Link || null
367
364
  : framework === 'react-router' || inReactRouter
368
- ? (ReactRouterLink as I18nRouterLink)
365
+ ? (routerApi?.Link ?? null)
369
366
  : null;
370
367
 
371
368
  return {
@@ -62,6 +62,34 @@ describe('react-i18next runtime boundary', () => {
62
62
  expect(adapterResolutions).toBeGreaterThan(0);
63
63
  });
64
64
 
65
+ test('gets router capabilities from the selected runtime provider', async () => {
66
+ await expect(
67
+ build({
68
+ bundle: true,
69
+ entryPoints: [resolve(runtimeRoot, 'routerAdapter.tsx')],
70
+ format: 'esm',
71
+ packages: 'external',
72
+ platform: 'neutral',
73
+ plugins: [
74
+ {
75
+ name: 'reject-direct-router-provider',
76
+ setup(buildApi) {
77
+ buildApi.onResolve(
78
+ { filter: /^@modern-js\/runtime\/router$/ },
79
+ () => {
80
+ throw new Error(
81
+ 'The i18n adapter loaded the React Router provider directly.',
82
+ );
83
+ },
84
+ );
85
+ },
86
+ },
87
+ ],
88
+ write: false,
89
+ }),
90
+ ).resolves.toBeDefined();
91
+ });
92
+
65
93
  test('keeps expensive lifecycle helpers behind asynchronous bundle edges', async () => {
66
94
  const result = await build({
67
95
  bundle: true,
@@ -253,6 +253,60 @@ function createMutableTanstackRouter(pathname = '/en') {
253
253
  };
254
254
  }
255
255
 
256
+ function createMutableReactRouter(
257
+ pathname = '/en',
258
+ params: Record<string, string> = { lang: pathname.slice(1) },
259
+ ) {
260
+ const listeners = new Set<() => void>();
261
+ const router = {
262
+ state: {
263
+ location: {
264
+ pathname,
265
+ search: '',
266
+ hash: '',
267
+ },
268
+ matches: [{ params }],
269
+ fetchers: new Map<string, unknown>(),
270
+ },
271
+ subscribe(listener: () => void) {
272
+ listeners.add(listener);
273
+ return () => listeners.delete(listener);
274
+ },
275
+ publishFetcherUpdate() {
276
+ router.state = {
277
+ ...router.state,
278
+ fetchers: new Map([['fetcher', { state: 'idle' }]]),
279
+ };
280
+ for (const listener of listeners) {
281
+ listener();
282
+ }
283
+ },
284
+ publishPathname(nextPathname: string) {
285
+ router.state = {
286
+ ...router.state,
287
+ location: {
288
+ ...router.state.location,
289
+ pathname: nextPathname,
290
+ },
291
+ };
292
+ for (const listener of listeners) {
293
+ listener();
294
+ }
295
+ },
296
+ publishParams(nextParams: Record<string, string>) {
297
+ router.state = {
298
+ ...router.state,
299
+ matches: [{ params: nextParams }],
300
+ };
301
+ for (const listener of listeners) {
302
+ listener();
303
+ }
304
+ },
305
+ };
306
+
307
+ return router;
308
+ }
309
+
256
310
  function createDeferredI18nInstance() {
257
311
  const resources = {
258
312
  en: { languageSwitcher: 'Language' },
@@ -843,6 +897,55 @@ describe('i18n router adapter', () => {
843
897
  expect(rendered.container.textContent).toBe('/cs');
844
898
  });
845
899
 
900
+ test('ignores React Router fetcher updates while observing location changes', async () => {
901
+ const router = createMutableReactRouter('/en');
902
+ let renders = 0;
903
+ const LocationProbe = () => {
904
+ renders += 1;
905
+ const { location } = useI18nRouterAdapter();
906
+ return <output>{location?.pathname}</output>;
907
+ };
908
+
909
+ rendered = await renderWithRuntime(
910
+ <LocationProbe />,
911
+ createReactRouterRuntimeContext(router),
912
+ );
913
+ const initialRenders = renders;
914
+
915
+ await act(async () => {
916
+ router.publishFetcherUpdate();
917
+ });
918
+ expect(renders).toBe(initialRenders);
919
+ expect(rendered.container.textContent).toBe('/en');
920
+
921
+ await act(async () => {
922
+ router.publishPathname('/cs');
923
+ });
924
+ expect(renders).toBe(initialRenders + 1);
925
+ expect(rendered.container.textContent).toBe('/cs');
926
+ });
927
+
928
+ test('observes distinct route params without serialized snapshot collisions', async () => {
929
+ const router = createMutableReactRouter('/products', {
930
+ a: '1&b=2',
931
+ });
932
+ const ParamsProbe = () => {
933
+ const { params } = useI18nRouterAdapter();
934
+ return <output>{JSON.stringify(params)}</output>;
935
+ };
936
+
937
+ rendered = await renderWithRuntime(
938
+ <ParamsProbe />,
939
+ createReactRouterRuntimeContext(router),
940
+ );
941
+ expect(rendered.container.textContent).toBe('{"a":"1&b=2"}');
942
+
943
+ await act(async () => {
944
+ router.publishParams({ a: '1', b: '2' });
945
+ });
946
+ expect(rendered.container.textContent).toBe('{"a":"1","b":"2"}');
947
+ });
948
+
846
949
  test('forwards warmup props through I18nLink with a localized string target', async () => {
847
950
  const router = createTanstackRouter('/cs/podminky-pouzivani', 'cs');
848
951
  rendered = await renderWithRuntime(