你們好,今天給你們帶來的乾貨是vue-i18n(v7.3.0)的源碼分析。vue-i18n是用於多語言適配的vue插件,主要用於前端項目的國際化應用html
這裏是vue-18n的gayhub地址 摸我前端
首先仍是先看看做者給咱們的一個簡單的例子:vue
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>getting started</title> <script src="../../node_modules/vue/dist/vue.min.js"></script> <script src="../../dist/vue-i18n.min.js"></script> </head> <body> <div id="app"> <p>{{ $t("message.hello") }}</p> </div> <script> var messages = { en: { message: { hello: 'hello world' } }, ja: { message: { hello: 'こんにちは、世界' } } } Vue.use(VueI18n) var i18n = new VueI18n({ locale: 'ja', messages: messages }) new Vue({ i18n: i18n }).$mount('#app') </script> </body> </html>
從這個簡單的小例子中,咱們能夠看到vue-i18n的使用很是的簡單,咱們只須要定義好對應的語言包messages,而後設置一個默認語言類型locale,而後實例化出一個i18n對象並傳入咱們的vue實例就能夠愉快的使用起來node
ps:插值表達式中的$t就是vue-i18n暴露給用戶的APIgit
接下來,咱們就一塊兒看看vue-i18n的源碼究竟是怎麼樣的github
首先仍是先看看目錄結構,咱們能夠看到,源碼目錄src中有9個文件,而這9個文件便構成了vue-i18n的所有內容vuex
咱們先看看入口文件 index.jsexpress
這個文件定義了而且導出一個名爲VueI18n的類,並在類上定義了availabilities,install,version三個靜態屬性,以便經過類名直接訪問api
/* @flow */ // 導入相應的資源 import { install, Vue } from './install' import { warn, isNull, parseArgs, fetchChoice, isPlainObject, isObject, looseClone, remove, canUseDateTimeFormat, canUseNumberFormat } from './util' import BaseFormatter from './format' import I18nPath from './path' import type { PathValue } from './path' // 定義而且一個VueI18n類 export default class VueI18n { // 定義靜態屬性,在後面有賦值操做 static install: () => void static version: string static availabilities: IntlAvailability // 私有變量 _vm: any _formatter: Formatter _root: ?I18n _sync: boolean _fallbackRoot: boolean _missing: ?MissingHandler _exist: Function _watcher: any _i18nWatcher: Function _silentTranslationWarn: boolean _dateTimeFormatters: Object _numberFormatters: Object _path: I18nPath _dataListeners: Array<any> // 構造函數,默認參數是一個空對象 constructor (options: I18nOptions = {}) { // 局部變量,若是默認參數沒有傳值,則使用默認值 // locale 用於指定頁面使用的語言類型,默認是英文 const locale: Locale = options.locale || 'en-US' // fallbackLocal TODO const fallbackLocale: Locale = options.fallbackLocale || 'en-US' // message 就是用戶定義的語言包,默認是一個空對像 const messages: LocaleMessages = options.messages || {} // dateTimeFormats 日期格式 const dateTimeFormats = options.dateTimeFormats || {} // numberFormats 數字格式 const numberFormats = options.numberFormats || {} // _vm 默認的vm對象 this._vm = null // _formatter 可自定義格式化 this._formatter = options.formatter || new BaseFormatter() // missing TODO this._missing = options.missing || null // _root 保存根節點 this._root = options.root || null this._sync = options.sync === undefined ? true : !!options.sync // fallbackRoot 保存中fallback語言包的根節點 this._fallbackRoot = options.fallbackRoot === undefined ? true : !!options.fallbackRoot this._silentTranslationWarn = options.silentTranslationWarn === undefined ? false : !!options.silentTranslationWarn this._dateTimeFormatters = {} this._numberFormatters = {} this._path = new I18nPath() this._dataListeners = [] // _exist方法,用於判斷某個key是否存在於這個messages語言包中 // 主要經過path模塊的getPathValue方法實現,後面會詳細說明,在此只須要知道他的用途 this._exist = (message: Object, key: Path): boolean => { if (!message || !key) { return false } return !isNull(this._path.getPathValue(message, key)) } // 初始化vm this._initVM({ locale, fallbackLocale, messages, dateTimeFormats, numberFormats }) } _initVM (data: { locale: Locale, fallbackLocale: Locale, messages: LocaleMessages, dateTimeFormats: DateTimeFormats, numberFormats: NumberFormats }): void { const silent = Vue.config.silent Vue.config.silent = true // 實例化一個vue對象,將傳入的參數變成vue中的響應式數據,大部分vue的插件都使用這種作法,好比vuex this._vm = new Vue({ data }) Vue.config.silent = silent } // 監聽vm數據的變化,將每次的vm數據push到監聽隊列中 subscribeDataChanging (vm: any): void { this._dataListeners.push(vm) } // 取消監聽vm數據的變化 unsubscribeDataChanging (vm: any): void { remove(this._dataListeners, vm) } // 監聽i18n中定義的數據的變化,若是數據變化了,就強制更新頁面 watchI18nData (): Function { const self = this // 利用vue中$watch的api,當this._vm的數據($data)發生改變,就會觸發監聽隊列中全部vm的視圖的變化 return this._vm.$watch('$data', () => { let i = self._dataListeners.length // 遍歷全部的vm,再利用vue中的$forceUpdate的api,實現強制更新 while (i--) { Vue.nextTick(() => { self._dataListeners[i] && self._dataListeners[i].$forceUpdate() }) } }, { deep: true }) } // 監聽根節點locale的變化 watchLocale (): ?Function { /* istanbul ignore if */ if (!this._sync || !this._root) { return null } // 獲取當前的vm const target: any = this._vm // 注意:這裏是根節點的vm return this._root.vm.$watch('locale', (val) => { // 設置當前vm的locale,並強制更新 target.$set(target, 'locale', val) target.$forceUpdate() }, { immediate: true }) } // 獲取當前的vm get vm (): any { return this._vm } // 獲取當前vm的messages屬性的內容,這裏使用了looseClone對js對象進行拷貝,詳細會在講解util.js中分析 get messages (): LocaleMessages { return looseClone(this._getMessages()) } // 獲取當前vm的dateTimeFormats屬性的內容 get dateTimeFormats (): DateTimeFormats { return looseClone(this._getDateTimeFormats()) } // 獲取當前vm的numberFormats屬性的內容 get numberFormats (): NumberFormats { return looseClone(this._getNumberFormats()) } // 獲取當前vm的locale屬性的內容 get locale (): Locale { return this._vm.locale } // 設置當前vm的locale屬性的內容 set locale (locale: Locale): void { this._vm.$set(this._vm, 'locale', locale) } // 獲取當前vm的fallbackLocale屬性的內容 get fallbackLocale (): Locale { return this._vm.fallbackLocale } // 設置當前vm的fallbackLocale屬性的內容 set fallbackLocale (locale: Locale): void { this._vm.$set(this._vm, 'fallbackLocale', locale) } // 同上 get missing (): ?MissingHandler { return this._missing } set missing (handler: MissingHandler): void { this._missing = handler } // 同上 get formatter (): Formatter { return this._formatter } set formatter (formatter: Formatter): void { this._formatter = formatter } // 同上 get silentTranslationWarn (): boolean { return this._silentTranslationWarn } set silentTranslationWarn (silent: boolean): void { this._silentTranslationWarn = silent } _getMessages (): LocaleMessages { return this._vm.messages } _getDateTimeFormats (): DateTimeFormats { return this._vm.dateTimeFormats } _getNumberFormats (): NumberFormats { return this._vm.numberFormats } // 提示方法 _warnDefault (locale: Locale, key: Path, result: ?any, vm: ?any): ?string { if (!isNull(result)) { return result } // 若是missing存在,則執行 if (this.missing) { this.missing.apply(null, [locale, key, vm]) } else { if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { warn( `Cannot translate the value of keypath '${key}'. ` + 'Use the value of keypath as default.' ) } } return key } // 判斷是否有回退方案 _isFallbackRoot (val: any): boolean { return !val && !isNull(this._root) && this._fallbackRoot } // 獲取message中某個key的值 _interpolate ( locale: Locale, message: LocaleMessageObject, key: Path, host: any, interpolateMode: string, values: any ): any { if (!message) { return null } // 利用getPathValue方法獲取message中key的值 const pathRet: PathValue = this._path.getPathValue(message, key) // 若是獲取的值是一個數組,則返回這個數組 if (Array.isArray(pathRet)) { return pathRet } let ret: mixed // 若是獲取的值是空 if (isNull(pathRet)) { /* istanbul ignore else */ if (isPlainObject(message)) { ret = message[key] if (typeof ret !== 'string') { if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { warn(`Value of key '${key}' is not a string!`) } return null } } else { return null } } else { /* istanbul ignore else */ if (typeof pathRet === 'string') { // 返回獲取的值 ret = pathRet } else { if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { warn(`Value of key '${key}' is not a string!`) } return null } } // Check for the existance of links within the translated string // 解析message中有@:的狀況 if (ret.indexOf('@:') >= 0) { ret = this._link(locale, message, ret, host, interpolateMode, values) } // 若是values爲false,則返回ret,不然返回this._render的執行結果 return !values ? ret : this._render(ret, interpolateMode, values) } // @:的狀況 _link ( locale: Locale, message: LocaleMessageObject, str: string, host: any, interpolateMode: string, values: any ): any { let ret: string = str // Match all the links within the local // We are going to replace each of // them with its translation // 匹配@:(link) const matches: any = ret.match(/(@:[\w\-_|.]+)/g) // 遍歷匹配的數組 for (const idx in matches) { // ie compatible: filter custom array // prototype method if (!matches.hasOwnProperty(idx)) { continue } // 獲取每一個link const link: string = matches[idx] // Remove the leading @: // 除去頭部的 @:,將獲得的linkPlaceholder做爲_interpolate方法的key繼續解析 const linkPlaceholder: string = link.substr(2) // Translate the link let translated: any = this._interpolate( locale, message, linkPlaceholder, host, interpolateMode === 'raw' ? 'string' : interpolateMode, interpolateMode === 'raw' ? undefined : values ) if (this._isFallbackRoot(translated)) { if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { warn(`Fall back to translate the link placeholder '${linkPlaceholder}' with root locale.`) } /* istanbul ignore if */ if (!this._root) { throw Error('unexpected error') } const root: any = this._root translated = root._translate( root._getMessages(), root.locale, root.fallbackLocale, linkPlaceholder, host, interpolateMode, values ) } // 獲取裝換的值 translated = this._warnDefault(locale, linkPlaceholder, translated, host) // Replace the link with the translated // 替換數據 ret = !translated ? ret : ret.replace(link, translated) } return ret } // 解析表達式,利用的是this._formatter.interpolate方法 _render (message: string, interpolateMode: string, values: any): any { const ret = this._formatter.interpolate(message, values) // if interpolateMode is **not** 'string' ('row'), // return the compiled data (e.g. ['foo', VNode, 'bar']) with formatter return interpolateMode === 'string' ? ret.join('') : ret } // 翻譯語言方法 _translate ( messages: LocaleMessages, locale: Locale, fallback: Locale, key: Path, host: any, interpolateMode: string, args: any ): any { // 經過_interpolate方法獲取結果 let res: any = this._interpolate(locale, messages[locale], key, host, interpolateMode, args) if (!isNull(res)) { return res } // 若是獲取的結果是null,則使用fallback語言包 res = this._interpolate(fallback, messages[fallback], key, host, interpolateMode, args) if (!isNull(res)) { if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { warn(`Fall back to translate the keypath '${key}' with '${fallback}' locale.`) } return res } else { return null } } _t (key: Path, _locale: Locale, messages: LocaleMessages, host: any, ...values: any): any { if (!key) { return '' } // 解析傳入的values參數 const parsedArgs = parseArgs(...values) // 獲取locale const locale: Locale = parsedArgs.locale || _locale // 調用_translate,設置模式爲string,並將parsedArgs.params傳入 const ret: any = this._translate( messages, locale, this.fallbackLocale, key, host, 'string', parsedArgs.params ) // 判斷是否有回退方案,這個適用於當子組件找不到對應的語言包字段的時候,往根節點查找是否有相應的字段 if (this._isFallbackRoot(ret)) { if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { warn(`Fall back to translate the keypath '${key}' with root locale.`) } /* istanbul ignore if */ if (!this._root) { throw Error('unexpected error') } // 調用根節點的t方法 return this._root.t(key, ...values) } else { return this._warnDefault(locale, key, ret, host) } } t (key: Path, ...values: any): TranslateResult { // 調用_t方法實現 return this._t(key, this.locale, this._getMessages(), null, ...values) } _i (key: Path, locale: Locale, messages: LocaleMessages, host: any, values: Object): any { // 設置interpolateMode爲raw const ret: any = this._translate(messages, locale, this.fallbackLocale, key, host, 'raw', values) if (this._isFallbackRoot(ret)) { if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { warn(`Fall back to interpolate the keypath '${key}' with root locale.`) } if (!this._root) { throw Error('unexpected error') } return this._root.i(key, locale, values) } else { return this._warnDefault(locale, key, ret, host) } } // 處理內置i18n組件邏輯 i (key: Path, locale: Locale, values: Object): TranslateResult { /* istanbul ignore if */ if (!key) { return '' } if (typeof locale !== 'string') { locale = this.locale } return this._i(key, locale, this._getMessages(), null, values) } _tc ( key: Path, _locale: Locale, messages: LocaleMessages, host: any, choice?: number, ...values: any ): any { if (!key) { return '' } if (choice === undefined) { choice = 1 } // 先調用this._t,在使用fetchChoice包裝,fetchChoice具體在util中會詳細分析 return fetchChoice(this._t(key, _locale, messages, host, ...values), choice) } tc (key: Path, choice?: number, ...values: any): TranslateResult { return this._tc(key, this.locale, this._getMessages(), null, choice, ...values) } _te (key: Path, locale: Locale, messages: LocaleMessages, ...args: any): boolean { const _locale: Locale = parseArgs(...args).locale || locale return this._exist(messages[_locale], key) } te (key: Path, locale?: Locale): boolean { return this._te(key, this.locale, this._getMessages(), locale) } // 獲取語言包 getLocaleMessage (locale: Locale): LocaleMessageObject { return looseClone(this._vm.messages[locale] || {}) } // 設置語言包,或者用於熱更新:i18n.setLocaleMessage('en', require('./en').default) setLocaleMessage (locale: Locale, message: LocaleMessageObject): void { this._vm.messages[locale] = message } mergeLocaleMessage (locale: Locale, message: LocaleMessageObject): void { this._vm.messages[locale] = Vue.util.extend(this._vm.messages[locale] || {}, message) } getDateTimeFormat (locale: Locale): DateTimeFormat { return looseClone(this._vm.dateTimeFormats[locale] || {}) } setDateTimeFormat (locale: Locale, format: DateTimeFormat): void { this._vm.dateTimeFormats[locale] = format } mergeDateTimeFormat (locale: Locale, format: DateTimeFormat): void { this._vm.dateTimeFormats[locale] = Vue.util.extend(this._vm.dateTimeFormats[locale] || {}, format) } // 本地格式化時間 _localizeDateTime ( value: number | Date, locale: Locale, fallback: Locale, dateTimeFormats: DateTimeFormats, key: string ): ?DateTimeFormatResult { // 獲取語言包 & 對應的日期格式對象 let _locale: Locale = locale let formats: DateTimeFormat = dateTimeFormats[_locale] // fallback locale // 判斷爲空 if (isNull(formats) || isNull(formats[key])) { if (process.env.NODE_ENV !== 'production') { warn(`Fall back to '${fallback}' datetime formats from '${locale} datetime formats.`) } _locale = fallback formats = dateTimeFormats[_locale] } // 判斷爲空 if (isNull(formats) || isNull(formats[key])) { return null } else { // 若是不爲空,本地獲取對應key下的format規則 const format: ?DateTimeFormatOptions = formats[key] // 生成相應的id const id = `${_locale}__${key}` // 獲取本地_dateTimeFormatters對象的緩存 let formatter = this._dateTimeFormatters[id] // 若是沒有的緩存規則,則實例化Intl.DateTimeFormat類,獲取對應的規則,並緩存在_dateTimeFormatters對象中 if (!formatter) { formatter = this._dateTimeFormatters[id] = new Intl.DateTimeFormat(_locale, format) } // 調用formatter的format方法,獲得格式化後的日期 return formatter.format(value) } } _d (value: number | Date, locale: Locale, key: ?string): DateTimeFormatResult { /* istanbul ignore if */ // 判斷是支持Intl.dateTimeFormat方法 if (process.env.NODE_ENV !== 'production' && !VueI18n.availabilities.dateTimeFormat) { warn('Cannot format a Date value due to not support Intl.DateTimeFormat.') return '' } // 若是key爲空,則直接實例化Intl.DateTimeFormat類,並調用api:format,獲得相應的日期格式 if (!key) { return new Intl.DateTimeFormat(locale).format(value) } // 若是key不爲空,則調用本地的格式化規則,_localizeDateTime方法 const ret: ?DateTimeFormatResult = this._localizeDateTime(value, locale, this.fallbackLocale, this._getDateTimeFormats(), key) if (this._isFallbackRoot(ret)) { if (process.env.NODE_ENV !== 'production') { warn(`Fall back to datetime localization of root: key '${key}' .`) } /* istanbul ignore if */ if (!this._root) { throw Error('unexpected error') } return this._root.d(value, key, locale) } else { return ret || '' } } // 日期格式化的入口 d (value: number | Date, ...args: any): DateTimeFormatResult { let locale: Locale = this.locale let key: ?string = null // 若是args的長度惟一,而且值爲字符串,則設置爲key if (args.length === 1) { if (typeof args[0] === 'string') { key = args[0] } else if (isObject(args[0])) { // 若是值爲對象,則解構這個對象 if (args[0].locale) { locale = args[0].locale } if (args[0].key) { key = args[0].key } } } else if (args.length === 2) { // 若是長度爲2,則設置key和locale if (typeof args[0] === 'string') { key = args[0] } if (typeof args[1] === 'string') { locale = args[1] } } // 調用_d方法 return this._d(value, locale, key) } getNumberFormat (locale: Locale): NumberFormat { return looseClone(this._vm.numberFormats[locale] || {}) } setNumberFormat (locale: Locale, format: NumberFormat): void { this._vm.numberFormats[locale] = format } mergeNumberFormat (locale: Locale, format: NumberFormat): void { this._vm.numberFormats[locale] = Vue.util.extend(this._vm.numberFormats[locale] || {}, format) } _localizeNumber ( value: number, locale: Locale, fallback: Locale, numberFormats: NumberFormats, key: string ): ?NumberFormatResult { let _locale: Locale = locale let formats: NumberFormat = numberFormats[_locale] // fallback locale // 判斷爲空 if (isNull(formats) || isNull(formats[key])) { if (process.env.NODE_ENV !== 'production') { warn(`Fall back to '${fallback}' number formats from '${locale} number formats.`) } _locale = fallback formats = numberFormats[_locale] } // 判斷爲空 if (isNull(formats) || isNull(formats[key])) { return null } else { // 獲取對應的key下的數字格式 const format: ?NumberFormatOptions = formats[key] // 生成id const id = `${_locale}__${key}` // 獲取相應Id下的緩存 let formatter = this._numberFormatters[id] // 若是沒有緩存,則實例化Intl.NumberFormat類,而且緩存在_numberFormatters對象中 if (!formatter) { formatter = this._numberFormatters[id] = new Intl.NumberFormat(_locale, format) } // 根據獲得的formatter,調用format方法獲得相應的數字 return formatter.format(value) } } _n (value: number, locale: Locale, key: ?string): NumberFormatResult { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !VueI18n.availabilities.numberFormat) { warn('Cannot format a Date value due to not support Intl.NumberFormat.') return '' } // 若是沒有key,則直接利用Intl.NumberFormat獲取相應的格式 if (!key) { return new Intl.NumberFormat(locale).format(value) } // 若是有key,則調用本地的格式化規則,_localizeNumber方法 const ret: ?NumberFormatResult = this._localizeNumber(value, locale, this.fallbackLocale, this._getNumberFormats(), key) if (this._isFallbackRoot(ret)) { if (process.env.NODE_ENV !== 'production') { warn(`Fall back to number localization of root: key '${key}' .`) } /* istanbul ignore if */ if (!this._root) { throw Error('unexpected error') } return this._root.n(value, key, locale) } else { return ret || '' } } // 數字格式化的入口 n (value: number, ...args: any): NumberFormatResult { let locale: Locale = this.locale let key: ?string = null // 解析參數,與上面的方法d相似 if (args.length === 1) { if (typeof args[0] === 'string') { key = args[0] } else if (isObject(args[0])) { if (args[0].locale) { locale = args[0].locale } if (args[0].key) { key = args[0].key } } } else if (args.length === 2) { if (typeof args[0] === 'string') { key = args[0] } if (typeof args[1] === 'string') { locale = args[1] } } // 調用_n return this._n(value, locale, key) } } VueI18n.availabilities = { dateTimeFormat: canUseDateTimeFormat, numberFormat: canUseNumberFormat } VueI18n.install = install VueI18n.version = '__VERSION__' /* istanbul ignore if */ if (typeof window !== 'undefined' && window.Vue) { window.Vue.use(VueI18n) }
其實index.js就已經包含了vue-i18n的主要內容了,看明白了這個文件就基本明白了vue-i18n的大致思路了數組
vue-i18n還有其餘優秀的地方,好比它提供了組件和指令,能夠供使用者更加方便和靈活的調用
接下來先看看component.js的代碼,看看i18n組件是怎麼實現的
/* @flow */ // 定義i18n組件 import { warn } from './util' export default { name: 'i18n', functional: true, props: { tag: { type: String, default: 'span' }, path: { type: String, required: true }, locale: { type: String }, places: { type: [Array, Object] } }, render (h: Function, { props, data, children, parent }: Object) { // 獲取父組件i18n實例 const i18n = parent.$i18n // 收集子組件 children = (children || []).filter(child => { return child.tag || (child.text = child.text.trim()) }) if (!i18n) { if (process.env.NODE_ENV !== 'production') { warn('Cannot find VueI18n instance!') } return children } // 獲取路徑,語言包,其餘參數 const path: Path = props.path const locale: ?Locale = props.locale const params: Object = {} const places: Array<any> | Object = props.places || {} // 判斷是否有places佔位符 const hasPlaces: boolean = Array.isArray(places) ? places.length > 0 : Object.keys(places).length > 0 const everyPlace: boolean = children.every(child => { if (child.data && child.data.attrs) { const place = child.data.attrs.place return (typeof place !== 'undefined') && place !== '' } }) if (hasPlaces && children.length > 0 && !everyPlace) { warn('If places prop is set, all child elements must have place prop set.') } // 提取組件自己的place if (Array.isArray(places)) { places.forEach((el, i) => { params[i] = el }) } else { Object.keys(places).forEach(key => { params[key] = places[key] }) } // 提取子組件的place children.forEach((child, i: number) => { const key: string = everyPlace ? `${child.data.attrs.place}` : `${i}` params[key] = child }) // 將參數做爲createElement方法的參數傳入 return h(props.tag, data, i18n.i(path, locale, params)) } }
i18組件不只僅提供了vue組件的基本功能,還提供了place佔位符,比較靈活
vue-i18n提供了v-t指令,主要就是實現了一個vue組件
/* @flow */ // 指令功能,用戶可經過指令v-t來進行多語言操做 import { warn, isPlainObject, looseEqual } from './util' // 定義了vue指令中的bind方法 export function bind (el: any, binding: Object, vnode: any): void { t(el, binding, vnode) } // 定義了vue指令中的update方法 export function update (el: any, binding: Object, vnode: any, oldVNode: any): void { if (looseEqual(binding.value, binding.oldValue)) { return } t(el, binding, vnode) } function t (el: any, binding: Object, vnode: any): void { // 解析參數 const value: any = binding.value // 獲取路徑,語言類型以及其餘參數 const { path, locale, args } = parseValue(value) if (!path && !locale && !args) { warn('not support value type') return } // 獲取當前的vm const vm: any = vnode.context if (!vm) { warn('not exist Vue instance in VNode context') return } if (!vm.$i18n) { warn('not exist VueI18n instance in Vue instance') return } if (!path) { warn('required `path` in v-t directive') return } // 最後調用vm實例上的t方法,而後進行賦值 el._vt = el.textContent = vm.$i18n.t(path, ...makeParams(locale, args)) } // 解析參數 function parseValue (value: any): Object { let path: ?string let locale: ?Locale let args: any // 參數只容許是字符串或是對象 if (typeof value === 'string') { path = value } else if (isPlainObject(value)) { path = value.path locale = value.locale args = value.args } return { path, locale, args } } // 對參數進行調整 function makeParams (locale: Locale, args: any): Array<any> { const params: Array<any> = [] locale && params.push(locale) if (args && (Array.isArray(args) || isPlainObject(args))) { params.push(args) } return params }
看完了組件和指令的實現,其餘的文件都是輔助函數,咱們一次來看看各個文件的實現
/* @flow */ // 主要是往Vue類的原型鏈擴展方法,調用的都是i18n的實例方法 export default function extend (Vue: any): void { Vue.prototype.$t = function (key: Path, ...values: any): TranslateResult { const i18n = this.$i18n return i18n._t(key, i18n.locale, i18n._getMessages(), this, ...values) } Vue.prototype.$tc = function (key: Path, choice?: number, ...values: any): TranslateResult { const i18n = this.$i18n return i18n._tc(key, i18n.locale, i18n._getMessages(), this, choice, ...values) } Vue.prototype.$te = function (key: Path, locale?: Locale): boolean { const i18n = this.$i18n return i18n._te(key, i18n.locale, i18n._getMessages(), locale) } Vue.prototype.$d = function (value: number | Date, ...args: any): DateTimeFormatResult { return this.$i18n.d(value, ...args) } Vue.prototype.$n = function (value: number, ...args: any): NumberFormatResult { return this.$i18n.n(value, ...args) } }
/* @flow */ import { isObject } from './util' /** * Path paerser * - Inspired: * Vue.js Path parser */ // actions // 定義了各類常量 const APPEND = 0 const PUSH = 1 const INC_SUB_PATH_DEPTH = 2 const PUSH_SUB_PATH = 3 // states const BEFORE_PATH = 0 const IN_PATH = 1 const BEFORE_IDENT = 2 const IN_IDENT = 3 const IN_SUB_PATH = 4 const IN_SINGLE_QUOTE = 5 const IN_DOUBLE_QUOTE = 6 const AFTER_PATH = 7 const ERROR = 8 const pathStateMachine: any = [] pathStateMachine[BEFORE_PATH] = { 'ws': [BEFORE_PATH], 'ident': [IN_IDENT, APPEND], '[': [IN_SUB_PATH], 'eof': [AFTER_PATH] } pathStateMachine[IN_PATH] = { 'ws': [IN_PATH], '.': [BEFORE_IDENT], '[': [IN_SUB_PATH], 'eof': [AFTER_PATH] } pathStateMachine[BEFORE_IDENT] = { 'ws': [BEFORE_IDENT], 'ident': [IN_IDENT, APPEND], '0': [IN_IDENT, APPEND], 'number': [IN_IDENT, APPEND] } pathStateMachine[IN_IDENT] = { 'ident': [IN_IDENT, APPEND], '0': [IN_IDENT, APPEND], 'number': [IN_IDENT, APPEND], 'ws': [IN_PATH, PUSH], '.': [BEFORE_IDENT, PUSH], '[': [IN_SUB_PATH, PUSH], 'eof': [AFTER_PATH, PUSH] } pathStateMachine[IN_SUB_PATH] = { "'": [IN_SINGLE_QUOTE, APPEND], '"': [IN_DOUBLE_QUOTE, APPEND], '[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH], ']': [IN_PATH, PUSH_SUB_PATH], 'eof': ERROR, 'else': [IN_SUB_PATH, APPEND] } pathStateMachine[IN_SINGLE_QUOTE] = { "'": [IN_SUB_PATH, APPEND], 'eof': ERROR, 'else': [IN_SINGLE_QUOTE, APPEND] } pathStateMachine[IN_DOUBLE_QUOTE] = { '"': [IN_SUB_PATH, APPEND], 'eof': ERROR, 'else': [IN_DOUBLE_QUOTE, APPEND] } /** * Check if an expression is a literal value. */ const literalValueRE: RegExp = /^\s?(true|false|-?[\d.]+|'[^']*'|"[^"]*")\s?$/ function isLiteral (exp: string): boolean { return literalValueRE.test(exp) } /** * Strip quotes from a string */ function stripQuotes (str: string): string | boolean { const a: number = str.charCodeAt(0) const b: number = str.charCodeAt(str.length - 1) return a === b && (a === 0x22 || a === 0x27) ? str.slice(1, -1) : str } /** * Determine the type of a character in a keypath. */ // 獲取path的類型 function getPathCharType (ch: ?string): string { if (ch === undefined || ch === null) { return 'eof' } // 獲取charCode,判斷各類類型,邏輯很簡單,不解釋 const code: number = ch.charCodeAt(0) switch (code) { case 0x5B: // [ case 0x5D: // ] case 0x2E: // . case 0x22: // " case 0x27: // ' case 0x30: // 0 return ch case 0x5F: // _ case 0x24: // $ case 0x2D: // - return 'ident' case 0x20: // Space case 0x09: // Tab case 0x0A: // Newline case 0x0D: // Return case 0xA0: // No-break space case 0xFEFF: // Byte Order Mark case 0x2028: // Line Separator case 0x2029: // Paragraph Separator return 'ws' } // a-z, A-Z if ((code >= 0x61 && code <= 0x7A) || (code >= 0x41 && code <= 0x5A)) { return 'ident' } // 1-9 if (code >= 0x31 && code <= 0x39) { return 'number' } return 'else' } /** * Format a subPath, return its plain form if it is * a literal string or number. Otherwise prepend the * dynamic indicator (*). */ // 格式化子路徑 function formatSubPath (path: string): boolean | string { const trimmed: string = path.trim() // invalid leading 0 if (path.charAt(0) === '0' && isNaN(path)) { return false } return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed } /** * Parse a string path into an array of segments */ // 路徑解析 function parse (path: Path): ?Array<string> { // 初始化變量 const keys: Array<string> = [] let index: number = -1 let mode: number = BEFORE_PATH let subPathDepth: number = 0 let c: ?string let key: any let newChar: any let type: string let transition: number let action: Function let typeMap: any const actions: Array<Function> = [] // 定義各類actions // 將key添加到keys數組中 actions[PUSH] = function () { if (key !== undefined) { keys.push(key) key = undefined } } // 設置key,若是key不爲空,則追加在key後面 actions[APPEND] = function () { if (key === undefined) { key = newChar } else { key += newChar } } actions[INC_SUB_PATH_DEPTH] = function () { actions[APPEND]() subPathDepth++ } actions[PUSH_SUB_PATH] = function () { if (subPathDepth > 0) { subPathDepth-- mode = IN_SUB_PATH actions[APPEND]() } else { subPathDepth = 0 key = formatSubPath(key) if (key === false) { return false } else { actions[PUSH]() } } } // 判斷是否爲"" 或者 爲 '' function maybeUnescapeQuote (): ?boolean { const nextChar: string = path[index + 1] if ((mode === IN_SINGLE_QUOTE && nextChar === "'") || (mode === IN_DOUBLE_QUOTE && nextChar === '"')) { index++ newChar = '\\' + nextChar actions[APPEND]() return true } } // 循環遍歷路徑,而後進行拆分 while (mode !== null) { index++ c = path[index] // 判斷是否爲\\ 而且判斷是否爲雙引號或單引號,若是是,就進入下個循環 if (c === '\\' && maybeUnescapeQuote()) { continue } // 獲取當前字符的類型 type = getPathCharType(c) // 獲取mode類型的映射表 typeMap = pathStateMachine[mode] // 獲得相應的transition transition = typeMap[type] || typeMap['else'] || ERROR if (transition === ERROR) { return // parse error } // 從新設置mode mode = transition[0] // 獲得相應的action action = actions[transition[1]] if (action) { newChar = transition[2] newChar = newChar === undefined ? c : newChar if (action() === false) { return } } if (mode === AFTER_PATH) { return keys } } } export type PathValue = PathValueObject | PathValueArray | string | number | boolean | null export type PathValueObject = { [key: string]: PathValue } export type PathValueArray = Array<PathValue> function empty (target: any): boolean { /* istanbul ignore else */ if (Array.isArray(target)) { return target.length === 0 } else { return false } } export default class I18nPath { _cache: Object constructor () { this._cache = Object.create(null) } /** * External parse that check for a cache hit first */ // 經過parse解析路徑,並緩存在_cache對象上 parsePath (path: Path): Array<string> { let hit: ?Array<string> = this._cache[path] if (!hit) { hit = parse(path) if (hit) { this._cache[path] = hit } } return hit || [] } /** * Get path value from path string */ getPathValue (obj: mixed, path: Path): PathValue { if (!isObject(obj)) { return null } // 獲得path路徑解析後的數組paths const paths: Array<string> = this.parsePath(path) if (empty(paths)) { return null } else { const length: number = paths.length let ret: any = null let last: any = obj let i: number = 0 // 遍歷查找obj中key對應的值 while (i < length) { const value: any = last[paths[i]] if (value === undefined) { last = null break } last = value i++ } ret = last return ret } } }
/* @flow */ import { warn, isObject } from './util' // 對應key的值進行基礎格式化 export default class BaseFormatter { _caches: { [key: string]: Array<Token> } constructor () { // 初始化一個緩存對象 this._caches = Object.create(null) } interpolate (message: string, values: any): Array<any> { // 先查看緩存中是否有token let tokens: Array<Token> = this._caches[message] // 若是沒有,則進一步解析 if (!tokens) { tokens = parse(message) this._caches[message] = tokens } // 獲得tokens以後進行編譯 return compile(tokens, values) } } type Token = { type: 'text' | 'named' | 'list' | 'unknown', value: string } const RE_TOKEN_LIST_VALUE: RegExp = /^(\d)+/ const RE_TOKEN_NAMED_VALUE: RegExp = /^(\w)+/ // 分析相應的token 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 !== '}') { sub += char char = format[position++] } const type = RE_TOKEN_LIST_VALUE.test(sub) ? 'list' : RE_TOKEN_NAMED_VALUE.test(sub) ? 'named' : 'unknown' tokens.push({ value: sub, type }) } else if (char === '%') { // when found rails i18n syntax, skip text capture if (format[(position)] !== '{') { text += char } } else { text += char } } // 最後生成對應的tokens text && tokens.push({ type: 'text', value: text }) return tokens } // 編譯函數 export function compile (tokens: Array<Token>, values: Object | Array<any>): Array<any> { const compiled: Array<any> = [] let index: number = 0 // 獲取mode const mode: string = Array.isArray(values) ? 'list' : isObject(values) ? 'named' : 'unknown' if (mode === 'unknown') { return compiled } // 根據token的各類類型進行編譯 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': // 若是是named,則將對應的value值push到complied數組中 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 }
/* @flow */ import VueI18n from './index' import { isPlainObject, warn, merge } from './util' export default { beforeCreate (): void { const options: any = this.$options options.i18n = options.i18n || (options.__i18n ? {} : null) if (options.i18n) { if (options.i18n instanceof VueI18n) { // init locale messages via custom blocks if (options.__i18n) { try { let localeMessages = {} options.__i18n.forEach(resource => { localeMessages = merge(localeMessages, JSON.parse(resource)) }) Object.keys(localeMessages).forEach((locale: Locale) => { options.i18n.mergeLocaleMessage(locale, localeMessages[locale]) }) } catch (e) { if (process.env.NODE_ENV !== 'production') { warn(`Cannot parse locale messages via custom blocks.`, e) } } } this._i18n = options.i18n this._i18nWatcher = this._i18n.watchI18nData() this._i18n.subscribeDataChanging(this) this._subscribing = true } else if (isPlainObject(options.i18n)) { // component local i18n if (this.$root && this.$root.$i18n && this.$root.$i18n instanceof VueI18n) { options.i18n.root = this.$root.$i18n options.i18n.fallbackLocale = this.$root.$i18n.fallbackLocale options.i18n.silentTranslationWarn = this.$root.$i18n.silentTranslationWarn } // init locale messages via custom blocks if (options.__i18n) { try { let localeMessages = {} options.__i18n.forEach(resource => { localeMessages = merge(localeMessages, JSON.parse(resource)) }) options.i18n.messages = localeMessages } catch (e) { if (process.env.NODE_ENV !== 'production') { warn(`Cannot parse locale messages via custom blocks.`, e) } } } this._i18n = new VueI18n(options.i18n) this._i18nWatcher = this._i18n.watchI18nData() this._i18n.subscribeDataChanging(this) this._subscribing = true if (options.i18n.sync === undefined || !!options.i18n.sync) { this._localeWatcher = this.$i18n.watchLocale() } } else { if (process.env.NODE_ENV !== 'production') { warn(`Cannot be interpreted 'i18n' option.`) } } } else if (this.$root && this.$root.$i18n && this.$root.$i18n instanceof VueI18n) { // root i18n this._i18n = this.$root.$i18n this._i18n.subscribeDataChanging(this) this._subscribing = true } else if (options.parent && options.parent.$i18n && options.parent.$i18n instanceof VueI18n) { // parent i18n this._i18n = options.parent.$i18n this._i18n.subscribeDataChanging(this) this._subscribing = true } }, beforeDestroy (): void { if (!this._i18n) { return } if (this._subscribing) { this._i18n.unsubscribeDataChanging(this) delete this._subscribing } if (this._i18nWatcher) { this._i18nWatcher() delete this._i18nWatcher } if (this._localeWatcher) { this._localeWatcher() delete this._localeWatcher } this._i18n = null } }
/* @flow */ /** * utilites */ export function warn (msg: string, err: ?Error): void { if (typeof console !== 'undefined') { console.warn('[vue-i18n] ' + msg) /* istanbul ignore if */ if (err) { console.warn(err.stack) } } } export function isObject (obj: mixed): boolean %checks { return obj !== null && typeof obj === 'object' } const toString: Function = Object.prototype.toString const OBJECT_STRING: string = '[object Object]' // 判斷是否爲一個對象 export function isPlainObject (obj: any): boolean { return toString.call(obj) === OBJECT_STRING } // 判斷是否爲空 export function isNull (val: mixed): boolean { return val === null || val === undefined } // 解析參數 export function parseArgs (...args: Array<mixed>): Object { let locale: ?string = null let params: mixed = null // 分析參數的長度,若是長度爲1 if (args.length === 1) { // 若是是對象或是數組,則將該參數設置爲params if (isObject(args[0]) || Array.isArray(args[0])) { params = args[0] } else if (typeof args[0] === 'string') { // 若是是字符串,則設置爲locale,當作是語言類型 locale = args[0] } } else if (args.length === 2) { // 長度爲2時,根據狀況設置locale和params if (typeof args[0] === 'string') { locale = args[0] } /* istanbul ignore if */ if (isObject(args[1]) || Array.isArray(args[1])) { params = args[1] } } // 最後返回{ locale, params }對象 return { locale, params } } // 若是索引值大於1,則返回1 function getOldChoiceIndexFixed (choice: number): number { return choice ? choice > 1 ? 1 : 0 : 1 } // 獲取索引的方法 function getChoiceIndex (choice: number, choicesLength: number): number { choice = Math.abs(choice) // 若是長度等於2,則調用getOldChoiceIndexFixed if (choicesLength === 2) { return getOldChoiceIndexFixed(choice) } // 確保索引值不大於2,這個很使人費解啊 return choice ? Math.min(choice, 2) : 0 } export function fetchChoice (message: string, choice: number): ?string { /* istanbul ignore if */ if (!message && typeof message !== 'string') { return null } // 將字符串分割爲數組 const choices: Array<string> = message.split('|') // 獲取索引 choice = getChoiceIndex(choice, choices.length) // 獲得數組中特定索引的值 if (!choices[choice]) { return message } // 去掉空格 return choices[choice].trim() } // 利用JSON的api實現對象的深拷貝 export function looseClone (obj: Object): Object { return JSON.parse(JSON.stringify(obj)) } // 刪除數組中的某一項 export function remove (arr: Array<any>, item: any): Array<any> | void { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } } const hasOwnProperty = Object.prototype.hasOwnProperty export function hasOwn (obj: Object | Array<*>, key: string): boolean { return hasOwnProperty.call(obj, key) } // 遞歸合併對象 export function merge (target: Object): Object { const output = Object(target) for (let i = 1; i < arguments.length; i++) { const source = arguments[i] if (source !== undefined && source !== null) { let key for (key in source) { if (hasOwn(source, key)) { if (isObject(source[key])) { output[key] = merge(output[key], source[key]) } else { output[key] = source[key] } } } } } return output } // 鬆散判斷是否相等 export function looseEqual (a: any, b: any): boolean { // 先判斷是否全等 if (a === b) { return true } const isObjectA: boolean = isObject(a) const isObjectB: boolean = isObject(b) // 若是二者都是對象 if (isObjectA && isObjectB) { try { const isArrayA: boolean = Array.isArray(a) const isArrayB: boolean = Array.isArray(b) // 若是二者都是數組 if (isArrayA && isArrayB) { // 若是長度相等,則遞歸對比數組中的每一項 return a.length === b.length && a.every((e: any, i: number): boolean => { // 遞歸調用looseEqual return looseEqual(e, b[i]) }) } else if (!isArrayA && !isArrayB) { // 若是不是數組,則當作對象來對比 const keysA: Array<string> = Object.keys(a) const keysB: Array<string> = Object.keys(b) // 若是key的數量相等,則遞歸對比每一個key對應的值 return keysA.length === keysB.length && keysA.every((key: string): boolean => { // 遞歸調用looseEqual return looseEqual(a[key], b[key]) }) } else { /* istanbul ignore next */ return false } } catch (e) { /* istanbul ignore next */ return false } } else if (!isObjectA && !isObjectB) { // 若是不是對象,則強制轉爲字符串進行對比 return String(a) === String(b) } else { return false } } // 判斷是否支持Intl.DateTimeFormat方法 export const canUseDateTimeFormat: boolean = typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat !== 'undefined' // 判斷是否支持Intl.NumberFormat方法 export const canUseNumberFormat: boolean = typeof Intl !== 'undefined' && typeof Intl.NumberFormat !== 'undefined'
/* @flow */ /** * utilites */ export function warn (msg: string, err: ?Error): void { if (typeof console !== 'undefined') { console.warn('[vue-i18n] ' + msg) /* istanbul ignore if */ if (err) { console.warn(err.stack) } } } export function isObject (obj: mixed): boolean %checks { return obj !== null && typeof obj === 'object' } const toString: Function = Object.prototype.toString const OBJECT_STRING: string = '[object Object]' // 判斷是否爲一個對象 export function isPlainObject (obj: any): boolean { return toString.call(obj) === OBJECT_STRING } // 判斷是否爲空 export function isNull (val: mixed): boolean { return val === null || val === undefined } // 解析參數 export function parseArgs (...args: Array<mixed>): Object { let locale: ?string = null let params: mixed = null // 分析參數的長度,若是長度爲1 if (args.length === 1) { // 若是是對象或是數組,則將該參數設置爲params if (isObject(args[0]) || Array.isArray(args[0])) { params = args[0] } else if (typeof args[0] === 'string') { // 若是是字符串,則設置爲locale,當作是語言類型 locale = args[0] } } else if (args.length === 2) { // 長度爲2時,根據狀況設置locale和params if (typeof args[0] === 'string') { locale = args[0] } /* istanbul ignore if */ if (isObject(args[1]) || Array.isArray(args[1])) { params = args[1] } } // 最後返回{ locale, params }對象 return { locale, params } } // 若是索引值大於1,則返回1 function getOldChoiceIndexFixed (choice: number): number { return choice ? choice > 1 ? 1 : 0 : 1 } // 獲取索引的方法 function getChoiceIndex (choice: number, choicesLength: number): number { choice = Math.abs(choice) // 若是長度等於2,則調用getOldChoiceIndexFixed if (choicesLength === 2) { return getOldChoiceIndexFixed(choice) } // 確保索引值不大於2,這個很使人費解啊 return choice ? Math.min(choice, 2) : 0 } export function fetchChoice (message: string, choice: number): ?string { /* istanbul ignore if */ if (!message && typeof message !== 'string') { return null } // 將字符串分割爲數組 const choices: Array<string> = message.split('|') // 獲取索引 choice = getChoiceIndex(choice, choices.length) // 獲得數組中特定索引的值 if (!choices[choice]) { return message } // 去掉空格 return choices[choice].trim() } // 利用JSON的api實現對象的深拷貝 export function looseClone (obj: Object): Object { return JSON.parse(JSON.stringify(obj)) } // 刪除數組中的某一項 export function remove (arr: Array<any>, item: any): Array<any> | void { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } } const hasOwnProperty = Object.prototype.hasOwnProperty export function hasOwn (obj: Object | Array<*>, key: string): boolean { return hasOwnProperty.call(obj, key) } // 遞歸合併對象 export function merge (target: Object): Object { const output = Object(target) for (let i = 1; i < arguments.length; i++) { const source = arguments[i] if (source !== undefined && source !== null) { let key for (key in source) { if (hasOwn(source, key)) { if (isObject(source[key])) { output[key] = merge(output[key], source[key]) } else { output[key] = source[key] } } } } } return output } // 鬆散判斷是否相等 export function looseEqual (a: any, b: any): boolean { // 先判斷是否全等 if (a === b) { return true } const isObjectA: boolean = isObject(a) const isObjectB: boolean = isObject(b) // 若是二者都是對象 if (isObjectA && isObjectB) { try { const isArrayA: boolean = Array.isArray(a) const isArrayB: boolean = Array.isArray(b) // 若是二者都是數組 if (isArrayA && isArrayB) { // 若是長度相等,則遞歸對比數組中的每一項 return a.length === b.length && a.every((e: any, i: number): boolean => { // 遞歸調用looseEqual return looseEqual(e, b[i]) }) } else if (!isArrayA && !isArrayB) { // 若是不是數組,則當作對象來對比 const keysA: Array<string> = Object.keys(a) const keysB: Array<string> = Object.keys(b) // 若是key的數量相等,則遞歸對比每一個key對應的值 return keysA.length === keysB.length && keysA.every((key: string): boolean => { // 遞歸調用looseEqual return looseEqual(a[key], b[key]) }) } else { /* istanbul ignore next */ return false } } catch (e) { /* istanbul ignore next */ return false } } else if (!isObjectA && !isObjectB) { // 若是不是對象,則強制轉爲字符串進行對比 return String(a) === String(b) } else { return false } } // 判斷是否支持Intl.DateTimeFormat方法 export const canUseDateTimeFormat: boolean = typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat !== 'undefined' // 判斷是否支持Intl.NumberFormat方法 export const canUseNumberFormat: boolean = typeof Intl !== 'undefined' && typeof Intl.NumberFormat !== 'undefined'
ok~今天就寫到這,但願對你們有所幫助,也歡迎拍磚