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 b0b0e176f fix(web): localize the remaining hardcoded strings on the
message page (#4633)
b0b0e176f is described below
commit b0b0e176f4d9a7b1f1d62cb7462b12ba3c2df712
Author: Apulupie <[email protected]>
AuthorDate: Mon Sep 21 21:08:06 2026 +0800
fix(web): localize the remaining hardcoded strings on the message page
(#4633)
`web/src/pages/instance/message.tsx` kept its remaining strings as Chinese
literals — the default query error, the query-type options, the completion and
truncation notices, the search button, the pagination `showTotal` — and the
trace diagnostics behind them were Chinese too, because
`messageTraceDiagnostics.ts` returned rendered text from the `STATUS_TEXT` and
`ISSUE_RECOMMENDATIONS` constant tables. An English UI showed both the page and
the diagnostics dialog untranslated.
The diagnostics interface now returns keys plus interpolation parameters
instead of text (`titleKey`/`descriptionKey` with `params`, `statusKey`, and
`recommendationCodes` in place of `recommendations`), the constant tables are
gone, and `translations.ts` gains 114 new `messagePage.*` keys in both
languages, 45 of them the per-issue
`messagePage.issue.<CODE>.{title,description,recommendation}` set. Both
consumers of the util — the page and its test — moved to the new shape, and the
Ch [...]
`clientDiagnostics` and `aclRiskDiagnostics`, rendered from `clients.tsx`
and `acl.tsx`, still carry the same hardcoded Chinese and are left for a
follow-up.
Fixes #4634
---
web/src/i18n/translations.ts | 299 +++++++++++++++++++++
.../pages/instance/__tests__/MessagePage.test.tsx | 59 +++-
web/src/pages/instance/message.tsx | 256 ++++++++++--------
web/src/utils/messageTraceDiagnostics.test.ts | 51 +++-
web/src/utils/messageTraceDiagnostics.ts | 137 +++++-----
5 files changed, 613 insertions(+), 189 deletions(-)
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index b341a428a..0a200d625 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -515,6 +515,305 @@ const translations: Record<string, Record<Lang, string>>
= {
'acl.title': { zh: 'ACL 管理', en: 'ACL Management' },
'message.title': { zh: '消息查询', en: 'Message Search' },
+ // ─── Message Page ───
+ 'messagePage.subtitle': {
+ zh: '按 Topic、Key 或 Message ID 检索消息',
+ en: 'Search messages by Topic, Key, or Message ID',
+ },
+ 'messagePage.queryMode.topic': { zh: '按 Topic 查询', en: 'Query by Topic' },
+ 'messagePage.queryMode.key': { zh: '按 Message Key', en: 'Query by Message
Key' },
+ 'messagePage.queryMode.msgid': { zh: '按 Message ID', en: 'Query by Message
ID' },
+ 'messagePage.queryMode.queue': { zh: '按队列浏览', en: 'Browse by Queue' },
+ 'messagePage.topicPlaceholder': { zh: '选择 Topic', en: 'Select a topic' },
+ 'messagePage.inputKeyPlaceholder': { zh: '输入 Message Key', en: 'Enter
message key' },
+ 'messagePage.inputMsgIdPlaceholder': { zh: '输入 Message ID', en: 'Enter
message ID' },
+ 'messagePage.query': { zh: '查询', en: 'Query' },
+ 'messagePage.serverHistory': { zh: '服务端历史', en: 'Server History' },
+ 'messagePage.selectTopicRequired': { zh: '请选择 Topic', en: 'Please select a
topic' },
+ 'messagePage.enterMessageKeyRequired': {
+ zh: '请输入 Message Key',
+ en: 'Please enter a message key',
+ },
+ 'messagePage.enterMessageIdRequired': {
+ zh: '请输入 Message ID',
+ en: 'Please enter a message ID',
+ },
+ 'messagePage.selectInstanceFirst': { zh: '请先选择实例', en: 'Select an instance
first' },
+ 'messagePage.loadingTopics': { zh: '正在加载 Topic 列表', en: 'Loading topic list'
},
+ 'messagePage.topicLoadFailedRetry': {
+ zh: 'Topic 列表加载失败,请先重试',
+ en: 'Failed to load topic list, please retry first',
+ },
+ 'messagePage.topicLoadFailed': { zh: 'Topic 列表加载失败', en: 'Failed to load
topic list' },
+ 'messagePage.loadTopicsFailed': { zh: '加载 Topic 列表失败', en: 'Could not load
topic list' },
+ 'messagePage.selectInstanceBeforeQuery': {
+ zh: '请先选择实例后再查询消息',
+ en: 'Select an instance before querying messages',
+ },
+ 'messagePage.queryCompleted': {
+ zh: '查询完成,共 {total} 条',
+ en: 'Query completed, {total} messages in total',
+ },
+ 'messagePage.totalMessages': { zh: '共 {total} 条消息', en: '{total} messages in
total' },
+ 'messagePage.truncatedWarning': {
+ zh: '查询结果达到服务端扫描上限,当前总数可能不完整。',
+ en: 'The query reached the server-side scan limit; the total count may be
incomplete.',
+ },
+ 'messagePage.queryFailed': {
+ zh: '消息查询失败,请稍后重试',
+ en: 'Failed to query messages, please try again later',
+ },
+ 'messagePage.traceLoadFailed': {
+ zh: '消息轨迹加载失败,请稍后重试',
+ en: 'Failed to load message trace, please try again later',
+ },
+ 'messagePage.verifyNotAvailable': {
+ zh: '消费验证接口尚未接入,无法确认该消息的真实消费状态',
+ en: 'Consume verification is not available yet; the actual consume status
of this message cannot be confirmed.',
+ },
+ 'messagePage.deliveryStatusPending': { zh: '等待中', en: 'Pending' },
+ 'messagePage.traceStatusCritical': { zh: '异常', en: 'Critical' },
+ 'messagePage.traceDiagnostics': { zh: '轨迹诊断', en: 'Trace Diagnostics' },
+ 'messagePage.healthScore': { zh: '健康分', en: 'Health Score' },
+ 'messagePage.traceStageCount': { zh: '轨迹阶段', en: 'Trace Stages' },
+ 'messagePage.endToEndLatency': { zh: '端到端耗时', en: 'End-to-End Latency' },
+ 'messagePage.totalStageDuration': { zh: '阶段耗时合计', en: 'Total Stage Duration'
},
+ 'messagePage.consumeSuccessRate': { zh: '消费成功率', en: 'Consume Success Rate'
},
+ 'messagePage.slowestStage': {
+ zh: '最慢阶段:{title},{duration}',
+ en: 'Slowest stage: {title}, {duration}',
+ },
+ 'messagePage.slowestStageWithGap': {
+ zh: '最慢阶段:{title},{duration};最大阶段间隔:{gapTitle},{gapDuration}',
+ en: 'Slowest stage: {title}, {duration}; largest stage gap: {gapTitle},
{gapDuration}',
+ },
+ 'messagePage.diagSeverity': { zh: '级别', en: 'Severity' },
+ 'messagePage.diagRisk': { zh: '风险', en: 'Risk' },
+ 'messagePage.diagDescription': { zh: '说明', en: 'Description' },
+ 'messagePage.traceStatusHealthy': { zh: '轨迹健康', en: 'Trace Healthy' },
+ 'messagePage.traceStatusWarning': { zh: '需要关注', en: 'Needs Attention' },
+ 'messagePage.traceStatusDeliveryCritical': { zh: '投递异常', en: 'Delivery
Critical' },
+ 'messagePage.issue.NO_TRACE_NODES.title': { zh: '缺少轨迹节点', en: 'No trace
stages' },
+ 'messagePage.issue.NO_TRACE_NODES.description': {
+ zh: '当前消息没有返回可展示的轨迹阶段。',
+ en: 'The message returned no trace stages to display.',
+ },
+ 'messagePage.issue.NO_TRACE_NODES.recommendation': {
+ zh: '确认消息轨迹已开启,并检查是否需要指定自定义轨迹 Topic。',
+ en: 'Confirm message tracing is enabled and check whether a custom trace
topic needs to be specified.',
+ },
+ 'messagePage.issue.FAILED_TRACE_NODE.title': { zh: '轨迹阶段失败', en: 'Trace
stage failed' },
+ 'messagePage.issue.FAILED_TRACE_NODE.description': {
+ zh: '{phase} 阶段返回失败状态。',
+ en: 'The {phase} stage returned a failed status.',
+ },
+ 'messagePage.issue.FAILED_TRACE_NODE.recommendation': {
+ zh: '优先查看失败阶段对应的生产者、Broker 或消费者日志,确认失败返回码和异常堆栈。',
+ en: 'Check the producer, broker, or consumer logs for the failed stage
first to confirm the failure code and exception stack.',
+ },
+ 'messagePage.issue.WAITING_TRACE_NODE.title': {
+ zh: '轨迹阶段未完成',
+ en: 'Trace stage not finished',
+ },
+ 'messagePage.issue.WAITING_TRACE_NODE.description': {
+ zh: '{phase} 阶段仍处于等待或处理中状态。',
+ en: 'The {phase} stage is still waiting or being processed.',
+ },
+ 'messagePage.issue.WAITING_TRACE_NODE.recommendation': {
+ zh: '等待或处理中阶段需要结合消费者在线状态和堆积情况确认是否仍在推进。',
+ en: 'For waiting or in-progress stages, check consumer availability and
backlog to confirm whether progress is being made.',
+ },
+ 'messagePage.issue.INVALID_TRACE_TIMESTAMP.title': {
+ zh: '轨迹时间不可用',
+ en: 'Trace time unavailable',
+ },
+ 'messagePage.issue.INVALID_TRACE_TIMESTAMP.description': {
+ zh: '{phase} 阶段没有可解析的时间戳。',
+ en: 'The {phase} stage has no parsable timestamp.',
+ },
+ 'messagePage.issue.INVALID_TRACE_TIMESTAMP.recommendation': {
+ zh: '检查生产者、Broker 与消费者机器时间,避免时钟漂移影响轨迹判断。',
+ en: 'Check the clocks on producers, brokers, and consumers to avoid clock
drift affecting trace analysis.',
+ },
+ 'messagePage.issue.TRACE_TIMESTAMP_REGRESSION.title': {
+ zh: '轨迹时间发生回退',
+ en: 'Trace time regressed',
+ },
+ 'messagePage.issue.TRACE_TIMESTAMP_REGRESSION.description': {
+ zh: '{phase} 比上一阶段早 {gap} ms。',
+ en: 'The {phase} stage is {gap} ms earlier than the previous stage.',
+ },
+ 'messagePage.issue.TRACE_TIMESTAMP_REGRESSION.recommendation': {
+ zh: '轨迹时间出现回退时,先确认各节点 NTP 同步和跨机房时间源配置。',
+ en: 'When trace time regresses, first verify NTP sync and cross-datacenter
time source configuration.',
+ },
+ 'messagePage.issue.INVALID_TRACE_COST.title': {
+ zh: '阶段耗时不可用',
+ en: 'Stage cost unavailable',
+ },
+ 'messagePage.issue.INVALID_TRACE_COST.description': {
+ zh: '{phase} 阶段返回了无效耗时。',
+ en: 'The {phase} stage returned an invalid cost time.',
+ },
+ 'messagePage.issue.INVALID_TRACE_COST.recommendation': {
+ zh: '忽略异常耗时值后再判断链路瓶颈,并核对服务端轨迹采集字段是否完整。',
+ en: 'Ignore the abnormal cost value before judging bottlenecks and verify
the server-side trace fields are complete.',
+ },
+ 'messagePage.issue.SLOW_TRACE_NODE.title': { zh: '阶段耗时偏高', en: 'High stage
cost' },
+ 'messagePage.issue.SLOW_TRACE_NODE.description': {
+ zh: '{phase} 阶段耗时 {cost} ms。',
+ en: 'The {phase} stage took {cost} ms.',
+ },
+ 'messagePage.issue.SLOW_TRACE_NODE.recommendation': {
+ zh: '对耗时最高的阶段做分段排查,区分发送、存储和消费处理时间。',
+ en: 'Break down the slowest stage to separate send, storage, and
consumption processing time.',
+ },
+ 'messagePage.issue.SLOW_TRACE_GAP.title': {
+ zh: '相邻阶段间隔偏高',
+ en: 'High gap between stages',
+ },
+ 'messagePage.issue.SLOW_TRACE_GAP.description': {
+ zh: '{phase} 与上一阶段相隔 {gap} ms。',
+ en: 'The {phase} stage started {gap} ms after the previous stage.',
+ },
+ 'messagePage.issue.SLOW_TRACE_GAP.recommendation': {
+ zh: '相邻阶段间隔过大时,检查 Broker 拉取、客户端长轮询和消费线程池排队。',
+ en: 'When the gap between stages is large, check broker pulls, client long
polling, and consumer thread pool queuing.',
+ },
+ 'messagePage.issue.SLOW_END_TO_END_TRACE.title': {
+ zh: '端到端轨迹耗时偏高',
+ en: 'High end-to-end trace latency',
+ },
+ 'messagePage.issue.SLOW_END_TO_END_TRACE.description': {
+ zh: '首尾轨迹阶段相隔 {latency} ms。',
+ en: 'The first and last trace stages are {latency} ms apart.',
+ },
+ 'messagePage.issue.SLOW_END_TO_END_TRACE.recommendation': {
+ zh: '端到端耗时过高时,结合 Topic 队列分布、Consumer Group 进度和客户端负载一起排查。',
+ en: 'For high end-to-end latency, investigate topic queue distribution,
consumer group progress, and client load together.',
+ },
+ 'messagePage.issue.MISSING_CONSUMER_STATUS.title': {
+ zh: '缺少消费状态',
+ en: 'Missing consumer status',
+ },
+ 'messagePage.issue.MISSING_CONSUMER_STATUS.description': {
+ zh: '轨迹中没有返回任何消费组的投递状态。',
+ en: 'The trace returned no delivery status for any consumer group.',
+ },
+ 'messagePage.issue.MISSING_CONSUMER_STATUS.recommendation': {
+ zh: '缺少消费状态时,可用直接消费或 Consumer Group 进度进一步确认消息是否可达。',
+ en: 'When consumer status is missing, use direct consumption or consumer
group progress to confirm deliverability.',
+ },
+ 'messagePage.issue.FAILED_CONSUMER_DELIVERY.title': {
+ zh: '消费投递失败',
+ en: 'Consumer delivery failed',
+ },
+ 'messagePage.issue.FAILED_CONSUMER_DELIVERY.description': {
+ zh: '{group} 返回失败消费状态。',
+ en: '{group} returned a failed consumption status.',
+ },
+ 'messagePage.issue.FAILED_CONSUMER_DELIVERY.recommendation': {
+ zh: '失败消费组需要检查消费异常、重试 Topic 和业务幂等处理。',
+ en: 'For failed consumer groups, check consumption errors, retry topics,
and business idempotency.',
+ },
+ 'messagePage.issue.PENDING_CONSUMER_DELIVERY.title': {
+ zh: '消费投递等待中',
+ en: 'Consumer delivery pending',
+ },
+ 'messagePage.issue.PENDING_CONSUMER_DELIVERY.description': {
+ zh: '{group} 尚未完成消费。',
+ en: '{group} has not finished consuming.',
+ },
+ 'messagePage.issue.PENDING_CONSUMER_DELIVERY.recommendation': {
+ zh: '等待中的消费组需要确认客户端是否在线、订阅是否匹配以及是否存在明显堆积。',
+ en: 'For pending consumer groups, confirm the client is online, the
subscription matches, and there is no significant backlog.',
+ },
+ 'messagePage.issue.UNKNOWN_CONSUMER_DELIVERY.title': {
+ zh: '消费状态未知',
+ en: 'Unknown consumer status',
+ },
+ 'messagePage.issue.UNKNOWN_CONSUMER_DELIVERY.description': {
+ zh: '{group} 返回未识别状态 {status}。',
+ en: '{group} returned an unrecognized status {status}.',
+ },
+ 'messagePage.issue.UNKNOWN_CONSUMER_DELIVERY.recommendation': {
+ zh: '未知消费状态需要回查服务端返回值,避免把未识别状态误判为成功。',
+ en: 'For unknown consumer status, check the server response to avoid
mistaking unrecognized states for success.',
+ },
+ 'messagePage.issue.RETRIED_CONSUMER_DELIVERY.title': {
+ zh: '消费发生重试',
+ en: 'Consumer delivery retried',
+ },
+ 'messagePage.issue.RETRIED_CONSUMER_DELIVERY.description': {
+ zh: '{group} 已重试 {retry} 次。',
+ en: '{group} has retried {retry} times.',
+ },
+ 'messagePage.issue.RETRIED_CONSUMER_DELIVERY.recommendation': {
+ zh: '存在重试时,检查消费耗时、异常类型和重试次数是否符合预期。',
+ en: 'When retries occur, check whether consumption latency, error types,
and retry counts are expected.',
+ },
+ 'messagePage.issue.INVALID_CONSUME_TIME.title': {
+ zh: '消费时间不可用',
+ en: 'Consume time unavailable',
+ },
+ 'messagePage.issue.INVALID_CONSUME_TIME.description': {
+ zh: '{group} 没有可解析的消费时间。',
+ en: '{group} has no parsable consume time.',
+ },
+ 'messagePage.issue.INVALID_CONSUME_TIME.recommendation': {
+ zh: '消费时间不可解析时,检查 trace 数据生成端是否返回了完整时间字段。',
+ en: 'When consume time cannot be parsed, check whether the trace producer
returns complete time fields.',
+ },
+ 'messagePage.storeTime': { zh: '存储时间', en: 'Store Time' },
+ 'messagePage.size': { zh: '大小', en: 'Size' },
+ 'messagePage.trace': { zh: '轨迹', en: 'Trace' },
+ 'messagePage.verify': { zh: '验证', en: 'Verify' },
+ 'messagePage.download': { zh: '下载', en: 'Download' },
+ 'messagePage.deliveryStatus': { zh: '投递状态', en: 'Delivery Status' },
+ 'messagePage.consumeTime': { zh: '消费时间', en: 'Consume Time' },
+ 'messagePage.retryCount': { zh: '重试次数', en: 'Retry Count' },
+ 'messagePage.tabContent': { zh: '消息内容', en: 'Message Content' },
+ 'messagePage.reconsumeTimes': { zh: '重投次数', en: 'Reconsume Times' },
+ 'messagePage.tabTrace': { zh: '消息轨迹', en: 'Message Trace' },
+ 'messagePage.traceMsgIdPlaceholder': {
+ zh: '消息 ID(默认当前消息)',
+ en: 'Message ID (defaults to current message)',
+ },
+ 'messagePage.traceTopicPlaceholder': {
+ zh: '轨迹 Topic(留空使用默认)',
+ en: 'Trace topic (leave empty to use default)',
+ },
+ 'messagePage.queryTrace': { zh: '查询轨迹', en: 'Query Trace' },
+ 'messagePage.loadingTrace': { zh: '正在加载轨迹数据…', en: 'Loading trace data…' },
+ 'messagePage.nodeCost': { zh: '耗时 {time}ms', en: 'Duration {time}ms' },
+ 'messagePage.noTraceData': { zh: '暂无轨迹数据', en: 'No trace data' },
+ 'messagePage.directConsume': { zh: '直接消费', en: 'Consume Directly' },
+ 'messagePage.directConsumeTitle': { zh: '直接消费消息', en: 'Consume Message
Directly' },
+ 'messagePage.execute': { zh: '执行', en: 'Execute' },
+ 'messagePage.directConsumeHint': {
+ zh: 'Broker 会请求指定在线客户端立即消费该消息。',
+ en: 'The broker will ask the specified online client to consume this
message immediately.',
+ },
+ 'messagePage.directConsumeNote': {
+ zh: '这不是向 Topic 重新发送消息;Broker 返回的消费结果会原样显示。',
+ en: 'This does not resend the message to the topic; the consume result
returned by the broker is displayed as-is.',
+ },
+ 'messagePage.directConsumeRequired': {
+ zh: '请填写目标消费组和在线客户端 ID',
+ en: 'Enter the target consumer group and an online client ID',
+ },
+ 'messagePage.directConsumeResult': {
+ zh: 'Broker 返回 {detail},耗时 {time} ms',
+ en: 'Broker returned {detail}, took {time} ms',
+ },
+ 'messagePage.directConsumeFailed': {
+ zh: '直接消费请求失败,请检查消费组和客户端是否在线',
+ en: 'Direct consume request failed; check that the consumer group and
client are online',
+ },
+ 'messagePage.downloadSuccess': { zh: '消息下载成功', en: 'Message downloaded' },
+ 'messagePage.consumerGroupPlaceholder': { zh: '目标消费者组', en: 'Target consumer
group' },
+ 'messagePage.clientIdPlaceholder': { zh: '在线客户端 ID', en: 'Online client ID'
},
+
// ─── Message Query History ───
'messageHistory.title': { zh: '服务端查询历史', en: 'Server Query History' },
'messageHistory.messageQueries': { zh: '消息查询', en: 'Message Queries' },
diff --git a/web/src/pages/instance/__tests__/MessagePage.test.tsx
b/web/src/pages/instance/__tests__/MessagePage.test.tsx
index af9be44fa..dcbe3e155 100644
--- a/web/src/pages/instance/__tests__/MessagePage.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePage.test.tsx
@@ -16,7 +16,7 @@
*/
import { App } from 'antd';
-import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor, within } from
'@testing-library/react';
import userEvent from '@testing-library/user-event';
import type React from 'react';
import { MemoryRouter } from 'react-router-dom';
@@ -405,6 +405,63 @@ describe('Message page query history', () => {
expect(locationItems[2]).toHaveTextContent('0');
});
+ it('renders trace diagnostics in English when the UI language is English',
async () => {
+ localStorage.setItem('rocketmq-studio-language', 'en');
+
messageServiceMocks.queryMessages.mockResolvedValue([createMessage('MID-TRACE-EN')]);
+ messageServiceMocks.getMessageTrace.mockResolvedValue({
+ nodes: [
+ {
+ title: 'Producer 发送',
+ timestamp: '2026-07-31T00:00:00.000Z',
+ costTime: 5,
+ status: 'finish',
+ description: 'producer sent the message',
+ },
+ {
+ title: 'Broker 存储',
+ timestamp: '2026-07-31T00:00:01.600Z',
+ costTime: 720,
+ status: 'finish',
+ description: 'broker persisted the message',
+ },
+ {
+ title: 'Consumer 消费',
+ timestamp: '2026-07-31T00:00:02.100Z',
+ costTime: 6200,
+ status: 'error',
+ description: 'consumer returned failure',
+ },
+ ],
+ consumerStatus: [
+ {
+ group: 'cg-billing',
+ deliveryStatus: 'failed',
+ consumeTime: '2026-07-31T00:00:05.000Z',
+ retryCount: 2,
+ },
+ ],
+ });
+ const user = userEvent.setup();
+ renderWithProviders(<MessagePage />);
+
+ await user.click(screen.getByText('Query by Message ID'));
+ await user.click(lastElement(screen.getAllByRole('combobox')));
+ await user.click(lastElement(await screen.findAllByText('order-create')));
+ await user.type(screen.getByPlaceholderText('Enter message ID'),
'MID-TRACE-EN');
+ await user.click(screen.getByRole('button', { name: /^searchQuery$/ }));
+
+ const row = await screen.findByRole('row', { name: /MID-TRACE-EN/ });
+ await user.click(within(row).getByRole('button', { name: /Trace/ }));
+
+ const dialog = await screen.findByRole('dialog', { name: 'Message Detail'
});
+ expect(await within(dialog).findByText('Trace
Diagnostics')).toBeInTheDocument();
+ expect(within(dialog).getByText('Delivery Critical')).toBeInTheDocument();
+ expect(within(dialog).getAllByText('Trace stage
failed')).not.toHaveLength(0);
+ expect(
+ within(dialog).getByText(/Check the producer, broker, or consumer logs/),
+ ).toBeInTheDocument();
+ });
+
it('renders placeholders on the detail panel when the storage location is
unknown', async () => {
const user = userEvent.setup();
messageServiceMocks.queryMessages.mockResolvedValue([
diff --git a/web/src/pages/instance/message.tsx
b/web/src/pages/instance/message.tsx
index e3e5c3122..4806404d4 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -78,13 +78,12 @@ import { tableScrollX } from '../../utils/table';
import {
analyzeMessageTrace,
type MessageTraceDiagnostics,
+ type TraceDiagnosticIssue,
type TraceDiagnosticStatus,
} from '../../utils/messageTraceDiagnostics';
const { Paragraph, Text } = Typography;
const { RangePicker } = DatePicker;
-const DEFAULT_QUERY_ERROR = '消息查询失败,请稍后重试';
-const DEFAULT_TRACE_ERROR = '消息轨迹加载失败,请稍后重试';
/* ─── Constants ─── */
@@ -100,16 +99,16 @@ type ApiErrorLike = {
};
const QUERY_OPTIONS = [
- { value: 'topic' as const, label: '按 Topic 查询' },
- { value: 'key' as const, label: '按 Message Key' },
- { value: 'msgid' as const, label: '按 Message ID' },
- { value: 'queue' as const, label: '按队列浏览' },
+ { value: 'topic' as const },
+ { value: 'key' as const },
+ { value: 'msgid' as const },
+ { value: 'queue' as const },
];
-const DELIVERY_STATUS_MAP: Record<string, { label: string; color: string }> = {
- success: { label: '成功', color: 'green' },
- failed: { label: '失败', color: 'red' },
- pending: { label: '等待中', color: 'gold' },
+const DELIVERY_STATUS_MAP: Record<string, { labelKey: string; color: string }>
= {
+ success: { labelKey: 'common.success', color: 'green' },
+ failed: { labelKey: 'common.failure', color: 'red' },
+ pending: { labelKey: 'messagePage.deliveryStatusPending', color: 'gold' },
};
const TOPIC_TAG_COLORS: Record<string, string> = {
@@ -153,10 +152,14 @@ const formatDurationMs = (value: number | null): string
=> {
return `${value} ms`;
};
-const getQueryValidationError = (mode: QueryMode, params: MessageQuery):
string | null => {
- if (!params.topic?.trim()) return '请选择 Topic';
- if (mode === 'key' && !params.key?.trim()) return '请输入 Message Key';
- if (mode === 'msgid' && !params.msgId?.trim()) return '请输入 Message ID';
+const getQueryValidationError = (
+ mode: QueryMode,
+ params: MessageQuery,
+ t: (key: string) => string,
+): string | null => {
+ if (!params.topic?.trim()) return t('messagePage.selectTopicRequired');
+ if (mode === 'key' && !params.key?.trim()) return
t('messagePage.enterMessageKeyRequired');
+ if (mode === 'msgid' && !params.msgId?.trim()) return
t('messagePage.enterMessageIdRequired');
return null;
};
@@ -205,13 +208,14 @@ const diagnosticTagColor: Record<TraceDiagnosticStatus,
string> = {
critical: 'error',
};
-const diagnosticStatusText: Record<TraceDiagnosticStatus, string> = {
- healthy: '健康',
- warning: '关注',
- critical: '异常',
+const diagnosticStatusKey: Record<TraceDiagnosticStatus, string> = {
+ healthy: 'clients.healthStatusHealthy',
+ warning: 'clients.healthStatusWarning',
+ critical: 'messagePage.traceStatusCritical',
};
const TraceDiagnosticsPanel = ({ diagnostics }: { diagnostics:
MessageTraceDiagnostics }) => {
+ const { t } = useLang();
const issueData = diagnostics.issues.slice(0, 8);
return (
@@ -221,11 +225,11 @@ const TraceDiagnosticsPanel = ({ diagnostics }: {
diagnostics: MessageTraceDiagn
type={diagnostics.statusColor}
message={
<Flex gap={8} align="center" wrap>
- <span>轨迹诊断</span>
- <Tag
color={diagnosticTagColor[diagnostics.status]}>{diagnostics.statusText}</Tag>
+ <span>{t('messagePage.traceDiagnostics')}</span>
+ <Tag
color={diagnosticTagColor[diagnostics.status]}>{t(diagnostics.statusKey)}</Tag>
{issueData.map((issue) => (
<Tag key={issue.id} color={diagnosticTagColor[issue.severity]}>
- {issue.title}
+ {t(issue.titleKey, issue.params)}
</Tag>
))}
</Flex>
@@ -233,24 +237,24 @@ const TraceDiagnosticsPanel = ({ diagnostics }: {
diagnostics: MessageTraceDiagn
/>
<Flex gap={16} wrap>
<div style={{ minWidth: 160 }}>
- <div style={{ color: '#8c8c8c', marginBottom: 6 }}>健康分</div>
+ <div style={{ color: '#8c8c8c', marginBottom: 6
}}>{t('messagePage.healthScore')}</div>
<Progress
percent={diagnostics.score}
status={diagnostics.status === 'critical' ? 'exception' : 'normal'}
strokeColor={diagnostics.status === 'healthy' ? '#52c41a' :
undefined}
/>
</div>
- <Statistic title="轨迹阶段" value={diagnostics.summary.nodeCount} />
+ <Statistic title={t('messagePage.traceStageCount')}
value={diagnostics.summary.nodeCount} />
<Statistic
- title="端到端耗时"
+ title={t('messagePage.endToEndLatency')}
value={formatDurationMs(diagnostics.summary.endToEndLatencyMs)}
/>
<Statistic
- title="阶段耗时合计"
+ title={t('messagePage.totalStageDuration')}
value={formatDurationMs(diagnostics.summary.totalNodeCostMs)}
/>
<Statistic
- title="消费成功率"
+ title={t('messagePage.consumeSuccessRate')}
value={
diagnostics.summary.successfulConsumerRate == null
? '-'
@@ -260,37 +264,45 @@ const TraceDiagnosticsPanel = ({ diagnostics }: {
diagnostics: MessageTraceDiagn
</Flex>
{diagnostics.summary.slowestNode && (
<Typography.Text type="secondary">
- 最慢阶段:{diagnostics.summary.slowestNode.title},
- {formatDurationMs(diagnostics.summary.slowestNode.valueMs)}
{diagnostics.summary.slowestGap
- ?
`;最大阶段间隔:${diagnostics.summary.slowestGap.title},${formatDurationMs(
- diagnostics.summary.slowestGap.valueMs,
- )}`
- : ''}
+ ? t('messagePage.slowestStageWithGap', {
+ title: diagnostics.summary.slowestNode.title,
+ duration:
formatDurationMs(diagnostics.summary.slowestNode.valueMs),
+ gapTitle: diagnostics.summary.slowestGap.title,
+ gapDuration:
formatDurationMs(diagnostics.summary.slowestGap.valueMs),
+ })
+ : t('messagePage.slowestStage', {
+ title: diagnostics.summary.slowestNode.title,
+ duration:
formatDurationMs(diagnostics.summary.slowestNode.valueMs),
+ })}
</Typography.Text>
)}
{issueData.length > 0 && (
<Table
columns={[
{
- title: '级别',
+ title: t('messagePage.diagSeverity'),
dataIndex: 'severity',
key: 'severity',
width: 90,
render: (severity: TraceDiagnosticStatus) => (
- <Tag
color={diagnosticTagColor[severity]}>{diagnosticStatusText[severity]}</Tag>
+ <Tag
color={diagnosticTagColor[severity]}>{t(diagnosticStatusKey[severity])}</Tag>
),
},
{
- title: '风险',
- dataIndex: 'title',
- key: 'title',
+ title: t('messagePage.diagRisk'),
+ dataIndex: 'titleKey',
+ key: 'titleKey',
width: 150,
+ render: (_: string, record: TraceDiagnosticIssue) =>
+ t(record.titleKey, record.params),
},
{
- title: '说明',
- dataIndex: 'description',
- key: 'description',
+ title: t('messagePage.diagDescription'),
+ dataIndex: 'descriptionKey',
+ key: 'descriptionKey',
+ render: (_: string, record: TraceDiagnosticIssue) =>
+ t(record.descriptionKey, record.params),
},
]}
dataSource={issueData}
@@ -299,11 +311,11 @@ const TraceDiagnosticsPanel = ({ diagnostics }: {
diagnostics: MessageTraceDiagn
size="small"
/>
)}
- {diagnostics.recommendations.length > 0 && (
+ {diagnostics.recommendationCodes.length > 0 && (
<Space direction="vertical" size={4}>
- {diagnostics.recommendations.slice(0, 4).map((recommendation) => (
- <Typography.Text key={recommendation} type="secondary">
- {recommendation}
+ {diagnostics.recommendationCodes.slice(0, 4).map((code) => (
+ <Typography.Text key={code} type="secondary">
+ {t(`messagePage.issue.${code}.recommendation`)}
</Typography.Text>
))}
</Space>
@@ -367,11 +379,11 @@ const MessagePageContent = ({
setTopicOptions(nextTopics.map((topic) => topic.name));
} catch (error: unknown) {
if (requestId !== topicRequestId.current) return;
- setTopicError(error instanceof Error ? error.message : '加载 Topic 列表失败');
+ setTopicError(error instanceof Error ? error.message :
t('messagePage.loadTopicsFailed'));
} finally {
if (requestId === topicRequestId.current) setTopicLoading(false);
}
- }, [selectedInstanceId]);
+ }, [selectedInstanceId, t]);
useEffect(() => {
void Promise.resolve().then(loadTopicOptions);
@@ -434,13 +446,13 @@ const MessagePageContent = ({
: queryMode === 'key'
? { topic: selectedTopic, key: keyInput || undefined }
: { topic: selectedTopic, msgId: msgIdInput || undefined };
- const queryValidationError = getQueryValidationError(queryMode,
currentQueryParams);
+ const queryValidationError = getQueryValidationError(queryMode,
currentQueryParams, t);
const queryDisabledReason = !selectedInstanceId
- ? '请先选择实例'
+ ? t('messagePage.selectInstanceFirst')
: topicLoading
- ? '正在加载 Topic 列表'
+ ? t('messagePage.loadingTopics')
: topicError
- ? 'Topic 列表加载失败,请先重试'
+ ? t('messagePage.topicLoadFailedRetry')
: queryValidationError;
/* ─── Handlers ─── */
@@ -479,12 +491,12 @@ const MessagePageContent = ({
const requestGeneration = queryGenerationRef.current + 1;
queryGenerationRef.current = requestGeneration;
if (!selectedInstanceId) {
- setQueryError('请先选择实例后再查询消息');
+ setQueryError(t('messagePage.selectInstanceBeforeQuery'));
setQueryLoading(false);
return;
}
const normalizedParams = normalizeMessageQuery(mode, params);
- const validationError = getQueryValidationError(mode, normalizedParams);
+ const validationError = getQueryValidationError(mode, normalizedParams, t);
if (validationError) {
setQueryError(validationError);
setQueryLoading(false);
@@ -509,10 +521,10 @@ const MessagePageContent = ({
setMessagePageSize(result.size);
setResultMayBeTruncated(result.resultMayBeTruncated);
setQueryError(null);
- message.success(`查询完成,共 ${result.total} 条`);
+ message.success(t('messagePage.queryCompleted', { total: result.total
}));
} catch (error) {
if (queryGenerationRef.current === requestGeneration) {
- setQueryError(getErrorMessage(error, DEFAULT_QUERY_ERROR));
+ setQueryError(getErrorMessage(error, t('messagePage.queryFailed')));
}
} finally {
if (queryGenerationRef.current === requestGeneration) {
@@ -563,7 +575,7 @@ const MessagePageContent = ({
};
const handleVerifyConsume = () => {
- message.warning('消费验证接口尚未接入,无法确认该消息的真实消费状态');
+ message.warning(t('messagePage.verifyNotAvailable'));
};
const loadMessageTrace = async (record: MessageRecord) => {
const requestGeneration = traceGenerationRef.current + 1;
@@ -600,7 +612,7 @@ const MessagePageContent = ({
setTraceError(null);
} catch (error) {
if (traceGenerationRef.current === requestGeneration) {
- setTraceError(getErrorMessage(error, DEFAULT_TRACE_ERROR));
+ setTraceError(getErrorMessage(error,
t('messagePage.traceLoadFailed')));
}
} finally {
if (traceGenerationRef.current === requestGeneration) {
@@ -634,7 +646,11 @@ const MessagePageContent = ({
// finally block will never reset traceLoading — stop it here instead.
setTraceData(null);
setTraceLoading(false);
- setTraceError(traceQueryMode === 'key' ? '请输入 Message Key' : '请输入
Message ID');
+ setTraceError(
+ traceQueryMode === 'key'
+ ? t('messagePage.enterMessageKeyRequired')
+ : t('messagePage.enterMessageIdRequired'),
+ );
return;
}
setTraceData(null);
@@ -655,7 +671,7 @@ const MessagePageContent = ({
setTraceError(null);
} catch (error) {
if (traceGenerationRef.current === requestGeneration) {
- setTraceError(getErrorMessage(error, DEFAULT_TRACE_ERROR));
+ setTraceError(getErrorMessage(error,
t('messagePage.traceLoadFailed')));
}
} finally {
if (traceGenerationRef.current === requestGeneration) {
@@ -684,7 +700,7 @@ const MessagePageContent = ({
!directConsumeGroup.trim() ||
!directConsumeClientId.trim()
) {
- message.warning('请填写目标消费组和在线客户端 ID');
+ message.warning(t('messagePage.directConsumeRequired'));
return;
}
setDirectConsumeSubmitting(true);
@@ -697,10 +713,15 @@ const MessagePageContent = ({
clientId: directConsumeClientId.trim(),
});
const detail = [result.consumeResult,
result.remark].filter(Boolean).join(':');
- message.info(`Broker 返回 ${detail || 'UNKNOWN'},耗时
${result.spentTimeMillis} ms`);
+ message.info(
+ t('messagePage.directConsumeResult', {
+ detail: detail || 'UNKNOWN',
+ time: result.spentTimeMillis,
+ }),
+ );
setDirectConsumeOpen(false);
} catch (error) {
- message.error(getErrorMessage(error, '直接消费请求失败,请检查消费组和客户端是否在线'));
+ message.error(getErrorMessage(error,
t('messagePage.directConsumeFailed')));
} finally {
setDirectConsumeSubmitting(false);
}
@@ -709,7 +730,7 @@ const MessagePageContent = ({
const handleDownload = (record: MessageRecord) => {
const blob = new Blob([formatBody(record.body)], { type:
'application/json' });
downloadBlob(blob, `${record.msgId}.json`);
- message.success('消息下载成功');
+ message.success(t('messagePage.downloadSuccess'));
};
/* ─── Table Columns ─── */
@@ -761,7 +782,7 @@ const MessagePageContent = ({
),
},
{
- title: '存储时间',
+ title: t('messagePage.storeTime'),
dataIndex: 'storeTime',
key: 'storeTime',
width: 185,
@@ -773,7 +794,7 @@ const MessagePageContent = ({
),
},
{
- title: '大小',
+ title: t('messagePage.size'),
dataIndex: 'size',
key: 'size',
width: 80,
@@ -781,7 +802,7 @@ const MessagePageContent = ({
render: (size: number) => formatSize(size),
},
{
- title: '操作',
+ title: t('common.actions'),
key: 'actions',
width: 260,
render: (_: unknown, record: MessageRecord) => (
@@ -792,7 +813,7 @@ const MessagePageContent = ({
style={{ borderColor: '#1677ff', color: '#1677ff' }}
onClick={() => void openDetail(record, 'content')}
>
- 详情
+ {t('common.detail')}
</Button>
<Button
size="small"
@@ -800,7 +821,7 @@ const MessagePageContent = ({
style={{ borderColor: '#722ed1', color: '#722ed1' }}
onClick={() => void openDetail(record, 'trace')}
>
- 轨迹
+ {t('messagePage.trace')}
</Button>
<Button
size="small"
@@ -808,7 +829,7 @@ const MessagePageContent = ({
style={{ borderColor: '#52c41a', color: '#52c41a' }}
onClick={handleVerifyConsume}
>
- 验证
+ {t('messagePage.verify')}
</Button>
<Button
size="small"
@@ -816,7 +837,7 @@ const MessagePageContent = ({
style={{ borderColor: '#fa8c16', color: '#fa8c16' }}
onClick={() => handleDownload(record)}
>
- 下载
+ {t('messagePage.download')}
</Button>
</Flex>
),
@@ -830,25 +851,25 @@ const MessagePageContent = ({
retryCount: number;
}> = [
{
- title: '消费者组',
+ title: t('topic.consumerGroup'),
dataIndex: 'group',
key: 'group',
render: (g: string) => <span style={{ fontFamily: 'monospace',
fontWeight: 500 }}>{g}</span>,
},
{
- title: '投递状态',
+ title: t('messagePage.deliveryStatus'),
dataIndex: 'deliveryStatus',
key: 'deliveryStatus',
render: (status: string) => {
const s = DELIVERY_STATUS_MAP[(status ?? '').toLowerCase()] || {
- label: status,
+ labelKey: status,
color: 'default',
};
- return <Tag color={s.color}>{s.label}</Tag>;
+ return <Tag color={s.color}>{t(s.labelKey)}</Tag>;
},
},
{
- title: '消费时间',
+ title: t('messagePage.consumeTime'),
dataIndex: 'consumeTime',
key: 'consumeTime',
render: (time: string) =>
@@ -859,7 +880,7 @@ const MessagePageContent = ({
),
},
{
- title: '重试次数',
+ title: t('messagePage.retryCount'),
dataIndex: 'retryCount',
key: 'retryCount',
align: 'center',
@@ -873,7 +894,7 @@ const MessagePageContent = ({
const modalTabs = [
{
key: 'content',
- label: '消息内容',
+ label: t('messagePage.tabContent'),
children: selectedMsg && (
<>
<Descriptions column={2} size="small" style={{ marginBottom: 24 }}>
@@ -893,8 +914,10 @@ const MessagePageContent = ({
<Descriptions.Item label="Key">
<span style={{ fontFamily: 'monospace'
}}>{selectedMsg.key}</span>
</Descriptions.Item>
- <Descriptions.Item
label="大小">{formatSize(selectedMsg.size)}</Descriptions.Item>
- <Descriptions.Item label="重投次数">
+ <Descriptions.Item label={t('messagePage.size')}>
+ {formatSize(selectedMsg.size)}
+ </Descriptions.Item>
+ <Descriptions.Item label={t('messagePage.reconsumeTimes')}>
<span style={{ fontFamily: 'monospace'
}}>{selectedMsg.reconsumeTimes ?? '-'}</span>
</Descriptions.Item>
<Descriptions.Item label="Born Host">
@@ -903,7 +926,7 @@ const MessagePageContent = ({
<Descriptions.Item label="Store Host">
<span style={{ fontFamily: 'monospace'
}}>{selectedMsg.storeHost}</span>
</Descriptions.Item>
- <Descriptions.Item label="存储时间" span={2}>
+ <Descriptions.Item label={t('messagePage.storeTime')} span={2}>
<span style={{ fontFamily: 'monospace'
}}>{formatTimeMs(selectedMsg.storeTime)}</span>
</Descriptions.Item>
<Descriptions.Item label="Broker">
@@ -917,7 +940,7 @@ const MessagePageContent = ({
</Descriptions.Item>
</Descriptions>
<Typography.Title level={5} style={{ marginBottom: 8 }}>
- 消息体
+ {t('topic.messageBody')}
</Typography.Title>
<Paragraph
copyable
@@ -940,15 +963,15 @@ const MessagePageContent = ({
},
{
key: 'trace',
- label: '消息轨迹',
+ label: t('messagePage.tabTrace'),
children: (
<>
<Space wrap size={8} style={{ marginBottom: 16 }}>
<Segmented
size="small"
options={[
- { value: 'msgid', label: '按 Message ID' },
- { value: 'key', label: '按 Message Key' },
+ { value: 'msgid', label: t('messagePage.queryMode.msgid') },
+ { value: 'key', label: t('messagePage.queryMode.key') },
]}
value={traceQueryMode}
onChange={(value) => setTraceQueryMode(value as 'msgid' | 'key')}
@@ -957,7 +980,9 @@ const MessagePageContent = ({
size="small"
style={{ width: 300 }}
placeholder={
- traceQueryMode === 'key' ? '输入 Message Key' : '消息 ID(默认当前消息)'
+ traceQueryMode === 'key'
+ ? t('messagePage.inputKeyPlaceholder')
+ : t('messagePage.traceMsgIdPlaceholder')
}
value={traceQueryValue}
onChange={(event) => setTraceQueryValue(event.target.value)}
@@ -965,7 +990,7 @@ const MessagePageContent = ({
<Input
size="small"
style={{ width: 260 }}
- placeholder="轨迹 Topic(留空使用默认)"
+ placeholder={t('messagePage.traceTopicPlaceholder')}
value={customTraceTopic}
onChange={(event) => setCustomTraceTopic(event.target.value)}
allowClear
@@ -976,11 +1001,11 @@ const MessagePageContent = ({
icon={<SearchOutlined />}
onClick={() => void runTraceQuery()}
>
- 查询轨迹
+ {t('messagePage.queryTrace')}
</Button>
</Space>
{traceLoading ? (
- <Typography.Text type="secondary">正在加载轨迹数据…</Typography.Text>
+ <Typography.Text
type="secondary">{t('messagePage.loadingTrace')}</Typography.Text>
) : traceError ? (
<Alert showIcon type="warning" message={traceError} />
) : traceData?.nodes?.length ? (
@@ -997,7 +1022,9 @@ const MessagePageContent = ({
{formatTimeMs(node.timestamp)}
</div>
<div style={{ marginTop: 2 }}>{node.description}</div>
- <div style={{ color: '#9CA3AF', fontSize: 14 }}>耗时
{node.costTime}ms</div>
+ <div style={{ color: '#9CA3AF', fontSize: 14 }}>
+ {t('messagePage.nodeCost', { time: node.costTime })}
+ </div>
</div>
),
status: node.status,
@@ -1005,14 +1032,14 @@ const MessagePageContent = ({
/>
</Space>
) : (
- <Typography.Text type="secondary">暂无轨迹数据</Typography.Text>
+ <Typography.Text
type="secondary">{t('messagePage.noTraceData')}</Typography.Text>
)}
</>
),
},
{
key: 'consumer',
- label: '验证',
+ label: t('messagePage.verify'),
children: (
<Table
columns={consumerStatusColumns}
@@ -1030,7 +1057,7 @@ const MessagePageContent = ({
═══════════════════════════════════════════ */
return (
<div style={{ padding: 24 }}>
- <PageHeader title={t('message.title')} subtitle="按 Topic、Key 或 Message
ID 检索消息" />
+ <PageHeader title={t('message.title')}
subtitle={t('messagePage.subtitle')} />
{/* ── Query Form ── */}
<Card style={{ marginBottom: 16 }}>
@@ -1043,7 +1070,10 @@ const MessagePageContent = ({
style={{ width: 220 }}
/>
<Segmented
- options={QUERY_OPTIONS}
+ options={QUERY_OPTIONS.map(({ value }) => ({
+ value,
+ label: t(`messagePage.queryMode.${value}`),
+ }))}
value={queryMode}
onChange={(v) => handleQueryModeChange(v as QueryMode)}
/>
@@ -1054,7 +1084,7 @@ const MessagePageContent = ({
{queryMode === 'topic' && (
<>
<Select
- placeholder="选择 Topic"
+ placeholder={t('messagePage.topicPlaceholder')}
style={{ width: 360 }}
value={selectedTopic}
onChange={setSelectedTopic}
@@ -1083,7 +1113,7 @@ const MessagePageContent = ({
{queryMode === 'key' && (
<>
<Select
- placeholder="选择 Topic"
+ placeholder={t('messagePage.topicPlaceholder')}
style={{ width: 360 }}
value={selectedTopic}
onChange={setSelectedTopic}
@@ -1097,7 +1127,7 @@ const MessagePageContent = ({
}))}
/>
<Input
- placeholder="输入 Message Key"
+ placeholder={t('messagePage.inputKeyPlaceholder')}
style={{ width: 240 }}
value={keyInput}
onChange={(e) => setKeyInput(e.target.value)}
@@ -1108,7 +1138,7 @@ const MessagePageContent = ({
{queryMode === 'msgid' && (
<>
<Select
- placeholder="选择 Topic"
+ placeholder={t('messagePage.topicPlaceholder')}
style={{ width: 360 }}
value={selectedTopic}
onChange={setSelectedTopic}
@@ -1122,7 +1152,7 @@ const MessagePageContent = ({
}))}
/>
<Input
- placeholder="输入 Message ID"
+ placeholder={t('messagePage.inputMsgIdPlaceholder')}
style={{ width: 400 }}
value={msgIdInput}
onChange={(e) => setMsgIdInput(e.target.value)}
@@ -1139,13 +1169,13 @@ const MessagePageContent = ({
void handleQuery();
}}
>
- 查询
+ {t('messagePage.query')}
</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>
- 重置
+ {t('common.reset')}
</Button>
<Button icon={<HistoryOutlined />} onClick={() =>
setHistoryDrawerOpen(true)}>
- 服务端历史
+ {t('messagePage.serverHistory')}
</Button>
</Space>
)}
@@ -1167,11 +1197,11 @@ const MessagePageContent = ({
<Alert
showIcon
type="error"
- message="Topic 列表加载失败"
+ message={t('messagePage.topicLoadFailed')}
description={topicError}
action={
<Button size="small" onClick={() => void loadTopicOptions()}>
- 重试
+ {t('common.retry')}
</Button>
}
style={{ marginBottom: 16 }}
@@ -1192,7 +1222,7 @@ const MessagePageContent = ({
<Alert
showIcon
type="warning"
- message="查询结果达到服务端扫描上限,当前总数可能不完整。"
+ message={t('messagePage.truncatedWarning')}
style={{ marginBottom: 16 }}
/>
)}
@@ -1210,7 +1240,7 @@ const MessagePageContent = ({
pageSize: messagePageSize,
total: messageTotal,
showSizeChanger: true,
- showTotal: (total) => `共 ${total} 条消息`,
+ showTotal: (total) => t('messagePage.totalMessages', { total }),
onChange: (page, pageSize) => {
const committed = committedQueryRef.current;
if (!committed) return;
@@ -1226,21 +1256,21 @@ const MessagePageContent = ({
{/* ── Message Detail Modal ── */}
<Modal
- title="消息详情"
+ title={t('message.detail')}
width={800}
open={modalOpen}
onCancel={closeDetail}
destroyOnHidden
footer={
<Flex justify="flex-end" gap={8}>
- <Button onClick={closeDetail}>关闭</Button>
+ <Button onClick={closeDetail}>{t('common.close')}</Button>
<Button
type="primary"
icon={<SendOutlined />}
disabled={!selectedInstanceId || !selectedMsg}
onClick={openDirectConsume}
>
- 直接消费
+ {t('messagePage.directConsume')}
</Button>
</Flex>
}
@@ -1249,19 +1279,19 @@ const MessagePageContent = ({
</Modal>
<Modal
- title="直接消费消息"
+ title={t('messagePage.directConsumeTitle')}
open={directConsumeOpen}
onCancel={() => setDirectConsumeOpen(false)}
onOk={() => void handleDirectConsume()}
confirmLoading={directConsumeSubmitting}
- okText="执行"
+ okText={t('messagePage.execute')}
destroyOnHidden
>
<Alert
showIcon
type="warning"
- message="Broker 会请求指定在线客户端立即消费该消息。"
- description="这不是向 Topic 重新发送消息;Broker 返回的消费结果会原样显示。"
+ message={t('messagePage.directConsumeHint')}
+ description={t('messagePage.directConsumeNote')}
style={{ marginBottom: 16 }}
/>
<Space direction="vertical" size={12} style={{ width: '100%' }}>
@@ -1270,13 +1300,13 @@ const MessagePageContent = ({
<Input
value={directConsumeGroup}
onChange={(event) => setDirectConsumeGroup(event.target.value)}
- placeholder="目标消费者组"
+ placeholder={t('messagePage.consumerGroupPlaceholder')}
addonBefore="Consumer group"
/>
<Input
value={directConsumeClientId}
onChange={(event) => setDirectConsumeClientId(event.target.value)}
- placeholder="在线客户端 ID"
+ placeholder={t('messagePage.clientIdPlaceholder')}
addonBefore="Client ID"
/>
</Space>
diff --git a/web/src/utils/messageTraceDiagnostics.test.ts
b/web/src/utils/messageTraceDiagnostics.test.ts
index 692a73e90..a9cde97e3 100644
--- a/web/src/utils/messageTraceDiagnostics.test.ts
+++ b/web/src/utils/messageTraceDiagnostics.test.ts
@@ -57,6 +57,7 @@ describe('message trace diagnostics', () => {
const diagnostics = analyzeMessageTrace(baseTrace());
expect(diagnostics.status).toBe('healthy');
+ expect(diagnostics.statusKey).toBe('messagePage.traceStatusHealthy');
expect(diagnostics.score).toBe(100);
expect(diagnostics.summary).toMatchObject({
nodeCount: 3,
@@ -70,20 +71,24 @@ describe('message trace diagnostics', () => {
valueMs: 70,
});
expect(diagnostics.issues).toEqual([]);
- expect(diagnostics.recommendations).toEqual([]);
+ expect(diagnostics.recommendationCodes).toEqual([]);
});
it('returns guidance when trace nodes are missing', () => {
const diagnostics = analyzeMessageTrace({ nodes: [], consumerStatus: [] });
expect(diagnostics.status).toBe('warning');
+ expect(diagnostics.statusKey).toBe('messagePage.traceStatusWarning');
expect(diagnostics.summary.endToEndLatencyMs).toBeNull();
expect(diagnostics.issues).toEqual([
- expect.objectContaining({ code: 'NO_TRACE_NODES', severity: 'warning' }),
+ expect.objectContaining({
+ code: 'NO_TRACE_NODES',
+ severity: 'warning',
+ titleKey: 'messagePage.issue.NO_TRACE_NODES.title',
+ descriptionKey: 'messagePage.issue.NO_TRACE_NODES.description',
+ }),
]);
- expect(diagnostics.recommendations).toContain(
- '确认消息轨迹已开启,并检查是否需要指定自定义轨迹 Topic。',
- );
+ expect(diagnostics.recommendationCodes).toContain('NO_TRACE_NODES');
});
it('flags failed and waiting trace phases with latency hotspots', () => {
@@ -99,6 +104,7 @@ describe('message trace diagnostics', () => {
);
expect(diagnostics.status).toBe('critical');
+
expect(diagnostics.statusKey).toBe('messagePage.traceStatusDeliveryCritical');
expect(diagnostics.summary).toMatchObject({
failedNodeCount: 1,
waitingNodeCount: 1,
@@ -120,6 +126,28 @@ describe('message trace diagnostics', () => {
'SLOW_TRACE_GAP',
]),
);
+ const failedIssue = diagnostics.issues.find((issue) => issue.code ===
'FAILED_TRACE_NODE');
+ expect(failedIssue).toMatchObject({
+ titleKey: 'messagePage.issue.FAILED_TRACE_NODE.title',
+ descriptionKey: 'messagePage.issue.FAILED_TRACE_NODE.description',
+ params: { phase: 'Consumer 消费' },
+ });
+ const slowNodeIssue = diagnostics.issues.find(
+ (issue) => issue.code === 'SLOW_TRACE_NODE' && issue.subject ===
'2:Consumer 消费',
+ );
+ expect(slowNodeIssue).toMatchObject({
+ params: { phase: 'Consumer 消费', cost: 9000 },
+ });
+ const slowGapIssue = diagnostics.issues.find((issue) => issue.code ===
'SLOW_TRACE_GAP');
+ expect(slowGapIssue).toMatchObject({
+ params: { phase: 'Broker 存储', gap: 2200 },
+ });
+ expect(diagnostics.recommendationCodes).toEqual([
+ 'SLOW_TRACE_NODE',
+ 'SLOW_TRACE_GAP',
+ 'FAILED_TRACE_NODE',
+ 'WAITING_TRACE_NODE',
+ ]);
});
it('flags consumer delivery failures, pending statuses, unknown states and
retries', () => {
@@ -162,6 +190,19 @@ describe('message trace diagnostics', () => {
'INVALID_CONSUME_TIME',
]),
);
+ const retriedIssue = diagnostics.issues.find(
+ (issue) => issue.code === 'RETRIED_CONSUMER_DELIVERY' && issue.subject
=== '1:cg-billing',
+ );
+ expect(retriedIssue).toMatchObject({
+ titleKey: 'messagePage.issue.RETRIED_CONSUMER_DELIVERY.title',
+ params: { group: 'cg-billing', retry: 3 },
+ });
+ const unknownIssue = diagnostics.issues.find(
+ (issue) => issue.code === 'UNKNOWN_CONSUMER_DELIVERY',
+ );
+ expect(unknownIssue).toMatchObject({
+ params: { group: 'cg-search', status: 'paused' },
+ });
});
it('detects invalid timing, timestamp regressions and slow end-to-end
traces', () => {
diff --git a/web/src/utils/messageTraceDiagnostics.ts
b/web/src/utils/messageTraceDiagnostics.ts
index 66aefb94e..b0d029117 100644
--- a/web/src/utils/messageTraceDiagnostics.ts
+++ b/web/src/utils/messageTraceDiagnostics.ts
@@ -40,8 +40,9 @@ export interface TraceDiagnosticIssue {
id: string;
code: TraceIssueCode;
severity: Exclude<TraceDiagnosticStatus, 'healthy'>;
- title: string;
- description: string;
+ titleKey: string;
+ descriptionKey: string;
+ params?: Record<string, string | number>;
subject?: string;
}
@@ -88,14 +89,14 @@ export interface TraceDiagnosticSummary {
export interface MessageTraceDiagnostics {
status: TraceDiagnosticStatus;
- statusText: string;
+ statusKey: string;
statusColor: 'success' | 'warning' | 'error';
score: number;
summary: TraceDiagnosticSummary;
phases: TraceDiagnosticPhase[];
deliveries: ConsumerDeliveryDiagnostic[];
issues: TraceDiagnosticIssue[];
- recommendations: string[];
+ recommendationCodes: TraceIssueCode[];
}
export interface TraceDiagnosticOptions {
@@ -114,10 +115,10 @@ const DEFAULT_OPTIONS: Required<TraceDiagnosticOptions> =
{
criticalEndToEndMs: 30000,
};
-const STATUS_TEXT: Record<TraceDiagnosticStatus, string> = {
- healthy: '轨迹健康',
- warning: '需要关注',
- critical: '投递异常',
+const STATUS_KEY: Record<TraceDiagnosticStatus, string> = {
+ healthy: 'messagePage.traceStatusHealthy',
+ warning: 'messagePage.traceStatusWarning',
+ critical: 'messagePage.traceStatusDeliveryCritical',
};
const STATUS_COLOR: Record<TraceDiagnosticStatus, 'success' | 'warning' |
'error'> = {
@@ -137,28 +138,6 @@ const ISSUE_SCORE_PENALTY:
Record<Exclude<TraceDiagnosticStatus, 'healthy'>, num
critical: 24,
};
-const ISSUE_RECOMMENDATIONS: Record<TraceIssueCode, string> = {
- NO_TRACE_NODES: '确认消息轨迹已开启,并检查是否需要指定自定义轨迹 Topic。',
- FAILED_TRACE_NODE:
- '优先查看失败阶段对应的生产者、Broker 或消费者日志,确认失败返回码和异常堆栈。',
- WAITING_TRACE_NODE: '等待或处理中阶段需要结合消费者在线状态和堆积情况确认是否仍在推进。',
- INVALID_TRACE_TIMESTAMP: '检查生产者、Broker 与消费者机器时间,避免时钟漂移影响轨迹判断。',
- TRACE_TIMESTAMP_REGRESSION: '轨迹时间出现回退时,先确认各节点 NTP 同步和跨机房时间源配置。',
- INVALID_TRACE_COST: '忽略异常耗时值后再判断链路瓶颈,并核对服务端轨迹采集字段是否完整。',
- SLOW_TRACE_NODE: '对耗时最高的阶段做分段排查,区分发送、存储和消费处理时间。',
- SLOW_TRACE_GAP: '相邻阶段间隔过大时,检查 Broker 拉取、客户端长轮询和消费线程池排队。',
- SLOW_END_TO_END_TRACE:
- '端到端耗时过高时,结合 Topic 队列分布、Consumer Group 进度和客户端负载一起排查。',
- MISSING_CONSUMER_STATUS:
- '缺少消费状态时,可用直接消费或 Consumer Group 进度进一步确认消息是否可达。',
- FAILED_CONSUMER_DELIVERY: '失败消费组需要检查消费异常、重试 Topic 和业务幂等处理。',
- PENDING_CONSUMER_DELIVERY:
- '等待中的消费组需要确认客户端是否在线、订阅是否匹配以及是否存在明显堆积。',
- UNKNOWN_CONSUMER_DELIVERY: '未知消费状态需要回查服务端返回值,避免把未识别状态误判为成功。',
- RETRIED_CONSUMER_DELIVERY: '存在重试时,检查消费耗时、异常类型和重试次数是否符合预期。',
- INVALID_CONSUME_TIME: '消费时间不可解析时,检查 trace 数据生成端是否返回了完整时间字段。',
-};
-
const parseTimestamp = (value: number | string): number | null => {
if (typeof value === 'number') {
return Number.isFinite(value) && value > 0 ? value : null;
@@ -180,15 +159,17 @@ const normalizeDeliveryStatus = (value: string): string
=> value.trim().toLowerC
const buildIssue = (
code: TraceIssueCode,
severity: Exclude<TraceDiagnosticStatus, 'healthy'>,
- title: string,
- description: string,
+ titleKey: string,
+ descriptionKey: string,
+ params?: Record<string, string | number>,
subject?: string,
): TraceDiagnosticIssue => ({
id: subject ? `${code}:${subject}` : code,
code,
severity,
- title,
- description,
+ titleKey,
+ descriptionKey,
+ params,
subject,
});
@@ -222,8 +203,9 @@ const collectPhaseIssues = (
buildIssue(
'FAILED_TRACE_NODE',
'critical',
- '轨迹阶段失败',
- `${phase.title} 阶段返回失败状态。`,
+ 'messagePage.issue.FAILED_TRACE_NODE.title',
+ 'messagePage.issue.FAILED_TRACE_NODE.description',
+ { phase: phase.title },
phase.key,
),
);
@@ -232,8 +214,9 @@ const collectPhaseIssues = (
buildIssue(
'WAITING_TRACE_NODE',
'warning',
- '轨迹阶段未完成',
- `${phase.title} 阶段仍处于等待或处理中状态。`,
+ 'messagePage.issue.WAITING_TRACE_NODE.title',
+ 'messagePage.issue.WAITING_TRACE_NODE.description',
+ { phase: phase.title },
phase.key,
),
);
@@ -244,8 +227,9 @@ const collectPhaseIssues = (
buildIssue(
'INVALID_TRACE_TIMESTAMP',
'warning',
- '轨迹时间不可用',
- `${phase.title} 阶段没有可解析的时间戳。`,
+ 'messagePage.issue.INVALID_TRACE_TIMESTAMP.title',
+ 'messagePage.issue.INVALID_TRACE_TIMESTAMP.description',
+ { phase: phase.title },
phase.key,
),
);
@@ -256,8 +240,9 @@ const collectPhaseIssues = (
buildIssue(
'INVALID_TRACE_COST',
'warning',
- '阶段耗时不可用',
- `${phase.title} 阶段返回了无效耗时。`,
+ 'messagePage.issue.INVALID_TRACE_COST.title',
+ 'messagePage.issue.INVALID_TRACE_COST.description',
+ { phase: phase.title },
phase.key,
),
);
@@ -266,8 +251,9 @@ const collectPhaseIssues = (
buildIssue(
'SLOW_TRACE_NODE',
severityForLatency(phase.costTimeMs, options.slowNodeCostMs,
options.criticalNodeCostMs),
- '阶段耗时偏高',
- `${phase.title} 阶段耗时 ${phase.costTimeMs} ms。`,
+ 'messagePage.issue.SLOW_TRACE_NODE.title',
+ 'messagePage.issue.SLOW_TRACE_NODE.description',
+ { phase: phase.title, cost: phase.costTimeMs },
phase.key,
),
);
@@ -278,8 +264,9 @@ const collectPhaseIssues = (
buildIssue(
'SLOW_TRACE_GAP',
'warning',
- '相邻阶段间隔偏高',
- `${phase.title} 与上一阶段相隔 ${phase.latencyFromPreviousMs} ms。`,
+ 'messagePage.issue.SLOW_TRACE_GAP.title',
+ 'messagePage.issue.SLOW_TRACE_GAP.description',
+ { phase: phase.title, gap: phase.latencyFromPreviousMs },
phase.key,
),
);
@@ -323,8 +310,9 @@ const collectTimelineIssues = (phases:
TraceDiagnosticPhase[]): TraceDiagnosticI
buildIssue(
'TRACE_TIMESTAMP_REGRESSION',
'warning',
- '轨迹时间发生回退',
- `${phase.title} 比上一阶段早 ${Math.abs(phase.latencyFromPreviousMs)} ms。`,
+ 'messagePage.issue.TRACE_TIMESTAMP_REGRESSION.title',
+ 'messagePage.issue.TRACE_TIMESTAMP_REGRESSION.description',
+ { phase: phase.title, gap: Math.abs(phase.latencyFromPreviousMs) },
phase.key,
),
);
@@ -375,8 +363,9 @@ const collectDeliveryIssues = (
buildIssue(
'FAILED_CONSUMER_DELIVERY',
'critical',
- '消费投递失败',
- `${delivery.group} 返回失败消费状态。`,
+ 'messagePage.issue.FAILED_CONSUMER_DELIVERY.title',
+ 'messagePage.issue.FAILED_CONSUMER_DELIVERY.description',
+ { group: delivery.group },
delivery.key,
),
);
@@ -385,8 +374,9 @@ const collectDeliveryIssues = (
buildIssue(
'PENDING_CONSUMER_DELIVERY',
'warning',
- '消费投递等待中',
- `${delivery.group} 尚未完成消费。`,
+ 'messagePage.issue.PENDING_CONSUMER_DELIVERY.title',
+ 'messagePage.issue.PENDING_CONSUMER_DELIVERY.description',
+ { group: delivery.group },
delivery.key,
),
);
@@ -395,8 +385,9 @@ const collectDeliveryIssues = (
buildIssue(
'UNKNOWN_CONSUMER_DELIVERY',
'warning',
- '消费状态未知',
- `${delivery.group} 返回未识别状态 ${delivery.deliveryStatus}。`,
+ 'messagePage.issue.UNKNOWN_CONSUMER_DELIVERY.title',
+ 'messagePage.issue.UNKNOWN_CONSUMER_DELIVERY.description',
+ { group: delivery.group, status: delivery.deliveryStatus },
delivery.key,
),
);
@@ -407,8 +398,9 @@ const collectDeliveryIssues = (
buildIssue(
'RETRIED_CONSUMER_DELIVERY',
'warning',
- '消费发生重试',
- `${delivery.group} 已重试 ${delivery.retryCount} 次。`,
+ 'messagePage.issue.RETRIED_CONSUMER_DELIVERY.title',
+ 'messagePage.issue.RETRIED_CONSUMER_DELIVERY.description',
+ { group: delivery.group, retry: delivery.retryCount },
delivery.key,
),
);
@@ -419,8 +411,9 @@ const collectDeliveryIssues = (
buildIssue(
'INVALID_CONSUME_TIME',
'warning',
- '消费时间不可用',
- `${delivery.group} 没有可解析的消费时间。`,
+ 'messagePage.issue.INVALID_CONSUME_TIME.title',
+ 'messagePage.issue.INVALID_CONSUME_TIME.description',
+ { group: delivery.group },
delivery.key,
),
);
@@ -506,7 +499,12 @@ const collectSummaryIssues = (
if (summary.nodeCount === 0) {
issues.push(
- buildIssue('NO_TRACE_NODES', 'warning', '缺少轨迹节点', '当前消息没有返回可展示的轨迹阶段。'),
+ buildIssue(
+ 'NO_TRACE_NODES',
+ 'warning',
+ 'messagePage.issue.NO_TRACE_NODES.title',
+ 'messagePage.issue.NO_TRACE_NODES.description',
+ ),
);
}
@@ -515,8 +513,8 @@ const collectSummaryIssues = (
buildIssue(
'MISSING_CONSUMER_STATUS',
'warning',
- '缺少消费状态',
- '轨迹中没有返回任何消费组的投递状态。',
+ 'messagePage.issue.MISSING_CONSUMER_STATUS.title',
+ 'messagePage.issue.MISSING_CONSUMER_STATUS.description',
),
);
}
@@ -530,8 +528,9 @@ const collectSummaryIssues = (
options.slowEndToEndMs,
options.criticalEndToEndMs,
),
- '端到端轨迹耗时偏高',
- `首尾轨迹阶段相隔 ${summary.endToEndLatencyMs} ms。`,
+ 'messagePage.issue.SLOW_END_TO_END_TRACE.title',
+ 'messagePage.issue.SLOW_END_TO_END_TRACE.description',
+ { latency: summary.endToEndLatencyMs },
),
);
}
@@ -539,11 +538,9 @@ const collectSummaryIssues = (
return issues;
};
-const buildRecommendations = (issues: TraceDiagnosticIssue[]): string[] => {
- const codes = new Set(issues.map((issue) => issue.code));
- const recommendations = [...codes].map((code) =>
ISSUE_RECOMMENDATIONS[code]);
- return recommendations.filter((item, index) => recommendations.indexOf(item)
=== index);
-};
+const buildRecommendationCodes = (issues: TraceDiagnosticIssue[]):
TraceIssueCode[] => [
+ ...new Set(issues.map((issue) => issue.code)),
+];
export function analyzeMessageTrace(
trace: TraceRecord | null | undefined,
@@ -565,13 +562,13 @@ export function analyzeMessageTrace(
return {
status,
- statusText: STATUS_TEXT[status],
+ statusKey: STATUS_KEY[status],
statusColor: STATUS_COLOR[status],
score: calculateScore(issues),
summary,
phases,
deliveries,
issues,
- recommendations: buildRecommendations(issues),
+ recommendationCodes: buildRecommendationCodes(issues),
};
}