diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a2db73e..ec696872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ ## Unreleased +### 调整 Changed + + - **自报段标签 chip 多语言适配(2026-10-26,单写者直修)**:「研究扫描 · X(自报段)」是后端冻结的简体数据,原先在 zh-TW/en 界面也直出简体;现标签列 chip 与展开面板证人行 native chip 两处展示面均经 localizeScannerLabel(threatDisplay.ts)跟 locale 走——zh-CN 与后端原字面量逐字一致(模板插值即还原),zh-TW 由 OpenCC 派生(研究掃描 · X(自報段)),en 英文;模式不匹配(sw_scanners 计数形「12 · X(3)」等)原样透传,数据面零改;顺带审计 = 两本字典键集对等 437/437、组件层无绕过 t() 的硬编码显示文案;门:vitest 441/441(两处渲染面各钉三语矩阵,zh-TW 例先 ensureOpenCC 同步就绪)+tsc+build 绿 + - **Vendor self-declared-range chips localized (2026-10-26, single-writer direct fix)**: the frozen Simplified-Chinese backend label 「研究扫描 · X(自报段)」 used to render verbatim even under zh-TW/en locales; both display faces (tags-column chip and the expanded panel's witness native chip) now pass through localizeScannerLabel (threatDisplay.ts) — zh-CN stays byte-identical to the backend literal, zh-TW derives via OpenCC (研究掃描 · X(自報段)), en gets English; non-matching shapes (sw_scanners count labels like 「12 · X(3)」) pass through raw, zero backend change; accompanying audit = locale dicts at key parity 437/437 and no hardcoded display strings bypassing t() in components; gates: vitest 441/441 (per-face three-locale matrices, zh-TW cases await ensureOpenCC) + tsc + build green + ### 新增 Added - **查询结果「标签」列新增 scanner 厂商自报段 chip(2026-10-26,单写者直修)**:scanner_ranges 源的 native_categories(「研究扫描 · X(自报段)」)原先只在展开行证人明细可见,现直接在结果表标签列出一条 chip(悬停 title=来源);噪声守门=仅 scanner_ranges 源的自报段声明进标签列,sw_scanners 等计数型标签仍留在展开面板;去重+封顶 3 溢出 +N;门:frontend vitest 440/440(新三例:自报段 chip 出+源在 title/他源计数标签不进/无 native 不渲染)+tsc+build 绿 diff --git a/frontend/src/components/ResultTable.tsx b/frontend/src/components/ResultTable.tsx index 57d834a9..223ac0e8 100644 --- a/frontend/src/components/ResultTable.tsx +++ b/frontend/src/components/ResultTable.tsx @@ -3,6 +3,7 @@ import { apiFetch, type LookupResult } from "../api"; import { confTextColor, VERDICT_STYLE, VERDICT_RANK, verdictLabelKey, normType, classLabel, familyShort, threatSummary, SCORE_SEMANTICS, + localizeScannerLabel, } from "./threatDisplay"; import { useI18n } from "../i18n"; import { IpDetailPanel } from "./IpDetailPanel"; @@ -156,6 +157,8 @@ function VerdictCell({ summary }: { summary: ReturnType }) // scanner 厂商自报段标签(scanner_ranges 批,PR #216):标签列只显自报段声明源的 // native_categories(「研究扫描 · X(自报段)」);sw_scanners 等计数型标签仅进展开面板, // 不进标签列(噪声守门,2026-10-26 用户裁「多加一个标签」)。去重 + 封顶 3 溢出 +N。 +// 展示面经 localizeScannerLabel 跟 locale 走(2026-10-26 用户裁「适配多语言」); +// 去重键用后端原字面量,与语言无关。 const VENDOR_TAG_SOURCE = "scanner_ranges"; const VENDOR_TAG_CAP = 3; @@ -224,7 +227,7 @@ function ThreatTags({ {shown.map((v) => ( - {v.label} + {localizeScannerLabel(v.label, t)} ))} {rest > 0 && +{rest}} diff --git a/frontend/src/components/SourceDetailRow.tsx b/frontend/src/components/SourceDetailRow.tsx index 263d6817..e22171d8 100644 --- a/frontend/src/components/SourceDetailRow.tsx +++ b/frontend/src/components/SourceDetailRow.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import type { ClassificationDetail } from "../api"; -import { verdictLabelKey, VERDICT_STYLE } from "./threatDisplay"; +import { verdictLabelKey, VERDICT_STYLE, localizeScannerLabel } from "./threatDisplay"; import { useI18n } from "../i18n"; function fmtRel(r: number): string { @@ -42,7 +42,7 @@ export function SourceDetailRow({ detail: d }: { detail: ClassificationDetail }) )} {nativeChips.map((c, i) => ( - [{c}] + [{localizeScannerLabel(c, t)}] ))} {d.native_confidence != null && ( native {d.native_confidence} diff --git a/frontend/src/components/__tests__/ResultTable.test.tsx b/frontend/src/components/__tests__/ResultTable.test.tsx index aacfc96a..bc541378 100644 --- a/frontend/src/components/__tests__/ResultTable.test.tsx +++ b/frontend/src/components/__tests__/ResultTable.test.tsx @@ -2,6 +2,7 @@ import { describe, it, expect, vi } from "vitest"; import { screen, fireEvent, within, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { renderWithI18n } from "../../test/i18nTestUtils"; +import { ensureOpenCC } from "../../i18n/opencc"; import { ResultTable } from "../ResultTable"; import type { ClassificationAssessment, LookupResult } from "../../api"; @@ -402,11 +403,31 @@ describe("ResultTable scanner vendor tag", () => { ]) } }; renderWithI18n(); const tds = screen.getByText("162.142.125.11").closest("tr")!.querySelectorAll("td"); - const chip = within(tds[6]).getByText("研究扫描 · Censys(自报段)"); + // 默认 en:chip 文案跟 locale 走(2026-10-26 用户裁「适配多语言」) + const chip = within(tds[6]).getByText("Research scanning · Censys (self-declared range)"); expect(chip.getAttribute("title")).toContain("scanner_ranges"); expect(within(tds[6]).getByText("Scanner")).toBeInTheDocument(); // 分类 chip 仍在 }); + it("localizes the vendor chip per locale (zh-CN byte-identical / zh-TW derived)", async () => { + await ensureOpenCC(); // 转换器同步就绪再渲染(生产路径 = Provider 异步武装后重渲染) + const r: LookupResult = { ...baseResult, ip: "162.142.125.11", + classifications: { scanner: caScanner([ + { source: "scanner_ranges", reliability: 0.9, verdict: "malicious", + native_categories: ["研究扫描 · Censys(自报段)"] }, + ]) } }; + const cases = [ + ["zh-CN", "研究扫描 · Censys(自报段)"], + ["zh-TW", "研究掃描 · Censys(自報段)"], + ] as const; + for (const [locale, expected] of cases) { + const { unmount } = renderWithI18n(, { locale }); + const tds = screen.getByText("162.142.125.11").closest("tr")!.querySelectorAll("td"); + expect(within(tds[6]).getByText(expected)).toBeInTheDocument(); + unmount(); + } + }); + it("does not surface native labels from other scanner sources (noise guard)", () => { const r: LookupResult = { ...baseResult, ip: "40.124.175.29", classifications: { scanner: caScanner([ diff --git a/frontend/src/components/__tests__/SourceDetailRow.test.tsx b/frontend/src/components/__tests__/SourceDetailRow.test.tsx index 5c7473f7..7d3d07e5 100644 --- a/frontend/src/components/__tests__/SourceDetailRow.test.tsx +++ b/frontend/src/components/__tests__/SourceDetailRow.test.tsx @@ -1,6 +1,7 @@ import { describe, it, expect } from "vitest"; import { screen, fireEvent } from "@testing-library/react"; import { renderWithI18n } from "../../test/i18nTestUtils"; +import { ensureOpenCC } from "../../i18n/opencc"; import { SourceDetailRow } from "../SourceDetailRow"; import type { ClassificationDetail } from "../../api"; @@ -86,20 +87,28 @@ describe("SourceDetailRow", () => { expect(screen.getByText(/\[16\]/)).toBeInTheDocument(); }); - it("renders the full scanner self-declared-range label chip (SR-T3 #214)", () => { + it("renders the full scanner self-declared-range label chip (SR-T3 #214)", async () => { // spec §5:scanner 证人行 native 标签「研究扫描 · <厂商/机构>(自报段)」 - // 全文原样出 chip(通用 native_categories 渲染,零新 UI 机制;钉法照上例)。 - // 验收例 = Stretchoid(40.124.175.29):标签全文一字不差地可见。 + // 全文出 chip(通用 native_categories 渲染,零新 UI 机制;钉法照上例)。 + // 2026-10-26 i18n 裁定:展示面跟 locale 走(zh-CN 必须与后端原字面量逐字一致, + // zh-TW 由 OpenCC 派生,en 英文;非自报段形状仍原样透传)。 + await ensureOpenCC(); // 转换器同步就绪再渲染(生产路径 = Provider 异步武装后重渲染) const d: ClassificationDetail = { source: "scanner_ranges", verdict: "malicious", reliability: 0.9, native_categories: ["研究扫描 · Stretchoid(自报段)"], }; - renderWithI18n(); - expect( - screen.getByText("[研究扫描 · Stretchoid(自报段)]") - ).toBeInTheDocument(); + const cases = [ + ["en", "[Research scanning · Stretchoid (self-declared range)]"], + ["zh-CN", "[研究扫描 · Stretchoid(自报段)]"], + ["zh-TW", "[研究掃描 · Stretchoid(自報段)]"], + ] as const; + for (const [locale, expected] of cases) { + const { unmount } = renderWithI18n(, { locale }); + expect(screen.getByText(expected)).toBeInTheDocument(); + unmount(); + } }); it("ignores retired extra.native_type (fallback removed)", () => { diff --git a/frontend/src/components/threatDisplay.ts b/frontend/src/components/threatDisplay.ts index 03457c5b..b064eac0 100644 --- a/frontend/src/components/threatDisplay.ts +++ b/frontend/src/components/threatDisplay.ts @@ -1,5 +1,20 @@ import type { LookupResult, ThreatSummary } from "../api"; +// scanner_ranges 自报段标签本地化(2026-10-26 用户裁定):后端冻结格式 +// 「研究扫描 · <厂商>(自报段)」(scanner_ranges 批 #214 契约钉死,简体数据直出) +// 在 zh-TW/en 界面须跟语言走。模式命中 → 拆厂商名经 t() 重组;不命中 +// (sw_scanners 计数形「12 · X(3)」等)原样透传——数据面零改,展示面本地化。 +const SELF_DECLARED_RE = /^研究扫描 · (.+)\(自报段\)$/; + +export function localizeScannerLabel( + label: string, + t: (key: string, vars?: Record) => string, +): string { + const m = label.match(SELF_DECLARED_RE); + if (!m) return label; + return t("class.scannerSelfDeclared", { vendor: m[1] }); +} + export function confColor(conf: number): string { if (conf >= 70) return "bg-emerald-500"; if (conf >= 30) return "bg-amber-500"; diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index d6c93e54..2b1f78d2 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -64,6 +64,7 @@ "cdn.notice": "Public infrastructure — banning not recommended", "cidrHint.label": "Supports IPv4/IPv6 and CIDR ranges, e.g. 1.2.3.0/24", "class.abuse_reports": "Abuse reports", + "class.scannerSelfDeclared": "Research scanning · {vendor} (self-declared range)", "class.blacklist": "Blacklist", "class.infected_system": "Infected System", "class.botnet_cc": "C2", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index d3ca2f13..41a9483d 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -64,6 +64,7 @@ "cdn.notice": "公共基础设施,不建议封禁", "cidrHint.label": "支持 IPv4/IPv6 与 CIDR 网段,如 1.2.3.0/24", "class.abuse_reports": "滥用举报", + "class.scannerSelfDeclared": "研究扫描 · {vendor}(自报段)", "class.blacklist": "黑名单", "class.infected_system": "受感染系统", "class.botnet_cc": "C2",