feat(dashboard): add i18n support with zh/en/ja/ko locales

- Add react-i18next + i18next + i18next-browser-languagedetector
- Create i18n config (singleton import) with zh/en/ja/ko JSON locale files
- Add language switcher Globe dropdown in Header topbar
- Replace all hardcoded Chinese strings in:
  - Layout (Header, Sidebar, NavItem, Layout, constants)
  - Settings (index, AppearanceTab, SecurityTab, OtherTab, AboutTab)
  - Auth page (auth.tsx)
  - Search dialog (searchItems via useMemo + t())
  - Restart overlay (getStatusConfig accepts t param)
  - Error boundary (ErrorFallback, ErrorDetails function components)
  - HTTP warning banner
- localStorage key: maibot-locale
- Compatible with Electron
This commit is contained in:
DrSmoothl
2026-03-03 20:50:06 +08:00
parent 5cc34f24c0
commit a65a40f85f
23 changed files with 7271 additions and 473 deletions

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import {
AlertCircle,
@@ -59,6 +60,7 @@ export function AuthPage() {
const [error, setError] = useState('')
const [checkingAuth, setCheckingAuth] = useState(true)
const navigate = useNavigate()
const { t } = useTranslation()
const { enableWavesBackground, setEnableWavesBackground } = useAnimation()
const { theme, setTheme } = useTheme()
@@ -100,7 +102,7 @@ export function AuthPage() {
setError('')
if (!token.trim()) {
setError('请输入 Access Token')
setError(t('auth.tokenRequired'))
return
}
@@ -160,12 +162,12 @@ export function AuthPage() {
}
} else {
console.error('Token 验证失败:', data.message)
setError(data.message || 'Token 验证失败,请检查后重试')
setError(data.message || t('auth.verifyFailed'))
}
} catch (err) {
console.error('Token 验证错误:', err)
setError(
err instanceof Error ? err.message : '连接服务器失败,请检查网络连接'
err instanceof Error ? err.message : t('auth.connFailed')
)
} finally {
setIsValidating(false)
@@ -177,7 +179,7 @@ export function AuthPage() {
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background p-4">
{enableWavesBackground && <WavesBackground />}
<div className="text-muted-foreground">...</div>
<div className="text-muted-foreground">{t('auth.checkingAuth')}</div>
</div>
)
}
@@ -193,7 +195,7 @@ export function AuthPage() {
<button
onClick={toggleTheme}
className="absolute right-4 top-4 rounded-lg p-2 hover:bg-accent transition-colors z-10 text-foreground"
title={actualTheme === 'dark' ? '切换到浅色模式' : '切换到深色模式'}
title={actualTheme === 'dark' ? t('auth.switchToLight') : t('auth.switchToDark')}
>
{actualTheme === 'dark' ? (
<Sun className="h-5 w-5" strokeWidth={2.5} fill="none" />
@@ -209,9 +211,9 @@ export function AuthPage() {
</div>
<div className="space-y-2">
<CardTitle className="text-2xl font-bold">使 MaiBot</CardTitle>
<CardTitle className="text-2xl font-bold">{t('auth.welcome')}</CardTitle>
<CardDescription className="text-base">
Access Token 访
{t('auth.accessDesc')}
</CardDescription>
</div>
</CardHeader>
@@ -228,7 +230,7 @@ export function AuthPage() {
<Input
id="token"
type="password"
placeholder="请输入您的 Access Token"
placeholder={t('auth.tokenPlaceholder')}
value={token}
onChange={(e) => setToken(e.target.value)}
className={cn('pl-10', error && 'border-red-500 focus-visible:ring-red-500')}
@@ -252,10 +254,10 @@ export function AuthPage() {
{isValidating ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
...
{t('auth.verifyingLabel')}
</>
) : (
'验证并进入'
t('auth.verifyEnter')
)}
</Button>
@@ -264,17 +266,17 @@ export function AuthPage() {
<DialogTrigger asChild>
<button className="w-full text-center text-sm text-primary hover:text-primary/80 transition-colors underline-offset-4 hover:underline flex items-center justify-center gap-1">
<HelpCircle className="h-4 w-4" strokeWidth={2} fill="none" />
Token Token
{t('auth.helpLink')}
</button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Lock className="h-5 w-5 text-primary" strokeWidth={2} fill="none" />
Access Token
{t('auth.helpTitle')}
</DialogTitle>
<DialogDescription>
Access Token 访 MaiBot WebUI
{t('auth.helpDesc')}
</DialogDescription>
</DialogHeader>
@@ -284,13 +286,13 @@ export function AuthPage() {
<div className="flex items-start gap-3">
<Terminal className="h-5 w-5 text-primary flex-shrink-0 mt-0.5" strokeWidth={2} fill="none" />
<div className="flex-1 space-y-2">
<h4 className="font-semibold text-sm"></h4>
<h4 className="font-semibold text-sm">{t('auth.method1Title')}</h4>
<p className="text-sm text-muted-foreground">
MaiBot WebUI Access Token
{t('auth.method1Desc')}
</p>
<div className="rounded bg-background p-2 font-mono text-xs">
<p className="text-muted-foreground">🔑 WebUI Access Token: abc123...</p>
<p className="text-muted-foreground">💡 使 Token WebUI</p>
<p className="text-muted-foreground">{t('auth.method1Example1')}</p>
<p className="text-muted-foreground">{t('auth.method1Example2')}</p>
</div>
</div>
</div>
@@ -301,15 +303,15 @@ export function AuthPage() {
<div className="flex items-start gap-3">
<FileText className="h-5 w-5 text-primary flex-shrink-0 mt-0.5" strokeWidth={2} fill="none" />
<div className="flex-1 space-y-2">
<h4 className="font-semibold text-sm"></h4>
<h4 className="font-semibold text-sm">{t('auth.method2Title')}</h4>
<p className="text-sm text-muted-foreground">
Token
{t('auth.method2Desc')}
</p>
<div className="rounded bg-background p-2 font-mono text-xs break-all">
<code className="text-primary">data/webui.json</code>
</div>
<p className="text-xs text-muted-foreground">
<code className="px-1 py-0.5 bg-background rounded">access_token</code>
{t('auth.method2FileHint')} <code className="px-1 py-0.5 bg-background rounded">access_token</code>
</p>
</div>
</div>
@@ -320,10 +322,10 @@ export function AuthPage() {
<div className="flex gap-2">
<AlertCircle className="h-4 w-4 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" strokeWidth={2} fill="none" />
<div className="text-sm text-yellow-800 dark:text-yellow-300 space-y-1">
<p className="font-semibold"></p>
<p className="font-semibold">{t('auth.securityTipTitle')}</p>
<ul className="list-disc list-inside space-y-0.5 text-xs">
<li> Token</li>
<li> Token</li>
<li>{t('auth.securityTip1')}</li>
<li>{t('auth.securityTip2')}</li>
</ul>
</div>
</div>
@@ -337,30 +339,30 @@ export function AuthPage() {
<AlertDialogTrigger asChild>
<button className="w-full text-center text-sm text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline flex items-center justify-center gap-1">
<Zap className="h-4 w-4" strokeWidth={2} fill="none" />
{t('auth.slowLink')}
</button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-primary" strokeWidth={2} fill="none" />
{t('auth.disableAnimTitle')}
</AlertDialogTitle>
<AlertDialogDescription>
{t('auth.disableAnimDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<div className="rounded-lg border bg-muted/50 p-4 space-y-2">
<p className="text-sm text-muted-foreground">
使
{t('auth.disableAnimDetail')}
</p>
</div>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={() => setEnableWavesBackground(false)}
>
{t('auth.disableAnimBtn')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>