Locale::Maketext - 本土化架構
package MyProgram;
use strict;
use MyProgram::L10N;
# ...which inherits from Locale::Maketext
my $lh = MyProgram::L10N->get_handle() || die "What language?";
...
# And then any messages your program emits, like:
warn $lh->maketext( "Can't open file [_1]: [_2]\n", $f, $! );
...
應用程式(無論是直接執行或透過網路)的常見功能是「在地化」——也就是針對英語使用者呈現英文介面,針對德語使用者呈現德語介面,以此類推,針對程式設計的所有語言都這麼做。Locale::Maketext 是軟體在地化的架構;它提供整理和存取在地化應用程式所需的文字和文字處理程式碼的工具。
為了了解 Maketext 及其所有元件如何結合,您可能應該先閱讀 Locale::Maketext::TPJ13,然後 再閱讀下列文件。
您可能也想閱讀 File::Findgrep
及其組成模組的原始碼——它們是使用 Maketext 的完整(儘管很小)範例應用程式。
Locale::Maketext 的基本設計是物件導向的,而 Locale::Maketext 是抽象基底類別,您可以從中衍生「專案類別」。專案類別(名稱類似「TkBocciBall::Localize」,您可以在模組中使用)反過來又是專案的所有「語言類別」的基底類別(名稱為「TkBocciBall::Localize::it」、「TkBocciBall::Localize::en」、「TkBocciBall::Localize::fr」等)。
語言類別是包含詞彙表短語的類別,作為類別資料,可能也包含一些方法,這些方法可用於詮釋詞彙表中的短語,或以其他方式處理該語言的文字。
屬於語言類別的物件稱為「語言控制代碼」;它通常是輕量級物件。
正常的動作流程是呼叫
use TkBocciBall::Localize; # the localization project class
$lh = TkBocciBall::Localize->get_handle();
# Depending on the user's locale, etc., this will
# make a language handle from among the classes available,
# and any defaults that you declare.
die "Couldn't make a language handle??" unless $lh;
從此之後,您使用 maketext
函式存取屬於您取得的語言控制代碼的任何詞彙表中的項目。因此,這
print $lh->maketext("You won!"), "\n";
...會發出此語言的正確文字。如果 $lh
中的物件屬於類別「TkBocciBall::Localize::fr」,且 %TkBocciBall::Localize::fr::Lexicon 包含 ("You won!" => "Tu as gagné!")
,則上述程式碼會開心地告訴使用者「Tu as gagné!」
Locale::Maketext 提供各種方法,分為三類
與建構語言控制代碼有關的方法。
maketext
及其他與存取特定語言控制代碼的 %Lexicon 資料有關的方法。
您可能會覺得好用的方法,來自您放入 %Lexicon 項目的常式。
以下部分將進行說明。
這些與建構語言句柄有關
$lh = YourProjClass->get_handle( ...langtags... ) || die "lg-handle?";
這會嘗試根據您提供的語言標籤載入類別(例如 ("en-US", "sk", "kon", "es-MX", "ja", "i-klingon")
),並針對成功的第一個類別傳回 YourProjClass::language->new()。
如果它執行完整個語言標籤清單,卻找不到符合這些確切字詞的類別,它就會嘗試「上位」語言類別。因此,如果找不到「en-US」類別(即 YourProjClass::en_us),也找不到清單中其他任何項目的類別,我們就會嘗試其上位語言「en」(即 YourProjClass::en),然後依序嘗試給定清單中的其他語言標籤:「es」。(我們範例清單中的其他語言標籤:碰巧沒有上位語言。)
如果這些語言標籤都沒有導致可載入類別,我們就會嘗試衍生自 YourProjClass->fallback_languages() 的類別,如果這樣還是不行,我們就會使用 YourProjClass->fallback_language_classes() 指定的類別。然後,在(可能不太可能)失敗的情況下,我們只會傳回 undef。
$lh = YourProjClass->get_handle() || die "lg-handle?";
當 get_handle
以空參數清單呼叫時,就會發生神奇的事
如果 get_handle
感測到它是在以 CGI 呼叫的程式中執行,它就會嘗試從環境變數「HTTP_ACCEPT_LANGUAGE」中取得語言標籤,並假裝這些語言標籤是傳遞給 get_handle
的參數。
否則(即如果不是 CGI),這會嘗試各種特定於作業系統的方法來取得目前語言環境/語言的語言標籤,然後假裝這些語言標籤是傳遞給 get_handle
的值。
目前,這個特定於作業系統的部分包括在環境變數「LANG」和「LANGUAGE」中尋找;在 MSWin 電腦上(這些變數通常未用),這也會嘗試使用模組 Win32::Locale 來取得目前在「區域設定」(或「國際」?)控制台中選取的語言/語言環境的語言標籤。我歡迎進一步建議,以便在支援在地化的其他作業系統中執行正確的操作。
如果您在保留組態檔的應用程式中使用在地化,您可能會考慮在專案類別中使用類似這樣的內容
sub get_handle_via_config {
my $class = $_[0];
my $chosen_language = $Config_settings{'language'};
my $lh;
if($chosen_language) {
$lh = $class->get_handle($chosen_language)
|| die "No language handle for \"$chosen_language\""
. " or the like";
} else {
# Config file missing, maybe?
$lh = $class->get_handle()
|| die "Can't get a language handle";
}
return $lh;
}
$lh = YourProjClass::langname->new();
這會建構語言句柄。您通常不會直接呼叫它,而是讓 get_handle
尋找語言類別以 use
,然後呼叫 ->new。
$lh->init();
這是由 ->new 呼叫來初始化新建立的語言控制代碼。如果您在您的類別中定義一個 init 方法,請記得通常會建議在其中呼叫 $lh->SUPER::init(假設在開頭),以便所有類別都有機會以它們認為合適的方式初始化一個新物件。
YourProjClass->fallback_languages()
get_handle
將此回傳值附加到您傳遞給 get_handle
的任何語言清單的尾端。除非您覆寫這個方法,否則您的專案類別會繼承 Locale::Maketext 的 fallback_languages
,它目前會回傳 ('i-default', 'en', 'en-US')
。(「i-default」在 RFC 2277 中定義)。
這個方法(讓它回傳一個現有語言類別的語言標籤名稱)可用於確保 get_handle
將永遠能夠建構一個語言控制代碼(假設您的語言類別在適當的 @INC 目錄中)。或者您可以使用下一個方法
YourProjClass->fallback_language_classes()
get_handle
將此回傳值附加到它將嘗試使用的類別清單的尾端。除非您覆寫這個方法,否則您的專案類別會繼承 Locale::Maketext 的 fallback_language_classes
,它目前會回傳一個空清單,()
。透過將此設定為某個值(也就是一個可載入語言類別的名稱),您可以確保 get_handle
將永遠能夠建構一個語言控制代碼。
這是 Locale::Maketext 中最重要的方法
$text = $lh->maketext(I<key>, ...parameters for this phrase...);
這會在語言控制代碼 $lh 和其所有超類別的 %Lexicon 中尋找,尋找一個其金鑰是字串 key 的項目。假設找到這樣的項目,則會發生各種事情,視找到的值而定
如果值是一個純量參考,則會取消純量參考並回傳(且任何參數都會被忽略)。
如果值是一個程式碼參考,則我們會回傳 &$value($lh, ...parameters...)。
如果值是一個字串,不像是在方括號表示法中,則我們會回傳它(在用一個純量參考取代它在 %Lexicon 中的內容後)。
如果值確實像是在方括號表示法中,則我們會將它編譯成一個子程式,用新的程式碼參考取代 %Lexicon 中的字串,然後我們會回傳 &$new_sub($lh, ...parameters...)。
方括號表示法會在後面的章節中討論。請注意,如果字串在語法上不正確(例如,沒有正確平衡方括號),則嘗試將字串編譯成方括號表示法可能會擲回例外狀況。
此外,呼叫 &$coderef($lh, ...parameters...) 可能會擲回任何類型的例外狀況(如果,例如,在該子程式中的程式碼嘗試除以零)。但是,當您有方括號表示法文字說要呼叫一個方法「foo」,但沒有這樣的時,就會發生一個非常常見的例外狀況。(例如,「You have [quatn,_1,ball]。」會在嘗試呼叫 $lh->quatn($_[1],'ball') 時擲回例外狀況 -- 您可能指的是「quant」)。maketext
會捕捉這些例外狀況,但只會讓錯誤訊息更易讀,然後它會重新擲回例外狀況。
如果在 $lh 的任何 %Lexicon hash 中找不到 key,可能會擲回例外狀況。如果找不到金鑰會發生什麼事,會在後面的章節「控制查詢失敗」中討論。
請注意,在某些情況下,您可能會覺得覆寫 maketext
方法並使用「after 方法」很有用,如果您想要轉譯編碼,甚至是指令碼
package YrProj::zh_cn; # Chinese with PRC-style glyphs
use base ('YrProj::zh_tw'); # Taiwan-style
sub maketext {
my $self = shift(@_);
my $value = $self->maketext(@_);
return Chineeze::taiwan2mainland($value);
}
或者您可能想要使用會捕捉任何例外狀況的東西來覆寫它,如果這對您的程式至關重要
sub maketext {
my($lh, @stuff) = @_;
my $out;
eval { $out = $lh->SUPER::maketext(@stuff) };
return $out unless $@;
...otherwise deal with the exception...
}
除了這兩種情況,我不認為覆寫 maketext
方法是有用的。(如果你遇到有用它的情況,我很樂意聽聽。)
這兩個方法在「控制查詢失敗」一節中討論。
這些方法在「方括號表示法安全性」一節中討論。
這些方法通常可以從你的 %Lexicon 常式(無論是否表示為方括號表示法)中輕鬆使用。
這通常用於在方括號表示法(稍後討論)中呼叫,如下所示
"Your search matched [quant,_1,document]!"
它用於量化名詞(即說明數量,同時提供正確的形式)。此方法的行為對於英語和一些其他西歐語言來說很方便,你應該為不適用的語言覆寫它。你可以隨時閱讀原始碼,但目前的實作基本上與這個偽程式碼描述的一樣
if $number is 0 and there's a $negative,
return $negative;
elsif $number is 1,
return "1 $singular";
elsif there's a $plural,
return "$number $plural";
else
return "$number " . $singular . "s";
#
# ...except that we actually call numf to
# stringify $number before returning it.
因此,對於英語(使用方括號表示法),"...[quant,_1,file]..."
是正確的(對於 0,它會傳回「0 個檔案」,對於 1,它會傳回「1 個檔案」,對於更多,它會傳回「2 個檔案」,等等)。
但是對於「目錄」,你會想要 "[quant,_1,directory,directories]"
,這樣我們的基本 quant
方法就不會認為「目錄」的複數形式是「directorys」。如果你指定否定形式,你可能會發現輸出聽起來更好,如下所示
"[quant,_1,file,files,No files] matched your query.\n"
請記住動詞一致性(或其他語言中的形容詞),如下所示
"[quant,_1,document] were matched.\n"
因為如果 _1 是 1,你會得到「1 個文件是匹配的」。這裡可以接受的技巧是執行以下操作
"[quant,_1,document was, documents were] matched.\n"
這會根據此語言的慣例,以美觀的方式傳回給定的數字格式。Maketext 的預設方法主要是採用數字的正常字串形式(僅對非常大的數字套用 sprintf "%G"),然後視需要加入逗號。(但如果 $language->{'numf_comma'} 為 true,我們會套用 tr/,./.,/
;這是一個小技巧,對將兩百萬表示為「2.000.000」而非「2,000,000」的語言很有用)。
如果您想要更花俏的內容,請考慮使用 Number::Format 或完全執行其他操作來覆寫此內容。
請注意,numf 會由 quant 呼叫,以將所有量化數字字串化。
這會根據此語言的慣例,傳回適用于數量 $number
的給定名詞形式。numerate
由 quant
內部使用,以量化名詞。直接使用它(通常來自方括號表示法)以避免 quant
隱含呼叫 numf
和輸出數字量。
這只是一個 Perl 正常 sprintf
函式的包裝器。提供它讓您可以在方括號表示法中使用「sprintf」
"Couldn't access datanode [sprintf,%10x=~[%s~],_1,_2]!\n"
傳回...
Couldn't access datanode Stuff=[thangamabob]!
目前這只會取得 ref($language)
的最後一部分,將底線轉為破折號,並傳回。因此,如果 $language 是 Hee::HOO::Haw::en_us 類別的物件,$language->language_tag() 會傳回「en-us」。(是的,該語言標籤的通常表示法為「en-US」,但大小寫在語言標籤比較中絕不會被視為有意義。)
您可以隨意覆寫此內容;Maketext 沒有將它用於任何用途。
目前這沒有用於任何用途,但提供它(預設值為 (ref($language) && $language->{'encoding'})) 或 "iso-8859-1"
)作為一種建議,表示將編碼與您的語言處理(無論是逐類別或甚至逐處理)關聯起來可能是有用/必要的。
語言處理是一個輕量級物件,也就是說,它(不一定)承載任何感興趣的資料,除了僅僅是它所屬類別的成員之外。
語言處理實作為一個受祝福的雜湊。您的子類別可以在雜湊中儲存您想要的任何資料。目前任何關鍵的 Maketext 方法所使用的唯一雜湊項目是「fail」,因此請隨時使用您喜歡的任何其他項目。
請記住:如果您對本文件有任何不清楚的地方,請不要害怕閱讀 Maketext 原始碼。本文件比模組原始碼本身長很多。
以下是 Locale::Maketext 對於所有語言類別所形成的類別階層的假設
您必須有一個專案基礎類別,您載入它,然後在呼叫 YourProjClass->get_handle(...) 時將它用作第一個引數。它應該衍生(直接或間接)自 Locale::Maketext。這個類別的名稱並不重要,儘管假設這是您超級巨型程式中的在地化元件,您的專案類別的良好名稱可能是 SuperMegaProgram::Localization、SuperMegaProgram::L10N、SuperMegaProgram::I18N、SuperMegaProgram::International,甚至 SuperMegaProgram::Languages 或 SuperMegaProgram::Messages。
語言類別是 YourProjClass->get_handle 將嘗試載入的類別。它會透過取得每個語言標籤(如果它看起來不像語言標籤或區域設定標籤,則略過它!),將其全部轉換為小寫,將破折號轉換為底線,並將其附加到 YourProjClass . "::"。因此,這樣
$lh = YourProjClass->get_handle(
'en-US', 'fr', 'kon', 'i-klingon', 'i-klingon-romanized'
);
將嘗試載入類別 YourProjClass::en_us(請注意小寫!)、YourProjClass::fr、YourProjClass::kon、YourProjClass::i_klingon 和 YourProjClass::i_klingon_romanized。(它將在實際載入的第一個類別停止。)
我假設每個語言類別都衍生(直接或間接)自您的專案類別,並且還定義了它的 @ISA、它的 %Lexicon 或兩者。但我預期如果這些假設不成立,不會造成嚴重的後果。
語言類別可以衍生自其他語言類別(儘管它們應該有「use Thatclassname」或「use base qw(...classes...)」)。它們可以衍生自專案類別。它們可以衍生自其他類別。或者透過多重繼承,它可以衍生自這些類別的任何組合。
我預見在您的語言類別階層中有多重繼承不會有任何問題。(然而,與往常一樣,如果您在階層中有一個循環,Perl 將會強烈抱怨:也就是說,如果任何類別都是它自己的祖先。)
一個典型的 %Lexicon 項目是用來表示一個詞組,它會帶入一些數字(0 或更多)的參數。一個項目會透過字串 key 在 $lh->maketext(key, ...parameters...) 中存取,它應該會傳回一個字串,通常用於對使用者「輸出」——無論這實際上是指列印到 STDOUT、寫入檔案,或放入 GUI 小工具中。
雖然 key 必須是字串值(因為這是 Perl 對雜湊 key 的基本限制),但 lexicon 中的值目前可以有幾種類型:已定義的純量、純量參考,或代碼參考。這些的使用方式已在「「maketext」方法」區段中說明,而字串的方括號表示法則在下一區段中討論。
雖然你可以對 lexicon key 使用任意的唯一 ID(例如「_min_larger_max_error」),但通常會讓項目的 key 本身成為一個有效的值,就像這個範例錯誤訊息
"Minimum ([_1]) is larger than maximum ([_2])!\n",
比較這段使用任意 ID 的程式碼...
die $lh->maketext( "_min_larger_max_error", $min, $max )
if $min > $max;
...與這段使用 key-as-value 的程式碼
die $lh->maketext(
"Minimum ([_1]) is larger than maximum ([_2])!\n",
$min, $max
) if $min > $max;
簡而言之,第二段程式碼較易讀。特別是,很明顯你提供給該詞組的參數數量(兩個)就是它想要接收的參數數量。(因為你看到 key 中使用了 _1 和 _2。)
此外,一旦專案完成且你開始將它在地化,你可以將所有你使用的各種 key 刮在一起,並傳遞給翻譯人員;然後,如果翻譯人員看到的是這個
"Minimum ([_1]) is larger than maximum ([_2])!\n",
=> "", # fill in something here, Jacques!
而不是這個較難理解的混亂內容
"_min_larger_max_error"
=> "", # fill in something here, Jacques
我認為將 key 作為 lexicon 值會讓完成的 lexicon 項目更易讀
"Minimum ([_1]) is larger than maximum ([_2])!\n",
=> "Le minimum ([_1]) est plus grand que le maximum ([_2])!\n",
此外,如果你設定一個 _AUTO lexicon,那麼將有效的值作為 key 會非常有用。_AUTO lexicon 會在後面的區段中討論。
我幾乎總是使用本身是有效 lexicon 值的 key。一個值得注意的例外是當值很長時。例如,要取得命令列程式在給定未知開關時可能會傳回的滿螢幕資料,我通常只會使用簡短且有說明性的 key,例如「_USAGE_MESSAGE」。在那個時間點,我接著會立即在 ProjectClass::L10N::en lexicon 中定義那個 lexicon 項目(因為英語永遠是我的「專案語言」)
'_USAGE_MESSAGE' => <<'EOSTUFF',
...long long message...
EOSTUFF
然後我就可以像這樣使用它
getopt('oDI', \%opts) or die $lh->maketext('_USAGE_MESSAGE');
順帶一提,請注意每個類別的 %Lexicon
會繼承並延伸其超類別中的 lexicon。這並非因為這些是特殊的雜湊 本身,而是因為你會透過 maketext
方法存取它們,而這個方法會在一個語言類別和所有其祖先類別中的所有 %Lexicon
雜湊中尋找項目。(這是因為「類別資料」的概念並未直接在 Perl 中實作,而是留給個別類別系統依它們認為合適的方式實作。)
請注意,您可能將資料儲存在詞彙表中,而這些資料不僅是輸出用語:例如,如果您的程式從鍵盤取得輸入,詢問「(Y/N)」問題,您可能需要知道在任何語言中「Y[es]/N[o]」的等效詞為何。您可能還需要知道答案「y」和「n」的等效詞為何。您可以將該資訊儲存在詞彙表中(例如,在鍵值「~answer_y」和「~answer_n」下,以及長式鍵值「~answer_yes」和「~answer_no」下,其中「~」只是一個臨時字元,用來指示程式設計師/翻譯人員這些並非輸出用語)。
或者,您可以在語言類別的詞彙表中儲存此資訊,也可以(在某些情況下,真的應該)將相同的知識表示為語言類別中方法中的程式碼。(這在詞彙表(我們知道如何說的內容)和詞彙表類別中的其他內容(我們知道如何做的內容)之間留下了一個明確的區別。)考慮這個處理法語「oui/non」問題回應的處理器範例
sub y_or_n {
return undef unless defined $_[1] and length $_[1];
my $answer = lc $_[1]; # smash case
return 1 if $answer eq 'o' or $answer eq 'oui';
return 0 if $answer eq 'n' or $answer eq 'non';
return undef;
}
...然後,您會在類似這樣的結構中呼叫它
my $response;
until(defined $response) {
print $lh->maketext("Open the pod bay door (y/n)? ");
$response = $lh->y_or_n( get_input_from_keyboard_somehow() );
}
if($response) { $pod_bay_door->open() }
else { $pod_bay_door->leave_closed() }
其他值得儲存在詞彙表中的資料可能是語言目標資源的檔案名稱
...
"_main_splash_png"
=> "/styles/en_us/main_splash.png",
"_main_splash_imagemap"
=> "/styles/en_us/main_splash.incl",
"_general_graphics_path"
=> "/styles/en_us/",
"_alert_sound"
=> "/styles/en_us/hey_there.wav",
"_forward_icon"
=> "left_arrow.png",
"_backward_icon"
=> "right_arrow.png",
# In some other languages, left equals
# BACKwards, and right is FOREwards.
...
您可能想對表達鍵位繫結或類似的內容執行相同的操作(因為將「q」硬編碼為退出畫面/選單/程式的功能的繫結,只有在您的語言將「q」與「quit」關聯起來時才有用!)
方括號表示法是 Locale::Maketext 的一項重要功能。我的意思是方括號表示法提供 sprintf 格式化的替代方案。您使用方括號表示法執行的所有操作都可以使用子區塊執行,但方括號表示法旨在更為簡潔。
方括號表示法就像一個微型的「範本」系統(就 Text::Template 的意義而言,而非 C++ 範本的意義),其中一般文字基本上會原封不動地傳遞,但特殊區域中的文字會特別進行詮釋。在方括號表示法中,您使用方括號(「[...]」),而非大括號(「{...}」),來標記特別詮釋的區段。
例如,這裡所有照字面意思取用的區域都用「^」加上底線,而所有方括號中的特殊區域都用 X 加上底線
"Minimum ([_1]) is larger than maximum ([_2])!\n",
^^^^^^^^^ XX ^^^^^^^^^^^^^^^^^^^^^^^^^^ XX ^^^^
當該字串從方括號表示法編譯成真正的 Perl 子程式時,它基本上會轉換成
sub {
my $lh = $_[0];
my @params = @_;
return join '',
"Minimum (",
...some code here...
") is larger than maximum (",
...some code here...
")!\n",
}
# to be called by $lh->maketext(KEY, params...)
換句話說,方括號群組外的文字會轉換成字串文字。方括號中的文字則更為複雜,目前遵循以下規則
為空的方括號群組,或僅包含空白的方括號群組,會被忽略。(範例:「[]」、「[ ]」,或方括號之間有換行符號和/或標籤和/或空白。)
否則,每個群組會被視為項目逗號分隔的群組,每個項目會被解釋如下
項目為「_數字」或「_-數字」會被解釋為 $_[值]。例如,「_1」會變成 $_[1],而「_-3」會被解釋為 $_[-3](這種情況下 @_ 應至少有三個元素)。請注意 $_[0] 是語言處理常式,通常不會直接命名。
項目「_*」會被解釋為「@_ 中的所有項目,除了 $_[0]」。例如,@_[1..$#_]
。請注意,對於沒有參數(除了 $_[0],語言處理常式)的呼叫,例如 $lh->maketext(key),這是一個空清單。
否則,每個項目會被解釋為字串文字。
群組整體會被解釋如下
如果括弧群組中的第一個項目看起來像方法名稱,則該群組會被解釋如下
$lh->that_method_name(
...rest of items in this group...
),
如果括弧群組中的第一個項目為「*」,則會視為俗稱「quant」方法的簡寫。類似地,如果括弧群組中的第一個項目為「#」,則會視為「numf」的簡寫。
如果括弧群組中的第一個項目為空字串、或「_*」或「_數字」或「_-數字」,則該群組會被解釋為僅內插其所有項目
join('',
...rest of items in this group...
),
範例:「[_1]」和「[, _1]」是同義詞;以及「[,ID-(,_4,-,_2,)]
」,編譯為 join "", "ID-(", $_[4], "-", $_[2], ")"
。
否則,此括弧群組無效。例如,在群組「[!@#,whatever]」中,第一個項目「!@#」既不是空字串、「_數字」、「_-數字」、「_*」,也不是有效的方法名稱;因此,如果您嘗試編譯包含此括弧群組的表達式,Locale::Maketext 會擲回例外狀況。
順帶一提,請注意,每個群組中的項目是逗號分隔,而不是 /\s*,\s*/
分隔。也就是說,您可能會預期此括弧群組
"Hoohah [foo, _1 , bar ,baz]!"
會編譯為此
sub {
my $lh = $_[0];
return join '',
"Hoohah ",
$lh->foo( $_[1], "bar", "baz"),
"!",
}
但實際上它編譯為此
sub {
my $lh = $_[0];
return join '',
"Hoohah ",
$lh->foo(" _1 ", " bar ", "baz"), # note the <space> in " bar "
"!",
}
在迄今討論的符號中,字元「[」和「]」被賦予特殊意義,用於開啟和關閉括弧群組,而「,」在括弧群組內有特殊意義,用於分隔群組中的項目。這引發了一個問題:您如何在括弧符號字串中表示一個文字「[」或「]」,以及如何在括弧群組內表示一個文字逗號。為此,我採用「~」(波浪號)作為跳脫字元:「~[」表示括弧符號字串中的任何位置的文字「[」字元(也就是說,無論您是否在括弧群組中),「~]」也是如此,表示文字「]」,而「~,」表示文字逗號。(雖然「,」在括弧群組外表示文字逗號,但只有在括弧群組內逗號才是特殊的。)
而且,如果您在括弧表達式中需要一個文字波浪號,您可以使用「~~」取得。
目前,在括號或逗號以外字元之前的未跳脫「~」表示只是一個「~」和該字元。亦即,「~X」的意思與「~~X」相同,也就是一個字面上的波浪號,然後是一個字面上的「X」。不過,透過使用「~X」,您假設 Maketext 的未來版本不會將「~X」用作魔術跳脫序列。實際上這不是什麼大問題,因為首先您可以只寫「~~X」而不必擔心;其次,我懷疑我會在方括號表示法中新增許多新的魔術字元;第三,您不太可能在訊息中需要字面上的「~」字元,因為它不是自然語言文字中廣泛使用的字元。
括號必須平衡,每個開括號都必須有一個匹配的閉括號,反之亦然。因此,這些都是無效的
"I ate [quant,_1,rhubarb pie."
"I ate [quant,_1,rhubarb pie[."
"I ate quant,_1,rhubarb pie]."
"I ate quant,_1,rhubarb pie[."
目前,方括號群組不會巢狀。也就是說,您不能說
"Foo [bar,baz,[quux,quuux]]\n";
如果您需要功能強大的表示法,請使用一般 Perl
%Lexicon = (
...
"some_key" => sub {
my $lh = $_[0];
join '',
"Foo ",
$lh->bar('baz', $lh->quux('quuux')),
"\n",
},
...
);
或撰寫「bar」方法,這樣您就不需要傳遞呼叫 quux 的輸出。
我預期您不需要(或特別想要)巢狀方括號群組,但歡迎您透過電子郵件向我提出令人信服的(真實生活)反對論點。
Locale::Maketext 不使用任何特殊語法來區分方括號表示法方法與一般類別或物件方法。此設計使其容易受到格式字串攻擊,只要它用於處理不受信任使用者提供的字串。
Locale::Maketext 支援拒絕清單和允許清單功能,以限制哪些方法可以作為方括號表示法方法呼叫。
預設情況下,Locale::Maketext 會拒絕 Locale::Maketext 名稱空間中所有以 '_' 字元開頭的方法,以及所有包含 Perl 名稱空間分隔字元的方法。
Locale::Maketext 的預設拒絕清單也會禁止在方括號表示法中使用下列方法
denylist
encoding
fail_with
failure_handler_auto
fallback_language_classes
fallback_languages
get_handle
init
language_tag
maketext
new
allowlist
whitelist
blacklist
此清單可以透過拒絕清單列出其他「已知不良」方法,或只允許清單列出「已知良好」方法來延伸。
若要防止在方括號表示法中呼叫特定方法,請使用 denylist() 方法
my $lh = MyProgram::L10N->get_handle();
$lh->denylist(qw{my_internal_method my_other_method});
$lh->maketext('[my_internal_method]'); # dies
若要將允許的括號表示法方法限制為特定清單,請使用 allowlist() 方法
my $lh = MyProgram::L10N->get_handle();
$lh->allowlist('numerate', 'numf');
$lh->maketext('[_1] [numerate, _1,shoe,shoes]', 12); # works
$lh->maketext('[my_internal_method]'); # dies
denylist() 和 allowlist() 方法會在每次呼叫時擴充其內部清單。若要重設 denylist 或 allowlist,請建立新的 maketext 物件。
my $lh = MyProgram::L10N->get_handle();
$lh->denylist('numerate');
$lh->denylist('numf');
$lh->maketext('[_1] [numerate,_1,shoe,shoes]', 12); # dies
對於使用內部快取的字典,已快取為已編譯形式的翻譯不會受到後續對 allowlist 或 denylist 設定的變更影響。使用外部快取的字典會在 allowlist 或 denylist 設定變更時清除其快取。這兩種快取類型之間的差異在「唯讀字典」區段中說明。
denylist 不允許的方法無法由 allowlist 允許。
注意:denylist() 是建議使用的名稱,而非歷史悠久且不具包容性的方法 blacklist()。blacklist() 可能會在這個套件的未來版本中移除,因此應從用法中移除。
注意:allowlist() 是建議使用的名稱,而非歷史悠久且不具包容性的方法 whitelist()。whitelist() 可能會在這個套件的未來版本中移除,因此應從用法中移除。
如果 maketext 要在個別 %Lexicon 中尋找 key 的項目(其中 key 沒有以底線開頭),並且沒有看到,但看到 "_AUTO" => some_true_value 的項目,那麼我們會立即定義 $Lexicon{key} = key,然後使用該值,就像它一直都在那裡一樣。這會在我們查看任何超類別 %Lexicons 之前發生!
(這有點像 Perl 函式呼叫系統中的 AUTOLOAD 機制,或者換個角度來看,就像 AutoLoader 模組一樣。)
我可以想像各種情況,你只是不希望查詢失敗(因為失敗通常表示 maketext 會擲回 die
,不過請參閱下一區段以獲得對此更大的控制權)。但以下情況是 _AUTO 字典特別有用的情況
在撰寫應用程式時,你可以隨時決定要發出哪些訊息。通常你會寫下以下內容
if(-e $filename) {
go_process_file($filename)
} else {
print qq{Couldn't find file "$filename"!\n};
}
但由於你預期會進行在地化,因此你會寫下
use ThisProject::I18N;
my $lh = ThisProject::I18N->get_handle();
# For the moment, assume that things are set up so
# that we load class ThisProject::I18N::en
# and that that's the class that $lh belongs to.
...
if(-e $filename) {
go_process_file($filename)
} else {
print $lh->maketext(
qq{Couldn't find file "[_1]"!\n}, $filename
);
}
現在,在你寫完上述程式碼之後,你通常必須開啟 ThisProject/I18N/en.pm 檔案,並立即新增一個項目
"Couldn't find file \"[_1]\"!\n"
=> "Couldn't find file \"[_1]\"!\n",
但我想這多少會分散掉讓主程式運作的工作,更別提我常常需要玩弄程式幾次才能決定訊息中我想要確切的用詞(這在這個案例中會需要我變更三行程式碼:呼叫 maketext 的那行,以及 ThisProject/I18N/en.pm 中的兩行)。
不過,如果你在 ThisProject/I18N/en.pm 的 %Lexicon 中設定 "_AUTO => 1"(假設所有程式設計師都會使用英文 (en) 作為此專案的內部訊息金鑰),那麼你就不必再加入像這樣的行
"Couldn't find file \"[_1]\"!\n"
=> "Couldn't find file \"[_1]\"!\n",
到 ThisProject/I18N/en.pm 中,因為如果 _AUTO 在此為 true,那麼只要在該詞彙表中尋找金鑰為 "找不到檔案 \"[_1]\"!\n" 的項目,就會將其加入,並附上該值!
請注意,以 "_" 開頭的金鑰之所以不受 _AUTO 影響,並不是因為底線字元有什麼特別的魔力,我只是想要一種方法讓大多數的詞彙表金鑰都能自動化,除了少數幾個金鑰外,而我武斷地決定使用開頭底線作為區分這幾個金鑰的訊號。
如果你的詞彙表是繫結雜湊,那麼快取已編譯值這個簡單的動作可能會致命。
例如,GDBM_File GDBM_READER 繫結雜湊會以類似的方式死亡
gdbm store returned -1, errno 2, key "..." at ...
你所需要做的,就是像這樣在詞彙表雜湊外部開啟快取
sub init {
my ($lh) = @_;
...
$lh->{'use_external_lex_cache'} = 1;
...
}
然後,它會將已編譯值儲存在 $lh->{'_external_lex_cache'} 中,而不是儲存在詞彙表雜湊中。
如果你呼叫 $lh->maketext(key, ...parameters...),而且在 $lh 的類別 %Lexicon 中,或在超類別 %Lexicon 雜湊中,沒有項目 key,而且我們無法自動建立 key(因為它以 "_" 開頭,或因為它的詞彙表都沒有 _AUTO => 1,
),那麼我們就無法找到建立 key 的正常方法。在這些失敗條件下會發生什麼,取決於 $lh 物件的 "fail" 屬性。
如果語言處理沒有 "fail" 屬性,maketext 僅會擲出例外(也就是說,它會呼叫 die
,並提到查詢失敗的 key,以及命名呼叫 $lh->maketext(key,...) 的行號。
如果語言處理有 "fail" 屬性,而其值是程式碼參考,那麼 $lh->maketext(key,...params...) 會放棄並呼叫
return $that_subref->($lh, $key, @params);
否則,"fail" 屬性的值應該是表示方法名稱的字串,以便 $lh->maketext(key,...params...) 可以放棄並執行
return $lh->$that_method_name($phrase, @params);
可以透過 fail_with
方法存取「fail」屬性
# Set to a coderef:
$lh->fail_with( \&failure_handler );
# Set to a method name:
$lh->fail_with( 'failure_method' );
# Set to nothing (i.e., so failure throws a plain exception)
$lh->fail_with( undef );
# Get the current value
$handler = $lh->fail_with();
現在,關於你可能想要如何處理這些處理常式:也許你想要記錄哪個類別的哪個金鑰失敗,然後結束。也許你不喜歡 die
,而想要將錯誤訊息傳送至 STDOUT(或任何地方),然後僅僅 exit()
。
或者也許你根本不想 die
!也許你可以使用像這樣的處理常式
# Make all lookups fall back onto an English value,
# but only after we log it for later fingerpointing.
my $lh_backup = ThisProject->get_handle('en');
open(LEX_FAIL_LOG, ">>wherever/lex.log") || die "GNAARGH $!";
sub lex_fail {
my($failing_lh, $key, $params) = @_;
print LEX_FAIL_LOG scalar(localtime), "\t",
ref($failing_lh), "\t", $key, "\n";
return $lh_backup->maketext($key,@params);
}
有些使用者表示,他們認為擁有「fail」屬性的整個機制似乎是一個相當無意義的複雜化。但我希望 Locale::Maketext 可用於任何規模和類型的軟體專案;而不同的軟體專案對於在失敗條件下採取何種正確措施有不同的想法。我可以簡單地說,失敗總是會擲回例外狀況,如果你想要小心,你只需要將每個呼叫 $lh->maketext 包裝在 eval { } 中即可。然而,我希望程式設計師保留權利(透過「fail」屬性),將查詢失敗視為與設定檔無法讀取或某些必要資源無法存取的嚴重性層級不同的例外狀況。
「fail」屬性一個可能實用的值是方法名稱「failure_handler_auto」。這是 Locale::Maketext 類別本身定義的方法。你可以使用以下方式設定它
$lh->fail_with('failure_handler_auto');
然後當你呼叫 $lh->maketext(key, ...parameters...),且在任何這些詞彙中都沒有 key 時,maketext 會放棄,並顯示
return $lh->failure_handler_auto($key, @params);
但是 failure_handler_auto 沒有結束或執行任何動作,而是編譯 $key,將其快取在
$lh->{'failure_lex'}{$key} = $compiled
然後呼叫編譯值,並傳回該值。(也就是說,如果 $key 看起來像括號表示法,$compiled 就是一個子常式,我們傳回 &{$compiled}(@params);但如果 $key 僅是一個純文字字串,我們只傳回該值。)
使用「failure_auto_handler」的效果就像 AUTO 詞彙,但它 1) 編譯 $key,即使它以「_」開頭,且 2) 你在新的 hashref $lh->{'failure_lex'} 中記錄這個物件所有失敗的金鑰。這應該可以避免你的程式結束 -- 只要你的金鑰實際上並非括號程式碼無效,且只要它們不會嘗試呼叫不存在的方法即可。
「failure_auto_handler」可能不是你想要的,但我希望它至少能讓你看到 maketext 失敗可以用許多非常靈活的方式來減輕。如果你能正式說明你想要什麼,你應該能夠將其表示為失敗處理常式。你甚至可以透過在該類別的 init 中設定,讓它成為給定類別的每個物件的預設值
sub init {
my $lh = $_[0]; # a newborn handle
$lh->SUPER::init();
$lh->fail_with('my_clever_failure_handler');
return;
}
sub my_clever_failure_handler {
...you clever things here...
}
以下是使用 Maketext 來在地化應用程式的簡要檢查清單
決定你要用於詞彙表金鑰的系統。如果你堅持,你可以使用不透明 ID(如果你懷念 catgets
),但我在上方的「每個詞彙表中的項目」章節中有更好的建議。假設你選擇有意義的金鑰,並將其加倍作為值(例如「最小值 ([_1]) 大於最大值 ([_2])!\n」),你必須決定這些金鑰應採用哪種語言。為了論證,我將稱之為英語,特別是美式英語,「en-US」。
為你的在地化專案建立一個類別。這是你將在慣用語中使用的類別名稱
use Projname::L10N;
my $lh = Projname::L10N->get_handle(...) || die "Language?";
假設你將類別命名為 Projname::L10N,建立一個類別,至少包含
package Projname::L10N;
use base qw(Locale::Maketext);
...any methods you might want all your languages to share...
# And, assuming you want the base class to be an _AUTO lexicon,
# as is discussed a few sections up:
1;
為你的內部金鑰所使用的語言建立一個類別。將類別命名為該語言的語言標籤,小寫,並將破折號變更為底線。假設你的專案的第一個語言是美式英語,你應該將其稱為 Projname::L10N::en_us。它至少應包含
package Projname::L10N::en_us;
use base qw(Projname::L10N);
%Lexicon = (
'_AUTO' => 1,
);
1;
(在本節的其餘部分,我將假設 Projname::L10N::en_us 的這個「第一語言類別」具有 _AUTO 詞彙表。)
開始撰寫你的程式。在你的程式中,你會說
print "Foobar $thing stuff\n";
改用 maketext 執行它,在金鑰中不使用變數內插
print $lh->maketext("Foobar [_1] stuff\n", $thing);
如果你厭倦了不斷說 print $lh->maketext
,請考慮為它建立一個函式包裝器,如下所示
use Projname::L10N;
our $lh;
$lh = Projname::L10N->get_handle(...) || die "Language?";
sub pmt (@) { print( $lh->maketext(@_)) }
# "pmt" is short for "Print MakeText"
$Carp::Verbose = 1;
# so if maketext fails, we see made the call to pmt
除了用於輸出的完整片語之外,任何與語言相關的內容都應放入類別 Projname::L10N::en_us,無論是以方法或詞彙表項目形式,這會在上方的「每個詞彙表中的項目」章節中討論。
當程式已完成,且其第一語言的在地化運作正常(透過 Projname::L10N::en_us 中的資料和方法),你可以收集資料進行翻譯。如果你的第一語言詞彙表不是 _AUTO 詞彙表,那麼你已經在詞彙表中明確地擁有所有訊息(否則在你呼叫 $lh->maketext 以取得不存在的訊息時,會產生例外)。但如果你(建議)很懶惰,並使用 _AUTO 詞彙表,那麼你必須列出所有你迄今為止讓 _AUTO 為你產生的詞組。有許多方法可以彙整此類清單。最直接的方法就是針對「maketext」的每個出現(或呼叫其包裝函式,例如上述的 pmt
函式)grep 原始碼,並記錄以下詞組。
此時你可能想要考慮你的基礎類別(Projname::L10N),所有詞彙表都繼承自此類別(Projname::L10N::en、Projname::L10N::es 等),是否應該是 _AUTO 詞彙表。理論上,所有需要的訊息都可能存在於每個語言類別中;但在假設不太可能或「不可能」查閱失敗的情況下,你應該考慮你的程式是否應該產生例外、發出英文(或專案的第一語言)文字,或如上文「控制查閱失敗」部分所述的更複雜的解決方案。
將所有訊息/詞組等提交給翻譯人員。
(事實上,如果你不確定是否已適當地抽象出程式碼中與語言相關的部分,你可能想要先從在地化到一種其他語言開始。)
翻譯人員可能會要求釐清特定詞組的出現情況。例如,在英文中,我們很樂意說「找到n個檔案」,無論我們的意思是「我尋找檔案,並找到n個檔案」或「我尋找其他東西(例如檔案中的行),並在過程中看到n個檔案」。這可能涉及重新思考你認為很清楚的事情:工具列上的「編輯」應該是名詞(「編輯」)還是動詞(「編輯」)?是否已經有慣用的方式來表達該選單選項,與目標語言的「編輯」正常字詞分開?
在所有涉及數量化(說「N 個檔案」,對於 N 的任何值)的非常常見現象的情況中,每個翻譯者都應清楚數字在句子中造成的哪些依賴關係。在許多情況下,依賴關係僅限於數字旁邊的字詞,在您可能預期它們出現的地方(「我找到-?複數 N 個空的-?複數目錄-?複數」),但在某些情況下,也會有意外的依賴關係(「我找到-?複數 ...」!)以及長距離依賴關係(「無法刪除 N 個目錄-?複數」!)。
提醒翻譯者考慮 N 為 0 的情況:「找到 0 個檔案」在任何語言中聽起來都不太自然,但在許多語言中可能無法接受,或者它可能會造成特殊類型的協調(類似於英文的「我沒有找到任何檔案」)。
請記住詢問您的翻譯者他們語言中的數字格式,以便您可以適當地覆寫 numf
方法。數字格式中的典型變數包括:用作小數點的是什麼(逗號?句點?);用作千分位分隔符的是什麼(空格?不換行空格?逗號?句點?小中點?單引號?撇號?);甚至所謂的「千分位分隔符」實際上是否每三位數字就出現一次,我聽說過一些印度(次大陸)語言將二十萬表示為「2,00,000」,除了不太令人意外的「200 000」、「200.000」、「200,000」和「200'000」之外。此外,使用一組除了常見的 ASCII「0」到「9」之外的數字字形可能會受到讚賞,例如透過 tr/0-9/\x{0966}-\x{096F}/
來取得天城文數字(用於印地語、孔卡尼語和其他語言)。
Locale::Maketext 提供的基本 quant
方法應該適用於許多語言。對於某些語言,修改它(或其組成部分 numerate
方法)以在 quant
的兩個引數呼叫中採用複數形式可能很有用(例如「[quant,_1,files]」),如果從複數形式推斷單數形式比從單數形式推斷複數形式更容易的話。
但對於其他語言(如 Locale::Maketext::TPJ13 中詳盡討論),單純的 quant
/numf
不夠用。對於特別有問題的斯拉夫語言,你可能需要一種方法,提供數字、名詞的引文形式以進行量化,以及句子句法投影到該名詞槽中的格和性別。然後,該方法將負責確定該數字投影到其名詞短語的語法數字,以及它可能用來覆寫常規格和性別的格和性別;然後,它將在提供所有所需變形形式的詞彙中查詢名詞。
你可能還希望與翻譯人員討論如何關聯同一個語言標籤的不同子形式,考慮這如何與 get_handle
對這些子形式的處理產生反應。例如,如果使用者接受「en、fr」的介面,而你提供「en-US」和「fr」的介面,他們應該得到什麼?你可能希望透過建立「en」和「en-US」有效同義,讓一個類別從另一個類別零衍生,來解決這個問題。
對於某些語言,這個問題可能永遠不會出現(丹麥語很少表示為「da-DK」,而是僅為「da」)。而對於其他語言,一個「通用」形式的整個概念可能會變得非常模糊,特別是對於涉及阿拉伯語或中文形式的語音媒體的介面。
一旦你為所有想要的語言本地化了你的程式/網站/等等,務必將結果(無論是實況或透過螢幕截圖)顯示給翻譯人員。一旦他們核准,盡一切努力讓至少另一位該語言的使用者檢查。即使(或特別是)翻譯是由你自己的程式設計師之一完成,這仍然成立。某些類型的系統可能比其他系統更難找到測試人員,具體取決於所涉及的特定領域術語和概念的數量——找到可以告訴你他們是否核准你在網路介面電子郵件中「刪除此訊息」翻譯的人,比找到可以對你在 XML 查詢工具介面中「屬性值」翻譯提供明智意見的人容易。
我建議閱讀所有這些
Locale::Maketext::TPJ13——我在 The Perl Journal 上關於 Maketext 的文章。它解釋了 Locale::Maketext 設計背後許多重要的概念,以及一些見解,說明為什麼 Maketext 比只有 sprintf 格式資料庫的訊息目錄的傳統舊方法更好。
File::Findgrep 是使用 Locale::Maketext 來本地化其訊息的範例應用程式/模組。對於更大的國際化系統,另請參閱 Apache::MP3。
RFC 3066,語言識別標籤,如 http://sunsite.dk/RFC/rfc/rfc3066.html
RFC 2277,IETF 字元集和語言政策位於 http://sunsite.dk/RFC/rfc/rfc2277.html——其中大部分只是對協定設計人員有興趣的事物,但它解釋了一些基本概念,例如區域設定和語言標籤之間的區別。
GNU gettext
手冊。gettext dist 可在 ftp://prep.ai.mit.edu/pub/gnu/
取得 -- 取得最近的 gettext tarball 並查看其「doc/」目錄,其中有易於瀏覽的 HTML 版本。gettext 文件提出了許多值得思考的問題,即使有些答案有時很奇怪,特別是當他們開始談論複數形式時。
Locale/Maketext.pm 原始碼。請注意,模組比其文件短得多!
版權所有 (c) 1999-2004 Sean M. Burke。保留所有權利。
此函式庫是免費軟體;您可以在與 Perl 相同的條款下重新分發或修改它。
散布此程式是希望它有用,但沒有任何保證;甚至沒有對適銷性或特定用途適用性的默示保證。
Sean M. Burke sburke@cpan.org