mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 02:30:51 +08:00
fix(i18n): switch language via popover menu and complete Russian Kimi translations
This commit is contained in:
@@ -194,26 +194,17 @@ export function MainLayout() {
|
||||
const language = useLanguageStore((state) => state.language);
|
||||
const setLanguage = useLanguageStore((state) => state.setLanguage);
|
||||
|
||||
const handleLanguageChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedLanguage = event.target.value;
|
||||
if (!isSupportedLanguage(selectedLanguage)) {
|
||||
return;
|
||||
}
|
||||
setLanguage(selectedLanguage);
|
||||
},
|
||||
[setLanguage]
|
||||
);
|
||||
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [checkingVersion, setCheckingVersion] = useState(false);
|
||||
const [languageMenuOpen, setLanguageMenuOpen] = useState(false);
|
||||
const [brandExpanded, setBrandExpanded] = useState(true);
|
||||
const [requestLogModalOpen, setRequestLogModalOpen] = useState(false);
|
||||
const [requestLogDraft, setRequestLogDraft] = useState(false);
|
||||
const [requestLogTouched, setRequestLogTouched] = useState(false);
|
||||
const [requestLogSaving, setRequestLogSaving] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
const languageMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const brandCollapseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const headerRef = useRef<HTMLElement | null>(null);
|
||||
const versionTapCount = useRef(0);
|
||||
@@ -313,6 +304,32 @@ export function MainLayout() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!languageMenuOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePointerDown = (event: MouseEvent) => {
|
||||
if (!languageMenuRef.current?.contains(event.target as Node)) {
|
||||
setLanguageMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setLanguageMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handlePointerDown);
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handlePointerDown);
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [languageMenuOpen]);
|
||||
|
||||
const handleBrandClick = useCallback(() => {
|
||||
if (!brandExpanded) {
|
||||
setBrandExpanded(true);
|
||||
@@ -332,6 +349,21 @@ export function MainLayout() {
|
||||
setRequestLogModalOpen(true);
|
||||
}, [requestLogEnabled]);
|
||||
|
||||
const toggleLanguageMenu = useCallback(() => {
|
||||
setLanguageMenuOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleLanguageSelect = useCallback(
|
||||
(nextLanguage: string) => {
|
||||
if (!isSupportedLanguage(nextLanguage)) {
|
||||
return;
|
||||
}
|
||||
setLanguage(nextLanguage);
|
||||
setLanguageMenuOpen(false);
|
||||
},
|
||||
[setLanguage]
|
||||
);
|
||||
|
||||
const handleRequestLogClose = useCallback(() => {
|
||||
setRequestLogModalOpen(false);
|
||||
setRequestLogTouched(false);
|
||||
@@ -580,22 +612,35 @@ export function MainLayout() {
|
||||
>
|
||||
{headerIcons.update}
|
||||
</Button>
|
||||
<div className="language-select-wrapper" title={t('language.switch')}>
|
||||
<span className="language-select-icon" aria-hidden="true">
|
||||
{headerIcons.language}
|
||||
</span>
|
||||
<select
|
||||
className="language-select"
|
||||
value={language}
|
||||
onChange={handleLanguageChange}
|
||||
<div className={`language-menu ${languageMenuOpen ? 'open' : ''}`} ref={languageMenuRef}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleLanguageMenu}
|
||||
title={t('language.switch')}
|
||||
aria-label={t('language.switch')}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={languageMenuOpen}
|
||||
>
|
||||
{LANGUAGE_ORDER.map((lang) => (
|
||||
<option key={lang} value={lang}>
|
||||
{t(LANGUAGE_LABEL_KEYS[lang])}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{headerIcons.language}
|
||||
</Button>
|
||||
{languageMenuOpen && (
|
||||
<div className="notification entering language-menu-popover" role="menu" aria-label={t('language.switch')}>
|
||||
{LANGUAGE_ORDER.map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
className={`language-menu-option ${language === lang ? 'active' : ''}`}
|
||||
onClick={() => handleLanguageSelect(lang)}
|
||||
role="menuitemradio"
|
||||
aria-checked={language === lang}
|
||||
>
|
||||
<span>{t(LANGUAGE_LABEL_KEYS[lang])}</span>
|
||||
{language === lang ? <span className="language-menu-check">✓</span> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={cycleTheme} title={t('theme.switch')}>
|
||||
{theme === 'auto'
|
||||
|
||||
@@ -376,6 +376,7 @@
|
||||
"filter_qwen": "Qwen",
|
||||
"filter_gemini": "Gemini",
|
||||
"filter_gemini-cli": "GeminiCLI",
|
||||
"filter_kimi": "Kimi",
|
||||
"filter_aistudio": "AIStudio",
|
||||
"filter_claude": "Claude",
|
||||
"filter_codex": "Codex",
|
||||
@@ -387,6 +388,7 @@
|
||||
"type_qwen": "Qwen",
|
||||
"type_gemini": "Gemini",
|
||||
"type_gemini-cli": "GeminiCLI",
|
||||
"type_kimi": "Kimi",
|
||||
"type_aistudio": "AIStudio",
|
||||
"type_claude": "Claude",
|
||||
"type_codex": "Codex",
|
||||
@@ -640,6 +642,17 @@
|
||||
"gemini_cli_oauth_status_error": "Ошибка аутентификации:",
|
||||
"gemini_cli_oauth_start_error": "Не удалось запустить Gemini CLI OAuth:",
|
||||
"gemini_cli_oauth_polling_error": "Не удалось проверить статус аутентификации:",
|
||||
"kimi_oauth_title": "Kimi OAuth",
|
||||
"kimi_oauth_button": "Начать вход Kimi",
|
||||
"kimi_oauth_hint": "Выполните вход в сервис Kimi через поток авторизации устройства и автоматически получите/сохраните файлы авторизации.",
|
||||
"kimi_oauth_url_label": "URL авторизации:",
|
||||
"kimi_open_link": "Открыть ссылку",
|
||||
"kimi_copy_link": "Скопировать ссылку",
|
||||
"kimi_oauth_status_waiting": "Ожидание аутентификации...",
|
||||
"kimi_oauth_status_success": "Аутентификация успешна!",
|
||||
"kimi_oauth_status_error": "Ошибка аутентификации:",
|
||||
"kimi_oauth_start_error": "Не удалось запустить Kimi OAuth:",
|
||||
"kimi_oauth_polling_error": "Не удалось проверить статус аутентификации:",
|
||||
"qwen_oauth_title": "Qwen OAuth",
|
||||
"qwen_oauth_button": "Начать вход Qwen",
|
||||
"qwen_oauth_hint": "Выполните вход в сервис Qwen через поток авторизации устройства и автоматически получите/сохраните файлы авторизации.",
|
||||
|
||||
@@ -190,38 +190,64 @@
|
||||
gap: $spacing-xs;
|
||||
flex-shrink: 0;
|
||||
|
||||
.language-select-wrapper {
|
||||
.language-menu {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--text-secondary);
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
.language-menu-popover {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
z-index: $z-dropdown;
|
||||
min-width: 164px;
|
||||
padding: $spacing-xs;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.language-select-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.language-menu-option {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: $radius-sm;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: background-color $transition-fast, color $transition-fast;
|
||||
|
||||
.language-select {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-md;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
&:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
background: var(--bg-secondary);
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.language-menu-check {
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
.language-menu-popover {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user