@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.6 → 3.5.0-ultramodern.75

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 (221) hide show
  1. package/dist/cjs/cli/index.js +2 -1
  2. package/dist/cjs/cli/locales.js +15 -15
  3. package/dist/cjs/runtime/Link.js +8 -89
  4. package/dist/cjs/runtime/context.js +78 -91
  5. package/dist/cjs/runtime/contextHelpers.js +143 -0
  6. package/dist/cjs/runtime/core.js +32 -132
  7. package/dist/cjs/runtime/hooks.js +54 -21
  8. package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
  9. package/dist/cjs/runtime/i18n/detection/cache.js +150 -0
  10. package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
  11. package/dist/cjs/runtime/i18n/detection/index.js +8 -263
  12. package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
  13. package/dist/cjs/runtime/i18n/detection/language.js +62 -0
  14. package/dist/cjs/runtime/i18n/detection/path.js +49 -0
  15. package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
  16. package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
  17. package/dist/cjs/runtime/i18n/detection/types.js +18 -0
  18. package/dist/cjs/runtime/i18n/instance.js +34 -10
  19. package/dist/cjs/runtime/linkHelpers.js +148 -0
  20. package/dist/cjs/runtime/pluginSetup.js +102 -0
  21. package/dist/cjs/runtime/providerComposition.js +115 -0
  22. package/dist/cjs/runtime/reactI18next.js +43 -0
  23. package/dist/cjs/runtime/routerAdapter.js +16 -8
  24. package/dist/cjs/runtime/utils.js +1 -9
  25. package/dist/cjs/server/apiPrefix.js +72 -0
  26. package/dist/cjs/server/detectorOptions.js +97 -0
  27. package/dist/cjs/server/index.js +17 -144
  28. package/dist/cjs/server/redirectPolicy.js +96 -0
  29. package/dist/cjs/shared/detection.js +2 -104
  30. package/dist/cjs/shared/localisedUrls/config.js +49 -0
  31. package/dist/cjs/shared/localisedUrls/index.js +91 -0
  32. package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
  33. package/dist/cjs/shared/localisedUrls/pathname.js +68 -0
  34. package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
  35. package/dist/cjs/shared/localisedUrls/redirect.js +103 -0
  36. package/dist/cjs/shared/localisedUrls/resolve.js +88 -0
  37. package/dist/cjs/shared/localisedUrls/routes.js +130 -0
  38. package/dist/cjs/shared/localisedUrls/types.js +18 -0
  39. package/dist/cjs/shared/localisedUrls.js +28 -294
  40. package/dist/cjs/shared/utils.js +2 -8
  41. package/dist/esm/cli/index.mjs +2 -1
  42. package/dist/esm/cli/locales.mjs +13 -13
  43. package/dist/esm/runtime/Link.mjs +1 -82
  44. package/dist/esm/runtime/context.mjs +76 -92
  45. package/dist/esm/runtime/contextHelpers.mjs +87 -0
  46. package/dist/esm/runtime/core.mjs +30 -133
  47. package/dist/esm/runtime/hooks.mjs +56 -23
  48. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
  49. package/dist/esm/runtime/i18n/detection/cache.mjs +103 -0
  50. package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
  51. package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
  52. package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
  53. package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
  54. package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
  55. package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
  56. package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
  57. package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
  58. package/dist/esm/runtime/i18n/instance.mjs +35 -8
  59. package/dist/esm/runtime/linkHelpers.mjs +98 -0
  60. package/dist/esm/runtime/pluginSetup.mjs +64 -0
  61. package/dist/esm/runtime/providerComposition.mjs +77 -0
  62. package/dist/esm/runtime/reactI18next.mjs +5 -0
  63. package/dist/esm/runtime/routerAdapter.mjs +16 -8
  64. package/dist/esm/runtime/utils.mjs +2 -10
  65. package/dist/esm/server/apiPrefix.mjs +31 -0
  66. package/dist/esm/server/detectorOptions.mjs +59 -0
  67. package/dist/esm/server/index.mjs +8 -135
  68. package/dist/esm/server/redirectPolicy.mjs +46 -0
  69. package/dist/esm/shared/detection.mjs +1 -71
  70. package/dist/esm/shared/localisedUrls/config.mjs +11 -0
  71. package/dist/esm/shared/localisedUrls/index.mjs +7 -0
  72. package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
  73. package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
  74. package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
  75. package/dist/esm/shared/localisedUrls/redirect.mjs +56 -0
  76. package/dist/esm/shared/localisedUrls/resolve.mjs +47 -0
  77. package/dist/esm/shared/localisedUrls/routes.mjs +89 -0
  78. package/dist/esm/shared/localisedUrls/types.mjs +0 -0
  79. package/dist/esm/shared/localisedUrls.mjs +1 -283
  80. package/dist/esm/shared/utils.mjs +1 -1
  81. package/dist/esm-node/cli/index.mjs +2 -1
  82. package/dist/esm-node/cli/locales.mjs +13 -13
  83. package/dist/esm-node/runtime/Link.mjs +1 -82
  84. package/dist/esm-node/runtime/context.mjs +76 -92
  85. package/dist/esm-node/runtime/contextHelpers.mjs +88 -0
  86. package/dist/esm-node/runtime/core.mjs +30 -133
  87. package/dist/esm-node/runtime/hooks.mjs +56 -23
  88. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
  89. package/dist/esm-node/runtime/i18n/detection/cache.mjs +104 -0
  90. package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
  91. package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
  92. package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
  93. package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
  94. package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
  95. package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
  96. package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
  97. package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
  98. package/dist/esm-node/runtime/i18n/instance.mjs +35 -8
  99. package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
  100. package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
  101. package/dist/esm-node/runtime/providerComposition.mjs +78 -0
  102. package/dist/esm-node/runtime/reactI18next.mjs +6 -0
  103. package/dist/esm-node/runtime/routerAdapter.mjs +16 -8
  104. package/dist/esm-node/runtime/utils.mjs +2 -10
  105. package/dist/esm-node/server/apiPrefix.mjs +32 -0
  106. package/dist/esm-node/server/detectorOptions.mjs +60 -0
  107. package/dist/esm-node/server/index.mjs +8 -135
  108. package/dist/esm-node/server/redirectPolicy.mjs +47 -0
  109. package/dist/esm-node/shared/detection.mjs +1 -71
  110. package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
  111. package/dist/esm-node/shared/localisedUrls/index.mjs +8 -0
  112. package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
  113. package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
  114. package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
  115. package/dist/esm-node/shared/localisedUrls/redirect.mjs +57 -0
  116. package/dist/esm-node/shared/localisedUrls/resolve.mjs +48 -0
  117. package/dist/esm-node/shared/localisedUrls/routes.mjs +90 -0
  118. package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
  119. package/dist/esm-node/shared/localisedUrls.mjs +1 -283
  120. package/dist/esm-node/shared/utils.mjs +1 -1
  121. package/dist/types/cli/locales.d.ts +1 -1
  122. package/dist/types/runtime/Link.d.ts +3 -7
  123. package/dist/types/runtime/context.d.ts +21 -5
  124. package/dist/types/runtime/contextHelpers.d.ts +29 -0
  125. package/dist/types/runtime/core.d.ts +4 -8
  126. package/dist/types/runtime/hooks.d.ts +3 -7
  127. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
  128. package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
  129. package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
  130. package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
  131. package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
  132. package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
  133. package/dist/types/runtime/i18n/detection/middleware.d.ts +4 -4
  134. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +4 -4
  135. package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
  136. package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
  137. package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
  138. package/dist/types/runtime/i18n/detection/types.d.ts +32 -0
  139. package/dist/types/runtime/i18n/instance.d.ts +31 -27
  140. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
  141. package/dist/types/runtime/i18n/utils.d.ts +14 -5
  142. package/dist/types/runtime/linkHelpers.d.ts +17 -0
  143. package/dist/types/runtime/pluginSetup.d.ts +32 -0
  144. package/dist/types/runtime/providerComposition.d.ts +16 -0
  145. package/dist/types/runtime/reactI18next.d.ts +7 -0
  146. package/dist/types/runtime/routerAdapter.d.ts +7 -6
  147. package/dist/types/runtime/utils.d.ts +7 -2
  148. package/dist/types/server/apiPrefix.d.ts +7 -0
  149. package/dist/types/server/detectorOptions.d.ts +21 -0
  150. package/dist/types/server/index.d.ts +1 -6
  151. package/dist/types/server/redirectPolicy.d.ts +22 -0
  152. package/dist/types/shared/detection.d.ts +1 -11
  153. package/dist/types/shared/localisedUrls/config.d.ts +9 -0
  154. package/dist/types/shared/localisedUrls/index.d.ts +9 -0
  155. package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
  156. package/dist/types/shared/localisedUrls/pathname.d.ts +4 -0
  157. package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
  158. package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
  159. package/dist/types/shared/localisedUrls/resolve.d.ts +7 -0
  160. package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
  161. package/dist/types/shared/localisedUrls/types.d.ts +14 -0
  162. package/dist/types/shared/localisedUrls.d.ts +2 -36
  163. package/dist/types/shared/utils.d.ts +0 -2
  164. package/package.json +17 -20
  165. package/rstest.config.mts +8 -1
  166. package/src/cli/index.ts +5 -4
  167. package/src/cli/locales.ts +3 -3
  168. package/src/runtime/Link.tsx +11 -138
  169. package/src/runtime/context.tsx +170 -207
  170. package/src/runtime/contextHelpers.ts +242 -0
  171. package/src/runtime/core.tsx +48 -275
  172. package/src/runtime/hooks.ts +86 -24
  173. package/src/runtime/i18n/backend/defaults.node.ts +2 -2
  174. package/src/runtime/i18n/backend/middleware.common.ts +5 -4
  175. package/src/runtime/i18n/detection/cache.ts +189 -0
  176. package/src/runtime/i18n/detection/detector.ts +166 -0
  177. package/src/runtime/i18n/detection/index.ts +10 -641
  178. package/src/runtime/i18n/detection/initOptions.ts +74 -0
  179. package/src/runtime/i18n/detection/language.ts +64 -0
  180. package/src/runtime/i18n/detection/middleware.node.ts +26 -7
  181. package/src/runtime/i18n/detection/middleware.ts +15 -7
  182. package/src/runtime/i18n/detection/path.ts +41 -0
  183. package/src/runtime/i18n/detection/priority.ts +82 -0
  184. package/src/runtime/i18n/detection/ssr.ts +47 -0
  185. package/src/runtime/i18n/detection/types.ts +41 -0
  186. package/src/runtime/i18n/instance.ts +98 -34
  187. package/src/runtime/i18n/react-i18next.ts +1 -6
  188. package/src/runtime/i18n/utils.ts +18 -9
  189. package/src/runtime/linkHelpers.ts +174 -0
  190. package/src/runtime/pluginSetup.ts +189 -0
  191. package/src/runtime/providerComposition.tsx +148 -0
  192. package/src/runtime/reactI18next.ts +20 -0
  193. package/src/runtime/routerAdapter.tsx +36 -15
  194. package/src/runtime/utils.ts +13 -35
  195. package/src/server/apiPrefix.ts +67 -0
  196. package/src/server/detectorOptions.ts +105 -0
  197. package/src/server/index.ts +14 -319
  198. package/src/server/redirectPolicy.ts +127 -0
  199. package/src/shared/detection.ts +1 -1
  200. package/src/shared/localisedUrls/config.ts +18 -0
  201. package/src/shared/localisedUrls/index.ts +24 -0
  202. package/src/shared/localisedUrls/normalise.ts +67 -0
  203. package/src/shared/localisedUrls/pathname.ts +60 -0
  204. package/src/shared/localisedUrls/patterns.ts +160 -0
  205. package/src/shared/localisedUrls/redirect.ts +93 -0
  206. package/src/shared/localisedUrls/resolve.ts +135 -0
  207. package/src/shared/localisedUrls/routes.ts +234 -0
  208. package/src/shared/localisedUrls/types.ts +18 -0
  209. package/src/shared/localisedUrls.ts +26 -623
  210. package/src/shared/utils.ts +2 -2
  211. package/tests/federatedI18nBoundary.client.test.tsx +112 -0
  212. package/tests/federatedI18nBoundary.test.tsx +120 -0
  213. package/tests/fixtures/localised-routes.golden.json +156 -0
  214. package/tests/link.test.tsx +111 -0
  215. package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
  216. package/tests/localisedUrls.test.ts +148 -2
  217. package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
  218. package/tests/redirectPolicy.test.ts +45 -0
  219. package/tests/routerAdapter.test.tsx +603 -0
  220. package/tests/type-fixture/fixture-globals.d.ts +11 -0
  221. package/tests/type-fixture/tsconfig.json +1 -0
@@ -1,4 +1,3 @@
1
- import { isBrowser } from '@modern-js/runtime';
2
1
  import type { FC, ReactNode } from 'react';
3
2
  import {
4
3
  createContext,
@@ -8,18 +7,19 @@ import {
8
7
  useMemo,
9
8
  } from 'react';
10
9
  import type { LocalisedUrlsOption } from '../shared/localisedUrls';
10
+ import {
11
+ changeModernI18nLanguage,
12
+ getPathLanguage,
13
+ isI18nLanguageSupported,
14
+ isI18nResourcesReady,
15
+ translateI18n,
16
+ } from './contextHelpers';
11
17
  import type { I18nInstance } from './i18n';
12
- import type { SdkBackend } from './i18n/backend/sdk-backend';
13
- import { cacheUserLanguage } from './i18n/detection';
18
+ import type { Resources } from './i18n/instance';
19
+ import { getActualI18nextInstance } from './i18n/instance';
14
20
  import { useI18nRouterAdapter } from './routerAdapter';
15
- import {
16
- buildLocalizedUrl,
17
- detectLanguageFromPath,
18
- getEntryPath,
19
- shouldIgnoreRedirect,
20
- } from './utils';
21
21
 
22
- export interface ModernI18nContextValue {
22
+ interface ModernI18nContextValue {
23
23
  language: string;
24
24
  i18nInstance: I18nInstance;
25
25
  // Plugin configuration for useModernI18n hook
@@ -30,6 +30,7 @@ export interface ModernI18nContextValue {
30
30
  localisedUrls?: LocalisedUrlsOption;
31
31
  // Callback to update language in context
32
32
  updateLanguage?: (newLang: string) => void;
33
+ synchronizeLanguage?: (newLang: string) => void;
33
34
  }
34
35
 
35
36
  const modernI18nContextKey = Symbol.for(
@@ -51,7 +52,7 @@ const getModernI18nContext = () => {
51
52
 
52
53
  const ModernI18nContext = getModernI18nContext();
53
54
 
54
- export interface ModernI18nProviderProps {
55
+ interface ModernI18nProviderProps {
55
56
  children: ReactNode;
56
57
  value: ModernI18nContextValue;
57
58
  }
@@ -67,7 +68,126 @@ export const ModernI18nProvider: FC<ModernI18nProviderProps> = ({
67
68
  );
68
69
  };
69
70
 
70
- export interface UseModernI18nReturn {
71
+ export interface FederatedI18nBoundaryProps {
72
+ children: ReactNode;
73
+ defaultNamespace: string;
74
+ fallbackLanguage?: string;
75
+ resources: Resources;
76
+ supportedLanguages?: string[];
77
+ }
78
+
79
+ /**
80
+ * Keeps a federated surface's translation resources inside its delivery unit.
81
+ * The host supplies only the active language; the remote owns and versions the
82
+ * resources used below this boundary.
83
+ */
84
+ export const FederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = ({
85
+ children,
86
+ defaultNamespace,
87
+ fallbackLanguage,
88
+ resources,
89
+ supportedLanguages,
90
+ }) => {
91
+ const parent = useContext(ModernI18nContext);
92
+ if (!parent) {
93
+ throw new Error(
94
+ 'FederatedI18nBoundary must be used within ModernI18nProvider',
95
+ );
96
+ }
97
+
98
+ const languages =
99
+ supportedLanguages ?? parent.languages ?? Object.keys(resources);
100
+ const scopedInstance = useMemo(() => {
101
+ const parentInstance = getActualI18nextInstance(parent.i18nInstance);
102
+ const clone = parentInstance.cloneInstance?.({
103
+ defaultNS: defaultNamespace,
104
+ fallbackLng: fallbackLanguage ?? languages[0] ?? parent.language,
105
+ forkResourceStore: true,
106
+ initImmediate: false,
107
+ lng: parent.language,
108
+ ns: [defaultNamespace],
109
+ resources,
110
+ supportedLngs: languages,
111
+ });
112
+ if (!clone) {
113
+ throw new Error(
114
+ 'FederatedI18nBoundary requires an i18n instance with cloneInstance support',
115
+ );
116
+ }
117
+ const resourceStore = clone.store;
118
+ const parentResourceStore = parentInstance.store;
119
+ const sharesNestedResourceState = Object.entries(
120
+ resourceStore?.data ?? {},
121
+ ).some(([language, namespaces]) => {
122
+ const parentNamespaces = parentResourceStore?.data?.[language];
123
+ return (
124
+ namespaces === parentNamespaces ||
125
+ Object.entries(namespaces).some(
126
+ ([namespace, resource]) =>
127
+ resource !== null &&
128
+ typeof resource === 'object' &&
129
+ resource === parentNamespaces?.[namespace],
130
+ )
131
+ );
132
+ });
133
+ if (
134
+ clone === parentInstance ||
135
+ resourceStore === parentResourceStore ||
136
+ resourceStore?.data === parentResourceStore?.data ||
137
+ sharesNestedResourceState
138
+ ) {
139
+ throw new Error(
140
+ 'FederatedI18nBoundary cloneInstance did not isolate the host resource store',
141
+ );
142
+ }
143
+ if (
144
+ !resourceStore?.addResourceBundle ||
145
+ !clone.removeResourceBundle ||
146
+ !resourceStore.data
147
+ ) {
148
+ throw new Error(
149
+ 'FederatedI18nBoundary requires an isolated mutable i18n resource store',
150
+ );
151
+ }
152
+ for (const [language, namespaces] of Object.entries(resourceStore.data)) {
153
+ for (const namespace of Object.keys(namespaces)) {
154
+ clone.removeResourceBundle(language, namespace);
155
+ }
156
+ }
157
+ for (const [language, namespaces] of Object.entries(resources)) {
158
+ for (const [namespace, resource] of Object.entries(namespaces)) {
159
+ resourceStore.addResourceBundle(
160
+ language,
161
+ namespace,
162
+ resource as Record<string, string>,
163
+ true,
164
+ true,
165
+ );
166
+ }
167
+ }
168
+ return clone;
169
+ }, [
170
+ defaultNamespace,
171
+ fallbackLanguage,
172
+ languages,
173
+ parent.i18nInstance,
174
+ parent.language,
175
+ resources,
176
+ ]);
177
+ const value = useMemo(
178
+ () => ({
179
+ ...parent,
180
+ i18nInstance: scopedInstance,
181
+ language: parent.language,
182
+ languages,
183
+ }),
184
+ [languages, parent, scopedInstance],
185
+ );
186
+
187
+ return <ModernI18nProvider value={value}>{children}</ModernI18nProvider>;
188
+ };
189
+
190
+ interface UseModernI18nReturn {
71
191
  language: string;
72
192
  changeLanguage: (newLang: string) => Promise<void>;
73
193
  t: (key: string | string[], ...args: any[]) => string;
@@ -75,7 +195,7 @@ export interface UseModernI18nReturn {
75
195
  supportedLanguages: string[];
76
196
  localisedUrls?: LocalisedUrlsOption;
77
197
  isLanguageSupported: (lang: string) => boolean;
78
- // Indicates if translation resources for current language are ready to use
198
+ // Indicates whether translation resources for current language are ready
79
199
  isResourcesReady: boolean;
80
200
  }
81
201
 
@@ -85,9 +205,9 @@ export interface UseModernI18nReturn {
85
205
  * This hook provides:
86
206
  * - Current language from URL params or i18n context
87
207
  * - changeLanguage function that updates both i18n instance and URL
88
- * - Direct access to the i18n instance
208
+ * - Direct access to i18n instance
89
209
  * - List of supported languages
90
- * - Helper function to check if a language is supported
210
+ * - Helper function to check if language is supported
91
211
  *
92
212
  * @param options - Optional configuration to override context settings
93
213
  * @returns Object containing i18n functionality and utilities
@@ -95,7 +215,7 @@ export interface UseModernI18nReturn {
95
215
  export const useModernI18n = (): UseModernI18nReturn => {
96
216
  const context = useContext(ModernI18nContext);
97
217
  if (!context) {
98
- throw new Error('useModernI18n must be used within a ModernI18nProvider');
218
+ throw new Error('useModernI18n must be used within ModernI18nProvider');
99
219
  }
100
220
 
101
221
  const {
@@ -106,155 +226,47 @@ export const useModernI18n = (): UseModernI18nReturn => {
106
226
  ignoreRedirectRoutes,
107
227
  localisedUrls,
108
228
  updateLanguage,
229
+ synchronizeLanguage,
109
230
  } = context;
110
231
 
111
232
  const { navigate, location, hasRouter } = useI18nRouterAdapter();
112
233
 
113
- const pathLanguage = useMemo(() => {
114
- if (!localePathRedirect || !location?.pathname) {
115
- return undefined;
116
- }
117
- const detected = detectLanguageFromPath(
118
- location.pathname,
119
- languages || [],
120
- localePathRedirect,
121
- );
122
- return detected.detected ? detected.language : undefined;
123
- }, [languages, localePathRedirect, location?.pathname]);
124
-
125
- const currentLanguage = pathLanguage || contextLanguage;
234
+ const pathLanguage = useMemo(
235
+ () => getPathLanguage(location?.pathname, languages, localePathRedirect),
236
+ [languages, localePathRedirect, location?.pathname],
237
+ );
126
238
 
127
239
  useEffect(() => {
128
- if (!pathLanguage || pathLanguage === contextLanguage) {
129
- return;
240
+ if (pathLanguage) {
241
+ synchronizeLanguage?.(pathLanguage);
130
242
  }
243
+ }, [pathLanguage, synchronizeLanguage]);
131
244
 
132
- updateLanguage?.(pathLanguage);
133
- i18nInstance?.setLang?.(pathLanguage);
134
- void i18nInstance?.changeLanguage?.(pathLanguage);
135
-
136
- if (isBrowser()) {
137
- const detectionOptions = i18nInstance.options?.detection;
138
- cacheUserLanguage(i18nInstance, pathLanguage, detectionOptions);
139
- }
140
- }, [contextLanguage, i18nInstance, pathLanguage, updateLanguage]);
245
+ const currentLanguage = contextLanguage;
141
246
 
142
247
  /**
143
- * Changes the current language and updates the URL accordingly.
248
+ * Changes the current language and updates URL accordingly.
144
249
  *
145
250
  * This function:
146
- * 1. Updates the i18n instance language
147
- * 2. Updates the URL by replacing the language prefix in the current path
148
- * 3. Triggers a navigation to the new URL
251
+ * 1. Updates i18n instance language
252
+ * 2. Updates URL by replacing language prefix in the current path
253
+ * 3. Triggers navigation to the new URL
149
254
  *
150
255
  * @param newLang - The new language code to switch to
151
256
  */
152
257
  const changeLanguage = useCallback(
153
- async (newLang: string) => {
154
- try {
155
- // Validate language
156
- if (!newLang || typeof newLang !== 'string') {
157
- throw new Error('Language must be a non-empty string');
158
- }
159
-
160
- await i18nInstance?.setLang?.(newLang);
161
- await i18nInstance?.changeLanguage?.(newLang);
162
-
163
- if (isBrowser()) {
164
- const detectionOptions = i18nInstance.options?.detection;
165
- cacheUserLanguage(i18nInstance, newLang, detectionOptions);
166
- }
167
-
168
- if (
169
- localePathRedirect &&
170
- isBrowser() &&
171
- hasRouter &&
172
- navigate &&
173
- location
174
- ) {
175
- const currentPath = location.pathname;
176
- const entryPath = getEntryPath();
177
- const relativePath = currentPath.replace(entryPath, '');
178
-
179
- // Check if the path already contains the target language
180
- const pathLanguage = detectLanguageFromPath(
181
- currentPath,
182
- languages || [],
183
- localePathRedirect,
184
- );
185
-
186
- // If path already has the target language, skip redirect
187
- if (pathLanguage.detected && pathLanguage.language === newLang) {
188
- return;
189
- }
190
-
191
- if (
192
- !shouldIgnoreRedirect(
193
- relativePath,
194
- languages || [],
195
- ignoreRedirectRoutes,
196
- )
197
- ) {
198
- const newPath = buildLocalizedUrl(
199
- relativePath,
200
- newLang,
201
- languages || [],
202
- localisedUrls,
203
- );
204
- const newUrl =
205
- entryPath + newPath + location.search + location.hash;
206
-
207
- await navigate(newUrl, { replace: true });
208
- }
209
- } else if (localePathRedirect && isBrowser() && !hasRouter) {
210
- const currentPath = window.location.pathname;
211
- const entryPath = getEntryPath();
212
- const relativePath = currentPath.replace(entryPath, '');
213
-
214
- // Check if the path already contains the target language
215
- const pathLanguage = detectLanguageFromPath(
216
- currentPath,
217
- languages || [],
218
- localePathRedirect,
219
- );
220
-
221
- // If path already has the target language, skip redirect
222
- if (pathLanguage.detected && pathLanguage.language === newLang) {
223
- return;
224
- }
225
-
226
- if (
227
- !shouldIgnoreRedirect(
228
- relativePath,
229
- languages || [],
230
- ignoreRedirectRoutes,
231
- )
232
- ) {
233
- const newPath = buildLocalizedUrl(
234
- relativePath,
235
- newLang,
236
- languages || [],
237
- localisedUrls,
238
- );
239
- const newUrl =
240
- entryPath +
241
- newPath +
242
- window.location.search +
243
- window.location.hash;
244
-
245
- window.history.pushState(null, '', newUrl);
246
- }
247
- }
248
-
249
- // Update language state after URL update
250
- if (updateLanguage) {
251
- updateLanguage(newLang);
252
- }
253
- } catch (error) {
254
- console.error('Failed to change language:', error);
255
- throw error;
256
- }
257
- },
258
+ (newLang: string) =>
259
+ changeModernI18nLanguage(newLang, {
260
+ i18nInstance,
261
+ updateLanguage,
262
+ localePathRedirect,
263
+ ignoreRedirectRoutes,
264
+ localisedUrls,
265
+ languages,
266
+ hasRouter,
267
+ navigate,
268
+ location,
269
+ }),
258
270
  [
259
271
  i18nInstance,
260
272
  updateLanguage,
@@ -269,72 +281,23 @@ export const useModernI18n = (): UseModernI18nReturn => {
269
281
  );
270
282
 
271
283
  const t = useCallback(
272
- (key: string | string[], ...args: any[]) => {
273
- if (typeof i18nInstance.t !== 'function') {
274
- throw new Error('i18nInstance.t is required');
275
- }
276
-
277
- return i18nInstance.t(key, ...args) as string;
278
- },
284
+ (key: string | string[], ...args: any[]) =>
285
+ translateI18n(i18nInstance, key, ...args),
279
286
  [currentLanguage, i18nInstance],
280
287
  );
281
288
 
282
- // Helper function to check if a language is supported
289
+ // Helper function to check if language is supported
283
290
  const isLanguageSupported = useCallback(
284
- (lang: string) => {
285
- return languages?.includes(lang) || false;
286
- },
291
+ (lang: string) => isI18nLanguageSupported(languages, lang),
287
292
  [languages],
288
293
  );
289
294
 
290
295
  // Check if current language resources are ready
291
- // This checks if all required namespaces for the current language are loaded
292
- const isResourcesReady = useMemo(() => {
293
- if (!i18nInstance?.isInitialized) {
294
- return false;
295
- }
296
-
297
- // Get backend instance
298
- const backend = i18nInstance?.services?.backend as SdkBackend | undefined;
299
-
300
- // If using SDK backend, check loading state
301
- if (backend && typeof backend.isLoading === 'function') {
302
- // Check if any resource for current language is loading
303
- const loadingResources = backend.getLoadingResources();
304
- const isCurrentLanguageLoading = loadingResources.some(
305
- ({ language }) => language === currentLanguage,
306
- );
307
- if (isCurrentLanguageLoading) {
308
- return false;
309
- }
310
- }
311
-
312
- // Check if resources exist in store
313
- const store = (i18nInstance as any).store;
314
- if (!store?.data) {
315
- return false;
316
- }
317
-
318
- const langData = store.data[currentLanguage];
319
- if (!langData || typeof langData !== 'object') {
320
- return false;
321
- }
322
-
323
- // Get required namespaces
324
- const options = i18nInstance.options;
325
- const namespaces = options?.ns || options?.defaultNS || ['translation'];
326
- const requiredNamespaces = Array.isArray(namespaces)
327
- ? namespaces
328
- : [namespaces];
329
-
330
- // Check if all required namespaces are loaded
331
- return requiredNamespaces.every(ns => {
332
- const nsData = langData[ns];
333
- return (
334
- nsData && typeof nsData === 'object' && Object.keys(nsData).length > 0
335
- );
336
- });
337
- }, [currentLanguage, i18nInstance]);
296
+ // This checks if all required namespaces for current language are loaded
297
+ const isResourcesReady = useMemo(
298
+ () => isI18nResourcesReady(i18nInstance, currentLanguage),
299
+ [currentLanguage, i18nInstance],
300
+ );
338
301
 
339
302
  return {
340
303
  language: currentLanguage,
@@ -0,0 +1,242 @@
1
+ import { isBrowser } from '@modern-js/runtime';
2
+ import type { LocalisedUrlsOption } from '../shared/localisedUrls';
3
+ import type { I18nInstance } from './i18n';
4
+ import type { SdkBackend } from './i18n/backend/sdk-backend';
5
+ import { cacheUserLanguage } from './i18n/detection';
6
+ import {
7
+ buildLocalizedUrl,
8
+ detectLanguageFromPath,
9
+ getEntryPath,
10
+ shouldIgnoreRedirect,
11
+ } from './utils';
12
+
13
+ type Navigate = (
14
+ url: string,
15
+ options?: {
16
+ replace?: boolean;
17
+ },
18
+ ) => Promise<void> | void;
19
+
20
+ type LocationLike = {
21
+ pathname: string;
22
+ search: string;
23
+ hash: string;
24
+ };
25
+
26
+ export function getPathLanguage(
27
+ pathname: string | undefined,
28
+ languages: string[] | undefined,
29
+ localePathRedirect: boolean | undefined,
30
+ ): string | undefined {
31
+ if (!localePathRedirect || !pathname) {
32
+ return undefined;
33
+ }
34
+
35
+ const detected = detectLanguageFromPath(
36
+ pathname,
37
+ languages || [],
38
+ localePathRedirect,
39
+ );
40
+ return detected.detected ? detected.language : undefined;
41
+ }
42
+
43
+ export function cacheI18nLanguage(
44
+ i18nInstance: I18nInstance,
45
+ language: string,
46
+ ) {
47
+ if (isBrowser()) {
48
+ const detectionOptions = i18nInstance.options?.detection;
49
+ cacheUserLanguage(i18nInstance, language, detectionOptions);
50
+ }
51
+ }
52
+
53
+ export async function changeI18nInstanceLanguage(
54
+ i18nInstance: I18nInstance,
55
+ language: string,
56
+ ): Promise<void> {
57
+ if (i18nInstance.setLang) {
58
+ await i18nInstance.setLang(language);
59
+ } else {
60
+ await i18nInstance.changeLanguage?.(language);
61
+ }
62
+ }
63
+
64
+ interface ChangeModernI18nLanguageOptions {
65
+ i18nInstance: I18nInstance;
66
+ updateLanguage?: (newLang: string) => void;
67
+ localePathRedirect?: boolean;
68
+ ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
69
+ localisedUrls?: LocalisedUrlsOption;
70
+ languages?: string[];
71
+ hasRouter: boolean;
72
+ navigate?: Navigate | null;
73
+ location?: LocationLike | null;
74
+ }
75
+
76
+ export async function changeModernI18nLanguage(
77
+ newLang: string,
78
+ options: ChangeModernI18nLanguageOptions,
79
+ ): Promise<void> {
80
+ const {
81
+ i18nInstance,
82
+ updateLanguage,
83
+ localePathRedirect,
84
+ ignoreRedirectRoutes,
85
+ localisedUrls,
86
+ languages,
87
+ hasRouter,
88
+ navigate,
89
+ location,
90
+ } = options;
91
+
92
+ try {
93
+ if (!newLang || typeof newLang !== 'string') {
94
+ throw new Error('Language must be non-empty string');
95
+ }
96
+
97
+ await changeI18nInstanceLanguage(i18nInstance, newLang);
98
+ cacheI18nLanguage(i18nInstance, newLang);
99
+
100
+ if (
101
+ localePathRedirect &&
102
+ isBrowser() &&
103
+ hasRouter &&
104
+ navigate &&
105
+ location
106
+ ) {
107
+ const currentPath = location.pathname;
108
+ const entryPath = getEntryPath();
109
+ const relativePath = currentPath.replace(entryPath, '');
110
+ const pathLanguage = detectLanguageFromPath(
111
+ currentPath,
112
+ languages || [],
113
+ localePathRedirect,
114
+ );
115
+
116
+ if (pathLanguage.detected && pathLanguage.language === newLang) {
117
+ updateLanguage?.(newLang);
118
+ return;
119
+ }
120
+
121
+ if (
122
+ !shouldIgnoreRedirect(
123
+ relativePath,
124
+ languages || [],
125
+ ignoreRedirectRoutes,
126
+ )
127
+ ) {
128
+ const newPath = buildLocalizedUrl(
129
+ relativePath,
130
+ newLang,
131
+ languages || [],
132
+ localisedUrls,
133
+ );
134
+ const newUrl = entryPath + newPath + location.search + location.hash;
135
+
136
+ await navigate(newUrl, { replace: true });
137
+ }
138
+ } else if (localePathRedirect && isBrowser() && !hasRouter) {
139
+ const currentPath = window.location.pathname;
140
+ const entryPath = getEntryPath();
141
+ const relativePath = currentPath.replace(entryPath, '');
142
+ const pathLanguage = detectLanguageFromPath(
143
+ currentPath,
144
+ languages || [],
145
+ localePathRedirect,
146
+ );
147
+
148
+ if (pathLanguage.detected && pathLanguage.language === newLang) {
149
+ updateLanguage?.(newLang);
150
+ return;
151
+ }
152
+
153
+ if (
154
+ !shouldIgnoreRedirect(
155
+ relativePath,
156
+ languages || [],
157
+ ignoreRedirectRoutes,
158
+ )
159
+ ) {
160
+ const newPath = buildLocalizedUrl(
161
+ relativePath,
162
+ newLang,
163
+ languages || [],
164
+ localisedUrls,
165
+ );
166
+ const newUrl =
167
+ entryPath + newPath + window.location.search + window.location.hash;
168
+
169
+ window.history.pushState(null, '', newUrl);
170
+ }
171
+ }
172
+
173
+ if (updateLanguage) {
174
+ updateLanguage(newLang);
175
+ }
176
+ } catch (error) {
177
+ console.error('Failed change language:', error);
178
+ throw error;
179
+ }
180
+ }
181
+
182
+ export function translateI18n(
183
+ i18nInstance: I18nInstance,
184
+ key: string | string[],
185
+ ...args: unknown[]
186
+ ): string {
187
+ if (typeof i18nInstance.t !== 'function') {
188
+ throw new Error('i18nInstance.t required');
189
+ }
190
+
191
+ return i18nInstance.t(key, ...args) as string;
192
+ }
193
+
194
+ export function isI18nLanguageSupported(
195
+ languages: string[] | undefined,
196
+ lang: string,
197
+ ): boolean {
198
+ return languages?.includes(lang) || false;
199
+ }
200
+
201
+ export function isI18nResourcesReady(
202
+ i18nInstance: I18nInstance,
203
+ currentLanguage: string,
204
+ ): boolean {
205
+ if (!i18nInstance?.isInitialized) {
206
+ return false;
207
+ }
208
+
209
+ const backend = i18nInstance?.services?.backend as SdkBackend | undefined;
210
+ if (backend && typeof backend.isLoading === 'function') {
211
+ const loadingResources = backend.getLoadingResources();
212
+ const isCurrentLanguageLoading = loadingResources.some(
213
+ ({ language }) => language === currentLanguage,
214
+ );
215
+ if (isCurrentLanguageLoading) {
216
+ return false;
217
+ }
218
+ }
219
+
220
+ const store = i18nInstance.store;
221
+ if (!store?.data) {
222
+ return false;
223
+ }
224
+
225
+ const langData = store.data[currentLanguage];
226
+ if (!langData || typeof langData !== 'object') {
227
+ return false;
228
+ }
229
+
230
+ const options = i18nInstance.options;
231
+ const namespaces = options?.ns || options?.defaultNS || ['translation'];
232
+ const requiredNamespaces = Array.isArray(namespaces)
233
+ ? namespaces
234
+ : [namespaces];
235
+
236
+ return requiredNamespaces.every(ns => {
237
+ const nsData = langData[ns];
238
+ return (
239
+ nsData && typeof nsData === 'object' && Object.keys(nsData).length > 0
240
+ );
241
+ });
242
+ }