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 /…