@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21
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/cli/index.js +10 -21
- package/dist/cjs/cli/localisedRoutes.js +47 -0
- package/dist/cjs/runtime/Link.js +19 -32
- package/dist/cjs/runtime/context.js +30 -73
- package/dist/cjs/runtime/contextHelpers.js +5 -5
- package/dist/cjs/runtime/contexts.js +49 -0
- package/dist/cjs/runtime/core.js +27 -14
- package/dist/cjs/runtime/hooks.js +7 -8
- package/dist/cjs/runtime/localizedPaths.js +11 -9
- package/dist/cjs/runtime/navigation.js +48 -0
- package/dist/cjs/runtime/providerComposition.js +37 -13
- package/dist/cjs/runtime/routerAdapter.js +33 -140
- package/dist/cjs/runtime/sharedContexts.js +43 -0
- package/dist/cjs/runtime/utils.js +21 -19
- package/dist/cjs/server/defaultUrlStrategy.js +48 -0
- package/dist/cjs/server/index.js +11 -7
- package/dist/cjs/server/mappedUrlStrategy.js +43 -0
- package/dist/cjs/server/redirectPolicy.js +33 -24
- package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
- package/dist/cjs/shared/urlStrategy.js +48 -0
- package/dist/esm/cli/index.mjs +10 -21
- package/dist/esm/cli/localisedRoutes.mjs +9 -0
- package/dist/esm/runtime/Link.mjs +19 -32
- package/dist/esm/runtime/context.mjs +20 -72
- package/dist/esm/runtime/contextHelpers.mjs +5 -5
- package/dist/esm/runtime/contexts.mjs +5 -0
- package/dist/esm/runtime/core.mjs +13 -6
- package/dist/esm/runtime/hooks.mjs +7 -8
- package/dist/esm/runtime/localizedPaths.mjs +11 -9
- package/dist/esm/runtime/navigation.mjs +7 -0
- package/dist/esm/runtime/providerComposition.mjs +38 -14
- package/dist/esm/runtime/routerAdapter.mjs +28 -141
- package/dist/esm/runtime/sharedContexts.mjs +1 -0
- package/dist/esm/runtime/utils.mjs +19 -17
- package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
- package/dist/esm/server/index.mjs +11 -7
- package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
- package/dist/esm/server/redirectPolicy.mjs +33 -24
- package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
- package/dist/esm/shared/urlStrategy.mjs +7 -0
- package/dist/esm-node/cli/index.mjs +10 -21
- package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
- package/dist/esm-node/runtime/Link.mjs +19 -32
- package/dist/esm-node/runtime/context.mjs +20 -72
- package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
- package/dist/esm-node/runtime/contexts.mjs +6 -0
- package/dist/esm-node/runtime/core.mjs +13 -6
- package/dist/esm-node/runtime/hooks.mjs +7 -8
- package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
- package/dist/esm-node/runtime/navigation.mjs +8 -0
- package/dist/esm-node/runtime/providerComposition.mjs +38 -14
- package/dist/esm-node/runtime/routerAdapter.mjs +28 -141
- package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
- package/dist/esm-node/runtime/utils.mjs +19 -17
- package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
- package/dist/esm-node/server/index.mjs +11 -7
- package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
- package/dist/esm-node/server/redirectPolicy.mjs +33 -24
- package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
- package/dist/esm-node/shared/urlStrategy.mjs +8 -0
- package/dist/types/cli/localisedRoutes.d.ts +19 -0
- package/dist/types/runtime/context.d.ts +8 -11
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/contexts.d.ts +6 -0
- package/dist/types/runtime/core.d.ts +12 -2
- package/dist/types/runtime/hooks.d.ts +4 -5
- package/dist/types/runtime/localizedPaths.d.ts +4 -4
- package/dist/types/runtime/navigation.d.ts +10 -0
- package/dist/types/runtime/providerComposition.d.ts +13 -1
- package/dist/types/runtime/routerAdapter.d.ts +23 -6
- package/dist/types/runtime/sharedContexts.d.ts +1 -0
- package/dist/types/runtime/utils.d.ts +4 -12
- package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
- package/dist/types/server/redirectPolicy.d.ts +5 -13
- package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
- package/dist/types/shared/type.d.ts +7 -11
- package/dist/types/shared/urlStrategy.d.ts +18 -0
- package/package.json +42 -19
- package/rslib.config.mts +8 -1
- package/rstest.config.mts +6 -5
- package/src/cli/index.ts +42 -42
- package/src/cli/localisedRoutes.ts +53 -0
- package/src/runtime/Link.tsx +20 -40
- package/src/runtime/context.tsx +33 -157
- package/src/runtime/contextHelpers.ts +7 -5
- package/src/runtime/contexts.ts +16 -0
- package/src/runtime/core.tsx +53 -6
- package/src/runtime/hooks.ts +14 -9
- package/src/runtime/i18n/instance.ts +1 -2
- package/src/runtime/localizedPaths.ts +24 -19
- package/src/runtime/navigation.tsx +20 -0
- package/src/runtime/providerComposition.tsx +71 -23
- package/src/runtime/routerAdapter.tsx +65 -339
- package/src/runtime/sharedContexts.ts +8 -0
- package/src/runtime/utils.ts +49 -31
- package/src/server/defaultUrlStrategy.ts +27 -0
- package/src/server/index.ts +39 -9
- package/src/server/mappedUrlStrategy.ts +10 -0
- package/src/server/redirectPolicy.ts +85 -70
- package/src/shared/mappedUrlStrategy.ts +59 -0
- package/src/shared/type.ts +7 -11
- package/src/shared/urlStrategy.ts +38 -0
- package/tests/backendDefaults.test.ts +2 -23
- package/tests/defaultUrlStrategy.test.ts +107 -0
- package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
- package/tests/i18nUtils.test.ts +31 -65
- package/tests/link.test.tsx +13 -511
- package/tests/localisedRouteGeneration.test.ts +200 -0
- package/tests/localisedUrls.test.ts +16 -208
- package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
- package/tests/navigation.test.tsx +403 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +8 -83
- package/tests/redirectPolicy.test.ts +89 -27
- package/tests/routerAdapter.test.tsx +98 -1171
- package/tests/urlStrategySerialization.test.ts +122 -0
- package/tsconfig.json +4 -2
- package/tests/federatedI18nBoundary.client.test.tsx +0 -129
- package/tests/federatedI18nBoundary.test.tsx +0 -132
- package/tests/linkTypes.test.ts +0 -59
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
- package/tests/peerCohort.test.ts +0 -25
- package/tests/type-fixture/fixture-globals.d.ts +0 -11
- package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
- package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
- package/tests/type-fixture/tsconfig.json +0 -16
|
@@ -1,56 +1,32 @@
|
|
|
1
1
|
import {
|
|
2
|
-
applyRouterRuntimeState,
|
|
3
2
|
InternalRuntimeContext,
|
|
4
3
|
RuntimeContext,
|
|
5
4
|
} from '@modern-js/runtime/context';
|
|
6
|
-
import {
|
|
7
|
-
createMemoryRouter,
|
|
8
|
-
Link as ReactRouterLink,
|
|
9
|
-
RouterProvider,
|
|
10
|
-
} from '@modern-js/runtime/router';
|
|
11
|
-
import i18next from 'i18next';
|
|
12
5
|
import type React from 'react';
|
|
13
|
-
import type {
|
|
14
|
-
import { act,
|
|
6
|
+
import type { PropsWithChildren } from 'react';
|
|
7
|
+
import { act, useEffect } from 'react';
|
|
15
8
|
import { createRoot, type Root } from 'react-dom/client';
|
|
16
|
-
import { useTranslation } from 'react-i18next';
|
|
17
|
-
import { i18nPlugin } from '../src/runtime';
|
|
18
9
|
import { ModernI18nProvider, useModernI18n } from '../src/runtime/context';
|
|
19
|
-
import { I18nLink } from '../src/runtime/I18nLink';
|
|
20
10
|
import type { I18nInstance } from '../src/runtime/i18n';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
11
|
+
import { Link } from '../src/runtime/Link';
|
|
12
|
+
import {
|
|
13
|
+
createI18nRootWrapper,
|
|
14
|
+
type I18nLanguageSynchronizationProps,
|
|
15
|
+
} from '../src/runtime/providerComposition';
|
|
16
|
+
import {
|
|
17
|
+
I18nNavigationProvider,
|
|
18
|
+
useI18nRouterAdapter,
|
|
19
|
+
} from '../src/runtime/routerAdapter';
|
|
24
20
|
|
|
25
21
|
(
|
|
26
22
|
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
27
23
|
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
28
24
|
|
|
29
|
-
const localisedUrls = {
|
|
30
|
-
'/terms-of-service': {
|
|
31
|
-
en: '/terms-of-service',
|
|
32
|
-
cs: '/podminky-pouzivani',
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
|
|
36
25
|
const requestContext = {
|
|
37
26
|
request: {},
|
|
38
27
|
response: {},
|
|
39
28
|
};
|
|
40
29
|
|
|
41
|
-
const capturedTanstackLinkProps: any[] = [];
|
|
42
|
-
|
|
43
|
-
const TanstackLink = ({ to, children, ...props }: any) => {
|
|
44
|
-
capturedTanstackLinkProps.push({ to, ...props });
|
|
45
|
-
const { prefetch: _prefetch, preload: _preload, ...anchorProps } = props;
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<a href={to} data-router-link="tanstack" {...anchorProps}>
|
|
49
|
-
{children}
|
|
50
|
-
</a>
|
|
51
|
-
);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
30
|
function createI18nInstance(language = 'en'): I18nInstance {
|
|
55
31
|
return {
|
|
56
32
|
language,
|
|
@@ -66,308 +42,6 @@ function createI18nInstance(language = 'en'): I18nInstance {
|
|
|
66
42
|
};
|
|
67
43
|
}
|
|
68
44
|
|
|
69
|
-
function createRuntimeContext(
|
|
70
|
-
router: unknown,
|
|
71
|
-
framework: 'tanstack' | 'react-router',
|
|
72
|
-
) {
|
|
73
|
-
const context = {
|
|
74
|
-
isBrowser: true,
|
|
75
|
-
requestContext,
|
|
76
|
-
context: requestContext,
|
|
77
|
-
router: {
|
|
78
|
-
...(framework === 'tanstack'
|
|
79
|
-
? { Link: TanstackLink, useRouter: () => router }
|
|
80
|
-
: { useLocation: () => undefined, useHref: () => undefined }),
|
|
81
|
-
},
|
|
82
|
-
} as any;
|
|
83
|
-
|
|
84
|
-
applyRouterRuntimeState(context, {
|
|
85
|
-
framework,
|
|
86
|
-
instance: router,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
return context;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function createTanstackRuntimeContext(router: unknown) {
|
|
93
|
-
return createRuntimeContext(router, 'tanstack');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function createReactRouterRuntimeContext(router: unknown) {
|
|
97
|
-
return createRuntimeContext(router, 'react-router');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function collectI18nWrapRoot() {
|
|
101
|
-
let wrapRoot: ((App: ComponentType<any>) => ComponentType<any>) | undefined;
|
|
102
|
-
|
|
103
|
-
i18nPlugin({
|
|
104
|
-
reactI18next: false,
|
|
105
|
-
localeDetection: {
|
|
106
|
-
fallbackLanguage: 'en',
|
|
107
|
-
},
|
|
108
|
-
}).setup?.({
|
|
109
|
-
getRuntimeConfig: () => ({}),
|
|
110
|
-
onBeforeRender: () => undefined,
|
|
111
|
-
wrapRoot: (callback: (App: ComponentType<any>) => ComponentType<any>) => {
|
|
112
|
-
wrapRoot = callback;
|
|
113
|
-
},
|
|
114
|
-
} as any);
|
|
115
|
-
|
|
116
|
-
if (!wrapRoot) {
|
|
117
|
-
throw new Error('Expected i18n runtime plugin to register wrapRoot');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return wrapRoot;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function collectI18nRuntime(
|
|
124
|
-
i18nInstance: I18nInstance,
|
|
125
|
-
reactI18next = false,
|
|
126
|
-
) {
|
|
127
|
-
let onBeforeRender:
|
|
128
|
-
| ((
|
|
129
|
-
context: ReturnType<typeof createTanstackRuntimeContext>,
|
|
130
|
-
) => Promise<void>)
|
|
131
|
-
| undefined;
|
|
132
|
-
let wrapRoot: ((App: ComponentType<any>) => ComponentType<any>) | undefined;
|
|
133
|
-
|
|
134
|
-
i18nPlugin({
|
|
135
|
-
i18nInstance,
|
|
136
|
-
reactI18next,
|
|
137
|
-
localeDetection: {
|
|
138
|
-
fallbackLanguage: 'en',
|
|
139
|
-
i18nextDetector: false,
|
|
140
|
-
languages: ['en', 'cs'],
|
|
141
|
-
localePathRedirect: true,
|
|
142
|
-
},
|
|
143
|
-
}).setup?.({
|
|
144
|
-
getRuntimeConfig: () => ({}),
|
|
145
|
-
onBeforeRender: (
|
|
146
|
-
callback: (
|
|
147
|
-
context: ReturnType<typeof createTanstackRuntimeContext>,
|
|
148
|
-
) => Promise<void>,
|
|
149
|
-
) => {
|
|
150
|
-
onBeforeRender = callback;
|
|
151
|
-
},
|
|
152
|
-
wrapRoot: (callback: (App: ComponentType<any>) => ComponentType<any>) => {
|
|
153
|
-
wrapRoot = callback;
|
|
154
|
-
},
|
|
155
|
-
} as any);
|
|
156
|
-
|
|
157
|
-
if (!onBeforeRender || !wrapRoot) {
|
|
158
|
-
throw new Error('Expected i18n runtime plugin lifecycle registrations');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return { onBeforeRender, wrapRoot };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function createEventEmittingDeferredI18nInstance() {
|
|
165
|
-
const instance = i18next.createInstance();
|
|
166
|
-
await instance.init({
|
|
167
|
-
lng: 'en',
|
|
168
|
-
fallbackLng: 'en',
|
|
169
|
-
resources: {
|
|
170
|
-
en: { translation: { languageSwitcher: 'Language' } },
|
|
171
|
-
cs: { translation: { languageSwitcher: 'Jazyk' } },
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
const originalHasLoadedNamespace = instance.hasLoadedNamespace;
|
|
175
|
-
instance.hasLoadedNamespace = function hasLoadedNamespace(
|
|
176
|
-
namespace,
|
|
177
|
-
options,
|
|
178
|
-
) {
|
|
179
|
-
if (this !== instance) {
|
|
180
|
-
throw new Error('i18next hasLoadedNamespace receiver was not preserved');
|
|
181
|
-
}
|
|
182
|
-
return originalHasLoadedNamespace.call(this, namespace, options);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const pending: Array<{
|
|
186
|
-
language: string;
|
|
187
|
-
promise: Promise<unknown>;
|
|
188
|
-
resolve: () => void;
|
|
189
|
-
}> = [];
|
|
190
|
-
const translator = (instance as unknown as { translator: I18nInstance })
|
|
191
|
-
.translator;
|
|
192
|
-
|
|
193
|
-
instance.changeLanguage = rstest.fn((language = 'en') => {
|
|
194
|
-
let resolveChange!: (value: unknown) => void;
|
|
195
|
-
const promise = new Promise<unknown>(resolve => {
|
|
196
|
-
resolveChange = resolve;
|
|
197
|
-
});
|
|
198
|
-
pending.push({
|
|
199
|
-
language,
|
|
200
|
-
promise,
|
|
201
|
-
resolve: () => {
|
|
202
|
-
instance.language = language;
|
|
203
|
-
translator.changeLanguage?.(language);
|
|
204
|
-
instance.emit('languageChanged', language);
|
|
205
|
-
resolveChange(instance.t.bind(instance));
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
return promise;
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
return { instance, pending };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function createMutableTanstackRouter(pathname = '/en') {
|
|
215
|
-
let location = {
|
|
216
|
-
pathname,
|
|
217
|
-
searchStr: '',
|
|
218
|
-
hash: '',
|
|
219
|
-
};
|
|
220
|
-
let matches = [{ params: { lang: pathname.slice(1) } }];
|
|
221
|
-
const listeners = new Set<() => void>();
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
navigate: rstest.fn(async () => undefined),
|
|
225
|
-
stores: {
|
|
226
|
-
location: {
|
|
227
|
-
get: () => location,
|
|
228
|
-
subscribe: (listener: () => void) => {
|
|
229
|
-
listeners.add(listener);
|
|
230
|
-
return () => listeners.delete(listener);
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
matches: {
|
|
234
|
-
get: () => matches,
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
publishPathname(nextPathname: string) {
|
|
238
|
-
location = {
|
|
239
|
-
pathname: nextPathname,
|
|
240
|
-
searchStr: '',
|
|
241
|
-
hash: '',
|
|
242
|
-
};
|
|
243
|
-
matches = [{ params: { lang: nextPathname.slice(1) } }];
|
|
244
|
-
for (const listener of listeners) {
|
|
245
|
-
listener();
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
publishStateUpdate() {
|
|
249
|
-
for (const listener of listeners) {
|
|
250
|
-
listener();
|
|
251
|
-
}
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
}
|
|
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
|
-
|
|
310
|
-
function createDeferredI18nInstance() {
|
|
311
|
-
const resources = {
|
|
312
|
-
en: { languageSwitcher: 'Language' },
|
|
313
|
-
cs: { languageSwitcher: 'Jazyk' },
|
|
314
|
-
};
|
|
315
|
-
const pending: Array<{
|
|
316
|
-
language: keyof typeof resources;
|
|
317
|
-
promise: Promise<void>;
|
|
318
|
-
resolve: () => void;
|
|
319
|
-
reject: (error: Error) => void;
|
|
320
|
-
}> = [];
|
|
321
|
-
const instance = createI18nInstance('en');
|
|
322
|
-
delete instance.setLang;
|
|
323
|
-
instance.t = (key: string) =>
|
|
324
|
-
resources[instance.language as keyof typeof resources][
|
|
325
|
-
key as keyof (typeof resources)['en']
|
|
326
|
-
];
|
|
327
|
-
instance.changeLanguage = rstest.fn((language = 'en') => {
|
|
328
|
-
let resolveChange!: () => void;
|
|
329
|
-
let rejectChange!: (error: Error) => void;
|
|
330
|
-
const promise = new Promise<void>((resolve, reject) => {
|
|
331
|
-
resolveChange = resolve;
|
|
332
|
-
rejectChange = reject;
|
|
333
|
-
});
|
|
334
|
-
pending.push({
|
|
335
|
-
language: language as keyof typeof resources,
|
|
336
|
-
promise,
|
|
337
|
-
resolve: () => {
|
|
338
|
-
instance.language = language;
|
|
339
|
-
resolveChange();
|
|
340
|
-
},
|
|
341
|
-
reject: rejectChange,
|
|
342
|
-
});
|
|
343
|
-
return promise;
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
return { instance, pending };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function createTanstackRouter(pathname = '/en/terms-of-service', lang = 'en') {
|
|
350
|
-
const url = new URL(pathname, 'https://modernjs.test');
|
|
351
|
-
|
|
352
|
-
return {
|
|
353
|
-
navigate: rstest.fn(async () => undefined),
|
|
354
|
-
state: {
|
|
355
|
-
location: {
|
|
356
|
-
pathname: url.pathname,
|
|
357
|
-
searchStr: url.search,
|
|
358
|
-
hash: url.hash,
|
|
359
|
-
},
|
|
360
|
-
matches: [
|
|
361
|
-
{
|
|
362
|
-
params: {
|
|
363
|
-
lang,
|
|
364
|
-
},
|
|
365
|
-
},
|
|
366
|
-
],
|
|
367
|
-
},
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
|
|
371
45
|
async function renderI18nRoot(node: React.ReactNode) {
|
|
372
46
|
const container = document.createElement('div');
|
|
373
47
|
document.body.appendChild(container);
|
|
@@ -393,52 +67,6 @@ async function renderI18nRoot(node: React.ReactNode) {
|
|
|
393
67
|
};
|
|
394
68
|
}
|
|
395
69
|
|
|
396
|
-
async function renderWithRuntime(
|
|
397
|
-
node: React.ReactNode,
|
|
398
|
-
runtimeContext: ReturnType<typeof createTanstackRuntimeContext>,
|
|
399
|
-
) {
|
|
400
|
-
const container = document.createElement('div');
|
|
401
|
-
document.body.appendChild(container);
|
|
402
|
-
const root = createRoot(container);
|
|
403
|
-
|
|
404
|
-
await act(async () => {
|
|
405
|
-
root.render(
|
|
406
|
-
<InternalRuntimeContext.Provider value={runtimeContext}>
|
|
407
|
-
{node}
|
|
408
|
-
</InternalRuntimeContext.Provider>,
|
|
409
|
-
);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
return {
|
|
413
|
-
container,
|
|
414
|
-
root,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
async function renderWithRuntimeContexts(
|
|
419
|
-
node: React.ReactNode,
|
|
420
|
-
runtimeContext: ReturnType<typeof createTanstackRuntimeContext>,
|
|
421
|
-
) {
|
|
422
|
-
const container = document.createElement('div');
|
|
423
|
-
document.body.appendChild(container);
|
|
424
|
-
const root = createRoot(container);
|
|
425
|
-
|
|
426
|
-
await act(async () => {
|
|
427
|
-
root.render(
|
|
428
|
-
<InternalRuntimeContext.Provider value={runtimeContext}>
|
|
429
|
-
<RuntimeContext.Provider value={runtimeContext}>
|
|
430
|
-
{node}
|
|
431
|
-
</RuntimeContext.Provider>
|
|
432
|
-
</InternalRuntimeContext.Provider>,
|
|
433
|
-
);
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
return {
|
|
437
|
-
container,
|
|
438
|
-
root,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
70
|
function cleanup(rendered?: { container: HTMLElement; root: Root }) {
|
|
443
71
|
if (!rendered) {
|
|
444
72
|
return;
|
|
@@ -460,38 +88,6 @@ describe('i18n runtime wrapRoot', () => {
|
|
|
460
88
|
window.history.replaceState(null, '', '/');
|
|
461
89
|
});
|
|
462
90
|
|
|
463
|
-
test('renders children when no root App exists yet', async () => {
|
|
464
|
-
const wrapRoot = collectI18nWrapRoot();
|
|
465
|
-
const I18nRoot = wrapRoot(undefined as unknown as ComponentType<any>);
|
|
466
|
-
|
|
467
|
-
rendered = await renderI18nRoot(
|
|
468
|
-
<I18nRoot>
|
|
469
|
-
<main>router content</main>
|
|
470
|
-
</I18nRoot>,
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
expect(rendered.container.textContent).toContain('router content');
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
test('preserves App props and children', async () => {
|
|
477
|
-
const wrapRoot = collectI18nWrapRoot();
|
|
478
|
-
const App = ({ children, label }: PropsWithChildren<{ label: string }>) => (
|
|
479
|
-
<main data-label={label}>{children}</main>
|
|
480
|
-
);
|
|
481
|
-
const I18nRoot = wrapRoot(App);
|
|
482
|
-
|
|
483
|
-
rendered = await renderI18nRoot(
|
|
484
|
-
<I18nRoot label="root">
|
|
485
|
-
<span>router content</span>
|
|
486
|
-
</I18nRoot>,
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
expect(
|
|
490
|
-
rendered.container.querySelector('main')?.getAttribute('data-label'),
|
|
491
|
-
).toBe('root');
|
|
492
|
-
expect(rendered.container.textContent).toContain('router content');
|
|
493
|
-
});
|
|
494
|
-
|
|
495
91
|
test('keeps the optional i18next provider inside Modern i18n context', async () => {
|
|
496
92
|
const i18nInstance = createI18nInstance('cs');
|
|
497
93
|
const observedLanguages: string[] = [];
|
|
@@ -519,776 +115,107 @@ describe('i18n runtime wrapRoot', () => {
|
|
|
519
115
|
expect(observedLanguages).toEqual(['cs:cs']);
|
|
520
116
|
expect(rendered.container.textContent).toContain('router content');
|
|
521
117
|
});
|
|
522
|
-
|
|
523
|
-
test('keeps the i18next provider instance stable across unrelated parent renders', async () => {
|
|
524
|
-
const i18nInstance = createI18nInstance('cs');
|
|
525
|
-
const providerInstances: I18nInstance[] = [];
|
|
526
|
-
const I18nextProvider = ({
|
|
527
|
-
children,
|
|
528
|
-
i18n,
|
|
529
|
-
}: PropsWithChildren<{ i18n: I18nInstance }>) => {
|
|
530
|
-
providerInstances.push(i18n);
|
|
531
|
-
return <>{children}</>;
|
|
532
|
-
};
|
|
533
|
-
const App = () => <main>router content</main>;
|
|
534
|
-
const I18nRoot = createI18nRootWrapper({
|
|
535
|
-
htmlLangAttr: false,
|
|
536
|
-
localePathRedirect: false,
|
|
537
|
-
languages: ['en', 'cs'],
|
|
538
|
-
fallbackLanguage: 'en',
|
|
539
|
-
getLatestI18nInstance: () => i18nInstance,
|
|
540
|
-
getI18nextProvider: () => I18nextProvider,
|
|
541
|
-
})(App);
|
|
542
|
-
const Parent = () => {
|
|
543
|
-
const [renderVersion, setRenderVersion] = useState(0);
|
|
544
|
-
return (
|
|
545
|
-
<>
|
|
546
|
-
<button
|
|
547
|
-
type="button"
|
|
548
|
-
onClick={() => setRenderVersion(version => version + 1)}
|
|
549
|
-
>
|
|
550
|
-
Render {renderVersion}
|
|
551
|
-
</button>
|
|
552
|
-
<I18nRoot renderVersion={renderVersion} />
|
|
553
|
-
</>
|
|
554
|
-
);
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
rendered = await renderI18nRoot(<Parent />);
|
|
558
|
-
const initialProviderInstance = providerInstances.at(-1);
|
|
559
|
-
|
|
560
|
-
await act(async () => {
|
|
561
|
-
rendered?.container
|
|
562
|
-
.querySelector('button')
|
|
563
|
-
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
expect(providerInstances.length).toBeGreaterThan(1);
|
|
567
|
-
expect(providerInstances.at(-1)).toBe(initialProviderInstance);
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
test('commits route language and translated copy after the instance changes', async () => {
|
|
571
|
-
window.history.replaceState(null, '', '/en');
|
|
572
|
-
const router = createMutableTanstackRouter();
|
|
573
|
-
const runtimeContext = createTanstackRuntimeContext(router);
|
|
574
|
-
const { instance, pending } = createDeferredI18nInstance();
|
|
575
|
-
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(instance);
|
|
576
|
-
await onBeforeRender(runtimeContext);
|
|
577
|
-
|
|
578
|
-
const Translation = ({ id }: { id: string }) => {
|
|
579
|
-
const { language, t } = useModernI18n();
|
|
580
|
-
return (
|
|
581
|
-
<span data-testid={id}>
|
|
582
|
-
{language}:{t('languageSwitcher')}
|
|
583
|
-
</span>
|
|
584
|
-
);
|
|
585
|
-
};
|
|
586
|
-
const App = () => (
|
|
587
|
-
<main>
|
|
588
|
-
<Translation id="first" />
|
|
589
|
-
<Translation id="second" />
|
|
590
|
-
</main>
|
|
591
|
-
);
|
|
592
|
-
const I18nRoot = wrapRoot(App);
|
|
593
|
-
|
|
594
|
-
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
595
|
-
|
|
596
|
-
expect(rendered.container.textContent).toBe('en:Languageen:Language');
|
|
597
|
-
|
|
598
|
-
await act(async () => {
|
|
599
|
-
router.publishPathname('/cs');
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
expect(instance.changeLanguage).toHaveBeenCalledTimes(1);
|
|
603
|
-
expect(instance.changeLanguage).toHaveBeenCalledWith('cs');
|
|
604
|
-
expect(rendered.container.textContent).toBe('en:Languageen:Language');
|
|
605
|
-
|
|
606
|
-
await act(async () => {
|
|
607
|
-
pending[0].resolve();
|
|
608
|
-
await pending[0].promise;
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
expect(rendered.container.textContent).toBe('cs:Jazykcs:Jazyk');
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
test('uses the wrapper language operation without changing twice', async () => {
|
|
615
|
-
window.history.replaceState(null, '', '/en');
|
|
616
|
-
const router = createMutableTanstackRouter();
|
|
617
|
-
const runtimeContext = createTanstackRuntimeContext(router);
|
|
618
|
-
const instance = createI18nInstance();
|
|
619
|
-
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(instance);
|
|
620
|
-
await onBeforeRender(runtimeContext);
|
|
621
|
-
|
|
622
|
-
const App = () => {
|
|
623
|
-
const { language } = useModernI18n();
|
|
624
|
-
return <main>{language}</main>;
|
|
625
|
-
};
|
|
626
|
-
const I18nRoot = wrapRoot(App);
|
|
627
|
-
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
628
|
-
|
|
629
|
-
await act(async () => {
|
|
630
|
-
router.publishPathname('/cs');
|
|
631
|
-
await Promise.resolve();
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
expect(instance.setLang).toHaveBeenCalledTimes(1);
|
|
635
|
-
expect(instance.setLang).toHaveBeenCalledWith('cs');
|
|
636
|
-
expect(instance.changeLanguage).not.toHaveBeenCalled();
|
|
637
|
-
expect(rendered.container.textContent).toBe('cs');
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
test('keeps the latest route when language changes resolve out of order', async () => {
|
|
641
|
-
window.history.replaceState(null, '', '/en');
|
|
642
|
-
const router = createMutableTanstackRouter();
|
|
643
|
-
const runtimeContext = createTanstackRuntimeContext(router);
|
|
644
|
-
const { instance, pending } = createDeferredI18nInstance();
|
|
645
|
-
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(instance);
|
|
646
|
-
await onBeforeRender(runtimeContext);
|
|
647
|
-
|
|
648
|
-
const App = () => {
|
|
649
|
-
const { language, t } = useModernI18n();
|
|
650
|
-
return (
|
|
651
|
-
<main>
|
|
652
|
-
{language}:{t('languageSwitcher')}
|
|
653
|
-
</main>
|
|
654
|
-
);
|
|
655
|
-
};
|
|
656
|
-
const I18nRoot = wrapRoot(App);
|
|
657
|
-
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
658
|
-
|
|
659
|
-
await act(async () => {
|
|
660
|
-
router.publishPathname('/cs');
|
|
661
|
-
});
|
|
662
|
-
expect(pending).toHaveLength(1);
|
|
663
|
-
|
|
664
|
-
await act(async () => {
|
|
665
|
-
router.publishPathname('/en');
|
|
666
|
-
});
|
|
667
|
-
expect(rendered.container.textContent).toBe('en:Language');
|
|
668
|
-
|
|
669
|
-
await act(async () => {
|
|
670
|
-
pending[0].resolve();
|
|
671
|
-
await pending[0].promise;
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
expect(rendered.container.textContent).toBe('en:Language');
|
|
675
|
-
expect(pending).toHaveLength(2);
|
|
676
|
-
expect(pending[1].language).toBe('en');
|
|
677
|
-
|
|
678
|
-
await act(async () => {
|
|
679
|
-
pending[1].resolve();
|
|
680
|
-
await pending[1].promise;
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
expect(rendered.container.textContent).toBe('en:Language');
|
|
684
|
-
expect(instance.language).toBe('en');
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
test('does not block a replacement instance behind abandoned language work', async () => {
|
|
688
|
-
window.history.replaceState(null, '', '/en');
|
|
689
|
-
const router = createMutableTanstackRouter();
|
|
690
|
-
const runtimeContext = createTanstackRuntimeContext(router);
|
|
691
|
-
const oldI18n = createDeferredI18nInstance();
|
|
692
|
-
runtimeContext.i18nInstance = oldI18n.instance;
|
|
693
|
-
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(
|
|
694
|
-
oldI18n.instance,
|
|
695
|
-
);
|
|
696
|
-
await onBeforeRender(runtimeContext);
|
|
697
|
-
|
|
698
|
-
const App = () => {
|
|
699
|
-
const { language } = useModernI18n();
|
|
700
|
-
return <main>{language}</main>;
|
|
701
|
-
};
|
|
702
|
-
const I18nRoot = wrapRoot(App);
|
|
703
|
-
rendered = await renderWithRuntimeContexts(<I18nRoot />, runtimeContext);
|
|
704
|
-
|
|
705
|
-
await act(async () => {
|
|
706
|
-
router.publishPathname('/cs');
|
|
707
|
-
});
|
|
708
|
-
expect(oldI18n.pending).toHaveLength(1);
|
|
709
|
-
|
|
710
|
-
const replacementI18n = createDeferredI18nInstance();
|
|
711
|
-
const replacementContext = createTanstackRuntimeContext(router);
|
|
712
|
-
replacementContext.i18nInstance = replacementI18n.instance;
|
|
713
|
-
await act(async () => {
|
|
714
|
-
rendered?.root.render(
|
|
715
|
-
<InternalRuntimeContext.Provider value={replacementContext}>
|
|
716
|
-
<RuntimeContext.Provider value={replacementContext}>
|
|
717
|
-
<I18nRoot />
|
|
718
|
-
</RuntimeContext.Provider>
|
|
719
|
-
</InternalRuntimeContext.Provider>,
|
|
720
|
-
);
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
expect(replacementI18n.pending).toHaveLength(1);
|
|
724
|
-
expect(replacementI18n.pending[0].language).toBe('cs');
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
test('does not expose an obsolete route language through react-i18next', async () => {
|
|
728
|
-
window.history.replaceState(null, '', '/en');
|
|
729
|
-
const router = createMutableTanstackRouter();
|
|
730
|
-
const runtimeContext = createTanstackRuntimeContext(router);
|
|
731
|
-
const { instance, pending } =
|
|
732
|
-
await createEventEmittingDeferredI18nInstance();
|
|
733
|
-
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(
|
|
734
|
-
instance,
|
|
735
|
-
true,
|
|
736
|
-
);
|
|
737
|
-
await onBeforeRender(runtimeContext);
|
|
738
|
-
|
|
739
|
-
const App = () => {
|
|
740
|
-
const { t } = useTranslation();
|
|
741
|
-
return <main>{t('languageSwitcher')}</main>;
|
|
742
|
-
};
|
|
743
|
-
const I18nRoot = wrapRoot(App);
|
|
744
|
-
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
745
|
-
|
|
746
|
-
await act(async () => {
|
|
747
|
-
router.publishPathname('/cs');
|
|
748
|
-
});
|
|
749
|
-
expect(pending).toHaveLength(1);
|
|
750
|
-
|
|
751
|
-
await act(async () => {
|
|
752
|
-
router.publishPathname('/en');
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
await act(async () => {
|
|
756
|
-
pending[0].resolve();
|
|
757
|
-
await pending[0].promise;
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
expect(rendered.container.textContent).toBe('Language');
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
test('keeps committed copy after a failed change and retries automatically', async () => {
|
|
764
|
-
rstest.useFakeTimers();
|
|
765
|
-
window.history.replaceState(null, '', '/en');
|
|
766
|
-
rstest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
767
|
-
const router = createMutableTanstackRouter();
|
|
768
|
-
const runtimeContext = createTanstackRuntimeContext(router);
|
|
769
|
-
const { instance, pending } = createDeferredI18nInstance();
|
|
770
|
-
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(instance);
|
|
771
|
-
await onBeforeRender(runtimeContext);
|
|
772
|
-
|
|
773
|
-
const RetryConsumer = () => {
|
|
774
|
-
useModernI18n();
|
|
775
|
-
return null;
|
|
776
|
-
};
|
|
777
|
-
const App = () => {
|
|
778
|
-
const [showRetryConsumer, setShowRetryConsumer] = useState(false);
|
|
779
|
-
const { language, t } = useModernI18n();
|
|
780
|
-
return (
|
|
781
|
-
<main>
|
|
782
|
-
{language}:{t('languageSwitcher')}
|
|
783
|
-
<button type="button" onClick={() => setShowRetryConsumer(true)}>
|
|
784
|
-
Retry
|
|
785
|
-
</button>
|
|
786
|
-
{showRetryConsumer && <RetryConsumer />}
|
|
787
|
-
</main>
|
|
788
|
-
);
|
|
789
|
-
};
|
|
790
|
-
const I18nRoot = wrapRoot(App);
|
|
791
|
-
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
792
|
-
|
|
793
|
-
await act(async () => {
|
|
794
|
-
router.publishPathname('/cs');
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
const languageError = new Error('failed to load Czech resources');
|
|
798
|
-
await act(async () => {
|
|
799
|
-
pending[0].reject(languageError);
|
|
800
|
-
await pending[0].promise.catch(() => undefined);
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
expect(rendered.container.textContent).toContain('en:Language');
|
|
804
|
-
|
|
805
|
-
await act(async () => {
|
|
806
|
-
await rstest.advanceTimersByTimeAsync(50);
|
|
807
|
-
});
|
|
808
|
-
expect(pending).toHaveLength(2);
|
|
809
|
-
|
|
810
|
-
await act(async () => {
|
|
811
|
-
pending[1].resolve();
|
|
812
|
-
await pending[1].promise;
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
expect(rendered.container.textContent).toContain('cs:Jazyk');
|
|
816
|
-
expect(instance.language).toBe('cs');
|
|
817
|
-
});
|
|
818
118
|
});
|
|
819
119
|
|
|
820
|
-
describe('
|
|
821
|
-
test('
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
rendered =
|
|
835
|
-
|
|
836
|
-
window.history.replaceState(null, '', '/');
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
test('uses the TanStack router Link for I18nLink rendering', async () => {
|
|
840
|
-
const router = createTanstackRouter('/cs/podminky-pouzivani', 'cs');
|
|
841
|
-
rendered = await renderWithRuntime(
|
|
842
|
-
<ModernI18nProvider
|
|
843
|
-
value={{
|
|
844
|
-
language: 'cs',
|
|
845
|
-
i18nInstance: createI18nInstance('cs'),
|
|
846
|
-
languages: ['en', 'cs'],
|
|
847
|
-
localePathRedirect: true,
|
|
848
|
-
localisedUrls,
|
|
849
|
-
}}
|
|
850
|
-
>
|
|
851
|
-
<I18nLink to="/terms-of-service" data-testid="terms-link">
|
|
852
|
-
Terms
|
|
853
|
-
</I18nLink>
|
|
854
|
-
</ModernI18nProvider>,
|
|
855
|
-
createTanstackRuntimeContext(router),
|
|
856
|
-
);
|
|
857
|
-
|
|
858
|
-
const link = rendered.container.querySelector<HTMLAnchorElement>(
|
|
859
|
-
'[data-testid="terms-link"]',
|
|
860
|
-
);
|
|
861
|
-
expect(link?.getAttribute('href')).toBe('/cs/podminky-pouzivani');
|
|
862
|
-
expect(link?.getAttribute('data-router-link')).toBe('tanstack');
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
test('ignores router state updates that do not change location or params', async () => {
|
|
866
|
-
const router = createMutableTanstackRouter('/en');
|
|
867
|
-
let renders = 0;
|
|
868
|
-
const LocationProbe = () => {
|
|
869
|
-
renders += 1;
|
|
870
|
-
const { location } = useI18nRouterAdapter();
|
|
871
|
-
return <output>{location?.pathname}</output>;
|
|
872
|
-
};
|
|
873
|
-
|
|
874
|
-
rendered = await renderWithRuntime(
|
|
875
|
-
<LocationProbe />,
|
|
876
|
-
createTanstackRuntimeContext(router),
|
|
877
|
-
);
|
|
878
|
-
const initialRenders = renders;
|
|
879
|
-
|
|
880
|
-
await act(async () => {
|
|
881
|
-
router.publishStateUpdate();
|
|
882
|
-
});
|
|
883
|
-
expect(renders).toBe(initialRenders);
|
|
884
|
-
expect(rendered.container.textContent).toBe('/en');
|
|
885
|
-
|
|
886
|
-
await act(async () => {
|
|
887
|
-
router.publishPathname('/cs');
|
|
888
|
-
});
|
|
889
|
-
expect(renders).toBe(initialRenders + 1);
|
|
890
|
-
expect(rendered.container.textContent).toBe('/cs');
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
test('ignores React Router fetcher updates while observing location changes', async () => {
|
|
894
|
-
const router = createMutableReactRouter('/en');
|
|
895
|
-
let renders = 0;
|
|
896
|
-
const LocationProbe = () => {
|
|
897
|
-
renders += 1;
|
|
898
|
-
const { location } = useI18nRouterAdapter();
|
|
899
|
-
return <output>{location?.pathname}</output>;
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
rendered = await renderWithRuntime(
|
|
903
|
-
<LocationProbe />,
|
|
904
|
-
createReactRouterRuntimeContext(router),
|
|
905
|
-
);
|
|
906
|
-
const initialRenders = renders;
|
|
907
|
-
|
|
908
|
-
await act(async () => {
|
|
909
|
-
router.publishFetcherUpdate();
|
|
910
|
-
});
|
|
911
|
-
expect(renders).toBe(initialRenders);
|
|
912
|
-
expect(rendered.container.textContent).toBe('/en');
|
|
913
|
-
|
|
914
|
-
await act(async () => {
|
|
915
|
-
router.publishPathname('/cs');
|
|
916
|
-
});
|
|
917
|
-
expect(renders).toBe(initialRenders + 1);
|
|
918
|
-
expect(rendered.container.textContent).toBe('/cs');
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
test('observes distinct route params without serialized snapshot collisions', async () => {
|
|
922
|
-
const router = createMutableReactRouter('/products', {
|
|
923
|
-
a: '1&b=2',
|
|
924
|
-
});
|
|
925
|
-
const ParamsProbe = () => {
|
|
926
|
-
const { params } = useI18nRouterAdapter();
|
|
927
|
-
return <output>{JSON.stringify(params)}</output>;
|
|
928
|
-
};
|
|
929
|
-
|
|
930
|
-
rendered = await renderWithRuntime(
|
|
931
|
-
<ParamsProbe />,
|
|
932
|
-
createReactRouterRuntimeContext(router),
|
|
933
|
-
);
|
|
934
|
-
expect(rendered.container.textContent).toBe('{"a":"1&b=2"}');
|
|
935
|
-
|
|
936
|
-
await act(async () => {
|
|
937
|
-
router.publishParams({ a: '1', b: '2' });
|
|
938
|
-
});
|
|
939
|
-
expect(rendered.container.textContent).toBe('{"a":"1","b":"2"}');
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
test('forwards warmup props through I18nLink with a localized string target', async () => {
|
|
943
|
-
const router = createTanstackRouter('/cs/podminky-pouzivani', 'cs');
|
|
944
|
-
rendered = await renderWithRuntime(
|
|
945
|
-
<ModernI18nProvider
|
|
946
|
-
value={{
|
|
947
|
-
language: 'cs',
|
|
948
|
-
i18nInstance: createI18nInstance('cs'),
|
|
949
|
-
languages: ['en', 'cs'],
|
|
950
|
-
localePathRedirect: true,
|
|
951
|
-
localisedUrls,
|
|
952
|
-
}}
|
|
953
|
-
>
|
|
954
|
-
<I18nLink
|
|
955
|
-
to="/terms-of-service"
|
|
956
|
-
data-testid="terms-link"
|
|
957
|
-
prefetch="viewport"
|
|
958
|
-
preload="intent"
|
|
959
|
-
>
|
|
960
|
-
Terms
|
|
961
|
-
</I18nLink>
|
|
962
|
-
</ModernI18nProvider>,
|
|
963
|
-
createTanstackRuntimeContext(router),
|
|
964
|
-
);
|
|
965
|
-
|
|
966
|
-
const linkProps = capturedTanstackLinkProps.at(-1);
|
|
967
|
-
// TanStack has no `prefetch` prop: the explicit native `preload` wins and
|
|
968
|
-
// `prefetch` must not be forwarded.
|
|
969
|
-
expect(linkProps).toMatchObject({
|
|
970
|
-
to: '/cs/podminky-pouzivani',
|
|
971
|
-
preload: 'intent',
|
|
972
|
-
});
|
|
973
|
-
expect(linkProps.prefetch).toBeUndefined();
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
test('does not leak warmup props to fallback anchors', async () => {
|
|
977
|
-
rendered = await renderI18nRoot(
|
|
978
|
-
<ModernI18nProvider
|
|
979
|
-
value={{
|
|
980
|
-
language: 'cs',
|
|
981
|
-
i18nInstance: createI18nInstance('cs'),
|
|
982
|
-
languages: ['en', 'cs'],
|
|
983
|
-
localePathRedirect: true,
|
|
984
|
-
localisedUrls,
|
|
985
|
-
}}
|
|
986
|
-
>
|
|
987
|
-
<I18nLink
|
|
988
|
-
to="/terms-of-service"
|
|
989
|
-
data-testid="terms-link"
|
|
990
|
-
prefetch="none"
|
|
991
|
-
preload={false}
|
|
992
|
-
>
|
|
993
|
-
Terms
|
|
994
|
-
</I18nLink>
|
|
995
|
-
</ModernI18nProvider>,
|
|
996
|
-
);
|
|
997
|
-
|
|
998
|
-
const link = rendered.container.querySelector<HTMLAnchorElement>(
|
|
999
|
-
'[data-testid="terms-link"]',
|
|
1000
|
-
);
|
|
1001
|
-
expect(link?.getAttribute('href')).toBe('/cs/podminky-pouzivani');
|
|
1002
|
-
expect(link?.hasAttribute('prefetch')).toBe(false);
|
|
1003
|
-
expect(link?.hasAttribute('preload')).toBe(false);
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
test('uses TanStack-shaped replacement when changeLanguage updates the URL', async () => {
|
|
1007
|
-
window.history.replaceState(
|
|
1008
|
-
null,
|
|
1009
|
-
'',
|
|
1010
|
-
'/en/terms-of-service?from=test#section',
|
|
1011
|
-
);
|
|
1012
|
-
|
|
1013
|
-
const router = createTanstackRouter(
|
|
1014
|
-
'/en/terms-of-service?from=test#section',
|
|
1015
|
-
);
|
|
1016
|
-
let changeLanguagePromise: Promise<void> | undefined;
|
|
1017
|
-
|
|
1018
|
-
const Harness = () => {
|
|
1019
|
-
const { changeLanguage } = useModernI18n();
|
|
1020
|
-
return (
|
|
1021
|
-
<button
|
|
1022
|
-
type="button"
|
|
1023
|
-
onClick={() => {
|
|
1024
|
-
changeLanguagePromise = changeLanguage('cs');
|
|
1025
|
-
}}
|
|
1026
|
-
>
|
|
1027
|
-
Change language
|
|
1028
|
-
</button>
|
|
1029
|
-
);
|
|
1030
|
-
};
|
|
1031
|
-
|
|
1032
|
-
rendered = await renderWithRuntime(
|
|
1033
|
-
<ModernI18nProvider
|
|
1034
|
-
value={{
|
|
1035
|
-
language: 'en',
|
|
1036
|
-
i18nInstance: createI18nInstance('en'),
|
|
1037
|
-
languages: ['en', 'cs'],
|
|
1038
|
-
localePathRedirect: true,
|
|
1039
|
-
localisedUrls,
|
|
1040
|
-
}}
|
|
1041
|
-
>
|
|
1042
|
-
<Harness />
|
|
1043
|
-
</ModernI18nProvider>,
|
|
1044
|
-
createTanstackRuntimeContext(router),
|
|
1045
|
-
);
|
|
1046
|
-
|
|
1047
|
-
const button = rendered.container.querySelector('button');
|
|
1048
|
-
|
|
1049
|
-
await act(async () => {
|
|
1050
|
-
button?.dispatchEvent(
|
|
1051
|
-
new MouseEvent('click', {
|
|
1052
|
-
bubbles: true,
|
|
1053
|
-
cancelable: true,
|
|
1054
|
-
button: 0,
|
|
1055
|
-
}),
|
|
1056
|
-
);
|
|
1057
|
-
await changeLanguagePromise;
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
|
-
expect(router.navigate).toHaveBeenCalledWith({
|
|
1061
|
-
to: '/cs/podminky-pouzivani?from=test#section',
|
|
1062
|
-
replace: true,
|
|
1063
|
-
});
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
test('updates provider language when target locale is already in URL', async () => {
|
|
1067
|
-
window.history.replaceState(null, '', '/cs/podminky-pouzivani');
|
|
1068
|
-
|
|
1069
|
-
const router = createTanstackRouter('/cs/podminky-pouzivani', 'cs');
|
|
1070
|
-
const i18nInstance = createI18nInstance('en');
|
|
1071
|
-
const updateLanguage = rstest.fn();
|
|
1072
|
-
let changeLanguagePromise: Promise<void> | undefined;
|
|
1073
|
-
|
|
1074
|
-
const Harness = () => {
|
|
1075
|
-
const { changeLanguage } = useModernI18n();
|
|
1076
|
-
return (
|
|
1077
|
-
<button
|
|
1078
|
-
type="button"
|
|
1079
|
-
onClick={() => {
|
|
1080
|
-
changeLanguagePromise = changeLanguage('cs');
|
|
1081
|
-
}}
|
|
1082
|
-
>
|
|
1083
|
-
Change language
|
|
1084
|
-
</button>
|
|
1085
|
-
);
|
|
1086
|
-
};
|
|
1087
|
-
|
|
1088
|
-
rendered = await renderWithRuntime(
|
|
1089
|
-
<ModernI18nProvider
|
|
1090
|
-
value={{
|
|
1091
|
-
language: 'en',
|
|
1092
|
-
i18nInstance,
|
|
1093
|
-
languages: ['en', 'cs'],
|
|
1094
|
-
localePathRedirect: true,
|
|
1095
|
-
localisedUrls,
|
|
1096
|
-
updateLanguage,
|
|
1097
|
-
}}
|
|
1098
|
-
>
|
|
1099
|
-
<Harness />
|
|
1100
|
-
</ModernI18nProvider>,
|
|
1101
|
-
createTanstackRuntimeContext(router),
|
|
1102
|
-
);
|
|
1103
|
-
|
|
1104
|
-
const button = rendered.container.querySelector('button');
|
|
1105
|
-
updateLanguage.mockClear();
|
|
1106
|
-
|
|
1107
|
-
await act(async () => {
|
|
1108
|
-
button?.dispatchEvent(
|
|
1109
|
-
new MouseEvent('click', {
|
|
1110
|
-
bubbles: true,
|
|
1111
|
-
cancelable: true,
|
|
1112
|
-
button: 0,
|
|
1113
|
-
}),
|
|
1114
|
-
);
|
|
1115
|
-
await changeLanguagePromise;
|
|
1116
|
-
});
|
|
1117
|
-
|
|
1118
|
-
expect(updateLanguage).toHaveBeenCalledWith('cs');
|
|
1119
|
-
expect(router.navigate).not.toHaveBeenCalled();
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
test('keeps React Router positional replacement when changeLanguage updates the URL', async () => {
|
|
1123
|
-
window.history.replaceState(null, '', '/en/terms-of-service');
|
|
1124
|
-
|
|
1125
|
-
const router = {
|
|
1126
|
-
navigate: rstest.fn(async () => undefined),
|
|
1127
|
-
};
|
|
1128
|
-
let changeLanguagePromise: Promise<void> | undefined;
|
|
1129
|
-
|
|
1130
|
-
const Harness = () => {
|
|
1131
|
-
const { changeLanguage } = useModernI18n();
|
|
1132
|
-
return (
|
|
1133
|
-
<button
|
|
1134
|
-
type="button"
|
|
1135
|
-
onClick={() => {
|
|
1136
|
-
changeLanguagePromise = changeLanguage('cs');
|
|
1137
|
-
}}
|
|
1138
|
-
>
|
|
1139
|
-
Change language
|
|
1140
|
-
</button>
|
|
1141
|
-
);
|
|
1142
|
-
};
|
|
1143
|
-
|
|
1144
|
-
rendered = await renderWithRuntime(
|
|
1145
|
-
<ModernI18nProvider
|
|
120
|
+
describe('native navigation extension contract', () => {
|
|
121
|
+
test('renders the supplied router primitive and exposes provider-scoped navigation', async () => {
|
|
122
|
+
const navigate = rstest.fn();
|
|
123
|
+
const RouterLink = ({ to, children }: any) => (
|
|
124
|
+
<a href={to} data-native="yes">
|
|
125
|
+
{children}
|
|
126
|
+
</a>
|
|
127
|
+
);
|
|
128
|
+
function Consumer() {
|
|
129
|
+
const adapter = useI18nRouterAdapter();
|
|
130
|
+
expect(adapter.navigate).toBe(navigate);
|
|
131
|
+
expect(adapter.params).toEqual({ lang: 'cs' });
|
|
132
|
+
return <Link to="/products?tag=x#detail">Products</Link>;
|
|
133
|
+
}
|
|
134
|
+
const rendered = await renderI18nRoot(
|
|
135
|
+
<I18nNavigationProvider
|
|
1146
136
|
value={{
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
137
|
+
hasRouter: true,
|
|
138
|
+
navigate,
|
|
139
|
+
params: { lang: 'cs' },
|
|
140
|
+
location: { pathname: '/cs/products', search: '', hash: '' },
|
|
141
|
+
Link: RouterLink,
|
|
1152
142
|
}}
|
|
1153
143
|
>
|
|
1154
|
-
<Harness />
|
|
1155
|
-
</ModernI18nProvider>,
|
|
1156
|
-
createReactRouterRuntimeContext(router),
|
|
1157
|
-
);
|
|
1158
|
-
|
|
1159
|
-
const button = rendered.container.querySelector('button');
|
|
1160
|
-
|
|
1161
|
-
await act(async () => {
|
|
1162
|
-
button?.dispatchEvent(
|
|
1163
|
-
new MouseEvent('click', {
|
|
1164
|
-
bubbles: true,
|
|
1165
|
-
cancelable: true,
|
|
1166
|
-
button: 0,
|
|
1167
|
-
}),
|
|
1168
|
-
);
|
|
1169
|
-
await changeLanguagePromise;
|
|
1170
|
-
});
|
|
1171
|
-
|
|
1172
|
-
expect(router.navigate).toHaveBeenCalledWith('/cs/podminky-pouzivani', {
|
|
1173
|
-
replace: true,
|
|
1174
|
-
});
|
|
1175
|
-
});
|
|
1176
|
-
|
|
1177
|
-
test('tracks React Router link and history navigation without Modern i18n consumers', async () => {
|
|
1178
|
-
window.history.replaceState(null, '', '/en');
|
|
1179
|
-
const i18nInstance = i18next.createInstance();
|
|
1180
|
-
await i18nInstance.init({
|
|
1181
|
-
lng: 'en',
|
|
1182
|
-
fallbackLng: 'en',
|
|
1183
|
-
resources: {
|
|
1184
|
-
en: { translation: { languageSwitcher: 'Language' } },
|
|
1185
|
-
cs: { translation: { languageSwitcher: 'Jazyk' } },
|
|
1186
|
-
},
|
|
1187
|
-
});
|
|
1188
|
-
|
|
1189
|
-
const RouteContent = () => {
|
|
1190
|
-
const { t } = useTranslation();
|
|
1191
|
-
return (
|
|
1192
|
-
<main>
|
|
1193
|
-
<span data-testid="translation">{t('languageSwitcher')}</span>
|
|
1194
|
-
<ReactRouterLink to="/cs">Czech</ReactRouterLink>
|
|
1195
|
-
</main>
|
|
1196
|
-
);
|
|
1197
|
-
};
|
|
1198
|
-
const router = createMemoryRouter(
|
|
1199
|
-
[{ path: '/:lang', element: <RouteContent /> }],
|
|
1200
|
-
{ initialEntries: ['/en'] },
|
|
1201
|
-
);
|
|
1202
|
-
const originalSubscribe = router.subscribe;
|
|
1203
|
-
router.subscribe = function subscribe(listener) {
|
|
1204
|
-
if (this !== router) {
|
|
1205
|
-
throw new Error('React Router subscribe receiver was not preserved');
|
|
1206
|
-
}
|
|
1207
|
-
return originalSubscribe.call(this, listener);
|
|
1208
|
-
};
|
|
1209
|
-
const runtimeContext = createReactRouterRuntimeContext(router);
|
|
1210
|
-
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(
|
|
1211
|
-
i18nInstance,
|
|
1212
|
-
true,
|
|
1213
|
-
);
|
|
1214
|
-
await onBeforeRender(runtimeContext);
|
|
1215
|
-
const I18nRoot = wrapRoot(() => <RouterProvider router={router} />);
|
|
1216
|
-
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
1217
|
-
|
|
1218
|
-
await act(async () => {
|
|
1219
|
-
rendered?.container
|
|
1220
|
-
.querySelector('a')
|
|
1221
|
-
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
1222
|
-
await Promise.resolve();
|
|
1223
|
-
});
|
|
1224
|
-
expect(
|
|
1225
|
-
rendered.container.querySelector('[data-testid="translation"]')
|
|
1226
|
-
?.textContent,
|
|
1227
|
-
).toBe('Jazyk');
|
|
1228
|
-
|
|
1229
|
-
await act(async () => {
|
|
1230
|
-
await router.navigate(-1);
|
|
1231
|
-
});
|
|
1232
|
-
expect(
|
|
1233
|
-
rendered.container.querySelector('[data-testid="translation"]')
|
|
1234
|
-
?.textContent,
|
|
1235
|
-
).toBe('Language');
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
test('exposes a language-scoped t function for rendered copy', async () => {
|
|
1239
|
-
const i18nInstance = createI18nInstance('en');
|
|
1240
|
-
i18nInstance.t = (key: string) => `${i18nInstance.language}:${key}`;
|
|
1241
|
-
const renderTranslations: Array<(key: string) => string> = [];
|
|
1242
|
-
let setProviderLanguage: ((language: string) => void) | undefined;
|
|
1243
|
-
|
|
1244
|
-
const StatefulI18nProvider = ({ children }: PropsWithChildren) => {
|
|
1245
|
-
const [language, setLanguage] = useState('en');
|
|
1246
|
-
setProviderLanguage = setLanguage;
|
|
1247
|
-
|
|
1248
|
-
return (
|
|
1249
144
|
<ModernI18nProvider
|
|
1250
145
|
value={{
|
|
1251
|
-
language,
|
|
1252
|
-
i18nInstance,
|
|
146
|
+
language: 'cs',
|
|
147
|
+
i18nInstance: createI18nInstance('cs'),
|
|
1253
148
|
languages: ['en', 'cs'],
|
|
1254
149
|
localePathRedirect: true,
|
|
1255
|
-
localisedUrls,
|
|
1256
|
-
updateLanguage: setLanguage,
|
|
1257
150
|
}}
|
|
1258
151
|
>
|
|
1259
|
-
|
|
152
|
+
<Consumer />
|
|
1260
153
|
</ModernI18nProvider>
|
|
1261
|
-
|
|
1262
|
-
};
|
|
1263
|
-
|
|
1264
|
-
const Harness = () => {
|
|
1265
|
-
const { t } = useModernI18n();
|
|
1266
|
-
renderTranslations.push(t);
|
|
1267
|
-
return <span data-testid="translation">{t('key')}</span>;
|
|
1268
|
-
};
|
|
1269
|
-
|
|
1270
|
-
rendered = await renderWithRuntime(
|
|
1271
|
-
<StatefulI18nProvider>
|
|
1272
|
-
<Harness />
|
|
1273
|
-
</StatefulI18nProvider>,
|
|
1274
|
-
createReactRouterRuntimeContext({ navigate: rstest.fn() }),
|
|
154
|
+
</I18nNavigationProvider>,
|
|
1275
155
|
);
|
|
156
|
+
try {
|
|
157
|
+
expect(rendered.container.querySelector('a')?.getAttribute('href')).toBe(
|
|
158
|
+
'/cs/products?tag=x#detail',
|
|
159
|
+
);
|
|
160
|
+
expect(
|
|
161
|
+
rendered.container.querySelector('a')?.getAttribute('data-native'),
|
|
162
|
+
).toBe('yes');
|
|
163
|
+
expect(navigate).not.toHaveBeenCalled();
|
|
164
|
+
} finally {
|
|
165
|
+
cleanup(rendered);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
1276
169
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
170
|
+
describe('native synchronization extension contract', () => {
|
|
171
|
+
test('uses the supplied component callback and preserves its state across parent renders', async () => {
|
|
172
|
+
const instance = createI18nInstance('en');
|
|
173
|
+
const synchronize = rstest.fn();
|
|
174
|
+
const mounts = rstest.fn();
|
|
175
|
+
const observed: I18nLanguageSynchronizationProps[] = [];
|
|
176
|
+
const Synchronization = (props: I18nLanguageSynchronizationProps) => {
|
|
177
|
+
observed.push(props);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
mounts();
|
|
180
|
+
props.setLang('cs');
|
|
181
|
+
}, []);
|
|
182
|
+
return props.children(synchronize);
|
|
183
|
+
};
|
|
184
|
+
const Root = createI18nRootWrapper({
|
|
185
|
+
htmlLangAttr: false,
|
|
186
|
+
localePathRedirect: true,
|
|
187
|
+
languages: ['en', 'cs'],
|
|
188
|
+
fallbackLanguage: 'en',
|
|
189
|
+
getLatestI18nInstance: () => instance,
|
|
190
|
+
getI18nextProvider: () => null,
|
|
191
|
+
LanguageSynchronization: Synchronization,
|
|
192
|
+
})(() => {
|
|
193
|
+
const { language } = useModernI18n();
|
|
194
|
+
return <main>{language}</main>;
|
|
1286
195
|
});
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
196
|
+
window.history.replaceState(null, '', '/cs/products');
|
|
197
|
+
const rendered = await renderI18nRoot(<Root />);
|
|
198
|
+
try {
|
|
199
|
+
expect(rendered.container.textContent).toBe('cs');
|
|
200
|
+
expect(synchronize).toHaveBeenCalledWith('cs');
|
|
201
|
+
expect(observed.at(-1)?.i18nInstance).toBe(instance);
|
|
202
|
+
expect(observed.at(-1)?.pathname).toBe('/cs/products');
|
|
203
|
+
expect(observed.at(-1)?.prevLangRef.current).toBe('cs');
|
|
204
|
+
await act(async () => {
|
|
205
|
+
rendered.root.render(
|
|
206
|
+
<RuntimeContext.Provider
|
|
207
|
+
value={{ isBrowser: true, requestContext, context: requestContext }}
|
|
208
|
+
>
|
|
209
|
+
<Root unrelated="updated" />
|
|
210
|
+
</RuntimeContext.Provider>,
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
expect(mounts).toHaveBeenCalledTimes(1);
|
|
214
|
+
expect(rendered.container.textContent).toBe('cs');
|
|
215
|
+
expect(instance.changeLanguage).not.toHaveBeenCalled();
|
|
216
|
+
} finally {
|
|
217
|
+
cleanup(rendered);
|
|
218
|
+
window.history.replaceState(null, '', '/');
|
|
219
|
+
}
|
|
1293
220
|
});
|
|
1294
221
|
});
|