feat(i18n): integrate internationalization into sidebar, title bar, and settings components

- Added useI18n hook to Sidebar and TitleBar components for translation support.
- Updated sidebar navigation items to use translation keys instead of hardcoded labels.
- Enhanced TitleBar buttons with localized titles for minimize, maximize, and close actions.
- Expanded i18n constants to include new namespaces for sidebar and login.
- Implemented translation messages for sidebar, settings, and login components in English, Chinese, and Japanese.
- Refactored Knowledge and Skills pages to utilize useLocale for locale management.
- Updated Login page to display localized text for form labels, placeholders, and error messages.
- Enhanced Account and General settings panels with localized titles and descriptions.
- Modified SettingMenu to use translation keys for menu items.
This commit is contained in:
DEV_DSW
2026-04-17 17:16:18 +08:00
parent bc3de923ed
commit eca70425cf
16 changed files with 533 additions and 141 deletions

View File

@@ -1,12 +1,13 @@
import { type ChangeEvent, type FormEvent, useEffect, useMemo, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Lock, User } from 'lucide-react';
import { useLocation, useNavigate } from 'react-router-dom';
import blueLogo from '../../assets/images/login/blue_logo.png';
import loginBackground from '../../assets/images/login/login_bg.png';
import loginIllustration from '../../assets/images/login/logo.png';
import userIcon from '../../assets/images/login/user_icon.png';
import { useI18n } from '../../i18n';
import TitleBar from '../../components/layout/TitleBar';
import { resolvePostLoginPath } from '../../router/auth';
import {
@@ -35,6 +36,7 @@ const credentialFieldInputClass =
'h-full min-w-0 flex-1 border-0 bg-transparent pr-4 text-[14px] text-gray-800 outline-none placeholder:text-[#99A0AE] disabled:cursor-not-allowed dark:text-gray-100 dark:placeholder:text-gray-500';
export default function LoginPage() {
const { t } = useI18n();
const navigate = useNavigate();
const location = useLocation();
const platform = (window as any).api?.platform ?? '';
@@ -72,9 +74,9 @@ export default function LoginPage() {
function validate(values: LoginFormValues): FormErrors {
const nextErrors: FormErrors = {};
if (!values.username.trim()) nextErrors.username = '请输入用户名';
if (!values.password.trim()) nextErrors.password = '请输入密码';
if (!values.code.trim()) nextErrors.code = '请输入验证码';
if (!values.username.trim()) nextErrors.username = t('login.usernameRequired');
if (!values.password.trim()) nextErrors.password = t('login.passwordRequired');
if (!values.code.trim()) nextErrors.code = t('login.codeRequired');
return nextErrors;
}
@@ -104,7 +106,7 @@ export default function LoginPage() {
navigate(resolvePostLoginPath(location.state as { from?: string } | null), { replace: true });
} catch (error) {
setErrors({
submit: error instanceof Error ? error.message : '登录失败,请稍后重试',
submit: error instanceof Error ? error.message : t('login.submitFailed'),
});
refreshCaptcha(false);
} finally {
@@ -121,7 +123,7 @@ export default function LoginPage() {
return (
<div
className="h-screen flex flex-col"
className="flex h-screen flex-col"
style={{
backgroundImage: `url(${loginBackground})`,
backgroundSize: '100% 100%',
@@ -132,27 +134,27 @@ export default function LoginPage() {
<TitleBar variant="light" />
<main
className={['box-border pl-2 pr-2 pb-2 flex-auto flex', platform !== 'linux' ? 'pt-2' : 'pt-11'].join(' ')}
className={['box-border flex flex-auto pl-2 pr-2 pb-2', platform !== 'linux' ? 'pt-2' : 'pt-11'].join(' ')}
>
<div className="w-[836px] box-border rounded-2xl border border-black/5 bg-white p-8 shadow-[0_18px_50px_rgba(15,23,42,0.12)] dark:border-[#2a2a2d] dark:bg-[#1b1b1d]">
<div className="box-border w-[836px] rounded-2xl border border-black/5 bg-white p-8 shadow-[0_18px_50px_rgba(15,23,42,0.12)] dark:border-[#2a2a2d] dark:bg-[#1b1b1d]">
<div className="flex items-center">
<img className="w-12 h-12" src={blueLogo} alt="zn-ai" />
<img className="h-12 w-12" src={blueLogo} alt="zn-ai" />
</div>
<div className="mb-6 box-border flex flex-col items-center justify-center pt-10">
<img className="mb-3 h-20 w-20" src={userIcon} alt="" />
<div className="mb-1 text-[24px] leading-[32px] font-medium text-gray-800 dark:text-gray-100">
{t('login.title')}
</div>
<div className="text-[16px] leading-[24px] text-gray-500 dark:text-gray-400">
24
{t('login.subtitle')}
</div>
</div>
<form className="mx-auto flex w-[392px] flex-col gap-4" onSubmit={handleSubmit}>
<div>
<label className="mb-2 block text-[14px] text-gray-600 dark:text-gray-300" htmlFor="login-username">
{t('login.username')}
</label>
<div className={credentialFieldShellClass}>
<User aria-hidden="true" className={credentialFieldIconClass} />
@@ -161,37 +163,37 @@ export default function LoginPage() {
className={credentialFieldInputClass}
autoComplete="username"
disabled={submitting}
placeholder="请输入账号"
placeholder={t('login.usernamePlaceholder')}
value={form.username}
onChange={(event) => handleInputChange('username', event)}
/>
</div>
{errors.username ? <div className="pt-2 text-[12px] text-[#dc2626]">{errors.username}</div> : null}
{errors.username ? <div className="pt-2 text-[12px] text-[#dc2626]">{errors.username}</div> : null}
</div>
<div>
<label className="mb-2 block text-[14px] text-gray-600 dark:text-gray-300" htmlFor="login-password">
{t('login.password')}
</label>
<div className={credentialFieldShellClass}>
<Lock aria-hidden="true" className={credentialFieldIconClass} />
<input
id="login-password"
className={credentialFieldInputClass}
autoComplete="current-password"
disabled={submitting}
placeholder="请输入密码"
type="password"
value={form.password}
onChange={(event) => handleInputChange('password', event)}
/>
<input
id="login-password"
className={credentialFieldInputClass}
autoComplete="current-password"
disabled={submitting}
placeholder={t('login.passwordPlaceholder')}
type="password"
value={form.password}
onChange={(event) => handleInputChange('password', event)}
/>
</div>
{errors.password ? <div className="pt-2 text-[12px] text-[#dc2626]">{errors.password}</div> : null}
</div>
<div>
<label className="mb-2 block text-[14px] text-gray-600 dark:text-gray-300" htmlFor="login-code">
{t('login.verificationCode')}
</label>
<div className="flex gap-3">
<input
@@ -199,7 +201,7 @@ export default function LoginPage() {
className="h-10 min-w-0 flex-1 rounded-[10px] border border-black/10 bg-gray-50 px-4 text-[14px] text-gray-800 outline-none transition focus:border-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#222225] dark:text-gray-100"
autoComplete="off"
disabled={submitting}
placeholder="请输入验证码"
placeholder={t('login.verificationCodePlaceholder')}
value={form.code}
onChange={(event) => handleInputChange('code', event)}
/>
@@ -210,9 +212,13 @@ export default function LoginPage() {
onClick={refreshCaptcha}
>
{captchaUrl ? (
<img className="h-full w-full object-cover" src={captchaUrl} alt="验证码" />
<img
className="h-full w-full object-cover"
src={captchaUrl}
alt={t('login.captchaAlt')}
/>
) : (
<span className="text-[12px] text-[#99A0AE]"></span>
<span className="text-[12px] text-[#99A0AE]">{t('login.loadingCaptcha')}</span>
)}
</button>
</div>
@@ -230,7 +236,7 @@ export default function LoginPage() {
className="mt-4 w-full rounded-lg bg-blue-600 py-2 text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-300"
disabled={submitting}
>
{submitting ? '登录中...' : '登录'}
{submitting ? t('login.submitting') : t('login.submit')}
</button>
</form>
</div>