Localize LLDP and topology classify English leftovers.

Translate job status/trigger/scope, item result, edge labels, and classify role/source option text in zh/en.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-05 21:28:17 +08:00
parent 617d4245b0
commit 7cd89aaae9
4 changed files with 167 additions and 24 deletions

View file

@ -389,6 +389,34 @@ const en = {
bPort: "Port B",
lastSeen: "Last seen",
},
jobStatus: {
running: "Running",
pending: "Pending",
paused: "Paused",
done: "Done",
failed: "Failed",
cancelled: "Cancelled",
},
triggerMode: {
manual: "Manual",
schedule: "Scheduled",
topology: "Topology canvas",
},
jobScope: {
all: "All NEs",
ne_ids: "Selected NEs",
selected: "Selected NEs",
},
itemResult: {
ok: "OK",
warn: "Warn",
fail: "Fail",
},
edgeSource: {
lldp: "LLDP",
manual: "Manual",
stale: "Stale",
},
},
configSync: {
title: "Config sync",
@ -1601,6 +1629,15 @@ const en = {
linkUme: "UME",
linkBoth: "Managed+UME",
linkOrphaned: "Detached",
roleCore: "Core",
roleAggregation: "Aggregation",
roleAccess: "Access",
roleUnknown: "Unknown",
sourceManual: "Manual",
sourceUmeSync: "UME sync",
sourceLldp: "LLDP placeholder",
sourceTopology: "Topology placeholder",
sourceWebcrt: "WebCRT",
regexTitle: "Regex find",
patternPh: "Regex, e.g. ^CORE- or -BJ-",
fieldName: "Name",

View file

@ -385,6 +385,34 @@ const zh = {
bPort: "对端口",
lastSeen: "最近看见",
},
jobStatus: {
running: "运行中",
pending: "排队中",
paused: "已暂停",
done: "已完成",
failed: "失败",
cancelled: "已取消",
},
triggerMode: {
manual: "手动",
schedule: "周期",
topology: "拓扑画布",
},
jobScope: {
all: "全部网元",
ne_ids: "指定网元",
selected: "指定网元",
},
itemResult: {
ok: "成功",
warn: "警告",
fail: "失败",
},
edgeSource: {
lldp: "LLDP",
manual: "手动",
stale: "陈旧",
},
},
configSync: {
title: "配置同步",
@ -1586,10 +1614,19 @@ const zh = {
filterLinkLinked: "已关联库存",
filterLinkOrphaned: "已解绑",
colLink: "关联",
linkManaged: "Managed",
linkManaged: "托管",
linkUme: "UME",
linkBoth: "Managed+UME",
linkBoth: "托管+UME",
linkOrphaned: "已解绑",
roleCore: "核心",
roleAggregation: "汇聚",
roleAccess: "接入",
roleUnknown: "未知",
sourceManual: "手动",
sourceUmeSync: "UME 同步",
sourceLldp: "LLDP 占位",
sourceTopology: "拓扑占位",
sourceWebcrt: "WebCRT",
regexTitle: "正则查找",
patternPh: "正则,如 ^CORE- 或 -BJ-",
fieldName: "名称",

View file

@ -25,6 +25,51 @@ const POLL_MS = 2500;
const TARGET_PAGE_SIZE = 20;
const SEP = " · ";
function lldpJobStatusLabel(t: (k: string) => string, status: string): string {
const s = String(status || "").trim().toLowerCase();
const key = `lldpLinks.jobStatus.${s}`;
const label = t(key);
return label === key ? status || "—" : label;
}
function lldpTriggerLabel(t: (k: string) => string, mode: string): string {
const s = String(mode || "").trim().toLowerCase();
const key = `lldpLinks.triggerMode.${s}`;
const label = t(key);
return label === key ? mode || "—" : label;
}
function lldpScopeLabel(t: (k: string) => string, scope: string): string {
const s = String(scope || "").trim().toLowerCase();
const key = `lldpLinks.jobScope.${s}`;
const label = t(key);
return label === key ? scope || "—" : label;
}
function lldpItemResultLabel(
t: (k: string) => string,
it: { ok?: boolean; parser_stub?: boolean | string | null; unmatched_count?: number; unmatched?: unknown[] },
): string {
if (!it.ok) return t("lldpLinks.itemResult.fail");
const unmatchedCount = it.unmatched_count ?? (it.unmatched?.length || 0);
if (it.parser_stub || unmatchedCount > 0) return t("lldpLinks.itemResult.warn");
return t("lldpLinks.itemResult.ok");
}
function lldpEdgeStatusLabel(t: (k: string) => string, status: string): string {
const s = status === "stale" ? "missing" : String(status || "").trim().toLowerCase();
if (s === "active") return t("lldpLinks.edgeStatusActive");
if (s === "missing") return t("lldpLinks.edgeStatusMissing");
return status || "—";
}
function lldpEdgeSourceLabel(t: (k: string) => string, source: string): string {
const s = String(source || "").trim().toLowerCase();
const key = `lldpLinks.edgeSource.${s}`;
const label = t(key);
return label === key ? source || "—" : label;
}
export function LldpLinksPage() {
const { t } = useI18n();
const { showOk, showError } = useToast();
@ -545,7 +590,7 @@ export function LldpLinksPage() {
const st = e.status === "stale" ? "missing" : e.status;
return (
<tr key={e.id}>
<td>{st}</td>
<td>{lldpEdgeStatusLabel(t, st)}</td>
<td>
<div>{aLabel}</div>
{e.a_ip && e.a_name ? <div className="muted">{e.a_ip}</div> : null}
@ -556,7 +601,7 @@ export function LldpLinksPage() {
{e.b_ip && e.b_name ? <div className="muted">{e.b_ip}</div> : null}
</td>
<td>{e.b_port || "—"}</td>
<td>{e.source}</td>
<td>{lldpEdgeSourceLabel(t, e.source)}</td>
<td>
{missCount || (st === "missing" ? 1 : 0)}
{replaced ? (
@ -635,8 +680,8 @@ export function LldpLinksPage() {
</button>
</td>
<td className="mono">{job.id.slice(0, 8)}</td>
<td>{job.trigger_mode}</td>
<td>{job.scope}</td>
<td>{lldpTriggerLabel(t, job.trigger_mode)}</td>
<td>{lldpScopeLabel(t, job.scope)}</td>
<td>
<span
className={
@ -647,7 +692,7 @@ export function LldpLinksPage() {
: undefined
}
>
{job.status}
{lldpJobStatusLabel(t, job.status)}
</span>
</td>
<td>
@ -744,7 +789,7 @@ export function LldpLinksPage() {
{(() => {
const job = jobDetailQuery.data;
if (job) {
return `${job.id.slice(0, 8)} · ${job.status} · ${job.done}/${job.total}`;
return `${job.id.slice(0, 8)} · ${lldpJobStatusLabel(t, job.status)} · ${job.done}/${job.total}`;
}
if (jobDetailQuery.isLoading) {
return `${expandedJobId.slice(0, 8)} · …`;
@ -793,7 +838,7 @@ export function LldpLinksPage() {
<td>{it.ne_name || it.ne_id || "—"}</td>
<td>{it.ne_ip || "—"}</td>
<td>
{!it.ok ? "fail" : it.parser_stub || unmatchedCount > 0 ? "warn" : "ok"}
{lldpItemResultLabel(t, it)}
</td>
<td>{it.neighbors}</td>
<td>

View file

@ -37,6 +37,27 @@ function isFabricNodeDeletable(n: FabricNodeSearchHit): boolean {
return src === "lldp" || src === "topology" || src === "webcrt";
}
const ROLE_OPTIONS = ["core", "aggregation", "access", "unknown"] as const;
function roleLabel(t: (k: string) => string, role: string): string {
const s = String(role || "").trim().toLowerCase();
if (s === "core") return t("topoClassify.roleCore");
if (s === "aggregation") return t("topoClassify.roleAggregation");
if (s === "access") return t("topoClassify.roleAccess");
if (s === "unknown") return t("topoClassify.roleUnknown");
return role || "-";
}
function managedSourceLabel(t: (k: string) => string, source: string): string {
const s = String(source || "").trim().toLowerCase();
if (s === "manual") return t("topoClassify.sourceManual");
if (s === "ume_sync") return t("topoClassify.sourceUmeSync");
if (s === "lldp") return t("topoClassify.sourceLldp");
if (s === "topology") return t("topoClassify.sourceTopology");
if (s === "webcrt") return t("topoClassify.sourceWebcrt");
return source;
}
export function TopologyClassifyPage() {
const { t } = useI18n();
const { showOk, showError } = useToast();
@ -285,10 +306,11 @@ export function TopologyClassifyPage() {
}}
>
<option value="">{t("topoClassify.filterRoleAll")}</option>
<option value="core">core</option>
<option value="aggregation">aggregation</option>
<option value="access">access</option>
<option value="unknown">unknown</option>
{ROLE_OPTIONS.map((r) => (
<option key={r} value={r}>
{roleLabel(t, r)}
</option>
))}
</select>
<select
className="input"
@ -380,10 +402,11 @@ export function TopologyClassifyPage() {
value={assignRole}
onChange={(e) => setAssignRole(e.target.value)}
>
<option value="core">core</option>
<option value="aggregation">aggregation</option>
<option value="access">access</option>
<option value="unknown">unknown</option>
{ROLE_OPTIONS.map((r) => (
<option key={r} value={r}>
{roleLabel(t, r)}
</option>
))}
</select>
) : null}
{assignWhat !== "role" ? (
@ -476,7 +499,7 @@ export function TopologyClassifyPage() {
<td>{n.ip}</td>
<td>
{linkLabel(n.link_status)}
{n.managed_source ? ` · ${n.managed_source}` : ""}
{n.managed_source ? ` · ${managedSourceLabel(t, n.managed_source)}` : ""}
</td>
<td>
{editing ? (
@ -486,13 +509,14 @@ export function TopologyClassifyPage() {
onChange={(e) => setEditRole(e.target.value)}
>
<option value="">-</option>
<option value="core">core</option>
<option value="aggregation">aggregation</option>
<option value="access">access</option>
<option value="unknown">unknown</option>
{ROLE_OPTIONS.map((r) => (
<option key={r} value={r}>
{roleLabel(t, r)}
</option>
))}
</select>
) : (
n.role || "-"
roleLabel(t, n.role || "")
)}
</td>
<td>
@ -681,7 +705,7 @@ export function TopologyClassifyPage() {
<li key={m.name}>
<strong>{m.name}</strong>
{" · "}
{m.role}
{roleLabel(t, m.role)}
{" · "}
{t("topoClassify.nodeCount").replace("{{count}}", String(m.node_count))}
</li>