@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.36 → 3.5.0-ultramodern.37
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/locales.js +15 -15
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
- package/dist/cjs/runtime/i18n/detection/cache.js +3 -2
- package/dist/cjs/runtime/i18n/instance.js +7 -7
- package/dist/cjs/runtime/utils.js +1 -9
- package/dist/cjs/server/redirectPolicy.js +14 -20
- package/dist/cjs/shared/detection.js +2 -104
- package/dist/cjs/shared/localisedUrls/index.js +16 -0
- package/dist/cjs/shared/localisedUrls/pathname.js +5 -2
- package/dist/cjs/shared/localisedUrls/redirect.js +95 -0
- package/dist/cjs/shared/localisedUrls/resolve.js +29 -32
- package/dist/cjs/shared/localisedUrls.js +15 -0
- package/dist/cjs/shared/utils.js +2 -8
- package/dist/esm/cli/locales.mjs +13 -13
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
- package/dist/esm/runtime/i18n/detection/cache.mjs +3 -2
- package/dist/esm/runtime/i18n/instance.mjs +8 -5
- package/dist/esm/runtime/utils.mjs +2 -10
- package/dist/esm/server/index.mjs +2 -2
- package/dist/esm/server/redirectPolicy.mjs +15 -21
- package/dist/esm/shared/detection.mjs +1 -71
- package/dist/esm/shared/localisedUrls/index.mjs +2 -1
- package/dist/esm/shared/localisedUrls/pathname.mjs +1 -1
- package/dist/esm/shared/localisedUrls/redirect.mjs +48 -0
- package/dist/esm/shared/localisedUrls/resolve.mjs +29 -32
- package/dist/esm/shared/localisedUrls.mjs +1 -1
- package/dist/esm/shared/utils.mjs +1 -1
- package/dist/esm-node/cli/locales.mjs +13 -13
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
- package/dist/esm-node/runtime/i18n/detection/cache.mjs +3 -2
- package/dist/esm-node/runtime/i18n/instance.mjs +8 -5
- package/dist/esm-node/runtime/utils.mjs +2 -10
- package/dist/esm-node/server/index.mjs +2 -2
- package/dist/esm-node/server/redirectPolicy.mjs +15 -21
- package/dist/esm-node/shared/detection.mjs +1 -71
- package/dist/esm-node/shared/localisedUrls/index.mjs +2 -1
- package/dist/esm-node/shared/localisedUrls/pathname.mjs +1 -1
- package/dist/esm-node/shared/localisedUrls/redirect.mjs +49 -0
- package/dist/esm-node/shared/localisedUrls/resolve.mjs +29 -32
- package/dist/esm-node/shared/localisedUrls.mjs +1 -1
- package/dist/esm-node/shared/utils.mjs +1 -1
- package/dist/types/cli/locales.d.ts +1 -1
- package/dist/types/runtime/context.d.ts +4 -3
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
- package/dist/types/runtime/i18n/detection/cache.d.ts +1 -1
- package/dist/types/runtime/i18n/detection/detector.d.ts +2 -2
- 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/types.d.ts +8 -5
- package/dist/types/runtime/i18n/instance.d.ts +28 -26
- package/dist/types/runtime/i18n/utils.d.ts +14 -5
- package/dist/types/runtime/pluginSetup.d.ts +1 -1
- package/dist/types/runtime/providerComposition.d.ts +2 -1
- package/dist/types/runtime/routerAdapter.d.ts +7 -6
- package/dist/types/server/redirectPolicy.d.ts +14 -8
- package/dist/types/shared/detection.d.ts +1 -11
- package/dist/types/shared/localisedUrls/index.d.ts +3 -1
- package/dist/types/shared/localisedUrls/pathname.d.ts +1 -0
- package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
- package/dist/types/shared/localisedUrls/resolve.d.ts +2 -3
- package/dist/types/shared/localisedUrls.d.ts +2 -2
- package/dist/types/shared/utils.d.ts +0 -2
- package/package.json +16 -18
- package/src/cli/index.ts +2 -2
- package/src/cli/locales.ts +3 -3
- package/src/runtime/context.tsx +3 -3
- package/src/runtime/contextHelpers.ts +3 -3
- package/src/runtime/hooks.ts +10 -2
- 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 +23 -9
- package/src/runtime/i18n/detection/detector.ts +15 -6
- package/src/runtime/i18n/detection/middleware.node.ts +26 -7
- package/src/runtime/i18n/detection/middleware.ts +15 -7
- package/src/runtime/i18n/detection/types.ts +13 -5
- package/src/runtime/i18n/instance.ts +56 -31
- package/src/runtime/i18n/utils.ts +18 -9
- package/src/runtime/pluginSetup.ts +1 -1
- package/src/runtime/providerComposition.tsx +1 -1
- package/src/runtime/routerAdapter.tsx +6 -6
- package/src/runtime/utils.ts +5 -29
- package/src/server/redirectPolicy.ts +51 -76
- package/src/shared/detection.ts +1 -1
- package/src/shared/localisedUrls/index.ts +12 -1
- package/src/shared/localisedUrls/pathname.ts +4 -1
- package/src/shared/localisedUrls/redirect.ts +91 -0
- package/src/shared/localisedUrls/resolve.ts +91 -74
- package/src/shared/localisedUrls.ts +6 -0
- package/src/shared/utils.ts +2 -2
- package/tests/redirectPolicy.test.ts +43 -0
|
@@ -1,106 +1,86 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isDefaultLocaleRedirectSkipPath,
|
|
3
|
+
localiseTargetPathname,
|
|
4
|
+
matchesPathPrefix,
|
|
5
|
+
shouldSkipLocaleRedirect,
|
|
6
|
+
stripLanguagePrefix,
|
|
7
|
+
} from '../shared/localisedUrls.js';
|
|
2
8
|
import type { LocaleDetectionOptions } from '../shared/type';
|
|
3
9
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
11
|
-
): boolean => {
|
|
12
|
-
if (!ignoreRedirectRoutes) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
10
|
+
interface LocaleRedirectRequest {
|
|
11
|
+
url: string;
|
|
12
|
+
header: () => {
|
|
13
|
+
host?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
const stripUrlPathPrefix = (pathname: string, urlPath: string): string => {
|
|
17
18
|
const basePath = urlPath.replace('/*', '');
|
|
19
|
+
|
|
20
|
+
if (!basePath || basePath === '/') {
|
|
21
|
+
return pathname;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
const remainingPath = pathname.startsWith(basePath)
|
|
19
25
|
? pathname.slice(basePath.length)
|
|
20
26
|
: pathname;
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
? remainingPath
|
|
25
|
-
: `/${remainingPath}`;
|
|
28
|
+
return remainingPath || '/';
|
|
29
|
+
};
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Check if pathname should ignore automatic locale redirect.
|
|
33
|
+
*/
|
|
34
|
+
export const shouldIgnoreRedirect = (
|
|
35
|
+
pathname: string,
|
|
36
|
+
urlPath: string,
|
|
37
|
+
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
38
|
+
): boolean => {
|
|
39
|
+
const remainingPath = stripUrlPathPrefix(pathname, urlPath);
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
return ignoreRedirectRoutes.some(pattern => {
|
|
33
|
-
// Support both exact match and prefix match
|
|
34
|
-
return (
|
|
35
|
-
normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`)
|
|
36
|
-
);
|
|
37
|
-
});
|
|
41
|
+
return shouldSkipLocaleRedirect(remainingPath, [], ignoreRedirectRoutes);
|
|
38
42
|
};
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
|
-
* Check if
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* 3. Paths with language prefix like /en/static/, /zh/static/
|
|
45
|
+
* Check if pathname is a static or federation resource request.
|
|
46
|
+
*
|
|
47
|
+
* This includes configured staticRoutePrefixes, the shared default skip policy
|
|
48
|
+
* from ADR-0002, and language-prefixed variants such as /en/static/app.js.
|
|
46
49
|
*/
|
|
47
50
|
export const isStaticResourceRequest = (
|
|
48
51
|
pathname: string,
|
|
49
52
|
staticRoutePrefixes: string[],
|
|
50
53
|
languages: string[] = [],
|
|
51
54
|
): boolean => {
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
staticRoutePrefixes.some(
|
|
55
|
-
prefix => pathname.startsWith(`${prefix}/`) || pathname === prefix,
|
|
56
|
-
)
|
|
57
|
-
) {
|
|
55
|
+
if (isDefaultLocaleRedirectSkipPath(pathname, languages)) {
|
|
58
56
|
return true;
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
const matchesStaticRoutePrefix = (targetPathname: string): boolean =>
|
|
60
|
+
staticRoutePrefixes.some(prefix =>
|
|
61
|
+
matchesPathPrefix(targetPathname, prefix),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (matchesStaticRoutePrefix(pathname)) {
|
|
64
65
|
return true;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
// Remove language prefix if present and check again
|
|
69
|
-
const pathSegments = pathname.split('/').filter(Boolean);
|
|
70
|
-
if (pathSegments.length > 0 && languages.includes(pathSegments[0])) {
|
|
71
|
-
const pathWithoutLang = '/' + pathSegments.slice(1).join('/');
|
|
72
|
-
if (
|
|
73
|
-
standardStaticPrefixes.some(prefix =>
|
|
74
|
-
pathWithoutLang.startsWith(prefix),
|
|
75
|
-
) ||
|
|
76
|
-
staticRoutePrefixes.some(
|
|
77
|
-
prefix =>
|
|
78
|
-
pathWithoutLang.startsWith(`${prefix}/`) ||
|
|
79
|
-
pathWithoutLang === prefix,
|
|
80
|
-
)
|
|
81
|
-
) {
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
68
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
85
69
|
|
|
86
|
-
return
|
|
70
|
+
return (
|
|
71
|
+
pathWithoutLanguage !== pathname &&
|
|
72
|
+
matchesStaticRoutePrefix(pathWithoutLanguage)
|
|
73
|
+
);
|
|
87
74
|
};
|
|
88
75
|
|
|
89
76
|
export const getLanguageFromPath = (
|
|
90
|
-
req:
|
|
77
|
+
req: LocaleRedirectRequest,
|
|
91
78
|
urlPath: string,
|
|
92
79
|
languages: string[],
|
|
93
80
|
): string | null => {
|
|
94
81
|
const url = new URL(req.url, `http://${req.header().host}`);
|
|
95
82
|
const pathname = url.pathname;
|
|
96
|
-
|
|
97
|
-
// Remove urlPath prefix to get remaining path
|
|
98
|
-
// urlPath format is /lang/*, need to remove /lang part
|
|
99
|
-
const basePath = urlPath.replace('/*', '');
|
|
100
|
-
const remainingPath = pathname.startsWith(basePath)
|
|
101
|
-
? pathname.slice(basePath.length)
|
|
102
|
-
: pathname;
|
|
103
|
-
|
|
83
|
+
const remainingPath = stripUrlPathPrefix(pathname, urlPath);
|
|
104
84
|
const segments = remainingPath.split('/').filter(Boolean);
|
|
105
85
|
const firstSegment = segments[0];
|
|
106
86
|
|
|
@@ -112,7 +92,7 @@ export const getLanguageFromPath = (
|
|
|
112
92
|
};
|
|
113
93
|
|
|
114
94
|
export const buildLocalizedUrl = (
|
|
115
|
-
req:
|
|
95
|
+
req: LocaleRedirectRequest,
|
|
116
96
|
urlPath: string,
|
|
117
97
|
language: string,
|
|
118
98
|
languages: string[],
|
|
@@ -120,12 +100,8 @@ export const buildLocalizedUrl = (
|
|
|
120
100
|
): string => {
|
|
121
101
|
const url = new URL(req.url);
|
|
122
102
|
const pathname = url.pathname;
|
|
123
|
-
|
|
124
|
-
// Remove urlPath prefix to get remaining path
|
|
125
103
|
const basePath = urlPath.replace('/*', '');
|
|
126
|
-
const remainingPath = pathname
|
|
127
|
-
? pathname.slice(basePath.length)
|
|
128
|
-
: pathname;
|
|
104
|
+
const remainingPath = stripUrlPathPrefix(pathname, urlPath);
|
|
129
105
|
|
|
130
106
|
const newPathname = localiseTargetPathname(
|
|
131
107
|
remainingPath,
|
|
@@ -133,7 +109,6 @@ export const buildLocalizedUrl = (
|
|
|
133
109
|
languages,
|
|
134
110
|
localisedUrls,
|
|
135
111
|
);
|
|
136
|
-
// Handle root path case to avoid double slashes like //en
|
|
137
112
|
const suffix = `${url.search}${url.hash}`;
|
|
138
113
|
const localizedUrl =
|
|
139
114
|
basePath === '/' ? newPathname + suffix : basePath + newPathname + suffix;
|
package/src/shared/detection.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { LanguageDetectorOptions } from '../runtime/i18n/instance';
|
|
|
8
8
|
* Detect language from request using the same detection logic as i18next
|
|
9
9
|
* This ensures consistency between server-side and client-side detection
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
function detectLanguageFromRequest(
|
|
12
12
|
req: {
|
|
13
13
|
url: string;
|
|
14
14
|
headers:
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
export { resolveLocalisedUrlsConfig } from './config';
|
|
2
2
|
export { normalisePathname, normalisePathPattern } from './normalise';
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
canonicalTargetPathname,
|
|
5
|
+
localiseTargetPathname,
|
|
6
|
+
stripLanguagePrefix,
|
|
7
|
+
} from './pathname';
|
|
4
8
|
export { buildPathFromPattern, matchPathPattern } from './patterns';
|
|
9
|
+
export type { LocaleRedirectSkipRule } from './redirect';
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_LOCALE_REDIRECT_SKIP_RULES,
|
|
12
|
+
isDefaultLocaleRedirectSkipPath,
|
|
13
|
+
matchesPathPrefix,
|
|
14
|
+
shouldSkipLocaleRedirect,
|
|
15
|
+
} from './redirect';
|
|
5
16
|
export { resolveCanonicalLocalisedPath, resolveLocalisedPath } from './resolve';
|
|
6
17
|
export { applyLocalisedUrlsToRoutes, validateLocalisedUrls } from './routes';
|
|
7
18
|
export type {
|
|
@@ -4,7 +4,10 @@ import { resolveCanonicalLocalisedPath, resolveLocalisedPath } from './resolve';
|
|
|
4
4
|
|
|
5
5
|
import type { LocalisedUrlsOption } from './types';
|
|
6
6
|
|
|
7
|
-
const stripLanguagePrefix = (
|
|
7
|
+
export const stripLanguagePrefix = (
|
|
8
|
+
pathname: string,
|
|
9
|
+
languages: string[],
|
|
10
|
+
): string => {
|
|
8
11
|
const segments = pathname.split('/').filter(Boolean);
|
|
9
12
|
const firstSegment = segments[0]?.toLowerCase();
|
|
10
13
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { normalisePathname } from './normalise';
|
|
2
|
+
import { stripLanguagePrefix } from './pathname';
|
|
3
|
+
|
|
4
|
+
export type LocaleRedirectSkipRule =
|
|
5
|
+
| {
|
|
6
|
+
type: 'exact';
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
| {
|
|
10
|
+
type: 'prefix';
|
|
11
|
+
path: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_LOCALE_REDIRECT_SKIP_RULES: readonly LocaleRedirectSkipRule[] =
|
|
15
|
+
[
|
|
16
|
+
{ type: 'exact', path: '/mf-manifest.json' },
|
|
17
|
+
{ type: 'exact', path: '/mf-stats.json' },
|
|
18
|
+
{ type: 'exact', path: '/remoteEntry.js' },
|
|
19
|
+
{ type: 'prefix', path: '/static/' },
|
|
20
|
+
{ type: 'prefix', path: '/upload/' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const stripTrailingSlash = (pathname: string): string =>
|
|
24
|
+
pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;
|
|
25
|
+
|
|
26
|
+
export const matchesPathPrefix = (
|
|
27
|
+
pathname: string,
|
|
28
|
+
prefix: string,
|
|
29
|
+
): boolean => {
|
|
30
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
31
|
+
const normalizedPrefix = stripTrailingSlash(normalisePathname(prefix));
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
normalizedPathname === normalizedPrefix ||
|
|
35
|
+
normalizedPathname.startsWith(`${normalizedPrefix}/`)
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const matchesSkipRule = (
|
|
40
|
+
pathname: string,
|
|
41
|
+
rule: LocaleRedirectSkipRule,
|
|
42
|
+
): boolean => {
|
|
43
|
+
if (rule.type === 'exact') {
|
|
44
|
+
return normalisePathname(pathname) === normalisePathname(rule.path);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return matchesPathPrefix(pathname, rule.path);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const isDefaultLocaleRedirectSkipPath = (
|
|
51
|
+
pathname: string,
|
|
52
|
+
languages: string[] = [],
|
|
53
|
+
): boolean => {
|
|
54
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
55
|
+
const pathWithoutLanguage = stripLanguagePrefix(
|
|
56
|
+
normalizedPathname,
|
|
57
|
+
languages,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return DEFAULT_LOCALE_REDIRECT_SKIP_RULES.some(rule =>
|
|
61
|
+
matchesSkipRule(pathWithoutLanguage, rule),
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const shouldSkipLocaleRedirect = (
|
|
66
|
+
pathname: string,
|
|
67
|
+
languages: string[] = [],
|
|
68
|
+
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
69
|
+
): boolean => {
|
|
70
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
71
|
+
const pathWithoutLanguage = stripLanguagePrefix(
|
|
72
|
+
normalizedPathname,
|
|
73
|
+
languages,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (isDefaultLocaleRedirectSkipPath(pathWithoutLanguage)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!ignoreRedirectRoutes) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (typeof ignoreRedirectRoutes === 'function') {
|
|
85
|
+
return ignoreRedirectRoutes(pathWithoutLanguage);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return ignoreRedirectRoutes.some(pattern =>
|
|
89
|
+
matchesPathPrefix(pathWithoutLanguage, pattern),
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -8,17 +8,25 @@ import {
|
|
|
8
8
|
|
|
9
9
|
import type { LocalisedUrlsMap } from './types';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
): string => {
|
|
17
|
-
const normalizedPathname = normalisePathname(pathname);
|
|
11
|
+
type CanonicalCandidate = {
|
|
12
|
+
pattern: string;
|
|
13
|
+
canonicalPattern: string;
|
|
14
|
+
localisedUrlEntry: LocalisedUrlsMap[string];
|
|
15
|
+
};
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
type ResolveLocalisedUrlPathOptions =
|
|
18
|
+
| {
|
|
19
|
+
mode: 'localised';
|
|
20
|
+
targetLanguage: string;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
mode: 'canonical';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const getCanonicalCandidates = (
|
|
27
|
+
localisedUrls: LocalisedUrlsMap,
|
|
28
|
+
): CanonicalCandidate[] =>
|
|
29
|
+
sortPatternsBySpecificity(
|
|
22
30
|
Object.entries(localisedUrls).map(
|
|
23
31
|
([canonicalPattern, localisedUrlEntry]) => ({
|
|
24
32
|
pattern: canonicalPattern,
|
|
@@ -28,91 +36,100 @@ export const resolveLocalisedPath = (
|
|
|
28
36
|
),
|
|
29
37
|
);
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
33
|
-
if (!targetPattern) {
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const params = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
38
|
-
if (params) {
|
|
39
|
-
return buildPathFromPattern(targetPattern, params);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const localisedCandidates = sortPatternsBySpecificity(
|
|
44
|
-
Object.values(localisedUrls).flatMap(localisedUrlEntry => {
|
|
45
|
-
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
46
|
-
if (!targetPattern) {
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return languages
|
|
51
|
-
.map(language => localisedUrlEntry[language])
|
|
52
|
-
.filter((sourcePattern): sourcePattern is string =>
|
|
53
|
-
Boolean(sourcePattern),
|
|
54
|
-
)
|
|
55
|
-
.map(sourcePattern => ({
|
|
56
|
-
pattern: sourcePattern,
|
|
57
|
-
sourcePattern,
|
|
58
|
-
targetPattern,
|
|
59
|
-
}));
|
|
60
|
-
}),
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
for (const { sourcePattern, targetPattern } of localisedCandidates) {
|
|
64
|
-
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
65
|
-
if (params) {
|
|
66
|
-
return buildPathFromPattern(targetPattern, params);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return normalizedPathname;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Reverse-map a language-specific pathname (without language prefix) back to
|
|
75
|
-
* the canonical, language-agnostic path: localized slug patterns are matched
|
|
76
|
-
* against every language variant and rebuilt from the canonical map key.
|
|
77
|
-
*/
|
|
78
|
-
export const resolveCanonicalLocalisedPath = (
|
|
39
|
+
const resolveLocalisedUrlPath = (
|
|
79
40
|
pathname: string,
|
|
80
41
|
languages: string[],
|
|
81
42
|
localisedUrls: LocalisedUrlsMap,
|
|
43
|
+
options: ResolveLocalisedUrlPathOptions,
|
|
82
44
|
): string => {
|
|
83
45
|
const normalizedPathname = normalisePathname(pathname);
|
|
84
|
-
|
|
85
|
-
const canonicalCandidates = sortPatternsBySpecificity(
|
|
86
|
-
Object.entries(localisedUrls).map(
|
|
87
|
-
([canonicalPattern, localisedUrlEntry]) => ({
|
|
88
|
-
pattern: canonicalPattern,
|
|
89
|
-
canonicalPattern,
|
|
90
|
-
localisedUrlEntry,
|
|
91
|
-
}),
|
|
92
|
-
),
|
|
93
|
-
);
|
|
46
|
+
const canonicalCandidates = getCanonicalCandidates(localisedUrls);
|
|
94
47
|
|
|
95
48
|
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates) {
|
|
49
|
+
const targetPattern =
|
|
50
|
+
options.mode === 'localised'
|
|
51
|
+
? localisedUrlEntry[options.targetLanguage]
|
|
52
|
+
: canonicalPattern;
|
|
53
|
+
|
|
54
|
+
if (!targetPattern) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
96
58
|
const canonicalParams = matchPathPattern(
|
|
97
59
|
normalizedPathname,
|
|
98
60
|
canonicalPattern,
|
|
99
61
|
);
|
|
100
62
|
if (canonicalParams) {
|
|
101
|
-
return buildPathFromPattern(
|
|
63
|
+
return buildPathFromPattern(targetPattern, canonicalParams);
|
|
102
64
|
}
|
|
103
65
|
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
66
|
+
if (options.mode === 'canonical') {
|
|
67
|
+
for (const language of languages) {
|
|
68
|
+
const sourcePattern = localisedUrlEntry[language];
|
|
69
|
+
if (!sourcePattern) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
74
|
+
if (params) {
|
|
75
|
+
return buildPathFromPattern(canonicalPattern, params);
|
|
76
|
+
}
|
|
108
77
|
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
109
80
|
|
|
81
|
+
if (options.mode === 'localised') {
|
|
82
|
+
const localisedCandidates = sortPatternsBySpecificity(
|
|
83
|
+
Object.values(localisedUrls).flatMap(localisedUrlEntry => {
|
|
84
|
+
const targetPattern = localisedUrlEntry[options.targetLanguage];
|
|
85
|
+
if (!targetPattern) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return languages
|
|
90
|
+
.map(language => localisedUrlEntry[language])
|
|
91
|
+
.filter((sourcePattern): sourcePattern is string =>
|
|
92
|
+
Boolean(sourcePattern),
|
|
93
|
+
)
|
|
94
|
+
.map(sourcePattern => ({
|
|
95
|
+
pattern: sourcePattern,
|
|
96
|
+
sourcePattern,
|
|
97
|
+
targetPattern,
|
|
98
|
+
}));
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
for (const { sourcePattern, targetPattern } of localisedCandidates) {
|
|
110
103
|
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
111
104
|
if (params) {
|
|
112
|
-
return buildPathFromPattern(
|
|
105
|
+
return buildPathFromPattern(targetPattern, params);
|
|
113
106
|
}
|
|
114
107
|
}
|
|
115
108
|
}
|
|
116
109
|
|
|
117
110
|
return normalizedPathname;
|
|
118
111
|
};
|
|
112
|
+
|
|
113
|
+
export const resolveLocalisedPath = (
|
|
114
|
+
pathname: string,
|
|
115
|
+
targetLanguage: string,
|
|
116
|
+
languages: string[],
|
|
117
|
+
localisedUrls: LocalisedUrlsMap,
|
|
118
|
+
): string =>
|
|
119
|
+
resolveLocalisedUrlPath(pathname, languages, localisedUrls, {
|
|
120
|
+
mode: 'localised',
|
|
121
|
+
targetLanguage,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Reverse-map language-specific pathnames back to language-agnostic
|
|
126
|
+
* LocalisedUrlsMap keys.
|
|
127
|
+
*/
|
|
128
|
+
export const resolveCanonicalLocalisedPath = (
|
|
129
|
+
pathname: string,
|
|
130
|
+
languages: string[],
|
|
131
|
+
localisedUrls: LocalisedUrlsMap,
|
|
132
|
+
): string =>
|
|
133
|
+
resolveLocalisedUrlPath(pathname, languages, localisedUrls, {
|
|
134
|
+
mode: 'canonical',
|
|
135
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type {
|
|
2
|
+
LocaleRedirectSkipRule,
|
|
2
3
|
LocalisedRoute,
|
|
3
4
|
LocalisedUrlPathMap,
|
|
4
5
|
LocalisedUrlsMap,
|
|
@@ -9,12 +10,17 @@ export {
|
|
|
9
10
|
applyLocalisedUrlsToRoutes,
|
|
10
11
|
buildPathFromPattern,
|
|
11
12
|
canonicalTargetPathname,
|
|
13
|
+
DEFAULT_LOCALE_REDIRECT_SKIP_RULES,
|
|
14
|
+
isDefaultLocaleRedirectSkipPath,
|
|
12
15
|
localiseTargetPathname,
|
|
16
|
+
matchesPathPrefix,
|
|
13
17
|
matchPathPattern,
|
|
14
18
|
normalisePathname,
|
|
15
19
|
normalisePathPattern,
|
|
16
20
|
resolveCanonicalLocalisedPath,
|
|
17
21
|
resolveLocalisedPath,
|
|
18
22
|
resolveLocalisedUrlsConfig,
|
|
23
|
+
shouldSkipLocaleRedirect,
|
|
24
|
+
stripLanguagePrefix,
|
|
19
25
|
validateLocalisedUrls,
|
|
20
26
|
} from './localisedUrls/index';
|
package/src/shared/utils.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
LocaleDetectionOptions,
|
|
6
6
|
} from './type';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function getEntryConfig<T extends Record<string, any>>(
|
|
9
9
|
entryName: string,
|
|
10
10
|
config: T,
|
|
11
11
|
entryKey: string,
|
|
@@ -16,7 +16,7 @@ export function getEntryConfig<T extends Record<string, any>>(
|
|
|
16
16
|
return entryConfigMap?.[entryName];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
function removeEntryConfigKey<T extends Record<string, any>>(
|
|
20
20
|
config: T,
|
|
21
21
|
entryKey: string,
|
|
22
22
|
): Omit<T, typeof entryKey> {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from '@rstest/core';
|
|
2
|
+
import { shouldIgnoreRedirect as shouldIgnoreRuntimeRedirect } from '../src/runtime/utils';
|
|
3
|
+
import {
|
|
4
|
+
isStaticResourceRequest,
|
|
5
|
+
shouldIgnoreRedirect as shouldIgnoreServerRedirect,
|
|
6
|
+
} from '../src/server/redirectPolicy';
|
|
7
|
+
|
|
8
|
+
const languages = ['en', 'cs'];
|
|
9
|
+
|
|
10
|
+
const defaultSkippedPaths = [
|
|
11
|
+
'/mf-manifest.json',
|
|
12
|
+
'/mf-stats.json',
|
|
13
|
+
'/remoteEntry.js',
|
|
14
|
+
'/static/app.js',
|
|
15
|
+
'/upload/avatar.png',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
describe('locale redirect default skip policy', () => {
|
|
19
|
+
test('skips ADR-0002 Module Federation and static endpoints on the server', () => {
|
|
20
|
+
for (const pathname of defaultSkippedPaths) {
|
|
21
|
+
expect(isStaticResourceRequest(pathname, [], languages)).toBe(true);
|
|
22
|
+
expect(shouldIgnoreServerRedirect(pathname, '/', undefined)).toBe(true);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('skips language-prefixed static and upload endpoints', () => {
|
|
27
|
+
expect(isStaticResourceRequest('/cs/static/app.js', [], languages)).toBe(
|
|
28
|
+
true,
|
|
29
|
+
);
|
|
30
|
+
expect(
|
|
31
|
+
isStaticResourceRequest('/en/upload/avatar.png', [], languages),
|
|
32
|
+
).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('uses the same default skip policy in runtime redirects', () => {
|
|
36
|
+
for (const pathname of defaultSkippedPaths) {
|
|
37
|
+
expect(shouldIgnoreRuntimeRedirect(pathname, languages)).toBe(true);
|
|
38
|
+
expect(shouldIgnoreRuntimeRedirect(`/cs${pathname}`, languages)).toBe(
|
|
39
|
+
true,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|