@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.3 → 3.5.0-ultramodern.31

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 (168) hide show
  1. package/dist/cjs/cli/index.js +2 -1
  2. package/dist/cjs/runtime/Link.js +8 -89
  3. package/dist/cjs/runtime/context.js +18 -82
  4. package/dist/cjs/runtime/contextHelpers.js +137 -0
  5. package/dist/cjs/runtime/core.js +29 -132
  6. package/dist/cjs/runtime/i18n/detection/cache.js +149 -0
  7. package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
  8. package/dist/cjs/runtime/i18n/detection/index.js +8 -263
  9. package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
  10. package/dist/cjs/runtime/i18n/detection/language.js +62 -0
  11. package/dist/cjs/runtime/i18n/detection/path.js +49 -0
  12. package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
  13. package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
  14. package/dist/cjs/runtime/i18n/detection/types.js +18 -0
  15. package/dist/cjs/runtime/linkHelpers.js +148 -0
  16. package/dist/cjs/runtime/pluginSetup.js +102 -0
  17. package/dist/cjs/runtime/providerComposition.js +113 -0
  18. package/dist/cjs/runtime/reactI18next.js +43 -0
  19. package/dist/cjs/server/apiPrefix.js +72 -0
  20. package/dist/cjs/server/detectorOptions.js +97 -0
  21. package/dist/cjs/server/index.js +17 -144
  22. package/dist/cjs/server/redirectPolicy.js +102 -0
  23. package/dist/cjs/shared/localisedUrls/config.js +49 -0
  24. package/dist/cjs/shared/localisedUrls/index.js +75 -0
  25. package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
  26. package/dist/cjs/shared/localisedUrls/pathname.js +65 -0
  27. package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
  28. package/dist/cjs/shared/localisedUrls/resolve.js +91 -0
  29. package/dist/cjs/shared/localisedUrls/routes.js +129 -0
  30. package/dist/cjs/shared/localisedUrls/types.js +18 -0
  31. package/dist/cjs/shared/localisedUrls.js +13 -294
  32. package/dist/esm/cli/index.mjs +2 -1
  33. package/dist/esm/runtime/Link.mjs +1 -82
  34. package/dist/esm/runtime/context.mjs +18 -82
  35. package/dist/esm/runtime/contextHelpers.mjs +84 -0
  36. package/dist/esm/runtime/core.mjs +30 -133
  37. package/dist/esm/runtime/i18n/detection/cache.mjs +102 -0
  38. package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
  39. package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
  40. package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
  41. package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
  42. package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
  43. package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
  44. package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
  45. package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
  46. package/dist/esm/runtime/linkHelpers.mjs +98 -0
  47. package/dist/esm/runtime/pluginSetup.mjs +64 -0
  48. package/dist/esm/runtime/providerComposition.mjs +75 -0
  49. package/dist/esm/runtime/reactI18next.mjs +5 -0
  50. package/dist/esm/server/apiPrefix.mjs +31 -0
  51. package/dist/esm/server/detectorOptions.mjs +59 -0
  52. package/dist/esm/server/index.mjs +6 -133
  53. package/dist/esm/server/redirectPolicy.mjs +52 -0
  54. package/dist/esm/shared/localisedUrls/config.mjs +11 -0
  55. package/dist/esm/shared/localisedUrls/index.mjs +6 -0
  56. package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
  57. package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
  58. package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
  59. package/dist/esm/shared/localisedUrls/resolve.mjs +50 -0
  60. package/dist/esm/shared/localisedUrls/routes.mjs +88 -0
  61. package/dist/esm/shared/localisedUrls/types.mjs +0 -0
  62. package/dist/esm/shared/localisedUrls.mjs +1 -283
  63. package/dist/esm-node/cli/index.mjs +2 -1
  64. package/dist/esm-node/runtime/Link.mjs +1 -82
  65. package/dist/esm-node/runtime/context.mjs +18 -82
  66. package/dist/esm-node/runtime/contextHelpers.mjs +85 -0
  67. package/dist/esm-node/runtime/core.mjs +30 -133
  68. package/dist/esm-node/runtime/i18n/detection/cache.mjs +103 -0
  69. package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
  70. package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
  71. package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
  72. package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
  73. package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
  74. package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
  75. package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
  76. package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
  77. package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
  78. package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
  79. package/dist/esm-node/runtime/providerComposition.mjs +76 -0
  80. package/dist/esm-node/runtime/reactI18next.mjs +6 -0
  81. package/dist/esm-node/server/apiPrefix.mjs +32 -0
  82. package/dist/esm-node/server/detectorOptions.mjs +60 -0
  83. package/dist/esm-node/server/index.mjs +6 -133
  84. package/dist/esm-node/server/redirectPolicy.mjs +53 -0
  85. package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
  86. package/dist/esm-node/shared/localisedUrls/index.mjs +7 -0
  87. package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
  88. package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
  89. package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
  90. package/dist/esm-node/shared/localisedUrls/resolve.mjs +51 -0
  91. package/dist/esm-node/shared/localisedUrls/routes.mjs +89 -0
  92. package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
  93. package/dist/esm-node/shared/localisedUrls.mjs +1 -283
  94. package/dist/types/runtime/Link.d.ts +3 -7
  95. package/dist/types/runtime/context.d.ts +2 -2
  96. package/dist/types/runtime/contextHelpers.d.ts +28 -0
  97. package/dist/types/runtime/core.d.ts +3 -7
  98. package/dist/types/runtime/hooks.d.ts +5 -0
  99. package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
  100. package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
  101. package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
  102. package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
  103. package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
  104. package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
  105. package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
  106. package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
  107. package/dist/types/runtime/i18n/detection/types.d.ts +29 -0
  108. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
  109. package/dist/types/runtime/linkHelpers.d.ts +17 -0
  110. package/dist/types/runtime/pluginSetup.d.ts +32 -0
  111. package/dist/types/runtime/providerComposition.d.ts +15 -0
  112. package/dist/types/runtime/reactI18next.d.ts +7 -0
  113. package/dist/types/runtime/utils.d.ts +7 -2
  114. package/dist/types/server/apiPrefix.d.ts +7 -0
  115. package/dist/types/server/detectorOptions.d.ts +21 -0
  116. package/dist/types/server/index.d.ts +1 -6
  117. package/dist/types/server/redirectPolicy.d.ts +16 -0
  118. package/dist/types/shared/localisedUrls/config.d.ts +9 -0
  119. package/dist/types/shared/localisedUrls/index.d.ts +7 -0
  120. package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
  121. package/dist/types/shared/localisedUrls/pathname.d.ts +3 -0
  122. package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
  123. package/dist/types/shared/localisedUrls/resolve.d.ts +8 -0
  124. package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
  125. package/dist/types/shared/localisedUrls/types.d.ts +14 -0
  126. package/dist/types/shared/localisedUrls.d.ts +2 -36
  127. package/package.json +10 -10
  128. package/src/cli/index.ts +3 -2
  129. package/src/runtime/Link.tsx +11 -138
  130. package/src/runtime/context.tsx +42 -196
  131. package/src/runtime/contextHelpers.ts +232 -0
  132. package/src/runtime/core.tsx +43 -274
  133. package/src/runtime/hooks.ts +3 -0
  134. package/src/runtime/i18n/detection/cache.ts +175 -0
  135. package/src/runtime/i18n/detection/detector.ts +157 -0
  136. package/src/runtime/i18n/detection/index.ts +10 -641
  137. package/src/runtime/i18n/detection/initOptions.ts +74 -0
  138. package/src/runtime/i18n/detection/language.ts +64 -0
  139. package/src/runtime/i18n/detection/path.ts +41 -0
  140. package/src/runtime/i18n/detection/priority.ts +82 -0
  141. package/src/runtime/i18n/detection/ssr.ts +47 -0
  142. package/src/runtime/i18n/detection/types.ts +33 -0
  143. package/src/runtime/i18n/react-i18next.ts +1 -6
  144. package/src/runtime/linkHelpers.ts +174 -0
  145. package/src/runtime/pluginSetup.ts +189 -0
  146. package/src/runtime/providerComposition.tsx +142 -0
  147. package/src/runtime/reactI18next.ts +20 -0
  148. package/src/runtime/utils.ts +7 -5
  149. package/src/server/apiPrefix.ts +67 -0
  150. package/src/server/detectorOptions.ts +105 -0
  151. package/src/server/index.ts +14 -319
  152. package/src/server/redirectPolicy.ts +152 -0
  153. package/src/shared/localisedUrls/config.ts +18 -0
  154. package/src/shared/localisedUrls/index.ts +13 -0
  155. package/src/shared/localisedUrls/normalise.ts +67 -0
  156. package/src/shared/localisedUrls/pathname.ts +57 -0
  157. package/src/shared/localisedUrls/patterns.ts +160 -0
  158. package/src/shared/localisedUrls/resolve.ts +118 -0
  159. package/src/shared/localisedUrls/routes.ts +226 -0
  160. package/src/shared/localisedUrls/types.ts +18 -0
  161. package/src/shared/localisedUrls.ts +20 -623
  162. package/tests/link.test.tsx +111 -0
  163. package/tests/linkTypes.test.ts +15 -4
  164. package/tests/localisedUrls.test.ts +54 -2
  165. package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
  166. package/tests/routerAdapter.test.tsx +56 -0
  167. package/tests/type-fixture/fixture-globals.d.ts +11 -0
  168. package/tests/type-fixture/tsconfig.json +1 -0
@@ -4,6 +4,7 @@ import { act } from 'react';
4
4
  import { createRoot, type Root } from 'react-dom/client';
5
5
  import { ModernI18nProvider } from '../src/runtime/context';
6
6
  import type { I18nInstance } from '../src/runtime/i18n';
7
+ import { detectLanguageWithPriority } from '../src/runtime/i18n/detection';
7
8
  import { interpolateRouteParams, Link } from '../src/runtime/Link';
8
9
  import {
9
10
  canonicalPath,
@@ -270,6 +271,60 @@ describe('localization utilities', () => {
270
271
  });
271
272
  });
272
273
 
274
+ describe('language detection priority', () => {
275
+ test('path locale overrides stale SSR data', async () => {
276
+ const previousSsrData = (window as any)._SSR_DATA;
277
+ (window as any)._SSR_DATA = { data: { i18nData: { lng: 'en' } } };
278
+
279
+ try {
280
+ await expect(
281
+ detectLanguageWithPriority(createI18nInstance('en'), {
282
+ languages,
283
+ fallbackLanguage: 'en',
284
+ localePathRedirect: true,
285
+ i18nextDetector: false,
286
+ detection: {},
287
+ userInitOptions: {},
288
+ pathname: '/cs/produkty',
289
+ ssrContext: undefined,
290
+ }),
291
+ ).resolves.toEqual({ detectedLanguage: 'cs', finalLanguage: 'cs' });
292
+ } finally {
293
+ if (previousSsrData === undefined) {
294
+ delete (window as any)._SSR_DATA;
295
+ } else {
296
+ (window as any)._SSR_DATA = previousSsrData;
297
+ }
298
+ }
299
+ });
300
+
301
+ test('regional SSR language resolves to supported base language', async () => {
302
+ const previousSsrData = (window as any)._SSR_DATA;
303
+ (window as any)._SSR_DATA = { data: { i18nData: { lng: 'en-US' } } };
304
+
305
+ try {
306
+ await expect(
307
+ detectLanguageWithPriority(createI18nInstance('cs'), {
308
+ languages,
309
+ fallbackLanguage: 'cs',
310
+ localePathRedirect: true,
311
+ i18nextDetector: false,
312
+ detection: {},
313
+ userInitOptions: {},
314
+ pathname: '/products',
315
+ ssrContext: undefined,
316
+ }),
317
+ ).resolves.toEqual({ detectedLanguage: 'en', finalLanguage: 'en' });
318
+ } finally {
319
+ if (previousSsrData === undefined) {
320
+ delete (window as any)._SSR_DATA;
321
+ } else {
322
+ (window as any)._SSR_DATA = previousSsrData;
323
+ }
324
+ }
325
+ });
326
+ });
327
+
273
328
  describe('framework Link', () => {
274
329
  let rendered: { container: HTMLElement; root: Root } | undefined;
275
330
 
@@ -328,6 +383,26 @@ describe('framework Link', () => {
328
383
  expect(props.hash).toBe('list');
329
384
  });
330
385
 
386
+ test('preserves array search values natively', async () => {
387
+ const router = createTanstackRouter('/en/products', 'en');
388
+ rendered = await renderWithRuntime(
389
+ <ModernI18nProvider value={providerValue('en')}>
390
+ <Link
391
+ to="/products"
392
+ search={{ tag: ['boots', 'sale'], page: 2 }}
393
+ data-testid="q"
394
+ >
395
+ Products
396
+ </Link>
397
+ </ModernI18nProvider>,
398
+ createTanstackRuntimeContext(router),
399
+ );
400
+
401
+ const props = capturedLinkProps.at(-1);
402
+ expect(props.to).toBe('/en/products');
403
+ expect(props.search).toEqual({ tag: ['boots', 'sale'], page: '2' });
404
+ });
405
+
331
406
  test('renders a plain anchor for external targets', async () => {
332
407
  const router = createTanstackRouter('/en', 'en');
333
408
  rendered = await renderWithRuntime(
@@ -381,6 +456,42 @@ describe('framework Link', () => {
381
456
  expect(link?.hasAttribute('prefetch')).toBe(false);
382
457
  });
383
458
 
459
+ test('serializes array search values for fallback anchors', async () => {
460
+ rendered = await renderWithRuntime(
461
+ <ModernI18nProvider value={providerValue('en')}>
462
+ <Link
463
+ to="/products"
464
+ search={{ tag: ['boots', 'sale'], page: 2 }}
465
+ data-testid="q"
466
+ >
467
+ Products
468
+ </Link>
469
+ </ModernI18nProvider>,
470
+ { isBrowser: true, requestContext, context: requestContext } as any,
471
+ );
472
+
473
+ const link = rendered.container.querySelector('[data-testid="q"]');
474
+ expect(link?.getAttribute('href')).toBe(
475
+ '/en/products?tag=boots&tag=sale&page=2',
476
+ );
477
+ });
478
+
479
+ test('empty search prop clears query from target fallback anchors', async () => {
480
+ rendered = await renderWithRuntime(
481
+ <ModernI18nProvider value={providerValue('en')}>
482
+ <Link to="/products?tag=x" search="" data-testid="clear-query">
483
+ Products
484
+ </Link>
485
+ </ModernI18nProvider>,
486
+ { isBrowser: true, requestContext, context: requestContext } as any,
487
+ );
488
+
489
+ const link = rendered.container.querySelector(
490
+ '[data-testid="clear-query"]',
491
+ );
492
+ expect(link?.getAttribute('href')).toBe('/en/products');
493
+ });
494
+
384
495
  test('maps prefetch to the TanStack preload prop', async () => {
385
496
  const router = createTanstackRouter('/en/products', 'en');
386
497
  rendered = await renderWithRuntime(
@@ -3,10 +3,21 @@ import path from 'node:path';
3
3
 
4
4
  const fixtureDir = path.resolve(__dirname, 'type-fixture');
5
5
 
6
- const tsgoBin = path.join(
7
- path.dirname(require.resolve('@typescript/native-preview/package.json')),
8
- 'bin/tsgo.js',
9
- );
6
+ function resolveTsgoBin() {
7
+ const pkgPath = require.resolve('@typescript/native-preview/package.json');
8
+ const pkgDir = path.dirname(pkgPath);
9
+ const pkg = require(pkgPath) as {
10
+ bin?:
11
+ | string
12
+ | {
13
+ tsgo?: string;
14
+ };
15
+ };
16
+ const binEntry = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.tsgo;
17
+ return path.resolve(pkgDir, binEntry ?? 'bin/tsgo.js');
18
+ }
19
+
20
+ const tsgoBin = resolveTsgoBin();
10
21
 
11
22
  describe('Link type-level tests', () => {
12
23
  test('fixture type-checks correctly: valid uses compile, invalid uses are rejected', () => {
@@ -272,6 +272,31 @@ describe('localisedUrls', () => {
272
272
  ).toBe('/products/cervena-bota');
273
273
  });
274
274
 
275
+ test('encodes wildcard params while preserving path separators', () => {
276
+ const localisedUrls = {
277
+ '/docs/*': {
278
+ en: '/docs/*',
279
+ cs: '/dokumenty/*',
280
+ },
281
+ };
282
+
283
+ expect(
284
+ resolveLocalisedPath(
285
+ '/docs/a%20b/c%23d',
286
+ 'cs',
287
+ ['en', 'cs'],
288
+ localisedUrls,
289
+ ),
290
+ ).toBe('/dokumenty/a%20b/c%23d');
291
+ expect(
292
+ resolveCanonicalLocalisedPath(
293
+ '/dokumenty/a%20b/c%23d',
294
+ ['en', 'cs'],
295
+ localisedUrls,
296
+ ),
297
+ ).toBe('/docs/a%20b/c%23d');
298
+ });
299
+
275
300
  test('resolves static patterns before param patterns', () => {
276
301
  const localisedUrls = {
277
302
  '/products/:slug': {
@@ -321,6 +346,31 @@ describe('localisedUrls', () => {
321
346
  ).toBe('/products/cervena-bota');
322
347
  });
323
348
 
349
+ test('strips case-insensitive locale prefixes before relocalising full pathnames', () => {
350
+ const localisedUrls = {
351
+ '/products/:slug': {
352
+ en: '/products/:slug',
353
+ cs: '/produkty/:slug',
354
+ },
355
+ };
356
+
357
+ expect(
358
+ localiseTargetPathname(
359
+ '/CS/produkty/cervena-bota',
360
+ 'en',
361
+ ['en', 'cs'],
362
+ localisedUrls,
363
+ ),
364
+ ).toBe('/en/products/cervena-bota');
365
+ expect(
366
+ canonicalTargetPathname(
367
+ '/CS/produkty/cervena-bota',
368
+ ['en', 'cs'],
369
+ localisedUrls,
370
+ ),
371
+ ).toBe('/products/cervena-bota');
372
+ });
373
+
324
374
  test('resolves nested optional route params with translated ancestors', () => {
325
375
  const localisedUrls = {
326
376
  '/checkout': {
@@ -509,7 +559,6 @@ describe('i18n server API prefix skips', () => {
509
559
  header: () => ({ host: 'localhost' }),
510
560
  },
511
561
  get: () => null,
512
- redirect: (url: string) => ({ redirectedTo: url }),
513
562
  }) as any;
514
563
 
515
564
  // Sanity: well-formed non-canonical slugs still redirect.
@@ -517,7 +566,10 @@ describe('i18n server API prefix skips', () => {
517
566
  createContext('/cs/products/bota'),
518
567
  async () => {},
519
568
  );
520
- expect(redirected).toEqual({ redirectedTo: '/cs/produkty/bota' });
569
+ expect(redirected.status).toBe(302);
570
+ expect(redirected.headers.get('location')).toBe('/cs/produkty/bota');
571
+ expect(redirected.headers.get('cache-control')).toBe('private, no-store');
572
+ expect(redirected.headers.get('vary')).toBe('Accept-Language, Cookie');
521
573
 
522
574
  // Malformed encoding must fall through to next() instead of throwing.
523
575
  let nextCalls = 0;
@@ -7,7 +7,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
7
7
  const runtimeRoot = resolve(__dirname, '../src/runtime');
8
8
 
9
9
  const readRuntimeSource = (file: string) =>
10
- readFileSync(resolve(runtimeRoot, file), 'utf8');
10
+ readFileSync(resolve(runtimeRoot, file), 'utf8').replace(/\r\n/gu, '\n');
11
11
 
12
12
  describe('react-i18next runtime boundary', () => {
13
13
  test('keeps the disabled runtime entry free of the optional adapter edge', () => {
@@ -21,11 +21,14 @@ describe('react-i18next runtime boundary', () => {
21
21
 
22
22
  test('keeps the runtime plugin factory entry synchronous for federation', () => {
23
23
  const core = readRuntimeSource('core.tsx');
24
+ const pluginSetup = readRuntimeSource('pluginSetup.ts');
24
25
 
25
26
  expect(core).not.toContain("from './i18n/backend/middleware'");
26
27
  expect(core).not.toContain("from './i18n/utils'");
27
- expect(core).toContain("import('./i18n/backend/middleware')");
28
- expect(core).toContain("import('./i18n/utils')");
28
+ expect(pluginSetup).not.toContain("from './i18n/backend/middleware'");
29
+ expect(pluginSetup).not.toContain("from './i18n/utils'");
30
+ expect(pluginSetup).toContain("import('./i18n/backend/middleware')");
31
+ expect(pluginSetup).toContain("import('./i18n/utils')");
29
32
  });
30
33
 
31
34
  test('keeps the Modern i18n context stable across federated runtime copies', () => {
@@ -399,6 +399,62 @@ describe('i18n router adapter', () => {
399
399
  });
400
400
  });
401
401
 
402
+ test('updates provider language when target locale is already in URL', async () => {
403
+ window.history.replaceState(null, '', '/cs/podminky-pouzivani');
404
+
405
+ const router = createTanstackRouter('/cs/podminky-pouzivani', 'cs');
406
+ const i18nInstance = createI18nInstance('en');
407
+ const updateLanguage = rstest.fn();
408
+ let changeLanguagePromise: Promise<void> | undefined;
409
+
410
+ const Harness = () => {
411
+ const { changeLanguage } = useModernI18n();
412
+ return (
413
+ <button
414
+ type="button"
415
+ onClick={() => {
416
+ changeLanguagePromise = changeLanguage('cs');
417
+ }}
418
+ >
419
+ Change language
420
+ </button>
421
+ );
422
+ };
423
+
424
+ rendered = await renderWithRuntime(
425
+ <ModernI18nProvider
426
+ value={{
427
+ language: 'en',
428
+ i18nInstance,
429
+ languages: ['en', 'cs'],
430
+ localePathRedirect: true,
431
+ localisedUrls,
432
+ updateLanguage,
433
+ }}
434
+ >
435
+ <Harness />
436
+ </ModernI18nProvider>,
437
+ createTanstackRuntimeContext(router),
438
+ );
439
+
440
+ const button = rendered.container.querySelector('button');
441
+ updateLanguage.mockClear();
442
+
443
+ await act(async () => {
444
+ button?.dispatchEvent(
445
+ new MouseEvent('click', {
446
+ bubbles: true,
447
+ cancelable: true,
448
+ button: 0,
449
+ }),
450
+ );
451
+ await changeLanguagePromise;
452
+ });
453
+
454
+ expect(updateLanguage).toHaveBeenCalledWith('cs');
455
+ expect(router.navigate).not.toHaveBeenCalled();
456
+ });
457
+
402
458
  test('keeps React Router positional replacement when changeLanguage updates the URL', async () => {
403
459
  window.history.replaceState(null, '', '/en/terms-of-service');
404
460
 
@@ -0,0 +1,11 @@
1
+ // The Window._SSR_DATA augmentation lives in @modern-js/runtime's internal
2
+ // core/types module, which this fixture's isolated program never pulls in.
3
+ interface Window {
4
+ _SSR_DATA?: {
5
+ data?: {
6
+ i18nData?: {
7
+ lng?: string;
8
+ };
9
+ };
10
+ };
11
+ }
@@ -7,6 +7,7 @@
7
7
  "noEmit": true,
8
8
  "skipLibCheck": true,
9
9
  "lib": ["ESNext", "DOM"],
10
+ "types": ["node"],
10
11
  "paths": {
11
12
  "@modern-js/plugin-i18n/runtime": ["../../src/runtime/index.tsx"]
12
13
  }