refactor(i18n): U44 残余硬编码中文清零 — 终端审计状态+凭证类型+流水线阶段+时间格式+CSV导出

20 文件 ~70 处 UI 硬编码中文清零:
- credential-manager/constants.ts: 8 凭证类型 label/desc → labelKey/descKey + i18n.t()
- pipeline-run-detail/types.ts: 6 阶段 label → i18n.t('deploy.stage.*') + labelKey
- pipeline-manager/constants.ts + pipeline-editor/types.ts: 分秒 duration → i18n.t('common.duration.minSec')
- LiveMonitorDialog: 6 终端状态消息 → t()
- RdpAuditViewer: 6 RDP审计状态 → i18n.t('monitor.rdp.*')
- WorkspaceTermDialog: 2 终端连接状态 → t()
- TerminalSessionsTab: 4 时长格式+状态标签 → i18n.t()/t()
- k8s-event-utils + QueryHistoryPanel: 6 相对时间 → i18n.t('common.duration.*Ago')
- export-audit.ts: 9 CSV导出表头 → i18n.t('system.auditExport.*')
- useAuditSettings: 4 审计保留策略消息 → i18n.t('system.auditSettings.*')
- event-handler.ts: 4 AgentCLI事件消息 → i18n.t('aiops.cli.*')
- 其他 9 文件: NotifyPolicyTab/RoleFormDialog/CronJobViewDialog/TerminalOutput/shared-components/prometheus-ds/helpers/CloudServerImport/AdvancedSettingsPanel/releaseManagerHelpers

新增 75 翻译键, zh/en 7987→8059 对齐
tsc -b=0, npm build✓ (17.13s), ESLint=0/43/any=0, go build=0, 17/17 测试通过
This commit is contained in:
attacker committed 2026-07-14 23:13:32 +08:00
1 parent bd1ccefa7f
commit 05ee051879
26 files changed
+307 -93

No files matched your search

@@ -39,7 +39,7 @@ export function createWsEventHandler(deps: WsEventHandlerDeps) {
variant: 'destructive',
});
wsRef.current?.send(JSON.stringify({ type: ok ? 'confirm' : 'deny', confirm_id: ev.confirm_id }));
if (!ok) push({ kind: 'info', content: `已拒绝执行:${ev.command || ev.tool_name}` });
if (!ok) push({ kind: 'info', content: i18n.t('aiops.cli.eventRejected', { command: ev.command || ev.tool_name }) });
}
// 处理后端事件
@@ -49,7 +49,7 @@ export function createWsEventHandler(deps: WsEventHandlerDeps) {
case 'session':
push({ kind: 'info', content: ev.message || '' });
// 仅「恢复历史会话」时进入可追问(新任务开始的 session 事件文案不同)
if (ev.message?.includes('已恢复历史会话')) {
if (ev.message?.includes(i18n.t('aiops.cli.eventRecovered'))) {
setViewMode(false);
setCanContinue(true);
}
@@ -127,7 +127,7 @@ export function createWsEventHandler(deps: WsEventHandlerDeps) {
return [...next, {
id: nextId(),
kind: 'done',
content: text || '任务已完成,请查看上方工具执行结果。',
content: text || i18n.t('aiops.cli.eventCompleted'),
round: ev.round,
}];
});
@@ -138,7 +138,7 @@ export function createWsEventHandler(deps: WsEventHandlerDeps) {
}
case 'error':
case 'aborted':
push({ kind: type === 'aborted' ? 'info' : 'error', content: ev.message || (type === 'aborted' ? '任务已中断' : '发生错误') });
push({ kind: type === 'aborted' ? 'info' : 'error', content: ev.message || (type === 'aborted' ? i18n.t('aiops.cli.eventAborted') : i18n.t('aiops.cli.eventFailed')) });
setRunning(false);
setCanContinue(type !== 'aborted');
setHistoryRefresh((k) => k + 1);
@@ -106,12 +106,12 @@ export function WorkspaceTermDialog({
ws.onclose = () => {
setConnected(false);
setHasOutput(true);
term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n');
term.write(`\r\n\x1b[31m${t('deploy.terminal.connClosed')}\x1b[0m\r\n`);
};
ws.onerror = () => {
setHasOutput(true);
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
term.write(`\r\n\x1b[31m${t('deploy.terminal.connError')}\x1b[0m\r\n`);
};
return () => {
@@ -120,6 +120,7 @@ export function WorkspaceTermDialog({
wsRef.current = null;
setConnected(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, runId]);
// 容器尺寸变化时 fit 并同步远端 PTY,避免缩放后黑边或铺不满。 // Logan.Li
@@ -3,17 +3,18 @@
import {
Key, Server, Container, Shield, Cloud, Database, FileText, Globe, Monitor,
} from 'lucide-react';
import i18n from '@/i18n';
import type { SecretType } from './types';
export const TYPE_CONFIG: Record<SecretType, { label: string; icon: typeof Key; color: string; desc: string }> = {
ssh: { label: 'SSH 密钥', icon: Server, color: 'dg-text-yellow', desc: 'SSH 私钥/公钥/密码' },
rdp: { label: 'Windows RDP 密码', icon: Monitor, color: 'dg-text-blue', desc: 'Windows 远程桌面用户名/密码' },
docker: { label: 'Docker 凭证', icon: Container, color: 'dg-text-cyan', desc: 'Docker Registry 登录凭据' },
tls: { label: 'TLS 证书', icon: Shield, color: 'dg-text-green', desc: 'TLS/SSL 证书与私钥' },
aksk: { label: 'AKSK 密钥', icon: Cloud, color: 'dg-text-purple', desc: '云服务 AccessKey/SecretKey' },
database: { label: '数据库凭证', icon: Database, color: 'dg-text-blue', desc: '数据库连接信息' },
api: { label: 'API 密钥', icon: Globe, color: 'dg-text-orange', desc: 'API Token/Key + 端点' },
generic: { label: '通用凭证', icon: FileText, color: 'dg-text-secondary', desc: '自定义 Key/Value 键值对' },
export const TYPE_CONFIG: Record<SecretType, { label: string; labelKey: string; icon: typeof Key; color: string; desc: string; descKey: string }> = {
ssh: { label: i18n.t('deploy.credType.ssh'), labelKey: 'deploy.credType.ssh', icon: Server, color: 'dg-text-yellow', desc: i18n.t('deploy.credTypeDesc.ssh'), descKey: 'deploy.credTypeDesc.ssh' },
rdp: { label: i18n.t('deploy.credType.rdp'), labelKey: 'deploy.credType.rdp', icon: Monitor, color: 'dg-text-blue', desc: i18n.t('deploy.credTypeDesc.rdp'), descKey: 'deploy.credTypeDesc.rdp' },
docker: { label: i18n.t('deploy.credType.docker'), labelKey: 'deploy.credType.docker', icon: Container, color: 'dg-text-cyan', desc: i18n.t('deploy.credTypeDesc.docker'), descKey: 'deploy.credTypeDesc.docker' },
tls: { label: i18n.t('deploy.credType.tls'), labelKey: 'deploy.credType.tls', icon: Shield, color: 'dg-text-green', desc: i18n.t('deploy.credTypeDesc.tls'), descKey: 'deploy.credTypeDesc.tls' },
aksk: { label: i18n.t('deploy.credType.aksk'), labelKey: 'deploy.credType.aksk', icon: Cloud, color: 'dg-text-purple', desc: i18n.t('deploy.credTypeDesc.aksk'), descKey: 'deploy.credTypeDesc.aksk' },
database: { label: i18n.t('deploy.credType.database'), labelKey: 'deploy.credType.database', icon: Database, color: 'dg-text-blue', desc: i18n.t('deploy.credTypeDesc.database'), descKey: 'deploy.credTypeDesc.database' },
api: { label: i18n.t('deploy.credType.api'), labelKey: 'deploy.credType.api', icon: Globe, color: 'dg-text-orange', desc: i18n.t('deploy.credTypeDesc.api'), descKey: 'deploy.credTypeDesc.api' },
generic: { label: i18n.t('deploy.credType.generic'), labelKey: 'deploy.credType.generic', icon: FileText, color: 'dg-text-secondary', desc: i18n.t('deploy.credTypeDesc.generic'), descKey: 'deploy.credTypeDesc.generic' },
};
export const TYPE_BADGE_CLASS: Record<SecretType, string> = {
@@ -1,3 +1,4 @@
import i18n from '@/i18n';
import {
GitBranch,
Wrench,
@@ -62,5 +63,5 @@ export function fmtDuration(s: number | undefined): string {
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
const sec = s % 60;
return sec > 0 ? `${m}分${sec}秒` : `${m}分`;
return sec > 0 ? i18n.t('common.duration.minSec', { m, s: sec }) : i18n.t('common.duration.min', { m });
}
@@ -32,7 +32,7 @@ export function fmtDuration(s: number | undefined): string {
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
const sec = s % 60;
return sec > 0 ? `${m}分${sec}秒` : `${m}分`;
return sec > 0 ? i18n.t('common.duration.minSec', { m, s: sec }) : i18n.t('common.duration.min', { m });
}
// 解析 stages 字段 // Logan.Li
@@ -80,7 +80,7 @@ export function AdvancedSettingsPanel({
</div>
<div className="space-y-1.5">
<Label className="text-xs">
Job 名称 <span className="text-[var(--dg-text-muted)]">(Webhook)</span>
{t('deploy.jobName')} <span className="text-[var(--dg-text-muted)]">(Webhook)</span>
</Label>
<Input
value={jobName}
@@ -1,3 +1,4 @@
import i18n from '@/i18n';
import {
CheckCircle2, Loader2, XCircle, Clock, AlertCircle,
GitBranch, Box, Container, Rocket, ShieldCheck,
@@ -9,12 +10,12 @@ export type { JsonData };
// ═══════════ 阶段定义 (与 PipelineManager 共用) ═══════════
export const STAGE_DEFS = [
{ key: 'checkout', label: '代码检出', icon: GitBranch, color: '#6366F1', order: 1 },
{ key: 'build', label: '构建', icon: Box, color: '#F59E0B', order: 2 },
{ key: 'test', label: '测试', icon: CheckCircle2, color: '#10B981', order: 3 },
{ key: 'image', label: '镜像打包', icon: Container, color: '#06B6D4', order: 4 },
{ key: 'deploy', label: '部署', icon: Rocket, color: '#EF4444', order: 5 },
{ key: 'verify', label: '验证', icon: ShieldCheck, color: '#3B82F6', order: 6 },
{ key: 'checkout', label: i18n.t('deploy.stage.checkout'), labelKey: 'deploy.stage.checkout', icon: GitBranch, color: '#6366F1', order: 1 },
{ key: 'build', label: i18n.t('deploy.stage.build'), labelKey: 'deploy.stage.build', icon: Box, color: '#F59E0B', order: 2 },
{ key: 'test', label: i18n.t('deploy.stage.test'), labelKey: 'deploy.stage.test', icon: CheckCircle2, color: '#10B981', order: 3 },
{ key: 'image', label: i18n.t('deploy.stage.image'), labelKey: 'deploy.stage.image', icon: Container, color: '#06B6D4', order: 4 },
{ key: 'deploy', label: i18n.t('deploy.stage.deploy'), labelKey: 'deploy.stage.deploy', icon: Rocket, color: '#EF4444', order: 5 },
{ key: 'verify', label: i18n.t('deploy.stage.verify'), labelKey: 'deploy.stage.verify', icon: ShieldCheck, color: '#3B82F6', order: 6 },
].sort((a, b) => a.order - b.order);
export interface PipelineRun {
@@ -14,10 +14,10 @@ import {
// 环境配置(值与后端 oneof=dev test staging prod 一致) // Logan.Li
export const ENVIRONMENTS: Record<string, { label: string; color: string }> = {
dev: { label: '开发环境', color: 'border-blue-500/30 dg-text-blue bg-blue-500/10' },
test: { label: '测试环境', color: 'border-cyan-500/30 dg-text-cyan bg-cyan-500/10' },
staging: { label: '预发环境', color: 'border-amber-500/30 dg-text-yellow bg-amber-500/10' },
prod: { label: '生产环境', color: 'border-red-500/30 dg-text-red bg-red-500/10' },
dev: { label: 'dev', color: 'border-blue-500/30 dg-text-blue bg-blue-500/10' },
test: { label: 'test', color: 'border-cyan-500/30 dg-text-cyan bg-cyan-500/10' },
staging: { label: 'staging', color: 'border-amber-500/30 dg-text-yellow bg-amber-500/10' },
prod: { label: 'prod', color: 'border-red-500/30 dg-text-red bg-red-500/10' },
};
const ENV_LABEL_KEY: Record<string, string> = {
@@ -61,10 +61,10 @@ export function formatEventRelative(ts: string): string {
if (diff < 0) return '';
const mins = Math.floor(diff / 60000);
if (mins < 1) return i18n.t('task.cronJob.time.justNow');
if (mins < 60) return `${mins}分钟前`;
if (mins < 60) return i18n.t('common.duration.minAgo', { count: mins });
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}小时前`;
return `${Math.floor(hrs / 24)}天前`;
if (hrs < 24) return i18n.t('common.duration.hourAgo', { count: hrs });
return i18n.t('common.duration.dayAgo', { count: Math.floor(hrs / 24) });
}
/** 重复跨度:首次 → 最后 // Logan.Li */
@@ -54,7 +54,7 @@ export function ContainerInfo({ c }: { c: JsonData }) {
CPU: <span className="text-[var(--dg-text-primary)] font-mono">{c.resources?.requests?.cpu || '-'}/{c.resources?.limits?.cpu || '-'}</span>
</div>
<div className="text-[var(--dg-text-secondary)]">
内存: <span className="text-[var(--dg-text-primary)] font-mono">{c.resources?.requests?.memory || '-'}/{c.resources?.limits?.memory || '-'}</span>
{i18n.t('k8s.workloadView.memoryLabel')}: <span className="text-[var(--dg-text-primary)] font-mono">{c.resources?.requests?.memory || '-'}/{c.resources?.limits?.memory || '-'}</span>
</div>
<div className="text-[var(--dg-text-secondary)]">
{i18n.t('k8s.workload.portsLabel')}: <span className="text-[var(--dg-text-primary)] font-mono">
@@ -1,6 +1,7 @@
// 告警集合通知策略 — 四类监控绑定联系组 + 消息模版 // Logan.Li
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
import { Server, Database, Globe, Network, Bell, Zap, Loader2, Save, FileText } from 'lucide-react';
import { SilenceRulesPanel } from './SilenceRulesPanel';
import { toast } from 'sonner';
@@ -41,7 +42,7 @@ const CATEGORIES = [
const DEFAULT_TITLE = '[{{severity_label}}] {{rule_name}} — {{hostname}} ({{metric}})';
const DEFAULT_FOOTER = '{{time}} | {{platform}}';
const DEFAULT_PLATFORM = 'AIOps 智能运维平台';
const DEFAULT_PLATFORM = i18n.t('monitor.notify.defaultPlatform');
const TEMPLATE_VARS = [
'{{time}}', '{{platform}}', '{{rule_name}}', '{{hostname}}',
@@ -105,7 +105,7 @@ export async function handleTestQuick(ds: PromDS): Promise<void> {
skip_tls_verify: ds.skip_tls_verify,
});
if (data?.success) {
toast.success(`连接成功 · ${data.target_count || 0} targets`);
toast.success(i18n.t('monitor.prometheus.connSuccess', { count: data.target_count || 0 }));
} else {
toast.error(data?.message || i18n.t('aiops.llmTestFailed'));
}
@@ -22,9 +22,9 @@ const ENGINE_ICONS: Record<string, { Icon: typeof Flame; color: string }> = {
function relativeTime(ts: number): string {
const diff = Date.now() - ts;
if (diff < 60000) return i18n.t('task.cronJob.time.justNow');
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`;
return `${Math.floor(diff / 86400000)}天前`;
if (diff < 3600000) return i18n.t('common.duration.minAgo', { count: Math.floor(diff / 60000) });
if (diff < 86400000) return i18n.t('common.duration.hourAgo', { count: Math.floor(diff / 3600000) });
return i18n.t('common.duration.dayAgo', { count: Math.floor(diff / 86400000) });
}
export function QueryHistoryPanel({ visible, onClose, onSelect, onCreateAlert }: Props) {
@@ -170,10 +170,11 @@ export function CloudServerImport({ open, onClose, initialAccountId }: CloudServ
setDiscoverErrors((res?.errors as string[]) || []);
} catch (e) {
console.error('发现主机失败:', e);
setDiscoverErrors([(e as Error)?.message || '发现主机失败']);
setDiscoverErrors([(e as Error)?.message || t('server.import.discoverFailed')]);
} finally {
setIsDiscovering(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAccount, selectedRegions]);
// ==================== 导入逻辑 ====================
@@ -325,7 +325,7 @@ export function CredentialManager() {
{/* 内容 */}
<div>
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-1">
{formData.type === 'ssh_key' ? 'SSH 私钥' : formData.type === 'api_token' ? 'Token' : formData.type === 'access_key' ? 'AK/SK (每行一对,用空格或逗号分隔)' : '密码'}
{formData.type === 'ssh_key' ? t('system.credTypeLabel.sshKey') : formData.type === 'api_token' ? t('system.credTypeLabel.apiToken') : formData.type === 'access_key' ? 'AK/SK (每行一对,用空格或逗号分隔)' : t('system.credTypeLabel.password')}
{!editingId && <span className="dg-text-red"> *</span>}
{editingId && <span className="text-xs text-[var(--dg-text-muted)] ml-1">{t('system.leaveBlankNoUpdate')}</span>}
</label>
@@ -102,7 +102,7 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
setConnected(true);
setConnectError('');
mountXterm(term, fitAddon, () => containerRef.current);
term.writeln('\x1b[36m[审计监控] 已连接到会话...\x1b[0m');
term.writeln(`\x1b[36m${t('monitor.terminal.auditConnected')}\x1b[0m`);
};
ws.onmessage = (event) => {
@@ -112,7 +112,7 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
case 'session_info':
setMeta(msg.payload);
term.writeln(
`\x1b[36m用户: ${msg.payload.username} 服务器: ${msg.payload.server_name} (${msg.payload.server_ip})\x1b[0m`,
`\x1b[36m${t('monitor.terminal.userServer', { username: msg.payload.username, serverName: msg.payload.server_name, serverIp: msg.payload.server_ip })}\x1b[0m`,
);
break;
case 'history':
@@ -127,14 +127,14 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
}
};
ws.onerror = () => setConnectError('WebSocket 连接失败');
ws.onerror = () => setConnectError(t('monitor.terminal.wsConnFailed'));
ws.onclose = (e) => {
if (!didOpen) {
setConnectError(`连接失败 (code=${e.code}): ${e.reason || '认证失败或网络问题'}`);
setConnectError(t('monitor.terminal.connFailedCode', { code: e.code, reason: e.reason || t('monitor.terminal.authOrNetwork') }));
}
setConnected(false);
term.writeln('\r\n\x1b[33m[审计监控] 连接已断开\x1b[0m');
term.writeln(`\r\n\x1b[33m${t('monitor.terminal.auditDisconnected')}\x1b[0m`);
};
const timeout = setTimeout(() => {
@@ -153,6 +153,7 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
wsRef.current = null;
setConnected(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, sessionId]);
// 容器尺寸变化时持续 fit // Logan.Li
@@ -126,7 +126,7 @@ export function RdpAuditViewer({ open, onOpenChange, sessionId, mode }: RdpAudit
useEffect(() => {
if (!open || !sessionId) return;
setStatus(mode === 'live' ? '正在连接 RDP 实时审计...' : '正在加载 RDP 录屏...');
setStatus(mode === 'live' ? i18n.t('monitor.rdp.connectingLive') : i18n.t('monitor.rdp.loadingRecording'));
setLoading(true);
setFrames([]);
setIndex(0);
@@ -161,8 +161,8 @@ export function RdpAuditViewer({ open, onOpenChange, sessionId, mode }: RdpAudit
/* ignore */
}
};
ws.onerror = () => { setLoading(false); setStatus('RDP 实时审计连接失败'); };
ws.onclose = () => setStatus((prev) => prev || 'RDP 实时审计已断开');
ws.onerror = () => { setLoading(false); setStatus(i18n.t('monitor.rdp.liveConnFailed')); };
ws.onclose = () => setStatus((prev) => prev || i18n.t('monitor.rdp.liveDisconnected'));
return () => {
ws.close();
wsRef.current = null;
@@ -182,13 +182,13 @@ export function RdpAuditViewer({ open, onOpenChange, sessionId, mode }: RdpAudit
setCanvasSize({ width: sizeFrame.width, height: sizeFrame.height });
}
setFrames(list);
setStatus(list.length ? '' : '该 RDP 会话没有可播放画面帧');
setStatus(list.length ? '' : i18n.t('monitor.rdp.noFrames'));
setPlaying(list.length > 0);
setLoading(false);
})
.catch(() => {
if (!cancelled) {
setStatus('RDP 录屏加载失败');
setStatus(i18n.t('monitor.rdp.loadFailed'));
setLoading(false);
}
});
@@ -45,7 +45,7 @@ const MODULE_GROUPS: { key: string; label: string; prefixes: string[] }[] = [
{ key: 'deploy', label: i18n.t('common.menuApp'), prefixes: ['deploy:'] },
{ key: 'monitor', label: i18n.t('common.menuMonitoring'), prefixes: ['monitor:', 'agent:'] },
{ key: 'auto', label: i18n.t('common.menuAutomation'), prefixes: ['auto:'] },
{ key: 'aiops', label: '🤖 AIOps智能', prefixes: ['aiops:'] },
{ key: 'aiops', label: i18n.t('system.role.aiopsGroup'), prefixes: ['aiops:'] },
{ key: 'approval', label: i18n.t('common.menuApproval'), prefixes: ['approval:'] },
{ key: 'audit', label: i18n.t('common.menuAudit'), prefixes: ['audit:'] },
{ key: 'system', label: i18n.t('common.menuSystem'), prefixes: ['system:'] },
@@ -17,16 +17,17 @@ import { useTerminalSessions, useTerminalSessionMutations } from '@/hooks/api/us
import { useConfirm } from '@/components/ui/confirm-dialog';
import { ReplayDialog } from './ReplayDialog';
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
function formatDuration(sec: number): string {
if (!sec || sec < 0) return '--';
if (sec < 60) return `${sec}秒`;
if (sec < 60) return i18n.t('common.duration.seconds', { count: sec });
const m = Math.floor(sec / 60);
const s = sec % 60;
if (m < 60) return `${m}分${s > 0 ? `${s}秒` : ''}`;
if (m < 60) return s > 0 ? i18n.t('common.duration.minSec', { m, s }) : i18n.t('common.duration.min', { m });
const h = Math.floor(m / 60);
const rm = m % 60;
return `${h}时${rm > 0 ? `${rm}分` : ''}`;
return rm > 0 ? i18n.t('common.duration.hoursMin', { h, m: rm }) : i18n.t('common.duration.hour', { count: h });
}
function formatSize(bytes: number): string {
@@ -170,7 +171,7 @@ export function TerminalSessionsTab() {
}
onClick={() => setStatusFilter(st)}
>
{st === '' ? '全部' : st === 'active' ? '活动中' : st === 'closed' ? '已关闭' : '失败'}
{st === '' ? t('system.terminalSession.all') : st === 'active' ? t('system.terminalSession.active') : st === 'closed' ? t('system.terminalSession.closed') : t('system.terminalSession.failed')}
</Button>
))}
</div>
@@ -24,14 +24,14 @@ function escapeCsv(s: string): string {
export function exportAuditReportCsv(meta: ReplaySessionMeta, commands: CommandBookmark[]) {
const lines: string[] = [
i18n.t('audit.reportTitle'),
`会话ID,${meta.id}`,
`用户,${escapeCsv(meta.username || '')}`,
`目标主机,${escapeCsv(meta.server_name || '')}`,
`${i18n.t('system.auditExport.sessionId')},${meta.id}`,
`${i18n.t('system.auditExport.user')},${escapeCsv(meta.username || '')}`,
`${i18n.t('system.auditExport.targetHost')},${escapeCsv(meta.server_name || '')}`,
`IP,${meta.server_ip}:${meta.server_port}`,
`客户端IP,${meta.client_ip || ''}`,
`开始时间,${meta.start_time || ''}`,
`时长(秒),${meta.duration || 0}`,
`录制大小,${meta.record_size || 0}`,
`${i18n.t('system.auditExport.clientIp')},${meta.client_ip || ''}`,
`${i18n.t('system.auditExport.startTime')},${meta.start_time || ''}`,
`${i18n.t('system.auditExport.duration')},${meta.duration || 0}`,
`${i18n.t('system.auditExport.status')},${meta.record_size || 0}`,
'',
i18n.t('audit.reportTitle'),
];
@@ -107,8 +107,8 @@ export function useAuditSettings() {
const total = res.cleanup?.total ?? 0;
toast.success(
total > 0
? `已应用 ${days} 天保留策略,立即清理 ${total} 条超期记录`
: `已应用 ${days} 天保留策略,暂无超期记录需清理`,
? i18n.t('system.auditSettings.appliedWithCleanup', { days, total })
: i18n.t('system.auditSettings.appliedNoCleanup', { days }),
);
} catch (e: unknown) {
toast.error(
@@ -131,16 +131,13 @@ export function useAuditSettings() {
total?: number;
};
if (typeof s.total === "number") {
detail =
s.total > 0
? `,清理 ${s.total} 条`
: i18n.t("system.auditSetting.noExpiredRecords");
detail = s.total > 0 ? i18n.t('system.auditSettings.cleanupCount', { count: s.total }) : i18n.t("system.auditSetting.noExpiredRecords");
}
} catch {
// ignore
}
}
return `上次清理:${t}${detail}`;
return i18n.t('system.auditSettings.lastCleanup', { time: t, detail });
})();
return {
@@ -87,7 +87,7 @@ export function CronJobViewDialog({ open, onOpenChange, job }: Props) {
{job.script?.name && (
<div className="text-xs dg-text-secondary">
关联脚本: <span className="text-[var(--dg-text-primary)]">{job.script.name}</span>
{t('task.cronJob.associatedScript')}: <span className="text-[var(--dg-text-primary)]">{job.script.name}</span>
</div>
)}
@@ -101,7 +101,7 @@ export function CronJobViewDialog({ open, onOpenChange, job }: Props) {
/>
) : (
<pre className="rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)] p-4 text-xs font-mono text-[var(--dg-text-primary)] whitespace-pre-wrap break-all max-h-96 overflow-auto">
{content || '(无命令内容)'}
{content || t('task.cronJob.noCommand')}
</pre>
)}
</div>
@@ -33,7 +33,7 @@ export function TerminalOutput({
status,
durationMs,
startedAt,
emptyText = '(无输出)',
emptyText = i18n.t('task.terminal.noOutput'),
className,
maxHeight = 'min(28rem, 55vh)',
variant = 'plain',