Skip to content

带网页引用角标的段落整段不翻译:hasNonTextContent 把行内 favicon <img> 当块级媒体,采集与段落按钮双双拒绝 #55

Description

@Teeeeeeeerry

现象

Google AI 概览(AI Overview)里,凡是句尾带来源角标(citation chip)的段落,整段不出译文;同一块内容中不带角标的段落一切正常。

用户实测截图中:

段落 句尾角标 译文
The Linux Foundation is a non-profit group… share code Linux Foundation +3 ❌ 无
Key Functions / Project Hosting / Community Support ✅ 有
Education and Training: … online learning paths Linux Foundation +2 ❌ 无
Recent Updates / AI Cost Standards ✅ 有
Firmware Security: … secure hardware supply chains. Facebook · The Linux … +2 ❌ 无

段落按钮也不会浮出,快捷键同样无反应 —— 用户完全无从判断是「不支持」还是「坏了」(这一点单列为 #57)。

根因

角标的 DOM 形如:

<div class="para">
  …It gives groups a safe and neutral place to share code
  <span role="button"><img src="favicon" width="16" height="16"><span class="wJwe6c">Linux Foundation +3</span></span>
</div>

src/dom/classify.ts:105NON_TEXT_SELECTOR 同时包含 img[role="button"],而 hasNonTextContent() 用的是 el.querySelector() —— 全子树无差别匹配,不问深度、不问是否处在文本流里

export function hasNonTextContent(el: Element): boolean {
  return el.querySelector(NON_TEXT_SELECTOR) !== null;
}

于是这个段落 isTranslationUnit() === true(它直接持有文本,chip 是内联子元素),但 hasNonTextContent() === true,被 #50 前移到采集侧的守卫拦掉:

  • src/dom/walker.ts:85 —— collect() 直接 continue,整页翻译不收这一段;子树里也没有更深的翻译单元可捡(文本是段落自己的直接文本节点),于是这段文本彻底无人认领;
  • src/dom/classify.ts:218 —— closestUnit() 命中后走 findTextOnlyDescendant(),同样找不到纯文本后代,继续向上;而祖先容器只会含更多 img,一路到顶返回 null,段落按钮不浮出;
  • src/dom/renderer.ts:35 的纵深防御用同一个判据,即使前两道被绕过也仍然拒绝。

判定本身自相矛盾:img 已经在 INLINE_SETclassify.ts:45),也就是说 isTranslationUnit() 明确认为「img 是段落文本流的一部分,不阻断段落判定」;而 NON_TEXT_SELECTOR 认为「含 img 就必须整体排除」。同一个节点,两条规则给出相反答案,后者赢。

NON_TEXT_SELECTOR 的立法本意(#22)是防止卡片里的缩略图、订阅按钮被搬进 display:none.pt-origin 把布局搞塌 —— 那是「独立于文本流的块级媒体」。行内 16×16 的 favicon 角标不属于这一类:它本来就跟原文一起显示、一起隐藏,搬进 .pt-origin 毫无副作用。

实证复现

用 jsdom 直接跑真实模块(src/dom/walker.ts + src/dom/classify.ts,esbuild 打包,无改写),最小复现页 p1/p3 带角标、p2 无角标、p4 是普通 <sup><a> 脚注引用:

collect() 结果: [ 'p2', 'p4' ]

┌──────┬─────────┬───────────────────┬───────────────────┬────────┬─────────────┐
│ id   │ hasChip │ isTranslationUnit │ hasNonTextContent │ 被采集 │ closestUnit │
├──────┼─────────┼───────────────────┼───────────────────┼────────┼─────────────┤
│ 'p1' │ true    │ true              │ true              │ false  │ null        │
│ 'p2' │ false   │ true              │ false             │ true   │ 'p2'        │
│ 'p3' │ true    │ true              │ true              │ false  │ null        │
│ 'p4' │ true    │ true              │ false             │ true   │ 'p4'        │
└──────┴─────────┴───────────────────┴───────────────────┴────────┴─────────────┘

p3 的角标只有 <img>、没有 role="button" 也照样被拒 —— 单一个 favicon 就足以让整段消失。p4 说明不含图片的传统脚注引用不受影响,问题精确地锁定在「带图标的角标」。

影响面

不止 Google AI 概览。任何「正文段落 + 行内图标」的排版都会整段丢失:

  • Google 搜索 AI 概览 / 精选摘要的来源角标
  • 维基百科段落内的行内图标、地图/音频小标记
  • 新闻站正文里的行内 logo、外链图标
  • 文档站正文里的行内徽章(badge)、<img> 形式的 emoji(Slack / GitHub 风格)
  • 任何在句尾挂 [role="button"] 展开控件的段落

这是一整类漏翻,而非单站兼容问题。

修复方向

把「非文本内容」的判定从存在性改为位置性:只有当非文本节点不在本段的内联文本流里时才阻断。

判据(已验证可行):元素自身持有直接文本,且从该元素到命中节点的路径上每一层都是 INLINE_SET 标签 → 视为行内装饰,放行;否则视为独立媒体块,仍然拒绝。

对同一组样例跑候选实现,并补上 #22 / #50 的回归场景(c1 = 缩略图卡片,c2 = 直接文本 + 块级 <button>):

┌──────┬────────┬───────────┬───────────────┐
│ id   │ isUnit │ 现状_拒绝 │ 候选修复_拒绝 │
├──────┼────────┼───────────┼───────────────┤
│ 'p1' │ true   │ true      │ false         │  ← 角标段落放行
│ 'p2' │ true   │ false     │ false         │
│ 'p3' │ true   │ true      │ false         │  ← 角标段落放行
│ 'p4' │ true   │ false     │ false         │
│ 'c1' │ false  │ true      │ true          │  ← #22 缩略图卡片仍拒绝
│ 'c2' │ true   │ true      │ true          │  ← 块级 button 仍拒绝
└──────┴────────┴───────────┴───────────────┘

三处调用点(walker.collectclassify.closestUnitrenderer.render)共用同一个函数,改一处即可保持 #50 确立的「采集与渲染准入一致」不变式 —— 这个不变式不能在修复中被破坏。

验收标准

修复后角标文字(Linux Foundation +3)会随段落文本一并送去翻译 —— 该问题见 #56,两者需配套落地。

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions