记得我刚来咱们公司的时候,接手如今负责的项目的时候,我就发觉了一个问题:全部的文本资源都是硬编码在代码里面。这固然会带来不少问题。但考虑到我负责的这个项目是公司内部的管理工具,同时大部分用户都是中国人,所以抽离文本资源,作i18n的需求并非十分强烈。javascript
这周公司招了一位外籍员工。我并不肯定她是哪一国人,不过从口音上来判断,以及言谈间她曾经提到的加利福尼亚州,我想应该是一位美国女性。老大说她会和其余的PM同样,居住在厦门,远程工做,偶尔来办公室上班。而且她也会使用我负责的这个工具。html
如今i18n就有比较强烈的需求了。有必要出一个合理的架构,一劳永逸的解决问题。java
i18n是Internationalization的缩写,实际上i18n应该是指建立或者调整产品,使得产品具备能轻松适配指定的语言和文化的能力。固然,咱们还有另一个概念,叫作Localization(简写L10n),也就是本地化。L10n正确的说是指已经全球化的产品,适配某一个具体语言和文化的这一个过程。node
有点绕口,简单说就是,i18n就是给产品添加新特性,使产品可以支持对多种语言和文化(货币,时间等等)。而L10n就是产品具体实现某一种语言和文化的过程。react
回过头来,i18n有这么几个主要的关注点:webpack
Date and times formattinggit
Number formattingweb
Language sensitive string comparison架构
Pluralizationapp
不一样国家对应的日期格式其实都是不一样的,尽管我不以为十分复杂,不过细节的处理上也是有不少选择:
weekday,你能够设置成显示全名字,好比zh-CN的星期四,en-US的Thursday等等
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"
数字的格式化,这个比较有趣。这里说的数字,包含了货币,百分比,浮点数。其中货币的显示应该是相对比较复杂的。就以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,那么应该这么说,królik
若是兔子的数量在2-4之间,那么应该说,królika
若是兔子的数量不是1,而且数量在0 - 1之间,或者5 - 9之间,或者12 - 14之间,都用królików
其余状况统一用króliki
使用facebook官方的create-react-app脚手架建立的react app
目前个人解决方案有这么几个关注点:
文本资源要可以轻易的导出
文本资源要孤立,避免和程序实现的耦合
提供极简的接口方法设计
处理语言复数形式的库,应该要能很好的拓展
/-- |--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/
就以此类推。
这个类很简单,封装了处理文本资源的相关方法。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); } }
这是一个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机制。所以咱们应该利用起来。
具体的两个文档:
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; } }
使用的时候也是至关简单,很少说,直接上代码。
class App extends React.PureComponent { render() { const { preferences } = this.props; return ( <IntlProvider locale={preferences.language}> <div>{this.props.children}</div> </IntlProvider> ); } }