唐宗漢
傲爾網
2002年12月
對應用程式進行國際化(Internationalization,i18n)的目的,是在使它支援多種語言、日期、貨幣格式,以及各地的習俗等「地區設定(locale)」。接下來,本土化(Localization,l10n)則負責實際將軟體轉譯,以切合特定地區的使用者需求。當前,網頁應用程式(Web Application)由於以文字作為界面描述的格式,已成為最熱門的本土化對象之一。
在自由軟體的世界裡,許多最具彈性、最受歡迎的技術,是用Perl語言開發的。對網站應用程式的開發者來說,Perl也是長期以來的不二選擇。本篇文章是筆者對Perl應用程式進行中譯的經驗談,包含實作方式、常用工具的優劣之處,以及管理本土化專案時需注意的事項。
「在這個世界上,人類的語言為數不少。」
--Harald Tveit Alvestrand,RFC 1766,『語言識別標記』
網站及網頁應用程式,為什麼要作本土化呢?
請讀者想像一下:假設有人在全球資訊網上提出這個問題,許多人用不同的語言提出意見、彼此討論。身為其中的一份子,你可能會聽到這些論點:
|
不幸的是,這些論點並非每個人都看得懂。這就造成了「語言障礙」--能共同討論的對象,往往侷限在少數語言相同、文化相近的「地區社群」中。
然而,我們看不懂的論點往往很有道理,並且能帶來新的省思。所以,最好能有人將這些意見、操作界面及其他資料翻譯成我們看得懂的文字:
|
正如同上列論點所述,通常無法要求所有人「書同文」,一律採用拉丁語、通用語、世界語、邏輯語或是英文。這時,就需要本土化了。
對專屬軟體而言,本土化通常是打入國外市場的先決條件。若是某地的預期利潤低於本土化的成本,廠商便不會進行翻譯;在沒有源碼的情況下,當地的使用者要自己進行翻譯,便是件困難(也可能違法)的任務。如果廠商設計軟體時,完全沒有考慮到國際化的架構,那更是回天乏術。
相較之下,對開放源碼的應用程式進行本土化,則要簡單多了。和專屬軟體一樣,早期的版本通常祗為單一語言而設計;不同的是,任何人都可以隨時加上國際化的架構。誠如Sean M. Burke所述:
「要讓開放源碼做得更好,我們可以在寫程式時多加留意,讓程式員和熱心的使用者,都能輕易進行本土化。(話說回來,「開放源碼」的本意,就是讓任何有意願、有技術的人,都能成為程式員。)」
本文敘述的技巧,能有效降低本土化的難度。雖然重點放在Perl寫的網站應用程式上,但是其中的原則應該也適用於其他地方。
「它一定得動起來。」
--網絡第一真理,RFC 1925
網頁可以粗分為兩種:「靜態」網頁直到下一次更新為止,隨時都提供相同的內容;「動態」網頁則依據各種因素,提供相應的資訊。通常我們稱前者為「網頁文件」,稱後者為「網頁應用程式」。
不過,對不同的使用者來說,靜態網頁並不一定得保持相同的「呈現方式」--它的語言、樣式或媒介可以因人而異。(例如,視障人士可能會偏好用語音,來代替影像輸出。)全球資訊網的長處之一,就在它能讓客戶端與伺服器進行交涉,決定最合適的呈現方式。
舉個實例來說,假設筆者有個中文網頁,放在http://www.autrijus.org/index.html:
<html><head><title>唐宗漢 - 家</title></head> <body>施工中, 請見諒</body></html> |
有天我心血來潮,想要將它譯成英文版給外地的朋友看:
<html><head><title>Audrey.Home</title></head> <body>Sorry, this page is under construction.</body></html> |
這時,許多網站會提供一個「語言選擇頁面」,讓訪客挑選適合的語言,如下所示:
Please choose your language: | |||
Čeština | Deutsch | English | Español |
Français | Hrvatski | Italiano | 日本語 |
한국어 | Nederlands | Polski | Русский язык |
Slovensky | Slovensci | Svenska |
中文 (GB) 中文 (Big5) |
對一般的使用者和程式來說,這樣的頁面都模糊不清、多餘而麻煩。它不但迫使每位訪客多按一個鍵,也對網頁代理程式的作者造成障礙:剖析這一頁的結構、選擇正確的鏈結,是件很容易出錯的事。
當然,要是能讓每個人自動取得適合的語言,那是最好了。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個經常更新的頁面,那很快就沒人肯做翻譯--至連朋友也當不成了。
從上面的例子看來,顯然有必要將資料與源碼(也就是文字和標籤)分開,並且自動化翻譯版本的生產流程。
事實上,上一段已經一語道盡了現代的國際化(i18n)流程:在進行網站應用程式本土化之前,必須先找出區分資料與源碼的方法。
多年以來,Perl就是網頁開發的首選語言,也提供了多不勝數的的模組與網站建製工具。其中最普遍的,要算是自1997年以來就併入標準程式庫的CGI.pm。底下的範例程式,就是利用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.」)。和觀光手冊上的「外語速成」一樣,詞典裡的字串也許還有待填的空白,如同下列的中文╱海地語詞典所示:
中文 | Haitian |
這個東西要 ___ 塊錢。 | Bagay la kute ___ dola yo. |
在理想情況下,譯者祗需要翻譯詞典的內容,不用管HTML或程式碼裡寫些什麼。可是,由於各種本土化架構的詞典格式彼此不同,我們得先選出最適合這項專案的架構。
「它比你想像中還複雜。」
--網絡第八真理,RFC 1925
要實作上一段裡的some_function()函式,需要能讀取詞典檔、查出相符的字串、甚至將新的項目解到詞典裡的程式庫。這樣的程式庫,就稱為「本土化架構」。
依筆者的經驗,本土化架構之間的不同,通常表現在詞典檔結構的差異上。接下來,讓我們看看Perl如何使用其中的三種架構,從最簡單的Msgcat開始。
Msgcat是最早的本土化架構,也是XPG3╱XPG4標準的一部份,因此在Unix平台上隨處可得。它是第一代的詞典檔架構:將各個字串以編號表示,循序存放在名為「訊息清單(message catalog)」的陣列裡。這種架構容易實作、節省記憶體,並且查詢速度很快。在Windows等平台上的「資源檔(resource file)」也是基於相同的概念。
對於每個網頁及程式檔,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
二進制詞典檔的內容,可以想成是如下所示的二維陣列:
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 _」:
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的程式,還經常遇到「複數形式」的問題。請看下列程式:
printf(_(8, "%d files were deleted."), $files); |
當$files的值為1時,輸出的訊息顯然是錯誤的,而"%d file(s) were deleted"也不合英文文法。因此,我們非得分成兩個字串表示不可:
printf(($files == 1) ? _(8, "%d file was deleted.") : _(9, "%d files were deleted."), $files); |
但就算這樣寫,在英文以外的語言仍然行不通--法文的單數形式在$files為0時也適用,而斯拉夫語系的複數形式更多達三到四種!想在Msgcat的架構下照顧到這些狀況,必然是徒勞無功。
為了解決Msgcat的諸多問題,Ulrich Drepper在1995年參考Uniforum的Gettext界面,為GNU計劃開發了一套本土化系統。時到如今,在以C語言開發的自由軟體當中,GNU gettext已儼然成為本土化的標準架構,也廣受C++、Tcl及Python程式員的歡迎。
Gettext不需要為每份源碼檔案準備各自的詞典,而是為整個專案,針對每種語言各製作一份詞典(又稱作「PO檔」)。舉例來說,上述網頁的德文詞典檔「de.po」可能內容如下:
#: 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模組來存取二進制執行檔,如下所示:
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函式達成的更換變數順序功能:
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於1998年由Sean M. Burke所開發,並於2001年5月修訂後,併入Perl 5.8版的核心程式庫。
此模組與Msgcat及Gettext等函式導向的架構不同,是以物件導向的設計,讓「Locale::Maketext」作為抽象的基底類別,衍生出用於特定專案的「專案類別」。專案類別(名稱如「MyApp::L10N」)進一步衍生出數個「語言類別」,如「MyApp::L10N::it」、「MyApp::L10N::fr」等等。 >
所謂的語言類別,就是具有全域%Lexicon雜湊的Perl模組。%Lexicon裡的鍵是原文(通常是英文)的字串,雜湊值則是翻譯過的字串。語言類別還可以定義某些方法,來處理詞典中的文法變換等事項。
請見底下範例:
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語法--不然的話,就得有人幫忙做格式轉換了。
2002年5月,有感於Locale::Maketext詞典太過鬆散的格式,我著手實作公司內部使用的詞典格式,並在perl-i18n郵遞論壇上徵詢大家的意見。Jesse Vincent問道:「為什麼不直接使用gettext的PO檔格式呢?」於是我就設計了抽換式的後端系統,能接受各種不同格式的詞典。這樣一來,「Locale::Maketext::Lexicon」就誕生了。
此模組設計時的初衷,在結合Locale::Maketext彈性的表達式,與廣泛支援的gettext或Msgcat檔案格式。它也支援繫結(Tie)界面,讓詞典也能存放在關聯式資料庫,或DBM檔中。
底下的應用程式,示範了Locale::Maketext::Lexicon的數種用法,以及「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 | 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列定義了四個語言類別,各自採用不同的詞典格式及來源:
第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」是第一個以Locale::Maketext::Lexicon作為本土化架構的應用程式。它的「基底語言類別」是「RT::I18N」,子類別則由同一個目錄下的*.po檔案產生。
除此之外,它的->maketext方法也利用「Encode」模組(在5.8版以前的Perl,則是利用我的「Encode::compat」模組)直接傳回UTF-8編碼的資料。這樣一來,負責中文的譯者就可以用Big5碼編輯詞典檔,系統卻可以將它視為萬國碼(Unicode)處理。
在RT的Perl源碼裡,所有物件都利用從RT::Base繼承的$self->loc方法來翻譯訊息:
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模版來說,則採用了兩種方式:
% $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」元件(也許還附加參數),最後再顯示該元件傳回的字串。底下是此元件的實作方式:
% my $hand = $session{'CurrentUser'}->LanguageHandle; % $m->print($hand->maketext($m->content, @_)); |
有了這些方式,祗要將既有模版裡的訊息解成詞典檔,再寄給譯者就行了。找出700多則訊息的工作約費時一個星期;整個國際化╱本土化的流程則花了不到兩個月的時間。
Slash(類似於Slashdot的自動說書首頁,Slashdot Like Automated Storytelling Homepage)是Slashdot背後的程式。不過,Slash更是完整的網站的建置環境,建立在Andy Wardley的「Template Toolkit」模組上。
基於TT2乾淨的設計,Slash將源碼與資料徹底分開,這點與RT╱Mason不同。因此,幾乎沒有必要在Perl程式檔裡進行本土化的工作。
在本文撰寫之前,有許多直接將模版翻譯而成的「本土化」版本,包括中文、日文、希伯來文等。然而,在新版釋出時需要做的合併(merge)動作非常困難(外掛程式就更別提了),因此翻譯版本往往延遲許久纔釋出。
這裡,我們提出一個較好的解決方式:在模版提供函式上架設「自動解譯層」,利用HTML::Parser及Template::Parser實作。它的功能如下所示:
輸入 | <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
從以上的兩個例子裡,可以約略看出本土化的共通流程。這一節會介紹如何經由十個階段,來本土化既有的網站應用程式,以及一些相關的小祕訣。
_("Found ") . $files . _(" file(s)."); # 不完整的句子--錯了! sprintf(_("Found %s file(s)."), $files); # 完整句(利用sprintf) _("Found [*,_1,file].", $files); # 完整句(用Locale::Maketext) |
#: 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早期版本的校訂協助。
最後,如果你願意依照本文裡的步驟,參與軟體的國際化與本地化,請容我向你致上最高的謝忱;讓我們一起打造「全球」的資訊網吧!