实战React App的i18n

记得我刚来我们公司的时候,接手现在负责的项目的时候,我就发觉了一个问题:所有的文本资源都是硬编码在代码里面。这当然会带来很多问题。但考虑到我负责的这个项目是公司内部的管理工具,同时大部分用户都是中国人,因此抽离文本资源,做i18n的需求并不是十分强烈。

这周公司招了一位外籍员工。我并不确定她是哪一国人,不过从口音上来判断,以及言谈间她曾经提到的加利福尼亚州,我想应该是一位美国女性。老大说她会和其他的PM一样,居住在厦门,远程工作,偶尔来办公室上班。并且她也会使用我负责的这个工具。

现在i18n就有比较强烈的需求了。有必要出一个合理的架构,一劳永逸的解决问题。

i18n的主要关注点

i18n是Internationalization的缩写,实际上i18n应该是指创建或者调整产品,使得产品具有能轻松适配指定的语言和文化的能力。当然,我们还有另外一个概念,叫做Localization(简写L10n),也就是本地化。L10n正确的说是指已经全球化的产品,适配某一个具体语言和文化的这一个过程。

有点绕口,简单说就是,i18n就是给产品添加新特性,使产品能够支持对多种语言和文化(货币,时间等等)。而L10n就是产品具体实现某一种语言和文化的过程。

回过头来,i18n有这么几个主要的关注点:

  1. Date and times formatting

  2. Number formatting

  3. Language sensitive string comparison

  4. Pluralization

Date and times formatting

不同国家对应的日期格式其实都是不同的,尽管我不觉得十分复杂,不过细节的处理上也是有很多选择:

  1. weekday,你可以设置成显示全名字,比如zh-CN的星期四,en-US的Thursday等等

  2. month,你可以设置成数字形式,全名,短名,类似于12,December,Dec

...

完整的例子:

var date = new Date(Date.UTC(2012,11,20,3,0));

// Results below use the time zone of America/Los_Angeles (UTC-0800,Pacific Standard Time)

// US English uses month-day-year order
console.log(new Intl.DateTimeFormat('en-US').format(date));
// → "12/19/2012"

// British English uses day-month-year order
console.log(new Intl.DateTimeFormat('en-GB').format(date));
// → "19/12/2012"

// Korean uses year-month-day order
console.log(new Intl.DateTimeFormat('ko-KR').format(date));
// → "2012. 12. 19."

// Arabic in most Arabic speaking countries uses real Arabic digits
console.log(new Intl.DateTimeFormat('ar-EG').format(date));
// → "١٩‏/١٢‏/٢٠١٢"

// for Japanese,applications may want to use the Japanese calendar,// where 2012 was the year 24 of the Heisei era
console.log(new Intl.DateTimeFormat('ja-JP-u-ca-japanese').format(date));
// → "24/12/19"

// when requesting a language that may not be supported,such as
// Balinese,include a fallback language,in this case Indonesian
console.log(new Intl.DateTimeFormat(['ban','id']).format(date));
// → "19/12/2012"

var date = new Date(Date.UTC(2012,0));
// request a weekday along with a long date
var options = { weekday: 'long',year: 'numeric',month: 'long',day: 'numeric' };
// an application may want to use UTC and make that visible
options.timeZone = 'UTC';
options.timeZoneName = 'short';
console.log(new Intl.DateTimeFormat('en-US',options).format(date));
// → "Thursday,December 20,2012,GMT"

Number formatting以及Pluralization

数字的格式化,这个比较有趣。这里说的数字,包含了货币,百分比,浮点数。其中货币的显示应该是相对比较复杂的。就以en-US来说,1000美元通常显示成$1,000.00,而1000人民币则会显示成¥1,000.00。货币的符号,以及数字分割方式各个国家都存在不同。

var number = 123456.789;

// German uses comma as decimal separator and period for thousands
console.log(new Intl.NumberFormat('de-DE').format(number));
// → 123.456,789

// India uses thousands/lakh/crore separators
console.log(new Intl.NumberFormat('en-IN').format(number));
// → 1,23,456.789

// the nu extension key requests a numbering system,e.g. Chinese decimal
console.log(new Intl.NumberFormat('zh-Hans-CN-u-nu-hanidec').format(number));
// → 一二三,四五六.七八九

var number = 123456.789;

// request a currency format
console.log(new Intl.NumberFormat('de-DE',{ style: 'currency',currency: 'EUR' }).format(number));
// → 123.456,79 €

// the Japanese yen doesn't use a minor unit
console.log(new Intl.NumberFormat('ja-JP',currency: 'JPY' }).format(number));
// → ¥123,457

涉及到数字的,还有另外一个问题,那就是语言的复数形式。中文似乎是没有复数形式的,比如我们经常说,一只兔子,两只兔子。但是如果你用英语,你就能明显发觉不对。在英语里,应该说one rabbit,two rabbits,many rabbits。是的,英语里主要有两种复数形式。

那么有没有其他的 复数形式?回答当然是肯定的,比如波兰语。在波兰语,兔子一词是królik,它的复数形式有这么几种情况:

  1. 兔子的数量是1,那么应该这么说,królik

  2. 如果兔子的数量在2-4之间,那么应该说,królika

  3. 如果兔子的数量不是1,并且数量在0 - 1之间,或者5 - 9之间,或者12 - 14之间,都用królików

  4. 其他情况统一用króliki

解决方案的设计

项目背景

  1. 使用facebook官方的create-react-app脚手架创建的react app

关注点

目前我的解决方案有这么几个关注点:

  1. 文本资源要能够轻易的导出

  2. 文本资源要孤立,避免和程序实现的耦合

  3. 提供极简的接口方法设计

  4. 处理语言复数形式的库,应该要能很好的拓展

技术选型

  1. FormatJS,a modular collection of JavaScript libraries for internationalization that are focused on formatting numbers,dates,and strings for displaying to people.

解决方案

项目的目录结构

/--
  |--node_modules
  |--public
  |--src
    |--app
    |--common
    |--components
    |--configs
    |--i18n
      |--app
        |--routes
          |--setting
            |--en-US.js
            |--zh-CN.js
        |--index_en-US.js
      |--common
        |--index_en-US.js
      |--components
        |--index_en-US.js
      |--index_en-US.js
      |--index_zh-CN.js
    |--index.js
    |--logo.svg
    |--setupTests.js

如上文所示,我将所有的文本资源都独立出来,单独存放在了i18n这个文件夹下。实际上,这些文本资源是有自己独立的命名空间的,比如/src/app相关的文本资源,就会单独放在这个文件夹下。其他的比如/src/common//src/components/就以此类推。

Intl类

这个类很简单,封装了处理文本资源的相关方法。getText的参数key需要特别注意,这个参数应该是绝对路径,比如app.routes.setting.preferences这样。那么,相关的资源应该是要放在/src/i18n/app/routes/setting/en-US.js文件里。

class Intl {
  static COMP_COMMON_TEXT = 'components.Common';
  constructor(locale,resource) {
    this.locale = locale || 'zh-CN';
    this.resource = resource;
  }
  getCommonText(key,params = {}) {
    return this.getText(`${Intl.COMP_COMMON_TEXT}.${key}`,params);
  }
  getText(key,params = {}) {
    let textResource = '';
    let source = this.resource;
    const locale = this.locale;
    const properties = key.split('.');
    const hasOwnProperty = Object.prototype.hasOwnProperty;
    properties.forEach((property,index) => {
      const stillNameSpace = index !== properties.length - 1;
      if (stillNameSpace) {
        source = source[property];
      } else if (hasOwnProperty.call(source[property],'default')) {
        textResource = source[property].default;
      } else {
        textResource = source[property] || '';
      }
    });
    const msg = new IntlMessageFormat(textResource,locale);
    return msg.format(params);
  }
}

IntlProvider

这是一个React组件。这里我们要利用React提供的Context这一特性,让整个React App范围内,都会从上下文中得到getText的方法。

我们都知道,Web app初始化的时候加载的Javascript脚本是越小越好,并且我们应该尽力保证按需加载所需要的资源。这也是我们为什么利用WebPack提供的Code Splitting机制让WebPack在打包的时候,切分出单独的chunk,减少包的体积。

在WebPack 1.x的时候,我们可以使用require.ensure()。但这个是WebPack自己的语法,并非标准,同时这个语法还会破坏Jest的测试,并不是一个很好的选择。WebPack 2.x以后就开始提供基于import()的Code Splitting机制。因此我们应该利用起来。

具体的两个文档:

  1. WebPack的Code Splitting with ES2015

  2. Dynamic import() proposal

class IntlProvider extends React.Component {
  static DEFAULT_LOCALE = 'zh-CN';
  static propTypes = {
    locale: PropTypes.string,children: PropTypes.element,};
  static defaultProps = {
    locale: 'zh-CN',children: null,};
  static childContextTypes = {
    getText: PropTypes.func,getCommonText: PropTypes.func,};
  state = {};
  constructor(props,context) {
    super(props,context);
    this.childContext = new Intl(props.locale);
  }
  async componentWillMount() {
    const { locale } = this.props;
    const lang = await import(`../i18n/index_${locale}.js`);
    this.childContext = new Intl(locale,lang);
    this.setState({
      lang,});
  }
  getChildContext() {
    if (!this.childContext) {
      return {
        getText: (key,params) => '',getCommonText: (key,};
    }
    return {
      getText: (key,params) => this.childContext.getText(key,params),params) => this.childContext.getCommonText(key,};
  }
  render() {
    const comp = (!this.state.lang)
    ? null
    : React.Children.only(this.props.children);
    return comp;
  }
}

App

使用的时候也是相当简单,不多说,直接上代码。

class App extends React.PureComponent {
  render() {
    const { preferences } = this.props;
    return (
      <IntlProvider locale={preferences.language}>
        <div>{this.props.children}</div>
      </IntlProvider>
    );
  }
}

参考文档

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom