diff --git a/src/i18n/index.ts b/src/i18n/index.ts
index 0a71883..600d753 100644
--- a/src/i18n/index.ts
+++ b/src/i18n/index.ts
@@ -6,14 +6,14 @@ import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import zhCN from './locales/zh-CN.json';
import en from './locales/en.json';
-import { STORAGE_KEY_LANGUAGE } from '@/utils/constants';
+import { getInitialLanguage } from '@/utils/language';
i18n.use(initReactI18next).init({
resources: {
'zh-CN': { translation: zhCN },
en: { translation: en }
},
- lng: localStorage.getItem(STORAGE_KEY_LANGUAGE) || 'zh-CN',
+ lng: getInitialLanguage(),
fallbackLng: 'zh-CN',
interpolation: {
escapeValue: false // React 已经转义
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
index 7b4bca4..140da2c 100644
--- a/src/pages/LoginPage.tsx
+++ b/src/pages/LoginPage.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { IconEye, IconEyeOff } from '@/components/ui/icons';
-import { useAuthStore, useNotificationStore } from '@/stores';
+import { useAuthStore, useLanguageStore, useNotificationStore } from '@/stores';
import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection';
export function LoginPage() {
@@ -12,6 +12,8 @@ export function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const { showNotification } = useNotificationStore();
+ const language = useLanguageStore((state) => state.language);
+ const toggleLanguage = useLanguageStore((state) => state.toggleLanguage);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const login = useAuthStore((state) => state.login);
const restoreSession = useAuthStore((state) => state.restoreSession);
@@ -27,6 +29,7 @@ export function LoginPage() {
const [error, setError] = useState('');
const detectedBase = useMemo(() => detectApiBaseFromLocation(), []);
+ const nextLanguageLabel = language === 'zh-CN' ? t('language.english') : t('language.chinese');
useEffect(() => {
const init = async () => {
@@ -49,10 +52,6 @@ export function LoginPage() {
return ;
}
- const handleUseCurrent = () => {
- setApiBase(detectedBase);
- };
-
const handleSubmit = async () => {
if (!managementKey.trim()) {
setError(t('login.error_required'));
@@ -79,7 +78,20 @@ export function LoginPage() {
-
{t('title.login')}
+
+
{t('title.login')}
+
+
{t('login.subtitle')}
@@ -136,14 +148,9 @@ export function LoginPage() {
}
/>
-
-
-
-
+
{error &&
{error}
}
diff --git a/src/stores/useLanguageStore.ts b/src/stores/useLanguageStore.ts
index 794a6a4..9de3bed 100644
--- a/src/stores/useLanguageStore.ts
+++ b/src/stores/useLanguageStore.ts
@@ -8,6 +8,7 @@ import { persist } from 'zustand/middleware';
import type { Language } from '@/types';
import { STORAGE_KEY_LANGUAGE } from '@/utils/constants';
import i18n from '@/i18n';
+import { getInitialLanguage } from '@/utils/language';
interface LanguageState {
language: Language;
@@ -18,7 +19,7 @@ interface LanguageState {
export const useLanguageStore = create
()(
persist(
(set, get) => ({
- language: 'zh-CN',
+ language: getInitialLanguage(),
setLanguage: (language) => {
// 切换 i18next 语言
diff --git a/src/styles/layout.scss b/src/styles/layout.scss
index 2c24c04..c9a7710 100644
--- a/src/styles/layout.scss
+++ b/src/styles/layout.scss
@@ -431,6 +431,7 @@
display: flex;
flex-direction: column;
gap: $spacing-sm;
+ text-align: center;
.title {
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 {
background: var(--bg-secondary);
border: 1px dashed var(--border-color);
diff --git a/src/utils/language.ts b/src/utils/language.ts
new file mode 100644
index 0000000..e4775cc
--- /dev/null
+++ b/src/utils/language.ts
@@ -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();