使用 gettext 來實做 PHP 多國語系支援(I18N)

要做多國語系支援的網站或程式有很多方式, 常見的是設某種國家的語系檔, 然後檔案內都是變數, 之後程式依照使用者的語系的, 去讀語系檔, 以此來達成多國語系.

而 gettext 是另一種標準的方案, 可以 ls /usr/share/locale/zh_TW/LC_MESSAGES 看看, /usr/share/locale 放著各種語系的翻譯檔(翻譯系統程式, ex: apt.mo, dpkg.mo)(*.mo是編譯過的翻譯檔).

現在來用 php + gettext 實做多國語系的支援吧~

通常多國語系有兩種模式

  1. 每個檔案一個翻譯檔
  2. 所有檔案一個翻譯檔

下面會先做 每個檔案一個翻譯檔 的方法, 最下面才是 所有檔案一個翻譯檔 的方法(基本上都大同小異就是了).

以下 以 Debian Linux 為例(/etc/locale.gen).

前置準備(程式, 設定 /etc/locale.gen)

需要下述程式

  • xgettext - extract gettext strings from source
  • msgfmt - compile message catalog to binary format
  • 註:apt-get install gettext

然後要設定 vim /etc/locale.gen, 檔案內容如下:(看你的語系要支援哪些, 就要有哪些)

zh_TW BIG5
en_US UTF-8
zh_CN UTF-8
zh_TW UTF-8

/etc/locale.gen 如果有改動, 記得要 sudo locale-gen, 不然不會生效 🙂

每個檔案一個翻譯檔 設置法(語系: 英文(en_US), 簡體(zh_CN), 正體(zh_TW))

建立基本架構

PS: 下述建立基本結構, 懶得做可直接下載 gettext_example.tgz, 解壓縮就有基本架構了.

先到要建立多國語系的 Project 目錄下

mkdir -p locale/zh_TW/LC_MESSAGES
mkdir -p locale/zh_CN/LC_MESSAGES
mkdir -p locale/en_US/LC_MESSAGES

在此以 hello.php 為例, 建立 hello.php 檔, 內容如下:(PHP 主要用到 putenv, setlocale, bindtextdomain, textdomain, gettext 的 function)

程式(hello.php, 或其它自己建的程式)注意下述幾點即可:

  1. 語系切換主要是靠 putenv, setlocale.(程式裡面現在是用 $lang 去切換語系, 如何切換可再想更好的方法)
  2. 裡面的 putenv, setlocale 指定的值, 會對應到 /etc/locale.gen 的值, 如果 locale.gen 沒有, 在 putenv, setlocale 設了也沒有用.
  3. PACKAGE 那個變數, 不用 define 也無所謂, 主要是當成 Project name 來用, 到時後翻譯檔建好後, 此名稱就不能再修改(修改翻譯檔需要全部重建(po, mo 檔重建))
  4. gettext() 包起來的就是會被抽取出來的字, 可以用較簡短的寫法 _() 即可.
將文字取出, 產生 po 檔
  • xgettext -d hello hello.php # -d hello => 產生 hello.po 檔(-d --default-domain=NAME, 跟 bindtextdomain, textdomain 的 PACKAGE 對應, 也和 po 的檔名相對應)
  • PS: 如果 _(), gettext() 包的不只是英文, 需要加 --from-code=encoding, ex: xgettext --from-code=UTF-8 -d hello hello.php
  • cp hello.po locale/zh_TW/LC_MESSAGES/hello.po
  • cp hello.po locale/zh_CN/LC_MESSAGES/hello.po
  • cp hello.po locale/en_US/LC_MESSAGES/hello.po
  • PS: locale/*/LC_MESSAGES/*.po 都是不需要的, 只需要 mo 檔, 但是放 po 在裡面是之後要修改並產生會比較方便.
修改 hello.po 檔(翻譯此檔案)
  • 翻譯主要就是要翻譯 locale/*/LC_MESSAGE/*.po 檔
  • vim locale/zh_TW/LC_MESSAGES/hello.po
  • vim locale/zh_CN/LC_MESSAGES/hello.po
  • vim locale/en_US/LC_MESSAGES/hello.po

修改 hello.po 上面的 header 處, 做以下幾個步驟:(1~5 不做沒關係, 但是 第 6 步 一定要做)

  1. 移除 #, fuzzy
  2. "Project-Id-Version: PACKAGE VERSION\n" => PACKAGE VERSION 改成自己 Project name 和版本, ex: hello-0.1
  3. "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" => ex: "PO-Revision-Date: 2007-09-16 20:08+0800\n"
  4. "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" => ex: "Last-Translator: Jon <jon@email_address>\n"
  5. "Language-Team: LANGUAGE <LL@li.org>\n" => ex: "Language-Team: Chinese <LL@li.org>\n"
  6. 將 "Content-Type: text/plain; charset=CHARSET\n" 修改成"Content-Type: text/plain; charset=UTF-8\n"
  7. 翻譯會看到 msgid "Hello World!",下面有 msgstr "",就是要修改 msgstr這邊,改成要翻譯的字串.
  8. ex: msgid "Hello World!", msgstr "嗨, 世界!",這樣子之後 "Hello World!" 就都會被換成"嗨, 世界!"
產生 hello.mo 檔
  • msgfmt -o hello.mo hello.po # 這個不是重點, 下面三行才是重點, 直接產生各種翻譯完的 mo 檔
  • msgfmt -o locale/zh_TW/LC_MESSAGES/hello.mo locale/zh_TW/LC_MESSAGES/hello.po
  • msgfmt -o locale/zh_CN/LC_MESSAGES/hello.mo locale/zh_CN/LC_MESSAGES/hello.po
  • msgfmt -o locale/en_US/LC_MESSAGES/hello.mo locale/en_US/LC_MESSAGES/hello.po
  • gettext 看的是 mo 檔, 不是 po 檔 🙂
測試

連到 http://Project_URL/hello.php,然後下面有三個連結, 都點點看, 應該就會看到各種不同語系的呈現了.

另一種設置法 所有檔案使用同一個翻譯檔(messages.po)

  1. xgettext *.php (第一次建議要加 -d NAME, ex: xgettext -d messages *.php)
  2. PS: 如果 _(), gettext() 包的不只是英文, 需要加 --from-code=encoding, ex: xgettext --from-code=UTF-8 *.php
  3. vim messages.po # 刪掉 #,fuzzy , 並修改表頭那些資訊
  4. msgfmt -cv messages.po # -cv 是 check 並列出來有哪些錯誤(若都不改表頭, 會出現不少錯誤)
  5. cp messages.po locale/zh_TW/LC_MESSAGES/ # 同理, 修改完 po 檔 cp 到 zh_CN/en_US
  6. msgfmt -o locale/zh_TW/LC_MESSAGES/messages.mo locale/zh_TW/LC_MESSAGES/messages.po # 同理, 可做 zh_CN/en_US
  7. 再來同上面的測試, 一樣測法.

修改檔案, 將新的翻譯合併(msgmerge)回原本的翻譯檔(po檔)

  1. 現在將 hello.php 裡面的 33, 34 行註解拿掉, 主要是 "echo _("New, line!");" 這行, 這代表有新增一個需要翻譯的資料.
  2. xgettext -d hello hello.php
  3. vim hello.po # 改 Content-Type 為 UTF-8, 要做翻譯可於此處就先做
  4. msgmerge -o locale/zh_TW/LC_MESSAGES/hello.po locale/zh_TW/LC_MESSAGES/hello.po hello.po # 合併到原始翻譯檔
  5. vim locale/zh_TW/LC_MESSAGES/hello.po # 若上一步沒翻譯, 則於此時做翻譯
  6. msgfmt -o locale/zh_TW/LC_MESSAGES/hello.mo locale/zh_TW/LC_MESSAGES/hello.po
  7. 這樣子就更新完成囉!

快速整理流程和做法

  1. xgettext -d hello hello.php 或 xgettext *.php (若包含非英文需加上 --from-code=UTF-8)
  2. vim *.po # 最懶的改法只要將 CHARSET 改成 UTF-8 即可.
  3. msgfmt -o hello.mo hello.po 或 msgfmt -o messages.mo messages.po (或 msgfmt -cv messages.po, -cv 會做較嚴格的檢查)
  4. 將 *.mo 放到 locale/*/LC_MESSAGES/ 去即可.

快速整理 更新合併 po 檔流程

  1. xgettext -d hello hello.php 或 xgettext *.php (若包含非英文需加上 --from-code=UTF-8)
  2. vim *.po # 將 CHARSET 改成 UTF-8, 並對新的做翻譯
  3. msgmerge -o hello.po locale/zh_TW/LC_MESSAGES/hello.po hello.po #合併, msgmerge -o "合併完要存的檔名" "現在使用的檔(要跟此檔合併)" "新的po檔案"
  4. vim *.po # 找看看有沒有 "#,fuzzy", 有的話就手動處理
  5. msgfmt -o hello.mo hello.po # 建立 mo 檔
  6. 將 *.mo 放到 locale/*/LC_MESSAGES/ 去即完成更新

附註

  1. 每次編 po 檔時, 請都要注意 #, fuzzy 的字, 這代表需要去人為修改, 改完後記得把 #,fuzzy 拿掉.(不管是剛開始的新檔案, 或是之後 msgmerge, 只要有需要人為介入的, po 檔 就會產生 #,fuzzy, 告訴你該去關心一下)
  2. 若要翻譯的是此 目錄*.php 和 多個子目錄的*.php, 可用 xgettext --from-code=UTF-8 */*.php *.php
  3. xgettext -d NAME, 此 NAME 會等同於 bindtextdomain(), textdomain(), NAME.po, 任何一個沒對應到, 翻譯結果就出不來.
  4. _() 不能包括變數($var), 如果 _($var) 有包到變數, 那這行就不會被 xgettext parse 出來.
  5. 要判斷要用哪種語系, 判斷法目前想到的有下面幾種:
    • PHP 版
      1. $_SERVER["HTTP_ACCEPT_LANGUAGE"]: zh-TW,zh,en-US,en; ...
      2. $_SERVER["HTTP_ACCEPT_CHARSET"]: Big5,utf-8;q=0.7,*;q=0.7
    • Javascript 版(可用 document.write() 印出來看看)
      1. navigator.browserLanguage # 我測試是 undefined
      2. navigator.userLanguage # 我測試是 undefined
      3. navigator.language # zh-TW

參考

Save


關於 Tsung

對新奇的事物都很有興趣, 喜歡簡單的東西, 過簡單的生活.
本篇發表於 Programming。將永久鏈結加入書籤。

使用 gettext 來實做 PHP 多國語系支援(I18N) 有 27 則回應

  1. Ben 說道:

    關於你這篇的內容 , 很詳細 可是似乎我還是不大理解...能不能.再次請教你

  2. Tsung 說道:

    隨時請問囉 🙂

  3. TR@SOE 說道:

    首先,感谢您链接了我的BLOG的文章:PHP中用gettext實現i18n,不过由于最近我的blog重新创建,这篇文章的连接已经改为:http://www.rsywx.net/wordpress/?p=161,还请拨冗修改。
    其次,您提到:語系切換主要是靠 putenv, setlocale.(程式裡面現在是用 $lang 去切換語系, 如何切換可再想更好的方法)。在我后来的实现方式中,直接使用客户端IP从而map到一个地区,从而进一步确定该地区的语种(缺省当然还是用en_US),这样用户在登陆时界面语言是自动调整了。当然这个有利有弊,仅供参考。

  4. Tsung 說道:

    喔喔, 但是這會有個小問題, 就是如果美國人來到台灣, 但是這樣子就會抓到中文, 不過或許可以整合一起當條件判斷~ 🙂

  5. Tsung 說道:

    我現在的作法還是先抓瀏覽器預設的語系, 我是覺得這樣子會是比較符合他自己語系的值, 參考看看囉~ 🙂

  6. Jerry 說道:

    我在linux下,使用gettext,沒有什么太大問題,可以正常輸出
    可是在 windows下,卻一直有個問題在困擾著
    環境是:Windows XP + Apache 2.2 + PHP 5.2
    問題是,使用gettext返回來的字符串都是 gbk編碼,請教這是什么問題?
    服務器也支持iconv,不知道是否還有其他什么需要設置的? 請指教
    我的郵箱 jerry2801@gmail.com

  7. Tsung 說道:

    應該是你裡面有中文字, gbk 應該是你系統編碼, 可以用 xgettext --from-code=UTF-8 指定編碼看看~ 🙂

  8. dennis 說道:

    問題同 http://twpug.net/modules/newbb/viewtopic.php?post_id=13039 差不多.
    我是用 PoEdit 來做的多語,
    messages.po 中的設定如下:
    "MIME-Version: 1.0\n"
    "Content-Type: text/plain; charset=utf-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=2; plural=n==1?0:1;\n"
    "X-Poedit-Language: Chinese\n"
    "X-Poedit-Country: TAIWAN\n"
    "X-Poedit-SourceCharset: utf-8\n"
    PHP 代碼
    function _initLocal($lang_code,$mo_filename,$localfile_url)
    {
    putenv("LANG=$lang_code");
    setlocale(LC_ALL, $lang_code);
    $package = $mo_filename;
    bindtextdomain($package, $localfile_url.'/local');
    textdomain($package);
    bind_textdomain_codeset($package, 'UTF-8');
    }
    _initLocal('zh_TW','messages',APPLICATION_PATH);

  9. Kate 說道:

    不好意思..可以請問2個問題嗎?
    1. 是不是得要以英文會輸入的內容..才會正常作轉換呢?..(在其他的站上..看到這樣的敘述)
    2. 如果要使用的話..是不是要建立MO檔作串連?..不知道能不能再介紹更詳細的使用方法呢??..因為看完了大大的文章之後..還是不會用..嗚..超笨的啦~..(OS:Windows XP)
    謝謝你~~~..

  10. kevin 說道:

    我用gettext_example.tgz這個sample直接放在我的www\下, 解壓後開啟hello.php點click不同的連結但都無法轉換不同語言?? 為什么會這樣?
    已開啟php.ini中的設定!

  11. Tsung 說道:

    我不確定 Windows 能不能跑 gettext 耶.
    確定 Linux 下是可以跑的. Orz..

  12. Tsung 說道:

    不一定要是英文, 中文也可以.
    不過, 我不確定 Windows 能不能跑. Orz..

  13. pofeng 說道:

    我也遇到這個問題
    ( for portable sahana on Wwin32 )

    有人有空試試這個解法嗎 ?

    http://www.kipras.com/getting-gettext-to-work-in-apache-on-windows/96

  14. Tsung 說道:

    確定 Windows 跑 gettext 是可以正常使用的. 🙂

  15. please 說道:

    您好,參考您的附註第2筆
    我輸入
    xgettext --from-code=UTF-8 */*.php *.php
    並沒有生成該目錄下的檔案及所屬子目錄檔案中該翻譯字串的po檔
    報出的訊息是
    xgettext: error while opening "*/*.php" for reading: invalid argument
    請問指令有沒下對嗎?謝謝

  16. please 說道:

    在windows下輸入
    xgettext --from-code=UTF-8 *\*.php *.php
    錯誤訊息
    xgettext: error while opening "*\*.php" for reading: invalid argument
    應該是xgettext不接受這個參數: "*\*.php"

  17. c9s 說道:

    為了解決這個麻煩的步驟,最近寫了 App::Po ,可以參考看看。 🙂
    http://github.com/c9s/App-Po/
    也不需要重新建立或手動管理了。

  18. Tsung 說道:

    頁面不存在耶. @.@a..

  19. planetoid 說道:

    作者改名成App::I18N http://github.com/c9s/App-I18N (Rename App::Po to App::I18N)

  20. eric 說道:

    我在linux cli下為什麼不能顯示繁體中文呢?簡體中文是可以顯示的。

  21. hyatt 說道:

    不好意思
    請問如何用 javacript 讀取mo檔
    感謝

  22. steven 說道:

    使用您的範例中,如果一直重新整理,或是點中文的部份(不管簡繁)

    會出現有時是亂碼,有時可以的情況發生,請問有可能是什麼樣的問題呢?

  23. chris 說道:

    請問我hello.php在新增一行 "echo _("Home");" 然後hello.po用notepade++新增

    msgid "Home"
    msgstr "首頁"

    然後再用Poedit開啟hello.po存檔產生hello.mo檔,之後開啟hello.php結果Home字眼無法翻譯,請問這是甚麼問題呢?謝謝

發表迴響