@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/cli/index.js +10 -21
- package/dist/cjs/cli/localisedRoutes.js +47 -0
- package/dist/cjs/runtime/Link.js +19 -32
- package/dist/cjs/runtime/context.js +30 -73
- package/dist/cjs/runtime/contextHelpers.js +5 -5
- package/dist/cjs/runtime/contexts.js +49 -0
- package/dist/cjs/runtime/core.js +27 -14
- package/dist/cjs/runtime/hooks.js +7 -8
- package/dist/cjs/runtime/localizedPaths.js +11 -9
- package/dist/cjs/runtime/navigation.js +48 -0
- package/dist/cjs/runtime/providerComposition.js +37 -13
- package/dist/cjs/runtime/routerAdapter.js +33 -140
- package/dist/cjs/runtime/sharedContexts.js +43 -0
- package/dist/cjs/runtime/utils.js +21 -19
- package/dist/cjs/server/defaultUrlStrategy.js +48 -0
- package/dist/cjs/server/index.js +11 -7
- package/dist/cjs/server/mappedUrlStrategy.js +43 -0
- package/dist/cjs/server/redirectPolicy.js +33 -24
- package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
- package/dist/cjs/shared/urlStrategy.js +48 -0
- package/dist/esm/cli/index.mjs +10 -21
- package/dist/esm/cli/localisedRoutes.mjs +9 -0
- package/dist/esm/runtime/Link.mjs +19 -32
- package/dist/esm/runtime/context.mjs +20 -72
- package/dist/esm/runtime/contextHelpers.mjs +5 -5
- package/dist/esm/runtime/contexts.mjs +5 -0
- package/dist/esm/runtime/core.mjs +13 -6
- package/dist/esm/runtime/hooks.mjs +7 -8
- package/dist/esm/runtime/localizedPaths.mjs +11 -9
- package/dist/esm/runtime/navigation.mjs +7 -0
- package/dist/esm/runtime/providerComposition.mjs +38 -14
- package/dist/esm/runtime/routerAdapter.mjs +28 -141
- package/dist/esm/runtime/sharedContexts.mjs +1 -0
- package/dist/esm/runtime/utils.mjs +19 -17
- package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
- package/dist/esm/server/index.mjs +11 -7
- package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
- package/dist/esm/server/redirectPolicy.mjs +33 -24
- package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
- package/dist/esm/shared/urlStrategy.mjs +7 -0
- package/dist/esm-node/cli/index.mjs +10 -21
- package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
- package/dist/esm-node/runtime/Link.mjs +19 -32
- package/dist/esm-node/runtime/context.mjs +20 -72
- package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
- package/dist/esm-node/runtime/contexts.mjs +6 -0
- package/dist/esm-node/runtime/core.mjs +13 -6
- package/dist/esm-node/runtime/hooks.mjs +7 -8
- package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
- package/dist/esm-node/runtime/navigation.mjs +8 -0
- package/dist/esm-node/runtime/providerComposition.mjs +38 -14
- package/dist/esm-node/runtime/routerAdapter.mjs +28 -141
- package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
- package/dist/esm-node/runtime/utils.mjs +19 -17
- package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
- package/dist/esm-node/server/index.mjs +11 -7
- package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
- package/dist/esm-node/server/redirectPolicy.mjs +33 -24
- package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
- package/dist/esm-node/shared/urlStrategy.mjs +8 -0
- package/dist/types/cli/localisedRoutes.d.ts +19 -0
- package/dist/types/runtime/context.d.ts +8 -11
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/contexts.d.ts +6 -0
- package/dist/types/runtime/core.d.ts +12 -2
- package/dist/types/runtime/hooks.d.ts +4 -5
- package/dist/types/runtime/localizedPaths.d.ts +4 -4
- package/dist/types/runtime/navigation.d.ts +10 -0
- package/dist/types/runtime/providerComposition.d.ts +13 -1
- package/dist/types/runtime/routerAdapter.d.ts +23 -6
- package/dist/types/runtime/sharedContexts.d.ts +1 -0
- package/dist/types/runtime/utils.d.ts +4 -12
- package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
- package/dist/types/server/redirectPolicy.d.ts +5 -13
- package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
- package/dist/types/shared/type.d.ts +7 -11
- package/dist/types/shared/urlStrategy.d.ts +18 -0
- package/package.json +42 -19
- package/rslib.config.mts +8 -1
- package/rstest.config.mts +6 -5
- package/src/cli/index.ts +42 -42
- package/src/cli/localisedRoutes.ts +53 -0
- package/src/runtime/Link.tsx +20 -40
- package/src/runtime/context.tsx +33 -157
- package/src/runtime/contextHelpers.ts +7 -5
- package/src/runtime/contexts.ts +16 -0
- package/src/runtime/core.tsx +53 -6
- package/src/runtime/hooks.ts +14 -9
- package/src/runtime/i18n/instance.ts +1 -2
- package/src/runtime/localizedPaths.ts +24 -19
- package/src/runtime/navigation.tsx +20 -0
- package/src/runtime/providerComposition.tsx +71 -23
- package/src/runtime/routerAdapter.tsx +65 -339
- package/src/runtime/sharedContexts.ts +8 -0
- package/src/runtime/utils.ts +49 -31
- package/src/server/defaultUrlStrategy.ts +27 -0
- package/src/server/index.ts +39 -9
- package/src/server/mappedUrlStrategy.ts +10 -0
- package/src/server/redirectPolicy.ts +85 -70
- package/src/shared/mappedUrlStrategy.ts +59 -0
- package/src/shared/type.ts +7 -11
- package/src/shared/urlStrategy.ts +38 -0
- package/tests/backendDefaults.test.ts +2 -23
- package/tests/defaultUrlStrategy.test.ts +107 -0
- package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
- package/tests/i18nUtils.test.ts +31 -65
- package/tests/link.test.tsx +13 -511
- package/tests/localisedRouteGeneration.test.ts +200 -0
- package/tests/localisedUrls.test.ts +16 -208
- package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
- package/tests/navigation.test.tsx +403 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +8 -83
- package/tests/redirectPolicy.test.ts +89 -27
- package/tests/routerAdapter.test.tsx +98 -1171
- package/tests/urlStrategySerialization.test.ts +122 -0
- package/tsconfig.json +4 -2
- package/tests/federatedI18nBoundary.client.test.tsx +0 -129
- package/tests/federatedI18nBoundary.test.tsx +0 -132
- package/tests/linkTypes.test.ts +0 -59
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
- package/tests/peerCohort.test.ts +0 -25
- package/tests/type-fixture/fixture-globals.d.ts +0 -11
- package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
- package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
- package/tests/type-fixture/tsconfig.json +0 -16
|
@@ -1,14 +1,8 @@
|
|
|
1
|
+
import { I18nNavigationContext } from '@modern-js/plugin-i18n/runtime/contexts';
|
|
1
2
|
import { isBrowser, RuntimeContext } from '@modern-js/runtime';
|
|
2
|
-
import {
|
|
3
|
-
getRouterRuntimeState,
|
|
4
|
-
InternalRuntimeContext,
|
|
5
|
-
type TInternalRuntimeContext,
|
|
6
|
-
type TRuntimeContext,
|
|
7
|
-
} from '@modern-js/runtime/context';
|
|
3
|
+
import { InternalRuntimeContext } from '@modern-js/runtime/context';
|
|
8
4
|
import type React from 'react';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
type I18nRouterFramework = 'react-router' | 'tanstack' | string;
|
|
5
|
+
import { useContext } from 'react';
|
|
12
6
|
|
|
13
7
|
interface I18nRouterLocation {
|
|
14
8
|
pathname: string;
|
|
@@ -16,14 +10,9 @@ interface I18nRouterLocation {
|
|
|
16
10
|
hash: string;
|
|
17
11
|
}
|
|
18
12
|
|
|
19
|
-
interface I18nRouterNavigateOptions {
|
|
20
|
-
replace?: boolean;
|
|
21
|
-
state?: unknown;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
13
|
type I18nRouterNavigate = (
|
|
25
14
|
href: string,
|
|
26
|
-
options?:
|
|
15
|
+
options?: { replace?: boolean; state?: unknown },
|
|
27
16
|
) => void | Promise<void>;
|
|
28
17
|
|
|
29
18
|
type I18nRouterLink = React.ComponentType<{
|
|
@@ -32,345 +21,82 @@ type I18nRouterLink = React.ComponentType<{
|
|
|
32
21
|
[key: string]: unknown;
|
|
33
22
|
}>;
|
|
34
23
|
|
|
35
|
-
interface
|
|
36
|
-
|
|
24
|
+
export interface I18nRouterLinkTarget {
|
|
25
|
+
pathname: string;
|
|
26
|
+
href: string;
|
|
27
|
+
search?: Record<string, unknown>;
|
|
28
|
+
hash?: string;
|
|
29
|
+
hashScrollIntoView?: boolean | ScrollIntoViewOptions;
|
|
30
|
+
prefetch?: 'intent' | 'render' | 'viewport' | 'none';
|
|
31
|
+
preload?: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface I18nRouterAdapter {
|
|
35
|
+
framework?: string;
|
|
37
36
|
hasRouter: boolean;
|
|
38
37
|
location: I18nRouterLocation | null;
|
|
39
38
|
navigate: I18nRouterNavigate | null;
|
|
40
39
|
Link: I18nRouterLink | null;
|
|
41
40
|
params: Record<string, string>;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
useInRouterContext?: () => boolean;
|
|
47
|
-
useRouter?: (options?: { warn?: boolean }) => unknown;
|
|
48
|
-
useLocation?: () => unknown;
|
|
49
|
-
useNavigate?: () => I18nRouterNavigate;
|
|
50
|
-
useParams?: () => Record<string, string>;
|
|
51
|
-
useHref?: () => unknown;
|
|
52
|
-
Link?: I18nRouterLink;
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
type InternalRuntimeContextWithRouter = TInternalRuntimeContext & {
|
|
57
|
-
router?: RuntimeContextWithRouter['router'];
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type RouterInstance = {
|
|
61
|
-
navigate?: (...args: any[]) => unknown;
|
|
62
|
-
state?: {
|
|
63
|
-
location?: unknown;
|
|
64
|
-
matches?: Array<{ params?: Record<string, string> }>;
|
|
65
|
-
};
|
|
66
|
-
stores?: {
|
|
67
|
-
location?: {
|
|
68
|
-
get?: () => unknown;
|
|
69
|
-
subscribe?: (listener: () => void) => () => void;
|
|
70
|
-
};
|
|
71
|
-
matches?: {
|
|
72
|
-
get?: () => Array<{ params?: Record<string, string> }>;
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
subscribe?:
|
|
76
|
-
| ((listener: () => void) => () => void)
|
|
77
|
-
| ((eventType: string, listener: () => void) => () => void);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const normalizeUrlPart = (value: unknown, prefix: '?' | '#'): string => {
|
|
81
|
-
if (typeof value !== 'string' || !value) {
|
|
82
|
-
return '';
|
|
83
|
-
}
|
|
84
|
-
return value.startsWith(prefix) ? value : `${prefix}${value}`;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const normalizeLocation = (location: unknown): I18nRouterLocation | null => {
|
|
88
|
-
if (!location || typeof location !== 'object') {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const locationValue = location as {
|
|
93
|
-
pathname?: unknown;
|
|
94
|
-
search?: unknown;
|
|
95
|
-
searchStr?: unknown;
|
|
96
|
-
hash?: unknown;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
if (typeof locationValue.pathname !== 'string') {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
pathname: locationValue.pathname,
|
|
105
|
-
search: normalizeUrlPart(
|
|
106
|
-
typeof locationValue.search === 'string'
|
|
107
|
-
? locationValue.search
|
|
108
|
-
: locationValue.searchStr,
|
|
109
|
-
'?',
|
|
110
|
-
),
|
|
111
|
-
hash: normalizeUrlPart(locationValue.hash, '#'),
|
|
112
|
-
};
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const getWindowLocation = (): I18nRouterLocation | null => {
|
|
116
|
-
if (!isBrowser()) {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
pathname: window.location.pathname,
|
|
122
|
-
search: window.location.search,
|
|
123
|
-
hash: window.location.hash,
|
|
41
|
+
/** Convert an already resolved target to the selected router's own props. */
|
|
42
|
+
createLinkProps?: (target: I18nRouterLinkTarget) => {
|
|
43
|
+
to: string;
|
|
44
|
+
[key: string]: unknown;
|
|
124
45
|
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const getRouterFramework = (
|
|
128
|
-
runtimeContext: RuntimeContextWithRouter,
|
|
129
|
-
internalContext: InternalRuntimeContextWithRouter,
|
|
130
|
-
inReactRouter: boolean,
|
|
131
|
-
): I18nRouterFramework | undefined => {
|
|
132
|
-
const framework =
|
|
133
|
-
getRouterRuntimeState(internalContext)?.framework ||
|
|
134
|
-
getRouterRuntimeState(runtimeContext)?.framework;
|
|
135
|
-
|
|
136
|
-
if (framework) {
|
|
137
|
-
return framework;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (internalContext.router?.useRouter || runtimeContext.router?.useRouter) {
|
|
141
|
-
return 'tanstack';
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
internalContext.router?.useLocation ||
|
|
146
|
-
internalContext.router?.useHref ||
|
|
147
|
-
runtimeContext.router?.useLocation ||
|
|
148
|
-
runtimeContext.router?.useHref
|
|
149
|
-
) {
|
|
150
|
-
return 'react-router';
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (inReactRouter) {
|
|
154
|
-
return 'react-router';
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return undefined;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const getRouterInstance = (
|
|
161
|
-
internalContext: InternalRuntimeContextWithRouter,
|
|
162
|
-
contextRouter?: RouterInstance | null,
|
|
163
|
-
): RouterInstance | null => {
|
|
164
|
-
if (contextRouter) {
|
|
165
|
-
return contextRouter;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const router = getRouterRuntimeState(internalContext)?.instance;
|
|
169
|
-
if (!router || typeof router !== 'object') {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
return router as RouterInstance;
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const getRouterStateLocation = (
|
|
176
|
-
internalContext: InternalRuntimeContextWithRouter,
|
|
177
|
-
contextRouter?: RouterInstance | null,
|
|
178
|
-
): I18nRouterLocation | null => {
|
|
179
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
180
|
-
return (
|
|
181
|
-
normalizeLocation(router?.stores?.location?.get?.()) ||
|
|
182
|
-
normalizeLocation(router?.state?.location)
|
|
183
|
-
);
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const getRouterParams = (
|
|
187
|
-
internalContext: InternalRuntimeContextWithRouter,
|
|
188
|
-
contextRouter?: RouterInstance | null,
|
|
189
|
-
): Record<string, string> => {
|
|
190
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
191
|
-
const matches = router?.stores?.matches?.get?.() || router?.state?.matches;
|
|
192
|
-
if (!Array.isArray(matches)) {
|
|
193
|
-
return {};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return matches.reduce<Record<string, string>>((params, match) => {
|
|
197
|
-
if (match?.params) {
|
|
198
|
-
Object.assign(params, match.params);
|
|
199
|
-
}
|
|
200
|
-
return params;
|
|
201
|
-
}, {});
|
|
202
|
-
};
|
|
46
|
+
}
|
|
203
47
|
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
) =>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
48
|
+
export const I18nNavigationProvider = ({
|
|
49
|
+
children,
|
|
50
|
+
value,
|
|
51
|
+
}: React.PropsWithChildren<{ value: I18nRouterAdapter }>) => (
|
|
52
|
+
<I18nNavigationContext.Provider value={value}>
|
|
53
|
+
{children}
|
|
54
|
+
</I18nNavigationContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
interface NativeRouterApi {
|
|
58
|
+
useInRouterContext?: () => boolean;
|
|
59
|
+
useNavigate?: () => I18nRouterNavigate;
|
|
60
|
+
useLocation?: () => I18nRouterLocation;
|
|
61
|
+
useParams?: () => Record<string, string>;
|
|
62
|
+
Link?: I18nRouterLink;
|
|
63
|
+
}
|
|
217
64
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
) as
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const reactRouterLocation = inReactRouter
|
|
65
|
+
/** Read native router hooks only when their own provider is present. */
|
|
66
|
+
export const useNativeI18nRouterAdapter = (): I18nRouterAdapter => {
|
|
67
|
+
const runtimeContext = useContext(RuntimeContext);
|
|
68
|
+
const internalContext = useContext(InternalRuntimeContext);
|
|
69
|
+
const routerApi = (internalContext.router || runtimeContext.router) as
|
|
70
|
+
| NativeRouterApi
|
|
71
|
+
| undefined;
|
|
72
|
+
const hasRouter = routerApi?.useInRouterContext?.() ?? false;
|
|
73
|
+
const navigate = hasRouter ? (routerApi?.useNavigate?.() ?? null) : null;
|
|
74
|
+
const routerLocation = hasRouter
|
|
229
75
|
? (routerApi?.useLocation?.() ?? null)
|
|
230
76
|
: null;
|
|
231
|
-
const
|
|
232
|
-
? (routerApi?.useParams?.() ?? {})
|
|
233
|
-
: {};
|
|
234
|
-
const framework = getRouterFramework(
|
|
235
|
-
runtimeContext,
|
|
236
|
-
internalContext,
|
|
237
|
-
inReactRouter,
|
|
238
|
-
);
|
|
239
|
-
const contextUseRouter =
|
|
240
|
-
!inReactRouter && framework === 'tanstack'
|
|
241
|
-
? internalContext.router?.useRouter || runtimeContext.router?.useRouter
|
|
242
|
-
: undefined;
|
|
243
|
-
const contextRouter = contextUseRouter
|
|
244
|
-
? (contextUseRouter({ warn: false }) as RouterInstance | null)
|
|
245
|
-
: null;
|
|
246
|
-
const hasRouter =
|
|
247
|
-
framework === 'tanstack' ||
|
|
248
|
-
framework === 'react-router' ||
|
|
249
|
-
Boolean(reactRouterNavigate);
|
|
250
|
-
|
|
251
|
-
const subscribeToRouter = useCallback(
|
|
252
|
-
(update: () => void) => {
|
|
253
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
254
|
-
if (!router) {
|
|
255
|
-
return () => undefined;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const unsubscribers: Array<() => void> = [];
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
framework === 'react-router' &&
|
|
262
|
-
!inReactRouter &&
|
|
263
|
-
typeof router.subscribe === 'function'
|
|
264
|
-
) {
|
|
265
|
-
const subscribe = router.subscribe as (
|
|
266
|
-
this: RouterInstance,
|
|
267
|
-
listener: () => void,
|
|
268
|
-
) => () => void;
|
|
269
|
-
const unsubscribe = subscribe.call(router, update);
|
|
270
|
-
if (typeof unsubscribe === 'function') {
|
|
271
|
-
unsubscribers.push(unsubscribe);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
framework === 'tanstack' &&
|
|
277
|
-
typeof router.stores?.location?.subscribe === 'function'
|
|
278
|
-
) {
|
|
279
|
-
const unsubscribe = router.stores.location.subscribe(update);
|
|
280
|
-
if (typeof unsubscribe === 'function') {
|
|
281
|
-
unsubscribers.push(unsubscribe);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (framework === 'tanstack' && typeof router.subscribe === 'function') {
|
|
286
|
-
const subscribe = router.subscribe as (
|
|
287
|
-
this: RouterInstance,
|
|
288
|
-
eventType: string,
|
|
289
|
-
listener: () => void,
|
|
290
|
-
) => () => void;
|
|
291
|
-
for (const eventType of ['onBeforeNavigate', 'onBeforeLoad']) {
|
|
292
|
-
const unsubscribe = subscribe.call(router, eventType, update);
|
|
293
|
-
if (typeof unsubscribe === 'function') {
|
|
294
|
-
unsubscribers.push(unsubscribe);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return () => {
|
|
300
|
-
for (const unsubscribe of unsubscribers) {
|
|
301
|
-
unsubscribe();
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
},
|
|
305
|
-
[contextRouter, framework, inReactRouter, internalContext],
|
|
306
|
-
);
|
|
307
|
-
const getSnapshot = useCallback(
|
|
308
|
-
() => getRouterSnapshot(internalContext, contextRouter),
|
|
309
|
-
[contextRouter, internalContext],
|
|
310
|
-
);
|
|
311
|
-
useSyncExternalStore(subscribeToRouter, getSnapshot, getSnapshot);
|
|
312
|
-
|
|
313
|
-
const navigate = useCallback<I18nRouterNavigate>(
|
|
314
|
-
(href, options) => {
|
|
315
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
316
|
-
const activeFramework = getRouterFramework(
|
|
317
|
-
runtimeContext,
|
|
318
|
-
internalContext,
|
|
319
|
-
inReactRouter,
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
if (activeFramework === 'tanstack') {
|
|
323
|
-
if (typeof router?.navigate === 'function') {
|
|
324
|
-
return router.navigate({
|
|
325
|
-
to: href,
|
|
326
|
-
replace: options?.replace,
|
|
327
|
-
...(options?.state === undefined ? {} : { state: options.state }),
|
|
328
|
-
}) as void | Promise<void>;
|
|
329
|
-
}
|
|
330
|
-
throw new Error('TanStack router instance is not available.');
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (reactRouterNavigate) {
|
|
334
|
-
return reactRouterNavigate(href, options);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (activeFramework === 'react-router') {
|
|
338
|
-
if (typeof router?.navigate === 'function') {
|
|
339
|
-
return router.navigate(href, options) as void | Promise<void>;
|
|
340
|
-
}
|
|
341
|
-
throw new Error('React Router instance is not available.');
|
|
342
|
-
}
|
|
343
|
-
},
|
|
344
|
-
[
|
|
345
|
-
contextRouter,
|
|
346
|
-
internalContext,
|
|
347
|
-
inReactRouter,
|
|
348
|
-
reactRouterNavigate,
|
|
349
|
-
runtimeContext,
|
|
350
|
-
],
|
|
351
|
-
);
|
|
352
|
-
|
|
77
|
+
const params = hasRouter ? (routerApi?.useParams?.() ?? {}) : {};
|
|
353
78
|
const location =
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
framework === 'tanstack'
|
|
363
|
-
? internalContext.router?.Link || runtimeContext.router?.Link || null
|
|
364
|
-
: framework === 'react-router' || inReactRouter
|
|
365
|
-
? (routerApi?.Link ?? null)
|
|
366
|
-
: null;
|
|
79
|
+
routerLocation ??
|
|
80
|
+
(isBrowser()
|
|
81
|
+
? {
|
|
82
|
+
pathname: window.location.pathname,
|
|
83
|
+
search: window.location.search,
|
|
84
|
+
hash: window.location.hash,
|
|
85
|
+
}
|
|
86
|
+
: null);
|
|
367
87
|
|
|
368
88
|
return {
|
|
369
|
-
framework,
|
|
89
|
+
framework: hasRouter ? 'react-router' : undefined,
|
|
370
90
|
hasRouter,
|
|
91
|
+
navigate,
|
|
371
92
|
location,
|
|
372
|
-
navigate: hasRouter ? navigate : null,
|
|
373
|
-
Link,
|
|
374
93
|
params,
|
|
94
|
+
Link: hasRouter ? (routerApi?.Link ?? null) : null,
|
|
375
95
|
};
|
|
376
96
|
};
|
|
97
|
+
|
|
98
|
+
export const useI18nRouterAdapter = (): I18nRouterAdapter => {
|
|
99
|
+
const provided = useContext(I18nNavigationContext);
|
|
100
|
+
const native = useNativeI18nRouterAdapter();
|
|
101
|
+
return provided ?? native;
|
|
102
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Upstream-owned runtime modules take the i18n contexts from this fork-owned
|
|
2
|
+
// module, which reaches them through the shared
|
|
3
|
+
// `@modern-js/plugin-i18n/runtime/contexts` request. That keeps the fork
|
|
4
|
+
// import edge out of the upstream-owned files.
|
|
5
|
+
export {
|
|
6
|
+
ModernI18nContext,
|
|
7
|
+
ReactI18nextProviderContext,
|
|
8
|
+
} from '@modern-js/plugin-i18n/runtime/contexts';
|
package/src/runtime/utils.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type LocalisedUrlsOption,
|
|
3
|
-
localiseTargetPathname,
|
|
4
|
-
shouldSkipLocaleRedirect,
|
|
5
|
-
} from '@modern-js/i18n-runtime-extensions';
|
|
6
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
7
2
|
import { getGlobalBasename } from '@modern-js/runtime/context';
|
|
3
|
+
import { splitUrlTarget } from '@modern-js/runtime-utils/url';
|
|
4
|
+
import { isDefaultLocaleRedirectSkipPath } from '../shared/mappedUrlStrategy';
|
|
5
|
+
import { asI18nUrlStrategy, type I18nUrlStrategy } from '../shared/urlStrategy';
|
|
6
|
+
|
|
7
|
+
export { splitUrlTarget } from '@modern-js/runtime-utils/url';
|
|
8
8
|
|
|
9
9
|
// Structural parameter: hooks.ts passes a public-TRuntimeContext-based
|
|
10
10
|
// context while core.tsx passes the internal one; both carry the request
|
|
@@ -47,24 +47,6 @@ export const getLanguageFromPath = (
|
|
|
47
47
|
return fallbackLanguage;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
* Split a link target into its pathname, search and hash parts without
|
|
52
|
-
* relying on `new URL` (SSR-hot path; targets are relative).
|
|
53
|
-
*/
|
|
54
|
-
export const splitUrlTarget = (
|
|
55
|
-
target: string,
|
|
56
|
-
): { pathname: string; search: string; hash: string } => {
|
|
57
|
-
const hashIndex = target.indexOf('#');
|
|
58
|
-
const hash = hashIndex >= 0 ? target.slice(hashIndex) : '';
|
|
59
|
-
const beforeHash = hashIndex >= 0 ? target.slice(0, hashIndex) : target;
|
|
60
|
-
const searchIndex = beforeHash.indexOf('?');
|
|
61
|
-
const search = searchIndex >= 0 ? beforeHash.slice(searchIndex) : '';
|
|
62
|
-
const pathname =
|
|
63
|
-
searchIndex >= 0 ? beforeHash.slice(0, searchIndex) : beforeHash;
|
|
64
|
-
|
|
65
|
-
return { pathname, search, hash };
|
|
66
|
-
};
|
|
67
|
-
|
|
68
50
|
/**
|
|
69
51
|
* Helper function to build localized URL
|
|
70
52
|
* @param target - The language-agnostic target; may include `?search` and `#hash`
|
|
@@ -76,15 +58,23 @@ export const buildLocalizedUrl = (
|
|
|
76
58
|
target: string,
|
|
77
59
|
language: string,
|
|
78
60
|
languages: string[],
|
|
79
|
-
|
|
61
|
+
urlStrategy?: I18nUrlStrategy,
|
|
80
62
|
): string => {
|
|
81
63
|
const { pathname, search, hash } = splitUrlTarget(target);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
language,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
64
|
+
const strategy = asI18nUrlStrategy(urlStrategy);
|
|
65
|
+
if (strategy) {
|
|
66
|
+
return `${strategy.localizePathname(pathname, language, languages)}${search}${hash}`;
|
|
67
|
+
}
|
|
68
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
69
|
+
if (
|
|
70
|
+
segments[0] &&
|
|
71
|
+
languages.some(item => item.toLowerCase() === segments[0].toLowerCase())
|
|
72
|
+
) {
|
|
73
|
+
segments[0] = language;
|
|
74
|
+
} else {
|
|
75
|
+
segments.unshift(language);
|
|
76
|
+
}
|
|
77
|
+
const localizedPathname = `/${segments.join('/')}`;
|
|
88
78
|
|
|
89
79
|
return `${localizedPathname}${search}${hash}`;
|
|
90
80
|
};
|
|
@@ -131,6 +121,34 @@ export const shouldIgnoreRedirect = (
|
|
|
131
121
|
pathname: string,
|
|
132
122
|
languages: string[],
|
|
133
123
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
124
|
+
urlStrategy?: I18nUrlStrategy,
|
|
134
125
|
): boolean => {
|
|
135
|
-
|
|
126
|
+
// Same native exclusions as the server: federation artifacts are never
|
|
127
|
+
// locale-redirected, with or without a configured strategy.
|
|
128
|
+
if (isDefaultLocaleRedirectSkipPath(pathname, languages)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
asI18nUrlStrategy(urlStrategy)?.shouldSkipRedirect?.(pathname, languages)
|
|
133
|
+
) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
if (!ignoreRedirectRoutes) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
140
|
+
if (
|
|
141
|
+
segments[0] &&
|
|
142
|
+
languages.some(item => item.toLowerCase() === segments[0].toLowerCase())
|
|
143
|
+
) {
|
|
144
|
+
segments.shift();
|
|
145
|
+
}
|
|
146
|
+
const normalizedPath = `/${segments.join('/')}`;
|
|
147
|
+
return typeof ignoreRedirectRoutes === 'function'
|
|
148
|
+
? ignoreRedirectRoutes(normalizedPath)
|
|
149
|
+
: ignoreRedirectRoutes.some(
|
|
150
|
+
pattern =>
|
|
151
|
+
normalizedPath === pattern ||
|
|
152
|
+
normalizedPath.startsWith(`${pattern}/`),
|
|
153
|
+
);
|
|
136
154
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
|
|
2
|
+
import type { I18nUrlStrategy } from '../shared/urlStrategy';
|
|
3
|
+
import { createMappedUrlStrategy } from './mappedUrlStrategy';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mapped locale URLs work without any extra plugin. `localeDetection.localisedUrls`
|
|
7
|
+
* alone is enough: the server derives the URL policy from the map it is given.
|
|
8
|
+
* An explicit `resolveUrlStrategy` still wins, so an integration can replace the
|
|
9
|
+
* policy wholesale.
|
|
10
|
+
*/
|
|
11
|
+
export const createDefaultUrlStrategy = (
|
|
12
|
+
localisedUrls?: LocalisedUrlsOption,
|
|
13
|
+
): I18nUrlStrategy => createMappedUrlStrategy(localisedUrls);
|
|
14
|
+
|
|
15
|
+
/** A non-empty map is the only configuration that needs a derived policy. */
|
|
16
|
+
export const resolveDefaultUrlStrategy = (
|
|
17
|
+
localisedUrls: unknown,
|
|
18
|
+
): I18nUrlStrategy | undefined => {
|
|
19
|
+
if (
|
|
20
|
+
!localisedUrls ||
|
|
21
|
+
typeof localisedUrls !== 'object' ||
|
|
22
|
+
Object.keys(localisedUrls).length === 0
|
|
23
|
+
) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return createDefaultUrlStrategy(localisedUrls as LocalisedUrlsOption);
|
|
27
|
+
};
|
package/src/server/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { resolveLocalisedUrlsConfig } from '@modern-js/i18n-runtime-extensions';
|
|
2
1
|
import * as honoPkg from '@modern-js/server-core/hono';
|
|
3
2
|
|
|
4
3
|
const { languageDetector } = honoPkg;
|
|
5
4
|
|
|
6
5
|
import type { Context, Next, ServerPlugin } from '@modern-js/server-runtime';
|
|
7
6
|
import type { LocaleDetectionOptions } from '../shared/type';
|
|
7
|
+
import { asI18nUrlStrategy, type I18nUrlStrategy } from '../shared/urlStrategy';
|
|
8
8
|
import { getLocaleDetectionOptions } from '../shared/utils.js';
|
|
9
9
|
import { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
|
|
10
|
+
import { resolveDefaultUrlStrategy } from './defaultUrlStrategy.js';
|
|
10
11
|
import { convertToHonoLanguageDetectorOptions } from './detectorOptions.js';
|
|
11
12
|
import {
|
|
12
13
|
buildLocalizedUrl,
|
|
@@ -21,6 +22,7 @@ export { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
|
|
|
21
22
|
export interface I18nPluginOptions {
|
|
22
23
|
localeDetection: LocaleDetectionOptions;
|
|
23
24
|
staticRoutePrefixes: string[];
|
|
25
|
+
resolveUrlStrategy?: (entryName: string) => I18nUrlStrategy | undefined;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
@@ -53,6 +55,10 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
53
55
|
if (!options.localeDetection) {
|
|
54
56
|
return;
|
|
55
57
|
}
|
|
58
|
+
const entryLocaleDetection = getLocaleDetectionOptions(
|
|
59
|
+
entryName,
|
|
60
|
+
options.localeDetection,
|
|
61
|
+
);
|
|
56
62
|
const {
|
|
57
63
|
localePathRedirect,
|
|
58
64
|
i18nextDetector = true,
|
|
@@ -60,8 +66,16 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
60
66
|
fallbackLanguage = 'en',
|
|
61
67
|
detection,
|
|
62
68
|
ignoreRedirectRoutes,
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
} = entryLocaleDetection;
|
|
70
|
+
const legacyLocalisedUrls = Reflect.get(
|
|
71
|
+
entryLocaleDetection,
|
|
72
|
+
'localisedUrls',
|
|
73
|
+
);
|
|
74
|
+
// `localeDetection.localisedUrls` is self-sufficient: when no explicit
|
|
75
|
+
// strategy is supplied the plugin derives one from the map itself.
|
|
76
|
+
const urlStrategy =
|
|
77
|
+
asI18nUrlStrategy(options.resolveUrlStrategy?.(entryName)) ??
|
|
78
|
+
resolveDefaultUrlStrategy(legacyLocalisedUrls);
|
|
65
79
|
const staticRoutePrefixes = options.staticRoutePrefixes;
|
|
66
80
|
const originUrlPath = route.urlPath;
|
|
67
81
|
const urlPath = originUrlPath.endsWith('/')
|
|
@@ -87,6 +101,18 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
87
101
|
return await next();
|
|
88
102
|
}
|
|
89
103
|
|
|
104
|
+
if (
|
|
105
|
+
shouldIgnoreRedirect(
|
|
106
|
+
pathname,
|
|
107
|
+
urlPath,
|
|
108
|
+
ignoreRedirectRoutes,
|
|
109
|
+
urlStrategy,
|
|
110
|
+
languages,
|
|
111
|
+
)
|
|
112
|
+
) {
|
|
113
|
+
return await next();
|
|
114
|
+
}
|
|
115
|
+
|
|
90
116
|
// For static resource requests, skip language detection
|
|
91
117
|
if (
|
|
92
118
|
isStaticResourceRequest(
|
|
@@ -138,7 +164,13 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
138
164
|
|
|
139
165
|
// Check if this route should ignore automatic redirect
|
|
140
166
|
if (
|
|
141
|
-
shouldIgnoreRedirect(
|
|
167
|
+
shouldIgnoreRedirect(
|
|
168
|
+
pathname,
|
|
169
|
+
urlPath,
|
|
170
|
+
ignoreRedirectRoutes,
|
|
171
|
+
urlStrategy,
|
|
172
|
+
languages,
|
|
173
|
+
)
|
|
142
174
|
) {
|
|
143
175
|
return await next();
|
|
144
176
|
}
|
|
@@ -168,19 +200,17 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
168
200
|
originUrlPath,
|
|
169
201
|
targetLanguage,
|
|
170
202
|
languages,
|
|
171
|
-
|
|
203
|
+
urlStrategy,
|
|
172
204
|
);
|
|
173
205
|
return createLocaleRedirectResponse(localizedUrl);
|
|
174
206
|
}
|
|
175
|
-
|
|
176
|
-
resolveLocalisedUrlsConfig(localisedUrls);
|
|
177
|
-
if (localisedUrlsConfig.enabled) {
|
|
207
|
+
if (urlStrategy) {
|
|
178
208
|
const expectedUrl = buildLocalizedUrl(
|
|
179
209
|
c.req,
|
|
180
210
|
originUrlPath,
|
|
181
211
|
language,
|
|
182
212
|
languages,
|
|
183
|
-
|
|
213
|
+
urlStrategy,
|
|
184
214
|
);
|
|
185
215
|
if (expectedUrl !== `${pathname}${url.search}${url.hash}`) {
|
|
186
216
|
return createLocaleRedirectResponse(expectedUrl);
|