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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user