@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.
Files changed (41) hide show
  1. package/dist/cjs/runtime/Link.js +33 -21
  2. package/dist/cjs/runtime/i18n/backend/defaults.node.js +42 -8
  3. package/dist/cjs/runtime/localizedPaths.js +1 -4
  4. package/dist/cjs/runtime/routerAdapter.js +2 -2
  5. package/dist/cjs/runtime/utils.js +2 -9
  6. package/dist/cjs/server/index.js +1 -9
  7. package/dist/cjs/shared/localisedUrls.js +107 -27
  8. package/dist/esm/runtime/Link.mjs +33 -21
  9. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +24 -3
  10. package/dist/esm/runtime/localizedPaths.mjs +2 -5
  11. package/dist/esm/runtime/routerAdapter.mjs +3 -3
  12. package/dist/esm/runtime/utils.mjs +3 -10
  13. package/dist/esm/server/index.mjs +2 -10
  14. package/dist/esm/shared/localisedUrls.mjs +99 -28
  15. package/dist/esm-node/runtime/Link.mjs +33 -21
  16. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +24 -3
  17. package/dist/esm-node/runtime/localizedPaths.mjs +2 -5
  18. package/dist/esm-node/runtime/routerAdapter.mjs +3 -3
  19. package/dist/esm-node/runtime/utils.mjs +3 -10
  20. package/dist/esm-node/server/index.mjs +2 -10
  21. package/dist/esm-node/shared/localisedUrls.mjs +99 -28
  22. package/dist/types/runtime/Link.d.ts +10 -0
  23. package/dist/types/runtime/i18n/backend/defaults.node.d.ts +3 -2
  24. package/dist/types/runtime/utils.d.ts +2 -2
  25. package/dist/types/shared/localisedUrls.d.ts +15 -0
  26. package/dist/types/shared/type.d.ts +7 -5
  27. package/package.json +16 -12
  28. package/rstest.config.mts +6 -1
  29. package/src/runtime/Link.tsx +28 -12
  30. package/src/runtime/i18n/backend/defaults.node.ts +40 -2
  31. package/src/runtime/localizedPaths.ts +6 -17
  32. package/src/runtime/routerAdapter.tsx +4 -5
  33. package/src/runtime/utils.ts +11 -23
  34. package/src/server/index.ts +7 -17
  35. package/src/shared/localisedUrls.ts +212 -42
  36. package/src/shared/type.ts +7 -5
  37. package/tests/backendDefaults.test.ts +51 -0
  38. package/tests/i18nUtils.test.ts +10 -3
  39. package/tests/link.test.tsx +51 -1
  40. package/tests/localisedUrls.test.ts +224 -0
  41. 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 { resolveLocalisedPath, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
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 segments = remainingPath.split('/').filter(Boolean);
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 normalisePathPattern = (path)=>{
6
+ const normaliseSlashes = (path)=>{
7
7
  const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
8
8
  const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
9
- const withoutTrailingSlash = withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash;
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 (false === option) return {
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: true,
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 = normalisePathPattern(pattern).split('/').filter(Boolean);
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
- return {
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(normalisePathPattern(pathname));
194
+ const match = regexp.exec(normalisePathname(pathname));
161
195
  if (!match) return null;
162
- return names.reduce((params, name, index)=>{
163
- params[name] = decodeURIComponent(match[index + 1] || '');
164
- return params;
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 = normalisePathPattern(pathname);
181
- for (const [canonicalPattern, localisedUrlEntry] of Object.entries(localisedUrls)){
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
- for (const localisedUrlEntry of Object.values(localisedUrls)){
229
+ const localisedCandidates = sortPatternsBySpecificity(Object.values(localisedUrls).flatMap((localisedUrlEntry)=>{
188
230
  const targetPattern = localisedUrlEntry[targetLanguage];
189
- if (targetPattern) for (const language of languages){
190
- const sourcePattern = localisedUrlEntry[language];
191
- if (!sourcePattern) continue;
192
- const params = matchPathPattern(normalizedPathname, sourcePattern);
193
- if (params) return buildPathFromPattern(targetPattern, params);
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 = normalisePathPattern(pathname);
200
- for (const [canonicalPattern, localisedUrlEntry] of Object.entries(localisedUrls)){
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
- export { applyLocalisedUrlsToRoutes, buildPathFromPattern, matchPathPattern, normalisePathPattern, resolveCanonicalLocalisedPath, resolveLocalisedPath, resolveLocalisedUrlsConfig, validateLocalisedUrls };
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 { prefetch: _prefetch, preload: _preload, replace: _replace, ...anchorProps } = rest;
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 { prefetch: _prefetch, preload: _preload, replace: _replace, ...anchorProps } = rest;
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) return /*#__PURE__*/ jsx(RouterLink, {
181
- to: target.localizedPathname,
182
- ...target.searchObject ? {
183
- search: target.searchObject
184
- } : {},
185
- ...target.hash ? {
186
- hash: target.hash
187
- } : {},
188
- ...void 0 === hashScrollIntoView ? {} : {
189
- hashScrollIntoView
190
- },
191
- ...rest,
192
- ...activeRest,
193
- ...activeAttributes,
194
- className: mergedClassName,
195
- style: mergedStyle,
196
- children: children
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: './config/public/locales/{{lng}}/{{ns}}.json',
4
- addPath: './config/public/locales/{{lng}}/{{ns}}.json'
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 { resolveCanonicalLocalisedPath, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
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 segments = pathname.split('/').filter(Boolean);
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.routerFramework || internalContext.routerRuntime?.framework || runtimeContext.routerFramework;
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.routerInstance || internalContext.routerRuntime?.instance;
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 { resolveLocalisedPath, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
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 segments = pathname.split('/').filter(Boolean);
36
- const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
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 { resolveLocalisedPath, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
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 segments = remainingPath.split('/').filter(Boolean);
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 normalisePathPattern = (path)=>{
7
+ const normaliseSlashes = (path)=>{
8
8
  const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
9
9
  const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
10
- const withoutTrailingSlash = withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash;
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 (false === option) return {
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: true,
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 = normalisePathPattern(pattern).split('/').filter(Boolean);
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
- return {
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(normalisePathPattern(pathname));
195
+ const match = regexp.exec(normalisePathname(pathname));
162
196
  if (!match) return null;
163
- return names.reduce((params, name, index)=>{
164
- params[name] = decodeURIComponent(match[index + 1] || '');
165
- return params;
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 = normalisePathPattern(pathname);
182
- for (const [canonicalPattern, localisedUrlEntry] of Object.entries(localisedUrls)){
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
- for (const localisedUrlEntry of Object.values(localisedUrls)){
230
+ const localisedCandidates = sortPatternsBySpecificity(Object.values(localisedUrls).flatMap((localisedUrlEntry)=>{
189
231
  const targetPattern = localisedUrlEntry[targetLanguage];
190
- if (targetPattern) for (const language of languages){
191
- const sourcePattern = localisedUrlEntry[language];
192
- if (!sourcePattern) continue;
193
- const params = matchPathPattern(normalizedPathname, sourcePattern);
194
- if (params) return buildPathFromPattern(targetPattern, params);
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 = normalisePathPattern(pathname);
201
- for (const [canonicalPattern, localisedUrlEntry] of Object.entries(localisedUrls)){
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
- export { applyLocalisedUrlsToRoutes, buildPathFromPattern, matchPathPattern, normalisePathPattern, resolveCanonicalLocalisedPath, resolveLocalisedPath, resolveLocalisedUrlsConfig, validateLocalisedUrls };
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. */