基于webpack & gettext 的前端多語言方案

gettext 介紹

gettext 是GNU 提供的一套國際化與本地化處理的相關(guān)函數(shù)庫麦锯。大多數(shù)語言都有對應(yīng)的gettext實現(xiàn)收恢。本文主要使用jed 來實現(xiàn)gettext 一系列方法對應(yīng)的功能。

pot/po文件

  • pot文件 是po文件的模板文件脖镀,一般是通過 xgettext 程序生成出來的瓦堵。
  • po文件 是根據(jù)pot文件通過msginit程序,設(shè)置對應(yīng)的國家語言生成用于填寫實際翻譯內(nèi)容的文件刷袍。

xgettext/msginit/msgmerge

  • xgettext 程序可以掃描指定的代碼文件,取出其中g(shù)ettext部分的內(nèi)容生成對應(yīng)的pot文件樊展。
  • msginit 根據(jù)對應(yīng)的pot文件生成對應(yīng)語言版本用于實際翻譯的po文件呻纹。
  • msgmerge 如果對應(yīng)語言版本的po文件存在的話,則需要使用msgmerge方式把pot文件中新加入的一些msgid合并到po文件當(dāng)中专缠。

多語言支持流程

安裝gettext

brew install gettext
brew link gettext

langs-loader 加載語言文件

  • 安裝

npm install git@github.com:ezbuy/langs-loader.git --save-dev

  • 配置

需要修改webpack.config.js文件在module->rules(webpack 2.0)下面添加loader規(guī)則:

{
    test: /\.pot$/i,
    use: [
        "json",
        {
            loader: 'langs',
            options: {isLoadAll: isDev,format:"jed1.x", "fallback-to-msgid":true, code:langCode}
        }
    ]
}
  • isLoadAll

isLoadAll表示會把所有語言版本的文件通過po2json轉(zhuǎn)換成json雷酪,然后組合成一個json作為數(shù)據(jù)傳給引用的地方。

  • code

code選項一般需要在代碼發(fā)布時指定涝婉,不同語言版本打包時通過傳入不同的code區(qū)分不同語言版本的代碼打包哥力。

代碼中使用gettext系列函數(shù)

各端開發(fā)時使用各自實現(xiàn)的gettext方法去包裝需要實現(xiàn)多語言的文案。使用gettext函數(shù)包裝后墩弯,langs-util就能知道代碼中有需要多語言翻譯的地方吩跋,并可以通過langs-util生成相關(guān)的pot及po文件。gettext方法底層我們使用jed 來實現(xiàn)其對應(yīng)功能渔工。


import toi18n from "common/i18n";

const _ = toi18n(require("langs/index.pot"));

const hiStr = _.gettext("Hi!");

const num = Math.ceil(Math.random() * 2);

const numStr = _.ngettext("item", "items", num);

// I like your red shirt.
console.log(_.sprintf( "I like your %1$s %2$s.", 'red', 'shirt'));

  • gettext

一般使用 gettext 方法包裝一個需要翻譯的字符串锌钮。

  • ngettext

可以用于單復(fù)數(shù)處理,第一個參數(shù)傳入單數(shù)情況下的翻譯引矩,第二個參數(shù)傳入復(fù)數(shù)情況下的翻譯梁丘,第三個傳入實際需要執(zhí)行判斷的數(shù)據(jù)。

  • sprintf

Jed內(nèi)置提供了sprintf的支持旺韭,它是使用javascript-sprintf的sprintf函數(shù)的實現(xiàn)氛谜。

通過gettext 一系列函數(shù)的配合,使用langs-util進(jìn)行生成處理后就能生成對應(yīng)的pot及po文件区端。


langs-util gen -i src/


# langs/index.pot

msgid "Hi!"
msgstr ""

msgid "item"
msgid_plural "items"
msgstr[0] ""
msgstr[1] ""


# langs/index.th.po

msgid "Hi"
msgstr ""

msgid "item"
msgid_plural "items"
msgstr[0] ""
msgstr[1] ""

把生成的文件交由對應(yīng)的翻譯人員完成翻譯就可以重新放回到文件夾當(dāng)中替換目標(biāo)po文件值漫。

打包&發(fā)布

各端打包方式都會有相應(yīng)的差異,最終目的無非就是使線上代碼可以按照不同語言的版本加載不同的語言文件织盼。對于后臺等系統(tǒng)則可以使用isLoadAll的方式把所有語言版本的文件都加載進(jìn)來杨何,通過i18n的包裝函數(shù)去從數(shù)據(jù)源中拿出不同國家的多語言數(shù)據(jù)文件。


cross-env LANG_CODE=th

移動端發(fā)布使用環(huán)境變量LANG_CODE來區(qū)分要編譯成的語言版本悔政。

const langCode = process.env.LANG_CODE

{
    test: /\.pot$/i,
    use: [
        "json",
        {
            loader: 'langs',
            options: {isLoadAll: isDev,format:"jed1.x", "fallback-to-msgid":true, code:langCode}
        }
    ]
}

生成完所有語言語言版本的靜態(tài)文件后晚吞,需要讓瀏覽器能夠運(yùn)行不同語言版本的js文件。一種方式可以通過后臺程序讀出語言版本谋国,再把正確語言版本的js路徑輸出到html當(dāng)中槽地。另外一種方式可以新建一個多語言loader的文件,通過前端js代碼動態(tài)插入正確語言版本的js代碼(文件打包的時候需要把各語言版本的js文件路徑輸出到頁面之中)。


import {languageCode} from "common/constant";

const loadScript = (path: string) => {
    const script = window.document.createElement("script");
    script.setAttribute("src", path);
    window.document.body.appendChild(script);
    return new Promise((resolve, reject) => {
        script.onload = resolve;
        script.onerror = reject;
    });
};

const {mainFilePath} = window;

if (typeof mainFilePath === "string") {
    loadScript(mainFilePath);
}else if (typeof mainFilePath !== "string") {
    loadScript(mainFilePath[languageCode] );
}

langs-loader 實現(xiàn)

在loader代碼中拿到require文件的實際路徑捌蚊,如果存在code選項的話則根據(jù)pot文件找到對應(yīng)code的po文件集畅,然后使用po2json轉(zhuǎn)換為對應(yīng)格式的json文件,最后再返回給引用的地方缅糟。如果存在isLoadAll選項并且isLoadAll選項為true的話挺智,則會load 同名pot文件的所有對應(yīng)語言版本的po文件,然后再通過po2json轉(zhuǎn)換組合成一個json文件窗宦,再返回出去赦颇。

langs-util 實現(xiàn)

langs-util主要整合了xgettext/msginit/msgmerge等方法。傳入所需要解析的文件或文件夾赴涵,生成對應(yīng)的po及pot文件媒怯。


const genPoFiles = (inputFilePaths: string | string[], potFilePath: string, langs: string[]) => {
    const potDirName = dirname(potFilePath);
    const filename = basename(potFilePath, extname(potFilePath));
    const filePaths = typeof inputFilePaths === "string" ? inputFilePaths : inputFilePaths.join(" ");
    execOnlyErrorOutput(`xgettext --language=JavaScript --add-comments --sort-output --from-code=UTF-8 --no-location --msgid-bugs-address=wanglin@ezbuy.com -o ${potFilePath} ${filePaths}`);

    checkFileExists(potFilePath).then((ifPotFileExists) => {
        if (ifPotFileExists) {
            console.log(green(potFilePath));
            writeFileSync(potFilePath, readFileSync(potFilePath).toString().replace("charset=CHARSET", "charset=UTF-8"));
            langs.forEach((lang) => {
                const poFilePath = join(potDirName, `${filename}${lang === "" ? "" : `.${lang}` }.po`);
                checkFileExists(poFilePath).then((ifExists) => {
                    if (ifExists) {
                        execOnlyErrorOutput(`msgmerge --output-file=${poFilePath} ${poFilePath} ${potFilePath}`);
                    }else {
                        execOnlyErrorOutput(`msginit --no-translator --input=${potFilePath} --locale=${lang} --output=${poFilePath}`);
                    }
                });
            });
        }
    });
};

生成po文件時,需要傳入需要給xgettext解析的文件列表以及目標(biāo)pot文件路徑還有生成的多語言版本種類髓窜。xgettext會根據(jù)傳入的解析文件生成新的pot文件扇苞,如果生成成功則開始生成對應(yīng)語言版本的po文件,如果對應(yīng)的po文件存在則使用msgmerge更新po文件寄纵,如果不存在則使用msginit創(chuàng)建新的po文件鳖敷。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市程拭,隨后出現(xiàn)的幾起案子定踱,更是在濱河造成了極大的恐慌,老刑警劉巖哺壶,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件屋吨,死亡現(xiàn)場離奇詭異,居然都是意外死亡山宾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門鳍徽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來资锰,“玉大人,你說我怎么就攤上這事阶祭”炼牛” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵濒募,是天一觀的道長鞭盟。 經(jīng)常有香客問我,道長瑰剃,這世上最難降的妖魔是什么齿诉? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上粤剧,老公的妹妹穿的比我還像新娘歇竟。我一直安慰自己,他們只是感情好抵恋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布焕议。 她就那樣靜靜地躺著,像睡著了一般弧关。 火紅的嫁衣襯著肌膚如雪盅安。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天世囊,我揣著相機(jī)與錄音别瞭,去河邊找鬼。 笑死茸习,一個胖子當(dāng)著我的面吹牛畜隶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播号胚,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼籽慢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了猫胁?” 一聲冷哼從身側(cè)響起箱亿,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎弃秆,沒想到半個月后届惋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡菠赚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年脑豹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衡查。...
    茶點(diǎn)故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡瘩欺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拌牲,到底是詐尸還是另有隱情俱饿,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布塌忽,位于F島的核電站拍埠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏土居。R本人自食惡果不足惜枣购,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一嬉探、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坷虑,春花似錦甲馋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至芹敌,卻和暖如春痊远,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背氏捞。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工碧聪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人液茎。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓逞姿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親捆等。 傳聞我的和親對象是個殘疾皇子滞造,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評論 2 353

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