前端多语言设计模式

前言


  • 国际化背景下,前端系统需要支持多语言的场景越来越多。
  • 当下已经存在开源的前端多语言解决方案(比如 i18n)。以React 为例,多语言解决方案有 i18n-react react-intl等。
  • 不管是为了 合理运用 现有解决方案,还是针对自己公司的 特殊性 需要 二次封装,甚至完全 造轮子,了解 多语言设计模式 都很有必要。

思路


  • 一套系统,支持多种语言,我们自然的会想到配置化
  • N 种语言 N套配置;在页面加载的时候,根据当前语言环境,注入对应的语言配置文件。在业务组件中通过对应的 API 读取配置展示文案。

步骤


  • 创建和管理多语言资源文件
  • 根据语言(key)获取资源文件
  • 注入多语言资源文件并暴露调用的 API
  • 使用多语言

创建和管理多语言资源文件


  • 创建:支持 N 种语言,就配置 N 套对应的语言文件;通过{ key: value } 的方式组织起来;
  • 粒度:为了扩展性和可维护性考虑,建议一个大的模块对应一套配置文件。对应特别通用的一些文案,可以配置一套公用配置文件。
  • 管理:建议公用配置文件放顶层目录管理,模块配置文件放在模块目录一起管理。这样方便尽量只注入模块需要的配置文件。
//目录结构
.
|-- common
  |-- locale
    |-- en_US.json
    |-- zh_CN.json
|-- pages
  |-- moduleA
    |-- public
      |-- locale
        |-- en_US.json
        |-- zh_CN.json
  |-- moduleB
    |-- public
      |-- locale
        |-- en_US.json
        |-- zh_CN.json

// 文件格式
{
  "app.moduleA.hello_word": "Hello word",
  "app.moduleA.hello_tom": "Hello tom"
  ...
}

注入多语言资源文件并暴露调用的API

  • 获取多语言key:根据实际情况,一般来源有:navigator.userAgent navigator.language URL参数等。
  • 顶层注入:封装多语言组件 (LocalProvider) 包裹业务组件。

// 顶层注入的核心代码...
const lang = comFun.getLang(); // 获取当前环境语言的封装方法
ReactDom.render((
  <LocalProvider lang={lang} files={
    [
      import('@common/locale/'+lang+'.json'), // 公共配置
      import('./public/locale/'+lang+'.json') // 模块配置
    ]
  }>
    {/*  AppContainer: 其他公共封装   */}
    <Route path="/" component={AppContainer}>  
      <IndexRoute component ={Index}/>
      <Route path="index" component={Index}/>
      {/*  ...   */}
    </Route>
  </LocalProvider>
), document.getElementById('app'));

// LocalProvider 核心 & 简易实现...
import React, { Component } from 'react'

/**
 * 多语言组件封装
 */
class LocaleProvider extends Component {
  constructor(props) {
    super(props)
    const { files, lang } = this.props
    this.Lang = lang
    this.transObj = {};  //配置存储
    this.state = {
      localeReady: files && files.length ? false : true
    }
    this.translateFile(files, this.Lang)
  }

  /**
   * 加载多语言文件
   * @param {Array} imports 异步加载语言文件
   * @param {String} lang 当前选择语言
   */
  translateFile = (imports, lang) => {
    if (imports && imports.length) {
      Promise.all(imports)
        .then(
          modules => {
            this.transObj = Object.assign({}, ...modules)
            React.Component.prototype.T = this.$T  // 方法注入
            this.setState({ localeReady: true })
          },
          _ => {
            console.log('require translate file fail')
          }
        )
        .catch(e => {
          console.error(e)
        })
    }
  }
  /**
   * 读取多语言配置
   * @param {String} key 具体配置的key
   */
  $T = (key) => {
    if (typeof key === 'string') {
      return this.transObj[key] || key // 找不到key的配置 便返回key
    } else {
      throw new TypeError('Translate item should be string, but got ' +
        typeof key)
    }
  }

  render() {
    return <div>{this.state.localeReady && this.props.children}</div>
  }
}
export default LocaleProvider

以上只是粗糙的简易实现,实际项目中还需要进行扩展,润色,封装。比如增加文案的 format 功能,可以插入变量。

使用多语言

// 略...
render() {

    return (
      <div>
        {/* 直接调用 LocaleProvider 中注入 react.Component 中的方法 T 使用*/}
        { this.T('app.moduleA.hello_word')}
      </div>
    )
}

注意:如果要在方法组件中使用,需要把 T 作为 propsComponent 父组件传递下去。也可以直接把文案作为 props 传递下去。