@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.
- package/dist/cjs/cli/index.js +22 -0
- package/dist/cjs/runtime/I18nLink.js +4 -12
- package/dist/cjs/runtime/context.js +32 -5
- package/dist/cjs/runtime/hooks.js +8 -5
- package/dist/cjs/runtime/i18n/backend/defaults.js +1 -1
- package/dist/cjs/runtime/i18n/backend/middleware.node.js +4 -4
- package/dist/cjs/runtime/i18n/instance.js +4 -2
- package/dist/cjs/runtime/index.js +7 -6
- package/dist/cjs/runtime/routerAdapter.js +163 -0
- package/dist/cjs/runtime/utils.js +63 -94
- package/dist/cjs/server/index.js +64 -8
- package/dist/cjs/shared/localisedUrls.js +237 -0
- package/dist/esm/cli/index.mjs +22 -0
- package/dist/esm/runtime/I18nLink.mjs +4 -12
- package/dist/esm/runtime/context.mjs +34 -7
- package/dist/esm/runtime/hooks.mjs +9 -6
- package/dist/esm/runtime/i18n/backend/defaults.mjs +1 -1
- package/dist/esm/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm/runtime/i18n/instance.mjs +4 -2
- package/dist/esm/runtime/index.mjs +7 -6
- package/dist/esm/runtime/routerAdapter.mjs +129 -0
- package/dist/esm/runtime/utils.mjs +11 -30
- package/dist/esm/server/index.mjs +57 -7
- package/dist/esm/shared/localisedUrls.mjs +191 -0
- package/dist/esm-node/cli/index.mjs +22 -0
- package/dist/esm-node/runtime/I18nLink.mjs +4 -12
- package/dist/esm-node/runtime/context.mjs +34 -7
- package/dist/esm-node/runtime/hooks.mjs +9 -6
- package/dist/esm-node/runtime/i18n/backend/defaults.mjs +1 -1
- package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm-node/runtime/i18n/instance.mjs +4 -2
- package/dist/esm-node/runtime/index.mjs +7 -6
- package/dist/esm-node/runtime/routerAdapter.mjs +130 -0
- package/dist/esm-node/runtime/utils.mjs +11 -30
- package/dist/esm-node/server/index.mjs +57 -7
- package/dist/esm-node/shared/localisedUrls.mjs +192 -0
- package/dist/types/cli/index.d.ts +21 -0
- package/dist/types/runtime/I18nLink.d.ts +23 -0
- package/dist/types/runtime/context.d.ts +41 -0
- package/dist/types/runtime/hooks.d.ts +30 -0
- package/dist/types/runtime/i18n/backend/config.d.ts +2 -0
- package/dist/types/runtime/i18n/backend/defaults.d.ts +13 -0
- package/dist/types/runtime/i18n/backend/defaults.node.d.ts +8 -0
- package/dist/types/runtime/i18n/backend/index.d.ts +3 -0
- package/dist/types/runtime/i18n/backend/middleware.common.d.ts +14 -0
- package/dist/types/runtime/i18n/backend/middleware.d.ts +12 -0
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +13 -0
- package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +53 -0
- package/dist/types/runtime/i18n/backend/sdk-event.d.ts +9 -0
- package/dist/types/runtime/i18n/detection/config.d.ts +11 -0
- package/dist/types/runtime/i18n/detection/index.d.ts +50 -0
- package/dist/types/runtime/i18n/detection/middleware.d.ts +24 -0
- package/dist/types/runtime/i18n/detection/middleware.node.d.ts +17 -0
- package/dist/types/runtime/i18n/index.d.ts +3 -0
- package/dist/types/runtime/i18n/instance.d.ts +96 -0
- package/dist/types/runtime/i18n/utils.d.ts +29 -0
- package/dist/types/runtime/index.d.ts +21 -0
- package/dist/types/runtime/routerAdapter.d.ts +26 -0
- package/dist/types/runtime/types.d.ts +15 -0
- package/dist/types/runtime/utils.d.ts +28 -0
- package/dist/types/server/index.d.ts +14 -0
- package/dist/types/shared/deepMerge.d.ts +1 -0
- package/dist/types/shared/detection.d.ts +11 -0
- package/dist/types/shared/localisedUrls.d.ts +13 -0
- package/dist/types/shared/type.d.ts +168 -0
- package/dist/types/shared/utils.d.ts +5 -0
- package/package.json +15 -15
- package/rstest.config.mts +39 -0
- package/src/cli/index.ts +43 -1
- package/src/runtime/I18nLink.tsx +10 -16
- package/src/runtime/context.tsx +45 -7
- package/src/runtime/hooks.ts +13 -4
- package/src/runtime/i18n/backend/defaults.ts +3 -1
- package/src/runtime/i18n/backend/middleware.node.ts +1 -1
- package/src/runtime/i18n/instance.ts +14 -5
- package/src/runtime/index.tsx +10 -2
- package/src/runtime/routerAdapter.tsx +333 -0
- package/src/runtime/utils.ts +22 -34
- package/src/server/index.ts +135 -10
- package/src/shared/localisedUrls.ts +393 -0
- package/src/shared/type.ts +12 -0
- package/tests/localisedUrls.test.ts +278 -0
- package/tests/routerAdapter.test.tsx +278 -0
- package/dist/esm/rslib-runtime.mjs +0 -18
- package/dist/esm-node/rslib-runtime.mjs +0 -19
package/src/runtime/context.tsx
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
2
2
|
import type { FC, ReactNode } from 'react';
|
|
3
|
-
import {
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
};
|
package/src/runtime/hooks.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
174
|
+
function getOptionalReactI18nextPackageName(): string {
|
|
175
|
+
return ['react', 'i18next'].join('-');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function tryImportReactI18next(): Promise<ReactI18nextModule | null> {
|
|
171
179
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
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;
|
package/src/runtime/index.tsx
CHANGED
|
@@ -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 =
|
|
94
|
-
|
|
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
|
+
};
|