mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
137 lines
3.8 KiB
TypeScript
137 lines
3.8 KiB
TypeScript
import { useState, useRef, useEffect } from 'react'
|
|
import { useTranslation, type Locale } from '@/lib/use-translation'
|
|
|
|
const locales: { value: Locale; label: string }[] = [
|
|
{ value: 'en', label: 'English' },
|
|
{ value: 'zh-CN', label: '简体中文' },
|
|
]
|
|
|
|
function LanguageIcon() {
|
|
return (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d="m5 8 6 6" />
|
|
<path d="m4 14 6-6 2-3" />
|
|
<path d="M2 5h12" />
|
|
<path d="M7 2h1" />
|
|
<path d="m22 22-5-10-5 10" />
|
|
<path d="M14 18h6" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function CheckIcon() {
|
|
return (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<polyline points="20 6 9 17 4 12" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
export function LanguageSwitcher() {
|
|
const { locale, setLocale, t } = useTranslation()
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
|
|
const handleLocaleChange = (newLocale: Locale) => {
|
|
setLocale(newLocale)
|
|
setIsOpen(false)
|
|
}
|
|
|
|
// Close dropdown when clicking outside
|
|
useEffect(() => {
|
|
if (!isOpen) return
|
|
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
setIsOpen(false)
|
|
}
|
|
}
|
|
|
|
document.addEventListener('mousedown', handleClickOutside)
|
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
}, [isOpen])
|
|
|
|
// Close on escape key
|
|
useEffect(() => {
|
|
if (!isOpen) return
|
|
|
|
const handleEscape = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') {
|
|
setIsOpen(false)
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', handleEscape)
|
|
return () => document.removeEventListener('keydown', handleEscape)
|
|
}, [isOpen])
|
|
|
|
return (
|
|
<div ref={containerRef} className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="flex items-center justify-center h-8 w-8 rounded-md text-[var(--app-hint)] hover:text-[var(--app-fg)] hover:bg-[var(--app-subtle-bg)] transition-colors"
|
|
title={t('language.title')}
|
|
aria-label={t('language.title')}
|
|
aria-expanded={isOpen}
|
|
aria-haspopup="listbox"
|
|
>
|
|
<LanguageIcon />
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div
|
|
className="absolute right-0 top-full mt-1 min-w-[140px] rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] shadow-lg overflow-hidden z-50"
|
|
role="listbox"
|
|
aria-label={t('language.title')}
|
|
>
|
|
{locales.map((loc) => {
|
|
const isSelected = locale === loc.value
|
|
return (
|
|
<button
|
|
key={loc.value}
|
|
type="button"
|
|
role="option"
|
|
aria-selected={isSelected}
|
|
onClick={() => handleLocaleChange(loc.value)}
|
|
className={`flex items-center justify-between w-full px-3 py-2 text-sm text-left transition-colors ${
|
|
isSelected
|
|
? 'text-[var(--app-link)] bg-[var(--app-subtle-bg)]'
|
|
: 'text-[var(--app-fg)] hover:bg-[var(--app-subtle-bg)]'
|
|
}`}
|
|
>
|
|
<span>{loc.label}</span>
|
|
{isSelected && (
|
|
<span className="ml-2 text-[var(--app-link)]">
|
|
<CheckIcon />
|
|
</span>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|