@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.8 → 3.2.0-ultramodern.80
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 +22 -0
- package/dist/cjs/runtime/I18nLink.js +4 -12
- package/dist/cjs/runtime/context.js +32 -5
- package/dist/cjs/runtime/hooks.js +8 -5
- package/dist/cjs/runtime/i18n/backend/defaults.js +1 -1
- package/dist/cjs/runtime/i18n/backend/middleware.node.js +4 -4
- package/dist/cjs/runtime/i18n/instance.js +0 -24
- package/dist/cjs/runtime/i18n/react-i18next.js +52 -0
- package/dist/cjs/runtime/i18n/utils.js +0 -12
- package/dist/cjs/runtime/index.js +13 -7
- package/dist/cjs/runtime/routerAdapter.js +163 -0
- package/dist/cjs/runtime/utils.js +63 -94
- package/dist/cjs/server/index.js +60 -8
- package/dist/cjs/shared/localisedUrls.js +237 -0
- package/dist/esm/cli/index.mjs +22 -0
- package/dist/esm/runtime/I18nLink.mjs +4 -12
- package/dist/esm/runtime/context.mjs +34 -7
- package/dist/esm/runtime/hooks.mjs +9 -6
- package/dist/esm/runtime/i18n/backend/defaults.mjs +1 -1
- package/dist/esm/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm/runtime/i18n/instance.mjs +1 -19
- package/dist/esm/runtime/i18n/react-i18next.mjs +18 -0
- package/dist/esm/runtime/i18n/utils.mjs +0 -12
- package/dist/esm/runtime/index.mjs +14 -8
- package/dist/esm/runtime/routerAdapter.mjs +129 -0
- package/dist/esm/runtime/utils.mjs +11 -30
- package/dist/esm/server/index.mjs +53 -7
- package/dist/esm/shared/localisedUrls.mjs +191 -0
- package/dist/esm-node/cli/index.mjs +22 -0
- package/dist/esm-node/runtime/I18nLink.mjs +4 -12
- package/dist/esm-node/runtime/context.mjs +34 -7
- package/dist/esm-node/runtime/hooks.mjs +9 -6
- package/dist/esm-node/runtime/i18n/backend/defaults.mjs +1 -1
- package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm-node/runtime/i18n/instance.mjs +1 -19
- package/dist/esm-node/runtime/i18n/react-i18next.mjs +19 -0
- package/dist/esm-node/runtime/i18n/utils.mjs +0 -12
- package/dist/esm-node/runtime/index.mjs +14 -8
- package/dist/esm-node/runtime/routerAdapter.mjs +130 -0
- package/dist/esm-node/runtime/utils.mjs +11 -30
- package/dist/esm-node/server/index.mjs +53 -7
- package/dist/esm-node/shared/localisedUrls.mjs +192 -0
- package/dist/types/runtime/I18nLink.d.ts +15 -0
- package/dist/types/runtime/context.d.ts +3 -0
- package/dist/types/runtime/hooks.d.ts +4 -2
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +1 -1
- package/dist/types/runtime/i18n/instance.d.ts +0 -5
- package/dist/types/runtime/i18n/react-i18next.d.ts +7 -0
- package/dist/types/runtime/index.d.ts +1 -0
- package/dist/types/runtime/routerAdapter.d.ts +26 -0
- package/dist/types/runtime/utils.d.ts +2 -7
- package/dist/types/server/index.d.ts +6 -0
- package/dist/types/shared/localisedUrls.d.ts +13 -0
- package/dist/types/shared/type.d.ts +12 -0
- package/package.json +15 -15
- package/rstest.config.mts +39 -0
- package/src/cli/index.ts +43 -1
- package/src/runtime/I18nLink.tsx +10 -16
- package/src/runtime/context.tsx +45 -7
- package/src/runtime/hooks.ts +13 -4
- package/src/runtime/i18n/backend/defaults.ts +3 -1
- package/src/runtime/i18n/backend/middleware.node.ts +1 -1
- package/src/runtime/i18n/instance.ts +0 -29
- package/src/runtime/i18n/react-i18next.ts +31 -0
- package/src/runtime/i18n/utils.ts +4 -26
- package/src/runtime/index.tsx +21 -9
- package/src/runtime/routerAdapter.tsx +333 -0
- package/src/runtime/utils.ts +22 -34
- package/src/server/index.ts +117 -10
- package/src/shared/localisedUrls.ts +393 -0
- package/src/shared/type.ts +12 -0
- package/tests/i18nUtils.test.ts +45 -0
- package/tests/localisedUrls.test.ts +312 -0
- package/tests/routerAdapter.test.tsx +278 -0
- package/dist/esm/rslib-runtime.mjs +0 -18
- package/dist/esm-node/rslib-runtime.mjs +0 -19
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type { NestedRouteForCli } from '@modern-js/types';
|
|
2
|
+
import { describe, expect, test } from '@rstest/core';
|
|
3
|
+
import {
|
|
4
|
+
collectApiPrefixes,
|
|
5
|
+
i18nServerPlugin,
|
|
6
|
+
matchesApiPrefix,
|
|
7
|
+
} from '../src/server';
|
|
8
|
+
import {
|
|
9
|
+
applyLocalisedUrlsToRoutes,
|
|
10
|
+
resolveLocalisedPath,
|
|
11
|
+
validateLocalisedUrls,
|
|
12
|
+
} from '../src/shared/localisedUrls';
|
|
13
|
+
|
|
14
|
+
const createRoute = (
|
|
15
|
+
path: string,
|
|
16
|
+
children?: NestedRouteForCli[],
|
|
17
|
+
): NestedRouteForCli => ({
|
|
18
|
+
id: path,
|
|
19
|
+
path,
|
|
20
|
+
type: 'nested',
|
|
21
|
+
origin: 'file-system',
|
|
22
|
+
routeType: children ? 'layout' : 'page',
|
|
23
|
+
_component: `${path}.tsx`,
|
|
24
|
+
children,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const createRequestContext = (pathname: string) =>
|
|
28
|
+
({
|
|
29
|
+
req: {
|
|
30
|
+
url: `http://localhost${pathname}`,
|
|
31
|
+
},
|
|
32
|
+
}) as any;
|
|
33
|
+
|
|
34
|
+
describe('localisedUrls', () => {
|
|
35
|
+
test('requires every localisable route path to define every language', () => {
|
|
36
|
+
const routes = [createRoute(':lang', [createRoute('terms-of-service')])];
|
|
37
|
+
|
|
38
|
+
expect(() =>
|
|
39
|
+
validateLocalisedUrls(routes, ['en', 'cs'], {
|
|
40
|
+
'/terms-of-service': {
|
|
41
|
+
en: '/terms-of-service',
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
).toThrow('missing languages: cs');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('expands route paths to localised aliases', () => {
|
|
48
|
+
const routes = [
|
|
49
|
+
createRoute(':lang', [
|
|
50
|
+
createRoute('terms-of-service'),
|
|
51
|
+
createRoute('products', [createRoute(':slug')]),
|
|
52
|
+
]),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const localisedRoutes = applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], {
|
|
56
|
+
'/terms-of-service': {
|
|
57
|
+
en: '/terms-of-service',
|
|
58
|
+
cs: '/podminky-pouzivani',
|
|
59
|
+
},
|
|
60
|
+
'/products': {
|
|
61
|
+
en: '/products',
|
|
62
|
+
cs: '/produkty',
|
|
63
|
+
},
|
|
64
|
+
'/products/:slug': {
|
|
65
|
+
en: '/products/:slug',
|
|
66
|
+
cs: '/produkty/:slug',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const localeRoute = localisedRoutes[0] as NestedRouteForCli;
|
|
71
|
+
expect(localeRoute.children?.map(route => route.path)).toEqual([
|
|
72
|
+
'terms-of-service',
|
|
73
|
+
'podminky-pouzivani',
|
|
74
|
+
'products',
|
|
75
|
+
'produkty',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const productRoutes = localeRoute.children?.filter(
|
|
79
|
+
route => route.path === 'products' || route.path === 'produkty',
|
|
80
|
+
);
|
|
81
|
+
expect(productRoutes?.[0].children?.map(route => route.path)).toEqual([
|
|
82
|
+
':slug',
|
|
83
|
+
]);
|
|
84
|
+
expect(productRoutes?.[1].children?.map(route => route.path)).toEqual([
|
|
85
|
+
':slug',
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('expands flat locale-prefixed route paths with canonical keys', () => {
|
|
90
|
+
const routes = [
|
|
91
|
+
createRoute('/:lang/about'),
|
|
92
|
+
createRoute('/:lang/products/:slug'),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const localisedRoutes = applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], {
|
|
96
|
+
'/about': {
|
|
97
|
+
en: '/about',
|
|
98
|
+
cs: '/o-nas',
|
|
99
|
+
},
|
|
100
|
+
'/products/:slug': {
|
|
101
|
+
en: '/products/:slug',
|
|
102
|
+
cs: '/produkty/:slug',
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(localisedRoutes.map(route => route.path)).toEqual([
|
|
107
|
+
':lang/about',
|
|
108
|
+
':lang/o-nas',
|
|
109
|
+
':lang/products/:slug',
|
|
110
|
+
':lang/produkty/:slug',
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('resolves current localized path to the target language', () => {
|
|
115
|
+
const localisedUrls = {
|
|
116
|
+
'/terms-of-service': {
|
|
117
|
+
en: '/terms-of-service',
|
|
118
|
+
cs: '/podminky-pouzivani',
|
|
119
|
+
},
|
|
120
|
+
'/products/:slug': {
|
|
121
|
+
en: '/products/:slug',
|
|
122
|
+
cs: '/produkty/:slug',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
expect(
|
|
127
|
+
resolveLocalisedPath(
|
|
128
|
+
'/terms-of-service',
|
|
129
|
+
'cs',
|
|
130
|
+
['en', 'cs'],
|
|
131
|
+
localisedUrls,
|
|
132
|
+
),
|
|
133
|
+
).toBe('/podminky-pouzivani');
|
|
134
|
+
expect(
|
|
135
|
+
resolveLocalisedPath('/produkty/cervena-bota', 'en', ['en', 'cs'], {
|
|
136
|
+
...localisedUrls,
|
|
137
|
+
'/products/:slug': {
|
|
138
|
+
en: '/products/:slug',
|
|
139
|
+
cs: '/produkty/:slug',
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
).toBe('/products/cervena-bota');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('resolves optional route params', () => {
|
|
146
|
+
const localisedUrls = {
|
|
147
|
+
'/products/:slug?': {
|
|
148
|
+
en: '/products/:slug?',
|
|
149
|
+
cs: '/produkty/:slug?',
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
expect(
|
|
154
|
+
resolveLocalisedPath('/products', 'cs', ['en', 'cs'], localisedUrls),
|
|
155
|
+
).toBe('/produkty');
|
|
156
|
+
expect(
|
|
157
|
+
resolveLocalisedPath(
|
|
158
|
+
'/produkty/cervena-bota',
|
|
159
|
+
'en',
|
|
160
|
+
['en', 'cs'],
|
|
161
|
+
localisedUrls,
|
|
162
|
+
),
|
|
163
|
+
).toBe('/products/cervena-bota');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('resolves nested optional route params with translated ancestors', () => {
|
|
167
|
+
const localisedUrls = {
|
|
168
|
+
'/checkout': {
|
|
169
|
+
en: '/checkout',
|
|
170
|
+
cs: '/pokladna',
|
|
171
|
+
},
|
|
172
|
+
'/checkout/thank-you': {
|
|
173
|
+
en: '/checkout/thank-you',
|
|
174
|
+
cs: '/pokladna/dekujeme',
|
|
175
|
+
},
|
|
176
|
+
'/checkout/thank-you/:orderId?': {
|
|
177
|
+
en: '/checkout/thank-you/:orderId?',
|
|
178
|
+
cs: '/pokladna/dekujeme/:orderId?',
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
expect(
|
|
183
|
+
resolveLocalisedPath(
|
|
184
|
+
'/checkout/thank-you',
|
|
185
|
+
'cs',
|
|
186
|
+
['en', 'cs'],
|
|
187
|
+
localisedUrls,
|
|
188
|
+
),
|
|
189
|
+
).toBe('/pokladna/dekujeme');
|
|
190
|
+
expect(
|
|
191
|
+
resolveLocalisedPath(
|
|
192
|
+
'/pokladna/dekujeme/ABC-123',
|
|
193
|
+
'en',
|
|
194
|
+
['en', 'cs'],
|
|
195
|
+
localisedUrls,
|
|
196
|
+
),
|
|
197
|
+
).toBe('/checkout/thank-you/ABC-123');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('i18n server API prefix skips', () => {
|
|
202
|
+
test('collects API route prefixes and normalized BFF config prefixes', () => {
|
|
203
|
+
expect(
|
|
204
|
+
collectApiPrefixes(
|
|
205
|
+
[
|
|
206
|
+
{ entryName: 'main', isApi: false, urlPath: '/' },
|
|
207
|
+
{ isApi: true, urlPath: '/bff-api' },
|
|
208
|
+
{ isApi: true, urlPath: '/rpc/*' },
|
|
209
|
+
{ isApi: true, urlPath: '/' },
|
|
210
|
+
{ isApi: true },
|
|
211
|
+
],
|
|
212
|
+
['bff-api/', '/internal-api'],
|
|
213
|
+
),
|
|
214
|
+
).toEqual(['/bff-api', '/rpc', '/internal-api']);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('matches API prefixes by exact path or slash-delimited segment', () => {
|
|
218
|
+
const prefixes = ['/bff-api'];
|
|
219
|
+
|
|
220
|
+
expect(matchesApiPrefix('/bff-api', prefixes)).toBe(true);
|
|
221
|
+
expect(matchesApiPrefix('/bff-api/ping', prefixes)).toBe(true);
|
|
222
|
+
expect(matchesApiPrefix('/bff-api-v2', prefixes)).toBe(false);
|
|
223
|
+
expect(matchesApiPrefix('/bff-api-v2/ping', prefixes)).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('skips language detector and redirect middleware for API routes', async () => {
|
|
227
|
+
const middlewares: any[] = [];
|
|
228
|
+
const routes = [
|
|
229
|
+
{ entryName: 'main', entryPath: '', urlPath: '/' },
|
|
230
|
+
{ entryPath: '', isApi: true, urlPath: '/bff-api' },
|
|
231
|
+
];
|
|
232
|
+
let prepare: (() => void) | undefined;
|
|
233
|
+
|
|
234
|
+
i18nServerPlugin({
|
|
235
|
+
localeDetection: {
|
|
236
|
+
fallbackLanguage: 'en',
|
|
237
|
+
languages: ['en', 'cs'],
|
|
238
|
+
localePathRedirect: true,
|
|
239
|
+
},
|
|
240
|
+
staticRoutePrefixes: [],
|
|
241
|
+
}).setup({
|
|
242
|
+
getServerConfig: () => ({}),
|
|
243
|
+
getServerContext: () => ({ middlewares, routes }),
|
|
244
|
+
onPrepare: fn => {
|
|
245
|
+
prepare = fn;
|
|
246
|
+
},
|
|
247
|
+
} as any);
|
|
248
|
+
|
|
249
|
+
prepare?.();
|
|
250
|
+
|
|
251
|
+
const detectorMiddleware = middlewares.find(
|
|
252
|
+
middleware => middleware.name === 'i18n-language-detector',
|
|
253
|
+
);
|
|
254
|
+
const redirectMiddleware = middlewares.find(
|
|
255
|
+
middleware => middleware.name === 'i18n-server-middleware',
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
expect(detectorMiddleware).toBeDefined();
|
|
259
|
+
expect(redirectMiddleware).toBeDefined();
|
|
260
|
+
|
|
261
|
+
for (const middleware of [detectorMiddleware, redirectMiddleware]) {
|
|
262
|
+
let nextCalls = 0;
|
|
263
|
+
const response = await middleware.handler(
|
|
264
|
+
createRequestContext('/bff-api/ping'),
|
|
265
|
+
async () => {
|
|
266
|
+
nextCalls++;
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(response).toBeUndefined();
|
|
271
|
+
expect(nextCalls).toBe(1);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('uses /api as the BFF prefix when BFF config is present without prefix', async () => {
|
|
276
|
+
const middlewares: any[] = [];
|
|
277
|
+
const routes = [{ entryName: 'main', entryPath: '', urlPath: '/' }];
|
|
278
|
+
let prepare: (() => void) | undefined;
|
|
279
|
+
|
|
280
|
+
i18nServerPlugin({
|
|
281
|
+
localeDetection: {
|
|
282
|
+
fallbackLanguage: 'en',
|
|
283
|
+
languages: ['en', 'cs'],
|
|
284
|
+
localePathRedirect: true,
|
|
285
|
+
},
|
|
286
|
+
staticRoutePrefixes: [],
|
|
287
|
+
}).setup({
|
|
288
|
+
getServerConfig: () => ({ bff: {} }),
|
|
289
|
+
getServerContext: () => ({ middlewares, routes }),
|
|
290
|
+
onPrepare: fn => {
|
|
291
|
+
prepare = fn;
|
|
292
|
+
},
|
|
293
|
+
} as any);
|
|
294
|
+
|
|
295
|
+
prepare?.();
|
|
296
|
+
|
|
297
|
+
const redirectMiddleware = middlewares.find(
|
|
298
|
+
middleware => middleware.name === 'i18n-server-middleware',
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
let nextCalls = 0;
|
|
302
|
+
const response = await redirectMiddleware.handler(
|
|
303
|
+
createRequestContext('/api/ping'),
|
|
304
|
+
async () => {
|
|
305
|
+
nextCalls++;
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
expect(response).toBeUndefined();
|
|
310
|
+
expect(nextCalls).toBe(1);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { InternalRuntimeContext } from '@modern-js/runtime/context';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
import { act } from 'react';
|
|
4
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
5
|
+
import { ModernI18nProvider, useModernI18n } from '../src/runtime/context';
|
|
6
|
+
import { I18nLink } from '../src/runtime/I18nLink';
|
|
7
|
+
import type { I18nInstance } from '../src/runtime/i18n';
|
|
8
|
+
|
|
9
|
+
(
|
|
10
|
+
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
11
|
+
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
12
|
+
|
|
13
|
+
const localisedUrls = {
|
|
14
|
+
'/terms-of-service': {
|
|
15
|
+
en: '/terms-of-service',
|
|
16
|
+
cs: '/podminky-pouzivani',
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const requestContext = {
|
|
21
|
+
request: {},
|
|
22
|
+
response: {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const TanstackLink = ({ to, children, ...props }: any) => (
|
|
26
|
+
<a href={to} data-router-link="tanstack" {...props}>
|
|
27
|
+
{children}
|
|
28
|
+
</a>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
function createI18nInstance(language = 'en'): I18nInstance {
|
|
32
|
+
return {
|
|
33
|
+
language,
|
|
34
|
+
isInitialized: true,
|
|
35
|
+
init: () => Promise.resolve(undefined),
|
|
36
|
+
use: () => {},
|
|
37
|
+
createInstance: () => createI18nInstance(language),
|
|
38
|
+
setLang: rstest.fn(async () => undefined),
|
|
39
|
+
changeLanguage: rstest.fn(async () => undefined),
|
|
40
|
+
services: {},
|
|
41
|
+
options: {},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createRuntimeContext(
|
|
46
|
+
router: unknown,
|
|
47
|
+
framework: 'tanstack' | 'react-router',
|
|
48
|
+
) {
|
|
49
|
+
return {
|
|
50
|
+
isBrowser: true,
|
|
51
|
+
requestContext,
|
|
52
|
+
context: requestContext,
|
|
53
|
+
routerFramework: framework,
|
|
54
|
+
routerInstance: router,
|
|
55
|
+
routerRuntime: {
|
|
56
|
+
framework,
|
|
57
|
+
instance: router,
|
|
58
|
+
},
|
|
59
|
+
router: {
|
|
60
|
+
...(framework === 'tanstack'
|
|
61
|
+
? { Link: TanstackLink, useRouter: () => router }
|
|
62
|
+
: { useLocation: () => undefined, useHref: () => undefined }),
|
|
63
|
+
},
|
|
64
|
+
} as any;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createTanstackRuntimeContext(router: unknown) {
|
|
68
|
+
return createRuntimeContext(router, 'tanstack');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createReactRouterRuntimeContext(router: unknown) {
|
|
72
|
+
return createRuntimeContext(router, 'react-router');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createTanstackRouter(pathname = '/en/terms-of-service', lang = 'en') {
|
|
76
|
+
const url = new URL(pathname, 'https://modernjs.test');
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
navigate: rstest.fn(async () => undefined),
|
|
80
|
+
state: {
|
|
81
|
+
location: {
|
|
82
|
+
pathname: url.pathname,
|
|
83
|
+
searchStr: url.search,
|
|
84
|
+
hash: url.hash,
|
|
85
|
+
},
|
|
86
|
+
matches: [
|
|
87
|
+
{
|
|
88
|
+
params: {
|
|
89
|
+
lang,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function renderWithRuntime(
|
|
98
|
+
node: React.ReactNode,
|
|
99
|
+
runtimeContext: ReturnType<typeof createTanstackRuntimeContext>,
|
|
100
|
+
) {
|
|
101
|
+
const container = document.createElement('div');
|
|
102
|
+
document.body.appendChild(container);
|
|
103
|
+
const root = createRoot(container);
|
|
104
|
+
|
|
105
|
+
await act(async () => {
|
|
106
|
+
root.render(
|
|
107
|
+
<InternalRuntimeContext.Provider value={runtimeContext}>
|
|
108
|
+
{node}
|
|
109
|
+
</InternalRuntimeContext.Provider>,
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
container,
|
|
115
|
+
root,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function cleanup(rendered?: { container: HTMLElement; root: Root }) {
|
|
120
|
+
if (!rendered) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
act(() => {
|
|
124
|
+
rendered.root.unmount();
|
|
125
|
+
});
|
|
126
|
+
rendered.container.remove();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
describe('i18n router adapter', () => {
|
|
130
|
+
let rendered: { container: HTMLElement; root: Root } | undefined;
|
|
131
|
+
|
|
132
|
+
afterEach(() => {
|
|
133
|
+
cleanup(rendered);
|
|
134
|
+
rendered = undefined;
|
|
135
|
+
window.history.replaceState(null, '', '/');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('uses the TanStack router Link for I18nLink rendering', async () => {
|
|
139
|
+
const router = createTanstackRouter('/cs/podminky-pouzivani', 'cs');
|
|
140
|
+
rendered = await renderWithRuntime(
|
|
141
|
+
<ModernI18nProvider
|
|
142
|
+
value={{
|
|
143
|
+
language: 'cs',
|
|
144
|
+
i18nInstance: createI18nInstance('cs'),
|
|
145
|
+
languages: ['en', 'cs'],
|
|
146
|
+
localePathRedirect: true,
|
|
147
|
+
localisedUrls,
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<I18nLink to="/terms-of-service" data-testid="terms-link">
|
|
151
|
+
Terms
|
|
152
|
+
</I18nLink>
|
|
153
|
+
</ModernI18nProvider>,
|
|
154
|
+
createTanstackRuntimeContext(router),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const link = rendered.container.querySelector<HTMLAnchorElement>(
|
|
158
|
+
'[data-testid="terms-link"]',
|
|
159
|
+
);
|
|
160
|
+
expect(link?.getAttribute('href')).toBe('/cs/podminky-pouzivani');
|
|
161
|
+
expect(link?.getAttribute('data-router-link')).toBe('tanstack');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('uses TanStack-shaped replacement when changeLanguage updates the URL', async () => {
|
|
165
|
+
window.history.replaceState(
|
|
166
|
+
null,
|
|
167
|
+
'',
|
|
168
|
+
'/en/terms-of-service?from=test#section',
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const router = createTanstackRouter(
|
|
172
|
+
'/en/terms-of-service?from=test#section',
|
|
173
|
+
);
|
|
174
|
+
let changeLanguagePromise: Promise<void> | undefined;
|
|
175
|
+
|
|
176
|
+
const Harness = () => {
|
|
177
|
+
const { changeLanguage } = useModernI18n();
|
|
178
|
+
return (
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
onClick={() => {
|
|
182
|
+
changeLanguagePromise = changeLanguage('cs');
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
Change language
|
|
186
|
+
</button>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
rendered = await renderWithRuntime(
|
|
191
|
+
<ModernI18nProvider
|
|
192
|
+
value={{
|
|
193
|
+
language: 'en',
|
|
194
|
+
i18nInstance: createI18nInstance('en'),
|
|
195
|
+
languages: ['en', 'cs'],
|
|
196
|
+
localePathRedirect: true,
|
|
197
|
+
localisedUrls,
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<Harness />
|
|
201
|
+
</ModernI18nProvider>,
|
|
202
|
+
createTanstackRuntimeContext(router),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const button = rendered.container.querySelector('button');
|
|
206
|
+
|
|
207
|
+
await act(async () => {
|
|
208
|
+
button?.dispatchEvent(
|
|
209
|
+
new MouseEvent('click', {
|
|
210
|
+
bubbles: true,
|
|
211
|
+
cancelable: true,
|
|
212
|
+
button: 0,
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
await changeLanguagePromise;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(router.navigate).toHaveBeenCalledWith({
|
|
219
|
+
to: '/cs/podminky-pouzivani?from=test#section',
|
|
220
|
+
replace: true,
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('keeps React Router positional replacement when changeLanguage updates the URL', async () => {
|
|
225
|
+
window.history.replaceState(null, '', '/en/terms-of-service');
|
|
226
|
+
|
|
227
|
+
const router = {
|
|
228
|
+
navigate: rstest.fn(async () => undefined),
|
|
229
|
+
};
|
|
230
|
+
let changeLanguagePromise: Promise<void> | undefined;
|
|
231
|
+
|
|
232
|
+
const Harness = () => {
|
|
233
|
+
const { changeLanguage } = useModernI18n();
|
|
234
|
+
return (
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={() => {
|
|
238
|
+
changeLanguagePromise = changeLanguage('cs');
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
Change language
|
|
242
|
+
</button>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
rendered = await renderWithRuntime(
|
|
247
|
+
<ModernI18nProvider
|
|
248
|
+
value={{
|
|
249
|
+
language: 'en',
|
|
250
|
+
i18nInstance: createI18nInstance('en'),
|
|
251
|
+
languages: ['en', 'cs'],
|
|
252
|
+
localePathRedirect: true,
|
|
253
|
+
localisedUrls,
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
<Harness />
|
|
257
|
+
</ModernI18nProvider>,
|
|
258
|
+
createReactRouterRuntimeContext(router),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const button = rendered.container.querySelector('button');
|
|
262
|
+
|
|
263
|
+
await act(async () => {
|
|
264
|
+
button?.dispatchEvent(
|
|
265
|
+
new MouseEvent('click', {
|
|
266
|
+
bubbles: true,
|
|
267
|
+
cancelable: true,
|
|
268
|
+
button: 0,
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
await changeLanguagePromise;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(router.navigate).toHaveBeenCalledWith('/cs/podminky-pouzivani', {
|
|
275
|
+
replace: true,
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
var __webpack_modules__ = {};
|
|
2
|
-
var __webpack_module_cache__ = {};
|
|
3
|
-
function __webpack_require__(moduleId) {
|
|
4
|
-
var cachedModule = __webpack_module_cache__[moduleId];
|
|
5
|
-
if (void 0 !== cachedModule) return cachedModule.exports;
|
|
6
|
-
var module = __webpack_module_cache__[moduleId] = {
|
|
7
|
-
exports: {}
|
|
8
|
-
};
|
|
9
|
-
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
10
|
-
return module.exports;
|
|
11
|
-
}
|
|
12
|
-
__webpack_require__.m = __webpack_modules__;
|
|
13
|
-
(()=>{
|
|
14
|
-
__webpack_require__.add = function(modules) {
|
|
15
|
-
Object.assign(__webpack_require__.m, modules);
|
|
16
|
-
};
|
|
17
|
-
})();
|
|
18
|
-
export { __webpack_require__ };
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import "node:module";
|
|
2
|
-
var __webpack_modules__ = {};
|
|
3
|
-
var __webpack_module_cache__ = {};
|
|
4
|
-
function __webpack_require__(moduleId) {
|
|
5
|
-
var cachedModule = __webpack_module_cache__[moduleId];
|
|
6
|
-
if (void 0 !== cachedModule) return cachedModule.exports;
|
|
7
|
-
var module = __webpack_module_cache__[moduleId] = {
|
|
8
|
-
exports: {}
|
|
9
|
-
};
|
|
10
|
-
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
11
|
-
return module.exports;
|
|
12
|
-
}
|
|
13
|
-
__webpack_require__.m = __webpack_modules__;
|
|
14
|
-
(()=>{
|
|
15
|
-
__webpack_require__.add = function(modules) {
|
|
16
|
-
Object.assign(__webpack_require__.m, modules);
|
|
17
|
-
};
|
|
18
|
-
})();
|
|
19
|
-
export { __webpack_require__ };
|