My code works, I don’t know why.

國王的耳朵是驢耳朵

C語言中使用gettext

| Comments

gettext 是1990年代Sun推出的軟體,用於處理Unix下面程式訊息的多國語言問題。後來GNU協會也推出了GNU gettext。本文以GNU的gettext為主。


目錄


概論

這張圖這張圖很清楚地說明整個流程。文字解釋如下:

  • 使用者在程式碼中加料,讓gettext工具可以認出來要處理的訊息
  • 執行xgettext,將程式碼中要處理的訊息抽出存到pot (Portable Object Template)檔
  • 執行msginit,指定需要的語系如正體中文、日文、法文等。程式會將pot檔案轉成對應的po(Portable Object)檔
  • 執行msgfmtpo檔產生和平台相關的文字訊息的mo(machine object)檔
  • 接下來如果程式碼又有新的加料訊息,就不是用msginit產生pot檔,而是透過msgmerge更新pot

範例

  • 測試環境
    • Ubuntu 12.04 LTS

套用gettext到C語言程式碼

講一下程式碼

  • 要include locale.hlibintl.h
  • 加入#define _(String) gettext(String),當有需要翻譯轉換的訊息使用。也可以直接使用char * gettext (const char * msgid);
  • 初始部份
    • setlocale(LC_ALL, "");:根據環境變數設定locale,有興趣的可以man locale
    • bindtextdomain(PACKAGE, LOCALEDIR);: PACKAGE, LOCALEDIR由外部傳入的巨集,請參考下面的Makefile。想像成namespace概念,基本上就是引導系統去特定目錄去取得LOCALEDIR/LC_MESSAGE/PACKAGE.mo檔。有興趣的可以man bindtextdomain
      • 一般來說系統上是存放在/usr/share/locale,然而這不是硬性規定。所以本次的Makefile故意放在local的po目錄之中,建議一定要看一下。
    • textdomain(PACKAGE);:讀取mo檔案,讓後面的gettext可以取得LC_ALL設定對應的訊息。
  • 請注意有些註解是/*-,和一般的不一樣,之後執行xgettext時可以下--add-comments=-抽出註解放到pot
  • 為了易讀性,拔掉錯誤檢查
test_gettext.c 拔掉錯誤檢查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <locale.h>
#include <libintl.h>
#define MAX_CHAR (32)

int main(int argc, char **argv)
{
    char dest[MAX_CHAR];
    char transport[MAX_CHAR];

    /* Init gettext related APIs*/
    setlocale(LC_ALL, "");
    bindtextdomain(PACKAGE, LOCALEDIR);
    textdomain(PACKAGE);

    /*- Set up strings */
    strncpy(dest, gettext("Taipei"), MAX_CHAR);
    strncpy(transport, gettext("bus"), MAX_CHAR);

    /*- Let's print out some message */
    printf(gettext("I will go to %s by %s.\n"), dest, transport);

    return 0;
}
Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
TARGET=test_gettext
LOCALE=zh_TW
PO_DIR=$(shell pwd)/po
CFLAGS=-Wall -Werror -g -DPACKAGE=\"$(TARGET)\" -DLOCALEDIR=\"$(PO_DIR)\"
OBJS=$(patsubst %, %.o, $(TARGET))

all: $(TARGET).pot $(LOCALE).po $(TARGET).mo $(TARGET)

$(TARGET).pot: $(TARGET).c
  if [ ! -f $(TARGET).pot ] ; then                                \
      xgettext -o $(TARGET).pot --add-comments=- -k_ $(TARGET).c; \
  fi

$(LOCALE).po: $(TARGET).pot
  if [ -f $(LOCALE).po ] ; then                               \
      msgmerge $(LOCALE).po $(TARGET).pot ;                   \
 else                                                        \
     msginit --locale=$(LOCALE) --input=$^ --no-translator ; \
 fi

$(TARGET).mo: $(LOCALE).po
  mkdir -p $(PO_DIR)/$(LOCALE)/LC_MESSAGES
  msgfmt $^ -o $(PO_DIR)/$(LOCALE)/LC_MESSAGES/$@


%.o: %.c
  $(CC) -o $(patsubst %.o, %, $@) $^

clean:
  rm -f *.o *~ $(TARGET) *.po *.pot *.mo
  @echo To remove po directory, use: rm -rf $(PO_DIR)

產生pot檔

  • Makefile中的xgettext -o $(TARGET).pot --add-comments=- -k_ $(TARGET).c會產生pot檔。產生的pot如下,我們可以看到剛才/*-抽出來的註解會被加入pot檔內。另外不要忘記更改template中的metadata。

如果是多個C檔怎麼辦呢?請用-D 指定目錄或是-f指定多個檔案

test_gettext.pot
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-05-30 23:11+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#. - Set up strings
#: test_gettext.c:22
msgid "Taipei"
msgstr ""

#: test_gettext.c:23
msgid "bus"
msgstr ""

#. - Let's print out some message
#: test_gettext.c:26
#, c-format
msgid "I will go to %s by %s\n"
msgstr ""

產生正體中文po檔

兩種情況

  • 還沒有po檔產生
    • msginit --locale=$(LOCALE) --input=$^ --no-translator
      • locale會根據指定locale產生對應的po檔,以我們例子就是zh_TW.po
    • --no-translator單純是我懶得回答互動問題,這個可有可無。
  • 產生po檔,就可以分配給專業的翻譯者處理
    • 已經翻譯過了,但是又有新的pot檔產生的話。此時需要合併以經翻譯的po檔案,產生新的po檔。新的po檔會有已經翻譯過的訊息己及新的未翻譯訊息。
      • msgmerge $(LOCALE).po $(TARGET).pot

這邊的翻譯過後po檔如下,幾點要注意的

  • charset要設成UTF-8
  • 由於語言特性,參數有可能會和英文順序不同。所以I will go to Taipei by bus換成正體中文可以轉成我搭乘公車到台北。因此,請注意gettext po檔訊息可以指定參數順序。
test_gettext.po
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Chinese translations for PACKAGE package.
# Copyright (C) 2014 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Automatically generated, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-05-30 23:03+0800\n"
"PO-Revision-Date: 2014-05-30 23:03+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"

#. - Set up strings
#: test_gettext.c:22
msgid "Taipei"
msgstr "台北"

#: test_gettext.c:23
msgid "bus"
msgstr "公車"

#. - Let's print out some message
#: test_gettext.c:26
#, c-format
msgid "I will go to %s by %s.\n"
msgstr "我搭乘%2$s到%1$s。\n"

po檔補充

po檔主要是msgidmsgstr成對,翻譯的文字放在msgstr這邊。而註解以#開頭。

另外gettext的工具在分析時也會使用特殊註解格式輔助產生正確的mo檔案,他們以,開頭,可以串接在同一行註解。列舉一些如下:

  • , fuzzy: 顯示翻譯可能不正確,可由翻譯者或是在msgmerge時產生。翻譯者review確定翻譯沒有問題需要把該註解拿掉。
  • , c-format:表示訊息會用到C的print format,如%s, %d等。

產生mo檔

請參考Makefile部份描述:

test_gettext.po
1
2
3
$(TARGET).mo: $(LOCALE).po
    mkdir -p $(PO_DIR)/$(LOCALE)/LC_MESSAGES
    msgfmt $^ -o $(PO_DIR)/$(LOCALE)/LC_MESSAGES/$@
  • $^表示prerequisite而$@表示target。
  • 一開始我們先建立local的$(PO_DIR)/$(LOCALE)/LC_MESSAGES/,將mo黨產生在該目錄。而$(PO_DIR)/$(LOCALE)目錄也是一開始傳給測試原始碼的定義。

範例結果

注意LC_ALL可以從locale -a指令列出系統支援的語系,所以這邊我們用zh_TW.utf8而不是zh_TW,有興趣的人可以自行改為zh_TW將會發現不會有中文顯示,有加錯誤檢查還會出現檔案找不到的錯誤訊息。

LC_ALL=”en_US.utf8”
1
2
$ LC_ALL="en_US.utf8" ./test_gettext
I will go to Taipei by bus.
LC_ALL=”zh_TW.utf8”
1
2
$ LC_ALL="zh_TW.UTF8" ./test_gettext
我搭乘公車到台北。

而關於, fuzzy我們可以做更多的實驗。我們把zh_TW.po的Taipei更改加入, fuzzy

加入`, fuzzy`
1
2
3
#, fuzzy
msgid "Taipei"
msgstr "台北"

跑起來就結果變成

加入`, fuzzy`
1
2
3
$ LC_ALL="zh_TW.utf8" ./test_gettext
test_gettext
我搭乘公車到Taipei。

參考資料

Comments