@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.3 → 3.9.0-ultramodern.5

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 (79) hide show
  1. package/dist/cjs/cli/index.js +0 -22
  2. package/dist/cjs/runtime/Link.js +19 -32
  3. package/dist/cjs/runtime/context.js +26 -61
  4. package/dist/cjs/runtime/contextHelpers.js +5 -5
  5. package/dist/cjs/runtime/core.js +17 -14
  6. package/dist/cjs/runtime/hooks.js +7 -8
  7. package/dist/cjs/runtime/localizedPaths.js +11 -9
  8. package/dist/cjs/runtime/providerComposition.js +37 -13
  9. package/dist/cjs/runtime/routerAdapter.js +35 -140
  10. package/dist/cjs/runtime/utils.js +17 -19
  11. package/dist/cjs/server/index.js +10 -7
  12. package/dist/cjs/server/redirectPolicy.js +36 -31
  13. package/dist/cjs/shared/urlStrategy.js +18 -0
  14. package/dist/esm/cli/index.mjs +0 -22
  15. package/dist/esm/runtime/Link.mjs +19 -32
  16. package/dist/esm/runtime/context.mjs +18 -62
  17. package/dist/esm/runtime/contextHelpers.mjs +5 -5
  18. package/dist/esm/runtime/core.mjs +8 -5
  19. package/dist/esm/runtime/hooks.mjs +7 -8
  20. package/dist/esm/runtime/localizedPaths.mjs +11 -9
  21. package/dist/esm/runtime/providerComposition.mjs +38 -14
  22. package/dist/esm/runtime/routerAdapter.mjs +30 -141
  23. package/dist/esm/runtime/utils.mjs +15 -17
  24. package/dist/esm/server/index.mjs +10 -7
  25. package/dist/esm/server/redirectPolicy.mjs +29 -24
  26. package/dist/esm/shared/urlStrategy.mjs +0 -0
  27. package/dist/esm-node/cli/index.mjs +0 -22
  28. package/dist/esm-node/runtime/Link.mjs +19 -32
  29. package/dist/esm-node/runtime/context.mjs +18 -62
  30. package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
  31. package/dist/esm-node/runtime/core.mjs +8 -5
  32. package/dist/esm-node/runtime/hooks.mjs +7 -8
  33. package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
  34. package/dist/esm-node/runtime/providerComposition.mjs +38 -14
  35. package/dist/esm-node/runtime/routerAdapter.mjs +30 -141
  36. package/dist/esm-node/runtime/utils.mjs +15 -17
  37. package/dist/esm-node/server/index.mjs +10 -7
  38. package/dist/esm-node/server/redirectPolicy.mjs +29 -24
  39. package/dist/esm-node/shared/urlStrategy.mjs +1 -0
  40. package/dist/types/runtime/context.d.ts +8 -11
  41. package/dist/types/runtime/contextHelpers.d.ts +2 -2
  42. package/dist/types/runtime/core.d.ts +11 -2
  43. package/dist/types/runtime/hooks.d.ts +4 -5
  44. package/dist/types/runtime/localizedPaths.d.ts +4 -4
  45. package/dist/types/runtime/providerComposition.d.ts +13 -1
  46. package/dist/types/runtime/routerAdapter.d.ts +23 -6
  47. package/dist/types/runtime/utils.d.ts +4 -12
  48. package/dist/types/server/index.d.ts +2 -0
  49. package/dist/types/server/redirectPolicy.d.ts +5 -13
  50. package/dist/types/shared/type.d.ts +0 -14
  51. package/dist/types/shared/urlStrategy.d.ts +7 -0
  52. package/package.json +21 -11
  53. package/rstest.config.mts +2 -8
  54. package/src/cli/index.ts +1 -43
  55. package/src/runtime/Link.tsx +20 -40
  56. package/src/runtime/context.tsx +34 -133
  57. package/src/runtime/contextHelpers.ts +7 -5
  58. package/src/runtime/core.tsx +26 -6
  59. package/src/runtime/hooks.ts +14 -9
  60. package/src/runtime/localizedPaths.ts +24 -19
  61. package/src/runtime/providerComposition.tsx +71 -23
  62. package/src/runtime/routerAdapter.tsx +73 -338
  63. package/src/runtime/utils.ts +40 -31
  64. package/src/server/index.ts +44 -9
  65. package/src/server/redirectPolicy.ts +70 -70
  66. package/src/shared/type.ts +0 -14
  67. package/src/shared/urlStrategy.ts +11 -0
  68. package/tests/federatedI18nBoundaryResolver.test.tsx +71 -0
  69. package/tests/i18nUtils.test.ts +31 -65
  70. package/tests/link.test.tsx +13 -511
  71. package/tests/linkTypes.test.ts +12 -3
  72. package/tests/localisedUrlRewriteMatrix.fork.test.ts +4 -96
  73. package/tests/localisedUrls.test.ts +7 -216
  74. package/tests/reactI18nextRuntimeBoundary.test.ts +1 -82
  75. package/tests/redirectPolicy.test.ts +39 -30
  76. package/tests/routerAdapter.test.tsx +95 -1055
  77. package/tests/federatedI18nBoundary.client.test.tsx +0 -129
  78. package/tests/federatedI18nBoundary.test.tsx +0 -132
  79. package/tests/peerCohort.test.ts +0 -25
@@ -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
- });
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
 
4
5
  const fixtureDir = path.resolve(__dirname, 'type-fixture');
@@ -18,9 +19,17 @@ function resolveTsgoBin() {
18
19
  }
19
20
 
20
21
  const tsgoBin = resolveTsgoBin();
21
- const useShell = process.platform === 'win32';
22
- const execOptions = { cwd: path.dirname(__dirname), shell: useShell };
23
- execFileSync('pnpm', ['build'], execOptions);
22
+ for (const artifact of [
23
+ 'dist/types/runtime/context.d.ts',
24
+ 'dist/cjs/cli/index.js',
25
+ 'dist/cjs/shared/utils.js',
26
+ ]) {
27
+ if (!fs.existsSync(path.resolve(__dirname, '..', artifact))) {
28
+ throw new Error(
29
+ `Missing plugin-i18n build artifact ${artifact}. Run pnpm build:required before unit tests.`,
30
+ );
31
+ }
32
+ }
24
33
 
25
34
  describe('Link type-level tests', () => {
26
35
  test('fixture type-checks correctly: valid uses compile, invalid uses are rejected', () => {
@@ -1,107 +1,15 @@
1
- import type { LocalisedUrlsMap } from '@modern-js/i18n-runtime-extensions';
2
1
  import { describe, expect, test } from '@rstest/core';
3
2
  import {
4
3
  interpolateRouteParams,
5
4
  normalizeSearch,
6
5
  } from '../src/runtime/linkHelpers';
7
- import { canonicalPath, localizePath } from '../src/runtime/localizedPaths';
8
- import { buildLocalizedUrl } from '../src/runtime/utils';
9
-
10
- const languages = ['en', 'cs'];
11
-
12
- const localisedUrls = {
13
- '/products/:slug': {
14
- en: '/products/:slug',
15
- cs: '/produkty/:slug',
16
- },
17
- '/files/*': {
18
- en: '/files/*',
19
- cs: '/soubory/*',
20
- },
21
- '/app/products/:slug': {
22
- en: '/app/products/:slug',
23
- cs: '/app/produkty/:slug',
24
- },
25
- } satisfies LocalisedUrlsMap;
26
-
27
- const pathsConfig = {
28
- languages,
29
- localisedUrls,
30
- };
31
-
32
- describe('fork-owned localised URL rewrite matrix', () => {
33
- const rewriteScenarios = [
34
- {
35
- name: 'adds the target locale prefix and localises canonical segments',
36
- target: '/products/red-shoe',
37
- language: 'cs',
38
- expected: '/cs/produkty/red-shoe',
39
- },
40
- {
41
- name: 'strips a case-insensitive source locale before relocalising',
42
- target: '/CS/produkty/red%20shoe?tag=boots#details',
43
- language: 'en',
44
- expected: '/en/products/red%20shoe?tag=boots#details',
45
- },
46
- {
47
- name: 'preserves repeated query keys and the fragment',
48
- target: '/products/red-shoe?tag=boots&tag=sale#details',
49
- language: 'cs',
50
- expected: '/cs/produkty/red-shoe?tag=boots&tag=sale#details',
51
- },
52
- {
53
- name: 'keeps a base-path segment inside the localised route map',
54
- target: '/app/products/red-shoe?ref=nav#details',
55
- language: 'cs',
56
- expected: '/cs/app/produkty/red-shoe?ref=nav#details',
57
- },
58
- {
59
- name: 'keeps the default locale explicitly prefixed',
60
- target: '/products/red-shoe',
61
- language: 'en',
62
- expected: '/en/products/red-shoe',
63
- },
64
- {
65
- name: 'does not double-prefix an already-localised URL',
66
- target: '/cs/produkty/red-shoe?tag=boots#details',
67
- language: 'cs',
68
- expected: '/cs/produkty/red-shoe?tag=boots#details',
69
- },
70
- ];
71
-
72
- for (const scenario of rewriteScenarios) {
73
- test(scenario.name, () => {
74
- expect(
75
- buildLocalizedUrl(
76
- scenario.target,
77
- scenario.language,
78
- languages,
79
- localisedUrls,
80
- ),
81
- ).toBe(scenario.expected);
82
- });
83
- }
84
-
85
- test('canonical and localized path helpers strip and add prefixes exactly', () => {
86
- expect(
87
- canonicalPath('/CS/produkty/red-shoe?tag=boots#details', pathsConfig),
88
- ).toBe('/products/red-shoe?tag=boots#details');
89
- expect(
90
- localizePath('/products/red-shoe?tag=boots#details', 'cs', pathsConfig),
91
- ).toBe('/cs/produkty/red-shoe?tag=boots#details');
92
- });
93
6
 
7
+ describe('native link target normalization', () => {
94
8
  test('splat params preserve separators and percent-encode each segment', () => {
95
- const target = interpolateRouteParams('/files/*', {
96
- '*': 'resume drafts/Q1 deck.pdf',
97
- });
98
-
99
- expect(target).toBe('/files/resume%20drafts/Q1%20deck.pdf');
100
- expect(buildLocalizedUrl(target, 'cs', languages, localisedUrls)).toBe(
101
- '/cs/soubory/resume%20drafts/Q1%20deck.pdf',
102
- );
9
+ expect(
10
+ interpolateRouteParams('/files/*', { '*': 'resume drafts/Q1 deck.pdf' }),
11
+ ).toBe('/files/resume%20drafts/Q1%20deck.pdf');
103
12
  });
104
-
105
13
  const searchScenarios: Array<{
106
14
  name: string;
107
15
  search: Parameters<typeof normalizeSearch>[0];