背景、
国际化我的项目会用到一大堆的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";
在组件中就能够失常应用了, 这里用的是react
的 function
组件来演示:
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;
能够看到useTranslation
是hook
的模式。
三、对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; };};
- 下面我连续了应用hook这种模式。
- 减少接管
defaultVal
默认值, 这样当i18n_key
翻译失败的时候, 能够展现兜底文案。 value === key ? defaultVal : value
这里的比拟是因为,react-i18next
默认是当无奈翻译的时候返回i18n_key
,但这样的解决很不敌对, 因为失去了可读性。- 翻译失败的场景有, 前端写错了
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
中, 能够获取到以后网站的dom
与window
信息。
可能把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"; } } }`;}
上述代码逻辑为, 当全局GlobalShowI18nKey
为true
时为展现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: 已复制到剪切板`);});
九、减少我的项目信息的展现
只有切换语言这一个性能有点大材小用了, 所以以后减少了一个展现我的项目信息的能力, 如图所示:
原理也是比拟直白, 辨认出web
的window.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
这次就是这样, 心愿与你一起提高。