@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.120 → 3.2.0-ultramodern.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/runtime/Link.js +33 -21
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +42 -8
- package/dist/cjs/runtime/localizedPaths.js +1 -4
- package/dist/cjs/runtime/routerAdapter.js +2 -2
- package/dist/cjs/runtime/utils.js +2 -9
- package/dist/cjs/server/index.js +1 -9
- package/dist/cjs/shared/localisedUrls.js +107 -27
- package/dist/esm/runtime/Link.mjs +33 -21
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +24 -3
- package/dist/esm/runtime/localizedPaths.mjs +2 -5
- package/dist/esm/runtime/routerAdapter.mjs +3 -3
- package/dist/esm/runtime/utils.mjs +3 -10
- package/dist/esm/server/index.mjs +2 -10
- package/dist/esm/shared/localisedUrls.mjs +99 -28
- package/dist/esm-node/runtime/Link.mjs +33 -21
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +24 -3
- package/dist/esm-node/runtime/localizedPaths.mjs +2 -5
- package/dist/esm-node/runtime/routerAdapter.mjs +3 -3
- package/dist/esm-node/runtime/utils.mjs +3 -10
- package/dist/esm-node/server/index.mjs +2 -10
- package/dist/esm-node/shared/localisedUrls.mjs +99 -28
- package/dist/types/runtime/Link.d.ts +10 -0
- package/dist/types/runtime/i18n/backend/defaults.node.d.ts +3 -2
- package/dist/types/runtime/utils.d.ts +2 -2
- package/dist/types/shared/localisedUrls.d.ts +15 -0
- package/dist/types/shared/type.d.ts +7 -5
- package/package.json +16 -12
- package/rstest.config.mts +6 -1
- package/src/runtime/Link.tsx +28 -12
- package/src/runtime/i18n/backend/defaults.node.ts +40 -2
- package/src/runtime/localizedPaths.ts +6 -17
- package/src/runtime/routerAdapter.tsx +4 -5
- package/src/runtime/utils.ts +11 -23
- package/src/server/index.ts +7 -17
- package/src/shared/localisedUrls.ts +212 -42
- package/src/shared/type.ts +7 -5
- package/tests/backendDefaults.test.ts +51 -0
- package/tests/i18nUtils.test.ts +10 -3
- package/tests/link.test.tsx +51 -1
- package/tests/localisedUrls.test.ts +224 -0
- package/tests/routerAdapter.test.tsx +12 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_I18NEXT_DETECTION_OPTIONS, mergeDetectionOptions } from "../runtime/i18n/detection/config.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { localiseTargetPathname, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
|
|
3
3
|
import { getLocaleDetectionOptions } from "../shared/utils.mjs";
|
|
4
4
|
import * as __rspack_external__modern_js_server_core_hono_a76ca254 from "@modern-js/server-core/hono";
|
|
5
5
|
const { languageDetector: languageDetector } = __rspack_external__modern_js_server_core_hono_a76ca254;
|
|
@@ -127,15 +127,7 @@ const buildLocalizedUrl = (req, urlPath, language, languages, localisedUrls)=>{
|
|
|
127
127
|
const pathname = url.pathname;
|
|
128
128
|
const basePath = urlPath.replace('/*', '');
|
|
129
129
|
const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
130
|
-
const
|
|
131
|
-
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
132
|
-
const pathWithoutLanguage = segments.length > 0 && languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : remainingPath;
|
|
133
|
-
const resolvedPath = localisedUrlsConfig.enabled ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
134
|
-
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
135
|
-
const newPathname = `/${[
|
|
136
|
-
language,
|
|
137
|
-
...resolvedSegments
|
|
138
|
-
].join('/')}`;
|
|
130
|
+
const newPathname = localiseTargetPathname(remainingPath, language, languages, localisedUrls);
|
|
139
131
|
const suffix = `${url.search}${url.hash}`;
|
|
140
132
|
const localizedUrl = '/' === basePath ? newPathname + suffix : basePath + newPathname + suffix;
|
|
141
133
|
return localizedUrl;
|
|
@@ -3,12 +3,13 @@ const LOCALE_PARAM_NAMES = new Set([
|
|
|
3
3
|
'locale',
|
|
4
4
|
'language'
|
|
5
5
|
]);
|
|
6
|
-
const
|
|
6
|
+
const normaliseSlashes = (path)=>{
|
|
7
7
|
const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
|
|
8
8
|
const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
|
|
9
|
-
|
|
10
|
-
return withoutTrailingSlash.replace(/\[(.+?)\]/g, ':$1');
|
|
9
|
+
return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash;
|
|
11
10
|
};
|
|
11
|
+
const normalisePathPattern = (path)=>normaliseSlashes(path).replace(/\[(.+?)\]/g, ':$1');
|
|
12
|
+
const normalisePathname = (pathname)=>normaliseSlashes(pathname);
|
|
12
13
|
const normaliseRoutePath = (path)=>{
|
|
13
14
|
const normalized = normalisePathPattern(path);
|
|
14
15
|
return '/' === normalized ? '' : normalized.slice(1);
|
|
@@ -34,16 +35,12 @@ const getLeadingLocaleParam = (path)=>{
|
|
|
34
35
|
return getLocaleParamSegment(segments[0] || '');
|
|
35
36
|
};
|
|
36
37
|
const resolveLocalisedUrlsConfig = (option)=>{
|
|
37
|
-
if (
|
|
38
|
-
enabled: false,
|
|
39
|
-
map: {}
|
|
40
|
-
};
|
|
41
|
-
if (option && 'object' == typeof option) return {
|
|
38
|
+
if (option && 'object' == typeof option && Object.keys(option).length > 0) return {
|
|
42
39
|
enabled: true,
|
|
43
40
|
map: option
|
|
44
41
|
};
|
|
45
42
|
return {
|
|
46
|
-
enabled:
|
|
43
|
+
enabled: false,
|
|
47
44
|
map: {}
|
|
48
45
|
};
|
|
49
46
|
};
|
|
@@ -135,9 +132,13 @@ const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
|
|
|
135
132
|
};
|
|
136
133
|
const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
137
134
|
const getParamName = (segment)=>segment.slice(1).replace(/\?$/, '');
|
|
135
|
+
const compiledPathPatternCache = new Map();
|
|
138
136
|
const compilePathPattern = (pattern)=>{
|
|
137
|
+
const normalizedPattern = normalisePathPattern(pattern);
|
|
138
|
+
const cached = compiledPathPatternCache.get(normalizedPattern);
|
|
139
|
+
if (cached) return cached;
|
|
139
140
|
const names = [];
|
|
140
|
-
const segments =
|
|
141
|
+
const segments = normalizedPattern.split('/').filter(Boolean);
|
|
141
142
|
const source = segments.map((segment)=>{
|
|
142
143
|
if (segment.startsWith(':')) {
|
|
143
144
|
names.push(getParamName(segment));
|
|
@@ -150,19 +151,55 @@ const compilePathPattern = (pattern)=>{
|
|
|
150
151
|
}
|
|
151
152
|
return `/${escapeRegExp(segment)}`;
|
|
152
153
|
}).join('');
|
|
153
|
-
|
|
154
|
+
const compiled = {
|
|
154
155
|
names,
|
|
155
156
|
regexp: new RegExp(`^${source || '/'}$`)
|
|
156
157
|
};
|
|
158
|
+
compiledPathPatternCache.set(normalizedPattern, compiled);
|
|
159
|
+
return compiled;
|
|
160
|
+
};
|
|
161
|
+
const getPatternSpecificity = (pattern)=>{
|
|
162
|
+
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
163
|
+
let staticSegments = 0;
|
|
164
|
+
let dynamicSegments = 0;
|
|
165
|
+
let splatSegments = 0;
|
|
166
|
+
for (const segment of segments)if ('*' === segment) splatSegments++;
|
|
167
|
+
else if (segment.startsWith(':')) dynamicSegments++;
|
|
168
|
+
else staticSegments++;
|
|
169
|
+
return {
|
|
170
|
+
staticSegments,
|
|
171
|
+
dynamicSegments,
|
|
172
|
+
splatSegments,
|
|
173
|
+
totalSegments: segments.length
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
const comparePatternSpecificity = (left, right)=>{
|
|
177
|
+
const a = getPatternSpecificity(left);
|
|
178
|
+
const b = getPatternSpecificity(right);
|
|
179
|
+
return b.staticSegments - a.staticSegments || b.totalSegments - a.totalSegments || a.splatSegments - b.splatSegments || a.dynamicSegments - b.dynamicSegments;
|
|
180
|
+
};
|
|
181
|
+
const sortPatternsBySpecificity = (patterns)=>patterns.map((pattern, index)=>({
|
|
182
|
+
pattern,
|
|
183
|
+
index
|
|
184
|
+
})).sort((left, right)=>comparePatternSpecificity(left.pattern.pattern, right.pattern.pattern) || left.index - right.index).map(({ pattern })=>pattern);
|
|
185
|
+
const decodePathParam = (value)=>{
|
|
186
|
+
try {
|
|
187
|
+
return decodeURIComponent(value);
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
157
191
|
};
|
|
158
192
|
const matchPathPattern = (pathname, pattern)=>{
|
|
159
193
|
const { names, regexp } = compilePathPattern(pattern);
|
|
160
|
-
const match = regexp.exec(
|
|
194
|
+
const match = regexp.exec(normalisePathname(pathname));
|
|
161
195
|
if (!match) return null;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
const params = {};
|
|
197
|
+
for(let index = 0; index < names.length; index++){
|
|
198
|
+
const decoded = decodePathParam(match[index + 1] || '');
|
|
199
|
+
if (null === decoded) return null;
|
|
200
|
+
params[names[index]] = decoded;
|
|
201
|
+
}
|
|
202
|
+
return params;
|
|
166
203
|
};
|
|
167
204
|
const buildPathFromPattern = (pattern, params)=>{
|
|
168
205
|
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
@@ -177,27 +214,41 @@ const buildPathFromPattern = (pattern, params)=>{
|
|
|
177
214
|
return `/${path}`;
|
|
178
215
|
};
|
|
179
216
|
const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls)=>{
|
|
180
|
-
const normalizedPathname =
|
|
181
|
-
|
|
217
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
218
|
+
const canonicalCandidates = sortPatternsBySpecificity(Object.entries(localisedUrls).map(([canonicalPattern, localisedUrlEntry])=>({
|
|
219
|
+
pattern: canonicalPattern,
|
|
220
|
+
canonicalPattern,
|
|
221
|
+
localisedUrlEntry
|
|
222
|
+
})));
|
|
223
|
+
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
|
|
182
224
|
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
183
225
|
if (!targetPattern) continue;
|
|
184
226
|
const params = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
185
227
|
if (params) return buildPathFromPattern(targetPattern, params);
|
|
186
228
|
}
|
|
187
|
-
|
|
229
|
+
const localisedCandidates = sortPatternsBySpecificity(Object.values(localisedUrls).flatMap((localisedUrlEntry)=>{
|
|
188
230
|
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
189
|
-
if (targetPattern)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
231
|
+
if (!targetPattern) return [];
|
|
232
|
+
return languages.map((language)=>localisedUrlEntry[language]).filter((sourcePattern)=>Boolean(sourcePattern)).map((sourcePattern)=>({
|
|
233
|
+
pattern: sourcePattern,
|
|
234
|
+
sourcePattern,
|
|
235
|
+
targetPattern
|
|
236
|
+
}));
|
|
237
|
+
}));
|
|
238
|
+
for (const { sourcePattern, targetPattern } of localisedCandidates){
|
|
239
|
+
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
240
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
195
241
|
}
|
|
196
242
|
return normalizedPathname;
|
|
197
243
|
};
|
|
198
244
|
const resolveCanonicalLocalisedPath = (pathname, languages, localisedUrls)=>{
|
|
199
|
-
const normalizedPathname =
|
|
200
|
-
|
|
245
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
246
|
+
const canonicalCandidates = sortPatternsBySpecificity(Object.entries(localisedUrls).map(([canonicalPattern, localisedUrlEntry])=>({
|
|
247
|
+
pattern: canonicalPattern,
|
|
248
|
+
canonicalPattern,
|
|
249
|
+
localisedUrlEntry
|
|
250
|
+
})));
|
|
251
|
+
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
|
|
201
252
|
const canonicalParams = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
202
253
|
if (canonicalParams) return buildPathFromPattern(canonicalPattern, canonicalParams);
|
|
203
254
|
for (const language of languages){
|
|
@@ -209,4 +260,24 @@ const resolveCanonicalLocalisedPath = (pathname, languages, localisedUrls)=>{
|
|
|
209
260
|
}
|
|
210
261
|
return normalizedPathname;
|
|
211
262
|
};
|
|
212
|
-
|
|
263
|
+
const stripLanguagePrefix = (pathname, languages)=>{
|
|
264
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
265
|
+
if (segments.length > 0 && languages.includes(segments[0])) return `/${segments.slice(1).join('/')}`;
|
|
266
|
+
return pathname || '/';
|
|
267
|
+
};
|
|
268
|
+
const localiseTargetPathname = (pathname, language, languages, localisedUrls)=>{
|
|
269
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
270
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
271
|
+
const resolvedPath = localisedUrlsConfig.enabled ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
272
|
+
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
273
|
+
return `/${[
|
|
274
|
+
language,
|
|
275
|
+
...resolvedSegments
|
|
276
|
+
].join('/')}`;
|
|
277
|
+
};
|
|
278
|
+
const canonicalTargetPathname = (pathname, languages, localisedUrls)=>{
|
|
279
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
280
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
281
|
+
return localisedUrlsConfig.enabled ? resolveCanonicalLocalisedPath(pathWithoutLanguage, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
282
|
+
};
|
|
283
|
+
export { applyLocalisedUrlsToRoutes, buildPathFromPattern, canonicalTargetPathname, localiseTargetPathname, matchPathPattern, normalisePathPattern, normalisePathname, resolveCanonicalLocalisedPath, resolveLocalisedPath, resolveLocalisedUrlsConfig, validateLocalisedUrls };
|
|
@@ -89,7 +89,7 @@ const mergeClassNames = (...values)=>{
|
|
|
89
89
|
return classNames.length > 0 ? classNames.join(' ') : void 0;
|
|
90
90
|
};
|
|
91
91
|
const Link = (props)=>{
|
|
92
|
-
const { to, params, children, hash: hashProp, search: searchProp, hashScrollIntoView, activeOptions, activeProps, ...rest } = props;
|
|
92
|
+
const { to, params, children, hash: hashProp, search: searchProp, hashScrollIntoView, activeOptions, activeProps, prefetch, preload, ...rest } = props;
|
|
93
93
|
const adapter = useI18nRouterAdapter();
|
|
94
94
|
const { language, supportedLanguages, localisedUrls } = useModernI18n();
|
|
95
95
|
const config = {
|
|
@@ -147,7 +147,7 @@ const Link = (props)=>{
|
|
|
147
147
|
'aria-current': rest['aria-current'] ?? resolvedActiveProps['aria-current'] ?? 'page'
|
|
148
148
|
} : {};
|
|
149
149
|
if (!target) {
|
|
150
|
-
const {
|
|
150
|
+
const { replace: _replace, ...anchorProps } = rest;
|
|
151
151
|
return /*#__PURE__*/ jsx("a", {
|
|
152
152
|
href: to,
|
|
153
153
|
...anchorProps,
|
|
@@ -156,7 +156,7 @@ const Link = (props)=>{
|
|
|
156
156
|
}
|
|
157
157
|
const { Link: RouterLink, hasRouter, framework } = adapter;
|
|
158
158
|
if (!hasRouter || !RouterLink) {
|
|
159
|
-
const {
|
|
159
|
+
const { replace: _replace, ...anchorProps } = rest;
|
|
160
160
|
const { className: activeClassName, style: activeStyle, ...activeRest } = resolvedActiveProps;
|
|
161
161
|
return /*#__PURE__*/ jsx("a", {
|
|
162
162
|
href: target.href,
|
|
@@ -177,26 +177,38 @@ const Link = (props)=>{
|
|
|
177
177
|
...rest.style,
|
|
178
178
|
...activeStyle
|
|
179
179
|
};
|
|
180
|
-
if ('tanstack' === framework)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
180
|
+
if ('tanstack' === framework) {
|
|
181
|
+
const tanstackPreload = void 0 !== preload ? preload : void 0 === prefetch ? void 0 : 'none' === prefetch ? false : prefetch;
|
|
182
|
+
return /*#__PURE__*/ jsx(RouterLink, {
|
|
183
|
+
to: target.localizedPathname,
|
|
184
|
+
...target.searchObject ? {
|
|
185
|
+
search: target.searchObject
|
|
186
|
+
} : {},
|
|
187
|
+
...target.hash ? {
|
|
188
|
+
hash: target.hash
|
|
189
|
+
} : {},
|
|
190
|
+
...void 0 === hashScrollIntoView ? {} : {
|
|
191
|
+
hashScrollIntoView
|
|
192
|
+
},
|
|
193
|
+
...void 0 === tanstackPreload ? {} : {
|
|
194
|
+
preload: tanstackPreload
|
|
195
|
+
},
|
|
196
|
+
...rest,
|
|
197
|
+
...activeRest,
|
|
198
|
+
...activeAttributes,
|
|
199
|
+
className: mergedClassName,
|
|
200
|
+
style: mergedStyle,
|
|
201
|
+
children: children
|
|
202
|
+
});
|
|
203
|
+
}
|
|
198
204
|
return /*#__PURE__*/ jsx(RouterLink, {
|
|
199
205
|
to: target.href,
|
|
206
|
+
...void 0 === prefetch ? {} : {
|
|
207
|
+
prefetch
|
|
208
|
+
},
|
|
209
|
+
...void 0 === preload ? {} : {
|
|
210
|
+
preload
|
|
211
|
+
},
|
|
200
212
|
...rest,
|
|
201
213
|
...activeRest,
|
|
202
214
|
...activeAttributes,
|
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path_0 from "path";
|
|
4
|
+
const CONVENTIONAL_LOCALES_DIRS = [
|
|
5
|
+
'./locales',
|
|
6
|
+
'./config/public/locales'
|
|
7
|
+
];
|
|
8
|
+
const isDirectory = (dirPath)=>{
|
|
9
|
+
try {
|
|
10
|
+
return fs.statSync(dirPath).isDirectory();
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const resolveDefaultLocalesDir = (cwd = process.cwd())=>{
|
|
16
|
+
for (const dir of CONVENTIONAL_LOCALES_DIRS)if (isDirectory(path_0.resolve(cwd, dir))) return dir;
|
|
17
|
+
return CONVENTIONAL_LOCALES_DIRS[0];
|
|
18
|
+
};
|
|
2
19
|
const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
|
|
3
|
-
loadPath
|
|
4
|
-
|
|
20
|
+
get loadPath () {
|
|
21
|
+
return `${resolveDefaultLocalesDir()}/{{lng}}/{{ns}}.json`;
|
|
22
|
+
},
|
|
23
|
+
get addPath () {
|
|
24
|
+
return `${resolveDefaultLocalesDir()}/{{lng}}/{{ns}}.json`;
|
|
25
|
+
}
|
|
5
26
|
};
|
|
6
27
|
function convertPath(path) {
|
|
7
28
|
if (!path) return path;
|
|
@@ -17,4 +38,4 @@ function convertBackendOptions(options) {
|
|
|
17
38
|
if (converted.addPath) converted.addPath = convertPath(converted.addPath);
|
|
18
39
|
return converted;
|
|
19
40
|
}
|
|
20
|
-
export { DEFAULT_I18NEXT_BACKEND_OPTIONS, convertBackendOptions };
|
|
41
|
+
export { DEFAULT_I18NEXT_BACKEND_OPTIONS, convertBackendOptions, resolveDefaultLocalesDir };
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { canonicalTargetPathname } from "../shared/localisedUrls.mjs";
|
|
4
4
|
import { useModernI18n } from "./context.mjs";
|
|
5
5
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
6
6
|
import { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
|
|
7
7
|
const localizePath = (pathname, language, config)=>buildLocalizedUrl(pathname, language, config.languages, config.localisedUrls);
|
|
8
8
|
const canonicalPath = (target, config)=>{
|
|
9
9
|
const { pathname, search, hash } = splitUrlTarget(target);
|
|
10
|
-
const
|
|
11
|
-
const pathWithoutLanguage = segments.length > 0 && config.languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : pathname || '/';
|
|
12
|
-
const localisedUrlsConfig = resolveLocalisedUrlsConfig(config.localisedUrls);
|
|
13
|
-
const resolvedPath = localisedUrlsConfig.enabled ? resolveCanonicalLocalisedPath(pathWithoutLanguage, config.languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
10
|
+
const resolvedPath = canonicalTargetPathname(pathname, config.languages, config.localisedUrls);
|
|
14
11
|
return `${resolvedPath}${search}${hash}`;
|
|
15
12
|
};
|
|
16
13
|
const useLocalizedPaths = ()=>{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { RuntimeContext, isBrowser } from "@modern-js/runtime";
|
|
3
|
-
import { InternalRuntimeContext } from "@modern-js/runtime/context";
|
|
3
|
+
import { InternalRuntimeContext, getRouterRuntimeState } from "@modern-js/runtime/context";
|
|
4
4
|
import { Link as router_Link, useInRouterContext, useLocation, useNavigate, useParams } from "@modern-js/runtime/router";
|
|
5
5
|
import { useCallback, useContext, useEffect, useState } from "react";
|
|
6
6
|
const normalizeUrlPart = (value, prefix)=>{
|
|
@@ -26,7 +26,7 @@ const getWindowLocation = ()=>{
|
|
|
26
26
|
};
|
|
27
27
|
};
|
|
28
28
|
const getRouterFramework = (runtimeContext, internalContext, inReactRouter)=>{
|
|
29
|
-
const framework = internalContext
|
|
29
|
+
const framework = getRouterRuntimeState(internalContext)?.framework || getRouterRuntimeState(runtimeContext)?.framework;
|
|
30
30
|
if (framework) return framework;
|
|
31
31
|
if (internalContext.router?.useRouter || runtimeContext.router?.useRouter) return 'tanstack';
|
|
32
32
|
if (internalContext.router?.useLocation || internalContext.router?.useHref || runtimeContext.router?.useLocation || runtimeContext.router?.useHref) return 'react-router';
|
|
@@ -34,7 +34,7 @@ const getRouterFramework = (runtimeContext, internalContext, inReactRouter)=>{
|
|
|
34
34
|
};
|
|
35
35
|
const getRouterInstance = (internalContext, contextRouter)=>{
|
|
36
36
|
if (contextRouter) return contextRouter;
|
|
37
|
-
const router = internalContext
|
|
37
|
+
const router = getRouterRuntimeState(internalContext)?.instance;
|
|
38
38
|
if (!router || 'object' != typeof router) return null;
|
|
39
39
|
return router;
|
|
40
40
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { isBrowser } from "@modern-js/runtime";
|
|
3
3
|
import { getGlobalBasename } from "@modern-js/runtime/context";
|
|
4
|
-
import {
|
|
4
|
+
import { localiseTargetPathname } from "../shared/localisedUrls.mjs";
|
|
5
5
|
const getPathname = (context)=>{
|
|
6
6
|
if (isBrowser()) return window.location.pathname;
|
|
7
7
|
return context.ssrContext?.request?.pathname || '/';
|
|
@@ -32,15 +32,8 @@ const splitUrlTarget = (target)=>{
|
|
|
32
32
|
};
|
|
33
33
|
const buildLocalizedUrl = (target, language, languages, localisedUrls)=>{
|
|
34
34
|
const { pathname, search, hash } = splitUrlTarget(target);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const pathWithoutLanguage = segments.length > 0 && languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : pathname || '/';
|
|
38
|
-
const resolvedPath = localisedUrlsConfig.enabled ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
39
|
-
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
40
|
-
return `/${[
|
|
41
|
-
language,
|
|
42
|
-
...resolvedSegments
|
|
43
|
-
].join('/')}${search}${hash}`;
|
|
35
|
+
const localizedPathname = localiseTargetPathname(pathname, language, languages, localisedUrls);
|
|
36
|
+
return `${localizedPathname}${search}${hash}`;
|
|
44
37
|
};
|
|
45
38
|
const detectLanguageFromPath = (pathname, languages, localePathRedirect)=>{
|
|
46
39
|
if (!localePathRedirect) return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { DEFAULT_I18NEXT_DETECTION_OPTIONS, mergeDetectionOptions } from "../runtime/i18n/detection/config.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { localiseTargetPathname, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
|
|
4
4
|
import { getLocaleDetectionOptions } from "../shared/utils.mjs";
|
|
5
5
|
import * as __rspack_external__modern_js_server_core_hono_a76ca254 from "@modern-js/server-core/hono";
|
|
6
6
|
const { languageDetector: languageDetector } = __rspack_external__modern_js_server_core_hono_a76ca254;
|
|
@@ -128,15 +128,7 @@ const buildLocalizedUrl = (req, urlPath, language, languages, localisedUrls)=>{
|
|
|
128
128
|
const pathname = url.pathname;
|
|
129
129
|
const basePath = urlPath.replace('/*', '');
|
|
130
130
|
const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
131
|
-
const
|
|
132
|
-
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
133
|
-
const pathWithoutLanguage = segments.length > 0 && languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : remainingPath;
|
|
134
|
-
const resolvedPath = localisedUrlsConfig.enabled ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
135
|
-
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
136
|
-
const newPathname = `/${[
|
|
137
|
-
language,
|
|
138
|
-
...resolvedSegments
|
|
139
|
-
].join('/')}`;
|
|
131
|
+
const newPathname = localiseTargetPathname(remainingPath, language, languages, localisedUrls);
|
|
140
132
|
const suffix = `${url.search}${url.hash}`;
|
|
141
133
|
const localizedUrl = '/' === basePath ? newPathname + suffix : basePath + newPathname + suffix;
|
|
142
134
|
return localizedUrl;
|
|
@@ -4,12 +4,13 @@ const LOCALE_PARAM_NAMES = new Set([
|
|
|
4
4
|
'locale',
|
|
5
5
|
'language'
|
|
6
6
|
]);
|
|
7
|
-
const
|
|
7
|
+
const normaliseSlashes = (path)=>{
|
|
8
8
|
const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
|
|
9
9
|
const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
|
|
10
|
-
|
|
11
|
-
return withoutTrailingSlash.replace(/\[(.+?)\]/g, ':$1');
|
|
10
|
+
return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash;
|
|
12
11
|
};
|
|
12
|
+
const normalisePathPattern = (path)=>normaliseSlashes(path).replace(/\[(.+?)\]/g, ':$1');
|
|
13
|
+
const normalisePathname = (pathname)=>normaliseSlashes(pathname);
|
|
13
14
|
const normaliseRoutePath = (path)=>{
|
|
14
15
|
const normalized = normalisePathPattern(path);
|
|
15
16
|
return '/' === normalized ? '' : normalized.slice(1);
|
|
@@ -35,16 +36,12 @@ const getLeadingLocaleParam = (path)=>{
|
|
|
35
36
|
return getLocaleParamSegment(segments[0] || '');
|
|
36
37
|
};
|
|
37
38
|
const resolveLocalisedUrlsConfig = (option)=>{
|
|
38
|
-
if (
|
|
39
|
-
enabled: false,
|
|
40
|
-
map: {}
|
|
41
|
-
};
|
|
42
|
-
if (option && 'object' == typeof option) return {
|
|
39
|
+
if (option && 'object' == typeof option && Object.keys(option).length > 0) return {
|
|
43
40
|
enabled: true,
|
|
44
41
|
map: option
|
|
45
42
|
};
|
|
46
43
|
return {
|
|
47
|
-
enabled:
|
|
44
|
+
enabled: false,
|
|
48
45
|
map: {}
|
|
49
46
|
};
|
|
50
47
|
};
|
|
@@ -136,9 +133,13 @@ const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
|
|
|
136
133
|
};
|
|
137
134
|
const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
138
135
|
const getParamName = (segment)=>segment.slice(1).replace(/\?$/, '');
|
|
136
|
+
const compiledPathPatternCache = new Map();
|
|
139
137
|
const compilePathPattern = (pattern)=>{
|
|
138
|
+
const normalizedPattern = normalisePathPattern(pattern);
|
|
139
|
+
const cached = compiledPathPatternCache.get(normalizedPattern);
|
|
140
|
+
if (cached) return cached;
|
|
140
141
|
const names = [];
|
|
141
|
-
const segments =
|
|
142
|
+
const segments = normalizedPattern.split('/').filter(Boolean);
|
|
142
143
|
const source = segments.map((segment)=>{
|
|
143
144
|
if (segment.startsWith(':')) {
|
|
144
145
|
names.push(getParamName(segment));
|
|
@@ -151,19 +152,55 @@ const compilePathPattern = (pattern)=>{
|
|
|
151
152
|
}
|
|
152
153
|
return `/${escapeRegExp(segment)}`;
|
|
153
154
|
}).join('');
|
|
154
|
-
|
|
155
|
+
const compiled = {
|
|
155
156
|
names,
|
|
156
157
|
regexp: new RegExp(`^${source || '/'}$`)
|
|
157
158
|
};
|
|
159
|
+
compiledPathPatternCache.set(normalizedPattern, compiled);
|
|
160
|
+
return compiled;
|
|
161
|
+
};
|
|
162
|
+
const getPatternSpecificity = (pattern)=>{
|
|
163
|
+
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
164
|
+
let staticSegments = 0;
|
|
165
|
+
let dynamicSegments = 0;
|
|
166
|
+
let splatSegments = 0;
|
|
167
|
+
for (const segment of segments)if ('*' === segment) splatSegments++;
|
|
168
|
+
else if (segment.startsWith(':')) dynamicSegments++;
|
|
169
|
+
else staticSegments++;
|
|
170
|
+
return {
|
|
171
|
+
staticSegments,
|
|
172
|
+
dynamicSegments,
|
|
173
|
+
splatSegments,
|
|
174
|
+
totalSegments: segments.length
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const comparePatternSpecificity = (left, right)=>{
|
|
178
|
+
const a = getPatternSpecificity(left);
|
|
179
|
+
const b = getPatternSpecificity(right);
|
|
180
|
+
return b.staticSegments - a.staticSegments || b.totalSegments - a.totalSegments || a.splatSegments - b.splatSegments || a.dynamicSegments - b.dynamicSegments;
|
|
181
|
+
};
|
|
182
|
+
const sortPatternsBySpecificity = (patterns)=>patterns.map((pattern, index)=>({
|
|
183
|
+
pattern,
|
|
184
|
+
index
|
|
185
|
+
})).sort((left, right)=>comparePatternSpecificity(left.pattern.pattern, right.pattern.pattern) || left.index - right.index).map(({ pattern })=>pattern);
|
|
186
|
+
const decodePathParam = (value)=>{
|
|
187
|
+
try {
|
|
188
|
+
return decodeURIComponent(value);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
158
192
|
};
|
|
159
193
|
const matchPathPattern = (pathname, pattern)=>{
|
|
160
194
|
const { names, regexp } = compilePathPattern(pattern);
|
|
161
|
-
const match = regexp.exec(
|
|
195
|
+
const match = regexp.exec(normalisePathname(pathname));
|
|
162
196
|
if (!match) return null;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
197
|
+
const params = {};
|
|
198
|
+
for(let index = 0; index < names.length; index++){
|
|
199
|
+
const decoded = decodePathParam(match[index + 1] || '');
|
|
200
|
+
if (null === decoded) return null;
|
|
201
|
+
params[names[index]] = decoded;
|
|
202
|
+
}
|
|
203
|
+
return params;
|
|
167
204
|
};
|
|
168
205
|
const buildPathFromPattern = (pattern, params)=>{
|
|
169
206
|
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
@@ -178,27 +215,41 @@ const buildPathFromPattern = (pattern, params)=>{
|
|
|
178
215
|
return `/${path}`;
|
|
179
216
|
};
|
|
180
217
|
const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls)=>{
|
|
181
|
-
const normalizedPathname =
|
|
182
|
-
|
|
218
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
219
|
+
const canonicalCandidates = sortPatternsBySpecificity(Object.entries(localisedUrls).map(([canonicalPattern, localisedUrlEntry])=>({
|
|
220
|
+
pattern: canonicalPattern,
|
|
221
|
+
canonicalPattern,
|
|
222
|
+
localisedUrlEntry
|
|
223
|
+
})));
|
|
224
|
+
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
|
|
183
225
|
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
184
226
|
if (!targetPattern) continue;
|
|
185
227
|
const params = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
186
228
|
if (params) return buildPathFromPattern(targetPattern, params);
|
|
187
229
|
}
|
|
188
|
-
|
|
230
|
+
const localisedCandidates = sortPatternsBySpecificity(Object.values(localisedUrls).flatMap((localisedUrlEntry)=>{
|
|
189
231
|
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
190
|
-
if (targetPattern)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
232
|
+
if (!targetPattern) return [];
|
|
233
|
+
return languages.map((language)=>localisedUrlEntry[language]).filter((sourcePattern)=>Boolean(sourcePattern)).map((sourcePattern)=>({
|
|
234
|
+
pattern: sourcePattern,
|
|
235
|
+
sourcePattern,
|
|
236
|
+
targetPattern
|
|
237
|
+
}));
|
|
238
|
+
}));
|
|
239
|
+
for (const { sourcePattern, targetPattern } of localisedCandidates){
|
|
240
|
+
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
241
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
196
242
|
}
|
|
197
243
|
return normalizedPathname;
|
|
198
244
|
};
|
|
199
245
|
const resolveCanonicalLocalisedPath = (pathname, languages, localisedUrls)=>{
|
|
200
|
-
const normalizedPathname =
|
|
201
|
-
|
|
246
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
247
|
+
const canonicalCandidates = sortPatternsBySpecificity(Object.entries(localisedUrls).map(([canonicalPattern, localisedUrlEntry])=>({
|
|
248
|
+
pattern: canonicalPattern,
|
|
249
|
+
canonicalPattern,
|
|
250
|
+
localisedUrlEntry
|
|
251
|
+
})));
|
|
252
|
+
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
|
|
202
253
|
const canonicalParams = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
203
254
|
if (canonicalParams) return buildPathFromPattern(canonicalPattern, canonicalParams);
|
|
204
255
|
for (const language of languages){
|
|
@@ -210,4 +261,24 @@ const resolveCanonicalLocalisedPath = (pathname, languages, localisedUrls)=>{
|
|
|
210
261
|
}
|
|
211
262
|
return normalizedPathname;
|
|
212
263
|
};
|
|
213
|
-
|
|
264
|
+
const stripLanguagePrefix = (pathname, languages)=>{
|
|
265
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
266
|
+
if (segments.length > 0 && languages.includes(segments[0])) return `/${segments.slice(1).join('/')}`;
|
|
267
|
+
return pathname || '/';
|
|
268
|
+
};
|
|
269
|
+
const localiseTargetPathname = (pathname, language, languages, localisedUrls)=>{
|
|
270
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
271
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
272
|
+
const resolvedPath = localisedUrlsConfig.enabled ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
273
|
+
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
274
|
+
return `/${[
|
|
275
|
+
language,
|
|
276
|
+
...resolvedSegments
|
|
277
|
+
].join('/')}`;
|
|
278
|
+
};
|
|
279
|
+
const canonicalTargetPathname = (pathname, languages, localisedUrls)=>{
|
|
280
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
281
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
282
|
+
return localisedUrlsConfig.enabled ? resolveCanonicalLocalisedPath(pathWithoutLanguage, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
283
|
+
};
|
|
284
|
+
export { applyLocalisedUrlsToRoutes, buildPathFromPattern, canonicalTargetPathname, localiseTargetPathname, matchPathPattern, normalisePathPattern, normalisePathname, resolveCanonicalLocalisedPath, resolveLocalisedPath, resolveLocalisedUrlsConfig, validateLocalisedUrls };
|
|
@@ -24,7 +24,17 @@ export interface LinkBaseProps extends AnchorRest {
|
|
|
24
24
|
search?: string | Record<string, unknown>;
|
|
25
25
|
hashScrollIntoView?: boolean | ScrollIntoViewOptions;
|
|
26
26
|
replace?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Prefetching behavior, forwarded to the underlying router link:
|
|
29
|
+
* react-router gets it verbatim (Modern.js `PrefetchLink` supports it),
|
|
30
|
+
* TanStack receives it as its native `preload` prop (`'none'` -> `false`).
|
|
31
|
+
* Stripped from plain `<a>` fallbacks (external / no-router targets).
|
|
32
|
+
*/
|
|
27
33
|
prefetch?: 'intent' | 'render' | 'viewport' | 'none';
|
|
34
|
+
/**
|
|
35
|
+
* Native preload value of the underlying router link. When set, it wins
|
|
36
|
+
* over `prefetch` on the TanStack branch.
|
|
37
|
+
*/
|
|
28
38
|
preload?: unknown;
|
|
29
39
|
activeOptions?: LinkActiveOptions;
|
|
30
40
|
/** Extra anchor props applied when the link is active. */
|