@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.2 → 3.2.0-ultramodern.23

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 (85) hide show
  1. package/dist/cjs/cli/index.js +22 -0
  2. package/dist/cjs/runtime/I18nLink.js +4 -12
  3. package/dist/cjs/runtime/context.js +32 -5
  4. package/dist/cjs/runtime/hooks.js +8 -5
  5. package/dist/cjs/runtime/i18n/backend/defaults.js +1 -1
  6. package/dist/cjs/runtime/i18n/backend/middleware.node.js +4 -4
  7. package/dist/cjs/runtime/i18n/instance.js +4 -2
  8. package/dist/cjs/runtime/index.js +7 -6
  9. package/dist/cjs/runtime/routerAdapter.js +163 -0
  10. package/dist/cjs/runtime/utils.js +63 -94
  11. package/dist/cjs/server/index.js +64 -8
  12. package/dist/cjs/shared/localisedUrls.js +237 -0
  13. package/dist/esm/cli/index.mjs +22 -0
  14. package/dist/esm/runtime/I18nLink.mjs +4 -12
  15. package/dist/esm/runtime/context.mjs +34 -7
  16. package/dist/esm/runtime/hooks.mjs +9 -6
  17. package/dist/esm/runtime/i18n/backend/defaults.mjs +1 -1
  18. package/dist/esm/runtime/i18n/backend/middleware.node.mjs +3 -3
  19. package/dist/esm/runtime/i18n/instance.mjs +4 -2
  20. package/dist/esm/runtime/index.mjs +7 -6
  21. package/dist/esm/runtime/routerAdapter.mjs +129 -0
  22. package/dist/esm/runtime/utils.mjs +11 -30
  23. package/dist/esm/server/index.mjs +57 -7
  24. package/dist/esm/shared/localisedUrls.mjs +191 -0
  25. package/dist/esm-node/cli/index.mjs +22 -0
  26. package/dist/esm-node/runtime/I18nLink.mjs +4 -12
  27. package/dist/esm-node/runtime/context.mjs +34 -7
  28. package/dist/esm-node/runtime/hooks.mjs +9 -6
  29. package/dist/esm-node/runtime/i18n/backend/defaults.mjs +1 -1
  30. package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +3 -3
  31. package/dist/esm-node/runtime/i18n/instance.mjs +4 -2
  32. package/dist/esm-node/runtime/index.mjs +7 -6
  33. package/dist/esm-node/runtime/routerAdapter.mjs +130 -0
  34. package/dist/esm-node/runtime/utils.mjs +11 -30
  35. package/dist/esm-node/server/index.mjs +57 -7
  36. package/dist/esm-node/shared/localisedUrls.mjs +192 -0
  37. package/dist/types/cli/index.d.ts +21 -0
  38. package/dist/types/runtime/I18nLink.d.ts +23 -0
  39. package/dist/types/runtime/context.d.ts +41 -0
  40. package/dist/types/runtime/hooks.d.ts +30 -0
  41. package/dist/types/runtime/i18n/backend/config.d.ts +2 -0
  42. package/dist/types/runtime/i18n/backend/defaults.d.ts +13 -0
  43. package/dist/types/runtime/i18n/backend/defaults.node.d.ts +8 -0
  44. package/dist/types/runtime/i18n/backend/index.d.ts +3 -0
  45. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +14 -0
  46. package/dist/types/runtime/i18n/backend/middleware.d.ts +12 -0
  47. package/dist/types/runtime/i18n/backend/middleware.node.d.ts +13 -0
  48. package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +53 -0
  49. package/dist/types/runtime/i18n/backend/sdk-event.d.ts +9 -0
  50. package/dist/types/runtime/i18n/detection/config.d.ts +11 -0
  51. package/dist/types/runtime/i18n/detection/index.d.ts +50 -0
  52. package/dist/types/runtime/i18n/detection/middleware.d.ts +24 -0
  53. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +17 -0
  54. package/dist/types/runtime/i18n/index.d.ts +3 -0
  55. package/dist/types/runtime/i18n/instance.d.ts +96 -0
  56. package/dist/types/runtime/i18n/utils.d.ts +29 -0
  57. package/dist/types/runtime/index.d.ts +21 -0
  58. package/dist/types/runtime/routerAdapter.d.ts +26 -0
  59. package/dist/types/runtime/types.d.ts +15 -0
  60. package/dist/types/runtime/utils.d.ts +28 -0
  61. package/dist/types/server/index.d.ts +14 -0
  62. package/dist/types/shared/deepMerge.d.ts +1 -0
  63. package/dist/types/shared/detection.d.ts +11 -0
  64. package/dist/types/shared/localisedUrls.d.ts +13 -0
  65. package/dist/types/shared/type.d.ts +168 -0
  66. package/dist/types/shared/utils.d.ts +5 -0
  67. package/package.json +15 -15
  68. package/rstest.config.mts +39 -0
  69. package/src/cli/index.ts +43 -1
  70. package/src/runtime/I18nLink.tsx +10 -16
  71. package/src/runtime/context.tsx +45 -7
  72. package/src/runtime/hooks.ts +13 -4
  73. package/src/runtime/i18n/backend/defaults.ts +3 -1
  74. package/src/runtime/i18n/backend/middleware.node.ts +1 -1
  75. package/src/runtime/i18n/instance.ts +14 -5
  76. package/src/runtime/index.tsx +10 -2
  77. package/src/runtime/routerAdapter.tsx +333 -0
  78. package/src/runtime/utils.ts +22 -34
  79. package/src/server/index.ts +135 -10
  80. package/src/shared/localisedUrls.ts +393 -0
  81. package/src/shared/type.ts +12 -0
  82. package/tests/localisedUrls.test.ts +278 -0
  83. package/tests/routerAdapter.test.tsx +278 -0
  84. package/dist/esm/rslib-runtime.mjs +0 -18
  85. package/dist/esm-node/rslib-runtime.mjs +0 -19
@@ -1,15 +1,22 @@
1
1
  import { isBrowser } from '@modern-js/runtime';
2
2
  import type { FC, ReactNode } from 'react';
3
- import { createContext, useCallback, useContext, useMemo } from 'react';
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ } from 'react';
10
+ import type { LocalisedUrlsOption } from '../shared/localisedUrls';
4
11
  import type { I18nInstance } from './i18n';
5
12
  import type { SdkBackend } from './i18n/backend/sdk-backend';
6
13
  import { cacheUserLanguage } from './i18n/detection';
14
+ import { useI18nRouterAdapter } from './routerAdapter';
7
15
  import {
8
16
  buildLocalizedUrl,
9
17
  detectLanguageFromPath,
10
18
  getEntryPath,
11
19
  shouldIgnoreRedirect,
12
- useRouterHooks,
13
20
  } from './utils';
14
21
 
15
22
  export interface ModernI18nContextValue {
@@ -20,6 +27,7 @@ export interface ModernI18nContextValue {
20
27
  languages?: string[];
21
28
  localePathRedirect?: boolean;
22
29
  ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
30
+ localisedUrls?: LocalisedUrlsOption;
23
31
  // Callback to update language in context
24
32
  updateLanguage?: (newLang: string) => void;
25
33
  }
@@ -47,6 +55,7 @@ export interface UseModernI18nReturn {
47
55
  changeLanguage: (newLang: string) => Promise<void>;
48
56
  i18nInstance: I18nInstance;
49
57
  supportedLanguages: string[];
58
+ localisedUrls?: LocalisedUrlsOption;
50
59
  isLanguageSupported: (lang: string) => boolean;
51
60
  // Indicates if translation resources for current language are ready to use
52
61
  isResourcesReady: boolean;
@@ -77,15 +86,40 @@ export const useModernI18n = (): UseModernI18nReturn => {
77
86
  languages,
78
87
  localePathRedirect,
79
88
  ignoreRedirectRoutes,
89
+ localisedUrls,
80
90
  updateLanguage,
81
91
  } = context;
82
92
 
83
- // Get router hooks safely
84
- const { navigate, location, hasRouter } = useRouterHooks();
93
+ const { navigate, location, hasRouter } = useI18nRouterAdapter();
94
+
95
+ const pathLanguage = useMemo(() => {
96
+ if (!localePathRedirect || !location?.pathname) {
97
+ return undefined;
98
+ }
99
+ const detected = detectLanguageFromPath(
100
+ location.pathname,
101
+ languages || [],
102
+ localePathRedirect,
103
+ );
104
+ return detected.detected ? detected.language : undefined;
105
+ }, [languages, localePathRedirect, location?.pathname]);
106
+
107
+ const currentLanguage = pathLanguage || contextLanguage;
85
108
 
86
- // Get current language from context (which reflects the actual current language)
87
- // URL params might be stale after language changes, so we prioritize the context language
88
- const currentLanguage = contextLanguage;
109
+ useEffect(() => {
110
+ if (!pathLanguage || pathLanguage === contextLanguage) {
111
+ return;
112
+ }
113
+
114
+ updateLanguage?.(pathLanguage);
115
+ i18nInstance?.setLang?.(pathLanguage);
116
+ void i18nInstance?.changeLanguage?.(pathLanguage);
117
+
118
+ if (isBrowser()) {
119
+ const detectionOptions = i18nInstance.options?.detection;
120
+ cacheUserLanguage(i18nInstance, pathLanguage, detectionOptions);
121
+ }
122
+ }, [contextLanguage, i18nInstance, pathLanguage, updateLanguage]);
89
123
 
90
124
  /**
91
125
  * Changes the current language and updates the URL accordingly.
@@ -147,6 +181,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
147
181
  relativePath,
148
182
  newLang,
149
183
  languages || [],
184
+ localisedUrls,
150
185
  );
151
186
  const newUrl =
152
187
  entryPath + newPath + location.search + location.hash;
@@ -181,6 +216,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
181
216
  relativePath,
182
217
  newLang,
183
218
  languages || [],
219
+ localisedUrls,
184
220
  );
185
221
  const newUrl =
186
222
  entryPath +
@@ -206,6 +242,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
206
242
  updateLanguage,
207
243
  localePathRedirect,
208
244
  ignoreRedirectRoutes,
245
+ localisedUrls,
209
246
  languages,
210
247
  hasRouter,
211
248
  navigate,
@@ -275,6 +312,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
275
312
  changeLanguage,
276
313
  i18nInstance,
277
314
  supportedLanguages: languages || [],
315
+ localisedUrls,
278
316
  isLanguageSupported,
279
317
  isResourcesReady,
280
318
  };
@@ -2,6 +2,7 @@ import type { TRuntimeContext } from '@modern-js/runtime';
2
2
  import { isBrowser } from '@modern-js/runtime';
3
3
  import type React from 'react';
4
4
  import { useEffect, useRef } from 'react';
5
+ import type { LocalisedUrlsOption } from '../shared/localisedUrls';
5
6
  import type { I18nInstance } from './i18n';
6
7
  import {
7
8
  getI18nSdkBackendId,
@@ -9,13 +10,13 @@ import {
9
10
  type I18nSdkResourcesLoadedEventDetail,
10
11
  } from './i18n/backend/sdk-event';
11
12
  import { cacheUserLanguage } from './i18n/detection';
13
+ import { useI18nRouterAdapter } from './routerAdapter';
12
14
  import {
13
15
  buildLocalizedUrl,
14
16
  detectLanguageFromPath,
15
17
  getEntryPath,
16
18
  getPathname,
17
19
  shouldIgnoreRedirect,
18
- useRouterHooks,
19
20
  } from './utils';
20
21
 
21
22
  interface RuntimeContextWithI18n extends TRuntimeContext {
@@ -41,6 +42,7 @@ export function createContextValue(
41
42
  languages: string[],
42
43
  localePathRedirect: boolean,
43
44
  ignoreRedirectRoutes: string[] | ((pathname: string) => boolean) | undefined,
45
+ localisedUrls: LocalisedUrlsOption | undefined,
44
46
  setLang: (lang: string) => void,
45
47
  ) {
46
48
  const instance = i18nInstance || createMinimalI18nInstance(lang);
@@ -51,6 +53,7 @@ export function createContextValue(
51
53
  languages,
52
54
  localePathRedirect,
53
55
  ignoreRedirectRoutes,
56
+ localisedUrls,
54
57
  updateLanguage: setLang,
55
58
  };
56
59
  }
@@ -162,10 +165,10 @@ export function useClientSideRedirect(
162
165
  languages: string[],
163
166
  fallbackLanguage: string,
164
167
  ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
168
+ localisedUrls?: LocalisedUrlsOption,
165
169
  ) {
166
170
  const hasRedirectedRef = useRef(false);
167
- // Get router hooks safely
168
- const { navigate, location, hasRouter } = useRouterHooks();
171
+ const { navigate, location, hasRouter } = useI18nRouterAdapter();
169
172
 
170
173
  useEffect(() => {
171
174
  if (process.env.MODERN_TARGET !== 'browser') {
@@ -220,7 +223,12 @@ export function useClientSideRedirect(
220
223
  const targetLanguage =
221
224
  i18nInstance.language || fallbackLanguage || languages[0] || 'en';
222
225
 
223
- const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages);
226
+ const newPath = buildLocalizedUrl(
227
+ relativePath,
228
+ targetLanguage,
229
+ languages,
230
+ localisedUrls,
231
+ );
224
232
  const newUrl = entryPath + newPath + currentSearch + currentHash;
225
233
 
226
234
  if (newUrl !== currentPathname + currentSearch + currentHash) {
@@ -244,6 +252,7 @@ export function useClientSideRedirect(
244
252
  languages,
245
253
  fallbackLanguage,
246
254
  ignoreRedirectRoutes,
255
+ localisedUrls,
247
256
  ]);
248
257
  }
249
258
 
@@ -15,7 +15,9 @@ function convertPath(path: string | undefined): string | undefined {
15
15
  }
16
16
  // If it's an absolute path (starts with /), convert to relative path
17
17
  if (path.startsWith('/')) {
18
- return `${window.__assetPrefix__ || ''}${path}`;
18
+ return typeof window === 'undefined'
19
+ ? path
20
+ : `${window.__assetPrefix__ || ''}${path}`;
19
21
  }
20
22
  return path;
21
23
  }
@@ -1,4 +1,4 @@
1
- import Backend from 'i18next-fs-backend';
1
+ import Backend from 'i18next-fs-backend/cjs';
2
2
  import type { ExtendedBackendOptions } from '../../../shared/type';
3
3
  import type { I18nInstance } from '../instance';
4
4
  import { useI18nextBackendCommon } from './middleware.common';
@@ -1,5 +1,9 @@
1
1
  import type { BaseBackendOptions } from '../../shared/type';
2
2
 
3
+ type ReactI18nextModule = typeof import('react-i18next');
4
+ type InitReactI18next = ReactI18nextModule['initReactI18next'];
5
+ type I18nextProviderComponent = ReactI18nextModule['I18nextProvider'];
6
+
3
7
  export interface I18nResourceStore {
4
8
  data?: {
5
9
  [language: string]: {
@@ -167,10 +171,15 @@ async function createI18nextInstance(): Promise<I18nInstance | null> {
167
171
  }
168
172
  }
169
173
 
170
- async function tryImportReactI18next() {
174
+ function getOptionalReactI18nextPackageName(): string {
175
+ return ['react', 'i18next'].join('-');
176
+ }
177
+
178
+ async function tryImportReactI18next(): Promise<ReactI18nextModule | null> {
171
179
  try {
172
- const reactI18next = await import('react-i18next');
173
- return reactI18next;
180
+ return (await import(
181
+ getOptionalReactI18nextPackageName()
182
+ )) as ReactI18nextModule;
174
183
  } catch (error) {
175
184
  return null;
176
185
  }
@@ -210,7 +219,7 @@ export async function getI18nInstance(
210
219
  throw new Error('No i18n instance found');
211
220
  }
212
221
 
213
- export async function getInitReactI18next() {
222
+ export async function getInitReactI18next(): Promise<InitReactI18next | null> {
214
223
  const reactI18nextModule = await tryImportReactI18next();
215
224
  if (reactI18nextModule) {
216
225
  return reactI18nextModule.initReactI18next;
@@ -218,7 +227,7 @@ export async function getInitReactI18next() {
218
227
  return null;
219
228
  }
220
229
 
221
- export async function getI18nextProvider() {
230
+ export async function getI18nextProvider(): Promise<I18nextProviderComponent | null> {
222
231
  const reactI18nextModule = await tryImportReactI18next();
223
232
  if (reactI18nextModule) {
224
233
  return reactI18nextModule.I18nextProvider;
@@ -54,6 +54,7 @@ export interface I18nPluginOptions {
54
54
  changeLanguage?: (lang: string) => void;
55
55
  initOptions?: I18nInitOptions;
56
56
  htmlLangAttr?: boolean;
57
+ reactI18next?: boolean;
57
58
  [key: string]: any;
58
59
  }
59
60
 
@@ -72,6 +73,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
72
73
  localeDetection,
73
74
  backend,
74
75
  htmlLangAttr = false,
76
+ reactI18next = true,
75
77
  } = options;
76
78
  const {
77
79
  localePathRedirect = false,
@@ -80,6 +82,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
80
82
  fallbackLanguage = 'en',
81
83
  detection,
82
84
  ignoreRedirectRoutes,
85
+ localisedUrls,
83
86
  } = localeDetection || {};
84
87
  const { enabled: backendEnabled = false } = backend || {};
85
88
  let latestI18nInstance: I18nInstance | undefined;
@@ -90,8 +93,10 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
90
93
  const { i18n: otherConfig } = api.getRuntimeConfig();
91
94
  const { initOptions: otherInitOptions } = otherConfig || {};
92
95
  const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
93
- const initReactI18next = await getInitReactI18next();
94
- I18nextProvider = await getI18nextProvider();
96
+ const initReactI18next = reactI18next
97
+ ? await getInitReactI18next()
98
+ : null;
99
+ I18nextProvider = reactI18next ? await getI18nextProvider() : null;
95
100
  if (initReactI18next) {
96
101
  i18nInstance.use(initReactI18next);
97
102
  }
@@ -227,6 +232,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
227
232
  languages,
228
233
  fallbackLanguage,
229
234
  ignoreRedirectRoutes,
235
+ localisedUrls,
230
236
  );
231
237
 
232
238
  const contextValue = useMemo(
@@ -238,6 +244,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
238
244
  languages,
239
245
  localePathRedirect,
240
246
  ignoreRedirectRoutes,
247
+ localisedUrls,
241
248
  setLang,
242
249
  ),
243
250
  [
@@ -247,6 +254,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
247
254
  languages,
248
255
  localePathRedirect,
249
256
  ignoreRedirectRoutes,
257
+ localisedUrls,
250
258
  forceUpdate,
251
259
  ],
252
260
  );
@@ -0,0 +1,333 @@
1
+ import { isBrowser, RuntimeContext } from '@modern-js/runtime';
2
+ import {
3
+ InternalRuntimeContext,
4
+ type TInternalRuntimeContext,
5
+ type TRuntimeContext,
6
+ } from '@modern-js/runtime/context';
7
+ import {
8
+ Link as ReactRouterLink,
9
+ useInRouterContext,
10
+ useLocation as useReactRouterLocation,
11
+ useNavigate as useReactRouterNavigate,
12
+ useParams as useReactRouterParams,
13
+ } from '@modern-js/runtime/router';
14
+ import type React from 'react';
15
+ import { useCallback, useContext, useEffect, useState } from 'react';
16
+
17
+ export type I18nRouterFramework = 'react-router' | 'tanstack' | string;
18
+
19
+ export interface I18nRouterLocation {
20
+ pathname: string;
21
+ search: string;
22
+ hash: string;
23
+ }
24
+
25
+ export interface I18nRouterNavigateOptions {
26
+ replace?: boolean;
27
+ state?: unknown;
28
+ }
29
+
30
+ export type I18nRouterNavigate = (
31
+ href: string,
32
+ options?: I18nRouterNavigateOptions,
33
+ ) => void | Promise<void>;
34
+
35
+ export type I18nRouterLink = React.ComponentType<{
36
+ to: string;
37
+ children?: React.ReactNode;
38
+ [key: string]: unknown;
39
+ }>;
40
+
41
+ export interface I18nRouterAdapter {
42
+ framework?: I18nRouterFramework;
43
+ hasRouter: boolean;
44
+ location: I18nRouterLocation | null;
45
+ navigate: I18nRouterNavigate | null;
46
+ Link: I18nRouterLink | null;
47
+ params: Record<string, string>;
48
+ }
49
+
50
+ type RuntimeContextWithRouter = TRuntimeContext & {
51
+ router?: {
52
+ useRouter?: (options?: { warn?: boolean }) => unknown;
53
+ useLocation?: () => unknown;
54
+ useHref?: () => unknown;
55
+ Link?: I18nRouterLink;
56
+ };
57
+ };
58
+
59
+ type InternalRuntimeContextWithRouter = TInternalRuntimeContext & {
60
+ router?: RuntimeContextWithRouter['router'];
61
+ };
62
+
63
+ type RouterInstance = {
64
+ navigate?: (...args: any[]) => unknown;
65
+ state?: {
66
+ location?: unknown;
67
+ matches?: Array<{ params?: Record<string, string> }>;
68
+ };
69
+ stores?: {
70
+ location?: {
71
+ get?: () => unknown;
72
+ subscribe?: (listener: () => void) => () => void;
73
+ };
74
+ matches?: {
75
+ get?: () => Array<{ params?: Record<string, string> }>;
76
+ };
77
+ };
78
+ subscribe?: (eventType: string, listener: () => void) => () => void;
79
+ };
80
+
81
+ const normalizeUrlPart = (value: unknown, prefix: '?' | '#'): string => {
82
+ if (typeof value !== 'string' || !value) {
83
+ return '';
84
+ }
85
+ return value.startsWith(prefix) ? value : `${prefix}${value}`;
86
+ };
87
+
88
+ const normalizeLocation = (location: unknown): I18nRouterLocation | null => {
89
+ if (!location || typeof location !== 'object') {
90
+ return null;
91
+ }
92
+
93
+ const locationValue = location as {
94
+ pathname?: unknown;
95
+ search?: unknown;
96
+ searchStr?: unknown;
97
+ hash?: unknown;
98
+ };
99
+
100
+ if (typeof locationValue.pathname !== 'string') {
101
+ return null;
102
+ }
103
+
104
+ return {
105
+ pathname: locationValue.pathname,
106
+ search: normalizeUrlPart(
107
+ typeof locationValue.search === 'string'
108
+ ? locationValue.search
109
+ : locationValue.searchStr,
110
+ '?',
111
+ ),
112
+ hash: normalizeUrlPart(locationValue.hash, '#'),
113
+ };
114
+ };
115
+
116
+ const getWindowLocation = (): I18nRouterLocation | null => {
117
+ if (!isBrowser()) {
118
+ return null;
119
+ }
120
+
121
+ return {
122
+ pathname: window.location.pathname,
123
+ search: window.location.search,
124
+ hash: window.location.hash,
125
+ };
126
+ };
127
+
128
+ const getRouterFramework = (
129
+ runtimeContext: RuntimeContextWithRouter,
130
+ internalContext: InternalRuntimeContextWithRouter,
131
+ inReactRouter: boolean,
132
+ ): I18nRouterFramework | undefined => {
133
+ const framework =
134
+ internalContext.routerFramework ||
135
+ internalContext.routerRuntime?.framework ||
136
+ runtimeContext.routerFramework;
137
+
138
+ if (framework) {
139
+ return framework;
140
+ }
141
+
142
+ if (internalContext.router?.useRouter || runtimeContext.router?.useRouter) {
143
+ return 'tanstack';
144
+ }
145
+
146
+ if (
147
+ internalContext.router?.useLocation ||
148
+ internalContext.router?.useHref ||
149
+ runtimeContext.router?.useLocation ||
150
+ runtimeContext.router?.useHref
151
+ ) {
152
+ return 'react-router';
153
+ }
154
+
155
+ if (inReactRouter) {
156
+ return 'react-router';
157
+ }
158
+
159
+ return undefined;
160
+ };
161
+
162
+ const getRouterInstance = (
163
+ internalContext: InternalRuntimeContextWithRouter,
164
+ contextRouter?: RouterInstance | null,
165
+ ): RouterInstance | null => {
166
+ if (contextRouter) {
167
+ return contextRouter;
168
+ }
169
+
170
+ const router =
171
+ internalContext.routerInstance || internalContext.routerRuntime?.instance;
172
+ if (!router || typeof router !== 'object') {
173
+ return null;
174
+ }
175
+ return router as RouterInstance;
176
+ };
177
+
178
+ const getRouterStateLocation = (
179
+ internalContext: InternalRuntimeContextWithRouter,
180
+ contextRouter?: RouterInstance | null,
181
+ ): I18nRouterLocation | null => {
182
+ const router = getRouterInstance(internalContext, contextRouter);
183
+ return (
184
+ normalizeLocation(router?.stores?.location?.get?.()) ||
185
+ normalizeLocation(router?.state?.location)
186
+ );
187
+ };
188
+
189
+ const getRouterParams = (
190
+ internalContext: InternalRuntimeContextWithRouter,
191
+ contextRouter?: RouterInstance | null,
192
+ ): Record<string, string> => {
193
+ const router = getRouterInstance(internalContext, contextRouter);
194
+ const matches = router?.stores?.matches?.get?.() || router?.state?.matches;
195
+ if (!Array.isArray(matches)) {
196
+ return {};
197
+ }
198
+
199
+ return matches.reduce<Record<string, string>>((params, match) => {
200
+ if (match?.params) {
201
+ Object.assign(params, match.params);
202
+ }
203
+ return params;
204
+ }, {});
205
+ };
206
+
207
+ export const useI18nRouterAdapter = (): I18nRouterAdapter => {
208
+ const runtimeContext = useContext(RuntimeContext) as RuntimeContextWithRouter;
209
+ const internalContext = useContext(
210
+ InternalRuntimeContext,
211
+ ) as InternalRuntimeContextWithRouter;
212
+ const inReactRouter = useInRouterContext();
213
+ const reactRouterNavigate = inReactRouter ? useReactRouterNavigate() : null;
214
+ const reactRouterLocation = inReactRouter ? useReactRouterLocation() : null;
215
+ const reactRouterParams = inReactRouter ? useReactRouterParams() : {};
216
+ const framework = getRouterFramework(
217
+ runtimeContext,
218
+ internalContext,
219
+ inReactRouter,
220
+ );
221
+ const contextUseRouter =
222
+ !inReactRouter && framework === 'tanstack'
223
+ ? internalContext.router?.useRouter || runtimeContext.router?.useRouter
224
+ : undefined;
225
+ const contextRouter = contextUseRouter
226
+ ? (contextUseRouter({ warn: false }) as RouterInstance | null)
227
+ : null;
228
+ const [, setRouterVersion] = useState(0);
229
+ const hasRouter =
230
+ framework === 'tanstack' ||
231
+ framework === 'react-router' ||
232
+ Boolean(reactRouterNavigate);
233
+
234
+ useEffect(() => {
235
+ if (framework !== 'tanstack') {
236
+ return;
237
+ }
238
+
239
+ const router = getRouterInstance(internalContext, contextRouter);
240
+ if (!router) {
241
+ return;
242
+ }
243
+
244
+ const update = () => setRouterVersion(version => version + 1);
245
+ const unsubscribers: Array<() => void> = [];
246
+
247
+ if (typeof router.stores?.location?.subscribe === 'function') {
248
+ const unsubscribe = router.stores.location.subscribe(update);
249
+ if (typeof unsubscribe === 'function') {
250
+ unsubscribers.push(unsubscribe);
251
+ }
252
+ }
253
+
254
+ if (typeof router.subscribe === 'function') {
255
+ for (const eventType of ['onBeforeNavigate', 'onBeforeLoad']) {
256
+ const unsubscribe = router.subscribe(eventType, update);
257
+ if (typeof unsubscribe === 'function') {
258
+ unsubscribers.push(unsubscribe);
259
+ }
260
+ }
261
+ }
262
+
263
+ return () => {
264
+ for (const unsubscribe of unsubscribers) {
265
+ unsubscribe();
266
+ }
267
+ };
268
+ }, [contextRouter, framework, internalContext]);
269
+
270
+ const navigate = useCallback<I18nRouterNavigate>(
271
+ (href, options) => {
272
+ const router = getRouterInstance(internalContext, contextRouter);
273
+ const activeFramework = getRouterFramework(
274
+ runtimeContext,
275
+ internalContext,
276
+ inReactRouter,
277
+ );
278
+
279
+ if (activeFramework === 'tanstack') {
280
+ if (typeof router?.navigate === 'function') {
281
+ return router.navigate({
282
+ to: href,
283
+ replace: options?.replace,
284
+ ...(options?.state === undefined ? {} : { state: options.state }),
285
+ }) as void | Promise<void>;
286
+ }
287
+ throw new Error('TanStack router instance is not available.');
288
+ }
289
+
290
+ if (reactRouterNavigate) {
291
+ return reactRouterNavigate(href, options);
292
+ }
293
+
294
+ if (activeFramework === 'react-router') {
295
+ if (typeof router?.navigate === 'function') {
296
+ return router.navigate(href, options) as void | Promise<void>;
297
+ }
298
+ throw new Error('React Router instance is not available.');
299
+ }
300
+ },
301
+ [
302
+ contextRouter,
303
+ internalContext,
304
+ inReactRouter,
305
+ reactRouterNavigate,
306
+ runtimeContext,
307
+ ],
308
+ );
309
+
310
+ const location =
311
+ (reactRouterLocation
312
+ ? normalizeLocation(reactRouterLocation)
313
+ : getRouterStateLocation(internalContext, contextRouter)) ||
314
+ getWindowLocation();
315
+ const params = inReactRouter
316
+ ? (reactRouterParams as Record<string, string>)
317
+ : getRouterParams(internalContext, contextRouter);
318
+ const Link =
319
+ framework === 'tanstack'
320
+ ? internalContext.router?.Link || runtimeContext.router?.Link || null
321
+ : framework === 'react-router' || inReactRouter
322
+ ? (ReactRouterLink as I18nRouterLink)
323
+ : null;
324
+
325
+ return {
326
+ framework,
327
+ hasRouter,
328
+ location,
329
+ navigate: hasRouter ? navigate : null,
330
+ Link,
331
+ params,
332
+ };
333
+ };