@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.4 → 3.5.0-ultramodern.41

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 (214) 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 +18 -82
  5. package/dist/cjs/runtime/contextHelpers.js +137 -0
  6. package/dist/cjs/runtime/core.js +29 -132
  7. package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
  8. package/dist/cjs/runtime/i18n/detection/cache.js +150 -0
  9. package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
  10. package/dist/cjs/runtime/i18n/detection/index.js +8 -263
  11. package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
  12. package/dist/cjs/runtime/i18n/detection/language.js +62 -0
  13. package/dist/cjs/runtime/i18n/detection/path.js +49 -0
  14. package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
  15. package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
  16. package/dist/cjs/runtime/i18n/detection/types.js +18 -0
  17. package/dist/cjs/runtime/i18n/instance.js +7 -7
  18. package/dist/cjs/runtime/linkHelpers.js +148 -0
  19. package/dist/cjs/runtime/pluginSetup.js +102 -0
  20. package/dist/cjs/runtime/providerComposition.js +114 -0
  21. package/dist/cjs/runtime/reactI18next.js +43 -0
  22. package/dist/cjs/runtime/utils.js +1 -9
  23. package/dist/cjs/server/apiPrefix.js +72 -0
  24. package/dist/cjs/server/detectorOptions.js +97 -0
  25. package/dist/cjs/server/index.js +17 -144
  26. package/dist/cjs/server/redirectPolicy.js +96 -0
  27. package/dist/cjs/shared/detection.js +2 -104
  28. package/dist/cjs/shared/localisedUrls/config.js +49 -0
  29. package/dist/cjs/shared/localisedUrls/index.js +91 -0
  30. package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
  31. package/dist/cjs/shared/localisedUrls/pathname.js +68 -0
  32. package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
  33. package/dist/cjs/shared/localisedUrls/redirect.js +95 -0
  34. package/dist/cjs/shared/localisedUrls/resolve.js +88 -0
  35. package/dist/cjs/shared/localisedUrls/routes.js +129 -0
  36. package/dist/cjs/shared/localisedUrls/types.js +18 -0
  37. package/dist/cjs/shared/localisedUrls.js +28 -294
  38. package/dist/cjs/shared/utils.js +2 -8
  39. package/dist/esm/cli/index.mjs +2 -1
  40. package/dist/esm/cli/locales.mjs +13 -13
  41. package/dist/esm/runtime/Link.mjs +1 -82
  42. package/dist/esm/runtime/context.mjs +18 -82
  43. package/dist/esm/runtime/contextHelpers.mjs +84 -0
  44. package/dist/esm/runtime/core.mjs +30 -133
  45. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
  46. package/dist/esm/runtime/i18n/detection/cache.mjs +103 -0
  47. package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
  48. package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
  49. package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
  50. package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
  51. package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
  52. package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
  53. package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
  54. package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
  55. package/dist/esm/runtime/i18n/instance.mjs +8 -5
  56. package/dist/esm/runtime/linkHelpers.mjs +98 -0
  57. package/dist/esm/runtime/pluginSetup.mjs +64 -0
  58. package/dist/esm/runtime/providerComposition.mjs +76 -0
  59. package/dist/esm/runtime/reactI18next.mjs +5 -0
  60. package/dist/esm/runtime/utils.mjs +2 -10
  61. package/dist/esm/server/apiPrefix.mjs +31 -0
  62. package/dist/esm/server/detectorOptions.mjs +59 -0
  63. package/dist/esm/server/index.mjs +8 -135
  64. package/dist/esm/server/redirectPolicy.mjs +46 -0
  65. package/dist/esm/shared/detection.mjs +1 -71
  66. package/dist/esm/shared/localisedUrls/config.mjs +11 -0
  67. package/dist/esm/shared/localisedUrls/index.mjs +7 -0
  68. package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
  69. package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
  70. package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
  71. package/dist/esm/shared/localisedUrls/redirect.mjs +48 -0
  72. package/dist/esm/shared/localisedUrls/resolve.mjs +47 -0
  73. package/dist/esm/shared/localisedUrls/routes.mjs +88 -0
  74. package/dist/esm/shared/localisedUrls/types.mjs +0 -0
  75. package/dist/esm/shared/localisedUrls.mjs +1 -283
  76. package/dist/esm/shared/utils.mjs +1 -1
  77. package/dist/esm-node/cli/index.mjs +2 -1
  78. package/dist/esm-node/cli/locales.mjs +13 -13
  79. package/dist/esm-node/runtime/Link.mjs +1 -82
  80. package/dist/esm-node/runtime/context.mjs +18 -82
  81. package/dist/esm-node/runtime/contextHelpers.mjs +85 -0
  82. package/dist/esm-node/runtime/core.mjs +30 -133
  83. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
  84. package/dist/esm-node/runtime/i18n/detection/cache.mjs +104 -0
  85. package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
  86. package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
  87. package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
  88. package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
  89. package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
  90. package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
  91. package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
  92. package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
  93. package/dist/esm-node/runtime/i18n/instance.mjs +8 -5
  94. package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
  95. package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
  96. package/dist/esm-node/runtime/providerComposition.mjs +77 -0
  97. package/dist/esm-node/runtime/reactI18next.mjs +6 -0
  98. package/dist/esm-node/runtime/utils.mjs +2 -10
  99. package/dist/esm-node/server/apiPrefix.mjs +32 -0
  100. package/dist/esm-node/server/detectorOptions.mjs +60 -0
  101. package/dist/esm-node/server/index.mjs +8 -135
  102. package/dist/esm-node/server/redirectPolicy.mjs +47 -0
  103. package/dist/esm-node/shared/detection.mjs +1 -71
  104. package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
  105. package/dist/esm-node/shared/localisedUrls/index.mjs +8 -0
  106. package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
  107. package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
  108. package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
  109. package/dist/esm-node/shared/localisedUrls/redirect.mjs +49 -0
  110. package/dist/esm-node/shared/localisedUrls/resolve.mjs +48 -0
  111. package/dist/esm-node/shared/localisedUrls/routes.mjs +89 -0
  112. package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
  113. package/dist/esm-node/shared/localisedUrls.mjs +1 -283
  114. package/dist/esm-node/shared/utils.mjs +1 -1
  115. package/dist/types/cli/locales.d.ts +1 -1
  116. package/dist/types/runtime/Link.d.ts +3 -7
  117. package/dist/types/runtime/context.d.ts +6 -5
  118. package/dist/types/runtime/contextHelpers.d.ts +28 -0
  119. package/dist/types/runtime/core.d.ts +3 -7
  120. package/dist/types/runtime/hooks.d.ts +5 -0
  121. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
  122. package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
  123. package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
  124. package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
  125. package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
  126. package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
  127. package/dist/types/runtime/i18n/detection/middleware.d.ts +4 -4
  128. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +4 -4
  129. package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
  130. package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
  131. package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
  132. package/dist/types/runtime/i18n/detection/types.d.ts +32 -0
  133. package/dist/types/runtime/i18n/instance.d.ts +28 -26
  134. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
  135. package/dist/types/runtime/i18n/utils.d.ts +14 -5
  136. package/dist/types/runtime/linkHelpers.d.ts +17 -0
  137. package/dist/types/runtime/pluginSetup.d.ts +32 -0
  138. package/dist/types/runtime/providerComposition.d.ts +16 -0
  139. package/dist/types/runtime/reactI18next.d.ts +7 -0
  140. package/dist/types/runtime/routerAdapter.d.ts +7 -6
  141. package/dist/types/runtime/utils.d.ts +7 -2
  142. package/dist/types/server/apiPrefix.d.ts +7 -0
  143. package/dist/types/server/detectorOptions.d.ts +21 -0
  144. package/dist/types/server/index.d.ts +1 -6
  145. package/dist/types/server/redirectPolicy.d.ts +22 -0
  146. package/dist/types/shared/detection.d.ts +1 -11
  147. package/dist/types/shared/localisedUrls/config.d.ts +9 -0
  148. package/dist/types/shared/localisedUrls/index.d.ts +9 -0
  149. package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
  150. package/dist/types/shared/localisedUrls/pathname.d.ts +4 -0
  151. package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
  152. package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
  153. package/dist/types/shared/localisedUrls/resolve.d.ts +7 -0
  154. package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
  155. package/dist/types/shared/localisedUrls/types.d.ts +14 -0
  156. package/dist/types/shared/localisedUrls.d.ts +2 -36
  157. package/dist/types/shared/utils.d.ts +0 -2
  158. package/package.json +16 -18
  159. package/rstest.config.mts +1 -0
  160. package/src/cli/index.ts +5 -4
  161. package/src/cli/locales.ts +3 -3
  162. package/src/runtime/Link.tsx +11 -138
  163. package/src/runtime/context.tsx +45 -199
  164. package/src/runtime/contextHelpers.ts +232 -0
  165. package/src/runtime/core.tsx +43 -274
  166. package/src/runtime/hooks.ts +13 -2
  167. package/src/runtime/i18n/backend/defaults.node.ts +2 -2
  168. package/src/runtime/i18n/backend/middleware.common.ts +5 -4
  169. package/src/runtime/i18n/detection/cache.ts +189 -0
  170. package/src/runtime/i18n/detection/detector.ts +166 -0
  171. package/src/runtime/i18n/detection/index.ts +10 -641
  172. package/src/runtime/i18n/detection/initOptions.ts +74 -0
  173. package/src/runtime/i18n/detection/language.ts +64 -0
  174. package/src/runtime/i18n/detection/middleware.node.ts +26 -7
  175. package/src/runtime/i18n/detection/middleware.ts +15 -7
  176. package/src/runtime/i18n/detection/path.ts +41 -0
  177. package/src/runtime/i18n/detection/priority.ts +82 -0
  178. package/src/runtime/i18n/detection/ssr.ts +47 -0
  179. package/src/runtime/i18n/detection/types.ts +41 -0
  180. package/src/runtime/i18n/instance.ts +56 -31
  181. package/src/runtime/i18n/react-i18next.ts +1 -6
  182. package/src/runtime/i18n/utils.ts +18 -9
  183. package/src/runtime/linkHelpers.ts +174 -0
  184. package/src/runtime/pluginSetup.ts +189 -0
  185. package/src/runtime/providerComposition.tsx +144 -0
  186. package/src/runtime/reactI18next.ts +20 -0
  187. package/src/runtime/routerAdapter.tsx +6 -6
  188. package/src/runtime/utils.ts +13 -35
  189. package/src/server/apiPrefix.ts +67 -0
  190. package/src/server/detectorOptions.ts +105 -0
  191. package/src/server/index.ts +14 -319
  192. package/src/server/redirectPolicy.ts +127 -0
  193. package/src/shared/detection.ts +1 -1
  194. package/src/shared/localisedUrls/config.ts +18 -0
  195. package/src/shared/localisedUrls/index.ts +24 -0
  196. package/src/shared/localisedUrls/normalise.ts +67 -0
  197. package/src/shared/localisedUrls/pathname.ts +60 -0
  198. package/src/shared/localisedUrls/patterns.ts +160 -0
  199. package/src/shared/localisedUrls/redirect.ts +91 -0
  200. package/src/shared/localisedUrls/resolve.ts +135 -0
  201. package/src/shared/localisedUrls/routes.ts +226 -0
  202. package/src/shared/localisedUrls/types.ts +18 -0
  203. package/src/shared/localisedUrls.ts +26 -623
  204. package/src/shared/utils.ts +2 -2
  205. package/tests/fixtures/localised-routes.golden.json +156 -0
  206. package/tests/link.test.tsx +111 -0
  207. package/tests/linkTypes.test.ts +15 -4
  208. package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
  209. package/tests/localisedUrls.test.ts +129 -2
  210. package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
  211. package/tests/redirectPolicy.test.ts +43 -0
  212. package/tests/routerAdapter.test.tsx +85 -0
  213. package/tests/type-fixture/fixture-globals.d.ts +11 -0
  214. 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,18 @@ import {
8
7
  useMemo,
9
8
  } from 'react';
10
9
  import type { LocalisedUrlsOption } from '../shared/localisedUrls';
10
+ import {
11
+ cacheI18nLanguage,
12
+ changeModernI18nLanguage,
13
+ getPathLanguage,
14
+ isI18nLanguageSupported,
15
+ isI18nResourcesReady,
16
+ translateI18n,
17
+ } from './contextHelpers';
11
18
  import type { I18nInstance } from './i18n';
12
- import type { SdkBackend } from './i18n/backend/sdk-backend';
13
- import { cacheUserLanguage } from './i18n/detection';
14
19
  import { useI18nRouterAdapter } from './routerAdapter';
15
- import {
16
- buildLocalizedUrl,
17
- detectLanguageFromPath,
18
- getEntryPath,
19
- shouldIgnoreRedirect,
20
- } from './utils';
21
20
 
22
- export interface ModernI18nContextValue {
21
+ interface ModernI18nContextValue {
23
22
  language: string;
24
23
  i18nInstance: I18nInstance;
25
24
  // Plugin configuration for useModernI18n hook
@@ -51,7 +50,7 @@ const getModernI18nContext = () => {
51
50
 
52
51
  const ModernI18nContext = getModernI18nContext();
53
52
 
54
- export interface ModernI18nProviderProps {
53
+ interface ModernI18nProviderProps {
55
54
  children: ReactNode;
56
55
  value: ModernI18nContextValue;
57
56
  }
@@ -67,7 +66,7 @@ export const ModernI18nProvider: FC<ModernI18nProviderProps> = ({
67
66
  );
68
67
  };
69
68
 
70
- export interface UseModernI18nReturn {
69
+ interface UseModernI18nReturn {
71
70
  language: string;
72
71
  changeLanguage: (newLang: string) => Promise<void>;
73
72
  t: (key: string | string[], ...args: any[]) => string;
@@ -75,7 +74,7 @@ export interface UseModernI18nReturn {
75
74
  supportedLanguages: string[];
76
75
  localisedUrls?: LocalisedUrlsOption;
77
76
  isLanguageSupported: (lang: string) => boolean;
78
- // Indicates if translation resources for current language are ready to use
77
+ // Indicates whether translation resources for current language are ready
79
78
  isResourcesReady: boolean;
80
79
  }
81
80
 
@@ -85,9 +84,9 @@ export interface UseModernI18nReturn {
85
84
  * This hook provides:
86
85
  * - Current language from URL params or i18n context
87
86
  * - changeLanguage function that updates both i18n instance and URL
88
- * - Direct access to the i18n instance
87
+ * - Direct access to i18n instance
89
88
  * - List of supported languages
90
- * - Helper function to check if a language is supported
89
+ * - Helper function to check if language is supported
91
90
  *
92
91
  * @param options - Optional configuration to override context settings
93
92
  * @returns Object containing i18n functionality and utilities
@@ -95,7 +94,7 @@ export interface UseModernI18nReturn {
95
94
  export const useModernI18n = (): UseModernI18nReturn => {
96
95
  const context = useContext(ModernI18nContext);
97
96
  if (!context) {
98
- throw new Error('useModernI18n must be used within a ModernI18nProvider');
97
+ throw new Error('useModernI18n must be used within ModernI18nProvider');
99
98
  }
100
99
 
101
100
  const {
@@ -110,17 +109,10 @@ export const useModernI18n = (): UseModernI18nReturn => {
110
109
 
111
110
  const { navigate, location, hasRouter } = useI18nRouterAdapter();
112
111
 
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]);
112
+ const pathLanguage = useMemo(
113
+ () => getPathLanguage(location?.pathname, languages, localePathRedirect),
114
+ [languages, localePathRedirect, location?.pathname],
115
+ );
124
116
 
125
117
  const currentLanguage = pathLanguage || contextLanguage;
126
118
 
@@ -132,129 +124,32 @@ export const useModernI18n = (): UseModernI18nReturn => {
132
124
  updateLanguage?.(pathLanguage);
133
125
  i18nInstance?.setLang?.(pathLanguage);
134
126
  void i18nInstance?.changeLanguage?.(pathLanguage);
135
-
136
- if (isBrowser()) {
137
- const detectionOptions = i18nInstance.options?.detection;
138
- cacheUserLanguage(i18nInstance, pathLanguage, detectionOptions);
139
- }
127
+ cacheI18nLanguage(i18nInstance, pathLanguage);
140
128
  }, [contextLanguage, i18nInstance, pathLanguage, updateLanguage]);
141
129
 
142
130
  /**
143
- * Changes the current language and updates the URL accordingly.
131
+ * Changes the current language and updates URL accordingly.
144
132
  *
145
133
  * 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
134
+ * 1. Updates i18n instance language
135
+ * 2. Updates URL by replacing language prefix in the current path
136
+ * 3. Triggers navigation to the new URL
149
137
  *
150
138
  * @param newLang - The new language code to switch to
151
139
  */
152
140
  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
- },
141
+ (newLang: string) =>
142
+ changeModernI18nLanguage(newLang, {
143
+ i18nInstance,
144
+ updateLanguage,
145
+ localePathRedirect,
146
+ ignoreRedirectRoutes,
147
+ localisedUrls,
148
+ languages,
149
+ hasRouter,
150
+ navigate,
151
+ location,
152
+ }),
258
153
  [
259
154
  i18nInstance,
260
155
  updateLanguage,
@@ -269,72 +164,23 @@ export const useModernI18n = (): UseModernI18nReturn => {
269
164
  );
270
165
 
271
166
  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
- },
167
+ (key: string | string[], ...args: any[]) =>
168
+ translateI18n(i18nInstance, key, ...args),
279
169
  [currentLanguage, i18nInstance],
280
170
  );
281
171
 
282
- // Helper function to check if a language is supported
172
+ // Helper function to check if language is supported
283
173
  const isLanguageSupported = useCallback(
284
- (lang: string) => {
285
- return languages?.includes(lang) || false;
286
- },
174
+ (lang: string) => isI18nLanguageSupported(languages, lang),
287
175
  [languages],
288
176
  );
289
177
 
290
178
  // 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]);
179
+ // This checks if all required namespaces for current language are loaded
180
+ const isResourcesReady = useMemo(
181
+ () => isI18nResourcesReady(i18nInstance, currentLanguage),
182
+ [currentLanguage, i18nInstance],
183
+ );
338
184
 
339
185
  return {
340
186
  language: currentLanguage,
@@ -0,0 +1,232 @@
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
+ interface ChangeModernI18nLanguageOptions {
54
+ i18nInstance: I18nInstance;
55
+ updateLanguage?: (newLang: string) => void;
56
+ localePathRedirect?: boolean;
57
+ ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
58
+ localisedUrls?: LocalisedUrlsOption;
59
+ languages?: string[];
60
+ hasRouter: boolean;
61
+ navigate?: Navigate | null;
62
+ location?: LocationLike | null;
63
+ }
64
+
65
+ export async function changeModernI18nLanguage(
66
+ newLang: string,
67
+ options: ChangeModernI18nLanguageOptions,
68
+ ): Promise<void> {
69
+ const {
70
+ i18nInstance,
71
+ updateLanguage,
72
+ localePathRedirect,
73
+ ignoreRedirectRoutes,
74
+ localisedUrls,
75
+ languages,
76
+ hasRouter,
77
+ navigate,
78
+ location,
79
+ } = options;
80
+
81
+ try {
82
+ if (!newLang || typeof newLang !== 'string') {
83
+ throw new Error('Language must be non-empty string');
84
+ }
85
+
86
+ await i18nInstance?.setLang?.(newLang);
87
+ await i18nInstance?.changeLanguage?.(newLang);
88
+ cacheI18nLanguage(i18nInstance, newLang);
89
+
90
+ if (
91
+ localePathRedirect &&
92
+ isBrowser() &&
93
+ hasRouter &&
94
+ navigate &&
95
+ location
96
+ ) {
97
+ const currentPath = location.pathname;
98
+ const entryPath = getEntryPath();
99
+ const relativePath = currentPath.replace(entryPath, '');
100
+ const pathLanguage = detectLanguageFromPath(
101
+ currentPath,
102
+ languages || [],
103
+ localePathRedirect,
104
+ );
105
+
106
+ if (pathLanguage.detected && pathLanguage.language === newLang) {
107
+ updateLanguage?.(newLang);
108
+ return;
109
+ }
110
+
111
+ if (
112
+ !shouldIgnoreRedirect(
113
+ relativePath,
114
+ languages || [],
115
+ ignoreRedirectRoutes,
116
+ )
117
+ ) {
118
+ const newPath = buildLocalizedUrl(
119
+ relativePath,
120
+ newLang,
121
+ languages || [],
122
+ localisedUrls,
123
+ );
124
+ const newUrl = entryPath + newPath + location.search + location.hash;
125
+
126
+ await navigate(newUrl, { replace: true });
127
+ }
128
+ } else if (localePathRedirect && isBrowser() && !hasRouter) {
129
+ const currentPath = window.location.pathname;
130
+ const entryPath = getEntryPath();
131
+ const relativePath = currentPath.replace(entryPath, '');
132
+ const pathLanguage = detectLanguageFromPath(
133
+ currentPath,
134
+ languages || [],
135
+ localePathRedirect,
136
+ );
137
+
138
+ if (pathLanguage.detected && pathLanguage.language === newLang) {
139
+ updateLanguage?.(newLang);
140
+ return;
141
+ }
142
+
143
+ if (
144
+ !shouldIgnoreRedirect(
145
+ relativePath,
146
+ languages || [],
147
+ ignoreRedirectRoutes,
148
+ )
149
+ ) {
150
+ const newPath = buildLocalizedUrl(
151
+ relativePath,
152
+ newLang,
153
+ languages || [],
154
+ localisedUrls,
155
+ );
156
+ const newUrl =
157
+ entryPath + newPath + window.location.search + window.location.hash;
158
+
159
+ window.history.pushState(null, '', newUrl);
160
+ }
161
+ }
162
+
163
+ if (updateLanguage) {
164
+ updateLanguage(newLang);
165
+ }
166
+ } catch (error) {
167
+ console.error('Failed change language:', error);
168
+ throw error;
169
+ }
170
+ }
171
+
172
+ export function translateI18n(
173
+ i18nInstance: I18nInstance,
174
+ key: string | string[],
175
+ ...args: unknown[]
176
+ ): string {
177
+ if (typeof i18nInstance.t !== 'function') {
178
+ throw new Error('i18nInstance.t required');
179
+ }
180
+
181
+ return i18nInstance.t(key, ...args) as string;
182
+ }
183
+
184
+ export function isI18nLanguageSupported(
185
+ languages: string[] | undefined,
186
+ lang: string,
187
+ ): boolean {
188
+ return languages?.includes(lang) || false;
189
+ }
190
+
191
+ export function isI18nResourcesReady(
192
+ i18nInstance: I18nInstance,
193
+ currentLanguage: string,
194
+ ): boolean {
195
+ if (!i18nInstance?.isInitialized) {
196
+ return false;
197
+ }
198
+
199
+ const backend = i18nInstance?.services?.backend as SdkBackend | undefined;
200
+ if (backend && typeof backend.isLoading === 'function') {
201
+ const loadingResources = backend.getLoadingResources();
202
+ const isCurrentLanguageLoading = loadingResources.some(
203
+ ({ language }) => language === currentLanguage,
204
+ );
205
+ if (isCurrentLanguageLoading) {
206
+ return false;
207
+ }
208
+ }
209
+
210
+ const store = i18nInstance.store;
211
+ if (!store?.data) {
212
+ return false;
213
+ }
214
+
215
+ const langData = store.data[currentLanguage];
216
+ if (!langData || typeof langData !== 'object') {
217
+ return false;
218
+ }
219
+
220
+ const options = i18nInstance.options;
221
+ const namespaces = options?.ns || options?.defaultNS || ['translation'];
222
+ const requiredNamespaces = Array.isArray(namespaces)
223
+ ? namespaces
224
+ : [namespaces];
225
+
226
+ return requiredNamespaces.every(ns => {
227
+ const nsData = langData[ns];
228
+ return (
229
+ nsData && typeof nsData === 'object' && Object.keys(nsData).length > 0
230
+ );
231
+ });
232
+ }