9
头图

background,

Internationalization projects will use a lot of i18n_key to process copywriting, just look at the following example:
image.png

image.png

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:
image.png

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:

image.png

image.png

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;
  };
};
  1. Above I continued the pattern of using hooks.
  2. Add the default value of receiving defaultVal , so that when the translation of i18n_key fails, you can display the bottom line.
  3. value === key ? defaultVal : value The comparison here is because, react-i18next The default is to return i18n_key when it cannot be translated, but this processing is very unfriendly, because it loses readability .
  4. 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 from server , 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"]
    }
  ]
}

image.png

Don't forget to turn on the developer mode, and then you can import the folder where manifest.json is located:

image.png

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中, 可以获取到当前网站的domwindow信息.

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

image.png

Click to display i18n_key
image.png

image.png

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

image.png

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

image.png

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:

image.png

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.


lulu_up
5.7k 声望6.9k 粉丝

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