@@ -11,11 +11,125 @@ import {
1111 cacheUserLanguage ,
1212 detectLanguage ,
1313 readLanguageFromStorage ,
14+ useI18nextLanguageDetector ,
1415} from './middleware' ;
1516
1617// Re-export cacheUserLanguage for use in context
1718export { cacheUserLanguage } ;
1819
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+
19133export function exportServerLngToWindow ( context : TRuntimeContext , lng : string ) {
20134 context . __i18nData__ = { lng } ;
21135}
@@ -186,74 +300,111 @@ const detectLanguageFromPathPriority = (
186300/**
187301 * Initialize i18n instance for detector if needed
188302 */
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+
189362const initializeI18nForDetector = async (
190363 i18nInstance : I18nInstance ,
191364 options : BaseLanguageDetectionOptions ,
192- ) : Promise < void > => {
193- if ( i18nInstance . isInitialized ) {
194- return ;
195- }
196-
365+ ) : Promise < DetectorInitResult > => {
197366 const mergedDetection = mergeDetectionOptions (
198367 options . i18nextDetector ,
199368 options . detection ,
200369 options . localePathRedirect ,
201370 options . userInitOptions ,
202371 ) ;
203372
204- // Don't set lng explicitly when detector is enabled, let the detector find the language
205- const userLng = options . userInitOptions ?. lng ;
206- // Exclude backend from userInitOptions to avoid overriding mergedBackend
207- // Backend should be set via mergedBackend which contains the properly merged configuration
208- const {
209- lng : _ ,
210- backend : _removedBackend ,
211- ...restUserOptions
212- } = options . userInitOptions || { } ;
213- const initOptions : any = {
214- ...restUserOptions ,
215- ...( userLng ? { lng : userLng } : { } ) ,
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 ,
216389 fallbackLng : options . fallbackLanguage ,
217390 supportedLngs : options . languages ,
218391 detection : mergedDetection ,
392+ initImmediate : true ,
219393 react : {
220- ...( ( options . userInitOptions as any ) ?. react || { } ) ,
221- useSuspense : isBrowser ( )
222- ? ( ( options . userInitOptions as any ) ?. react ?. useSuspense ?? true )
223- : false ,
394+ useSuspense : false ,
224395 } ,
225396 } ;
226397
227- // Set backend config from mergedBackend if available
228- // This ensures default backend config (like loadPath) is preserved when user only provides sdk
229- if ( options . mergedBackend ) {
230- const isChainedBackend = ! ! options . mergedBackend ?. _useChainedBackend ;
231- if ( isChainedBackend && options . mergedBackend . _chainedBackendConfig ) {
232- // For chained backend, we need to get backend classes from i18nInstance.options.backend.backends
233- // which were set by useI18nextBackend
234- const savedBackendConfig = i18nInstance . options ?. backend ;
235- if (
236- savedBackendConfig ?. backends &&
237- Array . isArray ( savedBackendConfig . backends )
238- ) {
239- initOptions . backend = {
240- backends : savedBackendConfig . backends ,
241- backendOptions :
242- options . mergedBackend . _chainedBackendConfig . backendOptions ,
243- cacheHitMode :
244- options . mergedBackend . cacheHitMode || 'refreshAndUpdateStore' ,
245- } ;
246- }
247- } else {
248- // For non-chained backend, pass the backend config directly
249- // Remove internal properties before passing to init()
250- const { _useChainedBackend, _chainedBackendConfig, ...cleanBackend } =
251- options . mergedBackend || { } ;
252- initOptions . backend = cleanBackend ;
253- }
398+ // Ensure the detector instance has the language detection plugin loaded
399+ useI18nextLanguageDetector ( instance ) ;
400+
401+ if ( ! instance . isInitialized ) {
402+ await instance . init ( initOptions ) ;
403+ } else if ( isTemporary ) {
404+ await instance . init ( initOptions ) ;
254405 }
255406
256- await i18nInstance . init ( initOptions ) ;
407+ return { detectorInstance : instance , isTemporary } ;
257408} ;
258409
259410/**
@@ -275,7 +426,10 @@ const detectLanguageFromI18nextDetector = async (
275426 options . userInitOptions ,
276427 ) ;
277428
278- await initializeI18nForDetector ( i18nInstance , options ) ;
429+ const { detectorInstance, isTemporary } = await initializeI18nForDetector (
430+ i18nInstance ,
431+ options ,
432+ ) ;
279433
280434 try {
281435 const request = options . ssrContext ?. request ;
@@ -284,7 +438,7 @@ const detectLanguageFromI18nextDetector = async (
284438 }
285439
286440 const detectorLang = detectLanguage (
287- i18nInstance ,
441+ detectorInstance ,
288442 request as any ,
289443 mergedDetection ,
290444 ) ;
@@ -302,14 +456,32 @@ const detectLanguageFromI18nextDetector = async (
302456 }
303457
304458 // Fallback to instance's current language if detector didn't detect
305- if ( i18nInstance . isInitialized && i18nInstance . language ) {
306- const currentLang = i18nInstance . language ;
459+ if ( detectorInstance . isInitialized && detectorInstance . language ) {
460+ const currentLang = detectorInstance . language ;
307461 if ( isLanguageSupported ( currentLang , options . languages ) ) {
308462 return currentLang ;
309463 }
310464 }
311465 } catch ( error ) {
312466 // Silently ignore errors
467+ } finally {
468+ // Clean up temporary instance to avoid affecting subsequent formal initialization
469+ if ( isTemporary && detectorInstance !== i18nInstance ) {
470+ // Temporary instance is saved in cache for reuse
471+ detectorInstanceCache . set ( i18nInstance , {
472+ instance : detectorInstance ,
473+ isTemporary : true ,
474+ configKey : buildDetectorConfigKey (
475+ options . languages ,
476+ options . fallbackLanguage ,
477+ mergedDetection ,
478+ ) ,
479+ } ) ;
480+ } else if ( detectorInstance === i18nInstance ) {
481+ // As a fallback, prevent i18nInstance from being polluted by detector init
482+ ( i18nInstance as any ) . isInitialized = false ;
483+ delete ( i18nInstance as any ) . language ;
484+ }
313485 }
314486
315487 return undefined ;
0 commit comments