Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 绿
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/ResultTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -156,6 +157,8 @@ function VerdictCell({ summary }: { summary: ReturnType<typeof threatSummary> })
// 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;

Expand Down Expand Up @@ -224,7 +227,7 @@ function ThreatTags({
{shown.map((v) => (
<span key={`vendor-${v.label}`} title={v.source}
className="rounded bg-orange-500/10 px-1.5 py-0.5 text-[11px] text-orange-300 ring-1 ring-orange-500/20">
{v.label}
{localizeScannerLabel(v.label, t)}
</span>
))}
{rest > 0 && <span className="text-[10px] text-zinc-500">+{rest}</span>}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/SourceDetailRow.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -42,7 +42,7 @@ export function SourceDetailRow({ detail: d }: { detail: ClassificationDetail })
</span>
)}
{nativeChips.map((c, i) => (
<span key={`nc-${i}`} className="rounded bg-sky-800/40 px-1 py-px mr-0.5 ml-1 text-sky-300" title={t("sourceDetail.nativeTypeTitle")}>[{c}]</span>
<span key={`nc-${i}`} className="rounded bg-sky-800/40 px-1 py-px mr-0.5 ml-1 text-sky-300" title={t("sourceDetail.nativeTypeTitle")}>[{localizeScannerLabel(c, t)}]</span>
))}
{d.native_confidence != null && (
<span className="text-zinc-500 ml-1">native {d.native_confidence}</span>
Expand Down
23 changes: 22 additions & 1 deletion frontend/src/components/__tests__/ResultTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -402,11 +403,31 @@ describe("ResultTable scanner vendor tag", () => {
]) } };
renderWithI18n(<ResultTable results={[r]} />);
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(<ResultTable results={[r]} />, { 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([
Expand Down
23 changes: 16 additions & 7 deletions frontend/src/components/__tests__/SourceDetailRow.test.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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(<SourceDetailRow detail={d} />);
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(<SourceDetailRow detail={d} />, { locale });
expect(screen.getByText(expected)).toBeInTheDocument();
unmount();
}
});

it("ignores retired extra.native_type (fallback removed)", () => {
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/components/threatDisplay.ts
Original file line number Diff line number Diff line change
@@ -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 | number>) => 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";
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading