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