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

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 (143) hide show
  1. package/dist/cjs/cli/index.js +10 -21
  2. package/dist/cjs/cli/localisedRoutes.js +46 -0
  3. package/dist/cjs/runtime/Link.js +19 -32
  4. package/dist/cjs/runtime/context.js +26 -61
  5. package/dist/cjs/runtime/contextHelpers.js +5 -5
  6. package/dist/cjs/runtime/core.js +27 -14
  7. package/dist/cjs/runtime/hooks.js +7 -8
  8. package/dist/cjs/runtime/localizedPaths.js +11 -9
  9. package/dist/cjs/runtime/navigation.js +48 -0
  10. package/dist/cjs/runtime/providerComposition.js +37 -13
  11. package/dist/cjs/runtime/routerAdapter.js +35 -140
  12. package/dist/cjs/runtime/utils.js +21 -19
  13. package/dist/cjs/server/defaultUrlStrategy.js +48 -0
  14. package/dist/cjs/server/index.js +11 -7
  15. package/dist/cjs/server/mappedUrlStrategy.js +43 -0
  16. package/dist/cjs/server/redirectPolicy.js +33 -24
  17. package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
  18. package/dist/cjs/shared/urlStrategy.js +48 -0
  19. package/dist/esm/cli/index.mjs +10 -21
  20. package/dist/esm/cli/localisedRoutes.mjs +8 -0
  21. package/dist/esm/runtime/Link.mjs +19 -32
  22. package/dist/esm/runtime/context.mjs +18 -62
  23. package/dist/esm/runtime/contextHelpers.mjs +5 -5
  24. package/dist/esm/runtime/core.mjs +13 -6
  25. package/dist/esm/runtime/hooks.mjs +7 -8
  26. package/dist/esm/runtime/localizedPaths.mjs +11 -9
  27. package/dist/esm/runtime/navigation.mjs +7 -0
  28. package/dist/esm/runtime/providerComposition.mjs +38 -14
  29. package/dist/esm/runtime/routerAdapter.mjs +30 -141
  30. package/dist/esm/runtime/utils.mjs +19 -17
  31. package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
  32. package/dist/esm/server/index.mjs +11 -7
  33. package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
  34. package/dist/esm/server/redirectPolicy.mjs +33 -24
  35. package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
  36. package/dist/esm/shared/urlStrategy.mjs +7 -0
  37. package/dist/esm-node/cli/index.mjs +10 -21
  38. package/dist/esm-node/cli/localisedRoutes.mjs +9 -0
  39. package/dist/esm-node/runtime/Link.mjs +19 -32
  40. package/dist/esm-node/runtime/context.mjs +18 -62
  41. package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
  42. package/dist/esm-node/runtime/core.mjs +13 -6
  43. package/dist/esm-node/runtime/hooks.mjs +7 -8
  44. package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
  45. package/dist/esm-node/runtime/navigation.mjs +8 -0
  46. package/dist/esm-node/runtime/providerComposition.mjs +38 -14
  47. package/dist/esm-node/runtime/routerAdapter.mjs +30 -141
  48. package/dist/esm-node/runtime/utils.mjs +19 -17
  49. package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
  50. package/dist/esm-node/server/index.mjs +11 -7
  51. package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
  52. package/dist/esm-node/server/redirectPolicy.mjs +33 -24
  53. package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
  54. package/dist/esm-node/shared/urlStrategy.mjs +8 -0
  55. package/dist/types/cli/index.d.ts +2 -2
  56. package/dist/types/cli/locales.d.ts +1 -1
  57. package/dist/types/cli/localisedRoutes.d.ts +18 -0
  58. package/dist/types/runtime/Link.d.ts +2 -2
  59. package/dist/types/runtime/context.d.ts +10 -13
  60. package/dist/types/runtime/contextHelpers.d.ts +3 -3
  61. package/dist/types/runtime/core.d.ts +23 -13
  62. package/dist/types/runtime/hooks.d.ts +5 -6
  63. package/dist/types/runtime/i18n/backend/config.d.ts +1 -1
  64. package/dist/types/runtime/i18n/backend/index.d.ts +2 -2
  65. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -2
  66. package/dist/types/runtime/i18n/backend/middleware.d.ts +2 -2
  67. package/dist/types/runtime/i18n/backend/middleware.node.d.ts +2 -2
  68. package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +1 -1
  69. package/dist/types/runtime/i18n/detection/cache.d.ts +1 -1
  70. package/dist/types/runtime/i18n/detection/config.d.ts +1 -1
  71. package/dist/types/runtime/i18n/detection/detector.d.ts +2 -2
  72. package/dist/types/runtime/i18n/detection/index.d.ts +4 -4
  73. package/dist/types/runtime/i18n/detection/initOptions.d.ts +1 -1
  74. package/dist/types/runtime/i18n/detection/middleware.d.ts +1 -1
  75. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +1 -1
  76. package/dist/types/runtime/i18n/detection/priority.d.ts +2 -2
  77. package/dist/types/runtime/i18n/detection/types.d.ts +1 -1
  78. package/dist/types/runtime/i18n/index.d.ts +3 -3
  79. package/dist/types/runtime/i18n/instance.d.ts +1 -1
  80. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -1
  81. package/dist/types/runtime/i18n/utils.d.ts +2 -2
  82. package/dist/types/runtime/index.d.ts +2 -2
  83. package/dist/types/runtime/localizedPaths.d.ts +4 -4
  84. package/dist/types/runtime/navigation.d.ts +10 -0
  85. package/dist/types/runtime/no-react-i18next.d.ts +2 -2
  86. package/dist/types/runtime/pluginSetup.d.ts +3 -3
  87. package/dist/types/runtime/providerComposition.d.ts +15 -3
  88. package/dist/types/runtime/routerAdapter.d.ts +23 -6
  89. package/dist/types/runtime/types.d.ts +1 -1
  90. package/dist/types/runtime/utils.d.ts +4 -12
  91. package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
  92. package/dist/types/server/detectorOptions.d.ts +1 -1
  93. package/dist/types/server/index.d.ts +3 -1
  94. package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
  95. package/dist/types/server/redirectPolicy.d.ts +5 -13
  96. package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
  97. package/dist/types/shared/type.d.ts +8 -12
  98. package/dist/types/shared/urlStrategy.d.ts +18 -0
  99. package/dist/types/shared/utils.d.ts +1 -1
  100. package/package.json +30 -18
  101. package/rstest.config.mts +6 -5
  102. package/src/cli/index.ts +42 -43
  103. package/src/cli/localisedRoutes.ts +43 -0
  104. package/src/runtime/Link.tsx +20 -40
  105. package/src/runtime/context.tsx +34 -133
  106. package/src/runtime/contextHelpers.ts +7 -5
  107. package/src/runtime/core.tsx +53 -6
  108. package/src/runtime/hooks.ts +14 -9
  109. package/src/runtime/i18n/instance.ts +1 -2
  110. package/src/runtime/localizedPaths.ts +24 -19
  111. package/src/runtime/navigation.tsx +20 -0
  112. package/src/runtime/providerComposition.tsx +71 -23
  113. package/src/runtime/routerAdapter.tsx +73 -338
  114. package/src/runtime/utils.ts +49 -31
  115. package/src/server/defaultUrlStrategy.ts +27 -0
  116. package/src/server/index.ts +39 -9
  117. package/src/server/mappedUrlStrategy.ts +10 -0
  118. package/src/server/redirectPolicy.ts +85 -70
  119. package/src/shared/mappedUrlStrategy.ts +59 -0
  120. package/src/shared/type.ts +7 -11
  121. package/src/shared/urlStrategy.ts +38 -0
  122. package/tests/backendDefaults.test.ts +2 -23
  123. package/tests/defaultUrlStrategy.test.ts +107 -0
  124. package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
  125. package/tests/i18nUtils.test.ts +31 -65
  126. package/tests/link.test.tsx +13 -511
  127. package/tests/localisedRouteGeneration.test.ts +159 -0
  128. package/tests/localisedUrls.test.ts +16 -208
  129. package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
  130. package/tests/navigation.test.tsx +365 -0
  131. package/tests/reactI18nextRuntimeBoundary.test.ts +1 -82
  132. package/tests/redirectPolicy.test.ts +89 -27
  133. package/tests/routerAdapter.test.tsx +98 -1171
  134. package/tests/urlStrategySerialization.test.ts +122 -0
  135. package/tests/federatedI18nBoundary.client.test.tsx +0 -129
  136. package/tests/federatedI18nBoundary.test.tsx +0 -132
  137. package/tests/linkTypes.test.ts +0 -59
  138. package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
  139. package/tests/peerCohort.test.ts +0 -25
  140. package/tests/type-fixture/fixture-globals.d.ts +0 -11
  141. package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
  142. package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
  143. package/tests/type-fixture/tsconfig.json +0 -16
@@ -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
  }
@@ -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 };
@@ -1,151 +1,40 @@
1
1
  import "node:module";
2
+ import { jsx } from "react/jsx-runtime";
2
3
  import { RuntimeContext, isBrowser } from "@modern-js/runtime";
3
- import { InternalRuntimeContext, getRouterRuntimeState } from "@modern-js/runtime/context";
4
- import { useCallback, useContext, useSyncExternalStore } from "react";
5
- const normalizeUrlPart = (value, prefix)=>{
6
- if ('string' != typeof value || !value) return '';
7
- return value.startsWith(prefix) ? value : `${prefix}${value}`;
8
- };
9
- const normalizeLocation = (location)=>{
10
- if (!location || 'object' != typeof location) return null;
11
- const locationValue = location;
12
- if ('string' != typeof locationValue.pathname) return null;
13
- return {
14
- pathname: locationValue.pathname,
15
- search: normalizeUrlPart('string' == typeof locationValue.search ? locationValue.search : locationValue.searchStr, '?'),
16
- hash: normalizeUrlPart(locationValue.hash, '#')
17
- };
18
- };
19
- const getWindowLocation = ()=>{
20
- if (!isBrowser()) return null;
21
- return {
22
- pathname: window.location.pathname,
23
- search: window.location.search,
24
- hash: window.location.hash
25
- };
26
- };
27
- const getRouterFramework = (runtimeContext, internalContext, inReactRouter)=>{
28
- const framework = getRouterRuntimeState(internalContext)?.framework || getRouterRuntimeState(runtimeContext)?.framework;
29
- if (framework) return framework;
30
- if (internalContext.router?.useRouter || runtimeContext.router?.useRouter) return 'tanstack';
31
- if (internalContext.router?.useLocation || internalContext.router?.useHref || runtimeContext.router?.useLocation || runtimeContext.router?.useHref) return 'react-router';
32
- if (inReactRouter) return 'react-router';
33
- };
34
- const getRouterInstance = (internalContext, contextRouter)=>{
35
- if (contextRouter) return contextRouter;
36
- const router = getRouterRuntimeState(internalContext)?.instance;
37
- if (!router || 'object' != typeof router) return null;
38
- return router;
39
- };
40
- const getRouterStateLocation = (internalContext, contextRouter)=>{
41
- const router = getRouterInstance(internalContext, contextRouter);
42
- return normalizeLocation(router?.stores?.location?.get?.()) || normalizeLocation(router?.state?.location);
43
- };
44
- const getRouterParams = (internalContext, contextRouter)=>{
45
- const router = getRouterInstance(internalContext, contextRouter);
46
- const matches = router?.stores?.matches?.get?.() || router?.state?.matches;
47
- if (!Array.isArray(matches)) return {};
48
- return matches.reduce((params, match)=>{
49
- if (match?.params) Object.assign(params, match.params);
50
- return params;
51
- }, {});
52
- };
53
- const getRouterSnapshot = (internalContext, contextRouter)=>{
54
- const location = getRouterStateLocation(internalContext, contextRouter);
55
- const params = getRouterParams(internalContext, contextRouter);
56
- return JSON.stringify([
57
- location?.pathname ?? '',
58
- location?.search ?? '',
59
- location?.hash ?? '',
60
- Object.entries(params).sort(([left], [right])=>left.localeCompare(right))
61
- ]);
62
- };
63
- const useI18nRouterAdapter = ()=>{
4
+ import { InternalRuntimeContext } from "@modern-js/runtime/context";
5
+ import { createContext, useContext } from "react";
6
+ const navigationContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/I18nNavigationContext');
7
+ const contextStore = globalThis;
8
+ const I18nNavigationContext = contextStore[navigationContextKey] ??= /*#__PURE__*/ createContext(null);
9
+ const I18nNavigationProvider = ({ children, value })=>/*#__PURE__*/ jsx(I18nNavigationContext.Provider, {
10
+ value: value,
11
+ children: children
12
+ });
13
+ const useNativeI18nRouterAdapter = ()=>{
64
14
  const runtimeContext = useContext(RuntimeContext);
65
15
  const internalContext = useContext(InternalRuntimeContext);
66
16
  const routerApi = internalContext.router || runtimeContext.router;
67
- const inReactRouter = routerApi?.useInRouterContext?.() ?? false;
68
- const reactRouterNavigate = inReactRouter ? routerApi?.useNavigate?.() ?? null : null;
69
- const reactRouterLocation = inReactRouter ? routerApi?.useLocation?.() ?? null : null;
70
- const reactRouterParams = inReactRouter ? routerApi?.useParams?.() ?? {} : {};
71
- const framework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
72
- const contextUseRouter = inReactRouter || 'tanstack' !== framework ? void 0 : internalContext.router?.useRouter || runtimeContext.router?.useRouter;
73
- const contextRouter = contextUseRouter ? contextUseRouter({
74
- warn: false
75
- }) : null;
76
- const hasRouter = 'tanstack' === framework || 'react-router' === framework || Boolean(reactRouterNavigate);
77
- const subscribeToRouter = useCallback((update)=>{
78
- const router = getRouterInstance(internalContext, contextRouter);
79
- if (!router) return ()=>void 0;
80
- const unsubscribers = [];
81
- if ('react-router' === framework && !inReactRouter && 'function' == typeof router.subscribe) {
82
- const subscribe = router.subscribe;
83
- const unsubscribe = subscribe.call(router, update);
84
- if ('function' == typeof unsubscribe) unsubscribers.push(unsubscribe);
85
- }
86
- if ('tanstack' === framework && 'function' == typeof router.stores?.location?.subscribe) {
87
- const unsubscribe = router.stores.location.subscribe(update);
88
- if ('function' == typeof unsubscribe) unsubscribers.push(unsubscribe);
89
- }
90
- if ('tanstack' === framework && 'function' == typeof router.subscribe) {
91
- const subscribe = router.subscribe;
92
- for (const eventType of [
93
- 'onBeforeNavigate',
94
- 'onBeforeLoad'
95
- ]){
96
- const unsubscribe = subscribe.call(router, eventType, update);
97
- if ('function' == typeof unsubscribe) unsubscribers.push(unsubscribe);
98
- }
99
- }
100
- return ()=>{
101
- for (const unsubscribe of unsubscribers)unsubscribe();
102
- };
103
- }, [
104
- contextRouter,
105
- framework,
106
- inReactRouter,
107
- internalContext
108
- ]);
109
- const getSnapshot = useCallback(()=>getRouterSnapshot(internalContext, contextRouter), [
110
- contextRouter,
111
- internalContext
112
- ]);
113
- useSyncExternalStore(subscribeToRouter, getSnapshot, getSnapshot);
114
- const navigate = useCallback((href, options)=>{
115
- const router = getRouterInstance(internalContext, contextRouter);
116
- const activeFramework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
117
- if ('tanstack' === activeFramework) {
118
- if ('function' == typeof router?.navigate) return router.navigate({
119
- to: href,
120
- replace: options?.replace,
121
- ...options?.state === void 0 ? {} : {
122
- state: options.state
123
- }
124
- });
125
- throw new Error('TanStack router instance is not available.');
126
- }
127
- if (reactRouterNavigate) return reactRouterNavigate(href, options);
128
- if ('react-router' === activeFramework) {
129
- if ('function' == typeof router?.navigate) return router.navigate(href, options);
130
- throw new Error('React Router instance is not available.');
131
- }
132
- }, [
133
- contextRouter,
134
- internalContext,
135
- inReactRouter,
136
- reactRouterNavigate,
137
- runtimeContext
138
- ]);
139
- const location = (reactRouterLocation ? normalizeLocation(reactRouterLocation) : getRouterStateLocation(internalContext, contextRouter)) || getWindowLocation();
140
- const params = inReactRouter ? reactRouterParams : getRouterParams(internalContext, contextRouter);
141
- const Link = 'tanstack' === framework ? internalContext.router?.Link || runtimeContext.router?.Link || null : 'react-router' === framework || inReactRouter ? routerApi?.Link ?? null : null;
17
+ const hasRouter = routerApi?.useInRouterContext?.() ?? false;
18
+ const navigate = hasRouter ? routerApi?.useNavigate?.() ?? null : null;
19
+ const routerLocation = hasRouter ? routerApi?.useLocation?.() ?? null : null;
20
+ const params = hasRouter ? routerApi?.useParams?.() ?? {} : {};
21
+ const location = routerLocation ?? (isBrowser() ? {
22
+ pathname: window.location.pathname,
23
+ search: window.location.search,
24
+ hash: window.location.hash
25
+ } : null);
142
26
  return {
143
- framework,
27
+ framework: hasRouter ? 'react-router' : void 0,
144
28
  hasRouter,
29
+ navigate,
145
30
  location,
146
- navigate: hasRouter ? navigate : null,
147
- Link,
148
- params
31
+ params,
32
+ Link: hasRouter ? routerApi?.Link ?? null : null
149
33
  };
150
34
  };
151
- export { useI18nRouterAdapter };
35
+ const useI18nRouterAdapter = ()=>{
36
+ const provided = useContext(I18nNavigationContext);
37
+ const native = useNativeI18nRouterAdapter();
38
+ return provided ?? native;
39
+ };
40
+ export { I18nNavigationProvider, useI18nRouterAdapter, useNativeI18nRouterAdapter };
@@ -1,7 +1,9 @@
1
1
  import "node:module";
2
- import { localiseTargetPathname, shouldSkipLocaleRedirect } from "@modern-js/i18n-runtime-extensions";
3
2
  import { isBrowser } from "@modern-js/runtime";
4
3
  import { getGlobalBasename } from "@modern-js/runtime/context";
4
+ import { splitUrlTarget } from "@modern-js/runtime-utils/url";
5
+ import { isDefaultLocaleRedirectSkipPath } from "../shared/mappedUrlStrategy.mjs";
6
+ import { asI18nUrlStrategy } from "../shared/urlStrategy.mjs";
5
7
  const getPathname = (context)=>{
6
8
  if (isBrowser()) return window.location.pathname;
7
9
  return context.ssrContext?.request?.pathname || '/';
@@ -17,22 +19,14 @@ const getLanguageFromPath = (pathname, languages, fallbackLanguage)=>{
17
19
  if (languages.includes(firstSegment)) return firstSegment;
18
20
  return fallbackLanguage;
19
21
  };
20
- const splitUrlTarget = (target)=>{
21
- const hashIndex = target.indexOf('#');
22
- const hash = hashIndex >= 0 ? target.slice(hashIndex) : '';
23
- const beforeHash = hashIndex >= 0 ? target.slice(0, hashIndex) : target;
24
- const searchIndex = beforeHash.indexOf('?');
25
- const search = searchIndex >= 0 ? beforeHash.slice(searchIndex) : '';
26
- const pathname = searchIndex >= 0 ? beforeHash.slice(0, searchIndex) : beforeHash;
27
- return {
28
- pathname,
29
- search,
30
- hash
31
- };
32
- };
33
- const buildLocalizedUrl = (target, language, languages, localisedUrls)=>{
22
+ const buildLocalizedUrl = (target, language, languages, urlStrategy)=>{
34
23
  const { pathname, search, hash } = splitUrlTarget(target);
35
- const localizedPathname = localiseTargetPathname(pathname, language, languages, localisedUrls);
24
+ const strategy = asI18nUrlStrategy(urlStrategy);
25
+ if (strategy) return `${strategy.localizePathname(pathname, language, languages)}${search}${hash}`;
26
+ const segments = pathname.split('/').filter(Boolean);
27
+ if (segments[0] && languages.some((item)=>item.toLowerCase() === segments[0].toLowerCase())) segments[0] = language;
28
+ else segments.unshift(language);
29
+ const localizedPathname = `/${segments.join('/')}`;
36
30
  return `${localizedPathname}${search}${hash}`;
37
31
  };
38
32
  const detectLanguageFromPath = (pathname, languages, localePathRedirect)=>{
@@ -52,5 +46,13 @@ const detectLanguageFromPath = (pathname, languages, localePathRedirect)=>{
52
46
  detected: false
53
47
  };
54
48
  };
55
- const shouldIgnoreRedirect = (pathname, languages, ignoreRedirectRoutes)=>shouldSkipLocaleRedirect(pathname, languages, ignoreRedirectRoutes);
49
+ const shouldIgnoreRedirect = (pathname, languages, ignoreRedirectRoutes, urlStrategy)=>{
50
+ if (isDefaultLocaleRedirectSkipPath(pathname, languages)) return true;
51
+ if (asI18nUrlStrategy(urlStrategy)?.shouldSkipRedirect?.(pathname, languages)) return true;
52
+ if (!ignoreRedirectRoutes) return false;
53
+ const segments = pathname.split('/').filter(Boolean);
54
+ if (segments[0] && languages.some((item)=>item.toLowerCase() === segments[0].toLowerCase())) segments.shift();
55
+ const normalizedPath = `/${segments.join('/')}`;
56
+ return 'function' == typeof ignoreRedirectRoutes ? ignoreRedirectRoutes(normalizedPath) : ignoreRedirectRoutes.some((pattern)=>normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`));
57
+ };
56
58
  export { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getLanguageFromPath, getPathname, shouldIgnoreRedirect, splitUrlTarget };