国际化与本地化
概述
国际化与本地化,或者说全球化,其目的是让你的站点支持多个国家和区域。其中国际化是指功能和代码设计能处理多种语言和文化习俗,能够在创建不同语言版本时,不需要重新设计源程序代码。国际化的英文单词是 Internationalization ,简称 I18N。 本地化是将站点按照特定国家、地区或语言市场的需要进行加工,使之满足特定用户对语言和文化的特殊要求。本地化的英文对应Localization,缩写为L10N。举例 Moment,其支持 setLocale
方法切换语言就是国际化,每一个 locale
的配置文件定义了具体区域时间格式就是本地化。
think-i18n
think-i18n 是 ThinkJS 3.0 国际化方案的实现, 基于 Jed, Moment 和 Numeral.
安装
npm install think-i18n --save
配置 extends.js
// ThinkJS config/extend.js
const createI18n = require('think-i18n');
const path = require('path');
module.exports = [
createI18n({
app: think.app, // 如果为空,__ 就不会被自动 `assign` 到 `think-view` 实例
i18nFolder: path.resolve(__dirname, '../i18n'),
localesMapping(locales) {return 'en';}
})
];
配置 locale 文件
每个 locale 一个文件,放在 i18nFolder 目录下。
- dateFormat 会应用到 moment.local(localeId, dateFormat); 如果不提供配置,默认使用 en
- numeralFormat 会应用到 numeral.locales[localeId] = numeralFormat; 如果不提供配置,默认使用 cn
- translation 相当于 Jed 里面的 locale_data, 如果你是使用 po 文件管理翻译,jed 推荐使用 po2json。
查看配置详情
Controller 和 View (nunjucks)
Controller
如果需要再controller 里面获取 I18n 的实例或者当前的 locale,可以调用
async indexAction(){
const __ = this.getI18n(/*forceLocale*/));
const locale = this.getLocale();
}
View
如果使用了 think-view 模块并配置了 app 参数, think-i18n 会自动调用注入一个实例到当前模板实例里,类似: this.assign('__', this.getI18n()), 这样在模板里面就可以使用直接使用 i18n 暴露的接口。
{{ __('some key') }}
{{ __.jed.dgettext('domain', 'some key') }}
{{ __.moment().format('llll') }}
{{ __.numeral(1000).format('currency') }}numberFormat.formats)
完整配置
- app:think.app 如果配置了此参数,则会监听 viewInit 事件并把 i18n 实例注入到模板的 __ 参数里面。
- i18nFolder:string 放置配置文件的目录
- localesMapping:function(locales){return localeId;} 从一个可能的 locale 数组,返回唯一一个 localeId。比如 header['accept-language'].split(',') 可以得到一个locale 的数组,我们可以写一些逻辑并返回最终我们想要的 localeId。
getLocale 如果为空,默认逻辑是从 http 头的 accept-language 里面获取,并用 ',' 分割开。 如果希望从 url 的 query 字段获取,设置为 {by: 'query', name: '字段名'}. 如果希望从 cookie 里面的某个字段获取, 设置为 {by: 'cookie', name: '字段名'} 也可以根据 controller.ctx 实现自己的逻辑, function(ctx) {return locale;}
debugLocale 用来调试某个 localeId
jedOptions 是一个对象,可以在里面配置 domain 和 missing_key_callback,具体请参考 jed options 文档.
默认的值是 {}, 最终用来实例化 jed 的 options 如下:
Object.assign(jedOptions, {locale_data: <your locale translation>})
背后的思考
- 你可能会觉得这个方案太复杂,但是 i18n 本来就很复杂,要想实现的好,你可能需要的只会更多。
- 既然基于 moment 和 numeral,为什么不直接使用它们自带的 i18n 配置?这里为的是配置的透明和可控制性,你可以灵活的组合在某一个 locale 下,分别使用什么样语言翻译,时间格式和数字格式。比如有一个在中国的购物网站,希望提供英文的翻译方便老外使用,但是货币数字和时间的格式仍然使用中国的标准。参考下面的配置:
// locale setting of en-CH.js
module.exports = {
localeId: 'en_CH',
translation: require('../english.po.json'),
dateFormat: require('../moment/en.json'),
numeralFormat: require('../numeral/en.json')
};
其中 ../moment/en.json
是一个json,格式参考 moment 的i18n文件,一模一样。
其中 ../numeral/en.json
是一个json,格式参考 numeral, 需要指出的是,额外的你可以在 numeral 的配置里面设置自定义的格式,并且这个是跟着locale走的,这个实现是个小小的黑魔法,但是对于 i18n 的最佳实践非常重要。
{
localeId: cn,
...
formats: [{name: 'currency', format: '000.00$'}]
}
最佳实践
总是使用自定义的格式,这样就可以通过配置定制不用locale下有不同的输出格式。同时也方便后期的维护,比如某天我们需要把所有长日期显示修改格式,不用到每个文件里面取修改,只需要改配置就好,相当于一层抽象。
- 使用 __.moment().format('llll') 而不是 moment().format('YYYY-MM-dd HH:mm').
- 使用 __.numeral(value).format('customFormat') 而不是 numeral(value).format('00.00$'), Numeral 是不支持对每个 locale 自定义格式的,类库里面通过额外的代码支持了这个功能(具体可以看源码),配置方式参考 locale.numeralFormat.formats。
注
如果定义了 en locale, 会覆盖 Numeral 默认的配置。
调试某个 locale
默认情况下,是通过读取 header['accept-language'] 的值,然后通过 localesMapping 转换后作为某一时刻采纳的 locale。如果需要调试,在 view 配置里面设置 debugLocal =