When I did some front-end internationalization projects before, because the business was not very complicated, the relevant requirements generally stayed in the translation of copywriting, that is, the internationalization of multiple languages, basically using the relevant I18n plug-in can meet the requirements of development. However, with the increase of business iteration and requirement complexity, these I18n plug-ins may not be able to meet the requirements of relevant development. Next, I will talk with you about the problems encountered in the process of internationalization project and the thinking.

Because the team’s technology stack is based primarily on Vue, the solution is also based on Vue and the associated internationalization plug-in (VUE-I18N).

The first phase of

background

We use VUE-I18N to complete the work related to internationalization. When the project is relatively simple and does not have a large number of language package files, packaging the language package directly into the business code is not much of a problem. However, once the number of language package files increases, it is possible to consider packaging the language package separately to reduce the volume of business code and use it in the way of asynchronous loading. In addition, considering that internationalized language packages are relatively infrequently modified, it can be considered to cache the language packages and preferentially get them from the cache during each page rendering to speed up page opening.

The solution

About subcontracting related work can use Webpack to automatically complete the subcontracting and asynchronous loading work. Since 1.x, Webpack has provided related apis such as require.ensure() to do the job of subcontracting the language pack, but at that point require.Ensure () had to accept a specified path, starting with 2.6.0, Webpack’s import syntax can specify different schema resolution dynamic imports, as described in the documentation. Therefore, combined with webpack and vuE-I18N to provide the relevant API can complete the subcontracting of the language package and asynchronous loading of the language package, while completing the language switch at run time.

Sample code:

File directory structure:

src
|--components
|--pages
|--di18n-locales  // Project application language package
|   |--zh-CN.js
|   |--en-US.js
|   |--pt-US.js
|--App.vue
|--main.js
Copy the code

main.js:

import Vue from 'vue'
import VueI18n from 'vue-i18n'
import App from './App.vue'

Vue.use(VueI18n)

const i18n = new VueI18n({
    locale: 'en'.messages: {}})function loadI18nMessages(lang) {
    return import(`./di18n-locales/${lang}`).then(msg= > {
        i18n.setLocaleMessage(lang, msg.default)
        i18n.locale = lang
        return Promise.resolve()
    })
}

loadI18nMessages('zh').then((a)= > {
  new Vue({
    el: '#app',
    i18n,
    render: h= > h(App)
  })
})
Copy the code

The problems of subcontracting and asynchronous loading of language packages are solved above.

Next, we will talk about how to cache language packages and the related caching mechanism.

After opening the page, determine whether localStorage has the corresponding language package file first. If so, obtain the language package directly from localStorage synchronously, and then complete the page rendering. If not, then need to obtain the language package asynchronously from CDN. And the language package cache to localStorage, and then complete the page rendering.

Of course, the following issues need to be considered in the implementation process:

  • If the language pack is updated, how do I update the language pack cached in localStorage?

    First, in the process of code compilation, the Webpack plug-in is used to collect the hash value of the language package version after each compilation and inject it into the business code at the same time. When the page is displayed and the service code starts to run, the system checks whether the language package version in the service code is the same as that cached in localStorage. If the version is the same, the system synchronously obtains the language package file. If the version is different, the system asynchronously obtains the language package

  • How is the version number and language package stored in localStorage?

    Data is stored in localStorage. LocalStorage is divided by domain name. If multiple internationalization projects are deployed under the same domain name, namespace can be divided by project name to avoid overwriting language package/version hash.

The above are some simple optimizations for the internationalization project at the initial stage. To sum up, the language package is separately packaged into chunk, and provides asynchronous loading and localStorage storage functions, which speeds up the next page opening speed.

Phase ii

background

With the iteration of projects and the increase of internationalization projects, more and more components are isolated into component libraries for reuse, some of which also need to support internationalization of multiple languages.

Existing solution

Vue-i18n supports component internationalization at this stage. Please refer to the documentation for specific usage. The general idea is to provide the ability to locally register vue-i18N instance objects. The vuE-i18N object instantiated on the child component is first retrieved, and the local language map is then performed.

It provides only partial component registration of the language package, which will be packaged into the business code during the final code compilation and packaging process, which is not compatible with our initial optimization goals for internationalization projects (although this is fine if your Component is asynchronous).

Optimization scheme

In order to continue to improve the component internationalization scheme on the basis of the initial goal, we try to decouple the language package of the component from the component, that is, the component does not need to introduce multi-language package separately, and the language package of the component can also be loaded asynchronously.

In this way, we may encounter the following problems within the scope of our expectations:

  • Project applications have their own multilingualism, so how do you manage multilingualism between project applications and components?
  • Vue-i18n plug-in provides a local registration mechanism for multilingual components. If multilingual packages and components are decoupled, how will the multilingual text be translated when the final component is rendered?
  • There are also parent-child/nested components inside the component library, so how should the multilingual package inside the component library be managed and organized?
  • .

First of all, within our team, postcompilation (postcompilation can poke me) should be standard in our technology stack, so our component library will eventually be released directly through source code, and used in project applications by introducing + postcompilation on demand.

The multilingual package organization of the project application should not be a problem and is generally placed in a separate directory (DI18n-Locales) :

// Directory structure:SRC ├ ─ ─ App. Vue ├ ─ ─ di18n - locales │ ├ ─ ─ en - US. Js │ └ ─ ─ useful - CN. Js └ ─ ─ the main, js// en-US.js
export default {
    messages: {
        'en-US': {
            viper: 'viper'.sk: 'sk'}}}// zh-CN.js
export default {
    messages: {
        'zh-CN': {
            viper: 'Underworld Sub-dragon'.sk: 'king of sand'}}}Copy the code

Each language package in the DI18N-Locales directory will eventually be packaged separately into a chunk. Therefore, we consider whether the language package of each component in the component library can also be packaged together with the language package of the project application as a chunk: This means that the en-us.js of the project application is packaged together with the en-us.js of all the components in the component library that are referenced by the project. The same applies to other language packages. The purpose of this is to decouple the language packages of the component library from the components (as opposed to vuE-I18n’s scheme), while simultaneously packaging the language packages of the project application for asynchronous loading. To this end, we plan the directory of the component library with the following convention: there will also be a DI18N-Locales (the same directory as the language package directory of the project application, but also configurable) directory, which holds the multilanguage package of each component:

├ ─ ─ node_modules | ├ ─ ─ @ didi | ├ ─ ─ common - biz - UI | └ ─ ─ the SRC | └ ─ ─ components | ├ ─ ─ coupon - list | │ ├ ─ ─ coupon - list. Vue | │ └ ─ ─ di18n - locales | │ ├ ─ ─ en. Js// The en language package for the current component| │ └ ─ ─ useful. Js// Specifies the zh language package of the current component| └ ─ ─ withdraw | ├ ─ ─ withdraw. Vue | └ ─ ─ di18n - locales | ├ ─ ─ en. Js// The en language package for the current component| └ ─ ─ useful. Js// Specifies the zh language package of the current component├ ─ ─ the SRC │ ├ ─ ─ App. Vue │ ├ ─ ─ di18n - locales │ │ ├ ─ ─ en. Js// The project uses the EN language package│ │ └ ─ ─ useful. Js// The project uses the zh language package│ └ ─ ─ the main jsCopy the code

When your project application uses a component from the component library:

// App.vue
<template>
    ...
</template>

<script>
import couponList from 'common-biz-ui/coupon-list'
export default {
    components: {
        couponList
    }
}
</script>
Copy the code

So without you having to manually import a language pack:

  1. How to get itcoupon-listLanguage packs under this component?
  2. willcoupon-listThe language package used by the component is packaged into the corresponding language package of the project application and output a chunk?

For this we developed a webpack plug-in: di18n-webpack-plugin. To solve the above two problems, let’s take a look at the core code of this plug-in:

compilation.plugin('finish-modules'.function(modules) {... for(const module of modules) {
        const resource = module.resource || ' '

        if (that.context.test(resource)) {
          const dirName = path.dirname(resource)
          const localePath = path.join(dirName, 'di18n-locales')
          if(fs.existsSync(localePath) && ! di18nComponents[dirName]) { di18nComponents[dirName] = {cNameArr: [].path: localePath
            }
            const files = fs.readdirSync(dirName)
            files.forEach(file= > {
              if (path.extname(file) === '.vue') {
                const baseName = path.basename(file, '.vue')
                const componentPath = path.join(dirName, file)
                const prefix = getComponentPrefix(componentPrefixMap, componentPath)
                let componentName = ' '
                if (prefix) {
                  // transform to camelize style
                  componentName = `${camelize(prefix)}${baseName.charAt(0).toUpperCase()}${camelize(baseName.slice(1))}`
                } else {
                  componentName = camelize(baseName)
                }
                // component namedi18nComponents[dirName].cNameArr.push(componentName) } }) ... }}})Copy the code

In the finishing-modules phase, all modules are compiled, so you can find out which components are used in the component library. Since we have already agreed that there will be a di18n-Locales directory at the same level as the component to store the multilingual files of the component, we can also find the language package used by the component. The result is a hook function that uses the component path as the key to complete the collection. This solves the first problem above.

Let’s look at the second question. When we use the finish-modules hook to see which components are imported as needed, we have an awkward problem. The finish-modules phase is triggered after all modules are compiled, and then the seal phase. However, no module compilation is done in the SEAL phase.

But by reading the Webpack source code, we found that there was a compilation method that defined a rebuildModule from the compilation. The method-specific internal implementation does call the buildModule method on the Compliation object to compile a module:

class Compilation extends Tapable {
    constructor() {... }... rebuildModule() { ... this.buildModule(module.false.module.null, err => { ... })}... }Copy the code

Since our goal from the beginning is that the multilingual packages in the component library and components are decoupled from each other and insensitive to the project application, so the WebPack plug-in is required to complete the packaging work during compilation. Therefore, in view of the second question above, After completing the finish-modules phase, we try to get the multilanguage package paths of all the components used by the project, and then automatically add the multilanguage package as dependencies to the source code of the project application, and re-compile the language package of the project application through the rebuildModule method. This completes the process of injecting the insensitive language bundle into the project application’s language bundle as a dependency.

Webpack’s buildModule flow is as follows:

We see that in the rebuild process, Webpack will use the loader of the corresponding file type again to load the source code of relevant files into the memory, so we can complete the addition of the dependent language package at this stage. Let’s take a look at the core code of the di18n-webpack-plugin:

compilation.plugin('build-module'.function (module) {
      if (!module.resource) {
        return
      }
      // di18n rules
      if (/src\/di18n-locales\//.test(module.resource) && module.createSource.name ! = ='di18nCreateSource') {
        ...

          if (!componentMsgs.length) {
            return createSource.call(this, source, resourceBuffer, sourceMap)
          }
          let vars = []
          const varReg = /export\s+default\s+([^{;] +) /
          const exportDefaultVar = source.match(varReg)

          source = `
          ${componentMsgs.map((item, index) => {
            const varname = `di18n${index + 1}`
            const { path, cNameStr } = item
            vars.push({
              varname,
              cNameStr
            })
            return `import ${varname} from "${path}";`
          }).join('')}
          ${
            exportDefaultVar
              ? source.replace(varReg, function (_, m) {
                  return `
                    ${m}.components = {
                      ${getComponentMsgMap(vars)}
                    };
                    export default ${m}
                  `
                })
              : source.replace(/export\s+default\s*\{([^]+)\}/i, function (_, m) {
                  return `export default {${m},
                    components: {
                      ${getComponentMsgMap(vars)}
                    }
                  }
                  ` `})}
          resourceBuffer = new Buffer(source)
          return createSource.call(this, source, resourceBuffer, sourceMap)
        }
      }
    })
Copy the code

The build-Module hook is exposed when the Module is first compiled using WebPack. Its callback is passed to the module being compiled. Before the createSource method is called, the component’s language package is introduced by rewriting the source code for the project application language package. After that, the process is handled by WebPack. Eventually, each language package of the project application is packaged separately into a chunk, and this language package also includes the language packages of the components imported on demand.

The end result is:

// The original project uses the Chinese (zh.js) language package

export default {
    messages: {
        zh: {
            hello: 'hello'.goodbye: 'goodbye'}}}Copy the code

Di18n-webpack-plugin for project application Chinese language package:

// Automatically introduce the corresponding language package of the project dependent components into the language package of the project application, complete compilation and output as a chunk

import bizCouponList from 'xxxx/xxxx/node_modules/xxx/src/components/coupon-list/di18n-locales/zh.js' // The path of the component language package is absolute

export default {
    messages: {
        zh: {
            hello: 'hello'.goodbye: 'goodbye'}},components: {
        bizCouponList
    }
}
Copy the code

(After importing the component’s language package here, we add a components field to the project language package and attach the name of the child component as key and the language package of the child component as value to the Components field.)

The above process solves some of the problems raised earlier:

  1. How do I get the language package used by the component
  2. How to package the language package used by the component into the language package of the project application and export a separate chunk
  3. How do you manage the organization of language packs between project applications and components

Now we through the Webpack plug-in in the compilation link has helped me to solve the project language package and component language package organization, build packaging and other problems. However, there is still a problem that has not been solved for the time being. That is, after decoupling the component language package from the component, that is, the language package of the component is no longer converged to the language package under the project application in the way of multi-language local registration provided by VUE-I18N, so how can the copy translation work of the component be completed?

We all know that Vue creates a unique Component name for each VNode when creating child Component vnodes:

// src/core/vdom/create-component.js

export function createComponent() {... const vnode =new VNode(
        `vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
        data, undefined.undefined.undefined, context,
        { Ctor, propsData, listeners, tag, children },
        asyncFactory
    )

    ...

}
Copy the code

In practice, we require that components have their own unique names.

Vue-i18n provides a strategy to locally register vue-i18N instance objects. Whenever the translation functions $t, $TC, etc. are called inside the child component, vue-i18N instance objects instantiated on the child component will be obtained first, and then to do the local language map mapping. At this time, we can change the way of thinking, we have unified management of the language package of the sub-component, and do not register vuE-i18n instance on the sub-component, but every time the sub-component calls $t, $TC and other translation functions, At this time, we obtain the content of the corresponding language package from the unified language package according to the component-name of the sub-component, and complete the translation work.

We also mentioned above how we manage the organization of language packs between project applications and components: After we import the component language package, we add a component field to the project language package, and attach the name of the subcomponent as the key and the language package of the subcomponent as the value to the components field. In this way, when the subcomponent calls the method of the translation function, it always first finds the key of the corresponding component name in the components field of the language package of the project application, and then completes the translation function. If it does not find the key, it uses the language copy of the corresponding field of the project application.

conclusion

The above is our reflection on some recent international projects, and a summary is as follows:

  • Language bundles are individually packaged into chunks and loaded asynchronously
  • Provides the local cache function of localStorage, so you do not need to load the language package separately next time you open the page
  • Component language packages are decoupled from components. Components are unaware of their language packages and do not need to register with them separately
  • The organization and management of component language pack and project application language pack are completed by Webpack plug-in

In fact, all of the above work is aimed at reducing the dependency of related functions on officially provided plug-ins and providing a general solution to flatten the technology stack.