阅读提示:有些图比较大,请点开看。
i18next是一个国际化框架,可以根据用户语言环境的不同来显示不同的文本信息。对于一些跨国使用网站来说,是必须用到的前端框架之一。
这次阅读的源代码总共代码行数1700多。相比前一个阅读的前端框架requirejs要少很多。(RequireJS框架源代码解析)开发语言用的是ESMAScript/ES。使用babal编译成JS版本。
1.基本结构
源代码目录很简单,仅几个文件。整个代码的结构图如下。
整个库的一些主要的类,i18n,Translator,BackendConnector,CacheConnector,ResoureStore这几个的基类都是EventEmitter。
2.On Emit模式
EventEmitter是一个观察者模式的封装,是前端环境中很常见的On,Emit结构。
从这个设计来说,I18next是不想依赖于浏览器端的dom和event。所以自己引入了一个EventEmitter类。这个类和其他的框架中的一些EventEmitter没有太大的区别。
比如说明文档中event部分,初始化后的回调event。
onInitialized
i18next.on('initialized', function(options) {})
Gets fired after initialization.
就是使用了EventEmitter中的功能。模拟出了一个initialized的event。因为是模拟的,所以event的触发调用,实际是一个同步调用,相对来说,回调函数不能运行太久,不然会把框架的“暂停”运行,出现性能问题。
也是因为基于不依赖于具体环境的On Emit设计。i18next的适应范围绝不限于前端环境。通过不同插件的组合,就可以适应于前端和后端代码。
3.插件
从i18next的源代码来看,整体功能是很简单的,只是提供了一个翻译功能。有不少的功能,是依赖于插件的。
从i18n.use()函数,可以知道i18next的外置模块,也就是官方文档说的插件,实际上有五种。
languageDetector,用户语言检测插件。
logger,日志插件,必须符合Logger的定义。
cache,快速缓存插件,通过CacheConnector进行管理。
backend,后端资源插件,通过BackendConnector进行管理。
external,第三方插件。第三方插件,只是进行了了初始化,并没有任何操作。
4.i18next的初始化
i18next的初始化可以分成两种情况。有插件加载和无插件加载。
第一种是没有加载插件的情况下,会根据默认的配置和用户的配置结合之后,进行初始化。主要通过changeLanguage()来控制使用那种语言的资源。并最后回调用户代码中注册的OnInitialized()事件,通知用户代码可以使用i18next了。
简化的调用顺序如下图。
第二种是有插件加载。基本的调用顺序不变,但Init(),changeLanguage(),loadResources(),这三个方法就有些逻辑的变化。
i18next.init()中,首先会把后续用到的一些组件,都归入services。
其中Logger和languageDetector两个插件存在的时候才进行初始化,而CacheConnector和backendConnector不管有没有对应的插件,都会初始化。
changeLanguage() 在存在langunageDectector的情况下会去调用,并检测用户环境的所使用的语言是汉语,英语或者其他的。并根据langunageDectector是同步还是异步来不同的操作。
loadResources(),则会去尝试调用cacheConnector.load()和backendConnector.load(). 如果无相关插件,则只是在调用一个空的方法。
完整的i18next初始化过程如下。
5.用户语言设置和语言检测插件
i18next在没有使用语言检测插件的情况下,那么就需要使用者在配置里设置,或者通过changeLanguage函数来调整需要翻译的语言。目前官方提供了一个语言插件,i18nextBrowserLanguageDetector,可以通过cookies,htmlTag,localstorage,querystring和navigtor来自动检测用户浏览器语言设置。使得i18next能输出对应的语言文本。这个语言检测插件代码比较简单,就略过,不再具体分析了。
6.翻译过程和BCP47
i18next的核心代码,翻译功能,全部都在Translator之中。主要逻辑是key/value查找。只是因为牵涉到多种语言,多个namespace,以及可能存在一些错误的输入参数和不存在的key。这部分纠错的代码就显得有点复杂了。
按设计,i18next的资源数据分成三级,语言,namespace和key,最后才是对应的文本。语言存在语系的定义。比如按http://www.iana.org/assignments/language-tags/language-tags.xhtml中定义,汉语就分成很多子类。
实际上,日常网页中,我们经常可以看到这类写法,来标示网页的语言特性。但是这种写法已经是废弃掉的不正规写法,按BCP 47 - Tags for Identifying Languages (https://tools.ietf.org/html/bcp47)的标准,也就是上面的表格,标准写法是
1. 简体中文页面:html lang=zh-cmn-Hanså
2. 繁体中文页面:html lang=zh-cmn-Hant
3. 英语页面:html lang=en
i18next是完全遵循了BCP47的标准来格式化语言标记,也就是code,语言编码。
比如LanguageUtils里面有两个函数
getScriptPartFromCode(code)
getLanguagePartFromCode(code)
直接看代码是有点迷糊的,什么是Script部分,什么是Language部分?
但按照BCP47的定义,就一目了然了。
按实际例子来说,lang=‘zh-cmn-Hans’
‘zh’是language部分
‘cmn’是script部分
‘Hans’是reqion定义。
i18next用到了code中的language部分和script部分。
虽如此,但对于不标准的zh-cn的用法,i18next也会把zh当作语言,cn当作script来处理。这似乎也刚好符合了i18next的代码逻辑,所以完全没毛病。
也正是如此,实际使用过程中,如果lang tag使用不规范,就容易出问题。建议尽量使用cookies来控制语言。
此外,一些特别的操作,比如
Interpolation,内嵌的变量格式化操作
Formatting,自定义格式化操作
Plural,复数形式处理
Nesting,关键字之间相互引用
context,根据上下文不同,返回不同的value。
Objects and arrays,允许key对应返回对象和数组。
都是在tranlater中完成的,有部分功能,interpolation,Nesting,Formatting则封装到extendTranslation方法当中。这部分功能,可能会调用一些用户设置的回调函数,或者用户方法。
这样,构成了一个整体的翻译过程。如下图。
8.CIMode
cimode在i18next里是一个神秘的设定,我搜索了下代码和网络,没有找到任何解释。启动这种模式,只需要在配置lng为cimode就可以了。
options.lng = 'cimode' 或者changeLanguage('cimode')
i18n.t(),Translator.translate(keys, options)也就是就只会返回key,而不是翻译后的文本。
看上只是用来测试的。
9.复数形式
如果单单看PluralResolver.js中的代码,完全是糊里糊涂的。因为代码似乎提供了一套判定规则,来判断不同的语言的复数形式。这部分的功能,是很古老的设计,来源于GNU的gettext的函数设计。
复数是本地化绕不开的一个问题。比如英语的 “1 page”vs “2 pages”,比如中文的“1页”vs“2页”就是完全不同的表达方式。
简单的来说,复数的处理分成两个部分的概念。
复数规则和复数的表现形式。复数规则是基于语言的语法规则。表现形式就是怎么写。比如上面提到的 page和pages的例子。
Mozilla的本地化标准中列出了18种复数规则。
随意列举一二。
Plural rule #0 (1 form)
Families: Asian (Chinese, Japanese, Korean), Persian, Turkic/Altaic (Turkish), Thai, Lao
everything:0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, …
这是第一条规则,表示无复数形式变种,只有一种表达方式。汉语就是其中之一。
Plural rule #1 (2 forms)
Families: Germanic (Danish, Dutch, English, Faroese, Frisian, German, Norwegian, Swedish), Finno-Ugric (Estonian, Finnish, Hungarian), Language isolate (Basque), Latin/Greek (Greek), Semitic (Hebrew), Romanic (Italian, Portuguese, Spanish, Catalan), Vietnamese
is 1:1
everything else:0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, …
第二条规则,除了1以外,都要成为复数形式。page和pages的区别。
更多的,可以看下https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals
i18n总共提供了21种复数形式的判断。
支持了足够多的语言的复数形式。这21种复数形式定义来源(http://translate.sourceforge.net/wiki/l10n/pluralforms)
11.使用设计
从整个源代码来看,设计上的难点在于要支持不同的需求。从Interpolation,Formatting,Plural,Nesting,context,Objects and arrays,这几个功能的设计来说,涵盖的适用范围是很广,而且是很灵活的。并非只是单纯的key/value对应的操作。留了不少的操作接口给用户代码。
从配置参数上,可以看到
missingKeyHandler: false,// function(lng, ns, key, fallbackValue) -> override if prefer on handling
returnNull: true,// allows null value as valid translation
returnEmptyString: true,// allows empty string value as valid translation
returnObjects: false,
joinArrays: false,// or string to join array
returnedObjectHandler: () => {},// function(key, value, options) triggered if key returns object but returnObjects is set to false
parseMissingKeyHandler: false,// function(key) parsed a key that was not found in t() before returning
这些都是留给用户来控制返回值,甚至是允许用户提供相关的方法来进行自定义操作的接口。这就要在设计上考虑到留有足够的灵活度,至少要分割出内部的翻译过程,以及后续的调用用户代码的方法。所以,我们可以看到Translator之中的extendTranslation方法。
至于剩下的,就是要考虑不存在的key和错误数据问题。因为这是一个应用框架,不能给用户返回不可预期的结果。整体设计上是通过fallback功能(提供默认值)以及Missing处理(加速对错误数据的处理)来确保每一次的i18n.t(keys,options)的调用都是存在明确的值的。而不是抛出一个异常。用户如果在配置里编写了key:object/key:array的数据,但没有修改returnObject的默认配置,则是返回一个错误文本提示。
12.后记
整体文章还是略显简单,虽然框架代码不多,用到的设计和知识确实不少。要写清楚,还是要不少篇幅,只能略带着提下。
各位,有问题,请留言。
相关复数形式,BCP47的相关资源,请关注之后,发送’i18next’来获取。
领取专属 10元无门槛券
私享最新 技术干货