@modern-js/plugin-i18n 2.69.4 → 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,641 @@
1
+ import { type TRuntimeContext, isBrowser } from '@modern-js/runtime';
2
+ import { detectLanguageFromPath } from '../../utils';
3
+ import type {
4
+ I18nInitOptions,
5
+ I18nInstance,
6
+ LanguageDetectorOptions,
7
+ } from '../instance';
8
+ import { isI18nWrapperInstance } from '../instance';
9
+ import { mergeDetectionOptions as mergeDetectionOptionsUtil } from './config';
10
+ import {
11
+ cacheUserLanguage,
12
+ detectLanguage,
13
+ readLanguageFromStorage,
14
+ useI18nextLanguageDetector,
15
+ } from './middleware';
16
+
17
+ // Re-export cacheUserLanguage for use in context
18
+ export { cacheUserLanguage };
19
+
20
+ interface DetectorCacheEntry {
21
+ instance: I18nInstance;
22
+ isTemporary: boolean;
23
+ configKey: string;
24
+ }
25
+
26
+ const detectorInstanceCache = new WeakMap<I18nInstance, DetectorCacheEntry>();
27
+
28
+ const DETECTOR_SAFE_OPTION_KEYS: string[] = [
29
+ 'lowerCaseLng',
30
+ 'nonExplicitSupportedLngs',
31
+ 'load',
32
+ 'partialBundledLanguages',
33
+ 'returnNull',
34
+ 'returnEmptyString',
35
+ 'returnObjects',
36
+ 'joinArrays',
37
+ 'keySeparator',
38
+ 'nsSeparator',
39
+ 'pluralSeparator',
40
+ 'contextSeparator',
41
+ 'fallbackNS',
42
+ 'ns',
43
+ 'defaultNS',
44
+ 'debug',
45
+ ];
46
+
47
+ /**
48
+ * Stable stringify that sorts object keys to ensure consistent output
49
+ * regardless of property order
50
+ */
51
+ const stableStringify = (value: any): string => {
52
+ if (value === null || value === undefined) {
53
+ return JSON.stringify(value);
54
+ }
55
+
56
+ if (typeof value !== 'object') {
57
+ return JSON.stringify(value);
58
+ }
59
+
60
+ if (Array.isArray(value)) {
61
+ // Arrays maintain their order
62
+ return `[${value.map(item => stableStringify(item)).join(',')}]`;
63
+ }
64
+
65
+ // For objects, sort keys and recursively stringify values
66
+ const sortedKeys = Object.keys(value).sort();
67
+ const sortedEntries = sortedKeys.map(key => {
68
+ const stringifiedValue = stableStringify(value[key]);
69
+ return `${JSON.stringify(key)}:${stringifiedValue}`;
70
+ });
71
+
72
+ return `{${sortedEntries.join(',')}}`;
73
+ };
74
+
75
+ const buildDetectorConfigKey = (
76
+ languages: string[],
77
+ fallbackLanguage: string,
78
+ mergedDetection: LanguageDetectorOptions,
79
+ ): string => {
80
+ return stableStringify({
81
+ languages,
82
+ fallbackLanguage,
83
+ detection: mergedDetection,
84
+ });
85
+ };
86
+
87
+ const pickSafeDetectionOptions = (
88
+ userInitOptions?: I18nInitOptions,
89
+ ): Partial<I18nInitOptions> & Record<string, any> => {
90
+ if (!userInitOptions) {
91
+ return {};
92
+ }
93
+ const safeOptions: Partial<I18nInitOptions> & Record<string, any> = {};
94
+ for (const key of DETECTOR_SAFE_OPTION_KEYS) {
95
+ const value = (userInitOptions as any)[key];
96
+ if (value !== undefined) {
97
+ safeOptions[key] = value;
98
+ }
99
+ }
100
+ if ((userInitOptions as any).interpolation) {
101
+ safeOptions.interpolation = { ...(userInitOptions as any).interpolation };
102
+ }
103
+ return safeOptions;
104
+ };
105
+
106
+ const cleanupDetectorCacheEntry = (entry?: DetectorCacheEntry) => {
107
+ if (!entry || !entry.isTemporary) {
108
+ return;
109
+ }
110
+ const instance = entry.instance as any;
111
+ try {
112
+ instance?.removeAllListeners?.();
113
+ } catch (error) {
114
+ void error;
115
+ }
116
+ try {
117
+ instance?.off?.('*');
118
+ } catch (error) {
119
+ void error;
120
+ }
121
+ try {
122
+ instance?.services?.backendConnector?.backend?.stop?.();
123
+ } catch (error) {
124
+ void error;
125
+ }
126
+ try {
127
+ instance?.services?.backendConnector?.backend?.close?.();
128
+ } catch (error) {
129
+ void error;
130
+ }
131
+ };
132
+
133
+ export function exportServerLngToWindow(context: TRuntimeContext, lng: string) {
134
+ context.__i18nData__ = { lng };
135
+ }
136
+
137
+ export const getLanguageFromSSRData = (window: Window): string | undefined => {
138
+ try {
139
+ const ssrData = window._SSR_DATA;
140
+ // Check if SSR data exists and has valid structure
141
+ if (!ssrData || !ssrData.data || !ssrData.data.i18nData) {
142
+ return undefined;
143
+ }
144
+ const lng = ssrData.data.i18nData.lng;
145
+ // Return language only if it's a non-empty string
146
+ return typeof lng === 'string' && lng.trim() !== '' ? lng : undefined;
147
+ } catch (error) {
148
+ // If accessing window._SSR_DATA throws an error, return undefined
149
+ return undefined;
150
+ }
151
+ };
152
+
153
+ export interface BaseLanguageDetectionOptions {
154
+ languages: string[];
155
+ fallbackLanguage: string;
156
+ localePathRedirect: boolean;
157
+ i18nextDetector: boolean;
158
+ detection?: LanguageDetectorOptions;
159
+ userInitOptions?: I18nInitOptions;
160
+ mergedBackend?: any;
161
+ }
162
+
163
+ export interface LanguageDetectionOptions extends BaseLanguageDetectionOptions {
164
+ pathname: string;
165
+ ssrContext?: any;
166
+ }
167
+
168
+ export interface LanguageDetectionResult {
169
+ detectedLanguage?: string;
170
+ finalLanguage: string;
171
+ }
172
+
173
+ /**
174
+ * Normalize language code (e.g., 'zh-CN' -> 'zh', 'en-US' -> 'en')
175
+ */
176
+ const normalizeLanguageCode = (language: string): string => {
177
+ if (!language) {
178
+ return language;
179
+ }
180
+ // Extract base language code (before hyphen)
181
+ const baseLang = language.split('-')[0];
182
+ return baseLang;
183
+ };
184
+
185
+ /**
186
+ * Check if a language is supported
187
+ * Also checks the base language code (e.g., 'zh-CN' matches 'zh')
188
+ */
189
+ const isLanguageSupported = (
190
+ language: string | undefined,
191
+ supportedLanguages: string[],
192
+ ): boolean => {
193
+ if (!language) {
194
+ return false;
195
+ }
196
+ if (supportedLanguages.length === 0) {
197
+ return true;
198
+ }
199
+ // Check exact match first
200
+ if (supportedLanguages.includes(language)) {
201
+ return true;
202
+ }
203
+ // Check base language code match (e.g., 'zh-CN' matches 'zh')
204
+ const baseLang = normalizeLanguageCode(language);
205
+ if (baseLang !== language && supportedLanguages.includes(baseLang)) {
206
+ return true;
207
+ }
208
+ return false;
209
+ };
210
+
211
+ /**
212
+ * Get the supported language that matches the given language
213
+ * Returns the exact match if available, otherwise returns the base language code match
214
+ * Returns undefined if no match is found
215
+ */
216
+ const getSupportedLanguage = (
217
+ language: string | undefined,
218
+ supportedLanguages: string[],
219
+ ): string | undefined => {
220
+ if (!language) {
221
+ return undefined;
222
+ }
223
+ if (supportedLanguages.length === 0) {
224
+ return language;
225
+ }
226
+ // Check exact match first
227
+ if (supportedLanguages.includes(language)) {
228
+ return language;
229
+ }
230
+ // Check base language code match (e.g., 'zh-CN' matches 'zh')
231
+ const baseLang = normalizeLanguageCode(language);
232
+ if (baseLang !== language && supportedLanguages.includes(baseLang)) {
233
+ return baseLang;
234
+ }
235
+ return undefined;
236
+ };
237
+
238
+ /**
239
+ * Priority 1: Detect language from SSR data
240
+ * Try to get language from window._SSR_DATA first (both SSR and CSR projects)
241
+ * Returns undefined if SSR data is not available or invalid
242
+ */
243
+ const detectLanguageFromSSR = (languages: string[]): string | undefined => {
244
+ if (!isBrowser()) {
245
+ return undefined;
246
+ }
247
+
248
+ try {
249
+ const ssrLanguage = getLanguageFromSSRData(window);
250
+ if (ssrLanguage && isLanguageSupported(ssrLanguage, languages)) {
251
+ return ssrLanguage;
252
+ }
253
+ } catch (error) {
254
+ // Silently ignore errors
255
+ }
256
+
257
+ return undefined;
258
+ };
259
+
260
+ /**
261
+ * Priority 2: Detect language from URL path
262
+ * Only returns a language if the path explicitly contains a language prefix
263
+ */
264
+ const detectLanguageFromPathPriority = (
265
+ pathname: string,
266
+ languages: string[],
267
+ localePathRedirect: boolean,
268
+ ): string | undefined => {
269
+ if (!localePathRedirect) {
270
+ return undefined;
271
+ }
272
+
273
+ // If no languages are configured, cannot detect from path
274
+ if (!languages || languages.length === 0) {
275
+ return undefined;
276
+ }
277
+
278
+ // If pathname is empty or invalid, no language in path
279
+ if (!pathname || pathname.trim() === '') {
280
+ return undefined;
281
+ }
282
+
283
+ try {
284
+ const pathDetection = detectLanguageFromPath(
285
+ pathname,
286
+ languages,
287
+ localePathRedirect,
288
+ );
289
+ // Only return language if explicitly detected in path
290
+ if (pathDetection.detected === true && pathDetection.language) {
291
+ return pathDetection.language;
292
+ }
293
+ } catch (error) {
294
+ // Silently ignore errors, return undefined
295
+ }
296
+
297
+ return undefined;
298
+ };
299
+
300
+ /**
301
+ * Initialize i18n instance for detector if needed
302
+ */
303
+ interface DetectorInitResult {
304
+ detectorInstance: I18nInstance;
305
+ isTemporary: boolean;
306
+ }
307
+
308
+ const createDetectorInstance = (
309
+ baseInstance: I18nInstance,
310
+ configKey: string,
311
+ ): { instance: I18nInstance; isTemporary: boolean } => {
312
+ const cached = detectorInstanceCache.get(baseInstance);
313
+ if (cached && cached.configKey === configKey) {
314
+ return { instance: cached.instance, isTemporary: cached.isTemporary };
315
+ }
316
+
317
+ if (cached) {
318
+ cleanupDetectorCacheEntry(cached);
319
+ detectorInstanceCache.delete(baseInstance);
320
+ }
321
+
322
+ const createNewInstance = (): {
323
+ instance: I18nInstance;
324
+ isTemporary: boolean;
325
+ } => {
326
+ if (typeof baseInstance.createInstance === 'function') {
327
+ try {
328
+ const created = baseInstance.createInstance();
329
+ if (created) {
330
+ return { instance: created, isTemporary: true };
331
+ }
332
+ } catch (error) {
333
+ void error;
334
+ }
335
+ }
336
+
337
+ if (typeof baseInstance.cloneInstance === 'function') {
338
+ try {
339
+ const cloned = baseInstance.cloneInstance();
340
+ if (cloned) {
341
+ return { instance: cloned, isTemporary: true };
342
+ }
343
+ } catch (error) {
344
+ void error;
345
+ }
346
+ }
347
+
348
+ return { instance: baseInstance, isTemporary: false };
349
+ };
350
+
351
+ const created = createNewInstance();
352
+ if (created.isTemporary) {
353
+ detectorInstanceCache.set(baseInstance, {
354
+ instance: created.instance,
355
+ isTemporary: true,
356
+ configKey,
357
+ });
358
+ }
359
+ return created;
360
+ };
361
+
362
+ const initializeI18nForDetector = async (
363
+ i18nInstance: I18nInstance,
364
+ options: BaseLanguageDetectionOptions,
365
+ ): Promise<DetectorInitResult> => {
366
+ const mergedDetection = mergeDetectionOptions(
367
+ options.i18nextDetector,
368
+ options.detection,
369
+ options.localePathRedirect,
370
+ options.userInitOptions,
371
+ );
372
+
373
+ const configKey = buildDetectorConfigKey(
374
+ options.languages,
375
+ options.fallbackLanguage,
376
+ mergedDetection,
377
+ );
378
+
379
+ const { instance, isTemporary } = createDetectorInstance(
380
+ i18nInstance,
381
+ configKey,
382
+ );
383
+
384
+ const safeUserOptions = pickSafeDetectionOptions(options.userInitOptions);
385
+
386
+ // Only initialize detection capability, don't load any resources to avoid conflicts with subsequent backend initialization
387
+ const initOptions: I18nInitOptions = {
388
+ ...safeUserOptions,
389
+ fallbackLng: options.fallbackLanguage,
390
+ supportedLngs: options.languages,
391
+ detection: mergedDetection,
392
+ initImmediate: true,
393
+ interpolation: {
394
+ ...(safeUserOptions?.interpolation || {}),
395
+ escapeValue: safeUserOptions?.interpolation?.escapeValue ?? false,
396
+ },
397
+ react: {
398
+ useSuspense: false,
399
+ },
400
+ };
401
+
402
+ // Ensure the detector instance has the language detection plugin loaded
403
+ useI18nextLanguageDetector(instance);
404
+
405
+ if (!instance.isInitialized) {
406
+ await instance.init(initOptions);
407
+ } else if (isTemporary) {
408
+ await instance.init(initOptions);
409
+ }
410
+
411
+ return { detectorInstance: instance, isTemporary };
412
+ };
413
+
414
+ /**
415
+ * Priority 3: Detect language using i18next detector
416
+ */
417
+ const detectLanguageFromI18nextDetector = async (
418
+ i18nInstance: I18nInstance,
419
+ options: BaseLanguageDetectionOptions & { ssrContext?: any },
420
+ ): Promise<string | undefined> => {
421
+ if (!options.i18nextDetector) {
422
+ return undefined;
423
+ }
424
+
425
+ // Merge detection options to pass to detector
426
+ const mergedDetection = mergeDetectionOptions(
427
+ options.i18nextDetector,
428
+ options.detection,
429
+ options.localePathRedirect,
430
+ options.userInitOptions,
431
+ );
432
+
433
+ const { detectorInstance, isTemporary } = await initializeI18nForDetector(
434
+ i18nInstance,
435
+ options,
436
+ );
437
+
438
+ try {
439
+ const request = options.ssrContext?.request;
440
+ if (!isBrowser() && !request) {
441
+ return undefined;
442
+ }
443
+
444
+ const detectorLang = detectLanguage(
445
+ detectorInstance,
446
+ request as any,
447
+ mergedDetection,
448
+ );
449
+
450
+ // Use getSupportedLanguage to get the matching supported language
451
+ // This handles both exact match and base language code match (e.g., 'zh-CN' -> 'zh')
452
+ if (detectorLang) {
453
+ const supportedLang = getSupportedLanguage(
454
+ detectorLang,
455
+ options.languages,
456
+ );
457
+ if (supportedLang) {
458
+ return supportedLang;
459
+ }
460
+ }
461
+
462
+ // Fallback to instance's current language if detector didn't detect
463
+ if (detectorInstance.isInitialized && detectorInstance.language) {
464
+ const currentLang = detectorInstance.language;
465
+ if (isLanguageSupported(currentLang, options.languages)) {
466
+ return currentLang;
467
+ }
468
+ }
469
+ } catch (error) {
470
+ // Silently ignore errors
471
+ } finally {
472
+ // Clean up temporary instance to avoid affecting subsequent formal initialization
473
+ if (isTemporary && detectorInstance !== i18nInstance) {
474
+ // Temporary instance is saved in cache for reuse
475
+ detectorInstanceCache.set(i18nInstance, {
476
+ instance: detectorInstance,
477
+ isTemporary: true,
478
+ configKey: buildDetectorConfigKey(
479
+ options.languages,
480
+ options.fallbackLanguage,
481
+ mergedDetection,
482
+ ),
483
+ });
484
+ } else if (detectorInstance === i18nInstance) {
485
+ // As a fallback, prevent i18nInstance from being polluted by detector init
486
+ (i18nInstance as any).isInitialized = false;
487
+ delete (i18nInstance as any).language;
488
+ }
489
+ }
490
+
491
+ return undefined;
492
+ };
493
+
494
+ /**
495
+ * Detect language with priority:
496
+ * Priority 1: SSR data (try window._SSR_DATA first, works for both SSR and CSR)
497
+ * Priority 2: Path detection
498
+ * Priority 3: i18next detector (reads from cookie/localStorage)
499
+ * Priority 4: User config language or fallback
500
+ */
501
+ export const detectLanguageWithPriority = async (
502
+ i18nInstance: I18nInstance,
503
+ options: LanguageDetectionOptions,
504
+ ): Promise<LanguageDetectionResult> => {
505
+ const {
506
+ languages,
507
+ fallbackLanguage,
508
+ localePathRedirect,
509
+ i18nextDetector,
510
+ detection,
511
+ userInitOptions,
512
+ pathname,
513
+ ssrContext,
514
+ } = options;
515
+
516
+ let detectedLanguage: string | undefined;
517
+
518
+ // Priority 1: Try SSR data first (works for both SSR and CSR projects)
519
+ // For CSR projects, if SSR data exists in window, use it; otherwise continue to next priority
520
+ detectedLanguage = detectLanguageFromSSR(languages);
521
+
522
+ // Priority 2: Path detection
523
+ if (!detectedLanguage) {
524
+ detectedLanguage = detectLanguageFromPathPriority(
525
+ pathname,
526
+ languages,
527
+ localePathRedirect,
528
+ );
529
+ }
530
+
531
+ // Priority 3: i18next detector (reads from cookie/localStorage)
532
+ if (!detectedLanguage && i18nextDetector) {
533
+ if (isI18nWrapperInstance(i18nInstance)) {
534
+ detectedLanguage = readLanguageFromStorage(
535
+ mergeDetectionOptions(
536
+ i18nextDetector,
537
+ detection,
538
+ localePathRedirect,
539
+ userInitOptions,
540
+ ),
541
+ );
542
+ } else {
543
+ detectedLanguage = await detectLanguageFromI18nextDetector(i18nInstance, {
544
+ languages,
545
+ fallbackLanguage,
546
+ localePathRedirect,
547
+ i18nextDetector,
548
+ detection,
549
+ userInitOptions,
550
+ mergedBackend: options.mergedBackend,
551
+ ssrContext,
552
+ });
553
+ }
554
+ }
555
+
556
+ // Priority 4: Use user config language or fallback
557
+ const finalLanguage =
558
+ detectedLanguage || userInitOptions?.lng || fallbackLanguage;
559
+
560
+ return { detectedLanguage, finalLanguage };
561
+ };
562
+
563
+ /**
564
+ * Options for building i18n init options
565
+ */
566
+ export interface BuildInitOptionsParams {
567
+ finalLanguage: string;
568
+ fallbackLanguage: string;
569
+ languages: string[];
570
+ userInitOptions?: I18nInitOptions;
571
+ mergedDetection?: any;
572
+ mergeBackend?: any;
573
+ }
574
+
575
+ /**
576
+ * Build i18n initialization options
577
+ */
578
+ export const buildInitOptions = (
579
+ params: BuildInitOptionsParams,
580
+ ): I18nInitOptions => {
581
+ const {
582
+ finalLanguage,
583
+ fallbackLanguage,
584
+ languages,
585
+ userInitOptions,
586
+ mergedDetection,
587
+ mergeBackend,
588
+ } = params;
589
+
590
+ return {
591
+ ...(userInitOptions || {}),
592
+ lng: finalLanguage,
593
+ fallbackLng: fallbackLanguage,
594
+ supportedLngs: languages,
595
+ detection: mergedDetection,
596
+ backend: mergeBackend,
597
+ interpolation: {
598
+ ...(userInitOptions?.interpolation || {}),
599
+ escapeValue: userInitOptions?.interpolation?.escapeValue ?? false,
600
+ },
601
+ react: {
602
+ useSuspense: isBrowser(),
603
+ },
604
+ } as any;
605
+ };
606
+
607
+ /**
608
+ * Merge detection and backend options
609
+ */
610
+ export const mergeDetectionOptions = (
611
+ i18nextDetector: boolean,
612
+ detection?: LanguageDetectorOptions,
613
+ localePathRedirect?: boolean,
614
+ userInitOptions?: I18nInitOptions,
615
+ ) => {
616
+ // Exclude 'path' from detection order to avoid conflict with manual path detection
617
+ let mergedDetection: LanguageDetectorOptions;
618
+ if (i18nextDetector) {
619
+ // mergeDetectionOptionsUtil always returns an object with default options
620
+ mergedDetection = mergeDetectionOptionsUtil(
621
+ detection,
622
+ userInitOptions?.detection,
623
+ );
624
+ } else {
625
+ // If detector is disabled, use user options or empty object
626
+ mergedDetection = userInitOptions?.detection || {};
627
+ }
628
+
629
+ // Ensure mergedDetection is always an object (should not be undefined after above)
630
+ if (!mergedDetection || typeof mergedDetection !== 'object') {
631
+ mergedDetection = {};
632
+ }
633
+
634
+ if (localePathRedirect && mergedDetection.order) {
635
+ mergedDetection.order = mergedDetection.order.filter(
636
+ (item: string) => item !== 'path',
637
+ );
638
+ }
639
+
640
+ return mergedDetection;
641
+ };
@@ -0,0 +1,84 @@
1
+ import { LanguageDetector } from 'i18next-http-middleware';
2
+ import type { I18nInstance } from '../instance';
3
+
4
+ export const cacheUserLanguage = (
5
+ _i18nInstance: I18nInstance,
6
+ _language: string,
7
+ _detectionOptions?: any,
8
+ ): void => {
9
+ return;
10
+ };
11
+
12
+ /**
13
+ * Read language directly from storage (localStorage/cookie)
14
+ * Not available in Node.js environment, returns undefined
15
+ */
16
+ export const readLanguageFromStorage = (
17
+ _detectionOptions?: any,
18
+ ): string | undefined => {
19
+ // In Node.js environment, storage-based detection is not available
20
+ return undefined;
21
+ };
22
+ /**
23
+ * Register LanguageDetector plugin to i18n instance
24
+ * Must be called before init() to properly register the detector
25
+ */
26
+ export const useI18nextLanguageDetector = (i18nInstance: I18nInstance) => {
27
+ if (!i18nInstance.isInitialized) {
28
+ return i18nInstance.use(LanguageDetector);
29
+ }
30
+ return i18nInstance;
31
+ };
32
+
33
+ /**
34
+ * Detect language using i18next-http-middleware LanguageDetector
35
+ * For initialized instances without detector in services, manually create a detector instance
36
+ */
37
+ export const detectLanguage = (
38
+ i18nInstance: I18nInstance,
39
+ request?: any,
40
+ detectionOptions?: any,
41
+ ): string | undefined => {
42
+ if (!request) {
43
+ return undefined;
44
+ }
45
+
46
+ try {
47
+ const detector = i18nInstance.services?.languageDetector;
48
+ if (detector && typeof detector.detect === 'function') {
49
+ const result = detector.detect(request, {});
50
+ if (typeof result === 'string') {
51
+ return result;
52
+ }
53
+ if (Array.isArray(result) && result.length > 0) {
54
+ return result[0];
55
+ }
56
+ return undefined;
57
+ }
58
+
59
+ if (
60
+ i18nInstance.isInitialized &&
61
+ i18nInstance.services &&
62
+ i18nInstance.options
63
+ ) {
64
+ const manualDetector = new LanguageDetector();
65
+ const optionsToUse = detectionOptions
66
+ ? { ...i18nInstance.options, detection: detectionOptions }
67
+ : i18nInstance.options;
68
+ manualDetector.init(i18nInstance.services, optionsToUse as any);
69
+
70
+ const result = (manualDetector.detect as any)(request, {}, undefined);
71
+ if (typeof result === 'string') {
72
+ return result;
73
+ }
74
+ if (Array.isArray(result) && result.length > 0) {
75
+ return result[0];
76
+ }
77
+ return undefined;
78
+ }
79
+ } catch (error) {
80
+ return undefined;
81
+ }
82
+
83
+ return undefined;
84
+ };