@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21

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 (127) hide show
  1. package/dist/cjs/cli/index.js +10 -21
  2. package/dist/cjs/cli/localisedRoutes.js +47 -0
  3. package/dist/cjs/runtime/Link.js +19 -32
  4. package/dist/cjs/runtime/context.js +30 -73
  5. package/dist/cjs/runtime/contextHelpers.js +5 -5
  6. package/dist/cjs/runtime/contexts.js +49 -0
  7. package/dist/cjs/runtime/core.js +27 -14
  8. package/dist/cjs/runtime/hooks.js +7 -8
  9. package/dist/cjs/runtime/localizedPaths.js +11 -9
  10. package/dist/cjs/runtime/navigation.js +48 -0
  11. package/dist/cjs/runtime/providerComposition.js +37 -13
  12. package/dist/cjs/runtime/routerAdapter.js +33 -140
  13. package/dist/cjs/runtime/sharedContexts.js +43 -0
  14. package/dist/cjs/runtime/utils.js +21 -19
  15. package/dist/cjs/server/defaultUrlStrategy.js +48 -0
  16. package/dist/cjs/server/index.js +11 -7
  17. package/dist/cjs/server/mappedUrlStrategy.js +43 -0
  18. package/dist/cjs/server/redirectPolicy.js +33 -24
  19. package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
  20. package/dist/cjs/shared/urlStrategy.js +48 -0
  21. package/dist/esm/cli/index.mjs +10 -21
  22. package/dist/esm/cli/localisedRoutes.mjs +9 -0
  23. package/dist/esm/runtime/Link.mjs +19 -32
  24. package/dist/esm/runtime/context.mjs +20 -72
  25. package/dist/esm/runtime/contextHelpers.mjs +5 -5
  26. package/dist/esm/runtime/contexts.mjs +5 -0
  27. package/dist/esm/runtime/core.mjs +13 -6
  28. package/dist/esm/runtime/hooks.mjs +7 -8
  29. package/dist/esm/runtime/localizedPaths.mjs +11 -9
  30. package/dist/esm/runtime/navigation.mjs +7 -0
  31. package/dist/esm/runtime/providerComposition.mjs +38 -14
  32. package/dist/esm/runtime/routerAdapter.mjs +28 -141
  33. package/dist/esm/runtime/sharedContexts.mjs +1 -0
  34. package/dist/esm/runtime/utils.mjs +19 -17
  35. package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
  36. package/dist/esm/server/index.mjs +11 -7
  37. package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
  38. package/dist/esm/server/redirectPolicy.mjs +33 -24
  39. package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
  40. package/dist/esm/shared/urlStrategy.mjs +7 -0
  41. package/dist/esm-node/cli/index.mjs +10 -21
  42. package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
  43. package/dist/esm-node/runtime/Link.mjs +19 -32
  44. package/dist/esm-node/runtime/context.mjs +20 -72
  45. package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
  46. package/dist/esm-node/runtime/contexts.mjs +6 -0
  47. package/dist/esm-node/runtime/core.mjs +13 -6
  48. package/dist/esm-node/runtime/hooks.mjs +7 -8
  49. package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
  50. package/dist/esm-node/runtime/navigation.mjs +8 -0
  51. package/dist/esm-node/runtime/providerComposition.mjs +38 -14
  52. package/dist/esm-node/runtime/routerAdapter.mjs +28 -141
  53. package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
  54. package/dist/esm-node/runtime/utils.mjs +19 -17
  55. package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
  56. package/dist/esm-node/server/index.mjs +11 -7
  57. package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
  58. package/dist/esm-node/server/redirectPolicy.mjs +33 -24
  59. package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
  60. package/dist/esm-node/shared/urlStrategy.mjs +8 -0
  61. package/dist/types/cli/localisedRoutes.d.ts +19 -0
  62. package/dist/types/runtime/context.d.ts +8 -11
  63. package/dist/types/runtime/contextHelpers.d.ts +2 -2
  64. package/dist/types/runtime/contexts.d.ts +6 -0
  65. package/dist/types/runtime/core.d.ts +12 -2
  66. package/dist/types/runtime/hooks.d.ts +4 -5
  67. package/dist/types/runtime/localizedPaths.d.ts +4 -4
  68. package/dist/types/runtime/navigation.d.ts +10 -0
  69. package/dist/types/runtime/providerComposition.d.ts +13 -1
  70. package/dist/types/runtime/routerAdapter.d.ts +23 -6
  71. package/dist/types/runtime/sharedContexts.d.ts +1 -0
  72. package/dist/types/runtime/utils.d.ts +4 -12
  73. package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
  74. package/dist/types/server/index.d.ts +2 -0
  75. package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
  76. package/dist/types/server/redirectPolicy.d.ts +5 -13
  77. package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
  78. package/dist/types/shared/type.d.ts +7 -11
  79. package/dist/types/shared/urlStrategy.d.ts +18 -0
  80. package/package.json +42 -19
  81. package/rslib.config.mts +8 -1
  82. package/rstest.config.mts +6 -5
  83. package/src/cli/index.ts +42 -42
  84. package/src/cli/localisedRoutes.ts +53 -0
  85. package/src/runtime/Link.tsx +20 -40
  86. package/src/runtime/context.tsx +33 -157
  87. package/src/runtime/contextHelpers.ts +7 -5
  88. package/src/runtime/contexts.ts +16 -0
  89. package/src/runtime/core.tsx +53 -6
  90. package/src/runtime/hooks.ts +14 -9
  91. package/src/runtime/i18n/instance.ts +1 -2
  92. package/src/runtime/localizedPaths.ts +24 -19
  93. package/src/runtime/navigation.tsx +20 -0
  94. package/src/runtime/providerComposition.tsx +71 -23
  95. package/src/runtime/routerAdapter.tsx +65 -339
  96. package/src/runtime/sharedContexts.ts +8 -0
  97. package/src/runtime/utils.ts +49 -31
  98. package/src/server/defaultUrlStrategy.ts +27 -0
  99. package/src/server/index.ts +39 -9
  100. package/src/server/mappedUrlStrategy.ts +10 -0
  101. package/src/server/redirectPolicy.ts +85 -70
  102. package/src/shared/mappedUrlStrategy.ts +59 -0
  103. package/src/shared/type.ts +7 -11
  104. package/src/shared/urlStrategy.ts +38 -0
  105. package/tests/backendDefaults.test.ts +2 -23
  106. package/tests/defaultUrlStrategy.test.ts +107 -0
  107. package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
  108. package/tests/i18nUtils.test.ts +31 -65
  109. package/tests/link.test.tsx +13 -511
  110. package/tests/localisedRouteGeneration.test.ts +200 -0
  111. package/tests/localisedUrls.test.ts +16 -208
  112. package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
  113. package/tests/navigation.test.tsx +403 -0
  114. package/tests/reactI18nextRuntimeBoundary.test.ts +8 -83
  115. package/tests/redirectPolicy.test.ts +89 -27
  116. package/tests/routerAdapter.test.tsx +98 -1171
  117. package/tests/urlStrategySerialization.test.ts +122 -0
  118. package/tsconfig.json +4 -2
  119. package/tests/federatedI18nBoundary.client.test.tsx +0 -129
  120. package/tests/federatedI18nBoundary.test.tsx +0 -132
  121. package/tests/linkTypes.test.ts +0 -59
  122. package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
  123. package/tests/peerCohort.test.ts +0 -25
  124. package/tests/type-fixture/fixture-globals.d.ts +0 -11
  125. package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
  126. package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
  127. package/tests/type-fixture/tsconfig.json +0 -16
@@ -1,8 +1,10 @@
1
1
  import "node:module";
2
- import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "@modern-js/i18n-runtime-extensions";
3
2
  import { getPublicDirRoutePrefixes } from "@modern-js/server-core";
3
+ import { logger } from "@modern-js/utils";
4
+ import { isUnusableI18nUrlStrategy } from "../shared/urlStrategy.mjs";
4
5
  import { getBackendOptions, getLocaleDetectionOptions } from "../shared/utils.mjs";
5
6
  import { applyDetectedBackendPaths, detectLocalesDirectory } from "./locales.mjs";
7
+ import { applyLocalisedRoutes } from "./localisedRoutes.mjs";
6
8
  import "../runtime/types.mjs";
7
9
  const i18nPlugin = (options = {})=>({
8
10
  name: '@modern-js/plugin-i18n',
@@ -32,6 +34,10 @@ const i18nPlugin = (options = {})=>({
32
34
  backend: backendOptions,
33
35
  ...extendedConfig
34
36
  };
37
+ if (isUnusableI18nUrlStrategy(config.urlStrategy)) {
38
+ delete config.urlStrategy;
39
+ logger.warn("[i18n] `urlStrategy` cannot be passed through `modern.config.ts`: the generated runtime registration carries plugin options as JSON, which drops its methods. Configure `localeDetection.localisedUrls` instead — the runtime and the server both derive the URL policy from it — or register a runtime plugin that supplies the strategy directly.");
40
+ }
35
41
  const { reactI18next } = extendedConfig;
36
42
  const runtimePluginPath = customPlugin?.runtime?.path || (false === reactI18next ? `@${metaName}/plugin-i18n/runtime/no-react-i18next` : `@${metaName}/plugin-i18n/runtime`);
37
43
  plugins.push({
@@ -44,27 +50,10 @@ const i18nPlugin = (options = {})=>({
44
50
  plugins
45
51
  };
46
52
  });
47
- api.modifyFileSystemRoutes(({ entrypoint, routes })=>{
48
- if (!localeDetection) return {
53
+ api.modifyFileSystemRoutes(({ entrypoint, routes })=>({
49
54
  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
- });
55
+ routes: applyLocalisedRoutes(routes, localeDetection ? getLocaleDetectionOptions(entrypoint.entryName, localeDetection) : void 0, entrypoint)
56
+ }));
68
57
  api._internalServerPlugins(({ plugins })=>{
69
58
  const { serverRoutes, metaName } = api.getAppContext();
70
59
  const normalizedConfig = api.getNormalizedConfig();
@@ -0,0 +1,10 @@
1
+ import "node:module";
2
+ import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "@modern-js/i18n-runtime-extensions";
3
+ import { getEntrypointRoutesOwner } from "@modern-js/runtime/cli";
4
+ const applyLocalisedRoutes = (routes, localeDetection, entrypoint)=>{
5
+ const { localePathRedirect, languages = [], localisedUrls } = localeDetection ?? {};
6
+ const resolved = resolveLocalisedUrlsConfig(localisedUrls);
7
+ if (!localePathRedirect || !languages.length || !resolved.enabled) return routes;
8
+ return applyLocalisedUrlsToRoutes(routes, languages, resolved.map, entrypoint && '@modern-js/plugin-tanstack' === getEntrypointRoutesOwner(entrypoint) ? 'canonical' : 'physical');
9
+ };
10
+ export { applyLocalisedRoutes };
@@ -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,18 +1,10 @@
1
1
  import "node:module";
2
2
  import { jsx } from "react/jsx-runtime";
3
- import { createContext, useCallback, useContext, useEffect, useMemo } from "react";
3
+ import { RuntimeComponentResolverContext } from "@modern-js/runtime/context";
4
+ import { 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
- const modernI18nContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ModernI18nContext');
8
- const reactI18nextProviderContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ReactI18nextProviderContext');
9
- const getGlobalContext = (key, defaultValue)=>{
10
- const globalStore = globalThis;
11
- globalStore[key] ??= /*#__PURE__*/ createContext(defaultValue);
12
- return globalStore[key];
13
- };
14
- const ModernI18nContext = getGlobalContext(modernI18nContextKey, null);
15
- const ReactI18nextProviderContext = getGlobalContext(reactI18nextProviderContextKey, null);
7
+ import { ModernI18nContext, ReactI18nextProviderContext } from "./sharedContexts.mjs";
16
8
  const ModernI18nProvider = ({ children, i18nextProvider, value })=>{
17
9
  const content = /*#__PURE__*/ jsx(ModernI18nContext.Provider, {
18
10
  value: value,
@@ -23,68 +15,23 @@ const ModernI18nProvider = ({ children, i18nextProvider, value })=>{
23
15
  children: content
24
16
  });
25
17
  };
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
18
+ const MissingFederatedI18nBoundary = ()=>{
19
+ throw new Error('FederatedI18nBoundary requires the @modern-js/i18n-integration runtime plugin');
20
+ };
21
+ const FederatedI18nBoundary = (props)=>{
22
+ const resolveComponent = useContext(RuntimeComponentResolverContext);
23
+ const Boundary = resolveComponent?.(MissingFederatedI18nBoundary, {
24
+ name: 'i18n.FederatedI18nBoundary'
25
+ }) ?? MissingFederatedI18nBoundary;
26
+ if (Boundary === FederatedI18nBoundary) throw new Error('FederatedI18nBoundary resolver returned its own wrapper');
27
+ return /*#__PURE__*/ jsx(Boundary, {
28
+ ...props
78
29
  });
79
- return I18nextProvider ? /*#__PURE__*/ jsx(I18nextProvider, {
80
- i18n: scopedInstance,
81
- children: scopedContent
82
- }) : scopedContent;
83
30
  };
84
31
  const useModernI18n = ()=>{
85
32
  const context = useContext(ModernI18nContext);
86
33
  if (!context) throw new Error('useModernI18n must be used within ModernI18nProvider');
87
- const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, updateLanguage, synchronizeLanguage } = context;
34
+ const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, urlStrategy, updateLanguage, synchronizeLanguage } = context;
88
35
  const { navigate, location, hasRouter } = useI18nRouterAdapter();
89
36
  const pathLanguage = useMemo(()=>getPathLanguage(location?.pathname, languages, localePathRedirect), [
90
37
  languages,
@@ -103,7 +50,7 @@ const useModernI18n = ()=>{
103
50
  updateLanguage,
104
51
  localePathRedirect,
105
52
  ignoreRedirectRoutes,
106
- localisedUrls,
53
+ urlStrategy,
107
54
  languages,
108
55
  hasRouter,
109
56
  navigate,
@@ -113,7 +60,7 @@ const useModernI18n = ()=>{
113
60
  updateLanguage,
114
61
  localePathRedirect,
115
62
  ignoreRedirectRoutes,
116
- localisedUrls,
63
+ urlStrategy,
117
64
  languages,
118
65
  hasRouter,
119
66
  navigate,
@@ -136,9 +83,10 @@ const useModernI18n = ()=>{
136
83
  t,
137
84
  i18nInstance: i18nInstance,
138
85
  supportedLanguages: languages || [],
139
- localisedUrls,
86
+ urlStrategy,
140
87
  isLanguageSupported,
141
88
  isResourcesReady
142
89
  };
143
90
  };
144
- export { FederatedI18nBoundary, ModernI18nProvider, useModernI18n };
91
+ export { getActualI18nextInstance } from "./i18n/instance.mjs";
92
+ 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
  }
@@ -0,0 +1,6 @@
1
+ import "node:module";
2
+ import { createContext } from "react";
3
+ const ModernI18nContext = createContext(null);
4
+ const ReactI18nextProviderContext = createContext(null);
5
+ const I18nNavigationContext = createContext(null);
6
+ export { I18nNavigationContext, ModernI18nContext, ReactI18nextProviderContext };
@@ -1,4 +1,7 @@
1
1
  import "node:module";
2
+ import { resolveMappedUrlStrategy } from "../shared/mappedUrlStrategy.mjs";
3
+ import { asI18nUrlStrategy } from "../shared/urlStrategy.mjs";
4
+ import { I18nRouterNavigationProvider, useIntegratedRouterAdapter } from "./navigation.mjs";
2
5
  import { setupI18nBeforeRender } from "./pluginSetup.mjs";
3
6
  import { createI18nRootWrapper } from "./providerComposition.mjs";
4
7
  import { resolveReactI18nextIntegration } from "./reactI18next.mjs";
@@ -6,9 +9,10 @@ import "./types.mjs";
6
9
  const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
7
10
  name: '@modern-js/plugin-i18n',
8
11
  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 || {};
12
+ const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true, urlStrategy, NavigationProvider, LanguageSynchronization } = options;
13
+ const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = localeDetection || {};
11
14
  const { enabled: backendEnabled = false } = backend || {};
15
+ const resolvedUrlStrategy = asI18nUrlStrategy(urlStrategy) ?? resolveMappedUrlStrategy(localeDetection);
12
16
  let latestI18nInstance;
13
17
  let I18nextProvider;
14
18
  api.onBeforeRender(async (context)=>{
@@ -36,15 +40,18 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
36
40
  languages,
37
41
  fallbackLanguage,
38
42
  ignoreRedirectRoutes,
39
- localisedUrls,
43
+ urlStrategy: resolvedUrlStrategy,
44
+ NavigationProvider: NavigationProvider ?? I18nRouterNavigationProvider,
45
+ LanguageSynchronization,
40
46
  getLatestI18nInstance: ()=>latestI18nInstance,
41
47
  getI18nextProvider: ()=>I18nextProvider
42
48
  }));
43
49
  }
44
50
  });
51
+ export { I18nNavigationProvider, useI18nRouterAdapter, useNativeI18nRouterAdapter } from "./routerAdapter.mjs";
45
52
  export { FederatedI18nBoundary, useModernI18n } from "./context.mjs";
46
53
  export { I18nLink } from "./I18nLink.mjs";
47
54
  export { Link } from "./Link.mjs";
48
- export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
49
- export { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
50
- export { createI18nPlugin };
55
+ export { useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
56
+ export { splitUrlTarget } from "./utils.mjs";
57
+ export { I18nRouterNavigationProvider, createI18nPlugin, useIntegratedRouterAdapter };
@@ -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
@@ -0,0 +1,8 @@
1
+ import "node:module";
2
+ import { createI18nRouterNavigation } from "@modern-js/i18n-runtime-extensions/router-navigation";
3
+ import { I18nNavigationProvider, useNativeI18nRouterAdapter } from "./routerAdapter.mjs";
4
+ const { I18nRouterNavigationProvider: I18nRouterNavigationProvider, useIntegratedRouterAdapter: useIntegratedRouterAdapter } = createI18nRouterNavigation({
5
+ I18nNavigationProvider: I18nNavigationProvider,
6
+ useNativeI18nRouterAdapter: useNativeI18nRouterAdapter
7
+ });
8
+ export { I18nRouterNavigationProvider, useIntegratedRouterAdapter };
@@ -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 };