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,5 +1,4 @@
import { useEffect, useState } from 'react';
import { getLocale, i18n } from '../../i18n';
import { useLocale } from '../../i18n';
import type { LanguageCode } from '../../types/runtime';
type Primitive = string | number;
@@ -216,9 +215,7 @@ function createKnowledgeTranslate(locale: LanguageCode): KnowledgeTranslate {
}
export function useKnowledgeCopy(): KnowledgeTranslate {
const [locale, setLocale] = useState<LanguageCode>(getLocale());
useEffect(() => i18n.subscribe(setLocale), []);
const locale = useLocale();
return createKnowledgeTranslate(locale);
}

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>

View File

@@ -1,3 +1,4 @@
import { useI18n } from '../../../i18n';
import SectionHeader from './SectionHeader';
import { CheckCircleIcon } from './SettingIcons';
@@ -5,40 +6,42 @@ const ACCOUNT_ID = '1234567890';
const LAST_LOGIN_TIME = '2022-11-09 16:24:30';
export default function AccountSettingsPanel() {
const { t } = useI18n();
return (
<section className="flex-1 h-full p-[20px] select-none">
<SectionHeader
title="Account Settings"
description="Please associate PMS and channel room names, smart mapping is available"
title={t('settings.account.title')}
description={t('settings.account.description')}
/>
<div className="w-full flex items-center mt-[20px] py-[20px] box-border border-b border-dashed border-[#E5E8EE] dark:border-gray-700">
<div className="text-[16px] font-medium text-[#171717] dark:text-gray-100 mr-[24px] whitespace-nowrap">
Account
<div className="mt-[20px] box-border flex w-full items-center border-b border-dashed border-[#E5E8EE] py-[20px] dark:border-gray-700">
<div className="mr-[24px] whitespace-nowrap text-[16px] font-medium text-[#171717] dark:text-gray-100">
{t('settings.account.accountLabel')}
</div>
<div className="text-[14px] font-medium text-[#171717] dark:text-gray-100">{ACCOUNT_ID}</div>
</div>
<div className="w-full flex items-center py-[20px] box-border border-b border-dashed border-[#E5E8EE] dark:border-gray-700 gap-3">
<div className="text-[16px] font-medium text-[#171717] dark:text-gray-100 mr-[24px] whitespace-nowrap">
Login Password
<div className="box-border flex w-full items-center gap-3 border-b border-dashed border-[#E5E8EE] py-[20px] dark:border-gray-700">
<div className="mr-[24px] whitespace-nowrap text-[16px] font-medium text-[#171717] dark:text-gray-100">
{t('settings.account.passwordLabel')}
</div>
<div className="text-[14px] text-[#99A0AE] dark:text-gray-500 min-w-0">
Used for investor login operations, last login time: {LAST_LOGIN_TIME}
<div className="min-w-0 text-[14px] text-[#99A0AE] dark:text-gray-500">
{t('settings.account.passwordHelp', { time: LAST_LOGIN_TIME })}
</div>
<div className="border border-[#E5E8EE] dark:border-gray-700 rounded-[6px] px-[6px] py-[4px] flex items-center shrink-0">
<CheckCircleIcon className="w-[16px] h-[16px] text-[#1FC16B]" />
<span className="text-[12px] text-[#525866] dark:text-gray-400 ml-[2px] whitespace-nowrap">
Configured
<div className="flex shrink-0 items-center rounded-[6px] border border-[#E5E8EE] px-[6px] py-[4px] dark:border-gray-700">
<CheckCircleIcon className="h-[16px] w-[16px] text-[#1FC16B]" />
<span className="ml-[2px] whitespace-nowrap text-[12px] text-[#525866] dark:text-gray-400">
{t('settings.account.configured')}
</span>
</div>
<button
type="button"
className="ml-auto text-[14px] text-[#2B7FFF] hover:text-[#1F6AE5] transition-colors whitespace-nowrap"
className="ml-auto whitespace-nowrap text-[14px] text-[#2B7FFF] transition-colors hover:text-[#1F6AE5]"
>
Change Password
{t('settings.account.changePassword')}
</button>
</div>
</section>

View File

@@ -1,4 +1,4 @@
import { t } from '../../../i18n';
import { useI18n } from '../../../i18n';
import { SUPPORTED_LANGUAGE_CODES } from '../../../i18n/constants';
import type { LanguageCode, ThemeMode } from '../../../types/runtime';
import type { SettingUpdateState } from '../useSettingUpdateState';
@@ -24,22 +24,28 @@ const THEME_OPTIONS: Array<{
{ value: 'system', icon: ComputerIcon, labelPath: 'theme.system' },
];
function getUpdateStatusText(updateState: SettingUpdateState) {
function getUpdateStatusText(t: ReturnType<typeof useI18n>['t'], updateState: SettingUpdateState) {
switch (updateState.status) {
case 'checking':
return 'Checking for updates...';
return t('settings.general.checkingForUpdates');
case 'not-available':
return 'You have the latest version';
return t('settings.general.latestVersion');
case 'available':
return `New version available: v${updateState.updateInfo?.version ?? ''}`;
return t('settings.general.newVersionAvailable', {
version: updateState.updateInfo?.version ?? '',
});
case 'downloading':
return `Downloading new version... ${Math.round(updateState.progress?.percent ?? 0)}%`;
return t('settings.general.downloadingVersion', {
percent: Math.round(updateState.progress?.percent ?? 0),
});
case 'downloaded':
return 'Download complete, ready to install';
return t('settings.general.downloadComplete');
case 'error':
return `Update error: ${updateState.error ?? 'Unknown error'}`;
return t('settings.general.updateError', {
error: updateState.error ?? t('common.unknownError'),
});
default:
return 'Check for updates to get latest features';
return t('settings.general.updateHint');
}
}
@@ -50,16 +56,18 @@ export default function GeneralSettingsPanel({
onLanguageChange,
updateState,
}: GeneralSettingsPanelProps) {
const { t } = useI18n();
return (
<section className="flex-1 h-full p-[20px] select-none">
<SectionHeader
title="Basic Settings"
description="Customize the look and feel of the application"
title={t('settings.general.title')}
description={t('settings.general.description')}
/>
<div className="w-full flex items-center mt-[20px] py-[20px] box-border border-b border-dashed border-[#E5E8EE] dark:border-gray-700">
<div className="text-[16px] font-medium text-[#171717] dark:text-gray-100 mr-[24px] whitespace-nowrap">
Theme Settings
<div className="mt-[20px] box-border flex w-full items-center border-b border-dashed border-[#E5E8EE] py-[20px] dark:border-gray-700">
<div className="mr-[24px] whitespace-nowrap text-[16px] font-medium text-[#171717] dark:text-gray-100">
{t('settings.general.themeSection')}
</div>
<div className="flex-1">
<div className="flex flex-wrap gap-3">
@@ -74,13 +82,13 @@ export default function GeneralSettingsPanel({
void onThemeChange(value);
}}
className={[
'px-5 py-1.5 rounded-full border text-[14px] font-medium transition-all duration-200 flex items-center gap-2',
'flex items-center gap-2 rounded-full border px-5 py-1.5 text-[14px] font-medium transition-all duration-200',
active
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500 dark:border-blue-400 text-blue-700 dark:text-blue-300'
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700',
? 'border-blue-500 bg-blue-50 text-blue-700 dark:border-blue-400 dark:bg-blue-900/20 dark:text-blue-300'
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700',
].join(' ')}
>
<Icon className="w-4 h-4" />
<Icon className="h-4 w-4" />
{t(labelPath)}
</button>
);
@@ -89,11 +97,11 @@ export default function GeneralSettingsPanel({
</div>
</div>
<div className="w-full flex items-center mt-[20px] py-[20px] box-border border-b border-dashed border-[#E5E8EE] dark:border-gray-700">
<div className="text-[16px] font-medium text-[#171717] dark:text-gray-100 mr-[24px] whitespace-nowrap">
Language
<div className="mt-[20px] box-border flex w-full items-center border-b border-dashed border-[#E5E8EE] py-[20px] dark:border-gray-700">
<div className="mr-[24px] whitespace-nowrap text-[16px] font-medium text-[#171717] dark:text-gray-100">
{t('settings.general.languageSection')}
</div>
<div className="flex gap-2 flex-wrap">
<div className="flex flex-wrap gap-2">
{SUPPORTED_LANGUAGE_CODES.map((code) => {
const active = language === code;
@@ -105,10 +113,10 @@ export default function GeneralSettingsPanel({
void onLanguageChange(code);
}}
className={[
'px-5 py-1.5 rounded-full border text-[14px] font-medium transition-all duration-200 flex items-center gap-2',
'flex items-center gap-2 rounded-full border px-5 py-1.5 text-[14px] font-medium transition-all duration-200',
active
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500 dark:border-blue-400 text-blue-700 dark:text-blue-300'
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700',
? 'border-blue-500 bg-blue-50 text-blue-700 dark:border-blue-400 dark:bg-blue-900/20 dark:text-blue-300'
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700',
].join(' ')}
>
{t(`language.${code}`)}
@@ -119,11 +127,15 @@ export default function GeneralSettingsPanel({
</div>
<div className="mt-[40px]">
<div className="text-[24px] font-medium text-[#171717] dark:text-gray-100 mb-[24px]">Updates</div>
<div className="mb-[24px] text-[24px] font-medium text-[#171717] dark:text-gray-100">
{t('settings.general.updatesTitle')}
</div>
<div className="flex items-center justify-between mb-[16px] gap-4">
<div className="mb-[16px] flex items-center justify-between gap-4">
<div>
<div className="text-[14px] text-[#525866] dark:text-gray-400 mb-[4px]">Current Version</div>
<div className="mb-[4px] text-[14px] text-[#525866] dark:text-gray-400">
{t('settings.general.currentVersion')}
</div>
<div className="text-[28px] font-bold text-[#171717] dark:text-gray-100">
v{updateState.currentVersion}
</div>
@@ -131,31 +143,29 @@ export default function GeneralSettingsPanel({
<button
type="button"
className="p-[8px] rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
className="rounded-full p-[8px] transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
onClick={() => {
void updateState.checkUpdate();
}}
aria-label="Check for updates"
aria-label={t('settings.general.checkForUpdates')}
>
<RefreshIcon
className={[
'w-5 h-5 text-[#525866] dark:text-gray-400',
'h-5 w-5 text-[#525866] dark:text-gray-400',
updateState.status === 'checking' ? 'animate-spin' : '',
].join(' ')}
/>
</button>
</div>
<div className="bg-[#F5F7FA] dark:bg-gray-800 rounded-[8px] p-[16px] flex items-center justify-between gap-4 mb-[16px]">
<div className="mb-[16px] flex items-center justify-between gap-4 rounded-[8px] bg-[#F5F7FA] p-[16px] dark:bg-gray-800">
<div
className={[
'text-[14px]',
updateState.status === 'error'
? 'text-red-500'
: 'text-[#525866] dark:text-gray-300',
updateState.status === 'error' ? 'text-red-500' : 'text-[#525866] dark:text-gray-300',
].join(' ')}
>
{getUpdateStatusText(updateState)}
{getUpdateStatusText(t, updateState)}
</div>
<div className="shrink-0">
@@ -165,9 +175,9 @@ export default function GeneralSettingsPanel({
onClick={() => {
void updateState.downloadUpdate();
}}
className="px-[14px] py-[8px] rounded-[8px] bg-[#2B7FFF] text-white text-[14px] font-medium hover:bg-[#1F6AE5] transition-colors"
className="rounded-[8px] bg-[#2B7FFF] px-[14px] py-[8px] text-[14px] font-medium text-white transition-colors hover:bg-[#1F6AE5]"
>
Download Update
{t('settings.general.downloadUpdate')}
</button>
) : updateState.status === 'downloaded' ? (
<button
@@ -175,9 +185,9 @@ export default function GeneralSettingsPanel({
onClick={() => {
void updateState.installUpdate();
}}
className="px-[14px] py-[8px] rounded-[8px] bg-[#1FC16B] text-white text-[14px] font-medium hover:bg-[#17A95C] transition-colors"
className="rounded-[8px] bg-[#1FC16B] px-[14px] py-[8px] text-[14px] font-medium text-white transition-colors hover:bg-[#17A95C]"
>
Restart and Install
{t('settings.general.restartAndInstall')}
</button>
) : (
<button
@@ -185,31 +195,31 @@ export default function GeneralSettingsPanel({
onClick={() => {
void updateState.checkUpdate();
}}
className="px-[14px] py-[8px] rounded-[8px] border border-[#E5E8EE] dark:border-gray-600 bg-white dark:bg-gray-700 text-[#171717] dark:text-gray-100 text-[14px] font-medium hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors inline-flex items-center gap-2"
className="inline-flex items-center gap-2 rounded-[8px] border border-[#E5E8EE] bg-white px-[14px] py-[8px] text-[14px] font-medium text-[#171717] transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
>
<RefreshIcon
className={[
'w-4 h-4',
'h-4 w-4',
updateState.status === 'checking' ? 'animate-spin' : '',
].join(' ')}
/>
Check for Updates
{t('settings.general.checkForUpdates')}
</button>
)}
</div>
</div>
<div className="text-[12px] text-[#99A0AE] dark:text-gray-500 mb-[32px]">
When auto-update is enabled, updates will be downloaded and installed automatically.
<div className="mb-[32px] text-[12px] text-[#99A0AE] dark:text-gray-500">
{t('settings.general.autoUpdateHint')}
</div>
<div className="flex items-center justify-between py-[16px] border-b border-[#E5E8EE] dark:border-gray-800 gap-4">
<div className="flex items-center justify-between gap-4 border-b border-[#E5E8EE] py-[16px] dark:border-gray-800">
<div>
<div className="text-[16px] text-[#171717] dark:text-gray-100 mb-[4px]">
Auto check for updates
<div className="mb-[4px] text-[16px] text-[#171717] dark:text-gray-100">
{t('settings.general.autoCheckTitle')}
</div>
<div className="text-[14px] text-[#99A0AE] dark:text-gray-500">
Check for updates on startup
{t('settings.general.autoCheckDescription')}
</div>
</div>
@@ -219,13 +229,13 @@ export default function GeneralSettingsPanel({
/>
</div>
<div className="flex items-center justify-between py-[16px] gap-4">
<div className="flex items-center justify-between gap-4 py-[16px]">
<div>
<div className="text-[16px] text-[#171717] dark:text-gray-100 mb-[4px]">
Auto download updates
<div className="mb-[4px] text-[16px] text-[#171717] dark:text-gray-100">
{t('settings.general.autoDownloadTitle')}
</div>
<div className="text-[14px] text-[#99A0AE] dark:text-gray-500">
Automatically download and install updates
{t('settings.general.autoDownloadDescription')}
</div>
</div>

View File

@@ -1,3 +1,4 @@
import { useI18n } from '../../../i18n';
import { SettingsIcon, UserIcon } from './SettingIcons';
export type SettingView = 'account' | 'general';
@@ -9,27 +10,31 @@ type SettingMenuProps = {
const MENU_ITEMS: Array<{
id: SettingView;
label: string;
labelKey: 'settings.menu.account' | 'settings.menu.general';
Icon: typeof UserIcon;
}> = [
{
id: 'account',
label: 'Account',
labelKey: 'settings.menu.account',
Icon: UserIcon,
},
{
id: 'general',
label: 'General',
labelKey: 'settings.menu.general',
Icon: SettingsIcon,
},
];
export default function SettingMenu({ currentView, onChange }: SettingMenuProps) {
return (
<aside className="w-[160px] h-full box-border border-r border-[#E5E8EE] dark:border-gray-700 py-[12px] px-[8px] flex flex-col gap-[4px] select-none shrink-0">
<div className="text-[12px] text-[#99A0AE] dark:text-gray-500 p-[4px]">System Settings</div>
const { t } = useI18n();
{MENU_ITEMS.map(({ id, label, Icon }) => {
return (
<aside className="box-border flex h-full w-[160px] shrink-0 flex-col gap-[4px] border-r border-[#E5E8EE] px-[8px] py-[12px] select-none dark:border-gray-700">
<div className="p-[4px] text-[12px] text-[#99A0AE] dark:text-gray-500">
{t('settings.menu.systemSettings')}
</div>
{MENU_ITEMS.map(({ id, labelKey, Icon }) => {
const active = currentView === id;
return (
@@ -38,19 +43,19 @@ export default function SettingMenu({ currentView, onChange }: SettingMenuProps)
type="button"
onClick={() => onChange(id)}
className={[
'box-border flex items-center py-[10px] px-[12px] rounded-[6px] cursor-pointer transition-colors',
'box-border flex cursor-pointer items-center rounded-[6px] px-[12px] py-[10px] transition-colors',
active ? 'bg-[#EFF6FF] dark:bg-[#222225]' : 'hover:bg-[#EFF6FF] dark:hover:bg-[#222225]',
].join(' ')}
>
<Icon
className="w-[20px] h-[20px]"
className="h-[20px] w-[20px]"
style={{ color: active ? '#2B7FFF' : '#525866' }}
/>
<span
className="box-border px-[8px] text-[14px] font-medium dark:text-gray-300"
style={{ color: active ? '#2B7FFF' : '#525866' }}
>
{label}
{t(labelKey)}
</span>
</button>
);

View File

@@ -1,5 +1,4 @@
import { useEffect, useState } from 'react';
import { getLocale, i18n } from '../../i18n';
import { useLocale } from '../../i18n';
import type { LanguageCode } from '../../types/runtime';
import { SKILLS_MESSAGES } from './messages';
@@ -46,9 +45,7 @@ function createSkillsTranslate(locale: LanguageCode): SkillsTranslate {
}
export function useSkillsCopy(): SkillsTranslate {
const [locale, setLocale] = useState<LanguageCode>(getLocale());
useEffect(() => i18n.subscribe(setLocale), []);
const locale = useLocale();
return createSkillsTranslate(locale);
}