feat(i18n): U3 国际化 — react-i18next 双语框架 + 侧边栏/Header/Login 全量翻译 + 语言切换器

- 安装 i18next + react-i18next,创建 src/i18n/ 配置实例
- 创建 zh-CN.json (120+ 键) + en-US.json 全量英文翻译
- 侧边栏 42+ 菜单项全量 labelKey 国际化
- Header 通知/主题/用户菜单全量翻译
- Login 页面表单/按钮/主题+语言切换器
- zustand useLanguageStore 持久化语言选择
- HTML lang 属性自动同步
- tsc -b=0, npm build=✅, 17/17 测试通过
This commit is contained in:
attacker committed 2026-06-27 16:54:07 +08:00
1 parent 084a3be2cf
commit 2e179ef7f4
15 files changed
+860 -112

No files matched your search

+61 -14
View File
@@ -1,10 +1,12 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { User, Lock, Palette, Check, Loader2, AlertCircle, Eye, EyeOff } from 'lucide-react';
import { useUserStore } from '@/store';
import { useTranslation } from 'react-i18next';
import { User, Lock, Palette, Check, Loader2, AlertCircle, Eye, EyeOff, Languages } from 'lucide-react';
import { useUserStore, useLanguageStore } from '@/store';
import { userApi } from '@/api';
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
import { cn } from '@/lib/utils';
import { SUPPORTED_LANGUAGES, type AppLanguage } from '@/i18n';
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -67,8 +69,10 @@ const ORBS = [
* 6. 背景光球动画更协调
*/
export function Login() {
const { t } = useTranslation();
const navigate = useNavigate();
const { theme, setTheme } = useTheme();
const { language, setLanguage } = useLanguageStore();
const [username, setUsername] = useState('admin');
const [password, setPassword] = useState('123456');
const [showPassword, setShowPassword] = useState(false);
@@ -90,7 +94,7 @@ export function Login() {
localStorage.setItem('token', result.token);
navigate('/', { replace: true });
} catch (err) {
const message = err instanceof Error ? (err as Record<string, any>).message : '登录失败,请检查用户名和密码';
const message = err instanceof Error ? err.message : t('login.loginFailed');
setError(message);
} finally {
setLoading(false);
@@ -131,16 +135,59 @@ export function Login() {
/>
))}
{/* Theme switcher */}
{/* Theme + Language switcher */}
<div
className="fixed top-4 right-4 z-50"
className="fixed top-4 right-4 z-50 flex items-center gap-2"
style={{ animation: 'login-fade-in 0.6s ease-out' }}
>
{/* Language switcher */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label="切换主题"
aria-label={t('header.language')}
className={cn(
'inline-flex items-center justify-center size-10 rounded-full',
'border border-[var(--dg-border)] transition-all duration-200',
'hover:scale-105 active:scale-95 hover:border-[var(--dg-border-hover)]',
'glass'
)}
style={{ color: 'var(--dg-text-secondary)' }}
>
<Languages className="size-5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-40 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
style={{ boxShadow: 'var(--shadow-dropdown)' }}
>
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">{t('header.languageSwitch')}</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
{SUPPORTED_LANGUAGES.map((lang) => (
<DropdownMenuItem
key={lang.code}
className={cn(
'flex items-center gap-2.5 cursor-pointer transition-colors',
'focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]',
language === lang.code ? 'text-[var(--dg-text-primary)]' : 'text-[var(--dg-text-secondary)]',
)}
onClick={() => setLanguage(lang.code as AppLanguage)}
>
<span className="text-base">{lang.flag}</span>
<span className="flex-1 text-sm">{lang.label}</span>
{language === lang.code && <Check className="size-4 shrink-0 text-[var(--dg-blue)]" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Theme switcher */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={t('header.theme')}
className={cn(
'inline-flex items-center justify-center size-10 rounded-full',
'border border-[var(--dg-border)] transition-all duration-200',
@@ -157,7 +204,7 @@ export function Login() {
className="w-60 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"
style={{ boxShadow: 'var(--shadow-dropdown)' }}
>
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">主题切换</DropdownMenuLabel>
<DropdownMenuLabel className="text-[var(--dg-text-secondary)]">{t('header.themeSwitch')}</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
{THEME_OPTIONS.map((option) => (
<DropdownMenuItem
@@ -226,7 +273,7 @@ export function Login() {
AIOps
</h1>
<p className="mt-2.5 text-sm text-[var(--dg-text-secondary)]">
智能运维 · 全栈可观测平台
{t('login.subtitle')}
</p>
</div>
@@ -250,7 +297,7 @@ export function Login() {
{/* Username */}
<div className="space-y-1.5">
<label className="block text-sm font-medium text-[var(--dg-text-secondary)]">
用户名
{t('login.username')}
</label>
<div className="relative">
<User
@@ -260,7 +307,7 @@ export function Login() {
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="请输入用户名"
placeholder={t('login.usernamePlaceholder')}
autoComplete="username"
autoFocus
className="dg-input w-full py-2.5 pl-10 pr-4 text-sm"
@@ -271,7 +318,7 @@ export function Login() {
{/* Password */}
<div className="space-y-1.5">
<label className="block text-sm font-medium text-[var(--dg-text-secondary)]">
密码
{t('login.password')}
</label>
<div className="relative">
<Lock
@@ -281,7 +328,7 @@ export function Login() {
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="请输入密码"
placeholder={t('login.passwordPlaceholder')}
autoComplete="current-password"
className="dg-input w-full py-2.5 pl-10 pr-12 text-sm"
/>
@@ -314,10 +361,10 @@ export function Login() {
{loading ? (
<span className="inline-flex items-center justify-center gap-2">
<Loader2 className="size-4 animate-spin" />
登录中...
{t('login.loggingIn')}
</span>
) : (
'登 录'
t('login.loginButton')
)}
</button>
</form>