@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.
Files changed (127) hide show
  1. package/dist/cjs/cli/index.js +10 -21
  2. package/dist/cjs/cli/localisedRoutes.js +47 -0
  3. package/dist/cjs/runtime/Link.js +19 -32
  4. package/dist/cjs/runtime/context.js +30 -73
  5. package/dist/cjs/runtime/contextHelpers.js +5 -5
  6. package/dist/cjs/runtime/contexts.js +49 -0
  7. package/dist/cjs/runtime/core.js +27 -14
  8. package/dist/cjs/runtime/hooks.js +7 -8
  9. package/dist/cjs/runtime/localizedPaths.js +11 -9
  10. package/dist/cjs/runtime/navigation.js +48 -0
  11. package/dist/cjs/runtime/providerComposition.js +37 -13
  12. package/dist/cjs/runtime/routerAdapter.js +33 -140
  13. package/dist/cjs/runtime/sharedContexts.js +43 -0
  14. package/dist/cjs/runtime/utils.js +21 -19
  15. package/dist/cjs/server/defaultUrlStrategy.js +48 -0
  16. package/dist/cjs/server/index.js +11 -7
  17. package/dist/cjs/server/mappedUrlStrategy.js +43 -0
  18. package/dist/cjs/server/redirectPolicy.js +33 -24
  19. package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
  20. package/dist/cjs/shared/urlStrategy.js +48 -0
  21. package/dist/esm/cli/index.mjs +10 -21
  22. package/dist/esm/cli/localisedRoutes.mjs +9 -0
  23. package/dist/esm/runtime/Link.mjs +19 -32
  24. package/dist/esm/runtime/context.mjs +20 -72
  25. package/dist/esm/runtime/contextHelpers.mjs +5 -5
  26. package/dist/esm/runtime/contexts.mjs +5 -0
  27. package/dist/esm/runtime/core.mjs +13 -6
  28. package/dist/esm/runtime/hooks.mjs +7 -8
  29. package/dist/esm/runtime/localizedPaths.mjs +11 -9
  30. package/dist/esm/runtime/navigation.mjs +7 -0
  31. package/dist/esm/runtime/providerComposition.mjs +38 -14
  32. package/dist/esm/runtime/routerAdapter.mjs +28 -141
  33. package/dist/esm/runtime/sharedContexts.mjs +1 -0
  34. package/dist/esm/runtime/utils.mjs +19 -17
  35. package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
  36. package/dist/esm/server/index.mjs +11 -7
  37. package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
  38. package/dist/esm/server/redirectPolicy.mjs +33 -24
  39. package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
  40. package/dist/esm/shared/urlStrategy.mjs +7 -0
  41. package/dist/esm-node/cli/index.mjs +10 -21
  42. package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
  43. package/dist/esm-node/runtime/Link.mjs +19 -32
  44. package/dist/esm-node/runtime/context.mjs +20 -72
  45. package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
  46. package/dist/esm-node/runtime/contexts.mjs +6 -0
  47. package/dist/esm-node/runtime/core.mjs +13 -6
  48. package/dist/esm-node/runtime/hooks.mjs +7 -8
  49. package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
  50. package/dist/esm-node/runtime/navigation.mjs +8 -0
  51. package/dist/esm-node/runtime/providerComposition.mjs +38 -14
  52. package/dist/esm-node/runtime/routerAdapter.mjs +28 -141
  53. package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
  54. package/dist/esm-node/runtime/utils.mjs +19 -17
  55. package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
  56. package/dist/esm-node/server/index.mjs +11 -7
  57. package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
  58. package/dist/esm-node/server/redirectPolicy.mjs +33 -24
  59. package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
  60. package/dist/esm-node/shared/urlStrategy.mjs +8 -0
  61. package/dist/types/cli/localisedRoutes.d.ts +19 -0
  62. package/dist/types/runtime/context.d.ts +8 -11
  63. package/dist/types/runtime/contextHelpers.d.ts +2 -2
  64. package/dist/types/runtime/contexts.d.ts +6 -0
  65. package/dist/types/runtime/core.d.ts +12 -2
  66. package/dist/types/runtime/hooks.d.ts +4 -5
  67. package/dist/types/runtime/localizedPaths.d.ts +4 -4
  68. package/dist/types/runtime/navigation.d.ts +10 -0
  69. package/dist/types/runtime/providerComposition.d.ts +13 -1
  70. package/dist/types/runtime/routerAdapter.d.ts +23 -6
  71. package/dist/types/runtime/sharedContexts.d.ts +1 -0
  72. package/dist/types/runtime/utils.d.ts +4 -12
  73. package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
  74. package/dist/types/server/index.d.ts +2 -0
  75. package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
  76. package/dist/types/server/redirectPolicy.d.ts +5 -13
  77. package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
  78. package/dist/types/shared/type.d.ts +7 -11
  79. package/dist/types/shared/urlStrategy.d.ts +18 -0
  80. package/package.json +42 -19
  81. package/rslib.config.mts +8 -1
  82. package/rstest.config.mts +6 -5
  83. package/src/cli/index.ts +42 -42
  84. package/src/cli/localisedRoutes.ts +53 -0
  85. package/src/runtime/Link.tsx +20 -40
  86. package/src/runtime/context.tsx +33 -157
  87. package/src/runtime/contextHelpers.ts +7 -5
  88. package/src/runtime/contexts.ts +16 -0
  89. package/src/runtime/core.tsx +53 -6
  90. package/src/runtime/hooks.ts +14 -9
  91. package/src/runtime/i18n/instance.ts +1 -2
  92. package/src/runtime/localizedPaths.ts +24 -19
  93. package/src/runtime/navigation.tsx +20 -0
  94. package/src/runtime/providerComposition.tsx +71 -23
  95. package/src/runtime/routerAdapter.tsx +65 -339
  96. package/src/runtime/sharedContexts.ts +8 -0
  97. package/src/runtime/utils.ts +49 -31
  98. package/src/server/defaultUrlStrategy.ts +27 -0
  99. package/src/server/index.ts +39 -9
  100. package/src/server/mappedUrlStrategy.ts +10 -0
  101. package/src/server/redirectPolicy.ts +85 -70
  102. package/src/shared/mappedUrlStrategy.ts +59 -0
  103. package/src/shared/type.ts +7 -11
  104. package/src/shared/urlStrategy.ts +38 -0
  105. package/tests/backendDefaults.test.ts +2 -23
  106. package/tests/defaultUrlStrategy.test.ts +107 -0
  107. package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
  108. package/tests/i18nUtils.test.ts +31 -65
  109. package/tests/link.test.tsx +13 -511
  110. package/tests/localisedRouteGeneration.test.ts +200 -0
  111. package/tests/localisedUrls.test.ts +16 -208
  112. package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
  113. package/tests/navigation.test.tsx +403 -0
  114. package/tests/reactI18nextRuntimeBoundary.test.ts +8 -83
  115. package/tests/redirectPolicy.test.ts +89 -27
  116. package/tests/routerAdapter.test.tsx +98 -1171
  117. package/tests/urlStrategySerialization.test.ts +122 -0
  118. package/tsconfig.json +4 -2
  119. package/tests/federatedI18nBoundary.client.test.tsx +0 -129
  120. package/tests/federatedI18nBoundary.test.tsx +0 -132
  121. package/tests/linkTypes.test.ts +0 -59
  122. package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
  123. package/tests/peerCohort.test.ts +0 -25
  124. package/tests/type-fixture/fixture-globals.d.ts +0 -11
  125. package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
  126. package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
  127. package/tests/type-fixture/tsconfig.json +0 -16
@@ -1,73 +1,10 @@
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 } from '../src/runtime/context';
6
1
  import type { I18nInstance } from '../src/runtime/i18n';
7
2
  import { detectLanguageWithPriority } from '../src/runtime/i18n/detection';
8
- import { interpolateRouteParams, Link } from '../src/runtime/Link';
9
- import {
10
- canonicalPath,
11
- localizePath,
12
- useLocalizedLocation,
13
- } from '../src/runtime/localizedPaths';
3
+ import { interpolateRouteParams } from '../src/runtime/Link';
4
+ import { canonicalPath, localizePath } from '../src/runtime/localizedPaths';
14
5
  import { buildLocalizedUrl, splitUrlTarget } from '../src/runtime/utils';
15
6
 
16
- (
17
- globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
18
- ).IS_REACT_ACT_ENVIRONMENT = true;
19
-
20
- const localisedUrls = {
21
- '/terms-of-service': {
22
- en: '/terms-of-service',
23
- cs: '/podminky-pouzivani',
24
- },
25
- '/products': {
26
- en: '/products',
27
- cs: '/produkty',
28
- },
29
- '/products/:slug': {
30
- en: '/products/:slug',
31
- cs: '/produkty/:slug',
32
- },
33
- // Canonical key that matches no language pattern.
34
- '/talks/:slug': {
35
- en: '/lectures/:slug',
36
- cs: '/prednasky/:slug',
37
- },
38
- };
39
-
40
7
  const languages = ['en', 'cs'];
41
- const pathsConfig = { languages, localisedUrls };
42
-
43
- const requestContext = {
44
- request: {},
45
- response: {},
46
- };
47
-
48
- const capturedLinkProps: any[] = [];
49
-
50
- // Mirrors the real TanStack Link contract: it consumes its own props
51
- // (`preload`, `search`, `hash`, ...) and spreads everything else onto the
52
- // anchor. Deliberately does NOT strip `prefetch` — TanStack has no such prop,
53
- // so a forwarded `prefetch` would leak into the DOM and fail assertions.
54
- const TanstackLink = ({ to, children, ...props }: any) => {
55
- capturedLinkProps.push({ to, ...props });
56
- const {
57
- preload: _preload,
58
- search: _search,
59
- hash: _hash,
60
- hashScrollIntoView: _hashScrollIntoView,
61
- replace: _replace,
62
- ...anchorProps
63
- } = props;
64
-
65
- return (
66
- <a href={to} data-router-link="tanstack" {...anchorProps}>
67
- {children}
68
- </a>
69
- );
70
- };
71
8
 
72
9
  function createI18nInstance(language = 'en'): I18nInstance {
73
10
  return {
@@ -82,95 +19,10 @@ function createI18nInstance(language = 'en'): I18nInstance {
82
19
  };
83
20
  }
84
21
 
85
- function createTanstackRouter(target = '/en/terms-of-service', lang = 'en') {
86
- const url = new URL(target, 'https://modernjs.test');
87
-
88
- return {
89
- navigate: rstest.fn(async () => undefined),
90
- state: {
91
- location: {
92
- pathname: url.pathname,
93
- searchStr: url.search,
94
- hash: url.hash,
95
- },
96
- matches: [{ params: { lang } }],
97
- },
98
- };
99
- }
100
-
101
- function createTanstackRuntimeContext(router: unknown) {
102
- return {
103
- isBrowser: true,
104
- requestContext,
105
- context: requestContext,
106
- routerFramework: 'tanstack',
107
- routerInstance: router,
108
- routerRuntime: {
109
- framework: 'tanstack',
110
- instance: router,
111
- },
112
- router: {
113
- Link: TanstackLink,
114
- useRouter: () => router,
115
- },
116
- } as any;
117
- }
118
-
119
- function providerValue(language: string) {
120
- return {
121
- language,
122
- i18nInstance: createI18nInstance(language),
123
- languages,
124
- localePathRedirect: true,
125
- localisedUrls,
126
- };
127
- }
128
-
129
- async function renderWithRuntime(node: React.ReactNode, runtimeContext: any) {
130
- const container = document.createElement('div');
131
- document.body.appendChild(container);
132
- const root = createRoot(container);
133
-
134
- await act(async () => {
135
- root.render(
136
- <InternalRuntimeContext.Provider value={runtimeContext}>
137
- {node}
138
- </InternalRuntimeContext.Provider>,
139
- );
140
- });
141
-
142
- return { container, root };
143
- }
144
-
145
- function cleanup(rendered?: { container: HTMLElement; root: Root }) {
146
- if (!rendered) {
147
- return;
148
- }
149
- act(() => {
150
- rendered.root.unmount();
151
- });
152
- rendered.container.remove();
153
- }
154
-
155
- describe('splitUrlTarget', () => {
156
- test('splits pathname, search and hash', () => {
157
- expect(splitUrlTarget('/talks?tag=x#abstract')).toEqual({
158
- pathname: '/talks',
159
- search: '?tag=x',
160
- hash: '#abstract',
161
- });
162
- expect(splitUrlTarget('/#work-with-me')).toEqual({
163
- pathname: '/',
164
- search: '',
165
- hash: '#work-with-me',
166
- });
167
- expect(splitUrlTarget('/talks')).toEqual({
168
- pathname: '/talks',
169
- search: '',
170
- hash: '#'.slice(1) === '' ? '' : '',
171
- });
172
- expect(splitUrlTarget('?q=1#x')).toEqual({
173
- pathname: '',
22
+ describe('splitUrlTarget public compatibility export', () => {
23
+ test('retains the shared suffix splitter', () => {
24
+ expect(splitUrlTarget('/a?q=1#x')).toEqual({
25
+ pathname: '/a',
174
26
  search: '?q=1',
175
27
  hash: '#x',
176
28
  });
@@ -190,39 +42,8 @@ describe('buildLocalizedUrl suffix handling', () => {
190
42
  );
191
43
  });
192
44
 
193
- test('query and hash survive localized slug mapping', () => {
194
- expect(
195
- buildLocalizedUrl(
196
- '/products/bota?tag=x#detail',
197
- 'cs',
198
- languages,
199
- localisedUrls,
200
- ),
201
- ).toBe('/cs/produkty/bota?tag=x#detail');
202
- });
203
-
204
45
  test('root path', () => {
205
- expect(buildLocalizedUrl('/', 'cs', languages, localisedUrls)).toBe('/cs');
206
- });
207
-
208
- test('localizes canonical keys that match no language pattern', () => {
209
- expect(
210
- buildLocalizedUrl(
211
- '/talks/ai-slop#abstract',
212
- 'cs',
213
- languages,
214
- localisedUrls,
215
- ),
216
- ).toBe('/cs/prednasky/ai-slop#abstract');
217
- expect(
218
- buildLocalizedUrl('/talks/ai-slop', 'en', languages, localisedUrls),
219
- ).toBe('/en/lectures/ai-slop');
220
- });
221
-
222
- test('re-localizes already-localized paths', () => {
223
- expect(
224
- buildLocalizedUrl('/cs/produkty/bota#x', 'en', languages, localisedUrls),
225
- ).toBe('/en/products/bota#x');
46
+ expect(buildLocalizedUrl('/', 'cs', languages)).toBe('/cs');
226
47
  });
227
48
  });
228
49
 
@@ -252,23 +73,14 @@ describe('interpolateRouteParams', () => {
252
73
  });
253
74
  });
254
75
 
255
- describe('localization utilities', () => {
256
- test('localizePath maps canonical paths per language', () => {
257
- expect(localizePath('/products/bota', 'cs', pathsConfig)).toBe(
258
- '/cs/produkty/bota',
76
+ describe('native URL strategy helpers', () => {
77
+ test('preserves suffixes when adding or stripping only a language prefix', () => {
78
+ expect(localizePath('/products?q=1#x', 'cs', { languages })).toBe(
79
+ '/cs/products?q=1#x',
259
80
  );
260
- expect(localizePath('/talks/x', 'en', pathsConfig)).toBe('/en/lectures/x');
261
- });
262
-
263
- test('canonicalPath strips language and reverse-maps localized slugs', () => {
264
- expect(canonicalPath('/cs/produkty/bota', pathsConfig)).toBe(
265
- '/products/bota',
81
+ expect(canonicalPath('/CS/products?q=1#x', { languages })).toBe(
82
+ '/products?q=1#x',
266
83
  );
267
- expect(canonicalPath('/en/lectures/x?q=1#h', pathsConfig)).toBe(
268
- '/talks/x?q=1#h',
269
- );
270
- expect(canonicalPath('/cs', pathsConfig)).toBe('/');
271
- expect(canonicalPath('/en/products', pathsConfig)).toBe('/products');
272
84
  });
273
85
  });
274
86
 
@@ -325,313 +137,3 @@ describe('language detection priority', () => {
325
137
  }
326
138
  });
327
139
  });
328
-
329
- describe('framework Link', () => {
330
- let rendered: { container: HTMLElement; root: Root } | undefined;
331
-
332
- afterEach(() => {
333
- cleanup(rendered);
334
- rendered = undefined;
335
- capturedLinkProps.length = 0;
336
- });
337
-
338
- test('localizes canonical paths through the TanStack Link', async () => {
339
- const router = createTanstackRouter('/cs/podminky-pouzivani', 'cs');
340
- rendered = await renderWithRuntime(
341
- <ModernI18nProvider value={providerValue('cs')}>
342
- <Link to="/products/$slug" params={{ slug: 'bota' }} data-testid="p">
343
- Product
344
- </Link>
345
- </ModernI18nProvider>,
346
- createTanstackRuntimeContext(router),
347
- );
348
-
349
- const link = rendered.container.querySelector('[data-testid="p"]');
350
- expect(link?.getAttribute('href')).toBe('/cs/produkty/bota');
351
- expect(link?.getAttribute('data-router-link')).toBe('tanstack');
352
- });
353
-
354
- test('passes hash natively for cross-page hash targets', async () => {
355
- const router = createTanstackRouter('/cs/podminky-pouzivani', 'cs');
356
- rendered = await renderWithRuntime(
357
- <ModernI18nProvider value={providerValue('cs')}>
358
- <Link to="/#work-with-me" data-testid="cta">
359
- CTA
360
- </Link>
361
- </ModernI18nProvider>,
362
- createTanstackRuntimeContext(router),
363
- );
364
-
365
- const props = capturedLinkProps.at(-1);
366
- expect(props.to).toBe('/cs');
367
- expect(props.hash).toBe('work-with-me');
368
- });
369
-
370
- test('passes query and hash from the target natively', async () => {
371
- const router = createTanstackRouter('/en/products', 'en');
372
- rendered = await renderWithRuntime(
373
- <ModernI18nProvider value={providerValue('en')}>
374
- <Link to="/products?tag=x#list" data-testid="q">
375
- Products
376
- </Link>
377
- </ModernI18nProvider>,
378
- createTanstackRuntimeContext(router),
379
- );
380
-
381
- const props = capturedLinkProps.at(-1);
382
- expect(props.to).toBe('/en/products');
383
- expect(props.search).toEqual({ tag: 'x' });
384
- expect(props.hash).toBe('list');
385
- });
386
-
387
- test('preserves array search values natively', async () => {
388
- const router = createTanstackRouter('/en/products', 'en');
389
- rendered = await renderWithRuntime(
390
- <ModernI18nProvider value={providerValue('en')}>
391
- <Link
392
- to="/products"
393
- search={{ tag: ['boots', 'sale'], page: 2 }}
394
- data-testid="q"
395
- >
396
- Products
397
- </Link>
398
- </ModernI18nProvider>,
399
- createTanstackRuntimeContext(router),
400
- );
401
-
402
- const props = capturedLinkProps.at(-1);
403
- expect(props.to).toBe('/en/products');
404
- expect(props.search).toEqual({ tag: ['boots', 'sale'], page: '2' });
405
- });
406
-
407
- test('renders a plain anchor for external targets', async () => {
408
- const router = createTanstackRouter('/en', 'en');
409
- rendered = await renderWithRuntime(
410
- <ModernI18nProvider value={providerValue('en')}>
411
- <Link to="https://ai.bleeding.dev" data-testid="ext" prefetch="none">
412
- AI
413
- </Link>
414
- </ModernI18nProvider>,
415
- createTanstackRuntimeContext(router),
416
- );
417
-
418
- const link = rendered.container.querySelector('[data-testid="ext"]');
419
- expect(link?.getAttribute('href')).toBe('https://ai.bleeding.dev');
420
- expect(link?.getAttribute('data-router-link')).toBeNull();
421
- expect(link?.hasAttribute('prefetch')).toBe(false);
422
- });
423
-
424
- test('renders a plain anchor for same-page hash targets', async () => {
425
- const router = createTanstackRouter('/en', 'en');
426
- rendered = await renderWithRuntime(
427
- <ModernI18nProvider value={providerValue('en')}>
428
- <Link to="#work-with-me" data-testid="anchor">
429
- Jump
430
- </Link>
431
- </ModernI18nProvider>,
432
- createTanstackRuntimeContext(router),
433
- );
434
-
435
- const link = rendered.container.querySelector('[data-testid="anchor"]');
436
- expect(link?.getAttribute('href')).toBe('#work-with-me');
437
- expect(link?.getAttribute('data-router-link')).toBeNull();
438
- });
439
-
440
- test('falls back to a localized anchor without a router', async () => {
441
- rendered = await renderWithRuntime(
442
- <ModernI18nProvider value={providerValue('cs')}>
443
- <Link
444
- to="/products/$slug?tag=x#detail"
445
- params={{ slug: 'bota' }}
446
- data-testid="f"
447
- prefetch="viewport"
448
- >
449
- Product
450
- </Link>
451
- </ModernI18nProvider>,
452
- { isBrowser: true, requestContext, context: requestContext } as any,
453
- );
454
-
455
- const link = rendered.container.querySelector('[data-testid="f"]');
456
- expect(link?.getAttribute('href')).toBe('/cs/produkty/bota?tag=x#detail');
457
- expect(link?.hasAttribute('prefetch')).toBe(false);
458
- });
459
-
460
- test('serializes array search values for fallback anchors', async () => {
461
- rendered = await renderWithRuntime(
462
- <ModernI18nProvider value={providerValue('en')}>
463
- <Link
464
- to="/products"
465
- search={{ tag: ['boots', 'sale'], page: 2 }}
466
- data-testid="q"
467
- >
468
- Products
469
- </Link>
470
- </ModernI18nProvider>,
471
- { isBrowser: true, requestContext, context: requestContext } as any,
472
- );
473
-
474
- const link = rendered.container.querySelector('[data-testid="q"]');
475
- expect(link?.getAttribute('href')).toBe(
476
- '/en/products?tag=boots&tag=sale&page=2',
477
- );
478
- });
479
-
480
- test('empty search prop clears query from target fallback anchors', async () => {
481
- rendered = await renderWithRuntime(
482
- <ModernI18nProvider value={providerValue('en')}>
483
- <Link to="/products?tag=x" search="" data-testid="clear-query">
484
- Products
485
- </Link>
486
- </ModernI18nProvider>,
487
- { isBrowser: true, requestContext, context: requestContext } as any,
488
- );
489
-
490
- const link = rendered.container.querySelector(
491
- '[data-testid="clear-query"]',
492
- );
493
- expect(link?.getAttribute('href')).toBe('/en/products');
494
- });
495
-
496
- test('maps prefetch to the TanStack preload prop', async () => {
497
- const router = createTanstackRouter('/en/products', 'en');
498
- rendered = await renderWithRuntime(
499
- <ModernI18nProvider value={providerValue('en')}>
500
- <Link to="/products" data-testid="pf" prefetch="intent">
501
- Products
502
- </Link>
503
- </ModernI18nProvider>,
504
- createTanstackRuntimeContext(router),
505
- );
506
-
507
- const props = capturedLinkProps.at(-1);
508
- expect(props.preload).toBe('intent');
509
- expect(props.prefetch).toBeUndefined();
510
-
511
- const link = rendered.container.querySelector('[data-testid="pf"]');
512
- expect(link?.hasAttribute('prefetch')).toBe(false);
513
- });
514
-
515
- test('maps prefetch="none" to preload={false}; explicit preload wins', async () => {
516
- const router = createTanstackRouter('/en/products', 'en');
517
- rendered = await renderWithRuntime(
518
- <ModernI18nProvider value={providerValue('en')}>
519
- <Link to="/products" data-testid="none" prefetch="none">
520
- Products
521
- </Link>
522
- <Link
523
- to="/products"
524
- data-testid="explicit"
525
- prefetch="intent"
526
- preload="viewport"
527
- >
528
- Products
529
- </Link>
530
- </ModernI18nProvider>,
531
- createTanstackRuntimeContext(router),
532
- );
533
-
534
- const noneProps = capturedLinkProps[capturedLinkProps.length - 2];
535
- expect(noneProps.preload).toBe(false);
536
- expect(noneProps.prefetch).toBeUndefined();
537
-
538
- const explicitProps = capturedLinkProps.at(-1);
539
- expect(explicitProps.preload).toBe('viewport');
540
- expect(explicitProps.prefetch).toBeUndefined();
541
- });
542
-
543
- test('marks the canonical target active on any localized variant', async () => {
544
- const router = createTanstackRouter('/cs/podminky-pouzivani', 'cs');
545
- rendered = await renderWithRuntime(
546
- <ModernI18nProvider value={providerValue('cs')}>
547
- <Link
548
- to="/terms-of-service"
549
- data-testid="active-link"
550
- activeProps={{ className: 'is-active' }}
551
- className="nav"
552
- >
553
- Terms
554
- </Link>
555
- <Link to="/products" data-testid="inactive-link">
556
- Products
557
- </Link>
558
- </ModernI18nProvider>,
559
- createTanstackRuntimeContext(router),
560
- );
561
-
562
- const active = rendered.container.querySelector(
563
- '[data-testid="active-link"]',
564
- );
565
- expect(active?.getAttribute('data-status')).toBe('active');
566
- expect(active?.getAttribute('aria-current')).toBe('page');
567
- expect(active?.getAttribute('class')).toBe('nav is-active');
568
-
569
- const inactive = rendered.container.querySelector(
570
- '[data-testid="inactive-link"]',
571
- );
572
- expect(inactive?.getAttribute('data-status')).toBeNull();
573
- expect(inactive?.getAttribute('aria-current')).toBeNull();
574
- });
575
-
576
- test('prefix-matches nested locations unless exact is requested', async () => {
577
- const router = createTanstackRouter('/en/products/shoe', 'en');
578
- rendered = await renderWithRuntime(
579
- <ModernI18nProvider value={providerValue('en')}>
580
- <Link to="/products" data-testid="prefix">
581
- Products
582
- </Link>
583
- <Link
584
- to="/products"
585
- activeOptions={{ exact: true }}
586
- data-testid="exact"
587
- >
588
- Products
589
- </Link>
590
- <Link to="/" data-testid="root">
591
- Home
592
- </Link>
593
- </ModernI18nProvider>,
594
- createTanstackRuntimeContext(router),
595
- );
596
-
597
- expect(
598
- rendered.container
599
- .querySelector('[data-testid="prefix"]')
600
- ?.getAttribute('data-status'),
601
- ).toBe('active');
602
- expect(
603
- rendered.container
604
- .querySelector('[data-testid="exact"]')
605
- ?.getAttribute('data-status'),
606
- ).toBeNull();
607
- expect(
608
- rendered.container
609
- .querySelector('[data-testid="root"]')
610
- ?.getAttribute('data-status'),
611
- ).toBeNull();
612
- });
613
-
614
- test('useLocalizedLocation exposes per-language alternates', async () => {
615
- const router = createTanstackRouter('/cs/podminky-pouzivani?q=1#top', 'cs');
616
- let snapshot: ReturnType<typeof useLocalizedLocation> | undefined;
617
-
618
- const Probe = () => {
619
- snapshot = useLocalizedLocation();
620
- return null;
621
- };
622
-
623
- rendered = await renderWithRuntime(
624
- <ModernI18nProvider value={providerValue('cs')}>
625
- <Probe />
626
- </ModernI18nProvider>,
627
- createTanstackRuntimeContext(router),
628
- );
629
-
630
- expect(snapshot?.language).toBe('cs');
631
- expect(snapshot?.canonical).toBe('/terms-of-service');
632
- expect(snapshot?.alternates).toEqual({
633
- en: '/en/terms-of-service?q=1#top',
634
- cs: '/cs/podminky-pouzivani?q=1#top',
635
- });
636
- });
637
- });