前端国际化与本地化

背景

在信息技术领域,国际化与本地化(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
POEditor

node-gettext + narp 的方案的优势在于:

  • 翻译字符串标注简洁,不似 react-intl 方案繁琐,对于代码的可读性和可维护性更佳;

  • 整套流程相对自动化,配合第三方翻译服务商,能够较好的实现开发与翻译工作的解耦,并且随着日后国际化的业务规模增加,其优势将进一步体现;

  • 本套方案生成的翻译文件格式是较为通用的 pot 文件,能够被主流的翻译工具(Poedit) 所支持,社区工具链较为丰富,能够对其进行进一步处理;

其劣势在于:

  • 整套流程的搭建工作相对较为复杂,所涉及的工具链较多,基础设施搭建工作具备一定复杂度;

  • 本套方案采用了第三方翻译服务商,当前使用的是免费版,词条额度为 1000 条,因此当额度超标后需要产生一定的资费;

  • node-gettext 仅提供字符串的转换,数字和时间需要自行使用第三方库实现;

[注意] 关于 po、pot 和 mo 文件的区别详见此文

结合本次赛事报名项目来看,本次项目采用 node-gettext + narp,原因是考虑其代码的良好维护性和简洁性、以及国际化工作的协作性。

其他

翻译词条

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容