Lingui.js—React 前端国际化工作流解决方案

1,576 阅读6分钟

       js 日期国际化,轮子满天飞,ECMAScript 推出原生Intl对象后,百花齐放的局面才开始迅速向原生靠拢... 并直接导致 moment.js 的弃坑 ,进化向了 luxen.js。采用js原生API的国际化日期库(day.js) 开始涌现,并以"玲珑小巧、相恨建晚的真香"姿态迅速抢占了相关市场份额,引领了一波国际化库迭代的潮流。react 前端国际化解决方案也在不断演进,如react-18nextreact-intl-universalLingui.jsreact-intl (format.js)fbtkiwi 等等。除了前两位大哥还停留在对原生js的友好支持外,其余均已迈向工作流化。本文从 传统前端国际化痛点、工作流解决方案、lingui.js 与 format.js(react-intl)对比 三个方面引入和介绍react国际化工作流的基本概念和流程,以及为什么推荐使用 lingui.js。

传统前端国际化痛点:

       笔者之前采用react-intl-universal。开始做点样例用起来还挺顺,文案按需加载照这着官方示例稍作修改即可。可是做着做着就发现不对劲了,json的文案得编辑通常需要集中管理,一个语言对应一个json文件。工作流程大致如下:每次新增个组件,遇到需要需要国际化的地方,都得小心翼翼的去locales目录下修改相应的json文件。并且命名还得有点规则。一不小心名字就冲突了,一不小心就遗漏了。国际化文案通常得由码农们自己去维护这几套json文案。组件新增、修改变化频率高,翻译协作困难。

传统国际化json文案制作

       如果想CDN发布国际化资源,json格式是首选。但集中的方式维护 json 文案比较麻烦,首先想到要有个支持js的,import来,export去,按组件管理文案,名字不就不容易冲突了吗?于是就找到了 react-18next、react-intl(<5版)。这么一看就能按组件分割开不同的国际化文档了。

传统js国际化文案制作

       这种方式由于采用js,如果国际化文案需要做动态加载时,通常会被分块打包到chunk块中。使用js,文案管理是清晰点了,名字也不容易冲突,但是遗忘导致不同步问题还是不能解决,工作的流程依然没有改变,团队协作依然困难。这种作坊式国际化的弊端总结如下:

  1. 维护国际化文案繁琐且困难,开发麻烦
  2. 无法知道项目中是不是还有未同步的文案,
  3. 调试查找麻烦 国际化涉及到多个相关人员,团队协作困难 正因为如此,不少库就提出了国际化全流程解决方案。

react国际化工作流解决方案

       目前react国际化工作流的解决方案,基本大同小异,核心的步骤通过命令完成提取,翻译,合并。 以format.js 为例, 国际化工作流解决方案通常分为以下步:

       提取:通过命令将组件或代码中的 defaultmessage 聚合到一个JSON或po文件中,并随附说明,以便翻译。        消息上传:此步骤将需要翻译的中间文案上传至翻译供应商(或翻译团队)。        翻译下载:将翻译供应商(或翻译团队)将翻译的结果提交给代码团队。        编译合并:代码团队通过命令,这将把翻译消息编译合并后提交回代码库。 format.js工作流示意图

       如果这个抽象图看起来太抽象,不之所云,不够直观,由于Lingui.js与 format.js国际化流程基本一致那么,以Lingui.js 结合上诉步骤,json格式为例:

lingui.js工作流示例        kiwi,fbt 的国际化工作流也大致类似。有些额外在IDE环节,或者翻译环节丰富了些工具。以kiwi官方文档的国际化全流程解决方案为例:

kiwi国际化全流程方案        fbt 和 kiwi 方案,有点复杂,上手难度较大,配置较多,踩坑多。简单上手,推荐使用lingui.js或者format.js。

lingui.js 与 format.js(react-intl)对比

该对比内容源自 Comparison with react-intl 官方文档,保留原汁原味,其中复数内容基本相同,故省略,其余仅作少量修改

       react-intl无疑是react最流行和使用最广泛的i18n库。lingui.js在很多方面非常相似:两个库对(消息的ICU-消息格式)使用相同的语法,而且它们也有非常相似的API。

       以下是react-intl 一个示例:

<FormattedMessage
   id="welcome"
   defaultMessage={`Hello {name}, you have {unreadCount, number} {unreadCount, plural,
     one {message}
     other {messages}
   }`}
   values={{name: <b>{name}</b>, unreadCount}}
 />

       lingui.js的底层API,没有太大区别:

<Trans
   id="welcome"
   defaults={`Hello {name}, you have {unreadCount, number} {unreadCount, plural,
     one {message}
     other {messages}
   }`}
   values={{name: <b>{name}</b>, unreadCount}}
 />

       看起来没太大区别的 API 为什么lingui 又得再造个轮子呢?

  • 带富文本标记的翻译

       假设我们有以下代码:

 <p>Read the <a href="/docs>documentation</a>.</p>

       在 react-intl中,它会翻译成:

<FormattedMessage
    id='msg.docs'
    defaultMessage='Read the <link>documentation</link>.'
    values={{
        link: (...chunks) => <a href="/docs">{chunks}</a>
    }}
/>

       而lingui.js扩展了 带标签的 ICU 消息格式,上面的例子会被翻译成:

<Trans
    id='msg.docs'
    defaults='Read the <0>documentation</0>.'
    components={[
        <a href="/docs" />
    ]}
/>

       消息提取后,翻译人员得到的信息是:Readthe<0>documentation</0>.

  • 基于组件的消息语法宏

       lingui.js 提供了自动生成消息语法的宏 @lingui/macro ,让我们回顾下上面的例子

<p>
   Read the <a href="/docs>documentation</a>.
</p>

       我们唯一需要做的就是将他包装在一个宏中:


<p>
   <Trans id="msg.docs">Read the <a href="/docs>documentation</a>.</Trans>
</p>

       这个宏然解析宏下的节点,并自动的生成defaults 和 组件的props。

       这个在对已存在的项目新增i18n国家化时非常有用,我们所需要所得就是将信息包装在宏中。 让我们比较以下react-intl 来看看其中得不同之处:

 <p>
   <FormattedMessage
       id='msg.docs'
       defaultMessage='Read the <link>documentation</link>.'
       values={{
           link: (...chunks) => <a href="/docs">{chunks}</a>
       }}
   />
</p>

       值得一提的是,消息ID是完全可选的。lingui.js在这种情况下是非专业的,并且可以完美地将消息作为ID使用

<p>
<Trans>Read the <a href="/docs>documentation</a>.</Trans>
</p>

       消息得ID是 Readthe<0>documentation</0>. 替代了 msg.id。这两种解决方案各有利弊,lingui允许您选择最适合您的解决方案。

  • 文本属性

       组件不能在某些上下文中使用,例如翻译文本属性。react-intl提供返回纯字符串的JS方法(例如:formatMessage),而lingui.js则为此类翻译提供了核心库。它还为这些用例提供了宏!

       举个栗子:

<a title={i18n._(t`The title of ${name}`)}>{name}</a>
<img alt={i18n._(plural({ value: count, one: "flag", other: "flags" }))} src="..." />

       同样支持自定义ID

<a title={i18n._(t("link.title")`The title of ${name}`}>{name}</a>
<img alt={i18n._(plural("img.alt", { value: count, one: "flag", other: "flags" }))} src="..." />
  • 扩展消息目录

       假设我们的应用程序已经国际化,现在我们想把消息发送给翻译程序。react-intl附带了Babel插件,它可以从单个文件中提取消息,但这取决于您将它们合并到一个文件中,然后发送给翻译人员。lingui.js提供了方便的CLI,可以提取消息并将其与任何现有的翻译进行合并。

  • 消息编译

       i18n库中最大、最慢的部分是消息解析和格式化程序。lingui.js将消息从消息格式语法编译成只接受插值的JS函数(例如组件、变量等)。这将使最终的包更小,并使库更快。编译后的目录还与区域设置数据(如复数)捆绑在一起,因此不必手动加载它们。

  • lingui.js的不足

       react intl已经有一段时间了,它肯定更受欢迎,使用率更高,很多生产网站都在运行它。社区规模更大,在StackOverflow和其他网站上查找帮助要容易得多。

       lingui.js是一个非常新的库,社区目前很小。它没有在许多生产现场进行测试。另一方面,lingui.js的测试套件非常大,所有的例子都被整合到集成测试套件中,以确保一切正常工作。最后但并非最不重要的是,lingui.js得到了积极的维护。错误会定期修复,新功能也会不断出现。目前正在进行webpack代码拆分的工作,以便只加载与块中代码相关的消息。

  • 对比总结

       两个库都使用相同的消息格式语法        有着相同的API

       lingui.js

  • 支持富文本消息
  • 提供宏以简化使用消息格式语法编写消息
  • 提供用于提取和编译消息的CLI
  • 非常小(<5kb Gzip)而且速度很快
  • 也适用于原生JS
  • 积极维护

       react-intl

  • 是React中最流行和使用的i18n lib
  • 在许多制作网站中使用(稳定性)
  • 有很多在线资源

总结

       笔者对以上方案均做了实践尝试。 最终结合 流程方式、文档友好、社区活跃、使用难易 等方面做简单对比,如下表格所示。ling.js使用的babel/macros,使得宏中能提供更丰富的信息给翻译团队,同时又生成更少的代码,在编译阶段屏蔽掉了只与翻译有关的信息。而且在语言检测方面提供丰富的工具。在文案按需加载方面,也提供灵活的API,稍作修改,即可兼容json、js的多种分发方式。lingui.js 是个非常值得上手的React前端国际化工作流方案。 各方案简单对比