@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.30 → 3.5.0-ultramodern.31
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/runtime/Link.js +8 -89
- package/dist/cjs/runtime/context.js +18 -82
- package/dist/cjs/runtime/contextHelpers.js +137 -0
- package/dist/cjs/runtime/core.js +29 -132
- package/dist/cjs/runtime/i18n/detection/cache.js +149 -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/linkHelpers.js +148 -0
- package/dist/cjs/runtime/pluginSetup.js +102 -0
- package/dist/cjs/runtime/providerComposition.js +113 -0
- package/dist/cjs/runtime/reactI18next.js +43 -0
- package/dist/cjs/server/apiPrefix.js +72 -0
- package/dist/cjs/server/detectorOptions.js +97 -0
- package/dist/cjs/server/index.js +17 -152
- package/dist/cjs/server/redirectPolicy.js +102 -0
- package/dist/cjs/shared/localisedUrls/config.js +49 -0
- package/dist/cjs/shared/localisedUrls/index.js +75 -0
- package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
- package/dist/cjs/shared/localisedUrls/pathname.js +65 -0
- package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
- package/dist/cjs/shared/localisedUrls/resolve.js +91 -0
- package/dist/cjs/shared/localisedUrls/routes.js +129 -0
- package/dist/cjs/shared/localisedUrls/types.js +18 -0
- package/dist/cjs/shared/localisedUrls.js +13 -294
- package/dist/esm/cli/index.mjs +2 -1
- package/dist/esm/runtime/Link.mjs +1 -82
- package/dist/esm/runtime/context.mjs +18 -82
- package/dist/esm/runtime/contextHelpers.mjs +84 -0
- package/dist/esm/runtime/core.mjs +30 -133
- package/dist/esm/runtime/i18n/detection/cache.mjs +102 -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/linkHelpers.mjs +98 -0
- package/dist/esm/runtime/pluginSetup.mjs +64 -0
- package/dist/esm/runtime/providerComposition.mjs +75 -0
- package/dist/esm/runtime/reactI18next.mjs +5 -0
- package/dist/esm/server/apiPrefix.mjs +31 -0
- package/dist/esm/server/detectorOptions.mjs +59 -0
- package/dist/esm/server/index.mjs +4 -139
- package/dist/esm/server/redirectPolicy.mjs +52 -0
- package/dist/esm/shared/localisedUrls/config.mjs +11 -0
- package/dist/esm/shared/localisedUrls/index.mjs +6 -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/resolve.mjs +50 -0
- package/dist/esm/shared/localisedUrls/routes.mjs +88 -0
- package/dist/esm/shared/localisedUrls/types.mjs +0 -0
- package/dist/esm/shared/localisedUrls.mjs +1 -283
- package/dist/esm-node/cli/index.mjs +2 -1
- package/dist/esm-node/runtime/Link.mjs +1 -82
- package/dist/esm-node/runtime/context.mjs +18 -82
- package/dist/esm-node/runtime/contextHelpers.mjs +85 -0
- package/dist/esm-node/runtime/core.mjs +30 -133
- package/dist/esm-node/runtime/i18n/detection/cache.mjs +103 -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/linkHelpers.mjs +99 -0
- package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
- package/dist/esm-node/runtime/providerComposition.mjs +76 -0
- package/dist/esm-node/runtime/reactI18next.mjs +6 -0
- 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 +4 -139
- package/dist/esm-node/server/redirectPolicy.mjs +53 -0
- package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
- package/dist/esm-node/shared/localisedUrls/index.mjs +7 -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/resolve.mjs +51 -0
- package/dist/esm-node/shared/localisedUrls/routes.mjs +89 -0
- package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
- package/dist/esm-node/shared/localisedUrls.mjs +1 -283
- package/dist/types/runtime/Link.d.ts +3 -7
- package/dist/types/runtime/context.d.ts +2 -2
- package/dist/types/runtime/contextHelpers.d.ts +28 -0
- package/dist/types/runtime/core.d.ts +3 -7
- 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/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 +29 -0
- package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
- 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 +15 -0
- package/dist/types/runtime/reactI18next.d.ts +7 -0
- 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 +16 -0
- package/dist/types/shared/localisedUrls/config.d.ts +9 -0
- package/dist/types/shared/localisedUrls/index.d.ts +7 -0
- package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
- package/dist/types/shared/localisedUrls/pathname.d.ts +3 -0
- package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
- package/dist/types/shared/localisedUrls/resolve.d.ts +8 -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 -42
- package/package.json +10 -10
- package/src/cli/index.ts +3 -2
- package/src/runtime/Link.tsx +11 -138
- package/src/runtime/context.tsx +42 -196
- package/src/runtime/contextHelpers.ts +232 -0
- package/src/runtime/core.tsx +42 -273
- package/src/runtime/i18n/detection/cache.ts +175 -0
- package/src/runtime/i18n/detection/detector.ts +157 -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/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 +33 -0
- package/src/runtime/i18n/react-i18next.ts +1 -6
- package/src/runtime/linkHelpers.ts +174 -0
- package/src/runtime/pluginSetup.ts +189 -0
- package/src/runtime/providerComposition.tsx +142 -0
- package/src/runtime/reactI18next.ts +20 -0
- package/src/server/apiPrefix.ts +67 -0
- package/src/server/detectorOptions.ts +105 -0
- package/src/server/index.ts +12 -327
- package/src/server/redirectPolicy.ts +152 -0
- package/src/shared/localisedUrls/config.ts +18 -0
- package/src/shared/localisedUrls/index.ts +13 -0
- package/src/shared/localisedUrls/normalise.ts +67 -0
- package/src/shared/localisedUrls/pathname.ts +57 -0
- package/src/shared/localisedUrls/patterns.ts +160 -0
- package/src/shared/localisedUrls/resolve.ts +118 -0
- package/src/shared/localisedUrls/routes.ts +226 -0
- package/src/shared/localisedUrls/types.ts +18 -0
- package/src/shared/localisedUrls.ts +20 -629
- package/tests/link.test.tsx +111 -0
- package/tests/localisedUrls.test.ts +50 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +5 -2
- package/tests/routerAdapter.test.tsx +56 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
const normalizeApiPrefix = (prefix)=>{
|
|
3
|
+
const trimmedPrefix = prefix.trim();
|
|
4
|
+
if (!trimmedPrefix) return null;
|
|
5
|
+
const prefixedPath = trimmedPrefix.startsWith('/') ? trimmedPrefix : `/${trimmedPrefix}`;
|
|
6
|
+
const withoutWildcard = prefixedPath.replace(/\/\*$/, '');
|
|
7
|
+
const normalizedPrefix = withoutWildcard.length > 1 ? withoutWildcard.replace(/\/+$/, '') : withoutWildcard;
|
|
8
|
+
return '/' === normalizedPrefix ? null : normalizedPrefix;
|
|
9
|
+
};
|
|
10
|
+
const collectApiPrefixes = (routes, bffPrefix)=>{
|
|
11
|
+
const prefixes = new Set();
|
|
12
|
+
for (const route of routes){
|
|
13
|
+
if (!route.isApi || !route.urlPath) continue;
|
|
14
|
+
const normalizedPrefix = normalizeApiPrefix(route.urlPath);
|
|
15
|
+
if (normalizedPrefix) prefixes.add(normalizedPrefix);
|
|
16
|
+
}
|
|
17
|
+
const bffPrefixes = Array.isArray(bffPrefix) ? bffPrefix : bffPrefix ? [
|
|
18
|
+
bffPrefix
|
|
19
|
+
] : [];
|
|
20
|
+
for (const prefix of bffPrefixes){
|
|
21
|
+
const normalizedPrefix = normalizeApiPrefix(prefix);
|
|
22
|
+
if (normalizedPrefix) prefixes.add(normalizedPrefix);
|
|
23
|
+
}
|
|
24
|
+
return [
|
|
25
|
+
...prefixes
|
|
26
|
+
];
|
|
27
|
+
};
|
|
28
|
+
const matchesApiPrefix = (pathname, apiPrefixes)=>{
|
|
29
|
+
const normalizedPathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
30
|
+
return apiPrefixes.some((prefix)=>normalizedPathname === prefix || normalizedPathname.startsWith(`${prefix}/`));
|
|
31
|
+
};
|
|
32
|
+
export { collectApiPrefixes, matchesApiPrefix };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { DEFAULT_I18NEXT_DETECTION_OPTIONS, mergeDetectionOptions } from "../runtime/i18n/detection/config.mjs";
|
|
3
|
+
const convertToHonoLanguageDetectorOptions = (languages, fallbackLanguage, detectionOptions)=>{
|
|
4
|
+
const mergedDetection = detectionOptions ? mergeDetectionOptions(detectionOptions) : DEFAULT_I18NEXT_DETECTION_OPTIONS;
|
|
5
|
+
const order = (mergedDetection.order || []).filter((item)=>![
|
|
6
|
+
'path',
|
|
7
|
+
'localStorage',
|
|
8
|
+
'navigator',
|
|
9
|
+
'htmlTag',
|
|
10
|
+
'subdomain'
|
|
11
|
+
].includes(item));
|
|
12
|
+
const detectionOrder = order.length > 0 ? order : [
|
|
13
|
+
'querystring',
|
|
14
|
+
'cookie',
|
|
15
|
+
'header'
|
|
16
|
+
];
|
|
17
|
+
const honoOrder = detectionOrder.map((item)=>{
|
|
18
|
+
if ('querystring' === item) return 'querystring';
|
|
19
|
+
if ('cookie' === item) return 'cookie';
|
|
20
|
+
if ('header' === item) return 'header';
|
|
21
|
+
return item;
|
|
22
|
+
});
|
|
23
|
+
const caches = false === mergedDetection.caches ? false : Array.isArray(mergedDetection.caches) && !mergedDetection.caches.includes('cookie') ? false : [
|
|
24
|
+
'cookie'
|
|
25
|
+
];
|
|
26
|
+
const cookieMinutes = mergedDetection.cookieMinutes;
|
|
27
|
+
const cookieMaxAge = 'number' == typeof cookieMinutes && Number.isFinite(cookieMinutes) ? Math.max(0, Math.floor(60 * cookieMinutes)) : 60 * DEFAULT_I18NEXT_DETECTION_OPTIONS.cookieMinutes;
|
|
28
|
+
const cookieDomain = mergedDetection.cookieDomain;
|
|
29
|
+
const cookieSecure = mergedDetection.cookieSecure;
|
|
30
|
+
const cookieHttpOnly = mergedDetection.cookieHttpOnly;
|
|
31
|
+
const cookieSameSite = mergedDetection.cookieSameSite;
|
|
32
|
+
const normalizedCookieDomain = 'string' == typeof cookieDomain ? cookieDomain : void 0;
|
|
33
|
+
const cookieOptions = {
|
|
34
|
+
maxAge: cookieMaxAge,
|
|
35
|
+
sameSite: 'None' === cookieSameSite || 'none' === cookieSameSite ? 'None' : 'Lax' === cookieSameSite || 'lax' === cookieSameSite ? 'Lax' : 'Strict',
|
|
36
|
+
secure: 'boolean' == typeof cookieSecure ? cookieSecure : false,
|
|
37
|
+
httpOnly: 'boolean' == typeof cookieHttpOnly ? cookieHttpOnly : false,
|
|
38
|
+
...normalizedCookieDomain ? {
|
|
39
|
+
domain: normalizedCookieDomain
|
|
40
|
+
} : {}
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
supportedLanguages: languages.length > 0 ? languages : [
|
|
44
|
+
fallbackLanguage
|
|
45
|
+
],
|
|
46
|
+
fallbackLanguage,
|
|
47
|
+
order: honoOrder,
|
|
48
|
+
lookupQueryString: mergedDetection.lookupQuerystring || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupQuerystring || 'lng',
|
|
49
|
+
lookupCookie: mergedDetection.lookupCookie || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupCookie || 'i18next',
|
|
50
|
+
lookupFromHeaderKey: mergedDetection.lookupHeader || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupHeader || 'accept-language',
|
|
51
|
+
...void 0 !== caches && {
|
|
52
|
+
caches
|
|
53
|
+
},
|
|
54
|
+
...false !== caches && {
|
|
55
|
+
cookieOptions
|
|
56
|
+
},
|
|
57
|
+
ignoreCase: true
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
export { convertToHonoLanguageDetectorOptions };
|
|
@@ -1,146 +1,11 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
-
import {
|
|
3
|
-
import { localiseTargetPathname, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
|
|
2
|
+
import { resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
|
|
4
3
|
import { getLocaleDetectionOptions } from "../shared/utils.mjs";
|
|
4
|
+
import { collectApiPrefixes, matchesApiPrefix } from "./apiPrefix.mjs";
|
|
5
|
+
import { convertToHonoLanguageDetectorOptions } from "./detectorOptions.mjs";
|
|
6
|
+
import { buildLocalizedUrl, createLocaleRedirectResponse, getLanguageFromPath, isStaticResourceRequest, shouldIgnoreRedirect } from "./redirectPolicy.mjs";
|
|
5
7
|
import * as __rspack_external__modern_js_server_core_hono_a76ca254 from "@modern-js/server-core/hono";
|
|
6
8
|
const { languageDetector: languageDetector } = __rspack_external__modern_js_server_core_hono_a76ca254;
|
|
7
|
-
const normalizeApiPrefix = (prefix)=>{
|
|
8
|
-
const trimmedPrefix = prefix.trim();
|
|
9
|
-
if (!trimmedPrefix) return null;
|
|
10
|
-
const prefixedPath = trimmedPrefix.startsWith('/') ? trimmedPrefix : `/${trimmedPrefix}`;
|
|
11
|
-
const withoutWildcard = prefixedPath.replace(/\/\*$/, '');
|
|
12
|
-
const normalizedPrefix = withoutWildcard.length > 1 ? withoutWildcard.replace(/\/+$/, '') : withoutWildcard;
|
|
13
|
-
return '/' === normalizedPrefix ? null : normalizedPrefix;
|
|
14
|
-
};
|
|
15
|
-
const collectApiPrefixes = (routes, bffPrefix)=>{
|
|
16
|
-
const prefixes = new Set();
|
|
17
|
-
for (const route of routes){
|
|
18
|
-
if (!route.isApi || !route.urlPath) continue;
|
|
19
|
-
const normalizedPrefix = normalizeApiPrefix(route.urlPath);
|
|
20
|
-
if (normalizedPrefix) prefixes.add(normalizedPrefix);
|
|
21
|
-
}
|
|
22
|
-
const bffPrefixes = Array.isArray(bffPrefix) ? bffPrefix : bffPrefix ? [
|
|
23
|
-
bffPrefix
|
|
24
|
-
] : [];
|
|
25
|
-
for (const prefix of bffPrefixes){
|
|
26
|
-
const normalizedPrefix = normalizeApiPrefix(prefix);
|
|
27
|
-
if (normalizedPrefix) prefixes.add(normalizedPrefix);
|
|
28
|
-
}
|
|
29
|
-
return [
|
|
30
|
-
...prefixes
|
|
31
|
-
];
|
|
32
|
-
};
|
|
33
|
-
const matchesApiPrefix = (pathname, apiPrefixes)=>{
|
|
34
|
-
const normalizedPathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
35
|
-
return apiPrefixes.some((prefix)=>normalizedPathname === prefix || normalizedPathname.startsWith(`${prefix}/`));
|
|
36
|
-
};
|
|
37
|
-
const convertToHonoLanguageDetectorOptions = (languages, fallbackLanguage, detectionOptions)=>{
|
|
38
|
-
const mergedDetection = detectionOptions ? mergeDetectionOptions(detectionOptions) : DEFAULT_I18NEXT_DETECTION_OPTIONS;
|
|
39
|
-
const order = (mergedDetection.order || []).filter((item)=>![
|
|
40
|
-
'path',
|
|
41
|
-
'localStorage',
|
|
42
|
-
'navigator',
|
|
43
|
-
'htmlTag',
|
|
44
|
-
'subdomain'
|
|
45
|
-
].includes(item));
|
|
46
|
-
const detectionOrder = order.length > 0 ? order : [
|
|
47
|
-
'querystring',
|
|
48
|
-
'cookie',
|
|
49
|
-
'header'
|
|
50
|
-
];
|
|
51
|
-
const honoOrder = detectionOrder.map((item)=>{
|
|
52
|
-
if ('querystring' === item) return 'querystring';
|
|
53
|
-
if ('cookie' === item) return 'cookie';
|
|
54
|
-
if ('header' === item) return 'header';
|
|
55
|
-
return item;
|
|
56
|
-
});
|
|
57
|
-
const caches = false === mergedDetection.caches ? false : Array.isArray(mergedDetection.caches) && !mergedDetection.caches.includes('cookie') ? false : [
|
|
58
|
-
'cookie'
|
|
59
|
-
];
|
|
60
|
-
const cookieMinutes = mergedDetection.cookieMinutes;
|
|
61
|
-
const cookieMaxAge = 'number' == typeof cookieMinutes && Number.isFinite(cookieMinutes) ? Math.max(0, Math.floor(60 * cookieMinutes)) : 60 * DEFAULT_I18NEXT_DETECTION_OPTIONS.cookieMinutes;
|
|
62
|
-
const cookieDomain = mergedDetection.cookieDomain;
|
|
63
|
-
const cookieSecure = mergedDetection.cookieSecure;
|
|
64
|
-
const cookieHttpOnly = mergedDetection.cookieHttpOnly;
|
|
65
|
-
const cookieSameSite = mergedDetection.cookieSameSite;
|
|
66
|
-
const normalizedCookieDomain = 'string' == typeof cookieDomain ? cookieDomain : void 0;
|
|
67
|
-
const cookieOptions = {
|
|
68
|
-
maxAge: cookieMaxAge,
|
|
69
|
-
sameSite: 'None' === cookieSameSite || 'none' === cookieSameSite ? 'None' : 'Lax' === cookieSameSite || 'lax' === cookieSameSite ? 'Lax' : 'Strict',
|
|
70
|
-
secure: 'boolean' == typeof cookieSecure ? cookieSecure : false,
|
|
71
|
-
httpOnly: 'boolean' == typeof cookieHttpOnly ? cookieHttpOnly : false,
|
|
72
|
-
...normalizedCookieDomain ? {
|
|
73
|
-
domain: normalizedCookieDomain
|
|
74
|
-
} : {}
|
|
75
|
-
};
|
|
76
|
-
return {
|
|
77
|
-
supportedLanguages: languages.length > 0 ? languages : [
|
|
78
|
-
fallbackLanguage
|
|
79
|
-
],
|
|
80
|
-
fallbackLanguage,
|
|
81
|
-
order: honoOrder,
|
|
82
|
-
lookupQueryString: mergedDetection.lookupQuerystring || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupQuerystring || 'lng',
|
|
83
|
-
lookupCookie: mergedDetection.lookupCookie || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupCookie || 'i18next',
|
|
84
|
-
lookupFromHeaderKey: mergedDetection.lookupHeader || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupHeader || 'accept-language',
|
|
85
|
-
...void 0 !== caches && {
|
|
86
|
-
caches
|
|
87
|
-
},
|
|
88
|
-
...false !== caches && {
|
|
89
|
-
cookieOptions
|
|
90
|
-
},
|
|
91
|
-
ignoreCase: true
|
|
92
|
-
};
|
|
93
|
-
};
|
|
94
|
-
const shouldIgnoreRedirect = (pathname, urlPath, ignoreRedirectRoutes)=>{
|
|
95
|
-
if (!ignoreRedirectRoutes) return false;
|
|
96
|
-
const basePath = urlPath.replace('/*', '');
|
|
97
|
-
const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
98
|
-
const normalizedPath = remainingPath.startsWith('/') ? remainingPath : `/${remainingPath}`;
|
|
99
|
-
if ('function' == typeof ignoreRedirectRoutes) return ignoreRedirectRoutes(normalizedPath);
|
|
100
|
-
return ignoreRedirectRoutes.some((pattern)=>normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`));
|
|
101
|
-
};
|
|
102
|
-
const isStaticResourceRequest = (pathname, staticRoutePrefixes, languages = [])=>{
|
|
103
|
-
if (staticRoutePrefixes.some((prefix)=>pathname.startsWith(`${prefix}/`) || pathname === prefix)) return true;
|
|
104
|
-
const standardStaticPrefixes = [
|
|
105
|
-
'/static/',
|
|
106
|
-
'/upload/'
|
|
107
|
-
];
|
|
108
|
-
if (standardStaticPrefixes.some((prefix)=>pathname.startsWith(prefix))) return true;
|
|
109
|
-
const pathSegments = pathname.split('/').filter(Boolean);
|
|
110
|
-
if (pathSegments.length > 0 && languages.includes(pathSegments[0])) {
|
|
111
|
-
const pathWithoutLang = '/' + pathSegments.slice(1).join('/');
|
|
112
|
-
if (standardStaticPrefixes.some((prefix)=>pathWithoutLang.startsWith(prefix)) || staticRoutePrefixes.some((prefix)=>pathWithoutLang.startsWith(`${prefix}/`) || pathWithoutLang === prefix)) return true;
|
|
113
|
-
}
|
|
114
|
-
return false;
|
|
115
|
-
};
|
|
116
|
-
const getLanguageFromPath = (req, urlPath, languages)=>{
|
|
117
|
-
const url = new URL(req.url, `http://${req.header().host}`);
|
|
118
|
-
const pathname = url.pathname;
|
|
119
|
-
const basePath = urlPath.replace('/*', '');
|
|
120
|
-
const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
121
|
-
const segments = remainingPath.split('/').filter(Boolean);
|
|
122
|
-
const firstSegment = segments[0];
|
|
123
|
-
if (languages.includes(firstSegment)) return firstSegment;
|
|
124
|
-
return null;
|
|
125
|
-
};
|
|
126
|
-
const buildLocalizedUrl = (req, urlPath, language, languages, localisedUrls)=>{
|
|
127
|
-
const url = new URL(req.url);
|
|
128
|
-
const pathname = url.pathname;
|
|
129
|
-
const basePath = urlPath.replace('/*', '');
|
|
130
|
-
const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
131
|
-
const newPathname = localiseTargetPathname(remainingPath, language, languages, localisedUrls);
|
|
132
|
-
const suffix = `${url.search}${url.hash}`;
|
|
133
|
-
const localizedUrl = '/' === basePath ? newPathname + suffix : basePath + newPathname + suffix;
|
|
134
|
-
return localizedUrl;
|
|
135
|
-
};
|
|
136
|
-
const createLocaleRedirectResponse = (location)=>new Response(null, {
|
|
137
|
-
status: 302,
|
|
138
|
-
headers: {
|
|
139
|
-
'Cache-Control': 'private, no-store',
|
|
140
|
-
Location: location,
|
|
141
|
-
Vary: 'Accept-Language, Cookie'
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
9
|
const i18nServerPlugin = (options)=>({
|
|
145
10
|
name: '@modern-js/plugin-i18n/server',
|
|
146
11
|
setup: (api)=>{
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { localiseTargetPathname } from "../shared/localisedUrls.mjs";
|
|
3
|
+
const shouldIgnoreRedirect = (pathname, urlPath, ignoreRedirectRoutes)=>{
|
|
4
|
+
if (!ignoreRedirectRoutes) return false;
|
|
5
|
+
const basePath = urlPath.replace('/*', '');
|
|
6
|
+
const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
7
|
+
const normalizedPath = remainingPath.startsWith('/') ? remainingPath : `/${remainingPath}`;
|
|
8
|
+
if ('function' == typeof ignoreRedirectRoutes) return ignoreRedirectRoutes(normalizedPath);
|
|
9
|
+
return ignoreRedirectRoutes.some((pattern)=>normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`));
|
|
10
|
+
};
|
|
11
|
+
const isStaticResourceRequest = (pathname, staticRoutePrefixes, languages = [])=>{
|
|
12
|
+
if (staticRoutePrefixes.some((prefix)=>pathname.startsWith(`${prefix}/`) || pathname === prefix)) return true;
|
|
13
|
+
const standardStaticPrefixes = [
|
|
14
|
+
'/static/',
|
|
15
|
+
'/upload/'
|
|
16
|
+
];
|
|
17
|
+
if (standardStaticPrefixes.some((prefix)=>pathname.startsWith(prefix))) return true;
|
|
18
|
+
const pathSegments = pathname.split('/').filter(Boolean);
|
|
19
|
+
if (pathSegments.length > 0 && languages.includes(pathSegments[0])) {
|
|
20
|
+
const pathWithoutLang = '/' + pathSegments.slice(1).join('/');
|
|
21
|
+
if (standardStaticPrefixes.some((prefix)=>pathWithoutLang.startsWith(prefix)) || staticRoutePrefixes.some((prefix)=>pathWithoutLang.startsWith(`${prefix}/`) || pathWithoutLang === prefix)) return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
};
|
|
25
|
+
const getLanguageFromPath = (req, urlPath, languages)=>{
|
|
26
|
+
const url = new URL(req.url, `http://${req.header().host}`);
|
|
27
|
+
const pathname = url.pathname;
|
|
28
|
+
const basePath = urlPath.replace('/*', '');
|
|
29
|
+
const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
30
|
+
const segments = remainingPath.split('/').filter(Boolean);
|
|
31
|
+
const firstSegment = segments[0];
|
|
32
|
+
if (languages.includes(firstSegment)) return firstSegment;
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
const buildLocalizedUrl = (req, urlPath, language, languages, localisedUrls)=>{
|
|
36
|
+
const url = new URL(req.url);
|
|
37
|
+
const pathname = url.pathname;
|
|
38
|
+
const basePath = urlPath.replace('/*', '');
|
|
39
|
+
const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
40
|
+
const newPathname = localiseTargetPathname(remainingPath, language, languages, localisedUrls);
|
|
41
|
+
const suffix = `${url.search}${url.hash}`;
|
|
42
|
+
const localizedUrl = '/' === basePath ? newPathname + suffix : basePath + newPathname + suffix;
|
|
43
|
+
return localizedUrl;
|
|
44
|
+
};
|
|
45
|
+
const createLocaleRedirectResponse = (location)=>new Response(null, {
|
|
46
|
+
status: 302,
|
|
47
|
+
headers: {
|
|
48
|
+
'Cache-Control': 'private, no-store',
|
|
49
|
+
Location: location,
|
|
50
|
+
Vary: 'Accept-Language, Cookie'
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
export { buildLocalizedUrl, createLocaleRedirectResponse, getLanguageFromPath, isStaticResourceRequest, shouldIgnoreRedirect };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
const resolveLocalisedUrlsConfig = (option)=>{
|
|
3
|
+
if (option && 'object' == typeof option && Object.keys(option).length > 0) return {
|
|
4
|
+
enabled: true,
|
|
5
|
+
map: option
|
|
6
|
+
};
|
|
7
|
+
return {
|
|
8
|
+
enabled: false,
|
|
9
|
+
map: {}
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export { resolveLocalisedUrlsConfig };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
export { resolveLocalisedUrlsConfig } from "./config.mjs";
|
|
3
|
+
export { normalisePathPattern, normalisePathname } from "./normalise.mjs";
|
|
4
|
+
export { canonicalTargetPathname, localiseTargetPathname } from "./pathname.mjs";
|
|
5
|
+
export { buildPathFromPattern, matchPathPattern } from "./patterns.mjs";
|
|
6
|
+
export { resolveCanonicalLocalisedPath, resolveLocalisedPath } from "./resolve.mjs";
|
|
7
|
+
export { applyLocalisedUrlsToRoutes, validateLocalisedUrls } from "./routes.mjs";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
const LOCALE_PARAM_NAMES = new Set([
|
|
3
|
+
'lang',
|
|
4
|
+
'locale',
|
|
5
|
+
'language'
|
|
6
|
+
]);
|
|
7
|
+
const normaliseSlashes = (path)=>{
|
|
8
|
+
const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
|
|
9
|
+
const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
|
|
10
|
+
return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash;
|
|
11
|
+
};
|
|
12
|
+
const normalisePathPattern = (path)=>normaliseSlashes(path).replace(/\[(.+?)\]/g, ':$1');
|
|
13
|
+
const normalisePathname = (pathname)=>normaliseSlashes(pathname);
|
|
14
|
+
const normaliseRoutePath = (path)=>{
|
|
15
|
+
const normalized = normalisePathPattern(path);
|
|
16
|
+
return '/' === normalized ? '' : normalized.slice(1);
|
|
17
|
+
};
|
|
18
|
+
const getLocaleParamSegment = (segment)=>{
|
|
19
|
+
if (!segment.startsWith(':')) return null;
|
|
20
|
+
const paramName = segment.slice(1).replace(/\?$/, '');
|
|
21
|
+
return LOCALE_PARAM_NAMES.has(paramName) ? segment : null;
|
|
22
|
+
};
|
|
23
|
+
const splitPathSegments = (path)=>{
|
|
24
|
+
if (!path) return [];
|
|
25
|
+
return normalisePathPattern(path).split('/').filter(Boolean);
|
|
26
|
+
};
|
|
27
|
+
const stripLeadingLocaleParam = (path)=>{
|
|
28
|
+
const segments = splitPathSegments(path);
|
|
29
|
+
const leadingLocaleParam = getLocaleParamSegment(segments[0] || '');
|
|
30
|
+
if (!leadingLocaleParam) return path;
|
|
31
|
+
const remainingPath = segments.slice(1).join('/');
|
|
32
|
+
return remainingPath ? `/${remainingPath}` : void 0;
|
|
33
|
+
};
|
|
34
|
+
const getLeadingLocaleParam = (path)=>{
|
|
35
|
+
const segments = splitPathSegments(path);
|
|
36
|
+
return getLocaleParamSegment(segments[0] || '');
|
|
37
|
+
};
|
|
38
|
+
export { getLeadingLocaleParam, normalisePathPattern, normalisePathname, normaliseRoutePath, stripLeadingLocaleParam };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { resolveLocalisedUrlsConfig } from "./config.mjs";
|
|
3
|
+
import { resolveCanonicalLocalisedPath, resolveLocalisedPath } from "./resolve.mjs";
|
|
4
|
+
const stripLanguagePrefix = (pathname, languages)=>{
|
|
5
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
6
|
+
const firstSegment = segments[0]?.toLowerCase();
|
|
7
|
+
if (firstSegment && languages.some((language)=>language.toLowerCase() === firstSegment)) return `/${segments.slice(1).join('/')}`;
|
|
8
|
+
return pathname || '/';
|
|
9
|
+
};
|
|
10
|
+
const localiseTargetPathname = (pathname, language, languages, localisedUrls)=>{
|
|
11
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
12
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
13
|
+
const resolvedPath = localisedUrlsConfig.enabled ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
14
|
+
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
15
|
+
return `/${[
|
|
16
|
+
language,
|
|
17
|
+
...resolvedSegments
|
|
18
|
+
].join('/')}`;
|
|
19
|
+
};
|
|
20
|
+
const canonicalTargetPathname = (pathname, languages, localisedUrls)=>{
|
|
21
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
22
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
23
|
+
return localisedUrlsConfig.enabled ? resolveCanonicalLocalisedPath(pathWithoutLanguage, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
24
|
+
};
|
|
25
|
+
export { canonicalTargetPathname, localiseTargetPathname };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { normalisePathPattern, normalisePathname } from "./normalise.mjs";
|
|
3
|
+
const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4
|
+
const getParamName = (segment)=>segment.slice(1).replace(/\?$/, '');
|
|
5
|
+
const compiledPathPatternCache = new Map();
|
|
6
|
+
const compilePathPattern = (pattern)=>{
|
|
7
|
+
const normalizedPattern = normalisePathPattern(pattern);
|
|
8
|
+
const cached = compiledPathPatternCache.get(normalizedPattern);
|
|
9
|
+
if (cached) return cached;
|
|
10
|
+
const names = [];
|
|
11
|
+
const segments = normalizedPattern.split('/').filter(Boolean);
|
|
12
|
+
const source = segments.map((segment)=>{
|
|
13
|
+
if (segment.startsWith(':')) {
|
|
14
|
+
names.push(getParamName(segment));
|
|
15
|
+
const paramPattern = '([^/]+)';
|
|
16
|
+
return segment.endsWith('?') ? `(?:/${paramPattern})?` : `/${paramPattern}`;
|
|
17
|
+
}
|
|
18
|
+
if ('*' === segment) {
|
|
19
|
+
names.push('*');
|
|
20
|
+
return '/(.*)';
|
|
21
|
+
}
|
|
22
|
+
return `/${escapeRegExp(segment)}`;
|
|
23
|
+
}).join('');
|
|
24
|
+
const compiled = {
|
|
25
|
+
names,
|
|
26
|
+
regexp: new RegExp(`^${source || '/'}$`)
|
|
27
|
+
};
|
|
28
|
+
compiledPathPatternCache.set(normalizedPattern, compiled);
|
|
29
|
+
return compiled;
|
|
30
|
+
};
|
|
31
|
+
const getPatternSpecificity = (pattern)=>{
|
|
32
|
+
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
33
|
+
let staticSegments = 0;
|
|
34
|
+
let dynamicSegments = 0;
|
|
35
|
+
let splatSegments = 0;
|
|
36
|
+
for (const segment of segments)if ('*' === segment) splatSegments++;
|
|
37
|
+
else if (segment.startsWith(':')) dynamicSegments++;
|
|
38
|
+
else staticSegments++;
|
|
39
|
+
return {
|
|
40
|
+
staticSegments,
|
|
41
|
+
dynamicSegments,
|
|
42
|
+
splatSegments,
|
|
43
|
+
totalSegments: segments.length
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const comparePatternSpecificity = (left, right)=>{
|
|
47
|
+
const a = getPatternSpecificity(left);
|
|
48
|
+
const b = getPatternSpecificity(right);
|
|
49
|
+
return b.staticSegments - a.staticSegments || b.totalSegments - a.totalSegments || a.splatSegments - b.splatSegments || a.dynamicSegments - b.dynamicSegments;
|
|
50
|
+
};
|
|
51
|
+
const sortPatternsBySpecificity = (patterns)=>patterns.map((pattern, index)=>({
|
|
52
|
+
pattern,
|
|
53
|
+
index
|
|
54
|
+
})).sort((left, right)=>comparePatternSpecificity(left.pattern.pattern, right.pattern.pattern) || left.index - right.index).map(({ pattern })=>pattern);
|
|
55
|
+
const decodePathParam = (value)=>{
|
|
56
|
+
try {
|
|
57
|
+
return decodeURIComponent(value);
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const encodeSplatParam = (value)=>value.split('/').map(encodeURIComponent).join('/');
|
|
63
|
+
const matchPathPattern = (pathname, pattern)=>{
|
|
64
|
+
const { names, regexp } = compilePathPattern(pattern);
|
|
65
|
+
const match = regexp.exec(normalisePathname(pathname));
|
|
66
|
+
if (!match) return null;
|
|
67
|
+
const params = {};
|
|
68
|
+
for(let index = 0; index < names.length; index++){
|
|
69
|
+
const decoded = decodePathParam(match[index + 1] || '');
|
|
70
|
+
if (null === decoded) return null;
|
|
71
|
+
params[names[index]] = decoded;
|
|
72
|
+
}
|
|
73
|
+
return params;
|
|
74
|
+
};
|
|
75
|
+
const buildPathFromPattern = (pattern, params)=>{
|
|
76
|
+
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
77
|
+
const path = segments.map((segment)=>{
|
|
78
|
+
if (segment.startsWith(':')) {
|
|
79
|
+
const param = params[getParamName(segment)];
|
|
80
|
+
return param ? encodeURIComponent(param) : '';
|
|
81
|
+
}
|
|
82
|
+
if ('*' === segment) return encodeSplatParam(params['*'] || '');
|
|
83
|
+
return segment;
|
|
84
|
+
}).filter(Boolean).join('/');
|
|
85
|
+
return `/${path}`;
|
|
86
|
+
};
|
|
87
|
+
export { buildPathFromPattern, matchPathPattern, sortPatternsBySpecificity };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { normalisePathname } from "./normalise.mjs";
|
|
3
|
+
import { buildPathFromPattern, matchPathPattern, sortPatternsBySpecificity } from "./patterns.mjs";
|
|
4
|
+
const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls)=>{
|
|
5
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
6
|
+
const canonicalCandidates = sortPatternsBySpecificity(Object.entries(localisedUrls).map(([canonicalPattern, localisedUrlEntry])=>({
|
|
7
|
+
pattern: canonicalPattern,
|
|
8
|
+
canonicalPattern,
|
|
9
|
+
localisedUrlEntry
|
|
10
|
+
})));
|
|
11
|
+
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
|
|
12
|
+
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
13
|
+
if (!targetPattern) continue;
|
|
14
|
+
const params = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
15
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
16
|
+
}
|
|
17
|
+
const localisedCandidates = sortPatternsBySpecificity(Object.values(localisedUrls).flatMap((localisedUrlEntry)=>{
|
|
18
|
+
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
19
|
+
if (!targetPattern) return [];
|
|
20
|
+
return languages.map((language)=>localisedUrlEntry[language]).filter((sourcePattern)=>Boolean(sourcePattern)).map((sourcePattern)=>({
|
|
21
|
+
pattern: sourcePattern,
|
|
22
|
+
sourcePattern,
|
|
23
|
+
targetPattern
|
|
24
|
+
}));
|
|
25
|
+
}));
|
|
26
|
+
for (const { sourcePattern, targetPattern } of localisedCandidates){
|
|
27
|
+
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
28
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
29
|
+
}
|
|
30
|
+
return normalizedPathname;
|
|
31
|
+
};
|
|
32
|
+
const resolveCanonicalLocalisedPath = (pathname, languages, localisedUrls)=>{
|
|
33
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
34
|
+
const canonicalCandidates = sortPatternsBySpecificity(Object.entries(localisedUrls).map(([canonicalPattern, localisedUrlEntry])=>({
|
|
35
|
+
pattern: canonicalPattern,
|
|
36
|
+
canonicalPattern,
|
|
37
|
+
localisedUrlEntry
|
|
38
|
+
})));
|
|
39
|
+
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
|
|
40
|
+
const canonicalParams = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
41
|
+
if (canonicalParams) return buildPathFromPattern(canonicalPattern, canonicalParams);
|
|
42
|
+
for (const language of languages){
|
|
43
|
+
const sourcePattern = localisedUrlEntry[language];
|
|
44
|
+
if (!sourcePattern) continue;
|
|
45
|
+
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
46
|
+
if (params) return buildPathFromPattern(canonicalPattern, params);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return normalizedPathname;
|
|
50
|
+
};
|
|
51
|
+
export { resolveCanonicalLocalisedPath, resolveLocalisedPath };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { getLeadingLocaleParam, normalisePathPattern, normaliseRoutePath, stripLeadingLocaleParam } from "./normalise.mjs";
|
|
3
|
+
const isLocalisableRoutePath = (path)=>{
|
|
4
|
+
const pathWithoutLocale = stripLeadingLocaleParam(path);
|
|
5
|
+
if (!pathWithoutLocale || '/' === pathWithoutLocale || '*' === pathWithoutLocale) return false;
|
|
6
|
+
return true;
|
|
7
|
+
};
|
|
8
|
+
const joinPath = (parentPath, routePath)=>{
|
|
9
|
+
if (!isLocalisableRoutePath(routePath)) return parentPath;
|
|
10
|
+
const segment = normaliseRoutePath(stripLeadingLocaleParam(routePath) || '');
|
|
11
|
+
return normalisePathPattern(`${parentPath}/${segment}`);
|
|
12
|
+
};
|
|
13
|
+
const ensureLocalisedUrlsForPath = (canonicalPath, languages, localisedUrls)=>{
|
|
14
|
+
const entry = localisedUrls[canonicalPath];
|
|
15
|
+
if (!entry) throw new Error(`localisedUrls is enabled, but route "${canonicalPath}" does not define localised URLs for languages: ${languages.join(', ')}. Add localisedUrls["${canonicalPath}"] or set localeDetection.localisedUrls to false.`);
|
|
16
|
+
const missingLanguages = languages.filter((language)=>!entry[language]);
|
|
17
|
+
if (missingLanguages.length > 0) throw new Error(`localisedUrls["${canonicalPath}"] is missing languages: ${missingLanguages.join(', ')}. Every configured language must have a localised URL.`);
|
|
18
|
+
return entry;
|
|
19
|
+
};
|
|
20
|
+
const validateLocalisedUrls = (routes, languages, localisedUrls)=>{
|
|
21
|
+
const visit = (route, parentPath)=>{
|
|
22
|
+
const canonicalPath = joinPath(parentPath, route.path);
|
|
23
|
+
if (isLocalisableRoutePath(route.path)) ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls);
|
|
24
|
+
if ('children' in route && route.children) route.children.forEach((child)=>visit(child, canonicalPath));
|
|
25
|
+
};
|
|
26
|
+
routes.forEach((route)=>visit(route, ''));
|
|
27
|
+
};
|
|
28
|
+
const getLocalisedRoutePaths = (canonicalPath, parentLocalisedPaths, languages, entry)=>{
|
|
29
|
+
const paths = languages.map((language)=>{
|
|
30
|
+
const fullPath = normalisePathPattern(entry[language]);
|
|
31
|
+
const parentPath = normalisePathPattern(parentLocalisedPaths[language] || '/');
|
|
32
|
+
if ('/' === parentPath) return normaliseRoutePath(fullPath) || void 0;
|
|
33
|
+
const parentPrefix = `${parentPath}/`;
|
|
34
|
+
if (!fullPath.startsWith(parentPrefix)) throw new Error(`localisedUrls["${canonicalPath}"].${language} must be nested under "${parentPath}" because its parent route is localised there.`);
|
|
35
|
+
return normaliseRoutePath(fullPath.slice(parentPath.length));
|
|
36
|
+
});
|
|
37
|
+
return Array.from(new Set(paths.filter(Boolean)));
|
|
38
|
+
};
|
|
39
|
+
const transformLocalisedRoute = (route, parentCanonicalPath, parentLocalisedPaths, languages, localisedUrls)=>{
|
|
40
|
+
const canonicalPath = joinPath(parentCanonicalPath, route.path);
|
|
41
|
+
const localisedUrlEntry = isLocalisableRoutePath(route.path) ? ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls) : void 0;
|
|
42
|
+
const routeLocalisedPaths = localisedUrlEntry ? languages.reduce((acc, language)=>{
|
|
43
|
+
acc[language] = normalisePathPattern(localisedUrlEntry[language]);
|
|
44
|
+
return acc;
|
|
45
|
+
}, {}) : parentLocalisedPaths;
|
|
46
|
+
const children = 'children' in route && route.children ? route.children.flatMap((child)=>transformLocalisedRoute(child, canonicalPath, routeLocalisedPaths, languages, localisedUrls)) : void 0;
|
|
47
|
+
const baseRoute = {
|
|
48
|
+
...route,
|
|
49
|
+
...children ? {
|
|
50
|
+
children
|
|
51
|
+
} : {}
|
|
52
|
+
};
|
|
53
|
+
if (!localisedUrlEntry) return [
|
|
54
|
+
baseRoute
|
|
55
|
+
];
|
|
56
|
+
return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index, canonicalPath));
|
|
57
|
+
};
|
|
58
|
+
const legalRouteIdPart = (value)=>value.replace(/[^a-zA-Z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'index';
|
|
59
|
+
const suffixRouteIds = (route, suffix)=>{
|
|
60
|
+
const children = 'children' in route && route.children ? route.children.map((child)=>suffixRouteIds(child, suffix)) : void 0;
|
|
61
|
+
return {
|
|
62
|
+
...route,
|
|
63
|
+
...route.id ? {
|
|
64
|
+
id: `${route.id}__localised_${suffix}`
|
|
65
|
+
} : {},
|
|
66
|
+
...children ? {
|
|
67
|
+
children
|
|
68
|
+
} : {}
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const cloneRouteWithLocalisedPath = (route, path, index, canonicalPath)=>{
|
|
72
|
+
const leadingLocaleParam = getLeadingLocaleParam(route.path);
|
|
73
|
+
const localisedPath = leadingLocaleParam ? normaliseRoutePath(`${leadingLocaleParam}/${path}`) : path;
|
|
74
|
+
const routeWithPath = {
|
|
75
|
+
...route,
|
|
76
|
+
path: localisedPath
|
|
77
|
+
};
|
|
78
|
+
routeWithPath.modernCanonicalPath = canonicalPath;
|
|
79
|
+
return 0 === index ? routeWithPath : suffixRouteIds(routeWithPath, legalRouteIdPart(localisedPath));
|
|
80
|
+
};
|
|
81
|
+
const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
|
|
82
|
+
const rootLocalisedPaths = languages.reduce((acc, language)=>{
|
|
83
|
+
acc[language] = '/';
|
|
84
|
+
return acc;
|
|
85
|
+
}, {});
|
|
86
|
+
validateLocalisedUrls(routes, languages, localisedUrls);
|
|
87
|
+
return routes.flatMap((route)=>transformLocalisedRoute(route, '', rootLocalisedPaths, languages, localisedUrls));
|
|
88
|
+
};
|
|
89
|
+
export { applyLocalisedUrlsToRoutes, validateLocalisedUrls };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "node:module";
|