@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.5 → 3.5.0-ultramodern.75
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 +2 -1
- package/dist/cjs/cli/locales.js +15 -15
- package/dist/cjs/runtime/Link.js +8 -89
- package/dist/cjs/runtime/context.js +78 -91
- package/dist/cjs/runtime/contextHelpers.js +143 -0
- package/dist/cjs/runtime/core.js +32 -132
- package/dist/cjs/runtime/hooks.js +54 -21
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
- package/dist/cjs/runtime/i18n/detection/cache.js +150 -0
- package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
- package/dist/cjs/runtime/i18n/detection/index.js +8 -263
- package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
- package/dist/cjs/runtime/i18n/detection/language.js +62 -0
- package/dist/cjs/runtime/i18n/detection/path.js +49 -0
- package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
- package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
- package/dist/cjs/runtime/i18n/detection/types.js +18 -0
- package/dist/cjs/runtime/i18n/instance.js +34 -10
- package/dist/cjs/runtime/linkHelpers.js +148 -0
- package/dist/cjs/runtime/pluginSetup.js +102 -0
- package/dist/cjs/runtime/providerComposition.js +115 -0
- package/dist/cjs/runtime/reactI18next.js +43 -0
- package/dist/cjs/runtime/routerAdapter.js +16 -8
- package/dist/cjs/runtime/utils.js +1 -9
- package/dist/cjs/server/apiPrefix.js +72 -0
- package/dist/cjs/server/detectorOptions.js +97 -0
- package/dist/cjs/server/index.js +17 -144
- package/dist/cjs/server/redirectPolicy.js +96 -0
- package/dist/cjs/shared/detection.js +2 -104
- package/dist/cjs/shared/localisedUrls/config.js +49 -0
- package/dist/cjs/shared/localisedUrls/index.js +91 -0
- package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
- package/dist/cjs/shared/localisedUrls/pathname.js +68 -0
- package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
- package/dist/cjs/shared/localisedUrls/redirect.js +103 -0
- package/dist/cjs/shared/localisedUrls/resolve.js +88 -0
- package/dist/cjs/shared/localisedUrls/routes.js +130 -0
- package/dist/cjs/shared/localisedUrls/types.js +18 -0
- package/dist/cjs/shared/localisedUrls.js +28 -294
- package/dist/cjs/shared/utils.js +2 -8
- package/dist/esm/cli/index.mjs +2 -1
- package/dist/esm/cli/locales.mjs +13 -13
- package/dist/esm/runtime/Link.mjs +1 -82
- package/dist/esm/runtime/context.mjs +76 -92
- package/dist/esm/runtime/contextHelpers.mjs +87 -0
- package/dist/esm/runtime/core.mjs +30 -133
- package/dist/esm/runtime/hooks.mjs +56 -23
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
- package/dist/esm/runtime/i18n/detection/cache.mjs +103 -0
- package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
- package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
- package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
- package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
- package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
- package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
- package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
- package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
- package/dist/esm/runtime/i18n/instance.mjs +35 -8
- package/dist/esm/runtime/linkHelpers.mjs +98 -0
- package/dist/esm/runtime/pluginSetup.mjs +64 -0
- package/dist/esm/runtime/providerComposition.mjs +77 -0
- package/dist/esm/runtime/reactI18next.mjs +5 -0
- package/dist/esm/runtime/routerAdapter.mjs +16 -8
- package/dist/esm/runtime/utils.mjs +2 -10
- package/dist/esm/server/apiPrefix.mjs +31 -0
- package/dist/esm/server/detectorOptions.mjs +59 -0
- package/dist/esm/server/index.mjs +8 -135
- package/dist/esm/server/redirectPolicy.mjs +46 -0
- package/dist/esm/shared/detection.mjs +1 -71
- package/dist/esm/shared/localisedUrls/config.mjs +11 -0
- package/dist/esm/shared/localisedUrls/index.mjs +7 -0
- package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
- package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
- package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
- package/dist/esm/shared/localisedUrls/redirect.mjs +56 -0
- package/dist/esm/shared/localisedUrls/resolve.mjs +47 -0
- package/dist/esm/shared/localisedUrls/routes.mjs +89 -0
- package/dist/esm/shared/localisedUrls/types.mjs +0 -0
- package/dist/esm/shared/localisedUrls.mjs +1 -283
- package/dist/esm/shared/utils.mjs +1 -1
- package/dist/esm-node/cli/index.mjs +2 -1
- package/dist/esm-node/cli/locales.mjs +13 -13
- package/dist/esm-node/runtime/Link.mjs +1 -82
- package/dist/esm-node/runtime/context.mjs +76 -92
- package/dist/esm-node/runtime/contextHelpers.mjs +88 -0
- package/dist/esm-node/runtime/core.mjs +30 -133
- package/dist/esm-node/runtime/hooks.mjs +56 -23
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
- package/dist/esm-node/runtime/i18n/detection/cache.mjs +104 -0
- package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
- package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
- package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
- package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
- package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
- package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
- package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
- package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
- package/dist/esm-node/runtime/i18n/instance.mjs +35 -8
- package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
- package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
- package/dist/esm-node/runtime/providerComposition.mjs +78 -0
- package/dist/esm-node/runtime/reactI18next.mjs +6 -0
- package/dist/esm-node/runtime/routerAdapter.mjs +16 -8
- package/dist/esm-node/runtime/utils.mjs +2 -10
- package/dist/esm-node/server/apiPrefix.mjs +32 -0
- package/dist/esm-node/server/detectorOptions.mjs +60 -0
- package/dist/esm-node/server/index.mjs +8 -135
- package/dist/esm-node/server/redirectPolicy.mjs +47 -0
- package/dist/esm-node/shared/detection.mjs +1 -71
- package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
- package/dist/esm-node/shared/localisedUrls/index.mjs +8 -0
- package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
- package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
- package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
- package/dist/esm-node/shared/localisedUrls/redirect.mjs +57 -0
- package/dist/esm-node/shared/localisedUrls/resolve.mjs +48 -0
- package/dist/esm-node/shared/localisedUrls/routes.mjs +90 -0
- package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
- package/dist/esm-node/shared/localisedUrls.mjs +1 -283
- package/dist/esm-node/shared/utils.mjs +1 -1
- package/dist/types/cli/locales.d.ts +1 -1
- package/dist/types/runtime/Link.d.ts +3 -7
- package/dist/types/runtime/context.d.ts +21 -5
- package/dist/types/runtime/contextHelpers.d.ts +29 -0
- package/dist/types/runtime/core.d.ts +4 -8
- package/dist/types/runtime/hooks.d.ts +3 -7
- package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
- package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
- package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
- package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
- package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
- package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
- package/dist/types/runtime/i18n/detection/middleware.d.ts +4 -4
- package/dist/types/runtime/i18n/detection/middleware.node.d.ts +4 -4
- package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
- package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
- package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
- package/dist/types/runtime/i18n/detection/types.d.ts +32 -0
- package/dist/types/runtime/i18n/instance.d.ts +31 -27
- package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
- package/dist/types/runtime/i18n/utils.d.ts +14 -5
- package/dist/types/runtime/linkHelpers.d.ts +17 -0
- package/dist/types/runtime/pluginSetup.d.ts +32 -0
- package/dist/types/runtime/providerComposition.d.ts +16 -0
- package/dist/types/runtime/reactI18next.d.ts +7 -0
- package/dist/types/runtime/routerAdapter.d.ts +7 -6
- package/dist/types/runtime/utils.d.ts +7 -2
- package/dist/types/server/apiPrefix.d.ts +7 -0
- package/dist/types/server/detectorOptions.d.ts +21 -0
- package/dist/types/server/index.d.ts +1 -6
- package/dist/types/server/redirectPolicy.d.ts +22 -0
- package/dist/types/shared/detection.d.ts +1 -11
- package/dist/types/shared/localisedUrls/config.d.ts +9 -0
- package/dist/types/shared/localisedUrls/index.d.ts +9 -0
- package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
- package/dist/types/shared/localisedUrls/pathname.d.ts +4 -0
- package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
- package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
- package/dist/types/shared/localisedUrls/resolve.d.ts +7 -0
- package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
- package/dist/types/shared/localisedUrls/types.d.ts +14 -0
- package/dist/types/shared/localisedUrls.d.ts +2 -36
- package/dist/types/shared/utils.d.ts +0 -2
- package/package.json +17 -20
- package/rstest.config.mts +8 -1
- package/src/cli/index.ts +5 -4
- package/src/cli/locales.ts +3 -3
- package/src/runtime/Link.tsx +11 -138
- package/src/runtime/context.tsx +170 -207
- package/src/runtime/contextHelpers.ts +242 -0
- package/src/runtime/core.tsx +48 -275
- package/src/runtime/hooks.ts +86 -24
- package/src/runtime/i18n/backend/defaults.node.ts +2 -2
- package/src/runtime/i18n/backend/middleware.common.ts +5 -4
- package/src/runtime/i18n/detection/cache.ts +189 -0
- package/src/runtime/i18n/detection/detector.ts +166 -0
- package/src/runtime/i18n/detection/index.ts +10 -641
- package/src/runtime/i18n/detection/initOptions.ts +74 -0
- package/src/runtime/i18n/detection/language.ts +64 -0
- package/src/runtime/i18n/detection/middleware.node.ts +26 -7
- package/src/runtime/i18n/detection/middleware.ts +15 -7
- package/src/runtime/i18n/detection/path.ts +41 -0
- package/src/runtime/i18n/detection/priority.ts +82 -0
- package/src/runtime/i18n/detection/ssr.ts +47 -0
- package/src/runtime/i18n/detection/types.ts +41 -0
- package/src/runtime/i18n/instance.ts +98 -34
- package/src/runtime/i18n/react-i18next.ts +1 -6
- package/src/runtime/i18n/utils.ts +18 -9
- package/src/runtime/linkHelpers.ts +174 -0
- package/src/runtime/pluginSetup.ts +189 -0
- package/src/runtime/providerComposition.tsx +148 -0
- package/src/runtime/reactI18next.ts +20 -0
- package/src/runtime/routerAdapter.tsx +36 -15
- package/src/runtime/utils.ts +13 -35
- package/src/server/apiPrefix.ts +67 -0
- package/src/server/detectorOptions.ts +105 -0
- package/src/server/index.ts +14 -319
- package/src/server/redirectPolicy.ts +127 -0
- package/src/shared/detection.ts +1 -1
- package/src/shared/localisedUrls/config.ts +18 -0
- package/src/shared/localisedUrls/index.ts +24 -0
- package/src/shared/localisedUrls/normalise.ts +67 -0
- package/src/shared/localisedUrls/pathname.ts +60 -0
- package/src/shared/localisedUrls/patterns.ts +160 -0
- package/src/shared/localisedUrls/redirect.ts +93 -0
- package/src/shared/localisedUrls/resolve.ts +135 -0
- package/src/shared/localisedUrls/routes.ts +234 -0
- package/src/shared/localisedUrls/types.ts +18 -0
- package/src/shared/localisedUrls.ts +26 -623
- package/src/shared/utils.ts +2 -2
- package/tests/federatedI18nBoundary.client.test.tsx +112 -0
- package/tests/federatedI18nBoundary.test.tsx +120 -0
- package/tests/fixtures/localised-routes.golden.json +156 -0
- package/tests/link.test.tsx +111 -0
- package/tests/linkTypes.test.ts +15 -4
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
- package/tests/localisedUrls.test.ts +148 -2
- package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
- package/tests/redirectPolicy.test.ts +45 -0
- package/tests/routerAdapter.test.tsx +603 -0
- package/tests/type-fixture/fixture-globals.d.ts +11 -0
- package/tests/type-fixture/tsconfig.json +1 -0
|
@@ -15,31 +15,31 @@ import {
|
|
|
15
15
|
import type React from 'react';
|
|
16
16
|
import { useCallback, useContext, useEffect, useState } from 'react';
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
type I18nRouterFramework = 'react-router' | 'tanstack' | string;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
interface I18nRouterLocation {
|
|
21
21
|
pathname: string;
|
|
22
22
|
search: string;
|
|
23
23
|
hash: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
interface I18nRouterNavigateOptions {
|
|
27
27
|
replace?: boolean;
|
|
28
28
|
state?: unknown;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
type I18nRouterNavigate = (
|
|
32
32
|
href: string,
|
|
33
33
|
options?: I18nRouterNavigateOptions,
|
|
34
34
|
) => void | Promise<void>;
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
type I18nRouterLink = React.ComponentType<{
|
|
37
37
|
to: string;
|
|
38
38
|
children?: React.ReactNode;
|
|
39
39
|
[key: string]: unknown;
|
|
40
40
|
}>;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
interface I18nRouterAdapter {
|
|
43
43
|
framework?: I18nRouterFramework;
|
|
44
44
|
hasRouter: boolean;
|
|
45
45
|
location: I18nRouterLocation | null;
|
|
@@ -76,7 +76,9 @@ type RouterInstance = {
|
|
|
76
76
|
get?: () => Array<{ params?: Record<string, string> }>;
|
|
77
77
|
};
|
|
78
78
|
};
|
|
79
|
-
subscribe?:
|
|
79
|
+
subscribe?:
|
|
80
|
+
| ((listener: () => void) => () => void)
|
|
81
|
+
| ((eventType: string, listener: () => void) => () => void);
|
|
80
82
|
};
|
|
81
83
|
|
|
82
84
|
const normalizeUrlPart = (value: unknown, prefix: '?' | '#'): string => {
|
|
@@ -231,10 +233,6 @@ export const useI18nRouterAdapter = (): I18nRouterAdapter => {
|
|
|
231
233
|
Boolean(reactRouterNavigate);
|
|
232
234
|
|
|
233
235
|
useEffect(() => {
|
|
234
|
-
if (framework !== 'tanstack') {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
236
|
const router = getRouterInstance(internalContext, contextRouter);
|
|
239
237
|
if (!router) {
|
|
240
238
|
return;
|
|
@@ -243,16 +241,39 @@ export const useI18nRouterAdapter = (): I18nRouterAdapter => {
|
|
|
243
241
|
const update = () => setRouterVersion(version => version + 1);
|
|
244
242
|
const unsubscribers: Array<() => void> = [];
|
|
245
243
|
|
|
246
|
-
if (
|
|
244
|
+
if (
|
|
245
|
+
framework === 'react-router' &&
|
|
246
|
+
!inReactRouter &&
|
|
247
|
+
typeof router.subscribe === 'function'
|
|
248
|
+
) {
|
|
249
|
+
const subscribe = router.subscribe as (
|
|
250
|
+
this: RouterInstance,
|
|
251
|
+
listener: () => void,
|
|
252
|
+
) => () => void;
|
|
253
|
+
const unsubscribe = subscribe.call(router, update);
|
|
254
|
+
if (typeof unsubscribe === 'function') {
|
|
255
|
+
unsubscribers.push(unsubscribe);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
framework === 'tanstack' &&
|
|
261
|
+
typeof router.stores?.location?.subscribe === 'function'
|
|
262
|
+
) {
|
|
247
263
|
const unsubscribe = router.stores.location.subscribe(update);
|
|
248
264
|
if (typeof unsubscribe === 'function') {
|
|
249
265
|
unsubscribers.push(unsubscribe);
|
|
250
266
|
}
|
|
251
267
|
}
|
|
252
268
|
|
|
253
|
-
if (typeof router.subscribe === 'function') {
|
|
269
|
+
if (framework === 'tanstack' && typeof router.subscribe === 'function') {
|
|
270
|
+
const subscribe = router.subscribe as (
|
|
271
|
+
this: RouterInstance,
|
|
272
|
+
eventType: string,
|
|
273
|
+
listener: () => void,
|
|
274
|
+
) => () => void;
|
|
254
275
|
for (const eventType of ['onBeforeNavigate', 'onBeforeLoad']) {
|
|
255
|
-
const unsubscribe =
|
|
276
|
+
const unsubscribe = subscribe.call(router, eventType, update);
|
|
256
277
|
if (typeof unsubscribe === 'function') {
|
|
257
278
|
unsubscribers.push(unsubscribe);
|
|
258
279
|
}
|
|
@@ -264,7 +285,7 @@ export const useI18nRouterAdapter = (): I18nRouterAdapter => {
|
|
|
264
285
|
unsubscribe();
|
|
265
286
|
}
|
|
266
287
|
};
|
|
267
|
-
}, [contextRouter, framework, internalContext]);
|
|
288
|
+
}, [contextRouter, framework, inReactRouter, internalContext]);
|
|
268
289
|
|
|
269
290
|
const navigate = useCallback<I18nRouterNavigate>(
|
|
270
291
|
(href, options) => {
|
package/src/runtime/utils.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
2
|
-
import {
|
|
3
|
-
getGlobalBasename,
|
|
4
|
-
type TInternalRuntimeContext,
|
|
5
|
-
} from '@modern-js/runtime/context';
|
|
2
|
+
import { getGlobalBasename } from '@modern-js/runtime/context';
|
|
6
3
|
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import {
|
|
5
|
+
localiseTargetPathname,
|
|
6
|
+
shouldSkipLocaleRedirect,
|
|
7
|
+
} from '../shared/localisedUrls';
|
|
8
|
+
|
|
9
|
+
// Structural parameter: hooks.ts passes a public-TRuntimeContext-based
|
|
10
|
+
// context while core.tsx passes the internal one; both carry the request
|
|
11
|
+
// pathname shape this helper needs.
|
|
12
|
+
export const getPathname = (context: {
|
|
13
|
+
ssrContext?: { request?: { pathname?: string } };
|
|
14
|
+
}): string => {
|
|
10
15
|
if (isBrowser()) {
|
|
11
16
|
return window.location.pathname;
|
|
12
17
|
}
|
|
@@ -127,32 +132,5 @@ export const shouldIgnoreRedirect = (
|
|
|
127
132
|
languages: string[],
|
|
128
133
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
129
134
|
): boolean => {
|
|
130
|
-
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Remove language prefix if present (e.g., /en/api -> /api)
|
|
135
|
-
const segments = pathname.split('/').filter(Boolean);
|
|
136
|
-
let pathWithoutLang = pathname;
|
|
137
|
-
if (segments.length > 0 && languages.includes(segments[0])) {
|
|
138
|
-
// Remove language prefix
|
|
139
|
-
pathWithoutLang = `/${segments.slice(1).join('/')}`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Normalize path (ensure it starts with /)
|
|
143
|
-
const normalizedPath = pathWithoutLang.startsWith('/')
|
|
144
|
-
? pathWithoutLang
|
|
145
|
-
: `/${pathWithoutLang}`;
|
|
146
|
-
|
|
147
|
-
if (typeof ignoreRedirectRoutes === 'function') {
|
|
148
|
-
return ignoreRedirectRoutes(normalizedPath);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Check if pathname matches any of the ignore patterns
|
|
152
|
-
return ignoreRedirectRoutes.some(pattern => {
|
|
153
|
-
// Support both exact match and prefix match
|
|
154
|
-
return (
|
|
155
|
-
normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`)
|
|
156
|
-
);
|
|
157
|
-
});
|
|
135
|
+
return shouldSkipLocaleRedirect(pathname, languages, ignoreRedirectRoutes);
|
|
158
136
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
type ApiPrefixInput = string | string[] | undefined;
|
|
2
|
+
|
|
3
|
+
const normalizeApiPrefix = (prefix: string): string | null => {
|
|
4
|
+
const trimmedPrefix = prefix.trim();
|
|
5
|
+
if (!trimmedPrefix) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const prefixedPath = trimmedPrefix.startsWith('/')
|
|
10
|
+
? trimmedPrefix
|
|
11
|
+
: `/${trimmedPrefix}`;
|
|
12
|
+
const withoutWildcard = prefixedPath.replace(/\/\*$/, '');
|
|
13
|
+
const normalizedPrefix =
|
|
14
|
+
withoutWildcard.length > 1
|
|
15
|
+
? withoutWildcard.replace(/\/+$/, '')
|
|
16
|
+
: withoutWildcard;
|
|
17
|
+
|
|
18
|
+
return normalizedPrefix === '/' ? null : normalizedPrefix;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const collectApiPrefixes = (
|
|
22
|
+
routes: Array<{ isApi?: boolean; urlPath?: string }>,
|
|
23
|
+
bffPrefix?: ApiPrefixInput,
|
|
24
|
+
): string[] => {
|
|
25
|
+
const prefixes = new Set<string>();
|
|
26
|
+
|
|
27
|
+
for (const route of routes) {
|
|
28
|
+
if (!route.isApi || !route.urlPath) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalizedPrefix = normalizeApiPrefix(route.urlPath);
|
|
33
|
+
if (normalizedPrefix) {
|
|
34
|
+
prefixes.add(normalizedPrefix);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const bffPrefixes = Array.isArray(bffPrefix)
|
|
39
|
+
? bffPrefix
|
|
40
|
+
: bffPrefix
|
|
41
|
+
? [bffPrefix]
|
|
42
|
+
: [];
|
|
43
|
+
|
|
44
|
+
for (const prefix of bffPrefixes) {
|
|
45
|
+
const normalizedPrefix = normalizeApiPrefix(prefix);
|
|
46
|
+
if (normalizedPrefix) {
|
|
47
|
+
prefixes.add(normalizedPrefix);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [...prefixes];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const matchesApiPrefix = (
|
|
55
|
+
pathname: string,
|
|
56
|
+
apiPrefixes: string[],
|
|
57
|
+
): boolean => {
|
|
58
|
+
const normalizedPathname = pathname.startsWith('/')
|
|
59
|
+
? pathname
|
|
60
|
+
: `/${pathname}`;
|
|
61
|
+
|
|
62
|
+
return apiPrefixes.some(
|
|
63
|
+
prefix =>
|
|
64
|
+
normalizedPathname === prefix ||
|
|
65
|
+
normalizedPathname.startsWith(`${prefix}/`),
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_I18NEXT_DETECTION_OPTIONS,
|
|
3
|
+
mergeDetectionOptions,
|
|
4
|
+
} from '../runtime/i18n/detection/config.js';
|
|
5
|
+
import type { LanguageDetectorOptions } from '../runtime/i18n/instance';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert i18next detection options to hono languageDetector options
|
|
9
|
+
*/
|
|
10
|
+
export const convertToHonoLanguageDetectorOptions = (
|
|
11
|
+
languages: string[],
|
|
12
|
+
fallbackLanguage: string,
|
|
13
|
+
detectionOptions?: LanguageDetectorOptions,
|
|
14
|
+
) => {
|
|
15
|
+
// Merge user detection options with defaults
|
|
16
|
+
const mergedDetection = detectionOptions
|
|
17
|
+
? mergeDetectionOptions(detectionOptions)
|
|
18
|
+
: DEFAULT_I18NEXT_DETECTION_OPTIONS;
|
|
19
|
+
|
|
20
|
+
// Get detection order, excluding 'path' and browser-only detectors
|
|
21
|
+
const order = (mergedDetection.order || []).filter(
|
|
22
|
+
(item: string) =>
|
|
23
|
+
!['path', 'localStorage', 'navigator', 'htmlTag', 'subdomain'].includes(
|
|
24
|
+
item,
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// If no order specified, use default server-side order
|
|
29
|
+
const detectionOrder =
|
|
30
|
+
order.length > 0 ? order : ['querystring', 'cookie', 'header'];
|
|
31
|
+
|
|
32
|
+
// Map i18next order to hono order
|
|
33
|
+
const honoOrder = detectionOrder.map(item => {
|
|
34
|
+
// Map 'querystring' to 'querystring', 'cookie' to 'cookie', 'header' to 'header'
|
|
35
|
+
if (item === 'querystring') return 'querystring';
|
|
36
|
+
if (item === 'cookie') return 'cookie';
|
|
37
|
+
if (item === 'header') return 'header';
|
|
38
|
+
return item;
|
|
39
|
+
}) as ('querystring' | 'cookie' | 'header' | 'path')[];
|
|
40
|
+
|
|
41
|
+
// Determine caches option
|
|
42
|
+
// hono languageDetector expects: false | "cookie"[] | undefined
|
|
43
|
+
const caches: false | ['cookie'] | undefined =
|
|
44
|
+
mergedDetection.caches === false
|
|
45
|
+
? false
|
|
46
|
+
: Array.isArray(mergedDetection.caches) &&
|
|
47
|
+
!mergedDetection.caches.includes('cookie')
|
|
48
|
+
? false
|
|
49
|
+
: (['cookie'] as ['cookie']);
|
|
50
|
+
|
|
51
|
+
const cookieMinutes = (mergedDetection as Record<string, unknown>)
|
|
52
|
+
.cookieMinutes;
|
|
53
|
+
const cookieMaxAge =
|
|
54
|
+
typeof cookieMinutes === 'number' && Number.isFinite(cookieMinutes)
|
|
55
|
+
? Math.max(0, Math.floor(cookieMinutes * 60))
|
|
56
|
+
: DEFAULT_I18NEXT_DETECTION_OPTIONS.cookieMinutes * 60;
|
|
57
|
+
|
|
58
|
+
const cookieDomain = (mergedDetection as Record<string, unknown>)
|
|
59
|
+
.cookieDomain;
|
|
60
|
+
const cookieSecure = (mergedDetection as Record<string, unknown>)
|
|
61
|
+
.cookieSecure;
|
|
62
|
+
const cookieHttpOnly = (mergedDetection as Record<string, unknown>)
|
|
63
|
+
.cookieHttpOnly;
|
|
64
|
+
const cookieSameSite = (mergedDetection as Record<string, unknown>)
|
|
65
|
+
.cookieSameSite;
|
|
66
|
+
|
|
67
|
+
const normalizedCookieDomain =
|
|
68
|
+
typeof cookieDomain === 'string' ? cookieDomain : undefined;
|
|
69
|
+
|
|
70
|
+
// Keep cookie defaults aligned with i18next language detector behavior:
|
|
71
|
+
// language cookie should be readable from browser-side detector.
|
|
72
|
+
const cookieOptions = {
|
|
73
|
+
maxAge: cookieMaxAge,
|
|
74
|
+
sameSite:
|
|
75
|
+
cookieSameSite === 'None' || cookieSameSite === 'none'
|
|
76
|
+
? ('None' as const)
|
|
77
|
+
: cookieSameSite === 'Lax' || cookieSameSite === 'lax'
|
|
78
|
+
? ('Lax' as const)
|
|
79
|
+
: ('Strict' as const),
|
|
80
|
+
secure: typeof cookieSecure === 'boolean' ? cookieSecure : false,
|
|
81
|
+
httpOnly: typeof cookieHttpOnly === 'boolean' ? cookieHttpOnly : false,
|
|
82
|
+
...(normalizedCookieDomain ? { domain: normalizedCookieDomain } : {}),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
supportedLanguages: languages.length > 0 ? languages : [fallbackLanguage],
|
|
87
|
+
fallbackLanguage,
|
|
88
|
+
order: honoOrder,
|
|
89
|
+
lookupQueryString:
|
|
90
|
+
mergedDetection.lookupQuerystring ||
|
|
91
|
+
DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupQuerystring ||
|
|
92
|
+
'lng',
|
|
93
|
+
lookupCookie:
|
|
94
|
+
mergedDetection.lookupCookie ||
|
|
95
|
+
DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupCookie ||
|
|
96
|
+
'i18next',
|
|
97
|
+
lookupFromHeaderKey:
|
|
98
|
+
mergedDetection.lookupHeader ||
|
|
99
|
+
DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupHeader ||
|
|
100
|
+
'accept-language',
|
|
101
|
+
...(caches !== undefined && { caches }),
|
|
102
|
+
...(caches !== false && { cookieOptions }),
|
|
103
|
+
ignoreCase: true,
|
|
104
|
+
};
|
|
105
|
+
};
|
package/src/server/index.ts
CHANGED
|
@@ -3,331 +3,26 @@ import * as honoPkg from '@modern-js/server-core/hono';
|
|
|
3
3
|
const { languageDetector } = honoPkg;
|
|
4
4
|
|
|
5
5
|
import type { Context, Next, ServerPlugin } from '@modern-js/server-runtime';
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_I18NEXT_DETECTION_OPTIONS,
|
|
8
|
-
mergeDetectionOptions,
|
|
9
|
-
} from '../runtime/i18n/detection/config.js';
|
|
10
|
-
import type { LanguageDetectorOptions } from '../runtime/i18n/instance';
|
|
11
|
-
import {
|
|
12
|
-
localiseTargetPathname,
|
|
13
|
-
resolveLocalisedUrlsConfig,
|
|
14
|
-
} from '../shared/localisedUrls.js';
|
|
6
|
+
import { resolveLocalisedUrlsConfig } from '../shared/localisedUrls.js';
|
|
15
7
|
import type { LocaleDetectionOptions } from '../shared/type';
|
|
16
8
|
import { getLocaleDetectionOptions } from '../shared/utils.js';
|
|
9
|
+
import { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
|
|
10
|
+
import { convertToHonoLanguageDetectorOptions } from './detectorOptions.js';
|
|
11
|
+
import {
|
|
12
|
+
buildLocalizedUrl,
|
|
13
|
+
createLocaleRedirectResponse,
|
|
14
|
+
getLanguageFromPath,
|
|
15
|
+
isStaticResourceRequest,
|
|
16
|
+
shouldIgnoreRedirect,
|
|
17
|
+
} from './redirectPolicy.js';
|
|
18
|
+
|
|
19
|
+
export { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
|
|
17
20
|
|
|
18
21
|
export interface I18nPluginOptions {
|
|
19
22
|
localeDetection: LocaleDetectionOptions;
|
|
20
23
|
staticRoutePrefixes: string[];
|
|
21
24
|
}
|
|
22
25
|
|
|
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
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Convert i18next detection options to hono languageDetector options
|
|
93
|
-
*/
|
|
94
|
-
const convertToHonoLanguageDetectorOptions = (
|
|
95
|
-
languages: string[],
|
|
96
|
-
fallbackLanguage: string,
|
|
97
|
-
detectionOptions?: LanguageDetectorOptions,
|
|
98
|
-
) => {
|
|
99
|
-
// Merge user detection options with defaults
|
|
100
|
-
const mergedDetection = detectionOptions
|
|
101
|
-
? mergeDetectionOptions(detectionOptions)
|
|
102
|
-
: DEFAULT_I18NEXT_DETECTION_OPTIONS;
|
|
103
|
-
|
|
104
|
-
// Get detection order, excluding 'path' and browser-only detectors
|
|
105
|
-
const order = (mergedDetection.order || []).filter(
|
|
106
|
-
(item: string) =>
|
|
107
|
-
!['path', 'localStorage', 'navigator', 'htmlTag', 'subdomain'].includes(
|
|
108
|
-
item,
|
|
109
|
-
),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
// If no order specified, use default server-side order
|
|
113
|
-
const detectionOrder =
|
|
114
|
-
order.length > 0 ? order : ['querystring', 'cookie', 'header'];
|
|
115
|
-
|
|
116
|
-
// Map i18next order to hono order
|
|
117
|
-
const honoOrder = detectionOrder.map(item => {
|
|
118
|
-
// Map 'querystring' to 'querystring', 'cookie' to 'cookie', 'header' to 'header'
|
|
119
|
-
if (item === 'querystring') return 'querystring';
|
|
120
|
-
if (item === 'cookie') return 'cookie';
|
|
121
|
-
if (item === 'header') return 'header';
|
|
122
|
-
return item;
|
|
123
|
-
}) as ('querystring' | 'cookie' | 'header' | 'path')[];
|
|
124
|
-
|
|
125
|
-
// Determine caches option
|
|
126
|
-
// hono languageDetector expects: false | "cookie"[] | undefined
|
|
127
|
-
const caches: false | ['cookie'] | undefined =
|
|
128
|
-
mergedDetection.caches === false
|
|
129
|
-
? false
|
|
130
|
-
: Array.isArray(mergedDetection.caches) &&
|
|
131
|
-
!mergedDetection.caches.includes('cookie')
|
|
132
|
-
? false
|
|
133
|
-
: (['cookie'] as ['cookie']);
|
|
134
|
-
|
|
135
|
-
const cookieMinutes = (mergedDetection as Record<string, unknown>)
|
|
136
|
-
.cookieMinutes;
|
|
137
|
-
const cookieMaxAge =
|
|
138
|
-
typeof cookieMinutes === 'number' && Number.isFinite(cookieMinutes)
|
|
139
|
-
? Math.max(0, Math.floor(cookieMinutes * 60))
|
|
140
|
-
: DEFAULT_I18NEXT_DETECTION_OPTIONS.cookieMinutes * 60;
|
|
141
|
-
|
|
142
|
-
const cookieDomain = (mergedDetection as Record<string, unknown>)
|
|
143
|
-
.cookieDomain;
|
|
144
|
-
const cookieSecure = (mergedDetection as Record<string, unknown>)
|
|
145
|
-
.cookieSecure;
|
|
146
|
-
const cookieHttpOnly = (mergedDetection as Record<string, unknown>)
|
|
147
|
-
.cookieHttpOnly;
|
|
148
|
-
const cookieSameSite = (mergedDetection as Record<string, unknown>)
|
|
149
|
-
.cookieSameSite;
|
|
150
|
-
|
|
151
|
-
const normalizedCookieDomain =
|
|
152
|
-
typeof cookieDomain === 'string' ? cookieDomain : undefined;
|
|
153
|
-
|
|
154
|
-
// Keep cookie defaults aligned with i18next language detector behavior:
|
|
155
|
-
// language cookie should be readable from browser-side detector.
|
|
156
|
-
const cookieOptions = {
|
|
157
|
-
maxAge: cookieMaxAge,
|
|
158
|
-
sameSite:
|
|
159
|
-
cookieSameSite === 'None' || cookieSameSite === 'none'
|
|
160
|
-
? ('None' as const)
|
|
161
|
-
: cookieSameSite === 'Lax' || cookieSameSite === 'lax'
|
|
162
|
-
? ('Lax' as const)
|
|
163
|
-
: ('Strict' as const),
|
|
164
|
-
secure: typeof cookieSecure === 'boolean' ? cookieSecure : false,
|
|
165
|
-
httpOnly: typeof cookieHttpOnly === 'boolean' ? cookieHttpOnly : false,
|
|
166
|
-
...(normalizedCookieDomain ? { domain: normalizedCookieDomain } : {}),
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
supportedLanguages: languages.length > 0 ? languages : [fallbackLanguage],
|
|
171
|
-
fallbackLanguage,
|
|
172
|
-
order: honoOrder,
|
|
173
|
-
lookupQueryString:
|
|
174
|
-
mergedDetection.lookupQuerystring ||
|
|
175
|
-
DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupQuerystring ||
|
|
176
|
-
'lng',
|
|
177
|
-
lookupCookie:
|
|
178
|
-
mergedDetection.lookupCookie ||
|
|
179
|
-
DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupCookie ||
|
|
180
|
-
'i18next',
|
|
181
|
-
lookupFromHeaderKey:
|
|
182
|
-
mergedDetection.lookupHeader ||
|
|
183
|
-
DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupHeader ||
|
|
184
|
-
'accept-language',
|
|
185
|
-
...(caches !== undefined && { caches }),
|
|
186
|
-
...(caches !== false && { cookieOptions }),
|
|
187
|
-
ignoreCase: true,
|
|
188
|
-
};
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Check if the given pathname should ignore automatic locale redirect
|
|
193
|
-
*/
|
|
194
|
-
const shouldIgnoreRedirect = (
|
|
195
|
-
pathname: string,
|
|
196
|
-
urlPath: string,
|
|
197
|
-
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
198
|
-
): boolean => {
|
|
199
|
-
if (!ignoreRedirectRoutes) {
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Remove urlPath prefix to get remaining path for matching
|
|
204
|
-
const basePath = urlPath.replace('/*', '');
|
|
205
|
-
const remainingPath = pathname.startsWith(basePath)
|
|
206
|
-
? pathname.slice(basePath.length)
|
|
207
|
-
: pathname;
|
|
208
|
-
|
|
209
|
-
// Normalize path (ensure it starts with /)
|
|
210
|
-
const normalizedPath = remainingPath.startsWith('/')
|
|
211
|
-
? remainingPath
|
|
212
|
-
: `/${remainingPath}`;
|
|
213
|
-
|
|
214
|
-
if (typeof ignoreRedirectRoutes === 'function') {
|
|
215
|
-
return ignoreRedirectRoutes(normalizedPath);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Check if pathname matches any of the ignore patterns
|
|
219
|
-
return ignoreRedirectRoutes.some(pattern => {
|
|
220
|
-
// Support both exact match and prefix match
|
|
221
|
-
return (
|
|
222
|
-
normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`)
|
|
223
|
-
);
|
|
224
|
-
});
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Check if the given pathname is a static resource request
|
|
229
|
-
* This includes:
|
|
230
|
-
* 1. Paths matching staticRoutePrefixes (from public directories)
|
|
231
|
-
* 2. Standard static resource paths like /static/, /upload/
|
|
232
|
-
* 3. Paths with language prefix like /en/static/, /zh/static/
|
|
233
|
-
*/
|
|
234
|
-
const isStaticResourceRequest = (
|
|
235
|
-
pathname: string,
|
|
236
|
-
staticRoutePrefixes: string[],
|
|
237
|
-
languages: string[] = [],
|
|
238
|
-
): boolean => {
|
|
239
|
-
// Check against staticRoutePrefixes (from public directories)
|
|
240
|
-
if (
|
|
241
|
-
staticRoutePrefixes.some(
|
|
242
|
-
prefix => pathname.startsWith(`${prefix}/`) || pathname === prefix,
|
|
243
|
-
)
|
|
244
|
-
) {
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Check standard static resource paths
|
|
249
|
-
const standardStaticPrefixes = ['/static/', '/upload/'];
|
|
250
|
-
if (standardStaticPrefixes.some(prefix => pathname.startsWith(prefix))) {
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Check paths with language prefix (e.g., /en/static/, /zh/static/)
|
|
255
|
-
// Remove language prefix if present and check again
|
|
256
|
-
const pathSegments = pathname.split('/').filter(Boolean);
|
|
257
|
-
if (pathSegments.length > 0 && languages.includes(pathSegments[0])) {
|
|
258
|
-
const pathWithoutLang = '/' + pathSegments.slice(1).join('/');
|
|
259
|
-
if (
|
|
260
|
-
standardStaticPrefixes.some(prefix =>
|
|
261
|
-
pathWithoutLang.startsWith(prefix),
|
|
262
|
-
) ||
|
|
263
|
-
staticRoutePrefixes.some(
|
|
264
|
-
prefix =>
|
|
265
|
-
pathWithoutLang.startsWith(`${prefix}/`) ||
|
|
266
|
-
pathWithoutLang === prefix,
|
|
267
|
-
)
|
|
268
|
-
) {
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return false;
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
const getLanguageFromPath = (
|
|
277
|
-
req: any,
|
|
278
|
-
urlPath: string,
|
|
279
|
-
languages: string[],
|
|
280
|
-
): string | null => {
|
|
281
|
-
const url = new URL(req.url, `http://${req.header().host}`);
|
|
282
|
-
const pathname = url.pathname;
|
|
283
|
-
|
|
284
|
-
// Remove urlPath prefix to get remaining path
|
|
285
|
-
// urlPath format is /lang/*, need to remove /lang part
|
|
286
|
-
const basePath = urlPath.replace('/*', '');
|
|
287
|
-
const remainingPath = pathname.startsWith(basePath)
|
|
288
|
-
? pathname.slice(basePath.length)
|
|
289
|
-
: pathname;
|
|
290
|
-
|
|
291
|
-
const segments = remainingPath.split('/').filter(Boolean);
|
|
292
|
-
const firstSegment = segments[0];
|
|
293
|
-
|
|
294
|
-
if (languages.includes(firstSegment)) {
|
|
295
|
-
return firstSegment;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return null;
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
const buildLocalizedUrl = (
|
|
302
|
-
req: any,
|
|
303
|
-
urlPath: string,
|
|
304
|
-
language: string,
|
|
305
|
-
languages: string[],
|
|
306
|
-
localisedUrls?: LocaleDetectionOptions['localisedUrls'],
|
|
307
|
-
): string => {
|
|
308
|
-
const url = new URL(req.url);
|
|
309
|
-
const pathname = url.pathname;
|
|
310
|
-
|
|
311
|
-
// Remove urlPath prefix to get remaining path
|
|
312
|
-
const basePath = urlPath.replace('/*', '');
|
|
313
|
-
const remainingPath = pathname.startsWith(basePath)
|
|
314
|
-
? pathname.slice(basePath.length)
|
|
315
|
-
: pathname;
|
|
316
|
-
|
|
317
|
-
const newPathname = localiseTargetPathname(
|
|
318
|
-
remainingPath,
|
|
319
|
-
language,
|
|
320
|
-
languages,
|
|
321
|
-
localisedUrls,
|
|
322
|
-
);
|
|
323
|
-
// Handle root path case to avoid double slashes like //en
|
|
324
|
-
const suffix = `${url.search}${url.hash}`;
|
|
325
|
-
const localizedUrl =
|
|
326
|
-
basePath === '/' ? newPathname + suffix : basePath + newPathname + suffix;
|
|
327
|
-
|
|
328
|
-
return localizedUrl;
|
|
329
|
-
};
|
|
330
|
-
|
|
331
26
|
export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
332
27
|
name: '@modern-js/plugin-i18n/server',
|
|
333
28
|
setup: api => {
|
|
@@ -475,7 +170,7 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
475
170
|
languages,
|
|
476
171
|
localisedUrls,
|
|
477
172
|
);
|
|
478
|
-
return
|
|
173
|
+
return createLocaleRedirectResponse(localizedUrl);
|
|
479
174
|
}
|
|
480
175
|
const localisedUrlsConfig =
|
|
481
176
|
resolveLocalisedUrlsConfig(localisedUrls);
|
|
@@ -488,7 +183,7 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
488
183
|
localisedUrls,
|
|
489
184
|
);
|
|
490
185
|
if (expectedUrl !== `${pathname}${url.search}${url.hash}`) {
|
|
491
|
-
return
|
|
186
|
+
return createLocaleRedirectResponse(expectedUrl);
|
|
492
187
|
}
|
|
493
188
|
}
|
|
494
189
|
await next();
|