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:
1 parent
084a3be2cf
commit
2e179ef7f4
15 files changed
+860
-112
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user