fix: defult language

This commit is contained in:
Supra4E8C
2025-12-29 00:17:44 +08:00
parent 5ef3406068
commit 769c05e459
5 changed files with 80 additions and 17 deletions

View File

@@ -6,14 +6,14 @@ import i18n from 'i18next';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import zhCN from './locales/zh-CN.json'; import zhCN from './locales/zh-CN.json';
import en from './locales/en.json'; import en from './locales/en.json';
import { STORAGE_KEY_LANGUAGE } from '@/utils/constants'; import { getInitialLanguage } from '@/utils/language';
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
resources: { resources: {
'zh-CN': { translation: zhCN }, 'zh-CN': { translation: zhCN },
en: { translation: en } en: { translation: en }
}, },
lng: localStorage.getItem(STORAGE_KEY_LANGUAGE) || 'zh-CN', lng: getInitialLanguage(),
fallbackLng: 'zh-CN', fallbackLng: 'zh-CN',
interpolation: { interpolation: {
escapeValue: false // React 已经转义 escapeValue: false // React 已经转义

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { IconEye, IconEyeOff } from '@/components/ui/icons'; import { IconEye, IconEyeOff } from '@/components/ui/icons';
import { useAuthStore, useNotificationStore } from '@/stores'; import { useAuthStore, useLanguageStore, useNotificationStore } from '@/stores';
import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection'; import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection';
export function LoginPage() { export function LoginPage() {
@@ -12,6 +12,8 @@ export function LoginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { showNotification } = useNotificationStore(); const { showNotification } = useNotificationStore();
const language = useLanguageStore((state) => state.language);
const toggleLanguage = useLanguageStore((state) => state.toggleLanguage);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const login = useAuthStore((state) => state.login); const login = useAuthStore((state) => state.login);
const restoreSession = useAuthStore((state) => state.restoreSession); const restoreSession = useAuthStore((state) => state.restoreSession);
@@ -27,6 +29,7 @@ export function LoginPage() {
const [error, setError] = useState(''); const [error, setError] = useState('');
const detectedBase = useMemo(() => detectApiBaseFromLocation(), []); const detectedBase = useMemo(() => detectApiBaseFromLocation(), []);
const nextLanguageLabel = language === 'zh-CN' ? t('language.english') : t('language.chinese');
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
@@ -49,10 +52,6 @@ export function LoginPage() {
return <Navigate to={redirect} replace />; return <Navigate to={redirect} replace />;
} }
const handleUseCurrent = () => {
setApiBase(detectedBase);
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (!managementKey.trim()) { if (!managementKey.trim()) {
setError(t('login.error_required')); setError(t('login.error_required'));
@@ -79,7 +78,20 @@ export function LoginPage() {
<div className="login-page"> <div className="login-page">
<div className="login-card"> <div className="login-card">
<div className="login-header"> <div className="login-header">
<div className="login-title-row">
<div className="title">{t('title.login')}</div> <div className="title">{t('title.login')}</div>
<Button
type="button"
variant="ghost"
size="sm"
className="login-language-btn"
onClick={toggleLanguage}
title={t('language.switch')}
aria-label={t('language.switch')}
>
{nextLanguageLabel}
</Button>
</div>
<div className="subtitle">{t('login.subtitle')}</div> <div className="subtitle">{t('login.subtitle')}</div>
</div> </div>
@@ -136,14 +148,9 @@ export function LoginPage() {
} }
/> />
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Button variant="secondary" onClick={handleUseCurrent}>
{t('login.use_current_address')}
</Button>
<Button fullWidth onClick={handleSubmit} loading={loading}> <Button fullWidth onClick={handleSubmit} loading={loading}>
{loading ? t('login.submitting') : t('login.submit_button')} {loading ? t('login.submitting') : t('login.submit_button')}
</Button> </Button>
</div>
{error && <div className="error-box">{error}</div>} {error && <div className="error-box">{error}</div>}

View File

@@ -8,6 +8,7 @@ import { persist } from 'zustand/middleware';
import type { Language } from '@/types'; import type { Language } from '@/types';
import { STORAGE_KEY_LANGUAGE } from '@/utils/constants'; import { STORAGE_KEY_LANGUAGE } from '@/utils/constants';
import i18n from '@/i18n'; import i18n from '@/i18n';
import { getInitialLanguage } from '@/utils/language';
interface LanguageState { interface LanguageState {
language: Language; language: Language;
@@ -18,7 +19,7 @@ interface LanguageState {
export const useLanguageStore = create<LanguageState>()( export const useLanguageStore = create<LanguageState>()(
persist( persist(
(set, get) => ({ (set, get) => ({
language: 'zh-CN', language: getInitialLanguage(),
setLanguage: (language) => { setLanguage: (language) => {
// 切换 i18next 语言 // 切换 i18next 语言

View File

@@ -431,6 +431,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $spacing-sm; gap: $spacing-sm;
text-align: center;
.title { .title {
font-size: 22px; font-size: 22px;
@@ -443,6 +444,18 @@
} }
} }
.login-title-row {
display: flex;
align-items: center;
justify-content: center;
gap: $spacing-sm;
flex-wrap: wrap;
}
.login-language-btn {
white-space: nowrap;
}
.connection-box { .connection-box {
background: var(--bg-secondary); background: var(--bg-secondary);
border: 1px dashed var(--border-color); border: 1px dashed var(--border-color);

42
src/utils/language.ts Normal file
View File

@@ -0,0 +1,42 @@
import type { Language } from '@/types';
import { STORAGE_KEY_LANGUAGE } from '@/utils/constants';
const parseStoredLanguage = (value: string): Language | null => {
try {
const parsed = JSON.parse(value);
const candidate = parsed?.state?.language ?? parsed?.language ?? parsed;
if (candidate === 'zh-CN' || candidate === 'en') {
return candidate;
}
} catch {
if (value === 'zh-CN' || value === 'en') {
return value;
}
}
return null;
};
const getStoredLanguage = (): Language | null => {
if (typeof window === 'undefined') {
return null;
}
try {
const stored = localStorage.getItem(STORAGE_KEY_LANGUAGE);
if (!stored) {
return null;
}
return parseStoredLanguage(stored);
} catch {
return null;
}
};
const getBrowserLanguage = (): Language => {
if (typeof navigator === 'undefined') {
return 'zh-CN';
}
const raw = navigator.languages?.[0] || navigator.language || 'zh-CN';
return raw.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en';
};
export const getInitialLanguage = (): Language => getStoredLanguage() ?? getBrowserLanguage();