资讯专栏INFORMATION COLUMN

前端国际化之Vue-i18n源码分析

不知名网友 / 1988人阅读

摘要:最近的工作当中有个任务是做国际化。下文有对的源码进行分析。因为英文的阅读方向也是从左到右,因此语言展示的方向不予考虑。服务端数据翻译前端样式的调整中文转英文后部分文案过长图片第三方插件地图等中文转英文后肯定会遇到文案过长的情况。

最近的工作当中有个任务是做国际化。这篇文章也是做个简单的总结。

部分网站的当前解决的方案

不同语言对应不同的页面。在本地开发的时候就分别打包输出了不同语言版本的静态及模板文件,通过页面及资源的url进行区分,需要维护多份代码。

在线翻译

统一模板文件,前端根据相应的语言映射表去做文案的替换。

面对的问题

语言标识谁来做?

页面完全由服务端直出(所有的数据均由服务端来处理)

服务端根据IP去下发语言标识字段(前端根据下发的表示字段切换语言环境)

前端去根据useragent.lang浏览器环境语言进行设定
当前项目中入口页面由服务端来渲染,其他的页面由前端来接管路由。在入口页面由服务器下发lang字段去做语言标识,在页面渲染出来前,前端来决定使用的语言包。语言包是在本地编译的过程中就将语言包编译进了代码当中,没有采用异步加载的方式。

前端静态资源翻译

单/复数

中文转英文

语言展示的方向
前端静态资源文案的翻译使用vue-i18n这个插件来进行。插件提供了单复数,中文转英文的方法。a下文有对vue-i18n的源码进行分析。因为英文的阅读方向也是从左到右,因此语言展示的方向不予考虑。但是在一些阿拉伯地区国家的语言是从右到左进行阅读的。

服务端数据翻译

前端样式的调整

中文转英文后部分文案过长

图片

第三方插件(地图,SDK等)

a.中文转英文后肯定会遇到文案过长的情况。那么可能需要精简翻译,使文案保持在一定的可接受的长度范围内。但是大部分的情况都是文案在保持原意的情况下无法再进行精简。这时必须要前端来进行样式上的调整,那么可能还需要设计的同学参与进来,对一些文案过多出现折行的情况再多带带做样式的定义。在细调样式这块,主要还是通过不同的语言标识去控制不同标签的class,来多带带定义样式。

此外,还有部分图片也是需要做调整,在C端中,大部分由产品方去输出内容,那么图片这块的话,还需要设计同学多带带出图。

在第三方插件中这个环节当中,因为使用了腾讯地图插件,由于腾讯地图并未推出国内地图的英文版,所以整个页面的地图部分暂时无法做到国际化。由此联想到,在你的应用当中使用的其他一些第三方插件或者SDK,在国际化的过程中需要去解决哪些问题。

跨地区xxxx

货币及支付方式

时间的格式

在一些支付场景下,货币符号单位价格的转化等。不同国家地区在时间的格式显示上有差异。

项目的长期维护

翻译工作

map表的维护

当前翻译的工作流程是拆页面,每拆一个页面,FE同学整理好可能会出现的中文文案,再交由翻译的同学去完成翻译的工作。负责不同页面的同学维护着不同的map表,在当前的整体页面架构中,不同功能模块和页面被拆分出去交由不同的同学去做,那么通过跳页面的方式去暂时缓解map表的维护问题。如果哪一天页面需要收敛,这也是一个需要去考虑的问题。如果从整个项目的一开始就考虑到国际化的问题并采取相关的措施都能减轻后期的工作量及维护成本。同时以后一旦map表内容过多,是否需要考虑需要将map表进行异步加载。

Vue-i18n的基本使用
    // 入口main.js文件
    import VueI18n from "vue-i18n"
    
    Vue.use(VueI18n)            // 通过插件的形式挂载
    
    const i18n = new VueI18n({
        locale: CONFIG.lang,    // 语言标识
        messages: {
            "zh-CN": require("./common/lang/zh"),   // 中文语言包
            "en-US": require("./common/lang/en")    // 英文语言包
        }
    })
    
    const app = new Vue({
        i18n,
        ...App
    }).$mout("#root")
    
    // 单vue文件
    

Vue-i18n是以插件的形式配合Vue进行工作的。通过全局的mixin的方式将插件提供的方法挂载到Vue的实例上。

具体的源码分析

其中install.jsVue的挂载函数,主要是为了将mixin.js里面的提供的方法挂载到Vue实例当中:

import { warn } from "./util"
import mixin from "./mixin"
import Asset from "./asset"

export let Vue

// 注入root Vue
export function install (_Vue) { 
  Vue = _Vue

  const version = (Vue.version && Number(Vue.version.split(".")[0])) || -1
  if (process.env.NODE_ENV !== "production" && install.installed) {
    warn("already installed.")
    return
  }
  install.installed = true

  if (process.env.NODE_ENV !== "production" && version < 2) {
    warn(`vue-i18n (${install.version}) need to use Vue 2.0 or later (Vue: ${Vue.version}).`)
    return
  }

  // 通过mixin的方式,将插件提供的methods,钩子函数等注入到全局,之后每次创建的vue实例都用拥有这些methods或者钩子函数
  Vue.mixin(mixin)

  Asset(Vue)
}

接下来就看下在Vue上混合了哪些methods或者钩子函数. 在mixin.js文件中:

/* @flow */

// VueI18n构造函数
import VueI18n from "./index"
import { isPlainObject, warn } from "./util"


// $i18n 是每创建一个Vue实例都会产生的实例对象
// 调用以下方法前都会判断实例上是否挂载了$i18n这个属性
// 最后实际调用的方法是插件内部定义的方法
export default {
  // 这里混合了computed计算属性, 注意这里计算属性返回的都是函数,这样就可以在vue模板里面使用{{ $t("hello") }}, 或者其他方法当中使用 this.$t("hello")。这种函数接收参数的方式
  computed: {
    // 翻译函数, 调用的是VueI18n实例上提供的方法
    $t () {
      if (!this.$i18n) {
        throw Error(`Failed in $t due to not find VueI18n instance`)
      }
      // add dependency tracking !!
      const locale: string = this.$i18n.locale          // 语言配置
      const messages: Messages = this.$i18n.messages    // 语言包
      // 返回一个函数. 接受一个key值. 即在map文件中定义的key值, 在模板中进行使用 {{ $t("你好") }}
      // ...args是传入的参数, 例如在模板中定义的一些替换符, 具体的支持的形式可翻阅文档https://kazupon.github.io/vue-i18n/formatting.html
      return (key: string, ...args: any): string => {
        return this.$i18n._t(key, locale, messages, this, ...args)
      }
    },

    // tc方法可以多带带定义组件内部语言设置选项, 如果没有定义组件内部语言,则还是使用global的配置
    $tc () {
      if (!this.$i18n) {
        throw Error(`Failed in $tc due to not find VueI18n instance`)
      }
      // add dependency tracking !!
      const locale: string = this.$i18n.locale
      const messages: Messages = this.$i18n.messages
      return (key: string, choice?: number, ...args: any): string => {
        return this.$i18n._tc(key, locale, messages, this, choice, ...args)
      }
    },

    // te方法
    $te () {
      if (!this.$i18n) {
        throw Error(`Failed in $te due to not find VueI18n instance`)
      }
      // add dependency tracking !!
      const locale: string = this.$i18n.locale
      const messages: Messages = this.$i18n.messages
      return (key: string, ...args: any): boolean => {
        return this.$i18n._te(key, locale, messages, ...args)
      }
    }
  },

  // 钩子函数
  // 被渲染前,在vue实例上添加$i18n属性
  // 在根组件初始化的过程中:
  /**
   * new Vue({
   *   i18n   // 这里是提供了自定义的属性 那么实例当中可以通过this.$option.i18n去访问这个属性
   *   // xxxx
   * })
   */
  beforeCreate () {
    const options: any = this.$options
    // 如果有i18n这个属性. 根实例化的时候传入了这个参数
    if (options.i18n) {
      if (options.i18n instanceof VueI18n) {
        // 如果是VueI18n的实例,那么挂载在Vue实例的$i18n属性上
        this.$i18n = options.i18n
        // 如果是个object
      } else if (isPlainObject(options.i18n)) {     // 如果是一个pobj
        // component local i18n
        // 访问root vue实例。
        if (this.$root && this.$root.$i18n && this.$root.$i18n instanceof VueI18n) {
          options.i18n.root = this.$root.$i18n
        }
        this.$i18n = new VueI18n(options.i18n)  // 创建属于component的local i18n
        if (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
      // 如果子Vue实例没有传入$i18n方法,且root挂载了$i18n,那么子实例也会使用root i18n
      this.$i18n = this.$root.$i18n
    }
  },

  // 实例被销毁的回调函数
  destroyed () {
    if (this._localeWatcher) {
      this.$i18n.unwatchLocale()
      delete this._localeWatcher
    }

    // 组件销毁后,同时也销毁实例上的$i18n方法
    this.$i18n = null
  }
}

这里注意下这几个方法的区别:

$tc这个方法可以用以返回翻译的复数字符串, 及一个key可以对应的翻译文本,通过|进行连接:

例如:

    // main.js
    new VueI18n({
        messages: {
            car: "car | cars"
        }
    })
    
    // template
    {{$tc("car", 1)}}   ===>>>  car
    {{$tc("car", 2)}}   ===>>>  cars

$te这个方法用以判断需要翻译的key在你提供的语言包(messages)中是否存在.

接下来就看看VueI18n构造函数及原型上提供了哪些可以被实例继承的属性或者方法

/* @flow */

import { install, Vue } from "./install"
import { warn, isNull, parseArgs, fetchChoice } from "./util"
import BaseFormatter from "./format"    // 转化函数 封装了format, 里面包含了template模板替换的方法
import getPathValue from "./path"

import type { PathValue } from "./path"

// VueI18n构造函数
export default class VueI18n {
  static install: () => void
  static version: string

  _vm: any
  _formatter: Formatter
  _root: ?I18n
  _sync: ?boolean
  _fallbackRoot: boolean
  _fallbackLocale: string
  _missing: ?MissingHandler
  _exist: Function
  _watcher: any

  // 实例化参数配置
  constructor (options: I18nOptions = {}) {
    const locale: string = options.locale || "en-US"    // vue-i18n初始化的时候语言参数配置
    const messages: Messages = options.messages || {}   // 本地配置的所有语言环境都是挂载到了messages这个属性上
    this._vm = null                 // ViewModel
    this._fallbackLocale = options.fallbackLocale || "en-US"  // 缺省语言配置
    this._formatter = options.formatter || new BaseFormatter()  // 翻译函数
    this._missing = options.missing
    this._root = options.root || null
    this._sync = options.sync || false   
    this._fallbackRoot = options.fallbackRoot || false

    this._exist = (message: Object, key: string): boolean => {
      if (!message || !key) { return false }
      return !isNull(getPathValue(message, key))
    }

    this._resetVM({ locale, messages })
  }

  // VM 
  // 重置viewModel
  _resetVM (data: { locale: string, messages: Messages }): void {
    const silent = Vue.config.silent
    Vue.config.silent = true
    this._vm = new Vue({ data })
    Vue.config.silent = silent
  }

  // 根实例的vm监听locale这个属性
  watchLocale (): any {
    if (!this._sync || !this._root) { return null }
    const target: any = this._vm
    // vm.$watch返回的是一个取消观察的函数,用来停止触发回调
    this._watcher = this._root.vm.$watch("locale", (val) => {
      target.$set(target, "locale", val)
    }, { immediate: true })
    return this._watcher
  }

  // 停止触发vm.$watch观察函数
  unwatchLocale (): boolean {
    if (!this._sync || !this._watcher) { return false }
    if (this._watcher) {
      this._watcher()
      delete this._watcher
    }
    return true
  }

  get vm (): any { return this._vm }

  get messages (): Messages { return this._vm.$data.messages }                  // get 获取messages参数
  set messages (messages: Messages): void { this._vm.$set(this._vm, "messages", messages) }  // set 设置messages参数

  get locale (): string { return this._vm.$data.locale }                        // get 获取语言配置参数
  set locale (locale: string): void { this._vm.$set(this._vm, "locale", locale) }     // set 重置语言配置参数

  get fallbackLocale (): string { return this._fallbackLocale }                 //  fallbackLocale 是什么?
  set fallbackLocale (locale: string): void { this._fallbackLocale = locale }

  get missing (): ?MissingHandler { return this._missing }
  set missing (handler: MissingHandler): void { this._missing = handler }

  get formatter (): Formatter { return this._formatter }                          // get 转换函数
  set formatter (formatter: Formatter): void { this._formatter = formatter }      // set 转换函数

  _warnDefault (locale: string, key: string, result: ?any, vm: ?any): ?string {
    if (!isNull(result)) { return result }
    if (this.missing) {
      this.missing.apply(null, [locale, key, vm])
    } else {
      if (process.env.NODE_ENV !== "production") {
        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
  }

  // 插入函数
  _interpolate (message: Messages, key: string, args: any): any {
    if (!message) { return null }

    // 获取key对应的字符串
    let val: PathValue = getPathValue(message, key)
    if (Array.isArray(val)) { return val }
    if (isNull(val)) { val = message[key] }
    if (isNull(val)) { return null }
    if (typeof val !== "string") {
      warn(`Value of key "${key}" is not a string!`)
      return null
    }


    // TODO ?? 这里的links是干什么的?
    // Check for the existance of links within the translated string
    if (val.indexOf("@:") >= 0) {
      // Match all the links within the local
      // We are going to replace each of
      // them with its translation
      const matches: any = val.match(/(@:[w|.]+)/g)
      for (const idx in matches) {
        const link = matches[idx]
        // Remove the leading @:
        const linkPlaceholder = link.substr(2)
        // Translate the link
        const translatedstring = this._interpolate(message, linkPlaceholder, args)
        // Replace the link with the translated string
        val = val.replace(link, translatedstring)
      }
    }

    // 如果没有传入需要替换的obj, 那么直接返回字符串, 否则调用this._format进行变量等的替换
    return !args ? val : this._format(val, args)    // 获取替换后的字符
  }

  _format (val: any, ...args: any): any {
    return this._formatter.format(val, ...args)
  }

  // 翻译函数
  _translate (messages: Messages, locale: string, fallback: string, key: string, args: any): any {
    let res: any = null
    /**
     * messages[locale] 使用哪个语言包
     * key 语言映射表的key
     * args 映射替换关系
     */
    res = this._interpolate(messages[locale], key, args)
    if (!isNull(res)) { return res }

    res = this._interpolate(messages[fallback], key, args)
    if (!isNull(res)) {
      if (process.env.NODE_ENV !== "production") {
        warn(`Fall back to translate the keypath "${key}" with "${fallback}" locale.`)
      }
      return res
    } else {
      return null
    }
  }

  // 翻译的核心函数
  /**
   * 这里的方法传入的参数参照mixin.js里面的定义的方法
   * key map的key值 (为接受的外部参数)
   * _locale 语言配置选项: "zh-CN" | "en-US" (内部变量)
   * messages 映射表 (内部变量)
   * host为这个i18n的实例 (内部变量)
   *
   */
  _t (key: string, _locale: string, messages: Messages, host: any, ...args: any): any {
    if (!key) { return "" }
    
    // parseArgs函数用以返回传入的局部语言配置, 及映射表
    const parsedArgs = parseArgs(...args)   // 接收的参数{ locale, params(映射表) }
    const locale = parsedArgs.locale || _locale   // 语言配置
    
    // 字符串替换
    /**
     * @params messages  语言包
     * @params locale  语言配置
     * @params fallbackLocale 缺省语言配置
     * @params key 替换的key值
     * @params parsedArgs.params 需要被替换的参数map表
     */
    const ret: any = this._translate(messages, locale, this.fallbackLocale, key, parsedArgs.params)
    if (this._isFallbackRoot(ret)) {
      if (process.env.NODE_ENV !== "production") {
        warn(`Fall back to translate the keypath "${key}" with root locale.`)
      }
      if (!this._root) { throw Error("unexpected error") }
      return this._root.t(key, ...args)
    } else {
      return this._warnDefault(locale, key, ret, host)
    }
  }

  // 转化函数
  t (key: string, ...args: any): string {
    return this._t(key, this.locale, this.messages, null, ...args)
  }

  _tc (key: string, _locale: string, messages: Messages, host: any, choice?: number, ...args: any): any {
    if (!key) { return "" }
    if (choice !== undefined) {
      return fetchChoice(this._t(key, _locale, messages, host, ...args), choice)
    } else {
      return this._t(key, _locale, messages, host, ...args)
    }
  }

  tc (key: string, choice?: number, ...args: any): any {
    return this._tc(key, this.locale, this.messages, null, choice, ...args)
  }

  _te (key: string, _locale: string, messages: Messages, ...args: any): boolean {
    const locale = parseArgs(...args).locale || _locale
    return this._exist(messages[locale], key)
  }

  te (key: string, ...args: any): boolean {
    return this._te(key, this.locale, this.messages, ...args)
  }
}

VueI18n.install = install
VueI18n.version = "__VERSION__"

// 如果是通过CDN或者外链的形式引入的Vue
if (typeof window !== "undefined" && window.Vue) {
  window.Vue.use(VueI18n)
}

另外还有一个比较重要的库函数format.js

/**
 *  String format template
 *  - Inspired:
 *    https://github.com/Matt-Esch/string-template/index.js
 */

// 变量的替换, 在字符串模板中写的站位符 {xxx} 进行替换
const RE_NARGS: RegExp = /(%|){([0-9a-zA-Z_]+)}/g

/**
 * template
 *
 * @param {String} string
 * @param {Array} ...args
 * @return {String}
 */

// 模板替换函数
export function template (str: string, ...args: any): string {
  // 如果第一个参数是一个obj
  if (args.length === 1 && typeof args[0] === "object") {
    args = args[0]
  } else {
    args = {}
  }

  if (!args || !args.hasOwnProperty) {
    args = {}
  }

  // str.prototype.replace(substr/regexp, newSubStr/function) 第二个参数如果是个函数的话,每次匹配都会调用这个函数
  // match 为匹配的子串
  return str.replace(RE_NARGS, (match, prefix, i, index) => {
    let result: string

    // match是匹配到的字符串
    // prefix ???
    // i 括号中需要替换的字符换
    // index是偏移量

    // 字符串中如果出现{xxx}不需要被替换。那么应该写成{{xxx}}
    if (str[index - 1] === "{" &&
      str[index + match.length] === "}") {
      return i
    } else {
      // 判断args obj是否包含这个key值
      // 返回替换值, 或者被匹配上的字符串的值
      result = hasOwn(args, i) ? args[i] : match
      if (isNull(result)) {
        return ""
      }

      return result
    }
  })
}
总结

这个页面是使用vue作为前端框架,使用vue-i18n作为国际化的工具:

和后端同学约定好语言标识字段

前端根据后端下发的语言标识字段来调用不同的语言包

文本内容使用vue-i18n进行替换

图片内容需要视觉同学提供多语言版本

样式需要根据多语言进行定制。比如在body上添加多语言的标识class属性

第三方的SDK插件的国际化推动

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82100.html

相关文章

  • Vue际化处理 vue-i18n 以及项目自动切换中英文

    摘要:直接上预览链接国际化处理以及项目自动切换中英文环境搭建命令进入项目目录,执行以下命令安装国际化插件项目增加国际化翻译文件在项目的下添加文件夹增加中文翻译文件以及英文翻译文件,里面分别存储项目中需要翻译的信息。 0. 直接上 预览链接 Vue国际化处理 vue-i18n 以及项目自动切换中英文 1. 环境搭建 命令进入项目目录,执行以下命令安装vue 国际化插件vue-i18n...

    wangtdgoodluck 评论0 收藏0
  • 记一次开源学习--D2Admin 人人企业版

    摘要:前言上个月月底开源组开源了使用适配人人企业版专业版的前端工程具体详情见人人企业版适配发布。当然,也督促自己产出一篇相关的文章,来记录这次有趣的学习之旅。 Created by huqi at 2019-5-5 13:01:14 Updated by huqi at 2019-5-20 15:57:37 前言 上个月月底@D2开源组 开源了使用 D2Admin 适配 人人企业版(专业版) 的...

    notebin 评论0 收藏0
  • vue-i18n结合Element-ui的配置

    摘要:官网已经做了详细介绍,这里依葫芦画瓢跟着实现一下为了实现插件的多语言切换按照如上把国际化文件都整合到一起,避免中大段引入相关代码。 使用方法: 在配合 Element-UI 一起使用时,会有2个问题: ####(1)、页面刷新后,通过按钮切换的语言还原成了最初的语言,无法保存 ####(2)、框架内部自带的提示文字无法更改,比如像时间选择框内部中的提示文字 关于第一个问题,可以在初始化...

    孙淑建 评论0 收藏0
  • vue,使用vue-i18n实现际化

    摘要:需求公司项目需要国际化,点击按钮切换中文英文安装注入实例中,项目中实现调用和模板语法语言标识通过切换的值来实现语言切换中文语言包英文语言包最后对应语言包中文语言包首页概览公司概述财务报表更多附录主要财务指标对比分析新闻事件档案 需求 公司项目需要国际化,点击按钮切换中文/英文 1、安装 npm install vue-i18n --save 2、注入 vue 实例中,项目中实现调用 ...

    jsummer 评论0 收藏0
  • vue 项目的I18n际化

    摘要:国内主要主要三点,一个是港澳台采用中文繁体英文,内陆通俗中文简体,新疆等地区采用文化标准。 I18n (internationalization ) ---未完善 产品国际化是产品后期维护及推广中重要的一环,通过国际化操作使得产品能更好适应不同语言和地区的需求 国际化重点:1、 语言语言本地化2、 文化颜色、习俗等3、 书写习惯日期格式、时区、数字格式、书写方向备...

    2i18ns 评论0 收藏0

发表评论

0条评论

不知名网友

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<