@modern-js/plugin-i18n 2.69.5 → 3.0.0-alpha.0

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 (151) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/cli/index.cjs +154 -0
  3. package/dist/cjs/runtime/I18nLink.cjs +68 -0
  4. package/dist/cjs/runtime/context.cjs +138 -0
  5. package/dist/cjs/runtime/hooks.cjs +189 -0
  6. package/dist/cjs/runtime/i18n/backend/config.cjs +39 -0
  7. package/dist/cjs/runtime/i18n/backend/defaults.cjs +56 -0
  8. package/dist/cjs/runtime/i18n/backend/defaults.node.cjs +56 -0
  9. package/dist/cjs/runtime/i18n/backend/index.cjs +108 -0
  10. package/dist/cjs/runtime/i18n/backend/middleware.cjs +54 -0
  11. package/dist/cjs/runtime/i18n/backend/middleware.common.cjs +105 -0
  12. package/dist/cjs/runtime/i18n/backend/middleware.node.cjs +58 -0
  13. package/dist/cjs/runtime/i18n/backend/sdk-backend.cjs +171 -0
  14. package/dist/cjs/runtime/i18n/detection/config.cjs +63 -0
  15. package/dist/cjs/runtime/i18n/detection/index.cjs +309 -0
  16. package/dist/cjs/runtime/i18n/detection/middleware.cjs +185 -0
  17. package/dist/cjs/runtime/i18n/detection/middleware.node.cjs +74 -0
  18. package/dist/cjs/runtime/i18n/index.cjs +43 -0
  19. package/dist/cjs/runtime/i18n/instance.cjs +132 -0
  20. package/dist/cjs/runtime/i18n/utils.cjs +185 -0
  21. package/dist/cjs/runtime/index.cjs +162 -0
  22. package/dist/cjs/runtime/types.cjs +18 -0
  23. package/dist/cjs/runtime/utils.cjs +134 -0
  24. package/dist/cjs/server/index.cjs +178 -0
  25. package/dist/cjs/shared/deepMerge.cjs +54 -0
  26. package/dist/cjs/shared/detection.cjs +105 -0
  27. package/dist/cjs/shared/type.cjs +18 -0
  28. package/dist/cjs/shared/utils.cjs +78 -0
  29. package/dist/esm/cli/index.js +106 -0
  30. package/dist/esm/runtime/I18nLink.js +31 -0
  31. package/dist/esm/runtime/context.js +101 -0
  32. package/dist/esm/runtime/hooks.js +146 -0
  33. package/dist/esm/runtime/i18n/backend/config.js +5 -0
  34. package/dist/esm/runtime/i18n/backend/defaults.js +19 -0
  35. package/dist/esm/runtime/i18n/backend/defaults.node.js +19 -0
  36. package/dist/esm/runtime/i18n/backend/index.js +74 -0
  37. package/dist/esm/runtime/i18n/backend/middleware.common.js +61 -0
  38. package/dist/esm/runtime/i18n/backend/middleware.js +7 -0
  39. package/dist/esm/runtime/i18n/backend/middleware.node.js +8 -0
  40. package/dist/esm/runtime/i18n/backend/sdk-backend.js +137 -0
  41. package/dist/esm/runtime/i18n/detection/config.js +26 -0
  42. package/dist/esm/runtime/i18n/detection/index.js +260 -0
  43. package/dist/esm/runtime/i18n/detection/middleware.js +132 -0
  44. package/dist/esm/runtime/i18n/detection/middleware.node.js +31 -0
  45. package/dist/esm/runtime/i18n/index.js +3 -0
  46. package/dist/esm/runtime/i18n/instance.js +77 -0
  47. package/dist/esm/runtime/i18n/utils.js +136 -0
  48. package/dist/esm/runtime/index.js +119 -0
  49. package/dist/esm/runtime/types.js +0 -0
  50. package/dist/esm/runtime/utils.js +82 -0
  51. package/dist/esm/server/index.js +168 -0
  52. package/dist/esm/shared/deepMerge.js +20 -0
  53. package/dist/esm/shared/detection.js +71 -0
  54. package/dist/esm/shared/type.js +0 -0
  55. package/dist/esm/shared/utils.js +35 -0
  56. package/dist/esm-node/cli/index.js +106 -0
  57. package/dist/esm-node/runtime/I18nLink.js +31 -0
  58. package/dist/esm-node/runtime/context.js +101 -0
  59. package/dist/esm-node/runtime/hooks.js +146 -0
  60. package/dist/esm-node/runtime/i18n/backend/config.js +5 -0
  61. package/dist/esm-node/runtime/i18n/backend/defaults.js +19 -0
  62. package/dist/esm-node/runtime/i18n/backend/defaults.node.js +19 -0
  63. package/dist/esm-node/runtime/i18n/backend/index.js +74 -0
  64. package/dist/esm-node/runtime/i18n/backend/middleware.common.js +61 -0
  65. package/dist/esm-node/runtime/i18n/backend/middleware.js +7 -0
  66. package/dist/esm-node/runtime/i18n/backend/middleware.node.js +8 -0
  67. package/dist/esm-node/runtime/i18n/backend/sdk-backend.js +137 -0
  68. package/dist/esm-node/runtime/i18n/detection/config.js +26 -0
  69. package/dist/esm-node/runtime/i18n/detection/index.js +260 -0
  70. package/dist/esm-node/runtime/i18n/detection/middleware.js +132 -0
  71. package/dist/esm-node/runtime/i18n/detection/middleware.node.js +31 -0
  72. package/dist/esm-node/runtime/i18n/index.js +3 -0
  73. package/dist/esm-node/runtime/i18n/instance.js +77 -0
  74. package/dist/esm-node/runtime/i18n/utils.js +136 -0
  75. package/dist/esm-node/runtime/index.js +119 -0
  76. package/dist/esm-node/runtime/types.js +0 -0
  77. package/dist/esm-node/runtime/utils.js +82 -0
  78. package/dist/esm-node/server/index.js +168 -0
  79. package/dist/esm-node/shared/deepMerge.js +20 -0
  80. package/dist/esm-node/shared/detection.js +71 -0
  81. package/dist/esm-node/shared/type.js +0 -0
  82. package/dist/esm-node/shared/utils.js +35 -0
  83. package/dist/types/cli/index.d.ts +21 -0
  84. package/dist/types/runtime/I18nLink.d.ts +8 -0
  85. package/dist/types/runtime/context.d.ts +38 -0
  86. package/dist/types/runtime/hooks.d.ts +28 -0
  87. package/dist/types/runtime/i18n/backend/config.d.ts +2 -0
  88. package/dist/types/runtime/i18n/backend/defaults.d.ts +13 -0
  89. package/dist/types/runtime/i18n/backend/defaults.node.d.ts +8 -0
  90. package/dist/types/runtime/i18n/backend/index.d.ts +3 -0
  91. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +14 -0
  92. package/dist/types/runtime/i18n/backend/middleware.d.ts +12 -0
  93. package/dist/types/runtime/i18n/backend/middleware.node.d.ts +13 -0
  94. package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +52 -0
  95. package/dist/types/runtime/i18n/detection/config.d.ts +11 -0
  96. package/dist/types/runtime/i18n/detection/index.d.ts +50 -0
  97. package/dist/types/runtime/i18n/detection/middleware.d.ts +24 -0
  98. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +17 -0
  99. package/dist/types/runtime/i18n/index.d.ts +3 -0
  100. package/dist/types/runtime/i18n/instance.d.ts +93 -0
  101. package/dist/types/runtime/i18n/utils.d.ts +29 -0
  102. package/dist/types/runtime/index.d.ts +19 -0
  103. package/dist/types/runtime/types.d.ts +15 -0
  104. package/dist/types/runtime/utils.d.ts +33 -0
  105. package/dist/types/server/index.d.ts +8 -0
  106. package/dist/types/shared/deepMerge.d.ts +1 -0
  107. package/dist/types/shared/detection.d.ts +11 -0
  108. package/dist/types/shared/type.d.ts +156 -0
  109. package/dist/types/shared/utils.d.ts +5 -0
  110. package/package.json +100 -34
  111. package/rslib.config.mts +4 -0
  112. package/src/cli/index.ts +245 -0
  113. package/src/runtime/I18nLink.tsx +76 -0
  114. package/src/runtime/context.tsx +256 -0
  115. package/src/runtime/hooks.ts +274 -0
  116. package/src/runtime/i18n/backend/config.ts +10 -0
  117. package/src/runtime/i18n/backend/defaults.node.ts +31 -0
  118. package/src/runtime/i18n/backend/defaults.ts +37 -0
  119. package/src/runtime/i18n/backend/index.ts +181 -0
  120. package/src/runtime/i18n/backend/middleware.common.ts +116 -0
  121. package/src/runtime/i18n/backend/middleware.node.ts +32 -0
  122. package/src/runtime/i18n/backend/middleware.ts +28 -0
  123. package/src/runtime/i18n/backend/sdk-backend.ts +292 -0
  124. package/src/runtime/i18n/detection/config.ts +32 -0
  125. package/src/runtime/i18n/detection/index.ts +641 -0
  126. package/src/runtime/i18n/detection/middleware.node.ts +84 -0
  127. package/src/runtime/i18n/detection/middleware.ts +251 -0
  128. package/src/runtime/i18n/index.ts +8 -0
  129. package/src/runtime/i18n/instance.ts +227 -0
  130. package/src/runtime/i18n/utils.ts +333 -0
  131. package/src/runtime/index.tsx +275 -0
  132. package/src/runtime/types.ts +17 -0
  133. package/src/runtime/utils.ts +151 -0
  134. package/src/server/index.ts +336 -0
  135. package/src/shared/deepMerge.ts +38 -0
  136. package/src/shared/detection.ts +131 -0
  137. package/src/shared/type.ts +170 -0
  138. package/src/shared/utils.ts +82 -0
  139. package/tsconfig.json +12 -0
  140. package/dist/cjs/index.js +0 -73
  141. package/dist/cjs/languageDetector.js +0 -51
  142. package/dist/cjs/utils/index.js +0 -39
  143. package/dist/esm/index.js +0 -61
  144. package/dist/esm/languageDetector.js +0 -33
  145. package/dist/esm/utils/index.js +0 -16
  146. package/dist/esm-node/index.js +0 -49
  147. package/dist/esm-node/languageDetector.js +0 -26
  148. package/dist/esm-node/utils/index.js +0 -15
  149. package/dist/types/index.d.ts +0 -34
  150. package/dist/types/languageDetector.d.ts +0 -6
  151. package/dist/types/utils/index.d.ts +0 -5
@@ -0,0 +1,333 @@
1
+ import { isBrowser } from '@modern-js/runtime';
2
+ import type { BaseBackendOptions } from '../../shared/type';
3
+ import { mergeBackendOptions } from './backend';
4
+ import { HttpBackendWithSave } from './backend/middleware';
5
+ import { useI18nextBackend } from './backend/middleware';
6
+ import { SdkBackend } from './backend/sdk-backend';
7
+ import { cacheUserLanguage } from './detection';
8
+ import { mergeDetectionOptions } from './detection';
9
+ import type { I18nInitOptions, I18nInstance } from './instance';
10
+ import {
11
+ getActualI18nextInstance,
12
+ isI18nInstance,
13
+ isI18nWrapperInstance,
14
+ } from './instance';
15
+
16
+ export function assertI18nInstance(obj: any): asserts obj is I18nInstance {
17
+ if (!isI18nInstance(obj)) {
18
+ throw new Error('Object does not implement I18nInstance interface');
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Build initialization options for i18n instance
24
+ */
25
+ export const buildInitOptions = async (
26
+ finalLanguage: string,
27
+ fallbackLanguage: string,
28
+ languages: string[],
29
+ mergedDetection: any,
30
+ mergedBackend: any,
31
+ userInitOptions?: I18nInitOptions,
32
+ useSuspense?: boolean,
33
+ i18nInstance?: I18nInstance,
34
+ ): Promise<I18nInitOptions> => {
35
+ const defaultUseSuspense =
36
+ useSuspense !== undefined
37
+ ? useSuspense
38
+ : isBrowser()
39
+ ? (userInitOptions?.react?.useSuspense ?? true)
40
+ : false;
41
+
42
+ // If backend is already configured via useI18nextBackend (has _useChainedBackend),
43
+ // we need to pass the chained backend config to init() so it can initialize properly
44
+ const isChainedBackend = !!mergedBackend?._useChainedBackend;
45
+
46
+ // If using chained backend, we need to pass the backend config to init()
47
+ // but exclude it from userInitOptions to avoid conflicts
48
+ // For non-chained backend, we also exclude it to ensure mergedBackend is used
49
+ const sanitizedUserInitOptions = userInitOptions
50
+ ? { ...userInitOptions, backend: undefined }
51
+ : undefined;
52
+
53
+ // Build base initOptions first, excluding backend to set it separately
54
+ const { backend: _removedBackend, ...userOptionsWithoutBackend } =
55
+ sanitizedUserInitOptions || {};
56
+
57
+ const initOptions: I18nInitOptions = {
58
+ lng: finalLanguage,
59
+ fallbackLng: fallbackLanguage,
60
+ supportedLngs: languages,
61
+ detection: mergedDetection,
62
+ initImmediate: sanitizedUserInitOptions?.initImmediate ?? true,
63
+ interpolation: {
64
+ ...(sanitizedUserInitOptions?.interpolation || {}),
65
+ escapeValue:
66
+ sanitizedUserInitOptions?.interpolation?.escapeValue ?? false,
67
+ },
68
+ react: {
69
+ ...(sanitizedUserInitOptions?.react || {}),
70
+ useSuspense: defaultUseSuspense,
71
+ },
72
+ // Spread user options (without backend) to allow user options to override
73
+ ...userOptionsWithoutBackend,
74
+ };
75
+
76
+ // For chained backend, we need to pass the backend config to init()
77
+ // The backend classes (Backend, SdkBackend) are already set via useI18nextBackend
78
+ // but we need to pass the complete chained backend config to init()
79
+ // IMPORTANT: For i18next-chained-backend, we need to pass backends array in init() options
80
+ // because ChainedBackend reads it from initOptions.backend.backends during initialization
81
+ // IMPORTANT: For non-chained backend, we need to pass the backend config to init() so i18next
82
+ // can load resources from the configured loadPath
83
+ // IMPORTANT: Set backend config AFTER spreading user options to ensure it's not overridden
84
+ if (mergedBackend) {
85
+ if (isChainedBackend && mergedBackend._chainedBackendConfig) {
86
+ // Try to get backend classes from i18nInstance.options.backend.backends first
87
+ // This avoids importing fs-backend in browser environment
88
+ let HttpBackend: any;
89
+ let SdkBackendClass: any;
90
+
91
+ if (
92
+ i18nInstance?.options?.backend?.backends &&
93
+ Array.isArray(i18nInstance.options.backend.backends) &&
94
+ i18nInstance.options.backend.backends.length >= 2
95
+ ) {
96
+ // Use the backend classes already set by useI18nextBackend
97
+ HttpBackend = i18nInstance.options.backend.backends[0];
98
+ SdkBackendClass = i18nInstance.options.backend.backends[1];
99
+ } else {
100
+ // Fallback: use backend classes from middleware
101
+ // Build tools will automatically select the correct file (.node.ts for Node.js, .ts for browser)
102
+ // HttpBackendWithSave is exported from both middleware.ts (browser) and middleware.node.ts (Node.js)
103
+ HttpBackend = HttpBackendWithSave;
104
+ SdkBackendClass = SdkBackend;
105
+ }
106
+
107
+ // For chained backend, pass the complete chained backend config structure
108
+ // Note: HttpBackend and SdkBackendClass are already wrapped
109
+ // with save methods to ensure i18next-chained-backend's refresh logic is triggered
110
+ initOptions.backend = {
111
+ backends: [HttpBackend, SdkBackendClass],
112
+ backendOptions: mergedBackend._chainedBackendConfig.backendOptions,
113
+ cacheHitMode: mergedBackend.cacheHitMode || 'refreshAndUpdateStore',
114
+ };
115
+ } else {
116
+ // For non-chained backend, pass the backend config directly
117
+ // This ensures i18next can load resources from the configured loadPath
118
+ // Remove internal properties (_useChainedBackend, _chainedBackendConfig) before passing to init()
119
+ const { _useChainedBackend, _chainedBackendConfig, ...cleanBackend } =
120
+ mergedBackend || {};
121
+ initOptions.backend = cleanBackend;
122
+ }
123
+ }
124
+
125
+ return initOptions;
126
+ };
127
+
128
+ /**
129
+ * Ensure i18n instance language matches the final detected language
130
+ */
131
+ export const ensureLanguageMatch = async (
132
+ i18nInstance: I18nInstance,
133
+ finalLanguage: string,
134
+ ): Promise<void> => {
135
+ if (i18nInstance.language !== finalLanguage) {
136
+ await i18nInstance.setLang?.(finalLanguage);
137
+ await i18nInstance.changeLanguage?.(finalLanguage);
138
+ }
139
+ };
140
+
141
+ /**
142
+ * Change language for i18n instance in onBeforeRender hook
143
+ * This function can be used by other runtime plugins to change language
144
+ * @param i18nInstance - The i18n instance
145
+ * @param newLang - The new language code to switch to
146
+ * @param options - Optional configuration
147
+ */
148
+ export const changeI18nLanguage = async (
149
+ i18nInstance: I18nInstance,
150
+ newLang: string,
151
+ options?: {
152
+ detectionOptions?: any;
153
+ },
154
+ ): Promise<void> => {
155
+ if (!newLang || typeof newLang !== 'string') {
156
+ throw new Error('Language must be a non-empty string');
157
+ }
158
+
159
+ if (!i18nInstance) {
160
+ throw new Error('i18nInstance is required');
161
+ }
162
+
163
+ // Update i18n instance language
164
+ await i18nInstance.setLang?.(newLang);
165
+ await i18nInstance.changeLanguage?.(newLang);
166
+
167
+ // Cache language in browser environment
168
+ if (isBrowser()) {
169
+ const detectionOptions =
170
+ options?.detectionOptions || i18nInstance.options?.detection;
171
+ cacheUserLanguage(i18nInstance, newLang, detectionOptions);
172
+ }
173
+ };
174
+
175
+ /**
176
+ * Initialize i18n instance if not already initialized
177
+ */
178
+ export const initializeI18nInstance = async (
179
+ i18nInstance: I18nInstance,
180
+ finalLanguage: string,
181
+ fallbackLanguage: string,
182
+ languages: string[],
183
+ mergedDetection: any,
184
+ mergedBackend: any,
185
+ userInitOptions?: I18nInitOptions,
186
+ useSuspense?: boolean,
187
+ ): Promise<void> => {
188
+ if (!i18nInstance.isInitialized) {
189
+ const initOptions = await buildInitOptions(
190
+ finalLanguage,
191
+ fallbackLanguage,
192
+ languages,
193
+ mergedDetection,
194
+ mergedBackend,
195
+ userInitOptions,
196
+ useSuspense,
197
+ i18nInstance,
198
+ );
199
+
200
+ // For i18next, backend configuration must be passed to init() via initOptions.backend
201
+ // The backend class is already registered via useI18nextBackend, but the config (loadPath, etc.)
202
+ // needs to be in initOptions.backend for init() to use it
203
+ const actualInstance = getActualI18nextInstance(i18nInstance);
204
+ const savedBackendConfig =
205
+ actualInstance?.options?.backend || i18nInstance.options?.backend;
206
+ const isChainedBackendFromSaved =
207
+ savedBackendConfig?.backends &&
208
+ Array.isArray(savedBackendConfig.backends);
209
+
210
+ await i18nInstance.init(initOptions);
211
+
212
+ if (mergedBackend) {
213
+ if (isI18nWrapperInstance(i18nInstance) && actualInstance?.options) {
214
+ if (isChainedBackendFromSaved && initOptions.backend) {
215
+ actualInstance.options.backend = {
216
+ ...initOptions.backend,
217
+ backends: savedBackendConfig.backends,
218
+ };
219
+ } else if (initOptions.backend) {
220
+ actualInstance.options.backend = {
221
+ ...actualInstance.options.backend,
222
+ ...initOptions.backend,
223
+ };
224
+ }
225
+ }
226
+
227
+ if (hasOptions(i18nInstance)) {
228
+ if (isChainedBackendFromSaved && initOptions.backend) {
229
+ i18nInstance.options.backend = {
230
+ ...initOptions.backend,
231
+ backends: savedBackendConfig.backends,
232
+ };
233
+ } else if (initOptions.backend) {
234
+ i18nInstance.options.backend = {
235
+ ...i18nInstance.options.backend,
236
+ ...initOptions.backend,
237
+ };
238
+ }
239
+ }
240
+ }
241
+
242
+ if (mergedBackend && hasOptions(i18nInstance)) {
243
+ // For chained backend with cacheHitMode: 'refreshAndUpdateStore',
244
+ // i18next-chained-backend automatically:
245
+ // 1. Loads from the first backend (HTTP/FS) and displays immediately
246
+ // 2. Asynchronously loads from the second backend (SDK) and updates the store
247
+ // 3. Triggers 'loaded' event when SDK resources are loaded, which causes React to re-render
248
+ //
249
+ // Note: i18next.init() returns a Promise that resolves when the first backend loads.
250
+ // For chained backend, it does NOT wait for the second backend (SDK) to load.
251
+ // The SDK backend loads asynchronously and triggers 'loaded' event automatically.
252
+ const defaultNS =
253
+ initOptions.defaultNS || initOptions.ns || 'translation';
254
+ const ns = Array.isArray(defaultNS) ? defaultNS[0] : defaultNS;
255
+
256
+ let retries = 20;
257
+ while (retries > 0) {
258
+ // Get the actual i18next instance to access store property
259
+ const actualInstance = getActualI18nextInstance(i18nInstance);
260
+ const store = (actualInstance as any).store;
261
+ if (store?.data?.[finalLanguage]?.[ns]) {
262
+ break;
263
+ }
264
+ await new Promise(resolve => setTimeout(resolve, 100));
265
+ retries--;
266
+ }
267
+ }
268
+ }
269
+ };
270
+
271
+ /**
272
+ * Type guard to check if i18n instance has options property
273
+ */
274
+ function hasOptions(instance: I18nInstance): instance is I18nInstance & {
275
+ options: NonNullable<I18nInstance['options']>;
276
+ } {
277
+ return instance.options !== undefined && instance.options !== null;
278
+ }
279
+
280
+ /**
281
+ * Setup cloned instance for SSR with backend support
282
+ */
283
+ export const setupClonedInstance = async (
284
+ i18nInstance: I18nInstance,
285
+ finalLanguage: string,
286
+ fallbackLanguage: string,
287
+ languages: string[],
288
+ backendEnabled: boolean,
289
+ backend: BaseBackendOptions | undefined,
290
+ i18nextDetector: boolean,
291
+ detection: any,
292
+ localePathRedirect: boolean,
293
+ userInitOptions: I18nInitOptions | undefined,
294
+ ): Promise<void> => {
295
+ const mergedBackend = mergeBackendOptions(backend, userInitOptions);
296
+ // Check if SDK is configured (allows standalone SDK usage even without locales directory)
297
+ const hasSdkConfig =
298
+ typeof userInitOptions?.backend?.sdk === 'function' ||
299
+ (mergedBackend?.sdk && typeof mergedBackend.sdk === 'function');
300
+
301
+ if (backendEnabled || hasSdkConfig) {
302
+ useI18nextBackend(i18nInstance, mergedBackend);
303
+ if (mergedBackend && hasOptions(i18nInstance)) {
304
+ i18nInstance.options.backend = {
305
+ ...i18nInstance.options.backend,
306
+ ...mergedBackend,
307
+ };
308
+ }
309
+
310
+ if (i18nInstance.isInitialized) {
311
+ await ensureLanguageMatch(i18nInstance, finalLanguage);
312
+ } else {
313
+ const mergedDetection = mergeDetectionOptions(
314
+ i18nextDetector,
315
+ detection,
316
+ localePathRedirect,
317
+ userInitOptions,
318
+ );
319
+ await initializeI18nInstance(
320
+ i18nInstance,
321
+ finalLanguage,
322
+ fallbackLanguage,
323
+ languages,
324
+ mergedDetection,
325
+ mergedBackend,
326
+ userInitOptions,
327
+ false, // SSR always uses false for useSuspense
328
+ );
329
+ }
330
+ } else {
331
+ await ensureLanguageMatch(i18nInstance, finalLanguage);
332
+ }
333
+ };
@@ -0,0 +1,275 @@
1
+ import {
2
+ type RuntimePlugin,
3
+ isBrowser,
4
+ useRuntimeContext,
5
+ } from '@modern-js/runtime';
6
+ import { merge } from '@modern-js/runtime-utils/merge';
7
+ import type { TInternalRuntimeContext } from '@modern-js/runtime/internal';
8
+ import type React from 'react';
9
+ import { useEffect, useMemo, useRef, useState } from 'react';
10
+ import type {
11
+ BaseBackendOptions,
12
+ BaseLocaleDetectionOptions,
13
+ } from '../shared/type';
14
+ import { ModernI18nProvider } from './context';
15
+ import {
16
+ createContextValue,
17
+ useClientSideRedirect,
18
+ useLanguageSync,
19
+ useSdkResourcesLoader,
20
+ } from './hooks';
21
+ import type { I18nInitOptions, I18nInstance } from './i18n';
22
+ import { getI18nInstance } from './i18n';
23
+ import { mergeBackendOptions } from './i18n/backend';
24
+ import { useI18nextBackend } from './i18n/backend/middleware';
25
+ import {
26
+ detectLanguageWithPriority,
27
+ exportServerLngToWindow,
28
+ mergeDetectionOptions,
29
+ } from './i18n/detection';
30
+ import { useI18nextLanguageDetector } from './i18n/detection/middleware';
31
+ import {
32
+ getI18nextInstanceForProvider,
33
+ getI18nextProvider,
34
+ getInitReactI18next,
35
+ } from './i18n/instance';
36
+ import {
37
+ changeI18nLanguage,
38
+ ensureLanguageMatch,
39
+ initializeI18nInstance,
40
+ setupClonedInstance,
41
+ } from './i18n/utils';
42
+ import { getPathname } from './utils';
43
+ import './types';
44
+
45
+ export type { I18nSdkLoader, I18nSdkLoadOptions } from '../shared/type';
46
+ export type { Resources } from './i18n/instance';
47
+
48
+ export interface I18nPluginOptions {
49
+ entryName?: string;
50
+ localeDetection?: BaseLocaleDetectionOptions;
51
+ backend?: BaseBackendOptions;
52
+ i18nInstance?: I18nInstance;
53
+ changeLanguage?: (lang: string) => void;
54
+ initOptions?: I18nInitOptions;
55
+ [key: string]: any;
56
+ }
57
+
58
+ interface RuntimeContextWithI18n extends TInternalRuntimeContext {
59
+ i18nInstance?: I18nInstance;
60
+ changeLanguage?: (lang: string) => Promise<void>;
61
+ }
62
+
63
+ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
64
+ name: '@modern-js/plugin-i18n',
65
+ setup: api => {
66
+ const {
67
+ entryName,
68
+ i18nInstance: userI18nInstance,
69
+ initOptions,
70
+ localeDetection,
71
+ backend,
72
+ } = options;
73
+ const {
74
+ localePathRedirect = false,
75
+ i18nextDetector = true,
76
+ languages = [],
77
+ fallbackLanguage = 'en',
78
+ detection,
79
+ ignoreRedirectRoutes,
80
+ } = localeDetection || {};
81
+ const { enabled: backendEnabled = false } = backend || {};
82
+ let I18nextProvider: React.FunctionComponent<any> | null;
83
+
84
+ api.onBeforeRender(async context => {
85
+ let i18nInstance = await getI18nInstance(userI18nInstance);
86
+ const { i18n: otherConfig } = api.getRuntimeConfig();
87
+ const { initOptions: otherInitOptions } = otherConfig || {};
88
+ const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
89
+ const initReactI18next = await getInitReactI18next();
90
+ I18nextProvider = await getI18nextProvider();
91
+ if (initReactI18next) {
92
+ i18nInstance.use(initReactI18next);
93
+ }
94
+
95
+ const pathname = getPathname(context);
96
+
97
+ if (i18nextDetector) {
98
+ useI18nextLanguageDetector(i18nInstance);
99
+ }
100
+
101
+ const mergedDetection = mergeDetectionOptions(
102
+ i18nextDetector,
103
+ detection,
104
+ localePathRedirect,
105
+ userInitOptions,
106
+ );
107
+ const mergedBackend = mergeBackendOptions(backend, userInitOptions);
108
+
109
+ // Register Backend BEFORE detectLanguageWithPriority
110
+ // This is critical because detectLanguageWithPriority may trigger init()
111
+ // through i18next detector, and backend must be registered before init()
112
+ // Register backend if:
113
+ // 1. enabled is true (explicitly or auto-detected), OR
114
+ // 2. SDK is configured (allows standalone SDK usage even without locales directory)
115
+ const hasSdkConfig =
116
+ typeof userInitOptions?.backend?.sdk === 'function' ||
117
+ (mergedBackend?.sdk && typeof mergedBackend.sdk === 'function');
118
+ if (mergedBackend && (backendEnabled || hasSdkConfig)) {
119
+ useI18nextBackend(i18nInstance, mergedBackend);
120
+ }
121
+
122
+ const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
123
+ languages,
124
+ fallbackLanguage,
125
+ localePathRedirect,
126
+ i18nextDetector,
127
+ detection,
128
+ userInitOptions,
129
+ mergedBackend,
130
+ pathname,
131
+ ssrContext: context.ssrContext,
132
+ });
133
+
134
+ await initializeI18nInstance(
135
+ i18nInstance,
136
+ finalLanguage,
137
+ fallbackLanguage,
138
+ languages,
139
+ mergedDetection,
140
+ mergedBackend,
141
+ userInitOptions,
142
+ );
143
+
144
+ if (!isBrowser() && i18nInstance.cloneInstance) {
145
+ i18nInstance = i18nInstance.cloneInstance();
146
+ await setupClonedInstance(
147
+ i18nInstance,
148
+ finalLanguage,
149
+ fallbackLanguage,
150
+ languages,
151
+ backendEnabled,
152
+ backend,
153
+ i18nextDetector,
154
+ detection,
155
+ localePathRedirect,
156
+ userInitOptions,
157
+ );
158
+ }
159
+
160
+ if (localePathRedirect) {
161
+ await ensureLanguageMatch(i18nInstance, finalLanguage);
162
+ }
163
+
164
+ if (!isBrowser()) {
165
+ exportServerLngToWindow(context, finalLanguage);
166
+ }
167
+ context.i18nInstance = i18nInstance;
168
+
169
+ // Add changeLanguage method to context for other runtime plugins to use
170
+ context.changeLanguage = async (newLang: string) => {
171
+ await changeI18nLanguage(i18nInstance, newLang, {
172
+ detectionOptions: mergedDetection,
173
+ });
174
+ };
175
+ });
176
+
177
+ api.wrapRoot(App => {
178
+ return props => {
179
+ const runtimeContext = useRuntimeContext() as RuntimeContextWithI18n;
180
+ const i18nInstance = runtimeContext.i18nInstance;
181
+ const initialLang = useMemo(
182
+ () =>
183
+ i18nInstance?.language ||
184
+ (localeDetection?.fallbackLanguage ?? 'en'),
185
+ [i18nInstance?.language, localeDetection?.fallbackLanguage],
186
+ );
187
+ const [lang, setLang] = useState(initialLang);
188
+ const [forceUpdate, setForceUpdate] = useState(0);
189
+ const prevLangRef = useRef(lang);
190
+ const runtimeContextRef = useRef(runtimeContext);
191
+ runtimeContextRef.current = runtimeContext;
192
+
193
+ useEffect(() => {
194
+ if (i18nInstance?.language) {
195
+ const translator = (i18nInstance as any).translator;
196
+ if (translator) {
197
+ translator.language = i18nInstance.language;
198
+ }
199
+ }
200
+ }, [i18nInstance?.language]);
201
+
202
+ useEffect(() => {
203
+ prevLangRef.current = lang;
204
+ }, [lang]);
205
+
206
+ useSdkResourcesLoader(i18nInstance, setForceUpdate);
207
+ useLanguageSync(
208
+ i18nInstance,
209
+ localePathRedirect,
210
+ languages,
211
+ runtimeContextRef,
212
+ prevLangRef,
213
+ setLang,
214
+ );
215
+ // Handle client-side redirect for static deployments
216
+ // Note: This hook only executes in browser environment and skips SSR scenarios
217
+ useClientSideRedirect(
218
+ i18nInstance,
219
+ localePathRedirect,
220
+ languages,
221
+ fallbackLanguage,
222
+ ignoreRedirectRoutes,
223
+ );
224
+
225
+ const contextValue = useMemo(
226
+ () =>
227
+ createContextValue(
228
+ lang,
229
+ i18nInstance,
230
+ entryName,
231
+ languages,
232
+ localePathRedirect,
233
+ ignoreRedirectRoutes,
234
+ setLang,
235
+ ),
236
+ [
237
+ lang,
238
+ i18nInstance,
239
+ entryName,
240
+ languages,
241
+ localePathRedirect,
242
+ ignoreRedirectRoutes,
243
+ forceUpdate,
244
+ ],
245
+ );
246
+
247
+ const appContent = (
248
+ <ModernI18nProvider value={contextValue}>
249
+ <App {...props} />
250
+ </ModernI18nProvider>
251
+ );
252
+
253
+ if (!i18nInstance) {
254
+ return appContent;
255
+ }
256
+
257
+ if (I18nextProvider) {
258
+ const i18nextInstanceForProvider =
259
+ getI18nextInstanceForProvider(i18nInstance);
260
+ return (
261
+ <I18nextProvider i18n={i18nextInstanceForProvider}>
262
+ {appContent}
263
+ </I18nextProvider>
264
+ );
265
+ }
266
+
267
+ return appContent;
268
+ };
269
+ });
270
+ },
271
+ });
272
+
273
+ export { useModernI18n } from './context';
274
+ export { I18nLink } from './I18nLink';
275
+ export default i18nPlugin;
@@ -0,0 +1,17 @@
1
+ import type { I18nInitOptions, I18nInstance } from './i18n';
2
+
3
+ declare module '@modern-js/runtime' {
4
+ interface RuntimeConfig {
5
+ i18n?: {
6
+ i18nInstance?: I18nInstance;
7
+ changeLanguage?: (lang: string) => void;
8
+ setLang?: (lang: string) => void;
9
+ initOptions?: I18nInitOptions;
10
+ };
11
+ }
12
+
13
+ interface TInternalRuntimeContext {
14
+ i18nInstance?: I18nInstance;
15
+ changeLanguage?: (lang: string) => Promise<void>;
16
+ }
17
+ }