refactor(i18n): U36 全量 JSX 属性 + toast 硬编码中文清零 — 20 文件 30 处 → 0

清理 23 处 JSX 属性 (title/placeholder/label/description) + 7 处 toast 调用硬编码中文:
- Deploy 模块 (AppRelease/DeployCredentials/PipelineManager/WorkspaceTermDialog/CredFormDialog/JobRoleCreateDialog/PipelineCreateDialog/AdvancedSettingsPanel)
- Server 模块 (ServerForm)
- System 模块 (LdapManagementSection/LiveMonitorDialog/SystemManager)
- Audit 模块 (ActiveSessionsPanel/HistorySessionsPanel)
- Monitor 模块 (AlertRulesTab/PrometheusDatasourceManager/SNMPMonitor)
- AIOps 模块 (PredictiveCapacityTab)
- Common/UI (TerminalLoadingOverlay/copy-button)

新增 25 翻译键 (common.connectingEllipsis/enteringWorkspace/connectingMonitor + deploy.* 11键 + server.jsonFormatPlaceholder + system.* 3键 + audit.* 2键 + monitor.* 3键 + aiops.aiAnalysisFailedToast + pipeline.subtitle)
copy-button.tsx 从 import i18n → useTranslation() hook
3 组件新增 useTranslation hook (TerminalLoadingOverlay/WorkspaceTermDialog/LiveMonitorDialog/PredictiveCapacityTab)

zh/en 7616→7640 +24 对齐, tsc -b=0, npm build✓ (17.25s), ESLint=0/43/0, go build=0, 17/17 测试通过
This commit is contained in:
attacker committed 2026-07-13 04:20:10 +08:00
1 parent 8d64936caf
commit 09b7fd4ad5
22 files changed
+114 -55

No files matched your search

@@ -4,6 +4,7 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { aiApi } from '@/api';
import { buildCapacityInsightPrompt, type CapacityForecast } from './predictUtils';
import { AgentMarkdown, looksLikeMarkdown } from './AgentMarkdown';
@@ -15,6 +16,7 @@ export function PredictiveCapacityTab({
forecasts: CapacityForecast[];
loading: boolean;
}) {
const { t } = useTranslation();
const [insight, setInsight] = useState('');
const [analyzing, setAnalyzing] = useState(false);
@@ -28,7 +30,7 @@ export function PredictiveCapacityTab({
const res = await aiApi.chat([{ role: 'user', content: prompt }]);
setInsight(res?.content || '');
} catch {
toast.error('AI 解读失败,请检查大模型配置或稍后重试');
toast.error(t('aiops.aiAnalysisFailedToast'));
} finally {
setAnalyzing(false);
}
@@ -1,5 +1,6 @@
import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
interface TerminalLoadingOverlayProps {
show: boolean;
@@ -7,14 +8,15 @@ interface TerminalLoadingOverlayProps {
className?: string;
}
export function TerminalLoadingOverlay({ show, label = '连接中...', className }: TerminalLoadingOverlayProps) {
export function TerminalLoadingOverlay({ show, label, className }: TerminalLoadingOverlayProps) {
const { t } = useTranslation();
if (!show) return null;
return (
<div className={cn('absolute inset-0 z-10 flex items-center justify-center bg-black/35', className)}>
<div className="flex flex-col items-center gap-2 text-white/85">
<Loader2 className="h-7 w-7 animate-spin" />
{label && <span className="text-xs">{label}</span>}
{label && <span className="text-xs">{label || t('common.connectingEllipsis')}</span>}
</div>
</div>
);
@@ -100,7 +100,7 @@ export function AppReleaseManager() {
</div>
<Select value={envFilter} onValueChange={setEnvFilter}>
<SelectTrigger className="w-32 h-9 dg-input">
<SelectValue placeholder="环境" />
<SelectValue placeholder={t('deploy.placeholderEnv')} />
</SelectTrigger>
<SelectContent className="dg-input">
<SelectItem value="all">{t('deploy.filterAllEnv')}</SelectItem>
@@ -112,7 +112,7 @@ export function AppReleaseManager() {
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-32 h-9 dg-input">
<SelectValue placeholder="状态" />
<SelectValue placeholder={t('deploy.placeholderStatus')} />
</SelectTrigger>
<SelectContent className="dg-input">
<SelectItem value="all">{t('deploy.filterAllStatus')}</SelectItem>
@@ -275,8 +275,8 @@ export default function DeployCredentials() {
{environments.length === 0 ? (
<PageEmpty
icon={<Globe className="h-10 w-10" />}
title="暂无环境配置"
description='点击"新增环境"创建'
title={t('deploy.noEnvConfig')}
description={t('deploy.clickEnvConfigHint')}
action={(
<Button size="sm" onClick={() => { setEditEnv(null); setEnvDialogOpen(true); }}>
<Plus className="w-4 h-4 mr-1" />新增环境
@@ -214,8 +214,8 @@ export function PipelineManager() {
return (
<PageShell
variant="list"
title="流水线"
subtitle="CI/CD 构建、镜像打包与双通道部署"
title={t('pipeline.title')}
subtitle={t('pipeline.subtitle')}
icon={<Workflow className="h-5 w-5" />}
loading={loading}
toolbar={toolbar}
@@ -13,6 +13,7 @@ import { cn } from '@/lib/utils';
import { attachTerminalResize, mountXterm } from '@/lib/xterm-utils';
import { useUserStore } from '@/store';
import { pipelineApi } from '@/api';
import { useTranslation } from 'react-i18next';
import { TerminalLoadingOverlay } from '@/components/common/TerminalLoadingOverlay';
import type { JsonData } from '@/types/common';
@@ -32,6 +33,7 @@ export function WorkspaceTermDialog({
runId,
buildNumber,
}: WorkspaceTermDialogProps) {
const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<XTerm | null>(null);
const wsRef = useRef<WebSocket | null>(null);
@@ -186,7 +188,7 @@ export function WorkspaceTermDialog({
</div>
<div className="relative bg-[#0a0e14]" style={{ height: fullscreen ? 'calc(100vh - 49px)' : '480px' }}>
<div ref={containerRef} className="xterm-terminal-host h-full" />
<TerminalLoadingOverlay show={!hasOutput} label="正在进入工作区..." />
<TerminalLoadingOverlay show={!hasOutput} label={t('common.enteringWorkspace')} />
</div>
</DialogContent>
</Dialog>
@@ -123,8 +123,8 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
if (sshPrivateKey.trim()) payload.ssh_private_key = sshPrivateKey.trim();
if (sshPublicKey.trim()) payload.ssh_public_key = sshPublicKey.trim();
} else if (secretType === 'rdp') {
if (!rdpUsername.trim()) { toast.error('请填写 RDP 用户'); return; }
if (!isEdit && !rdpPassword.trim()) { toast.error('请填写 RDP 密码'); return; }
if (!rdpUsername.trim()) { toast.error(t('deploy.rdpUserRequired')); return; }
if (!isEdit && !rdpPassword.trim()) { toast.error(t('deploy.rdpPasswordRequired')); return; }
if (rdpUsername.trim()) payload.rdp_username = rdpUsername.trim();
if (rdpPassword.trim()) payload.rdp_password = rdpPassword.trim();
if (rdpDomain.trim()) payload.rdp_domain = rdpDomain.trim();
@@ -310,11 +310,11 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
</Select>
</div>
{/* 用户名(左上)与密码(左下)同列上下行,安全模式/域归右列 // Logan.Li */}
<div><FieldWithToggle label="Windows 密码" value={rdpPassword} onChange={setRdpPassword}
placeholder="Windows 登录密码" fieldKey="rdp_pwd" required /></div>
<div><FieldWithToggle label={t('deploy.rdpPassword')} value={rdpPassword} onChange={setRdpPassword}
placeholder={t('deploy.rdpPasswordPlaceholder')} fieldKey="rdp_pwd" required /></div>
<div className="space-y-1">
<Label className="text-[var(--dg-text-secondary)] text-xs">域</Label>
<Input value={rdpDomain} onChange={e => setRdpDomain(e.target.value)} placeholder="可选"
<Input value={rdpDomain} onChange={e => setRdpDomain(e.target.value)} placeholder={t('common.optional')}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
</div>
</div>
@@ -89,7 +89,7 @@ export function JobRoleCreateDialog({
<Input
value={pattern}
onChange={(e) => onPatternChange(e.target.value)}
placeholder="^cex-.* 或 ^api-server$"
placeholder={t('deploy.jobNamePatternHint')}
className="h-8 text-sm font-mono dg-input"
/>
<p className="text-[10px] text-[var(--dg-text-muted)]">{patternHint}</p>
@@ -371,7 +371,7 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
onValueChange={(v) => setNewGitCredentialId(v === '__none' ? '' : v)}
>
<SelectTrigger className="h-8 text-xs dg-input">
<SelectValue placeholder="选择凭证" />
<SelectValue placeholder={t('deploy.selectCredentialPlaceholder')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">{t('deploy.notLinked')}</SelectItem>
@@ -396,7 +396,7 @@ export function PipelineCreateDialog({ open, onOpenChange, onCreated }: Pipeline
onValueChange={(v) => setNewRegistryCredentialId(v === '__none' ? '' : v)}
>
<SelectTrigger className="h-8 text-xs dg-input">
<SelectValue placeholder="Docker 凭证" />
<SelectValue placeholder={t('deploy.dockerCredPlaceholder')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">不关联</SelectItem>
@@ -106,7 +106,7 @@ export function AdvancedSettingsPanel({
<Input
value={watchBranches}
onChange={(e) => onWatchBranchesChange(e.target.value)}
placeholder="监听分支,如 main, release/*"
placeholder={t('deploy.branchListenPlaceholder')}
className="h-8 text-xs font-mono dg-input"
/>
)}
@@ -82,7 +82,7 @@ export function AlertRulesTab({
if (!ok) return;
try {
await alertApi.batchDeleteRules(selectedIds);
toast.success(`已删除 ${selectedIds.length} 条告警规则`);
toast.success(i18n.t('monitor.deletedRules', { count: selectedIds.length }));
setSelectedIds([]);
onReload();
} catch (error) {
@@ -65,7 +65,7 @@ export default function PrometheusDatasourceManager() {
const handleSetDefault = async (ds: PromDS) => {
try {
await setDefaultMutation.mutateAsync(ds.id);
toast.success(`已将「${ds.name}」设为默认数据源`);
toast.success(i18n.t('monitor.setDefaultDsSuccess', { name: ds.name }));
} catch {
toast.error(i18n.t('monitor.promDs2.deleteFailed'));
}
@@ -396,7 +396,7 @@ export function ServerForm({
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">{t('server.formTags')}</Label>
<Input
placeholder='JSON 格式,如 {"project":"web","team":"ops"}'
placeholder={t('server.jsonFormatPlaceholder')}
value={formData.tags}
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] font-mono text-sm"
@@ -288,7 +288,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
</div>
<div>
<label className="text-xs dg-text-muted mb-1 block">{t('system.userDisplayName')}</label>
<Input value={newUser.display_name} onChange={(e) => setNewUser({ ...newUser, display_name: e.target.value })} placeholder="张三" className="dg-input" />
<Input value={newUser.display_name} onChange={(e) => setNewUser({ ...newUser, display_name: e.target.value })} placeholder={t('system.namePlaceholder')} className="dg-input" />
</div>
<div>
<label className="text-xs dg-text-muted mb-1 block">{t('system.userEmail')}</label>
@@ -450,7 +450,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
</div>
<div>
<label className="text-xs dg-text-muted mb-1 block">{t('system.jobTitle')}</label>
<Input value={editLdapForm.title} onChange={(e) => setEditLdapForm({ ...editLdapForm, title: e.target.value })} placeholder="运维工程师" className="dg-input" />
<Input value={editLdapForm.title} onChange={(e) => setEditLdapForm({ ...editLdapForm, title: e.target.value })} placeholder={t('system.roleTitlePlaceholder')} className="dg-input" />
</div>
</div>
)}
@@ -1,5 +1,6 @@
// 终端审计实时监控 Dialog — 与 WebSSH 终端弹框一致的布局与适配 // Logan.Li
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Terminal as XTerm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
@@ -33,6 +34,7 @@ interface LiveMonitorProps {
}
export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }: LiveMonitorProps) {
const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<XTerm | null>(null);
const fitRef = useRef<FitAddon | null>(null);
@@ -250,7 +252,7 @@ export function LiveMonitorDialog({ open, onOpenChange, sessionId, sessionInfo }
<div className="relative bg-[#0a0e14]" style={{ height: fullscreen ? 'calc(100vh - 49px)' : '520px' }}>
<div ref={containerRef} className="xterm-terminal-host h-full" />
<TerminalLoadingOverlay show={!connected && !connectError} label="正在连接监控..." />
<TerminalLoadingOverlay show={!connected && !connectError} label={t('common.connectingMonitor')} />
</div>
</DialogContent>
</Dialog>
@@ -237,7 +237,7 @@ export function SystemManager() {
const res = (await userApi.batchDelete(selectedUserIds)) as {
message?: string;
};
toast.success(res?.message || `已删除 ${selectedUserIds.length} 个用户`);
toast.success(res?.message || t('system.batchDeleteUsers', { count: selectedUserIds.length }));
setSelectedUserIds([]);
await loadUsers();
} catch (error: unknown) {
+11 -8
View File
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
import { useToast } from '@/hooks/use-toast';
import { copyToClipboard } from '@/utils/clipboard';
import { cn } from '@/lib/utils';
import i18n from '@/i18n';
import { useTranslation } from 'react-i18next';
export interface CopyButtonProps {
text: string;
@@ -22,14 +22,17 @@ export interface CopyButtonProps {
export function CopyButton({
text,
label = '已复制',
label,
children,
className,
size = 'sm',
variant = 'outline',
disabled,
title = '复制',
title,
}: CopyButtonProps) {
const { t } = useTranslation();
const resolvedLabel = label ?? t('common.copied');
const resolvedTitle = title ?? t('common.copy');
const { toast } = useToast();
const [copied, setCopied] = useState(false);
@@ -37,12 +40,12 @@ export function CopyButton({
const ok = await copyToClipboard(text);
if (ok) {
setCopied(true);
toast({ title: label });
toast({ title: resolvedLabel });
setTimeout(() => setCopied(false), 2000);
} else {
toast({
title: i18n.t('monitor.logPanel.copyFailed'),
description: i18n.t('common.copyManually'),
title: t('monitor.logPanel.copyFailed'),
description: t('common.copyManually'),
variant: 'destructive',
});
}
@@ -61,8 +64,8 @@ export function CopyButton({
)}
onClick={handleCopy}
disabled={disabled || !text}
title={title}
aria-label={title}
title={resolvedTitle}
aria-label={resolvedTitle}
>
{copied ? (
<Check className={cn('dg-text-green', children ? 'h-3.5 w-3.5 mr-1' : 'h-3.5 w-3.5')} />