Locale::Maketext::TPJ13 -- 关于软件本地化的文章
# This an article, not a module.
Sean M. Burke 和 Jordan Lachler 撰写的以下文章首次出现在《Perl 杂志》第 13 期,版权归 1999 年《Perl 杂志》所有。它是在 Jon Orwant 和《Perl 杂志》的许可下发布的。本文件可以根据与 Perl 本身相同的条款进行分发。
作者:Sean M. Burke 和 Jordan Lachler
本文指出了 gettext(一种常见的用于本地化软件界面的系统 - 即使其在用户的选择语言中工作)由于人类语言之间的基本差异而失败的情况。本文随后描述了 Maketext,这是一种能够正确处理这些差异的新系统。
"世界上有许多人类使用的语言。"
-- Harald Tveit Alvestrand,在 RFC 1766 中,"语言识别标签"
想象一下,你今天的工作是本地化一个软件——幸运的是,这个程序唯一输出的是两条消息,如下所示
I scanned 12 directories.
Your query matched 10 files in 4 directories.
那么这有多难呢?你查看生成第一项的代码,它显示为
printf("I scanned %g directories.",
$directory_count);
你思考了一下,意识到它甚至对英语都不起作用,因为它可以产生以下输出
I scanned 1 directories.
所以你将其重写为
printf("I scanned %g %s.",
$directory_count,
$directory_count == 1 ?
"directory" : "directories",
);
...这做对了。(如果你不记得,"%g" 用于特定于区域设置的数字插值,而 "%s" 用于字符串插值。)
但你仍然需要为所有你正在为其制作此软件的语言进行本地化,所以你从 CPAN 上拉取 Locale::gettext,以便你可以访问你听说过用于本地化任务的标准 gettext
C 函数。
然后你写道
printf(gettext("I scanned %g %s."),
$dir_scan_count,
$dir_scan_count == 1 ?
gettext("directory") : gettext("directories"),
);
但你随后在 gettext 手册(Drepper、Miller 和 Pinard 1995)中读到,这不是一个好主意,因为像 "directory" 或 "directories" 这样的单个单词的翻译可能取决于上下文——这是真的,因为在像德语或俄语这样的语言中,你可能需要在第一个实例(单词是动词的宾语)中使用不同的格结尾,而不是在第二个实例(单词是介词的宾语,"in %g directories")中——假设这些在翻译成这些语言时保持相同的语法。
因此,根据 gettext 手册的建议,你重写为
printf( $dir_scan_count == 1 ?
gettext("I scanned %g directory.") :
gettext("I scanned %g directories."),
$dir_scan_count );
所以,你给不同的翻译人员发送邮件(老板决定这次要翻译的语言是中文、阿拉伯语、俄语和意大利语,所以你分别找了一位翻译),要求他们翻译“我扫描了 %g 个目录”和“我扫描了 %g 个目录”。当他们回复后,你就会把这些翻译内容放到 gettext 的词典中,以便在软件本地化时使用。这样,当用户在“zh”(中文)区域设置下运行时,gettext("我扫描了 %g 个目录") 将返回相应的中文文本,其中包含一个 "%g",printf 可以用 $dir_scan 来填充它。
你的中文翻译很快就回复了——他说这两个短语在中文中翻译成同一个意思,因为用语言学术语来说,中文“没有数作为语法范畴”——而英语有。也就是说,英语有关于“数”的语法规则,即事物在语法上是单数还是复数;其中一条规则是强制名词在复数语境中使用复数后缀(通常是“s”),就像它们在“一”以外的数字(包括奇怪的“零”)之后一样。中文没有这样的规则,所以只有一个短语对应英语的两个短语。但是,没关系,你可以将这个中文短语作为你程序“zh” gettext 词典中两个英语短语的翻译。
受此鼓舞,你开始着手处理软件需要输出的第二个短语:“您的查询在 4 个目录中匹配了 10 个文件”。你注意到,如果你想像 gettext 手册中明智地建议的那样,将短语视为不可分割的整体,那么现在你需要四种情况而不是两种情况来涵盖 $dir_count 和 $file_count 的单复数排列。所以你尝试了这个
printf( $file_count == 1 ?
( $directory_count == 1 ?
gettext("Your query matched %g file in %g directory.") :
gettext("Your query matched %g file in %g directories.") ) :
( $directory_count == 1 ?
gettext("Your query matched %g files in %g directory.") :
gettext("Your query matched %g files in %g directories.") ),
$file_count, $directory_count,
);
(我想,“1 个文件在 2 个[或更多]目录中”的情况可能发生在符号链接或类似的情况下。)
你意识到这并不是你写过的最漂亮的代码,但似乎这是最好的方法。你给翻译人员发送邮件,要求他们翻译这四种情况。中文翻译回复说这些情况在中文中都翻译成同一个短语,这个短语包含两个 "%g",这是应该的——但有一个问题。他逐字翻译回来:“在 %g 个目录中包含 %g 个文件匹配您的查询。” "%g" 的位置与英语中的顺序相反。你不知道如何让 gettext 处理这种情况。
但你暂时把它放在一边,乐观地希望其他翻译人员不会遇到这个问题,他们的语言会更规范——也就是说,它们会像英语一样。
阿拉伯语翻译人员第一个回复。首先,你代码中“我扫描了 %g 个目录”或“我扫描了 %g 个目录”的代码假设只有单数和复数。但再次使用语言学术语,阿拉伯语有语法数,就像英语一样(但不像中文),但它是一个三项类别:单数、双数和复数。换句话说,你如何说“目录”取决于是否有 1 个目录,或者 2 个,或者 2 个以上。你的 ($directory == 1)
测试不再有效。这意味着,英语的语法数类别只需要根据“目录[单数]”和“目录[复数]”来进行第一句话的两种排列,而阿拉伯语有三种 - 更糟糕的是,在第二句话(“你的查询在 %g 个目录中的 %g 个文件中匹配”)中,英语有四种,阿拉伯语有九种。你感觉到了一种不受欢迎的指数级趋势正在形成。
你的意大利语翻译人员通过电子邮件回复你,说“我搜索了 0 个目录”(你程序可能输出的英语)很生硬,如果你认为这是好的英语,那是你的问题,但这在但丁的语言中是行不通的。他坚持认为,当 $directory_count 为 0 时,你的程序应该生成意大利语文本“我没有扫描任何目录”。同样地,“我没有在任何目录中匹配任何文件”,尽管他说最后关于“在任何目录中”的部分可能应该省略。
你不知道如何让 gettext 处理这个问题;为了适应阿拉伯语、中文和意大利语在这些简单短语中处理数字的方式,你需要编写代码,根据所讨论的数值是 1、2、大于 2 还是在某些情况下为 0 来向 gettext 提出不同的查询,而且你还没有解决中文中不同词序的问题。
然后你的俄罗斯语翻译人员打电话来,亲自告诉你关于你生活将变得多么糟糕的坏消息。
俄语,就像德语或拉丁语一样,是一种屈折语言;也就是说,名词和形容词必须带上取决于其格(即主格、宾格、属格等)的词尾——这大致上是它们在句子语法中所起作用的问题——以及名词的语法性别(即阳性、阴性、中性)和数(即单数或复数),以及名词的词类。但与大多数其他屈折语言不同的是,在俄语中,在名词前面加上一个数词短语(如“十”或“四十三”,或它们的阿拉伯数字等价物)会改变名词的格和数,因此也会改变你必须加在它上面的词尾。
他详细解释道:在“我扫描了 %g 个目录”中,你期望“目录”处于宾格(因为它是句子的直接宾语)和复数,除了 $directory_count 为 1 时,你当然会期望单数。就像拉丁语或德语一样。但是!当 $directory_count % 10 为 1(“%”表示取模,记住)时,假设 $directory_count 是一个整数,并且除了 $directory_count % 100 为 11 时,“目录”被迫变成语法上的单数,这意味着它获得了宾格单数的词尾……你开始想象实现这个功能所需的代码,并且仍然适用于中文、阿拉伯语和意大利语,以及需要多少个 gettext 项目,但他继续说……但是当 $directory_count % 10 为 2、3 或 4(除了 $directory_count % 100 为 12、13 或 14 时),“目录”这个词被迫变成属格单数——这意味着另一个词尾……房间开始慢慢地旋转起来……但是对于所有其他整数值,由于“目录”是一个无生命名词,当它被一个数字放在前面并且处于主格或宾格(就像这里一样,真是你的幸运!)时,它确实保持复数,但它被迫进入属格——又是另一个词尾……你从未听到他说过你将如何遇到类似(但可能微妙地不同)的关于其他斯拉夫语言(如波兰语)的问题,因为地板迎面而来,你昏迷过去。
以上警示故事讲述了如何将本地化尝试从程序员的困惑,变成程序的混淆,再变成对镇静剂的需求。但仔细评估表明,你选择的工具仅仅需要进一步考虑。
"这比你想象的要复杂得多。"
-- RFC 1925 中的第八条网络真理
在过去的一个世纪里,语言学领域投入了大量的精力试图找到跨语言的语法模式;这是一个不断有人提出适用于所有语言的概括,然后发现这些概括往往失败的过程——有时只对少数语言失败,有时对整个语言类别失败,有时几乎对世界上除英语以外的所有语言都失败。在“平均语言”的规则可能是什么样子、必须是什么样子以及不能是什么样子方面,明显的统计趋势是显而易见的。但“平均语言”就像“平均人”一样是一个不切实际的概念——它遇到了这样一个事实,即没有一种语言(或人)实际上是平均的。过去的经验告诉我们,任何一种语言都可以按照它想要的任何顺序,以任何类型的语法类别为依据来做任何它想做的事情——格、数、时态、词语所指代事物的真实或比喻特征、基于词语可以接受的词尾或前缀的任意或可预测的词语分类、对表达的陈述的真实性的确定程度或方式,等等,无穷无尽。
幸运的是,大多数本地化任务都是找到方法来翻译整个短语,通常是句子,其中上下文相对固定,内容的唯一变化通常是表达的数字——如上面的示例句子。在实践中,翻译特定的、完整的句子是相当可靠的——这很好,因为这就是许多游客依赖的短语手册中的内容。现在,一个给定的短语(无论是在短语手册中还是在 gettext 词典中)在一种语言中可能比该短语翻译成另一种语言的适用性更大或更小——例如,严格地说,在阿拉伯语中,“你的”在“你的查询匹配...”中将采用不同的形式,具体取决于用户是男性还是女性;因此,阿拉伯语翻译“你的[女性]查询”的适用范围比相应的英语短语更小,后者不区分用户的性别。(在实践中,让程序知道用户的性别是不切实际的,因此默认情况下通常使用阿拉伯语中的男性“你”。)
但总的来说,当翻译整个句子时,尤其是当功能上下文仅限于计算机与用户交互以传达事实或提示信息时,这种意外情况很少见。因此,出于本地化的目的,按短语(通常按句子)翻译是最简单也是最不麻烦的。
"它必须工作。"
-- 第一个网络真理,RFC 1925
考虑一下旅游词典中的句子,它们分为两种类型:一种像“我怎么去市场?”,没有需要填写的空格;另一种像“这些___多少钱?”,有一个或多个空格需要填写(通常与一个词语列表相关联,你可以将这些词语填入空格中:“鱼”、“土豆”、“西红柿”等等)。没有空格的句子没有问题,但填空句子可能并不直截了当。例如,如果这是一本斯瓦希里语词典,作者可能不会告诉你动词“cost”根据你填入空格的名词而改变其词缀的复杂方式。市场上的商人仍然能理解你在说什么,即使你在“how much do these potatoes cost?”中使用了错误的“cost”词缀。毕竟,你不会说流利的斯瓦希里语,你只是一个游客。但游客可以很愚蠢,而计算机应该很聪明;计算机应该能够填补空白,并使结果仍然是语法正确的。
换句话说,词典条目将一些值作为参数(你填入空格中的内容),并根据这些参数提供一个值,其中你从给定值中获得最终值的方式,严格来说,可能涉及任意复杂的运算系列。(在汉语中,这并不复杂,至少在本文开头提到的例子中是如此;而在俄语中,这将是一个相当复杂的运算系列。在某些语言中,复杂性可能以不同的方式分布:将数字表达式放在名词短语前面本身可能并不复杂,但它可能会改变你必须如何,例如,在句子的其他地方改变动词的词尾。这在语法中被称为“长距离依赖”。)
这种关于参数和任意复杂性的讨论只是另一种说法,即词典中的条目在编程语言中被称为“函数”。为了避免你错过,这是本文的核心:短语是一个函数;词典是一堆函数。
使用 gettext 遇到瓶颈的原因(就像上面第二人称恐怖故事中描述的那样)在于,你试图用一个字符串(或者更糟,一堆字符串中的一个选择)来完成实际上需要函数才能完成的任务,这是徒劳的。对从 gettext 获取的字符串进行 (s)printf 插值确实可以让你以相当好的方式完成一些常见的事情……有时……有点;但是,用一些人对 csh
脚本编程的评价来打个比方,“它让你误以为可以用它来做真正的事情,但你不能,而且你直到花了很多时间尝试之后才发现这一点,到那时已经太晚了。”
因此,需要用一个支持函数词典而不是字符串词典的系统来替换 gettext。来自这种系统的词典中的一个条目不应该像这样
"J'ai trouv\xE9 %g fichiers dans %g r\xE9pertoires"
[\xE9 在 Latin-1 中是 e-acute。如果我在此处使用实际字符,一些 pod 渲染器会尖叫。-- SB]
而是像这样,记住这只是一个初步尝试
sub I_found_X1_files_in_X2_directories {
my( $files, $dirs ) = @_[0,1];
$files = sprintf("%g %s", $files,
$files == 1 ? 'fichier' : 'fichiers');
$dirs = sprintf("%g %s", $dirs,
$dirs == 1 ? "r\xE9pertoire" : "r\xE9pertoires");
return "J'ai trouv\xE9 $files dans $dirs.";
}
现在,在 gettext 词典中除了字符串之外,没有特别明显的方法可以存储任何东西;所以看起来我们只能从头开始,做一些更好的东西。我将我尝试的 gettext 替代系统称为“Maketext”,或者在 CPAN 术语中,称为 Locale::Maketext。
在设计 Maketext 时,我选择以“符合流行语”的方式规划其主要功能。以下是这些流行语
你试图输出的语言的复杂性完全抽象在(并封装在)该接口的 Maketext 模块中。当你调用
print $lang->maketext("You have [quant,_1,piece] of new mail.",
scalar(@messages));
你不知道(实际上也无法轻易找出)这是否会涉及大量计算,比如在俄语中(如果 $lang 是俄语模块的句柄),或者相对较少,比如在中文中。这种抽象和封装可能会鼓励其他令人愉快的流行语,比如模块化和分层,这取决于你做出的设计决策。
“同构”的意思是“具有相同的结构或形式”;在程序设计讨论中,这个词具有特殊的、特定的含义,即你对问题的解决方案的实现具有相同的结构,比如对解决方案的非正式口头描述,或者可能是问题本身。总的来说,同构是一件好事——它应该是解决问题(和实现解决方案)的样子。
像这样的使用 gettext 的代码有什么问题……
printf( $file_count == 1 ?
( $directory_count == 1 ?
"Your query matched %g file in %g directory." :
"Your query matched %g file in %g directories." ) :
( $directory_count == 1 ?
"Your query matched %g files in %g directory." :
"Your query matched %g files in %g directories." ),
$file_count, $directory_count,
);
首先,它没有很好地抽象——这些测试语法数的方法(比如 foo == 1 ? singular_form : plural_form
这样的表达式)应该抽象到每个语言模块中,因为你如何获得语法数是特定于语言的。
但第二点,它不是同构的——从这四个英文短语到一个适合所有短语的中文短语的“解决方案”(即,词典条目)。换句话说,非正式的解决方案将是“用一个短语‘对于你的问题,在 Y 目录中你会找到 X 文件’来表达你想用中文说的话”——因此,实现的解决方案应该同构地,只是一个直接输出该短语的方法,并正确地插入数字。它不应该从其他语言的复杂性映射到这种语言的简单性。
在相关方言的模块之间共享短语,或在相关语言之间共享辅助函数,存在大量的重用可能性。(“辅助函数”是指不生成短语文本的函数,而是返回对“这个数字是否需要在其后使用复数名词”的答案。此类辅助函数将用于实际生成短语文本的函数的内部逻辑中。)
在共享短语的情况下,请考虑您已经有一个针对美式英语本地化的界面(可能是通过将其编写为本地语言来实现的,但这只是一个偶然情况)。从实际角度来看,将其本地化为英式英语应该仅仅是将其交给一个英国人,并指示他们指出哪些短语需要更改拼写或进行轻微的改写。在这种情况下,您应该只在英式英语本地化模块中放入那些特定于英国的短语,而对于所有其他短语,则从美式英语模块继承。(我预计这种情况也适用于巴西葡萄牙语和大陆葡萄牙语,可能还有一些非常密切相关的语言,如捷克语和斯洛伐克语,以及我听说存在于台湾和中国大陆的书面普通话的略微不同的“版本”。)
关于辅助函数的共享,考虑本文开头提到的俄语数字问题;显然,您只想编写一次用于处理数字值的复杂代码,该代码可以返回给定量词名词应该使用哪种格和数的规范。但假设您在为乌克兰语(一种与俄语相关的斯拉夫语言,有数百万人口使用,其中许多人会很高兴地发现您的网站或软件的界面可以用他们的语言使用)本地化界面时发现,乌克兰语的量词规则与俄语相同,并且可能适用于许多其他语法功能。虽然俄语和乌克兰语之间可能没有共同的短语,但您仍然可以选择让乌克兰语模块继承自俄语模块,仅仅是为了继承所有各种语法方法。或者,从组织角度来看,您可能更愿意将这些函数移到一个名为_E_Slavic
的模块中,俄语和乌克兰语可以从该模块继承有用的函数,但该模块(可能)不会提供任何词汇表。
好的,简洁不是一个关键词。但它应该是一个关键词,所以我宣布它是一个新的关键词,“简洁”意味着简单常见的事物应该可以用很少的代码行(甚至可能只是一些字符)来表达——称之为“使简单的事情变得容易,使困难的事情变得可能”的特殊情况,并且还可以参考它在 MIDI::Simple 语言中的作用,本文的其他部分对此进行了讨论 [TPJ#13]。
考虑我们对“函数词典”中条目的第一次尝试
sub I_found_X1_files_in_X2_directories {
my( $files, $dirs ) = @_[0,1];
$files = sprintf("%g %s", $files,
$files == 1 ? 'fichier' : 'fichiers');
$dirs = sprintf("%g %s", $dirs,
$dirs == 1 ? "r\xE9pertoire" : "r\xE9pertoires");
return "J'ai trouv\xE9 $files dans $dirs.";
}
您可能会感觉到,一个由函数组成的词汇表(使用一个不带承诺的通用术语来表示您知道如何说的一组事物,无论它们是短语还是单词),如果像上面那样表达,会使代码变得冗长且重复——即使您明智地重写了它,使量词(我们称之为在名词短语中添加数字表达式)成为一个像这样调用的函数
sub I_found_X1_files_in_X2_directories {
my( $files, $dirs ) = @_[0,1];
$files = quant($files, "fichier");
$dirs = quant($dirs, "r\xE9pertoire");
return "J'ai trouv\xE9 $files dans $dirs.";
}
您可能还会感觉到,您不想让您的翻译人员费心编写 Perl 代码——您更希望他们将非常宝贵的时间花在翻译上。更不用说几乎不可能找到一个知道简单 Perl 代码的商业翻译人员了。
在 Maketext 的第一次尝试实现中,每个语言模块的词汇表看起来像这样
%Lexicon = (
"I found %g files in %g directories"
=> sub {
my( $files, $dirs ) = @_[0,1];
$files = quant($files, "fichier");
$dirs = quant($dirs, "r\xE9pertoire");
return "J'ai trouv\xE9 $files dans $dirs.";
},
... and so on with other phrase => sub mappings ...
);
但我立即开始寻找一种更简洁的方法来基本上表示相同的短语函数——一种方法,它也可以用来简洁地表示大多数语言词汇表中的大多数短语函数。经过长时间的思考,甚至一些实际的思考,我决定使用以下系统
* 当 %Lexicon 哈希中的值是一个内容字符串而不是一个匿名子程序(或者,可以想象,是一个代码引用)时,它将被解释为对子程序执行内容的简写表达式。当在会话中第一次访问它时,它会被解析,转换为 Perl 代码,然后被 eval 为一个匿名子程序;然后,该子程序替换词汇表中的原始字符串。(这样,解析和评估给定短语的简写形式的工作最多只在每个会话中完成一次。)
* 对maketext
(作为 Maketext 的主函数)的调用通过一个“语言会话句柄”进行,从概念上讲,它非常类似于一个 IO 句柄,因为您在会话开始时打开一个句柄,并使用它向一个对象“发送信号”,以便它返回您想要的文本。
所以,这
$lang->maketext("You have [quant,_1,piece] of new mail.",
scalar(@messages));
基本上意味着:在词典中查找 $lang(它可能从任意数量的其他词典继承),并找到与字符串“You have [quant,_1,piece] of new mail”关联的函数(它在本地语言中是,也应该是,该函数的“简写”——在本例中为英语)。如果找到这样的函数,则使用 $lang 作为其第一个参数(就像它是方法一样)调用它,然后将标量(@messages) 的副本作为其第二个参数,然后返回该值。如果找到该函数,但它是字符串简写而不是完全指定的函数,则在第一次调用它之前解析它并将其转换为函数。
* 简写使用方括号中的代码来指示应执行的方法调用。这里没有必要进行完整解释,但几个例子就足够了
"You have [quant,_1,piece] of new mail."
上面的代码是简写,将被解释为:
sub {
my $handle = $_[0];
my(@params) = @_;
return join '',
"You have ",
$handle->quant($params[1], 'piece'),
"of new mail.";
}
其中“quant”是用于用数字 $params[0] 对名词“piece”进行量化的一个方法的名称。
没有方括号调用的字符串,例如:
"Your search expression was malformed."
有点退化的情况,只是变成了
sub { return "Your search expression was malformed." }
但是,并非所有可以在 Perl 代码中编写的代码都可以在上面的简写系统中编写——远远没有。例如,考虑本文开头提到的意大利语翻译,他希望将“I didn't find any files”的意大利语作为特殊情况,而不是“I found 0 files”。这在我们的简写系统中无法指定(至少不容易或简单),并且必须完整地写出来,如下所示
sub { # pretend the English strings are in Italian
my($handle, $files, $dirs) = @_[0,1,2];
return "I didn't find any files" unless $files;
return join '',
"I found ",
$handle->quant($files, 'file'),
" in ",
$handle->quant($dirs, 'directory'),
".";
}
在一个充满简写代码的词典旁边,这种代码就像一根刺眼的大拇指——但毕竟这是一个特殊情况;至少它是可能的,即使不像往常那样简洁。
至于如何实现本文开头提到的俄语示例,嗯,There's More Than One Way To Do It,但它可能类似于这样(使用英语单词表示俄语,以便您知道发生了什么)
"I [quant,_1,directory,accusative] scanned."
这将复杂性的负担转移到了 quant 方法上。该方法的参数是:它将用来量化某物的数值;它将用来量化的俄语单词;以及参数“accusative”,您使用它来表示该句子的语法需要一个宾格名词,尽管该量化方法可能需要为了语法原因而覆盖它,您可能还记得本文开头的语法原因。
现在,这里的俄语 quant 方法不仅负责实现确定俄语数字短语如何对名词短语施加格和数的奇怪逻辑,还负责对俄语单词“directory”进行词形变化。这种词形变化如何进行并非小事,在我见过的解决方案中,有些(例如在哈希表中查找所有可能形式的简单变体,其中提供了所有必要单词的所有可能形式)很简单,但当您需要对数十个以上的单词进行词形变化时,可能会变得很麻烦;而其他解决方案(例如使用算法来模拟词形变化,只存储词根形式和不规则形式)可能会涉及比对除最大词典之外的所有词典都合理的开销更大。
幸运的是,这个设计决策只有在最复杂的屈折语言中才会变得至关重要,而俄语虽然不是最糟糕的情况,但比大多数语言都要糟糕。大多数语言的屈折系统比较简单;例如,在英语或斯瓦希里语中,一个给定名词通常只有两种可能的屈折形式(“error/errors”;“kosa/makosa”),并且生成这些形式的规则相当简单——或者至少,可以制定适用于大多数单词的简单规则,然后可以将例外情况视为“不规则”,至少相对于你的临时规则而言。更简单的屈折系统(更简单的规则,更少的形式)意味着设计决策对于保持理智来说不那么重要,而同样的决策在俄语等语言中可能会导致开销与可扩展性问题。对于所讨论的语言,无论简单还是复杂,代码(可能是 Perl,如 Lingua::EN::Inflect,用于英语名词)可能已经编写好了。
此外,第三种可能性甚至比上面讨论的任何内容都更简单:“只需要求在调用给定语言的 quant 方法时提供所有可能的(或至少适用的)形式,例如:”
"I found [quant,_1,file,files]."
这样,quant 只需要选择它需要的形式,而无需查找或生成任何内容。虽然可能不是俄语的最佳选择,但这应该适用于大多数其他语言,因为在这些语言中,量化不是一项复杂的运算。
Maketext 比上面描述的要多得多——例如,还有语言标签(“en-US”,“i-pwn”,“fi”等)或区域设置 ID(“en_US”)如何与实际模块命名(“BogoQuery/Locale/en_us.pm”)交互,以及会发生什么魔法;还有如何记录(并可能协商)Maketext 将以何种字符编码返回文本的细节(UTF8?Latin-1?KOI8?)。有趣的是,Maketext 用于本地化,但实际上没有任何地方使用“use locale;
”。对于好奇的人来说,我实际上是如何实现像数据继承这样的东西,以便跨模块的 %Lexicon 哈希的搜索可以与 Perl 如何实现方法继承相平行,这有点令人恐惧。
最重要的是,还有如何从 Maketext 派生以供你的接口使用,以及用于开始和维护单个语言模块的各种工具和约定等所有实际细节。
所有这些都在 Locale::Maketext 及其附带模块的文档中有所介绍,可在 CPAN 上获得。在阅读了本文之后,本文介绍了 Maketext 的原因,文档介绍了 Maketext 的方法,应该非常简单明了。
Maketext 和 gettext 有一个显著的区别:gettext 是用 C 编写的,可以通过 C 库调用访问,而 Maketext 是用 Perl 编写的,实际上无法在没有 Perl 解释器的情况下工作(虽然我认为可以用 C 为它编写类似的东西)。历史的偶然性(并非一定是幸运的偶然性)使 C++ 成为实现像文字处理器、Web 浏览器,甚至许多内部应用程序(如自定义查询系统)等应用程序的最常见语言。目前的情况使得这些类型的应用程序中下一个应用程序不太可能用 Perl 编写,尽管这显然更多地是出于习惯和惯性,而不是出于对哪种工具最适合工作的考虑。
然而,历史上的其他事件使得 Perl 成为一种广为接受的语言,用于设计网站界面(通常以 CGI 形式)的服务器端程序。网站静态页面的本地化非常简单,可以通过 Apache 等服务器中的简单语言协商功能,或者通过某种形式的服务器端包含语言相关文本到布局模板中来实现。但是,我认为 Perl 基于搜索系统(或其他类型的动态内容)的网站本地化,无论它们是公开的还是访问受限的,是 Maketext 将会得到最大应用的地方。
我假设,只有极少数的网站会针对英语、中文、意大利语、阿拉伯语和俄语进行本地化,这些语言都是本文开头提到的——更不用说德语、西班牙语、法语、日语、芬兰语和印地语了,这些语言都拥有大量的程序员或网站用户,或者两者兼而有之。
然而,网络的国际化程度不断提高(无论是从内容数量、内容作者或程序员数量,还是从内容受众规模来看),这使得平均基于网络的动态内容服务的界面越来越有可能针对两种或三种语言进行本地化。我希望 Maketext 能使这项任务尽可能简单,并消除以前针对与英语不同的语言进行本地化的障碍。
__END__
Sean M. Burke ([email protected]) 拥有西北大学语言学硕士学位;他专门从事语言技术。Jordan Lachler ([email protected]) 是新墨西哥大学语言学系的一名博士生;他专门研究北美土著语言的形态学和教学法。
Alvestrand, Harald Tveit. 1995. RFC 1766: 语言标识标签。 http://www.ietf.org/rfc/rfc1766.txt
[现在请参阅 RFC 3066。]
Callon, Ross, 编辑。1996. RFC 1925: 十二条网络真理。 http://www.ietf.org/rfc/rfc1925.txt
Drepper, Ulrich, Peter Miller 和 François Pinard. 1995-2001. GNU gettext
。可在 ftp://prep.ai.mit.edu/pub/gnu/
中获取,发行版 tarball 中包含大量文档。[自从我在 1998 年写这篇文章以来,我发现 gettext 文档现在正试图更多地理解复数。是否从中得出了有用的结论,这是一个完全不同的问题。-- SMB,2001 年 5 月]
Forbes, Nevill. 1964. 俄语语法。 第三版,由 J. C. Dumbreck 修订。牛津大学出版社。