@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.99 → 3.8.1-ultramodern.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.
@@ -77,21 +77,20 @@ const createI18nRootWrapper = (options)=>(App)=>(props)=>{
77
77
  forceUpdate,
78
78
  synchronizeLanguage
79
79
  ]);
80
+ const I18nextProvider = getI18nextProvider();
81
+ const i18nextInstanceForProvider = (0, external_react_namespaceObject.useMemo)(()=>i18nInstance ? (0, instance_js_namespaceObject.getI18nextInstanceForProvider)(i18nInstance, lang) : void 0, [
82
+ i18nInstance,
83
+ lang
84
+ ]);
80
85
  const children = props.children;
81
86
  let appContent = App ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(App, {
82
87
  ...props,
83
88
  children: children
84
89
  }) : children;
85
- if (i18nInstance) {
86
- const I18nextProvider = getI18nextProvider();
87
- if (I18nextProvider) {
88
- const i18nextInstanceForProvider = (0, instance_js_namespaceObject.getI18nextInstanceForProvider)(i18nInstance, lang);
89
- appContent = /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(I18nextProvider, {
90
- i18n: i18nextInstanceForProvider,
91
- children: appContent
92
- });
93
- }
94
- }
90
+ if (I18nextProvider && i18nextInstanceForProvider) appContent = /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(I18nextProvider, {
91
+ i18n: i18nextInstanceForProvider,
92
+ children: appContent
93
+ });
95
94
  return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)(jsx_runtime_namespaceObject.Fragment, {
96
95
  children: [
97
96
  Boolean(htmlLangAttr) && /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(head_namespaceObject.Helmet, {
@@ -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 '';
@@ -82,24 +81,33 @@ const getRouterParams = (internalContext, contextRouter)=>{
82
81
  return params;
83
82
  }, {});
84
83
  };
84
+ const getRouterSnapshot = (internalContext, contextRouter)=>{
85
+ const location = getRouterStateLocation(internalContext, contextRouter);
86
+ const params = getRouterParams(internalContext, contextRouter);
87
+ return JSON.stringify([
88
+ location?.pathname ?? '',
89
+ location?.search ?? '',
90
+ location?.hash ?? '',
91
+ Object.entries(params).sort(([left], [right])=>left.localeCompare(right))
92
+ ]);
93
+ };
85
94
  const useI18nRouterAdapter = ()=>{
86
95
  const runtimeContext = (0, external_react_namespaceObject.useContext)(runtime_namespaceObject.RuntimeContext);
87
96
  const internalContext = (0, external_react_namespaceObject.useContext)(context_namespaceObject.InternalRuntimeContext);
88
- const inReactRouter = (0, router_namespaceObject.useInRouterContext)();
89
- const reactRouterNavigate = inReactRouter ? (0, router_namespaceObject.useNavigate)() : null;
90
- const reactRouterLocation = inReactRouter ? (0, router_namespaceObject.useLocation)() : null;
91
- 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?.() ?? {} : {};
92
102
  const framework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
93
103
  const contextUseRouter = inReactRouter || 'tanstack' !== framework ? void 0 : internalContext.router?.useRouter || runtimeContext.router?.useRouter;
94
104
  const contextRouter = contextUseRouter ? contextUseRouter({
95
105
  warn: false
96
106
  }) : null;
97
- const [, setRouterVersion] = (0, external_react_namespaceObject.useState)(0);
98
107
  const hasRouter = 'tanstack' === framework || 'react-router' === framework || Boolean(reactRouterNavigate);
99
- (0, external_react_namespaceObject.useEffect)(()=>{
108
+ const subscribeToRouter = (0, external_react_namespaceObject.useCallback)((update)=>{
100
109
  const router = getRouterInstance(internalContext, contextRouter);
101
- if (!router) return;
102
- const update = ()=>setRouterVersion((version)=>version + 1);
110
+ if (!router) return ()=>void 0;
103
111
  const unsubscribers = [];
104
112
  if ('react-router' === framework && !inReactRouter && 'function' == typeof router.subscribe) {
105
113
  const subscribe = router.subscribe;
@@ -129,6 +137,11 @@ const useI18nRouterAdapter = ()=>{
129
137
  inReactRouter,
130
138
  internalContext
131
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);
132
145
  const navigate = (0, external_react_namespaceObject.useCallback)((href, options)=>{
133
146
  const router = getRouterInstance(internalContext, contextRouter);
134
147
  const activeFramework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
@@ -156,7 +169,7 @@ const useI18nRouterAdapter = ()=>{
156
169
  ]);
157
170
  const location = (reactRouterLocation ? normalizeLocation(reactRouterLocation) : getRouterStateLocation(internalContext, contextRouter)) || getWindowLocation();
158
171
  const params = inReactRouter ? reactRouterParams : getRouterParams(internalContext, contextRouter);
159
- 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;
160
173
  return {
161
174
  framework,
162
175
  hasRouter,
@@ -45,21 +45,20 @@ const createI18nRootWrapper = (options)=>(App)=>(props)=>{
45
45
  forceUpdate,
46
46
  synchronizeLanguage
47
47
  ]);
48
+ const I18nextProvider = getI18nextProvider();
49
+ const i18nextInstanceForProvider = useMemo(()=>i18nInstance ? getI18nextInstanceForProvider(i18nInstance, lang) : void 0, [
50
+ i18nInstance,
51
+ lang
52
+ ]);
48
53
  const children = props.children;
49
54
  let appContent = App ? /*#__PURE__*/ jsx(App, {
50
55
  ...props,
51
56
  children: children
52
57
  }) : children;
53
- if (i18nInstance) {
54
- const I18nextProvider = getI18nextProvider();
55
- if (I18nextProvider) {
56
- const i18nextInstanceForProvider = getI18nextInstanceForProvider(i18nInstance, lang);
57
- appContent = /*#__PURE__*/ jsx(I18nextProvider, {
58
- i18n: i18nextInstanceForProvider,
59
- children: appContent
60
- });
61
- }
62
- }
58
+ if (I18nextProvider && i18nextInstanceForProvider) appContent = /*#__PURE__*/ jsx(I18nextProvider, {
59
+ i18n: i18nextInstanceForProvider,
60
+ children: appContent
61
+ });
63
62
  return /*#__PURE__*/ jsxs(Fragment, {
64
63
  children: [
65
64
  Boolean(htmlLangAttr) && /*#__PURE__*/ jsx(Helmet, {
@@ -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, 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}`;
@@ -50,24 +49,33 @@ const getRouterParams = (internalContext, contextRouter)=>{
50
49
  return params;
51
50
  }, {});
52
51
  };
52
+ const getRouterSnapshot = (internalContext, contextRouter)=>{
53
+ const location = getRouterStateLocation(internalContext, contextRouter);
54
+ const params = getRouterParams(internalContext, contextRouter);
55
+ return JSON.stringify([
56
+ location?.pathname ?? '',
57
+ location?.search ?? '',
58
+ location?.hash ?? '',
59
+ Object.entries(params).sort(([left], [right])=>left.localeCompare(right))
60
+ ]);
61
+ };
53
62
  const useI18nRouterAdapter = ()=>{
54
63
  const runtimeContext = useContext(RuntimeContext);
55
64
  const internalContext = useContext(InternalRuntimeContext);
56
- const inReactRouter = useInRouterContext();
57
- const reactRouterNavigate = inReactRouter ? useNavigate() : null;
58
- const reactRouterLocation = inReactRouter ? useLocation() : null;
59
- 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?.() ?? {} : {};
60
70
  const framework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
61
71
  const contextUseRouter = inReactRouter || 'tanstack' !== framework ? void 0 : internalContext.router?.useRouter || runtimeContext.router?.useRouter;
62
72
  const contextRouter = contextUseRouter ? contextUseRouter({
63
73
  warn: false
64
74
  }) : null;
65
- const [, setRouterVersion] = useState(0);
66
75
  const hasRouter = 'tanstack' === framework || 'react-router' === framework || Boolean(reactRouterNavigate);
67
- useEffect(()=>{
76
+ const subscribeToRouter = useCallback((update)=>{
68
77
  const router = getRouterInstance(internalContext, contextRouter);
69
- if (!router) return;
70
- const update = ()=>setRouterVersion((version)=>version + 1);
78
+ if (!router) return ()=>void 0;
71
79
  const unsubscribers = [];
72
80
  if ('react-router' === framework && !inReactRouter && 'function' == typeof router.subscribe) {
73
81
  const subscribe = router.subscribe;
@@ -97,6 +105,11 @@ const useI18nRouterAdapter = ()=>{
97
105
  inReactRouter,
98
106
  internalContext
99
107
  ]);
108
+ const getSnapshot = useCallback(()=>getRouterSnapshot(internalContext, contextRouter), [
109
+ contextRouter,
110
+ internalContext
111
+ ]);
112
+ useSyncExternalStore(subscribeToRouter, getSnapshot, getSnapshot);
100
113
  const navigate = useCallback((href, options)=>{
101
114
  const router = getRouterInstance(internalContext, contextRouter);
102
115
  const activeFramework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
@@ -124,7 +137,7 @@ const useI18nRouterAdapter = ()=>{
124
137
  ]);
125
138
  const location = (reactRouterLocation ? normalizeLocation(reactRouterLocation) : getRouterStateLocation(internalContext, contextRouter)) || getWindowLocation();
126
139
  const params = inReactRouter ? reactRouterParams : getRouterParams(internalContext, contextRouter);
127
- 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;
128
141
  return {
129
142
  framework,
130
143
  hasRouter,
@@ -46,21 +46,20 @@ const createI18nRootWrapper = (options)=>(App)=>(props)=>{
46
46
  forceUpdate,
47
47
  synchronizeLanguage
48
48
  ]);
49
+ const I18nextProvider = getI18nextProvider();
50
+ const i18nextInstanceForProvider = useMemo(()=>i18nInstance ? getI18nextInstanceForProvider(i18nInstance, lang) : void 0, [
51
+ i18nInstance,
52
+ lang
53
+ ]);
49
54
  const children = props.children;
50
55
  let appContent = App ? /*#__PURE__*/ jsx(App, {
51
56
  ...props,
52
57
  children: children
53
58
  }) : children;
54
- if (i18nInstance) {
55
- const I18nextProvider = getI18nextProvider();
56
- if (I18nextProvider) {
57
- const i18nextInstanceForProvider = getI18nextInstanceForProvider(i18nInstance, lang);
58
- appContent = /*#__PURE__*/ jsx(I18nextProvider, {
59
- i18n: i18nextInstanceForProvider,
60
- children: appContent
61
- });
62
- }
63
- }
59
+ if (I18nextProvider && i18nextInstanceForProvider) appContent = /*#__PURE__*/ jsx(I18nextProvider, {
60
+ i18n: i18nextInstanceForProvider,
61
+ children: appContent
62
+ });
64
63
  return /*#__PURE__*/ jsxs(Fragment, {
65
64
  children: [
66
65
  Boolean(htmlLangAttr) && /*#__PURE__*/ jsx(Helmet, {
@@ -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, 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}`;
@@ -51,24 +50,33 @@ const getRouterParams = (internalContext, contextRouter)=>{
51
50
  return params;
52
51
  }, {});
53
52
  };
53
+ const getRouterSnapshot = (internalContext, contextRouter)=>{
54
+ const location = getRouterStateLocation(internalContext, contextRouter);
55
+ const params = getRouterParams(internalContext, contextRouter);
56
+ return JSON.stringify([
57
+ location?.pathname ?? '',
58
+ location?.search ?? '',
59
+ location?.hash ?? '',
60
+ Object.entries(params).sort(([left], [right])=>left.localeCompare(right))
61
+ ]);
62
+ };
54
63
  const useI18nRouterAdapter = ()=>{
55
64
  const runtimeContext = useContext(RuntimeContext);
56
65
  const internalContext = useContext(InternalRuntimeContext);
57
- const inReactRouter = useInRouterContext();
58
- const reactRouterNavigate = inReactRouter ? useNavigate() : null;
59
- const reactRouterLocation = inReactRouter ? useLocation() : null;
60
- 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?.() ?? {} : {};
61
71
  const framework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
62
72
  const contextUseRouter = inReactRouter || 'tanstack' !== framework ? void 0 : internalContext.router?.useRouter || runtimeContext.router?.useRouter;
63
73
  const contextRouter = contextUseRouter ? contextUseRouter({
64
74
  warn: false
65
75
  }) : null;
66
- const [, setRouterVersion] = useState(0);
67
76
  const hasRouter = 'tanstack' === framework || 'react-router' === framework || Boolean(reactRouterNavigate);
68
- useEffect(()=>{
77
+ const subscribeToRouter = useCallback((update)=>{
69
78
  const router = getRouterInstance(internalContext, contextRouter);
70
- if (!router) return;
71
- const update = ()=>setRouterVersion((version)=>version + 1);
79
+ if (!router) return ()=>void 0;
72
80
  const unsubscribers = [];
73
81
  if ('react-router' === framework && !inReactRouter && 'function' == typeof router.subscribe) {
74
82
  const subscribe = router.subscribe;
@@ -98,6 +106,11 @@ const useI18nRouterAdapter = ()=>{
98
106
  inReactRouter,
99
107
  internalContext
100
108
  ]);
109
+ const getSnapshot = useCallback(()=>getRouterSnapshot(internalContext, contextRouter), [
110
+ contextRouter,
111
+ internalContext
112
+ ]);
113
+ useSyncExternalStore(subscribeToRouter, getSnapshot, getSnapshot);
101
114
  const navigate = useCallback((href, options)=>{
102
115
  const router = getRouterInstance(internalContext, contextRouter);
103
116
  const activeFramework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
@@ -125,7 +138,7 @@ const useI18nRouterAdapter = ()=>{
125
138
  ]);
126
139
  const location = (reactRouterLocation ? normalizeLocation(reactRouterLocation) : getRouterStateLocation(internalContext, contextRouter)) || getWindowLocation();
127
140
  const params = inReactRouter ? reactRouterParams : getRouterParams(internalContext, contextRouter);
128
- 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;
129
142
  return {
130
143
  framework,
131
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.99",
20
+ "version": "3.8.1-ultramodern.2",
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.99",
95
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.99",
96
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.99",
97
- "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.5.0-ultramodern.99",
98
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.99",
99
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.99",
94
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.8.1-ultramodern.2",
95
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.8.1-ultramodern.2",
96
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.8.1-ultramodern.2",
97
+ "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.8.1-ultramodern.2",
98
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.8.1-ultramodern.2",
99
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.8.1-ultramodern.2",
100
100
  "@swc/helpers": "^0.5.23",
101
101
  "i18next-browser-languagedetector": "^8.2.1",
102
102
  "i18next-chained-backend": "^5.0.5",
@@ -105,11 +105,11 @@
105
105
  "i18next-http-middleware": "^3.9.7"
106
106
  },
107
107
  "peerDependencies": {
108
- "@modern-js/runtime": "3.5.0-ultramodern.99",
109
- "i18next": ">=26.3.1",
110
- "react": "^19.2.7",
111
- "react-dom": "^19.2.7",
112
- "react-i18next": "^17.0.9"
108
+ "@modern-js/runtime": "3.8.1-ultramodern.2",
109
+ "i18next": ">=25.7.4",
110
+ "react": ">=18",
111
+ "react-dom": ">=18",
112
+ "react-i18next": ">=15.7.4"
113
113
  },
114
114
  "peerDependenciesMeta": {
115
115
  "i18next": {
@@ -120,14 +120,15 @@
120
120
  }
121
121
  },
122
122
  "devDependencies": {
123
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.99",
124
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.99",
123
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.8.1-ultramodern.2",
124
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.8.1-ultramodern.2",
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
+ "esbuild": "^0.28.1",
128
129
  "i18next": "26.3.6",
129
- "react": "^19.2.7",
130
- "react-dom": "^19.2.7",
130
+ "react": "^19.2.8",
131
+ "react-dom": "^19.2.8",
131
132
  "react-i18next": "17.0.9",
132
133
  "ts-node": "^10.9.2",
133
134
  "typescript": "^7.0.2"
@@ -113,6 +113,14 @@ export const createI18nRootWrapper =
113
113
  synchronizeLanguage,
114
114
  ],
115
115
  );
116
+ const I18nextProvider = getI18nextProvider();
117
+ const i18nextInstanceForProvider = useMemo(
118
+ () =>
119
+ i18nInstance
120
+ ? getI18nextInstanceForProvider(i18nInstance, lang)
121
+ : undefined,
122
+ [i18nInstance, lang],
123
+ );
116
124
 
117
125
  const children = (props as React.PropsWithChildren).children;
118
126
  let appContent: React.ReactNode = App ? (
@@ -121,19 +129,12 @@ export const createI18nRootWrapper =
121
129
  children
122
130
  );
123
131
 
124
- if (i18nInstance) {
125
- const I18nextProvider = getI18nextProvider();
126
- if (I18nextProvider) {
127
- const i18nextInstanceForProvider = getI18nextInstanceForProvider(
128
- i18nInstance,
129
- lang,
130
- );
131
- appContent = (
132
- <I18nextProvider i18n={i18nextInstanceForProvider}>
133
- {appContent}
134
- </I18nextProvider>
135
- );
136
- }
132
+ if (I18nextProvider && i18nextInstanceForProvider) {
133
+ appContent = (
134
+ <I18nextProvider i18n={i18nextInstanceForProvider}>
135
+ {appContent}
136
+ </I18nextProvider>
137
+ );
137
138
  }
138
139
 
139
140
  return (
@@ -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, 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
  };
@@ -205,15 +201,36 @@ const getRouterParams = (
205
201
  }, {});
206
202
  };
207
203
 
204
+ const getRouterSnapshot = (
205
+ internalContext: InternalRuntimeContextWithRouter,
206
+ contextRouter?: RouterInstance | null,
207
+ ) => {
208
+ const location = getRouterStateLocation(internalContext, contextRouter);
209
+ const params = getRouterParams(internalContext, contextRouter);
210
+ return JSON.stringify([
211
+ location?.pathname ?? '',
212
+ location?.search ?? '',
213
+ location?.hash ?? '',
214
+ Object.entries(params).sort(([left], [right]) => left.localeCompare(right)),
215
+ ]);
216
+ };
217
+
208
218
  export const useI18nRouterAdapter = (): I18nRouterAdapter => {
209
219
  const runtimeContext = useContext(RuntimeContext) as RuntimeContextWithRouter;
210
220
  const internalContext = useContext(
211
221
  InternalRuntimeContext,
212
222
  ) as InternalRuntimeContextWithRouter;
213
- const inReactRouter = useInRouterContext();
214
- const reactRouterNavigate = inReactRouter ? useReactRouterNavigate() : null;
215
- const reactRouterLocation = inReactRouter ? useReactRouterLocation() : null;
216
- 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
+ : {};
217
234
  const framework = getRouterFramework(
218
235
  runtimeContext,
219
236
  internalContext,
@@ -226,66 +243,72 @@ export const useI18nRouterAdapter = (): I18nRouterAdapter => {
226
243
  const contextRouter = contextUseRouter
227
244
  ? (contextUseRouter({ warn: false }) as RouterInstance | null)
228
245
  : null;
229
- const [, setRouterVersion] = useState(0);
230
246
  const hasRouter =
231
247
  framework === 'tanstack' ||
232
248
  framework === 'react-router' ||
233
249
  Boolean(reactRouterNavigate);
234
250
 
235
- useEffect(() => {
236
- const router = getRouterInstance(internalContext, contextRouter);
237
- if (!router) {
238
- return;
239
- }
240
-
241
- const update = () => setRouterVersion(version => version + 1);
242
- const unsubscribers: Array<() => void> = [];
243
-
244
- if (
245
- framework === 'react-router' &&
246
- !inReactRouter &&
247
- typeof router.subscribe === 'function'
248
- ) {
249
- const subscribe = router.subscribe as (
250
- this: RouterInstance,
251
- listener: () => void,
252
- ) => () => void;
253
- const unsubscribe = subscribe.call(router, update);
254
- if (typeof unsubscribe === 'function') {
255
- unsubscribers.push(unsubscribe);
251
+ const subscribeToRouter = useCallback(
252
+ (update: () => void) => {
253
+ const router = getRouterInstance(internalContext, contextRouter);
254
+ if (!router) {
255
+ return () => undefined;
256
256
  }
257
- }
258
257
 
259
- if (
260
- framework === 'tanstack' &&
261
- typeof router.stores?.location?.subscribe === 'function'
262
- ) {
263
- const unsubscribe = router.stores.location.subscribe(update);
264
- if (typeof unsubscribe === 'function') {
265
- 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
+ }
266
273
  }
267
- }
268
274
 
269
- if (framework === 'tanstack' && typeof router.subscribe === 'function') {
270
- const subscribe = router.subscribe as (
271
- this: RouterInstance,
272
- eventType: string,
273
- listener: () => void,
274
- ) => () => void;
275
- for (const eventType of ['onBeforeNavigate', 'onBeforeLoad']) {
276
- 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);
277
280
  if (typeof unsubscribe === 'function') {
278
281
  unsubscribers.push(unsubscribe);
279
282
  }
280
283
  }
281
- }
282
284
 
283
- return () => {
284
- for (const unsubscribe of unsubscribers) {
285
- 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
+ }
286
297
  }
287
- };
288
- }, [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);
289
312
 
290
313
  const navigate = useCallback<I18nRouterNavigate>(
291
314
  (href, options) => {
@@ -339,7 +362,7 @@ export const useI18nRouterAdapter = (): I18nRouterAdapter => {
339
362
  framework === 'tanstack'
340
363
  ? internalContext.router?.Link || runtimeContext.router?.Link || null
341
364
  : framework === 'react-router' || inReactRouter
342
- ? (ReactRouterLink as I18nRouterLink)
365
+ ? (routerApi?.Link ?? null)
343
366
  : null;
344
367
 
345
368
  return {
@@ -1,5 +1,3 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
1
  import type { NestedRouteForCli } from '@modern-js/types';
4
2
  import { describe, expect, test } from '@rstest/core';
5
3
  import { i18nPlugin as i18nCliPlugin } from '../src/cli';
@@ -19,12 +17,6 @@ import {
19
17
  validateLocalisedUrls,
20
18
  } from '../src/shared/localisedUrls';
21
19
 
22
- const localisedRoutesGoldenPath = path.join(
23
- __dirname,
24
- 'fixtures',
25
- 'localised-routes.golden.json',
26
- );
27
-
28
20
  const createRoute = (
29
21
  path: string,
30
22
  children?: NestedRouteForCli[],
@@ -45,9 +37,6 @@ const createRequestContext = (pathname: string) =>
45
37
  },
46
38
  }) as any;
47
39
 
48
- const serializeRoutesForGolden = (routes: unknown): string =>
49
- `${JSON.stringify(routes, null, 2)}\n`;
50
-
51
40
  describe('resolveLocalisedUrlsConfig', () => {
52
41
  test('is opt-in: only a non-empty map enables the feature', () => {
53
42
  const map = { '/about': { en: '/about', cs: '/o-nas' } };
@@ -119,7 +108,7 @@ describe('cli modifyFileSystemRoutes', () => {
119
108
  return modifyRoutes!;
120
109
  };
121
110
 
122
- const generateLocalisedRoutesGolden = (): string => {
111
+ const generateLocalisedRoutes = () => {
123
112
  const modifyRoutes = setupModifyRoutes({
124
113
  localePathRedirect: true,
125
114
  languages: ['en', 'cs', 'de'],
@@ -164,23 +153,53 @@ describe('cli modifyFileSystemRoutes', () => {
164
153
  routes,
165
154
  });
166
155
 
167
- return serializeRoutesForGolden(result.routes);
156
+ return result.routes;
168
157
  };
169
158
 
170
- test('matches checked-in localised routes golden output', async () => {
171
- const localisedRoutes = generateLocalisedRoutesGolden();
172
- const repeatedLocalisedRoutes = generateLocalisedRoutesGolden();
159
+ test('creates deterministic localized aliases with canonical route identity', () => {
160
+ const localisedRoutes = generateLocalisedRoutes();
161
+ expect(generateLocalisedRoutes()).toEqual(localisedRoutes);
173
162
 
174
- expect(repeatedLocalisedRoutes).toBe(localisedRoutes);
175
-
176
- if (process.env.UPDATE_I18N_LOCALISED_ROUTES_GOLDEN === '1') {
177
- await mkdir(path.dirname(localisedRoutesGoldenPath), { recursive: true });
178
- await writeFile(localisedRoutesGoldenPath, localisedRoutes);
163
+ const [localeLayout] = localisedRoutes;
164
+ expect(localeLayout.children?.map(route => route.path)).toEqual([
165
+ 'about',
166
+ 'o-nas',
167
+ 'ueber-uns',
168
+ 'products',
169
+ 'produkty',
170
+ 'produkte',
171
+ 'docs',
172
+ 'dokumenty',
173
+ 'dokumente',
174
+ ]);
175
+ expect(localeLayout.children?.slice(0, 3)).toMatchObject([
176
+ {
177
+ _component: 'about.tsx',
178
+ modernCanonicalPath: '/about',
179
+ path: 'about',
180
+ },
181
+ {
182
+ _component: 'about.tsx',
183
+ modernCanonicalPath: '/about',
184
+ path: 'o-nas',
185
+ },
186
+ {
187
+ _component: 'about.tsx',
188
+ modernCanonicalPath: '/about',
189
+ path: 'ueber-uns',
190
+ },
191
+ ]);
192
+ for (const productRoute of localeLayout.children?.slice(3, 6) ?? []) {
193
+ expect(productRoute).toMatchObject({
194
+ _component: 'products.tsx',
195
+ modernCanonicalPath: '/products',
196
+ });
197
+ expect(productRoute.children?.[0]).toMatchObject({
198
+ _component: ':slug.tsx',
199
+ modernCanonicalPath: '/products/:slug',
200
+ path: ':slug',
201
+ });
179
202
  }
180
-
181
- await expect(readFile(localisedRoutesGoldenPath, 'utf8')).resolves.toBe(
182
- localisedRoutes,
183
- );
184
203
  });
185
204
 
186
205
  test('upstream-style configs without a map keep routes untouched', () => {
@@ -1,48 +1,198 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { basename, resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
4
5
  import { describe, expect, test } from '@rstest/core';
6
+ import { build } from 'esbuild';
7
+ import i18next from 'i18next';
8
+ import { createElement } from 'react';
9
+ import { renderToStaticMarkup } from 'react-dom/server';
10
+ import { i18nPlugin as noReactI18nextPlugin } from '../src/runtime/no-react-i18next';
5
11
 
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
12
  const runtimeRoot = resolve(__dirname, '../src/runtime');
8
13
 
9
- const readRuntimeSource = (file: string) =>
10
- readFileSync(resolve(runtimeRoot, file), 'utf8').replace(/\r\n/gu, '\n');
11
-
12
14
  describe('react-i18next runtime boundary', () => {
13
- test('keeps the disabled runtime entry free of the optional adapter edge', () => {
14
- const noReactEntry = readRuntimeSource('no-react-i18next.tsx');
15
- const core = readRuntimeSource('core.tsx');
16
-
17
- expect(noReactEntry).not.toContain('./i18n/react-i18next');
18
- expect(core).not.toContain('./i18n/react-i18next');
19
- expect(core).not.toContain("import('react-i18next')");
15
+ test('bundles the disabled runtime entry when react-i18next is unavailable', async () => {
16
+ await expect(
17
+ build({
18
+ bundle: true,
19
+ entryPoints: [resolve(runtimeRoot, 'no-react-i18next.tsx')],
20
+ format: 'esm',
21
+ packages: 'external',
22
+ platform: 'node',
23
+ plugins: [
24
+ {
25
+ name: 'reject-react-i18next',
26
+ setup(buildApi) {
27
+ buildApi.onResolve({ filter: /^react-i18next$/ }, () => {
28
+ throw new Error(
29
+ 'disabled runtime entry reached optional react-i18next',
30
+ );
31
+ });
32
+ },
33
+ },
34
+ ],
35
+ write: false,
36
+ }),
37
+ ).resolves.toBeDefined();
20
38
  });
21
39
 
22
- test('keeps the runtime plugin factory entry synchronous for federation', () => {
23
- const core = readRuntimeSource('core.tsx');
24
- const pluginSetup = readRuntimeSource('pluginSetup.ts');
40
+ test('bundles the default runtime entry with the react-i18next adapter', async () => {
41
+ let adapterResolutions = 0;
42
+ await build({
43
+ bundle: true,
44
+ entryPoints: [resolve(runtimeRoot, 'index.tsx')],
45
+ format: 'esm',
46
+ packages: 'external',
47
+ platform: 'node',
48
+ plugins: [
49
+ {
50
+ name: 'observe-react-i18next',
51
+ setup(buildApi) {
52
+ buildApi.onResolve({ filter: /^react-i18next$/ }, args => {
53
+ adapterResolutions += 1;
54
+ return { external: true, path: args.path };
55
+ });
56
+ },
57
+ },
58
+ ],
59
+ write: false,
60
+ });
61
+
62
+ expect(adapterResolutions).toBeGreaterThan(0);
63
+ });
25
64
 
26
- expect(core).not.toContain("from './i18n/backend/middleware'");
27
- expect(core).not.toContain("from './i18n/utils'");
28
- expect(pluginSetup).not.toContain("from './i18n/backend/middleware'");
29
- expect(pluginSetup).not.toContain("from './i18n/utils'");
30
- expect(pluginSetup).toContain("import('./i18n/backend/middleware')");
31
- expect(pluginSetup).toContain("import('./i18n/utils')");
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();
32
91
  });
33
92
 
34
- test('keeps the Modern i18n context stable across federated runtime copies', () => {
35
- const context = readRuntimeSource('context.tsx');
93
+ test('keeps expensive lifecycle helpers behind asynchronous bundle edges', async () => {
94
+ const result = await build({
95
+ bundle: true,
96
+ entryPoints: [resolve(runtimeRoot, 'pluginSetup.ts')],
97
+ format: 'esm',
98
+ metafile: true,
99
+ outdir: 'out',
100
+ packages: 'external',
101
+ platform: 'node',
102
+ splitting: true,
103
+ write: false,
104
+ });
105
+
106
+ const entryOutput = Object.values(result.metafile.outputs).find(output =>
107
+ output.entryPoint?.endsWith('/pluginSetup.ts'),
108
+ );
109
+ if (!entryOutput) {
110
+ throw new Error('pluginSetup entry output was not generated');
111
+ }
112
+ const dynamicOutputNames = entryOutput.imports
113
+ .filter(moduleImport => moduleImport.kind === 'dynamic-import')
114
+ .map(moduleImport => basename(moduleImport.path));
115
+ const dynamicOutputs = Object.entries(result.metafile.outputs)
116
+ .filter(([outputPath]) =>
117
+ dynamicOutputNames.includes(basename(outputPath)),
118
+ )
119
+ .map(([, output]) => output);
120
+ const dynamicEntries = dynamicOutputs.map(output =>
121
+ basename(output.entryPoint ?? ''),
122
+ );
36
123
 
37
- expect(context).toContain(
38
- "Symbol.for(\n '@modern-js/plugin-i18n/runtime/ModernI18nContext'",
124
+ expect(dynamicEntries).toEqual(
125
+ expect.arrayContaining(['middleware.ts', 'utils.ts']),
39
126
  );
40
- expect(context).toContain('globalStore[modernI18nContextKey] ??=');
41
127
  });
42
128
 
43
- test('keeps the default runtime entry wired to react-i18next integration', () => {
44
- const defaultEntry = readRuntimeSource('index.tsx');
129
+ test('registers plugin lifecycle hooks synchronously', () => {
130
+ let beforeRender: unknown;
131
+ let rootWrapper: unknown;
132
+ const plugin = noReactI18nextPlugin({ reactI18next: false });
133
+ const setupResult = plugin.setup?.({
134
+ getRuntimeConfig: () => ({}),
135
+ onBeforeRender: hook => {
136
+ beforeRender = hook;
137
+ },
138
+ wrapRoot: wrapper => {
139
+ rootWrapper = wrapper;
140
+ },
141
+ } as any);
142
+
143
+ expect(setupResult).toBeUndefined();
144
+ expect(typeof beforeRender).toBe('function');
145
+ expect(typeof rootWrapper).toBe('function');
146
+ });
147
+
148
+ test('shares Modern i18n context across independently bundled runtime copies', async () => {
149
+ const tempDir = await mkdtemp(
150
+ resolve(__dirname, '.modern-i18n-runtime-boundary-'),
151
+ );
152
+ try {
153
+ const contextEntry = resolve(runtimeRoot, 'context.tsx');
154
+ const copies = await Promise.all(
155
+ ['copy-a.mjs', 'copy-b.mjs'].map(async filename => {
156
+ const outfile = resolve(tempDir, filename);
157
+ await build({
158
+ bundle: true,
159
+ entryPoints: [contextEntry],
160
+ format: 'esm',
161
+ jsx: 'automatic',
162
+ outfile,
163
+ packages: 'external',
164
+ platform: 'node',
165
+ });
166
+ return import(pathToFileURL(outfile).href);
167
+ }),
168
+ );
169
+ const [copyA, copyB] = copies;
170
+ const instance = i18next.createInstance();
171
+ await instance.init({
172
+ initImmediate: false,
173
+ lng: 'cs',
174
+ resources: { cs: { translation: { language: 'Jazyk' } } },
175
+ });
176
+ const Consumer = () =>
177
+ createElement('span', null, copyB.useModernI18n().language);
178
+
179
+ const html = renderToStaticMarkup(
180
+ createElement(
181
+ copyA.ModernI18nProvider,
182
+ {
183
+ value: {
184
+ i18nInstance: instance,
185
+ language: 'cs',
186
+ languages: ['en', 'cs'],
187
+ },
188
+ },
189
+ createElement(Consumer),
190
+ ),
191
+ );
45
192
 
46
- expect(defaultEntry).toContain('./i18n/react-i18next');
193
+ expect(html).toBe('<span>cs</span>');
194
+ } finally {
195
+ await rm(tempDir, { force: true, recursive: true });
196
+ }
47
197
  });
48
198
  });
@@ -20,6 +20,7 @@ import { I18nLink } from '../src/runtime/I18nLink';
20
20
  import type { I18nInstance } from '../src/runtime/i18n';
21
21
  import { getReactI18nextIntegration } from '../src/runtime/i18n/react-i18next';
22
22
  import { createI18nRootWrapper } from '../src/runtime/providerComposition';
23
+ import { useI18nRouterAdapter } from '../src/runtime/routerAdapter';
23
24
 
24
25
  (
25
26
  globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
@@ -244,9 +245,68 @@ function createMutableTanstackRouter(pathname = '/en') {
244
245
  listener();
245
246
  }
246
247
  },
248
+ publishStateUpdate() {
249
+ for (const listener of listeners) {
250
+ listener();
251
+ }
252
+ },
247
253
  };
248
254
  }
249
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
+
250
310
  function createDeferredI18nInstance() {
251
311
  const resources = {
252
312
  en: { languageSwitcher: 'Language' },
@@ -459,6 +519,53 @@ describe('i18n runtime wrapRoot', () => {
459
519
  expect(rendered.container.textContent).toContain('router content');
460
520
  });
461
521
 
522
+ test('keeps the i18next provider instance stable across unrelated parent renders', async () => {
523
+ const i18nInstance = createI18nInstance('cs');
524
+ const providerInstances: I18nInstance[] = [];
525
+ const I18nextProvider = ({
526
+ children,
527
+ i18n,
528
+ }: PropsWithChildren<{ i18n: I18nInstance }>) => {
529
+ providerInstances.push(i18n);
530
+ return <>{children}</>;
531
+ };
532
+ const App = () => <main>router content</main>;
533
+ const I18nRoot = createI18nRootWrapper({
534
+ htmlLangAttr: false,
535
+ localePathRedirect: false,
536
+ languages: ['en', 'cs'],
537
+ fallbackLanguage: 'en',
538
+ getLatestI18nInstance: () => i18nInstance,
539
+ getI18nextProvider: () => I18nextProvider,
540
+ })(App);
541
+ const Parent = () => {
542
+ const [renderVersion, setRenderVersion] = useState(0);
543
+ return (
544
+ <>
545
+ <button
546
+ type="button"
547
+ onClick={() => setRenderVersion(version => version + 1)}
548
+ >
549
+ Render {renderVersion}
550
+ </button>
551
+ <I18nRoot renderVersion={renderVersion} />
552
+ </>
553
+ );
554
+ };
555
+
556
+ rendered = await renderI18nRoot(<Parent />);
557
+ const initialProviderInstance = providerInstances.at(-1);
558
+
559
+ await act(async () => {
560
+ rendered?.container
561
+ .querySelector('button')
562
+ ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
563
+ });
564
+
565
+ expect(providerInstances.length).toBeGreaterThan(1);
566
+ expect(providerInstances.at(-1)).toBe(initialProviderInstance);
567
+ });
568
+
462
569
  test('commits route language and translated copy after the instance changes', async () => {
463
570
  window.history.replaceState(null, '', '/en');
464
571
  const router = createMutableTanstackRouter();
@@ -762,6 +869,83 @@ describe('i18n router adapter', () => {
762
869
  expect(link?.getAttribute('data-router-link')).toBe('tanstack');
763
870
  });
764
871
 
872
+ test('ignores router state updates that do not change location or params', async () => {
873
+ const router = createMutableTanstackRouter('/en');
874
+ let renders = 0;
875
+ const LocationProbe = () => {
876
+ renders += 1;
877
+ const { location } = useI18nRouterAdapter();
878
+ return <output>{location?.pathname}</output>;
879
+ };
880
+
881
+ rendered = await renderWithRuntime(
882
+ <LocationProbe />,
883
+ createTanstackRuntimeContext(router),
884
+ );
885
+ const initialRenders = renders;
886
+
887
+ await act(async () => {
888
+ router.publishStateUpdate();
889
+ });
890
+ expect(renders).toBe(initialRenders);
891
+ expect(rendered.container.textContent).toBe('/en');
892
+
893
+ await act(async () => {
894
+ router.publishPathname('/cs');
895
+ });
896
+ expect(renders).toBe(initialRenders + 1);
897
+ expect(rendered.container.textContent).toBe('/cs');
898
+ });
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
+
765
949
  test('forwards warmup props through I18nLink with a localized string target', async () => {
766
950
  const router = createTanstackRouter('/cs/podminky-pouzivani', 'cs');
767
951
  rendered = await renderWithRuntime(
@@ -1,156 +0,0 @@
1
- [
2
- {
3
- "id": ":lang",
4
- "path": ":lang",
5
- "type": "nested",
6
- "origin": "file-system",
7
- "routeType": "layout",
8
- "_component": ":lang.tsx",
9
- "children": [
10
- {
11
- "id": "about",
12
- "path": "about",
13
- "type": "nested",
14
- "origin": "file-system",
15
- "routeType": "page",
16
- "_component": "about.tsx",
17
- "modernCanonicalPath": "/about"
18
- },
19
- {
20
- "id": "about__localised_o-nas",
21
- "path": "o-nas",
22
- "type": "nested",
23
- "origin": "file-system",
24
- "routeType": "page",
25
- "_component": "about.tsx",
26
- "modernCanonicalPath": "/about"
27
- },
28
- {
29
- "id": "about__localised_ueber-uns",
30
- "path": "ueber-uns",
31
- "type": "nested",
32
- "origin": "file-system",
33
- "routeType": "page",
34
- "_component": "about.tsx",
35
- "modernCanonicalPath": "/about"
36
- },
37
- {
38
- "id": "products",
39
- "path": "products",
40
- "type": "nested",
41
- "origin": "file-system",
42
- "routeType": "layout",
43
- "_component": "products.tsx",
44
- "children": [
45
- {
46
- "id": ":slug",
47
- "path": ":slug",
48
- "type": "nested",
49
- "origin": "file-system",
50
- "routeType": "page",
51
- "_component": ":slug.tsx",
52
- "modernCanonicalPath": "/products/:slug"
53
- }
54
- ],
55
- "modernCanonicalPath": "/products"
56
- },
57
- {
58
- "id": "products__localised_produkty",
59
- "path": "produkty",
60
- "type": "nested",
61
- "origin": "file-system",
62
- "routeType": "layout",
63
- "_component": "products.tsx",
64
- "children": [
65
- {
66
- "id": ":slug__localised_produkty",
67
- "path": ":slug",
68
- "type": "nested",
69
- "origin": "file-system",
70
- "routeType": "page",
71
- "_component": ":slug.tsx",
72
- "modernCanonicalPath": "/products/:slug"
73
- }
74
- ],
75
- "modernCanonicalPath": "/products"
76
- },
77
- {
78
- "id": "products__localised_produkte",
79
- "path": "produkte",
80
- "type": "nested",
81
- "origin": "file-system",
82
- "routeType": "layout",
83
- "_component": "products.tsx",
84
- "children": [
85
- {
86
- "id": ":slug__localised_produkte",
87
- "path": ":slug",
88
- "type": "nested",
89
- "origin": "file-system",
90
- "routeType": "page",
91
- "_component": ":slug.tsx",
92
- "modernCanonicalPath": "/products/:slug"
93
- }
94
- ],
95
- "modernCanonicalPath": "/products"
96
- },
97
- {
98
- "id": "docs",
99
- "path": "docs",
100
- "type": "nested",
101
- "origin": "file-system",
102
- "routeType": "layout",
103
- "_component": "docs.tsx",
104
- "children": [
105
- {
106
- "id": "*",
107
- "path": "*",
108
- "type": "nested",
109
- "origin": "file-system",
110
- "routeType": "page",
111
- "_component": "*.tsx"
112
- }
113
- ],
114
- "modernCanonicalPath": "/docs"
115
- },
116
- {
117
- "id": "docs__localised_dokumenty",
118
- "path": "dokumenty",
119
- "type": "nested",
120
- "origin": "file-system",
121
- "routeType": "layout",
122
- "_component": "docs.tsx",
123
- "children": [
124
- {
125
- "id": "*__localised_dokumenty",
126
- "path": "*",
127
- "type": "nested",
128
- "origin": "file-system",
129
- "routeType": "page",
130
- "_component": "*.tsx"
131
- }
132
- ],
133
- "modernCanonicalPath": "/docs"
134
- },
135
- {
136
- "id": "docs__localised_dokumente",
137
- "path": "dokumente",
138
- "type": "nested",
139
- "origin": "file-system",
140
- "routeType": "layout",
141
- "_component": "docs.tsx",
142
- "children": [
143
- {
144
- "id": "*__localised_dokumente",
145
- "path": "*",
146
- "type": "nested",
147
- "origin": "file-system",
148
- "routeType": "page",
149
- "_component": "*.tsx"
150
- }
151
- ],
152
- "modernCanonicalPath": "/docs"
153
- }
154
- ]
155
- }
156
- ]