@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.12 → 3.2.0-ultramodern.121
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/README.md +221 -11
- package/dist/cjs/cli/index.js +32 -5
- package/dist/cjs/runtime/I18nLink.js +17 -28
- package/dist/cjs/runtime/Link.js +264 -0
- package/dist/cjs/runtime/canonicalRoutes.js +18 -0
- package/dist/cjs/runtime/context.js +41 -10
- package/dist/cjs/runtime/hooks.js +17 -10
- package/dist/cjs/runtime/i18n/backend/config.js +9 -5
- package/dist/cjs/runtime/i18n/backend/defaults.js +15 -10
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +47 -8
- package/dist/cjs/runtime/i18n/backend/index.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.common.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.node.js +13 -9
- package/dist/cjs/runtime/i18n/backend/sdk-backend.js +9 -5
- package/dist/cjs/runtime/i18n/backend/sdk-event.js +16 -11
- package/dist/cjs/runtime/i18n/detection/config.js +9 -5
- package/dist/cjs/runtime/i18n/detection/index.js +9 -5
- package/dist/cjs/runtime/i18n/detection/middleware.js +9 -5
- package/dist/cjs/runtime/i18n/detection/middleware.node.js +9 -5
- package/dist/cjs/runtime/i18n/index.js +9 -5
- package/dist/cjs/runtime/i18n/instance.js +17 -37
- package/dist/cjs/runtime/i18n/react-i18next.js +53 -0
- package/dist/cjs/runtime/i18n/utils.js +9 -17
- package/dist/cjs/runtime/index.js +50 -15
- package/dist/cjs/runtime/localizedPaths.js +102 -0
- package/dist/cjs/runtime/routerAdapter.js +167 -0
- package/dist/cjs/runtime/utils.js +80 -97
- package/dist/cjs/server/index.js +62 -14
- package/dist/cjs/shared/deepMerge.js +12 -8
- package/dist/cjs/shared/detection.js +9 -5
- package/dist/cjs/shared/localisedUrls.js +351 -0
- package/dist/cjs/shared/utils.js +15 -11
- package/dist/esm/cli/index.mjs +23 -0
- package/dist/esm/runtime/I18nLink.mjs +7 -22
- package/dist/esm/runtime/Link.mjs +221 -0
- package/dist/esm/runtime/canonicalRoutes.mjs +0 -0
- 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/defaults.node.mjs +24 -3
- package/dist/esm/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm/runtime/i18n/instance.mjs +1 -19
- package/dist/esm/runtime/i18n/react-i18next.mjs +15 -0
- package/dist/esm/runtime/i18n/utils.mjs +0 -12
- package/dist/esm/runtime/index.mjs +23 -13
- package/dist/esm/runtime/localizedPaths.mjs +55 -0
- package/dist/esm/runtime/routerAdapter.mjs +129 -0
- package/dist/esm/runtime/utils.mjs +19 -31
- package/dist/esm/server/index.mjs +46 -8
- package/dist/esm/shared/localisedUrls.mjs +283 -0
- package/dist/esm-node/cli/index.mjs +23 -0
- package/dist/esm-node/runtime/I18nLink.mjs +7 -22
- package/dist/esm-node/runtime/Link.mjs +222 -0
- package/dist/esm-node/runtime/canonicalRoutes.mjs +1 -0
- 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/defaults.node.mjs +24 -3
- package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm-node/runtime/i18n/instance.mjs +1 -19
- package/dist/esm-node/runtime/i18n/react-i18next.mjs +16 -0
- package/dist/esm-node/runtime/i18n/utils.mjs +0 -12
- package/dist/esm-node/runtime/index.mjs +23 -13
- package/dist/esm-node/runtime/localizedPaths.mjs +56 -0
- package/dist/esm-node/runtime/routerAdapter.mjs +130 -0
- package/dist/esm-node/runtime/utils.mjs +19 -31
- package/dist/esm-node/server/index.mjs +46 -8
- package/dist/esm-node/shared/localisedUrls.mjs +284 -0
- package/dist/types/cli/index.d.ts +1 -0
- package/dist/types/runtime/I18nLink.d.ts +6 -0
- package/dist/types/runtime/Link.d.ts +66 -0
- package/dist/types/runtime/canonicalRoutes.d.ts +60 -0
- package/dist/types/runtime/context.d.ts +3 -0
- package/dist/types/runtime/hooks.d.ts +4 -2
- package/dist/types/runtime/i18n/backend/defaults.node.d.ts +3 -2
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +1 -1
- package/dist/types/runtime/i18n/instance.d.ts +4 -6
- package/dist/types/runtime/i18n/react-i18next.d.ts +7 -0
- package/dist/types/runtime/index.d.ts +6 -1
- package/dist/types/runtime/localizedPaths.d.ts +39 -0
- package/dist/types/runtime/routerAdapter.d.ts +26 -0
- package/dist/types/runtime/types.d.ts +1 -1
- package/dist/types/runtime/utils.d.ts +13 -9
- package/dist/types/server/index.d.ts +6 -0
- package/dist/types/shared/localisedUrls.d.ts +36 -0
- package/dist/types/shared/type.d.ts +14 -0
- package/package.json +24 -24
- package/rstest.config.mts +44 -0
- package/src/cli/index.ts +44 -1
- package/src/runtime/I18nLink.tsx +14 -51
- package/src/runtime/Link.tsx +430 -0
- package/src/runtime/canonicalRoutes.ts +93 -0
- package/src/runtime/context.tsx +45 -7
- package/src/runtime/hooks.ts +13 -4
- package/src/runtime/i18n/backend/defaults.node.ts +40 -2
- 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 +3 -30
- package/src/runtime/i18n/react-i18next.ts +25 -0
- package/src/runtime/i18n/utils.ts +4 -26
- package/src/runtime/index.tsx +47 -12
- package/src/runtime/localizedPaths.ts +107 -0
- package/src/runtime/routerAdapter.tsx +332 -0
- package/src/runtime/types.ts +1 -1
- package/src/runtime/utils.ts +33 -38
- package/src/server/index.ts +108 -11
- package/src/shared/localisedUrls.ts +623 -0
- package/src/shared/type.ts +14 -0
- package/tests/backendDefaults.test.ts +51 -0
- package/tests/i18nUtils.test.ts +59 -0
- package/tests/link.test.tsx +525 -0
- package/tests/linkTypes.test.ts +28 -0
- package/tests/localisedUrls.test.ts +536 -0
- package/tests/routerAdapter.test.tsx +456 -0
- package/tests/type-fixture/linkTypes.fixture.tsx +51 -0
- package/tests/type-fixture/tsconfig.json +15 -0
- package/dist/esm/rslib-runtime.mjs +0 -18
- package/dist/esm-node/rslib-runtime.mjs +0 -19
|
@@ -0,0 +1,332 @@
|
|
|
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';
|
|
8
|
+
import {
|
|
9
|
+
Link as ReactRouterLink,
|
|
10
|
+
useInRouterContext,
|
|
11
|
+
useLocation as useReactRouterLocation,
|
|
12
|
+
useNavigate as useReactRouterNavigate,
|
|
13
|
+
useParams as useReactRouterParams,
|
|
14
|
+
} from '@modern-js/runtime/router';
|
|
15
|
+
import type React from 'react';
|
|
16
|
+
import { useCallback, useContext, useEffect, useState } from 'react';
|
|
17
|
+
|
|
18
|
+
export type I18nRouterFramework = 'react-router' | 'tanstack' | string;
|
|
19
|
+
|
|
20
|
+
export interface I18nRouterLocation {
|
|
21
|
+
pathname: string;
|
|
22
|
+
search: string;
|
|
23
|
+
hash: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface I18nRouterNavigateOptions {
|
|
27
|
+
replace?: boolean;
|
|
28
|
+
state?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type I18nRouterNavigate = (
|
|
32
|
+
href: string,
|
|
33
|
+
options?: I18nRouterNavigateOptions,
|
|
34
|
+
) => void | Promise<void>;
|
|
35
|
+
|
|
36
|
+
export type I18nRouterLink = React.ComponentType<{
|
|
37
|
+
to: string;
|
|
38
|
+
children?: React.ReactNode;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
export interface I18nRouterAdapter {
|
|
43
|
+
framework?: I18nRouterFramework;
|
|
44
|
+
hasRouter: boolean;
|
|
45
|
+
location: I18nRouterLocation | null;
|
|
46
|
+
navigate: I18nRouterNavigate | null;
|
|
47
|
+
Link: I18nRouterLink | null;
|
|
48
|
+
params: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type RuntimeContextWithRouter = TRuntimeContext & {
|
|
52
|
+
router?: {
|
|
53
|
+
useRouter?: (options?: { warn?: boolean }) => unknown;
|
|
54
|
+
useLocation?: () => unknown;
|
|
55
|
+
useHref?: () => unknown;
|
|
56
|
+
Link?: I18nRouterLink;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type InternalRuntimeContextWithRouter = TInternalRuntimeContext & {
|
|
61
|
+
router?: RuntimeContextWithRouter['router'];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type RouterInstance = {
|
|
65
|
+
navigate?: (...args: any[]) => unknown;
|
|
66
|
+
state?: {
|
|
67
|
+
location?: unknown;
|
|
68
|
+
matches?: Array<{ params?: Record<string, string> }>;
|
|
69
|
+
};
|
|
70
|
+
stores?: {
|
|
71
|
+
location?: {
|
|
72
|
+
get?: () => unknown;
|
|
73
|
+
subscribe?: (listener: () => void) => () => void;
|
|
74
|
+
};
|
|
75
|
+
matches?: {
|
|
76
|
+
get?: () => Array<{ params?: Record<string, string> }>;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
subscribe?: (eventType: string, listener: () => void) => () => void;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const normalizeUrlPart = (value: unknown, prefix: '?' | '#'): string => {
|
|
83
|
+
if (typeof value !== 'string' || !value) {
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
return value.startsWith(prefix) ? value : `${prefix}${value}`;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const normalizeLocation = (location: unknown): I18nRouterLocation | null => {
|
|
90
|
+
if (!location || typeof location !== 'object') {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const locationValue = location as {
|
|
95
|
+
pathname?: unknown;
|
|
96
|
+
search?: unknown;
|
|
97
|
+
searchStr?: unknown;
|
|
98
|
+
hash?: unknown;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (typeof locationValue.pathname !== 'string') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
pathname: locationValue.pathname,
|
|
107
|
+
search: normalizeUrlPart(
|
|
108
|
+
typeof locationValue.search === 'string'
|
|
109
|
+
? locationValue.search
|
|
110
|
+
: locationValue.searchStr,
|
|
111
|
+
'?',
|
|
112
|
+
),
|
|
113
|
+
hash: normalizeUrlPart(locationValue.hash, '#'),
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const getWindowLocation = (): I18nRouterLocation | null => {
|
|
118
|
+
if (!isBrowser()) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
pathname: window.location.pathname,
|
|
124
|
+
search: window.location.search,
|
|
125
|
+
hash: window.location.hash,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const getRouterFramework = (
|
|
130
|
+
runtimeContext: RuntimeContextWithRouter,
|
|
131
|
+
internalContext: InternalRuntimeContextWithRouter,
|
|
132
|
+
inReactRouter: boolean,
|
|
133
|
+
): I18nRouterFramework | undefined => {
|
|
134
|
+
const framework =
|
|
135
|
+
getRouterRuntimeState(internalContext)?.framework ||
|
|
136
|
+
getRouterRuntimeState(runtimeContext)?.framework;
|
|
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 = getRouterRuntimeState(internalContext)?.instance;
|
|
171
|
+
if (!router || typeof router !== 'object') {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return router as RouterInstance;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const getRouterStateLocation = (
|
|
178
|
+
internalContext: InternalRuntimeContextWithRouter,
|
|
179
|
+
contextRouter?: RouterInstance | null,
|
|
180
|
+
): I18nRouterLocation | null => {
|
|
181
|
+
const router = getRouterInstance(internalContext, contextRouter);
|
|
182
|
+
return (
|
|
183
|
+
normalizeLocation(router?.stores?.location?.get?.()) ||
|
|
184
|
+
normalizeLocation(router?.state?.location)
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const getRouterParams = (
|
|
189
|
+
internalContext: InternalRuntimeContextWithRouter,
|
|
190
|
+
contextRouter?: RouterInstance | null,
|
|
191
|
+
): Record<string, string> => {
|
|
192
|
+
const router = getRouterInstance(internalContext, contextRouter);
|
|
193
|
+
const matches = router?.stores?.matches?.get?.() || router?.state?.matches;
|
|
194
|
+
if (!Array.isArray(matches)) {
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return matches.reduce<Record<string, string>>((params, match) => {
|
|
199
|
+
if (match?.params) {
|
|
200
|
+
Object.assign(params, match.params);
|
|
201
|
+
}
|
|
202
|
+
return params;
|
|
203
|
+
}, {});
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const useI18nRouterAdapter = (): I18nRouterAdapter => {
|
|
207
|
+
const runtimeContext = useContext(RuntimeContext) as RuntimeContextWithRouter;
|
|
208
|
+
const internalContext = useContext(
|
|
209
|
+
InternalRuntimeContext,
|
|
210
|
+
) as InternalRuntimeContextWithRouter;
|
|
211
|
+
const inReactRouter = useInRouterContext();
|
|
212
|
+
const reactRouterNavigate = inReactRouter ? useReactRouterNavigate() : null;
|
|
213
|
+
const reactRouterLocation = inReactRouter ? useReactRouterLocation() : null;
|
|
214
|
+
const reactRouterParams = inReactRouter ? useReactRouterParams() : {};
|
|
215
|
+
const framework = getRouterFramework(
|
|
216
|
+
runtimeContext,
|
|
217
|
+
internalContext,
|
|
218
|
+
inReactRouter,
|
|
219
|
+
);
|
|
220
|
+
const contextUseRouter =
|
|
221
|
+
!inReactRouter && framework === 'tanstack'
|
|
222
|
+
? internalContext.router?.useRouter || runtimeContext.router?.useRouter
|
|
223
|
+
: undefined;
|
|
224
|
+
const contextRouter = contextUseRouter
|
|
225
|
+
? (contextUseRouter({ warn: false }) as RouterInstance | null)
|
|
226
|
+
: null;
|
|
227
|
+
const [, setRouterVersion] = useState(0);
|
|
228
|
+
const hasRouter =
|
|
229
|
+
framework === 'tanstack' ||
|
|
230
|
+
framework === 'react-router' ||
|
|
231
|
+
Boolean(reactRouterNavigate);
|
|
232
|
+
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (framework !== 'tanstack') {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const router = getRouterInstance(internalContext, contextRouter);
|
|
239
|
+
if (!router) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const update = () => setRouterVersion(version => version + 1);
|
|
244
|
+
const unsubscribers: Array<() => void> = [];
|
|
245
|
+
|
|
246
|
+
if (typeof router.stores?.location?.subscribe === 'function') {
|
|
247
|
+
const unsubscribe = router.stores.location.subscribe(update);
|
|
248
|
+
if (typeof unsubscribe === 'function') {
|
|
249
|
+
unsubscribers.push(unsubscribe);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (typeof router.subscribe === 'function') {
|
|
254
|
+
for (const eventType of ['onBeforeNavigate', 'onBeforeLoad']) {
|
|
255
|
+
const unsubscribe = router.subscribe(eventType, update);
|
|
256
|
+
if (typeof unsubscribe === 'function') {
|
|
257
|
+
unsubscribers.push(unsubscribe);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return () => {
|
|
263
|
+
for (const unsubscribe of unsubscribers) {
|
|
264
|
+
unsubscribe();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}, [contextRouter, framework, internalContext]);
|
|
268
|
+
|
|
269
|
+
const navigate = useCallback<I18nRouterNavigate>(
|
|
270
|
+
(href, options) => {
|
|
271
|
+
const router = getRouterInstance(internalContext, contextRouter);
|
|
272
|
+
const activeFramework = getRouterFramework(
|
|
273
|
+
runtimeContext,
|
|
274
|
+
internalContext,
|
|
275
|
+
inReactRouter,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (activeFramework === 'tanstack') {
|
|
279
|
+
if (typeof router?.navigate === 'function') {
|
|
280
|
+
return router.navigate({
|
|
281
|
+
to: href,
|
|
282
|
+
replace: options?.replace,
|
|
283
|
+
...(options?.state === undefined ? {} : { state: options.state }),
|
|
284
|
+
}) as void | Promise<void>;
|
|
285
|
+
}
|
|
286
|
+
throw new Error('TanStack router instance is not available.');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (reactRouterNavigate) {
|
|
290
|
+
return reactRouterNavigate(href, options);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (activeFramework === 'react-router') {
|
|
294
|
+
if (typeof router?.navigate === 'function') {
|
|
295
|
+
return router.navigate(href, options) as void | Promise<void>;
|
|
296
|
+
}
|
|
297
|
+
throw new Error('React Router instance is not available.');
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
[
|
|
301
|
+
contextRouter,
|
|
302
|
+
internalContext,
|
|
303
|
+
inReactRouter,
|
|
304
|
+
reactRouterNavigate,
|
|
305
|
+
runtimeContext,
|
|
306
|
+
],
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const location =
|
|
310
|
+
(reactRouterLocation
|
|
311
|
+
? normalizeLocation(reactRouterLocation)
|
|
312
|
+
: getRouterStateLocation(internalContext, contextRouter)) ||
|
|
313
|
+
getWindowLocation();
|
|
314
|
+
const params = inReactRouter
|
|
315
|
+
? (reactRouterParams as Record<string, string>)
|
|
316
|
+
: getRouterParams(internalContext, contextRouter);
|
|
317
|
+
const Link =
|
|
318
|
+
framework === 'tanstack'
|
|
319
|
+
? internalContext.router?.Link || runtimeContext.router?.Link || null
|
|
320
|
+
: framework === 'react-router' || inReactRouter
|
|
321
|
+
? (ReactRouterLink as I18nRouterLink)
|
|
322
|
+
: null;
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
framework,
|
|
326
|
+
hasRouter,
|
|
327
|
+
location,
|
|
328
|
+
navigate: hasRouter ? navigate : null,
|
|
329
|
+
Link,
|
|
330
|
+
params,
|
|
331
|
+
};
|
|
332
|
+
};
|
package/src/runtime/types.ts
CHANGED
package/src/runtime/utils.ts
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
getGlobalBasename,
|
|
4
4
|
type TInternalRuntimeContext,
|
|
5
5
|
} from '@modern-js/runtime/context';
|
|
6
|
+
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
7
|
+
import { localiseTargetPathname } from '../shared/localisedUrls';
|
|
6
8
|
|
|
7
9
|
export const getPathname = (context: TInternalRuntimeContext): string => {
|
|
8
10
|
if (isBrowser()) {
|
|
@@ -40,29 +42,46 @@ export const getLanguageFromPath = (
|
|
|
40
42
|
return fallbackLanguage;
|
|
41
43
|
};
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Split a link target into its pathname, search and hash parts without
|
|
47
|
+
* relying on `new URL` (SSR-hot path; targets are relative).
|
|
48
|
+
*/
|
|
49
|
+
export const splitUrlTarget = (
|
|
50
|
+
target: string,
|
|
51
|
+
): { pathname: string; search: string; hash: string } => {
|
|
52
|
+
const hashIndex = target.indexOf('#');
|
|
53
|
+
const hash = hashIndex >= 0 ? target.slice(hashIndex) : '';
|
|
54
|
+
const beforeHash = hashIndex >= 0 ? target.slice(0, hashIndex) : target;
|
|
55
|
+
const searchIndex = beforeHash.indexOf('?');
|
|
56
|
+
const search = searchIndex >= 0 ? beforeHash.slice(searchIndex) : '';
|
|
57
|
+
const pathname =
|
|
58
|
+
searchIndex >= 0 ? beforeHash.slice(0, searchIndex) : beforeHash;
|
|
59
|
+
|
|
60
|
+
return { pathname, search, hash };
|
|
61
|
+
};
|
|
62
|
+
|
|
43
63
|
/**
|
|
44
64
|
* Helper function to build localized URL
|
|
45
|
-
* @param
|
|
65
|
+
* @param target - The language-agnostic target; may include `?search` and `#hash`
|
|
46
66
|
* @param language - The target language
|
|
47
67
|
* @param languages - Array of supported languages
|
|
48
|
-
* @returns The localized URL path
|
|
68
|
+
* @returns The localized URL path with search and hash re-appended verbatim
|
|
49
69
|
*/
|
|
50
70
|
export const buildLocalizedUrl = (
|
|
51
|
-
|
|
71
|
+
target: string,
|
|
52
72
|
language: string,
|
|
53
73
|
languages: string[],
|
|
74
|
+
localisedUrls?: LocalisedUrlsOption,
|
|
54
75
|
): string => {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return `/${segments.join('/')}`;
|
|
76
|
+
const { pathname, search, hash } = splitUrlTarget(target);
|
|
77
|
+
const localizedPathname = localiseTargetPathname(
|
|
78
|
+
pathname,
|
|
79
|
+
language,
|
|
80
|
+
languages,
|
|
81
|
+
localisedUrls,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return `${localizedPathname}${search}${hash}`;
|
|
66
85
|
};
|
|
67
86
|
|
|
68
87
|
export const detectLanguageFromPath = (
|
|
@@ -137,27 +156,3 @@ export const shouldIgnoreRedirect = (
|
|
|
137
156
|
);
|
|
138
157
|
});
|
|
139
158
|
};
|
|
140
|
-
|
|
141
|
-
// Safe hook wrapper to handle cases where router context is not available
|
|
142
|
-
export const useRouterHooks = () => {
|
|
143
|
-
try {
|
|
144
|
-
const {
|
|
145
|
-
useLocation,
|
|
146
|
-
useNavigate,
|
|
147
|
-
useParams,
|
|
148
|
-
} = require('@modern-js/runtime/router');
|
|
149
|
-
return {
|
|
150
|
-
navigate: useNavigate(),
|
|
151
|
-
location: useLocation(),
|
|
152
|
-
params: useParams(),
|
|
153
|
-
hasRouter: true,
|
|
154
|
-
};
|
|
155
|
-
} catch (error) {
|
|
156
|
-
return {
|
|
157
|
-
navigate: null,
|
|
158
|
-
location: null,
|
|
159
|
-
params: {},
|
|
160
|
-
hasRouter: false,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
};
|
package/src/server/index.ts
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
mergeDetectionOptions,
|
|
9
9
|
} from '../runtime/i18n/detection/config.js';
|
|
10
10
|
import type { LanguageDetectorOptions } from '../runtime/i18n/instance';
|
|
11
|
+
import {
|
|
12
|
+
localiseTargetPathname,
|
|
13
|
+
resolveLocalisedUrlsConfig,
|
|
14
|
+
} from '../shared/localisedUrls.js';
|
|
11
15
|
import type { LocaleDetectionOptions } from '../shared/type';
|
|
12
16
|
import { getLocaleDetectionOptions } from '../shared/utils.js';
|
|
13
17
|
|
|
@@ -16,6 +20,74 @@ export interface I18nPluginOptions {
|
|
|
16
20
|
staticRoutePrefixes: string[];
|
|
17
21
|
}
|
|
18
22
|
|
|
23
|
+
type ApiPrefixInput = string | string[] | undefined;
|
|
24
|
+
|
|
25
|
+
const normalizeApiPrefix = (prefix: string): string | null => {
|
|
26
|
+
const trimmedPrefix = prefix.trim();
|
|
27
|
+
if (!trimmedPrefix) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const prefixedPath = trimmedPrefix.startsWith('/')
|
|
32
|
+
? trimmedPrefix
|
|
33
|
+
: `/${trimmedPrefix}`;
|
|
34
|
+
const withoutWildcard = prefixedPath.replace(/\/\*$/, '');
|
|
35
|
+
const normalizedPrefix =
|
|
36
|
+
withoutWildcard.length > 1
|
|
37
|
+
? withoutWildcard.replace(/\/+$/, '')
|
|
38
|
+
: withoutWildcard;
|
|
39
|
+
|
|
40
|
+
return normalizedPrefix === '/' ? null : normalizedPrefix;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const collectApiPrefixes = (
|
|
44
|
+
routes: Array<{ isApi?: boolean; urlPath?: string }>,
|
|
45
|
+
bffPrefix?: ApiPrefixInput,
|
|
46
|
+
): string[] => {
|
|
47
|
+
const prefixes = new Set<string>();
|
|
48
|
+
|
|
49
|
+
for (const route of routes) {
|
|
50
|
+
if (!route.isApi || !route.urlPath) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const normalizedPrefix = normalizeApiPrefix(route.urlPath);
|
|
55
|
+
if (normalizedPrefix) {
|
|
56
|
+
prefixes.add(normalizedPrefix);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const bffPrefixes = Array.isArray(bffPrefix)
|
|
61
|
+
? bffPrefix
|
|
62
|
+
: bffPrefix
|
|
63
|
+
? [bffPrefix]
|
|
64
|
+
: [];
|
|
65
|
+
|
|
66
|
+
for (const prefix of bffPrefixes) {
|
|
67
|
+
const normalizedPrefix = normalizeApiPrefix(prefix);
|
|
68
|
+
if (normalizedPrefix) {
|
|
69
|
+
prefixes.add(normalizedPrefix);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [...prefixes];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const matchesApiPrefix = (
|
|
77
|
+
pathname: string,
|
|
78
|
+
apiPrefixes: string[],
|
|
79
|
+
): boolean => {
|
|
80
|
+
const normalizedPathname = pathname.startsWith('/')
|
|
81
|
+
? pathname
|
|
82
|
+
: `/${pathname}`;
|
|
83
|
+
|
|
84
|
+
return apiPrefixes.some(
|
|
85
|
+
prefix =>
|
|
86
|
+
normalizedPathname === prefix ||
|
|
87
|
+
normalizedPathname.startsWith(`${prefix}/`),
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
19
91
|
/**
|
|
20
92
|
* Convert i18next detection options to hono languageDetector options
|
|
21
93
|
*/
|
|
@@ -231,6 +303,7 @@ const buildLocalizedUrl = (
|
|
|
231
303
|
urlPath: string,
|
|
232
304
|
language: string,
|
|
233
305
|
languages: string[],
|
|
306
|
+
localisedUrls?: LocaleDetectionOptions['localisedUrls'],
|
|
234
307
|
): string => {
|
|
235
308
|
const url = new URL(req.url);
|
|
236
309
|
const pathname = url.pathname;
|
|
@@ -241,17 +314,12 @@ const buildLocalizedUrl = (
|
|
|
241
314
|
? pathname.slice(basePath.length)
|
|
242
315
|
: pathname;
|
|
243
316
|
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// If path doesn't start with language, add language prefix
|
|
251
|
-
segments.unshift(language);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const newPathname = `/${segments.join('/')}`;
|
|
317
|
+
const newPathname = localiseTargetPathname(
|
|
318
|
+
remainingPath,
|
|
319
|
+
language,
|
|
320
|
+
languages,
|
|
321
|
+
localisedUrls,
|
|
322
|
+
);
|
|
255
323
|
// Handle root path case to avoid double slashes like //en
|
|
256
324
|
const suffix = `${url.search}${url.hash}`;
|
|
257
325
|
const localizedUrl =
|
|
@@ -265,6 +333,11 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
265
333
|
setup: api => {
|
|
266
334
|
api.onPrepare(() => {
|
|
267
335
|
const { middlewares, routes } = api.getServerContext();
|
|
336
|
+
const serverConfig = api.getServerConfig();
|
|
337
|
+
const bffPrefix = serverConfig?.bff
|
|
338
|
+
? (serverConfig.bff.prefix ?? '/api')
|
|
339
|
+
: undefined;
|
|
340
|
+
const apiPrefixes = collectApiPrefixes(routes, bffPrefix);
|
|
268
341
|
|
|
269
342
|
// Collect all non-root entry paths for cross-entry path detection
|
|
270
343
|
const entryPaths = new Set<string>();
|
|
@@ -292,6 +365,7 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
292
365
|
fallbackLanguage = 'en',
|
|
293
366
|
detection,
|
|
294
367
|
ignoreRedirectRoutes,
|
|
368
|
+
localisedUrls,
|
|
295
369
|
} = getLocaleDetectionOptions(entryName, options.localeDetection);
|
|
296
370
|
const staticRoutePrefixes = options.staticRoutePrefixes;
|
|
297
371
|
const originUrlPath = route.urlPath;
|
|
@@ -314,6 +388,10 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
314
388
|
const url = new URL(c.req.url);
|
|
315
389
|
const pathname = url.pathname;
|
|
316
390
|
|
|
391
|
+
if (matchesApiPrefix(pathname, apiPrefixes)) {
|
|
392
|
+
return await next();
|
|
393
|
+
}
|
|
394
|
+
|
|
317
395
|
// For static resource requests, skip language detection
|
|
318
396
|
if (
|
|
319
397
|
isStaticResourceRequest(
|
|
@@ -348,6 +426,10 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
348
426
|
const url = new URL(c.req.url);
|
|
349
427
|
const pathname = url.pathname;
|
|
350
428
|
|
|
429
|
+
if (matchesApiPrefix(pathname, apiPrefixes)) {
|
|
430
|
+
return await next();
|
|
431
|
+
}
|
|
432
|
+
|
|
351
433
|
// For static resource requests, skip i18n processing
|
|
352
434
|
if (
|
|
353
435
|
isStaticResourceRequest(
|
|
@@ -391,9 +473,24 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
391
473
|
originUrlPath,
|
|
392
474
|
targetLanguage,
|
|
393
475
|
languages,
|
|
476
|
+
localisedUrls,
|
|
394
477
|
);
|
|
395
478
|
return c.redirect(localizedUrl);
|
|
396
479
|
}
|
|
480
|
+
const localisedUrlsConfig =
|
|
481
|
+
resolveLocalisedUrlsConfig(localisedUrls);
|
|
482
|
+
if (localisedUrlsConfig.enabled) {
|
|
483
|
+
const expectedUrl = buildLocalizedUrl(
|
|
484
|
+
c.req,
|
|
485
|
+
originUrlPath,
|
|
486
|
+
language,
|
|
487
|
+
languages,
|
|
488
|
+
localisedUrls,
|
|
489
|
+
);
|
|
490
|
+
if (expectedUrl !== `${pathname}${url.search}${url.hash}`) {
|
|
491
|
+
return c.redirect(expectedUrl);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
397
494
|
await next();
|
|
398
495
|
},
|
|
399
496
|
});
|