Browse Source

feat: 后台设置默认语言

管理员可在系统设置页面配置全局默认语言,未登录用户和未设置
语言偏好的已登录用户将强制使用该语言,忽略浏览器语言检测。

- 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
fengsilin 3 weeks ago
parent
commit
9eb58c684e
6 changed files with 43 additions and 2 deletions
  1. +1
    -0
      common/constants.go
  2. +1
    -0
      controller/misc.go
  3. +2
    -0
      model/option.go
  4. +4
    -0
      web/src/components/layout/PageLayout.jsx
  5. +29
    -1
      web/src/components/settings/SystemSetting.jsx
  6. +6
    -1
      web/src/context/User/index.jsx

+ 1
- 0
common/constants.go View File

@@ -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 = ""


+ 1
- 0
controller/misc.go View File

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


+ 2
- 0
model/option.go View File

@@ -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":


+ 4
- 0
web/src/components/layout/PageLayout.jsx View File

@@ -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');
} }


+ 29
- 1
web/src/components/settings/SystemSetting.jsx View File

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




+ 6
- 1
web/src/context/User/index.jsx View File

@@ -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]}>


Loading…
Cancel
Save