From 700bff1d03b93737229baf6e6c5f686e6f8b04e1 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 7 Feb 2026 00:43:36 +0800 Subject: [PATCH] fix(i18n): harden language switching and enforce language list consistency --- src/components/layout/MainLayout.tsx | 8 ++++++-- src/pages/LoginPage.tsx | 9 +++++++-- src/stores/useLanguageStore.ts | 20 +++++++++++++++++--- src/utils/constants.ts | 8 ++++++-- src/utils/language.ts | 11 +++++++---- 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index d4802bb..233150c 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -36,7 +36,7 @@ import { import { configApi, versionApi } from '@/services/api'; import { triggerHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { LANGUAGE_LABEL_KEYS, LANGUAGE_ORDER } from '@/utils/constants'; -import type { Language } from '@/types'; +import { isSupportedLanguage } from '@/utils/language'; const sidebarIcons: Record = { dashboard: , @@ -196,7 +196,11 @@ export function MainLayout() { const handleLanguageChange = useCallback( (event: React.ChangeEvent) => { - setLanguage(event.target.value as Language); + const selectedLanguage = event.target.value; + if (!isSupportedLanguage(selectedLanguage)) { + return; + } + setLanguage(selectedLanguage); }, [setLanguage] ); diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 829bfe6..8e8daf1 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -7,8 +7,9 @@ import { IconEye, IconEyeOff } from '@/components/ui/icons'; import { useAuthStore, useLanguageStore, useNotificationStore } from '@/stores'; import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection'; import { LANGUAGE_LABEL_KEYS, LANGUAGE_ORDER } from '@/utils/constants'; +import { isSupportedLanguage } from '@/utils/language'; import { INLINE_LOGO_JPEG } from '@/assets/logoInline'; -import type { ApiError, Language } from '@/types'; +import type { ApiError } from '@/types'; import styles from './LoginPage.module.scss'; /** @@ -81,7 +82,11 @@ export function LoginPage() { const detectedBase = useMemo(() => detectApiBaseFromLocation(), []); const handleLanguageChange = useCallback( (event: React.ChangeEvent) => { - setLanguage(event.target.value as Language); + const selectedLanguage = event.target.value; + if (!isSupportedLanguage(selectedLanguage)) { + return; + } + setLanguage(selectedLanguage); }, [setLanguage] ); diff --git a/src/stores/useLanguageStore.ts b/src/stores/useLanguageStore.ts index e80183e..7b6eb23 100644 --- a/src/stores/useLanguageStore.ts +++ b/src/stores/useLanguageStore.ts @@ -8,11 +8,11 @@ import { persist } from 'zustand/middleware'; import type { Language } from '@/types'; import { LANGUAGE_ORDER, STORAGE_KEY_LANGUAGE } from '@/utils/constants'; import i18n from '@/i18n'; -import { getInitialLanguage } from '@/utils/language'; +import { getInitialLanguage, isSupportedLanguage } from '@/utils/language'; interface LanguageState { language: Language; - setLanguage: (language: Language) => void; + setLanguage: (language: string) => void; toggleLanguage: () => void; } @@ -22,6 +22,9 @@ export const useLanguageStore = create()( language: getInitialLanguage(), setLanguage: (language) => { + if (!isSupportedLanguage(language)) { + return; + } // 切换 i18next 语言 i18n.changeLanguage(language); set({ language }); @@ -35,7 +38,18 @@ export const useLanguageStore = create()( } }), { - name: STORAGE_KEY_LANGUAGE + name: STORAGE_KEY_LANGUAGE, + merge: (persistedState, currentState) => { + const nextLanguage = (persistedState as Partial)?.language; + if (typeof nextLanguage === 'string' && isSupportedLanguage(nextLanguage)) { + return { + ...currentState, + ...(persistedState as Partial), + language: nextLanguage + }; + } + return currentState; + } } ) ); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index a15163d..e3b36c5 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -5,6 +5,10 @@ import type { Language } from '@/types'; +const defineLanguageOrder = ( + languages: T & ([Language] extends [T[number]] ? unknown : never) +) => languages; + // 缓存过期时间(毫秒) export const CACHE_EXPIRY_MS = 30 * 1000; // 与基线保持一致,减少管理端压力 @@ -36,13 +40,13 @@ export const STORAGE_KEY_SIDEBAR = 'cli-proxy-sidebar-collapsed'; export const STORAGE_KEY_AUTH_FILES_PAGE_SIZE = 'cli-proxy-auth-files-page-size'; // 语言配置 -export const LANGUAGE_ORDER: Language[] = ['zh-CN', 'en', 'ru']; +export const LANGUAGE_ORDER = defineLanguageOrder(['zh-CN', 'en', 'ru'] as const); export const LANGUAGE_LABEL_KEYS: Record = { 'zh-CN': 'language.chinese', en: 'language.english', ru: 'language.russian' }; -export const SUPPORTED_LANGUAGES: Language[] = LANGUAGE_ORDER; +export const SUPPORTED_LANGUAGES = LANGUAGE_ORDER; // 通知持续时间 export const NOTIFICATION_DURATION_MS = 3000; diff --git a/src/utils/language.ts b/src/utils/language.ts index 8aa1767..ea8b2cc 100644 --- a/src/utils/language.ts +++ b/src/utils/language.ts @@ -1,16 +1,19 @@ import type { Language } from '@/types'; import { STORAGE_KEY_LANGUAGE, SUPPORTED_LANGUAGES } from '@/utils/constants'; +export const isSupportedLanguage = (value: string): value is Language => + SUPPORTED_LANGUAGES.includes(value as Language); + const parseStoredLanguage = (value: string): Language | null => { try { const parsed = JSON.parse(value); const candidate = parsed?.state?.language ?? parsed?.language ?? parsed; - if (SUPPORTED_LANGUAGES.includes(candidate as Language)) { - return candidate as Language; + if (typeof candidate === 'string' && isSupportedLanguage(candidate)) { + return candidate; } } catch { - if (SUPPORTED_LANGUAGES.includes(value as Language)) { - return value as Language; + if (isSupportedLanguage(value)) { + return value; } } return null;