乐趣区

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

背景、

     国际化我的项目会用到一大堆的 i18n_key 来解决文案, 间接看上面的例子吧:

然而实际上咱们的代码里可能是这样的:

  <p className="home_title">{t("page_home_title_welcome")}</p>
  <button> {t("page_home_nav_switch_language")}</button>
  <div>{t("page_home_main_content")}</div>

     我心愿做一款谷歌插件, 它能够让网站随便切换为上面这个样子:

一、这插件什么场景应用?

     随着我的项目的一直壮大, 像是上图的 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 我的项目

     为了演示插件的成果, 我这里实在的搭建一个繁难的 react_i18n 我的项目:

npx react-react-app react_i18n

     进入创立好的我的项目内, 装置 i18n 相干包:

yarn add i18next react-i18next

     在 src 下新建 i18n 文件夹,以寄存国际化相干配置:

     对 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 (
    <div className="App">
      <p className="home_title">{t("page_home_title_welcome")}</p>
      <button
       onClick={() => {i18n.changeLanguage(i18n.language === "zh" ? "en" : "zh");
       }}
       >
        {t("page_home_nav_switch_language")}
      </button>
      <div>{t("page_home_main_content")}</div>
    </div>
  );
}

export default App;

     能够看到 useTranslationhook的模式。

三、对 i18n 函数的封装

     对 i18n 函数进行封装的益处是, 能够对立治理一些默认值, 或者是各种报错的埋点, 并且能够配合咱们的插件, 在 src 下创立 usei18nformat.js 文件:

import {useTranslation} from "react-i18next";

export default () => {const { t} = useTranslation();
  return (key, defaultVal) => {const value = t(key);
    return value === key ? defaultVal : value;
  };
};
  1. 下面我连续了应用 hook 这种模式。
  2. 减少接管 defaultVal 默认值, 这样当 i18n_key 翻译失败的时候, 能够展现兜底文案。
  3. value === 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": ["<all_urls>"],
      "js": ["content/index.js"],
      "css": ["content/index.css"]
    }
  ]
}

     千万别忘了开启开发者模式, 而后就能够导入 manifest.json 所在文件夹了:

五、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 = 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 = 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

先封装一个创立按钮的办法, 并附加上一些根本款式:

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

八、兼容未适配的我的项目

     大多数网站是没有适配这个插件的, 所以须要咱们来适配这个状况, 先创立一个 ” 我的项目未适配 ” 的按钮:

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: 已复制到剪切板 `);
});

九、减少我的项目信息的展现

     只有切换语言这一个性能有点大材小用了, 所以以后减少了一个展现我的项目信息的能力, 如图所示:

     原理也是比拟直白, 辨认出 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 = 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 += "<td>"+item+"</td>"})

    var tdContextListString = ""
    data.context.forEach((trItem)=>{
      var str = ""
      trItem.forEach((tdItem)=>{str += "<td>"+tdItem+"</td>"})
      tdContextListString += "<tr>"+ str +"</tr>"
    })
    
    oProjectInfor.innerHTML = \`
    <table id="am-project-information-table">
    <thead>
      <tr> \${tdTitleListString} </tr>
    </thead>
    <tbody> \${tdContextListString} </tbody>
  </table>
  \`
  }
  `;
}

end

     这次就是这样, 心愿与你一起提高。

退出移动版