记得我刚来我们公司的时候,接手现在负责的项目的时候,我就发觉了一个问题:所有的文本资源都是硬编码在代码里面。这当然会带来很多问题。但考虑到我负责的这个项目是公司内部的管理工具,同时大部分用户都是中国人,因此抽离文本资源,做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, 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, 11, 20, 3, 0, 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', { style: 'currency', 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, params) => '',};}return {getText: (key, params) => this.childContext.getText(key, params),getCommonText: (key, params) => this.childContext.getCommonText(key, params),};}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>);}
}

参考文档

  • Tags for Identifying Languages

  • ECMAScript Internationalization API

  • Pluralization for JavaScript

  • ICU User Guide

实战React App的i18n相关推荐

  1. 实战react技术栈+express前后端博客项目(3)-- 后端路由、代理以及静态资源托管等配置说明...

    项目地址:github.com/Nealyang/Re- 本想等项目做完再连载一波系列博客,随着开发的进行,也是的确遇到了不少坑,请教了不少人.遂想,何不一边记录踩坑,一边分享收获呢.分享当然是好的, ...

  2. 深入浅出 Create React App

    本文差点难产而死.因为总结的过程中,多次怀疑本文是对官方文档的直接翻译和简单诺列:同时官方文档很全面,全范围的介绍无疑加深了写作的心智负担.但在最终的梳理中,发现走出了一条与众不同的路,于是坚持分享出 ...

  3. 【翻译】基于 Create React App路由4.0的异步组件加载(Code Splitting)

    基于 Create React App路由4.0的异步组件加载 本文章是一个额外的篇章,它可以在你的React app中,帮助加快初始的加载组件时间.当然这个操作不是完全必要的,但如果你好奇的话,请随 ...

  4. 实战react技术栈+express前后端博客项目(8)-- 前端管理界面标签管理+后端对应接口开发...

    项目地址:https://github.com/Nealyang/R... 本想等项目做完再连载一波系列博客,随着开发的进行,也是的确遇到了不少坑,请教了不少人.遂想,何不一边记录踩坑,一边分享收获呢 ...

  5. 在 .NET Core 5 中集成 Create React app

    翻译自 Camilo Reyes 2021年2月22日的文章 <Integrate Create React app with .NET Core 5> [1] 本文演示了如何将 Crea ...

  6. react.js app_如何创建Next.js入门程序以轻松引导新的React App

    react.js app Getting started with a new React app is easier than ever with frameworks like Next.js. ...

  7. tailwind css_什么是Tailwind CSS,如何将其添加到我的网站或React App中?

    tailwind css CSS is a technology that can be your best or worst friend. While it's incredibly flexib ...

  8. 如何使用Create React App DevOps自动化工作中所有无聊的部分

    by James Y Rauhut 詹姆士·鲁豪(James Y Rauhut) 如何使用Create React App DevOps自动化工作中所有无聊的部分 (How I automate al ...

  9. Create React App 2.0 华丽登场

    贺! Create React App 2.0 在 10/02 正式发布 ?????? Create React App 是由官方所维护的开发工具,主要提供了专属于 React 开发环境的前置工作.简 ...

最新文章

  1. windows内存管理和API函数
  2. 145.单工、半单工、双工
  3. POJ-4004:数字组合(用位移方法解组合数问题,Java版)
  4. Android Studio安装问题及填坑
  5. 在Linux环境下mysql的root密码忘记解决方法 1.首先确认服务器出于安全的状态,也就是没有人能够任意地连接MySQL数据库。 2.修改MySQL的登录设置: # vi /etc/my.c
  6. easyui使用心得
  7. idea package自动生成_IDEA -- 自动创建POJO
  8. latex设置一级标题样式不居中_Word应用“样式”的设置
  9. 请教关于 license.licx 不能转换成2进制文件!(c# 开发web应用程序)
  10. 客户端手册_增值税发票管理系统“2.0”版——客户端环境配置问题
  11. 《C++ Primer Plus》第15章 友元、异常和其他 学习笔记
  12. BZOJ 2560(子集DP+容斥原理)
  13. 细节也可以决定网站中交互设计的成败
  14. FreeFileSync - 最佳免费开源文件夹同步备份软件 (FTP/局域网/移动硬盘)
  15. TextCNN代码解读及实战
  16. iphone捷径未能连接服务器,ios13无法安装第三方捷径怎么办 不允许不受信任的快捷指令解决方法...
  17. 【Excel】【行列转换:转置粘贴 or TRANSPOSE】
  18. E-R图、N-S图、PAD图、程序流程图
  19. java基础之异常_繁星漫天_新浪博客
  20. 数据库误删了数据再也不用跑路了,

热门文章

  1. 机器学习中的回归分析
  2. 移植 Python 项目到容器 (Container) 中
  3. 学习红客技术必备,手把手教你成为“安防第一人”
  4. Openlayers3 实现点击不同的图标弹出不同的popup信息
  5. style.cssText
  6. 关于jsp页面上传照片的后台方法
  7. 分类战士html,王者荣耀战士分为哪几种类型
  8. 微信截图时菜单栏消失怎么办
  9. Q2# ZK SYN Flood与参数优化
  10. Taro小程序如何引入echarts