@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.
Files changed (42) hide show
  1. package/README.md +221 -11
  2. package/dist/cjs/runtime/I18nLink.js +7 -17
  3. package/dist/cjs/runtime/Link.js +252 -0
  4. package/dist/cjs/runtime/canonicalRoutes.js +18 -0
  5. package/dist/cjs/runtime/index.js +23 -0
  6. package/dist/cjs/runtime/localizedPaths.js +105 -0
  7. package/dist/cjs/runtime/utils.js +22 -5
  8. package/dist/cjs/shared/localisedUrls.js +32 -2
  9. package/dist/esm/runtime/I18nLink.mjs +6 -16
  10. package/dist/esm/runtime/Link.mjs +209 -0
  11. package/dist/esm/runtime/canonicalRoutes.mjs +0 -0
  12. package/dist/esm/runtime/index.mjs +4 -2
  13. package/dist/esm/runtime/localizedPaths.mjs +58 -0
  14. package/dist/esm/runtime/utils.mjs +18 -4
  15. package/dist/esm/shared/localisedUrls.mjs +24 -3
  16. package/dist/esm-node/runtime/I18nLink.mjs +6 -16
  17. package/dist/esm-node/runtime/Link.mjs +210 -0
  18. package/dist/esm-node/runtime/canonicalRoutes.mjs +1 -0
  19. package/dist/esm-node/runtime/index.mjs +4 -2
  20. package/dist/esm-node/runtime/localizedPaths.mjs +59 -0
  21. package/dist/esm-node/runtime/utils.mjs +18 -4
  22. package/dist/esm-node/shared/localisedUrls.mjs +24 -3
  23. package/dist/types/runtime/I18nLink.d.ts +4 -13
  24. package/dist/types/runtime/Link.d.ts +56 -0
  25. package/dist/types/runtime/canonicalRoutes.d.ts +60 -0
  26. package/dist/types/runtime/index.d.ts +5 -1
  27. package/dist/types/runtime/localizedPaths.d.ts +39 -0
  28. package/dist/types/runtime/utils.d.ts +12 -3
  29. package/dist/types/shared/localisedUrls.d.ts +8 -0
  30. package/package.json +13 -13
  31. package/rstest.config.mts +2 -2
  32. package/src/runtime/I18nLink.tsx +13 -46
  33. package/src/runtime/Link.tsx +414 -0
  34. package/src/runtime/canonicalRoutes.ts +93 -0
  35. package/src/runtime/index.tsx +24 -2
  36. package/src/runtime/localizedPaths.ts +118 -0
  37. package/src/runtime/utils.ts +24 -5
  38. package/src/shared/localisedUrls.ts +63 -3
  39. package/tests/link.test.tsx +475 -0
  40. package/tests/linkTypes.test.ts +28 -0
  41. package/tests/type-fixture/linkTypes.fixture.tsx +51 -0
  42. 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 buildLocalizedUrl = (pathname, language, languages, localisedUrls)=>{
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 { useModernI18n } from "./context.mjs";
3
- import { useI18nRouterAdapter } from "./routerAdapter.mjs";
4
- import { buildLocalizedUrl } from "./utils.mjs";
2
+ import { Link } from "./Link.mjs";
3
+ let warnedDeprecation = false;
5
4
  const I18nLink = ({ to, children, ...props })=>{
6
- const { Link, params, hasRouter } = useI18nRouterAdapter();
7
- const { language, supportedLanguages, localisedUrls } = useModernI18n();
8
- const currentLang = language;
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: localizedTo,
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 buildLocalizedUrl = (pathname, language, languages, localisedUrls)=>{
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
- export { applyLocalisedUrlsToRoutes, normalisePathPattern, resolveLocalisedPath, resolveLocalisedUrlsConfig, validateLocalisedUrls };
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 };