现象
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:105 的 NON_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 前移到采集侧的守卫拦掉:
判定本身自相矛盾:img 已经在 INLINE_SET 里(classify.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.collect、classify.closestUnit、renderer.render)共用同一个函数,改一处即可保持 #50 确立的「采集与渲染准入一致」不变式 —— 这个不变式不能在修复中被破坏。
验收标准
修复后角标文字(Linux Foundation +3)会随段落文本一并送去翻译 —— 该问题见 #56,两者需配套落地。
现象
Google AI 概览(AI Overview)里,凡是句尾带来源角标(citation chip)的段落,整段不出译文;同一块内容中不带角标的段落一切正常。
用户实测截图中:
Linux Foundation +3Linux Foundation +2Facebook · The Linux … +2段落按钮也不会浮出,快捷键同样无反应 —— 用户完全无从判断是「不支持」还是「坏了」(这一点单列为 #57)。
根因
角标的 DOM 形如:
src/dom/classify.ts:105的NON_TEXT_SELECTOR同时包含img和[role="button"],而hasNonTextContent()用的是el.querySelector()—— 全子树无差别匹配,不问深度、不问是否处在文本流里:于是这个段落
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_SET里(classify.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>脚注引用:p3 的角标只有
<img>、没有role="button"也照样被拒 —— 单一个 favicon 就足以让整段消失。p4 说明不含图片的传统脚注引用不受影响,问题精确地锁定在「带图标的角标」。影响面
不止 Google AI 概览。任何「正文段落 + 行内图标」的排版都会整段丢失:
<img>形式的 emoji(Slack / GitHub 风格)[role="button"]展开控件的段落这是一整类漏翻,而非单站兼容问题。
修复方向
把「非文本内容」的判定从存在性改为位置性:只有当非文本节点不在本段的内联文本流里时才阻断。
判据(已验证可行):元素自身持有直接文本,且从该元素到命中节点的路径上每一层都是
INLINE_SET标签 → 视为行内装饰,放行;否则视为独立媒体块,仍然拒绝。对同一组样例跑候选实现,并补上 #22 / #50 的回归场景(c1 = 缩略图卡片,c2 = 直接文本 + 块级
<button>):三处调用点(
walker.collect、classify.closestUnit、renderer.render)共用同一个函数,改一处即可保持 #50 确立的「采集与渲染准入一致」不变式 —— 这个不变式不能在修复中被破坏。验收标准
closestUnit()对角标段落返回该段落本身,段落按钮正常浮出