@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.4 → 3.9.0-ultramodern.6

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 (79) hide show
  1. package/dist/cjs/cli/index.js +0 -22
  2. package/dist/cjs/runtime/Link.js +19 -32
  3. package/dist/cjs/runtime/context.js +26 -61
  4. package/dist/cjs/runtime/contextHelpers.js +5 -5
  5. package/dist/cjs/runtime/core.js +17 -14
  6. package/dist/cjs/runtime/hooks.js +7 -8
  7. package/dist/cjs/runtime/localizedPaths.js +11 -9
  8. package/dist/cjs/runtime/providerComposition.js +37 -13
  9. package/dist/cjs/runtime/routerAdapter.js +35 -140
  10. package/dist/cjs/runtime/utils.js +17 -19
  11. package/dist/cjs/server/index.js +10 -7
  12. package/dist/cjs/server/redirectPolicy.js +36 -31
  13. package/dist/cjs/shared/urlStrategy.js +18 -0
  14. package/dist/esm/cli/index.mjs +0 -22
  15. package/dist/esm/runtime/Link.mjs +19 -32
  16. package/dist/esm/runtime/context.mjs +18 -62
  17. package/dist/esm/runtime/contextHelpers.mjs +5 -5
  18. package/dist/esm/runtime/core.mjs +8 -5
  19. package/dist/esm/runtime/hooks.mjs +7 -8
  20. package/dist/esm/runtime/localizedPaths.mjs +11 -9
  21. package/dist/esm/runtime/providerComposition.mjs +38 -14
  22. package/dist/esm/runtime/routerAdapter.mjs +30 -141
  23. package/dist/esm/runtime/utils.mjs +15 -17
  24. package/dist/esm/server/index.mjs +10 -7
  25. package/dist/esm/server/redirectPolicy.mjs +29 -24
  26. package/dist/esm/shared/urlStrategy.mjs +0 -0
  27. package/dist/esm-node/cli/index.mjs +0 -22
  28. package/dist/esm-node/runtime/Link.mjs +19 -32
  29. package/dist/esm-node/runtime/context.mjs +18 -62
  30. package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
  31. package/dist/esm-node/runtime/core.mjs +8 -5
  32. package/dist/esm-node/runtime/hooks.mjs +7 -8
  33. package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
  34. package/dist/esm-node/runtime/providerComposition.mjs +38 -14
  35. package/dist/esm-node/runtime/routerAdapter.mjs +30 -141
  36. package/dist/esm-node/runtime/utils.mjs +15 -17
  37. package/dist/esm-node/server/index.mjs +10 -7
  38. package/dist/esm-node/server/redirectPolicy.mjs +29 -24
  39. package/dist/esm-node/shared/urlStrategy.mjs +1 -0
  40. package/dist/types/runtime/context.d.ts +8 -11
  41. package/dist/types/runtime/contextHelpers.d.ts +2 -2
  42. package/dist/types/runtime/core.d.ts +11 -2
  43. package/dist/types/runtime/hooks.d.ts +4 -5
  44. package/dist/types/runtime/localizedPaths.d.ts +4 -4
  45. package/dist/types/runtime/providerComposition.d.ts +13 -1
  46. package/dist/types/runtime/routerAdapter.d.ts +23 -6
  47. package/dist/types/runtime/utils.d.ts +4 -12
  48. package/dist/types/server/index.d.ts +2 -0
  49. package/dist/types/server/redirectPolicy.d.ts +5 -13
  50. package/dist/types/shared/type.d.ts +0 -14
  51. package/dist/types/shared/urlStrategy.d.ts +7 -0
  52. package/package.json +21 -11
  53. package/rstest.config.mts +2 -8
  54. package/src/cli/index.ts +1 -43
  55. package/src/runtime/Link.tsx +20 -40
  56. package/src/runtime/context.tsx +34 -133
  57. package/src/runtime/contextHelpers.ts +7 -5
  58. package/src/runtime/core.tsx +26 -6
  59. package/src/runtime/hooks.ts +14 -9
  60. package/src/runtime/localizedPaths.ts +24 -19
  61. package/src/runtime/providerComposition.tsx +71 -23
  62. package/src/runtime/routerAdapter.tsx +73 -338
  63. package/src/runtime/utils.ts +40 -31
  64. package/src/server/index.ts +44 -9
  65. package/src/server/redirectPolicy.ts +70 -70
  66. package/src/shared/type.ts +0 -14
  67. package/src/shared/urlStrategy.ts +11 -0
  68. package/tests/federatedI18nBoundaryResolver.test.tsx +71 -0
  69. package/tests/i18nUtils.test.ts +31 -65
  70. package/tests/link.test.tsx +13 -511
  71. package/tests/linkTypes.test.ts +12 -3
  72. package/tests/localisedUrlRewriteMatrix.fork.test.ts +4 -96
  73. package/tests/localisedUrls.test.ts +7 -216
  74. package/tests/reactI18nextRuntimeBoundary.test.ts +1 -82
  75. package/tests/redirectPolicy.test.ts +39 -30
  76. package/tests/routerAdapter.test.tsx +95 -1055
  77. package/tests/federatedI18nBoundary.client.test.tsx +0 -129
  78. package/tests/federatedI18nBoundary.test.tsx +0 -132
  79. package/tests/peerCohort.test.ts +0 -25
@@ -1,4 +1,3 @@
1
- import { resolveLocalisedUrlsConfig } from "@modern-js/i18n-runtime-extensions";
2
1
  import { getLocaleDetectionOptions } from "../shared/utils.mjs";
3
2
  import { collectApiPrefixes, matchesApiPrefix } from "./apiPrefix.mjs";
4
3
  import { convertToHonoLanguageDetectorOptions } from "./detectorOptions.mjs";
@@ -24,7 +23,11 @@ const i18nServerPlugin = (options)=>({
24
23
  const { entryName } = route;
25
24
  if (!entryName) return;
26
25
  if (!options.localeDetection) return;
27
- const { localePathRedirect, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes, localisedUrls } = getLocaleDetectionOptions(entryName, options.localeDetection);
26
+ const entryLocaleDetection = getLocaleDetectionOptions(entryName, options.localeDetection);
27
+ const { localePathRedirect, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = entryLocaleDetection;
28
+ const legacyLocalisedUrls = Reflect.get(entryLocaleDetection, 'localisedUrls');
29
+ const urlStrategy = options.resolveUrlStrategy?.(entryName);
30
+ if (legacyLocalisedUrls && 'object' == typeof legacyLocalisedUrls && Object.keys(legacyLocalisedUrls).length > 0 && !urlStrategy) throw new Error('Mapped locale URLs require a URL strategy. Use the i18n integration server plugin or supply resolveUrlStrategy.');
28
31
  const staticRoutePrefixes = options.staticRoutePrefixes;
29
32
  const originUrlPath = route.urlPath;
30
33
  const urlPath = originUrlPath.endsWith('/') ? `${originUrlPath}*` : `${originUrlPath}/*`;
@@ -39,6 +42,7 @@ const i18nServerPlugin = (options)=>({
39
42
  const url = new URL(c.req.url);
40
43
  const pathname = url.pathname;
41
44
  if (matchesApiPrefix(pathname, apiPrefixes)) return await next();
45
+ if (shouldIgnoreRedirect(pathname, urlPath, ignoreRedirectRoutes, urlStrategy, languages)) return await next();
42
46
  if (isStaticResourceRequest(pathname, staticRoutePrefixes, languages)) return await next();
43
47
  if ('/' === originUrlPath) {
44
48
  const pathSegments = pathname.split('/').filter(Boolean);
@@ -59,7 +63,7 @@ const i18nServerPlugin = (options)=>({
59
63
  const pathname = url.pathname;
60
64
  if (matchesApiPrefix(pathname, apiPrefixes)) return await next();
61
65
  if (isStaticResourceRequest(pathname, staticRoutePrefixes, languages)) return await next();
62
- if (shouldIgnoreRedirect(pathname, urlPath, ignoreRedirectRoutes)) return await next();
66
+ if (shouldIgnoreRedirect(pathname, urlPath, ignoreRedirectRoutes, urlStrategy, languages)) return await next();
63
67
  if ('/' === originUrlPath) {
64
68
  const pathSegments = pathname.split('/').filter(Boolean);
65
69
  if (pathSegments.length > 0) {
@@ -72,12 +76,11 @@ const i18nServerPlugin = (options)=>({
72
76
  let detectedLanguage = null;
73
77
  if (i18nextDetector) detectedLanguage = c.get('language') || null;
74
78
  const targetLanguage = detectedLanguage || fallbackLanguage;
75
- const localizedUrl = buildLocalizedUrl(c.req, originUrlPath, targetLanguage, languages, localisedUrls);
79
+ const localizedUrl = buildLocalizedUrl(c.req, originUrlPath, targetLanguage, languages, urlStrategy);
76
80
  return createLocaleRedirectResponse(localizedUrl);
77
81
  }
78
- const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
79
- if (localisedUrlsConfig.enabled) {
80
- const expectedUrl = buildLocalizedUrl(c.req, originUrlPath, language, languages, localisedUrls);
82
+ if (urlStrategy) {
83
+ const expectedUrl = buildLocalizedUrl(c.req, originUrlPath, language, languages, urlStrategy);
81
84
  if (expectedUrl !== `${pathname}${url.search}${url.hash}`) return createLocaleRedirectResponse(expectedUrl);
82
85
  }
83
86
  await next();
@@ -1,39 +1,44 @@
1
- import { isDefaultLocaleRedirectSkipPath, localiseTargetPathname, matchesPathPrefix, shouldSkipLocaleRedirect, stripLanguagePrefix } from "@modern-js/i18n-runtime-extensions";
2
1
  const stripUrlPathPrefix = (pathname, urlPath)=>{
3
2
  const basePath = urlPath.replace('/*', '');
4
3
  if (!basePath || '/' === basePath) return pathname;
5
- const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
6
- return remainingPath || '/';
4
+ return pathname === basePath || pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) || '/' : pathname;
7
5
  };
8
- const shouldIgnoreRedirect = (pathname, urlPath, ignoreRedirectRoutes)=>{
6
+ const shouldIgnoreRedirect = (pathname, urlPath, ignoreRedirectRoutes, urlStrategy, languages = [])=>{
9
7
  const remainingPath = stripUrlPathPrefix(pathname, urlPath);
10
- return shouldSkipLocaleRedirect(remainingPath, [], ignoreRedirectRoutes);
8
+ if (urlStrategy?.shouldSkipRedirect?.(remainingPath, languages)) return true;
9
+ if (!ignoreRedirectRoutes) return false;
10
+ const segments = remainingPath.split('/').filter(Boolean);
11
+ if (segments[0] && languages.some((language)=>language.toLowerCase() === segments[0].toLowerCase())) segments.shift();
12
+ const normalizedPath = `/${segments.join('/')}`;
13
+ return 'function' == typeof ignoreRedirectRoutes ? ignoreRedirectRoutes(normalizedPath) : ignoreRedirectRoutes.some((pattern)=>normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`));
11
14
  };
12
15
  const isStaticResourceRequest = (pathname, staticRoutePrefixes, languages = [])=>{
13
- if (isDefaultLocaleRedirectSkipPath(pathname, languages)) return true;
14
- const matchesStaticRoutePrefix = (targetPathname)=>staticRoutePrefixes.some((prefix)=>matchesPathPrefix(targetPathname, prefix));
15
- if (matchesStaticRoutePrefix(pathname)) return true;
16
- const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
17
- return pathWithoutLanguage !== pathname && matchesStaticRoutePrefix(pathWithoutLanguage);
16
+ const prefixes = [
17
+ ...staticRoutePrefixes,
18
+ '/static',
19
+ '/upload'
20
+ ];
21
+ const matches = (target)=>prefixes.some((prefix)=>target === prefix || target.startsWith(`${prefix}/`));
22
+ if (matches(pathname)) return true;
23
+ const segments = pathname.split('/').filter(Boolean);
24
+ if (segments[0] && languages.some((language)=>language.toLowerCase() === segments[0].toLowerCase())) return matches(`/${segments.slice(1).join('/')}`);
25
+ return false;
18
26
  };
19
27
  const getLanguageFromPath = (req, urlPath, languages)=>{
20
- const url = new URL(req.url, `http://${req.header().host}`);
21
- const pathname = url.pathname;
22
- const remainingPath = stripUrlPathPrefix(pathname, urlPath);
23
- const segments = remainingPath.split('/').filter(Boolean);
24
- const firstSegment = segments[0];
25
- if (languages.includes(firstSegment)) return firstSegment;
26
- return null;
28
+ const host = req.header?.().host;
29
+ const url = new URL(req.url, host ? `http://${host}` : void 0);
30
+ const firstSegment = stripUrlPathPrefix(url.pathname, urlPath).split('/').filter(Boolean)[0];
31
+ return languages.find((language)=>language.toLowerCase() === firstSegment?.toLowerCase()) ?? null;
27
32
  };
28
- const buildLocalizedUrl = (req, urlPath, language, languages, localisedUrls)=>{
33
+ const buildLocalizedUrl = (req, urlPath, language, languages, urlStrategy)=>{
29
34
  const url = new URL(req.url);
30
- const pathname = url.pathname;
31
35
  const basePath = urlPath.replace('/*', '');
32
- const remainingPath = stripUrlPathPrefix(pathname, urlPath);
33
- const newPathname = localiseTargetPathname(remainingPath, language, languages, localisedUrls);
34
- const suffix = `${url.search}${url.hash}`;
35
- const localizedUrl = '/' === basePath ? newPathname + suffix : basePath + newPathname + suffix;
36
- return localizedUrl;
36
+ const remainingPath = stripUrlPathPrefix(url.pathname, urlPath);
37
+ const segments = remainingPath.split('/').filter(Boolean);
38
+ if (segments[0] && languages.some((item)=>item.toLowerCase() === segments[0].toLowerCase())) segments[0] = language;
39
+ else segments.unshift(language);
40
+ const pathname = urlStrategy ? urlStrategy.localizePathname(remainingPath, language, languages) : `/${segments.join('/')}`;
41
+ return `${'/' === basePath ? '' : basePath}${pathname}${url.search}${url.hash}`;
37
42
  };
38
43
  const createLocaleRedirectResponse = (location)=>new Response(null, {
39
44
  status: 302,
File without changes
@@ -1,5 +1,4 @@
1
1
  import "node:module";
2
- import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "@modern-js/i18n-runtime-extensions";
3
2
  import { getPublicDirRoutePrefixes } from "@modern-js/server-core";
4
3
  import { getBackendOptions, getLocaleDetectionOptions } from "../shared/utils.mjs";
5
4
  import { applyDetectedBackendPaths, detectLocalesDirectory } from "./locales.mjs";
@@ -44,27 +43,6 @@ const i18nPlugin = (options = {})=>({
44
43
  plugins
45
44
  };
46
45
  });
47
- api.modifyFileSystemRoutes(({ entrypoint, routes })=>{
48
- if (!localeDetection) return {
49
- entrypoint,
50
- routes
51
- };
52
- const localeDetectionOptions = getLocaleDetectionOptions(entrypoint.entryName, localeDetection);
53
- const { localePathRedirect, languages = [], localisedUrls } = localeDetectionOptions;
54
- if (!localePathRedirect || 0 === languages.length) return {
55
- entrypoint,
56
- routes
57
- };
58
- const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
59
- if (!localisedUrlsConfig.enabled) return {
60
- entrypoint,
61
- routes
62
- };
63
- return {
64
- entrypoint,
65
- routes: applyLocalisedUrlsToRoutes(routes, languages, localisedUrlsConfig.map)
66
- };
67
- });
68
46
  api._internalServerPlugins(({ plugins })=>{
69
47
  const { serverRoutes, metaName } = api.getAppContext();
70
48
  const normalizedConfig = api.getNormalizedConfig();
@@ -10,10 +10,10 @@ const EXTERNAL_TARGET_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
10
10
  const Link = (props)=>{
11
11
  const { to, params, children, hash: hashProp, search: searchProp, hashScrollIntoView, activeOptions, activeProps, prefetch, preload, ...rest } = props;
12
12
  const adapter = useI18nRouterAdapter();
13
- const { language, supportedLanguages, localisedUrls } = useModernI18n();
13
+ const { language, supportedLanguages, urlStrategy } = useModernI18n();
14
14
  const config = {
15
15
  languages: supportedLanguages,
16
- localisedUrls
16
+ urlStrategy
17
17
  };
18
18
  const isExternal = EXTERNAL_TARGET_RE.test(to);
19
19
  const isBareHash = to.startsWith('#');
@@ -23,7 +23,7 @@ const Link = (props)=>{
23
23
  const interpolated = interpolateRouteParams(pathname || '/', params);
24
24
  const firstSegment = interpolated.split('/').filter(Boolean)[0];
25
25
  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.`);
26
- const localizedPathname = buildLocalizedUrl(interpolated, language, supportedLanguages, localisedUrls);
26
+ const localizedPathname = buildLocalizedUrl(interpolated, language, supportedLanguages, urlStrategy);
27
27
  const hash = hashProp ?? (hashFromTo ? hashFromTo.slice(1) : '');
28
28
  const { searchString, searchObject } = normalizeSearch(searchProp, searchFromTo);
29
29
  return {
@@ -43,7 +43,7 @@ const Link = (props)=>{
43
43
  isBareHash,
44
44
  language,
45
45
  supportedLanguages,
46
- localisedUrls
46
+ urlStrategy
47
47
  ]);
48
48
  const isActive = useMemo(()=>{
49
49
  if (!target || !adapter.location) return false;
@@ -58,7 +58,7 @@ const Link = (props)=>{
58
58
  adapter.location,
59
59
  activeOptions?.exact,
60
60
  supportedLanguages,
61
- localisedUrls
61
+ urlStrategy
62
62
  ]);
63
63
  const resolvedActiveProps = splitActiveProps(isActive, activeProps);
64
64
  const activeAttributes = isActive ? {
@@ -73,7 +73,7 @@ const Link = (props)=>{
73
73
  children: children
74
74
  });
75
75
  }
76
- const { Link: RouterLink, hasRouter, framework } = adapter;
76
+ const { Link: RouterLink, hasRouter } = adapter;
77
77
  if (!hasRouter || !RouterLink) {
78
78
  const { replace: _replace, ...anchorProps } = rest;
79
79
  const { className: activeClassName, style: activeStyle, ...activeRest } = resolvedActiveProps;
@@ -96,38 +96,25 @@ const Link = (props)=>{
96
96
  ...rest.style,
97
97
  ...activeStyle
98
98
  };
99
- if ('tanstack' === framework) {
100
- const tanstackPreload = void 0 !== preload ? preload : void 0 === prefetch ? void 0 : 'none' === prefetch ? false : prefetch;
101
- return /*#__PURE__*/ jsx(RouterLink, {
102
- to: target.localizedPathname,
103
- ...target.searchObject ? {
104
- search: target.searchObject
105
- } : {},
106
- ...target.hash ? {
107
- hash: target.hash
108
- } : {},
109
- ...void 0 === hashScrollIntoView ? {} : {
110
- hashScrollIntoView
111
- },
112
- ...void 0 === tanstackPreload ? {} : {
113
- preload: tanstackPreload
114
- },
115
- ...rest,
116
- ...activeRest,
117
- ...activeAttributes,
118
- className: mergedClassName,
119
- style: mergedStyle,
120
- children: children
121
- });
122
- }
123
- return /*#__PURE__*/ jsx(RouterLink, {
99
+ const routerProps = adapter.createLinkProps?.({
100
+ pathname: target.localizedPathname,
101
+ href: target.href,
102
+ search: target.searchObject,
103
+ hash: target.hash,
104
+ hashScrollIntoView,
105
+ prefetch,
106
+ preload
107
+ }) ?? {
124
108
  to: target.href,
125
109
  ...void 0 === prefetch ? {} : {
126
110
  prefetch
127
111
  },
128
112
  ...void 0 === preload ? {} : {
129
113
  preload
130
- },
114
+ }
115
+ };
116
+ return /*#__PURE__*/ jsx(RouterLink, {
117
+ ...routerProps,
131
118
  ...rest,
132
119
  ...activeRest,
133
120
  ...activeAttributes,
@@ -1,8 +1,8 @@
1
1
  import "node:module";
2
2
  import { jsx } from "react/jsx-runtime";
3
+ import { RuntimeComponentResolverContext } from "@modern-js/runtime/context";
3
4
  import { createContext, useCallback, useContext, useEffect, useMemo } from "react";
4
5
  import { changeModernI18nLanguage, getPathLanguage, isI18nLanguageSupported, isI18nResourcesReady, translateI18n } from "./contextHelpers.mjs";
5
- import { getActualI18nextInstance } from "./i18n/instance.mjs";
6
6
  import { useI18nRouterAdapter } from "./routerAdapter.mjs";
7
7
  const modernI18nContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ModernI18nContext');
8
8
  const reactI18nextProviderContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ReactI18nextProviderContext');
@@ -23,68 +23,23 @@ const ModernI18nProvider = ({ children, i18nextProvider, value })=>{
23
23
  children: content
24
24
  });
25
25
  };
26
- const FederatedI18nBoundary = ({ children, defaultNamespace, fallbackLanguage, resources, supportedLanguages })=>{
27
- const parent = useContext(ModernI18nContext);
28
- const I18nextProvider = useContext(ReactI18nextProviderContext);
29
- if (!parent) throw new Error('FederatedI18nBoundary must be used within ModernI18nProvider');
30
- const languages = supportedLanguages ?? parent.languages ?? Object.keys(resources);
31
- const scopedInstance = useMemo(()=>{
32
- const parentInstance = getActualI18nextInstance(parent.i18nInstance);
33
- const clone = parentInstance.cloneInstance?.({
34
- defaultNS: defaultNamespace,
35
- fallbackLng: fallbackLanguage ?? languages[0] ?? parent.language,
36
- forkResourceStore: true,
37
- initImmediate: false,
38
- lng: parent.language,
39
- ns: [
40
- defaultNamespace
41
- ],
42
- resources,
43
- supportedLngs: languages
44
- });
45
- if (!clone) throw new Error('FederatedI18nBoundary requires an i18n instance with cloneInstance support');
46
- const resourceStore = clone.store;
47
- const parentResourceStore = parentInstance.store;
48
- const sharesNestedResourceState = Object.entries(resourceStore?.data ?? {}).some(([language, namespaces])=>{
49
- const parentNamespaces = parentResourceStore?.data?.[language];
50
- return namespaces === parentNamespaces || Object.entries(namespaces).some(([namespace, resource])=>null !== resource && 'object' == typeof resource && resource === parentNamespaces?.[namespace]);
51
- });
52
- if (clone === parentInstance || resourceStore === parentResourceStore || resourceStore?.data === parentResourceStore?.data || sharesNestedResourceState) throw new Error('FederatedI18nBoundary cloneInstance did not isolate the host resource store');
53
- if (!resourceStore?.addResourceBundle || !clone.removeResourceBundle || !resourceStore.data) throw new Error('FederatedI18nBoundary requires an isolated mutable i18n resource store');
54
- for (const [language, namespaces] of Object.entries(resourceStore.data))for (const namespace of Object.keys(namespaces))clone.removeResourceBundle(language, namespace);
55
- for (const [language, namespaces] of Object.entries(resources))for (const [namespace, resource] of Object.entries(namespaces))resourceStore.addResourceBundle(language, namespace, resource, true, true);
56
- return clone;
57
- }, [
58
- defaultNamespace,
59
- fallbackLanguage,
60
- languages,
61
- parent.i18nInstance,
62
- parent.language,
63
- resources
64
- ]);
65
- const value = useMemo(()=>({
66
- ...parent,
67
- i18nInstance: scopedInstance,
68
- language: parent.language,
69
- languages
70
- }), [
71
- languages,
72
- parent,
73
- scopedInstance
74
- ]);
75
- const scopedContent = /*#__PURE__*/ jsx(ModernI18nProvider, {
76
- value: value,
77
- children: children
26
+ const MissingFederatedI18nBoundary = ()=>{
27
+ throw new Error('FederatedI18nBoundary requires the @modern-js/i18n-integration runtime plugin');
28
+ };
29
+ const FederatedI18nBoundary = (props)=>{
30
+ const resolveComponent = useContext(RuntimeComponentResolverContext);
31
+ const Boundary = resolveComponent?.(MissingFederatedI18nBoundary, {
32
+ name: 'i18n.FederatedI18nBoundary'
33
+ }) ?? MissingFederatedI18nBoundary;
34
+ if (Boundary === FederatedI18nBoundary) throw new Error('FederatedI18nBoundary resolver returned its own wrapper');
35
+ return /*#__PURE__*/ jsx(Boundary, {
36
+ ...props
78
37
  });
79
- return I18nextProvider ? /*#__PURE__*/ jsx(I18nextProvider, {
80
- i18n: scopedInstance,
81
- children: scopedContent
82
- }) : scopedContent;
83
38
  };
84
39
  const useModernI18n = ()=>{
85
40
  const context = useContext(ModernI18nContext);
86
41
  if (!context) throw new Error('useModernI18n must be used within ModernI18nProvider');
87
- const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, updateLanguage, synchronizeLanguage } = context;
42
+ const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, urlStrategy, updateLanguage, synchronizeLanguage } = context;
88
43
  const { navigate, location, hasRouter } = useI18nRouterAdapter();
89
44
  const pathLanguage = useMemo(()=>getPathLanguage(location?.pathname, languages, localePathRedirect), [
90
45
  languages,
@@ -103,7 +58,7 @@ const useModernI18n = ()=>{
103
58
  updateLanguage,
104
59
  localePathRedirect,
105
60
  ignoreRedirectRoutes,
106
- localisedUrls,
61
+ urlStrategy,
107
62
  languages,
108
63
  hasRouter,
109
64
  navigate,
@@ -113,7 +68,7 @@ const useModernI18n = ()=>{
113
68
  updateLanguage,
114
69
  localePathRedirect,
115
70
  ignoreRedirectRoutes,
116
- localisedUrls,
71
+ urlStrategy,
117
72
  languages,
118
73
  hasRouter,
119
74
  navigate,
@@ -136,9 +91,10 @@ const useModernI18n = ()=>{
136
91
  t,
137
92
  i18nInstance: i18nInstance,
138
93
  supportedLanguages: languages || [],
139
- localisedUrls,
94
+ urlStrategy,
140
95
  isLanguageSupported,
141
96
  isResourcesReady
142
97
  };
143
98
  };
144
- export { FederatedI18nBoundary, ModernI18nProvider, useModernI18n };
99
+ export { getActualI18nextInstance } from "./i18n/instance.mjs";
100
+ export { FederatedI18nBoundary, ModernI18nContext, ModernI18nProvider, ReactI18nextProviderContext, useModernI18n };
@@ -18,7 +18,7 @@ async function changeI18nInstanceLanguage(i18nInstance, language) {
18
18
  else await i18nInstance.changeLanguage?.(language);
19
19
  }
20
20
  async function changeModernI18nLanguage(newLang, options) {
21
- const { i18nInstance, updateLanguage, localePathRedirect, ignoreRedirectRoutes, localisedUrls, languages, hasRouter, navigate, location } = options;
21
+ const { i18nInstance, updateLanguage, localePathRedirect, ignoreRedirectRoutes, urlStrategy, languages, hasRouter, navigate, location } = options;
22
22
  try {
23
23
  if (!newLang || 'string' != typeof newLang) throw new Error('Language must be non-empty string');
24
24
  await changeI18nInstanceLanguage(i18nInstance, newLang);
@@ -29,8 +29,8 @@ async function changeModernI18nLanguage(newLang, options) {
29
29
  const relativePath = currentPath.replace(entryPath, '');
30
30
  const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
31
31
  if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
32
- if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
33
- const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
32
+ if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes, urlStrategy)) {
33
+ const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], urlStrategy);
34
34
  const newUrl = entryPath + newPath + location.search + location.hash;
35
35
  await navigate(newUrl, {
36
36
  replace: true
@@ -42,8 +42,8 @@ async function changeModernI18nLanguage(newLang, options) {
42
42
  const relativePath = currentPath.replace(entryPath, '');
43
43
  const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
44
44
  if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
45
- if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
46
- const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
45
+ if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes, urlStrategy)) {
46
+ const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], urlStrategy);
47
47
  const newUrl = entryPath + newPath + window.location.search + window.location.hash;
48
48
  window.history.pushState(null, '', newUrl);
49
49
  }
@@ -6,8 +6,8 @@ import "./types.mjs";
6
6
  const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
7
7
  name: '@modern-js/plugin-i18n',
8
8
  setup: (api)=>{
9
- const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true } = options;
10
- const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes, localisedUrls } = localeDetection || {};
9
+ const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true, urlStrategy, NavigationProvider, LanguageSynchronization } = options;
10
+ const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = localeDetection || {};
11
11
  const { enabled: backendEnabled = false } = backend || {};
12
12
  let latestI18nInstance;
13
13
  let I18nextProvider;
@@ -36,15 +36,18 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
36
36
  languages,
37
37
  fallbackLanguage,
38
38
  ignoreRedirectRoutes,
39
- localisedUrls,
39
+ urlStrategy,
40
+ NavigationProvider,
41
+ LanguageSynchronization,
40
42
  getLatestI18nInstance: ()=>latestI18nInstance,
41
43
  getI18nextProvider: ()=>I18nextProvider
42
44
  }));
43
45
  }
44
46
  });
47
+ export { I18nNavigationProvider, useI18nRouterAdapter, useNativeI18nRouterAdapter } from "./routerAdapter.mjs";
45
48
  export { FederatedI18nBoundary, useModernI18n } from "./context.mjs";
46
49
  export { I18nLink } from "./I18nLink.mjs";
47
50
  export { Link } from "./Link.mjs";
48
- export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
49
- export { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
51
+ export { useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
52
+ export { splitUrlTarget } from "./utils.mjs";
50
53
  export { createI18nPlugin };
@@ -19,7 +19,7 @@ function createMinimalI18nInstance(language) {
19
19
  };
20
20
  return minimalInstance;
21
21
  }
22
- function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang, synchronizeLanguage) {
22
+ function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang, urlStrategy) {
23
23
  const instance = i18nInstance || createMinimalI18nInstance(lang);
24
24
  return {
25
25
  language: lang,
@@ -28,9 +28,8 @@ function createContextValue(lang, i18nInstance, entryName, languages, localePath
28
28
  languages,
29
29
  localePathRedirect,
30
30
  ignoreRedirectRoutes,
31
- localisedUrls,
32
- updateLanguage: setLang,
33
- synchronizeLanguage
31
+ urlStrategy,
32
+ updateLanguage: setLang
34
33
  };
35
34
  }
36
35
  function useSdkResourcesLoader(i18nInstance, setForceUpdate) {
@@ -79,7 +78,7 @@ function useSdkResourcesLoader(i18nInstance, setForceUpdate) {
79
78
  setForceUpdate
80
79
  ]);
81
80
  }
82
- function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls) {
81
+ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy) {
83
82
  const urlRef = useRef('');
84
83
  const { navigate, location, hasRouter } = useI18nRouterAdapter();
85
84
  useEffect(()=>{
@@ -96,10 +95,10 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
96
95
  const currentHash = hasRouter && location ? location.hash : window.location.hash;
97
96
  const entryPath = getEntryPath();
98
97
  const relativePath = currentPathname.replace(entryPath, '');
99
- if (shouldIgnoreRedirect(relativePath, languages, ignoreRedirectRoutes)) return;
98
+ if (shouldIgnoreRedirect(relativePath, languages, ignoreRedirectRoutes, urlStrategy)) return;
100
99
  const pathDetection = detectLanguageFromPath(currentPathname, languages, localePathRedirect);
101
100
  const targetLanguage = pathDetection.language || i18nInstance.language || fallbackLanguage || languages[0] || 'en';
102
- const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages, localisedUrls);
101
+ const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages, urlStrategy);
103
102
  const newUrl = entryPath + newPath + currentSearch + currentHash;
104
103
  urlRef.current = currentPathname;
105
104
  if (newUrl !== currentPathname + currentSearch + currentHash) if (hasRouter && navigate && location) navigate(newUrl, {
@@ -115,7 +114,7 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
115
114
  languages,
116
115
  fallbackLanguage,
117
116
  ignoreRedirectRoutes,
118
- localisedUrls
117
+ urlStrategy
119
118
  ]);
120
119
  }
121
120
  function useLanguageSync(i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang) {
@@ -1,21 +1,23 @@
1
1
  import "node:module";
2
- import { canonicalTargetPathname } from "@modern-js/i18n-runtime-extensions";
3
2
  import { useMemo } from "react";
4
3
  import { useModernI18n } from "./context.mjs";
5
4
  import { useI18nRouterAdapter } from "./routerAdapter.mjs";
6
5
  import { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
7
- const localizePath = (pathname, language, config)=>buildLocalizedUrl(pathname, language, config.languages, config.localisedUrls);
6
+ const localizePath = (pathname, language, config)=>buildLocalizedUrl(pathname, language, config.languages, config.urlStrategy);
8
7
  const canonicalPath = (target, config)=>{
9
8
  const { pathname, search, hash } = splitUrlTarget(target);
10
- const resolvedPath = canonicalTargetPathname(pathname, config.languages, config.localisedUrls);
9
+ if (config.urlStrategy) return `${config.urlStrategy.canonicalPathname(pathname, config.languages)}${search}${hash}`;
10
+ const segments = pathname.split('/').filter(Boolean);
11
+ if (segments[0] && config.languages.some((language)=>language.toLowerCase() === segments[0].toLowerCase())) segments.shift();
12
+ const resolvedPath = `/${segments.join('/')}`;
11
13
  return `${resolvedPath}${search}${hash}`;
12
14
  };
13
15
  const useLocalizedPaths = ()=>{
14
- const { supportedLanguages, localisedUrls } = useModernI18n();
16
+ const { supportedLanguages, urlStrategy } = useModernI18n();
15
17
  return useMemo(()=>{
16
18
  const config = {
17
19
  languages: supportedLanguages,
18
- localisedUrls
20
+ urlStrategy
19
21
  };
20
22
  return {
21
23
  localizePath: (pathname, language)=>localizePath(pathname, language, config),
@@ -23,11 +25,11 @@ const useLocalizedPaths = ()=>{
23
25
  };
24
26
  }, [
25
27
  supportedLanguages,
26
- localisedUrls
28
+ urlStrategy
27
29
  ]);
28
30
  };
29
31
  const useLocalizedLocation = ()=>{
30
- const { language, supportedLanguages, localisedUrls } = useModernI18n();
32
+ const { language, supportedLanguages, urlStrategy } = useModernI18n();
31
33
  const { location } = useI18nRouterAdapter();
32
34
  const pathname = location?.pathname ?? '/';
33
35
  const search = location?.search ?? '';
@@ -35,7 +37,7 @@ const useLocalizedLocation = ()=>{
35
37
  return useMemo(()=>{
36
38
  const config = {
37
39
  languages: supportedLanguages,
38
- localisedUrls
40
+ urlStrategy
39
41
  };
40
42
  const alternates = {};
41
43
  for (const supportedLanguage of supportedLanguages)alternates[supportedLanguage] = `${localizePath(pathname, supportedLanguage, config)}${search}${hash}`;
@@ -47,7 +49,7 @@ const useLocalizedLocation = ()=>{
47
49
  }, [
48
50
  language,
49
51
  supportedLanguages,
50
- localisedUrls,
52
+ urlStrategy,
51
53
  pathname,
52
54
  search,
53
55
  hash
@@ -1,15 +1,20 @@
1
1
  import "node:module";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useLanguageSync } from "@modern-js/i18n-runtime-extensions";
4
3
  import { RuntimeContext } from "@modern-js/runtime";
5
4
  import { Helmet } from "@modern-js/runtime/head";
6
5
  import { useContext, useEffect, useMemo, useRef, useState } from "react";
7
6
  import { ModernI18nProvider } from "./context.mjs";
8
- import { createContextValue, useClientSideRedirect, useSdkResourcesLoader } from "./hooks.mjs";
7
+ import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.mjs";
9
8
  import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
10
9
  import { useI18nRouterAdapter } from "./routerAdapter.mjs";
11
- const createI18nRootWrapper = (options)=>(App)=>(props)=>{
12
- const { entryName, htmlLangAttr, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls, getLatestI18nInstance, getI18nextProvider } = options;
10
+ const NativeLanguageSynchronization = ({ i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang, children })=>{
11
+ const synchronizeLanguage = useLanguageSync(i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang);
12
+ return children(synchronizeLanguage);
13
+ };
14
+ const createI18nRootWrapper = (options)=>(App)=>{
15
+ const LanguageSynchronization = options.LanguageSynchronization ?? NativeLanguageSynchronization;
16
+ const I18nRoot = (props)=>{
17
+ const { entryName, htmlLangAttr, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy, getLatestI18nInstance, getI18nextProvider } = options;
13
18
  const runtimeContext = useContext(RuntimeContext);
14
19
  const i18nInstance = runtimeContext.i18nInstance || getLatestI18nInstance();
15
20
  const initialLang = useMemo(()=>i18nInstance?.language || fallbackLanguage, [
@@ -34,18 +39,16 @@ const createI18nRootWrapper = (options)=>(App)=>(props)=>{
34
39
  lang
35
40
  ]);
36
41
  useSdkResourcesLoader(i18nInstance, setForceUpdate);
37
- const synchronizeLanguage = useLanguageSync(i18nInstance, localePathRedirect, languages, location?.pathname, prevLangRef, setLang);
38
- useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls);
39
- const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang, synchronizeLanguage), [
42
+ useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy);
43
+ const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang, urlStrategy), [
40
44
  lang,
41
45
  i18nInstance,
42
46
  entryName,
43
47
  languages,
44
48
  localePathRedirect,
45
49
  ignoreRedirectRoutes,
46
- localisedUrls,
47
- forceUpdate,
48
- synchronizeLanguage
50
+ urlStrategy,
51
+ forceUpdate
49
52
  ]);
50
53
  const I18nextProvider = getI18nextProvider();
51
54
  const i18nextInstanceForProvider = useMemo(()=>i18nInstance ? getI18nextInstanceForProvider(i18nInstance, lang) : void 0, [
@@ -68,12 +71,33 @@ const createI18nRootWrapper = (options)=>(App)=>(props)=>{
68
71
  lang
69
72
  }
70
73
  }),
71
- /*#__PURE__*/ jsx(ModernI18nProvider, {
72
- i18nextProvider: I18nextProvider,
73
- value: contextValue,
74
- children: appContent
74
+ /*#__PURE__*/ jsx(LanguageSynchronization, {
75
+ i18nInstance: i18nInstance,
76
+ localePathRedirect: localePathRedirect,
77
+ languages: languages,
78
+ pathname: location?.pathname,
79
+ prevLangRef: prevLangRef,
80
+ setLang: setLang,
81
+ children: (synchronizeLanguage)=>/*#__PURE__*/ jsx(ModernI18nProvider, {
82
+ i18nextProvider: I18nextProvider,
83
+ value: {
84
+ ...contextValue,
85
+ synchronizeLanguage
86
+ },
87
+ children: appContent
88
+ })
75
89
  })
76
90
  ]
77
91
  });
78
92
  };
93
+ const { NavigationProvider } = options;
94
+ if (!NavigationProvider) return I18nRoot;
95
+ return function(props) {
96
+ return /*#__PURE__*/ jsx(NavigationProvider, {
97
+ children: /*#__PURE__*/ jsx(I18nRoot, {
98
+ ...props
99
+ })
100
+ });
101
+ };
102
+ };
79
103
  export { createI18nRootWrapper };