This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 14da4b95a fix(studio): localize settings management tabs (#2565)
14da4b95a is described below
commit 14da4b95a3eeedd86bdec544c8f40baeff92004f
Author: aias00 <[email protected]>
AuthorDate: Tue Aug 25 18:08:09 2026 +0800
fix(studio): localize settings management tabs (#2565)
---
web/src/i18n/LangContext.tsx | 2 +-
web/src/i18n/translations.ts | 218 +++++++++++++++++++++
web/src/pages/settings/AiAssistantTab.tsx | 110 ++++++-----
web/src/pages/settings/CloudCredentialTab.tsx | 113 ++++++-----
web/src/pages/settings/DataSourceTab.tsx | 105 +++++-----
web/src/pages/settings/GeneralSettingsTab.tsx | 74 +++----
.../settings/__tests__/AiAssistantTab.test.tsx | 10 +
.../settings/__tests__/CloudCredentialTab.test.tsx | 9 +
.../settings/__tests__/DataSourceTab.test.tsx | 15 ++
.../settings/__tests__/GeneralSettingsTab.test.tsx | 18 +-
web/src/pages/settings/index.tsx | 16 +-
11 files changed, 496 insertions(+), 194 deletions(-)
diff --git a/web/src/i18n/LangContext.tsx b/web/src/i18n/LangContext.tsx
index 120baef20..d4a27545e 100644
--- a/web/src/i18n/LangContext.tsx
+++ b/web/src/i18n/LangContext.tsx
@@ -28,7 +28,7 @@ interface LangContextType {
const LangContext = createContext<LangContextType>({
lang: 'zh',
setLang: () => {},
- t: (key) => key,
+ t: (key) => translations[key]?.zh ?? key,
});
export const LangProvider = ({ children }: { children: ReactNode }) => {
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 49dd7263c..41ff6fb9d 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -629,7 +629,225 @@ const translations: Record<string, Record<Lang, string>>
= {
// ─── Settings ───
'settings.title': { zh: '设置', en: 'Settings' },
'settings.subtitle': { zh: '管理应用配置与数据源', en: 'Manage app settings and data
sources' },
+ 'settings.tabGeneral': { zh: '通用设置', en: 'General' },
+ 'settings.tabAi': { zh: 'AI 助手', en: 'AI Assistant' },
+ 'settings.tabCredential': { zh: '云凭据管理', en: 'Cloud Credentials' },
+ 'settings.tabDatasource': { zh: '数据源管理', en: 'Data Sources' },
+ 'settings.tabAbout': { zh: '关于', en: 'About' },
'settings.dataSourceNotTested': { zh: '未检测', en: 'Not tested' },
+ 'settings.loadFailed': {
+ zh: '设置加载失败,请稍后重试',
+ en: 'Failed to load settings. Please try again later.',
+ },
+ 'settings.saveSuccess': { zh: '设置已保存', en: 'Settings saved' },
+ 'settings.saveFailed': {
+ zh: '设置保存失败,请稍后重试',
+ en: 'Failed to save settings. Please try again later.',
+ },
+ 'settings.saveSettings': { zh: '保存设置', en: 'Save settings' },
+ 'settings.testConnection': { zh: '测试连接', en: 'Test connection' },
+ 'settings.confirmAction': { zh: '确定', en: 'Confirm' },
+ 'settings.appearance': { zh: '界面偏好', en: 'Appearance preferences' },
+ 'settings.themeMode': { zh: '主题模式', en: 'Theme mode' },
+ 'settings.themeHelp': {
+ zh: '作用于前端展示,跟随系统时随操作系统外观变化',
+ en: 'Changes the UI appearance and follows your operating system when set
to System.',
+ },
+ 'settings.lightTheme': { zh: '浅色', en: 'Light' },
+ 'settings.darkTheme': { zh: '深色', en: 'Dark' },
+ 'settings.systemTheme': { zh: '跟随系统', en: 'System' },
+ 'settings.compactMode': { zh: '紧凑模式', en: 'Compact mode' },
+ 'settings.compactHelp': {
+ zh: '减小组件间距,单屏展示更多内容',
+ en: 'Reduces component spacing to show more content on one screen.',
+ },
+ 'settings.securitySession': { zh: '安全与会话', en: 'Security and sessions' },
+ 'settings.sessionTimeout': { zh: '会话超时', en: 'Session timeout' },
+ 'settings.sessionTimeoutHelp': {
+ zh: '应用于新创建的会话,已登录用户保持原到期时间',
+ en: 'Applies to new sessions; existing sessions keep their current
expiration.',
+ },
+ 'settings.sessionTimeoutRequired': { zh: '请输入会话超时时长', en: 'Enter a session
timeout.' },
+ 'settings.minutes': { zh: '分钟', en: 'minutes' },
+ 'settings.sessionTimeoutUnit': { zh: '会话超时单位', en: 'Session timeout unit' },
+ 'settings.loginProtectionHelp': {
+ zh: '登录保护由服务端 STUDIO_AUTH_LOGIN_REQUIRED 配置决定,修改后重启服务生效。',
+ en: 'Login protection is controlled by the server-side
STUDIO_AUTH_LOGIN_REQUIRED setting and takes effect after restart.',
+ },
+ 'settings.notification': { zh: '通知设置', en: 'Notification settings' },
+ 'settings.dingtalkWebhook': { zh: '钉钉机器人 Webhook', en: 'DingTalk bot
webhook' },
+ 'settings.dingtalkWebhookHelp': {
+ zh: '告警规则选择钉钉渠道时推送到该机器人',
+ en: 'Alert rules using DingTalk are sent to this bot.',
+ },
+ 'settings.emailRecipients': { zh: '邮件收件人', en: 'Email recipients' },
+ 'settings.emailRecipientsHelp': {
+ zh: '多个地址用英文逗号分隔;邮件发送通道后续接入',
+ en: 'Separate addresses with commas; email delivery will be added later.',
+ },
+ 'settings.smsWebhook': { zh: '短信网关 Webhook', en: 'SMS gateway webhook' },
+ 'settings.smsWebhookHelp': {
+ zh: '占位配置,短信发送通道后续接入',
+ en: 'Placeholder configuration; SMS delivery will be added later.',
+ },
+ 'settings.dataSourceLoadFailed': {
+ zh: '数据源加载失败,请稍后重试',
+ en: 'Failed to load data sources. Please try again later.',
+ },
+ 'settings.dataSourceAuthTestHint': {
+ zh: '认证数据源请编辑后输入凭据再测试连接',
+ en: 'Edit this authenticated data source and enter credentials before
testing.',
+ },
+ 'settings.connectionTestFailed': {
+ zh: '连接测试失败,请稍后重试',
+ en: 'Connection test failed. Please try again later.',
+ },
+ 'settings.dataSourceUpdated': { zh: '数据源已更新', en: 'Data source updated' },
+ 'settings.dataSourceAdded': { zh: '数据源已添加', en: 'Data source added' },
+ 'settings.dataSourceSaveFailed': {
+ zh: '保存数据源失败,请稍后重试',
+ en: 'Failed to save data source. Please try again later.',
+ },
+ 'settings.dataSourceDeleted': { zh: '数据源已删除', en: 'Data source deleted' },
+ 'settings.dataSourceDeleteFailed': {
+ zh: '删除数据源失败,请稍后重试',
+ en: 'Failed to delete data source. Please try again later.',
+ },
+ 'settings.instances': { zh: '适用实例', en: 'Applicable instances' },
+ 'settings.global': { zh: '全局', en: 'Global' },
+ 'settings.authentication': { zh: '认证方式', en: 'Authentication' },
+ 'settings.deleteDataSourceConfirm': {
+ zh: '确定要删除该数据源吗?',
+ en: 'Delete this data source?',
+ },
+ 'settings.searchDataSources': { zh: '搜索数据源名称', en: 'Search data source
names' },
+ 'settings.allTypes': { zh: '全部类型', en: 'All types' },
+ 'settings.addDataSource': { zh: '添加数据源', en: 'Add data source' },
+ 'settings.editDataSource': { zh: '编辑数据源', en: 'Edit data source' },
+ 'settings.dataSourceNameRequired': { zh: '请输入数据源名称', en: 'Enter a data
source name.' },
+ 'settings.dataSourceNameExample': {
+ zh: '例如:Prometheus 生产监控',
+ en: 'Example: Production Prometheus',
+ },
+ 'settings.selectType': { zh: '请选择类型', en: 'Select a type' },
+ 'settings.dataSourceUrlRequired': { zh: '请输入数据源 URL', en: 'Enter a data
source URL.' },
+ 'settings.instancesHelp': {
+ zh: '留空时可用于所有实例',
+ en: 'Leave empty to use this data source for all instances.',
+ },
+ 'settings.selectDataSourceInstances': {
+ zh: '选择此数据源对应的实例',
+ en: 'Select instances for this data source',
+ },
+ 'settings.username': { zh: '用户名', en: 'Username' },
+ 'settings.usernameRequired': { zh: '请输入用户名', en: 'Enter a username.' },
+ 'settings.password': { zh: '密码', en: 'Password' },
+ 'settings.passwordRequired': { zh: '请输入密码', en: 'Enter a password.' },
+ 'settings.bearerTokenRequired': { zh: '请输入 Bearer Token', en: 'Enter a
bearer token.' },
+ 'settings.enterToken': { zh: '请输入 Token', en: 'Enter a token' },
+ 'settings.totalRecords': { zh: '共 {total} 条', en: '{total} records' },
+ 'settings.credentialLoadFailed': {
+ zh: '云凭据加载失败,请稍后重试',
+ en: 'Failed to load cloud credentials. Please try again later.',
+ },
+ 'settings.credentialUpdated': { zh: '云凭据已更新', en: 'Cloud credential updated'
},
+ 'settings.credentialAdded': { zh: '云凭据已添加', en: 'Cloud credential added' },
+ 'settings.credentialSaveFailed': {
+ zh: '保存云凭据失败,请稍后重试',
+ en: 'Failed to save cloud credential. Please try again later.',
+ },
+ 'settings.credentialDeleted': { zh: '云凭据已删除', en: 'Cloud credential deleted'
},
+ 'settings.credentialDeleteFailed': {
+ zh: '删除云凭据失败(可能仍被实例引用),请稍后重试',
+ en: 'Failed to delete cloud credential. It may still be used by an
instance.',
+ },
+ 'settings.cloudVendor': { zh: '云厂商', en: 'Cloud vendor' },
+ 'settings.remark': { zh: '备注', en: 'Remark' },
+ 'settings.createdAt': { zh: '创建时间', en: 'Created at' },
+ 'settings.deleteCredentialConfirm': {
+ zh: '确定要删除该云凭据吗?',
+ en: 'Delete this cloud credential?',
+ },
+ 'settings.searchCredentials': { zh: '搜索凭据名称', en: 'Search credential names'
},
+ 'settings.allCloudVendors': { zh: '全部云厂商', en: 'All cloud vendors' },
+ 'settings.addCredential': { zh: '添加云凭据', en: 'Add credential' },
+ 'settings.editCredential': { zh: '编辑云凭据', en: 'Edit credential' },
+ 'settings.credentialNameRequired': { zh: '请输入凭据名称', en: 'Enter a credential
name.' },
+ 'settings.credentialNameExample': {
+ zh: '例如:阿里云测试账号',
+ en: 'Example: Aliyun test account',
+ },
+ 'settings.selectCloudVendor': { zh: '请选择云厂商', en: 'Select a cloud vendor.' },
+ 'settings.accessKeyRequired': { zh: '请输入 AccessKey', en: 'Enter an
AccessKey.' },
+ 'settings.secretKeyRequired': { zh: '请输入 SecretKey', en: 'Enter a
SecretKey.' },
+ 'settings.keepSecretKey': {
+ zh: '留空表示保持原 SecretKey 不变',
+ en: 'Leave empty to keep the existing SecretKey.',
+ },
+ 'settings.enterSecretKey': { zh: '请输入 SecretKey', en: 'Enter a SecretKey' },
+ 'settings.optional': { zh: '可选', en: 'Optional' },
+ 'settings.aliyun': { zh: '阿里云', en: 'Aliyun' },
+ 'settings.tencent': { zh: '腾讯云', en: 'Tencent Cloud' },
+ 'settings.aiEngineModel': { zh: '执行引擎与模型', en: 'Execution engine and model'
},
+ 'settings.aiEngine': { zh: '执行引擎', en: 'Execution engine' },
+ 'settings.aiEngineHelp': {
+ zh: 'Claude Code / Qoder 引擎在服务器上以 CLI 子进程方式运行,凭据经环境变量注入;HTTP 引擎直连 OpenAI
兼容接口',
+ en: 'Claude Code and Qoder run as CLI subprocesses on the server with
credentials supplied through environment variables; HTTP connects directly to
an OpenAI-compatible API.',
+ },
+ 'settings.modelProvider': { zh: '模型服务商', en: 'Model provider' },
+ 'settings.modelProviderRequired': { zh: '请选择模型服务商', en: 'Select a model
provider.' },
+ 'settings.model': { zh: '模型', en: 'Model' },
+ 'settings.modelRequired': { zh: '请选择或输入模型', en: 'Select or enter a model.' },
+ 'settings.modelHelp': {
+ zh: '可从下拉选择,也可直接输入自定义模型 ID',
+ en: 'Select from the list or enter a custom model ID.',
+ },
+ 'settings.selectOrEnterModel': { zh: '选择或输入模型', en: 'Select or enter a
model' },
+ 'settings.credentialsEndpoint': { zh: '凭据与接入地址', en: 'Credentials and
endpoint' },
+ 'settings.apiKeyConfiguredHelp': {
+ zh: '已配置(可能来自环境变量 RMQ_LLM_TOKEN);留空将保留现有密钥',
+ en: 'Configured (possibly from RMQ_LLM_TOKEN); leave empty to keep the
existing key.',
+ },
+ 'settings.enterApiKey': { zh: '请输入 API Key', en: 'Enter an API key' },
+ 'settings.apiKeyConfiguredPlaceholder': {
+ zh: '••••••••(已配置,留空保留)',
+ en: '•••••••• (configured; leave empty to keep)',
+ },
+ 'settings.apiKeyConfigured': { zh: '密钥已配置', en: 'API key configured' },
+ 'settings.apiBaseRequired': { zh: '请输入 API Base URL', en: 'Enter an API base
URL.' },
+ 'settings.apiBaseInvalid': { zh: '需为 http/https 地址', en: 'Use an http or
https URL.' },
+ 'settings.generationParameters': { zh: '生成参数', en: 'Generation parameters' },
+ 'settings.connectionSucceeded': { zh: '连接成功', en: 'Connection succeeded' },
+ 'settings.connectionTestFailedShort': { zh: '连接测试失败', en: 'Connection test
failed' },
+ 'settings.connectionTestRequestFailed': {
+ zh: '连接测试请求失败,请稍后重试',
+ en: 'Connection test request failed. Please try again later.',
+ },
+ 'settings.saveSucceeded': { zh: '保存成功', en: 'Saved' },
+ 'settings.saveFailedShort': { zh: '保存失败', en: 'Save failed' },
+ 'settings.saveRequestFailed': {
+ zh: '保存请求失败,请稍后重试',
+ en: 'Save request failed. Please try again later.',
+ },
+ 'settings.tongyi': { zh: '通义千问(DashScope)', en: 'Tongyi Qianwen (DashScope)'
},
+ 'settings.ollamaLocal': { zh: 'Ollama(本地)', en: 'Ollama (local)' },
+ 'settings.claudeCodeDefault': { zh: 'Claude Code(默认)', en: 'Claude Code
(default)' },
+ 'settings.httpOpenAiCompatible': { zh: 'HTTP(OpenAI 兼容)', en: 'HTTP
(OpenAI-compatible)' },
+ 'settings.dashscopeStandard': {
+ zh: '百炼 DashScope 标准(compatible-mode)',
+ en: 'DashScope standard (compatible-mode)',
+ },
+ 'settings.tokenPlanOpenAi': {
+ zh: 'Token Plan 网关(OpenAI 兼容)',
+ en: 'Token Plan gateway (OpenAI-compatible)',
+ },
+ 'settings.tokenPlanAnthropic': {
+ zh: 'Token Plan 网关(Anthropic 兼容)',
+ en: 'Token Plan gateway (Anthropic-compatible)',
+ },
+ 'settings.openaiOfficial': { zh: 'OpenAI 官方', en: 'OpenAI official' },
+ 'settings.anthropicOfficial': { zh: 'Anthropic 官方', en: 'Anthropic official'
},
+ 'settings.deepseekOfficial': { zh: 'DeepSeek 官方', en: 'DeepSeek official' },
// ─── Certs ───
'cert.clusterName': { zh: 'K8s 集群名称', en: 'K8s Cluster Name' },
diff --git a/web/src/pages/settings/AiAssistantTab.tsx
b/web/src/pages/settings/AiAssistantTab.tsx
index e920ec3bd..d0d80d901 100644
--- a/web/src/pages/settings/AiAssistantTab.tsx
+++ b/web/src/pages/settings/AiAssistantTab.tsx
@@ -45,56 +45,58 @@ import {
} from '../../api/llm';
import { fallbackModelOptions } from '../studio/llmModelOptions';
-const PROVIDER_OPTIONS = [
- { value: 'tongyi', label: '通义千问(DashScope)' },
+const DEFAULT_BASE_URL: Record<string, string> = {
+ tongyi: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
+ openai: 'https://api.openai.com/v1',
+ anthropic: 'https://api.anthropic.com',
+ deepseek: 'https://api.deepseek.com/v1',
+ ollama: 'http://localhost:11434/v1',
+};
+
+const providerOptions = (t: (key: string) => string) => [
+ { value: 'tongyi', label: t('settings.tongyi') },
{ value: 'openai', label: 'OpenAI' },
- { value: 'anthropic', label: 'Anthropic(Claude)' },
+ { value: 'anthropic', label: 'Anthropic (Claude)' },
{ value: 'azure', label: 'Azure OpenAI' },
{ value: 'deepseek', label: 'DeepSeek' },
- { value: 'ollama', label: 'Ollama(本地)' },
+ { value: 'ollama', label: t('settings.ollamaLocal') },
{ value: 'bedrock', label: 'AWS Bedrock' },
];
-const ENGINE_OPTIONS = [
- { value: 'claude-code', label: 'Claude Code(默认)' },
+const engineOptions = (t: (key: string) => string) => [
+ { value: 'claude-code', label: t('settings.claudeCodeDefault') },
{ value: 'qoder', label: 'Qoder CLI' },
- { value: 'http', label: 'HTTP(OpenAI 兼容)' },
+ { value: 'http', label: t('settings.httpOpenAiCompatible') },
];
-const DEFAULT_BASE_URL: Record<string, string> = {
- tongyi: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
- openai: 'https://api.openai.com/v1',
- anthropic: 'https://api.anthropic.com',
- deepseek: 'https://api.deepseek.com/v1',
- ollama: 'http://localhost:11434/v1',
-};
-
-const BASE_URL_PRESETS: Record<string, { value: string; label: string }[]> = {
+const baseUrlPresets = (
+ t: (key: string) => string,
+): Record<string, { value: string; label: string }[]> => ({
tongyi: [
{
value: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
- label: '百炼 DashScope 标准(compatible-mode)',
+ label: t('settings.dashscopeStandard'),
},
{
value:
'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1',
- label: 'Token Plan 网关(OpenAI 兼容)',
+ label: t('settings.tokenPlanOpenAi'),
},
{
value: 'https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic',
- label: 'Token Plan 网关(Anthropic 兼容)',
+ label: t('settings.tokenPlanAnthropic'),
},
],
- openai: [{ value: 'https://api.openai.com/v1', label: 'OpenAI 官方' }],
+ openai: [{ value: 'https://api.openai.com/v1', label:
t('settings.openaiOfficial') }],
anthropic: [
- { value: 'https://api.anthropic.com', label: 'Anthropic 官方' },
+ { value: 'https://api.anthropic.com', label:
t('settings.anthropicOfficial') },
{
value: 'https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic',
- label: 'Token Plan 网关(Anthropic 兼容)',
+ label: t('settings.tokenPlanAnthropic'),
},
],
- deepseek: [{ value: 'https://api.deepseek.com/v1', label: 'DeepSeek 官方' }],
- ollama: [{ value: 'http://localhost:11434/v1', label: 'Ollama 本地' }],
-};
+ deepseek: [{ value: 'https://api.deepseek.com/v1', label:
t('settings.deepseekOfficial') }],
+ ollama: [{ value: 'http://localhost:11434/v1', label:
t('settings.ollamaLocal') }],
+});
interface TestState {
success: boolean;
@@ -222,11 +224,11 @@ export const AiAssistantTab = () => {
const applyTestResult = (result: LlmTestResult) => {
if (result.status === 0) {
- setTestResult({ success: true, msg: result.msg || '连接成功' });
+ setTestResult({ success: true, msg: result.msg ||
t('settings.connectionSucceeded') });
} else {
setTestResult({
success: false,
- msg: result.errMsg || '连接测试失败',
+ msg: result.errMsg || t('settings.connectionTestFailedShort'),
hint: result.hint,
});
}
@@ -250,7 +252,7 @@ export const AiAssistantTab = () => {
}
} catch {
if (testRequestIdRef.current === requestId) {
- setTestResult({ success: false, msg: '连接测试请求失败,请稍后重试' });
+ setTestResult({ success: false, msg:
t('settings.connectionTestRequestFailed') });
}
} finally {
if (testRequestIdRef.current === requestId) {
@@ -266,7 +268,7 @@ export const AiAssistantTab = () => {
try {
const result = await saveLlmConfig(payload);
if (result.status === 0) {
- message.success('保存成功');
+ message.success(t('settings.saveSucceeded'));
if (payload.apiKey) {
setApiKeyConfigured(true);
form.setFieldValue('apiKey', undefined);
@@ -279,10 +281,10 @@ export const AiAssistantTab = () => {
message.warning(t('ai.modelsRefreshFailedAfterSave'));
}
} else {
- message.error(result.errMsg || '保存失败');
+ message.error(result.errMsg || t('settings.saveFailedShort'));
}
} catch {
- message.error('保存请求失败,请稍后重试');
+ message.error(t('settings.saveRequestFailed'));
} finally {
setSaving(false);
}
@@ -302,40 +304,40 @@ export const AiAssistantTab = () => {
title={
<Space>
<RobotOutlined />
- 执行引擎与模型
+ {t('settings.aiEngineModel')}
</Space>
}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
- label="执行引擎"
+ label={t('settings.aiEngine')}
name="engine"
- extra="Claude Code / Qoder 引擎在服务器上以 CLI
子进程方式运行,凭据经环境变量注入;HTTP 引擎直连 OpenAI 兼容接口"
+ extra={t('settings.aiEngineHelp')}
>
- <Select options={ENGINE_OPTIONS} />
+ <Select options={engineOptions(t)} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
- label="模型服务商"
+ label={t('settings.modelProvider')}
name="provider"
- rules={[{ required: true, message: '请选择模型服务商' }]}
+ rules={[{ required: true, message:
t('settings.modelProviderRequired') }]}
>
- <Select options={PROVIDER_OPTIONS}
onChange={handleProviderChange} />
+ <Select options={providerOptions(t)}
onChange={handleProviderChange} />
</Form.Item>
</Col>
</Row>
<Form.Item
- label="模型"
+ label={t('settings.model')}
name="model"
- rules={[{ required: true, message: '请选择或输入模型' }]}
- extra="可从下拉选择,也可直接输入自定义模型 ID"
+ rules={[{ required: true, message: t('settings.modelRequired')
}]}
+ extra={t('settings.modelHelp')}
style={{ marginBottom: 0 }}
>
<AutoComplete
options={modelOptions}
- placeholder="选择或输入模型"
+ placeholder={t('settings.selectOrEnterModel')}
filterOption={(input, option) =>
String(option?.value ?? '')
.toLowerCase()
@@ -351,7 +353,7 @@ export const AiAssistantTab = () => {
title={
<Space>
<KeyOutlined />
- 凭据与接入地址
+ {t('settings.credentialsEndpoint')}
</Space>
}
>
@@ -359,19 +361,19 @@ export const AiAssistantTab = () => {
label="API Key"
name="apiKey"
extra={
- apiKeyConfigured
- ? '已配置(可能来自环境变量 RMQ_LLM_TOKEN);留空将保留现有密钥'
- : '请输入 API Key'
+ apiKeyConfigured ? t('settings.apiKeyConfiguredHelp') :
t('settings.enterApiKey')
}
>
<Input.Password
- placeholder={apiKeyConfigured ? '••••••••(已配置,留空保留)' :
'sk-...'}
+ placeholder={
+ apiKeyConfigured ? t('settings.apiKeyConfiguredPlaceholder')
: 'sk-...'
+ }
autoComplete="new-password"
/>
</Form.Item>
{apiKeyConfigured && (
<div style={{ marginTop: -16, marginBottom: 16 }}>
- <Tag color="green">密钥已配置</Tag>
+ <Tag color="green">{t('settings.apiKeyConfigured')}</Tag>
</div>
)}
@@ -379,10 +381,10 @@ export const AiAssistantTab = () => {
label="API Base URL"
name="apiBase"
rules={[
- { required: true, message: '请输入 API Base URL' },
+ { required: true, message: t('settings.apiBaseRequired') },
{
pattern: /^https?:\/\/.+/,
- message: '需为 http/https 地址',
+ message: t('settings.apiBaseInvalid'),
},
]}
style={{
@@ -391,7 +393,7 @@ export const AiAssistantTab = () => {
}}
>
<AutoComplete
- options={BASE_URL_PRESETS[selectedProvider ?? 'tongyi'] ?? []}
+ options={baseUrlPresets(t)[selectedProvider ?? 'tongyi'] ?? []}
placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1"
suffixIcon={<CaretDownOutlined style={{ color: '#9CA3AF' }} />}
filterOption={(input, option) =>
@@ -447,7 +449,7 @@ export const AiAssistantTab = () => {
title={
<Space>
<ControlOutlined />
- 生成参数
+ {t('settings.generationParameters')}
</Space>
}
>
@@ -481,10 +483,10 @@ export const AiAssistantTab = () => {
disabled={loading}
onClick={() => void handleSave()}
>
- 保存
+ {t('common.save')}
</Button>
<Button loading={testing} disabled={loading} onClick={() => void
handleTest()}>
- 测试连接
+ {t('settings.testConnection')}
</Button>
</Space>
</Flex>
diff --git a/web/src/pages/settings/CloudCredentialTab.tsx
b/web/src/pages/settings/CloudCredentialTab.tsx
index 5846636bd..077e08355 100644
--- a/web/src/pages/settings/CloudCredentialTab.tsx
+++ b/web/src/pages/settings/CloudCredentialTab.tsx
@@ -33,6 +33,7 @@ import {
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { MagnifyingGlass } from '@phosphor-icons/react';
+import { useLang } from '../../i18n/LangContext';
import {
createCloudCredential,
@@ -43,21 +44,11 @@ import {
import type { CloudCredential } from '../../api/cloudCredential';
import type { InstanceVendor } from '../../api/instance';
-const vendorLabel: Record<string, string> = {
- ALIYUN: '阿里云',
- TENCENT: '腾讯云',
-};
-
const vendorTagColor: Record<string, string> = {
ALIYUN: 'orange',
TENCENT: 'blue',
};
-const VENDOR_OPTIONS = [
- { value: 'ALIYUN', label: '阿里云' },
- { value: 'TENCENT', label: '腾讯云' },
-];
-
const PAGE_SIZE_OPTIONS = [20, 50, 100];
interface CredentialFormValues {
@@ -69,6 +60,7 @@ interface CredentialFormValues {
}
export const CloudCredentialTab = () => {
+ const { t } = useLang();
const [credentials, setCredentials] = useState<CloudCredential[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
@@ -110,7 +102,7 @@ export const CloudCredentialTab = () => {
setTotal(result.total);
} catch {
if (requestId === requestSeqRef.current) {
- message.error('云凭据加载失败,请稍后重试');
+ message.error(t('settings.credentialLoadFailed'));
}
} finally {
if (requestId === requestSeqRef.current) {
@@ -118,7 +110,7 @@ export const CloudCredentialTab = () => {
}
}
})();
- }, [debouncedSearch, page, pageSize, vendorFilter]);
+ }, [debouncedSearch, page, pageSize, t, vendorFilter]);
useEffect(() => {
void loadCredentials();
@@ -175,7 +167,7 @@ export const CloudCredentialTab = () => {
remark: values.remark,
});
setCredentials((previous) => previous.map((item) => (item.id ===
saved.id ? saved : item)));
- message.success('云凭据已更新');
+ message.success(t('settings.credentialUpdated'));
} else {
await createCloudCredential({
name: values.name,
@@ -185,7 +177,7 @@ export const CloudCredentialTab = () => {
remark: values.remark,
});
setPage(1);
- message.success('云凭据已添加');
+ message.success(t('settings.credentialAdded'));
}
await loadCredentials();
closeModal();
@@ -193,7 +185,7 @@ export const CloudCredentialTab = () => {
if (error && typeof error === 'object' && 'errorFields' in error) {
return; // validation failure; antd already shows field-level errors
}
- message.error('保存云凭据失败,请稍后重试');
+ message.error(t('settings.credentialSaveFailed'));
} finally {
setSubmitting(false);
}
@@ -208,27 +200,33 @@ export const CloudCredentialTab = () => {
} else {
await loadCredentials();
}
- message.success('云凭据已删除');
+ message.success(t('settings.credentialDeleted'));
} catch {
- message.error('删除云凭据失败(可能仍被实例引用),请稍后重试');
+ message.error(t('settings.credentialDeleteFailed'));
}
};
const columns: ColumnsType<CloudCredential> = [
- { title: '名称', dataIndex: 'name', key: 'name' },
+ { title: t('common.name'), dataIndex: 'name', key: 'name' },
{
- title: '云厂商',
+ title: t('settings.cloudVendor'),
dataIndex: 'vendor',
key: 'vendor',
render: (vendor: string) => (
- <Tag color={vendorTagColor[vendor]}>{vendorLabel[vendor] ??
vendor}</Tag>
+ <Tag color={vendorTagColor[vendor]}>
+ {vendor === 'ALIYUN'
+ ? t('settings.aliyun')
+ : vendor === 'TENCENT'
+ ? t('settings.tencent')
+ : vendor}
+ </Tag>
),
},
{ title: 'AccessKey', dataIndex: 'accessKey', key: 'accessKey' },
- { title: '备注', dataIndex: 'remark', key: 'remark' },
- { title: '创建时间', dataIndex: 'gmtCreate', key: 'gmtCreate' },
+ { title: t('settings.remark'), dataIndex: 'remark', key: 'remark' },
+ { title: t('settings.createdAt'), dataIndex: 'gmtCreate', key: 'gmtCreate'
},
{
- title: '操作',
+ title: t('common.actions'),
key: 'action',
render: (_: unknown, record: CloudCredential) => (
<Space size="small">
@@ -238,16 +236,16 @@ export const CloudCredentialTab = () => {
icon={<EditOutlined />}
onClick={() => openEditModal(record)}
>
- 编辑
+ {t('common.edit')}
</Button>
<Popconfirm
- title="确定要删除该云凭据吗?"
+ title={t('settings.deleteCredentialConfirm')}
onConfirm={() => void handleDelete(record)}
- okText="确定"
- cancelText="取消"
+ okText={t('settings.confirmAction')}
+ cancelText={t('common.cancel')}
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
- 删除
+ {t('common.delete')}
</Button>
</Popconfirm>
</Space>
@@ -262,22 +260,25 @@ export const CloudCredentialTab = () => {
<Input
allowClear
prefix={<MagnifyingGlass size={14} color="#9CA3AF" />}
- placeholder="搜索凭据名称"
+ placeholder={t('settings.searchCredentials')}
style={{ width: 240 }}
value={search}
onChange={(event) => changeSearch(event.target.value)}
/>
<Select<InstanceVendor>
allowClear
- placeholder="全部云厂商"
+ placeholder={t('settings.allCloudVendors')}
style={{ width: 160 }}
value={vendorFilter}
onChange={changeVendorFilter}
- options={VENDOR_OPTIONS}
+ options={[
+ { value: 'ALIYUN', label: t('settings.aliyun') },
+ { value: 'TENCENT', label: t('settings.tencent') },
+ ]}
/>
</Flex>
<Button type="primary" icon={<PlusOutlined />}
onClick={openCreateModal} disabled={loading}>
- 添加云凭据
+ {t('settings.addCredential')}
</Button>
</Flex>
@@ -292,7 +293,7 @@ export const CloudCredentialTab = () => {
total,
showSizeChanger: true,
pageSizeOptions: PAGE_SIZE_OPTIONS.map(String),
- showTotal: (count) => `共 ${count} 条`,
+ showTotal: (count) => t('settings.totalRecords', { total: count }),
onChange: (nextPage, nextPageSize) => {
if (nextPageSize !== pageSize) {
setPage(1);
@@ -306,7 +307,7 @@ export const CloudCredentialTab = () => {
/>
<Modal
- title={editingCredential ? '编辑云凭据' : '添加云凭据'}
+ title={t(editingCredential ? 'settings.editCredential' :
'settings.addCredential')}
open={modalOpen}
onCancel={closeModal}
onOk={() => void handleSubmit()}
@@ -321,8 +322,13 @@ export const CloudCredentialTab = () => {
items={[
{
key: 'vendor',
- label: '云厂商',
- children: vendorLabel[editingCredential.vendor] ??
editingCredential.vendor,
+ label: t('settings.cloudVendor'),
+ children:
+ editingCredential.vendor === 'ALIYUN'
+ ? t('settings.aliyun')
+ : editingCredential.vendor === 'TENCENT'
+ ? t('settings.tencent')
+ : editingCredential.vendor,
},
{ key: 'accessKey', label: 'AccessKey', children:
editingCredential.accessKey },
]}
@@ -330,27 +336,34 @@ export const CloudCredentialTab = () => {
)}
<Form form={form} layout="vertical" preserve={false}>
<Form.Item
- label="名称"
+ label={t('common.name')}
name="name"
- rules={[{ required: true, message: '请输入凭据名称' }]}
+ rules={[{ required: true, message:
t('settings.credentialNameRequired') }]}
>
- <Input placeholder="例如:阿里云测试账号" />
+ <Input placeholder={t('settings.credentialNameExample')} />
</Form.Item>
{!editingCredential && (
<>
<Form.Item
- label="云厂商"
+ label={t('settings.cloudVendor')}
name="vendor"
- rules={[{ required: true, message: '请选择云厂商' }]}
+ rules={[{ required: true, message:
t('settings.selectCloudVendor') }]}
>
- <Select placeholder="请选择" virtual={false}
options={VENDOR_OPTIONS} />
+ <Select
+ placeholder={t('settings.selectCloudVendor')}
+ virtual={false}
+ options={[
+ { value: 'ALIYUN', label: t('settings.aliyun') },
+ { value: 'TENCENT', label: t('settings.tencent') },
+ ]}
+ />
</Form.Item>
<Form.Item
label="AccessKey"
name="accessKey"
- rules={[{ required: true, message: '请输入 AccessKey' }]}
+ rules={[{ required: true, message:
t('settings.accessKeyRequired') }]}
>
<Input autoComplete="off" placeholder="LTAI..." />
</Form.Item>
@@ -360,14 +373,18 @@ export const CloudCredentialTab = () => {
<Form.Item
label="SecretKey"
name="secretKey"
- rules={editingCredential ? [] : [{ required: true, message: '请输入
SecretKey' }]}
- extra={editingCredential ? '留空表示保持原 SecretKey 不变' : undefined}
+ rules={
+ editingCredential
+ ? []
+ : [{ required: true, message: t('settings.secretKeyRequired')
}]
+ }
+ extra={editingCredential ? t('settings.keepSecretKey') : undefined}
>
- <Input.Password autoComplete="off" placeholder="请输入 SecretKey" />
+ <Input.Password autoComplete="off"
placeholder={t('settings.enterSecretKey')} />
</Form.Item>
- <Form.Item label="备注" name="remark">
- <Input.TextArea rows={2} placeholder="可选" />
+ <Form.Item label={t('settings.remark')} name="remark">
+ <Input.TextArea rows={2} placeholder={t('settings.optional')} />
</Form.Item>
</Form>
</Modal>
diff --git a/web/src/pages/settings/DataSourceTab.tsx
b/web/src/pages/settings/DataSourceTab.tsx
index ccd2d5306..d0e7729ed 100644
--- a/web/src/pages/settings/DataSourceTab.tsx
+++ b/web/src/pages/settings/DataSourceTab.tsx
@@ -142,7 +142,7 @@ export const DataSourceTab = () => {
setTotal(result.total);
} catch {
if (requestId === requestSeqRef.current) {
- message.error('数据源加载失败,请稍后重试');
+ message.error(t('settings.dataSourceLoadFailed'));
}
} finally {
if (requestId === requestSeqRef.current) {
@@ -150,7 +150,7 @@ export const DataSourceTab = () => {
}
}
})();
- }, [debouncedSearch, page, pageSize, typeFilter]);
+ }, [debouncedSearch, page, pageSize, t, typeFilter]);
useEffect(() => {
void loadDataSources();
@@ -182,7 +182,7 @@ export const DataSourceTab = () => {
key: string,
) => {
if (key !== 'modal' && authNeedsSecret(data.auth)) {
- message.warning('认证数据源请编辑后输入凭据再测试连接');
+ message.warning(t('settings.dataSourceAuthTestHint'));
return;
}
setTestingKeys((previous) => new Set(previous).add(key));
@@ -191,7 +191,7 @@ export const DataSourceTab = () => {
if (result.success) message.success(result.message);
else message.error(result.message);
} catch {
- message.error('连接测试失败,请稍后重试');
+ message.error(t('settings.connectionTestFailed'));
} finally {
setTestingKeys((previous) => {
const next = new Set(previous);
@@ -230,14 +230,16 @@ export const DataSourceTab = () => {
setPage(1);
}
await loadDataSources();
- message.success(editingDataSource ? '数据源已更新' : '数据源已添加');
+ message.success(
+ t(editingDataSource ? 'settings.dataSourceUpdated' :
'settings.dataSourceAdded'),
+ );
setModalOpen(false);
dsForm.resetFields();
} catch (error) {
if (error && typeof error === 'object' && 'errorFields' in error) {
return; // validation failure; antd already shows field-level errors
}
- message.error('保存数据源失败,请稍后重试');
+ message.error(t('settings.dataSourceSaveFailed'));
} finally {
setSubmitting(false);
}
@@ -251,27 +253,27 @@ export const DataSourceTab = () => {
} else {
await loadDataSources();
}
- message.success('数据源已删除');
+ message.success(t('settings.dataSourceDeleted'));
} catch {
- message.error('删除数据源失败,请稍后重试');
+ message.error(t('settings.dataSourceDeleteFailed'));
}
};
const columns: ColumnsType<DataSource> = [
- { title: '名称', dataIndex: 'name', key: 'name' },
+ { title: t('common.name'), dataIndex: 'name', key: 'name' },
{
- title: '类型',
+ title: t('common.type'),
dataIndex: 'type',
key: 'type',
render: (t: string) => <Tag color={typeTagColor[t]}>{t}</Tag>,
},
{ title: 'URL', dataIndex: 'url', key: 'url' },
{
- title: '适用实例',
+ title: t('settings.instances'),
dataIndex: 'instanceIds',
key: 'instanceIds',
render: (instanceIds: string[] | undefined) => {
- if (!instanceIds?.length) return '全局';
+ if (!instanceIds?.length) return t('settings.global');
return instanceIds
.map(
(instanceId) =>
@@ -282,9 +284,9 @@ export const DataSourceTab = () => {
.join('、');
},
},
- { title: '认证方式', dataIndex: 'auth', key: 'auth' },
+ { title: t('settings.authentication'), dataIndex: 'auth', key: 'auth' },
{
- title: '状态',
+ title: t('common.status'),
dataIndex: 'status',
key: 'status',
render: (status: DataSource['status']) =>
@@ -295,7 +297,7 @@ export const DataSourceTab = () => {
),
},
{
- title: '操作',
+ title: t('common.actions'),
key: 'action',
render: (_: unknown, record: DataSource) => (
<Space size="small">
@@ -305,12 +307,10 @@ export const DataSourceTab = () => {
icon={<ApiOutlined />}
loading={testingKeys.has(record.key)}
disabled={authNeedsSecret(record.auth)}
- title={
- authNeedsSecret(record.auth) ? '认证数据源请编辑后输入凭据再测试连接' : undefined
- }
+ title={authNeedsSecret(record.auth) ?
t('settings.dataSourceAuthTestHint') : undefined}
onClick={() => void handleTestConnection(record, record.key)}
>
- 测试连接
+ {t('settings.testConnection')}
</Button>
<Button
type="link"
@@ -318,16 +318,16 @@ export const DataSourceTab = () => {
icon={<EditOutlined />}
onClick={() => openEditModal(record)}
>
- 编辑
+ {t('common.edit')}
</Button>
<Popconfirm
- title="确定要删除该数据源吗?"
+ title={t('settings.deleteDataSourceConfirm')}
onConfirm={() => void handleDelete(record)}
- okText="确定"
- cancelText="取消"
+ okText={t('settings.confirmAction')}
+ cancelText={t('common.cancel')}
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
- 删除
+ {t('common.delete')}
</Button>
</Popconfirm>
</Space>
@@ -342,7 +342,7 @@ export const DataSourceTab = () => {
<Input
allowClear
prefix={<MagnifyingGlass size={14} color="#9CA3AF" />}
- placeholder="搜索数据源名称"
+ placeholder={t('settings.searchDataSources')}
style={{ width: 240 }}
value={search}
onChange={(event) => {
@@ -352,7 +352,7 @@ export const DataSourceTab = () => {
/>
<Select
allowClear
- placeholder="全部类型"
+ placeholder={t('settings.allTypes')}
style={{ width: 160 }}
value={typeFilter}
onChange={(value) => {
@@ -363,7 +363,7 @@ export const DataSourceTab = () => {
/>
</Flex>
<Button type="primary" icon={<PlusOutlined />}
onClick={openCreateModal} disabled={loading}>
- 添加数据源
+ {t('settings.addDataSource')}
</Button>
</Flex>
@@ -378,7 +378,7 @@ export const DataSourceTab = () => {
total,
showSizeChanger: true,
pageSizeOptions: PAGE_SIZE_OPTIONS.map(String),
- showTotal: (count) => `共 ${count} 条`,
+ showTotal: (count) => t('settings.totalRecords', { total: count }),
onChange: (nextPage, nextPageSize) => {
if (nextPageSize !== pageSize) {
setPage(1);
@@ -392,7 +392,7 @@ export const DataSourceTab = () => {
/>
<Modal
- title={editingDataSource ? '编辑数据源' : '添加数据源'}
+ title={t(editingDataSource ? 'settings.editDataSource' :
'settings.addDataSource')}
open={modalOpen}
onCancel={() => {
setModalOpen(false);
@@ -405,34 +405,42 @@ export const DataSourceTab = () => {
>
<Form form={dsForm} layout="vertical" preserve={false}>
<Form.Item
- label="名称"
+ label={t('common.name')}
name="name"
- rules={[{ required: true, message: '请输入数据源名称' }]}
+ rules={[{ required: true, message:
t('settings.dataSourceNameRequired') }]}
>
- <Input placeholder="例如:Prometheus 生产监控" />
+ <Input placeholder={t('settings.dataSourceNameExample')} />
</Form.Item>
<Form.Item
- label="类型"
+ label={t('common.type')}
name="type"
- rules={[{ required: true, message: '请选择数据源类型' }]}
+ rules={[{ required: true, message: t('settings.selectType') }]}
>
- <Select placeholder="请选择" virtual={false}
options={DATA_SOURCE_TYPE_OPTIONS} />
+ <Select
+ placeholder={t('settings.selectType')}
+ virtual={false}
+ options={DATA_SOURCE_TYPE_OPTIONS}
+ />
</Form.Item>
<Form.Item
label="URL"
name="url"
- rules={[{ required: true, message: '请输入数据源 URL' }]}
+ rules={[{ required: true, message:
t('settings.dataSourceUrlRequired') }]}
>
<Input placeholder="http://localhost:9090" />
</Form.Item>
- <Form.Item label="适用实例" name="instanceIds" extra="留空时可用于所有实例">
+ <Form.Item
+ label={t('settings.instances')}
+ name="instanceIds"
+ extra={t('settings.instancesHelp')}
+ >
<Select
mode="multiple"
allowClear
- placeholder="选择此数据源对应的实例"
+ placeholder={t('settings.selectDataSourceInstances')}
options={instances.map((instance) => ({
value: instance.name,
label: instance.name,
@@ -440,7 +448,7 @@ export const DataSourceTab = () => {
/>
</Form.Item>
- <Form.Item label="认证方式" name="auth" initialValue="None">
+ <Form.Item label={t('settings.authentication')} name="auth"
initialValue="None">
<Select
virtual={false}
onChange={() => {
@@ -461,18 +469,21 @@ export const DataSourceTab = () => {
{authValue === 'Basic Auth' && (
<>
<Form.Item
- label="用户名"
+ label={t('settings.username')}
name="username"
- rules={[{ required: true, message: '请输入用户名' }]}
+ rules={[{ required: true, message:
t('settings.usernameRequired') }]}
>
<Input autoComplete="username" placeholder="prometheus" />
</Form.Item>
<Form.Item
- label="密码"
+ label={t('settings.password')}
name="password"
- rules={[{ required: true, message: '请输入密码' }]}
+ rules={[{ required: true, message:
t('settings.passwordRequired') }]}
>
- <Input.Password autoComplete="current-password"
placeholder="请输入密码" />
+ <Input.Password
+ autoComplete="current-password"
+ placeholder={t('settings.passwordRequired')}
+ />
</Form.Item>
</>
)}
@@ -481,9 +492,9 @@ export const DataSourceTab = () => {
<Form.Item
label="Bearer Token"
name="bearerToken"
- rules={[{ required: true, message: '请输入 Bearer Token' }]}
+ rules={[{ required: true, message:
t('settings.bearerTokenRequired') }]}
>
- <Input.Password autoComplete="off" placeholder="请输入 Token" />
+ <Input.Password autoComplete="off"
placeholder={t('settings.enterToken')} />
</Form.Item>
)}
@@ -498,7 +509,7 @@ export const DataSourceTab = () => {
}}
style={{ marginTop: 8 }}
>
- 测试连接
+ {t('settings.testConnection')}
</Button>
</Form>
</Modal>
diff --git a/web/src/pages/settings/GeneralSettingsTab.tsx
b/web/src/pages/settings/GeneralSettingsTab.tsx
index 445ef5b0f..67ad953e4 100644
--- a/web/src/pages/settings/GeneralSettingsTab.tsx
+++ b/web/src/pages/settings/GeneralSettingsTab.tsx
@@ -34,15 +34,10 @@ import { getGeneralSettings, saveGeneralSettings } from
'../../api/settings';
import type { GeneralSettings, GeneralSettingsUpdate } from
'../../api/settings';
import { useTheme } from '../../theme/useTheme';
import type { ThemeMode } from '../../theme/themePreference';
+import { useLang } from '../../i18n/LangContext';
const { Text } = Typography;
-const THEME_OPTIONS = [
- { value: 'light', label: '浅色' },
- { value: 'dark', label: '深色' },
- { value: 'system', label: '跟随系统' },
-];
-
const toThemeMode = (value?: string): ThemeMode =>
value === 'light' || value === 'dark' ? value : 'system';
@@ -62,6 +57,7 @@ const buildPayload = (settings: GeneralSettings):
GeneralSettingsUpdate => ({
});
export const GeneralSettingsTab = () => {
+ const { t } = useLang();
const { message } = App.useApp();
const { setThemeMode, setCompact } = useTheme();
const [settings, setSettings] = useState<GeneralSettings | null>(null);
@@ -88,7 +84,7 @@ export const GeneralSettingsTab = () => {
});
})
.catch(() => {
- if (!cancelled) message.error('通用设置加载失败,请稍后重试');
+ if (!cancelled) message.error(t('settings.loadFailed'));
})
.finally(() => {
if (!cancelled) setLoading(false);
@@ -97,8 +93,7 @@ export const GeneralSettingsTab = () => {
return () => {
cancelled = true;
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [message, notifyForm, securityForm, t]);
const persistPreference = async (patch: Partial<GeneralSettings>) => {
if (!settings) return;
@@ -107,9 +102,9 @@ export const GeneralSettingsTab = () => {
setSavingPreference(true);
try {
await saveGeneralSettings(buildPayload(next));
- message.success('设置已保存');
+ message.success(t('settings.saveSuccess'));
} catch {
- message.error('设置保存失败,请稍后重试');
+ message.error(t('settings.saveFailed'));
} finally {
setSavingPreference(false);
}
@@ -122,7 +117,7 @@ export const GeneralSettingsTab = () => {
setSettings({ ...settings, ...patch });
return true;
} catch {
- message.error('设置保存失败,请稍后重试');
+ message.error(t('settings.saveFailed'));
return false;
}
};
@@ -133,7 +128,7 @@ export const GeneralSettingsTab = () => {
setSavingSecurity(true);
try {
if (await mergeAndSave({ sessionTimeout: values.sessionTimeout })) {
- message.success('设置已保存');
+ message.success(t('settings.saveSuccess'));
}
} finally {
securityInFlightRef.current = false;
@@ -151,7 +146,7 @@ export const GeneralSettingsTab = () => {
setSavingNotification(true);
try {
if (await mergeAndSave(values)) {
- message.success('设置已保存');
+ message.success(t('settings.saveSuccess'));
}
} finally {
notifyInFlightRef.current = false;
@@ -165,7 +160,7 @@ export const GeneralSettingsTab = () => {
title={
<Space>
<SkinOutlined />
- 界面偏好
+ {t('settings.appearance')}
</Space>
}
loading={loading}
@@ -173,14 +168,18 @@ export const GeneralSettingsTab = () => {
<Flex vertical gap={20}>
<Flex justify="space-between" align="center" gap={16}>
<div>
- <div>主题模式</div>
+ <div>{t('settings.themeMode')}</div>
<Text type="secondary" style={{ fontSize: 14 }}>
- 作用于前端展示,跟随系统时随操作系统外观变化
+ {t('settings.themeHelp')}
</Text>
</div>
<Segmented
value={toThemeMode(settings?.theme)}
- options={THEME_OPTIONS}
+ options={[
+ { value: 'light', label: t('settings.lightTheme') },
+ { value: 'dark', label: t('settings.darkTheme') },
+ { value: 'system', label: t('settings.systemTheme') },
+ ]}
disabled={savingPreference}
onChange={(value) => {
const mode = value as ThemeMode;
@@ -191,9 +190,9 @@ export const GeneralSettingsTab = () => {
</Flex>
<Flex justify="space-between" align="center" gap={16}>
<div>
- <div>紧凑模式</div>
+ <div>{t('settings.compactMode')}</div>
<Text type="secondary" style={{ fontSize: 14 }}>
- 减小组件间距,单屏展示更多内容
+ {t('settings.compactHelp')}
</Text>
</div>
<Switch
@@ -212,37 +211,42 @@ export const GeneralSettingsTab = () => {
title={
<Space>
<SafetyOutlined />
- 安全与会话
+ {t('settings.securitySession')}
</Space>
}
loading={loading}
>
<Form form={securityForm} layout="vertical"
onFinish={handleSecurityFinish}>
<Form.Item
- label="会话超时"
- extra="应用于新创建的会话,已登录用户保持原到期时间"
+ label={t('settings.sessionTimeout')}
+ extra={t('settings.sessionTimeoutHelp')}
style={{ marginBottom: 16 }}
>
<Space.Compact>
<Form.Item
name="sessionTimeout"
noStyle
- rules={[{ required: true, message: '请输入会话超时时长' }]}
+ rules={[{ required: true, message:
t('settings.sessionTimeoutRequired') }]}
>
<InputNumber min={5} max={1440} style={{ width: 120 }} />
</Form.Item>
- <Input aria-label="会话超时单位" readOnly value="分钟" style={{ width:
64 }} />
+ <Input
+ aria-label={t('settings.sessionTimeoutUnit')}
+ readOnly
+ value={t('settings.minutes')}
+ style={{ width: 64 }}
+ />
</Space.Compact>
</Form.Item>
<Form.Item style={{ marginBottom: 16 }}>
<Button type="primary" htmlType="submit" loading={savingSecurity}
disabled={loading}>
- 保存设置
+ {t('settings.saveSettings')}
</Button>
</Form.Item>
<Text type="secondary" style={{ fontSize: 14 }}>
- 登录保护由服务端 STUDIO_AUTH_LOGIN_REQUIRED 配置决定,修改后重启服务生效。
+ {t('settings.loginProtectionHelp')}
</Text>
</Form>
</Card>
@@ -251,32 +255,32 @@ export const GeneralSettingsTab = () => {
title={
<Space>
<BellOutlined />
- 通知设置
+ {t('settings.notification')}
</Space>
}
loading={loading}
>
<Form form={notifyForm} layout="vertical"
onFinish={handleNotifyFinish}>
<Form.Item
- label="钉钉机器人 Webhook"
+ label={t('settings.dingtalkWebhook')}
name="dingtalkWebhook"
- extra="告警规则选择钉钉渠道时推送到该机器人"
+ extra={t('settings.dingtalkWebhookHelp')}
>
<Input
placeholder="https://oapi.dingtalk.com/robot/send?access_token=..." />
</Form.Item>
<Form.Item
- label="邮件收件人"
+ label={t('settings.emailRecipients')}
name="emailRecipients"
- extra="多个地址用英文逗号分隔;邮件发送通道后续接入"
+ extra={t('settings.emailRecipientsHelp')}
>
<Input.TextArea rows={2} placeholder="[email protected],
[email protected]" />
</Form.Item>
<Form.Item
- label="短信网关 Webhook"
+ label={t('settings.smsWebhook')}
name="smsWebhook"
- extra="占位配置,短信发送通道后续接入"
+ extra={t('settings.smsWebhookHelp')}
>
<Input placeholder="https://sms-gateway.example.com/notify" />
</Form.Item>
@@ -288,7 +292,7 @@ export const GeneralSettingsTab = () => {
loading={savingNotification}
disabled={loading}
>
- 保存设置
+ {t('settings.saveSettings')}
</Button>
</Form.Item>
</Form>
diff --git a/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx
b/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx
index bf2050050..7bb6aad54 100644
--- a/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx
+++ b/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx
@@ -20,6 +20,7 @@ import { act, render, screen, waitFor } from
'@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { LangProvider } from '../../../i18n/LangContext';
+import { LANGUAGE_STORAGE_KEY } from '../../../i18n/languagePreference';
import { AiAssistantTab } from '../AiAssistantTab';
const llmApiMocks = vi.hoisted(() => ({
@@ -59,6 +60,7 @@ const renderPage = () =>
describe('AiAssistantTab', () => {
beforeEach(() => {
vi.clearAllMocks();
+ localStorage.removeItem(LANGUAGE_STORAGE_KEY);
llmApiMocks.getLlmConfig.mockResolvedValue({
provider: 'tongyi',
apiBase: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
@@ -83,6 +85,14 @@ describe('AiAssistantTab', () => {
expect(screen.getByDisplayValue('qwen3.8-max')).toBeInTheDocument();
});
+ it('renders assistant configuration labels in English when English is
selected', async () => {
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+ renderPage();
+
+ expect(await screen.findByText('Execution engine and
model')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Test connection'
})).toBeInTheDocument();
+ });
+
it('saves without sending an apiKey when the input stays empty', async () =>
{
const user = userEvent.setup();
renderPage();
diff --git a/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
b/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
index bcb4dde62..f205e049d 100644
--- a/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
+++ b/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
@@ -27,6 +27,7 @@ import {
updateCloudCredential,
} from '../../../api/cloudCredential';
import { LangProvider } from '../../../i18n/LangContext';
+import { LANGUAGE_STORAGE_KEY } from '../../../i18n/languagePreference';
import { CloudCredentialTab } from '../CloudCredentialTab';
vi.mock('../../../api/cloudCredential', () => ({
@@ -88,6 +89,7 @@ beforeAll(() => {
describe('CloudCredentialTab', () => {
beforeEach(() => {
vi.clearAllMocks();
+ localStorage.removeItem(LANGUAGE_STORAGE_KEY);
vi.mocked(listCloudCredentials).mockResolvedValue(credentials);
});
@@ -105,6 +107,13 @@ describe('CloudCredentialTab', () => {
await waitFor(() =>
expect(listCloudCredentials).toHaveBeenCalledWith(undefined, '', 1, 20));
});
+ it('renders management controls in English when English is selected', async
() => {
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+ renderTab();
+
+ expect(await screen.findByPlaceholderText('Search credential
names')).toBeInTheDocument();
+ });
+
it('resets to the first page and queries filters after they change', async
() => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderTab();
diff --git a/web/src/pages/settings/__tests__/DataSourceTab.test.tsx
b/web/src/pages/settings/__tests__/DataSourceTab.test.tsx
index a050acf80..b7d886e73 100644
--- a/web/src/pages/settings/__tests__/DataSourceTab.test.tsx
+++ b/web/src/pages/settings/__tests__/DataSourceTab.test.tsx
@@ -22,6 +22,7 @@ import { App } from 'antd';
import type { DataSource, DataSourcePage } from '../../../api/settings';
import { createDataSource, listDataSourcesPage, testDataSource } from
'../../../api/settings';
import { LangProvider } from '../../../i18n/LangContext';
+import { LANGUAGE_STORAGE_KEY } from '../../../i18n/languagePreference';
import { DataSourceTab } from '../DataSourceTab';
vi.mock('../../../api/settings', () => ({
@@ -88,6 +89,7 @@ beforeAll(() => {
describe('DataSourceTab', () => {
beforeEach(() => {
vi.clearAllMocks();
+ localStorage.removeItem(LANGUAGE_STORAGE_KEY);
vi.mocked(listDataSourcesPage).mockResolvedValue(sourcePage);
});
@@ -128,6 +130,19 @@ describe('DataSourceTab', () => {
expect(screen.queryByText('离线')).not.toBeInTheDocument();
});
+ it('renders management controls in English when English is selected', async
() => {
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+ render(
+ <LangProvider>
+ <App>
+ <DataSourceTab />
+ </App>
+ </LangProvider>,
+ );
+
+ expect(await screen.findByPlaceholderText('Search data source
names')).toBeInTheDocument();
+ });
+
it('shows connection test loading only on the clicked row', async () => {
let resolveTest: (value: { success: boolean; message: string }) => void =
() => undefined;
vi.mocked(testDataSource).mockReturnValue(
diff --git a/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
b/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
index ba0f78b0c..f7ee93cfe 100644
--- a/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
+++ b/web/src/pages/settings/__tests__/GeneralSettingsTab.test.tsx
@@ -21,6 +21,8 @@ import userEvent from '@testing-library/user-event';
import { App } from 'antd';
import { getGeneralSettings, saveGeneralSettings } from
'../../../api/settings';
import { ThemeProvider } from '../../../theme/ThemeProvider';
+import { LangProvider } from '../../../i18n/LangContext';
+import { LANGUAGE_STORAGE_KEY } from '../../../i18n/languagePreference';
import { GeneralSettingsTab } from '../GeneralSettingsTab';
beforeAll(() => {
@@ -52,9 +54,11 @@ vi.mock('../../../api/settings', () => ({
const renderTab = () =>
render(
<App>
- <ThemeProvider>
- <GeneralSettingsTab />
- </ThemeProvider>
+ <LangProvider>
+ <ThemeProvider>
+ <GeneralSettingsTab />
+ </ThemeProvider>
+ </LangProvider>
</App>,
);
@@ -123,6 +127,14 @@ describe('GeneralSettingsTab', () => {
);
});
+ it('renders section labels in English when English is selected', async () =>
{
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+ renderTab();
+
+ expect(await screen.findByText('Appearance
preferences')).toBeInTheDocument();
+ expect(screen.getByText('Session timeout')).toBeInTheDocument();
+ });
+
it('saves immediately when the theme mode preference changes', async () => {
vi.mocked(saveGeneralSettings).mockResolvedValue();
const user = userEvent.setup();
diff --git a/web/src/pages/settings/index.tsx b/web/src/pages/settings/index.tsx
index 5400e098c..22c138ea6 100644
--- a/web/src/pages/settings/index.tsx
+++ b/web/src/pages/settings/index.tsx
@@ -39,17 +39,21 @@ const SettingsPage = () => {
return (
<div style={{ padding: 24 }}>
- <PageHeader title={t('settings.title')} subtitle="管理应用配置与数据源" />
+ <PageHeader title={t('settings.title')}
subtitle={t('settings.subtitle')} />
<Tabs
activeKey={activeKey}
onChange={(key) => setSearchParams({ tab: key })}
items={[
- { key: 'general', label: '通用设置', children: <GeneralSettingsTab /> },
- { key: 'ai', label: 'AI 助手', children: <AiAssistantTab /> },
- { key: 'credential', label: '云凭据管理', children: <CloudCredentialTab
/> },
- { key: 'datasource', label: '数据源管理', children: <DataSourceTab /> },
- { key: 'about', label: '关于', children: <AboutTab /> },
+ { key: 'general', label: t('settings.tabGeneral'), children:
<GeneralSettingsTab /> },
+ { key: 'ai', label: t('settings.tabAi'), children: <AiAssistantTab
/> },
+ {
+ key: 'credential',
+ label: t('settings.tabCredential'),
+ children: <CloudCredentialTab />,
+ },
+ { key: 'datasource', label: t('settings.tabDatasource'), children:
<DataSourceTab /> },
+ { key: 'about', label: t('settings.tabAbout'), children: <AboutTab
/> },
]}
/>
</div>