11"use client" ;
22
3- import LocaleSwitcher from "@/components/LocaleSwitcher" ;
4- import { Alert , AlertDescription , AlertTitle } from "@/components/ui/alert" ;
53import { Button } from "@/components/ui/button" ;
6- import { DEFAULT_LOCALE , routing } from "@/i18n/routing" ;
4+ import { Link as I18nLink , LOCALE_NAMES , routing } from "@/i18n/routing" ;
5+ import { cn } from "@/lib/utils" ;
76import { useLocaleStore } from "@/stores/localeStore" ;
8- import { Globe , X } from "lucide-react" ;
7+ import { ArrowRight , Globe , X } from "lucide-react" ;
98import { useLocale } from "next-intl" ;
10- import { useEffect , useState } from "react" ;
9+ import { useCallback , useEffect , useState } from "react" ;
1110
1211export function LanguageDetectionAlert ( ) {
1312 const [ countdown , setCountdown ] = useState ( 10 ) ; // countdown 10s and dismiss
13+ const [ isVisible , setIsVisible ] = useState ( false ) ;
1414 const locale = useLocale ( ) ;
15- const [ currentLocale , setCurrentLocale ] = useState ( locale ) ;
15+ const [ detectedLocale , setDetectedLocale ] = useState < string | null > ( null ) ;
1616 const {
1717 showLanguageAlert,
1818 setShowLanguageAlert,
1919 dismissLanguageAlert,
2020 getLangAlertDismissed,
2121 } = useLocaleStore ( ) ;
2222
23+ const handleDismiss = useCallback ( ( ) => {
24+ setIsVisible ( false ) ;
25+ setTimeout ( ( ) => {
26+ dismissLanguageAlert ( ) ;
27+ } , 300 ) ;
28+ } , [ dismissLanguageAlert ] ) ;
29+
30+ const handleSwitchLanguage = useCallback ( ( ) => {
31+ dismissLanguageAlert ( ) ;
32+ } , [ dismissLanguageAlert ] ) ;
33+
2334 useEffect ( ( ) => {
24- const detectedLang = navigator . language ; // Get full language code, e.g., zh-HK
35+ const detectedLang = navigator . language ; // Get full language code, e.g., zh_HK
2536 const storedDismiss = getLangAlertDismissed ( ) ;
2637
2738 if ( ! storedDismiss ) {
28- // Check if the full language code is supported (e.g., zh-HK)
2939 let supportedLang = routing . locales . find ( ( l ) => l === detectedLang ) ;
3040
31- // If full code isn't supported, check primary language (e.g., zh)
3241 if ( ! supportedLang ) {
33- const mainLang = detectedLang . split ( "-" ) [ 0 ] ; // Get primary language code
42+ const mainLang = detectedLang . split ( "-" ) [ 0 ] ;
3443 supportedLang = routing . locales . find ( ( l ) => l . startsWith ( mainLang ) ) ;
3544 }
3645
37- // If language still isn't supported, default to English
38- setCurrentLocale ( supportedLang || DEFAULT_LOCALE ) ;
39- setShowLanguageAlert ( supportedLang !== locale ) ;
46+ if ( supportedLang && supportedLang !== locale ) {
47+ setDetectedLocale ( supportedLang ) ;
48+ setShowLanguageAlert ( true ) ;
49+ setTimeout ( ( ) => setIsVisible ( true ) , 100 ) ;
50+ }
4051 }
41- } , [ locale , getLangAlertDismissed , setCurrentLocale , setShowLanguageAlert ] ) ;
52+ } , [ locale , getLangAlertDismissed , setShowLanguageAlert ] ) ;
4253
43- // countdown
4454 useEffect ( ( ) => {
4555 let timer : NodeJS . Timeout ;
4656
@@ -55,40 +65,80 @@ export function LanguageDetectionAlert() {
5565 } ;
5666 } , [ showLanguageAlert , countdown ] ) ;
5767
58- // dismiss alert after countdown = 0
5968 useEffect ( ( ) => {
6069 if ( countdown === 0 && showLanguageAlert ) {
61- dismissLanguageAlert ( ) ;
70+ handleDismiss ( ) ;
6271 }
63- } , [ countdown , showLanguageAlert , dismissLanguageAlert ] ) ;
72+ } , [ countdown , showLanguageAlert , handleDismiss ] ) ;
6473
65- if ( ! showLanguageAlert ) return null ;
74+ if ( ! showLanguageAlert || ! detectedLocale ) return null ;
6675
67- const messages = require ( `@/i18n/messages/${ currentLocale } .json` ) ;
76+ const messages = require ( `@/i18n/messages/${ detectedLocale } .json` ) ;
6877 const alertMessages = messages . LanguageDetection ;
6978
7079 return (
71- < Alert className = "mb-4 relative" >
72- < Button
73- variant = "ghost"
74- size = "icon"
75- className = "absolute right-2 top-2 h-6 w-6"
76- onClick = { dismissLanguageAlert }
77- >
78- < X className = "h-4 w-4" />
79- </ Button >
80- < Globe className = "h-4 w-4" />
81- < AlertTitle >
82- { alertMessages . title } { " " }
83- < span className = " mt-2 text-sm text-muted-foreground" >
84- { alertMessages . countdown . replace ( "{countdown}" , countdown . toString ( ) ) }
85- </ span >
86- </ AlertTitle >
87- < AlertDescription >
88- < div className = "flex items-center gap-2" >
89- { alertMessages . description } < LocaleSwitcher />
80+ < div
81+ className = { cn (
82+ "fixed top-16 right-4 z-50 max-w-sm w-full mx-4 sm:mx-0 sm:w-96" ,
83+ "transform transition-all duration-300 ease-in-out" ,
84+ isVisible
85+ ? "translate-x-0 translate-y-0 opacity-100"
86+ : "translate-x-full opacity-0"
87+ ) }
88+ role = "banner"
89+ aria-live = "polite"
90+ aria-label = "Language detection alert"
91+ >
92+ < div className = "bg-background/95 backdrop-blur-md border border-border rounded-xl shadow-lg p-4 relative" >
93+ < Button
94+ variant = "ghost"
95+ size = "icon"
96+ className = "absolute right-2 top-2 h-6 w-6 opacity-50 hover:opacity-100"
97+ onClick = { handleDismiss }
98+ aria-label = "Dismiss language suggestion"
99+ >
100+ < X className = "h-4 w-4" />
101+ </ Button >
102+
103+ < div className = "pr-8" >
104+ < div className = "flex items-center gap-2 mb-3" >
105+ < Globe className = "h-4 w-4 text-primary" />
106+ < h3 className = "font-medium text-sm text-foreground" >
107+ { alertMessages . title }
108+ </ h3 >
109+ </ div >
110+
111+ < p className = "text-xs text-muted-foreground mb-4 leading-relaxed" >
112+ { alertMessages . description }
113+ </ p >
114+
115+ < div className = "flex items-center justify-between" >
116+ < Button asChild onClick = { handleSwitchLanguage } >
117+ < I18nLink
118+ href = "/"
119+ title = { `${ alertMessages . switchTo } ${ LOCALE_NAMES [ detectedLocale ] } ` }
120+ locale = { detectedLocale as any }
121+ className = { cn (
122+ "flex items-center gap-2 px-3 py-2 rounded-lg" ,
123+ "bg-primary text-primary-foreground hover:bg-primary/90" ,
124+ "text-sm font-medium transition-colors" ,
125+ "group focus:outline-none focus:ring-2 focus:ring-primary/50"
126+ ) }
127+ aria-label = { `${ alertMessages . switchTo } ${ LOCALE_NAMES [ detectedLocale ] } ` }
128+ >
129+ < span >
130+ { alertMessages . switchTo } { LOCALE_NAMES [ detectedLocale ] }
131+ </ span >
132+ < ArrowRight className = "h-3 w-3 transition-transform group-hover:translate-x-0.5" />
133+ </ I18nLink >
134+ </ Button >
135+
136+ < span className = "text-xs text-muted-foreground" > { countdown } s</ span >
137+ </ div >
90138 </ div >
91- </ AlertDescription >
92- </ Alert >
139+
140+ < div className = "absolute inset-0 rounded-xl bg-gradient-to-r from-primary/10 to-transparent pointer-events-none opacity-50" />
141+ </ div >
142+ </ div >
93143 ) ;
94144}
0 commit comments