19
头图

你真的认为Google翻译不影响"前端"页面功能吗?

背景

     又是一个时光飞逝的工作日, QA同学突然提出bug, 文本输入框的"计数"功能失效, 经过大家多方查找最后发现竟然是因为测试同学开启了"谷歌翻译"造成的无法"计数", 这就引起了我的浓厚兴趣, 到底这个看起来"人畜无害"的翻译功能是如何影响了我的输入框? 魏蜀吴争霸从此揭开序幕,啊~额(中箭)。

     这篇文章是按我的探究过程编写的, 所以并不是一下就找到正确的方向, 跟着我当时的思路一起探索吧。

image.png

一、dom结构的改变 (以react代码为例)

     想要知道为什么"翻译功能"会影响页面, 要从观察每次"翻译"功能生效时页面结构的变化开始, 最为简单的一段代码:

return ( <div>你好</div> )

翻译前:
image.png

翻译后:
image.png

     查询一下font标签的特性, 竟然已经不建议使用了:
image.png

     尝试一下是否可以获取到这个font标签元素, 将代码改装一下

  function handleShowDom() {
    console.log(document.getElementById("box").children);
  }

 return( 
   <div id="box" onClick={handleShowDom}>
    你好
   </div>
 )

image.png

     可以获取到这个元素就有意思了, 如果某些情况下代码里面存在通过获取子元素进行操作的逻辑, 那么理论上就会出问题啊。

     难道是由于dom结构的改变, 导致的文本输入框的计数功能会消失吗?

二、对比vue与react各类ui库

     通过查看element ui的源码没有发现特殊的代码逻辑, 那究竟是怎么回事? 满心的疑问让我萌生一种想法, 难道跟vue与react框架本身的原理有关? 那么接下来我会分别实验4种ui框架的效果:

1: react -> antd (无bug 👍🏻)

antd官网
image.png

2: react -> arco (有bug)

arco 官网

image.png

3: vue -> element(有bug)

     这个不用演示了, 开局就是靠它。

4: vue -> iview(有bug)

iview 官网

image.png

     上述只有 antd是完美解决了这个bug (必须: respect), 那必须要研究一下它是如何"力压群芳"的呢?

三、深究textarea

     既然是这个输入框元素发生的问题, 那么我们就从它开始吧, 一起看一下element-uiantd分别产生的textarea元素长什么样子:

image.png

image.png

     区别还是比较明显的, element-ui的内容信息不是放在标签内部的, 因为我们在dom结构的视角无法查看到内容, 但是antd恰恰相反, 这也许可以成为一个突破口。

     那我们把"火力"集中在<textarea>身上, 看看它到底有多少种赋值写法:

  return (
    <div className="App">
      <textarea rows="5" cols="40">
        内容1
      </textarea>
      <br />
      <textarea rows="5" cols="40" value="内容2"></textarea>
      <br />
      <textarea rows="5" cols="40" defaultValue="内容3"></textarea>
   </div>

image.png

     很奇妙, 上述三种写法都会产生同样的dom结构, 那么就比较好奇是什么样的写法可以隐藏标签内的文字? 我就尝试了一下js指定value的方式:

  useEffect(() => {
    document.getElementById("wrap").value = "你好世界";
  }, []);

  return (
    <div className="App">
      <textarea id="wrap" rows="5" cols="40">
        内容4
      </textarea>
   </div>
 )

image.png

     果然问题出在这里, dom结构内保存的居然是初始值, 外界显示的是正确的值, 除了这些还有什么区别那? 我们把相关的值都打印出来吧:

image.png

vs🌩

image.png

     真是不研究不知道, 原来同样的组件效果不同的库实现出来差这么多, 感觉属性值差的还挺多的。

     但是通过实验发现, 这些属性并不是这个bug的直接原因, 我们还需要深入源码进行探究, 此时我意识到这个问题可能不是elemnet的责任。

四、谷歌翻译是否影响vue

     经过多次尝试连自己都不敢相信, 竟然vue的计算属性等双向绑定的数据会在翻译后失效:

<template>
  <div id="app">
    <button @click="handleClick">你好: {{n}}</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods:{
    handleClick(){
      this.n += 1;
      console.log(this.n)
    }
  },
  data(){
    return {
      n:1
    }
  },
}
</script>

image.png

     上面可以看出vue的变量已经变化, 只是由于dom结构的改变导致了页面无法被正确更新。

     这是一个很危险的隐患, 因为有的用户可能会选择"总是翻译", 这就导致页面功能完全乱掉了啊!

image.png

五、谷歌翻译是否影响react

     既然react的arco框架会出问题, 那么react框架也一定会被影响, 让我们尝试一下什么情况下会出bug, 翻译之后点击:

import { useState } from "react";
import "./App.css";

function App() {
  const [n, setN] = useState(0);

  function handleClick() {
    setN(n + 1);
    console.log(n + 1);
  }
  return <div onClick={handleClick}>你好:{n}</div>;
}

export default App;

image.png

如果改成则不会受影响:

<div onClick={handleClick}>{n}</div>;

image.png

六、受影响范围 (以react的写法为例)

     通过观察antd与element-ui实现的不同我发现, antd是使用after伪类类做的, 难道此事与伪类有关? 这也证明了某些情况是不受影响的, 让我们实验一下谷歌翻译都影响哪些写法:

1: 拼接文本

     不受影响的写法, 甚至这种写法每次n发生变化, 会移除font标签的dom结构。

<div>{n}</div>;

     受影响的写法主要是拼接的文本。

<div>你好 {n}</div>;

<div>{n} {n}</div>;
2: 伪元素(不会被翻译)

空空的dom

<div className={"box"}>.</div>

定义伪元素

.box {
  height: 10px;
  width: 10px;
  border: 1px solid red;
}
.box::after {
  content: 'hello';
  display: inline-block;
}

image.png

所以antd很可能是因为伪元素才没有出bug, 如果antd不是无意之举那就太细节了。

3: 属性(非常特殊)

     之所以说它非常特殊, 是因为属性是否被翻译需要分两种情况讨论, 是否有文本元素, 并且这个文本不能是input内部的文本。

第一种情况: 页面只有一个输入框

    <div>
      <span>
        <input
          type="Please enter content"
          value={value}
          onChange={change}
          placeholder={"Please enter content"}
        />
      </span>
    </div>

显而易见, 这个输入框没有被翻译:

image.png

第二种情况: 随意添加一个字符串

    <div>
      <span>你好</span>
      <span>
        <input
          type="Please enter content"
          value={value}
          onChange={change}
          placeholder={"Please enter content"}
        />
      </span>
    </div>

image.png

     成功进行了翻译并且像placeholder这种可能会展示给用户的属性也被翻译了, 其余功能性的属性并不会被翻译。

<span xxx={"xxxxxxxxx hello"}>你好</span>

image.png

七、如何屏蔽翻译

     为防止因谷歌翻译引起不良体验, 只需要在html标签上添加translate="no"属性即可屏蔽掉翻译功能:

image.png

image.png

八、检测被翻译成了什么

     其实存在一部分用户默认就开启谷歌翻译功能的, 更有甚者你做的是国际化项目, 不同语言的人使用你的网站更有可能使用默认的谷歌翻译, 那么不编直接屏蔽的情况下, 我们要至少要监听一下用户都将我们的网站翻译成了什么语言? 这样也方便我们日后对网站的i18n进行优化。

     比如我们网站自己没有韩语的翻译, 但是经常被用过翻译成韩语, 那么我们可以考虑增加韩语呢?

     再比如我们虽然提供了韩语的翻译, 但是用户仍然主动将网站翻译成韩语, 那就要考虑是不是我们可以将已有的翻译功能, 更明确的提示给用户使用? 毕竟我们自带的翻译更准确体验也更好。

1: MutationObserver 简介

     可以监控dom元素本身的所有改变, MutationObserver实例化后要传入一个配置项, 这个配置项可以自定义需要监听dom的哪些变化, 下面列出主要的几个属性:

  1. attributes 布尔值, 监测dom的属性变化
  2. attributeFilter 数组, 监测dom的具体某个属性的变化, 填写这个参数就不用每次判断哪个属性变化了
  3. childList布尔值, 子元素的变化
  4. characterData布尔值, 监视指定目标节点或子节点树中节点所包含的字符数据的变化

     需要注意的是: childList,attributes 或者 characterData 三个属性之中,至少有一个必须为 true,否则会抛出 TypeError 异常。

2: 监控翻译的思路

     由于每次谷歌翻译都会修改我们的<html lang="en">标签的lang属性, 所以我们就监控这个属性的变化, 并且由于哪怕当前是lang="en"谷歌翻译成英语还是lang="en", 也是会被检测到的 , 所以这个技术方案也是可行的。

     新建一个listenerI18n.js文件, 已插件的形式引入即可:

(function () {
  const oHtml = document.getElementsByTagName("html")[0];
  const observer = new window.MutationObserver((mutations) => {
    console.log(`上报: 翻译为  ${oHtml.getAttribute("lang")}`);
  });
  observer.observe(oHtml, { attributes: true, attributeFilter: ["lang"] });
})();

     比较幸运的是就算你设置了<html lang="en" translate="no">禁止使用翻译功能, 上述方法仍能检测到用户想要用谷歌翻译翻译成什么语言。

image.png

我整理了一份谷歌翻译的对照表, 有需要的同学请自取, 里面并不全面但罗列出比较常用的国家:

[
  {
    "name": "简体中文",
    "lang": "zh-CN"
  },
  {
    "name": "英语",
    "lang": "en"
  },
  {
    "name": "阿拉伯语",
    "lang": "ar"
  },
  {
    "name": "爱尔兰语",
    "lang": "ga"
  },
  {
    "name": "白俄罗斯语",
    "lang": "be"
  },
  {
    "name": "保加利亚语",
    "lang": "bg"
  },

  {
    "name": "繁体中文",
    "lang": "zh-TW"
  },
  {
    "name": "波兰语",
    "lang": "pl"
  },
  {
    "name": "波斯语",
    "lang": "fa"
  },
  {
    "name": "丹麦语",
    "lang": "da"
  },
  {
    "name": "德语",
    "lang": "de"
  },
  {
    "name": "俄语",
    "lang": "ru"
  },

  {
    "name": "法语",
    "lang": "fr"
  },
  {
    "name": "菲律宾语",
    "lang": "tl"
  },
  {
    "name": "芬兰语",
    "lang": "fi"
  },
  {
    "name": "高棉语",
    "lang": "km"
  },
  {
    "name": "格鲁吉亚语",
    "lang": "ka"
  },
  {
    "name": "哈萨克语",
    "lang": "kk"
  },
  {
    "name": "韩语",
    "lang": "ko"
  },
  {
    "name": "荷兰语",
    "lang": "nl"
  },
  {
    "name": "老挝语",
    "lang": "lo"
  },
  {
    "name": "罗马尼亚语",
    "lang": "ro"
  },
  {
    "name": "马来语",
    "lang": "ms"
  },
  {
    "name": "蒙古语",
    "lang": "mn"
  },
  {
    "name": "孟加拉语",
    "lang": "bn"
  },
  {
    "name": "缅甸语",
    "lang": "my"
  },
  {
    "name": "尼泊尔语",
    "lang": "ne"
  },
  {
    "name": "挪威语",
    "lang": "no"
  },
  {
    "name": "葡头牙语",
    "lang": "pt"
  },
  {
    "name": "日语",
    "lang": "ja"
  },
  {
    "name": "瑞典语",
    "lang": "sv"
  },
  {
    "name": "世界语",
    "lang": "eo"
  },
  {
    "name": "泰语",
    "lang": "th"
  },
  {
    "name": "土耳其语",
    "lang": "tr"
  },
  {
    "name": "乌克兰语",
    "lang": "uk"
  },
  {
    "name": "西班牙语",
    "lang": "es"
  },
  {
    "name": "希腊语",
    "lang": "el"
  },
  {
    "name": "匈牙利语",
    "lang": "hu"
  },
  {
    "name": "意大利语",
    "lang": "it"
  },
  {
    "name": "印度尼西亚语",
    "lang": "id"
  },
  {
    "name": "越南语",
    "lang": "vi"
  },
  {
    "name": "爪哇语",
    "lang": "jw"
  }
]

九、总结

     看似"人畜无害"的翻译功能, 背后竟然隐藏着各种深浅不一的"坑井", 假设在某些"金额结算页"用户使用了谷歌翻译, 那么是否会造成不小的问题, 所以最好的办法还是提供完善的"语言切换"功能才是王道啊。

end

     这次就是这样, 希望与你一起进步。


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者