background,
Internationalization projects will use a lot of i18n_key to process copywriting, just look at the following example:
But actually our code might look like this:
<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>
I want to make a google plugin, which can make the website switch to the following at will:
1. In what scenarios is this plugin used?
With the continuous growth of the project, like the picture above page_home_nav_switch_language
this i18n_key
, there have been more than n thousand entries, and every time the function is merged or revised, it may involve Rewrite of i18n_key
.
If a website is compatible with multiple languages at the same time, such as providing the languages of 8 countries, then the problems related to the display of translated copy will increase.
The practical problem I have encountered many times is that there is a problem with the text in the xx country language of a button of a certain module. At this time, the product classmates will att me and ask me to help find the key corresponding to this copy, find the key The process is not easy, because there are too many repetitions in the translated copy. For example, a button copy is "ok", then the global keys correspond to "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",
//...
I usually need to determine the file where the code is located through business, and then check one by one. Only after this process can I know how much "ink" is there, so I must make a plug-in to save PM and myself.
After the plugin was made, I received strong thanks from the product classmates😁!
Second, build a simple i18n project
In order to demonstrate the effect of the plug-in, I will actually build a simple react_i18n
project here:
npx react-react-app react_i18n
Enter the created project, install i18n
related packages:
yarn add i18next react-i18next
Create a new i18n folder under src to store internationalization-related configurations:
Configure the index.js file:
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;
The above i18n
code is initialized once in the index.js
entry file:
import i18n from "./i18n/index";
It can be used normally in the component. Here is the react
function
component of ---61a30ea844b2d5da3c56684034d3db47--- to demonstrate:
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;
It can be seen that useTranslation
is in the form of hook
.
3. Encapsulation of i18n functions
The advantage of encapsulating the i18n function is that it can manage some default values in a unified way, or the buried points of various errors, and can cooperate with our plug-in to create a usei18nformat.js
src
under ---cbf798b2e7e55e838b180669ddd128c1--- :
import { useTranslation } from "react-i18next";
export default () => {
const { t } = useTranslation();
return (key, defaultVal) => {
const value = t(key);
return value === key ? defaultVal : value;
};
};
- Above I continued the pattern of using hooks.
- Add the default value of receiving
defaultVal
, so that when the translation ofi18n_key
fails, you can display the bottom line. -
value === key ? defaultVal : value
The comparison here is because,react-i18next
The default is to returni18n_key
when it cannot be translated, but this processing is very unfriendly, because it loses readability . - The translation failure scenarios are, the front end is wrongly
i18n_key
,i18n_key
updated but the front end is not updated, and with the increase of translation,i18n
in the folder The files are all obtained asynchronously fromserver
, so a network problem will cause the translation to fail.
4. Create a Google Plugin
Finally, the "protagonist" appeared. If you haven't developed a Google plug-in, please take a look at my introductory article first:
Recommended articles for getting started with Google plugins (Part 1)
Recommended articles for getting started with Google plugins (below)
First show manifest.json
file configuration:
{
"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"]
}
]
}
Don't forget to turn on the developer mode, and then you can import the folder where manifest.json
is located:
5. content_scripts is up to you
content_scripts
is a capability provided by the Google plugin, developers can insert a script
tag into the code of "any website" or "designated website" html
3adc03aa441a18d7d865b5b2d747dcca---,也就是开发者写的js代码
web
中, 可以获取到当前网站的dom
与window
信息.
If you can inject js
code into the web, you can implement the intrusion code, and you can call the existing methods in the web project.
我想到的办法是, 用的i18n
项目里面useTranslation
, 当window.xxx
true
的时候, then directly returns the value of key
, which realizes the page display i18n_key
.
Here is an example, in the react_i18n
project:
import { useTranslation } from "react-i18next";
export default () => {
const { t } = useTranslation();
return (key, defaultVal) => {
const value = t(key);
return value === key ? defaultVal : value;
};
};
Rewrite as:
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;
};
};
How to make react refresh
Mandatory react
refreshing is more difficult to do, first of all react
itself is also a closure operation, the internal values are not exposed, then the idea is to call react
have my own method, here I am using the method of "switching language" to also mount it to the window
object, so that every time I modify the value of window.GlobalShowI18nKey
it will take the initiative Call the switch language method once, the specific code is as follows:
import i18n from "./i18n/index";
window.GlobalChangeLanguage = () => i18n.changeLanguage(i18n.language);
There is no need to worry about the same language switching problem in the above code. For example, if the current 'English' is called and switched to 'English', react
can still be refreshed.
6. Start writing from the button
content_scripts
能力js
代码, 那么我们就用js
"按钮dom元素" body
superior.
Now create a container with two buttons, the buttons are "Display i18n_key button" and "Display translation result".
Click to display i18n_key
First encapsulate a method to create a button, and attach some basic styles:
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;
}
Create two buttons
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: "展示:翻译结果",
});
The button style css
is too basic to not show, after you understand the principle style, you can do whatever you want.
possible delay
Users may not mount ---c33336b4fbf216c69118fbb7b93df7ec GlobalChangeLanguage
to window
at the first time, so it is necessary to check whether there is an "update translation" method.
What I choose here is to monitor the mouse-in operation of the container component, and only after the mouse is moved in can the button appear or hide.
oTipWrap.addEventListener("mouseover", () => {
// ... 移入后决定按钮的显隐
});
7. The window has been 'sandboxed'
At that time, I wrote that I encountered a pit and everyone must be careful, that is, the window
object on the webpage obtained through content_scripts
--- is sandboxed, that is, window
can't monitor the changes of the object, and I can't modify the value of the window
object and it can't be fed back to the real window
, which is what I got window
object is a deep copy of the copied object...
I understand very well the limitation of the Google plugin on the ability of widnow
. After all, safety is no small matter, but it will be more laborious to develop in this case.
The solution is also on the body
. I can dynamically insert the script
tag into ---01e127402d94ba08231325926813ee59---. This inserted tag can get the global real widnow
object. That is, a lot of logic has to be written in this script
tag, let's take a look at the method of "showing and hiding" the control button below:
Step 1: Define the mouse to enter the outer container:
oTipWrap.addEventListener("mouseover", () => {
createScript();
creatScript2updataBtStyle();
bodyAppendChildScript();
});
CREATE 脚本
let script = null;
function createScript() {
if (script) script.remove();
script = document.createElement("script");
script.type = "text/javascript";
script.innerHTML = "";
}
insert script
function bodyAppendChildScript() {
document.body.appendChild(script);
}
Step 2: Give the script js
logic:
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";
}
}
}
`;
}
The above code logic is, when the global GlobalShowI18nKey
is true
, it will display i18n_key
the "restore button" should be displayed at this time and so on.
The click event of the button is put here because I am afraid that some items are given widnow.GlobalChangeLanguage
method is asynchronous.
The reason for using var
instead of const
is that there are occasional duplicate definitions of bug
.
8. Compatible with unadapted projects
Most websites are not adapted to this plugin, so we need to adapt to this situation, first create a "project not adapted" button:
const oGlobalNoConfigurationBt = createBt({
id: "am-global_no_configuration-bt",
text: "此项目未适配",
});
After clicking this button, a prompt box will appear alert
and display the "plug-in's official website" (although there is no), but for example, copy the current article address to the user's clipboard.
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: 已复制到剪切板`);
});
9. Increase the display of project information
Only the function of switching the language is a bit overkill, so the ability to display project information is currently added, as shown in the figure:
The principle is also relatively straightforward, identifying the web
of window.GlobalProjectInformation
has a value, and then displaying it in the form of a table, first showing the configuration of the i18n
project:
window.GlobalProjectInformation = {
title:['name','Version', 'user', 'env'],
context:[
['home页面','v2.13.09', 'lulu', '测试环境'],
['user页面','v3.8.06', 'lulu', '测试环境']
]
};
Here is a method for parsing project information:
oTipWrap.addEventListener("mouseover", () => {
createScript();
creatScript2updataBtStyle();
// 新增代码---- ↓
showProjectInformation();
// 新增代码---- ↑
bodyAppendChildScript();
});
Dynamically insert the table element, if the user does not configure it, do nothing:
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
That's it this time, hope to progress with you.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。