chrome浏览器翻译可能引发的页面问题

遇到问题

前段时间,我接手负责的某项目的产品同事,反馈了某用户遇到的2个页面问题

  1. 页面文字内容和其他人的不一样
  2. 审批流程处理后,提醒的待办数据没有刷新

第一个问题很奇怪,我一时想不到可能原因;第二个问题估计是代码存在bug,似乎更好解决一点。于是先着手在测试环境去复现第二个问题,可怎么也复现不了,看了代码,也确实在流程处理后刷新动作。一时陷入了困境的我又想着先试试第一个问题。

看着用户给的截图里不一样的页面文案,我突然意识到,这会不会是翻译造成的,于是赶紧在鼠标右键选择翻译成中文,果然,页面上不仅非中文的文字被翻译了,连原本是中文的一部分文字也被改了


翻译前


翻译后

可以看到部分中文被替换,而替换后的文案可能是乱的。

继续查看页面元素内容,发现原来dom根部的html标签的lang属性被设置为'en'(当初项目应该是用框架脚手架生成的,这种情况默认lang='en'),浏览器将页面当作了英文页面,而浏览器可设置自动翻译外文。后来询问得知,用户确实是开启了自动翻译英文页面,导致页面文字错乱。

找到原因就好办了。将html标签的lang属性改为'zh-CN',这样浏览器就不会自动翻译了。但如果用户手动强行翻译,页面上还是会有小部分文字被替换。考虑到目前这个系统并没有外文用户,在和产品商量后,又给html标签添加了translate="no",从根本上禁止了页面被翻译的情况。

接下来是第二个问题。细想一下,待办数据更新是我接手之前就有的功能,上线已经蛮久了,如果有问题,应该一早就有用户反馈。联想到翻译会更改文字,那会不会是更改了文字导致数据不刷新的?我赶紧撤回改动,开启翻译,第二个问题果然复现了。前端页面是用react写的,初步排查应该是和react相关。因为加了translate="no"已经避免了触发翻译的情况,时间关系当时先用这个办法一起解决了,后续经过一番研究,终于理清楚了内部的原因。

原因探究

chrome浏览器在翻译时都做了什么

已经知道问题是由chrome翻译引起,就得知道在翻译时,都对页面做了什么改动

使用vite+react创建一个最简单的项目,查看dom内容如下

html 复制代码
<html lang="en">
  <head>
    <script type="module" src="/@vite/client"></script>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  </head>
  <body>
    <div id="root"><h1>Hello World !</h1></div>
    <script type="module" src="/src/main.jsx?t=1699602638956"></script>
  </body>
</html>

启用翻译后,dom变为

html 复制代码
<html lang="zh-CN" class="translated-ltr">
  <head>
    <script type="module" src="/@vite/client"></script>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link
      type="text/css"
      rel="stylesheet"
      charset="UTF-8"
      href="https://www.gstatic.com/_/translate_http/_/ss/k=translate_http.tr.qhDXWpKopYk.L.W.O/am=CAM/d=0/rs=AN8SPfqeKn8wA30q4viup18yaci8udUjKQ/m=el_main_css"
    />
  </head>
  <body>
    <div id="root">
      <h1><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">你好世界 !</font></font></h1>
    </div>
    <script type="module" src="/src/main.jsx?t=1699602638956"></script>
    <div
      id="goog-gt-tt"
      class="VIpgJd-yAWNEb-L7lbkb skiptranslate"
      style="
        border-radius: 12px;
        margin: 0 0 0 -23px;
        padding: 0;
        font-family: 'Google Sans', Arial, sans-serif;
      "
      data-id=""
    >
      <div id="goog-gt-tt" class="VIpgJd-yAWNEb-L7lbkb skiptranslate" style="border-radius: 12px; margin: 0 0 0 -23px; padding: 0; font-family: 'Google Sans', Arial, sans-serif;" data-id=""><div id="goog-gt-vt" class="VIpgJd-yAWNEb-hvhgNd"><div class=" VIpgJd-yAWNEb-hvhgNd-l4eHX-i3jM8c"><img src="https://fonts.gstatic.com/s/i/productlogos/translate/v14/24px.svg" width="24" height="24" alt=""></div><div class=" VIpgJd-yAWNEb-hvhgNd-k77Iif-i3jM8c"><div class="VIpgJd-yAWNEb-hvhgNd-IuizWc" dir="ltr">原文</div><div id="goog-gt-original-text" class="VIpgJd-yAWNEb-nVMfcd-fmcmS VIpgJd-yAWNEb-hvhgNd-axAV1"></div></div><div class="VIpgJd-yAWNEb-hvhgNd-N7Eqid ltr"><div class="VIpgJd-yAWNEb-hvhgNd-N7Eqid-B7I4Od ltr" dir="ltr"><div class="VIpgJd-yAWNEb-hvhgNd-UTujCb">请对此翻译评分</div><div class="VIpgJd-yAWNEb-hvhgNd-eO9mKe">您的反馈将用于改进谷歌翻译</div></div><div class="VIpgJd-yAWNEb-hvhgNd-xgov5 ltr"><button id="goog-gt-thumbUpButton" type="button" class="VIpgJd-yAWNEb-hvhgNd-bgm6sf" title="翻译质量很棒" aria-label="翻译质量很棒" aria-pressed="false"><span id="goog-gt-thumbUpIcon"><svg width="24" height="24" viewBox="0 0 24 24" focusable="false" class="VIpgJd-yAWNEb-hvhgNd-THI6Vb NMm5M"><path d="M21 7h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 0S7.08 6.85 7 7H2v13h16c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73V9c0-1.1-.9-2-2-2zM7 18H4V9h3v9zm14-7l-3 7H9V8l4.34-4.34L12 9h9v2z"></path></svg></span><span id="goog-gt-thumbUpIconFilled"><svg width="24" height="24" viewBox="0 0 24 24" focusable="false" class="VIpgJd-yAWNEb-hvhgNd-THI6Vb NMm5M"><path d="M21 7h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 0S7.08 6.85 7 7v13h11c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73V9c0-1.1-.9-2-2-2zM5 7H1v13h4V7z"></path></svg></span></button><button id="goog-gt-thumbDownButton" type="button" class="VIpgJd-yAWNEb-hvhgNd-bgm6sf" title="翻译质量很差" aria-label="翻译质量很差" aria-pressed="false"><span id="goog-gt-thumbDownIcon"><svg width="24" height="24" viewBox="0 0 24 24" focusable="false" class="VIpgJd-yAWNEb-hvhgNd-THI6Vb NMm5M"><path d="M3 17h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 24s7.09-6.85 7.17-7h5V4H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2zM17 6h3v9h-3V6zM3 13l3-7h9v10l-4.34 4.34L12 15H3v-2z"></path></svg></span><span id="goog-gt-thumbDownIconFilled"><svg width="24" height="24" viewBox="0 0 24 24" focusable="false" class="VIpgJd-yAWNEb-hvhgNd-THI6Vb NMm5M"><path d="M3 17h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 24s7.09-6.85 7.17-7V4H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2zm16 0h4V4h-4v13z"></path></svg></span></button></div></div><div id="goog-gt-votingHiddenPane" class="VIpgJd-yAWNEb-hvhgNd-aXYTce"><form id="goog-gt-votingForm" action="//translate.googleapis.com/translate_voting?client=te_lib" method="post" target="votingFrame" class="VIpgJd-yAWNEb-hvhgNd-aXYTce"><input type="text" name="sl" id="goog-gt-votingInputSrcLang"><input type="text" name="tl" id="goog-gt-votingInputTrgLang"><input type="text" name="query" id="goog-gt-votingInputSrcText"><input type="text" name="gtrans" id="goog-gt-votingInputTrgText"><input type="text" name="vote" id="goog-gt-votingInputVote"></form><iframe name="votingFrame" frameborder="0"></iframe></div></div></div>
    </div>
  </body>
</html>

对比之后可以发现,翻译的改动有

  1. 将html的属性lang改为翻译后的语言代码'zh-CN',同时添加类名'translated-ltr'
  2. 加载了一个css文件
  3. 翻译的文字被包裹了2层
  4. 最后面多了一块id为goog-gt-tt的dom结构

简单研究后可以发现,4是一段被隐藏的的让用户给翻译评分的结构,而2的css仅用于设置4这段dom的样式(记得最早的时候,chorme的翻译是可以按段落显示原文的,这段dom应该是那个功能的遗留产物,不明白为什么没有直接去掉),真正对页面元素造成破坏的是3这个改动。

复现错误

给页面添加一个再简单不过的计数器

jsx 复制代码
import { useState } from 'react'
function App() {
  const [count, setCount] = useState(1)
  return (
    <>
      <h1>Hello World !</h1>
      <p>{count}</p>
      {count}
      <div>
        <button onClick={() => setCount(count + 1)}>计数</button>
      </div>
    </>
  )
}
export default App

通过下面的录屏可以发现,在翻译前都是正常的,在启动翻译后,第一个计数值显示仍然正常,第二个却不更新了

(双击放大重播git动画)

后来甚至发现,如果涉及到元素移除,页面还会报错。通过报错信息,可知是react-dom在试图通过父元素的removeChild移除指定node时发现指定node不是父元素的子元素。

jsx 复制代码
function App() {
  const [count, setCount] = useState(1)
  return (
    <div>
      <h1>Hello World !</h1>
      <p>{count}</p>
      {count}
      <br />
      {count < 3 && '不能超过3'}
      <div>
        <button onClick={() => setCount(count + 1)}>计数</button>
      </div>
    </div>
  )
}
export default App

(双击放大重播git动画)

源码分析

这里以最新的react-dom@18为例,通过查看其源码我们可以发现,react在作diff时,会将某节点中被移除的节点放到其fiber对象的deletions属性中,然后在后续循环deletions,最终调用parentInstance.removeChild(child)来移除节点。

上面例子中,被移除的文字被包裹添加了font标签,导致'不能超过3'这个文本节点(text node)不再是原上层div的子节点,这时调用parentInstance.removeChild(child)也就因为找不到这子节点而报错了。如果给这个文本节点外包裹P标签,翻译添加的font标签在P标签内,P标签依然是原父节点的子节点,dom更新就不会有问题。

那么一开始的更新无效也是找不到子节点吗?并不相同。在更新操作中,react-dom对标签节点和文本节点采用了2种不同的更新方式

对于标签节点是更其children,即使翻译后被包裹front改变改变了结构,也有node.textContent = text兜底,直接把整个节点内容替换掉

所以如果查看dom结构,可以发现翻译后的

<p><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">1</font></font></p>

在更新后先变成了<p>2</p>,然后又很快被浏览器翻译转换为

<p><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">2</font></font></p>

而如果是文本节点,则是直接修改其nodeValue。

虽然此时文本节点的引用还在,但dom结构被翻译改变,其对应的dom已不再存在,即使改了nodeValue,页面也不会更新。

如何避免

  1. 避免在有多个子节点的节点内使用文本节点,应该在外包裹一层标签,变成标签节点。历史代码太多的话,可以考虑写插件来实现自动转换
less 复制代码
<div>
  <h1>Hello World !</h1>
  {name}  // bad
</div>
<div>
  <h1>Hello World !</h1>
  <span>{name}<span>  // good
</div>
  1. 设置合适的lang值避免自动翻译,必要的时候还可以添加translate="no"属性

扩展

1.其他浏览器是否有同样的情况

目前发现edge浏览器也自带翻译,开启翻译后,更新文本节点正常,但移除文本节点同样会报错。edge翻译对dom的改变少得多,只会对文本节点包裹一层font标签。这里猜测更新能正常是因为edge翻译是在原文本节点基础上进行修改,这保留了dom和文本节点对象的映射关系。

2.vue3是否有同样的情况

vue3同样有翻译后页面不更新的情况,并且template写法比render写法问题少,但不存在删除文本节点报错的情况。和react的方式应该有差异,这里就不再分析源码了,各位可以自行研究。有趣的是,vue3结合edge浏览器,正好可以同时避免更新和删除这2个问题。

相关推荐
贩卖纯净水.3 分钟前
Chrome调试工具(查看CSS属性)
前端·chrome
阿伟来咯~2 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
bysking3 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
September_ning8 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人8 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0018 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
Rattenking10 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫11 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js