refactor(i18n): U40 Monitor + Agent 子组件残余硬编码中文 i18n 补全 — 14 文件 ~50 处→0

- ContextSwitchCard: CTX/IRQ/次 → t()
- ProcessTable: agentWaitData/cpuUsagePercent → t()
- FDUsageCard: fdHandles → t()
- PortMonitorTab: 黑名单/白名单/策略配置/暂无端口数据 → t()
- PortPolicyDialog: 白名单全量上报/黑名单仅上报/端口列表/逗号分隔/白名单无需配置/已输入端口 → t()
- HostTimeRangePicker: 时间范围/自定义/取消/应用 → t()
- DualLineChart: 接收/发送 → t(), i18n.t→useTranslation hook
- ZoomableAreaChart: 暂无数据 → t()
- SiteResultsPanel: 选择任务/次/可用率/立即拨测/SSL证书预警/到期/剩余/阈值/天 → t()
- MiddlewareMonitor: Redis单机集群/上次更新/添加平台默认/暂无实例 → t()
- SiteMonitor: subtitle → t()
- SNMPMonitor: 标题/采集间隔/IP地址/OID模板/SNMP版本/确认/暂无设备/采集指标/暂无指标 → t()
- HealthProbeSection: TCP目标地址/TCP端口 → t()
- AgentAlerts: Agent告警/刷新/未恢复/已恢复/恢复/新建规则/创建第一条规则 → i18n.t()

新增 monitor.lbl 46 语义化翻译键 + agent.alerts 6 键
zh/en 7762→7814 对齐
tsc -b=0, npm build✓ (16.88s), ESLint=0/43/any=0, go build=0, 17/17 测试通过
This commit is contained in:
attacker committed 2026-07-13 19:45:45 +08:00
1 parent 70a7c2907c
commit 4e2ba77aa8
17 files changed
+165 -61

No files matched your search

@@ -153,10 +153,10 @@ export function MiddlewareMonitor() {
<div>
<h2 className="text-lg font-semibold"> {t('monitor.alertHelper.srcMiddleware')} </h2>
<p className="text-xs text-[var(--dg-text-muted)] mt-0.5">
Redis 单机/集群 · MySQL · 30s 自动刷新
{t('monitor.lbl.redisMysqlAutoRefresh')}
{lastRefreshAt && (
<span className="ml-2">
上次更新 {lastRefreshAt.toLocaleTimeString()}
{t('monitor.lbl.lastUpdate')} {lastRefreshAt.toLocaleTimeString()}
{refreshing && t('monitor.lbl.refreshing')}
</span>
)}
@@ -172,7 +172,7 @@ export function MiddlewareMonitor() {
{targets.length === 0 && (
<Button variant="outline" size="sm" onClick={handleSeedDefaults} disabled={seeding} className="gap-1.5">
{seeding ? <Loader2 className="h-4 w-4 animate-spin" /> : <Database className="h-4 w-4" />}
添加平台默认
{t('monitor.lbl.addPlatformDefault')}
</Button>
)}
<Button size="sm" onClick={() => openCreate()} className="gap-1.5">
@@ -252,7 +252,7 @@ export function MiddlewareMonitor() {
{currentTargets.length === 0 ? (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-8 text-center text-sm text-[var(--dg-text-muted)]">
暂无 {tab === 'redis' ? 'Redis' : 'MySQL'} 实例,
{t('monitor.lbl.noInstancePrefix')} {tab === 'redis' ? 'Redis' : 'MySQL'} {t('monitor.lbl.instanceSuffix')},
<button type="button" className="dg-text-cyan ml-1 underline" onClick={() => openCreate(tab)}> {t('monitor.middlewareMonit.addNow')} </button>
</CardContent>
</Card>
+9 -9
View File
@@ -183,8 +183,8 @@ export function SNMPMonitor() {
{/* 工具栏 */}
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<h2 className="text-lg font-semibold">SNMP 监控</h2>
<p className="text-xs text-[var(--dg-text-muted)]">5 分钟采集 · 指标保留 7 天</p>
<h2 className="text-lg font-semibold">{t('monitor.lbl.snmpMonitorTitle')}</h2>
<p className="text-xs text-[var(--dg-text-muted)]">{t('monitor.lbl.snmpCollectInterval')}</p>
</div>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={refresh} className="h-8">
@@ -208,7 +208,7 @@ export function SNMPMonitor() {
</div>
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-1.5">
<Label>IP 地址</Label>
<Label>{t('monitor.lbl.ipAddress')}</Label>
<Input value={form.ip} onChange={(e) => setForm({ ...form, ip: e.target.value })} placeholder="192.168.1.1" />
</div>
<div className="grid gap-1.5">
@@ -217,7 +217,7 @@ export function SNMPMonitor() {
</div>
</div>
<div className="grid gap-1.5">
<Label>OID 模板</Label>
<Label>{t('monitor.lbl.snmpOidTemplate')}</Label>
<Select value={form.template_id} onValueChange={(v) => setForm({ ...form, template_id: v })}>
<SelectTrigger><SelectValue placeholder={t('pipeline.selectTemplate')} /></SelectTrigger>
<SelectContent>
@@ -229,7 +229,7 @@ export function SNMPMonitor() {
</div>
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-1.5">
<Label>SNMP 版本</Label>
<Label>{t('monitor.lbl.snmpVersion')}</Label>
<Select value={form.snmp_version} onValueChange={(v) => setForm({ ...form, snmp_version: v as 'v2c' | 'v3' })}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
@@ -247,7 +247,7 @@ export function SNMPMonitor() {
<DialogFooter>
<Button variant="outline" onClick={() => setDialogOpen(false)}> {t('rbac.jobRole.cancel')} </Button>
<Button onClick={handleAdd} disabled={submitting}>
{submitting && <Loader2 className="h-4 w-4 animate-spin mr-1" />}确认
{submitting && <Loader2 className="h-4 w-4 animate-spin mr-1" />}{t('common.confirm')}
</Button>
</DialogFooter>
</DialogContent>
@@ -278,7 +278,7 @@ export function SNMPMonitor() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="py-12 text-center text-sm text-[var(--dg-text-muted)]">
<Router className="h-10 w-10 mx-auto mb-3 opacity-40" />
暂无 SNMP 设备,点击「添加设备」开始监控
{t('monitor.lbl.snmpNoDevice')}
</CardContent>
</Card>
) : (
@@ -348,14 +348,14 @@ export function SNMPMonitor() {
<Dialog open={metricsOpen} onOpenChange={setMetricsOpen}>
<DialogContent className="sm:max-w-2xl max-h-[80vh] overflow-y-auto bg-[var(--dg-bg-secondary)]">
<DialogHeader>
<DialogTitle>{metricsDevice?.name} — 采集指标</DialogTitle>
<DialogTitle>{metricsDevice?.name} — {t('monitor.lbl.snmpCollectMetrics')}</DialogTitle>
<DialogDescription>{metricsDevice?.ip} · {templateName(metricsDevice?.template_id || 0)}</DialogDescription>
</DialogHeader>
{metricsLoading ? (
<div className="py-8 text-center text-sm text-muted-foreground"> {t('monitor.logPanel.logResults.loading')} </div>
) : metrics.length === 0 ? (
<div className="py-8 text-center text-sm text-muted-foreground">
暂无指标,请先采集
{t('monitor.lbl.snmpNoMetrics')}
{metricsDevice?.last_error && <p className="mt-2 text-xs dg-text-red">{metricsDevice.last_error}</p>}
</div>
) : (
+1 -1
View File
@@ -213,7 +213,7 @@ export function SiteMonitor() {
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<h2 className="text-lg font-semibold"> {t('monitor.alertRulesTab.catSite')} </h2>
<p className="text-xs text-[var(--dg-text-muted)]">HTTP Web 拨测 · SSL 证书检测 · TCP / DNS / PING</p>
<p className="text-xs text-[var(--dg-text-muted)]">{t('monitor.lbl.siteMonitorSubtitle')}</p>
</div>
<div className="flex gap-2">
<Button size="sm" variant="outline" className="h-8" onClick={() => { loadStats(); loadTasks(); }} disabled={refreshing}>
@@ -6,7 +6,7 @@ import { ZoomableAreaChart, type ZoomableChartHandle } from './ZoomableAreaChart
import { ChartToolbar } from './ChartToolbar';
import { MetricIconBox } from './MetricIconBox';
import type { AggType, BucketPoint } from './types';
import i18n from '@/i18n';
import { useTranslation } from 'react-i18next';
interface DualLineChartProps {
title: string;
@@ -21,6 +21,7 @@ interface DualLineChartProps {
export function DualLineChart({
title, currentRx, currentTx, rxData, txData, aggType, unit,
}: DualLineChartProps) {
const { t } = useTranslation();
const chartRef = useRef<ZoomableChartHandle>(null);
const [zoomed, setZoomed] = useState(false);
@@ -43,11 +44,11 @@ export function DualLineChart({
<div className="flex items-center gap-3 text-xs">
<span>
<span className="inline-block w-2.5 h-2.5 rounded-sm bg-blue-400 mr-1 align-middle" />
接收 <b className="text-blue-300">{currentRx}</b>
{t('monitor.lbl.rx')} <b className="text-blue-300">{currentRx}</b>
</span>
<span>
<span className="inline-block w-2.5 h-2.5 rounded-sm bg-emerald-400 mr-1 align-middle" />
发送 <b className="dg-text-green">{currentTx}</b>
{t('monitor.lbl.tx')} <b className="dg-text-green">{currentTx}</b>
</span>
</div>
<ChartToolbar
@@ -61,14 +62,14 @@ export function DualLineChart({
ref={chartRef}
data={chartData}
series={[
{ dataKey: 'rx', color: '#3b82f6', label: i18n.t('common.receive') },
{ dataKey: 'tx', color: '#10b981', label: i18n.t('aiops.sendMessage') },
{ dataKey: 'rx', color: '#3b82f6', label: t('common.receive') },
{ dataKey: 'tx', color: '#10b981', label: t('aiops.sendMessage') },
]}
yDomain={[0, 'auto']}
unit={unit}
chartId={`dual-${title.replace(/\s/g, '')}`}
title={title}
subtitle={i18n.t('monitor.lbl.receiveSend', { rx: currentRx, tx: currentTx })}
subtitle={t('monitor.lbl.receiveSend', { rx: currentRx, tx: currentTx })}
height={208}
toolbarPlacement="header"
onZoomedChange={setZoomed}
@@ -200,9 +200,9 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
{probeForm.probe_type === 'tcp_socket' && (
<div className="grid grid-cols-2 gap-2">
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">TCP 目标地址</label>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">{t('monitor.lbl.tcpTargetAddr')}</label>
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.tcp_host} onChange={e => setProbeForm({ ...probeForm, tcp_host: e.target.value })} placeholder="localhost" /></div>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">TCP 端口</label>
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">{t('monitor.lbl.tcpPort')}</label>
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.tcp_port} onChange={e => setProbeForm({ ...probeForm, tcp_port: Number(e.target.value) })} /></div>
</div>
)}
@@ -58,7 +58,7 @@ export function HostTimeRangePicker({ value, onChange }: Props) {
<div className="flex items-center gap-2 flex-wrap">
<span className="text-[11px] dg-text-muted hidden sm:inline-flex items-center gap-1">
<Timer className="h-3 w-3" />
时间范围
{t('monitor.lbl.timeRange')}
</span>
<div className="flex items-center gap-1 rounded-md border border-[var(--dg-border)] p-0.5 bg-[var(--dg-bg-secondary)] flex-wrap">
{TIME_RANGE_PRESETS.map((p) => (
@@ -82,7 +82,7 @@ export function HostTimeRangePicker({ value, onChange }: Props) {
onClick={() => setCustomOpen((v) => !v)}
>
<Calendar className="h-3 w-3" />
自定义
{t('monitor.lbl.custom')}
<ChevronDown className={`h-3 w-3 transition-transform ${customOpen ? 'rotate-180' : ''}`} />
</Button>
{customOpen && (
@@ -110,10 +110,10 @@ export function HostTimeRangePicker({ value, onChange }: Props) {
</div>
<div className="flex justify-end gap-2 mt-3">
<Button type="button" variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setCustomOpen(false)}>
取消
{t('common.cancel')}
</Button>
<Button type="button" size="sm" className="h-7 text-xs dg-btn-primary" onClick={applyCustom}>
应用
{t('monitor.lbl.apply')}
</Button>
</div>
</div>
@@ -43,13 +43,13 @@ export function PortPolicyDialog({
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'whitelist' ? 'bg-blue-500/20 border-blue-500 dg-text-blue' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
onClick={() => onModeChange('whitelist')}
>
白名单 ─ 全量上报
{t('monitor.lbl.whitelistFullReport')}
</button>
<button
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'blacklist' ? 'bg-amber-500/20 border-amber-500 dg-text-yellow' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
onClick={() => onModeChange('blacklist')}
>
黑名单 ─ 仅上报选中
{t('monitor.lbl.blacklistSelectedOnly')}
</button>
</div>
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1.5 leading-relaxed">
@@ -61,8 +61,8 @@ export function PortPolicyDialog({
<div>
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">
端口列表 <span className="text-[var(--dg-text-muted)]">(逗号分隔,如: 80,443,3306,6379)</span>
{portPolicyMode === 'whitelist' && <span className="dg-text-blue ml-1">— 白名单模式无需配置</span>}
{t('monitor.lbl.portListLabel')} <span className="text-[var(--dg-text-muted)]">{t('monitor.lbl.portListHint')}</span>
{portPolicyMode === 'whitelist' && <span className="dg-text-blue ml-1">{t('monitor.lbl.whitelistNoConfig')}</span>}
</label>
<textarea
className={`w-full h-20 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 py-1.5 text-xs font-mono resize-none ${
@@ -78,7 +78,7 @@ export function PortPolicyDialog({
readOnly={portPolicyMode === 'whitelist'}
/>
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1">
已输入 {portWhitelist.length} 个端口
{t('monitor.lbl.portsEntered', { count: portWhitelist.length })}
</p>
</div>
@@ -286,7 +286,7 @@ export const ZoomableAreaChart = forwardRef<ZoomableChartHandle, Props>(function
className="flex items-center justify-center text-xs dg-text-muted border border-dashed border-[var(--dg-border)] rounded-md w-full"
style={{ height }}
>
暂无数据
{t('monitor.lbl.noData')}
</div>
);
}
@@ -16,18 +16,18 @@ export function ContextSwitchCard({ csInfo }: { csInfo: JsonData }) {
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3 text-center">
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">CTX 切换/秒</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">{t('monitor.lbl.ctxPerSec')}</div>
<div className="text-lg font-bold font-mono dg-text-green">
{(csInfo.ctx_per_sec / 1e6).toFixed(1)}<span className="text-xs font-normal text-[var(--dg-text-muted)] ml-0.5">M</span>
</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1 font-mono">{csInfo.ctx_per_sec?.toLocaleString()} 次</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1 font-mono">{csInfo.ctx_per_sec?.toLocaleString()} {t('monitor.lbl.times')}</div>
</div>
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3 text-center">
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">IRQ 中断/秒</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">{t('monitor.lbl.irqPerSec')}</div>
<div className="text-lg font-bold font-mono dg-text-yellow">
{(csInfo.irq_per_sec / 1e6).toFixed(1)}<span className="text-xs font-normal text-[var(--dg-text-muted)] ml-0.5">M</span>
</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1 font-mono">{csInfo.irq_per_sec?.toLocaleString()} 次</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1 font-mono">{csInfo.irq_per_sec?.toLocaleString()} {t('monitor.lbl.times')}</div>
</div>
</div>
<div className="space-y-1">
@@ -26,7 +26,7 @@ export function FDUsageCard({ fdInfo, fdMetrics, aggType, fdChartRef, fdChartZoo
<div className="flex items-start justify-between gap-2 mb-3">
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] flex items-center gap-2 leading-8">
<MetricIconBox icon={<HardDrive />} tone="purple" />
FD 文件句柄
{t('monitor.lbl.fdHandles')}
</h3>
{fdMetrics.length > 0 && (
<ChartToolbar
@@ -23,22 +23,22 @@ export function PortMonitorTab({ portInfo, portPolicyMode, portWhitelist, onOpen
<span className="ml-auto flex items-center gap-2">
{portPolicyMode === 'blacklist' && portWhitelist.length > 0 && (
<Badge variant="outline" className="text-[10px] dg-text-yellow border-amber-500/30">
黑名单: {portWhitelist.join(',')}
{t('monitor.lbl.blacklistLabel')}: {portWhitelist.join(',')}
</Badge>
)}
{portPolicyMode === 'whitelist' && (
<Badge variant="outline" className="text-[10px] dg-text-blue border-blue-500/30">
白名单模式
{t('monitor.lbl.whitelistModeLabel')}
</Badge>
)}
<Button variant="outline" size="sm" className="h-6 text-[10px] px-2" onClick={onOpenPortPolicy}>
策略配置
{t('monitor.lbl.policyConfig')}
</Button>
</span>
</h3>
{portInfo.length === 0 ? (
<div className="text-center py-6 text-xs text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
暂无端口数据
{t('monitor.lbl.noPortData')}
</div>
) : (
<div className="overflow-x-auto">
@@ -14,7 +14,7 @@ export function ProcessTable({ processTop }: { processTop: JsonData[] }) {
</h3>
{processTop.length === 0 ? (
<div className="text-center py-6 text-xs text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
Agent v2.0+ 支持,等待数据上报…
{t('monitor.lbl.agentWaitData')}
</div>
) : (
<div className="overflow-x-auto">
@@ -24,7 +24,7 @@ export function ProcessTable({ processTop }: { processTop: JsonData[] }) {
<th className="py-1.5 text-left font-medium w-[40%]"> {t('monitor.lbl.processMonitor')} </th>
<th className="py-1.5 text-right font-medium w-[8%]">PID</th>
<th className="py-1.5 text-right font-medium w-[16%]"> {t('monitor.lbl.processMonitor')} </th>
<th className="py-1.5 text-right font-medium w-[16%]">CPU使用率(%)</th>
<th className="py-1.5 text-right font-medium w-[16%]">{t('monitor.lbl.cpuUsagePercent')}</th>
<th className="py-1.5 text-right font-medium w-[20%]"> {t('monitor.lbl.processMonitor')} </th>
</tr>
</thead>
@@ -98,7 +98,7 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
if (!task) {
const empty = (
<div className="p-8 text-center text-sm text-[var(--dg-text-muted)]">
选择任务查看协议专属可观测指标
{t('monitor.lbl.selectTaskHint')}
</div>
);
return (
@@ -218,7 +218,7 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
</>
)}
<span className="text-[10px] text-[var(--dg-text-muted)] shrink-0">
{rangeLabel} · {results.length} 次 · 可用率 {availSummary}
{rangeLabel} · {results.length} {t('monitor.lbl.times')} · {t('monitor.lbl.availabilityRate')} {availSummary}
</span>
</div>
<div className="flex items-center gap-1.5 shrink-0">
@@ -235,7 +235,7 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
<RefreshCw className={`h-3 w-3 mr-1 ${loading ? 'animate-spin' : ''}`} />{t("common.refresh")}
</Button>
<Button size="sm" className="h-7 text-xs" onClick={handleProbeNow} disabled={probing}>
{probing ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : <Zap className="h-3 w-3 mr-1" />}立即拨测
{probing ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : <Zap className="h-3 w-3 mr-1" />}{t('monitor.lbl.probeNow')}
</Button>
</div>
</div>
@@ -244,9 +244,9 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
<div className="mb-4 flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2.5 text-xs">
<ShieldAlert className="h-4 w-4 shrink-0 mt-0.5 dg-text-yellow" />
<div>
<p className="font-medium text-[var(--dg-text-primary)]">SSL 证书预警</p>
<p className="font-medium text-[var(--dg-text-primary)]">{t('monitor.lbl.sslCertWarning')}</p>
<p className="mt-0.5 text-[var(--dg-text-muted)]">
到期 <span className="font-mono">{task.ssl_cert_expiry?.slice(0, 10) || '--'}</span>,剩余 <span className={sslInfo.tone}>{sslInfo.text}</span>(阈值 {SSL_EXPIRY_WARN_DAYS} 天)
{t('monitor.lbl.sslExpiring')} <span className="font-mono">{task.ssl_cert_expiry?.slice(0, 10) || '--'}</span>,{t('monitor.lbl.sslRemaining')} <span className={sslInfo.tone}>{sslInfo.text}</span>({t('monitor.lbl.sslThreshold')} {SSL_EXPIRY_WARN_DAYS} {t('monitor.lbl.sslDays')})
</p>
</div>
</div>