Files
new-api/web/src/hooks/common/useHeaderBar.js
CaIon c444746088 feat(i18n): add backend multi-language support with user language preference
- Add go-i18n library for internationalization
- Create i18n package with translation keys and YAML locale files (zh/en)
- Implement i18n middleware for language detection from user settings and Accept-Language header
- Add Language field to UserSetting DTO
- Update API response helpers with i18n support (ApiErrorI18n, ApiSuccessI18n)
- Migrate hardcoded messages in token, redemption, and user controllers
- Add frontend language preference settings component
- Sync language preference across header selector and user settings
- Auto-restore user language preference on login
2026-02-05 00:09:32 +08:00

238 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useState, useEffect, useContext, useCallback, useMemo } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { UserContext } from '../../context/User';
import { StatusContext } from '../../context/Status';
import { useSetTheme, useTheme, useActualTheme } from '../../context/Theme';
import { getLogo, getSystemName, API, showSuccess } from '../../helpers';
import { useIsMobile } from './useIsMobile';
import { useSidebarCollapsed } from './useSidebarCollapsed';
import { useMinimumLoadingTime } from './useMinimumLoadingTime';
export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
const { t, i18n } = useTranslation();
const [userState, userDispatch] = useContext(UserContext);
const [statusState] = useContext(StatusContext);
const isMobile = useIsMobile();
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
const [logoLoaded, setLogoLoaded] = useState(false);
const navigate = useNavigate();
const [currentLang, setCurrentLang] = useState(i18n.language);
const location = useLocation();
const loading = statusState?.status === undefined;
const isLoading = useMinimumLoadingTime(loading, 200);
const systemName = getSystemName();
const logo = getLogo();
const currentDate = new Date();
const isNewYear = currentDate.getMonth() === 0 && currentDate.getDate() === 1;
const isSelfUseMode = statusState?.status?.self_use_mode_enabled || false;
const docsLink = statusState?.status?.docs_link || '';
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
// 获取顶栏模块配置
const headerNavModulesConfig = statusState?.status?.HeaderNavModules;
// 使用useMemo确保headerNavModules正确响应statusState变化
const headerNavModules = useMemo(() => {
if (headerNavModulesConfig) {
try {
const modules = JSON.parse(headerNavModulesConfig);
// 处理向后兼容性如果pricing是boolean转换为对象格式
if (typeof modules.pricing === 'boolean') {
modules.pricing = {
enabled: modules.pricing,
requireAuth: false, // 默认不需要登录鉴权
};
}
return modules;
} catch (error) {
console.error('解析顶栏模块配置失败:', error);
return null;
}
}
return null;
}, [headerNavModulesConfig]);
// 获取模型广场权限配置
const pricingRequireAuth = useMemo(() => {
if (headerNavModules?.pricing) {
return typeof headerNavModules.pricing === 'object'
? headerNavModules.pricing.requireAuth
: false; // 默认不需要登录
}
return false; // 默认不需要登录
}, [headerNavModules]);
const isConsoleRoute = location.pathname.startsWith('/console');
const theme = useTheme();
const actualTheme = useActualTheme();
const setTheme = useSetTheme();
// Logo loading effect
useEffect(() => {
setLogoLoaded(false);
if (!logo) return;
const img = new Image();
img.src = logo;
img.onload = () => setLogoLoaded(true);
}, [logo]);
// Send theme to iframe
useEffect(() => {
try {
const iframe = document.querySelector('iframe');
const cw = iframe && iframe.contentWindow;
if (cw) {
cw.postMessage({ themeMode: actualTheme }, '*');
}
} catch (e) {
// Silently ignore cross-origin or access errors
}
}, [actualTheme]);
// Language change effect
useEffect(() => {
const handleLanguageChanged = (lng) => {
setCurrentLang(lng);
try {
const iframe = document.querySelector('iframe');
const cw = iframe && iframe.contentWindow;
if (cw) {
cw.postMessage({ lang: lng }, '*');
}
} catch (e) {
// Silently ignore cross-origin or access errors
}
};
i18n.on('languageChanged', handleLanguageChanged);
return () => {
i18n.off('languageChanged', handleLanguageChanged);
};
}, [i18n]);
// Actions
const logout = useCallback(async () => {
await API.get('/api/user/logout');
showSuccess(t('注销成功!'));
userDispatch({ type: 'logout' });
localStorage.removeItem('user');
navigate('/login');
}, [navigate, t, userDispatch]);
const handleLanguageChange = useCallback(
async (lang) => {
// Change language immediately for responsive UX
i18n.changeLanguage(lang);
// If user is logged in, save preference to backend
if (userState?.user?.id) {
try {
const res = await API.put('/api/user/self', {
language: lang,
});
if (res.data.success) {
// Update user context with new setting
if (userState?.user?.setting) {
try {
const settings = JSON.parse(userState.user.setting);
settings.language = lang;
userDispatch({
type: 'login',
payload: {
...userState.user,
setting: JSON.stringify(settings),
},
});
} catch (e) {
// Ignore parse errors
}
}
}
} catch (error) {
// Silently ignore errors - language was already changed locally
console.error('Failed to save language preference:', error);
}
}
},
[i18n, userState, userDispatch],
);
const handleThemeToggle = useCallback(
(newTheme) => {
if (
!newTheme ||
(newTheme !== 'light' && newTheme !== 'dark' && newTheme !== 'auto')
) {
return;
}
setTheme(newTheme);
},
[setTheme],
);
const handleMobileMenuToggle = useCallback(() => {
if (isMobile) {
onMobileMenuToggle();
} else {
toggleCollapsed();
}
}, [isMobile, onMobileMenuToggle, toggleCollapsed]);
return {
// State
userState,
statusState,
isMobile,
collapsed,
logoLoaded,
currentLang,
location,
isLoading,
systemName,
logo,
isNewYear,
isSelfUseMode,
docsLink,
isDemoSiteMode,
isConsoleRoute,
theme,
drawerOpen,
headerNavModules,
pricingRequireAuth,
// Actions
logout,
handleLanguageChange,
handleThemeToggle,
handleMobileMenuToggle,
navigate,
t,
};
};