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 3bcd14d45 fix(certs): localize the K8s certificate page (#4619)
3bcd14d45 is described below

commit 3bcd14d4552b1fa9f3a9142730f2c34409ed181a
Author: Apulupie <[email protected]>
AuthorDate: Mon Oct 19 19:57:05 2026 +0800

    fix(certs): localize the K8s certificate page (#4619)
    
    Verified against `rocketmq-studio` at c702d3010 before merging: the defect 
is still present on the
    current trunk and the change merges cleanly.
    
    Maintainer follow-ups on top of the contributor commit:
    - fix(certs): keep the localized load error without re-fetching on language 
change
    
    Each follow-up commit message records the review finding it addresses.
    
    Locally verified: targeted suites green on the integration branch; a full 
backend and frontend
    build plus test pass runs on the trunk once this batch lands.
---
 web/src/i18n/translations.ts                       | 49 +++++++++--
 .../pages/cluster/__tests__/K8sCertsPage.test.tsx  | 37 +++++++-
 web/src/pages/cluster/certs.tsx                    | 99 ++++++++++++----------
 3 files changed, 131 insertions(+), 54 deletions(-)

diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index d4ef5d1d1..bed2185af 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -2736,22 +2736,31 @@ const translations: Record<string, Record<Lang, 
string>> = {
   'settings.deepseekOfficial': { zh: 'DeepSeek 官方', en: 'DeepSeek official' },
 
   // ─── Certs ───
+  'cert.pageTitle': { zh: 'K8s 证书配置', en: 'K8s Certificate Config' },
   'cert.clusterName': { zh: 'K8s 集群名称', en: 'K8s Cluster Name' },
-  'cert.certName': { zh: '证书名称', en: 'Certificate Name' },
+  'cert.k8sId': { zh: 'k8s ID', en: 'k8s ID' },
+  'cert.typeLabel': { zh: '类型', en: 'Type' },
   'cert.issuer': { zh: '签发者', en: 'Issuer' },
   'cert.expiryTime': { zh: '到期时间', en: 'Expiry Time' },
   'cert.daysRemaining': { zh: '剩余天数', en: 'Days Remaining' },
+  'cert.status': { zh: '状态', en: 'Status' },
+  'cert.action': { zh: '操作', en: 'Actions' },
   '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' },
+    zh: '确定要删除证书「{name}」吗?',
+    en: 'Are you sure you want to delete certificate "{name}"?',
+  },
+  'cert.deleted': { zh: '证书「{name}」已删除', en: 'Certificate "{name}" deleted' },
+  'cert.certCreated': { zh: '证书「{name}」已添加', en: 'Certificate "{name}" added' 
},
+  'cert.addCert': { zh: '新增证书', en: 'Add Certificate' },
+  'cert.add': { zh: '添加', en: 'Add' },
+  'cert.delete': { zh: '删除', en: 'Delete' },
+  'cert.cancel': { zh: '取消', en: 'Cancel' },
+  'cert.all': { zh: '全部', en: 'All' },
+  'cert.searchPlaceholder': { zh: '搜索 k8s ID 或集群', en: 'Search k8s ID or 
cluster' },
   'cert.editCert': { zh: '编辑证书 — {name}', en: 'Edit Certificate — {name}' },
   'cert.certUpdated': { zh: '证书「{name}」已更新', en: 'Certificate "{name}" 
updated' },
   'cert.namespace': { zh: '命名空间', en: 'Namespace' },
@@ -2759,6 +2768,32 @@ const translations: Record<string, Record<Lang, string>> 
= {
   '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' },
+  'cert.localMetadataTitle': {
+    zh: '当前证书记录仅保存为 Studio 本地配置',
+    en: 'Certificate records are only kept as Studio local configuration',
+  },
+  'cert.localMetadataDescription': {
+    zh: '这些操作不会连接 Kubernetes 集群或修改集群中的证书资源。请在集群侧管理实际证书。',
+    en: 'These operations do not connect to the Kubernetes cluster or modify 
certificate resources in it. Manage the real certificates on the cluster side.',
+  },
+  'cert.requestFailed': { zh: '请求失败,请稍后重试', en: 'Request failed, please retry 
later' },
+  'cert.k8sIdRequired': { zh: '请输入 k8s ID', en: 'Please enter the k8s ID' },
+  'cert.k8sIdPlaceholder': { zh: '例如:kubernetes-daily', en: 'e.g. 
kubernetes-daily' },
+  'cert.clusterNameRequired': { zh: '请输入集群名称', en: 'Please enter the cluster 
name' },
+  'cert.clusterNamePlaceholder': {
+    zh: '例如:kubernetes(120.26.99.191:6443)',
+    en: 'e.g. kubernetes (120.26.99.191:6443)',
+  },
+  'cert.certPemLabel': { zh: '证书内容(PEM)', en: 'Certificate Content (PEM)' },
+  'cert.certPemHelp': {
+    zh: '粘贴 PEM 格式证书,签发者、有效期与 SAN 将自动解析;留空时有效期按一年占位',
+    en: 'Paste the PEM certificate; issuer, validity and SANs are parsed 
automatically. Leave blank to default to a one-year validity.',
+  },
+  'cert.keyPemLabel': { zh: '私钥内容(PEM)', en: 'Private Key (PEM)' },
+  'cert.keyPemHelp': {
+    zh: '仅保存,不会在页面展示或返回',
+    en: 'Stored only; never displayed or returned in the UI',
+  },
 
   // ─── Cluster ───
   'cluster.brokerCount': { zh: 'Broker 数', en: 'Broker Count' },
diff --git a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx 
b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
index a8032ca69..d94801d47 100644
--- a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
@@ -22,6 +22,7 @@ import { App } from 'antd';
 import type { K8sCertInfo } from '../../../api/cluster';
 import { listK8sCerts, createK8sCert, deleteK8sCert } from 
'../../../services/clusterService';
 import K8sCertsPage from '../certs';
+import { LangProvider } from '../../../i18n/LangContext';
 
 vi.mock('../../../services/clusterService', () => ({
   listK8sCerts: vi.fn(),
@@ -29,6 +30,13 @@ vi.mock('../../../services/clusterService', () => ({
   deleteK8sCert: vi.fn(),
 }));
 
+const languagePreference = vi.hoisted(() => ({ initial: 'zh' as 'zh' | 'en' 
}));
+
+vi.mock('../../../i18n/languagePreference', () => ({
+  getInitialLanguage: () => languagePreference.initial,
+  persistLanguage: vi.fn(),
+}));
+
 const certs: K8sCertInfo[] = [
   {
     id: 1,
@@ -87,6 +95,7 @@ beforeAll(() => {
 
 describe('K8sCertsPage', () => {
   beforeEach(() => {
+    languagePreference.initial = 'zh';
     vi.clearAllMocks();
     vi.mocked(listK8sCerts).mockResolvedValue(certs);
   });
@@ -94,7 +103,9 @@ describe('K8sCertsPage', () => {
   const renderPage = () =>
     render(
       <App>
-        <K8sCertsPage />
+        <LangProvider>
+          <K8sCertsPage />
+        </LangProvider>
       </App>,
     );
 
@@ -269,3 +280,27 @@ describe('K8sCertsPage', () => {
     expect(screen.getByText('共 3 个证书')).toBeInTheDocument();
   });
 });
+
+describe('K8sCertsPage English localization', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    languagePreference.initial = 'en';
+    vi.mocked(listK8sCerts).mockResolvedValue(certs);
+  });
+
+  it('renders English labels inside the language provider', async () => {
+    render(
+      <LangProvider>
+        <K8sCertsPage />
+      </LangProvider>,
+    );
+
+    await screen.findByText('rocketmq-prod-tls');
+    expect(screen.getByRole('heading', { name: 'K8s Certificate Config' 
})).toBeInTheDocument();
+    expect(screen.getByRole('button', { name: /Add Certificate/ 
})).toBeInTheDocument();
+    expect(screen.getByRole('columnheader', { name: 'K8s Cluster Name' 
})).toBeInTheDocument();
+    expect(screen.getByRole('columnheader', { name: 'Expiry Time' 
})).toBeInTheDocument();
+    expect(screen.getByRole('columnheader', { name: 'Days Remaining' 
})).toBeInTheDocument();
+    expect(screen.getByRole('columnheader', { name: 'Actions' 
})).toBeInTheDocument();
+  });
+});
diff --git a/web/src/pages/cluster/certs.tsx b/web/src/pages/cluster/certs.tsx
index 033fdcda7..14313fca1 100644
--- a/web/src/pages/cluster/certs.tsx
+++ b/web/src/pages/cluster/certs.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import {
   Table,
   Tag,
@@ -45,8 +45,6 @@ import { useLang } from '../../i18n/LangContext';
 
 const { Text } = Typography;
 
-const DEFAULT_REQUEST_ERROR = '请求失败,请稍后重试';
-
 interface CreateCertFormValues {
   k8sId: string;
   cluster: string;
@@ -57,6 +55,7 @@ interface CreateCertFormValues {
 
 const K8sCertsPage = () => {
   const { t } = useLang();
+  const tRef = useRef(t);
   const [certs, setCerts] = useState<K8sCertInfo[]>([]);
   const [loading, setLoading] = useState(true);
   const [loadFailed, setLoadFailed] = useState(false);
@@ -68,6 +67,14 @@ const K8sCertsPage = () => {
   const [deletingId, setDeletingId] = useState<number | null>(null);
   const [createForm] = Form.useForm<CreateCertFormValues>();
 
+  // The load effect below must run once per mount and per explicit reload, 
while the translation
+  // function changes identity with the display language. Holding it in a ref 
keeps the failure
+  // message localized without re-issuing the certificate list request on 
every language switch
+  // (same pattern as pages/cluster/index.tsx).
+  useEffect(() => {
+    tRef.current = t;
+  }, [t]);
+
   useEffect(() => {
     let active = true;
     listK8sCerts()
@@ -81,7 +88,7 @@ const K8sCertsPage = () => {
         // Keeping the previous rows and marking the failure: clearing the 
list made a failed read
         // look like a cluster without certificates, and nothing offered a 
retry.
         if (active) {
-          message.error(describeThrownMessage(error) || DEFAULT_REQUEST_ERROR);
+          message.error(describeThrownMessage(error) || 
tRef.current('cert.requestFailed'));
           setLoadFailed(true);
         }
       })
@@ -121,11 +128,11 @@ const K8sCertsPage = () => {
         keyPem: values.keyPem?.trim() || undefined,
       });
       setCerts((previous) => [...previous, created]);
-      message.success(`证书「${created.k8sId}」已添加`);
+      message.success(t('cert.certCreated', { name: created.k8sId }));
       setCreateModalOpen(false);
       createForm.resetFields();
     } catch (error: unknown) {
-      message.error(describeThrownMessage(error) || DEFAULT_REQUEST_ERROR);
+      message.error(describeThrownMessage(error) || t('cert.requestFailed'));
     } finally {
       setCreating(false);
     }
@@ -136,9 +143,9 @@ const K8sCertsPage = () => {
     try {
       await deleteK8sCert(cert.id);
       setCerts((previous) => previous.filter((item) => item.id !== cert.id));
-      message.success(`证书「${cert.k8sId}」已删除`);
+      message.success(t('cert.deleted', { name: cert.k8sId }));
     } catch (error: unknown) {
-      message.error(describeThrownMessage(error) || DEFAULT_REQUEST_ERROR);
+      message.error(describeThrownMessage(error) || t('cert.requestFailed'));
     } finally {
       setDeletingId(null);
     }
@@ -146,7 +153,7 @@ const K8sCertsPage = () => {
 
   const certColumns: ColumnsType<K8sCertInfo> = [
     {
-      title: 'K8s 集群名称',
+      title: t('cert.clusterName'),
       dataIndex: 'cluster',
       key: 'cluster',
       width: 260,
@@ -155,7 +162,7 @@ const K8sCertsPage = () => {
       render: (name: string) => <Text strong>{name}</Text>,
     },
     {
-      title: 'k8s ID',
+      title: t('cert.k8sId'),
       dataIndex: 'k8sId',
       key: 'k8sId',
       width: 240,
@@ -165,7 +172,7 @@ const K8sCertsPage = () => {
       ),
     },
     {
-      title: '类型',
+      title: t('cert.typeLabel'),
       dataIndex: 'type',
       key: 'type',
       width: 110,
@@ -180,7 +187,7 @@ const K8sCertsPage = () => {
       },
     },
     {
-      title: '签发者',
+      title: t('cert.issuer'),
       dataIndex: 'issuer',
       key: 'issuer',
       // 唯一可伸展列:余量集中在此,其余列保持声明宽度。选签发者而非集群名,
@@ -191,7 +198,7 @@ const K8sCertsPage = () => {
       ellipsis: true,
     },
     {
-      title: '到期时间',
+      title: t('cert.expiryTime'),
       dataIndex: 'notAfter',
       key: 'notAfter',
       width: 156,
@@ -203,7 +210,7 @@ const K8sCertsPage = () => {
       ),
     },
     {
-      title: '剩余天数',
+      title: t('cert.daysRemaining'),
       dataIndex: 'daysRemaining',
       key: 'daysRemaining',
       width: 100,
@@ -220,32 +227,32 @@ const K8sCertsPage = () => {
       ),
     },
     {
-      title: '状态',
+      title: t('cert.status'),
       dataIndex: 'status',
       key: 'status',
       width: 100,
       sorter: (a, b) => (a.status ?? '').localeCompare(b.status ?? ''),
       render: (status: string | null) => {
         const map: Record<string, { color: string; label: string }> = {
-          valid: { color: 'green', label: '有效' },
-          expiring: { color: 'orange', label: '即将过期' },
-          expired: { color: 'red', label: '已过期' },
+          valid: { color: 'green', label: t('cert.statusValid') },
+          expiring: { color: 'orange', label: t('cert.statusExpiring') },
+          expired: { color: 'red', label: t('cert.statusExpired') },
         };
         const cfg = status ? (map[status] ?? { color: 'default', label: status 
}) : null;
         return cfg ? <Tag color={cfg.color}>{cfg.label}</Tag> : '-';
       },
     },
     {
-      title: '操作',
+      title: t('cert.action'),
       key: 'action',
       width: 90,
       fixed: 'right',
       render: (_: unknown, cert: K8sCertInfo) => (
         <Popconfirm
-          title={`确定要删除证书「${cert.k8sId}」吗?`}
+          title={t('cert.deleteConfirm', { name: cert.k8sId })}
           onConfirm={() => void handleDelete(cert)}
-          okText="删除"
-          cancelText="取消"
+          okText={t('cert.delete')}
+          cancelText={t('cert.cancel')}
           okButtonProps={{ danger: true }}
         >
           <Button
@@ -254,7 +261,7 @@ const K8sCertsPage = () => {
             style={{ borderColor: '#ff4d4f', color: '#ff4d4f' }}
             loading={deletingId === cert.id}
           >
-            删除
+            {t('cert.delete')}
           </Button>
         </Popconfirm>
       ),
@@ -264,8 +271,12 @@ const K8sCertsPage = () => {
   return (
     <div style={{ padding: 24 }}>
       <PageHeader
-        title="K8s 证书配置"
-        subtitle={loadFailed ? t('common.unavailable') : `共 
${filteredCerts.length} 个证书`}
+        title={t('cert.pageTitle')}
+        subtitle={
+          loadFailed
+            ? t('common.unavailable')
+            : t('cert.totalCount', { count: filteredCerts.length })
+        }
       />
       {loadFailed && (
         <Alert
@@ -286,13 +297,13 @@ const K8sCertsPage = () => {
       )}
       <InfoBanner
         data-testid="k8s-cert-local-metadata-notice"
-        title="当前证书记录仅保存为 Studio 本地配置"
-        description="这些操作不会连接 Kubernetes 集群或修改集群中的证书资源。请在集群侧管理实际证书。"
+        title={t('cert.localMetadataTitle')}
+        description={t('cert.localMetadataDescription')}
       />
       <Flex justify="space-between" style={{ marginBottom: 16 }}>
         <Space>
           <Input.Search
-            placeholder="搜索 k8s ID 或集群"
+            placeholder={t('cert.searchPlaceholder')}
             allowClear
             onSearch={setCertSearch}
             onChange={(e) => !e.target.value && setCertSearch('')}
@@ -303,7 +314,7 @@ const K8sCertsPage = () => {
             onChange={setCertTypeFilter}
             style={{ width: 160 }}
             options={[
-              { value: '', label: '全部' },
+              { value: '', label: t('cert.all') },
               { value: 'TLS', label: 'TLS' },
               { value: 'mTLS', label: 'mTLS' },
               { value: 'ServiceAccount', label: 'ServiceAccount' },
@@ -311,7 +322,7 @@ const K8sCertsPage = () => {
           />
         </Space>
         <Button type="primary" icon={<PlusOutlined />} onClick={() => 
setCreateModalOpen(true)}>
-          新增证书
+          {t('cert.addCert')}
         </Button>
       </Flex>
       <Card styles={{ body: { padding: 0 } }}>
@@ -328,7 +339,7 @@ const K8sCertsPage = () => {
       </Card>
 
       <Modal
-        title="新增证书"
+        title={t('cert.addCert')}
         open={createModalOpen}
         onCancel={() => {
           setCreateModalOpen(false);
@@ -336,27 +347,27 @@ const K8sCertsPage = () => {
         }}
         onOk={() => void handleCreate()}
         confirmLoading={creating}
-        okText="添加"
-        cancelText="取消"
+        okText={t('cert.add')}
+        cancelText={t('cert.cancel')}
         width={640}
         destroyOnHidden
       >
         <Form form={createForm} layout="vertical" preserve={false}>
           <Form.Item
-            label="k8s ID"
+            label={t('cert.k8sId')}
             name="k8sId"
-            rules={[{ required: true, message: '请输入 k8s ID' }]}
+            rules={[{ required: true, message: t('cert.k8sIdRequired') }]}
           >
-            <Input placeholder="例如:kubernetes-daily" />
+            <Input placeholder={t('cert.k8sIdPlaceholder')} />
           </Form.Item>
           <Form.Item
-            label="K8s 集群名称"
+            label={t('cert.clusterName')}
             name="cluster"
-            rules={[{ required: true, message: '请输入集群名称' }]}
+            rules={[{ required: true, message: t('cert.clusterNameRequired') 
}]}
           >
-            <Input placeholder="例如:kubernetes(120.26.99.191:6443)" />
+            <Input placeholder={t('cert.clusterNamePlaceholder')} />
           </Form.Item>
-          <Form.Item label="类型" name="type" initialValue="TLS">
+          <Form.Item label={t('cert.typeLabel')} name="type" 
initialValue="TLS">
             <Select
               virtual={false}
               options={[
@@ -366,18 +377,14 @@ const K8sCertsPage = () => {
               ]}
             />
           </Form.Item>
-          <Form.Item
-            label="证书内容(PEM)"
-            name="certPem"
-            extra="粘贴 PEM 格式证书,签发者、有效期与 SAN 将自动解析;留空时有效期按一年占位"
-          >
+          <Form.Item label={t('cert.certPemLabel')} name="certPem" 
extra={t('cert.certPemHelp')}>
             <Input.TextArea
               rows={6}
               placeholder="-----BEGIN CERTIFICATE-----..."
               style={{ fontFamily: 'monospace' }}
             />
           </Form.Item>
-          <Form.Item label="私钥内容(PEM)" name="keyPem" extra="仅保存,不会在页面展示或返回">
+          <Form.Item label={t('cert.keyPemLabel')} name="keyPem" 
extra={t('cert.keyPemHelp')}>
             <Input.TextArea
               rows={6}
               placeholder="-----BEGIN PRIVATE KEY-----..."

Reply via email to