9
头图

背景、

     国际化项目会用到一大堆的i18n_key来处理文案, 直接看下面的例子吧:
image.png

image.png

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

  <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>

     我希望做一款谷歌插件, 它可以让网站随意切换为下面这个样子:
image.png

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

     随着项目的不断壮大, 像是上图的 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文件夹,以存放国际化相关配置:

image.png

image.png

     对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"]
    }
  ]
}

image.png

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

image.png

五、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按钮"与"展示翻译结果"。

image.png

点击可以展示i18n_key
image.png

image.png

先封装一个创建按钮的方法, 并附加上一些基本样式:

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

八、兼容未适配的项目

image.png

     大多数网站是没有适配这个插件的, 所以需要我们来适配这个情况, 先创建一个"项目未适配"的按钮:

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

image.png

九、增加项目信息的展示

     只有切换语言这一个功能有点大材小用了, 所以当前增加了一个展示项目信息的能力, 如图所示:

image.png

     原理也是比较直白, 识别出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

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


lulu_up
5.7k 声望6.9k 粉丝

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