Vala with GNU gettext
Vala 作為 GNOME 開發環境下新興的開發語言,帶入了許多新的功能,其中亦包含國際化(i18n)的支援項目。 儘管 Vala 的線上教學文件沒有隻字片語提到 i18n/l10n,但事實上 Vala 已經將 GNU gettext 作為內建的語言功能,使用它實現 i18n/l10n 能力。 Vala 提供了名為 _ 的函數,只要我們的 vala 程式碼使用了 _() 函數,就會使用 GNU gettext 取得本地訊息。
但是現階段使用這個內建功能時,還有一個文件未記載的不完善之處必須解決。待我說來。
Vala 轉譯後的 C 源碼,將會引入 gi18n-lib.h 負責 gettext 的程式碼。
而 gi18n-lib.h 要求程序員必須使用 GETTEXT_PACKAGE 符號定義你的內容範圍(text domain)。
但是目前的 valac 在調用 gcc 時,並不會幫我們產生 GETTEXT_PACKAGE 的符號定義,
所以 valac 會丟出一個乍看之下莫名奇妙的錯誤訊息:
#error You must define GETTEXT_PACKAGE before including gi18n-lib.h.
Did you forget to include config.h?
若未追踪它轉譯後的 C 源碼,很難理解我們的 vala 程式碼怎麼會跟 gi18n-lib.h 扯上關係。
解決方法是使用 valac 時,自己加上一個額外傳遞給 gcc 的參數選項,即: -X -DGETTEXT_PACKAGE="your_text_domain" 。 下列是一個完整的使用範例:
$ valac -X -DGETTEXT_PACKAGE="pcmanfm" hello.vala
See also: http://www.mail-archive.com/vala-list@gnome.org/msg02887.html
或許 vala 未來的版本會提供適當地處理方式。但目前僅能使用這個不優雅的解法。
Hello gettext
GNU gettext 將本地訊息包裝在副檔名為 .mo 的文件中(以下以 MO 文件稱呼包裝過的本地訊息文件),它是獨立於應用軟體之外的資料。 換言之,你的軟體也可以直接使用別的軟體的 MO 文件。 在說明如何產生自己的 MO 文件之前,我們先在自己的應用軟體中使用別人的 MO 文件,體驗一下 gettext 的能力。
我的 Ubuntu 系統中安裝了 pcmanfm 這套檔案管理工具,我將使用它的 MO 文件示範如何顯示我的程式中要區域化的訊息。
程式說明
- GETTEXT_PACKAGE: 很遺憾,雖然我在這裡定義了常值 GETTEXT_PACKAGE ,但 valac 並不會用它帶出傳給 gcc 的符號參數 -DGETTEXT_PACKAGE=? ,所以我們還是要手動加上那個參數。但是它還是有一個安全作用:當 vala 程式碼中的 GETTEXT_PACKAGE 與符號參數 -DGETTEXT_PACKAGE 兩者之值不相同時, gcc 會抱怨 GETTEXT_PACKAGE 被重新定義了。
-
Intl.setlocale(): 使用 GNU gettext 的本地化功能前,必須指定我們要啟用的類型。
在本例中,我只用到訊息的本地化功能,所以我只啟用 LocaleCategory.MESSAGES。
第二個參數指定區碼,若為空字串,則表示由環境變數指定區碼。具影響力的環境變數,依優先等級排列為: LANGUAGE, LC_ALL, LC_MESSAGES, LANG;LANGUAGE 是 GNU gettext 的擴充項目,不是 POSIX 規範項目 (參考 Locale Environment Variables)。若為 null ,則是查詢目前使用的區碼;但是 Vala 的實作內容似乎有 bug ,它總是回傳環境變數 LANG 之值。
區域化內容類型,除了訊息的本地化外,還有貨幣符號、數字格式、日期格式等等的本地化。如果我們啟用 LocaleCategory.ALL ,則會影響所有可以本地化的內容類型。有時這並非我們想要的結果,應謹慎使用。 - Intl.textdomain(): Vala 的 _() 函數,實際上是調用 g_dgettext(GETTEXT_PACKAGE, msgid)。由於 g_dgettext() 會自帶範圍名稱,所以我們不需要用 Intl.textdomain() 設定預設範圍。
- Intl.bind_textdomain_codeset(): 當我僅啟用本地化訊息功能時(LocaleCategory.MESSAGES),我必須指定本地訊息的字元編碼,否則 gettext 會用當地慣用的區域編碼系統,例如 zh_TW 預設是 BIG5。 在早期,這個動作很有用。但是隨著 UTF-8 編碼系統的推廣,現在大部份的本地訊息文件也都是用 UTF-8 編碼,故現在要指定字元編碼為 'utf-8',否則輸出的訊息通常是所謂的亂碼。
- Intl.bindtextdomain(): 若你的 MO 文件放置的位置不在系統預設路徑內,則你必須調用 Intl.bindtextdomain() 指示訊息文件的搜尋路徑。 在本例中,pcman.mo 正放置於系統預設路徑內,其實可以省略此動作。
- _(): 將原始訊息交給內建函數 _(),取得本地化的訊息文字。如果你使用的 MO 文件中未包含對應的本地訊息,則會直接傳回原始訊息。
執行範例如下。我透過環境變數 LANG 調整程式輸出的訊息內容。
$ valac -X -DGETTEXT_PACKAGE="pcmanfm" hello1.vala
$ export LANG=zh_TW.utf8
$ ./hello1
執行
$ export LANG=en_US.utf8
$ ./hello1
Execute
建立你的訊息文件
前一節中說明了如何利用 GNU gettext 函數庫,取得原始訊息的本地訊息後輸出。接下來的內容,則要說明如何建立自己的訊息文件。相關內容包括區域訊息文件的編寫格式、操作工具等。
編寫區域訊息文件
上節提過 MO 文件是包裝過的本地訊息文件,它的內容是索引資料檔,這顯然不是可用文字編輯器編寫的格式。 其實我們主要的操作對象並不是 MO 文件,而是未包裝過的純文字本地訊息文件;GNU gettext 稱為 PO 文件。 PO 文件的格式說明請參閱《The Format of PO Files》。其實它的格式非常簡單,主體只是由成對的 msgid/msgstr 描述所組成。其他項目都是選擇性的註釋內容。
下列是一個最簡單的 PO 文件全文。你可以用你最慣用的文字編輯器編寫這些內容。
按照 GNU 開發者慣例,PO 文件採用區碼作為主檔名。上列的訊息文件是漢語系臺灣區( zh_TW,這是根據文化別所作為區分)的 PO 文件,就取名為 zh_TW.po。再使用 GNU gettext 提供的工具 msgfmt 將 PO 文件包裝成 MO 文件,就可以讓我們的軟體使用了。
下列指令可將 zh_TW.po 包裝成 hello.mo 文件,並將它按區碼與類型,放置於 locale 目錄下。我們放置區域化內容的目錄,慣例命名為 locale。其目錄結構則有標準規範:第一層是區碼,第二層是類型。故 zh_TW 區碼的本地訊息文件,就要放在 zh_TW/LC_MESSAGES 目錄下。至於 MO 文件的主檔名,則是此訊息文件的範圍名稱。
# 建立本文所需的區域化內容的目錄結構。
$ mkdir -p locale/zh_TW/LC_MESSAGES
$ mkdir -p locale/eu_US/LC_MESSAGES
$ msgfmt --output=locale/zh_TW/LC_MESSAGES/hello.mo zh_TW.po
MO 文件的主檔名即為訊息的範圍名稱(text domain)。我將使用 hello.mo 訊息文件的內容,故接下來的 vala 程式碼中,我要指定的範圍名稱就是 "hello"。在範例中,我都將自製的 MO 文件放在現行目錄的 locale 子目錄內,故 Intl.bindtextdomain() 指向 "./locale"。實務上,則應以絕對路徑表示。通常我們自製的 MO 文件,會放在 /usr/local/share/locale。
下列為執行結果。
$ valac -X -DGETTEXT_PACKAGE="hello" hello2.vala
$ export LANG=zh_TW.utf8
$ ./hello2
你好 gettext.
歡迎來到我的部落格 - 石頭閒語。
$ export LANG=en_US.utf8
$ ./hello2
Hello gettext.
Welcome to my blog, Rock's saying.
從程式碼中擷取訊息
實務上,我們通常不是先寫 PO 文件再套進程式中,那太麻煩了。一般是把要區域化的訊息寫在程式碼中,再用 GNU gettext 的工具 xgettext 將需翻譯的訊息從程式碼中擷取出來。如下例 hello3.vala。我先寫好程式,並把需要區域化的訊息改成用 _() 函數處理。
接著使用 xgettext 擷取訊息。因為 xgettext 還不認得 Vala 的語法,所以我要指定一個最接近的語言(--language)給它參考;在此我用 C#。Vala 的 gettext 函數名稱是 _ ,所以指定辨識的關鍵字(--keyword) 為 _。最後,按照 GNU 開發慣例,xgettext 擷取出來的待翻譯訊息文件的副檔名取為 .pot (.pot 之意為 PO Template。正在翻譯或已經翻好的訊息文件,則是 .po。)。
$ xgettext --language="C#" --keyword=_ --from-code=utf-8 \
--output=hello3.pot hello3.vala
$ cp hello3.pot hello3-zh_TW.po
$ cp hello3.pot hello3-en_US.po
最後,我們將 POT 文件複製給翻譯人員,填入翻譯的本地訊息。將翻譯者譯好的 PO 文件收集起來之後,再用 msgfmt 包裝成 MO 文件使用。hello3-zh_TW.po 是翻譯好的區域訊息文件。眼尖的讀者會發覺我這份文件少了很多欄位。再次強調,只有 msgid/msgstr 是必要的主體,其他都是選擇性的註釋,可有可無,刪了也不會影響後續操作。
下列為本小節的示範操作過程。
# 1.撰寫程式碼 hello3.vala
# 2. xgettext 擷取訊息
$ xgettext --language="C#" --keyword=_ --from-code=utf-8 \
--output=hello3.pot hello3.vala
# 3. 散佈 POT 文件給其他人翻譯
$ cp hello3.pot hello3-zh_TW.po
$ cp hello3.pot hello3-en_US.po
# 4. 收集 PO 文件包裝成 MO 文件
$ msgfmt --output=locale/zh_TW/LC_MESSAGES/hello3.mo hello3-zh_TW.po
$ msgfmt --output=locale/en_US/LC_MESSAGES/hello3.mo hello3-en_US.po
$ valac -X -DGETTEXT_PACKAGE="hello3" hello3.vala
$ export LANG=en_US.utf8
$ ./hello3
hello3. 這一行訊息不需要翻譯
Hello gettext
First line of long message.
Second line of long message.
$ export LANG=zh_TW.utf8
$ ./hello3
hello3. 這一行訊息不需要翻譯
你好 gettext
長訊息的第一行。
長訊息的第二行。
$ unset LANG
$ ./hello3
hello3. 這一行訊息不需要翻譯
好你 gettext
行一第的息訊長。
行二第的息訊長。
實務上,我們會持續編寫程式碼與擷取訊息的工作,再使用 GNU gettext 工具 msgmerge 將 POT 文件新增的待翻譯項目併入已經翻譯好的 PO 文件中。不必每次擷取 POT 文件後都得重頭再翻譯一次。
參考文件與後記
- GNU gettext manual
- Vala Intl
- manpage setlocale(3) - 包含區碼的表達格式。
基於歷史與傳統因素,I18N 的區碼並沒有採用嚴格的格式,同一個文化語系在不同的系統上,可能是用不同的區碼表示。這多少產生了程式人員在編寫跨平台的多語化應用軟體時的困擾。或是使用者會抱怨明明設定了環境變數,但軟體顯示的訊息還是沒變。這都是區碼表達格式不匹配的結果。請程序員秉持國際觀,以寬容的文化包容心態多了解區域性差異。善用 setlocale() 的回傳值或是環境變數 LANGUAGE 改進你的程式內容,提高環境的包容程度。
GNU gettext 在執行 setlocale() 時,採取完全相符的比對策略。但是執行 gettext() 則是從寬搜尋。例如你的系統區碼表中只有 en_US.utf8 的區碼,那麼執行 setlocale(TYPE, "en_US")
就會判定不匹配。但是執行 gettext(msgid)
時,則會依序從 $locale_dir/en_US.utf8/LC_MESSAGES、$locale_dir/en_US/LC_MESSAGES、$locale_dir/en/LC_MESSAGES 這些目錄中搜尋指定的 MO 文件,條件寬鬆許多。這幾點是初學者需要留意之處。
GNU gettext 包含了相當豐富的內容,在本文中並沒有詳細說明。請自行閱讀 GNU gettext manual。
樂多舊回應