# 用 Perl 進行網站本土化

唐宗漢
傲爾網
2002年12月

## 摘要

對應用程式進行國際化（Internationalization，i18n）的目的，是在使它支援多種語言、日期、貨幣格式，以及各地的習俗等「地區設定（locale）」。接下來，本土化（Localization，l10n）則負責實際將軟體轉譯，以切合特定地區的使用者需求。當前，網頁應用程式（Web Application）由於以文字作為界面描述的格式，已成為最熱門的本土化對象之一。

在自由軟體的世界裡，許多最具彈性、最受歡迎的技術，是用Perl語言開發的。對網站應用程式的開發者來說，Perl也是長期以來的不二選擇。本篇文章是筆者對Perl應用程式進行中譯的經驗談，包含實作方式、常用工具的優劣之處，以及管理本土化專案時需注意的事項。

## 簡介

「在這個世界上，人類的語言為數不少。」
－－Harald Tveit Alvestrand，RFC 1766，『語言識別標記』

網站及網頁應用程式，為什麼要作本土化呢？

請讀者想像一下：假設有人在全球資訊網上提出這個問題，許多人用不同的語言提出意見、彼此討論。身為其中的一份子，你可能會聽到這些論點：

圖1: 本土化的理由(中文化之前)
Иностранная валюта, формат даты, язык и обычаи могут казаться нам пугающими Menschen sind produktiver wenn sie in ihrer gewohnten Umgebung arbeiten Tas veicina daudz labâku sapraðanu un mijiedarbîbu starp daâdâm kultrâm Un progretto con molti collaboratori internazionali si evolverá piú in fretta e meglio 地区化的过程, 有助於软件的模块化与可移植性

不幸的是，這些論點並非每個人都看得懂。這就造成了「語言障礙」－－能共同討論的對象，往往侷限在少數語言相同、文化相近的「地區社群」中。

然而，我們看不懂的論點往往很有道理，並且能帶來新的省思。所以，最好能有人將這些意見、操作界面及其他資料翻譯成我們看得懂的文字：

圖2: 本土化的理由(中文化之後)
要遷就外地的語言、日期、貨幣及習俗，是件煩人的事。 人在熟悉的操作環境下，會比較有生產力。 它能促進文化間的瞭解與交流。 擁有許多國際同好的專案，進步的速度更快，品質也更好。 本土化的過程，有助於軟體的模組化與可移植性。

正如同上列論點所述，通常無法要求所有人「書同文」，一律採用拉丁語、通用語、世界語、邏輯語或是英文。這時，就需要本土化了。

對專屬軟體而言，本土化通常是打入國外市場的先決條件。若是某地的預期利潤低於本土化的成本，廠商便不會進行翻譯；在沒有源碼的情況下，當地的使用者要自己進行翻譯，便是件困難（也可能違法）的任務。如果廠商設計軟體時，完全沒有考慮到國際化的架構，那更是回天乏術。

相較之下，對開放源碼的應用程式進行本土化，則要簡單多了。和專屬軟體一樣，早期的版本通常祗為單一語言而設計；不同的是，任何人都可以隨時加上國際化的架構。誠如Sean M. Burke所述：

> 「要讓開放源碼做得更好，我們可以在寫程式時多加留意，讓程式員和熱心的使用者，都能輕易進行本土化。（話說回來，「開放源碼」的本意，就是讓任何有意願、有技術的人，都能成為程式員。）」

本文敘述的技巧，能有效降低本土化的難度。雖然重點放在Perl寫的網站應用程式上，但是其中的原則應該也適用於其他地方。

## 對靜態網站進行本土化

「它一定得動起來。」
－－網絡第一真理，RFC 1925

網頁可以粗分為兩種：「靜態」網頁直到下一次更新為止，隨時都提供相同的內容；「動態」網頁則依據各種因素，提供相應的資訊。通常我們稱前者為「網頁文件」，稱後者為「網頁應用程式」。

不過，對不同的使用者來說，靜態網頁並不一定得保持相同的「呈現方式」－－它的語言、樣式或媒介可以因人而異。（例如，視障人士可能會偏好用語音，來代替影像輸出。）全球資訊網的長處之一，就在它能讓客戶端與伺服器進行交涉，決定最合適的呈現方式。

舉個實例來說，假設筆者有個中文網頁，放在`http://www.autrijus.org/index.html`：

列表1. 基本的中文網頁
    <html><head><title>唐宗漢 - 家</title></head>
    <body>施工中, 請見諒</body></html>

有天我心血來潮，想要將它譯成英文版給外地的朋友看：

列表2. 翻譯英文的網頁
    <html><head><title>Audrey.Home</title></head>
    <body>Sorry, this page is under construction.</body></html>

這時，許多網站會提供一個「語言選擇頁面」，讓訪客挑選適合的語言，如下所示：

圖3: 典型的語言選擇頁面
Please choose your language:
Čeština | Deutsch | English | Español
Français | Hrvatski | Italiano | 日本語
한국어 | Nederlands | Polski | Русский язык
Slovensky | Slovensci | Svenska | 中文 (GB) 中文 (Big5)

對一般的使用者和程式來說，這樣的頁面都模糊不清、多餘而麻煩。它不但迫使每位訪客多按一個鍵，也對網頁代理程式的作者造成障礙：剖析這一頁的結構、選擇正確的鏈結，是件很容易出錯的事。

### MultiViews：最簡單的本土化架構

當然，要是能讓每個人自動取得適合的語言，那是最好了。HTTP 1.1版提供的「內容交涉（Content Negotiation）」功能，就是達成這個目標的好辦法。

在內容交涉的架構下，瀏覽器會傳送「`Accept-Language`」標頭，代表使用者偏好的語言。舉例來說，「`zh-tw, en-us, en`」的意思就是「正體中文、美式英文，不然就是英文」。

網站伺服器接到這項資訊之後，便負責挑選最合適的語言版本，傳回給使用者。實作此項流程的方式，各種伺服器可能有所不同；在最受歡迎的Apache下，可以利用「`MultiViews`」技術來達成。

要使用`MultiViews`時，我們將英文版存成`index.html.en`（注意後面的`.en`），再到`httpd.conf`或`.htaccess`設定檔裡，加上這一列：

    	Options +MultiViews

在此之後，Apache就會在接到`http://www.autrijus.org/index.html`的要求時，檢查客戶端是否於`Accept-Language`裡偏好英文（`en`）。這樣一來，英語系的讀者就會看到英文頁面，其他人則看到原本的`index.html`。

利用這個技術，我可以請國際友人幫忙，逐漸加上新的翻譯版本－－法文版是`index.html.fr`，`index.html.he`代表希伯來文等等。

由於網路上的許多人，都祗會自己的母語和英文，因此新的版本通常都不是從中文，而是由英文翻譯過去的。不過，既然中英文的內容相同，這也不成問題。

...真的沒問題了嗎？要是我哪天更新了中文版呢？

### 保持翻譯時效並不容易

在我改完原本的網頁之後，就會馬上發現，負責法文和希伯來文的朋友沒辦法看懂中文－－顯然，我有必要準備一份英文的「標準版本」。許多自由軟體專案正是因為這樣，就算核心團隊的母語不是英文，仍然採用英文作為程式的預設語系。

此外，就算祗是改了背景顏色（像是`<body bgcolor="gold">`），我還是得更新所有的譯本，好讓格式保持一致。

若是我同時更新格式和內容，事情就麻煩了。一旦失去了原先的HTML標籤，幫忙翻譯的朋友就必需從頭來過！除非他們都是HTML大師，不然馬上就會出錯。要是網站上有20個經常更新的頁面，那很快就沒人肯做翻譯－－至連朋友也當不成了。

從上面的例子看來，顯然有必要將資料與源碼（也就是文字和標籤）分開，並且自動化翻譯版本的生產流程。

### 利用CGI.pm將資料與源碼分開

事實上，上一段已經一語道盡了現代的國際化（i18n）流程：在進行網站應用程式本土化之前，必須先找出區分資料與源碼的方法。

多年以來，Perl就是網頁開發的首選語言，也提供了多不勝數的的模組與網站建製工具。其中最普遍的，要算是自1997年以來就併入標準程式庫的`CGI.pm`。底下的範例程式，就是利用`CGI.pm`來自動產生翻譯版本：

列表3. 利用MultiViews及CGI.pm進行本土化
    use CGI ':standard'; # 此程式的模版系統（Templating System）
    foreach my $language (qw(zh_tw en de fr)) {
        open OUT, ">index.html.$language" or die $!;
        print OUT start_html({ title => _("Audrey.Home") }),
    	      _("Sorry, this page is under construction."),
    	      end_html;
        sub _ { some_function($language, @_) } # XXX: 需補上本土化架構
    }

此程式利用`CGI.pm`的HTML相關函式，達成資料與源碼的分隔，這點與單純的HTML頁面不同。標籤（如`<html>`）變成了函式呼叫（`start_html()`），文字則以字串表示。因此，在產生每份本地化的頁面時（`index.html.zh_tw`、`index.html.en`等等），就能保持相同的HTML格式。

「`sub _`」這個函式負責叫用`some_function()`，將輸入的英文字句翻譯成`$language`變數所代表的語言。`some_function()`就是此程式的「本土化架構（localization framework）」；在下一節裡，我們會介紹三種不同的架構。

寫完上面這段小程式後，我們祗需用`grep`找出所有「`_(...)`」裡的字串，將它們解到一份詞典（lexicon）檔裡，再請譯者填入所需的翻譯即可。在此，「詞典」的定義是兩種語言之間的對照表：其中有些項目祗有一個單字（如「`Cancel`」），但通常則包含整個句子（如「`Do you want to overwrite?`」或「`5 files found.`」）。和觀光手冊上的「外語速成」一樣，詞典裡的字串也許還有待填的空白，如同下列的中文╱海地語詞典所示：

圖4: 中文 => 海地語詞典
中文 | Haitian
這個東西要 ___ 塊錢。 | Bagay la kute ___ dola yo.

在理想情況下，譯者祗需要翻譯詞典的內容，不用管HTML或程式碼裡寫些什麼。可是，由於各種本土化架構的詞典格式彼此不同，我們得先選出最適合這項專案的架構。

## 本土化架構一覽

「它比你想像中還複雜。」
－－網絡第八真理，RFC 1925

要實作上一段裡的`some_function()`函式，需要能讀取詞典檔、查出相符的字串、甚至將新的項目解到詞典裡的程式庫。這樣的程式庫，就稱為「本土化架構」。

依筆者的經驗，本土化架構之間的不同，通常表現在詞典檔結構的差異上。接下來，讓我們看看Perl如何使用其中的三種架構，從最簡單的`Msgcat`開始。

### Msgcat：用陣列當詞典

Msgcat是最早的本土化架構，也是XPG3╱XPG4標準的一部份，因此在Unix平台上隨處可得。它是第一代的詞典檔架構：將各個字串以編號表示，循序存放在名為「訊息清單（message catalog）」的陣列裡。這種架構容易實作、節省記憶體，並且查詢速度很快。在Windows等平台上的「資源檔（resource file）」也是基於相同的概念。

對於每個網頁及程式檔，Msgcat都需要一份相應的詞典檔，格式如下：

列表4. `Msgcat` 詞典檔
    $set 7 # $Id: nls/de/index.pl.m
    1 Audrey'.Haus
    2 Wir bitten um Entschudigung. Diese Seite ist im Aufbau.

這份檔案包含index.html裡所有字串的德文翻譯；它的「集合編號（set id）」是7。在做完所有頁面的翻譯之後，我們利用gencat這支程式來產生二進制的詞典檔：

    	% gencat nls/de.cat nls/de/*.m

二進制詞典檔的內容，可以想成是如下所示的二維陣列：

圖5: `nls/de.cat` 的內容
set_id msg_id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
1 | ... | ... | ... | ... | ... | ... | `Audrey'.Haus` | ... | ...
2 | ... |  | ... | ... | ... | ... | `Wir bitten um Entschudigung...` | ... | ...
3 | ... |  | ... | ... |  | ... | ... | ... | ...

要從詞典檔中讀取字串，就得利用CPAN（Perl 綜合典藏網）上的`Locale::Msgcat`模組，來實作前面提到的「`sub _`」：

列表5. 示範 `Locale::Msgcat` 的用法
    use Locale::Msgcat;
    my $cat = Locale::Msgcat->new;
    $cat->catopen("nls/$language.cat", 1); # 想成是二維陣列
    sub _ { $cat->catgets(7, @_) } # 7 是 index.html 的集合編號（set_id）
    print _(1, "Audrey.House");  # 1 是這串文字的詢息編號（msg_id）

要注意的是，祗有第一個參數（`msg_id`）有作用；接在後面的`"Audrey.House"`祗是作為查詢失敗時的預設傳回值，以及讓人比較容易看懂而已。

因為`set_id`及`msg_id`的組合必須獨一無二、不能更改，因此更新時祗能刪除某個編號，而無法重複利用。這項特性往往造成更新困難，正如Drepper等人在GNU `gettext`說明文件裡指出的：

> 「程式員每次遇到要翻譯的字串時，都得先定義一個數值（或常數符號），再將它加進訊息清單裡。他還得費心避免重複的字串、重複的訊息編號等等。如果想具有與GNU `gettext`一樣的訊息清單品質，還得在裡面加上說明，並註明程式裡各個字串的位置。這簡直是『不可能的任務』。」

因此，祗有在詞典十分穩定的情況下，纔可以考慮使用`Msgcat`作為本地化架構。

採用`Msgcat`的程式，還經常遇到「複數形式」的問題。請看下列程式：

列表6. 錯誤的複數形式處理
    printf(_(8, "%d files were deleted."), $files);

當`$files`的值為`1`時，輸出的訊息顯然是錯誤的，而`"%d file(s) were deleted"`也不合英文文法。因此，我們非得分成兩個字串表示不可：

列表7. 祗考慮英文的複數形式處理
    printf(($files == 1) ? _(8, "%d file was deleted.")
    		     : _(9, "%d files were deleted."), $files);

但就算這樣寫，在英文以外的語言仍然行不通－－法文的單數形式在`$files`為`0`時也適用，而斯拉夫語系的複數形式更多達三到四種！想在`Msgcat`的架構下照顧到這些狀況，必然是徒勞無功。

### Gettext：用雜湊當詞典

為了解決`Msgcat`的諸多問題，Ulrich Drepper在1995年參考Uniforum的`Gettext`界面，為GNU計劃開發了一套本土化系統。時到如今，在以C語言開發的自由軟體當中，GNU `gettext`已儼然成為本土化的標準架構，也廣受C++、Tcl及Python程式員的歡迎。

Gettext不需要為每份源碼檔案準備各自的詞典，而是為整個專案，針對每種語言各製作一份詞典（又稱作「PO檔」）。舉例來說，上述網頁的德文詞典檔「`de.po`」可能內容如下：

列表8. `Gettext` 詞典檔
    #: index.pl:4
    msgid "Audrey.Home"
    msgstr "Audrey'.Haus"

    #: index.pl:5
    msgid "Sorry, this site is under construction."
    msgstr "Wir bitten um Entschudigung. Diese Seite ist im Aufbau."

以「`#:`」開頭的兩列是由xgettext這支程式自動產生的。該程式會找出源碼裡呼叫`gettext()`的地方，將它們依序解到詞典檔裡。

接下來，我們執行`msgfmt`，從`po/de.po`產生二進制的詞典檔`locale/de/LC_MESSAGES/web.mo`：

    	% msgfmt locale/de/LC_MESSAGES/web.mo po/de.po

這樣一來，程式就可以用CPAN上的`Locale::gettext`模組來存取二進制執行檔，如下所示：

列表9. 示範 `Locale::gettext 的用法`
    use POSIX;
    use Locale::gettext;
    POSIX::setlocale(LC_MESSAGES, $language); # 設定目的語言
    textdomain("web"); # 通常和程式名稱相同
    sub _ { gettext(@_) } # 祗是 gettext() 的簡寫
    print _("Sorry, this site is under construction.");

新版的`gettext`（glibc 2.2版以上）更提供了`ngettext("%d file", "%d files", $files)`的複數形式處理；不過，`Locale::gettext`模組目前還未支援此項界面。筆者已將修正檔送出，希望該模組的作者會盡快處理。

Also, `gettext` lexicons support multi-line strings, as well as reordering via `printf` and `sprintf`: 此外，`gettext`的詞典檔也支援多列字串，以及利用`printf`與`sprintf`函式達成的更換變數順序功能：

列表10. 使用編號參數的多列字串
    msgid ""
    "This is a multiline string"
    "with %1$s and %2$s as arguments"
    msgstr ""
    "これは多線ひも変数として"
    "%2$s と %1$s のである"

最後，GNU `gettext`套件附有相當完整的工具集（`msgattrib`、`msgcmp`、`msgconv`、`msgexec`、`msgfmt`、`msgcat`、`msgcomm`...），大幅簡化了合併、更新、管理詞典檔的流程。

### Locale::Maketext：用物件當詞典！

`Locale::Maketext`於1998年由Sean M. Burke所開發，並於2001年5月修訂後，併入Perl 5.8版的核心程式庫。

此模組與`Msgcat`及`Gettext`等函式導向的架構不同，是以物件導向的設計，讓「`Locale::Maketext`」作為抽象的基底類別，衍生出用於特定專案的「專案類別」。專案類別（名稱如「`MyApp::L10N`」）進一步衍生出數個「語言類別」，如「`MyApp::L10N::it`」、「`MyApp::L10N::fr`」等等。 >

所謂的語言類別，就是具有全域`%Lexicon`雜湊的Perl模組。`%Lexicon`裡的鍵是原文（通常是英文）的字串，雜湊值則是翻譯過的字串。語言類別還可以定義某些方法，來處理詞典中的文法變換等事項。

請見底下範例：

列表11. `Locale::Maketext` 詞典檔及使用範例
    package MyApp::L10N;
    use base 'Locale::Maketext';

    package MyApp::L10N::de;
    use base 'MyApp::L10N';
    our %Lexicon = (
        "[quant,_1,camel was,camels were] released." =>
        "[quant,_1,Kamel wurde,Kamele wurden] freigegeben.",
    );

    package main;
    my $lh = MyApp::L10N->get_handle('de');
    print $lh->maketext("[quant,_1,camel was,camels were] released.", 5);

利用Maketext的「方括號語法」，譯者可以在字串中取用各種文法函式。上面的例子示範了內建的複數形式與量詞支援；對於需要特別處理複數形式的語言，祗需實作相應的`quant()`函式即可。同樣的，也很容易加入轉換序數與時間格式的功能。

每個語言類別，還可以定義自己的`->encoding`方法，描述其中的詞典編碼；這可以用來傳給「`Encode`」模組，進行即時轉碼。語言類別間也可以相互繼承：`fr_ca.pm`（加拿大法語）裡缺少的字串，預設會由`fr.pm`（一般法語）補上。

內建的`->get_handle()`方法，在沒有參數時，會自動在CGI、mod_perl與命令列下，偵測HTTP、POSIX及Win32的地區設定；程式員毋須再做偵測，就可以立刻呈現適合的語系給使用者。

不過，「`Locale::Maketext`」並非完美無缺。它最大的問題，就是缺乏如GNU `gettext`的工具集；也因為詞典檔的語法太有彈性，使得編輯器難有像Emacs的「`PO Mode`」這樣的支援模式。

最後，因為採用Perl模組作為詞典檔，導致譯者必須熟悉基本的Perl語法－－不然的話，就得有人幫忙做格式轉換了。

### Locale::Maketext::Lexicon：兩全其美

2002年5月，有感於`Locale::Maketext`詞典太過鬆散的格式，我著手實作公司內部使用的詞典格式，並在perl-i18n郵遞論壇上徵詢大家的意見。Jesse Vincent問道：「為什麼不直接使用`gettext`的PO檔格式呢？」於是我就設計了抽換式的後端系統，能接受各種不同格式的詞典。這樣一來，「`Locale::Maketext::Lexicon`」就誕生了。

此模組設計時的初衷，在結合`Locale::Maketext`彈性的表達式，與廣泛支援的`gettext`或`Msgcat`檔案格式。它也支援繫結（Tie）界面，讓詞典也能存放在關聯式資料庫，或DBM檔中。

底下的應用程式，示範了`Locale::Maketext::Lexicon`的數種用法，以及「Gettext」後端所支援的延伸PO格式：

列表12. `Locale::Maketext::Lexicon` 的範例程式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    use CGI ':standard';
    use base 'Locale::Maketext';      # 繼承 get_handle()

    # 各種詞典格式及來源
    use Locale::Maketext::Lexicon {
        en => ['Auto'],              fr    => ['Tie' => 'DB_File', 'fr.db'],
        de => ['Gettext' => \*DATA], zh_tw => ['Gettext' => 'zh_tw.mo'],
    };

    # 對 main 的各個子類別，定義該語言裡的序數函式
    use Lingua::EN::Numbers::Ordinate; use Lingua::FR::Numbers::Ordinate;
    sub en::ord { ordinate($_[1]) } sub fr::ord { ordinate_fr($_[1]) }
    sub de::ord { "$_[1]." }        sub zh_tw::ord { "第 $_[1] 個" }

    my $lh = __PACKAGE__->get_handle; # 自動取得目前的地區設定
    sub _ { $lh->maketext(@_) }       # 如有需要，也可以自動進行轉碼

    print header, start_html,         # [*,...] 是 [quant,...] 的簡寫
    	_("You are my [ord,_1] guest in [*,_2,day].", $hits, $days), end_html;

    __DATA__
    # 以延伸 PO 格式寫成的德文詞典
    msgid "You are my %ord(%1) guest in %*(%2,day)."
    msgstr "Innerhalb %*(%2,Tages,Tagen), sie sind mein %ord(%1) Gast."

程式的第2列讓`main`套件繼承`Locale::Maketext`，以取得`get_handle`方法。第5∼8列定義了四個語言類別，各自採用不同的詞典格式及來源：

- 「Auto」後端告訴`Locale::Maketext`無須特別處理英文－－直接將接到的雜湊鍵傳回即可。此後端特別適合剛開始撰寫程式、還不想處理本地化的狀況。
- 「Tie」後端將法文的`%Lexicon`雜湊繫結到一個Berkeley DB檔；其中的字串在需要時纔會取用，因此不會浪費額外的記憶體。
- 「Gettext」後端從磁碟上讀取編譯過的二進制MO檔，作為中文的詞典；此外，它也從`DATA`檔案代號取得PO檔格式的德文詞典。

第11∼13列實作各個語言類別的`ord`方法，負責將參數轉換為該語言的序數（如1st、2nd、3rd...）。英文與法文採用了兩個CPAN模組達成，而德文及中文則祗需要字串安插即可。

第15列取得目前套件的「語言代號（language handle）」物件。因為沒有給定參數，它會自動偵測HTTP_ACCEPT_LANGUAGE環境變數、POSIX的`setlocale()`設定，以及Windows環境下的`Win32::Locale`。第16列實作了簡單的本地化函式，將參數直接交給該物件的`maketext`方法處理。

最後，第18∼19列會印出一則經由本地化處理的訊息。第一個參數`$hits`會交給`ord`方法，而`$days`則交給內建的`quant`方法處理－－「`[*...]`」是前面提過的「`[quant,...]`」的簡寫。

第22∼24列是以延伸PO格式寫成的範例詞典。除了藉由`%1`及`%2`調動參數順序之後，它也支援「`%function(args...)`」語法，代表`Locale::Maketext`語法中的「`[function,args...]`」；args裡出現的`%1`、`%2`等變數，都會自動替換成`_1`、`_2`等等。

## 實例討論

「沒有大小通喫這回事。」
－－網絡第十真理，RFC 1925

瞭解了本土化架構的原理後，讓我們來看看它們如何應用到實際的應用程式上。

對網站應用程式來說，本土化架構幾乎都在「呈現系統」（或稱「模版系統」）上實作，因為它決定了應用程式如何分隔資料與源碼。舉例來說，「Template Toolkit」鼓勵乾淨的三層式資料/源碼/模版架構；同樣受歡迎的「Mason」系統則傾向將Perl程式碼內嵌在模版裡。這一節裡，我們會看到適用於這兩種架構的本土化方式；其中的原則，也同樣適用於「AxKit」、「HTML::Embperl」等模版系統上。

### Request Tracker（Mason）

「Request Tracker」是第一個以`Locale::Maketext::Lexicon`作為本土化架構的應用程式。它的「基底語言類別」是「`RT::I18N`」，子類別則由同一個目錄下的`*.po`檔案產生。

除此之外，它的`->maketext`方法也利用「`Encode`」模組（在5.8版以前的Perl，則是利用我的「`Encode::compat`」模組）直接傳回UTF-8編碼的資料。這樣一來，負責中文的譯者就可以用Big5碼編輯詞典檔，系統卻可以將它視為萬國碼（Unicode）處理。

在RT的Perl源碼裡，所有物件都利用從`RT::Base`繼承的`$self->loc`方法來翻譯訊息：

列表13. RT 的本土化架構
    sub RT::Base::loc
        { $self->CurrentUser->loc(@_) }
    sub RT::CurrentUser::loc
        { $self->LanguageHandle->maketext(@_) }
    sub RT::CurrentUser::LanguageHandle
        { $self->{'LangHandle'} ||= RT::I18N->get_handle(@_) }

如上所示，翻譯時採用的是現行使用者的語系，因此多個使用者可以藉由不同語言，同時執行這個程式。對Mason模版來說，則採用了兩種方式：

列表14. 兩種標示 Mason 模版中字串的方式
    % $m->print(loc("Another line of text", $args...));
    <&|/l, $args...&>Single line of text</&>

第一列的方式，用在內嵌的Perl源碼與`<%PERL>`段落之中；它會自動叫用現行使用者的`->loc`方法，交由上述的函式處理。

第二列則利用`HTML::Mason`模組所提供的「過濾元件（filter component）」功能，將中間的"Single line of text"傳給「`/l`」元件（也許還附加參數），最後再顯示該元件傳回的字串。底下是此元件的實作方式：

列表15. `html/l` 過濾元件的實作方式
    % my $hand = $session{'CurrentUser'}->LanguageHandle;
    % $m->print($hand->maketext($m->content, @_));

有了這些方式，祗要將既有模版裡的訊息解成詞典檔，再寄給譯者就行了。找出700多則訊息的工作約費時一個星期；整個國際化╱本土化的流程則花了不到兩個月的時間。

### Slash（Template Toolkit）

Slash（類似於Slashdot的自動說書首頁，Slashdot Like Automated Storytelling Homepage）是Slashdot背後的程式。不過，Slash更是完整的網站的建置環境，建立在Andy Wardley的「Template Toolkit」模組上。

基於TT2乾淨的設計，Slash將源碼與資料徹底分開，這點與RT╱Mason不同。因此，幾乎沒有必要在Perl程式檔裡進行本土化的工作。

在本文撰寫之前，有許多直接將模版翻譯而成的「本土化」版本，包括中文、日文、希伯來文等。然而，在新版釋出時需要做的合併（merge）動作非常困難（外掛程式就更別提了），因此翻譯版本往往延遲許久纔釋出。

這裡，我們提出一個較好的解決方式：在模版提供函式上架設「自動解譯層」，利用`HTML::Parser`及`Template::Parser`實作。它的功能如下所示：

列表16. TT2 解譯層的輸入與輸出
輸入 |
    <B>from the [% story.dept %] dept.</B>
輸出 |
    <B>[%|loc( story.dept )%]from the [_1] dept.[%END%]</B>

眼尖的讀者會發現，這樣的轉譯層會碰到跟`Msgcat`相同的同題－－要是我們想將`[% story.dept %]`轉成序數，或是把`dept.`依複數形式展開成`department`╱`departments`的話，該怎麼辦呢？同樣的問題，也出現在RT的網頁界面裡，需要翻譯從外部模組傳回的訊息時；舉例來說，`"Successfully deleted 7 ticket(s) in 'c:\temp'."`這樣的字串要怎麼翻呢？

筆者的解決方法，是開發`Locale::Maketext::Fuzzy`模組，用來將已經安插變數的字串，與詞典檔進行比對－－例如前述的字串，就可能符合`"Successfully deleted [*,_1,ticket] in '[_2]'."`這則訊息。要是符合的字串不止一個（畢竟，`"Successfully [_1]."`也算符合），該模組會依循經驗法則，找出最適合的答案。

搭配`xgettext.pl`這支程式，開發者可以為每套外掛程式及佈景主題附上各自的詞典檔，供Slash系統進行多層次的處理：先嘗試所屬外掛程式的詞典；接著再試佈景主題；最後纔以全域的詞典檔作為預設。

## 總結

「所謂盡善盡美，並非不能再加，而是不能再減。」
－－網絡第十二真理，RFC 1925

從以上的兩個例子裡，可以約略看出本土化的共通流程。這一節會介紹如何經由十個階段，來本土化既有的網站應用程式，以及一些相關的小祕訣。

### 本土化的流程

本土化的流程，可以依序總結成下列幾項步驟：

1. 評估網頁的模版系統。
2. 選擇一項本土化架構，將它與模版系統銜接上。
3. 用程式找出模版裡的文字字串，用過濾函式進行代換。
4. 解出測試用的詞典；手動修掉明顯的問題。
5. 手動找出源碼裡的文字字串，將它們代換成「`_(...)`」函式呼叫。
6. 再解出一份測試詞典，交由機器翻譯。
7. 實地測試本土化版本；修掉餘下的問題。
8. 解出試用版的詞典，寄給翻譯團隊校訂。
9. 修掉譯者回報的問題，寄出正式版的詞典！
10. 在每個新版釋出前，定期整理詞典裡新的項目，通知譯者進行翻譯。

照著以上的步驟，你就可以輕鬆地管理本土化的專案，並且保持翻譯時效、減少錯誤。

### 一些小祕訣

最後，這裡是對網頁與其他應用程式進行本土化時，幾點需要注意的事項：

- 在設計及實作階段，都應將資料與源碼分開。
- 不要在網站或程式成形前，就先設計國際化/本土化的架構。
- 避免內含文字的圖檔。
- 預留標籤和按鈕旁邊的空白－－避免讓操作介面過度擁擠。
- 使用完整的句子，而非片段：

列表17. 片段與完整句
    _("Found ") . $files . _(" file(s).");   # 不完整的句子－－錯了！
    sprintf(_("Found %s file(s)."), $files); # 完整句（利用sprintf）
    _("Found [*,_1,file].", $files);         # 完整句（用Locale::Maketext）

- 不同語境下的同樣字串，應該作出區分。舉例來說，RT裡的`"Home"`原本具有`"Homepage"`及`"Home Phone No."`等兩種用法。
- 平等對待譯者，別將他們視為下屬；避免擅自修改詞典檔。
- 先讓一個人譯出草稿，再請其他人修改。
- 盡量在詞典檔裡提供註解及描述資訊：

列表18. 詞典檔裡的註解
    #: lib/RT/Transaction_Overlay.pm:579
    #. ($field, $self->OldValue, $self->NewValue)
    # 請注意：底下的「changed to」意思是「已改為...」。
    msgid "%1 %2 changed to %3"
    msgstr "%1 %2 cambiado a %3"

利用`Locale::Maketext::Lexicon`套件中附的「`xgettext.pl`」這支程式，可以自動產生詞典檔中的源碼檔名、列號（以`#:`表示）、變數（以`#.`表示），並隨時更新。如上所示，對太短或意義不明的句子加上註解（以`#`表示），對翻譯也會有很大的幫助。

## 結論

對非英語系國家的人民來說，本土化往往是參與自由軟體專案的前提。以臺灣而言，CLE（中文Linux環境）、Debian-Chinese及FreeBSD-Chinese等本土化專案，都是社群貢獻的聚集焦點。然而，拜以英文為主的僵化軟體架構所賜，這往往也是耗時費力、容易出錯的工作。對譯者來說，「進入障礙」還是太高了些。

話說回來，在日漸昇高的網頁國際化趨勢下，網站應用程式經常有翻譯成多國語言的機會。舉例來說，Sean M.Burke在2002年領導熱心的使用者，將廣受歡迎的「Apache::MP3」線上點播模組譯成數十種語言的版本。模組的原作者Lincoln D. Stein完全未參與翻譯－－他祗要將修正檔和詞典併入下個釋出版本就行了。

自由軟體並非一堆抽象的程式，而是靠人們共享源碼、彼此提供建議的熱情而存在的。因此，我誠摰地希望本文介紹的技巧，能鼓勵程式員及使用者在被動翻譯之外，也能主動國際化既有的應用程式。

## 致謝

感謝Jesse Vincent建議我開發`Locale::Maketext::Lexicon`，以及讓我協助他設計RT的本土化架構。我也要感謝Sean M. Burke創造了`Locale::Maketext`，並鼓勵我實驗各種不同的詞典格式。

感謝我在傲爾網的同事們（簡信昌、高嘉良、翁千婷、林克寰）為本土化網頁應用程式付出的努力。也謝謝駱馬書（Perl學習手冊）的譯者群，讓我學到了分散式翻譯團隊的力量。

感謝Nick Ing-Simmons、小飼弾及Jarkko Hietaniemi教我如何利用`Encode`模組；Bruno Haible允許我借用他強大的GNU libiconv；以及宮川達彥對`Locale::Maketext::Lexicon`早期版本的校訂協助。

最後，如果你願意依照本文裡的步驟，參與軟體的國際化與本地化，請容我向你致上最高的謝忱；讓我們一起打造「全球」的資訊網吧！

## 參考資料

- Harald Tveit Alvestrand，「RFC 1766: Tags for the Identification of Languages」，1995年，`ftp://ftp.isi.edu/in-notes/rfc1766.txt`。
- Ross Callon編，「RFC 1925: The Twelve Networking Truths」，1996年，`ftp://ftp.isi.edu/in-notes/rfc1925.txt`
- Ulrich Drepper、Peter Miller、Francois Pinard，GNU "gettext"，1995-2001年。附在`ftp://prep.ai.mit.edu/pub/gnu/`套件的說明文件中。
- Burke、Lachler，「Localization and Perl: gettext breaks, Maketext fixes」，1999年，出版於Perl Journal，第13期。
- Sean M. Burke，「Localizing Open-Source Software」，2002年，出版於Perl Journal，2002年秋季號。
- W3C 國際化行動宣言，2001年，`http://www.w3.org/International/Activity.html`
- Mozilla 國際化及本土化指引，1999年，`http://www.mozilla.org/docs/refList/i18n/`