管理员可在系统设置页面配置全局默认语言,未登录用户和未设置 语言偏好的已登录用户将强制使用该语言,忽略浏览器语言检测。 - common/constants.go: 新增 DefaultLanguage 变量 - model/option.go: 新增 DefaultLanguage option handler - controller/misc.go: /api/status 暴露 default_language - SystemSetting.jsx: 添加语言下拉框到通用设置 - PageLayout.jsx: 未登录用户应用默认语言 - UserContext.jsx: 无偏好用户回退到默认语言 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>master
| @@ -16,6 +16,7 @@ var SystemName = "New API" | |||||
| var Footer = "" | var Footer = "" | ||||
| var Logo = "" | var Logo = "" | ||||
| var TopUpLink = "" | var TopUpLink = "" | ||||
| var DefaultLanguage = "" // admin-configured default language; empty = follow browser detection | |||||
| // var ChatLink = "" | // var ChatLink = "" | ||||
| // var ChatLink2 = "" | // var ChatLink2 = "" | ||||
| @@ -87,6 +87,7 @@ func GetStatus(c *gin.Context) { | |||||
| "demo_site_enabled": operation_setting.DemoSiteEnabled, | "demo_site_enabled": operation_setting.DemoSiteEnabled, | ||||
| "self_use_mode_enabled": operation_setting.SelfUseModeEnabled, | "self_use_mode_enabled": operation_setting.SelfUseModeEnabled, | ||||
| "default_use_auto_group": setting.DefaultUseAutoGroup, | "default_use_auto_group": setting.DefaultUseAutoGroup, | ||||
| "default_language": common.DefaultLanguage, | |||||
| "usd_exchange_rate": operation_setting.USDExchangeRate, | "usd_exchange_rate": operation_setting.USDExchangeRate, | ||||
| "price": operation_setting.Price, | "price": operation_setting.Price, | ||||
| @@ -456,6 +456,8 @@ func updateOptionMap(key string, value string) (err error) { | |||||
| common.SystemName = value | common.SystemName = value | ||||
| case "Logo": | case "Logo": | ||||
| common.Logo = value | common.Logo = value | ||||
| case "DefaultLanguage": | |||||
| common.DefaultLanguage = value | |||||
| case "WeChatServerAddress": | case "WeChatServerAddress": | ||||
| common.WeChatServerAddress = value | common.WeChatServerAddress = value | ||||
| case "WeChatServerToken": | case "WeChatServerToken": | ||||
| @@ -91,6 +91,10 @@ const PageLayout = () => { | |||||
| if (success) { | if (success) { | ||||
| statusDispatch({ type: 'set', payload: data }); | statusDispatch({ type: 'set', payload: data }); | ||||
| setStatusData(data); | setStatusData(data); | ||||
| // Apply admin-configured default language for unauthenticated users | |||||
| if (data.default_language && !localStorage.getItem('user')) { | |||||
| i18n.changeLanguage(data.default_language); | |||||
| } | |||||
| } else { | } else { | ||||
| showError('Unable to connect to server'); | showError('Unable to connect to server'); | ||||
| } | } | ||||
| @@ -100,6 +100,7 @@ const SystemSetting = () => { | |||||
| LinuxDOClientSecret: '', | LinuxDOClientSecret: '', | ||||
| LinuxDOMinimumTrustLevel: '', | LinuxDOMinimumTrustLevel: '', | ||||
| ServerAddress: '', | ServerAddress: '', | ||||
| DefaultLanguage: '', | |||||
| // SSRF防护配置 | // SSRF防护配置 | ||||
| 'fetch_setting.enable_ssrf_protection': true, | 'fetch_setting.enable_ssrf_protection': true, | ||||
| 'fetch_setting.allow_private_ip': '', | 'fetch_setting.allow_private_ip': '', | ||||
| @@ -317,6 +318,10 @@ const SystemSetting = () => { | |||||
| await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]); | await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]); | ||||
| }; | }; | ||||
| const submitDefaultLanguage = async () => { | |||||
| await updateOptions([{ key: 'DefaultLanguage', value: inputs.DefaultLanguage || '' }]); | |||||
| }; | |||||
| const submitSMTP = async () => { | const submitSMTP = async () => { | ||||
| const options = []; | const options = []; | ||||
| @@ -716,7 +721,7 @@ const SystemSetting = () => { | |||||
| <Row | <Row | ||||
| gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} | ||||
| > | > | ||||
| <Col xs={24} sm={24} md={24} lg={24} xl={24}> | |||||
| <Col xs={24} sm={24} md={24} lg={12} xl={12}> | |||||
| <Form.Input | <Form.Input | ||||
| field='ServerAddress' | field='ServerAddress' | ||||
| label={t('服务器地址')} | label={t('服务器地址')} | ||||
| @@ -726,10 +731,33 @@ const SystemSetting = () => { | |||||
| )} | )} | ||||
| /> | /> | ||||
| </Col> | </Col> | ||||
| <Col xs={24} sm={24} md={24} lg={12} xl={12}> | |||||
| <Form.Select | |||||
| field='DefaultLanguage' | |||||
| label={t('默认语言')} | |||||
| placeholder={t('未设置时跟随浏览器语言')} | |||||
| optionList={[ | |||||
| { label: t('自动(跟随浏览器)'), value: '' }, | |||||
| { label: '简体中文', value: 'zh-CN' }, | |||||
| { label: '繁體中文', value: 'zh-TW' }, | |||||
| { label: 'English', value: 'en' }, | |||||
| { label: 'Français', value: 'fr' }, | |||||
| { label: '日本語', value: 'ja' }, | |||||
| { label: 'Русский', value: 'ru' }, | |||||
| { label: 'Tiếng Việt', value: 'vi' }, | |||||
| ]} | |||||
| extraText={t( | |||||
| '设置后,未登录用户和未设置语言偏好的已登录用户将强制使用此语言', | |||||
| )} | |||||
| /> | |||||
| </Col> | |||||
| </Row> | </Row> | ||||
| <Button onClick={submitServerAddress}> | <Button onClick={submitServerAddress}> | ||||
| {t('更新服务器地址')} | {t('更新服务器地址')} | ||||
| </Button> | </Button> | ||||
| <Button onClick={submitDefaultLanguage}> | |||||
| {t('保存默认语言')} | |||||
| </Button> | |||||
| </Form.Section> | </Form.Section> | ||||
| </Card> | </Card> | ||||
| @@ -20,6 +20,7 @@ For commercial licensing, please contact support@quantumnous.com | |||||
| import React, { useEffect } from 'react'; | import React, { useEffect } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { reducer, initialState } from './reducer'; | import { reducer, initialState } from './reducer'; | ||||
| import { StatusContext } from '../Status'; | |||||
| export const UserContext = React.createContext({ | export const UserContext = React.createContext({ | ||||
| state: initialState, | state: initialState, | ||||
| @@ -29,6 +30,7 @@ export const UserContext = React.createContext({ | |||||
| export const UserProvider = ({ children }) => { | export const UserProvider = ({ children }) => { | ||||
| const [state, dispatch] = React.useReducer(reducer, initialState); | const [state, dispatch] = React.useReducer(reducer, initialState); | ||||
| const { i18n } = useTranslation(); | const { i18n } = useTranslation(); | ||||
| const [statusState] = React.useContext(StatusContext); | |||||
| // Sync language preference when user data is loaded | // Sync language preference when user data is loaded | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -37,12 +39,15 @@ export const UserProvider = ({ children }) => { | |||||
| const settings = JSON.parse(state.user.setting); | const settings = JSON.parse(state.user.setting); | ||||
| if (settings.language && settings.language !== i18n.language) { | if (settings.language && settings.language !== i18n.language) { | ||||
| i18n.changeLanguage(settings.language); | i18n.changeLanguage(settings.language); | ||||
| } else if (!settings.language && statusState.status?.default_language) { | |||||
| // No personal preference — fall back to admin default | |||||
| i18n.changeLanguage(statusState.status.default_language); | |||||
| } | } | ||||
| } catch (e) { | } catch (e) { | ||||
| // Ignore parse errors | // Ignore parse errors | ||||
| } | } | ||||
| } | } | ||||
| }, [state.user?.setting, i18n]); | |||||
| }, [state.user?.setting, statusState.status?.default_language, i18n]); | |||||
| return ( | return ( | ||||
| <UserContext.Provider value={[state, dispatch]}> | <UserContext.Provider value={[state, dispatch]}> | ||||