@@ -10,10 +10,33 @@ import { ModernI18nProvider } from './context';
1010import type { I18nInitOptions , I18nInstance } from './i18n' ;
1111import { getI18nInstance } from './i18n' ;
1212import { detectLanguage , useI18nextLanguageDetector } from './i18n/detection' ;
13- import { mergeDetectionOptions } from './i18n/detection/config' ;
13+ import {
14+ exportServerLngToWindow ,
15+ mergeDetectionOptions ,
16+ } from './i18n/detection/config' ;
1417import { getI18nextProvider , getInitReactI18next } from './i18n/instance' ;
1518import { getEntryPath } from './utils' ;
1619
20+ /**
21+ * Validate languages array configuration
22+ * @param languages - Array of language codes to validate
23+ * @returns true if all languages are valid, false otherwise
24+ */
25+ const validateLanguages = ( languages : string [ ] ) : boolean => {
26+ return languages . every ( lang => typeof lang === 'string' && lang . length > 0 ) ;
27+ } ;
28+
29+ /**
30+ * Get language from SSR data in a type-safe way
31+ * @param window - The window object
32+ * @returns The language from SSR data or undefined
33+ */
34+ const getLanguageFromSSRData = ( window : Window ) : string | undefined => {
35+ // Type-safe access to SSR data via global Window interface
36+ const ssrData = window . _SSR_DATA ;
37+ return ssrData ?. data ?. i18nData ?. lng ;
38+ } ;
39+
1740export interface I18nPluginOptions {
1841 entryName ?: string ;
1942 localeDetection ?: BaseLocaleDetectionOptions ;
@@ -37,11 +60,17 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
3760 languages = [ ] ,
3861 fallbackLanguage = 'en' ,
3962 } = localeDetection || { } ;
63+
64+ // Validate languages configuration
65+ if ( ! validateLanguages ( languages ) ) {
66+ console . warn (
67+ '[@modern-js/plugin-i18n] Invalid languages configuration. All language codes must be non-empty strings.' ,
68+ { languages } ,
69+ ) ;
70+ }
71+
4072 let I18nextProvider : React . FunctionComponent < any > | null ;
4173
42- // Helper function to detect language from path
43- // Returns: { detected: true, language: string } if language found in path
44- // { detected: false } if no language found in path
4574 const detectLanguageFromPath = (
4675 pathname : string ,
4776 ) : {
@@ -56,7 +85,6 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
5685 const segments = relativePath . split ( '/' ) . filter ( Boolean ) ;
5786 const firstSegment = segments [ 0 ] ;
5887
59- // Check if first segment is a valid language
6088 if ( firstSegment && languages . includes ( firstSegment ) ) {
6189 return { detected : true , language : firstSegment } ;
6290 }
@@ -73,33 +101,45 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
73101 i18nInstance . use ( initReactI18next ) ;
74102 }
75103
76- // Get pathname from appropriate source
77104 const getPathname = ( ) => {
78105 if ( isBrowser ( ) ) {
79106 return window . location . pathname ;
80107 }
81108 return context . ssrContext ?. request ?. pathname || '/' ;
82109 } ;
83110
84- // Step 1: Detect language with priority: path > i18next detector > fallback
111+ // Language detection priority: SSR data > path > i18next detector > fallback
85112 let detectedLanguage : string | undefined ;
86113
87- // Priority 1: Path detection (if enabled)
88- if ( localePathRedirect ) {
89- const pathname = getPathname ( ) ;
90- const pathDetection = detectLanguageFromPath ( pathname ) ;
91- if ( pathDetection . detected && pathDetection . language ) {
92- detectedLanguage = pathDetection . language ;
93- }
114+ // Priority 1: SSR data
115+ if ( isBrowser ( ) ) {
116+ try {
117+ const ssrLanguage = getLanguageFromSSRData ( window ) ;
118+ if (
119+ ssrLanguage &&
120+ ( languages . length === 0 || languages . includes ( ssrLanguage ) )
121+ ) {
122+ detectedLanguage = ssrLanguage ;
123+ }
124+ } catch ( error ) { }
125+ }
126+
127+ // Priority 2: Path detection
128+ if ( ! detectedLanguage && localePathRedirect ) {
129+ try {
130+ const pathname = getPathname ( ) ;
131+ const pathDetection = detectLanguageFromPath ( pathname ) ;
132+ if ( pathDetection . detected && pathDetection . language ) {
133+ detectedLanguage = pathDetection . language ;
134+ }
135+ } catch ( error ) { }
94136 }
95137
96- // Step 2: Register detector and prepare detection options if enabled
97138 if ( i18nextDetector ) {
98139 useI18nextLanguageDetector ( i18nInstance ) ;
99140 }
100141
101- // Merge detection options and exclude 'path' if localePathRedirect is enabled
102- // to avoid conflict with manual path detection
142+ // Exclude 'path' from detection order to avoid conflict with manual path detection
103143 const mergedDetection = i18nextDetector
104144 ? mergeDetectionOptions ( userInitOptions ?. detection )
105145 : userInitOptions ?. detection ;
@@ -109,59 +149,77 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
109149 ) ;
110150 }
111151
112- // Step 3: Use i18next detector if path didn't detect (Priority 2)
152+ // Priority 3: i18next detector
113153 if ( ! detectedLanguage && i18nextDetector ) {
114- // Initialize with fallback language first to enable detector
115- const initOptions : I18nInitOptions = {
116- fallbackLng : fallbackLanguage ,
117- supportedLngs : languages ,
118- ...( userInitOptions || { } ) ,
119- detection : mergedDetection ,
120- } ;
121- await i18nInstance . init ( initOptions ) ;
154+ if ( ! i18nInstance . isInitialized ) {
155+ const initialLng = userInitOptions ?. lng || fallbackLanguage ;
156+ const initOptions : any = {
157+ ...( userInitOptions || { } ) ,
158+ lng : initialLng ,
159+ fallbackLng : fallbackLanguage ,
160+ supportedLngs : languages ,
161+ detection : mergedDetection ,
162+ } ;
163+ await i18nInstance . init ( initOptions ) ;
164+ }
122165
123- // Use detector to detect language
124166 let detectorLang : string | undefined ;
125- if ( isBrowser ( ) ) {
126- detectorLang = detectLanguage ( i18nInstance ) ;
127- } else {
128- const request = context . ssrContext ?. request ;
129- if ( request ) {
130- detectorLang = detectLanguage ( i18nInstance , request as any ) ;
167+ try {
168+ if ( isBrowser ( ) ) {
169+ detectorLang = detectLanguage ( i18nInstance ) ;
170+ } else {
171+ const request = context . ssrContext ?. request ;
172+ if ( request ) {
173+ detectorLang = detectLanguage ( i18nInstance , request as any ) ;
174+ }
131175 }
132- }
133176
134- // Validate detected language
135- if ( detectorLang ) {
136- if ( languages . length === 0 || languages . includes ( detectorLang ) ) {
137- detectedLanguage = detectorLang ;
177+ if ( detectorLang ) {
178+ if ( languages . length === 0 || languages . includes ( detectorLang ) ) {
179+ detectedLanguage = detectorLang ;
180+ }
181+ } else if ( i18nInstance . isInitialized && i18nInstance . language ) {
182+ // Fallback to instance's current language if detector didn't detect
183+ const currentLang = i18nInstance . language ;
184+ if ( languages . length === 0 || languages . includes ( currentLang ) ) {
185+ detectedLanguage = currentLang ;
186+ }
138187 }
139- }
188+ } catch ( error ) { }
140189 }
141190
142- // Priority 3: Use fallback language
143- const finalLanguage = detectedLanguage || fallbackLanguage ;
191+ // Priority 4: Use user config language or fallback
192+ const finalLanguage =
193+ detectedLanguage || userInitOptions ?. lng || fallbackLanguage ;
144194
145- // Step 4: Initialize i18n if not already initialized
146195 if ( ! i18nInstance . isInitialized ) {
147- const initOptions : I18nInitOptions = {
196+ const initOptions : any = {
197+ ...( userInitOptions || { } ) ,
148198 lng : finalLanguage ,
149199 fallbackLng : fallbackLanguage ,
150200 supportedLngs : languages ,
151- ...( userInitOptions || { } ) ,
152201 detection : mergedDetection ,
153202 } ;
154203 await i18nInstance . init ( initOptions ) ;
155- } else {
156- // Update language if different
204+ } else if ( i18nInstance . language !== finalLanguage ) {
205+ await i18nInstance . changeLanguage ( finalLanguage ) ;
206+ }
207+
208+ // Clone instance for SSR
209+ if ( ! isBrowser ( ) && i18nInstance . cloneInstance ) {
210+ i18nInstance = i18nInstance . cloneInstance ( ) ;
157211 if ( i18nInstance . language !== finalLanguage ) {
158212 await i18nInstance . changeLanguage ( finalLanguage ) ;
159213 }
160214 }
161215
162- // Clone instance for SSR if needed
163- if ( ! isBrowser ( ) && i18nInstance . cloneInstance ) {
164- i18nInstance = i18nInstance . cloneInstance ( ) ;
216+ // Ensure language is correct (critical for SSR)
217+ if ( i18nInstance . language !== finalLanguage ) {
218+ await i18nInstance . changeLanguage ( finalLanguage ) ;
219+ }
220+
221+ if ( ! isBrowser ( ) ) {
222+ exportServerLngToWindow ( context , finalLanguage ) ;
165223 }
166224
167225 context . i18nInstance = i18nInstance ;
@@ -171,23 +229,22 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
171229 return props => {
172230 const runtimeContext = useRuntimeContext ( ) ;
173231 const i18nInstance = ( runtimeContext as any ) . i18nInstance ;
174- const [ lang , setLang ] = useState ( i18nInstance . language ) ;
232+ const initialLang =
233+ i18nInstance ?. language || ( localeDetection ?. fallbackLanguage ?? 'en' ) ;
234+ const [ lang , setLang ] = useState ( initialLang ) ;
175235
176- if ( ! isBrowser ( ) ) {
177- ( i18nInstance as any ) . translator . language = i18nInstance . language ;
236+ // Sync translator.language with i18nInstance.language
237+ if ( i18nInstance ?. language && i18nInstance . translator ) {
238+ i18nInstance . translator . language = i18nInstance . language ;
178239 }
179240
180- // Get pathname from appropriate source based on environment
181241 const getCurrentPathname = ( ) => {
182242 if ( isBrowser ( ) ) {
183243 return window . location . pathname ;
184- } else {
185- // In SSR, get pathname from request context
186- return runtimeContext . request ?. pathname || '/' ;
187244 }
245+ return runtimeContext . request ?. pathname || '/' ;
188246 } ;
189247
190- // Initialize language from URL on mount (only when localeDetection is enabled)
191248 useEffect ( ( ) => {
192249 if ( localePathRedirect ) {
193250 const currentPathname = getCurrentPathname ( ) ;
@@ -196,15 +253,17 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
196253 const currentLang = pathDetection . language ;
197254 if ( currentLang !== lang ) {
198255 setLang ( currentLang ) ;
199- // Update i18n instance language
200256 i18nInstance . changeLanguage ( currentLang ) ;
201257 }
202258 }
259+ } else {
260+ // Sync language when localePathRedirect is false (important for CSR)
261+ const instanceLang = i18nInstance . language ;
262+ if ( instanceLang && instanceLang !== lang ) {
263+ setLang ( instanceLang ) ;
264+ }
203265 }
204266 } , [ ] ) ;
205-
206- // Context value contains language, i18nInstance, and plugin configuration
207- // changeLanguage is now implemented in the useModernI18n hook
208267 const contextValue = {
209268 language : lang ,
210269 i18nInstance,
0 commit comments