@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,64 +1,27 @@
1
- import {
2
- isBrowser,
3
- RuntimeContext,
4
- type RuntimePlugin,
5
- } from '@modern-js/runtime';
6
- import { Helmet } from '@modern-js/runtime/head';
7
- import type { TInternalRuntimeContext } from '@modern-js/runtime/internal';
8
- import { merge } from '@modern-js/runtime-utils/merge';
1
+ import type { RuntimePlugin } from '@modern-js/runtime';
9
2
  import type React from 'react';
10
- import { useContext, useEffect, useMemo, useRef, useState } from 'react';
11
3
  import type {
12
4
  BaseBackendOptions,
13
5
  BaseLocaleDetectionOptions,
14
6
  } from '../shared/type';
15
- import { ModernI18nProvider } from './context';
16
- import {
17
- createContextValue,
18
- useClientSideRedirect,
19
- useLanguageSync,
20
- useSdkResourcesLoader,
21
- } from './hooks';
22
7
  import type { I18nInitOptions, I18nInstance } from './i18n';
23
- import { getI18nInstance } from './i18n';
24
- import { mergeBackendOptions } from './i18n/backend';
25
8
  import {
26
- detectLanguageWithPriority,
27
- exportServerLngToWindow,
28
- mergeDetectionOptions,
29
- } from './i18n/detection';
30
- import { useI18nextLanguageDetector } from './i18n/detection/middleware';
31
- import { getI18nextInstanceForProvider } from './i18n/instance';
32
- import { getPathname } from './utils';
9
+ type RuntimeContextWithI18n,
10
+ setupI18nBeforeRender,
11
+ } from './pluginSetup';
12
+ import { createI18nRootWrapper } from './providerComposition';
13
+ import {
14
+ type LoadReactI18nextIntegration,
15
+ resolveReactI18nextIntegration,
16
+ } from './reactI18next';
33
17
  import './types';
34
18
 
35
19
  export type { I18nSdkLoader, I18nSdkLoadOptions } from '../shared/type';
36
20
  export type { Resources } from './i18n/instance';
37
-
38
- type I18nLifecycleHelpers = {
39
- useI18nextBackend: typeof import('./i18n/backend/middleware')['useI18nextBackend'];
40
- changeI18nLanguage: typeof import('./i18n/utils')['changeI18nLanguage'];
41
- ensureLanguageMatch: typeof import('./i18n/utils')['ensureLanguageMatch'];
42
- initializeI18nInstance: typeof import('./i18n/utils')['initializeI18nInstance'];
43
- setupClonedInstance: typeof import('./i18n/utils')['setupClonedInstance'];
44
- };
45
-
46
- let i18nLifecycleHelpersPromise: Promise<I18nLifecycleHelpers> | undefined;
47
-
48
- function loadI18nLifecycleHelpers(): Promise<I18nLifecycleHelpers> {
49
- i18nLifecycleHelpersPromise ??= Promise.all([
50
- import('./i18n/backend/middleware'),
51
- import('./i18n/utils'),
52
- ]).then(([backendMiddleware, utils]) => ({
53
- useI18nextBackend: backendMiddleware.useI18nextBackend,
54
- changeI18nLanguage: utils.changeI18nLanguage,
55
- ensureLanguageMatch: utils.ensureLanguageMatch,
56
- initializeI18nInstance: utils.initializeI18nInstance,
57
- setupClonedInstance: utils.setupClonedInstance,
58
- }));
59
-
60
- return i18nLifecycleHelpersPromise;
61
- }
21
+ export type {
22
+ LoadReactI18nextIntegration,
23
+ ReactI18nextIntegration,
24
+ } from './reactI18next';
62
25
 
63
26
  export interface I18nPluginOptions {
64
27
  entryName?: string;
@@ -72,19 +35,6 @@ export interface I18nPluginOptions {
72
35
  [key: string]: any;
73
36
  }
74
37
 
75
- interface RuntimeContextWithI18n extends TInternalRuntimeContext {
76
- i18nInstance?: I18nInstance;
77
- changeLanguage?: (lang: string) => Promise<void>;
78
- }
79
-
80
- export interface ReactI18nextIntegration {
81
- I18nextProvider: React.ComponentType<any> | null;
82
- initReactI18next: any | null;
83
- }
84
-
85
- export type LoadReactI18nextIntegration =
86
- () => Promise<ReactI18nextIntegration | null>;
87
-
88
38
  export const createI18nPlugin =
89
39
  (
90
40
  loadReactI18nextIntegration?: LoadReactI18nextIntegration,
@@ -114,223 +64,42 @@ export const createI18nPlugin =
114
64
  let latestI18nInstance: I18nInstance | undefined;
115
65
  let I18nextProvider: React.ComponentType<any> | null;
116
66
 
117
- const resolveReactI18nextIntegration = async () => {
118
- if (!reactI18next) {
119
- return null;
120
- }
121
- return loadReactI18nextIntegration?.() ?? null;
122
- };
123
-
124
- api.onBeforeRender(async context => {
125
- const {
126
- useI18nextBackend,
127
- changeI18nLanguage,
128
- ensureLanguageMatch,
129
- initializeI18nInstance,
130
- setupClonedInstance,
131
- } = await loadI18nLifecycleHelpers();
132
- let i18nInstance = await getI18nInstance(userI18nInstance);
133
- const { i18n: otherConfig } = api.getRuntimeConfig();
134
- const { initOptions: otherInitOptions } = otherConfig || {};
135
- const userInitOptions = merge(
136
- otherInitOptions || {},
137
- initOptions || {},
138
- );
139
- const reactI18nextIntegration = await resolveReactI18nextIntegration();
140
- I18nextProvider = reactI18nextIntegration?.I18nextProvider ?? null;
141
- if (reactI18nextIntegration?.initReactI18next) {
142
- i18nInstance.use(reactI18nextIntegration.initReactI18next);
143
- }
144
-
145
- const pathname = getPathname(context);
146
-
147
- if (i18nextDetector) {
148
- useI18nextLanguageDetector(i18nInstance);
149
- }
150
-
151
- const mergedDetection = mergeDetectionOptions(
67
+ api.onBeforeRender(async (context: RuntimeContextWithI18n) => {
68
+ latestI18nInstance = await setupI18nBeforeRender(context, {
69
+ api,
70
+ userI18nInstance,
71
+ initOptions,
72
+ backend,
73
+ backendEnabled,
152
74
  i18nextDetector,
153
75
  detection,
154
76
  localePathRedirect,
155
- userInitOptions,
156
- );
157
- const mergedBackend = mergeBackendOptions(backend, userInitOptions);
158
-
159
- // Register Backend BEFORE detectLanguageWithPriority
160
- // This is critical because detectLanguageWithPriority may trigger init()
161
- // through i18next detector, and backend must be registered before init()
162
- // Register backend if:
163
- // 1. enabled is true (explicitly or auto-detected), OR
164
- // 2. SDK is configured (allows standalone SDK usage even without locales directory)
165
- const hasSdkConfig =
166
- typeof userInitOptions?.backend?.sdk === 'function' ||
167
- (mergedBackend?.sdk && typeof mergedBackend.sdk === 'function');
168
- if (mergedBackend && (backendEnabled || hasSdkConfig)) {
169
- useI18nextBackend(i18nInstance, mergedBackend);
170
- }
171
-
172
- const { finalLanguage } = await detectLanguageWithPriority(
173
- i18nInstance,
174
- {
175
- languages,
176
- fallbackLanguage,
177
- localePathRedirect,
178
- i18nextDetector,
179
- detection,
180
- userInitOptions,
181
- mergedBackend,
182
- pathname,
183
- ssrContext: context.ssrContext,
184
- },
185
- );
186
-
187
- await initializeI18nInstance(
188
- i18nInstance,
189
- finalLanguage,
190
- fallbackLanguage,
191
77
  languages,
192
- mergedDetection,
193
- mergedBackend,
194
- userInitOptions,
195
- );
196
-
197
- if (!isBrowser() && i18nInstance.cloneInstance) {
198
- i18nInstance = i18nInstance.cloneInstance();
199
- await setupClonedInstance(
200
- i18nInstance,
201
- finalLanguage,
202
- fallbackLanguage,
203
- languages,
204
- backendEnabled,
205
- backend,
206
- i18nextDetector,
207
- detection,
208
- localePathRedirect,
209
- userInitOptions,
210
- );
211
- }
212
-
213
- if (localePathRedirect) {
214
- await ensureLanguageMatch(i18nInstance, finalLanguage);
215
- }
216
-
217
- if (!isBrowser()) {
218
- exportServerLngToWindow(context, finalLanguage);
219
- }
220
- context.i18nInstance = i18nInstance;
221
- latestI18nInstance = i18nInstance;
222
-
223
- // Add changeLanguage method to context for other runtime plugins to use
224
- context.changeLanguage = async (newLang: string) => {
225
- await changeI18nLanguage(i18nInstance, newLang, {
226
- detectionOptions: mergedDetection,
227
- });
228
- };
78
+ fallbackLanguage,
79
+ resolveReactI18nextIntegration: () =>
80
+ resolveReactI18nextIntegration(
81
+ reactI18next,
82
+ loadReactI18nextIntegration,
83
+ ),
84
+ setI18nextProvider: provider => {
85
+ I18nextProvider = provider;
86
+ },
87
+ });
229
88
  });
230
89
 
231
- api.wrapRoot(App => {
232
- return props => {
233
- const runtimeContext = useContext(
234
- RuntimeContext,
235
- ) as RuntimeContextWithI18n;
236
- const i18nInstance =
237
- runtimeContext.i18nInstance || latestI18nInstance;
238
- const initialLang = useMemo(
239
- () =>
240
- i18nInstance?.language ||
241
- (localeDetection?.fallbackLanguage ?? 'en'),
242
- [i18nInstance?.language, localeDetection?.fallbackLanguage],
243
- );
244
- const [lang, setLang] = useState(initialLang);
245
- const [forceUpdate, setForceUpdate] = useState(0);
246
- const prevLangRef = useRef(lang);
247
- const runtimeContextRef = useRef(runtimeContext);
248
- runtimeContextRef.current = runtimeContext;
249
-
250
- useEffect(() => {
251
- if (i18nInstance?.language) {
252
- const translator = (i18nInstance as any).translator;
253
- if (translator) {
254
- translator.language = i18nInstance.language;
255
- }
256
- }
257
- }, [i18nInstance?.language]);
258
-
259
- useEffect(() => {
260
- prevLangRef.current = lang;
261
- }, [lang]);
262
-
263
- useSdkResourcesLoader(i18nInstance, setForceUpdate);
264
- useLanguageSync(
265
- i18nInstance,
266
- localePathRedirect,
267
- languages,
268
- runtimeContextRef,
269
- prevLangRef,
270
- setLang,
271
- );
272
- // Handle client-side redirect for static deployments
273
- // Note: This hook only executes in browser environment and skips SSR scenarios
274
- useClientSideRedirect(
275
- i18nInstance,
276
- localePathRedirect,
277
- languages,
278
- fallbackLanguage,
279
- ignoreRedirectRoutes,
280
- localisedUrls,
281
- );
282
-
283
- const contextValue = useMemo(
284
- () =>
285
- createContextValue(
286
- lang,
287
- i18nInstance,
288
- entryName,
289
- languages,
290
- localePathRedirect,
291
- ignoreRedirectRoutes,
292
- localisedUrls,
293
- setLang,
294
- ),
295
- [
296
- lang,
297
- i18nInstance,
298
- entryName,
299
- languages,
300
- localePathRedirect,
301
- ignoreRedirectRoutes,
302
- localisedUrls,
303
- forceUpdate,
304
- ],
305
- );
306
-
307
- const children = (props as React.PropsWithChildren).children;
308
- const appContent = (
309
- <>
310
- {Boolean(htmlLangAttr) && <Helmet htmlAttributes={{ lang }} />}
311
- <ModernI18nProvider value={contextValue}>
312
- {App ? <App {...props}>{children}</App> : children}
313
- </ModernI18nProvider>
314
- </>
315
- );
316
-
317
- if (!i18nInstance) {
318
- return appContent;
319
- }
320
-
321
- if (I18nextProvider) {
322
- const i18nextInstanceForProvider =
323
- getI18nextInstanceForProvider(i18nInstance);
324
- return (
325
- <I18nextProvider i18n={i18nextInstanceForProvider}>
326
- {appContent}
327
- </I18nextProvider>
328
- );
329
- }
330
-
331
- return appContent;
332
- };
333
- });
90
+ api.wrapRoot(
91
+ createI18nRootWrapper({
92
+ entryName,
93
+ htmlLangAttr,
94
+ localePathRedirect,
95
+ languages,
96
+ fallbackLanguage,
97
+ ignoreRedirectRoutes,
98
+ localisedUrls,
99
+ getLatestI18nInstance: () => latestI18nInstance,
100
+ getI18nextProvider: () => I18nextProvider,
101
+ }),
102
+ );
334
103
  },
335
104
  });
336
105
 
@@ -339,7 +108,11 @@ export type {
339
108
  CanonicalRoutePath,
340
109
  UltramodernCanonicalRoutes,
341
110
  } from './canonicalRoutes';
342
- export { useModernI18n } from './context';
111
+ export {
112
+ FederatedI18nBoundary,
113
+ type FederatedI18nBoundaryProps,
114
+ useModernI18n,
115
+ } from './context';
343
116
  export { I18nLink, type I18nLinkProps } from './I18nLink';
344
117
  export {
345
118
  Link,
@@ -1,27 +1,32 @@
1
- import type { TRuntimeContext } from '@modern-js/runtime';
2
1
  import { isBrowser } from '@modern-js/runtime';
3
2
  import type React from 'react';
4
- import { useEffect, useRef } from 'react';
3
+ import { useCallback, useEffect, useRef } from 'react';
5
4
  import type { LocalisedUrlsOption } from '../shared/localisedUrls';
5
+ import {
6
+ cacheI18nLanguage,
7
+ changeI18nInstanceLanguage,
8
+ } from './contextHelpers';
6
9
  import type { I18nInstance } from './i18n';
7
10
  import {
8
11
  getI18nSdkBackendId,
9
12
  I18N_SDK_RESOURCES_LOADED_EVENT,
10
13
  type I18nSdkResourcesLoadedEventDetail,
11
14
  } from './i18n/backend/sdk-event';
12
- import { cacheUserLanguage } from './i18n/detection';
13
15
  import { useI18nRouterAdapter } from './routerAdapter';
14
16
  import {
15
17
  buildLocalizedUrl,
16
18
  detectLanguageFromPath,
17
19
  getEntryPath,
18
- getPathname,
19
20
  shouldIgnoreRedirect,
20
21
  } from './utils';
21
22
 
22
- interface RuntimeContextWithI18n extends TRuntimeContext {
23
- i18nInstance?: I18nInstance;
24
- }
23
+ type WindowWithSSRData = Window & {
24
+ _SSR_DATA?: unknown;
25
+ };
26
+
27
+ type ResourceStoreWithEvents = NonNullable<I18nInstance['store']> & {
28
+ emit?: (event: string, ...args: unknown[]) => void;
29
+ };
25
30
 
26
31
  function createMinimalI18nInstance(language: string): I18nInstance {
27
32
  const minimalInstance: I18nInstance = {
@@ -44,6 +49,7 @@ export function createContextValue(
44
49
  ignoreRedirectRoutes: string[] | ((pathname: string) => boolean) | undefined,
45
50
  localisedUrls: LocalisedUrlsOption | undefined,
46
51
  setLang: (lang: string) => void,
52
+ synchronizeLanguage: (lang: string) => void,
47
53
  ) {
48
54
  const instance = i18nInstance || createMinimalI18nInstance(lang);
49
55
  return {
@@ -55,6 +61,7 @@ export function createContextValue(
55
61
  ignoreRedirectRoutes,
56
62
  localisedUrls,
57
63
  updateLanguage: setLang,
64
+ synchronizeLanguage,
58
65
  };
59
66
  }
60
67
 
@@ -94,7 +101,7 @@ export function useSdkResourcesLoader(
94
101
  }
95
102
 
96
103
  const triggerUpdate = (retryCount = 0) => {
97
- const store = (i18nInstance as any).store;
104
+ const store = i18nInstance.store as ResourceStoreWithEvents | undefined;
98
105
  const hasResource = store?.data?.[language]?.[namespace];
99
106
 
100
107
  if (hasResource || retryCount >= 10) {
@@ -179,7 +186,7 @@ export function useClientSideRedirect(
179
186
  }
180
187
 
181
188
  try {
182
- const ssrData = (window as any)._SSR_DATA;
189
+ const ssrData = (window as WindowWithSSRData)._SSR_DATA;
183
190
  if (ssrData) {
184
191
  return;
185
192
  }
@@ -260,36 +267,88 @@ export function useLanguageSync(
260
267
  i18nInstance: I18nInstance | undefined,
261
268
  localePathRedirect: boolean,
262
269
  languages: string[],
263
- runtimeContextRef: React.MutableRefObject<RuntimeContextWithI18n>,
270
+ pathname: string | undefined,
264
271
  prevLangRef: React.MutableRefObject<string>,
265
272
  setLang: (lang: string) => void,
266
273
  ) {
274
+ const latestRequestRef = useRef(0);
275
+ const syncQueueRef = useRef(Promise.resolve());
276
+ const isMountedRef = useRef(false);
277
+ const desiredLanguageRef = useRef<string | undefined>(undefined);
278
+
279
+ useEffect(() => {
280
+ isMountedRef.current = true;
281
+ return () => {
282
+ isMountedRef.current = false;
283
+ latestRequestRef.current += 1;
284
+ };
285
+ }, []);
286
+
287
+ useEffect(() => {
288
+ desiredLanguageRef.current = undefined;
289
+ latestRequestRef.current += 1;
290
+ syncQueueRef.current = Promise.resolve();
291
+ }, [i18nInstance]);
292
+
293
+ const synchronizeLanguage = useCallback(
294
+ (currentLang: string) => {
295
+ if (
296
+ !i18nInstance ||
297
+ !currentLang ||
298
+ desiredLanguageRef.current === currentLang
299
+ ) {
300
+ return;
301
+ }
302
+
303
+ desiredLanguageRef.current = currentLang;
304
+ const requestId = ++latestRequestRef.current;
305
+ syncQueueRef.current = syncQueueRef.current.then(async () => {
306
+ if (requestId !== latestRequestRef.current || !isMountedRef.current) {
307
+ return;
308
+ }
309
+
310
+ try {
311
+ if (i18nInstance.language !== currentLang) {
312
+ await changeI18nInstanceLanguage(i18nInstance, currentLang);
313
+ }
314
+
315
+ if (requestId !== latestRequestRef.current || !isMountedRef.current) {
316
+ return;
317
+ }
318
+
319
+ prevLangRef.current = currentLang;
320
+ setLang(currentLang);
321
+ cacheI18nLanguage(i18nInstance, currentLang);
322
+ } catch (error) {
323
+ if (requestId === latestRequestRef.current && isMountedRef.current) {
324
+ desiredLanguageRef.current = undefined;
325
+ console.error(
326
+ `Failed to synchronize i18n language "${currentLang}".`,
327
+ error,
328
+ );
329
+ }
330
+ }
331
+ });
332
+ },
333
+ [i18nInstance, prevLangRef, setLang],
334
+ );
335
+
267
336
  useEffect(() => {
268
337
  if (!i18nInstance) {
269
338
  return;
270
339
  }
271
340
 
272
341
  if (localePathRedirect) {
273
- const currentPathname = getPathname(runtimeContextRef.current);
274
342
  const pathDetection = detectLanguageFromPath(
275
- currentPathname,
343
+ pathname || '',
276
344
  languages,
277
345
  localePathRedirect,
278
346
  );
279
347
  if (pathDetection.detected && pathDetection.language) {
280
- const currentLang = pathDetection.language;
281
- if (currentLang !== prevLangRef.current) {
282
- prevLangRef.current = currentLang;
283
- setLang(currentLang);
284
- i18nInstance.setLang?.(currentLang);
285
- i18nInstance.changeLanguage?.(currentLang);
286
- if (isBrowser()) {
287
- const detectionOptions = i18nInstance.options?.detection;
288
- cacheUserLanguage(i18nInstance, currentLang, detectionOptions);
289
- }
290
- }
348
+ synchronizeLanguage(pathDetection.language);
291
349
  }
292
350
  } else {
351
+ latestRequestRef.current += 1;
293
352
  const instanceLang = i18nInstance.language;
294
353
  if (instanceLang && instanceLang !== prevLangRef.current) {
295
354
  prevLangRef.current = instanceLang;
@@ -300,8 +359,11 @@ export function useLanguageSync(
300
359
  i18nInstance,
301
360
  localePathRedirect,
302
361
  languages,
303
- runtimeContextRef,
362
+ pathname,
304
363
  prevLangRef,
305
364
  setLang,
365
+ synchronizeLanguage,
306
366
  ]);
367
+
368
+ return synchronizeLanguage;
307
369
  }
@@ -1,5 +1,5 @@
1
- import fs from 'fs';
2
- import nodePath from 'path';
1
+ import fs from 'node:fs';
2
+ import nodePath from 'node:path';
3
3
 
4
4
  /**
5
5
  * Conventional locales roots, in the same priority order as the CLI plugin's
@@ -9,6 +9,7 @@ import { SdkBackend } from './sdk-backend';
9
9
 
10
10
  type BackendConfigWithChained = BaseBackendOptions &
11
11
  Partial<ChainedBackendConfig>;
12
+ type BackendConstructor = new (...args: never[]) => unknown;
12
13
 
13
14
  function checkBackendConfig(backend?: BackendConfigWithChained) {
14
15
  const hasSdk = backend?.sdk && typeof backend.sdk === 'function';
@@ -20,7 +21,7 @@ function checkBackendConfig(backend?: BackendConfigWithChained) {
20
21
 
21
22
  function buildChainedBackendConfig(
22
23
  backend: BackendConfigWithChained,
23
- BackendWithSave: new (...args: any[]) => any,
24
+ BackendWithSave: BackendConstructor,
24
25
  ) {
25
26
  const cacheHitMode = backend.cacheHitMode || 'refreshAndUpdateStore';
26
27
 
@@ -51,7 +52,7 @@ function buildChainedBackendConfig(
51
52
  function setupChainedBackend(
52
53
  i18nInstance: I18nInstance,
53
54
  backend: BackendConfigWithChained,
54
- BackendWithSave: new (...args: any[]) => any,
55
+ BackendWithSave: BackendConstructor,
55
56
  ) {
56
57
  i18nInstance.use(ChainedBackend);
57
58
  const actualInstance = getActualI18nextInstance(i18nInstance);
@@ -86,8 +87,8 @@ function cleanBackendConfig(backend: BackendConfigWithChained) {
86
87
  */
87
88
  export function useI18nextBackendCommon(
88
89
  i18nInstance: I18nInstance,
89
- BackendWithSave: new (...args: any[]) => any,
90
- BackendBase: new (...args: any[]) => any,
90
+ BackendWithSave: BackendConstructor,
91
+ BackendBase: BackendConstructor,
91
92
  backend?: BackendConfigWithChained,
92
93
  ) {
93
94
  if (!backend) {