@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.9 → 3.2.0-ultramodern.90

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 (80) 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/defaults.node.js +2 -2
  7. package/dist/cjs/runtime/i18n/backend/middleware.node.js +4 -4
  8. package/dist/cjs/runtime/i18n/instance.js +0 -24
  9. package/dist/cjs/runtime/i18n/react-i18next.js +49 -0
  10. package/dist/cjs/runtime/i18n/utils.js +0 -12
  11. package/dist/cjs/runtime/index.js +18 -10
  12. package/dist/cjs/runtime/routerAdapter.js +163 -0
  13. package/dist/cjs/runtime/utils.js +63 -94
  14. package/dist/cjs/server/index.js +60 -8
  15. package/dist/cjs/shared/localisedUrls.js +237 -0
  16. package/dist/esm/cli/index.mjs +22 -0
  17. package/dist/esm/runtime/I18nLink.mjs +4 -12
  18. package/dist/esm/runtime/context.mjs +34 -7
  19. package/dist/esm/runtime/hooks.mjs +9 -6
  20. package/dist/esm/runtime/i18n/backend/defaults.mjs +1 -1
  21. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +2 -2
  22. package/dist/esm/runtime/i18n/backend/middleware.node.mjs +3 -3
  23. package/dist/esm/runtime/i18n/instance.mjs +1 -19
  24. package/dist/esm/runtime/i18n/react-i18next.mjs +15 -0
  25. package/dist/esm/runtime/i18n/utils.mjs +0 -12
  26. package/dist/esm/runtime/index.mjs +19 -11
  27. package/dist/esm/runtime/routerAdapter.mjs +129 -0
  28. package/dist/esm/runtime/utils.mjs +11 -30
  29. package/dist/esm/server/index.mjs +53 -7
  30. package/dist/esm/shared/localisedUrls.mjs +191 -0
  31. package/dist/esm-node/cli/index.mjs +22 -0
  32. package/dist/esm-node/runtime/I18nLink.mjs +4 -12
  33. package/dist/esm-node/runtime/context.mjs +34 -7
  34. package/dist/esm-node/runtime/hooks.mjs +9 -6
  35. package/dist/esm-node/runtime/i18n/backend/defaults.mjs +1 -1
  36. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +2 -2
  37. package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +3 -3
  38. package/dist/esm-node/runtime/i18n/instance.mjs +1 -19
  39. package/dist/esm-node/runtime/i18n/react-i18next.mjs +16 -0
  40. package/dist/esm-node/runtime/i18n/utils.mjs +0 -12
  41. package/dist/esm-node/runtime/index.mjs +19 -11
  42. package/dist/esm-node/runtime/routerAdapter.mjs +130 -0
  43. package/dist/esm-node/runtime/utils.mjs +11 -30
  44. package/dist/esm-node/server/index.mjs +53 -7
  45. package/dist/esm-node/shared/localisedUrls.mjs +192 -0
  46. package/dist/types/runtime/I18nLink.d.ts +15 -0
  47. package/dist/types/runtime/context.d.ts +3 -0
  48. package/dist/types/runtime/hooks.d.ts +4 -2
  49. package/dist/types/runtime/i18n/backend/middleware.node.d.ts +1 -1
  50. package/dist/types/runtime/i18n/instance.d.ts +0 -5
  51. package/dist/types/runtime/i18n/react-i18next.d.ts +7 -0
  52. package/dist/types/runtime/index.d.ts +1 -0
  53. package/dist/types/runtime/routerAdapter.d.ts +26 -0
  54. package/dist/types/runtime/utils.d.ts +2 -7
  55. package/dist/types/server/index.d.ts +6 -0
  56. package/dist/types/shared/localisedUrls.d.ts +13 -0
  57. package/dist/types/shared/type.d.ts +12 -0
  58. package/package.json +18 -22
  59. package/rstest.config.mts +39 -0
  60. package/src/cli/index.ts +43 -1
  61. package/src/runtime/I18nLink.tsx +10 -16
  62. package/src/runtime/context.tsx +45 -7
  63. package/src/runtime/hooks.ts +13 -4
  64. package/src/runtime/i18n/backend/defaults.node.ts +2 -2
  65. package/src/runtime/i18n/backend/defaults.ts +3 -1
  66. package/src/runtime/i18n/backend/middleware.node.ts +1 -1
  67. package/src/runtime/i18n/instance.ts +0 -29
  68. package/src/runtime/i18n/react-i18next.ts +25 -0
  69. package/src/runtime/i18n/utils.ts +4 -26
  70. package/src/runtime/index.tsx +23 -10
  71. package/src/runtime/routerAdapter.tsx +333 -0
  72. package/src/runtime/utils.ts +22 -34
  73. package/src/server/index.ts +117 -10
  74. package/src/shared/localisedUrls.ts +393 -0
  75. package/src/shared/type.ts +12 -0
  76. package/tests/i18nUtils.test.ts +52 -0
  77. package/tests/localisedUrls.test.ts +312 -0
  78. package/tests/routerAdapter.test.tsx +382 -0
  79. package/dist/esm/rslib-runtime.mjs +0 -18
  80. package/dist/esm-node/rslib-runtime.mjs +0 -19
@@ -0,0 +1,191 @@
1
+ const LOCALE_PARAM_NAMES = new Set([
2
+ 'lang',
3
+ 'locale',
4
+ 'language'
5
+ ]);
6
+ const normalisePathPattern = (path)=>{
7
+ const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
8
+ const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
9
+ const withoutTrailingSlash = withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash;
10
+ return withoutTrailingSlash.replace(/\[(.+?)\]/g, ':$1');
11
+ };
12
+ const normaliseRoutePath = (path)=>{
13
+ const normalized = normalisePathPattern(path);
14
+ return '/' === normalized ? '' : normalized.slice(1);
15
+ };
16
+ const getLocaleParamSegment = (segment)=>{
17
+ if (!segment.startsWith(':')) return null;
18
+ const paramName = segment.slice(1).replace(/\?$/, '');
19
+ return LOCALE_PARAM_NAMES.has(paramName) ? segment : null;
20
+ };
21
+ const splitPathSegments = (path)=>{
22
+ if (!path) return [];
23
+ return normalisePathPattern(path).split('/').filter(Boolean);
24
+ };
25
+ const stripLeadingLocaleParam = (path)=>{
26
+ const segments = splitPathSegments(path);
27
+ const leadingLocaleParam = getLocaleParamSegment(segments[0] || '');
28
+ if (!leadingLocaleParam) return path;
29
+ const remainingPath = segments.slice(1).join('/');
30
+ return remainingPath ? `/${remainingPath}` : void 0;
31
+ };
32
+ const getLeadingLocaleParam = (path)=>{
33
+ const segments = splitPathSegments(path);
34
+ return getLocaleParamSegment(segments[0] || '');
35
+ };
36
+ const resolveLocalisedUrlsConfig = (option)=>{
37
+ if (false === option) return {
38
+ enabled: false,
39
+ map: {}
40
+ };
41
+ if (option && 'object' == typeof option) return {
42
+ enabled: true,
43
+ map: option
44
+ };
45
+ return {
46
+ enabled: true,
47
+ map: {}
48
+ };
49
+ };
50
+ const isLocalisableRoutePath = (path)=>{
51
+ const pathWithoutLocale = stripLeadingLocaleParam(path);
52
+ if (!pathWithoutLocale || '/' === pathWithoutLocale || '*' === pathWithoutLocale) return false;
53
+ return true;
54
+ };
55
+ const joinPath = (parentPath, routePath)=>{
56
+ if (!isLocalisableRoutePath(routePath)) return parentPath;
57
+ const segment = normaliseRoutePath(stripLeadingLocaleParam(routePath) || '');
58
+ return normalisePathPattern(`${parentPath}/${segment}`);
59
+ };
60
+ const ensureLocalisedUrlsForPath = (canonicalPath, languages, localisedUrls)=>{
61
+ const entry = localisedUrls[canonicalPath];
62
+ 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.`);
63
+ const missingLanguages = languages.filter((language)=>!entry[language]);
64
+ if (missingLanguages.length > 0) throw new Error(`localisedUrls["${canonicalPath}"] is missing languages: ${missingLanguages.join(', ')}. Every configured language must have a localised URL.`);
65
+ return entry;
66
+ };
67
+ const validateLocalisedUrls = (routes, languages, localisedUrls)=>{
68
+ const visit = (route, parentPath)=>{
69
+ const canonicalPath = joinPath(parentPath, route.path);
70
+ if (isLocalisableRoutePath(route.path)) ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls);
71
+ if ('children' in route && route.children) route.children.forEach((child)=>visit(child, canonicalPath));
72
+ };
73
+ routes.forEach((route)=>visit(route, ''));
74
+ };
75
+ const getLocalisedRoutePaths = (canonicalPath, parentLocalisedPaths, languages, entry)=>{
76
+ const paths = languages.map((language)=>{
77
+ const fullPath = normalisePathPattern(entry[language]);
78
+ const parentPath = normalisePathPattern(parentLocalisedPaths[language] || '/');
79
+ if ('/' === parentPath) return normaliseRoutePath(fullPath) || void 0;
80
+ const parentPrefix = `${parentPath}/`;
81
+ if (!fullPath.startsWith(parentPrefix)) throw new Error(`localisedUrls["${canonicalPath}"].${language} must be nested under "${parentPath}" because its parent route is localised there.`);
82
+ return normaliseRoutePath(fullPath.slice(parentPath.length));
83
+ });
84
+ return Array.from(new Set(paths.filter(Boolean)));
85
+ };
86
+ const transformLocalisedRoute = (route, parentCanonicalPath, parentLocalisedPaths, languages, localisedUrls)=>{
87
+ const canonicalPath = joinPath(parentCanonicalPath, route.path);
88
+ const localisedUrlEntry = isLocalisableRoutePath(route.path) ? ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls) : void 0;
89
+ const routeLocalisedPaths = localisedUrlEntry ? languages.reduce((acc, language)=>{
90
+ acc[language] = normalisePathPattern(localisedUrlEntry[language]);
91
+ return acc;
92
+ }, {}) : parentLocalisedPaths;
93
+ const children = 'children' in route && route.children ? route.children.flatMap((child)=>transformLocalisedRoute(child, canonicalPath, routeLocalisedPaths, languages, localisedUrls)) : void 0;
94
+ const baseRoute = {
95
+ ...route,
96
+ ...children ? {
97
+ children
98
+ } : {}
99
+ };
100
+ if (!localisedUrlEntry) return [
101
+ baseRoute
102
+ ];
103
+ return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index));
104
+ };
105
+ const legalRouteIdPart = (value)=>value.replace(/[^a-zA-Z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'index';
106
+ const suffixRouteIds = (route, suffix)=>{
107
+ const children = 'children' in route && route.children ? route.children.map((child)=>suffixRouteIds(child, suffix)) : void 0;
108
+ return {
109
+ ...route,
110
+ ...route.id ? {
111
+ id: `${route.id}__localised_${suffix}`
112
+ } : {},
113
+ ...children ? {
114
+ children
115
+ } : {}
116
+ };
117
+ };
118
+ const cloneRouteWithLocalisedPath = (route, path, index)=>{
119
+ const leadingLocaleParam = getLeadingLocaleParam(route.path);
120
+ const localisedPath = leadingLocaleParam ? normaliseRoutePath(`${leadingLocaleParam}/${path}`) : path;
121
+ const routeWithPath = {
122
+ ...route,
123
+ path: localisedPath
124
+ };
125
+ return 0 === index ? routeWithPath : suffixRouteIds(routeWithPath, legalRouteIdPart(localisedPath));
126
+ };
127
+ const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
128
+ const rootLocalisedPaths = languages.reduce((acc, language)=>{
129
+ acc[language] = '/';
130
+ return acc;
131
+ }, {});
132
+ validateLocalisedUrls(routes, languages, localisedUrls);
133
+ return routes.flatMap((route)=>transformLocalisedRoute(route, '', rootLocalisedPaths, languages, localisedUrls));
134
+ };
135
+ const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
136
+ const getParamName = (segment)=>segment.slice(1).replace(/\?$/, '');
137
+ const compilePathPattern = (pattern)=>{
138
+ const names = [];
139
+ const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
140
+ const source = segments.map((segment)=>{
141
+ if (segment.startsWith(':')) {
142
+ names.push(getParamName(segment));
143
+ const paramPattern = '([^/]+)';
144
+ return segment.endsWith('?') ? `(?:/${paramPattern})?` : `/${paramPattern}`;
145
+ }
146
+ if ('*' === segment) {
147
+ names.push('*');
148
+ return '/(.*)';
149
+ }
150
+ return `/${escapeRegExp(segment)}`;
151
+ }).join('');
152
+ return {
153
+ names,
154
+ regexp: new RegExp(`^${source || '/'}$`)
155
+ };
156
+ };
157
+ const matchPathPattern = (pathname, pattern)=>{
158
+ const { names, regexp } = compilePathPattern(pattern);
159
+ const match = regexp.exec(normalisePathPattern(pathname));
160
+ if (!match) return null;
161
+ return names.reduce((params, name, index)=>{
162
+ params[name] = decodeURIComponent(match[index + 1] || '');
163
+ return params;
164
+ }, {});
165
+ };
166
+ const buildPathFromPattern = (pattern, params)=>{
167
+ const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
168
+ const path = segments.map((segment)=>{
169
+ if (segment.startsWith(':')) {
170
+ const param = params[getParamName(segment)];
171
+ return param ? encodeURIComponent(param) : '';
172
+ }
173
+ if ('*' === segment) return params['*'] || '';
174
+ return segment;
175
+ }).filter(Boolean).join('/');
176
+ return `/${path}`;
177
+ };
178
+ const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls)=>{
179
+ const normalizedPathname = normalisePathPattern(pathname);
180
+ for (const localisedUrlEntry of Object.values(localisedUrls)){
181
+ const targetPattern = localisedUrlEntry[targetLanguage];
182
+ if (targetPattern) for (const language of languages){
183
+ const sourcePattern = localisedUrlEntry[language];
184
+ if (!sourcePattern) continue;
185
+ const params = matchPathPattern(normalizedPathname, sourcePattern);
186
+ if (params) return buildPathFromPattern(targetPattern, params);
187
+ }
188
+ }
189
+ return normalizedPathname;
190
+ };
191
+ export { applyLocalisedUrlsToRoutes, normalisePathPattern, resolveLocalisedPath, resolveLocalisedUrlsConfig, validateLocalisedUrls };
@@ -2,6 +2,7 @@ import "node:module";
2
2
  import { getPublicDirRoutePrefixes } from "@modern-js/server-core";
3
3
  import fs from "fs";
4
4
  import path from "path";
5
+ import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
5
6
  import { getBackendOptions, getLocaleDetectionOptions } from "../shared/utils.mjs";
6
7
  function hasJsonFiles(dirPath) {
7
8
  try {
@@ -81,6 +82,27 @@ const i18nPlugin = (options = {})=>({
81
82
  plugins
82
83
  };
83
84
  });
85
+ api.modifyFileSystemRoutes(({ entrypoint, routes })=>{
86
+ if (!localeDetection) return {
87
+ entrypoint,
88
+ routes
89
+ };
90
+ const localeDetectionOptions = getLocaleDetectionOptions(entrypoint.entryName, localeDetection);
91
+ const { localePathRedirect, languages = [], localisedUrls } = localeDetectionOptions;
92
+ if (!localePathRedirect || 0 === languages.length) return {
93
+ entrypoint,
94
+ routes
95
+ };
96
+ const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
97
+ if (!localisedUrlsConfig.enabled) return {
98
+ entrypoint,
99
+ routes
100
+ };
101
+ return {
102
+ entrypoint,
103
+ routes: applyLocalisedUrlsToRoutes(routes, languages, localisedUrlsConfig.map)
104
+ };
105
+ });
84
106
  api._internalServerPlugins(({ plugins })=>{
85
107
  const { serverRoutes, metaName } = api.getAppContext();
86
108
  const normalizedConfig = api.getNormalizedConfig();
@@ -1,21 +1,13 @@
1
1
  import "node:module";
2
2
  import { jsx } from "react/jsx-runtime";
3
- import { Link as router_Link, useInRouterContext, useParams } from "@modern-js/runtime/router";
4
3
  import { useModernI18n } from "./context.mjs";
4
+ import { useI18nRouterAdapter } from "./routerAdapter.mjs";
5
5
  import { buildLocalizedUrl } from "./utils.mjs";
6
- const useRouterHooks = ()=>{
7
- const inRouter = useInRouterContext();
8
- return {
9
- Link: inRouter ? router_Link : null,
10
- params: inRouter ? useParams() : {},
11
- hasRouter: inRouter
12
- };
13
- };
14
6
  const I18nLink = ({ to, children, ...props })=>{
15
- const { Link, params, hasRouter } = useRouterHooks();
16
- const { language, supportedLanguages } = useModernI18n();
7
+ const { Link, params, hasRouter } = useI18nRouterAdapter();
8
+ const { language, supportedLanguages, localisedUrls } = useModernI18n();
17
9
  const currentLang = language;
18
- const localizedTo = buildLocalizedUrl(to, currentLang, supportedLanguages);
10
+ const localizedTo = buildLocalizedUrl(to, currentLang, supportedLanguages, localisedUrls);
19
11
  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.");
20
12
  if (!hasRouter || !Link) return /*#__PURE__*/ jsx("a", {
21
13
  href: localizedTo,
@@ -1,9 +1,10 @@
1
1
  import "node:module";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { isBrowser } from "@modern-js/runtime";
4
- import { createContext, useCallback, useContext, useMemo } from "react";
4
+ import { createContext, useCallback, useContext, useEffect, useMemo } from "react";
5
5
  import { cacheUserLanguage } from "./i18n/detection/index.mjs";
6
- import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, shouldIgnoreRedirect, useRouterHooks } from "./utils.mjs";
6
+ import { useI18nRouterAdapter } from "./routerAdapter.mjs";
7
+ import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, shouldIgnoreRedirect } from "./utils.mjs";
7
8
  const ModernI18nContext = /*#__PURE__*/ createContext(null);
8
9
  const ModernI18nProvider = ({ children, value })=>/*#__PURE__*/ jsx(ModernI18nContext.Provider, {
9
10
  value: value,
@@ -12,9 +13,33 @@ const ModernI18nProvider = ({ children, value })=>/*#__PURE__*/ jsx(ModernI18nCo
12
13
  const useModernI18n = ()=>{
13
14
  const context = useContext(ModernI18nContext);
14
15
  if (!context) throw new Error('useModernI18n must be used within a ModernI18nProvider');
15
- const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, updateLanguage } = context;
16
- const { navigate, location, hasRouter } = useRouterHooks();
17
- const currentLanguage = contextLanguage;
16
+ const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, updateLanguage } = context;
17
+ const { navigate, location, hasRouter } = useI18nRouterAdapter();
18
+ const pathLanguage = useMemo(()=>{
19
+ if (!localePathRedirect || !location?.pathname) return;
20
+ const detected = detectLanguageFromPath(location.pathname, languages || [], localePathRedirect);
21
+ return detected.detected ? detected.language : void 0;
22
+ }, [
23
+ languages,
24
+ localePathRedirect,
25
+ location?.pathname
26
+ ]);
27
+ const currentLanguage = pathLanguage || contextLanguage;
28
+ useEffect(()=>{
29
+ if (!pathLanguage || pathLanguage === contextLanguage) return;
30
+ updateLanguage?.(pathLanguage);
31
+ i18nInstance?.setLang?.(pathLanguage);
32
+ i18nInstance?.changeLanguage?.(pathLanguage);
33
+ if (isBrowser()) {
34
+ const detectionOptions = i18nInstance.options?.detection;
35
+ cacheUserLanguage(i18nInstance, pathLanguage, detectionOptions);
36
+ }
37
+ }, [
38
+ contextLanguage,
39
+ i18nInstance,
40
+ pathLanguage,
41
+ updateLanguage
42
+ ]);
18
43
  const changeLanguage = useCallback(async (newLang)=>{
19
44
  try {
20
45
  if (!newLang || 'string' != typeof newLang) throw new Error('Language must be a non-empty string');
@@ -31,7 +56,7 @@ const useModernI18n = ()=>{
31
56
  const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
32
57
  if (pathLanguage.detected && pathLanguage.language === newLang) return;
33
58
  if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
34
- const newPath = buildLocalizedUrl(relativePath, newLang, languages || []);
59
+ const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
35
60
  const newUrl = entryPath + newPath + location.search + location.hash;
36
61
  await navigate(newUrl, {
37
62
  replace: true
@@ -44,7 +69,7 @@ const useModernI18n = ()=>{
44
69
  const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
45
70
  if (pathLanguage.detected && pathLanguage.language === newLang) return;
46
71
  if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
47
- const newPath = buildLocalizedUrl(relativePath, newLang, languages || []);
72
+ const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
48
73
  const newUrl = entryPath + newPath + window.location.search + window.location.hash;
49
74
  window.history.pushState(null, '', newUrl);
50
75
  }
@@ -59,6 +84,7 @@ const useModernI18n = ()=>{
59
84
  updateLanguage,
60
85
  localePathRedirect,
61
86
  ignoreRedirectRoutes,
87
+ localisedUrls,
62
88
  languages,
63
89
  hasRouter,
64
90
  navigate,
@@ -99,6 +125,7 @@ const useModernI18n = ()=>{
99
125
  changeLanguage,
100
126
  i18nInstance,
101
127
  supportedLanguages: languages || [],
128
+ localisedUrls,
102
129
  isLanguageSupported,
103
130
  isResourcesReady
104
131
  };
@@ -3,7 +3,8 @@ import { isBrowser } from "@modern-js/runtime";
3
3
  import { useEffect, useRef } from "react";
4
4
  import { I18N_SDK_RESOURCES_LOADED_EVENT, getI18nSdkBackendId } from "./i18n/backend/sdk-event.mjs";
5
5
  import { cacheUserLanguage } from "./i18n/detection/index.mjs";
6
- import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getPathname, shouldIgnoreRedirect, useRouterHooks } from "./utils.mjs";
6
+ import { useI18nRouterAdapter } from "./routerAdapter.mjs";
7
+ import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getPathname, shouldIgnoreRedirect } from "./utils.mjs";
7
8
  function createMinimalI18nInstance(language) {
8
9
  const minimalInstance = {
9
10
  language,
@@ -15,7 +16,7 @@ function createMinimalI18nInstance(language) {
15
16
  };
16
17
  return minimalInstance;
17
18
  }
18
- function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang) {
19
+ function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang) {
19
20
  const instance = i18nInstance || createMinimalI18nInstance(lang);
20
21
  return {
21
22
  language: lang,
@@ -24,6 +25,7 @@ function createContextValue(lang, i18nInstance, entryName, languages, localePath
24
25
  languages,
25
26
  localePathRedirect,
26
27
  ignoreRedirectRoutes,
28
+ localisedUrls,
27
29
  updateLanguage: setLang
28
30
  };
29
31
  }
@@ -73,9 +75,9 @@ function useSdkResourcesLoader(i18nInstance, setForceUpdate) {
73
75
  setForceUpdate
74
76
  ]);
75
77
  }
76
- function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes) {
78
+ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls) {
77
79
  const hasRedirectedRef = useRef(false);
78
- const { navigate, location, hasRouter } = useRouterHooks();
80
+ const { navigate, location, hasRouter } = useI18nRouterAdapter();
79
81
  useEffect(()=>{
80
82
  if ('browser' !== process.env.MODERN_TARGET) return;
81
83
  if (!localePathRedirect || !i18nInstance) return;
@@ -94,7 +96,7 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
94
96
  const pathDetection = detectLanguageFromPath(currentPathname, languages, localePathRedirect);
95
97
  if (pathDetection.detected) return;
96
98
  const targetLanguage = i18nInstance.language || fallbackLanguage || languages[0] || 'en';
97
- const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages);
99
+ const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages, localisedUrls);
98
100
  const newUrl = entryPath + newPath + currentSearch + currentHash;
99
101
  if (newUrl !== currentPathname + currentSearch + currentHash) {
100
102
  hasRedirectedRef.current = true;
@@ -111,7 +113,8 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
111
113
  i18nInstance,
112
114
  languages,
113
115
  fallbackLanguage,
114
- ignoreRedirectRoutes
116
+ ignoreRedirectRoutes,
117
+ localisedUrls
115
118
  ]);
116
119
  }
117
120
  function useLanguageSync(i18nInstance, localePathRedirect, languages, runtimeContextRef, prevLangRef, setLang) {
@@ -5,7 +5,7 @@ const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
5
5
  };
6
6
  function convertPath(path) {
7
7
  if (!path) return path;
8
- if (path.startsWith('/')) return `${window.__assetPrefix__ || ''}${path}`;
8
+ if (path.startsWith('/')) return "u" < typeof window ? path : `${window.__assetPrefix__ || ''}${path}`;
9
9
  return path;
10
10
  }
11
11
  function convertBackendOptions(options) {
@@ -1,7 +1,7 @@
1
1
  import "node:module";
2
2
  const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
3
- loadPath: './locales/{{lng}}/{{ns}}.json',
4
- addPath: './locales/{{lng}}/{{ns}}.json'
3
+ loadPath: './config/public/locales/{{lng}}/{{ns}}.json',
4
+ addPath: './config/public/locales/{{lng}}/{{ns}}.json'
5
5
  };
6
6
  function convertPath(path) {
7
7
  if (!path) return path;
@@ -1,9 +1,9 @@
1
1
  import "node:module";
2
- import i18next_fs_backend from "i18next-fs-backend";
2
+ import cjs from "i18next-fs-backend/cjs";
3
3
  import { useI18nextBackendCommon } from "./middleware.common.mjs";
4
- class FsBackendWithSave extends i18next_fs_backend {
4
+ class FsBackendWithSave extends cjs {
5
5
  save(_language, _namespace, _data) {}
6
6
  }
7
7
  const HttpBackendWithSave = FsBackendWithSave;
8
- const useI18nextBackend = (i18nInstance, backend)=>useI18nextBackendCommon(i18nInstance, FsBackendWithSave, i18next_fs_backend, backend);
8
+ const useI18nextBackend = (i18nInstance, backend)=>useI18nextBackendCommon(i18nInstance, FsBackendWithSave, cjs, backend);
9
9
  export { FsBackendWithSave, HttpBackendWithSave, useI18nextBackend };
@@ -41,14 +41,6 @@ async function createI18nextInstance() {
41
41
  return null;
42
42
  }
43
43
  }
44
- async function tryImportReactI18next() {
45
- try {
46
- const reactI18next = await import("react-i18next");
47
- return reactI18next;
48
- } catch (error) {
49
- return null;
50
- }
51
- }
52
44
  function getI18nextInstanceForProvider(instance) {
53
45
  if (isI18nWrapperInstance(instance)) {
54
46
  const i18nextInstance = getI18nWrapperI18nextInstance(instance);
@@ -65,14 +57,4 @@ async function getI18nInstance(userInstance) {
65
57
  if (i18nextInstance) return i18nextInstance;
66
58
  throw new Error('No i18n instance found');
67
59
  }
68
- async function getInitReactI18next() {
69
- const reactI18nextModule = await tryImportReactI18next();
70
- if (reactI18nextModule) return reactI18nextModule.initReactI18next;
71
- return null;
72
- }
73
- async function getI18nextProvider() {
74
- const reactI18nextModule = await tryImportReactI18next();
75
- if (reactI18nextModule) return reactI18nextModule.I18nextProvider;
76
- return null;
77
- }
78
- export { getActualI18nextInstance, getI18nInstance, getI18nWrapperI18nextInstance, getI18nextInstanceForProvider, getI18nextProvider, getInitReactI18next, isI18nInstance, isI18nWrapperInstance };
60
+ export { getActualI18nextInstance, getI18nInstance, getI18nWrapperI18nextInstance, getI18nextInstanceForProvider, isI18nInstance, isI18nWrapperInstance };
@@ -0,0 +1,16 @@
1
+ import "node:module";
2
+ async function tryImportReactI18next() {
3
+ try {
4
+ return await import("react-i18next");
5
+ } catch (error) {
6
+ return null;
7
+ }
8
+ }
9
+ async function getReactI18nextIntegration() {
10
+ const reactI18nextModule = await tryImportReactI18next();
11
+ return {
12
+ I18nextProvider: reactI18nextModule?.I18nextProvider ?? null,
13
+ initReactI18next: reactI18nextModule?.initReactI18next ?? null
14
+ };
15
+ }
16
+ export { getReactI18nextIntegration };
@@ -101,18 +101,6 @@ const initializeI18nInstance = async (i18nInstance, finalLanguage, fallbackLangu
101
101
  };
102
102
  }
103
103
  }
104
- if (mergedBackend && hasOptions(i18nInstance)) {
105
- const defaultNS = initOptions.defaultNS || initOptions.ns || 'translation';
106
- const ns = Array.isArray(defaultNS) ? defaultNS[0] : defaultNS;
107
- let retries = 20;
108
- while(retries > 0){
109
- const actualInstance = getActualI18nextInstance(i18nInstance);
110
- const store = actualInstance.store;
111
- if (store?.data?.[finalLanguage]?.[ns]) break;
112
- await new Promise((resolve)=>setTimeout(resolve, 100));
113
- retries--;
114
- }
115
- }
116
104
  }
117
105
  };
118
106
  function hasOptions(instance) {
@@ -11,26 +11,31 @@ import { mergeBackendOptions } from "./i18n/backend/index.mjs";
11
11
  import { useI18nextBackend } from "./i18n/backend/middleware.mjs";
12
12
  import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOptions } from "./i18n/detection/index.mjs";
13
13
  import { useI18nextLanguageDetector } from "./i18n/detection/middleware.mjs";
14
- import { getI18nextInstanceForProvider, getI18nextProvider, getInitReactI18next } from "./i18n/instance.mjs";
14
+ import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
15
15
  import { changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } from "./i18n/utils.mjs";
16
16
  import { getPathname } from "./utils.mjs";
17
17
  import "./types.mjs";
18
18
  const i18nPlugin = (options)=>({
19
19
  name: '@modern-js/plugin-i18n',
20
20
  setup: (api)=>{
21
- const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false } = options;
22
- const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = localeDetection || {};
21
+ const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true } = options;
22
+ const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes, localisedUrls } = localeDetection || {};
23
23
  const { enabled: backendEnabled = false } = backend || {};
24
24
  let latestI18nInstance;
25
25
  let I18nextProvider;
26
+ const loadReactI18nextIntegration = async ()=>{
27
+ if (!reactI18next) return null;
28
+ const { getReactI18nextIntegration } = await import("./i18n/react-i18next.mjs");
29
+ return getReactI18nextIntegration();
30
+ };
26
31
  api.onBeforeRender(async (context)=>{
27
32
  let i18nInstance = await getI18nInstance(userI18nInstance);
28
33
  const { i18n: otherConfig } = api.getRuntimeConfig();
29
34
  const { initOptions: otherInitOptions } = otherConfig || {};
30
35
  const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
31
- const initReactI18next = await getInitReactI18next();
32
- I18nextProvider = await getI18nextProvider();
33
- if (initReactI18next) i18nInstance.use(initReactI18next);
36
+ const reactI18nextIntegration = await loadReactI18nextIntegration();
37
+ I18nextProvider = reactI18nextIntegration?.I18nextProvider ?? null;
38
+ if (reactI18nextIntegration?.initReactI18next) i18nInstance.use(reactI18nextIntegration.initReactI18next);
34
39
  const pathname = getPathname(context);
35
40
  if (i18nextDetector) useI18nextLanguageDetector(i18nInstance);
36
41
  const mergedDetection = mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions);
@@ -90,16 +95,18 @@ const i18nPlugin = (options)=>({
90
95
  ]);
91
96
  useSdkResourcesLoader(i18nInstance, setForceUpdate);
92
97
  useLanguageSync(i18nInstance, localePathRedirect, languages, runtimeContextRef, prevLangRef, setLang);
93
- useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes);
94
- const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang), [
98
+ useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls);
99
+ const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang), [
95
100
  lang,
96
101
  i18nInstance,
97
102
  entryName,
98
103
  languages,
99
104
  localePathRedirect,
100
105
  ignoreRedirectRoutes,
106
+ localisedUrls,
101
107
  forceUpdate
102
108
  ]);
109
+ const children = props.children;
103
110
  const appContent = /*#__PURE__*/ jsxs(Fragment, {
104
111
  children: [
105
112
  Boolean(htmlLangAttr) && /*#__PURE__*/ jsx(Helmet, {
@@ -109,9 +116,10 @@ const i18nPlugin = (options)=>({
109
116
  }),
110
117
  /*#__PURE__*/ jsx(ModernI18nProvider, {
111
118
  value: contextValue,
112
- children: /*#__PURE__*/ jsx(App, {
113
- ...props
114
- })
119
+ children: App ? /*#__PURE__*/ jsx(App, {
120
+ ...props,
121
+ children: children
122
+ }) : children
115
123
  })
116
124
  ]
117
125
  });