@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.
- package/dist/cjs/runtime/routerAdapter.js +19 -19
- package/dist/esm/runtime/routerAdapter.mjs +20 -20
- package/dist/esm-node/runtime/routerAdapter.mjs +20 -20
- package/package.json +15 -15
- package/src/runtime/routerAdapter.tsx +73 -76
- package/tests/reactI18nextRuntimeBoundary.test.ts +28 -0
- package/tests/routerAdapter.test.tsx +103 -0
|
@@ -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
|
-
|
|
89
|
-
|
|
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
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
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.
|
|
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 ?
|
|
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 {
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
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
|
-
|
|
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 ?
|
|
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 {
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
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
|
-
|
|
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 ?
|
|
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.
|
|
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.
|
|
95
|
-
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.
|
|
96
|
-
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.
|
|
97
|
-
"@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.5.0-ultramodern.
|
|
98
|
-
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.
|
|
99
|
-
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.
|
|
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.
|
|
108
|
+
"@modern-js/runtime": "3.5.0-ultramodern.103",
|
|
109
109
|
"i18next": ">=26.3.1",
|
|
110
|
-
"react": "^19.2.
|
|
111
|
-
"react-dom": "^19.2.
|
|
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.
|
|
124
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.
|
|
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.
|
|
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.
|
|
131
|
-
"react-dom": "^19.2.
|
|
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,
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
? (
|
|
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(
|