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

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,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
 
@@ -21,8 +21,19 @@ import {
21
21
 
22
22
  interface RuntimeContextWithI18n extends TRuntimeContext {
23
23
  i18nInstance?: I18nInstance;
24
+ // Present when the context actually is the internal runtime context;
25
+ // getPathname only ever reads this on the server.
26
+ ssrContext?: { request?: { pathname?: string } };
24
27
  }
25
28
 
29
+ type WindowWithSSRData = Window & {
30
+ _SSR_DATA?: unknown;
31
+ };
32
+
33
+ type ResourceStoreWithEvents = NonNullable<I18nInstance['store']> & {
34
+ emit?: (event: string, ...args: unknown[]) => void;
35
+ };
36
+
26
37
  function createMinimalI18nInstance(language: string): I18nInstance {
27
38
  const minimalInstance: I18nInstance = {
28
39
  language,
@@ -94,7 +105,7 @@ export function useSdkResourcesLoader(
94
105
  }
95
106
 
96
107
  const triggerUpdate = (retryCount = 0) => {
97
- const store = (i18nInstance as any).store;
108
+ const store = i18nInstance.store as ResourceStoreWithEvents | undefined;
98
109
  const hasResource = store?.data?.[language]?.[namespace];
99
110
 
100
111
  if (hasResource || retryCount >= 10) {
@@ -179,7 +190,7 @@ export function useClientSideRedirect(
179
190
  }
180
191
 
181
192
  try {
182
- const ssrData = (window as any)._SSR_DATA;
193
+ const ssrData = (window as WindowWithSSRData)._SSR_DATA;
183
194
  if (ssrData) {
184
195
  return;
185
196
  }
@@ -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) {
@@ -0,0 +1,189 @@
1
+ import type {
2
+ I18nInitOptions,
3
+ I18nInstance,
4
+ LanguageDetectorOptions,
5
+ } from '../instance';
6
+
7
+ interface DetectorCacheEntry {
8
+ instance: I18nInstance;
9
+ isTemporary: boolean;
10
+ configKey: string;
11
+ }
12
+
13
+ type DetectorCleanupInstance = I18nInstance & {
14
+ removeAllListeners?: () => void;
15
+ off?: (event: string) => void;
16
+ services?: I18nInstance['services'] & {
17
+ backendConnector?: {
18
+ backend?: {
19
+ stop?: () => void;
20
+ close?: () => void;
21
+ };
22
+ };
23
+ };
24
+ };
25
+
26
+ export const detectorInstanceCache = new WeakMap<
27
+ I18nInstance,
28
+ DetectorCacheEntry
29
+ >();
30
+
31
+ const DETECTOR_SAFE_OPTION_KEYS: string[] = [
32
+ 'lowerCaseLng',
33
+ 'nonExplicitSupportedLngs',
34
+ 'load',
35
+ 'partialBundledLanguages',
36
+ 'returnNull',
37
+ 'returnEmptyString',
38
+ 'returnObjects',
39
+ 'joinArrays',
40
+ 'keySeparator',
41
+ 'nsSeparator',
42
+ 'pluralSeparator',
43
+ 'contextSeparator',
44
+ 'fallbackNS',
45
+ 'ns',
46
+ 'defaultNS',
47
+ 'debug',
48
+ ];
49
+
50
+ /**
51
+ * Stable stringify that sorts object keys to ensure consistent output
52
+ * regardless of property order
53
+ */
54
+ const stableStringify = (value: unknown): string => {
55
+ if (value === null || value === undefined) {
56
+ return JSON.stringify(value);
57
+ }
58
+
59
+ if (typeof value !== 'object') {
60
+ return JSON.stringify(value);
61
+ }
62
+
63
+ if (Array.isArray(value)) {
64
+ // Arrays maintain their order
65
+ return `[${value.map(item => stableStringify(item)).join(',')}]`;
66
+ }
67
+
68
+ // For objects, sort keys and recursively stringify values
69
+ const record = value as Record<string, unknown>;
70
+ const sortedKeys = Object.keys(record).sort();
71
+ const sortedEntries = sortedKeys.map(key => {
72
+ const stringifiedValue = stableStringify(record[key]);
73
+ return `${JSON.stringify(key)}:${stringifiedValue}`;
74
+ });
75
+
76
+ return `{${sortedEntries.join(',')}}`;
77
+ };
78
+
79
+ export const buildDetectorConfigKey = (
80
+ languages: string[],
81
+ fallbackLanguage: string,
82
+ mergedDetection: LanguageDetectorOptions,
83
+ ): string => {
84
+ return stableStringify({
85
+ languages,
86
+ fallbackLanguage,
87
+ detection: mergedDetection,
88
+ });
89
+ };
90
+
91
+ export const pickSafeDetectionOptions = (
92
+ userInitOptions?: I18nInitOptions,
93
+ ): Partial<I18nInitOptions> & Record<string, unknown> => {
94
+ if (!userInitOptions) {
95
+ return {};
96
+ }
97
+ const safeOptions: Partial<I18nInitOptions> & Record<string, unknown> = {};
98
+ for (const key of DETECTOR_SAFE_OPTION_KEYS) {
99
+ const value = (userInitOptions as Record<string, unknown>)[key];
100
+ if (value !== undefined) {
101
+ safeOptions[key] = value;
102
+ }
103
+ }
104
+ if (userInitOptions.interpolation) {
105
+ safeOptions.interpolation = { ...userInitOptions.interpolation };
106
+ }
107
+ return safeOptions;
108
+ };
109
+
110
+ const cleanupDetectorCacheEntry = (entry?: DetectorCacheEntry) => {
111
+ if (!entry || !entry.isTemporary) {
112
+ return;
113
+ }
114
+ const instance = entry.instance as DetectorCleanupInstance;
115
+ try {
116
+ instance?.removeAllListeners?.();
117
+ } catch (error) {
118
+ void error;
119
+ }
120
+ try {
121
+ instance?.off?.('*');
122
+ } catch (error) {
123
+ void error;
124
+ }
125
+ try {
126
+ instance?.services?.backendConnector?.backend?.stop?.();
127
+ } catch (error) {
128
+ void error;
129
+ }
130
+ try {
131
+ instance?.services?.backendConnector?.backend?.close?.();
132
+ } catch (error) {
133
+ void error;
134
+ }
135
+ };
136
+
137
+ export const createDetectorInstance = (
138
+ baseInstance: I18nInstance,
139
+ configKey: string,
140
+ ): { instance: I18nInstance; isTemporary: boolean } => {
141
+ const cached = detectorInstanceCache.get(baseInstance);
142
+ if (cached && cached.configKey === configKey) {
143
+ return { instance: cached.instance, isTemporary: cached.isTemporary };
144
+ }
145
+
146
+ if (cached) {
147
+ cleanupDetectorCacheEntry(cached);
148
+ detectorInstanceCache.delete(baseInstance);
149
+ }
150
+
151
+ const createNewInstance = (): {
152
+ instance: I18nInstance;
153
+ isTemporary: boolean;
154
+ } => {
155
+ if (typeof baseInstance.createInstance === 'function') {
156
+ try {
157
+ const created = baseInstance.createInstance();
158
+ if (created) {
159
+ return { instance: created, isTemporary: true };
160
+ }
161
+ } catch (error) {
162
+ void error;
163
+ }
164
+ }
165
+
166
+ if (typeof baseInstance.cloneInstance === 'function') {
167
+ try {
168
+ const cloned = baseInstance.cloneInstance();
169
+ if (cloned) {
170
+ return { instance: cloned, isTemporary: true };
171
+ }
172
+ } catch (error) {
173
+ void error;
174
+ }
175
+ }
176
+
177
+ return { instance: baseInstance, isTemporary: false };
178
+ };
179
+
180
+ const created = createNewInstance();
181
+ if (created.isTemporary) {
182
+ detectorInstanceCache.set(baseInstance, {
183
+ instance: created.instance,
184
+ isTemporary: true,
185
+ configKey,
186
+ });
187
+ }
188
+ return created;
189
+ };