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
commit 1e4fa1a8cf5ee297a5bd7a4a2804e1ee150f82e2 Author: zhaohai <[email protected]> AuthorDate: Fri Jul 24 13:42:12 2026 +0800 feat: enhance i18n support with labelKey-based translations (#495) Migrate hardcoded Chinese UI text to i18n labelKey pattern, add ~280 translation entries (zh+en), fix duplicate key compilation errors. --- web/src/components/StatusBadge.tsx | 4 +- web/src/components/__tests__/StatusBadge.test.tsx | 77 ++++++ web/src/constants/__tests__/theme.test.ts | 97 ++++++++ web/src/constants/theme.ts | 50 ++-- web/src/i18n/__tests__/LangContext.test.tsx | 2 +- web/src/i18n/translations.ts | 287 +++++++++++++++++++++- web/src/pages/cluster/index.tsx | 57 ++--- web/src/pages/home/dashboard.tsx | 2 +- web/src/pages/instance/consumer.tsx | 13 +- web/src/pages/instance/topic.tsx | 14 +- 10 files changed, 529 insertions(+), 74 deletions(-) diff --git a/web/src/components/StatusBadge.tsx b/web/src/components/StatusBadge.tsx index b2c02ec9..1a38781f 100644 --- a/web/src/components/StatusBadge.tsx +++ b/web/src/components/StatusBadge.tsx @@ -17,6 +17,7 @@ import { Badge, Space, Typography } from 'antd'; import { STATUS_MAP } from '../constants/theme'; +import { useLang } from '../i18n/LangContext'; const { Text } = Typography; @@ -27,8 +28,9 @@ interface StatusBadgeProps { } const StatusBadge = ({ status, text, showDot = true }: StatusBadgeProps) => { + const { t } = useLang(); const config = STATUS_MAP[status] || STATUS_MAP.offline; - const label = text || config.label; + const label = text || t(config.labelKey); return ( <Space size={4} role="status" aria-label={`状态:${label}`}> {showDot && <Badge color={config.dot} aria-hidden="true" />} diff --git a/web/src/components/__tests__/StatusBadge.test.tsx b/web/src/components/__tests__/StatusBadge.test.tsx new file mode 100644 index 00000000..07f08c7e --- /dev/null +++ b/web/src/components/__tests__/StatusBadge.test.tsx @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import StatusBadge from '../StatusBadge'; +import { LangProvider } from '../../i18n/LangContext'; + +const renderWithLang = (ui: React.ReactElement) => render(<LangProvider>{ui}</LangProvider>); + +describe('StatusBadge', () => { + it('renders the translated label for a known status in Chinese', () => { + renderWithLang(<StatusBadge status="healthy" />); + // theme.healthy in zh is '运行中' + expect(screen.getByText('运行中')).toBeInTheDocument(); + }); + + it('renders the translated label for warning status', () => { + renderWithLang(<StatusBadge status="warning" />); + // theme.warning in zh is '告警' + expect(screen.getByText('告警')).toBeInTheDocument(); + }); + + it('renders the translated label for error status', () => { + renderWithLang(<StatusBadge status="error" />); + // theme.error in zh is '异常' + expect(screen.getByText('异常')).toBeInTheDocument(); + }); + + it('renders the translated label for offline status', () => { + renderWithLang(<StatusBadge status="offline" />); + expect(screen.getByText('离线')).toBeInTheDocument(); + }); + + it('renders the translated label for connecting status', () => { + renderWithLang(<StatusBadge status="connecting" />); + expect(screen.getByText('连接中')).toBeInTheDocument(); + }); + + it('falls back to offline config for unknown status', () => { + renderWithLang(<StatusBadge status="unknown_status" />); + // Should render offline label + expect(screen.getByText('离线')).toBeInTheDocument(); + }); + + it('uses custom text when provided', () => { + renderWithLang(<StatusBadge status="healthy" text="自定义" />); + expect(screen.getByText('自定义')).toBeInTheDocument(); + }); + + it('hides the dot when showDot is false', () => { + const { container } = renderWithLang(<StatusBadge status="healthy" showDot={false} />); + // No Badge component should be rendered + const badges = container.querySelectorAll('.ant-badge'); + expect(badges.length).toBe(0); + }); + + it('has role=status for accessibility', () => { + const { container } = renderWithLang(<StatusBadge status="healthy" />); + const statusEl = container.querySelector('[role="status"]'); + expect(statusEl).toBeInTheDocument(); + }); +}); diff --git a/web/src/constants/__tests__/theme.test.ts b/web/src/constants/__tests__/theme.test.ts new file mode 100644 index 00000000..c3945f0d --- /dev/null +++ b/web/src/constants/__tests__/theme.test.ts @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from 'vitest'; +import { STATUS_MAP, CLUSTER_TYPE_MAP, TOPIC_TYPE_MAP, PROTOCOL_MAP, THEME_COLORS } from '../theme'; + +describe('theme constants', () => { + describe('STATUS_MAP', () => { + it('has labelKey for all status entries', () => { + for (const [key, val] of Object.entries(STATUS_MAP)) { + expect(val.labelKey, `STATUS_MAP[${key}].labelKey should exist`).toBeDefined(); + expect(val.labelKey, `STATUS_MAP[${key}].labelKey should start with theme.`).toMatch( + /^theme\./, + ); + } + }); + + it('has color and dot for all status entries', () => { + for (const [key, val] of Object.entries(STATUS_MAP)) { + expect(val.color, `STATUS_MAP[${key}].color should exist`).toBeDefined(); + expect(val.dot, `STATUS_MAP[${key}].dot should exist`).toBeDefined(); + } + }); + + it('contains required status keys', () => { + expect(STATUS_MAP.healthy).toBeDefined(); + expect(STATUS_MAP.warning).toBeDefined(); + expect(STATUS_MAP.error).toBeDefined(); + expect(STATUS_MAP.offline).toBeDefined(); + expect(STATUS_MAP.connecting).toBeDefined(); + }); + }); + + describe('CLUSTER_TYPE_MAP', () => { + it('has labelKey for all cluster type entries', () => { + for (const [key, val] of Object.entries(CLUSTER_TYPE_MAP)) { + expect(val.labelKey, `CLUSTER_TYPE_MAP[${key}].labelKey should exist`).toBeDefined(); + expect(val.color, `CLUSTER_TYPE_MAP[${key}].color should exist`).toBeDefined(); + } + }); + + it('contains V4_DIRECT, V5_PROXY_LOCAL, V5_PROXY_CLUSTER', () => { + expect(CLUSTER_TYPE_MAP.V4_DIRECT).toBeDefined(); + expect(CLUSTER_TYPE_MAP.V5_PROXY_LOCAL).toBeDefined(); + expect(CLUSTER_TYPE_MAP.V5_PROXY_CLUSTER).toBeDefined(); + }); + }); + + describe('TOPIC_TYPE_MAP', () => { + it('has labelKey for all topic type entries', () => { + for (const [key, val] of Object.entries(TOPIC_TYPE_MAP)) { + expect(val.labelKey, `TOPIC_TYPE_MAP[${key}].labelKey should exist`).toBeDefined(); + expect(val.color, `TOPIC_TYPE_MAP[${key}].color should exist`).toBeDefined(); + } + }); + + it('contains NORMAL, FIFO, DELAY, TRANSACTION, LITE', () => { + expect(TOPIC_TYPE_MAP.NORMAL).toBeDefined(); + expect(TOPIC_TYPE_MAP.FIFO).toBeDefined(); + expect(TOPIC_TYPE_MAP.DELAY).toBeDefined(); + expect(TOPIC_TYPE_MAP.TRANSACTION).toBeDefined(); + expect(TOPIC_TYPE_MAP.LITE).toBeDefined(); + }); + }); + + describe('PROTOCOL_MAP', () => { + it('has labelKey for all protocol entries', () => { + for (const [key, val] of Object.entries(PROTOCOL_MAP)) { + expect(val.labelKey, `PROTOCOL_MAP[${key}].labelKey should exist`).toBeDefined(); + expect(val.color, `PROTOCOL_MAP[${key}].color should exist`).toBeDefined(); + } + }); + }); + + describe('THEME_COLORS', () => { + it('contains primary colors', () => { + expect(THEME_COLORS.primary).toBeDefined(); + expect(THEME_COLORS.success).toBeDefined(); + expect(THEME_COLORS.warning).toBeDefined(); + expect(THEME_COLORS.error).toBeDefined(); + }); + }); +}); diff --git a/web/src/constants/theme.ts b/web/src/constants/theme.ts index 2cedb9bc..58ff5406 100644 --- a/web/src/constants/theme.ts +++ b/web/src/constants/theme.ts @@ -29,29 +29,41 @@ export const THEME_COLORS = { clusterV5Cluster: '#722ed1', } as const; -export const CLUSTER_TYPE_MAP: Record<string, { label: string; color: TagProps['color'] }> = { - V4_DIRECT: { label: 'V4 直连', color: 'orange' }, - V5_PROXY_LOCAL: { label: 'V5 Proxy 单节点', color: 'blue' }, - V5_PROXY_CLUSTER: { label: 'V5 Proxy 集群', color: 'purple' }, +/** + * Cluster type map — labels are i18n keys, resolved at render time via t(). + */ +export const CLUSTER_TYPE_MAP: Record<string, { labelKey: string; color: TagProps['color'] }> = { + V4_DIRECT: { labelKey: 'theme.clusterV4', color: 'orange' }, + V5_PROXY_LOCAL: { labelKey: 'theme.clusterV5Local', color: 'blue' }, + V5_PROXY_CLUSTER: { labelKey: 'theme.clusterV5Cluster', color: 'purple' }, }; -export const STATUS_MAP: Record<string, { label: string; color: string; dot: string }> = { - healthy: { label: '运行中', color: '#52c41a', dot: '#52c41a' }, - warning: { label: '告警', color: '#faad14', dot: '#faad14' }, - error: { label: '异常', color: '#ff4d4f', dot: '#ff4d4f' }, - offline: { label: '离线', color: '#d9d9d9', dot: '#d9d9d9' }, - connecting: { label: '连接中', color: '#1677ff', dot: '#1677ff' }, +/** + * Status map — labels are i18n keys, resolved at render time via t(). + */ +export const STATUS_MAP: Record<string, { labelKey: string; color: string; dot: string }> = { + healthy: { labelKey: 'theme.healthy', color: '#52c41a', dot: '#52c41a' }, + warning: { labelKey: 'theme.warning', color: '#faad14', dot: '#faad14' }, + error: { labelKey: 'theme.error', color: '#ff4d4f', dot: '#ff4d4f' }, + offline: { labelKey: 'theme.offline', color: '#d9d9d9', dot: '#d9d9d9' }, + connecting: { labelKey: 'theme.connecting', color: '#1677ff', dot: '#1677ff' }, }; -export const TOPIC_TYPE_MAP: Record<string, { label: string; color: TagProps['color'] }> = { - NORMAL: { label: '普通', color: 'default' }, - FIFO: { label: '顺序', color: 'blue' }, - DELAY: { label: '延迟', color: 'orange' }, - TRANSACTION: { label: '事务', color: 'purple' }, - LITE: { label: 'LiteTopic', color: 'magenta' }, +/** + * Topic type map — labels are i18n keys, resolved at render time via t(). + */ +export const TOPIC_TYPE_MAP: Record<string, { labelKey: string; color: TagProps['color'] }> = { + NORMAL: { labelKey: 'theme.topicNormal', color: 'default' }, + FIFO: { labelKey: 'theme.topicFifo', color: 'blue' }, + DELAY: { labelKey: 'theme.topicDelay', color: 'orange' }, + TRANSACTION: { labelKey: 'theme.topicTransaction', color: 'purple' }, + LITE: { labelKey: 'theme.topicLite', color: 'magenta' }, }; -export const PROTOCOL_MAP: Record<string, { label: string; color: TagProps['color'] }> = { - REMOTING: { label: 'Remoting', color: 'geekblue' }, - GRPC: { label: 'gRPC', color: 'green' }, +/** + * Protocol map — labels are i18n keys, resolved at render time via t(). + */ +export const PROTOCOL_MAP: Record<string, { labelKey: string; color: TagProps['color'] }> = { + REMOTING: { labelKey: 'theme.protocolRemoting', color: 'geekblue' }, + GRPC: { labelKey: 'theme.protocolGrpc', color: 'green' }, }; diff --git a/web/src/i18n/__tests__/LangContext.test.tsx b/web/src/i18n/__tests__/LangContext.test.tsx index 1e3ac571..5a81cab8 100644 --- a/web/src/i18n/__tests__/LangContext.test.tsx +++ b/web/src/i18n/__tests__/LangContext.test.tsx @@ -119,4 +119,4 @@ describe('LangContext', () => { ); expect(screen.getByTestId('alias-lang')).toHaveTextContent('zh'); }); -}); \ No newline at end of file +}); diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index dd619874..3b37ff10 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -87,6 +87,14 @@ const translations: Record<string, Record<Lang, string>> = { 'dashboard.group': { zh: 'Group', en: 'Group' }, 'dashboard.trend': { zh: '趋势', en: 'Trend' }, 'dashboard.millionMessages': { zh: '约 {n}M 条消息', en: '~{n}M messages' }, + 'dashboard.tpsTrend': { zh: 'TPS 趋势', en: 'TPS Trend' }, + 'dashboard.tpsInLabel': { zh: 'TPS In', en: 'TPS In' }, + 'dashboard.tpsOutLabel': { zh: 'TPS Out', en: 'TPS Out' }, + 'dashboard.brokers': { zh: '{n} Broker', en: '{n} Brokers' }, + 'dashboard.proxies': { zh: '{n} Proxy', en: '{n} Proxies' }, + 'dashboard.consumerGroups': { zh: '{n} 消费组', en: '{n} Groups' }, + 'dashboard.healthy': { zh: '健康', en: 'Healthy' }, + 'dashboard.last12h': { zh: '近 12 小时', en: 'Last 12 hours' }, // ─── Cluster Page ─── 'cluster.title': { zh: 'RocketMQ 集群', en: 'RocketMQ Cluster' }, @@ -332,10 +340,276 @@ const translations: Record<string, Record<Lang, string>> = { 'acl.required': { zh: '请选择{field}', en: 'Please select {field}' }, 'acl.inputRequired': { zh: '请输入{field}', en: 'Please enter {field}' }, + // ─── Topic Page (unique keys, duplicates merged into Topic section below) ─── + 'topic.name': { zh: 'Topic 名称', en: 'Topic Name' }, + 'topic.action': { zh: '操作', en: 'Actions' }, + 'topic.detail': { zh: '详情', en: 'Detail' }, + 'topic.route': { zh: '路由', en: 'Route' }, + 'topic.send': { zh: '发送', en: 'Send' }, + 'topic.delete': { zh: '删除', en: 'Delete' }, + 'topic.deleteContent': { + zh: '确定要删除 Topic「{name}」吗?此操作不可撤销。', + en: 'Are you sure to delete Topic "{name}"? This cannot be undone.', + }, + 'topic.allTypes': { zh: '全部类型', en: 'All Types' }, + 'topic.normal': { zh: '普通', en: 'Normal' }, + 'topic.fifo': { zh: '顺序', en: 'FIFO' }, + 'topic.delay': { zh: '延迟', en: 'Delay' }, + 'topic.transaction': { zh: '事务', en: 'Transaction' }, + 'topic.lite': { zh: 'LiteTopic', en: 'LiteTopic' }, + 'topic.batchDelete': { zh: '批量删除', en: 'Batch Delete' }, + 'topic.bodyOrder': { zh: '订单事件', en: 'Order Event' }, + 'topic.bodyUserEvent': { zh: '用户行为', en: 'User Event' }, + 'topic.bodyPayment': { zh: '支付回调', en: 'Payment Callback' }, + 'topic.bodyInventory': { zh: '库存变更', en: 'Inventory Change' }, + 'topic.bodyNotification': { zh: '通知消息', en: 'Notification' }, + 'topic.bodyMetrics': { zh: '监控指标', en: 'Metrics' }, + 'topic.randomBody': { zh: '随机消息体', en: 'Random Body' }, + 'topic.sendTestMessage': { zh: '发送测试消息', en: 'Send Test Message' }, + 'topic.tag': { zh: 'Tag', en: 'Tag' }, + 'topic.key': { zh: 'Key', en: 'Key' }, + 'topic.messageBody': { zh: '消息体', en: 'Message Body' }, + 'topic.properties': { zh: '属性', en: 'Properties' }, + 'topic.topicConfig': { zh: 'Topic 配置', en: 'Topic Config' }, + 'topic.queueCount': { zh: '队列数', en: 'Queue Count' }, + + // ─── Consumer Page ─── + 'consumer.name': { zh: 'Group 名称', en: 'Group Name' }, + 'consumer.subType': { zh: '订阅组类型', en: 'Sub Type' }, + 'consumer.subMode': { zh: '订阅模式', en: 'Sub Mode' }, + 'consumer.onlineClients': { zh: '在线客户端', en: 'Online Clients' }, + 'consumer.totalLag': { zh: '总堆积量', en: 'Total Lag' }, + 'consumer.delay': { zh: '消费延迟', en: 'Consume Delay' }, + 'consumer.createdAt': { zh: '创建时间', en: 'Created' }, + 'consumer.updatedAt': { zh: '修改时间', en: 'Updated' }, + 'consumer.action': { zh: '操作', en: 'Actions' }, + 'consumer.detail': { zh: '详情', en: 'Detail' }, + 'consumer.resetOffset': { zh: '重置位点', en: 'Reset Offset' }, + 'consumer.confirmDelete': { + zh: '确认删除消费组 "{name}"?', + en: 'Delete consumer group "{name}"?', + }, + 'consumer.deleteWarning': { + zh: '删除后该消费组的所有配置和消费进度将被清除,此操作不可恢复。', + en: 'All configurations and progress will be removed. This cannot be undone.', + }, + 'consumer.deleted': { zh: '消费组 {name} 已删除', en: 'Consumer group {name} deleted' }, + 'consumer.searchPlaceholder': { zh: '搜索 Group 名称或 Topic', en: 'Search group name or topic' }, + 'consumer.allModes': { zh: '全部模式', en: 'All Modes' }, + 'consumer.push': { zh: 'Push', en: 'Push' }, + 'consumer.pull': { zh: 'Pull', en: 'Pull' }, + 'consumer.sortNameAsc': { zh: '名称升序', en: 'Name A-Z' }, + 'consumer.sortLagDesc': { zh: '堆积量降序', en: 'Lag Descending' }, + 'consumer.createGroup': { zh: '新建消费组', en: 'Create Group' }, + 'consumer.subTopic': { zh: 'Topic 主题', en: 'Topic' }, + 'consumer.consistency': { zh: '订阅一致性', en: 'Consistency' }, + 'consumer.consistent': { zh: '一致', en: 'Consistent' }, + 'consumer.inconsistent': { zh: '不一致', en: 'Inconsistent' }, + 'consumer.filterMode': { zh: '订阅模式', en: 'Filter Mode' }, + 'consumer.filterAll': { zh: '全量', en: 'Full' }, + 'consumer.filterTag': { zh: 'Tag 过滤', en: 'Tag Filter' }, + 'consumer.filterSql92': { zh: 'SQL92 过滤', en: 'SQL92 Filter' }, + 'consumer.expression': { zh: '订阅表达式', en: 'Expression' }, + 'consumer.viewDistribution': { zh: '查看分布', en: 'View Distribution' }, + 'consumer.resetToTime': { zh: '重置到指定时间', en: 'Reset to Time' }, + 'consumer.resetSuccess': { zh: '位点重置成功', en: 'Offset reset successfully' }, + + // ─── Home Page (additional) ─── + 'home.banner': { + zh: 'RocketMQ Studio — 跨集群 · 跨架构 · 跨云的统一管控平台', + en: 'RocketMQ Studio — Cross-cluster · Cross-arch · Cross-cloud unified management', + }, + 'home.placeholder': { + zh: '向 RocketMQ Bot 提问,全程加密、安全、可信', + en: 'Ask RocketMQ Bot, fully encrypted, secure, trusted', + }, + 'home.tools': { zh: '工具', en: 'Tools' }, + 'home.promptEnhance': { zh: 'Prompt 增强', en: 'Prompt Enhance' }, + 'home.docs': { zh: '文档中心', en: 'Documentation' }, + 'home.community': { zh: 'RocketMQ 社区', en: 'RocketMQ Community' }, + 'home.brand': { zh: 'RocketMQ Studio 出品', en: 'Powered by RocketMQ Studio' }, + + // ─── AI Page (additional) ─── + 'ai.recommended': { zh: '推荐', en: 'Rec.' }, + + // ─── Theme Constants ─── + 'theme.clusterV4': { zh: 'V4 直连', en: 'V4 Direct' }, + 'theme.clusterV5Local': { zh: 'V5 Proxy 单节点', en: 'V5 Proxy Local' }, + 'theme.clusterV5Cluster': { zh: 'V5 Proxy 集群', en: 'V5 Proxy Cluster' }, + 'theme.healthy': { zh: '运行中', en: 'Healthy' }, + 'theme.warning': { zh: '告警', en: 'Warning' }, + 'theme.error': { zh: '异常', en: 'Error' }, + 'theme.offline': { zh: '离线', en: 'Offline' }, + 'theme.connecting': { zh: '连接中', en: 'Connecting' }, + 'theme.topicNormal': { zh: '普通', en: 'Normal' }, + 'theme.topicFifo': { zh: '顺序', en: 'FIFO' }, + 'theme.topicDelay': { zh: '延迟', en: 'Delay' }, + 'theme.topicTransaction': { zh: '事务', en: 'Transaction' }, + 'theme.topicLite': { zh: 'LiteTopic', en: 'LiteTopic' }, + 'theme.protocolRemoting': { zh: 'Remoting', en: 'Remoting' }, + 'theme.protocolGrpc': { zh: 'gRPC', en: 'gRPC' }, + // ─── Settings ─── 'settings.title': { zh: '设置', en: 'Settings' }, 'settings.subtitle': { zh: '管理应用配置与数据源', en: 'Manage app settings and data sources' }, + // ─── Certs ─── + 'cert.clusterName': { zh: 'K8s 集群名称', en: 'K8s Cluster Name' }, + 'cert.certName': { zh: '证书名称', en: 'Certificate Name' }, + 'cert.issuer': { zh: '签发者', en: 'Issuer' }, + 'cert.expiryTime': { zh: '到期时间', en: 'Expiry Time' }, + 'cert.daysRemaining': { zh: '剩余天数', en: 'Days Remaining' }, + 'cert.statusValid': { zh: '有效', en: 'Valid' }, + 'cert.statusExpiring': { zh: '即将过期', en: 'Expiring' }, + 'cert.statusExpired': { zh: '已过期', en: 'Expired' }, + 'cert.confirmDelete': { zh: '确认删除', en: 'Confirm Delete' }, + 'cert.deleteConfirm': { + zh: '确定要删除证书"{name}"吗?', + en: 'Are you sure to delete certificate "{name}"?', + }, + 'cert.deleted': { zh: '证书已删除: {name}', en: 'Certificate deleted: {name}' }, + 'cert.addCert': { zh: '添加证书', en: 'Add Certificate' }, + 'cert.searchPlaceholder': { zh: '搜索证书名称或集群', en: 'Search cert name or cluster' }, + 'cert.editCert': { zh: '编辑证书 — {name}', en: 'Edit Certificate — {name}' }, + 'cert.certUpdated': { zh: '证书「{name}」已更新', en: 'Certificate "{name}" updated' }, + 'cert.namespace': { zh: '命名空间', en: 'Namespace' }, + 'cert.issuerPlaceholder': { zh: '例:kubernetes-ca', en: 'e.g. kubernetes-ca' }, + 'cert.namespacePlaceholder': { zh: '例:kube-system', en: 'e.g. kube-system' }, + 'cert.featureWip': { zh: '添加证书功能开发中', en: 'Add certificate feature is in development' }, + 'cert.totalCount': { zh: '共 {count} 个证书', en: '{count} certificates total' }, + + // ─── Cluster ─── + 'cluster.brokerCount': { zh: 'Broker 数', en: 'Broker Count' }, + 'cluster.topicCount': { zh: 'Topic 数', en: 'Topic Count' }, + 'cluster.messageCount': { zh: '消息总量', en: 'Message Total' }, + 'cluster.totalTps': { zh: '总 TPS', en: 'Total TPS' }, + 'cluster.avgTps': { zh: '平均 TPS', en: 'Avg TPS' }, + 'cluster.consumerGroupCount': { zh: '消费者组数', en: 'Consumer Group Count' }, + 'cluster.createClusterWip': { + zh: '新建集群功能开发中', + en: 'Create cluster feature is in development', + }, + 'cluster.configTitle': { zh: '配置 - {name}', en: 'Config - {name}' }, + 'cluster.configUpdated': { zh: '配置已更新', en: 'Configuration updated' }, + 'cluster.flushDiskType': { zh: '刷盘方式', en: 'Flush Disk Type' }, + 'cluster.syncFlush': { zh: '同步刷盘', en: 'Sync Flush' }, + 'cluster.asyncFlush': { zh: '异步刷盘', en: 'Async Flush' }, + 'cluster.autoCreateTopic': { zh: '自动创建 Topic', en: 'Auto Create Topic' }, + 'cluster.autoCreateSubGroup': { zh: '自动创建订阅组', en: 'Auto Create Subscription Group' }, + 'cluster.maxMessageSize': { zh: '最大消息大小 (MB)', en: 'Max Message Size (MB)' }, + 'cluster.fileReservedTime': { zh: '文件保留时长 (小时)', en: 'File Retention Time (hours)' }, + 'cluster.writeQueues': { zh: '写队列数', en: 'Write Queues' }, + 'cluster.readQueues': { zh: '读队列数', en: 'Read Queues' }, + 'cluster.brokerPermission': { zh: 'Broker 权限', en: 'Broker Permission' }, + 'cluster.viewDetail': { zh: '查看详情: {addr}', en: 'View detail: {addr}' }, + 'cluster.restartProxyConfirm': { + zh: '确定要重启 Proxy "{addr}" 吗?', + en: 'Are you sure to restart Proxy "{addr}"?', + }, + 'cluster.restartProxySubmitted': { + zh: 'Proxy 重启已提交: {addr}', + en: 'Proxy restart submitted: {addr}', + }, + + // ─── Topic ─── + 'topic.type': { zh: '类型', en: 'Type' }, + 'topic.status': { zh: '状态', en: 'Status' }, + 'topic.serving': { zh: '服务中', en: 'Serving' }, + 'topic.createdAt': { zh: '创建时间', en: 'Created At' }, + 'topic.updatedAt': { zh: '修改时间', en: 'Updated At' }, + 'topic.topicName': { zh: 'Topic 名称', en: 'Topic Name' }, + 'topic.namespace': { zh: '命名空间', en: 'Namespace' }, + 'topic.cluster': { zh: '集群', en: 'Cluster' }, + 'topic.writeQueues': { zh: '写队列数', en: 'Write Queues' }, + 'topic.readQueues': { zh: '读队列数', en: 'Read Queues' }, + 'topic.perm': { zh: '权限', en: 'Permission' }, + 'topic.messageCount': { zh: '今日消息量', en: "Today's Messages" }, + 'topic.tps': { zh: 'TPS', en: 'TPS' }, + 'topic.consumerGroupCount': { zh: '消费者组数', en: 'Consumer Groups' }, + 'topic.quickFill': { zh: '快速填入:', en: 'Quick fill:' }, + 'topic.customProps': { zh: '自定义属性(可选)', en: 'Custom Properties (optional)' }, + 'topic.allNamespaces': { zh: '全部', en: 'All' }, + 'topic.remark': { zh: '备注', en: 'Remark' }, + 'topic.confirmDelete': { zh: '确认删除', en: 'Confirm Delete' }, + 'topic.deleteConfirm': { + zh: '确定要删除 Topic「{name}」吗?此操作不可撤销。', + en: 'Are you sure to delete Topic "{name}"? This action cannot be undone.', + }, + 'topic.deleted': { zh: 'Topic「{name}」已删除', en: 'Topic "{name}" deleted' }, + 'topic.permRW': { zh: '读写', en: 'Read/Write' }, + 'topic.permRO': { zh: '只读', en: 'Read-only' }, + 'topic.permWO': { zh: '只写', en: 'Write-only' }, + 'topic.brokerName': { zh: 'Broker 名称', en: 'Broker Name' }, + 'topic.brokerAddr': { zh: 'Broker 地址', en: 'Broker Address' }, + 'topic.writeQueue': { zh: '写队列', en: 'Write Queue' }, + 'topic.readQueue': { zh: '读队列', en: 'Read Queue' }, + 'topic.consumerGroup': { zh: '消费者组', en: 'Consumer Group' }, + 'topic.consumeMode': { zh: '消费模式', en: 'Consume Mode' }, + 'topic.consumeTps': { zh: '消费 TPS', en: 'Consume TPS' }, + 'topic.backlog': { zh: '堆积量', en: 'Backlog' }, + 'topic.broadcast': { zh: '广播消费', en: 'Broadcast' }, + 'topic.clustering': { zh: '集群消费', en: 'Clustering' }, + 'topic.basicInfo': { zh: '基本信息', en: 'Basic Info' }, + 'topic.routeInfo': { zh: '路由信息', en: 'Route Info' }, + 'topic.consumerInfo': { zh: '消费者', en: 'Consumers' }, + 'topic.createTopic': { zh: '创建 Topic', en: 'Create Topic' }, + 'topic.createSuccess': { + zh: 'Topic「{name}」创建成功', + en: 'Topic "{name}" created successfully', + }, + 'topic.topicNamePlaceholder': { zh: '请输入 Topic 名称', en: 'Enter topic name' }, + 'topic.topicNameRule': { + zh: '仅支持字母、数字、下划线、中划线、斜杠和星号', + en: 'Only letters, numbers, underscore, hyphen, slash and asterisk', + }, + 'topic.topicNameRequired': { zh: '请输入 Topic 名称', en: 'Please enter topic name' }, + 'topic.queueExtra': { zh: '每个 Broker 节点 8 个队列', en: '8 queues per Broker node' }, + 'topic.remarkPlaceholder': { + zh: '可选,描述 Topic 用途', + en: 'Optional, describe the topic purpose', + }, + 'topic.sendMsg': { zh: '发送消息到 {name}', en: 'Send message to {name}' }, + 'topic.sendSuccess': { + zh: '消息发送成功!MsgId: {id}', + en: 'Message sent successfully! MsgId: {id}', + }, + 'topic.tagPlaceholder': { zh: '可选,消息标签', en: 'Optional, message tag' }, + 'topic.keyPlaceholder': { + zh: '可选,消息 Key(用于查询)', + en: 'Optional, message key (for query)', + }, + 'topic.bodyLabel': { zh: '消息体 Body', en: 'Message Body' }, + 'topic.bodyRequired': { zh: '请输入消息体', en: 'Please enter message body' }, + 'topic.bodyPlaceholder': { zh: 'JSON 格式消息体', en: 'JSON format message body' }, + 'topic.propName': { zh: '属性名', en: 'Property Name' }, + 'topic.propValue': { zh: '属性值', en: 'Property Value' }, + 'topic.addProp': { zh: '添加属性', en: 'Add Property' }, + 'topic.confirmBatchDelete': { zh: '确认批量删除', en: 'Confirm Batch Delete' }, + 'topic.batchDeleteConfirm': { + zh: '确定要删除选中的 {count} 个 Topic 吗?此操作不可撤销。', + en: 'Are you sure to delete {count} selected topics? This action cannot be undone.', + }, + 'topic.batchDeleted': { zh: '已删除 {count} 个 Topic', en: '{count} topics deleted' }, + 'topic.importWip': { zh: '导入功能开发中', en: 'Import feature is in development' }, + 'topic.exported': { zh: '已导出 {count} 个 Topic', en: '{count} topics exported' }, + 'topic.totalCount': { zh: '共 {count} 个 Topic', en: '{count} topics total' }, + 'topic.searchPlaceholder': { zh: '搜索 Topic 名称', en: 'Search topic name' }, + 'topic.typeFilter': { zh: '类型筛选', en: 'Type filter' }, + 'topic.listView': { zh: '列表', en: 'List' }, + 'topic.cardView': { zh: '卡片', en: 'Card' }, + 'topic.showTotal': { zh: '共 {total} 条', en: '{total} total' }, + 'topic.orderEvent': { zh: '订单事件', en: 'Order Event' }, + 'topic.userEvent': { zh: '用户行为', en: 'User Event' }, + 'topic.paymentCallback': { zh: '支付回调', en: 'Payment Callback' }, + 'topic.inventoryChange': { zh: '库存变更', en: 'Inventory Change' }, + 'topic.notification': { zh: '通知消息', en: 'Notification' }, + 'topic.metrics': { zh: '监控指标', en: 'Metrics' }, + + // ─── Consumer ─── + 'consumer.subscriptionMode': { zh: '订阅模式', en: 'Subscription Mode' }, + 'consumer.subGroupType': { zh: '订阅组类型', en: 'Subscription Group Type' }, + 'consumer.maxRetry': { zh: '最大重试次数', en: 'Max Retries' }, + // ─── User Menu ─── 'user.profile': { zh: '个人中心', en: 'Profile' }, 'user.logout': { zh: '退出登录', en: 'Logout' }, @@ -412,18 +686,9 @@ const translations: Record<string, Record<Lang, string>> = { 'topic.add': { zh: '添加主题', en: 'Add Topic' }, 'topic.config': { zh: '主题配置', en: 'Topic Config' }, 'topic.change': { zh: '修改主题', en: 'Modify Topic' }, - 'topic.name': { zh: '主题名', en: 'Topic Name' }, - 'topic.type': { zh: '消息类型', en: 'Message Type' }, - 'topic.normal': { zh: '普通消息', en: 'Normal' }, - 'topic.fifo': { zh: '顺序消息', en: 'FIFO' }, - 'topic.delay': { zh: '定时/延时消息', en: 'Delay' }, - 'topic.transaction': { zh: '事务消息', en: 'Transaction' }, 'topic.unspecified': { zh: '未指定', en: 'Unspecified' }, - 'topic.perm': { zh: '权限', en: 'Permission' }, 'topic.readQueueNums': { zh: '读队列数量', en: 'Read Queue Num' }, 'topic.writeQueueNums': { zh: '写队列数量', en: 'Write Queue Num' }, - 'topic.brokerName': { zh: 'Broker 名称', en: 'Broker Name' }, - 'topic.brokerAddr': { zh: 'Broker 地址', en: 'Broker Address' }, 'topic.clusterName': { zh: '集群名', en: 'Cluster Name' }, 'topic.selectCluster': { zh: '请选择集群', en: 'Select Cluster' }, 'topic.selectBroker': { zh: '请选择 Broker', en: 'Select Broker' }, @@ -431,15 +696,11 @@ const translations: Record<string, Record<Lang, string>> = { 'topic.minOffset': { zh: '最小位点', en: 'Min Offset' }, 'topic.maxOffset': { zh: '最大位点', en: 'Max Offset' }, 'topic.lastUpdateTime': { zh: '上次更新时间', en: 'Last Update Time' }, - 'topic.sendMsg': { zh: '发送消息', en: 'Send Message' }, 'topic.resetOffset': { zh: '重置位点', en: 'Reset Offset' }, 'topic.skipAccumulate': { zh: '跳过堆积', en: 'Skip Accumulate' }, - 'topic.delete': { zh: '删除主题', en: 'Delete Topic' }, - 'topic.confirmDelete': { zh: '确认删除此主题?', en: 'Delete this topic?' }, 'topic.deleteWarning': { zh: '删除后无法恢复,请确认。', en: 'This cannot be undone.' }, 'topic.fetchFailed': { zh: '获取主题列表失败', en: 'Failed to fetch topic list' }, 'topic.operationSuccess': { zh: 'Topic 操作成功', en: 'Topic operation successful' }, - 'topic.searchPlaceholder': { zh: '搜索主题名称', en: 'Search topic name' }, 'topic.filterType': { zh: '消息类型', en: 'Message Type' }, 'topic.filterAll': { zh: '全部类型', en: 'All Types' }, diff --git a/web/src/pages/cluster/index.tsx b/web/src/pages/cluster/index.tsx index f22ca74a..5fdcf63e 100644 --- a/web/src/pages/cluster/index.tsx +++ b/web/src/pages/cluster/index.tsx @@ -363,7 +363,7 @@ const ClusterPage = () => { <Button type="primary" icon={<PlusOutlined />} - onClick={() => message.info('新建集群功能开发中')} + onClick={() => message.info(t('cluster.createClusterWip'))} > {t('cluster.createCluster')} </Button> @@ -380,51 +380,51 @@ const ClusterPage = () => { {selectedCluster && ( <Modal - title={`配置 - ${selectedCluster.name}`} + title={t('cluster.configTitle', { name: selectedCluster.name })} open={configModalOpen} onCancel={() => setConfigModalOpen(false)} onOk={() => { configForm.validateFields().then(() => { - message.success('配置已更新'); + message.success(t('cluster.configUpdated')); setConfigModalOpen(false); }); }} width={560} > <Form form={configForm} layout="vertical"> - <Form.Item label="刷盘方式" name="flushDiskType"> + <Form.Item label={t('cluster.flushDiskType')} name="flushDiskType"> <Radio.Group> - <Radio value="SYNC_FLUSH">同步刷盘</Radio> - <Radio value="ASYNC_FLUSH">异步刷盘</Radio> + <Radio value="SYNC_FLUSH">{t('cluster.syncFlush')}</Radio> + <Radio value="ASYNC_FLUSH">{t('cluster.asyncFlush')}</Radio> </Radio.Group> </Form.Item> <Form.Item - label="自动创建 Topic" + label={t('cluster.autoCreateTopic')} name="autoCreateTopicEnable" valuePropName="checked" > <Switch /> </Form.Item> <Form.Item - label="自动创建订阅组" + label={t('cluster.autoCreateSubGroup')} name="autoCreateSubscriptionGroup" valuePropName="checked" > <Switch /> </Form.Item> - <Form.Item label="最大消息大小 (MB)" name="maxMessageSizeMB"> + <Form.Item label={t('cluster.maxMessageSize')} name="maxMessageSizeMB"> <InputNumber min={1} max={128} style={{ width: '100%' }} /> </Form.Item> - <Form.Item label="文件保留时长 (小时)" name="fileReservedTime"> + <Form.Item label={t('cluster.fileReservedTime')} name="fileReservedTime"> <InputNumber min={1} max={720} style={{ width: '100%' }} /> </Form.Item> - <Form.Item label="写队列数" name="writeQueueNums"> + <Form.Item label={t('cluster.writeQueues')} name="writeQueueNums"> <InputNumber min={1} max={256} style={{ width: '100%' }} /> </Form.Item> - <Form.Item label="读队列数" name="readQueueNums"> + <Form.Item label={t('cluster.readQueues')} name="readQueueNums"> <InputNumber min={1} max={256} style={{ width: '100%' }} /> </Form.Item> - <Form.Item label="Broker 权限" name="brokerPermission"> + <Form.Item label={t('cluster.brokerPermission')} name="brokerPermission"> <InputNumber min={0} max={7} style={{ width: '100%' }} /> </Form.Item> </Form> @@ -468,9 +468,9 @@ const ClusterPage = () => { render: (status: string) => { const map: Record<string, { color: string; label: string }> = { healthy: { color: 'green', label: t('cluster.running') }, - warning: { color: 'gold', label: '告警' }, - error: { color: 'red', label: '异常' }, - offline: { color: 'default', label: '离线' }, + warning: { color: 'gold', label: t('cluster.warning') }, + error: { color: 'red', label: t('cluster.error') }, + offline: { color: 'default', label: t('cluster.offline') }, }; const cfg = map[status] ?? { color: 'default', label: status }; return <Tag color={cfg.color}>{cfg.label}</Tag>; @@ -530,9 +530,9 @@ const ClusterPage = () => { render: (status: string) => { const map: Record<string, { color: string; label: string }> = { healthy: { color: 'green', label: t('cluster.running') }, - warning: { color: 'gold', label: '告警' }, - error: { color: 'red', label: '异常' }, - offline: { color: 'default', label: '离线' }, + warning: { color: 'gold', label: t('cluster.warning') }, + error: { color: 'red', label: t('cluster.error') }, + offline: { color: 'default', label: t('cluster.offline') }, }; const cfg = map[status] ?? { color: 'default', label: status }; return <Tag color={cfg.color}>{cfg.label}</Tag>; @@ -661,9 +661,9 @@ const ClusterPage = () => { render: (status: string) => { const map: Record<string, { color: string; label: string }> = { healthy: { color: 'green', label: t('cluster.running') }, - warning: { color: 'gold', label: '告警' }, - error: { color: 'red', label: '异常' }, - offline: { color: 'default', label: '离线' }, + warning: { color: 'gold', label: t('cluster.warning') }, + error: { color: 'red', label: t('cluster.error') }, + offline: { color: 'default', label: t('cluster.offline') }, }; const cfg = map[status] ?? { color: 'default', label: status }; return <Tag color={cfg.color}>{cfg.label}</Tag>; @@ -704,7 +704,7 @@ const ClusterPage = () => { size="small" icon={<EyeOutlined />} style={{ borderColor: '#1677ff', color: '#1677ff' }} - onClick={() => message.info(`查看详情: ${record.addr}`)} + onClick={() => message.info(t('cluster.viewDetail', { addr: record.addr }))} > {t('common.detail')} </Button> @@ -715,10 +715,11 @@ const ClusterPage = () => { onClick={() => { Modal.confirm({ title: t('cluster.confirmRestart'), - content: `确定要重启 Proxy "${record.addr}" 吗?`, - okText: '确认', - cancelText: '取消', - onOk: () => message.success(`Proxy 重启已提交: ${record.addr}`), + content: t('cluster.restartProxyConfirm', { addr: record.addr }), + okText: t('common.confirm'), + cancelText: t('common.cancel'), + onOk: () => + message.success(t('cluster.restartProxySubmitted', { addr: record.addr })), }); }} > @@ -744,7 +745,7 @@ const ClusterPage = () => { <Button type="primary" icon={<PlusOutlined />} - onClick={() => message.info('新建集群功能开发中')} + onClick={() => message.info(t('cluster.createClusterWip'))} > {t('cluster.createCluster')} </Button> diff --git a/web/src/pages/home/dashboard.tsx b/web/src/pages/home/dashboard.tsx index 349b7250..1242fca6 100644 --- a/web/src/pages/home/dashboard.tsx +++ b/web/src/pages/home/dashboard.tsx @@ -123,7 +123,7 @@ const DashboardPage = () => { key: 'type', render: (type: string) => { const info = CLUSTER_TYPE_MAP[type]; - return info ? <Tag color={info.color}>{info.label}</Tag> : type; + return info ? <Tag color={info.color}>{t(info.labelKey)}</Tag> : type; }, }, { diff --git a/web/src/pages/instance/consumer.tsx b/web/src/pages/instance/consumer.tsx index 9e0ec7c3..d7cec904 100644 --- a/web/src/pages/instance/consumer.tsx +++ b/web/src/pages/instance/consumer.tsx @@ -177,8 +177,8 @@ const ConsumerPage = () => { width: 110, sorter: (a, b) => a.subscriptionDataType.localeCompare(b.subscriptionDataType), render: (type: string) => { - const config = TOPIC_TYPE_MAP[type] || { label: type, color: 'default' }; - return <Tag color={config.color}>{config.label}</Tag>; + const config = TOPIC_TYPE_MAP[type] || { labelKey: type, color: 'default' }; + return <Tag color={config.color}>{t(config.labelKey)}</Tag>; }, }, { @@ -374,8 +374,8 @@ const ConsumerPage = () => { key: 'protocol', width: 100, render: (protocol: string) => { - const config = PROTOCOL_MAP[protocol] || { label: protocol, color: 'default' }; - return <Tag color={config.color}>{config.label}</Tag>; + const config = PROTOCOL_MAP[protocol] || { labelKey: protocol, color: 'default' }; + return <Tag color={config.color}>{t(config.labelKey)}</Tag>; }, }, { @@ -710,8 +710,9 @@ const ConsumerPage = () => { TOPIC_TYPE_MAP[selectedGroup.subscriptionDataType]?.color || 'default' } > - {TOPIC_TYPE_MAP[selectedGroup.subscriptionDataType]?.label || - selectedGroup.subscriptionDataType} + {TOPIC_TYPE_MAP[selectedGroup.subscriptionDataType] + ? t(TOPIC_TYPE_MAP[selectedGroup.subscriptionDataType].labelKey) + : selectedGroup.subscriptionDataType} </Tag> </Descriptions.Item> <Descriptions.Item label="消费延迟"> diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx index fc3af2eb..4569bfac 100644 --- a/web/src/pages/instance/topic.tsx +++ b/web/src/pages/instance/topic.tsx @@ -349,7 +349,7 @@ const TopicPage = () => { sorter: (a, b) => a.type.localeCompare(b.type), render: (type: string) => { const cfg = TOPIC_TYPE_MAP[type]; - return cfg ? <Tag color={cfg.color}>{cfg.label}</Tag> : <Tag>{type}</Tag>; + return cfg ? <Tag color={cfg.color}>{t(cfg.labelKey)}</Tag> : <Tag>{type}</Tag>; }, }, { @@ -458,7 +458,9 @@ const TopicPage = () => { {topic.name} </Descriptions.Item> <Descriptions.Item label="类型"> - <Tag color={typeInfo?.color}>{typeInfo?.label}</Tag> + <Tag color={typeInfo?.color}> + {typeInfo?.labelKey ? t(typeInfo.labelKey) : topic.type} + </Tag> </Descriptions.Item> <Descriptions.Item label="命名空间"> <Tag>{topic.namespace}</Tag> @@ -466,7 +468,7 @@ const TopicPage = () => { <Descriptions.Item label="集群" span={2}> <Space> <Text>{topic.clusterId}</Text> - {clusterType && <Tag color={clusterType.color}>{clusterType.label}</Tag>} + {clusterType && <Tag color={clusterType.color}>{t(clusterType.labelKey)}</Tag>} </Space> </Descriptions.Item> <Descriptions.Item label="写队列数">{topic.writeQueues}</Descriptions.Item> @@ -506,7 +508,9 @@ const TopicPage = () => { <Text strong style={{ fontSize: 15 }}> {topic.name} </Text> - <Tag color={typeInfo?.color}>{typeInfo?.label}</Tag> + <Tag color={typeInfo?.color}> + {typeInfo?.labelKey ? t(typeInfo.labelKey) : topic.type} + </Tag> </Flex> {/* Namespace + cluster tags */} @@ -514,7 +518,7 @@ const TopicPage = () => { <Tag style={{ fontSize: 11 }}>{topic.namespace}</Tag> {clusterType && ( <Tag color={clusterType.color} style={{ fontSize: 11 }}> - {clusterType.label} + {t(clusterType.labelKey)} </Tag> )} </Space>
