谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火

背景、 ???? 国际化项目会用到一大堆的i18n_key来处理文案, 直接看下面的例子吧:
谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

但是实际上我们的代码里可能是这样的:

{t("page_home_title_welcome")}
{t("page_home_main_content")}

???? 我希望做一款谷歌插件, 它可以让网站随意切换为下面这个样子:
谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

一、这插件什么场景使用? ???? 随着项目的不断壮大, 像是上图的 page_home_nav_switch_language这种i18n_key, 已经 n千多条了, 并且每次功能的合并或者是改版, 可能都会涉及到i18n_key的改写。
???? 如果一个网站同时兼容多国语言, 比如提供8个国家的语言, 那么翻译后的文案展示相关问题会激增。
???? 我遇到多次的实际问题就是, 某个模块的某个按钮的xx国家语言下文案出了问题, 此时产品同学就会at我, 让我帮忙找这个文案对应的key是什么, 寻找key的过程也不容易, 因为翻译的文案重复的太多了, 比如一个按钮文案是"ok", 那么全局这些key都对应着"ok",
page_home_title_model_ok:"ok", page_user_nav_create_model_ok: "ok", page_user_title_error_ok:"ok", user_detail_model_ok:"ok", //...

???? 我一般需要通过业务来确定代码所在文件, 然后再逐一排查, 这个过程经历过才知道有多"墨迹", 所以一定要做一款插件解救pm也解救自己。
【谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火】???? 插件做出来后收到了产品同学的强烈感谢!
二、搭建简易的i18n项目 ???? 为了演示插件的效果, 我这里真实的搭建一个简易的react_i18n项目:
npx react-react-app react_i18n

???? 进入创建好的项目内, 安装 i18n 相关包:
yarn add i18next react-i18next

???? 在src下新建i18n文件夹,以存放国际化相关配置:
谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

???? 对index.js文件进行配置:
import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import enTranslation from "./en.json"; import zhTranslation from "./zh.json"; const lng = "zh"; i18n.use(initReactI18next).init({ resources: { en: { translation: enTranslation }, zh: { translation: zhTranslation }, }, lng, fallbackLng: lng, interpolation: { escapeValue: false }, }); export default i18n;

???? 上述i18n代码在index.js入口文件里面初始化一次:
import i18n from "./i18n/index";

???? 在组件中就可以正常使用了, 这里用的是reactfunction组件来演示:
import i18n from "./i18n/index"; import { useTranslation } from "react-i18next"; function App() { const { t } = useTranslation(); return ({t("page_home_title_welcome")}
{t("page_home_main_content")}); }export default App;

???? 可以看到useTranslationhook的形式。
三、对i18n函数的封装 ???? 对i18n函数进行封装的好处是, 可以统一管理一些默认值, 或者是各种报错的埋点, 并且可以配合我们的插件, 在src下创建usei18nformat.js文件:
import { useTranslation } from "react-i18next"; export default () => { const { t } = useTranslation(); return (key, defaultVal) => { const value = https://www.it610.com/article/t(key); return value === key ? defaultVal : value; }; };

  1. 上面我延续了使用hook这种模式。
  2. 增加接收defaultVal默认值, 这样当i18n_key翻译失败的时候, 可以展示兜底文案。
  3. value =https://www.it610.com/article/== key ? defaultVal : value这里的比较是因为, react-i18next默认是当无法翻译的时候返回i18n_key,但这样的处理很不友好, 因为失去了可读性。
  4. 翻译失败的场景有, 前端写错了i18n_key, i18n_key更新了但是前端未更新, 以及随着翻译的增多, i18n文件夹内的文件都是从server异步获取的, 所以网络出现问题会导致翻译失败。
四、创建谷歌插件 ???? 终于"主人公"出现了, 未开发过谷歌插件的推荐先看看我的入门文章:
???? 谷歌插件入门文章推荐(上)
???? 谷歌插件入门文章推荐(下)
???? 先展示manifest.json文件配置:
{ "manifest_version": 2, "name": "随便起个插件名", "description": "展示i18n的key", "version": "0.1", "browser_action": { "default_icon": "images/logo.png" }, "permissions": ["contextMenus"], "background": { "page": "background/background.html" }, "content_scripts": [ { "matches": [""], "js": ["content/index.js"], "css": ["content/index.css"] } ] }

谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

???? 千万别忘了开启开发者模式, 然后就可以导入manifest.json所在文件夹了:
谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

五、content_scripts 靠你了 ???? content_scripts 是谷歌插件提供的一种能力, 开发者可以向"任意网站"或"指定网站"的html代码里插入一个script标签, 也就是开发者写的一段js代码可以运行在任何的web中, 可以获取到当前网站的domwindow信息。
???? 能够把js代码注入到web中就可以实现侵入代码啦, 可以调用web项目内已有的方法。
???? 我想到的办法是, 用的i18n项目里面的 useTranslation方法增加一个判断, 当window.xxx的值为true的时候, 则直接返回key的值, 这不就实现了页面展示i18n_key吗。
???? 这里举个例子吧, 在react_i18n项目中:
import { useTranslation } from "react-i18next"; export default () => { const { t } = useTranslation(); return (key, defaultVal) => { const value = https://www.it610.com/article/t(key); return value === key ? defaultVal : value; }; };

改写成:
import { useTranslation } from "react-i18next"; export default () => { const { t } = useTranslation(); return (key, defaultVal) => { // 新增的代码-----↓ if (window.GlobalShowI18nKey === true) { return key; } // 新增的代码-----↑ const value = https://www.it610.com/article/t(key); return value === key ? defaultVal : value; }; };

如何让react刷新 ???? 强制react刷新这个事比较难办, 首先react自身也属于闭包操作, 内部的值都是不外露的, 那思路就剩下调用react内部自己的方法了, 这里我采取的是将"切换语言"的方法同样挂载到window对象上, 这样每次我修改window.GlobalShowI18nKey的值都主动调用一次切换语言方法, 具体代码如下所示:
import i18n from "./i18n/index"; window.GlobalChangeLanguage = () => i18n.changeLanguage(i18n.language);

???? 上述代码里不用担心同语言切换问题, 比如当前是'英语'再次调用切换到'英语'依旧可以让react刷新。
六、从按钮开始编写 ???? 既然content_scripts能力让我们可以插入js代码, 那么我们就用js创建一些"按钮dom元素"并插入到body上。
???? 现在先创建一个容器两个按钮, 按钮分别是"展示i18n_key按钮"与"展示翻译结果"。
谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

点击可以展示i18n_key
谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

先封装一个创建按钮的方法, 并附加上一些基本样式:
function createBt(config) { const oBt = document.createElement("div"); oBt.classList.add("am-i18n_key-bt"); oBt.setAttribute("id", config.id); oBt.innerText = config.text; oBt.style.display = config.display || "none"; return oBt; }

创建两个按钮
const oShowI18nKeyBt = createBt({ id: "am-i18n_key_show_key-bt", text: "展示:i18n_key", display: "block", }); const oHiddenI18nKeyBt = createBt({ id: "am-i18n_key_hidden_key-bt", text: "展示:翻译结果", });

???? 按钮样式的css不展示了毕竟太基础了, 懂得了原理样式你可以天马行空。
可能存在的延迟 ???? 用户可能并不是第一时间就把GlobalChangeLanguage挂载到window上, 所以这边要做好多次判断是否有"更新翻译"的方法存在。
???? 我这里选择的是, 监听容器组件的鼠标移入操作, 鼠标移入后才决定按钮的显隐,
oTipWrap.addEventListener("mouseover", () => { // ... 移入后决定按钮的显隐 });

七、window竟然被'沙盒'了 ???? 当时写到这里遇到了个坑大家一定也要小心啊, 就是通过content_scripts获取到的网页上的window对象被沙盒了, 也就是window对象的变化我监听不到, 我对window对象身上的值进行修改也无法反馈到真正的window上, 也就是我获得的window对象就是一个深复制过来的拷贝对象...
???? 非常理解谷歌插件对widnow能力的限制, 毕竟安全无小事, 但是这种情况下进行开发就会比较费力。
???? 解决方法也呼之欲出, 我可以动态往body里面插入script标签啊, 这个插入的标签是可以获取到全局真正的widnow对象的, 缺点就是好多逻辑都要写在这个script标签里面, 一起看看下面这段控制按钮"显隐"的方法:
???? 第一步: 定义鼠标进入外层容器:
oTipWrap.addEventListener("mouseover", () => { createScript(); creatScript2updataBtStyle(); bodyAppendChildScript(); });

创建脚本
let script = null; function createScript() { if (script) script.remove(); script = document.createElement("script"); script.type = "text/javascript"; script.innerHTML = ""; }

插入脚本
function bodyAppendChildScript() { document.body.appendChild(script); }

???? 第二步: 为脚本赋予js逻辑:
function creatScript2updataBtStyle() { script.innerHTML += ` var GLOBAL_SHOW_I18N_KEY = 'GlobalShowI18nKey'; var GLOBAL_CHANGE_LANGUAGE = 'GlobalChangeLanguage'; var i18nKeyShowKeyBt = document.getElementById("am-i18n_key_show_key-bt"); var i18nKeyHiddenKeyBt = document.getElementById("am-i18n_key_hidden_key-bt"); if(window[GLOBAL_CHANGE_LANGUAGE]){ i18nKeyHiddenKeyBt.onclick = () => { window[GLOBAL_SHOW_I18N_KEY] = false; window[GLOBAL_CHANGE_LANGUAGE]() changeBtStatus() }; i18nKeyShowKeyBt.onclick = () => { window[GLOBAL_SHOW_I18N_KEY] = true; window[GLOBAL_CHANGE_LANGUAGE]() changeBtStatus() }; function changeBtStatus(){ if (window[GLOBAL_SHOW_I18N_KEY]) { i18nKeyShowKeyBt.style.display = "none"; i18nKeyHiddenKeyBt.style.display = "block"; } else { i18nKeyShowKeyBt.style.display = "block"; i18nKeyHiddenKeyBt.style.display = "none"; } } } `; }

???? 上述代码逻辑为, 当全局GlobalShowI18nKeytrue时为展示i18n_key此时应该展示"还原按钮"以此类推。
???? 将按钮的点击事件放在这里是因为怕某些项目赋予widnow.GlobalChangeLanguage方法是异步的。
???? 之所以使用var而不是const是因为偶会出现重复定义的bug
八、兼容未适配的项目 谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

???? 大多数网站是没有适配这个插件的, 所以需要我们来适配这个情况, 先创建一个"项目未适配"的按钮:
const oGlobalNoConfigurationBt = createBt({ id: "am-global_no_configuration-bt", text: "此项目未适配", });

???? 这个按钮点击后会alert出提示框, 并且展示"插件的官网"(虽然没有), 但是比如把当前这篇文章地址复制到用户的剪切板里。
oGlobalNoConfigurationBt.addEventListener("click", () => { const aux = document.createElement("input"); aux.setAttribute( "value", `xxxxxxxxxx官网地址` ); document.body.appendChild(aux); aux.select(); document.execCommand("copy"); document.body.removeChild(aux); alert(`插件文档url: 已复制到剪切板`); });

谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

九、增加项目信息的展示 ???? 只有切换语言这一个功能有点大材小用了, 所以当前增加了一个展示项目信息的能力, 如图所示:
谷歌插件:|谷歌插件: 页面展示i18n的原始key, 营救pm于水火
文章图片

???? 原理也是比较直白, 识别出webwindow.GlobalProjectInformation上有值, 然后再以table的形式进行展示, 先展示i18n项目的配置:
window.GlobalProjectInformation = { title:['name','Version', 'user', 'env'], context:[ ['home页面','v2.13.09', 'lulu', '测试环境'], ['user页面','v3.8.06', 'lulu', '测试环境'] ] };

???? 这里增加一个解析项目信息的方法:
oTipWrap.addEventListener("mouseover", () => { createScript(); creatScript2updataBtStyle(); // 新增代码---- ↓ showProjectInformation(); // 新增代码---- ↑ bodyAppendChildScript(); });

动态插入table元素即可, 若用户未配置则不作操作:
function showProjectInformation() { script.innerHTML += ` var GLOBAL_PROJECT_INFOR = 'GlobalProjectInformation'; var data = https://www.it610.com/article/window[GLOBAL_PROJECT_INFOR] if(data){ var oProjectInfor = document.getElementById("am-project-information-wrap"); oProjectInfor.style.display = "block"var tdTitleListString = "" data.title.forEach((item)=>{ tdTitleListString += ""+item+"" })var tdContextListString = "" data.context.forEach((trItem)=>{ var str = "" trItem.forEach((tdItem)=>{ str += ""+tdItem+"" }) tdContextListString += ""+ str +" " })oProjectInfor.innerHTML = \` \${tdTitleListString} \${tdContextListString}
\` } `; }

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

    推荐阅读