Vue-i18n brief introduction and use

Hello, I’m Gopal. Now I am working in Shopee, a cross-border e-commerce company. Since our business involves many countries, our various systems will involve international translation. Vue I18n is an internationalized plug-in for vue.js that makes it easy to integrate localized features into your vue.js application.

The source code reading for this article is based on version 8.24.4

Let’s take a look at an official demo

<! DOCTYPEhtml>
<html>
  <head>
    <meta charset="utf-8">
    <title>ES modules browser example</title>
    <script src=".. /.. /dist/vue-i18n.js"></script>
  </head>
  <body>
    <div id="app">
      <p>{{ $t('message.hello') }}</p>
    </div>
    <script type="module">
      // If using a modular system (for example through vuei-CLI), import vue and VueI18n, then call vuei.use (VueI18n).
      import Vue from 'https://unpkg.com/[email protected]/dist/vue.esm.browser.js'
      Vue.use(VueI18n)

      new Vue({
        // Create a Vue instance with the 'i18n' option
        // Create an instance of VueI18n with options
        i18n: new VueI18n({
          locale: 'zh'.// Set the locale
          // Prepare locale information for translation
          // Set the region information
          messages: {
            en: {
              message: {
                hello: 'hello, I am Gopal'}},zh: {
              message: {
                hello: 'Hello, this is Gopal One.'
              }
            }
          }
        })
      }).$mount('#app')
    </script>
  </body>
</html>

Copy the code

It is relatively simple to use. In this paper, we deeply understand the working principle of VUE-I18N and explore the mystery of internationalization implementation. Include:

  • What is the overall vuE-I18N architecture?
  • How does the above demo work?
  • Why can we use $t directly in the template? What did it do?
  • How does the demo manage to update the page without refreshing it?
  • Implementation of global component < i18N > and global custom instructions?

Code structure and entry

Let’s take a look at vuE-18N’s code structure as follows

├── ├─ ├.js // <i18n> Components │ ├─ ├─ design.js // Extension methods ├─ Form.js // Parse and compile the core implementation of ├─ index.js // import file ├─ install.js // Registration methods ├─ mixin.js // Handling life cycle ├─ Path. The js └ ─ ─ util. JsCopy the code

As for the overall architecture of VUE-18N, a relatively appropriate diagram has been found on the Internet, as follows. Vue-i18n provides some methods, components, custom instructions and other capabilities on the left, while vuE-I18N provides data management on the right

The entry file is index.js, which is registered by calling the install method from constructor of the VueI18n class

// Auto install if it is not done yet and `window` has `Vue`.
// To allow users to avoid auto-installation in some cases,
// this code should be placed here. See #290
/* istanbul ignore if */
if(! Vue &&typeof window! = ='undefined' && window.Vue) {
  install(window.Vue)
}
Copy the code

In the install method, there are a few main things that are done, and the following code comments, which will be mentioned later, give you an idea

// Extend the method in the Vue prototype, the code is extend.js
extend(Vue)
// Mixin Vue
Vue.mixin(mixin)
// Global directives
Vue.directive('t', { bind, update, unbind })
// Global components
Vue.component(interpolationComponent.name, interpolationComponent)
Vue.component(numberComponent.name, numberComponent)
Copy the code

After registration is complete, _initVM is called, which basically creates a Vue instance object, and many subsequent functions are associated with this._ vm

// VueI18n is not actually a Vue object, but it creates a Vue object VM internally, and many functions are associated with this VM
this._initVM({
  locale,
  fallbackLocale,
  messages,
  dateTimeFormats,
  numberFormats
})

_initVM (data: {
         locale: Locale,
         fallbackLocale: FallbackLocale,
         messages: LocaleMessages,
         dateTimeFormats: DateTimeFormats,
         numberFormats: NumberFormats
         }): void {
  // Disable Vue to print messages
  const silent = Vue.config.silent
  Vue.config.silent = true
  this._vm = new Vue({ data }) // Create a Vue instance object
  Vue.config.silent = silent
}
Copy the code

Implementation of the global method $t

Let’s take a look at the implementation of vuE-I18N’s $T method to uncover the mystery of international translation

In extent.js, we saw the $t method mounted in the Vue prototype, which is why we were able to use it directly in the template.

// Mount the $t method in the Vue prototype. This is why we can use it directly in the template
// Inject all the methods of the VueI18n object instance into the Vue instance
Vue.prototype.$t = function (key: Path, ... values: any) :TranslateResult {
  const i18n = this.$i18n
  // Use of proxy mode
  return i18n._t(key, i18n.locale, i18n._getMessages(), this. values) }Copy the code

What you see is a method that calls $t in index.js

// $t last called method
_t (key: Path, _locale: Locale, messages: LocaleMessages, host: any, ... values: any): any {if(! key) {return ' ' }
  constparsedArgs = parseArgs(... values)// If escapeParameterHtml is set to true, the interpolation parameter will be escaped before converting the message.
  if(this._escapeParameterHtml) {
    parsedArgs.params = escapeParams(parsedArgs.params)
  }
  const locale: Locale = parsedArgs.locale || _locale
  / / translation
  let ret: any = this._translate(
    messages, locale, this.fallbackLocale, key,
    host, 'string', parsedArgs.params
  )
}
Copy the code

_interpolate

Back to the main line, when _translate is called, then call

this._interpolate(step, messages[step], key, host, interpolateMode, args, [key])
Copy the code

And return

this._render(ret, interpolateMode, values, key)
Copy the code

In the _render method, custom methods can be called to handle interpolated objects, or the default method can be used to handle interpolated objects.

_render (message: string | MessageFunction, interpolateMode: string, values: any, path: string): any {
  // Create a custom interpolation object
  let ret = this._formatter.interpolate(message, values, path)

  // If the custom formatter refuses to work - apply the default one
  if(! ret) {// The default interpolation object
    ret = defaultFormatter.interpolate(message, values, path)
  }

  // if interpolateMode is **not** 'string' ('row'),
  // return the compiled data (e.g. ['foo', VNode, 'bar']) with formatter
  return interpolateMode === 'string' && !isString(ret) ? ret.join(' ') : ret
}
Copy the code

Let’s focus on the default method handling, which is done primarily in format.js

Parse and compile in form.js

Form.js implements the BaseFormatter class, where _caches provide a layer of caching optimization that is also common. If there is no interpolating object below, return [message] and you are done.

export default class BaseFormatter {
  // Implement the cache effect
  _caches: { [key: string]: Array<Token> }

  constructor () {
    this._caches = Object.create(null)
  }

  interpolate (message: string, values: any): Array<any> {
    // If there is no interpolation object, return it directly
    if(! values) {return [message]
    }
    // If tokens exist, the assembly value is returned
    let tokens: Array<Token> = this._caches[message]
    if(! tokens) {// If there are no tokens, split them
      tokens = parse(message)
      this._caches[message] = tokens
    }
    return compile(tokens, values)
  }
}
Copy the code

When encountering the following usage

<p>{{ $t('message.sayHi', { name: 'Gopal' })}}</p>
Copy the code

Parse is an intuitive code that essentially iterates through strings. Then when you see a string with {} wrapped around it, attach the contents and type them, put them into tokens and return them.

// Iterate through strings of tokens and then find tokens with {} wrapped around them, add them to tokens and return them.
export function parse (format: string) :Array<Token> {
  const tokens: Array<Token> = []
  let position: number = 0

  let text: string = ' '
  while (position < format.length) {
    let char: string = format[position++]
    if (char === '{') {
      if (text) {
        tokens.push({ type: 'text'.value: text })
      }

      text = ' '
      let sub: string = ' '
      char = format[position++]
      while(char ! = =undefined&& char ! = ='} ') {
        sub += char
        char = format[position++]
      }
      const isClosed = char === '} '

      const type = RE_TOKEN_LIST_VALUE.test(sub)
        ? 'list'
        : isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
          ? 'named'
          : 'unknown'
      tokens.push({ value: sub, type })
    } else if (char === The '%') {
      // when found rails i18n syntax, skip text capture
      if(format[(position)] ! = ='{') {
        text += char
      }
    } else {
      text += char
    }
  }

  text && tokens.push({ type: 'text'.value: text })

  return tokens
}
Copy the code

Tokens return to the above demo as follows:

[{"type": "text"."value": "hi, I am "
    },
    {
        "value": "name"."type": "named"}]Copy the code

And parse, which assembles all of the above

// Put everything together
export function compile (tokens: Array<Token>, values: Object | Array<any>) :Array<any> {
  const compiled: Array<any> = []
  let index: number = 0

  const mode: string = Array.isArray(values)
    ? 'list'
    : isObject(values)
      ? 'named'
      : 'unknown'
  if (mode === 'unknown') { return compiled }

  while (index < tokens.length) {
    const token: Token = tokens[index]
    switch (token.type) {
      case 'text':
        compiled.push(token.value)
        break
      case 'list':
        compiled.push(values[parseInt(token.value, 10)])
        break
      case 'named':
        if (mode === 'named') {
          compiled.push((values: any)[token.value])
        } else {
          if(process.env.NODE_ENV ! = ='production') {
            warn(`Type of token '${token.type}' and format of value '${mode}' don't match! `)}}break
      case 'unknown':
        if(process.env.NODE_ENV ! = ='production') {
          warn(`Detect 'unknown' type of token! `)}break
    }
    index++
  }

  return compiled
}
Copy the code

The above demo returns [“hi, I am “, “Gopal”], and then a simple splicing is done, and the translation is done

How does VUE-I18N avoid XSS?

There is an _escapeParameterHtml in the _t method above. EscapeParams is a function of VUE-I18n to prevent XSS attacks. If escapeParameterHtml is set to true, the interpolation parameter will be escaped before converting the message.

// If escapeParameterHtml is set to true, the interpolation parameter will be escaped before converting the message.
if(this._escapeParameterHtml) {
  parsedArgs.params = escapeParams(parsedArgs.params)
}
Copy the code
/**
 * Sanitizes html special characters from input strings. For mitigating risk of XSS attacks.
 * @param rawText The raw input from the user that should be escaped.
 */
function escapeHtml(rawText: string) :string {
  return rawText
    .replace(/</g.'< ')
    .replace(/>/g.'> ')
    .replace(/"/g.'" ')
    .replace(/'/g.'' ')}/** * Escapes html tags and special symbols from all provided params which were returned from parseArgs().params. * This  method performs an in-place operation on the params object. * *@param {any} params Parameters as provided from `parseArgs().params`.
 *                     May be either an array of strings or a string->any map.
 *
 * @returns The manipulated `params` object.
 */
export function escapeParams(params: any) :any {
  if(params ! =null) {
    Object.keys(params).forEach(key= > {
      if(typeof(params[key]) == 'string') {
        // Handle parameters to prevent XSS attacks
        params[key] = escapeHtml(params[key])
      }
    })
  }
  return params
}
Copy the code

How to update the page without refreshing

And what we found in the demo is that if I change the locale or the message value, the page doesn’t refresh, but the page does update the data. This functionality is similar to Vue’s two-way data binding. How is it implemented?

Vue-i18n uses observer mode, and in the _initVM method we mentioned above, we pass the translation-related data to the this. _VM instance via new Vue. Now all you have to do is listen for changes in the data

The main logic for vuE-i18n is to call the watchI18nData method in the beforeCreate in the mixin.js file. The implementation of this method is as follows:

// To listen for translation variable changes
watchI18nData (): Function {
  const self = this
  // Use the $watch method in the vue instance to force a refresh when the data changes
  // The component's data option is a function. Vue calls this function during the creation of a new component instance. It should return an object, which Vue then wraps through a responsive system and stores in the component instance as $data
  return this._vm.$watch('$data'.() = > {
    self._dataListeners.forEach(e= > {
      Vue.nextTick(() = > {
        e && e.$forceUpdate()
      })
    })
  }, { deep: true})}Copy the code

Where _dataListeners, I understand to be instances of each one (but I didn’t think of a specific scenario where vuE-18n new was used in the system?) . SubscribeDataChanging and unsubscribeDataChanging are the functions you use to add and remove subscribers

// Add a subscriber to add an instance to use
subscribeDataChanging (vm: any): void {
  this._dataListeners.add(vm)
}

// Remove the subscriber
unsubscribeDataChanging (vm: any): void {
  remove(this._dataListeners, vm)
}
Copy the code

They are called in beforeMount and beforeDestroy in mixin.js

// Simplified code
// remove listeners can be added beforeMount and beforeDestroy after _i18n object is generated
beforeMount (): void {
  const options: any = this.$options
  options.i18n = options.i18n || (options.__i18n ? {} : null)

  this._i18n.subscribeDataChanging(this)
},


  beforeDestroy (): void {
    if (!this._i18n) { return }
    const self = this
    this.$nextTick(() = > {
      if (self._subscribing) {
        // When the component is destroyed, the instance is removed
        self._i18n.unsubscribeDataChanging(self)
        delete self._subscribing
      }
    })
}
Copy the code

To summarize, the beforeCreate will go to the watch data changes and add the subscriber in beforeMount. If data changes, the corresponding instance updates the component by force. Remove the subscriber in beforeDestroy to prevent memory overflow, as shown in the following figure

Global custom instructions and implementation of global components

In extent.js, we talked about registering global directives and global components. How do we do that

// Global directives
Vue.directive('t', { bind, update, unbind })
// Global components
Vue.component(interpolationComponent.name, interpolationComponent)
Vue.component(numberComponent.name, numberComponent)
Copy the code

Global instruction t

For details on how to use directive T, please refer to the official documentation

Here’s an example:

<! -- String syntax: literal -->
<p v-t="'foo.bar'"></p>

<! String syntax: bind by data or computed property -->
<p v-t="msg"></p>

<! Object syntax: literal -->
<p v-t="{ path: 'hi', locale: 'ja', args: { name: 'kazupon' } }"></p>

<! Object syntax: bind by data or computed property -->
<p v-t="{ path: greeting, args: { name: fullName } }"></p>

<! -- 'preserve' modifier -->
<p v-t.preserve="'foo.bar'"></p>
Copy the code

In directive.js, we see that the t and TC methods are actually called and assigned to the textContent method. (the textContent property represents the textContent of a node and its descendants.)

// Call t method and tc method
if(choice ! =null) { el._vt = el.textContent = vm.$i18n.tc(path, choice, ... makeParams(locale, args)) }else{ el._vt = el.textContent = vm.$i18n.t(path, ... makeParams(locale, args)) }Copy the code

When you unbind, you empty the textContent

Global component I18n

I18n functional components are used as follows:

<div id="app"> <! -... --><i18n path="term" tag="label" for="tos">
    <a :href="url" target="_blank">{{ $t('tos') }}</a>
  </i18n><! -... --> </div>Copy the code

The source code to achieve the SRC/components/interpolation. Js, including the tag said outer label. If false is passed, the outer layer is not required.

export default {
  name: 'i18n'.functional: true.props: {
    // Outer label. If false is passed, the outer layer is not required
    tag: {
      type: [String.Boolean.Object].default: 'span'
    },
    path: {
      type: String.required: true
    },
    locale: {
      type: String
    },
    places: {
      type: [Array.Object]
    }
  },
  render (h: Function, { data, parent, props, slots }: Object) {
    const { $i18n } = parent

    const { path, locale, places } = props
    // Implement through slot
    const params = slots()
    // Get the children list
    const children = $i18n.i(
      path,
      locale,
      onlyHasDefaultPlace(params) || places
        ? useLegacyPlaces(params.default, places)
        : params
    )

    consttag = (!! props.tag && props.tag ! = =true) || props.tag === false ? props.tag : 'span'
    // Whether the outer label is required for rendering
    return tag ? h(tag, data, children) : children
  }
}
Copy the code

Note that the Places syntax will be deprecated in the next version

function useLegacyPlaces (children, places) {
  const params = places ? createParamsFromPlaces(places) : {}

  if(! children) {return params }

  // Filter empty text nodes
  children = children.filter(child= > {
    returnchild.tag || child.text.trim() ! = =' '
  })

  const everyPlace = children.every(vnodeHasPlaceAttribute)
  if(process.env.NODE_ENV ! = ='production' && everyPlace) {
    warn('`place` attribute is deprecated in next major version. Please switch to Vue slots.')}return children.reduce(
    everyPlace ? assignChildPlace : assignChildIndex,
    params
  )
}
Copy the code

conclusion

The overall VUE-i18N code wasn’t complicated, but it took me a bit of time and was a bit of a challenge. From vuE-I18N, I learned

  • International translation vuE-I18N architecture organization and $T principle, when encountered interpolation objects, need to parse and compile
  • Vue-i18n avoids XSS by escaping characters
  • Monitor and update data through observer mode, so as to update the page without refreshing
  • Implementation of global custom directives and global components

reference

  • zhuanlan.zhihu.com/p/110112552
  • Hellogithub2014. Making. IO / 2018/07/17 /…