国际化软件包
难易程度: 初学者
技能要求:
您应该熟悉前面的“内容组件”章节的内容;
需要熟悉页面模板;
需要具备Gettext工具的基础知识(可选)。
问题/任务:
现在我们有一个可以正常工作的软件包,如今我们需要替我们的海外朋友想想了,事实上不是每个人都能讲英语。因此我们现在的任务就把代码国际化和本地化...让我们试试德语。
解决方案:
在我们开始编码以前,对一些基础概念做一些了解是很重要的,您可能已经想知道国际化和本地化条款之间的所存在的差异。
国际化(I18n)就是制作可翻译软件包的过程,程序员的基本任务就是插入必要的代码,把字符串翻译成可理解的格式并且日期/时间被格式化,这与用户的本地设置(“locale”)有关。
本地化(L10n)实际上就是转换成一种特殊语言的过程。翻译常常不是有程序员来做,而是由翻译者来做(或者由正式的本地供货商来做)。
但是一个所谓的“locale(本地)”是什么? Locales是一个包含世界上一个特定物理区域/抽象区域的信息对象,例如语言、方言、货币单位、日期/时间/数字 格式等。例子中的locale将是“de_DE_PREEURO”(语言、国家/地区,差异),在Euro被介绍之前以德国来描述。然而,“de”也是一个有效的locale,涉及到所有讲德语的地区。因此您可以想象的到这儿有一个locale体系。“de_DE_PREEURO”是比“de_DE”更加特殊,当然更特殊于“de”。因而如果用户的locale设置是“de_DE_PREEURO”并且我们想寻找一个日期格式模板,系统将在“de_DE_PREEURO”这个路径里寻找该模板,然后依次是“de_DE”,最后才是“de”。
注意这章与Python开发完全无关,但知道一些仍然是有用的,因为所有Zope3核心组件都要求被国际化。
第I步: Python代码国际化
这儿有几处必须国际化,视图中的可翻译的字符串,它通常在页面模板中编码。另外就是schema,因为它总是为声明的字段定义一个可理解的标题(title)、描述(description)和缺省的文本值(text value)。
Zope用message id标识string为可翻译。可翻译字符串必须携带一个域,因而我们就知道哪个翻译域我们可以挑选。
我们使用消息id工厂(factory)创建一个消息id:
第1-2行:每个包含可翻译字符串Python文件都必须包含这个样板文件。
Import用"zope"域创建一个消息id工厂。
第2行:底线字符普遍用于表示"翻译功能(translation function)"(来自gettext)。当前情况下它被用作消息id constructor/factory.。MessageIDFactory参数就是我们当前的messageboard域。
但是为什么我们在开始位置需要域呢,比如说单词Sun对域有什么需求呢。Sun在英语有三种不同的意思:(1)太阳星球 (2)星期天的缩写 (3)Sun微系统公司。所有的这些意思在德语中都对应着不同的翻译。因此您能通过指定域来区分他们,例如"astronomy(天文学)"、"calendar(日历)"和"companies(公司)",域的作用几乎类似库,它也允许我们组织和重用翻译。例如,不是每一个单独地软件包都需要把它自己的"calendar(日历)"翻译收集到它的库中,所有的软件包能受益于一个结合的域。
分类翻译的另一个方法就是通过创建抽象message字符串。例如,Add button的value变成了add-button替代通常的Add,翻译这个字符串然后插入这个可理解串,例如英语中Add或德语中的Hinzufgen。我们将在页面模板中看到这个特定的用法(看下一章)。这些"抽象message字符串"被认为是"明确的message ID"。
您也可能想知道我们为什么必须使用message id概念,为什么用message id来代替直接使用翻译功能。这里我们需要回顾一下Zope,由于Zope是一个应用程序服务器并且有很多用户通过网络接受它的服务,因此当一个程序块被调用后,我们不会知道用户究竟想要使用哪种语言。通过分析,我们发现只有视图(Python代码和页面模板)有关于用户和用户本地设置的信息,当然按照惯例,本地设置中就包括了我们需要的语言信息,为了获得相应的语言信息,我们就不得不对翻译做尽可能的扩展。根据以往经验,我们认为翻译是在提供给最终用户输出应用程序应该做的最后一个任务。当然,Zope 3也会遵从这个规则。
让我们回到翻译Python代码。既然接口有大多数可翻译的字符串,我们就从它们开始。打开interfaces.py模块并且添加我们上面提到的样板文件。现在我们国际化每个字段。例如,ImessageBoard 架构中的description字段由:
改变成:
注意下划线message id工厂简单函数就如同一个翻译的message。在接口模块中对所有的架构文件做相同的转换。注意当title和description要求unicode字符串,我们能简单地把一个规则的字符串放进message id工厂,因此message id使用unicode作为它的基本类,使message id看起来像一个unicode对象。另外一个小转换是在fields模块中的ForbiddenTags类的__doc__属性。确定需要以相同的风格来国际化它。
更多有趣的是在message.py、MessageSized类、sizeForDisplay()方法里发现标记message字符串,源代码是:
对于这些过于简单化的message id,这些用法将带来问题,因为在我们的string里现在有如同于`messages`+_("replies")的变量,由于下划线对象与gettext应用程序恰好相反,它实际上不做转换。此时查询转换只会失败,因为系统将寻找类似"2 replies"、 "3 replies"等等转换。这就意味着实际变量需要在转换后被插入到text之内。正确的应该是,MessageId对象有一个映射(mapping)属性,属性能存储所有的在转换完成后将要插入它们的变量。这当然也意味着我们也必须以不同的方法标注我们的文本字符串(text string),因此新代码变成了:
第1-8行:这里我们处理了我们可能遇到的四种不同情况。这可能不是最有效率的方法,不过它允许我们分开的列出所有的四个组合, 所以消息串(message string)提取工具将能够发现它。这个寻找字符串的工具被附上了_()。
注意:%i被替换成了${messages} 和 ${attachments},该方法用于标记的转换域插入相应的变量。
第10行:一旦message id 被构造,我们添加映射给这两个必需的变量值。
由于我们已经为大小适配器编写了测试,如今我们需要改正它们。在继续阅读之前,您可以自己试着修正该测试,改变sizeForDisplay() 方法的doc字符串:
>> size = MessageSized(Message())
4
5 Here are some examples of the expected output. 这儿有一些预期输出的例子。
6
7 >>> str = size.sizeForDisplay()
8 >>> str
9 u'${messages} replies, ${attachments} attachments'
10 >>> 'msgs: %(messages)s, atts: %(attachments)s' %str.mapping
11 'msgs: 0, atts: 0'
12 >>> size._message['msg1'] = Message()
13 >>> str = size.sizeForDisplay()
14 >>> str
15 u'1 reply, ${attachments} attachments'
16 >>> 'msgs: %(messages)s, atts: %(attachments)s' %str.mapping
17 'msgs: 1, atts: 0'
18 >>> size._message['att1'] = object()
19 >>> str = size.sizeForDisplay()
20 >>> str
21 u'1 reply, 1 attachment'
22 >>> 'msgs: %(messages)s, atts: %(attachments)s' %str.mapping
23 'msgs: 1, atts: 1'
24 >>> size._message['msg2'] = Message()
25 >>> str = size.sizeForDisplay()
26 >>> str
27 u'${messages} replies, 1 attachment'
28 >>> 'msgs: %(messages)s, atts: %(attachments)s' %str.mapping
29 'msgs: 2, atts: 1'
30 >>> size._message['att2'] = object()
31 >>> str = size.sizeForDisplay()
32 >>> str
33 u'${messages} replies, ${attachments} attachments'
34 >>> 'msgs: %(messages)s, atts: %(attachments)s' %str.mapping
35 'msgs: 2, atts: 2'
]]>
第7-11行:sizeForDisplay() 方法现在一个message id 对象。为了表示message id 只使用它的文本部分,在下列的行中它被检查映射存在以及包含正确的值。
第12-35行:使用不同数目的回复和附件重复测试。
我们必须国际化一些Python代码输出在browser/message.py里。字符串'unknown'必须被装入message id 工厂调用。MessageDetails视图类modified()方法返回的字符串必须适合使用用户的locale信息,因为它返回了一个date/time字符串格式。由于MessageDetails是一个视图类,我们有可用的用户locale,因此我们能改变旧版本。
对于国际化版本非常容易。
每个 BrowserRequest 实例有一个locale对象,它代表了用户的区域设置。getFormatter() 方法返回一个能格式化的实例,能基于locale把一个时间对象格式化一个字符串。涉及到的API参考可以查看locale的所有功能。
这已经是必须在Python代码做的每件事。如果您不相信我, 您可以针对可转换字符串检查其它Python模块--您找不到任何东西。按规则,Python仅包含一些位置的可读字符串,这当然是一个很好的现象。
第II步: 国际化页面模板
采用多种方法对页面模板进行国际化是一件非常有趣的事情。由于有太多的嵌套,复杂得已经难以让我们承受。因此发现正确的标记进行国际化不仅仅是我们必须担心的内容。我的建议是:尽可能保持可转换标记简单化,尝试可转换文本不包含太多HTML和TAL代码。
为了在Zope 3的页面模板里实现支持国际化,我们设计了一个新的i18n命名空间。文档您可以在 找到。这儿有三个通用属性,分别是:i18n:domain、i18n:translate和i18n:attributes。注意:i18n已经被很好的移植到了Zope 2,因此您可能对它已经很熟悉了。
在browser软件包里,details.pt是最简洁的页面模板,因此,让我们最先来对该页面进行国际化:
2
3
4
5
Message Details
6
7
11
12
16
17
21
22
30
31
35
36
37
38