1+ "use client" ;
2+
3+ import React , { useState , useEffect , useCallback } from 'react' ;
4+ import { NextIntlClientProvider } from 'next-intl' ;
5+ import { Loader2 } from 'lucide-react' ;
6+
7+ const AVAILABLE_LOCALES = [ 'en' , 'zh' ] ;
8+ const DEFAULT_LOCALE = 'zh' ;
9+ const LOCALE_STORAGE_KEY = 'csoj_locale' ;
10+
11+ interface ClientIntlProviderProps {
12+ children : React . ReactNode ;
13+ }
14+
15+ interface LocaleContextType {
16+ switchLocale : ( newLocale : string ) => void ;
17+ locale : string | null ;
18+ }
19+
20+ const LocaleContext = React . createContext < LocaleContextType > ( {
21+ switchLocale : ( ) => {
22+ console . warn ( "switchLocale called outside ClientIntlProvider." ) ;
23+ } ,
24+ locale : null
25+ } ) ;
26+
27+ export const useClientLocale = ( ) => {
28+ const context = React . useContext ( LocaleContext ) ;
29+ if ( ! context ) {
30+ throw new Error ( 'useClientLocale must be used within ClientIntlProvider' ) ;
31+ }
32+ return context ;
33+ } ;
34+
35+ export const ClientIntlProvider : React . FC < ClientIntlProviderProps > = ( { children } ) => {
36+ const [ locale , setLocale ] = useState < string | null > ( null ) ;
37+ const [ messages , setMessages ] = useState < Record < string , string > | null > ( null ) ;
38+ const [ isLoading , setIsLoading ] = useState ( true ) ;
39+
40+ // Use a ref to track if a language load is in progress, preventing jittering from concurrent requests.
41+ const loadingRef = React . useRef ( false ) ;
42+
43+ const loadMessages = useCallback ( async ( targetLocale : string , updateStorage : boolean = true ) => {
44+ // Avoid concurrent calls
45+ if ( loadingRef . current ) return false ;
46+
47+ loadingRef . current = true ;
48+ setIsLoading ( true ) ;
49+
50+ try {
51+ // Check if the target language is available
52+ const finalLocale = AVAILABLE_LOCALES . includes ( targetLocale ) ? targetLocale : DEFAULT_LOCALE ;
53+
54+ const response = await fetch ( `/messages/${ finalLocale } .json` ) ;
55+ if ( ! response . ok ) {
56+ throw new Error ( `Failed to load messages for locale: ${ finalLocale } ` ) ;
57+ }
58+ const newMessages = await response . json ( ) ;
59+
60+ // Update state upon success
61+ setMessages ( newMessages ) ;
62+ setLocale ( finalLocale ) ;
63+
64+ // Update local storage if requested
65+ if ( updateStorage && typeof window !== 'undefined' ) {
66+ localStorage . setItem ( LOCALE_STORAGE_KEY , finalLocale ) ;
67+ }
68+ return true ;
69+
70+ } catch ( error ) {
71+ console . error ( `Failed to load locale: ${ targetLocale } .` , error ) ;
72+ // On failure, only set default locale/messages if no locale has been loaded yet (initial load failure)
73+ if ( ! locale ) {
74+ setMessages ( { } ) ;
75+ setLocale ( DEFAULT_LOCALE ) ;
76+ }
77+ return false ;
78+ } finally {
79+ setIsLoading ( false ) ;
80+ loadingRef . current = false ;
81+ }
82+ } , [ locale ] ) ;
83+
84+
85+ // Load initial locale on mount
86+ useEffect ( ( ) => {
87+ let initialLocale = DEFAULT_LOCALE ;
88+ if ( typeof window !== 'undefined' ) {
89+ const savedLocale = localStorage . getItem ( LOCALE_STORAGE_KEY ) ;
90+ if ( savedLocale && AVAILABLE_LOCALES . includes ( savedLocale ) ) {
91+ initialLocale = savedLocale ;
92+ }
93+ }
94+
95+ // Asynchronously load the initial language
96+ loadMessages ( initialLocale ) ;
97+
98+ } , [ loadMessages ] ) ;
99+
100+ // Handle locale switching
101+ const switchLocale = async ( newLocale : string ) => {
102+ if ( ! AVAILABLE_LOCALES . includes ( newLocale ) || newLocale === locale || loadingRef . current ) {
103+ return ;
104+ }
105+
106+ // Attempt to load the new language pack
107+ const success = await loadMessages ( newLocale , true ) ; // Update storage on successful load
108+
109+ if ( ! success ) {
110+ console . warn ( `Could not load ${ newLocale } . Sticking to current locale.` ) ;
111+ }
112+ } ;
113+
114+ // Show full-screen loader if initial load is pending
115+ if ( ! locale || ! messages ) {
116+ if ( isLoading ) {
117+ return (
118+ < div className = "flex h-screen flex-col items-center justify-center gap-4" >
119+ < Loader2 className = "h-12 w-12 animate-spin text-primary" />
120+ </ div >
121+ ) ;
122+ }
123+ }
124+
125+ return (
126+ < LocaleContext . Provider value = { { switchLocale, locale } } >
127+ < NextIntlClientProvider locale = { locale ! } messages = { messages ! } >
128+ { children }
129+ </ NextIntlClientProvider >
130+ </ LocaleContext . Provider >
131+ ) ;
132+ } ;
0 commit comments