diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 4500b1ec..650b038f 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -808,12 +808,19 @@
},
"topology": {
"title": "Topology",
- "subtitle": "Host architecture view",
+ "subtitle": "Service dependencies & health status",
"noHosts": "No host data available",
"fetchError": "Failed to load host data",
"services": "Services",
"cpu": "CPU",
- "ram": "RAM"
+ "ram": "RAM",
+ "groupInfra": "Infrastructure",
+ "groupSecurity": "Security",
+ "groupK3s": "K3s Cluster",
+ "groupAiData": "AI/Data Center",
+ "allHealthy": "All Healthy",
+ "warning": "Warning",
+ "healthy": "Healthy"
},
"notifications": {
"title": "Notifications",
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index bbbb5562..74a03094 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -809,12 +809,19 @@
},
"topology": {
"title": "拓撲圖",
- "subtitle": "主機架構視圖",
+ "subtitle": "服務依賴與健康狀態",
"noHosts": "目前無主機資料",
"fetchError": "無法取得主機資料",
"services": "服務",
"cpu": "CPU",
- "ram": "RAM"
+ "ram": "RAM",
+ "groupInfra": "基礎設施",
+ "groupSecurity": "安全中心",
+ "groupK3s": "K3s 叢集",
+ "groupAiData": "AI/數據中心",
+ "allHealthy": "全部健康",
+ "warning": "異常",
+ "healthy": "健康"
},
"notifications": {
"title": "通知",
diff --git a/apps/web/src/components/topology/ServiceTopology.tsx b/apps/web/src/components/topology/ServiceTopology.tsx
index ece151b9..7e5faa83 100644
--- a/apps/web/src/components/topology/ServiceTopology.tsx
+++ b/apps/web/src/components/topology/ServiceTopology.tsx
@@ -29,6 +29,7 @@ import {
} from '@xyflow/react'
import '@xyflow/react/dist/style.css'
+import { useTranslations } from 'next-intl'
import { useTopologyData } from './hooks/useTopologyData'
import { ServiceNode } from './nodes/ServiceNode'
import { GroupNode } from './nodes/GroupNode'
@@ -72,6 +73,7 @@ export function ServiceTopology({
showMiniMap = false,
height = '100%',
}: ServiceTopologyProps) {
+ const t = useTranslations('dashboard')
const { nodes, edges, hostCount, serviceCount, healthyCount, warningCount } = useTopologyData()
// 空狀態
@@ -82,9 +84,9 @@ export function ServiceTopology({
height: typeof height === 'number' ? height : '100%',
color: '#87867f', fontSize: 13,
}}>
-
🌐
- 等待主機資料...
- Dashboard API 連線中
+ --
+ {t('waitingHostData')}
+ {t('dashboardConnecting')}
)
}
@@ -99,10 +101,10 @@ export function ServiceTopology({
background: 'rgba(250,249,243,0.9)', padding: '4px 10px', borderRadius: 6,
border: '0.5px solid #e0ddd4',
}}>
- {hostCount} 主機
- {serviceCount} 服務
- {healthyCount} 健康
- {warningCount > 0 && {warningCount} 異常}
+ {hostCount} {t('hostsLabel')}
+ {serviceCount}
+ {healthyCount}
+ {warningCount > 0 && {warningCount}}
= {
- devops: { label: '基礎設施', color: '#3B82F6', borderColor: 'rgba(59,130,246,0.25)', bgColor: 'rgba(59,130,246,0.02)' },
- security: { label: '安全中心', color: '#cc2200', borderColor: 'rgba(204,34,0,0.25)', bgColor: 'rgba(204,34,0,0.02)' },
- k3s: { label: 'K3s 叢集', color: '#A855F7', borderColor: 'rgba(168,85,247,0.25)', bgColor: 'rgba(168,85,247,0.02)' },
- ai_web: { label: 'AI/數據中心', color: '#F97316', borderColor: 'rgba(249,115,22,0.25)', bgColor: 'rgba(249,115,22,0.02)' },
+// C2: 群組標籤用 key 而非硬編碼中文(i18n 在元件層處理)
+const GROUP_CONFIG: Record = {
+ devops: { labelKey: 'infra', color: '#3B82F6', borderColor: 'rgba(59,130,246,0.25)', bgColor: 'rgba(59,130,246,0.02)' },
+ security: { labelKey: 'security', color: '#cc2200', borderColor: 'rgba(204,34,0,0.25)', bgColor: 'rgba(204,34,0,0.02)' },
+ k3s: { labelKey: 'k3s', color: '#A855F7', borderColor: 'rgba(168,85,247,0.25)', bgColor: 'rgba(168,85,247,0.02)' },
+ ai_web: { labelKey: 'aiData', color: '#F97316', borderColor: 'rgba(249,115,22,0.25)', bgColor: 'rgba(249,115,22,0.02)' },
}
// =============================================================================
@@ -85,7 +86,8 @@ export function useTopologyData(): TopologyData {
type: 'groupNode',
position: { x: (hostIdx % 2) * 350, y: Math.floor(hostIdx / 2) * 300 },
data: {
- label: `${config.label} (${host.ip.split('.').pop()})`,
+ labelKey: config.labelKey,
+ ipSuffix: host.ip.split('.').pop(),
role: host.role,
status: host.status,
ip: host.ip,
diff --git a/apps/web/src/components/topology/nodes/GroupNode.tsx b/apps/web/src/components/topology/nodes/GroupNode.tsx
index 67e02eba..03547c20 100644
--- a/apps/web/src/components/topology/nodes/GroupNode.tsx
+++ b/apps/web/src/components/topology/nodes/GroupNode.tsx
@@ -13,8 +13,15 @@
import { memo, type CSSProperties } from 'react'
import { type NodeProps } from '@xyflow/react'
+import { useTranslations } from 'next-intl'
+
+// C2: 群組標籤 i18n
+const GROUP_LABELS: Record = {
+ infra: 'groupInfra', security: 'groupSecurity', k3s: 'groupK3s', aiData: 'groupAiData',
+}
function GroupNodeInner({ data }: NodeProps) {
+ const t = useTranslations('topology')
const d = data as Record
const borderColor = d.borderColor || '#e0ddd4'
const bgColor = d.bgColor || 'rgba(0,0,0,0.01)'
@@ -39,15 +46,15 @@ function GroupNodeInner({ data }: NodeProps) {
{/* 群組標題 */}
- {d.label}
+ {t(GROUP_LABELS[d.labelKey] || 'groupInfra')} (.{d.ipSuffix})
{/* 摘要 */}
- {totalSvc} 服務 · {hasWarning
- ? ⚠ {totalSvc - healthySvc} 異常
- : ✓ 全部健康
+ {totalSvc} {t('services')} · {hasWarning
+ ? {totalSvc - healthySvc} {t('warning')}
+ : {t('allHealthy')}
}