背景
在信息技術(shù)領(lǐng)域,國(guó)際化與本地化(internationalization and localization)是指修改軟件使之能使用目標(biāo)市場(chǎng)的的語言蹬竖、地區(qū)差異以及技術(shù)需要;
具體來說滥嘴,國(guó)際化是在設(shè)計(jì)軟件审胚,將軟件與特定語言及地區(qū)脫鉤的進(jìn)程,當(dāng)軟件被移植到不同的語言及地區(qū)時(shí)监憎,軟件內(nèi)部不用改變或修正;本地化則是當(dāng)移植軟件時(shí)婶溯,加上與特定區(qū)域設(shè)置有關(guān)的信息和翻譯文件的進(jìn)程鲸阔;
國(guó)際化工作概述
軟件國(guó)際化與本地化的實(shí)現(xiàn)主要涉及以下部分:
- 前端國(guó)際化
- 服務(wù)端國(guó)際化
- 國(guó)際化資源文件管理
- 項(xiàng)目開發(fā)者與翻譯者之間的協(xié)作
本次國(guó)際化方案將只針對(duì)前端國(guó)際化展開敘述,并采用 React UI 框架迄委;
技術(shù)方案
目前褐筛,主流的國(guó)際化解決方案有基于 GNU gettext 的軟件包以及基于 CLDR 標(biāo)準(zhǔn)的 ICU 函數(shù)庫;
GNU gettext
GNU gettext 是 GNU 國(guó)際化與本地化函數(shù)庫叙身,常被用于編寫多語言程序渔扎,node-gettext 是 gettext 在 JavaScript 語言中的實(shí)現(xiàn);
CLDR 標(biāo)準(zhǔn)
Unicode CLDR 為軟件提供了支持世界語言的關(guān)鍵構(gòu)建模塊信轿,提供了最大晃痴,最廣泛的語言環(huán)境數(shù)據(jù)庫。
這些數(shù)據(jù)被廣泛的公司用于其軟件國(guó)際化和本地化财忽,使軟件適應(yīng)不同語言的慣例以用于此類常見軟件任務(wù)倘核。
ICU 函數(shù)庫
ICU 有一套自定義的國(guó)際化語法規(guī)范,不同的語言有各自的類庫實(shí)現(xiàn)定罢,在 JavaScript 中有 messageformat笤虫;
方案評(píng)估
本次國(guó)際化候選輸出方案有兩套,react-intl 和 node-gettext + react-gettext-parser + narp(可選);
react-intl
react-intl 是 yahoo 推出的基于 FormatJS 的 react 應(yīng)用的國(guó)際化方案琼蚯,F(xiàn)ormatJS 的核心庫是 Intl MessageFormat酬凳,遵循的是上述的 ICU 語法規(guī)范;
其基本原理是維護(hù)幾份不同語言包的映射表遭庶,然后通過設(shè)置當(dāng)前應(yīng)用的語言動(dòng)態(tài)的選擇不同的語言包宁仔,應(yīng)用內(nèi)部組件根據(jù)語言包的映射表的 id 找到對(duì)應(yīng)的特定語言版本詞條,從而實(shí)現(xiàn)國(guó)際化峦睡,具體實(shí)現(xiàn)流程可參考:react-intl 實(shí)現(xiàn) React 國(guó)際化多語言翎苫;
react-intl 的方案優(yōu)點(diǎn)在于:
- 提供了國(guó)際化轉(zhuǎn)換的整套方案,支持字符串榨了、日期煎谍、時(shí)間、貨幣和量詞等龙屉;
- 對(duì) ICU 語法規(guī)范的良好遵循呐粘;
- 流程清晰,基礎(chǔ)設(shè)施搭建較為簡(jiǎn)單转捕,與 react 框架的良好結(jié)合作岖;
其缺點(diǎn)在于:
- 每一條翻譯詞條的 id 沒有與其一一綁定,并且需要開發(fā)者手動(dòng)定義和維護(hù)五芝,如果出現(xiàn)相同詞條需要定義不同 id 從而出現(xiàn)詞條冗余問題痘儡;
- 每一個(gè)需要待翻譯的詞條均需要引入 react-intl 的類型組件并傳遞對(duì)應(yīng)的數(shù)據(jù),代碼編寫量較大和可讀性較差枢步;
- 生成的翻譯文件其格式為 js 或是 json沉删,并且未能很好的整合社區(qū)的現(xiàn)有的翻譯工具,所以對(duì)于翻譯者的翻譯工作可能帶來一定的困難和低效醉途;
node-gettext + react-gettext-parser + narp
本套方案是沿革至 GNU gettext 的翻譯工作流丑念,結(jié)合上述類庫,其實(shí)現(xiàn)步驟如下:
1. 使用 node-gettext 庫提供的相關(guān)方法结蟋,對(duì)源碼進(jìn)行翻譯標(biāo)記
這里為了減少代碼量,對(duì) gettext 進(jìn)行封裝為 "_" 等形式渔彰;
// 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('一個(gè)蘋果', '%d 個(gè)蘋果', 4)}
</p>
<p>
{_('姓名:%s', 'teren')}
</p>
</div>
)
}
}
由于 node-gettext 不支持插值,所以結(jié)合 sprintf-js 實(shí)現(xiàn)插值輸入功能恍涂;
// 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 工具庫提取源碼中的標(biāo)記信息宝惰,生成 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 "一個(gè)蘋果"
msgid_plural "%d 個(gè)蘋果"
msgstr[0] ""
#: src/pages/Index/index.tsx:20
msgid "你好,悅跑圈"
msgstr ""
#: src/pages/Index/index.tsx:26
msgid "姓名:%s"
msgstr ""
3. 將 pot 文件交由翻譯者再沧,翻譯者使用翻譯工具(如 poedit 等)將 pot 文件導(dǎo)入后逐條翻譯
將翻譯后的文件保存為對(duì)應(yīng)的 po 文件尼夺,如 en.po 和 zh_Hant.po,再調(diào)用 gettext-parser 將其轉(zhuǎn)換為對(duì)應(yīng)的 json 文件;
const fs = require('fs');
const input = fs.readFileSync('en.po');
const po = gettextParser.po.parse(input);
fs.writeFileSync('en.json', po);
[注意] 對(duì)于后續(xù)新增的翻譯詞條淤堵,需要使用 pot-merge 將 pot 文件進(jìn)行合并操作
$ 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 步驟如果沒有一套工具鏈配合,實(shí)際操作起來相對(duì)繁瑣拐邪,所幸社區(qū)提供一個(gè)工作流工具 narp 簡(jiǎn)化工作流慰毅;
narp 提供 push 和 pull 兩個(gè)命令;
push 操作先通過 react-gettext-parser 從源碼中提取待翻譯的字符串扎阶,形成中間 pot 文件汹胃,然后通過 pot-merge 合并從上游翻譯服務(wù)器的和本地的翻譯文件,最后將合并后的新的 pot 文件上傳至翻譯服務(wù)器(Transifex or POEditor)东臀;
pull 操作則是從上游翻譯服務(wù)器下載翻譯好的 po 文件着饥,然后通過 gettext-parser 將 po 文件轉(zhuǎn)換為 json 文件并寫入磁盤;
node-gettext + narp 的方案的優(yōu)勢(shì)在于:
翻譯字符串標(biāo)注簡(jiǎn)潔惰赋,不似 react-intl 方案繁瑣宰掉,對(duì)于代碼的可讀性和可維護(hù)性更佳;
整套流程相對(duì)自動(dòng)化谤逼,配合第三方翻譯服務(wù)商贵扰,能夠較好的實(shí)現(xiàn)開發(fā)與翻譯工作的解耦,并且隨著日后國(guó)際化的業(yè)務(wù)規(guī)模增加流部,其優(yōu)勢(shì)將進(jìn)一步體現(xiàn)戚绕;
本套方案生成的翻譯文件格式是較為通用的 pot 文件,能夠被主流的翻譯工具(Poedit) 所支持枝冀,社區(qū)工具鏈較為豐富舞丛,能夠?qū)ζ溥M(jìn)行進(jìn)一步處理;
其劣勢(shì)在于:
整套流程的搭建工作相對(duì)較為復(fù)雜果漾,所涉及的工具鏈較多球切,基礎(chǔ)設(shè)施搭建工作具備一定復(fù)雜度;
本套方案采用了第三方翻譯服務(wù)商绒障,當(dāng)前使用的是免費(fèi)版吨凑,詞條額度為 1000 條,因此當(dāng)額度超標(biāo)后需要產(chǎn)生一定的資費(fèi)户辱;
node-gettext 僅提供字符串的轉(zhuǎn)換鸵钝,數(shù)字和時(shí)間需要自行使用第三方庫實(shí)現(xiàn);
[注意] 關(guān)于 po庐镐、pot 和 mo 文件的區(qū)別詳見此文
結(jié)合本次賽事報(bào)名項(xiàng)目來看恩商,本次項(xiàng)目采用 node-gettext + narp,原因是考慮其代碼的良好維護(hù)性和簡(jiǎn)潔性必逆、以及國(guó)際化工作的協(xié)作性怠堪。
其他
翻譯詞條