i18next框架源代码解析

阅读提示:有些图比较大,请点开看。

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’来获取。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180327G0JTPR00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券