@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.119 → 3.2.0-ultramodern.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -11
- package/dist/cjs/runtime/I18nLink.js +7 -17
- package/dist/cjs/runtime/Link.js +252 -0
- package/dist/cjs/runtime/canonicalRoutes.js +18 -0
- package/dist/cjs/runtime/index.js +23 -0
- package/dist/cjs/runtime/localizedPaths.js +105 -0
- package/dist/cjs/runtime/utils.js +22 -5
- package/dist/cjs/shared/localisedUrls.js +32 -2
- package/dist/esm/runtime/I18nLink.mjs +6 -16
- package/dist/esm/runtime/Link.mjs +209 -0
- package/dist/esm/runtime/canonicalRoutes.mjs +0 -0
- package/dist/esm/runtime/index.mjs +4 -2
- package/dist/esm/runtime/localizedPaths.mjs +58 -0
- package/dist/esm/runtime/utils.mjs +18 -4
- package/dist/esm/shared/localisedUrls.mjs +24 -3
- package/dist/esm-node/runtime/I18nLink.mjs +6 -16
- package/dist/esm-node/runtime/Link.mjs +210 -0
- package/dist/esm-node/runtime/canonicalRoutes.mjs +1 -0
- package/dist/esm-node/runtime/index.mjs +4 -2
- package/dist/esm-node/runtime/localizedPaths.mjs +59 -0
- package/dist/esm-node/runtime/utils.mjs +18 -4
- package/dist/esm-node/shared/localisedUrls.mjs +24 -3
- package/dist/types/runtime/I18nLink.d.ts +4 -13
- package/dist/types/runtime/Link.d.ts +56 -0
- package/dist/types/runtime/canonicalRoutes.d.ts +60 -0
- package/dist/types/runtime/index.d.ts +5 -1
- package/dist/types/runtime/localizedPaths.d.ts +39 -0
- package/dist/types/runtime/utils.d.ts +12 -3
- package/dist/types/shared/localisedUrls.d.ts +8 -0
- package/package.json +13 -13
- package/rstest.config.mts +2 -2
- package/src/runtime/I18nLink.tsx +13 -46
- package/src/runtime/Link.tsx +414 -0
- package/src/runtime/canonicalRoutes.ts +93 -0
- package/src/runtime/index.tsx +24 -2
- package/src/runtime/localizedPaths.ts +118 -0
- package/src/runtime/utils.ts +24 -5
- package/src/shared/localisedUrls.ts +63 -3
- package/tests/link.test.tsx +475 -0
- package/tests/linkTypes.test.ts +28 -0
- package/tests/type-fixture/linkTypes.fixture.tsx +51 -0
- package/tests/type-fixture/tsconfig.json +15 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.d = (exports1, getters, values)=>{
|
|
5
|
+
var define = (defs, kind)=>{
|
|
6
|
+
for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
[kind]: defs[key]
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
define(getters, "get");
|
|
12
|
+
define(values, "value");
|
|
13
|
+
};
|
|
14
|
+
})();
|
|
15
|
+
(()=>{
|
|
16
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
17
|
+
})();
|
|
18
|
+
(()=>{
|
|
19
|
+
__webpack_require__.r = (exports1)=>{
|
|
20
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
21
|
+
value: 'Module'
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
24
|
+
value: true
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
})();
|
|
28
|
+
var __webpack_exports__ = {};
|
|
29
|
+
__webpack_require__.r(__webpack_exports__);
|
|
30
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
31
|
+
canonicalPath: ()=>canonicalPath,
|
|
32
|
+
localizePath: ()=>localizePath,
|
|
33
|
+
useLocalizedLocation: ()=>useLocalizedLocation,
|
|
34
|
+
useLocalizedPaths: ()=>useLocalizedPaths
|
|
35
|
+
});
|
|
36
|
+
const external_react_namespaceObject = require("react");
|
|
37
|
+
const localisedUrls_js_namespaceObject = require("../shared/localisedUrls.js");
|
|
38
|
+
const external_context_js_namespaceObject = require("./context.js");
|
|
39
|
+
const external_routerAdapter_js_namespaceObject = require("./routerAdapter.js");
|
|
40
|
+
const external_utils_js_namespaceObject = require("./utils.js");
|
|
41
|
+
const localizePath = (pathname, language, config)=>(0, external_utils_js_namespaceObject.buildLocalizedUrl)(pathname, language, config.languages, config.localisedUrls);
|
|
42
|
+
const canonicalPath = (target, config)=>{
|
|
43
|
+
const { pathname, search, hash } = (0, external_utils_js_namespaceObject.splitUrlTarget)(target);
|
|
44
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
45
|
+
const pathWithoutLanguage = segments.length > 0 && config.languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : pathname || '/';
|
|
46
|
+
const localisedUrlsConfig = (0, localisedUrls_js_namespaceObject.resolveLocalisedUrlsConfig)(config.localisedUrls);
|
|
47
|
+
const resolvedPath = localisedUrlsConfig.enabled ? (0, localisedUrls_js_namespaceObject.resolveCanonicalLocalisedPath)(pathWithoutLanguage, config.languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
48
|
+
return `${resolvedPath}${search}${hash}`;
|
|
49
|
+
};
|
|
50
|
+
const useLocalizedPaths = ()=>{
|
|
51
|
+
const { supportedLanguages, localisedUrls } = (0, external_context_js_namespaceObject.useModernI18n)();
|
|
52
|
+
return (0, external_react_namespaceObject.useMemo)(()=>{
|
|
53
|
+
const config = {
|
|
54
|
+
languages: supportedLanguages,
|
|
55
|
+
localisedUrls
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
localizePath: (pathname, language)=>localizePath(pathname, language, config),
|
|
59
|
+
canonicalPath: (pathname)=>canonicalPath(pathname, config)
|
|
60
|
+
};
|
|
61
|
+
}, [
|
|
62
|
+
supportedLanguages,
|
|
63
|
+
localisedUrls
|
|
64
|
+
]);
|
|
65
|
+
};
|
|
66
|
+
const useLocalizedLocation = ()=>{
|
|
67
|
+
const { language, supportedLanguages, localisedUrls } = (0, external_context_js_namespaceObject.useModernI18n)();
|
|
68
|
+
const { location } = (0, external_routerAdapter_js_namespaceObject.useI18nRouterAdapter)();
|
|
69
|
+
const pathname = location?.pathname ?? '/';
|
|
70
|
+
const search = location?.search ?? '';
|
|
71
|
+
const hash = location?.hash ?? '';
|
|
72
|
+
return (0, external_react_namespaceObject.useMemo)(()=>{
|
|
73
|
+
const config = {
|
|
74
|
+
languages: supportedLanguages,
|
|
75
|
+
localisedUrls
|
|
76
|
+
};
|
|
77
|
+
const alternates = {};
|
|
78
|
+
for (const supportedLanguage of supportedLanguages)alternates[supportedLanguage] = `${localizePath(pathname, supportedLanguage, config)}${search}${hash}`;
|
|
79
|
+
return {
|
|
80
|
+
language,
|
|
81
|
+
canonical: canonicalPath(pathname, config),
|
|
82
|
+
alternates
|
|
83
|
+
};
|
|
84
|
+
}, [
|
|
85
|
+
language,
|
|
86
|
+
supportedLanguages,
|
|
87
|
+
localisedUrls,
|
|
88
|
+
pathname,
|
|
89
|
+
search,
|
|
90
|
+
hash
|
|
91
|
+
]);
|
|
92
|
+
};
|
|
93
|
+
exports.canonicalPath = __webpack_exports__.canonicalPath;
|
|
94
|
+
exports.localizePath = __webpack_exports__.localizePath;
|
|
95
|
+
exports.useLocalizedLocation = __webpack_exports__.useLocalizedLocation;
|
|
96
|
+
exports.useLocalizedPaths = __webpack_exports__.useLocalizedPaths;
|
|
97
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
98
|
+
"canonicalPath",
|
|
99
|
+
"localizePath",
|
|
100
|
+
"useLocalizedLocation",
|
|
101
|
+
"useLocalizedPaths"
|
|
102
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
103
|
+
Object.defineProperty(exports, '__esModule', {
|
|
104
|
+
value: true
|
|
105
|
+
});
|
|
@@ -33,7 +33,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
33
33
|
getEntryPath: ()=>getEntryPath,
|
|
34
34
|
getLanguageFromPath: ()=>getLanguageFromPath,
|
|
35
35
|
getPathname: ()=>getPathname,
|
|
36
|
-
shouldIgnoreRedirect: ()=>shouldIgnoreRedirect
|
|
36
|
+
shouldIgnoreRedirect: ()=>shouldIgnoreRedirect,
|
|
37
|
+
splitUrlTarget: ()=>splitUrlTarget
|
|
37
38
|
});
|
|
38
39
|
const runtime_namespaceObject = require("@modern-js/runtime");
|
|
39
40
|
const context_namespaceObject = require("@modern-js/runtime/context");
|
|
@@ -53,16 +54,30 @@ const getLanguageFromPath = (pathname, languages, fallbackLanguage)=>{
|
|
|
53
54
|
if (languages.includes(firstSegment)) return firstSegment;
|
|
54
55
|
return fallbackLanguage;
|
|
55
56
|
};
|
|
56
|
-
const
|
|
57
|
+
const splitUrlTarget = (target)=>{
|
|
58
|
+
const hashIndex = target.indexOf('#');
|
|
59
|
+
const hash = hashIndex >= 0 ? target.slice(hashIndex) : '';
|
|
60
|
+
const beforeHash = hashIndex >= 0 ? target.slice(0, hashIndex) : target;
|
|
61
|
+
const searchIndex = beforeHash.indexOf('?');
|
|
62
|
+
const search = searchIndex >= 0 ? beforeHash.slice(searchIndex) : '';
|
|
63
|
+
const pathname = searchIndex >= 0 ? beforeHash.slice(0, searchIndex) : beforeHash;
|
|
64
|
+
return {
|
|
65
|
+
pathname,
|
|
66
|
+
search,
|
|
67
|
+
hash
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
const buildLocalizedUrl = (target, language, languages, localisedUrls)=>{
|
|
71
|
+
const { pathname, search, hash } = splitUrlTarget(target);
|
|
57
72
|
const segments = pathname.split('/').filter(Boolean);
|
|
58
73
|
const localisedUrlsConfig = (0, localisedUrls_js_namespaceObject.resolveLocalisedUrlsConfig)(localisedUrls);
|
|
59
|
-
const pathWithoutLanguage = segments.length > 0 && languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : pathname;
|
|
74
|
+
const pathWithoutLanguage = segments.length > 0 && languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : pathname || '/';
|
|
60
75
|
const resolvedPath = localisedUrlsConfig.enabled ? (0, localisedUrls_js_namespaceObject.resolveLocalisedPath)(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
61
76
|
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
62
77
|
return `/${[
|
|
63
78
|
language,
|
|
64
79
|
...resolvedSegments
|
|
65
|
-
].join('/')}`;
|
|
80
|
+
].join('/')}${search}${hash}`;
|
|
66
81
|
};
|
|
67
82
|
const detectLanguageFromPath = (pathname, languages, localePathRedirect)=>{
|
|
68
83
|
if (!localePathRedirect) return {
|
|
@@ -96,13 +111,15 @@ exports.getEntryPath = __webpack_exports__.getEntryPath;
|
|
|
96
111
|
exports.getLanguageFromPath = __webpack_exports__.getLanguageFromPath;
|
|
97
112
|
exports.getPathname = __webpack_exports__.getPathname;
|
|
98
113
|
exports.shouldIgnoreRedirect = __webpack_exports__.shouldIgnoreRedirect;
|
|
114
|
+
exports.splitUrlTarget = __webpack_exports__.splitUrlTarget;
|
|
99
115
|
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
100
116
|
"buildLocalizedUrl",
|
|
101
117
|
"detectLanguageFromPath",
|
|
102
118
|
"getEntryPath",
|
|
103
119
|
"getLanguageFromPath",
|
|
104
120
|
"getPathname",
|
|
105
|
-
"shouldIgnoreRedirect"
|
|
121
|
+
"shouldIgnoreRedirect",
|
|
122
|
+
"splitUrlTarget"
|
|
106
123
|
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
107
124
|
Object.defineProperty(exports, '__esModule', {
|
|
108
125
|
value: true
|
|
@@ -129,7 +129,7 @@ const transformLocalisedRoute = (route, parentCanonicalPath, parentLocalisedPath
|
|
|
129
129
|
if (!localisedUrlEntry) return [
|
|
130
130
|
baseRoute
|
|
131
131
|
];
|
|
132
|
-
return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index));
|
|
132
|
+
return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index, canonicalPath));
|
|
133
133
|
};
|
|
134
134
|
const legalRouteIdPart = (value)=>value.replace(/[^a-zA-Z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'index';
|
|
135
135
|
const suffixRouteIds = (route, suffix)=>{
|
|
@@ -144,13 +144,14 @@ const suffixRouteIds = (route, suffix)=>{
|
|
|
144
144
|
} : {}
|
|
145
145
|
};
|
|
146
146
|
};
|
|
147
|
-
const cloneRouteWithLocalisedPath = (route, path, index)=>{
|
|
147
|
+
const cloneRouteWithLocalisedPath = (route, path, index, canonicalPath)=>{
|
|
148
148
|
const leadingLocaleParam = getLeadingLocaleParam(route.path);
|
|
149
149
|
const localisedPath = leadingLocaleParam ? normaliseRoutePath(`${leadingLocaleParam}/${path}`) : path;
|
|
150
150
|
const routeWithPath = {
|
|
151
151
|
...route,
|
|
152
152
|
path: localisedPath
|
|
153
153
|
};
|
|
154
|
+
routeWithPath.modernCanonicalPath = canonicalPath;
|
|
154
155
|
return 0 === index ? routeWithPath : suffixRouteIds(routeWithPath, legalRouteIdPart(localisedPath));
|
|
155
156
|
};
|
|
156
157
|
const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
|
|
@@ -206,6 +207,12 @@ const buildPathFromPattern = (pattern, params)=>{
|
|
|
206
207
|
};
|
|
207
208
|
const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls)=>{
|
|
208
209
|
const normalizedPathname = normalisePathPattern(pathname);
|
|
210
|
+
for (const [canonicalPattern, localisedUrlEntry] of Object.entries(localisedUrls)){
|
|
211
|
+
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
212
|
+
if (!targetPattern) continue;
|
|
213
|
+
const params = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
214
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
215
|
+
}
|
|
209
216
|
for (const localisedUrlEntry of Object.values(localisedUrls)){
|
|
210
217
|
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
211
218
|
if (targetPattern) for (const language of languages){
|
|
@@ -217,21 +224,44 @@ const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls
|
|
|
217
224
|
}
|
|
218
225
|
return normalizedPathname;
|
|
219
226
|
};
|
|
227
|
+
const resolveCanonicalLocalisedPath = (pathname, languages, localisedUrls)=>{
|
|
228
|
+
const normalizedPathname = normalisePathPattern(pathname);
|
|
229
|
+
for (const [canonicalPattern, localisedUrlEntry] of Object.entries(localisedUrls)){
|
|
230
|
+
const canonicalParams = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
231
|
+
if (canonicalParams) return buildPathFromPattern(canonicalPattern, canonicalParams);
|
|
232
|
+
for (const language of languages){
|
|
233
|
+
const sourcePattern = localisedUrlEntry[language];
|
|
234
|
+
if (!sourcePattern) continue;
|
|
235
|
+
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
236
|
+
if (params) return buildPathFromPattern(canonicalPattern, params);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return normalizedPathname;
|
|
240
|
+
};
|
|
220
241
|
__webpack_require__.d(__webpack_exports__, {}, {
|
|
221
242
|
applyLocalisedUrlsToRoutes: applyLocalisedUrlsToRoutes,
|
|
243
|
+
buildPathFromPattern: buildPathFromPattern,
|
|
244
|
+
matchPathPattern: matchPathPattern,
|
|
222
245
|
normalisePathPattern: normalisePathPattern,
|
|
246
|
+
resolveCanonicalLocalisedPath: resolveCanonicalLocalisedPath,
|
|
223
247
|
resolveLocalisedPath: resolveLocalisedPath,
|
|
224
248
|
resolveLocalisedUrlsConfig: resolveLocalisedUrlsConfig,
|
|
225
249
|
validateLocalisedUrls: validateLocalisedUrls
|
|
226
250
|
});
|
|
227
251
|
exports.applyLocalisedUrlsToRoutes = __webpack_exports__.applyLocalisedUrlsToRoutes;
|
|
252
|
+
exports.buildPathFromPattern = __webpack_exports__.buildPathFromPattern;
|
|
253
|
+
exports.matchPathPattern = __webpack_exports__.matchPathPattern;
|
|
228
254
|
exports.normalisePathPattern = __webpack_exports__.normalisePathPattern;
|
|
255
|
+
exports.resolveCanonicalLocalisedPath = __webpack_exports__.resolveCanonicalLocalisedPath;
|
|
229
256
|
exports.resolveLocalisedPath = __webpack_exports__.resolveLocalisedPath;
|
|
230
257
|
exports.resolveLocalisedUrlsConfig = __webpack_exports__.resolveLocalisedUrlsConfig;
|
|
231
258
|
exports.validateLocalisedUrls = __webpack_exports__.validateLocalisedUrls;
|
|
232
259
|
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
233
260
|
"applyLocalisedUrlsToRoutes",
|
|
261
|
+
"buildPathFromPattern",
|
|
262
|
+
"matchPathPattern",
|
|
234
263
|
"normalisePathPattern",
|
|
264
|
+
"resolveCanonicalLocalisedPath",
|
|
235
265
|
"resolveLocalisedPath",
|
|
236
266
|
"resolveLocalisedUrlsConfig",
|
|
237
267
|
"validateLocalisedUrls"
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { buildLocalizedUrl } from "./utils.mjs";
|
|
2
|
+
import { Link } from "./Link.mjs";
|
|
3
|
+
let warnedDeprecation = false;
|
|
5
4
|
const I18nLink = ({ to, children, ...props })=>{
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const localizedTo = buildLocalizedUrl(to, currentLang, supportedLanguages, localisedUrls);
|
|
10
|
-
if ('development' === process.env.NODE_ENV && hasRouter && !params.lang) console.warn("I18nLink is being used outside of a :lang dynamic route context. This may cause unexpected behavior. Please ensure I18nLink is used within a route that has a :lang parameter.");
|
|
11
|
-
if (!hasRouter || !Link) {
|
|
12
|
-
const { prefetch: _prefetch, preload: _preload, ...anchorProps } = props;
|
|
13
|
-
return /*#__PURE__*/ jsx("a", {
|
|
14
|
-
href: localizedTo,
|
|
15
|
-
...anchorProps,
|
|
16
|
-
children: children
|
|
17
|
-
});
|
|
5
|
+
if ('development' === process.env.NODE_ENV && !warnedDeprecation) {
|
|
6
|
+
warnedDeprecation = true;
|
|
7
|
+
console.warn("[plugin-i18n] I18nLink is deprecated. Import { Link } from '@modern-js/plugin-i18n/runtime' instead — it accepts the same language-agnostic `to` values.");
|
|
18
8
|
}
|
|
19
9
|
return /*#__PURE__*/ jsx(Link, {
|
|
20
|
-
to:
|
|
10
|
+
to: to,
|
|
21
11
|
...props,
|
|
22
12
|
children: children
|
|
23
13
|
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { useModernI18n } from "./context.mjs";
|
|
4
|
+
import { canonicalPath } from "./localizedPaths.mjs";
|
|
5
|
+
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
6
|
+
import { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
|
|
7
|
+
const EXTERNAL_TARGET_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
8
|
+
const warnedTargets = new Set();
|
|
9
|
+
const warnOnce = (key, message)=>{
|
|
10
|
+
if ('development' !== process.env.NODE_ENV || warnedTargets.has(key)) return;
|
|
11
|
+
warnedTargets.add(key);
|
|
12
|
+
console.warn(message);
|
|
13
|
+
};
|
|
14
|
+
const interpolateRouteParams = (pathname, params)=>{
|
|
15
|
+
if (!/[$:*{]/.test(pathname)) return pathname;
|
|
16
|
+
const resolveParam = (name)=>{
|
|
17
|
+
const value = params?.[name];
|
|
18
|
+
return void 0 === value ? void 0 : String(value);
|
|
19
|
+
};
|
|
20
|
+
const segments = pathname.split('/').map((segment)=>{
|
|
21
|
+
if (!segment) return segment;
|
|
22
|
+
if (segment.startsWith('{-$') && segment.endsWith('}')) {
|
|
23
|
+
const value = resolveParam(segment.slice(3, -1));
|
|
24
|
+
return void 0 === value ? null : encodeURIComponent(value);
|
|
25
|
+
}
|
|
26
|
+
if ('$' === segment || '*' === segment) {
|
|
27
|
+
const value = resolveParam('_splat') ?? resolveParam('*');
|
|
28
|
+
return void 0 === value ? null : value.split('/').map(encodeURIComponent).join('/');
|
|
29
|
+
}
|
|
30
|
+
if (segment.startsWith('$')) {
|
|
31
|
+
const value = resolveParam(segment.slice(1));
|
|
32
|
+
if (void 0 === value) {
|
|
33
|
+
warnOnce(`missing-param:${pathname}:${segment}`, `[plugin-i18n] <Link to="${pathname}"> is missing required param "${segment.slice(1)}".`);
|
|
34
|
+
return segment;
|
|
35
|
+
}
|
|
36
|
+
return encodeURIComponent(value);
|
|
37
|
+
}
|
|
38
|
+
if (segment.startsWith(':')) {
|
|
39
|
+
const optional = segment.endsWith('?');
|
|
40
|
+
const name = segment.slice(1, optional ? -1 : void 0);
|
|
41
|
+
const value = resolveParam(name);
|
|
42
|
+
if (void 0 === value) {
|
|
43
|
+
if (optional) return null;
|
|
44
|
+
warnOnce(`missing-param:${pathname}:${segment}`, `[plugin-i18n] <Link to="${pathname}"> is missing required param "${name}".`);
|
|
45
|
+
return segment;
|
|
46
|
+
}
|
|
47
|
+
return encodeURIComponent(value);
|
|
48
|
+
}
|
|
49
|
+
return segment;
|
|
50
|
+
}).filter((segment)=>null !== segment);
|
|
51
|
+
return segments.join('/') || '/';
|
|
52
|
+
};
|
|
53
|
+
const normalizeSearch = (search, searchFromTo)=>{
|
|
54
|
+
if (search && 'object' == typeof search) {
|
|
55
|
+
const entries = Object.entries(search).filter(([, value])=>null != value);
|
|
56
|
+
const searchObject = Object.fromEntries(entries.map(([key, value])=>[
|
|
57
|
+
key,
|
|
58
|
+
String(value)
|
|
59
|
+
]));
|
|
60
|
+
const params = new URLSearchParams(searchObject);
|
|
61
|
+
const serialized = params.toString();
|
|
62
|
+
return {
|
|
63
|
+
searchString: serialized ? `?${serialized}` : '',
|
|
64
|
+
searchObject
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const raw = 'string' == typeof search && search ? search : searchFromTo;
|
|
68
|
+
if (!raw) return {
|
|
69
|
+
searchString: '',
|
|
70
|
+
searchObject: void 0
|
|
71
|
+
};
|
|
72
|
+
const searchString = raw.startsWith('?') ? raw : `?${raw}`;
|
|
73
|
+
const searchObject = {};
|
|
74
|
+
new URLSearchParams(searchString).forEach((value, key)=>{
|
|
75
|
+
searchObject[key] = value;
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
searchString,
|
|
79
|
+
searchObject
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const splitActiveProps = (active, activeProps)=>{
|
|
83
|
+
if (!active || !activeProps) return {};
|
|
84
|
+
return activeProps;
|
|
85
|
+
};
|
|
86
|
+
const mergeClassNames = (...values)=>{
|
|
87
|
+
const classNames = values.filter((value)=>'string' == typeof value && value.length > 0);
|
|
88
|
+
return classNames.length > 0 ? classNames.join(' ') : void 0;
|
|
89
|
+
};
|
|
90
|
+
const Link = (props)=>{
|
|
91
|
+
const { to, params, children, hash: hashProp, search: searchProp, hashScrollIntoView, activeOptions, activeProps, ...rest } = props;
|
|
92
|
+
const adapter = useI18nRouterAdapter();
|
|
93
|
+
const { language, supportedLanguages, localisedUrls } = useModernI18n();
|
|
94
|
+
const config = {
|
|
95
|
+
languages: supportedLanguages,
|
|
96
|
+
localisedUrls
|
|
97
|
+
};
|
|
98
|
+
const isExternal = EXTERNAL_TARGET_RE.test(to);
|
|
99
|
+
const isBareHash = to.startsWith('#');
|
|
100
|
+
const target = useMemo(()=>{
|
|
101
|
+
if (isExternal || isBareHash) return null;
|
|
102
|
+
const { pathname, search: searchFromTo, hash: hashFromTo } = splitUrlTarget(to);
|
|
103
|
+
const interpolated = interpolateRouteParams(pathname || '/', params);
|
|
104
|
+
const firstSegment = interpolated.split('/').filter(Boolean)[0];
|
|
105
|
+
if (firstSegment && supportedLanguages.includes(firstSegment)) warnOnce(`lang-prefix:${to}`, `[plugin-i18n] <Link to="${to}"> starts with a language prefix. Write language-agnostic canonical paths; the Link localizes them automatically.`);
|
|
106
|
+
const localizedPathname = buildLocalizedUrl(interpolated, language, supportedLanguages, localisedUrls);
|
|
107
|
+
const hash = hashProp ?? (hashFromTo ? hashFromTo.slice(1) : '');
|
|
108
|
+
const { searchString, searchObject } = normalizeSearch(searchProp, searchFromTo);
|
|
109
|
+
return {
|
|
110
|
+
canonicalPathname: interpolated,
|
|
111
|
+
localizedPathname,
|
|
112
|
+
hash,
|
|
113
|
+
searchString,
|
|
114
|
+
searchObject,
|
|
115
|
+
href: `${localizedPathname}${searchString}${hash ? `#${hash}` : ''}`
|
|
116
|
+
};
|
|
117
|
+
}, [
|
|
118
|
+
to,
|
|
119
|
+
params,
|
|
120
|
+
hashProp,
|
|
121
|
+
searchProp,
|
|
122
|
+
isExternal,
|
|
123
|
+
isBareHash,
|
|
124
|
+
language,
|
|
125
|
+
supportedLanguages,
|
|
126
|
+
localisedUrls
|
|
127
|
+
]);
|
|
128
|
+
const isActive = useMemo(()=>{
|
|
129
|
+
if (!target || !adapter.location) return false;
|
|
130
|
+
const current = canonicalPath(adapter.location.pathname, config);
|
|
131
|
+
const targetCanonical = canonicalPath(target.canonicalPathname, config);
|
|
132
|
+
const exact = activeOptions?.exact ?? '/' === targetCanonical;
|
|
133
|
+
if (current === targetCanonical) return true;
|
|
134
|
+
if (exact) return false;
|
|
135
|
+
return current.startsWith('/' === targetCanonical ? '/' : `${targetCanonical}/`);
|
|
136
|
+
}, [
|
|
137
|
+
target,
|
|
138
|
+
adapter.location,
|
|
139
|
+
activeOptions?.exact,
|
|
140
|
+
supportedLanguages,
|
|
141
|
+
localisedUrls
|
|
142
|
+
]);
|
|
143
|
+
const resolvedActiveProps = splitActiveProps(isActive, activeProps);
|
|
144
|
+
const activeAttributes = isActive ? {
|
|
145
|
+
'data-status': 'active',
|
|
146
|
+
'aria-current': rest['aria-current'] ?? resolvedActiveProps['aria-current'] ?? 'page'
|
|
147
|
+
} : {};
|
|
148
|
+
if (!target) {
|
|
149
|
+
const { prefetch: _prefetch, preload: _preload, replace: _replace, ...anchorProps } = rest;
|
|
150
|
+
return /*#__PURE__*/ jsx("a", {
|
|
151
|
+
href: to,
|
|
152
|
+
...anchorProps,
|
|
153
|
+
children: children
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const { Link: RouterLink, hasRouter, framework } = adapter;
|
|
157
|
+
if (!hasRouter || !RouterLink) {
|
|
158
|
+
const { prefetch: _prefetch, preload: _preload, replace: _replace, ...anchorProps } = rest;
|
|
159
|
+
const { className: activeClassName, style: activeStyle, ...activeRest } = resolvedActiveProps;
|
|
160
|
+
return /*#__PURE__*/ jsx("a", {
|
|
161
|
+
href: target.href,
|
|
162
|
+
...anchorProps,
|
|
163
|
+
...activeRest,
|
|
164
|
+
...activeAttributes,
|
|
165
|
+
className: mergeClassNames(rest.className, activeClassName),
|
|
166
|
+
style: {
|
|
167
|
+
...rest.style,
|
|
168
|
+
...activeStyle
|
|
169
|
+
},
|
|
170
|
+
children: children
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const { className: activeClassName, style: activeStyle, ...activeRest } = resolvedActiveProps;
|
|
174
|
+
const mergedClassName = mergeClassNames(rest.className, activeClassName);
|
|
175
|
+
const mergedStyle = {
|
|
176
|
+
...rest.style,
|
|
177
|
+
...activeStyle
|
|
178
|
+
};
|
|
179
|
+
if ('tanstack' === framework) return /*#__PURE__*/ jsx(RouterLink, {
|
|
180
|
+
to: target.localizedPathname,
|
|
181
|
+
...target.searchObject ? {
|
|
182
|
+
search: target.searchObject
|
|
183
|
+
} : {},
|
|
184
|
+
...target.hash ? {
|
|
185
|
+
hash: target.hash
|
|
186
|
+
} : {},
|
|
187
|
+
...void 0 === hashScrollIntoView ? {} : {
|
|
188
|
+
hashScrollIntoView
|
|
189
|
+
},
|
|
190
|
+
...rest,
|
|
191
|
+
...activeRest,
|
|
192
|
+
...activeAttributes,
|
|
193
|
+
className: mergedClassName,
|
|
194
|
+
style: mergedStyle,
|
|
195
|
+
children: children
|
|
196
|
+
});
|
|
197
|
+
return /*#__PURE__*/ jsx(RouterLink, {
|
|
198
|
+
to: target.href,
|
|
199
|
+
...rest,
|
|
200
|
+
...activeRest,
|
|
201
|
+
...activeAttributes,
|
|
202
|
+
className: mergedClassName,
|
|
203
|
+
style: mergedStyle,
|
|
204
|
+
children: children
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
const runtime_Link = Link;
|
|
208
|
+
export default runtime_Link;
|
|
209
|
+
export { Link, interpolateRouteParams };
|
|
File without changes
|
|
@@ -12,7 +12,7 @@ import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOpti
|
|
|
12
12
|
import { useI18nextLanguageDetector } from "./i18n/detection/middleware.mjs";
|
|
13
13
|
import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
|
|
14
14
|
import { changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } from "./i18n/utils.mjs";
|
|
15
|
-
import { getPathname } from "./utils.mjs";
|
|
15
|
+
import { buildLocalizedUrl, getPathname, splitUrlTarget } from "./utils.mjs";
|
|
16
16
|
import "./types.mjs";
|
|
17
17
|
const i18nPlugin = (options)=>({
|
|
18
18
|
name: '@modern-js/plugin-i18n',
|
|
@@ -136,5 +136,7 @@ const i18nPlugin = (options)=>({
|
|
|
136
136
|
});
|
|
137
137
|
const runtime = i18nPlugin;
|
|
138
138
|
export { I18nLink } from "./I18nLink.mjs";
|
|
139
|
+
export { Link } from "./Link.mjs";
|
|
140
|
+
export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
|
|
139
141
|
export default runtime;
|
|
140
|
-
export { i18nPlugin, useModernI18n };
|
|
142
|
+
export { buildLocalizedUrl, i18nPlugin, splitUrlTarget, useModernI18n };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { resolveCanonicalLocalisedPath, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
|
|
3
|
+
import { useModernI18n } from "./context.mjs";
|
|
4
|
+
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
5
|
+
import { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
|
|
6
|
+
const localizePath = (pathname, language, config)=>buildLocalizedUrl(pathname, language, config.languages, config.localisedUrls);
|
|
7
|
+
const canonicalPath = (target, config)=>{
|
|
8
|
+
const { pathname, search, hash } = splitUrlTarget(target);
|
|
9
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
10
|
+
const pathWithoutLanguage = segments.length > 0 && config.languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : pathname || '/';
|
|
11
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(config.localisedUrls);
|
|
12
|
+
const resolvedPath = localisedUrlsConfig.enabled ? resolveCanonicalLocalisedPath(pathWithoutLanguage, config.languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
13
|
+
return `${resolvedPath}${search}${hash}`;
|
|
14
|
+
};
|
|
15
|
+
const useLocalizedPaths = ()=>{
|
|
16
|
+
const { supportedLanguages, localisedUrls } = useModernI18n();
|
|
17
|
+
return useMemo(()=>{
|
|
18
|
+
const config = {
|
|
19
|
+
languages: supportedLanguages,
|
|
20
|
+
localisedUrls
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
localizePath: (pathname, language)=>localizePath(pathname, language, config),
|
|
24
|
+
canonicalPath: (pathname)=>canonicalPath(pathname, config)
|
|
25
|
+
};
|
|
26
|
+
}, [
|
|
27
|
+
supportedLanguages,
|
|
28
|
+
localisedUrls
|
|
29
|
+
]);
|
|
30
|
+
};
|
|
31
|
+
const useLocalizedLocation = ()=>{
|
|
32
|
+
const { language, supportedLanguages, localisedUrls } = useModernI18n();
|
|
33
|
+
const { location } = useI18nRouterAdapter();
|
|
34
|
+
const pathname = location?.pathname ?? '/';
|
|
35
|
+
const search = location?.search ?? '';
|
|
36
|
+
const hash = location?.hash ?? '';
|
|
37
|
+
return useMemo(()=>{
|
|
38
|
+
const config = {
|
|
39
|
+
languages: supportedLanguages,
|
|
40
|
+
localisedUrls
|
|
41
|
+
};
|
|
42
|
+
const alternates = {};
|
|
43
|
+
for (const supportedLanguage of supportedLanguages)alternates[supportedLanguage] = `${localizePath(pathname, supportedLanguage, config)}${search}${hash}`;
|
|
44
|
+
return {
|
|
45
|
+
language,
|
|
46
|
+
canonical: canonicalPath(pathname, config),
|
|
47
|
+
alternates
|
|
48
|
+
};
|
|
49
|
+
}, [
|
|
50
|
+
language,
|
|
51
|
+
supportedLanguages,
|
|
52
|
+
localisedUrls,
|
|
53
|
+
pathname,
|
|
54
|
+
search,
|
|
55
|
+
hash
|
|
56
|
+
]);
|
|
57
|
+
};
|
|
58
|
+
export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths };
|
|
@@ -16,16 +16,30 @@ const getLanguageFromPath = (pathname, languages, fallbackLanguage)=>{
|
|
|
16
16
|
if (languages.includes(firstSegment)) return firstSegment;
|
|
17
17
|
return fallbackLanguage;
|
|
18
18
|
};
|
|
19
|
-
const
|
|
19
|
+
const splitUrlTarget = (target)=>{
|
|
20
|
+
const hashIndex = target.indexOf('#');
|
|
21
|
+
const hash = hashIndex >= 0 ? target.slice(hashIndex) : '';
|
|
22
|
+
const beforeHash = hashIndex >= 0 ? target.slice(0, hashIndex) : target;
|
|
23
|
+
const searchIndex = beforeHash.indexOf('?');
|
|
24
|
+
const search = searchIndex >= 0 ? beforeHash.slice(searchIndex) : '';
|
|
25
|
+
const pathname = searchIndex >= 0 ? beforeHash.slice(0, searchIndex) : beforeHash;
|
|
26
|
+
return {
|
|
27
|
+
pathname,
|
|
28
|
+
search,
|
|
29
|
+
hash
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const buildLocalizedUrl = (target, language, languages, localisedUrls)=>{
|
|
33
|
+
const { pathname, search, hash } = splitUrlTarget(target);
|
|
20
34
|
const segments = pathname.split('/').filter(Boolean);
|
|
21
35
|
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
22
|
-
const pathWithoutLanguage = segments.length > 0 && languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : pathname;
|
|
36
|
+
const pathWithoutLanguage = segments.length > 0 && languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : pathname || '/';
|
|
23
37
|
const resolvedPath = localisedUrlsConfig.enabled ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
24
38
|
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
25
39
|
return `/${[
|
|
26
40
|
language,
|
|
27
41
|
...resolvedSegments
|
|
28
|
-
].join('/')}`;
|
|
42
|
+
].join('/')}${search}${hash}`;
|
|
29
43
|
};
|
|
30
44
|
const detectLanguageFromPath = (pathname, languages, localePathRedirect)=>{
|
|
31
45
|
if (!localePathRedirect) return {
|
|
@@ -53,4 +67,4 @@ const shouldIgnoreRedirect = (pathname, languages, ignoreRedirectRoutes)=>{
|
|
|
53
67
|
if ('function' == typeof ignoreRedirectRoutes) return ignoreRedirectRoutes(normalizedPath);
|
|
54
68
|
return ignoreRedirectRoutes.some((pattern)=>normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`));
|
|
55
69
|
};
|
|
56
|
-
export { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getLanguageFromPath, getPathname, shouldIgnoreRedirect };
|
|
70
|
+
export { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getLanguageFromPath, getPathname, shouldIgnoreRedirect, splitUrlTarget };
|
|
@@ -100,7 +100,7 @@ const transformLocalisedRoute = (route, parentCanonicalPath, parentLocalisedPath
|
|
|
100
100
|
if (!localisedUrlEntry) return [
|
|
101
101
|
baseRoute
|
|
102
102
|
];
|
|
103
|
-
return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index));
|
|
103
|
+
return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index, canonicalPath));
|
|
104
104
|
};
|
|
105
105
|
const legalRouteIdPart = (value)=>value.replace(/[^a-zA-Z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'index';
|
|
106
106
|
const suffixRouteIds = (route, suffix)=>{
|
|
@@ -115,13 +115,14 @@ const suffixRouteIds = (route, suffix)=>{
|
|
|
115
115
|
} : {}
|
|
116
116
|
};
|
|
117
117
|
};
|
|
118
|
-
const cloneRouteWithLocalisedPath = (route, path, index)=>{
|
|
118
|
+
const cloneRouteWithLocalisedPath = (route, path, index, canonicalPath)=>{
|
|
119
119
|
const leadingLocaleParam = getLeadingLocaleParam(route.path);
|
|
120
120
|
const localisedPath = leadingLocaleParam ? normaliseRoutePath(`${leadingLocaleParam}/${path}`) : path;
|
|
121
121
|
const routeWithPath = {
|
|
122
122
|
...route,
|
|
123
123
|
path: localisedPath
|
|
124
124
|
};
|
|
125
|
+
routeWithPath.modernCanonicalPath = canonicalPath;
|
|
125
126
|
return 0 === index ? routeWithPath : suffixRouteIds(routeWithPath, legalRouteIdPart(localisedPath));
|
|
126
127
|
};
|
|
127
128
|
const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
|
|
@@ -177,6 +178,12 @@ const buildPathFromPattern = (pattern, params)=>{
|
|
|
177
178
|
};
|
|
178
179
|
const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls)=>{
|
|
179
180
|
const normalizedPathname = normalisePathPattern(pathname);
|
|
181
|
+
for (const [canonicalPattern, localisedUrlEntry] of Object.entries(localisedUrls)){
|
|
182
|
+
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
183
|
+
if (!targetPattern) continue;
|
|
184
|
+
const params = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
185
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
186
|
+
}
|
|
180
187
|
for (const localisedUrlEntry of Object.values(localisedUrls)){
|
|
181
188
|
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
182
189
|
if (targetPattern) for (const language of languages){
|
|
@@ -188,4 +195,18 @@ const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls
|
|
|
188
195
|
}
|
|
189
196
|
return normalizedPathname;
|
|
190
197
|
};
|
|
191
|
-
|
|
198
|
+
const resolveCanonicalLocalisedPath = (pathname, languages, localisedUrls)=>{
|
|
199
|
+
const normalizedPathname = normalisePathPattern(pathname);
|
|
200
|
+
for (const [canonicalPattern, localisedUrlEntry] of Object.entries(localisedUrls)){
|
|
201
|
+
const canonicalParams = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
202
|
+
if (canonicalParams) return buildPathFromPattern(canonicalPattern, canonicalParams);
|
|
203
|
+
for (const language of languages){
|
|
204
|
+
const sourcePattern = localisedUrlEntry[language];
|
|
205
|
+
if (!sourcePattern) continue;
|
|
206
|
+
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
207
|
+
if (params) return buildPathFromPattern(canonicalPattern, params);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return normalizedPathname;
|
|
211
|
+
};
|
|
212
|
+
export { applyLocalisedUrlsToRoutes, buildPathFromPattern, matchPathPattern, normalisePathPattern, resolveCanonicalLocalisedPath, resolveLocalisedPath, resolveLocalisedUrlsConfig, validateLocalisedUrls };
|