最近更新: 2011-02-17

PHP 網頁訊息國際化與 gettext 使用經驗

PHP 很早就已經支援 GNU gettext 的國際化模組。在線上手冊與網路上也可以找到許多相關的討論文章。 但是部份內容沒有完善地理解區碼的設定方式,以至於程序員在 LANGsetlocale() 的問題之中糾纏不休。

本文首先將說明區碼的設定問題,再示範 gettext 的使用方式。

區碼的問題

當我們想要用 gettext 將我們以 PHP 設計的網站添加國際化的區域性訊息功能時,首先我們必須要使用 setlocale() 指定訊息區碼;這點與一般應用程式透過環境變數 LANG 的方式不同。例如《Vala with GNU gettext》中的範例,調用 setlocale() 時都不指定區碼,而由環境變數決定。但這種作法不適用於 PHP 建立的 Web 應用程式。因為 PHP 程式係由 httpd 服務行程調用,而 httpd 行程在系統啟動時便已載入,其環境變數已經固定。故我們基本上不應透過環境變數取定訊息區碼,而應使用 setlocale() 指定明確的區碼。

初學者在使用 setlocale() 設定區碼時,最常反應的問題是「我明明照文章上的寫法 setlocale(LC_ALL, "zh_TW")了,但是仍然不會顯示中文訊息」。這個問題的正確答案是「你的系統不認得 zh_TW 這個區碼」。基於歷史與傳統因素,I18N 的區碼並沒有採用嚴格的格式,同一個文化語系在不同的系統上,可能是用不同的區碼表示。例如臺灣地區正體中文語系的區碼,有些系統是用 "zh_TW",有些卻是 "zh_TW.utf8",甚至更早期的還有 "zh_TW.big5"。區區數字之差,就是令使用者抱怨明明設定了環境變數或 setlocale() 了,但軟體顯示的訊息還是沒變的原因。

php-gettext 可用的區碼係由作業系統的區碼表決定。執行指令 locale -a 將會列出作業系統目前的區碼表。 以 Ubuntu 10.04 為例,在我的系統設置上, locale -a 列出的臺灣區碼只有'zh_TW.utf8'。故我調用 setlocale() 時,必須指定 'zh_TW.utf8',gettext 才會正常地取得本地訊息。指定 'zh_TW' 則不會有影響。 但是有些系統並不是用這個名稱,例如有些系統用 'zh_TW'。此時我傳 'zh_TW.utf8' 給 setlocale() 反而錯了。

克服此系統設置差異的解決方案有二種:

  1. 修改區碼名稱表。
    在 Debian/Ubuntu 家族中,區碼名稱表的文件名稱是 /etc/locale.alias 。 但這個方案需要經由系統管理者操作,則會影響到整個作業系統,並不建議採用。
  2. 調用 setlocale() 時,給它多個區域代碼。
    PHP 4.3.0 之後,setlocale() 允許傳多個區域代碼給它,它將逐一嘗試直到可用為止。 下節將細說此方案。
setlocale 的用法

PHP 4.3.0 之後,setlocale() 允許傳多個區域代碼給它,它將逐一嘗試直到可用為止。例如 setlocale(LC_ALL, "zh_TW.utf8", "zh_TW", "zh");

至於舊版的 PHP 用戶,也有解。當 setlocale() 發現你給它的區域代碼不可用時,它會回傳 false 。利用這個特性,舊版 PHP 也可以自行撰寫嘗試動作。下列為範例

<?php
    $possible_locale_name_list = array(
        'zh_TW.utf8', 'zh_TW', 'zh'
    );

    if (PHP_VERSION_ID >= 40300) {
        $result = setlocale(LC_MESSAGES, $possible_locale_name_list);
    }
    else {
        foreach ($possible_locale_name_list as $l) {
            $result = setlocale(LC_MESSAGES, $l);
            if ($result)
                break;
        }
    }

    if ( $result ) {
        $current_locale = setlocale(LC_MESSAGES, 0);
        echo "目前的區碼是: $current_locale\n";
    }
    else {
        echo "setlocale() 找不到符合的區碼\n";
    }
?>

使用 gettext 顯示區域化訊息

當我們正確地理解 setlocale() 與區碼的使用方式後,接著就要使用 gettext 取出本地化訊息顯示了。

在動手撰寫或修改程式之前,你需要先了解關於 gettext 訊息文件的編輯與產生的知識。這部份內容請參考《Vala with GNU gettext》,本文不再複述。

下列的範例程式 hello.php,有兩種執行形式。其一是在命令列執行,第一個參數指定訊息區碼。其二是網頁形式,在網頁網址後加上查詢字串 ?locale=區碼。如果不指定區碼,那麼就會顯示原始訊息 - 倒寫的句子。

<?php
error_reporting(E_ALL);
header('Content-type: text/plain; charset=utf-8');

const GETTEXT_PACKAGE = 'hello';

$arg_locale = false;
if (PHP_SAPI == 'cli') {
    if ($argc >= 2)
        $arg_locale = $argv[1];
}
else {
    header('Content-type: text/plain; charset=utf-8');
    if (isset($_GET['locale']))
        $arg_locale = $_GET['locale'];
}


if ($arg_locale) {
    $possible_locale_name_list = array(
        $arg_locale . '.utf8', $arg_locale
    );

    if (PHP_VERSION_ID >= 40300) {
        $result = setlocale(LC_MESSAGES, $possible_locale_name_list);
    }
    else {
        foreach ($possible_locale_name_list as $l) {
            $result = setlocale(LC_MESSAGES, $l);
            if ($result)
                break;
        }
    }

    if ( $result ) {
        $current_locale = setlocale(LC_MESSAGES, 0);
        echo "current locale: $current_locale\n";
    }
    else {
        echo "failed to setlocale\n";
    }

    bindtextdomain(GETTEXT_PACKAGE, './locale'); // 區域化內容放置路徑
    textdomain(GETTEXT_PACKAGE); // 使用 hello.mo 的訊息
    bind_textdomain_codeset(GETTEXT_PACKAGE, 'utf-8');
}

echo _("好你"), "\n";
echo _("行一第的息訊長。\n行二第的息訊長。\n");
?>

首先, hello.php 對於區碼格式採用新的方法從寬認定。例如使用者輸入 'zh_TW' 時,就會產生 {'zh_TW.utf8', 'zh_TW'} 的區碼清單,交給 setlocale() 指定訊息區碼。

指定區碼之後,接著要指示區域化內容放置路徑、文字範圍與訊息字元編碼格式。分別調用 bindtextdomain()textdomain()bind_textdomain_codeset() 函數指示前述項目。由於 PHP 被 httpd 行程調用時,其工作目錄就是 PHP 程式碼所在目錄,故我們可以將區域化內容資料夾建立在 PHP 程式碼所在目錄下。例如你的 PHP 程式碼放在 /var/www/my_web ,則可以建立 /var/www/my_web/locale 資料夾與區域化內容的目錄架構;將區域訊息文件(MO文件)放置在相對應用的區碼路徑,如 /var/www/my_web/locale/zh_TW/LC_MESSAGES。

最後使用 php-gettext 的函式取得本地化訊息。php-gettext 提供了多個相關的訊息函式,但以 gettext() 最為常用,並有一個慣例的簡短別名,即 _()

寫好程式後,我們就可以使用 xgettext 工具,將待處理的訊息文字擷取為 POT 文件,再分別翻譯為各語系區碼的 PO 文件。 最後使用 msgfmt 工具產生 MO 文件後,便大工告成。具體的工具操作如下列所示。

$ xgettext --language=php --from-code=utf-8 --output=hello.pot hello.php
$ cp hello.pot hello-en_US.po
$ edit hello-en_US.po
$ cp hello.pot hello-zh_TW.po
$ edit hello-zh_TW.po
$ msgfmt --output=locale/en/LC_MESSAGES/hello.mo hello-en_US.po
$ msgfmt --output=locale/zh_TW/LC_MESSAGES/hello.mo hello-zh_TW.po

以下為範例程式以命令列形式執行的結果。

$ php hello.php
好你
行一第的息訊長。
行二第的息訊長。

$ php hello.php zh_TW
current locale: zh_TW.utf8
你好
長訊息的第一行。
長訊息的第二行。

$ php hello.php en_US
current locale: en_US.utf8
Hello
First line of long message.
Second line of long message.

以下為範例程式透過 Web 呈現的結果。

配合參數 ?locale=en_US 顯示英文訊息
配合參數 ?locale=zh_TW 顯示中文訊息

參考文件

樂多舊網址: http://blog.roodo.com/rocksaying/archives/15193601.html

樂多舊回應
未留名 (#comment-21743121)
Fri, 06 May 2011 22:03:28 +0800
第三行的header似乎是多出來了
未留名 (#comment-21748749)
Tue, 10 May 2011 14:40:59 +0800
沒多。

如果是用 php-cli 模式執行,那行不會有作用。

但若透過瀏覽器調用,那麼那行會令瀏覽器以純文字的文件形式顯示輸出結果,就會有換行效果。