From 012b91e7a5c7b8421197290cc23549cdad19aed5 Mon Sep 17 00:00:00 2001 From: steponeerror Date: Mon, 26 Oct 2026 17:32:40 +0800 Subject: [PATCH] i18n: localize scanner self-declared-range label to zh-CN/zh-TW/en MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 标签列 chip 与证人行 native chip 经 localizeScannerLabel 跟 locale 走; zh-CN 逐字还原后端冻结字面量,zh-TW OpenCC 派生,en 英文; 计数形/未知形状原样透传。vitest 441/441 + tsc + build。 --- CHANGELOG.md | 5 ++++ frontend/src/components/ResultTable.tsx | 5 +++- frontend/src/components/SourceDetailRow.tsx | 4 ++-- .../components/__tests__/ResultTable.test.tsx | 23 ++++++++++++++++++- .../__tests__/SourceDetailRow.test.tsx | 23 +++++++++++++------ frontend/src/components/threatDisplay.ts | 15 ++++++++++++ frontend/src/i18n/locales/en.json | 1 + frontend/src/i18n/locales/zh-CN.json | 1 + 8 files changed, 66 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 538dc89f..25d9a9a9 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 - **Anchor readout family v0 (issue #233, four SDD blocks; ADR 0013; two grill rounds + dual research legs 2026-10-28 — known-label IPs become per-predicate measurement anchors; every readout measures, none gates)**: T1 — ANCHORS 19→32 members (+13 root-server letter IPv4s from internic root.zone 2026-10-28, expect=clean, quarterly re-check note in-code) + the same 13 as infra_services benign-assertion rows (two independent observation lines: root_servers asset-abstains, infra_services asserts benign — benign witnesses never vote, only the conflict face lights; expected displacement after next rebuild: pure-benign dns groups' top-level threat benign/0→benign/conf≈r per #188 semantics, scoring path untouched); anchor mirror 32 members, selftest MIRROR sentinel green. Pre-check live specimen: k.root 193.0.14.129 accused by cps_collected_iocs — adjudication pending, the monthly anchors gate stays red until ruled (by design). T2 — sinkhole pollution probe: MISP warninglists sinkholes frozen as eval-only corpus (203 = 186 bare IPs + 17 CIDRs; double-pinned list version 20261014 + upstream brakmic/Sinkholes commit + SHA256s, provenance sidecar + notes/sinkholes-provenance doc; corpus notes record the dual-coverage attribution — bare-IP match wins, conservatively deflating the strong layer); layered readout CIDR-strong vs IP-weak, accuser pool recomputed per the agg-share precedent with dedup_lineage so mirrors never double-count. T3 — two readouts in backend/ipdb/_eval/anchor_readouts.py: clean-anchor FP rate (census N=22 over all clean anchors, Clopper-Pearson exact bounds + zero-event exact upper, accuser triples retained for the human cause-taxonomy — no auto-classification; first real-data reading FP=2/22=9.09% CP95[1.1,29.2], live specimens k.root←cps + 223.5.5.5 AliDNS←tweetfeed single-tweet accusation with reporter+tweet URL in the evidence face) and scanner-anchor recall (truth=scanner_ranges self-declared members, numerator leave-out excludes scanner_ranges itself, stable seeded sampling with N/seed/probe-list shipped; first reading 241/300=80.33% CP95[75.4,84.7], self_echo=0 on dev data — monthly first run must confirm >0). Statistics reuse model.beta_ppf with explicit degenerate branches, verified against an independent binomial-tail implementation (12-digit agreement) and pinned with the real-data parameters. T4 — monthly wiring: LEG_ORDER 8→10 legs (sinkhole-probe + anchor-readouts after agg-share, before history readers; anchor-readouts = one leg, two artifacts, both required), leg-status propagates missing-layer semantics (missing corpus → explicit absence, never 0), HADES NDSS'25 manipulation note as a static line in the monthly report, GLOSSARY code anchors for four terms (property-truth anchor marked not-landed v1); check_family refusal message now names the missing pattern. Gates (integration finals): backend tests/eval 531 green + CI-shape per-dir/per-file green, ops golden 10/10 + selftest 115/115 (mirror 32), bit-identity smoke PASS, monthly --dry-run 10 legs. diff --git a/frontend/src/components/ResultTable.tsx b/frontend/src/components/ResultTable.tsx index b752efaf..b06dfaf2 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"; @@ -159,6 +160,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; @@ -227,7 +230,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 29d9e7d4..c5bcea5a 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"; @@ -405,11 +406,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",