@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.4 → 3.9.0-ultramodern.6
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 +0 -22
- package/dist/cjs/runtime/Link.js +19 -32
- package/dist/cjs/runtime/context.js +26 -61
- package/dist/cjs/runtime/contextHelpers.js +5 -5
- package/dist/cjs/runtime/core.js +17 -14
- package/dist/cjs/runtime/hooks.js +7 -8
- package/dist/cjs/runtime/localizedPaths.js +11 -9
- package/dist/cjs/runtime/providerComposition.js +37 -13
- package/dist/cjs/runtime/routerAdapter.js +35 -140
- package/dist/cjs/runtime/utils.js +17 -19
- package/dist/cjs/server/index.js +10 -7
- package/dist/cjs/server/redirectPolicy.js +36 -31
- package/dist/cjs/shared/urlStrategy.js +18 -0
- package/dist/esm/cli/index.mjs +0 -22
- package/dist/esm/runtime/Link.mjs +19 -32
- package/dist/esm/runtime/context.mjs +18 -62
- package/dist/esm/runtime/contextHelpers.mjs +5 -5
- package/dist/esm/runtime/core.mjs +8 -5
- package/dist/esm/runtime/hooks.mjs +7 -8
- package/dist/esm/runtime/localizedPaths.mjs +11 -9
- package/dist/esm/runtime/providerComposition.mjs +38 -14
- package/dist/esm/runtime/routerAdapter.mjs +30 -141
- package/dist/esm/runtime/utils.mjs +15 -17
- package/dist/esm/server/index.mjs +10 -7
- package/dist/esm/server/redirectPolicy.mjs +29 -24
- package/dist/esm/shared/urlStrategy.mjs +0 -0
- package/dist/esm-node/cli/index.mjs +0 -22
- package/dist/esm-node/runtime/Link.mjs +19 -32
- package/dist/esm-node/runtime/context.mjs +18 -62
- package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
- package/dist/esm-node/runtime/core.mjs +8 -5
- package/dist/esm-node/runtime/hooks.mjs +7 -8
- package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
- package/dist/esm-node/runtime/providerComposition.mjs +38 -14
- package/dist/esm-node/runtime/routerAdapter.mjs +30 -141
- package/dist/esm-node/runtime/utils.mjs +15 -17
- package/dist/esm-node/server/index.mjs +10 -7
- package/dist/esm-node/server/redirectPolicy.mjs +29 -24
- package/dist/esm-node/shared/urlStrategy.mjs +1 -0
- package/dist/types/runtime/context.d.ts +8 -11
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/core.d.ts +11 -2
- package/dist/types/runtime/hooks.d.ts +4 -5
- package/dist/types/runtime/localizedPaths.d.ts +4 -4
- package/dist/types/runtime/providerComposition.d.ts +13 -1
- package/dist/types/runtime/routerAdapter.d.ts +23 -6
- package/dist/types/runtime/utils.d.ts +4 -12
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/redirectPolicy.d.ts +5 -13
- package/dist/types/shared/type.d.ts +0 -14
- package/dist/types/shared/urlStrategy.d.ts +7 -0
- package/package.json +21 -11
- package/rstest.config.mts +2 -8
- package/src/cli/index.ts +1 -43
- package/src/runtime/Link.tsx +20 -40
- package/src/runtime/context.tsx +34 -133
- package/src/runtime/contextHelpers.ts +7 -5
- package/src/runtime/core.tsx +26 -6
- package/src/runtime/hooks.ts +14 -9
- package/src/runtime/localizedPaths.ts +24 -19
- package/src/runtime/providerComposition.tsx +71 -23
- package/src/runtime/routerAdapter.tsx +73 -338
- package/src/runtime/utils.ts +40 -31
- package/src/server/index.ts +44 -9
- package/src/server/redirectPolicy.ts +70 -70
- package/src/shared/type.ts +0 -14
- package/src/shared/urlStrategy.ts +11 -0
- package/tests/federatedI18nBoundaryResolver.test.tsx +71 -0
- package/tests/i18nUtils.test.ts +31 -65
- package/tests/link.test.tsx +13 -511
- package/tests/linkTypes.test.ts +12 -3
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +4 -96
- package/tests/localisedUrls.test.ts +7 -216
- package/tests/reactI18nextRuntimeBoundary.test.ts +1 -82
- package/tests/redirectPolicy.test.ts +39 -30
- package/tests/routerAdapter.test.tsx +95 -1055
- package/tests/federatedI18nBoundary.client.test.tsx +0 -129
- package/tests/federatedI18nBoundary.test.tsx +0 -132
- package/tests/peerCohort.test.ts +0 -25
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
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';
|
|
2
|
+
import { InternalRuntimeContext } from '@modern-js/runtime/context';
|
|
8
3
|
import type React from 'react';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
type I18nRouterFramework = 'react-router' | 'tanstack' | string;
|
|
4
|
+
import { createContext, useContext } from 'react';
|
|
12
5
|
|
|
13
6
|
interface I18nRouterLocation {
|
|
14
7
|
pathname: string;
|
|
@@ -16,14 +9,9 @@ interface I18nRouterLocation {
|
|
|
16
9
|
hash: string;
|
|
17
10
|
}
|
|
18
11
|
|
|
19
|
-
interface I18nRouterNavigateOptions {
|
|
20
|
-
replace?: boolean;
|
|
21
|
-
state?: unknown;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
12
|
type I18nRouterNavigate = (
|
|
25
13
|
href: string,
|
|
26
|
-
options?:
|
|
14
|
+
options?: { replace?: boolean; state?: unknown },
|
|
27
15
|
) => void | Promise<void>;
|
|
28
16
|
|
|
29
17
|
type I18nRouterLink = React.ComponentType<{
|
|
@@ -32,345 +20,92 @@ type I18nRouterLink = React.ComponentType<{
|
|
|
32
20
|
[key: string]: unknown;
|
|
33
21
|
}>;
|
|
34
22
|
|
|
35
|
-
interface
|
|
36
|
-
|
|
23
|
+
export interface I18nRouterLinkTarget {
|
|
24
|
+
pathname: string;
|
|
25
|
+
href: string;
|
|
26
|
+
search?: Record<string, unknown>;
|
|
27
|
+
hash?: string;
|
|
28
|
+
hashScrollIntoView?: boolean | ScrollIntoViewOptions;
|
|
29
|
+
prefetch?: 'intent' | 'render' | 'viewport' | 'none';
|
|
30
|
+
preload?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface I18nRouterAdapter {
|
|
34
|
+
framework?: string;
|
|
37
35
|
hasRouter: boolean;
|
|
38
36
|
location: I18nRouterLocation | null;
|
|
39
37
|
navigate: I18nRouterNavigate | null;
|
|
40
38
|
Link: I18nRouterLink | null;
|
|
41
39
|
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,
|
|
40
|
+
/** Convert an already resolved target to the selected router's own props. */
|
|
41
|
+
createLinkProps?: (target: I18nRouterLinkTarget) => {
|
|
42
|
+
to: string;
|
|
43
|
+
[key: string]: unknown;
|
|
124
44
|
};
|
|
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
|
-
};
|
|
45
|
+
}
|
|
203
46
|
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return JSON.stringify([
|
|
211
|
-
location?.pathname ?? '',
|
|
212
|
-
location?.search ?? '',
|
|
213
|
-
location?.hash ?? '',
|
|
214
|
-
Object.entries(params).sort(([left], [right]) => left.localeCompare(right)),
|
|
215
|
-
]);
|
|
47
|
+
const navigationContextKey = Symbol.for(
|
|
48
|
+
'@modern-js/plugin-i18n/runtime/I18nNavigationContext',
|
|
49
|
+
);
|
|
50
|
+
// Only the React context identity is shared. Its values remain provider-owned.
|
|
51
|
+
const contextStore = globalThis as typeof globalThis & {
|
|
52
|
+
[navigationContextKey]?: React.Context<I18nRouterAdapter | null>;
|
|
216
53
|
};
|
|
54
|
+
const I18nNavigationContext = (contextStore[navigationContextKey] ??=
|
|
55
|
+
createContext<I18nRouterAdapter | null>(null));
|
|
56
|
+
|
|
57
|
+
export const I18nNavigationProvider = ({
|
|
58
|
+
children,
|
|
59
|
+
value,
|
|
60
|
+
}: React.PropsWithChildren<{ value: I18nRouterAdapter }>) => (
|
|
61
|
+
<I18nNavigationContext.Provider value={value}>
|
|
62
|
+
{children}
|
|
63
|
+
</I18nNavigationContext.Provider>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
interface NativeRouterApi {
|
|
67
|
+
useInRouterContext?: () => boolean;
|
|
68
|
+
useNavigate?: () => I18nRouterNavigate;
|
|
69
|
+
useLocation?: () => I18nRouterLocation;
|
|
70
|
+
useParams?: () => Record<string, string>;
|
|
71
|
+
Link?: I18nRouterLink;
|
|
72
|
+
}
|
|
217
73
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
) as
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const reactRouterLocation = inReactRouter
|
|
74
|
+
/** Read native router hooks only when their own provider is present. */
|
|
75
|
+
export const useNativeI18nRouterAdapter = (): I18nRouterAdapter => {
|
|
76
|
+
const runtimeContext = useContext(RuntimeContext);
|
|
77
|
+
const internalContext = useContext(InternalRuntimeContext);
|
|
78
|
+
const routerApi = (internalContext.router || runtimeContext.router) as
|
|
79
|
+
| NativeRouterApi
|
|
80
|
+
| undefined;
|
|
81
|
+
const hasRouter = routerApi?.useInRouterContext?.() ?? false;
|
|
82
|
+
const navigate = hasRouter ? (routerApi?.useNavigate?.() ?? null) : null;
|
|
83
|
+
const routerLocation = hasRouter
|
|
229
84
|
? (routerApi?.useLocation?.() ?? null)
|
|
230
85
|
: 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
|
-
|
|
86
|
+
const params = hasRouter ? (routerApi?.useParams?.() ?? {}) : {};
|
|
353
87
|
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;
|
|
88
|
+
routerLocation ??
|
|
89
|
+
(isBrowser()
|
|
90
|
+
? {
|
|
91
|
+
pathname: window.location.pathname,
|
|
92
|
+
search: window.location.search,
|
|
93
|
+
hash: window.location.hash,
|
|
94
|
+
}
|
|
95
|
+
: null);
|
|
367
96
|
|
|
368
97
|
return {
|
|
369
|
-
framework,
|
|
98
|
+
framework: hasRouter ? 'react-router' : undefined,
|
|
370
99
|
hasRouter,
|
|
100
|
+
navigate,
|
|
371
101
|
location,
|
|
372
|
-
navigate: hasRouter ? navigate : null,
|
|
373
|
-
Link,
|
|
374
102
|
params,
|
|
103
|
+
Link: hasRouter ? (routerApi?.Link ?? null) : null,
|
|
375
104
|
};
|
|
376
105
|
};
|
|
106
|
+
|
|
107
|
+
export const useI18nRouterAdapter = (): I18nRouterAdapter => {
|
|
108
|
+
const provided = useContext(I18nNavigationContext);
|
|
109
|
+
const native = useNativeI18nRouterAdapter();
|
|
110
|
+
return provided ?? native;
|
|
111
|
+
};
|
package/src/runtime/utils.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
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 type { I18nUrlStrategy } from '../shared/urlStrategy';
|
|
5
|
+
|
|
6
|
+
export { splitUrlTarget } from '@modern-js/runtime-utils/url';
|
|
8
7
|
|
|
9
8
|
// Structural parameter: hooks.ts passes a public-TRuntimeContext-based
|
|
10
9
|
// context while core.tsx passes the internal one; both carry the request
|
|
@@ -47,24 +46,6 @@ export const getLanguageFromPath = (
|
|
|
47
46
|
return fallbackLanguage;
|
|
48
47
|
};
|
|
49
48
|
|
|
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
49
|
/**
|
|
69
50
|
* Helper function to build localized URL
|
|
70
51
|
* @param target - The language-agnostic target; may include `?search` and `#hash`
|
|
@@ -76,15 +57,22 @@ export const buildLocalizedUrl = (
|
|
|
76
57
|
target: string,
|
|
77
58
|
language: string,
|
|
78
59
|
languages: string[],
|
|
79
|
-
|
|
60
|
+
urlStrategy?: I18nUrlStrategy,
|
|
80
61
|
): string => {
|
|
81
62
|
const { pathname, search, hash } = splitUrlTarget(target);
|
|
82
|
-
|
|
83
|
-
pathname,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
63
|
+
if (urlStrategy) {
|
|
64
|
+
return `${urlStrategy.localizePathname(pathname, language, languages)}${search}${hash}`;
|
|
65
|
+
}
|
|
66
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
67
|
+
if (
|
|
68
|
+
segments[0] &&
|
|
69
|
+
languages.some(item => item.toLowerCase() === segments[0].toLowerCase())
|
|
70
|
+
) {
|
|
71
|
+
segments[0] = language;
|
|
72
|
+
} else {
|
|
73
|
+
segments.unshift(language);
|
|
74
|
+
}
|
|
75
|
+
const localizedPathname = `/${segments.join('/')}`;
|
|
88
76
|
|
|
89
77
|
return `${localizedPathname}${search}${hash}`;
|
|
90
78
|
};
|
|
@@ -131,6 +119,27 @@ export const shouldIgnoreRedirect = (
|
|
|
131
119
|
pathname: string,
|
|
132
120
|
languages: string[],
|
|
133
121
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
122
|
+
urlStrategy?: I18nUrlStrategy,
|
|
134
123
|
): boolean => {
|
|
135
|
-
|
|
124
|
+
if (urlStrategy?.shouldSkipRedirect?.(pathname, languages)) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (!ignoreRedirectRoutes) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
131
|
+
if (
|
|
132
|
+
segments[0] &&
|
|
133
|
+
languages.some(item => item.toLowerCase() === segments[0].toLowerCase())
|
|
134
|
+
) {
|
|
135
|
+
segments.shift();
|
|
136
|
+
}
|
|
137
|
+
const normalizedPath = `/${segments.join('/')}`;
|
|
138
|
+
return typeof ignoreRedirectRoutes === 'function'
|
|
139
|
+
? ignoreRedirectRoutes(normalizedPath)
|
|
140
|
+
: ignoreRedirectRoutes.some(
|
|
141
|
+
pattern =>
|
|
142
|
+
normalizedPath === pattern ||
|
|
143
|
+
normalizedPath.startsWith(`${pattern}/`),
|
|
144
|
+
);
|
|
136
145
|
};
|
package/src/server/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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 type { I18nUrlStrategy } from '../shared/urlStrategy';
|
|
8
8
|
import { getLocaleDetectionOptions } from '../shared/utils.js';
|
|
9
9
|
import { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
|
|
10
10
|
import { convertToHonoLanguageDetectorOptions } from './detectorOptions.js';
|
|
@@ -21,6 +21,7 @@ export { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
|
|
|
21
21
|
export interface I18nPluginOptions {
|
|
22
22
|
localeDetection: LocaleDetectionOptions;
|
|
23
23
|
staticRoutePrefixes: string[];
|
|
24
|
+
resolveUrlStrategy?: (entryName: string) => I18nUrlStrategy | undefined;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
@@ -53,6 +54,10 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
53
54
|
if (!options.localeDetection) {
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
57
|
+
const entryLocaleDetection = getLocaleDetectionOptions(
|
|
58
|
+
entryName,
|
|
59
|
+
options.localeDetection,
|
|
60
|
+
);
|
|
56
61
|
const {
|
|
57
62
|
localePathRedirect,
|
|
58
63
|
i18nextDetector = true,
|
|
@@ -60,8 +65,22 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
60
65
|
fallbackLanguage = 'en',
|
|
61
66
|
detection,
|
|
62
67
|
ignoreRedirectRoutes,
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
} = entryLocaleDetection;
|
|
69
|
+
const legacyLocalisedUrls = Reflect.get(
|
|
70
|
+
entryLocaleDetection,
|
|
71
|
+
'localisedUrls',
|
|
72
|
+
);
|
|
73
|
+
const urlStrategy = options.resolveUrlStrategy?.(entryName);
|
|
74
|
+
if (
|
|
75
|
+
legacyLocalisedUrls &&
|
|
76
|
+
typeof legacyLocalisedUrls === 'object' &&
|
|
77
|
+
Object.keys(legacyLocalisedUrls).length > 0 &&
|
|
78
|
+
!urlStrategy
|
|
79
|
+
) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'Mapped locale URLs require a URL strategy. Use the i18n integration server plugin or supply resolveUrlStrategy.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
65
84
|
const staticRoutePrefixes = options.staticRoutePrefixes;
|
|
66
85
|
const originUrlPath = route.urlPath;
|
|
67
86
|
const urlPath = originUrlPath.endsWith('/')
|
|
@@ -87,6 +106,18 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
87
106
|
return await next();
|
|
88
107
|
}
|
|
89
108
|
|
|
109
|
+
if (
|
|
110
|
+
shouldIgnoreRedirect(
|
|
111
|
+
pathname,
|
|
112
|
+
urlPath,
|
|
113
|
+
ignoreRedirectRoutes,
|
|
114
|
+
urlStrategy,
|
|
115
|
+
languages,
|
|
116
|
+
)
|
|
117
|
+
) {
|
|
118
|
+
return await next();
|
|
119
|
+
}
|
|
120
|
+
|
|
90
121
|
// For static resource requests, skip language detection
|
|
91
122
|
if (
|
|
92
123
|
isStaticResourceRequest(
|
|
@@ -138,7 +169,13 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
138
169
|
|
|
139
170
|
// Check if this route should ignore automatic redirect
|
|
140
171
|
if (
|
|
141
|
-
shouldIgnoreRedirect(
|
|
172
|
+
shouldIgnoreRedirect(
|
|
173
|
+
pathname,
|
|
174
|
+
urlPath,
|
|
175
|
+
ignoreRedirectRoutes,
|
|
176
|
+
urlStrategy,
|
|
177
|
+
languages,
|
|
178
|
+
)
|
|
142
179
|
) {
|
|
143
180
|
return await next();
|
|
144
181
|
}
|
|
@@ -168,19 +205,17 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
168
205
|
originUrlPath,
|
|
169
206
|
targetLanguage,
|
|
170
207
|
languages,
|
|
171
|
-
|
|
208
|
+
urlStrategy,
|
|
172
209
|
);
|
|
173
210
|
return createLocaleRedirectResponse(localizedUrl);
|
|
174
211
|
}
|
|
175
|
-
|
|
176
|
-
resolveLocalisedUrlsConfig(localisedUrls);
|
|
177
|
-
if (localisedUrlsConfig.enabled) {
|
|
212
|
+
if (urlStrategy) {
|
|
178
213
|
const expectedUrl = buildLocalizedUrl(
|
|
179
214
|
c.req,
|
|
180
215
|
originUrlPath,
|
|
181
216
|
language,
|
|
182
217
|
languages,
|
|
183
|
-
|
|
218
|
+
urlStrategy,
|
|
184
219
|
);
|
|
185
220
|
if (expectedUrl !== `${pathname}${url.search}${url.hash}`) {
|
|
186
221
|
return createLocaleRedirectResponse(expectedUrl);
|