使用 gettext 方案製做多國語系程式 - 製做語系檔

當我們依據 gettext 的規範修改我們的程式碼後, 接下來就可以使用 gettext 的各種工具掃描分析、加入翻譯資料、並最終產生語系資源檔了。

本篇將針對程式開發人員建置專案工作的部份, 以一個範例來描述如何從分析專案程式碼到最終生成語系檔案的工作。

上面流程圖簡單扼要的說明了從分析程式碼到最終產出語系資源檔的流程、工具、和產生的各種檔案, 但對於初次接觸的人可能還是稍嫌複雜, 因此接下來我要稍稍解釋它們,使其對初學者較容易理解記憶。

生成的檔案類型

整個處理文字翻譯的過程中大約會產生 .pot、.po、和 .mo 三種檔案,分別解釋如下:

  • PO (Portable Object)

    該檔案就是在整個翻譯過程中最重要的檔案, 每一個(要支援的)地方語言就需要(至少)一個對應的 PO 檔, 這個檔案是語言文字翻譯翻譯人員直接面對的一個文字檔, 裡面記載著大量原始文字對應到某個特定語文文字的資料條目、以及其他有用的註解訊息。

  • MO (Machine Object)

    應用程式若直接讀取解析 PO 檔案的話,會消耗比較多的系統資源, 因此實際上由應用程式直接取用的是經過編譯後的二進位文件,也就是 MO 檔, 而這也就是在本系列文章中所稱呼的語系資源檔。

  • POT (Portable Object Template)

    因為每一種語言都需要一個與之對應的 PO 檔,因此實際上可能會存在數量龐大的 PO 檔群。 PO 檔最初是由解析工具掃描整份專案程式碼檔案而來, 那麼多次重複掃描一樣的文件以產生不同語系的 PO 檔就顯得不太有效率。 因此實務上通常只會進行一次掃描解析工作來產生一個通用的樣板檔案, 再用這個通用檔案去變化成每個具體語系所使用的檔案,而這個通用的樣板檔案就是 POT 檔。 實際上 POT 和 PO 的格式一模一樣,其檔案的初始內容也幾乎相同。

生成檔案的步驟

在了解各種檔案的用途後,接下來以一個範例解釋產生它們的流程步驟:

  1. 使用 xgettext 程式掃描整個專案的程式碼以產生 POT 檔:

    xgettext \
    	--package-name=gettext-demo \
    	--package-version=v0.1 \
    	--copyright-holder=Testman \
    	-c+ \
    	-k_ \
    	-o demo.pot \
    	demo_part1.c demo_part2.c ...

    常用選項:

    • -o FILE: 指定輸出檔名。

    • -cTAG: 將程式碼內以 TAG 做為開頭的註解一併寫到 POT 檔案裡, 這些註解可能可以幫助翻譯人員進行翻譯時閱讀參照。 因此需要與程式開發人員協調出一個約定的符號, 讓開發人員為那些他們覺得可能會對翻譯有所幫助的註解說明加上這個約定的符號。 在這個範例裡,我們使用「+」符號。

    • kWORD: 除了 gettext 以外,也一併將 WORD 視為關鍵字搜尋程式碼檔, 這個功能在使用了自定義的 gettext 別名的時候有用。 在這個範例裡,我們一併將底線符號「_」視為關鍵字搜尋。

    • --package-name=PACKAGE: 設定這個程式專案的名稱,不設也不影響後續流程。

    • --package-version=VERSION: 設定這個程式專案當前的版本號名稱,不設也不影響後續流程。

    • --copyright-holder=STRING: 設定版權所有人或單位的名稱,不設也不影響後續流程。

  2. 使用 msginit 讀取 POT 檔來產生 PO 檔(適合第一次產生該語系 PO 檔時使用):

    msginit -l zh_TW.UTF-8 --no-translator -o demo-zh_TW.po -i demo.pot

    常用選項:

    • -i FILE: 輸入的 POT 檔。

    • -o FILE: 輸出的 PO 檔; 若不指定輸出檔,則預設會以 locale 名稱產生輸出檔; 若設輸出檔為 - 則會從 standard output 輸出結果。

    • -l LL_CC: 指定的語系,格式為 <LOCALE>_<COUNTRY>[.<ENCODING>]。 雖未要求輸入 encoding, 但在沒有 encoding 的情況下產生的檔案 encoding 通常是 ASCII, 此時若在翻譯檔內輸入非 ASCII 的文字 — 比如說 UTF-8 格式的中文 — 則會在後面編譯時產生錯誤; 雖然這個 encoding 可以在文件產生後再手工修改 charset 欄位, 但若閒麻煩的話就可以在產生 PO 檔的時候直接指定 encoding。

    • --no-translator: 不設定翻譯者資訊, 產生的 PO 檔相關欄位會被設為 "automatically generated"; 若不使用這個選項,則程式會交互式詢問翻譯者名稱和信箱,可能不利自動化腳本, 因此一般建議翻譯者資訊由翻譯者在 PO 檔內自行修改。

  3. 使用 msgmerge 更新 PO 檔(適合更新已有的語系 PO 檔時使用):

    msgmerge -U --backup=off demo-zh_TW.po demo.pot

    除了第一次產生某個語系的 PO 檔之外, 比較常遇到的情況應該是程式有修改更新,而可能會多出一些需要翻譯的文字。 此時若使用 msginit 每次都重新產生 PO 檔然後全部翻譯重來一遍的話, 應該會讓翻譯人員吃不消! 這時改用 msgmerge 來產生合併後的 PO 檔,即保留原有的翻譯、並加入新的條目, 就會是比較合適的做法。

    常用選項:

    • -U: 直接將更新後的結果寫回 PO 檔; 若沒有其他設定要求,則會產生一個原檔案的備份檔。

    • -o FILE: 指定輸出檔名; 若不指定輸出也沒有設定更新,則會將結果輸出到 standard output。

    • --backup=off: 在更新模式下不要產生備份檔。

  4. 人工修改 PO 檔資訊、增加翻譯字串:

    這個步驟其實沒有建置人員的事情, 只要把新產生的 PO 檔交給翻譯人員處理,或者將翻譯人員提交的 PO 檔合併進來,就可以了。 有關於 PO 檔案的格式內容、以及該如何編輯的部份到後面再做解釋。

  5. 使用 msgfmt 讀取 PO 檔以產生 MO 檔:

    test -d locale/zh_TW/LC_MESSAGES || mkdir -p locale/zh_TW/LC_MESSAGES
    msgfmt -o locale/zh_TW/LC_MESSAGES/demo.mo demo-zh_TW.po

    常用選項:

    • -o FILE: 指定輸出檔名。

    • -a NUMBER: 設定資料對齊 NUMBER bytes。 視 gettext 執行時庫的實做而定,將對齊設為適合目標平臺 CPU 的數值, 對於加速 MO 檔內資料查詢可能有幫助。 (default: 1)

    • --endianness=BYTEORDER: 設定使用的 endian 格式, BYTEORDER 可為 big 或 little。 將 endian 設定為與目標平臺相同, 則可能可以加速一點點在應用程式讀取 MO 檔時的速度。 (default: same as platform)

發佈 MO 檔

編譯完成的 MO 檔就可以發佈出去了。 由於 MO 檔本身與平臺無關,因此同一個 MO 檔可以拿到所有的平臺下使用, 而無需重新編譯為個別平臺下使用的版本。

也由於 MO 檔本身獨立於應用程式執行檔之外,是由應用程式通常在程式起始時載入, 就算載入失敗或找不到檔案也不會讓程式當掉或發生其他異常,就只是等於沒有翻譯而已; 就算 MO 檔內沒有完整記載程式所有需要翻譯的字串資料(也就是還沒翻譯完的意思), 那就是在程式中的部份字串會以原始字串呈現而已。 因此 MO 檔本身的使用彈性很大,可以獨立於執行檔進行更新升級以補充或修正翻譯條目, 也可以直接加入原來沒有的新語系的 MO 檔,就可以讓應用程式支援這個語系。

那麼 MO 檔究竟要放到哪裡去呢?答案是個別的 MO 檔最終要放到下面這個位置:

<RESOURCE-DIR>/<LOCALE>/LC_MESSAGES/<DOMAIN>.mo

其中:

  • <DOMAIN> 通常就是專案名稱或程式名稱,實際要與開發人員協調約定;

  • <LOCALE> LL_CC 格式的語系名稱,例如 zh_TW

  • <RESOURCE-DIR> 是所有語系的 MO 檔會放置的共同目錄, 在 Linux 上面通常就是 /usr/share/locale,實際要與開發人員協調約定, 比如說可能是執行檔所在目錄下的 data 資料匣等等。

假設我們的範例使用了 Linux 上的一般慣例, 那麼我們編出來的臺灣中文的 MO 檔就應該要放在下面這個地方:

/usr/share/locale/zh_TW/LC_MESSAGES/demo.mo

然而為了方便測試的緣故,本系列文章的範例實際使用的是工作目錄下的 locale 資料匣, 我們的 domain name 設定的也是 gettext-demo 而非 demo。 那麼假設你會在家目錄下的 demospace 目錄下執行範例程式的話, 那麼我們的臺灣中文 MO 檔就應該要放在:

~/demospace/locale/zh_TW/LC_MESSAGES/gettext-demo.mo

PO 檔案內容

PO 檔可說是整個流程所生成的檔案中最重要、富含最多訊息,並且也是翻譯人員直接面對的檔案, 因此有必要說明一下這個檔案的格式和內容。 下面我們先來看看本文所範例所使用的 PO 檔案內容:

# Chinese translations for gettext-demo package
# traditional Chinese translation for gettext-demo.
# Copyright (C) 2019 Testman
# This file is distributed under the same license as the gettext-demo package.
# Automatically generated, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: gettext-demo v0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-11 09:43+0800\n"
"PO-Revision-Date: 2019-11-11 09:43+0800\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: zh_TW\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

#: gettext-demo.c:66
msgid "This is a simple message."
msgstr "這是一個簡單的訊息。"

#. + This is a special comment that will be output to the PO file.
#: gettext-demo.c:70
#, c-format
msgid "I will go to %s by %s.\n"
msgstr "我要搭%2$s去%1$s。\n"

#: gettext-demo.c:70
msgid "Taipei"
msgstr "臺北"

#: gettext-demo.c:70
msgid "bus"
msgstr "公車"

翻譯條目

PO 檔主要記載著大量翻譯條目, 也就是成對的 msgid 和 msgstr 標籤,分別對應原始字串和翻譯後字串, 原始字串內容由解析程式生成,而翻譯字串內容則是由翻譯人員填寫; 若翻譯字串(msgstr)的內容為空的話,則該條目就不會被編進 MO 檔, 應用程式最終就會顯示原始字串。

msgid "This is a simple message."
msgstr "這是一個簡單的訊息。"

以上面條目為例,這個條目的 ID 就是 msgid 的內容即 "This is a simple message.", 而對應的翻譯為 msgstr 內容,原本是空的, 因此由我寫上 "這是一個簡單的訊息。" 這個翻譯結果。

檔頭資訊

PO 檔案頭部有一個特別的空字串 ID, 其所對應的內容實為語系檔案相關資訊註記,供後續處理程式所用。 其中大部份資訊是由程式所產生,一般不需理會, 只有部份的資料內容可能需要翻譯人員或建置人員修改更新其內容 (雖然多半不改也不會影響後續檔案產生流程), 例如:

  • Project-Id-Version 標籤後面的 PACKAGEVERSION 應更改為 專案或程式名稱、以及其版本。

  • Last-Translator 標籤後面應填寫翻譯者姓名和信箱。

  • charset 標籤後面要修改為所使用的編碼格式 (如果在產生 PO 檔的時候沒有指定文字編碼的話)。

註解與特殊註解

PO 檔內以「#」開頭的那一行文字就是是註解訊息,一般的註解會被後續處理程式所忽略; 然而那些在註解符號後面不是空白,而是緊跟其他符號的註解, 則為提供後續處理程式、或翻譯人員所使用的特殊資訊,例如:

#. + This is a special comment that will be output to the PO file.
#: gettext-demo.c:70
#, c-format
msgid "I will go to %s by %s.\n"
msgstr "我要搭%2$s去%1$s。\n"
  • #. 表示這是從程式碼中掃描出來的特別的註解,可能是程式人員認為對翻譯有幫助的說明。

  • #: 記載該原始字串出現於程式碼的位置,以利在需要的時候直接向開發人員查詢相關疑惑。

  • #, 表示這是供 MO 產生器使用的資訊,其後面文字可能為:

    • fuzzy:該註記可能由翻譯人員加上, 表示該翻譯可能不正確或還存在爭議,暫不將該條目編入 MO 檔。 當翻譯結果確認無誤後,由翻譯人員把該註解刪除即可。

    • c-format:表示翻譯字串內含有 C 語言的 printf 類函式的格式字。 若程式的執行環境為 UNIX 相容平臺, 還可以使用特別的 1$2$ 等符號標記來變更後面參數出現的次序, 這樣就可以因應不同語言的語法順序可能不一樣的狀況來調整挪移文句,詳請參閱範例程式。

PO 檔相關工具

理論上在此之前的說明已經足以從頭到尾完成整個翻譯檔案產生的流程了; 然而如同我們在系列文章中一直強調的, 在實際的專案上,人們經常面對的是內含大量翻譯條目的 PO 檔, 其中有些已翻譯、有些未翻譯、有些還待確定, 同時程式本身也在修改,時不時會多出一些新的待翻譯條目(或可能會少一些條目)。 那麼翻譯人員就要在成千上萬行的 PO 檔裡面到處人工閱覽搜索,再補上需要的翻譯? 當然不是! 因此接下來就要介紹一些專門用來分析處理 PO 檔的工具程式。

potool

potool 是廣泛被使用的 PO 檔分析處理工具, 使用套件管理工具的話只要安裝 potool 套件即可。 potool 主要提供三支程式:

  • potool

    這是最主要的程式,有兩種工作模式(至於使用參數選項則太多,詳請參閱該程式說明文件):

    • Filter Mode: 當只有輸入一個 PO 檔時為 filter mode, 作用為依指定的條件過濾並輸出檔案內的條目。

    • Merge Mode: 當輸入兩個 PO 檔時為 merge mode, 作用為將兩個檔案內容合併,類似於 msgmerge 的工作。

  • postats

    這是 potool 程式的再包裝腳本, 用以分析並輸出 PO 檔的統計資訊如已翻譯條目、未翻譯條目、完成比例等, 簡單用法如下:

    postats FILE.po
  • poedit (potooledit)

    這是 potool 程式的再包裝腳本,會讀取並只顯示尚未完成翻譯的條目讓使用者編輯後回存。 potooledit 是這支程式的另一個別名,為避免與其他也叫作 poedit 的程式發生同名衝突而設! 簡單用法如下:

    poedit FILE.po

potool 雖然小巧便利, 但因是指令列工具,要讓一般的文字翻譯人員平常使用的話可能還存在一些難以跨越的門檻。 potool 一般的用途比較多是用來自動分析專案內的語系翻譯進度等資訊, 或者程式人員小規模修改翻譯條目時所使用。

GUI 程式

除了指令列程式以外,一般的語言翻譯人員可能更喜歡使用方便直覺的 Desktop GUI 程式。 這裡推薦使用簡單容易上手的 Poedit (基於 wxWidgets)、 或者還有可能有點複雜但功能非常強大的 OmegaT (基於 Java), 他們都是跨平臺的自由軟體。