前端國(guó)際化與本地化

背景

在信息技術(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 文件并寫入磁盤;

POEditor
POEditor

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é)作性怠堪。

其他

翻譯詞條

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末揽乱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子粟矿,更是在濱河造成了極大的恐慌凰棉,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嚷炉,死亡現(xiàn)場(chǎng)離奇詭異渊啰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)申屹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門绘证,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人哗讥,你說我怎么就攤上這事嚷那。” “怎么了杆煞?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵魏宽,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我决乎,道長(zhǎng)队询,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任构诚,我火速辦了婚禮蚌斩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘范嘱。我一直安慰自己送膳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布丑蛤。 她就那樣靜靜地躺著叠聋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪受裹。 梳的紋絲不亂的頭發(fā)上碌补,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音棉饶,去河邊找鬼脑慧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛砰盐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播坑律,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼岩梳,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼囊骤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起冀值,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤也物,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后列疗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滑蚯,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年抵栈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了告材。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡古劲,死狀恐怖斥赋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情产艾,我是刑警寧澤疤剑,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站闷堡,受9級(jí)特大地震影響隘膘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杠览,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一弯菊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧倦零,春花似錦误续、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至葫隙,卻和暖如春栽烂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背恋脚。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工腺办, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人糟描。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓怀喉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親船响。 傳聞我的和親對(duì)象是個(gè)殘疾皇子躬拢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容