Files
chaos-api/web/default/src/features/profile/components/language-preferences-card.tsx
T
2026-04-30 19:53:02 +08:00

137 lines
4.4 KiB
TypeScript
Vendored

import { useEffect, useMemo, useState } from 'react'
import { Languages, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { useAuthStore } from '@/stores/auth-store'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { TitledCard } from '@/components/ui/titled-card'
import { updateUserLanguage } from '../api'
import { parseUserSettings } from '../lib'
import type { UserProfile } from '../types'
const LANGUAGE_OPTIONS = [
{ value: 'zh', label: '简体中文' },
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'Français' },
{ value: 'ru', label: 'Русский' },
{ value: 'ja', label: '日本語' },
{ value: 'vi', label: 'Tiếng Việt' },
] as const
function normalizeLanguage(value?: string | null): string {
if (!value) return 'en'
const normalized = value.trim().replace(/_/g, '-').toLowerCase()
if (normalized.startsWith('zh')) return 'zh'
return LANGUAGE_OPTIONS.some((lang) => lang.value === normalized)
? normalized
: 'en'
}
type LanguagePreferencesCardProps = {
profile: UserProfile | null
onProfileUpdate: () => void
}
export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
const { t, i18n } = useTranslation()
const { auth } = useAuthStore()
const [saving, setSaving] = useState(false)
const savedLanguage = useMemo(() => {
const settings = parseUserSettings(props.profile?.setting)
return normalizeLanguage(settings.language || i18n.language)
}, [props.profile?.setting, i18n.language])
const [currentLanguage, setCurrentLanguage] = useState(savedLanguage)
useEffect(() => {
setCurrentLanguage(savedLanguage)
}, [savedLanguage])
const handleLanguageChange = async (language: string) => {
const nextLanguage = normalizeLanguage(language)
if (nextLanguage === currentLanguage) return
const previousLanguage = currentLanguage
setCurrentLanguage(nextLanguage)
setSaving(true)
await i18n.changeLanguage(nextLanguage)
try {
const response = await updateUserLanguage(nextLanguage)
if (!response.success) {
throw new Error(response.message || t('Failed to update settings'))
}
if (auth.user) {
const existingSetting =
typeof auth.user.setting === 'string'
? parseUserSettings(auth.user.setting)
: (auth.user.setting ?? {})
auth.setUser({
...auth.user,
setting: JSON.stringify({
...existingSetting,
language: nextLanguage,
}),
})
}
props.onProfileUpdate()
toast.success(t('Language preference saved'))
} catch (_error) {
setCurrentLanguage(previousLanguage)
await i18n.changeLanguage(previousLanguage)
toast.error(t('Failed to update settings'))
} finally {
setSaving(false)
}
}
return (
<TitledCard
title={t('Language Preferences')}
description={t('Set the language used across the interface')}
icon={<Languages className='h-4 w-4' />}
>
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4'>
<div className='space-y-1'>
<div className='text-sm font-medium'>{t('Interface Language')}</div>
<p className='text-muted-foreground line-clamp-2 text-xs sm:text-sm'>
{t(
'Language preferences sync across your signed-in devices and affect API error messages.'
)}
</p>
</div>
<div className='flex items-center gap-2 sm:min-w-48'>
<Select
value={currentLanguage}
onValueChange={handleLanguageChange}
disabled={saving}
>
<SelectTrigger className='w-full sm:w-48'>
<SelectValue placeholder={t('Select language')} />
</SelectTrigger>
<SelectContent>
{LANGUAGE_OPTIONS.map((language) => (
<SelectItem key={language.value} value={language.value}>
{language.label}
</SelectItem>
))}
</SelectContent>
</Select>
{saving && (
<Loader2 className='text-muted-foreground size-4 animate-spin' />
)}
</div>
</div>
</TitledCard>
)
}