fix(i18n): harden language switching and enforce language list consistency

This commit is contained in:
LTbinglingfeng
2026-02-07 00:43:36 +08:00
parent 680b24026c
commit 700bff1d03
5 changed files with 43 additions and 13 deletions

View File

@@ -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<string, ReactNode> = {
dashboard: <IconLayoutDashboard size={18} />,
@@ -196,7 +196,11 @@ export function MainLayout() {
const handleLanguageChange = useCallback(
(event: React.ChangeEvent<HTMLSelectElement>) => {
setLanguage(event.target.value as Language);
const selectedLanguage = event.target.value;
if (!isSupportedLanguage(selectedLanguage)) {
return;
}
setLanguage(selectedLanguage);
},
[setLanguage]
);

View File

@@ -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<HTMLSelectElement>) => {
setLanguage(event.target.value as Language);
const selectedLanguage = event.target.value;
if (!isSupportedLanguage(selectedLanguage)) {
return;
}
setLanguage(selectedLanguage);
},
[setLanguage]
);

View File

@@ -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<LanguageState>()(
language: getInitialLanguage(),
setLanguage: (language) => {
if (!isSupportedLanguage(language)) {
return;
}
// 切换 i18next 语言
i18n.changeLanguage(language);
set({ language });
@@ -35,7 +38,18 @@ export const useLanguageStore = create<LanguageState>()(
}
}),
{
name: STORAGE_KEY_LANGUAGE
name: STORAGE_KEY_LANGUAGE,
merge: (persistedState, currentState) => {
const nextLanguage = (persistedState as Partial<LanguageState>)?.language;
if (typeof nextLanguage === 'string' && isSupportedLanguage(nextLanguage)) {
return {
...currentState,
...(persistedState as Partial<LanguageState>),
language: nextLanguage
};
}
return currentState;
}
}
)
);

View File

@@ -5,6 +5,10 @@
import type { Language } from '@/types';
const defineLanguageOrder = <T extends readonly Language[]>(
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<Language, string> = {
'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;

View File

@@ -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;