Webnovel 国际化实践

avatar
前端工程师 @上海阅文信息技术有限公司

本文作者:张卓

原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 ( id: yuewen_YFE ) 获取授权,并注明作者、出处和链接。

前言

Webnovel(起点海外项目)在今年开始了国际化的脚步,在刚刚上线的版本当中加入了对印尼、马来西亚和菲律宾语言及内容的支持。在做国际化的过程中,我们遇到了不少问题,这篇文章就重点分享一下这些问题以及它们的解决方案。

国际化和本地化

在开始之前,我们先明确两个概念: 国际化和本地化。国际化(i18n) 是一个设计和准备应用程序的过程,使其能用于不同的语言。 而 本地化(l10n) 是一个把国际化的应用针对部分区域翻译成特定语言的过程。这篇文章的标题是“国际化”实践,所以重点讲的也是如何准备应用程序让其能够进行本地化。

需要解决的问题

将一个网站进行多语言化看似是一件简单的事情,在大多数情况下的确是的,只是将一个字符串映射到另一个字符串的过程,但是起点海外作为一款有追求的产品,我们当然不会采用这么简单的方式。要把国际化这件事情做好,那就会遇到许多问题,例如单复数、富文本等。下面的部分会介绍一些常见的多语言问题以及一些通用的解决方案:

单复数问题

在我们中文当中,没有单复数的概念,“一小时”和“两小时”中的“小时”是一样的,但在其它许多语言当中,不同数量的形式,有着不同的规则,英文中有单数和复数两种规则,如“1 hour” 和 ”2 hours”,而有的语言可能有更多。在一些语言中,基数和序数的规则可能也是不同的,如英文的“1st”,“2nd”,“3rd”,“4th”。

Unicode 标准已经将世界上绝大部分语言的单复数规则进行了归类,总结下来最多只有 6 种规则,分别是:

  • zero
  • one
  • two
  • few
  • many (如果有一个单独的分类的话,也用于分数)
  • other (必须,如果语言只有一个单一形式也使用)

阿拉伯语基数的单复数规则

如英文中,基数只有 one (1 hour)和 other (0 hours,2.5 hours)两种规则,序数则有 one(1st,11st…),two(2nd),few(3rd),和 other(4th)四种规则。

规则是有了,我们如何在实际的应用中使用呢?一种比较通用的方式是使用 ICU MessageFormat。ICU MessageFormat 是一种语法格式,通过 {key} 的形式来定义变量;通过一些关键词来帮助我们更方便的处理不同语言中的一些复杂情况,例如使用 {key, plural, matches}来处理单复数规则:

You have {itemCount, plural,
    =0 {no items}
    one {1 item}
    other {{itemCount} items}
}.

ICU MessageFormat 不仅可以方便单复数情况的使用,同样可以用于日期、性别以及其它复杂情况,并且已经拥有了非常广泛的使用,不仅绝大部分 JavaScript 的多语言库使用了它,在其他语言例如 Java 和 PHP 中也同样内置了这套规则。

日期、数字以及货币 不同语言、国家和地区在表示日期和数字时也会有一些差异,如美国习惯“月/日/年”的形式来表示日期,而同样使用英语的英国却更习惯“日/月/年”。而货币就更不用说了,符号首先不同,如人民币和日元的 “¥” 以及欧元的 “€”,并且它们放置的位置可能也不相同,日元习惯将货币符号放在数字前面,而欧元恰恰相反。

作为开发者,我们几乎不可能去一一了解这些差异,好在 ECMAScript Internationalization API 提供了4个方法帮助我们解决上面的问题:

  • Intl.Collator
  • Intl.DateTimeFormat
  • Intl.NumberFormat
  • Intl.PluralRules

Intl.Collator 并不常用,主要是用于语言敏感字符串比较的;Intl.DateTimeFormat 可以帮助我们根据不同地区语言格式化时间和日期;Intl.NumberFormat 则用来格式化数字和货币;而Intl.PluralRules 用于判断单复数,可以告诉我们指定数量在某种语言下的分类(如 “one”,“other”)。其中前三个 API 已经相对稳定,浏览器也有较好的支持,DateTimeFormatNumberFormat 也有 polyfill 来让我们有更广泛的使用范围,如 Node 和 React Native 环境中;PluralRules 还处在草案阶段,浏览器支持性也比较差,不建议直接使用。

另外,我们看到浏览器尤其是 Chrome 对国际化的支持正在逐渐加大,除了上面已经进入标准的 4 个 API 外,Chrome 分别在 71 和 72 版本支持了 Intl.RelativeTimeFormatIntl.ListFormat 两个 API,其中 Intl.RelativeTimeFormat 用来格式化相对时间,类似 Moment.js 中的功能,例如:

const rtf = new Intl.RelativeTimeFormat('en');
    
rtf.format(3.14, 'second');
// → 'in 3.14 seconds'
    
rtf.format(-15, 'minute');
// → '15 minutes ago'

Intl.ListFormat 用来格式化列表,例如:

const lf = new Intl.ListFormat('zh');
    
lf.format(['永锋', '新宇']);
// → '永锋和新宇'

这些 API 都远比上面展示的例子强大,具体的用法可以参考 MDN 和 Google Developers 官网,也相信今后在 Web 上进行国际化会越来越容易。

含义和语境

我们知道,不论是中文还是其它语言,一个字/词语在不同场景下可能会有不同的含义,“About” 如果当作一个页面的标题,表达的可能是“关于/简介”的含义,但放在一句话中就可能是“大约”的意思了。

对于这个问题,我们可以通过提供给译者更多的信息来解决这个问题。可通过文字描述帮助译者来了解语境,发送截图等方式来确保译者能够准确的翻译。

谁来翻译

谁来翻译,看起来似乎不是一个问题,但它决定着我们整个翻译的流程,我们需要在进行多语言时尽早确定。一般来说,可能是由专业翻译或者用户/志愿者来翻译,这两种方式各有优劣:

  • 由专业翻译进行翻译。专业翻译基本可以保证较高的质量和效率,主要问题是成本较高。
  • 由用户/志愿者进行翻译。很多开源项目都采用来这种方式。Twitter 也采用了这种方式,专门搭建了一个翻译平台来让用户更好的进行翻译,在短短一年的时间内有超过 40 万的志愿者帮助其进行了翻译,上线了 21 种语言。这种方式成本低,并且由于可能参与的人数众多,通过多人 review 等方式可以保证翻译质量;唯一需要担心的是翻译时间不可控。而我们一般也不需要自己搭建一套平台,可以选择已有的成熟的商业平台如 crowdin.com 或者开源的平台如 Mozilla 的 pontoon。

除了上面提到的多语言问题,我们在国际化的过程中可能还要面临多方合作、分国际/地区运营等其它类型的诸多问题,这里篇幅有限,就不一一讨论了。

解决方案

在做多语言的 Web 应用时,一种比较通用的方式是:将不同语言的字符放在不同的 JSON 或其它形式的文件当中,然后获取用户倾向的语言,加载对应语言的字符文件,然后在应用中展示即可。

在这种方式下,需要解决的最大问题是一些比较复杂的情况,例如上文提到的单复数。在上文种我们也提到了可以通过 ICU MessageFormat 来解决这个问题,具体的做法是将 ICU MessageFormat 解析成 AST 然后转化为函数,在应用中传递对应参数到对应函数即可。

上面的方式还有一些细节值得讨论,篇幅原因这里就不讲了,接下来我们看一下相对比较成熟的基于主流框架的 i18n 解决方案。

React Intl

React Intl 是雅虎开源的基于 React 的国际化解决方案。遵循 BCP 47 和 Unicode CLDR 标准,支持 ICU Message Format,并支持日期、时间和数字等的国际化。

React Intl 通过组件的形式实现多语言:

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

更具体的使用方式可以参考它的 Github:github.com/yahoo/react…

Angular 的方案

Angular 应该是目前主流前端框架中唯一自带 i18n 解决方案的框架,它同样遵循 BCP 47 和 Unicode CLDR 标准,支持 ICU Message Format。

与一般的 i18n 方案不同,Angular 不需要提前准备一份 JSON 或其它形式的多语言映射表,只需使用 i18n 属性来标记需要进行多语言的文本即可,例如:

<h1 i18n>Hello, webnovel</h1>

通过执行 ng xi18n 命令,Angular 会自动提取所有含义 i18n 属性的字符,并生成一份 xlf 文件(xlf 是一种基于XML的交换格式,旨在标准化本地化过程中在工具之间传递可本地化数据的方式),我们可以直接将 xlf 文件发送给译者,译者通过一些专门的软件进行翻译然后将这份文件返回给我们。最后,我们通过预编译或者即时编译的方式将多语言内容注入到应用当中即可完成全部工作。

Angular 的方案非常完善,对我们可能不太注意的一些地方也做了支持,如我们想对 img 标签的 title 属性进行多语言的话, 只需再加上 i18n-title 的属性即可,例如: <img [src]="logo" i18n-title title="Webnovel logo" />

Webnovel 的方案

Webnovel 的多语言版本

对比了上述的几种方案,我们认为目前 Angular 的方案是最为理想。它相对完善;没有很强的入侵性;得益于它能自动提取所需的字符串并生成 ID,使用它的便利程度也优于其他框架;它的多语言支持预编译和即时编译,在性能和灵活度上都有了保证。

但比较遗憾的是 Webnovel 目前没有使用 Angular,我们的移动站点和 App 分别基于 React 和 React Native 构建,由于已有的基于 react 的多语言库 react-intl 不能很好的满足我们的需要,我们决定自己构建 i18n 的基础库,它需要做到:

  • 能够在常用 JavaScript 环境中使用,包括浏览器、 Node(服务端渲染)和 React Native
  • 遵循国际标准,支持 ICU MessageFormat
  • 支持字符模版的预编译和即时编译
  • 高性能,支持动态加载语言
  • 支持语言/字符串的 fallback
  • 使用成本低
  • 插件化

在有了基本目标以后,我们开发了新的多语言库 react-i18n:

@react-i18n/core

核心库,通过 React 的 Context API,提供 withI18n 的高阶组件以及 Message 组件来帮助应用进行多语言。其中 withI18n 将 i18n 信息传递给组件的 props,而 Message 组件类似于 React Intl 中的 FormattedMessage 组件,通过传递对应字符串的 id 和参数来直接渲染出字符。

@react-i18n/cli

命令行工具,主要提供预编译字符模版和一些辅助功能,如

  • Excel 处理:将 Excel 转化为 JSON。我们和翻译是通过在线的 Excel 来进行信息同步。
  • 自动生成 ID:如 “hello world” 将生成 HELLO_WORLD_6f5902ac237024bdd0c176cb93063dc4 的 ID,既保证了在使用编辑器是能够通过自动补全方便输入,也保证了唯一性。
  • 机器翻译:通过使用 Microsoft Translator Text API 和 Google Translation API 将机器翻译的结果直接填入表格,帮助译者更快的进行翻译。

react-i18n 库已经满足了我们的基本要求,并且已经在 Webnovel 的移动站点和 App 中运行了一段时间。由于时间仓促, react-i18n 库还不够健全,我们暂时还不能将其开源。接下来,我们会将这个库进行完善,尽早回馈给开源社区。

其他需要注意的问题

遵循已有标准

遵循已有标准对于国际化来说非常重要。我们在写一个应用时,几乎无法避免与第三方合作,例如支付。在合作的过程中,我们如果使用相同的标准,那沟通、调试的成本将大大降低。关于国际化的标准非常多,有些也比较复杂,需要我们耐心认真的阅读。值得注意的是,标准并不是一直不变的,例如上文中提到的 BCP 47 标准当中的地区标示,旧版中印尼的代码是 in ,而新版中改为了 id,一般我们应该遵循更新的标准。

更早、更多的了解成熟方案

对于国际化这样已经存在非常多年的问题,一定是有成熟方案可以借鉴的,在开始之前,去尽可能多的了解已有方案,会让我们少走许多弯路。

结语

本文重点讲述了基于 Web 技术的一些国际化方案以及在 Webnovel 中的实际应用。国际化一直以来都是一个非常困难的问题,所以更需要我们长远的去思考;国际化也不仅仅是多语言,从排版布局到文化差异,都是我们需要考虑的,Webnovel 的国际化才刚刚开始。

相关链接

查看更多分享,请关注阅文集团前端团队公众号: