@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.2 → 3.2.0-ultramodern.23

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 (85) hide show
  1. package/dist/cjs/cli/index.js +22 -0
  2. package/dist/cjs/runtime/I18nLink.js +4 -12
  3. package/dist/cjs/runtime/context.js +32 -5
  4. package/dist/cjs/runtime/hooks.js +8 -5
  5. package/dist/cjs/runtime/i18n/backend/defaults.js +1 -1
  6. package/dist/cjs/runtime/i18n/backend/middleware.node.js +4 -4
  7. package/dist/cjs/runtime/i18n/instance.js +4 -2
  8. package/dist/cjs/runtime/index.js +7 -6
  9. package/dist/cjs/runtime/routerAdapter.js +163 -0
  10. package/dist/cjs/runtime/utils.js +63 -94
  11. package/dist/cjs/server/index.js +64 -8
  12. package/dist/cjs/shared/localisedUrls.js +237 -0
  13. package/dist/esm/cli/index.mjs +22 -0
  14. package/dist/esm/runtime/I18nLink.mjs +4 -12
  15. package/dist/esm/runtime/context.mjs +34 -7
  16. package/dist/esm/runtime/hooks.mjs +9 -6
  17. package/dist/esm/runtime/i18n/backend/defaults.mjs +1 -1
  18. package/dist/esm/runtime/i18n/backend/middleware.node.mjs +3 -3
  19. package/dist/esm/runtime/i18n/instance.mjs +4 -2
  20. package/dist/esm/runtime/index.mjs +7 -6
  21. package/dist/esm/runtime/routerAdapter.mjs +129 -0
  22. package/dist/esm/runtime/utils.mjs +11 -30
  23. package/dist/esm/server/index.mjs +57 -7
  24. package/dist/esm/shared/localisedUrls.mjs +191 -0
  25. package/dist/esm-node/cli/index.mjs +22 -0
  26. package/dist/esm-node/runtime/I18nLink.mjs +4 -12
  27. package/dist/esm-node/runtime/context.mjs +34 -7
  28. package/dist/esm-node/runtime/hooks.mjs +9 -6
  29. package/dist/esm-node/runtime/i18n/backend/defaults.mjs +1 -1
  30. package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +3 -3
  31. package/dist/esm-node/runtime/i18n/instance.mjs +4 -2
  32. package/dist/esm-node/runtime/index.mjs +7 -6
  33. package/dist/esm-node/runtime/routerAdapter.mjs +130 -0
  34. package/dist/esm-node/runtime/utils.mjs +11 -30
  35. package/dist/esm-node/server/index.mjs +57 -7
  36. package/dist/esm-node/shared/localisedUrls.mjs +192 -0
  37. package/dist/types/cli/index.d.ts +21 -0
  38. package/dist/types/runtime/I18nLink.d.ts +23 -0
  39. package/dist/types/runtime/context.d.ts +41 -0
  40. package/dist/types/runtime/hooks.d.ts +30 -0
  41. package/dist/types/runtime/i18n/backend/config.d.ts +2 -0
  42. package/dist/types/runtime/i18n/backend/defaults.d.ts +13 -0
  43. package/dist/types/runtime/i18n/backend/defaults.node.d.ts +8 -0
  44. package/dist/types/runtime/i18n/backend/index.d.ts +3 -0
  45. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +14 -0
  46. package/dist/types/runtime/i18n/backend/middleware.d.ts +12 -0
  47. package/dist/types/runtime/i18n/backend/middleware.node.d.ts +13 -0
  48. package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +53 -0
  49. package/dist/types/runtime/i18n/backend/sdk-event.d.ts +9 -0
  50. package/dist/types/runtime/i18n/detection/config.d.ts +11 -0
  51. package/dist/types/runtime/i18n/detection/index.d.ts +50 -0
  52. package/dist/types/runtime/i18n/detection/middleware.d.ts +24 -0
  53. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +17 -0
  54. package/dist/types/runtime/i18n/index.d.ts +3 -0
  55. package/dist/types/runtime/i18n/instance.d.ts +96 -0
  56. package/dist/types/runtime/i18n/utils.d.ts +29 -0
  57. package/dist/types/runtime/index.d.ts +21 -0
  58. package/dist/types/runtime/routerAdapter.d.ts +26 -0
  59. package/dist/types/runtime/types.d.ts +15 -0
  60. package/dist/types/runtime/utils.d.ts +28 -0
  61. package/dist/types/server/index.d.ts +14 -0
  62. package/dist/types/shared/deepMerge.d.ts +1 -0
  63. package/dist/types/shared/detection.d.ts +11 -0
  64. package/dist/types/shared/localisedUrls.d.ts +13 -0
  65. package/dist/types/shared/type.d.ts +168 -0
  66. package/dist/types/shared/utils.d.ts +5 -0
  67. package/package.json +15 -15
  68. package/rstest.config.mts +39 -0
  69. package/src/cli/index.ts +43 -1
  70. package/src/runtime/I18nLink.tsx +10 -16
  71. package/src/runtime/context.tsx +45 -7
  72. package/src/runtime/hooks.ts +13 -4
  73. package/src/runtime/i18n/backend/defaults.ts +3 -1
  74. package/src/runtime/i18n/backend/middleware.node.ts +1 -1
  75. package/src/runtime/i18n/instance.ts +14 -5
  76. package/src/runtime/index.tsx +10 -2
  77. package/src/runtime/routerAdapter.tsx +333 -0
  78. package/src/runtime/utils.ts +22 -34
  79. package/src/server/index.ts +135 -10
  80. package/src/shared/localisedUrls.ts +393 -0
  81. package/src/shared/type.ts +12 -0
  82. package/tests/localisedUrls.test.ts +278 -0
  83. package/tests/routerAdapter.test.tsx +278 -0
  84. package/dist/esm/rslib-runtime.mjs +0 -18
  85. package/dist/esm-node/rslib-runtime.mjs +0 -19
@@ -24,13 +24,46 @@ var __webpack_require__ = {};
24
24
  var __webpack_exports__ = {};
25
25
  __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
+ collectApiPrefixes: ()=>collectApiPrefixes,
27
28
  default: ()=>server,
28
- i18nServerPlugin: ()=>i18nServerPlugin
29
+ i18nServerPlugin: ()=>i18nServerPlugin,
30
+ matchesApiPrefix: ()=>matchesApiPrefix
29
31
  });
30
32
  const hono_namespaceObject = require("@modern-js/server-core/hono");
31
33
  const config_js_namespaceObject = require("../runtime/i18n/detection/config.js");
34
+ const localisedUrls_js_namespaceObject = require("../shared/localisedUrls.js");
32
35
  const utils_js_namespaceObject = require("../shared/utils.js");
33
36
  const { languageDetector } = hono_namespaceObject;
37
+ const normalizeApiPrefix = (prefix)=>{
38
+ const trimmedPrefix = prefix.trim();
39
+ if (!trimmedPrefix) return null;
40
+ const prefixedPath = trimmedPrefix.startsWith('/') ? trimmedPrefix : `/${trimmedPrefix}`;
41
+ const withoutWildcard = prefixedPath.replace(/\/\*$/, '');
42
+ const normalizedPrefix = withoutWildcard.length > 1 ? withoutWildcard.replace(/\/+$/, '') : withoutWildcard;
43
+ return '/' === normalizedPrefix ? null : normalizedPrefix;
44
+ };
45
+ const collectApiPrefixes = (routes, bffPrefix)=>{
46
+ const prefixes = new Set();
47
+ for (const route of routes){
48
+ if (!route.isApi || !route.urlPath) continue;
49
+ const normalizedPrefix = normalizeApiPrefix(route.urlPath);
50
+ if (normalizedPrefix) prefixes.add(normalizedPrefix);
51
+ }
52
+ const bffPrefixes = Array.isArray(bffPrefix) ? bffPrefix : bffPrefix ? [
53
+ bffPrefix
54
+ ] : [];
55
+ for (const prefix of bffPrefixes){
56
+ const normalizedPrefix = normalizeApiPrefix(prefix);
57
+ if (normalizedPrefix) prefixes.add(normalizedPrefix);
58
+ }
59
+ return [
60
+ ...prefixes
61
+ ];
62
+ };
63
+ const matchesApiPrefix = (pathname, apiPrefixes)=>{
64
+ const normalizedPathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
65
+ return apiPrefixes.some((prefix)=>normalizedPathname === prefix || normalizedPathname.startsWith(`${prefix}/`));
66
+ };
34
67
  const convertToHonoLanguageDetectorOptions = (languages, fallbackLanguage, detectionOptions)=>{
35
68
  const mergedDetection = detectionOptions ? (0, config_js_namespaceObject.mergeDetectionOptions)(detectionOptions) : config_js_namespaceObject.DEFAULT_I18NEXT_DETECTION_OPTIONS;
36
69
  const order = (mergedDetection.order || []).filter((item)=>![
@@ -120,15 +153,20 @@ const getLanguageFromPath = (req, urlPath, languages)=>{
120
153
  if (languages.includes(firstSegment)) return firstSegment;
121
154
  return null;
122
155
  };
123
- const buildLocalizedUrl = (req, urlPath, language, languages)=>{
156
+ const buildLocalizedUrl = (req, urlPath, language, languages, localisedUrls)=>{
124
157
  const url = new URL(req.url);
125
158
  const pathname = url.pathname;
126
159
  const basePath = urlPath.replace('/*', '');
127
160
  const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
128
161
  const segments = remainingPath.split('/').filter(Boolean);
129
- if (segments.length > 0 && languages.includes(segments[0])) segments[0] = language;
130
- else segments.unshift(language);
131
- const newPathname = `/${segments.join('/')}`;
162
+ const localisedUrlsConfig = (0, localisedUrls_js_namespaceObject.resolveLocalisedUrlsConfig)(localisedUrls);
163
+ const pathWithoutLanguage = segments.length > 0 && languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : remainingPath;
164
+ const resolvedPath = localisedUrlsConfig.enabled ? (0, localisedUrls_js_namespaceObject.resolveLocalisedPath)(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
165
+ const resolvedSegments = resolvedPath.split('/').filter(Boolean);
166
+ const newPathname = `/${[
167
+ language,
168
+ ...resolvedSegments
169
+ ].join('/')}`;
132
170
  const suffix = `${url.search}${url.hash}`;
133
171
  const localizedUrl = '/' === basePath ? newPathname + suffix : basePath + newPathname + suffix;
134
172
  return localizedUrl;
@@ -138,6 +176,9 @@ const i18nServerPlugin = (options)=>({
138
176
  setup: (api)=>{
139
177
  api.onPrepare(()=>{
140
178
  const { middlewares, routes } = api.getServerContext();
179
+ const serverConfig = api.getServerConfig();
180
+ const bffPrefix = serverConfig?.bff ? serverConfig.bff.prefix ?? '/api' : void 0;
181
+ const apiPrefixes = collectApiPrefixes(routes, bffPrefix);
141
182
  const entryPaths = new Set();
142
183
  routes.forEach((route)=>{
143
184
  if (route.entryName && route.urlPath && '/' !== route.urlPath) {
@@ -149,7 +190,7 @@ const i18nServerPlugin = (options)=>({
149
190
  const { entryName } = route;
150
191
  if (!entryName) return;
151
192
  if (!options.localeDetection) return;
152
- const { localePathRedirect, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = (0, utils_js_namespaceObject.getLocaleDetectionOptions)(entryName, options.localeDetection);
193
+ const { localePathRedirect, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes, localisedUrls } = (0, utils_js_namespaceObject.getLocaleDetectionOptions)(entryName, options.localeDetection);
153
194
  const staticRoutePrefixes = options.staticRoutePrefixes;
154
195
  const originUrlPath = route.urlPath;
155
196
  const urlPath = originUrlPath.endsWith('/') ? `${originUrlPath}*` : `${originUrlPath}/*`;
@@ -163,6 +204,7 @@ const i18nServerPlugin = (options)=>({
163
204
  handler: async (c, next)=>{
164
205
  const url = new URL(c.req.url);
165
206
  const pathname = url.pathname;
207
+ if (matchesApiPrefix(pathname, apiPrefixes)) return await next();
166
208
  if (isStaticResourceRequest(pathname, staticRoutePrefixes, languages)) return await next();
167
209
  if ('/' === originUrlPath) {
168
210
  const pathSegments = pathname.split('/').filter(Boolean);
@@ -181,6 +223,7 @@ const i18nServerPlugin = (options)=>({
181
223
  handler: async (c, next)=>{
182
224
  const url = new URL(c.req.url);
183
225
  const pathname = url.pathname;
226
+ if (matchesApiPrefix(pathname, apiPrefixes)) return await next();
184
227
  if (isStaticResourceRequest(pathname, staticRoutePrefixes, languages)) return await next();
185
228
  if (shouldIgnoreRedirect(pathname, urlPath, ignoreRedirectRoutes)) return await next();
186
229
  if ('/' === originUrlPath) {
@@ -195,9 +238,18 @@ const i18nServerPlugin = (options)=>({
195
238
  let detectedLanguage = null;
196
239
  if (i18nextDetector) detectedLanguage = c.get('language') || null;
197
240
  const targetLanguage = detectedLanguage || fallbackLanguage;
198
- const localizedUrl = buildLocalizedUrl(c.req, originUrlPath, targetLanguage, languages);
241
+ const localizedUrl = buildLocalizedUrl(c.req, originUrlPath, targetLanguage, languages, localisedUrls);
199
242
  return c.redirect(localizedUrl);
200
243
  }
244
+ const localisedUrlsConfig = (0, localisedUrls_js_namespaceObject.resolveLocalisedUrlsConfig)(localisedUrls);
245
+ if (localisedUrlsConfig.enabled) {
246
+ const basePath = originUrlPath.replace('/*', '');
247
+ const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
248
+ const pathWithoutLanguage = remainingPath.split('/').filter(Boolean).slice(1).join('/');
249
+ const canonicalLocalizedPath = (0, localisedUrls_js_namespaceObject.resolveLocalisedPath)(`/${pathWithoutLanguage}`, language, languages, localisedUrlsConfig.map);
250
+ const expectedPathname = '/' === basePath ? `/${language}${'/' === canonicalLocalizedPath ? '' : canonicalLocalizedPath}` : `${basePath}/${language}${'/' === canonicalLocalizedPath ? '' : canonicalLocalizedPath}`;
251
+ if (expectedPathname !== pathname) return c.redirect(`${expectedPathname}${url.search}${url.hash}`);
252
+ }
201
253
  await next();
202
254
  }
203
255
  });
@@ -207,11 +259,15 @@ const i18nServerPlugin = (options)=>({
207
259
  }
208
260
  });
209
261
  const server = i18nServerPlugin;
262
+ exports.collectApiPrefixes = __webpack_exports__.collectApiPrefixes;
210
263
  exports["default"] = __webpack_exports__["default"];
211
264
  exports.i18nServerPlugin = __webpack_exports__.i18nServerPlugin;
265
+ exports.matchesApiPrefix = __webpack_exports__.matchesApiPrefix;
212
266
  for(var __rspack_i in __webpack_exports__)if (-1 === [
267
+ "collectApiPrefixes",
213
268
  "default",
214
- "i18nServerPlugin"
269
+ "i18nServerPlugin",
270
+ "matchesApiPrefix"
215
271
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
216
272
  Object.defineProperty(exports, '__esModule', {
217
273
  value: true
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ applyLocalisedUrlsToRoutes: ()=>applyLocalisedUrlsToRoutes,
28
+ normalisePathPattern: ()=>normalisePathPattern,
29
+ resolveLocalisedPath: ()=>resolveLocalisedPath,
30
+ resolveLocalisedUrlsConfig: ()=>resolveLocalisedUrlsConfig,
31
+ validateLocalisedUrls: ()=>validateLocalisedUrls
32
+ });
33
+ const LOCALE_PARAM_NAMES = new Set([
34
+ 'lang',
35
+ 'locale',
36
+ 'language'
37
+ ]);
38
+ const normalisePathPattern = (path)=>{
39
+ const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
40
+ const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
41
+ const withoutTrailingSlash = withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash;
42
+ return withoutTrailingSlash.replace(/\[(.+?)\]/g, ':$1');
43
+ };
44
+ const normaliseRoutePath = (path)=>{
45
+ const normalized = normalisePathPattern(path);
46
+ return '/' === normalized ? '' : normalized.slice(1);
47
+ };
48
+ const getLocaleParamSegment = (segment)=>{
49
+ if (!segment.startsWith(':')) return null;
50
+ const paramName = segment.slice(1).replace(/\?$/, '');
51
+ return LOCALE_PARAM_NAMES.has(paramName) ? segment : null;
52
+ };
53
+ const splitPathSegments = (path)=>{
54
+ if (!path) return [];
55
+ return normalisePathPattern(path).split('/').filter(Boolean);
56
+ };
57
+ const stripLeadingLocaleParam = (path)=>{
58
+ const segments = splitPathSegments(path);
59
+ const leadingLocaleParam = getLocaleParamSegment(segments[0] || '');
60
+ if (!leadingLocaleParam) return path;
61
+ const remainingPath = segments.slice(1).join('/');
62
+ return remainingPath ? `/${remainingPath}` : void 0;
63
+ };
64
+ const getLeadingLocaleParam = (path)=>{
65
+ const segments = splitPathSegments(path);
66
+ return getLocaleParamSegment(segments[0] || '');
67
+ };
68
+ const resolveLocalisedUrlsConfig = (option)=>{
69
+ if (false === option) return {
70
+ enabled: false,
71
+ map: {}
72
+ };
73
+ if (option && 'object' == typeof option) return {
74
+ enabled: true,
75
+ map: option
76
+ };
77
+ return {
78
+ enabled: true,
79
+ map: {}
80
+ };
81
+ };
82
+ const isLocalisableRoutePath = (path)=>{
83
+ const pathWithoutLocale = stripLeadingLocaleParam(path);
84
+ if (!pathWithoutLocale || '/' === pathWithoutLocale || '*' === pathWithoutLocale) return false;
85
+ return true;
86
+ };
87
+ const joinPath = (parentPath, routePath)=>{
88
+ if (!isLocalisableRoutePath(routePath)) return parentPath;
89
+ const segment = normaliseRoutePath(stripLeadingLocaleParam(routePath) || '');
90
+ return normalisePathPattern(`${parentPath}/${segment}`);
91
+ };
92
+ const ensureLocalisedUrlsForPath = (canonicalPath, languages, localisedUrls)=>{
93
+ const entry = localisedUrls[canonicalPath];
94
+ if (!entry) throw new Error(`localisedUrls is enabled, but route "${canonicalPath}" does not define localised URLs for languages: ${languages.join(', ')}. Add localisedUrls["${canonicalPath}"] or set localeDetection.localisedUrls to false.`);
95
+ const missingLanguages = languages.filter((language)=>!entry[language]);
96
+ if (missingLanguages.length > 0) throw new Error(`localisedUrls["${canonicalPath}"] is missing languages: ${missingLanguages.join(', ')}. Every configured language must have a localised URL.`);
97
+ return entry;
98
+ };
99
+ const validateLocalisedUrls = (routes, languages, localisedUrls)=>{
100
+ const visit = (route, parentPath)=>{
101
+ const canonicalPath = joinPath(parentPath, route.path);
102
+ if (isLocalisableRoutePath(route.path)) ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls);
103
+ if ('children' in route && route.children) route.children.forEach((child)=>visit(child, canonicalPath));
104
+ };
105
+ routes.forEach((route)=>visit(route, ''));
106
+ };
107
+ const getLocalisedRoutePaths = (canonicalPath, parentLocalisedPaths, languages, entry)=>{
108
+ const paths = languages.map((language)=>{
109
+ const fullPath = normalisePathPattern(entry[language]);
110
+ const parentPath = normalisePathPattern(parentLocalisedPaths[language] || '/');
111
+ if ('/' === parentPath) return normaliseRoutePath(fullPath) || void 0;
112
+ const parentPrefix = `${parentPath}/`;
113
+ if (!fullPath.startsWith(parentPrefix)) throw new Error(`localisedUrls["${canonicalPath}"].${language} must be nested under "${parentPath}" because its parent route is localised there.`);
114
+ return normaliseRoutePath(fullPath.slice(parentPath.length));
115
+ });
116
+ return Array.from(new Set(paths.filter(Boolean)));
117
+ };
118
+ const transformLocalisedRoute = (route, parentCanonicalPath, parentLocalisedPaths, languages, localisedUrls)=>{
119
+ const canonicalPath = joinPath(parentCanonicalPath, route.path);
120
+ const localisedUrlEntry = isLocalisableRoutePath(route.path) ? ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls) : void 0;
121
+ const routeLocalisedPaths = localisedUrlEntry ? languages.reduce((acc, language)=>{
122
+ acc[language] = normalisePathPattern(localisedUrlEntry[language]);
123
+ return acc;
124
+ }, {}) : parentLocalisedPaths;
125
+ const children = 'children' in route && route.children ? route.children.flatMap((child)=>transformLocalisedRoute(child, canonicalPath, routeLocalisedPaths, languages, localisedUrls)) : void 0;
126
+ const baseRoute = {
127
+ ...route,
128
+ ...children ? {
129
+ children
130
+ } : {}
131
+ };
132
+ if (!localisedUrlEntry) return [
133
+ baseRoute
134
+ ];
135
+ return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index));
136
+ };
137
+ const legalRouteIdPart = (value)=>value.replace(/[^a-zA-Z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'index';
138
+ const suffixRouteIds = (route, suffix)=>{
139
+ const children = 'children' in route && route.children ? route.children.map((child)=>suffixRouteIds(child, suffix)) : void 0;
140
+ return {
141
+ ...route,
142
+ ...route.id ? {
143
+ id: `${route.id}__localised_${suffix}`
144
+ } : {},
145
+ ...children ? {
146
+ children
147
+ } : {}
148
+ };
149
+ };
150
+ const cloneRouteWithLocalisedPath = (route, path, index)=>{
151
+ const leadingLocaleParam = getLeadingLocaleParam(route.path);
152
+ const localisedPath = leadingLocaleParam ? normaliseRoutePath(`${leadingLocaleParam}/${path}`) : path;
153
+ const routeWithPath = {
154
+ ...route,
155
+ path: localisedPath
156
+ };
157
+ return 0 === index ? routeWithPath : suffixRouteIds(routeWithPath, legalRouteIdPart(localisedPath));
158
+ };
159
+ const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
160
+ const rootLocalisedPaths = languages.reduce((acc, language)=>{
161
+ acc[language] = '/';
162
+ return acc;
163
+ }, {});
164
+ validateLocalisedUrls(routes, languages, localisedUrls);
165
+ return routes.flatMap((route)=>transformLocalisedRoute(route, '', rootLocalisedPaths, languages, localisedUrls));
166
+ };
167
+ const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
168
+ const getParamName = (segment)=>segment.slice(1).replace(/\?$/, '');
169
+ const compilePathPattern = (pattern)=>{
170
+ const names = [];
171
+ const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
172
+ const source = segments.map((segment)=>{
173
+ if (segment.startsWith(':')) {
174
+ names.push(getParamName(segment));
175
+ const paramPattern = '([^/]+)';
176
+ return segment.endsWith('?') ? `(?:/${paramPattern})?` : `/${paramPattern}`;
177
+ }
178
+ if ('*' === segment) {
179
+ names.push('*');
180
+ return '/(.*)';
181
+ }
182
+ return `/${escapeRegExp(segment)}`;
183
+ }).join('');
184
+ return {
185
+ names,
186
+ regexp: new RegExp(`^${source || '/'}$`)
187
+ };
188
+ };
189
+ const matchPathPattern = (pathname, pattern)=>{
190
+ const { names, regexp } = compilePathPattern(pattern);
191
+ const match = regexp.exec(normalisePathPattern(pathname));
192
+ if (!match) return null;
193
+ return names.reduce((params, name, index)=>{
194
+ params[name] = decodeURIComponent(match[index + 1] || '');
195
+ return params;
196
+ }, {});
197
+ };
198
+ const buildPathFromPattern = (pattern, params)=>{
199
+ const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
200
+ const path = segments.map((segment)=>{
201
+ if (segment.startsWith(':')) {
202
+ const param = params[getParamName(segment)];
203
+ return param ? encodeURIComponent(param) : '';
204
+ }
205
+ if ('*' === segment) return params['*'] || '';
206
+ return segment;
207
+ }).filter(Boolean).join('/');
208
+ return `/${path}`;
209
+ };
210
+ const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls)=>{
211
+ const normalizedPathname = normalisePathPattern(pathname);
212
+ for (const localisedUrlEntry of Object.values(localisedUrls)){
213
+ const targetPattern = localisedUrlEntry[targetLanguage];
214
+ if (targetPattern) for (const language of languages){
215
+ const sourcePattern = localisedUrlEntry[language];
216
+ if (!sourcePattern) continue;
217
+ const params = matchPathPattern(normalizedPathname, sourcePattern);
218
+ if (params) return buildPathFromPattern(targetPattern, params);
219
+ }
220
+ }
221
+ return normalizedPathname;
222
+ };
223
+ exports.applyLocalisedUrlsToRoutes = __webpack_exports__.applyLocalisedUrlsToRoutes;
224
+ exports.normalisePathPattern = __webpack_exports__.normalisePathPattern;
225
+ exports.resolveLocalisedPath = __webpack_exports__.resolveLocalisedPath;
226
+ exports.resolveLocalisedUrlsConfig = __webpack_exports__.resolveLocalisedUrlsConfig;
227
+ exports.validateLocalisedUrls = __webpack_exports__.validateLocalisedUrls;
228
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
229
+ "applyLocalisedUrlsToRoutes",
230
+ "normalisePathPattern",
231
+ "resolveLocalisedPath",
232
+ "resolveLocalisedUrlsConfig",
233
+ "validateLocalisedUrls"
234
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
235
+ Object.defineProperty(exports, '__esModule', {
236
+ value: true
237
+ });
@@ -1,6 +1,7 @@
1
1
  import { getPublicDirRoutePrefixes } from "@modern-js/server-core";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
+ import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
4
5
  import { getBackendOptions, getLocaleDetectionOptions } from "../shared/utils.mjs";
5
6
  function hasJsonFiles(dirPath) {
6
7
  try {
@@ -80,6 +81,27 @@ const i18nPlugin = (options = {})=>({
80
81
  plugins
81
82
  };
82
83
  });
84
+ api.modifyFileSystemRoutes(({ entrypoint, routes })=>{
85
+ if (!localeDetection) return {
86
+ entrypoint,
87
+ routes
88
+ };
89
+ const localeDetectionOptions = getLocaleDetectionOptions(entrypoint.entryName, localeDetection);
90
+ const { localePathRedirect, languages = [], localisedUrls } = localeDetectionOptions;
91
+ if (!localePathRedirect || 0 === languages.length) return {
92
+ entrypoint,
93
+ routes
94
+ };
95
+ const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
96
+ if (!localisedUrlsConfig.enabled) return {
97
+ entrypoint,
98
+ routes
99
+ };
100
+ return {
101
+ entrypoint,
102
+ routes: applyLocalisedUrlsToRoutes(routes, languages, localisedUrlsConfig.map)
103
+ };
104
+ });
83
105
  api._internalServerPlugins(({ plugins })=>{
84
106
  const { serverRoutes, metaName } = api.getAppContext();
85
107
  const normalizedConfig = api.getNormalizedConfig();
@@ -1,20 +1,12 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { Link as router_Link, useInRouterContext, useParams } from "@modern-js/runtime/router";
3
2
  import { useModernI18n } from "./context.mjs";
3
+ import { useI18nRouterAdapter } from "./routerAdapter.mjs";
4
4
  import { buildLocalizedUrl } from "./utils.mjs";
5
- const useRouterHooks = ()=>{
6
- const inRouter = useInRouterContext();
7
- return {
8
- Link: inRouter ? router_Link : null,
9
- params: inRouter ? useParams() : {},
10
- hasRouter: inRouter
11
- };
12
- };
13
5
  const I18nLink = ({ to, children, ...props })=>{
14
- const { Link, params, hasRouter } = useRouterHooks();
15
- const { language, supportedLanguages } = useModernI18n();
6
+ const { Link, params, hasRouter } = useI18nRouterAdapter();
7
+ const { language, supportedLanguages, localisedUrls } = useModernI18n();
16
8
  const currentLang = language;
17
- const localizedTo = buildLocalizedUrl(to, currentLang, supportedLanguages);
9
+ const localizedTo = buildLocalizedUrl(to, currentLang, supportedLanguages, localisedUrls);
18
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.");
19
11
  if (!hasRouter || !Link) return /*#__PURE__*/ jsx("a", {
20
12
  href: localizedTo,
@@ -1,8 +1,9 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { isBrowser } from "@modern-js/runtime";
3
- import { createContext, useCallback, useContext, useMemo } from "react";
3
+ import { createContext, useCallback, useContext, useEffect, useMemo } from "react";
4
4
  import { cacheUserLanguage } from "./i18n/detection/index.mjs";
5
- import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, shouldIgnoreRedirect, useRouterHooks } from "./utils.mjs";
5
+ import { useI18nRouterAdapter } from "./routerAdapter.mjs";
6
+ import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, shouldIgnoreRedirect } from "./utils.mjs";
6
7
  const ModernI18nContext = /*#__PURE__*/ createContext(null);
7
8
  const ModernI18nProvider = ({ children, value })=>/*#__PURE__*/ jsx(ModernI18nContext.Provider, {
8
9
  value: value,
@@ -11,9 +12,33 @@ const ModernI18nProvider = ({ children, value })=>/*#__PURE__*/ jsx(ModernI18nCo
11
12
  const useModernI18n = ()=>{
12
13
  const context = useContext(ModernI18nContext);
13
14
  if (!context) throw new Error('useModernI18n must be used within a ModernI18nProvider');
14
- const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, updateLanguage } = context;
15
- const { navigate, location, hasRouter } = useRouterHooks();
16
- const currentLanguage = contextLanguage;
15
+ const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, updateLanguage } = context;
16
+ const { navigate, location, hasRouter } = useI18nRouterAdapter();
17
+ const pathLanguage = useMemo(()=>{
18
+ if (!localePathRedirect || !location?.pathname) return;
19
+ const detected = detectLanguageFromPath(location.pathname, languages || [], localePathRedirect);
20
+ return detected.detected ? detected.language : void 0;
21
+ }, [
22
+ languages,
23
+ localePathRedirect,
24
+ location?.pathname
25
+ ]);
26
+ const currentLanguage = pathLanguage || contextLanguage;
27
+ useEffect(()=>{
28
+ if (!pathLanguage || pathLanguage === contextLanguage) return;
29
+ updateLanguage?.(pathLanguage);
30
+ i18nInstance?.setLang?.(pathLanguage);
31
+ i18nInstance?.changeLanguage?.(pathLanguage);
32
+ if (isBrowser()) {
33
+ const detectionOptions = i18nInstance.options?.detection;
34
+ cacheUserLanguage(i18nInstance, pathLanguage, detectionOptions);
35
+ }
36
+ }, [
37
+ contextLanguage,
38
+ i18nInstance,
39
+ pathLanguage,
40
+ updateLanguage
41
+ ]);
17
42
  const changeLanguage = useCallback(async (newLang)=>{
18
43
  try {
19
44
  if (!newLang || 'string' != typeof newLang) throw new Error('Language must be a non-empty string');
@@ -30,7 +55,7 @@ const useModernI18n = ()=>{
30
55
  const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
31
56
  if (pathLanguage.detected && pathLanguage.language === newLang) return;
32
57
  if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
33
- const newPath = buildLocalizedUrl(relativePath, newLang, languages || []);
58
+ const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
34
59
  const newUrl = entryPath + newPath + location.search + location.hash;
35
60
  await navigate(newUrl, {
36
61
  replace: true
@@ -43,7 +68,7 @@ const useModernI18n = ()=>{
43
68
  const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
44
69
  if (pathLanguage.detected && pathLanguage.language === newLang) return;
45
70
  if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
46
- const newPath = buildLocalizedUrl(relativePath, newLang, languages || []);
71
+ const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
47
72
  const newUrl = entryPath + newPath + window.location.search + window.location.hash;
48
73
  window.history.pushState(null, '', newUrl);
49
74
  }
@@ -58,6 +83,7 @@ const useModernI18n = ()=>{
58
83
  updateLanguage,
59
84
  localePathRedirect,
60
85
  ignoreRedirectRoutes,
86
+ localisedUrls,
61
87
  languages,
62
88
  hasRouter,
63
89
  navigate,
@@ -98,6 +124,7 @@ const useModernI18n = ()=>{
98
124
  changeLanguage,
99
125
  i18nInstance,
100
126
  supportedLanguages: languages || [],
127
+ localisedUrls,
101
128
  isLanguageSupported,
102
129
  isResourcesReady
103
130
  };
@@ -2,7 +2,8 @@ import { isBrowser } from "@modern-js/runtime";
2
2
  import { useEffect, useRef } from "react";
3
3
  import { I18N_SDK_RESOURCES_LOADED_EVENT, getI18nSdkBackendId } from "./i18n/backend/sdk-event.mjs";
4
4
  import { cacheUserLanguage } from "./i18n/detection/index.mjs";
5
- import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getPathname, shouldIgnoreRedirect, useRouterHooks } from "./utils.mjs";
5
+ import { useI18nRouterAdapter } from "./routerAdapter.mjs";
6
+ import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getPathname, shouldIgnoreRedirect } from "./utils.mjs";
6
7
  function createMinimalI18nInstance(language) {
7
8
  const minimalInstance = {
8
9
  language,
@@ -14,7 +15,7 @@ function createMinimalI18nInstance(language) {
14
15
  };
15
16
  return minimalInstance;
16
17
  }
17
- function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang) {
18
+ function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang) {
18
19
  const instance = i18nInstance || createMinimalI18nInstance(lang);
19
20
  return {
20
21
  language: lang,
@@ -23,6 +24,7 @@ function createContextValue(lang, i18nInstance, entryName, languages, localePath
23
24
  languages,
24
25
  localePathRedirect,
25
26
  ignoreRedirectRoutes,
27
+ localisedUrls,
26
28
  updateLanguage: setLang
27
29
  };
28
30
  }
@@ -72,9 +74,9 @@ function useSdkResourcesLoader(i18nInstance, setForceUpdate) {
72
74
  setForceUpdate
73
75
  ]);
74
76
  }
75
- function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes) {
77
+ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls) {
76
78
  const hasRedirectedRef = useRef(false);
77
- const { navigate, location, hasRouter } = useRouterHooks();
79
+ const { navigate, location, hasRouter } = useI18nRouterAdapter();
78
80
  useEffect(()=>{
79
81
  if ('browser' !== process.env.MODERN_TARGET) return;
80
82
  if (!localePathRedirect || !i18nInstance) return;
@@ -93,7 +95,7 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
93
95
  const pathDetection = detectLanguageFromPath(currentPathname, languages, localePathRedirect);
94
96
  if (pathDetection.detected) return;
95
97
  const targetLanguage = i18nInstance.language || fallbackLanguage || languages[0] || 'en';
96
- const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages);
98
+ const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages, localisedUrls);
97
99
  const newUrl = entryPath + newPath + currentSearch + currentHash;
98
100
  if (newUrl !== currentPathname + currentSearch + currentHash) {
99
101
  hasRedirectedRef.current = true;
@@ -110,7 +112,8 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
110
112
  i18nInstance,
111
113
  languages,
112
114
  fallbackLanguage,
113
- ignoreRedirectRoutes
115
+ ignoreRedirectRoutes,
116
+ localisedUrls
114
117
  ]);
115
118
  }
116
119
  function useLanguageSync(i18nInstance, localePathRedirect, languages, runtimeContextRef, prevLangRef, setLang) {
@@ -4,7 +4,7 @@ const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
4
4
  };
5
5
  function convertPath(path) {
6
6
  if (!path) return path;
7
- if (path.startsWith('/')) return `${window.__assetPrefix__ || ''}${path}`;
7
+ if (path.startsWith('/')) return "u" < typeof window ? path : `${window.__assetPrefix__ || ''}${path}`;
8
8
  return path;
9
9
  }
10
10
  function convertBackendOptions(options) {
@@ -1,8 +1,8 @@
1
- import i18next_fs_backend from "i18next-fs-backend";
1
+ import cjs from "i18next-fs-backend/cjs";
2
2
  import { useI18nextBackendCommon } from "./middleware.common.mjs";
3
- class FsBackendWithSave extends i18next_fs_backend {
3
+ class FsBackendWithSave extends cjs {
4
4
  save(_language, _namespace, _data) {}
5
5
  }
6
6
  const HttpBackendWithSave = FsBackendWithSave;
7
- const useI18nextBackend = (i18nInstance, backend)=>useI18nextBackendCommon(i18nInstance, FsBackendWithSave, i18next_fs_backend, backend);
7
+ const useI18nextBackend = (i18nInstance, backend)=>useI18nextBackendCommon(i18nInstance, FsBackendWithSave, cjs, backend);
8
8
  export { FsBackendWithSave, HttpBackendWithSave, useI18nextBackend };
@@ -40,10 +40,12 @@ async function createI18nextInstance() {
40
40
  return null;
41
41
  }
42
42
  }
43
+ function getOptionalReactI18nextPackageName() {
44
+ return "react-i18next";
45
+ }
43
46
  async function tryImportReactI18next() {
44
47
  try {
45
- const reactI18next = await import("react-i18next");
46
- return reactI18next;
48
+ return await import(getOptionalReactI18nextPackageName());
47
49
  } catch (error) {
48
50
  return null;
49
51
  }