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, $! );
...
应用程序(无论是直接运行还是通过 Web 运行)的一个常见功能是“本地化”——即,它们向英语使用者呈现英语界面,向德语使用者呈现德语界面,依此类推,适用于其编程的所有语言。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 哈希中找不到key,可能会引发异常。如果找不到键,将在后面的部分“控制查找失败”中讨论。
请注意,在某些情况下,如果您要转换编码,甚至脚本,则可以使用“after 方法”覆盖 maketext
方法。
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 个文档”。这里的一个可接受的技巧是执行类似这样的操作
"[quant,_1,document was, documents were] matched.\n"
这会根据此语言的惯例以漂亮的方式返回给定的数字。Maketext 的默认方法主要是只采用数字的普通字符串形式(仅对非常大的数字应用 sprintf "%G"),然后根据需要添加逗号。(但如果 $language->{'numf_comma'} 为 true,我们会应用 tr/,./.,/
;这是一个有点技巧的方法,适用于将 200 万表示为“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 个或更多)的参数。一个条目旨在通过 $lh->maketext(key, ...parameters...) 中的字符串键进行访问,它应该返回一个通常用于对用户进行“输出”的字符串——无论这实际上意味着打印到 STDOUT、写入文件还是放入 GUI 小组件中。
虽然键必须是字符串值(因为这是 Perl 对哈希键施加的基本限制),但词典中的值当前可以是多种类型:已定义标量、标量引用或代码引用。这些用法在““maketext”方法”部分中进行了说明,下一部分将讨论字符串的方括号表示法。
虽然你可以为词典键使用任意唯一 ID(如“_min_larger_max_error”),但通常情况下,如果一个词条的键本身是一个有效值,则很有用,就像此示例错误消息一样
"Minimum ([_1]) is larger than maximum ([_2])!\n",
将此使用任意 ID 的代码与...
die $lh->maketext( "_min_larger_max_error", $min, $max )
if $min > $max;
...使用键作为值的代码进行比较
die $lh->maketext(
"Minimum ([_1]) is larger than maximum ([_2])!\n",
$min, $max
) if $min > $max;
简而言之,第二个更易读。特别是,很明显你提供给该短语的参数数量(两个)是它希望提供的参数数量。(因为你在键中看到了 _1 和 _2。)
此外,一旦项目完成,你开始对其进行本地化,你可以将使用的所有不同键收集在一起,并将其传递给翻译人员;然后,如果向翻译人员展示的内容如下,他们的工作将进行得更快
"Minimum ([_1]) is larger than maximum ([_2])!\n",
=> "", # fill in something here, Jacques!
而不是这种更晦涩的混乱
"_min_larger_max_error"
=> "", # fill in something here, Jacques
我认为将键作为词典值可以使完成的词典条目更具可读性
"Minimum ([_1]) is larger than maximum ([_2])!\n",
=> "Le minimum ([_1]) est plus grand que le maximum ([_2])!\n",
此外,如果你设置了 _AUTO 词典,则拥有作为键的有效值将非常有用。_AUTO 词典将在后面的部分中讨论。
我几乎总是使用本身是有效词典值的键。一个值得注意的例外是当值很长时。例如,为了获取命令行程序在给定未知开关时可能返回的屏幕数据,我通常只使用一个简短的自解释键,例如“_USAGE_MESSAGE”。然后,我立即转到 ProjectClass::L10N::en 词典中定义该词典条目(因为英语始终是我的“项目语言”)
'_USAGE_MESSAGE' => <<'EOSTUFF',
...long long message...
EOSTUFF
然后我可以用它作为
getopt('oDI', \%opts) or die $lh->maketext('_USAGE_MESSAGE');
顺便说一下,请注意每个类的 %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...)
换句话说,方括号组之外的文本会变成字符串文字。方括号中的文本更为复杂,目前遵循以下规则
为空或仅包含空格的方括号组会被忽略。(示例:“[]”、“[ ]”或一个 [ 和一个 ],其间有回车符和/或制表符和/或空格。
否则,每个组都被视为一个逗号分隔的项目组,每个项目解释如下
解释为 $_[value] 的项目为“_digits”或“_-digits”。即,“_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() 方法在每次调用时都会扩展其内部列表。若要重置拒绝列表或允许列表,请创建一个新的 maketext 对象。
my $lh = MyProgram::L10N->get_handle();
$lh->denylist('numerate');
$lh->denylist('numf');
$lh->maketext('[_1] [numerate,_1,shoe,shoes]', 12); # dies
对于使用内部缓存的词典,已在其编译形式中缓存的翻译不受随后对允许列表或拒绝列表设置的更改的影响。使用外部缓存的词典将在允许列表或拒绝列表设置更改时清除其缓存。两种缓存类型之间的差异在“只读词典”部分中进行了说明。
拒绝列表不允许的方法不能由允许列表允许。
注意: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 为真,那么只需在该词典中查找键为 "Couldn't find file \"[_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 为您生成的短语。有很多方法可以汇编此类列表。最直接的方法是简单地 grep 源代码中“maketext”的每次出现(或对其周围的包装器的调用,如上面的 pmt
函数),并记录以下短语。
此时,您可能需要考虑您的基类(Projname::L10N),从中所有词汇表继承(Projname::L10N::en、Projname::L10N::es 等)是否应为 _AUTO 词汇表。理论上,所有必需的消息都将在每个语言类中,这可能是正确的;但在查找失败的不太可能或“不可能”的情况下,您应考虑您的程序是否应抛出异常、以英语(或您的项目的首选语言)发出文本,或如上文“控制查找失败”部分中所述的更复杂的解决方案。
向翻译人员提交所有消息/短语等。
(如果您不确定是否已正确抽象了代码中与语言相关的部分,则实际上,您可能希望首先从本地化到一种其他语言开始。)
翻译人员可能会要求澄清在特定短语中发现的情况。例如,在英语中,我们完全乐意说“找到n个文件”,无论我们的意思是“我查找了文件,找到了n个文件”,还是“我查找了其他内容(如文件中的行),并且在此过程中我看到了n个文件”。这可能涉及重新考虑您认为非常清楚的事情:“编辑”在工具栏上应该是名词(“编辑”)还是动词(“编辑”)?是否已经有一种约定俗成的表达该菜单选项的方法,与目标语言的正常单词“编辑”分开?
在涉及量化(对任何 N 值说“N个文件”)这一非常普遍现象的所有情况下,每个翻译人员都应明确数字在句子中引起的依赖关系。在许多情况下,依赖关系仅限于数字旁边的单词,在您可能预期它们的位置(“我找到了-?复数N个空-?复数目录-?复数”),但在某些情况下,还有意外的依赖关系(“我找到了-?复数……”!)以及长距离依赖关系(“N个目录-?复数无法删除-?复数”!)
提醒翻译人员考虑 N 为 0 的情况:“0 个文件已找到”在任何语言中听起来都不太自然,但在许多语言中可能不可接受——或者可能需要达成某种特殊协议(类似于英语“I didN'T find ANY files”)。
记得向翻译人员询问其语言中的数字格式,以便您可以根据需要覆盖 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”)。对于其他语言,整个“通用”形式的概念可能几乎毫无用处,尤其是对于涉及阿拉伯语或中文形式的语音媒体的界面。
一旦您为所有所需语言本地化了您的程序/网站/等,请务必向翻译人员展示结果(无论是实时还是通过屏幕截图)。一旦他们批准,请尽一切努力让该语言的至少另一位使用者进行检查。即使(或尤其当)翻译是由您自己的程序员之一完成时,这一点也成立。根据所涉及的特定领域术语和概念的数量,某些类型的系统可能比其他类型的系统更难找到测试人员——找到可以告诉您他们是否认可您在电子邮件通过 Web 界面中“删除此消息”的翻译的人比找到可以就 XML 查询工具界面中“属性值”的翻译向您提供明智意见的人更容易。
我建议阅读所有这些内容
Locale::Maketext::TPJ13——我在《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 分发版可在 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 [email protected]