前端国际化与本地化
背景
在信息技术领域,国际化与本地化(internationalization and localization)是指修改软件使之能使用目标市场的的语言、地区差异以及技术需要;
具体来说,国际化是在设计软件,将软件与特定语言及地区脱钩的进程,当软件被移植到不同的语言及地区时,软件内部不用改变或修正;本地化则是当移植软件时,加上与特定区域设置有关的信息和翻译文件的进程;
国际化工作概述
软件国际化与本地化的实现主要涉及以下部分:
- 前端国际化
- 服务端国际化
- 国际化资源文件管理
- 项目开发者与翻译者之间的协作
本次国际化方案将只针对前端国际化展开叙述,并采用 React UI 框架;
技术方案
目前,主流的国际化解决方案有基于 GNU gettext 的软件包以及基于 CLDR 标准的 ICU 函数库;
GNU gettext
GNU gettext 是 GNU 国际化与本地化函数库,常被用于编写多语言程序,node-gettext 是 gettext 在 JavaScript 语言中的实现;
CLDR 标准
Unicode CLDR 为软件提供了支持世界语言的关键构建模块,提供了最大,最广泛的语言环境数据库。
这些数据被广泛的公司用于其软件国际化和本地化,使软件适应不同语言的惯例以用于此类常见软件任务。
ICU 函数库
ICU 有一套自定义的国际化语法规范,不同的语言有各自的类库实现,在 JavaScript 中有 messageformat;
方案评估
本次国际化候选输出方案有两套,react-intl 和 node-gettext + react-gettext-parser + narp(可选);
react-intl
react-intl 是 yahoo 推出的基于 FormatJS 的 react 应用的国际化方案,FormatJS 的核心库是 Intl MessageFormat,遵循的是上述的 ICU 语法规范;
其基本原理是维护几份不同语言包的映射表,然后通过设置当前应用的语言动态的选择不同的语言包,应用内部组件根据语言包的映射表的 id 找到对应的特定语言版本词条,从而实现国际化,具体实现流程可参考:react-intl 实现 React 国际化多语言;
react-intl 的方案优点在于:
- 提供了国际化转换的整套方案,支持字符串、日期、时间、货币和量词等;
- 对 ICU 语法规范的良好遵循;
- 流程清晰,基础设施搭建较为简单,与 react 框架的良好结合;
其缺点在于:
- 每一条翻译词条的 id 没有与其一一绑定,并且需要开发者手动定义和维护,如果出现相同词条需要定义不同 id 从而出现词条冗余问题;
- 每一个需要待翻译的词条均需要引入 react-intl 的类型组件并传递对应的数据,代码编写量较大和可读性较差;
- 生成的翻译文件其格式为 js 或是 json,并且未能很好的整合社区的现有的翻译工具,所以对于翻译者的翻译工作可能带来一定的困难和低效;
node-gettext + react-gettext-parser + narp
本套方案是沿革至 GNU gettext 的翻译工作流,结合上述类库,其实现步骤如下:
1. 使用 node-gettext 库提供的相关方法,对源码进行翻译标记
这里为了减少代码量,对 gettext 进行封装为 "_" 等形式;
// src/pages/Index/index.tsx
import * as React from 'react';
import {_, _p} from 'utils/gettext';
export default class Index extends React {
render() {
return (
<div className='index'>
<h3>
{_('你好,悦跑圈')}
</h3>
<p>
{_p('一个苹果', '%d 个苹果', 4)}
</p>
<p>
{_('姓名:%s', 'teren')}
</p>
</div>
)
}
}
由于 node-gettext 不支持插值,所以结合 sprintf-js 实现插值输入功能;
// src/utils/gettext.ts
import Gettext from 'node-gettext';
import {sprintf, vsprintf} from 'sprintf-js';
const gt: Gettext = new Gettext();
export function _(msgid: string, value?: IValue): string {
const str = gt.gettext(msgid);
return (
value
? value instanceof Array
? vsprintf(str, value)
: sprintf(str, value)
: str
);
}
2. 使用 react-gettext-parser 工具库提取源码中的标记信息,生成 pot (portable object template)文件
$ react-gettext-parser --output messages.pot 'src/**/{*.js,*.jsx,*.ts,*.tsx}' '!src/test.js'
提取出来的 pot 文件如下:
#: src/pages/Index/index.tsx:26
msgid "姓名"
msgstr ""
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: POEditor.com\n"
"Project-Id-Version: joyrun-match-enrollment\n"
"Language: zh-CN\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: src/pages/Index/index.tsx:23
msgid "一个苹果"
msgid_plural "%d 个苹果"
msgstr[0] ""
#: src/pages/Index/index.tsx:20
msgid "你好,悦跑圈"
msgstr ""
#: src/pages/Index/index.tsx:26
msgid "姓名:%s"
msgstr ""
3. 将 pot 文件交由翻译者,翻译者使用翻译工具(如 poedit 等)将 pot 文件导入后逐条翻译
将翻译后的文件保存为对应的 po 文件,如 en.po 和 zh_Hant.po,再调用 gettext-parser 将其转换为对应的 json 文件;
const fs = require('fs');
const input = fs.readFileSync('en.po');
const po = gettextParser.po.parse(input);
fs.writeFileSync('en.json', po);
[注意] 对于后续新增的翻译词条,需要使用 pot-merge 将 pot 文件进行合并操作
$ node pot-merge.js -a message.pot -b en.po -o en.po
4. 将翻译好的语言包引入代码中
import Gettext from 'node-gettext'
import enTrans from './en.json'
const gt = new Gettext()
gt.addTranslations('en', 'messages', enTrans)
gt.setLocale('en')
gt.gettext('你好,悦跑圈')
// -> "Hello, Joyrun"
上述的第 2 和 第 3 步骤如果没有一套工具链配合,实际操作起来相对繁琐,所幸社区提供一个工作流工具 narp 简化工作流;
narp 提供 push 和 pull 两个命令;
push 操作先通过 react-gettext-parser 从源码中提取待翻译的字符串,形成中间 pot 文件,然后通过 pot-merge 合并从上游翻译服务器的和本地的翻译文件,最后将合并后的新的 pot 文件上传至翻译服务器(Transifex or POEditor);
pull 操作则是从上游翻译服务器下载翻译好的 po 文件,然后通过 gettext-parser 将 po 文件转换为 json 文件并写入磁盘;
POEditor POEditornode-gettext + narp 的方案的优势在于:
-
翻译字符串标注简洁,不似 react-intl 方案繁琐,对于代码的可读性和可维护性更佳;
-
整套流程相对自动化,配合第三方翻译服务商,能够较好的实现开发与翻译工作的解耦,并且随着日后国际化的业务规模增加,其优势将进一步体现;
-
本套方案生成的翻译文件格式是较为通用的 pot 文件,能够被主流的翻译工具(Poedit) 所支持,社区工具链较为丰富,能够对其进行进一步处理;
其劣势在于:
-
整套流程的搭建工作相对较为复杂,所涉及的工具链较多,基础设施搭建工作具备一定复杂度;
-
本套方案采用了第三方翻译服务商,当前使用的是免费版,词条额度为 1000 条,因此当额度超标后需要产生一定的资费;
-
node-gettext 仅提供字符串的转换,数字和时间需要自行使用第三方库实现;
[注意] 关于 po、pot 和 mo 文件的区别详见此文
结合本次赛事报名项目来看,本次项目采用 node-gettext + narp,原因是考虑其代码的良好维护性和简洁性、以及国际化工作的协作性。
其他
翻译词条