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 c2bd8d169 feat(i18n): complete instance and query history translations
(#2674)
c2bd8d169 is described below
commit c2bd8d16911d63d9fe98964e8f2aab433828032e
Author: aias00 <[email protected]>
AuthorDate: Tue Sep 8 11:35:34 2026 +0800
feat(i18n): complete instance and query history translations (#2674)
Route the instance management page and server query history drawer through
the shared translation catalog while preserving the latest import failure and
trace-topic behavior.
Constraint: exclude all K8s certificate page and vendorOptions changes from
this rework.
Tested: NODE_ENV=test npm test --
src/components/__tests__/MessageQueryHistoryDrawer.test.tsx
src/pages/instance/__tests__/InstancePage.test.tsx
Tested: npm run lint
Tested: npm run build
Signed-off-by: liuhy <[email protected]>
---
web/src/components/MessageQueryHistoryDrawer.tsx | 71 ++--
.../__tests__/MessageQueryHistoryDrawer.test.tsx | 34 +-
web/src/i18n/translations.ts | 230 ++++++++++++-
.../pages/instance/__tests__/InstancePage.test.tsx | 14 +
web/src/pages/instance/index.tsx | 362 ++++++++++++---------
5 files changed, 531 insertions(+), 180 deletions(-)
diff --git a/web/src/components/MessageQueryHistoryDrawer.tsx
b/web/src/components/MessageQueryHistoryDrawer.tsx
index fc25f3963..9c4b0e08e 100644
--- a/web/src/components/MessageQueryHistoryDrawer.tsx
+++ b/web/src/components/MessageQueryHistoryDrawer.tsx
@@ -15,6 +15,7 @@ import {
type QueryHistorySummary,
type TraceQueryHistory,
} from '../api/messageHistory';
+import { useLang } from '../i18n/LangContext';
interface Props {
open: boolean;
@@ -38,6 +39,7 @@ const MessageQueryHistoryDrawer = ({
onSelectMessage,
onSelectTrace,
}: Props) => {
+ const { t } = useLang();
const [tab, setTab] = useState<'messages' | 'traces'>('messages');
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
@@ -82,12 +84,12 @@ const MessageQueryHistoryDrawer = ({
else setTraceRows(result.items as TraceQueryHistory[]);
} catch (loadError) {
if (id === requestId.current) {
- setError(loadError instanceof Error ? loadError.message : '查询历史加载失败');
+ setError(loadError instanceof Error ? loadError.message :
t('messageHistory.loadFailed'));
}
} finally {
if (id === requestId.current) setLoading(false);
}
- }, [clusterId, open, page, search, tab]);
+ }, [clusterId, open, page, search, t, tab]);
useEffect(() => {
// Loading is asynchronous; state updates happen after the history API
resolves.
@@ -99,38 +101,65 @@ const MessageQueryHistoryDrawer = ({
}, [load]);
const messageColumns: ColumnsType<MessageQueryHistory> = [
- { title: '类型', dataIndex: 'queryType', width: 90, render: (value) =>
<Tag>{value}</Tag> },
+ {
+ title: t('common.type'),
+ dataIndex: 'queryType',
+ width: 90,
+ render: (value) => <Tag>{value}</Tag>,
+ },
{ title: 'Topic', dataIndex: 'topic', ellipsis: true },
{ title: 'Message ID / Key', render: (_, row) => row.msgId ||
row.messageKey || '-' },
- { title: '结果数', dataIndex: 'resultCount', width: 80 },
- { title: '操作者', dataIndex: 'queriedBy', width: 110 },
- { title: '查询时间', dataIndex: 'queriedAt', width: 180, render: formatTime },
+ { title: t('messageHistory.resultCount'), dataIndex: 'resultCount', width:
80 },
+ { title: t('messageHistory.operator'), dataIndex: 'queriedBy', width: 110
},
+ {
+ title: t('messageHistory.queryTime'),
+ dataIndex: 'queriedAt',
+ width: 180,
+ render: formatTime,
+ },
];
const traceColumns: ColumnsType<TraceQueryHistory> = [
{ title: 'Message ID', dataIndex: 'msgId', ellipsis: true },
{ title: 'Topic', dataIndex: 'topic', ellipsis: true },
{
- title: '轨迹 Topic',
+ title: t('messageHistory.traceTopic'),
dataIndex: 'traceTopic',
ellipsis: true,
- render: (value?: string) => value?.trim() || '默认',
+ render: (value?: string) => value?.trim() || t('common.default'),
+ },
+ { title: t('messageHistory.traceNodes'), dataIndex: 'nodeCount', width: 90
},
+ { title: t('messageHistory.consumers'), dataIndex: 'consumerCount', width:
90 },
+ { title: t('messageHistory.operator'), dataIndex: 'queriedBy', width: 110
},
+ {
+ title: t('messageHistory.queryTime'),
+ dataIndex: 'queriedAt',
+ width: 180,
+ render: formatTime,
},
- { title: '轨迹节点', dataIndex: 'nodeCount', width: 90 },
- { title: '消费者', dataIndex: 'consumerCount', width: 90 },
- { title: '操作者', dataIndex: 'queriedBy', width: 110 },
- { title: '查询时间', dataIndex: 'queriedAt', width: 180, render: formatTime },
];
return (
- <Drawer title="服务端查询历史" width={900} open={open} onClose={onClose}
destroyOnHidden>
+ <Drawer
+ title={t('messageHistory.title')}
+ width={900}
+ open={open}
+ onClose={onClose}
+ destroyOnHidden
+ >
<Flex gap={32} style={{ marginBottom: 16 }}>
- <Statistic title="消息查询" value={summary?.messageQueries ?? 0} />
- <Statistic title="轨迹查询" value={summary?.traceQueries ?? 0} />
- <Statistic title="最近查询" value={formatTime(summary?.latestQueryAt)} />
+ <Statistic
+ title={t('messageHistory.messageQueries')}
+ value={summary?.messageQueries ?? 0}
+ />
+ <Statistic title={t('messageHistory.traceQueries')}
value={summary?.traceQueries ?? 0} />
+ <Statistic
+ title={t('messageHistory.latestQuery')}
+ value={formatTime(summary?.latestQueryAt)}
+ />
</Flex>
<Input.Search
allowClear
- placeholder="搜索 Topic、轨迹 Topic、Message ID、Key 或操作者"
+ placeholder={t('messageHistory.searchPlaceholder')}
onSearch={(value) => {
setPage(1);
setSearch(value.trim());
@@ -141,11 +170,11 @@ const MessageQueryHistoryDrawer = ({
<Alert
type="error"
showIcon
- message="查询历史加载失败"
+ message={t('messageHistory.loadFailed')}
description={error}
action={
<Button size="small" onClick={() => void load()}>
- 重试
+ {t('common.retry')}
</Button>
}
style={{ marginBottom: 12 }}
@@ -160,7 +189,7 @@ const MessageQueryHistoryDrawer = ({
items={[
{
key: 'messages',
- label: '消息查询',
+ label: t('messageHistory.messageQueries'),
children: (
<Table
rowKey="id"
@@ -181,7 +210,7 @@ const MessageQueryHistoryDrawer = ({
},
{
key: 'traces',
- label: '轨迹查询',
+ label: t('messageHistory.traceQueries'),
children: (
<Table
rowKey="id"
diff --git a/web/src/components/__tests__/MessageQueryHistoryDrawer.test.tsx
b/web/src/components/__tests__/MessageQueryHistoryDrawer.test.tsx
index 474234be4..3de2391b1 100644
--- a/web/src/components/__tests__/MessageQueryHistoryDrawer.test.tsx
+++ b/web/src/components/__tests__/MessageQueryHistoryDrawer.test.tsx
@@ -9,6 +9,8 @@ import { render, screen, waitFor } from
'@testing-library/react';
import userEvent from '@testing-library/user-event';
import { App } from 'antd';
import MessageQueryHistoryDrawer from '../MessageQueryHistoryDrawer';
+import { LangProvider } from '../../i18n/LangContext';
+import { LANGUAGE_STORAGE_KEY } from '../../i18n/languagePreference';
import {
getQueryHistorySummary,
listMessageQueryHistory,
@@ -37,6 +39,7 @@ beforeAll(() => {
describe('MessageQueryHistoryDrawer', () => {
beforeEach(() => {
vi.clearAllMocks();
+ localStorage.clear();
vi.mocked(getQueryHistorySummary).mockResolvedValue({ messageQueries: 4,
traceQueries: 2 });
vi.mocked(listMessageQueryHistory).mockResolvedValue({
items: [
@@ -77,7 +80,9 @@ describe('MessageQueryHistoryDrawer', () => {
const user = userEvent.setup();
render(
<App>
- <MessageQueryHistoryDrawer open clusterId="instance-a"
onClose={vi.fn()} />
+ <LangProvider>
+ <MessageQueryHistoryDrawer open clusterId="instance-a"
onClose={vi.fn()} />
+ </LangProvider>
</App>,
);
@@ -94,7 +99,9 @@ describe('MessageQueryHistoryDrawer', () => {
it('clears stale rows and offers retry when a new instance load fails',
async () => {
const view = render(
<App>
- <MessageQueryHistoryDrawer open clusterId="instance-a"
onClose={vi.fn()} />
+ <LangProvider>
+ <MessageQueryHistoryDrawer open clusterId="instance-a"
onClose={vi.fn()} />
+ </LangProvider>
</App>,
);
expect(await screen.findByText('order-1')).toBeInTheDocument();
@@ -102,7 +109,9 @@ describe('MessageQueryHistoryDrawer', () => {
view.rerender(
<App>
- <MessageQueryHistoryDrawer open clusterId="instance-b"
onClose={vi.fn()} />
+ <LangProvider>
+ <MessageQueryHistoryDrawer open clusterId="instance-b"
onClose={vi.fn()} />
+ </LangProvider>
</App>,
);
@@ -110,4 +119,23 @@ describe('MessageQueryHistoryDrawer', () => {
expect(screen.queryByText('order-1')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: /重\s*试/ })).toBeEnabled();
});
+
+ it('renders query history drawer copy in English mode', async () => {
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+ render(
+ <App>
+ <LangProvider>
+ <MessageQueryHistoryDrawer open clusterId="instance-a"
onClose={vi.fn()} />
+ </LangProvider>
+ </App>,
+ );
+
+ expect(await screen.findByText('order-1')).toBeInTheDocument();
+ expect(screen.getByText('Server Query History')).toBeInTheDocument();
+ expect(screen.getAllByText('Message Queries').length).toBeGreaterThan(1);
+ expect(
+ screen.getByPlaceholderText('Search Topic, trace Topic, Message ID, Key
or operator'),
+ ).toBeInTheDocument();
+ expect(screen.queryByText('服务端查询历史')).not.toBeInTheDocument();
+ });
});
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 85c342f66..353df823b 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -74,6 +74,7 @@ const translations: Record<string, Record<Lang, string>> = {
'common.yes': { zh: '是', en: 'Yes' },
'common.no': { zh: '否', en: 'No' },
'common.retry': { zh: '重试', en: 'Retry' },
+ 'common.unavailable': { zh: '不可用', en: 'Unavailable' },
// ─── Global layout controls ───
'layout.skipToMain': { zh: '跳到主要内容', en: 'Skip to main content' },
@@ -204,11 +205,19 @@ const translations: Record<string, Record<Lang, string>>
= {
// ─── Instance / Topic / Group / ACL / Message ───
'instance.title': { zh: '实例列表', en: 'Instance List' },
'instance.subtitle': { zh: '管理 RocketMQ 集群连接', en: 'Manage RocketMQ cluster
connections' },
+ 'instance.managementSubtitle': {
+ zh: '接入并管理 RocketMQ 实例(开源自建 / 阿里云 / 腾讯云),当前显示 {count} 个实例',
+ en: 'Connect and manage RocketMQ instances (Apache / Aliyun / Tencent).
Showing {count} instances.',
+ },
'instance.count': { zh: '共 {n} 个实例', en: '{n} instances' },
'instance.searchPlaceholder': { zh: '搜索实例 ID 或地址', en: 'Search instance ID
or endpoint' },
'instance.allTypes': { zh: '全部架构', en: 'All Types' },
+ 'instance.cloudType': { zh: '云服务', en: 'Cloud Service' },
+ 'instance.proxyLocalMode': { zh: 'Proxy Local 模式', en: 'Proxy Local Mode' },
+ 'instance.proxyClusterMode': { zh: 'Proxy Cluster 模式', en: 'Proxy Cluster
Mode' },
'instance.directMode': { zh: 'Direct 模式', en: 'Direct Mode' },
'instance.addInstance': { zh: '添加实例', en: 'Add Instance' },
+ 'instance.editInstanceTitle': { zh: '编辑实例 — {name}', en: 'Edit Instance —
{name}' },
'instance.instanceName': { zh: '实例 ID', en: 'Instance ID' },
'instance.namePlaceholder': { zh: '例:rocketmq-production', en: 'e.g.
rocketmq-production' },
'instance.accessType': { zh: '接入方式', en: 'Access Type' },
@@ -219,22 +228,241 @@ const translations: Record<string, Record<Lang, string>>
= {
en: 'e.g. proxy.example.com:8080',
},
'instance.remark': { zh: '备注', en: 'Remark' },
+ 'instance.remarkPlaceholder': {
+ zh: '可选,描述实例用途',
+ en: 'Optional, describe the instance purpose',
+ },
+ 'instance.remarkEditPlaceholder': { zh: '描述实例用途', en: 'Describe the instance
purpose' },
'instance.connect': { zh: '连接', en: 'Connect' },
'instance.cancel': { zh: '取消', en: 'Cancel' },
'instance.edit': { zh: '编辑', en: 'Edit' },
'instance.delete': { zh: '删除', en: 'Delete' },
'instance.confirmDelete': { zh: '确认删除 "{name}"?', en: 'Delete "{name}"?' },
+ 'instance.confirmBatchDelete': {
+ zh: '确认删除选中的 {count} 个实例?',
+ en: 'Delete {count} selected instances?',
+ },
+ 'instance.batchDeleteContent': {
+ zh: '将删除:{names}。{warning}',
+ en: 'Instances to delete: {names}. {warning}',
+ },
+ 'instance.cloudBatchDeleteWarning': {
+ zh: '云厂商实例仅从 Studio 移除记录,不会释放云上的 RocketMQ 实例;仍有 Topic/Group 的开源实例无法删除。',
+ en: 'Cloud instances are only removed from Studio; the RocketMQ instances
in the cloud are not released. Apache instances with Topic/Group resources
still cannot be deleted.',
+ },
+ 'instance.batchDeleteWarning': {
+ zh: '仍有 Topic/Group 的开源实例无法删除。',
+ en: 'Apache instances with Topic/Group resources still cannot be deleted.',
+ },
+ 'instance.cloudDeleteWarning': {
+ zh: '仅从 Studio 移除该实例记录,不会释放云上的 RocketMQ 实例。',
+ en: 'Only removes this instance record from Studio. The RocketMQ instance
in the cloud is not released.',
+ },
'instance.deleteWarning': { zh: '此操作不可恢复。', en: 'This cannot be undone.' },
'instance.deleted': { zh: '已删除', en: 'Deleted' },
- 'instance.added': { zh: '实例添加成功', en: 'Instance added' },
+ 'instance.added': { zh: '实例「{name}」添加成功', en: 'Instance "{name}" added' },
+ 'instance.updated': { zh: '实例「{name}」已更新', en: 'Instance "{name}" updated' },
'instance.enter': { zh: '进入 {name}', en: 'Enter {name}' },
'instance.createdAt': { zh: '创建时间', en: 'Created' },
'instance.updatedAt': { zh: '修改时间', en: 'Updated' },
+ 'instance.region': { zh: '地域', en: 'Region' },
+ 'instance.vendor': { zh: '厂商', en: 'Vendor' },
+ 'instance.aliyunEdition': { zh: 'Aliyun 版', en: 'Aliyun' },
+ 'instance.tencentEdition': { zh: 'Tencent 版', en: 'Tencent' },
+ 'instance.apacheDescription': {
+ zh: '接入自建 Apache RocketMQ 开源集群,支持 Proxy / Direct 两种接入方式',
+ en: 'Connect a self-managed Apache RocketMQ cluster through Proxy or
Direct access',
+ },
+ 'instance.aliyunDescription': {
+ zh: '选择已录入的云凭据与云上实例完成接入,接入点自动解析',
+ en: 'Select stored cloud credentials and a cloud instance; the endpoint is
resolved automatically',
+ },
+ 'instance.tencentDescription': {
+ zh: '接入腾讯云 TDMQ RocketMQ 版实例,接入地址填写实例的接入点',
+ en: 'Connect a Tencent Cloud TDMQ for RocketMQ instance using its
endpoint',
+ },
+ 'instance.openSourceEdition': { zh: '开源版', en: 'Apache' },
+ 'instance.listLoadFailed': {
+ zh: '实例列表加载失败,请稍后重试',
+ en: 'Failed to load instances. Please try again later.',
+ },
+ 'instance.cloudCredentialLoadFailed': {
+ zh: '云凭据列表加载失败',
+ en: 'Failed to load cloud credentials',
+ },
+ 'instance.cloudRegionLoadFailed': {
+ zh: '云地域列表加载失败',
+ en: 'Failed to load cloud regions',
+ },
+ 'instance.cloudInstanceLoadFailed': {
+ zh: '云实例列表加载失败',
+ en: 'Failed to load cloud instances',
+ },
+ 'instance.createFailed': {
+ zh: '添加实例失败,请稍后重试',
+ en: 'Failed to add instance. Please try again later.',
+ },
+ 'instance.updateFailed': {
+ zh: '更新实例失败,请稍后重试',
+ en: 'Failed to update instance. Please try again later.',
+ },
+ 'instance.deleteFailed': {
+ zh: '删除实例失败,请稍后重试',
+ en: 'Failed to delete instance. Please try again later.',
+ },
+ 'instance.batchDeleteFailed': {
+ zh: '批量删除失败,请稍后重试',
+ en: 'Failed to delete selected instances. Please try again later.',
+ },
+ 'instance.selectCloudCredentialFirst': {
+ zh: '请先选择云凭据',
+ en: 'Select cloud credentials first',
+ },
+ 'instance.importAll': { zh: '一键导入', en: 'Import All' },
+ 'instance.importAllTooltip': {
+ zh: '遍历该凭据下全部地域,将所有云上实例导入(幂等,已存在的自动跳过),备注自动取自云上实例',
+ en: 'Scan all regions for this credential and import cloud instances.
Existing instances are skipped and remarks come from the cloud instance.',
+ },
+ 'instance.importSuccess': {
+ zh: '导入完成:共同步 {total} 个实例(新导入 {imported},已存在跳过 {skipped})',
+ en: 'Import complete: synced {total} instances ({imported} imported,
{skipped} skipped)',
+ },
+ 'instance.importAllSkipped': {
+ zh: '云上实例均已在 Studio 中(共 {skipped} 个),无需重复导入',
+ en: 'All cloud instances are already in Studio ({skipped} total). Nothing
to import.',
+ },
+ 'instance.importIncomplete': {
+ zh: '导入未完成:新导入 {imported} 个,已存在跳过 {skipped} 个',
+ en: 'Import incomplete: {imported} imported, {skipped} skipped',
+ },
+ 'instance.importPartialFailure': {
+ zh: '{summary},失败 {count} 个{omitted}{details}',
+ en: '{summary}. {count} failed{omitted}{details}',
+ },
+ 'instance.importFailureDetailsTruncated': {
+ zh: '(仅显示前 {count} 条)',
+ en: ' (showing the first {count})',
+ },
+ 'instance.importFailed': {
+ zh: '一键导入失败,请稍后重试',
+ en: 'Failed to import cloud instances. Please try again later.',
+ },
+ 'instance.deletedCount': { zh: '已删除 {count} 个', en: 'Deleted {count}' },
+ 'instance.batchDeletePartialFailure': {
+ zh: '{summary},{count} 个未能删除:{failed}',
+ en: '{summary}. {count} failed to delete: {failed}',
+ },
+ 'instance.cloudCredential': { zh: '云凭据', en: 'Cloud Credential' },
+ 'instance.cloudCredentialRequired': { zh: '请选择云凭据', en: 'Select cloud
credentials' },
+ 'instance.cloudCredentialExtraPrefix': {
+ zh: '凭据为{vendor}账号的 AK/SK,',
+ en: 'Credentials are the AK/SK of the {vendor} account. ',
+ },
+ 'instance.cloudCredentialSettingsLink': {
+ zh: '前往「设置 - 云凭据管理」添加',
+ en: 'Add them in Settings - Cloud Credentials',
+ },
+ 'instance.selectStoredCredential': {
+ zh: '选择已录入的 AK/SK 凭据',
+ en: 'Select stored AK/SK credentials',
+ },
+ 'instance.loading': { zh: '加载中…', en: 'Loading...' },
+ 'instance.noCloudCredential': {
+ zh: '暂无{vendor}凭据,',
+ en: 'No {vendor} credentials. ',
+ },
+ 'instance.addInSettings': { zh: '去设置中添加', en: 'Add in settings' },
+ 'instance.regionRequired': { zh: '请选择地域', en: 'Select a region' },
+ 'instance.selectRegion': { zh: '选择地域', en: 'Select a region' },
+ 'instance.cloudInstance': { zh: '云上实例', en: 'Cloud Instance' },
+ 'instance.cloudInstanceRequired': { zh: '请选择云上实例', en: 'Select a cloud
instance' },
+ 'instance.cloudInstanceExtra': {
+ zh: '商业版实例来自云端目录,无法手工创建',
+ en: 'Commercial instances come from the cloud catalog and cannot be
created manually',
+ },
+ 'instance.selectCloudInstance': { zh: '选择云上实例', en: 'Select a cloud
instance' },
+ 'instance.selectRegionFirst': { zh: '请先选择地域', en: 'Select a region first' },
+ 'instance.nameRequired': { zh: '请输入实例 ID', en: 'Enter an instance ID' },
+ 'instance.nameMax': {
+ zh: '实例 ID 不能超过 64 个字符',
+ en: 'Instance ID cannot exceed 64 characters',
+ },
+ 'instance.cloudNamePlaceholder': {
+ zh: '默认取云上实例 ID',
+ en: 'Defaults to the cloud instance ID',
+ },
+ 'instance.accessTypeRequired': { zh: '请选择接入方式', en: 'Select an access type'
},
+ 'instance.endpointRequired': { zh: '请输入接入地址', en: 'Enter an endpoint' },
+ 'instance.endpointHelp': {
+ zh: '接入地址为客户端访问入口,会展示在 Topic 等页面供客户端配置使用。若客户端环境无法解析该地址(如 K8s 内部 Service
域名),可自行配置 DNS 解析或在客户端 hosts 中映射。',
+ en: 'The endpoint is the client access entry and is shown on Topic pages
for client configuration. If clients cannot resolve it, configure DNS or hosts
mapping.',
+ },
+ 'instance.directEndpointExtra': {
+ zh: 'Direct 模式请填写 NameServer SLB 地址(K8s 场景下一般为 NameServer Service 地址,如
namesrv.mq.svc:9876)',
+ en: 'For Direct mode, enter the NameServer SLB address. In K8s this is
usually the NameServer Service address, such as namesrv.mq.svc:9876.',
+ },
+ 'instance.proxyLocalEndpointExtra': {
+ zh: 'Proxy Local 模式请填写与 Broker 同进程部署的 Proxy 接入地址(如
broker-proxy.mq.svc:8080)',
+ en: 'For Proxy Local mode, enter the Proxy endpoint deployed with the
Broker, such as broker-proxy.mq.svc:8080.',
+ },
+ 'instance.proxyClusterEndpointExtra': {
+ zh: 'Proxy Cluster 模式请填写独立 Proxy 集群的 SLB 内网地址(如 proxy.mq.svc:8080)',
+ en: 'For Proxy Cluster mode, enter the internal SLB endpoint of the
standalone Proxy cluster, such as proxy.mq.svc:8080.',
+ },
+ 'instance.cloudEndpointExtra': {
+ zh: '云服务实例接入地址由云厂商目录解析,不支持手动修改',
+ en: 'Cloud instance endpoints are resolved from the cloud catalog and
cannot be edited manually.',
+ },
+ 'instance.selectAccessTypeFirst': {
+ zh: '请先选择接入方式',
+ en: 'Select an access type first',
+ },
+ 'instance.directEndpointPlaceholder': {
+ zh: '例:namesrv.mq.svc.cluster.local:9876',
+ en: 'e.g. namesrv.mq.svc.cluster.local:9876',
+ },
+ 'instance.proxyEndpointPlaceholder': {
+ zh: '例:proxy.mq.svc.cluster.local:8080',
+ en: 'e.g. proxy.mq.svc.cluster.local:8080',
+ },
+ 'instance.adminCredentialRef': { zh: '管理凭据引用', en: 'Admin Credential Ref' },
+ 'instance.adminCredentialRefExtra': {
+ zh: '可选。仅保存服务端配置中的凭据引用,不会保存或传输 AK/SK。',
+ en: 'Optional. Only stores the credential reference in server config;
AK/SK is not stored or transferred.',
+ },
+ 'instance.adminCredentialRefEditExtra': {
+ zh: '仅保存服务端配置中的引用,不会保存或传输 AK/SK。',
+ en: 'Only stores the reference in server config; AK/SK is not stored or
transferred.',
+ },
+ 'instance.adminCredentialRefPlaceholder': {
+ zh: '例:production-admin',
+ en: 'e.g. production-admin',
+ },
'topic.title': { zh: 'Topic 管理', en: 'Topic Management' },
'group.title': { zh: 'Group 管理', en: 'Group Management' },
'acl.title': { zh: 'ACL 管理', en: 'ACL Management' },
'message.title': { zh: '消息查询', en: 'Message Search' },
+ // ─── Message Query History ───
+ 'messageHistory.title': { zh: '服务端查询历史', en: 'Server Query History' },
+ 'messageHistory.messageQueries': { zh: '消息查询', en: 'Message Queries' },
+ 'messageHistory.traceQueries': { zh: '轨迹查询', en: 'Trace Queries' },
+ 'messageHistory.latestQuery': { zh: '最近查询', en: 'Latest Query' },
+ 'messageHistory.searchPlaceholder': {
+ zh: '搜索 Topic、轨迹 Topic、Message ID、Key 或操作者',
+ en: 'Search Topic, trace Topic, Message ID, Key or operator',
+ },
+ 'messageHistory.loadFailed': {
+ zh: '查询历史加载失败',
+ en: 'Failed to load query history',
+ },
+ 'messageHistory.resultCount': { zh: '结果数', en: 'Results' },
+ 'messageHistory.operator': { zh: '操作者', en: 'Operator' },
+ 'messageHistory.queryTime': { zh: '查询时间', en: 'Query Time' },
+ 'messageHistory.traceTopic': { zh: '轨迹 Topic', en: 'Trace Topic' },
+ 'messageHistory.traceNodes': { zh: '轨迹节点', en: 'Trace Nodes' },
+ 'messageHistory.consumers': { zh: '消费者', en: 'Consumers' },
+
// ─── Dead Letter Queue ───
'dlq.title': { zh: '死信队列', en: 'Dead Letter Queue' },
diff --git a/web/src/pages/instance/__tests__/InstancePage.test.tsx
b/web/src/pages/instance/__tests__/InstancePage.test.tsx
index b0c7fed96..ec2494ef3 100644
--- a/web/src/pages/instance/__tests__/InstancePage.test.tsx
+++ b/web/src/pages/instance/__tests__/InstancePage.test.tsx
@@ -26,6 +26,7 @@ import * as tencentCatalogApi from
'../../../api/tencentCatalog';
import type { CloudCredential, CloudCredentialPage } from
'../../../api/cloudCredential';
import type { Instance } from '../../../api/instance';
import { LangProvider } from '../../../i18n/LangContext';
+import { LANGUAGE_STORAGE_KEY } from '../../../i18n/languagePreference';
import * as instanceService from '../../../services/instanceService';
import InstancePage from '../index';
@@ -116,6 +117,7 @@ const deferred = <T,>() => {
describe('InstancePage', () => {
beforeEach(() => {
vi.clearAllMocks();
+ localStorage.clear();
vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue(cloudCredentialPage([]));
vi.mocked(aliyunCatalogApi.listAliyunRegions).mockResolvedValue([]);
vi.mocked(aliyunCatalogApi.listAliyunInstances).mockResolvedValue([]);
@@ -160,6 +162,18 @@ describe('InstancePage', () => {
);
});
+ it('renders instance management copy in English mode', async () => {
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+ renderPage();
+
+ expect(await screen.findByText('production-proxy')).toBeInTheDocument();
+ expect(screen.getByText('Instance List')).toBeInTheDocument();
+ expect(screen.getByText(/Connect and manage RocketMQ
instances/)).toBeInTheDocument();
+ expect(screen.getByPlaceholderText('Search instance ID or
endpoint')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /Add Instance/
})).toBeInTheDocument();
+ expect(screen.queryByText('添加实例')).not.toBeInTheDocument();
+ });
+
it('keeps unavailable resource counts after available values in both sort
directions', async () => {
vi.mocked(instanceService.listInstances).mockResolvedValue([
{
diff --git a/web/src/pages/instance/index.tsx b/web/src/pages/instance/index.tsx
index 5399cf97d..9b5e4155d 100644
--- a/web/src/pages/instance/index.tsx
+++ b/web/src/pages/instance/index.tsx
@@ -66,14 +66,32 @@ const DEFAULT_CLOUD_REGION_IDS:
Partial<Record<InstanceVendor, string>> = {
TENCENT: 'ap-chengdu',
};
+const VENDOR_LABEL_KEYS: Record<InstanceVendor, string> = {
+ APACHE: 'instance.openSourceEdition',
+ ALIYUN: 'instance.aliyunEdition',
+ TENCENT: 'instance.tencentEdition',
+};
+
+const VENDOR_DESCRIPTION_KEYS: Record<InstanceVendor, string> = {
+ APACHE: 'instance.apacheDescription',
+ ALIYUN: 'instance.aliyunDescription',
+ TENCENT: 'instance.tencentDescription',
+};
+
/* ─── Helpers ─── */
-const typeLabel: Record<string, { text: string; color: string }> = {
- CLOUD: { text: '云服务', color: 'blue' },
- PROXY_LOCAL: { text: 'Proxy Local', color: 'cyan' },
- PROXY_CLUSTER: { text: 'Proxy Cluster', color: 'blue' },
- DIRECT: { text: 'Direct', color: 'orange' },
+const typeLabel: Record<string, { labelKey: string; color: string }> = {
+ CLOUD: { labelKey: 'instance.cloudType', color: 'blue' },
+ PROXY_LOCAL: { labelKey: 'instance.proxyLocalMode', color: 'cyan' },
+ PROXY_CLUSTER: { labelKey: 'instance.proxyClusterMode', color: 'blue' },
+ DIRECT: { labelKey: 'instance.directMode', color: 'orange' },
};
+const APACHE_ACCESS_TYPE_OPTIONS = [
+ { value: 'PROXY_LOCAL', labelKey: 'instance.proxyLocalMode' },
+ { value: 'PROXY_CLUSTER', labelKey: 'instance.proxyClusterMode' },
+ { value: 'DIRECT', labelKey: 'instance.directMode' },
+] as const;
+
function describeApiError(error: unknown, fallback: string): string {
const serverMessage = (error as { response?: { data?: { message?: unknown }
} })?.response?.data
?.message;
@@ -104,7 +122,7 @@ function compareResourceCounts(
InstancePage
═══════════════════════════════════════════ */
const InstancePage = () => {
- const { t } = useLang();
+ const { lang, t } = useLang();
const navigate = useNavigate();
const [instances, setInstances] = useState<Instance[]>([]);
const [loading, setLoading] = useState(true);
@@ -153,14 +171,14 @@ const InstancePage = () => {
}
} catch {
if (requestId === requestIdRef.current) {
- message.error('实例列表加载失败,请稍后重试');
+ message.error(t('instance.listLoadFailed'));
}
} finally {
if (requestId === requestIdRef.current) {
setLoading(false);
}
}
- }, []);
+ }, [t]);
useEffect(() => {
listQueryRef.current = {
@@ -193,7 +211,7 @@ const InstancePage = () => {
})
.catch(() => {
if (active) {
- message.error('云凭据列表加载失败');
+ message.error(t('instance.cloudCredentialLoadFailed'));
}
})
.finally(() => {
@@ -206,7 +224,7 @@ const InstancePage = () => {
active = false;
window.clearTimeout(timer);
};
- }, [vendor, cloudVendor, addModalOpen]);
+ }, [vendor, cloudVendor, addModalOpen, t]);
useEffect(() => {
if (!cloudVendor || !addCredentialId) {
@@ -237,7 +255,7 @@ const InstancePage = () => {
})
.catch((error) => {
if (active) {
- message.error(describeApiError(error, '云地域列表加载失败'));
+ message.error(describeApiError(error,
t('instance.cloudRegionLoadFailed')));
}
})
.finally(() => {
@@ -250,7 +268,7 @@ const InstancePage = () => {
active = false;
window.clearTimeout(timer);
};
- }, [vendor, cloudVendor, addCredentialId, addForm]);
+ }, [vendor, cloudVendor, addCredentialId, addForm, t]);
useEffect(() => {
if (!cloudVendor || !addCredentialId || !addRegionId) {
@@ -272,7 +290,7 @@ const InstancePage = () => {
})
.catch((error) => {
if (active) {
- message.error(describeApiError(error, '云实例列表加载失败'));
+ message.error(describeApiError(error,
t('instance.cloudInstanceLoadFailed')));
}
})
.finally(() => {
@@ -285,7 +303,7 @@ const InstancePage = () => {
active = false;
window.clearTimeout(timer);
};
- }, [vendor, cloudVendor, addCredentialId, addRegionId]);
+ }, [vendor, cloudVendor, addCredentialId, addRegionId, t]);
const handleVendorChange = (nextVendor: string) => {
setVendor(nextVendor as InstanceVendor);
@@ -313,6 +331,24 @@ const InstancePage = () => {
addForm.setFieldsValue({ cloudInstanceId: undefined });
};
+ const getVendorLabel = (nextVendor: InstanceVendor) => {
+ return t(VENDOR_LABEL_KEYS[nextVendor]);
+ };
+
+ const getAccessTypeOptions = () =>
+ APACHE_ACCESS_TYPE_OPTIONS.map((option) => ({
+ value: option.value,
+ label: t(option.labelKey),
+ }));
+
+ const getEndpointExtra = (type?: Instance['type']) => {
+ if (type === 'DIRECT') return t('instance.directEndpointExtra');
+ if (type === 'PROXY_LOCAL') return t('instance.proxyLocalEndpointExtra');
+ if (type === 'PROXY_CLUSTER') return
t('instance.proxyClusterEndpointExtra');
+ if (type === 'CLOUD') return t('instance.cloudEndpointExtra');
+ return t('instance.selectAccessTypeFirst');
+ };
+
const handleCreate = async () => {
if (mutationInFlightRef.current) return;
mutationInFlightRef.current = true;
@@ -331,7 +367,7 @@ const InstancePage = () => {
: values;
const created = await createInstance(payload);
await loadInstances();
- message.success(`实例「${created.name}」添加成功`);
+ message.success(t('instance.added', { name: created.name }));
setAddModalOpen(false);
addForm.resetFields();
setVendor(DEFAULT_VENDOR);
@@ -339,7 +375,7 @@ const InstancePage = () => {
if (error && typeof error === 'object' && 'errorFields' in error) {
return; // validation failure; antd already shows field-level errors
}
- message.error('添加实例失败,请稍后重试');
+ message.error(t('instance.createFailed'));
} finally {
mutationInFlightRef.current = false;
setSubmitting(false);
@@ -350,7 +386,7 @@ const InstancePage = () => {
if (importing || vendor === 'APACHE') return;
const credentialId = addForm.getFieldValue('credentialId') as number |
undefined;
if (!credentialId) {
- message.warning('请先选择云凭据');
+ message.warning(t('instance.selectCloudCredentialFirst'));
return;
}
setImporting(true);
@@ -360,16 +396,33 @@ const InstancePage = () => {
const failedCount = result.failedCount ?? result.failed.length;
const summary =
result.imported > 0
- ? `导入完成:共同步 ${result.imported + result.skipped} 个实例(新导入
${result.imported},已存在跳过 ${result.skipped})`
+ ? t('instance.importSuccess', {
+ total: result.imported + result.skipped,
+ imported: result.imported,
+ skipped: result.skipped,
+ })
: failedCount > 0
- ? `导入未完成:新导入 ${result.imported} 个,已存在跳过 ${result.skipped} 个`
- : `云上实例均已在 Studio 中(共 ${result.skipped} 个),无需重复导入`;
+ ? t('instance.importIncomplete', {
+ imported: result.imported,
+ skipped: result.skipped,
+ })
+ : t('instance.importAllSkipped', { skipped: result.skipped });
if (failedCount > 0) {
- const details = result.failed.length > 0 ?
`:${result.failed.join(';')}` : '';
+ const details =
+ result.failed.length > 0
+ ? `${lang === 'zh' ? ':' : ': '}${result.failed.join(lang === 'zh'
? ';' : '; ')}`
+ : '';
const omitted = result.failureDetailsTruncated
- ? `(仅显示前 ${result.failed.length} 条)`
+ ? t('instance.importFailureDetailsTruncated', { count:
result.failed.length })
: '';
- message.warning(`${summary},失败 ${failedCount} 个${omitted}${details}`);
+ message.warning(
+ t('instance.importPartialFailure', {
+ summary,
+ count: failedCount,
+ omitted,
+ details,
+ }),
+ );
} else {
message.success(summary);
}
@@ -379,7 +432,7 @@ const InstancePage = () => {
setRegions([]);
setCloudInstances([]);
} catch (error) {
- message.error(describeApiError(error, '一键导入失败,请稍后重试'));
+ message.error(describeApiError(error, t('instance.importFailed')));
} finally {
setImporting(false);
}
@@ -399,14 +452,14 @@ const InstancePage = () => {
adminCredentialRef: values.adminCredentialRef,
});
await loadInstances();
- message.success(`实例「${updated.name}」已更新`);
+ message.success(t('instance.updated', { name: updated.name }));
setEditModalOpen(false);
editForm.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('instance.updateFailed'));
} finally {
mutationInFlightRef.current = false;
setSubmitting(false);
@@ -417,9 +470,9 @@ const InstancePage = () => {
try {
await deleteInstance(instance.name);
await loadInstances();
- message.success('已删除');
+ message.success(t('instance.deleted'));
} catch (error) {
- message.error(describeApiError(error, '删除实例失败,请稍后重试'));
+ message.error(describeApiError(error, t('instance.deleteFailed')));
}
};
@@ -435,28 +488,35 @@ const InstancePage = () => {
(instance) => instance.vendor === 'ALIYUN' || instance.vendor ===
'TENCENT',
);
const warning = hasCloud
- ? '云厂商实例仅从 Studio 移除记录,不会释放云上的 RocketMQ 实例;仍有 Topic/Group 的开源实例无法删除。'
- : '仍有 Topic/Group 的开源实例无法删除。';
+ ? t('instance.cloudBatchDeleteWarning')
+ : t('instance.batchDeleteWarning');
Modal.confirm({
- title: `确认删除选中的 ${names.length} 个实例?`,
- content: `将删除:${names.join('、')}。${warning}`,
- okText: '删除',
+ title: t('instance.confirmBatchDelete', { count: names.length }),
+ content: t('instance.batchDeleteContent', {
+ names: names.join(lang === 'zh' ? '、' : ', '),
+ warning,
+ }),
+ okText: t('common.delete'),
okButtonProps: { danger: true },
onOk: async () => {
try {
const result = await deleteInstancesBatch(names);
await loadInstances();
setSelectedRowKeys([]);
- const summary = `已删除 ${result.deleted} 个`;
+ const summary = t('instance.deletedCount', { count: result.deleted
});
if (result.failed.length > 0) {
message.warning(
- `${summary},${result.failed.length}
个未能删除:${result.failed.join(';')}`,
+ t('instance.batchDeletePartialFailure', {
+ summary,
+ count: result.failed.length,
+ failed: result.failed.join(lang === 'zh' ? ';' : '; '),
+ }),
);
} else {
message.success(summary);
}
} catch (error) {
- message.error(describeApiError(error, '批量删除失败,请稍后重试'));
+ message.error(describeApiError(error,
t('instance.batchDeleteFailed')));
}
},
});
@@ -464,7 +524,7 @@ const InstancePage = () => {
const columns: ColumnsType<Instance> = [
{
- title: '地域',
+ title: t('instance.region'),
dataIndex: 'regionId',
key: 'regionId',
width: 130,
@@ -474,13 +534,13 @@ const InstancePage = () => {
render: (regionId: string | undefined, record: Instance) => (
<Text type="secondary" style={{ fontSize: 14 }}>
{!record.vendor || record.vendor === 'APACHE'
- ? '开源版'
+ ? t('instance.openSourceEdition')
: record.regionName || regionId || '-'}
</Text>
),
},
{
- title: '实例 ID',
+ title: t('instance.instanceName'),
dataIndex: 'name',
key: 'name',
ellipsis: true,
@@ -497,7 +557,7 @@ const InstancePage = () => {
),
},
{
- title: '备注',
+ title: t('instance.remark'),
dataIndex: 'remark',
key: 'remark',
ellipsis: { showTitle: false },
@@ -517,7 +577,7 @@ const InstancePage = () => {
),
},
{
- title: '厂商',
+ title: t('instance.vendor'),
dataIndex: 'vendor',
key: 'vendor',
width: 100,
@@ -529,22 +589,22 @@ const InstancePage = () => {
}
return (
<Space size={6}>
- <img src={option.logo} alt={option.label} style={{ height: 16 }} />
- <Text style={{ fontSize: 14 }}>{option.label}</Text>
+ <img src={option.logo} alt={getVendorLabel(option.key)} style={{
height: 16 }} />
+ <Text style={{ fontSize: 14 }}>{getVendorLabel(option.key)}</Text>
</Space>
);
},
},
{
- title: '类型',
+ title: t('common.type'),
dataIndex: 'type',
key: 'type',
width: 110,
align: 'center' as const,
sorter: (a, b) => a.type.localeCompare(b.type),
render: (type: string) => {
- const t = typeLabel[type] || { text: type, color: 'default' };
- return <Tag color={t.color}>{t.text}</Tag>;
+ const config = typeLabel[type] || { labelKey: type, color: 'default' };
+ return <Tag color={config.color}>{typeLabel[type] ? t(config.labelKey)
: type}</Tag>;
},
},
{
@@ -555,7 +615,7 @@ const InstancePage = () => {
align: 'center' as const,
sorter: (a, b, sortOrder) => compareResourceCounts(a, b, 'topicCount',
sortOrder),
render: (count: number, record: Instance) =>
- record.resourceCountsAvailable === false ? '不可用' : count,
+ record.resourceCountsAvailable === false ? t('common.unavailable') :
count,
},
{
title: 'Group',
@@ -565,10 +625,10 @@ const InstancePage = () => {
align: 'center' as const,
sorter: (a, b, sortOrder) => compareResourceCounts(a, b,
'consumerGroupCount', sortOrder),
render: (count: number, record: Instance) =>
- record.resourceCountsAvailable === false ? '不可用' : count,
+ record.resourceCountsAvailable === false ? t('common.unavailable') :
count,
},
{
- title: '创建时间',
+ title: t('instance.createdAt'),
dataIndex: 'gmtCreate',
key: 'gmtCreate',
width: 150,
@@ -580,7 +640,7 @@ const InstancePage = () => {
),
},
{
- title: '修改时间',
+ title: t('instance.updatedAt'),
dataIndex: 'gmtModified',
key: 'gmtModified',
width: 150,
@@ -592,7 +652,7 @@ const InstancePage = () => {
),
},
{
- title: '操作',
+ title: t('common.actions'),
key: 'actions',
width: 150,
render: (_: unknown, record: Instance) => (
@@ -612,7 +672,7 @@ const InstancePage = () => {
setEditModalOpen(true);
}}
>
- 编辑
+ {t('common.edit')}
</Button>
<Button
size="small"
@@ -621,17 +681,17 @@ const InstancePage = () => {
onClick={() => {
const isCloudInstance = record.vendor === 'ALIYUN' ||
record.vendor === 'TENCENT';
Modal.confirm({
- title: `确认删除 "${record.name}"?`,
+ title: t('instance.confirmDelete', { name: record.name }),
content: isCloudInstance
- ? '仅从 Studio 移除该实例记录,不会释放云上的 RocketMQ 实例。'
- : '此操作不可恢复。',
- okText: '删除',
+ ? t('instance.cloudDeleteWarning')
+ : t('instance.deleteWarning'),
+ okText: t('common.delete'),
okButtonProps: { danger: true },
onOk: () => handleDelete(record),
});
}}
>
- 删除
+ {t('common.delete')}
</Button>
</Flex>
),
@@ -644,7 +704,7 @@ const InstancePage = () => {
<div style={{ marginBottom: 20 }}>
<h2 style={{ margin: 0, fontSize: 20, fontWeight: 600
}}>{t('instance.title')}</h2>
<div style={{ marginTop: 6, fontSize: 14, color: '#9CA3AF' }}>
- 接入并管理 RocketMQ 实例(开源自建 / 阿里云 / 腾讯云),当前显示 {instances.length} 个实例
+ {t('instance.managementSubtitle', { count: instances.length })}
</div>
</div>
@@ -658,7 +718,7 @@ const InstancePage = () => {
>
<Space size={12} wrap>
<Input
- placeholder="搜索实例 ID 或地址"
+ placeholder={t('instance.searchPlaceholder')}
prefix={<MagnifyingGlass size={14} color="#9CA3AF" />}
value={search}
onChange={(e) => setSearch(e.target.value)}
@@ -670,11 +730,11 @@ const InstancePage = () => {
onChange={setTypeFilter}
style={{ width: 140 }}
options={[
- { value: 'ALL', label: '全部架构' },
- { value: 'CLOUD', label: '云服务' },
- { value: 'PROXY_LOCAL', label: 'Proxy Local 模式' },
- { value: 'PROXY_CLUSTER', label: 'Proxy Cluster 模式' },
- { value: 'DIRECT', label: 'Direct 模式' },
+ { value: 'ALL', label: t('instance.allTypes') },
+ { value: 'CLOUD', label: t('instance.cloudType') },
+ { value: 'PROXY_LOCAL', label: t('instance.proxyLocalMode') },
+ { value: 'PROXY_CLUSTER', label: t('instance.proxyClusterMode')
},
+ { value: 'DIRECT', label: t('instance.directMode') },
]}
/>
</Space>
@@ -685,14 +745,14 @@ const InstancePage = () => {
disabled={selectedRowKeys.length === 0}
onClick={handleBatchDelete}
>
- 删除
+ {t('common.delete')}
</Button>
<Button
type="primary"
icon={<Plus size={14} weight="bold" />}
onClick={() => setAddModalOpen(true)}
>
- 添加实例
+ {t('instance.addInstance')}
</Button>
</Space>
</Flex>
@@ -718,7 +778,7 @@ const InstancePage = () => {
{/* Add Instance Modal */}
<Modal
- title="添加实例"
+ title={t('instance.addInstance')}
open={addModalOpen}
onCancel={() => {
setAddModalOpen(false);
@@ -731,13 +791,13 @@ const InstancePage = () => {
footer={
<Flex justify="flex-end" gap={8}>
{cloudVendor && (
- <Tooltip title="遍历该凭据下全部地域,将所有云上实例导入(幂等,已存在的自动跳过),备注自动取自云上实例">
+ <Tooltip title={t('instance.importAllTooltip')}>
<Button
loading={importing}
disabled={!addCredentialId}
onClick={() => void handleImportAll()}
>
- 一键导入
+ {t('instance.importAll')}
</Button>
</Tooltip>
)}
@@ -750,10 +810,10 @@ const InstancePage = () => {
setCloudInstances([]);
}}
>
- 取消
+ {t('common.cancel')}
</Button>
<Button type="primary" loading={submitting} onClick={() => void
handleCreate()}>
- 连接
+ {t('instance.connect')}
</Button>
</Flex>
}
@@ -767,40 +827,46 @@ const InstancePage = () => {
key: option.key,
label: (
<span style={{ display: 'inline-flex', alignItems: 'center',
gap: 8 }}>
- <img src={option.logo} alt={option.label} style={{ height: 18,
maxWidth: 80 }} />
- {option.label}
+ <img
+ src={option.logo}
+ alt={getVendorLabel(option.key)}
+ style={{ height: 18, maxWidth: 80 }}
+ />
+ {getVendorLabel(option.key)}
</span>
),
}))}
/>
<Text type="secondary" style={{ display: 'block', fontSize: 14,
marginBottom: 12 }}>
- {VENDOR_OPTIONS.find((option) => option.key === vendor)?.description}
+ {t(VENDOR_DESCRIPTION_KEYS[vendor])}
</Text>
{cloudVendor ? (
<>
<Form form={addForm} layout="vertical">
<Form.Item
- label="云凭据"
+ label={t('instance.cloudCredential')}
name="credentialId"
- rules={[{ required: true, message: '请选择云凭据' }]}
+ rules={[{ required: true, message:
t('instance.cloudCredentialRequired') }]}
extra={
<span>
- 凭据为{vendor === 'ALIYUN' ? '阿里云' : '腾讯云'}账号的 AK/SK,
- <Link to="/settings?tab=credential">前往「设置 - 云凭据管理」添加</Link>
+ {t('instance.cloudCredentialExtraPrefix', { vendor:
getVendorLabel(vendor) })}
+ <Link to="/settings?tab=credential">
+ {t('instance.cloudCredentialSettingsLink')}
+ </Link>
</span>
}
>
<Select
- placeholder="选择已录入的 AK/SK 凭据"
+ placeholder={t('instance.selectStoredCredential')}
loading={credentialsLoading}
onChange={handleCredentialChange}
notFoundContent={
credentialsLoading ? (
- '加载中…'
+ t('instance.loading')
) : (
<span>
- 暂无{vendor === 'ALIYUN' ? '阿里云' : '腾讯云'}凭据,
- <Link to="/settings?tab=credential">去设置中添加</Link>
+ {t('instance.noCloudCredential', { vendor:
getVendorLabel(vendor) })}
+ <Link
to="/settings?tab=credential">{t('instance.addInSettings')}</Link>
</span>
)
}
@@ -811,12 +877,16 @@ const InstancePage = () => {
/>
</Form.Item>
<Form.Item
- label="地域"
+ label={t('instance.region')}
name="regionId"
- rules={[{ required: true, message: '请选择地域' }]}
+ rules={[{ required: true, message:
t('instance.regionRequired') }]}
>
<Select
- placeholder={addCredentialId ? '选择地域' : '请先选择云凭据'}
+ placeholder={
+ addCredentialId
+ ? t('instance.selectRegion')
+ : t('instance.selectCloudCredentialFirst')
+ }
disabled={!addCredentialId}
loading={regionsLoading}
onChange={handleRegionChange}
@@ -827,15 +897,19 @@ const InstancePage = () => {
/>
</Form.Item>
<Form.Item
- label="云上实例"
+ label={t('instance.cloudInstance')}
name="cloudInstanceId"
- rules={[{ required: true, message: '请选择云上实例' }]}
- extra="商业版实例来自云端目录,无法手工创建"
+ rules={[{ required: true, message:
t('instance.cloudInstanceRequired') }]}
+ extra={t('instance.cloudInstanceExtra')}
>
<Select
showSearch
optionFilterProp="label"
- placeholder={addRegionId ? '选择云上实例' : '请先选择地域'}
+ placeholder={
+ addRegionId
+ ? t('instance.selectCloudInstance')
+ : t('instance.selectRegionFirst')
+ }
disabled={!addRegionId}
loading={cloudInstancesLoading}
options={cloudInstances.map((item) => ({
@@ -851,84 +925,72 @@ const InstancePage = () => {
/>
</Form.Item>
<Form.Item
- label="实例 ID"
+ label={t('instance.instanceName')}
name="name"
rules={[
- { required: true, message: '请输入实例 ID' },
- { max: 64, message: '实例 ID 不能超过 64 个字符' },
+ { required: true, message: t('instance.nameRequired') },
+ { max: 64, message: t('instance.nameMax') },
]}
>
- <Input placeholder="默认取云上实例 ID" />
+ <Input placeholder={t('instance.cloudNamePlaceholder')} />
</Form.Item>
- <Form.Item label="备注" name="remark">
- <Input.TextArea rows={2} placeholder="可选,描述实例用途" />
+ <Form.Item label={t('instance.remark')} name="remark">
+ <Input.TextArea rows={2}
placeholder={t('instance.remarkPlaceholder')} />
</Form.Item>
</Form>
</>
) : (
<Form form={addForm} layout="vertical">
<Form.Item
- label="实例 ID"
+ label={t('instance.instanceName')}
name="name"
rules={[
- { required: true, message: '请输入实例 ID' },
- { max: 64, message: '实例 ID 不能超过 64 个字符' },
+ { required: true, message: t('instance.nameRequired') },
+ { max: 64, message: t('instance.nameMax') },
]}
>
- <Input placeholder="例:rocketmq-production" />
+ <Input placeholder={t('instance.namePlaceholder')} />
</Form.Item>
<Form.Item
- label="接入方式"
+ label={t('instance.accessType')}
name="type"
- rules={[{ required: true, message: '请选择接入方式' }]}
+ rules={[{ required: true, message:
t('instance.accessTypeRequired') }]}
>
<Select
- placeholder="选择接入方式"
- options={[
- { value: 'PROXY_LOCAL', label: 'Proxy Local 模式' },
- { value: 'PROXY_CLUSTER', label: 'Proxy Cluster 模式' },
- { value: 'DIRECT', label: 'Direct 模式' },
- ]}
+ placeholder={t('instance.selectAccessType')}
+ options={getAccessTypeOptions()}
/>
</Form.Item>
<Form.Item
label={
<span>
- 接入地址{' '}
- <Tooltip title="接入地址为客户端访问入口,会展示在 Topic
等页面供客户端配置使用。若客户端环境无法解析该地址(如 K8s 内部 Service 域名),可自行配置 DNS 解析或在客户端 hosts 中映射。">
+ {t('instance.endpoint')}{' '}
+ <Tooltip title={t('instance.endpointHelp')}>
<QuestionCircleOutlined style={{ color: '#9CA3AF', cursor:
'help' }} />
</Tooltip>
</span>
}
name="endpoint"
- rules={[{ required: true, message: '请输入接入地址' }]}
- extra={
- addInstanceType === 'DIRECT'
- ? 'Direct 模式请填写 NameServer SLB 地址(K8s 场景下一般为 NameServer
Service 地址,如 namesrv.mq.svc:9876)'
- : addInstanceType === 'PROXY_LOCAL'
- ? 'Proxy Local 模式请填写与 Broker 同进程部署的 Proxy 接入地址(如
broker-proxy.mq.svc:8080)'
- : addInstanceType === 'PROXY_CLUSTER'
- ? 'Proxy Cluster 模式请填写独立 Proxy 集群的 SLB 内网地址(如
proxy.mq.svc:8080)'
- : '请先选择接入方式'
- }
+ rules={[{ required: true, message:
t('instance.endpointRequired') }]}
+ extra={getEndpointExtra(addInstanceType)}
>
<Input
placeholder={
addInstanceType === 'DIRECT'
- ? '例:namesrv.mq.svc.cluster.local:9876'
- : '例:proxy.mq.svc.cluster.local:8080'
+ ? t('instance.directEndpointPlaceholder')
+ : t('instance.proxyEndpointPlaceholder')
}
/>
</Form.Item>
<Form.Item
- label="管理凭据引用"
+ label={t('instance.adminCredentialRef')}
name="adminCredentialRef"
- extra="可选。仅保存服务端配置中的凭据引用,不会保存或传输 AK/SK。"
+ extra={t('instance.adminCredentialRefExtra')}
>
- <Input placeholder="例:production-admin" />
+ <Input placeholder={t('instance.adminCredentialRefPlaceholder')}
/>
</Form.Item>
- <Form.Item label="备注" name="remark">
- <Input.TextArea rows={2} placeholder="可选,描述实例用途" />
+ <Form.Item label={t('instance.remark')} name="remark">
+ <Input.TextArea rows={2}
placeholder={t('instance.remarkPlaceholder')} />
</Form.Item>
</Form>
)}
@@ -936,7 +998,7 @@ const InstancePage = () => {
{/* Edit Instance Modal */}
<Modal
- title={`编辑实例 — ${editingInstance?.name || ''}`}
+ title={t('instance.editInstanceTitle', { name: editingInstance?.name
|| '' })}
open={editModalOpen}
onCancel={() => {
setEditModalOpen(false);
@@ -944,69 +1006,59 @@ const InstancePage = () => {
}}
onOk={() => void handleUpdate()}
confirmLoading={submitting}
- okText="保存"
- cancelText="取消"
+ okText={t('common.save')}
+ cancelText={t('common.cancel')}
width={520}
>
<Form form={editForm} layout="vertical" style={{ marginTop: 16 }}>
- <Form.Item label="实例 ID">
+ <Form.Item label={t('instance.instanceName')}>
<Input value={editingInstance?.name} disabled />
</Form.Item>
<Form.Item
- label="接入方式"
+ label={t('instance.accessType')}
name="type"
- rules={[{ required: true, message: '请选择接入方式' }]}
+ rules={[{ required: true, message:
t('instance.accessTypeRequired') }]}
>
<Select
options={
editingInstance?.vendor && editingInstance.vendor !== 'APACHE'
- ? [{ value: 'CLOUD', label: '云服务' }]
- : [
- { value: 'PROXY_LOCAL', label: 'Proxy Local 模式' },
- { value: 'PROXY_CLUSTER', label: 'Proxy Cluster 模式' },
- { value: 'DIRECT', label: 'Direct 模式' },
- ]
+ ? [{ value: 'CLOUD', label: t('instance.cloudType') }]
+ : getAccessTypeOptions()
}
/>
</Form.Item>
<Form.Item
label={
<span>
- 接入地址{' '}
- <Tooltip title="接入地址为客户端访问入口,会展示在 Topic
等页面供客户端配置使用。若客户端环境无法解析该地址(如 K8s 内部 Service 域名),可自行配置 DNS 解析或在客户端 hosts 中映射。">
+ {t('instance.endpoint')}{' '}
+ <Tooltip title={t('instance.endpointHelp')}>
<QuestionCircleOutlined style={{ color: '#9CA3AF', cursor:
'help' }} />
</Tooltip>
</span>
}
name="endpoint"
- rules={[{ required: true, message: '请输入接入地址' }]}
- extra={
- editInstanceType === 'DIRECT'
- ? 'Direct 模式请填写 NameServer SLB 地址(K8s 场景下一般为 NameServer
Service 地址,如 namesrv.mq.svc:9876)'
- : editInstanceType === 'CLOUD'
- ? '云服务实例接入地址由云厂商目录解析,不支持手动修改'
- : '请先选择接入方式'
- }
+ rules={[{ required: true, message: t('instance.endpointRequired')
}]}
+ extra={getEndpointExtra(editInstanceType)}
>
<Input
placeholder={
editInstanceType === 'DIRECT'
- ? '例:namesrv.mq.svc.cluster.local:9876'
- : '例:proxy.mq.svc.cluster.local:8080'
+ ? t('instance.directEndpointPlaceholder')
+ : t('instance.proxyEndpointPlaceholder')
}
/>
</Form.Item>
{editingInstance?.vendor === 'APACHE' && (
<Form.Item
- label="管理凭据引用"
+ label={t('instance.adminCredentialRef')}
name="adminCredentialRef"
- extra="仅保存服务端配置中的引用,不会保存或传输 AK/SK。"
+ extra={t('instance.adminCredentialRefEditExtra')}
>
- <Input placeholder="例:production-admin" />
+ <Input placeholder={t('instance.adminCredentialRefPlaceholder')}
/>
</Form.Item>
)}
- <Form.Item label="备注" name="remark">
- <Input.TextArea rows={3} placeholder="描述实例用途" />
+ <Form.Item label={t('instance.remark')} name="remark">
+ <Input.TextArea rows={3}
placeholder={t('instance.remarkEditPlaceholder')} />
</Form.Item>
</Form>
</Modal>