国际化软件包 难易程度: 初学者 技能要求: 您应该熟悉前面的“内容组件”章节的内容; 需要熟悉页面模板; 需要具备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
8
Title
9
10
11 12
13
Author
14
15
16 17
18
Date/Time
19
20
21 22
23
Parent
24
25 28
29
30 31
32
Body
33
34
35 36
37 38 ]]> 第3行:在div标记中指定域(domain)是最好的选择,因为它在一个特定的区域了并且不会影响到其它模板文件的域设置。 第8、13、18、23&32行:标有i18n:translate="" 的div标记里面的内容将被转换。 注意在这儿我们没有必要使用i18n:attributes。然而,当我们处理按钮(button),我们还是经常这样做,实例如下: ]]> 类似与tal:attributes,value-attribute的值将被add-button的转换替代,如果转换没有被发现,就保持缺省字符串(Add)。 这是页面模板真正需要的。通过练习1、2,自行完成其它的模板。(提醒:如果您做练习1,您可以跳过练习2)。 第III步: 国际化ZCML 您需要在configure.zcml文件中的configure标记里添加i18n_domain="messageboard"属性。指令负责指定哪个属性值应该被转换成message id,因此您不需要有任何的担忧。所有的这些看起来都有些不可思议,Zope 3强大的翻译系统真的令人难以置信,因为它能让国际化ZCML代码开销减少到最少。 设置该属性时,您将忽略您所遇到的所有警告,直到Zope 3被启动。 第IV步: 创建语言目录 翻译的目录结构必须遵循一个严格的格式,因为我们尝试让它与gettext保持一致。现在创建一个名叫locales的目录,按照约定,我们把所有的message目录以及所有的message目录模板都放进locales里面。这个目录包含了message目录模板文件(扩展名为pot)和不同的语言目录,比如说en。既然我们想实现从英语转译成德语,那么现在我们应该创建en和de。 首先需要说明的是:接下来的部分可能没有任何意义。语言目录不直接包含message目录,但是需要在每个语言目录里面创建另一个名叫LC_MESSAGES的目录。 既然英语是我们的缺省语言,以致于系统总是会使用缺省语言。因此message目录能够都为空(或者只包含元数据)。在locales/en/LC_MESSAGES目录里创建文件messageboard.po,并且在该文件里加上如下注释和元数据: 现在我们已经做完了准备工作。在我们本地化留言簿程序之前,我们需要创建一个message目录,在下一节我们将继续讨论。 第V步: 提取翻译字符串 Zope 提供了一个非常强大的提取工具来获取所有来自Python、页面模板、ZCML文件的可翻译文本字符串。在message目录模板文件里,就每个可翻译的字符串以注释记录了文件和行号。 收集所有的字符串并且把副本并入各自的条目,工具在名叫<domain>.pot的message目录模板文件里保存了字符串。这才是本地化的开始,从现在开始我们唯一需要关心的就是用模板创建翻译。 i18nextract.py就是系统中的提取工具,您可以在ZOPE3/utilities里发现它,在运行该工具之前,为了发现所有必须的模块,把您的Zope 3 源目录加入到PYTHONPATH。具体做法如下: 运行工具,在messageboard目录里输入如下命令。由于该工具不能很好的以symlinks工作,因此您需要确认您输入ZOPE3的绝对路径。 这将从留言簿软件包中提取所有的可翻译字符串并且把messageboard/locales/messageboard.pot当作存储模板文件。 正如您所看到的,可以通过帮助查看该工具的三个附加选项: -h/--help -获取帮助 -d/--domain<domain> -指定域 -p/--path<path> - 指定路径. -odir - 指定目录 如果您希望更新Zope 3核心的message目录模板文件,您需要在不指定任何附加参数的情况下运行该提取工具。 第VI步: 翻译消息字符串 既然我们已经有了一个Message目录模板文件,那么我们现在就可以着手创建一个翻译。由于现在还没有message目录,您可以拷贝POT模板文件到相应的语言目录。在Unix您可以使用如下命令: 用文本编辑器打开de/LC_MESSAGES/messageboard.po文件。在这里,为了保持文件格式的完整性,我们强烈建议您使用专有的gettext翻译工具。比如说Vim/Emacs、KBabel都是些不错的选择。 高级工具KBabel已经成为进行本地化开发的标准应用程序。它有很多功能可以让翻译工作变得更加容易和高效。我和我妻子使用KBabel翻译了许多的文件,KBabel真是一个让人感到奇妙的工具。举例来说, 它排列所有未翻译或模糊字符串,并且通过提供message编号和统计学来帮助管理message字符串。 在做完翻译工作后,保存您所做过的设置。 我们已经有了翻译功能,但当您又开发了新代码,并且需要更新模板和目录文件,遇到这种情况我们需要采取什么样的措施呢?由于模板从一开始就被创建好了,因此创建的模板您不需要有任何改动。不过对于目录文件,为了不遗漏任何存在的翻译,就需要做一些其它的工作。每个Linux系统都提供了gettext实用工具,其中有个叫msgmerge的命令行工具(在Windows下,您能使用gettext软件包Cygwin版)。msgemerge能把所有改变了的POT文件并进了message目录,保留所有的注解和已经存在的未改变的翻译,并且标识被改变的翻译用做"fuzzy"。 msgmerge工具使用如下: 第VII步: 编译和注册Message目录 在使用我们的新翻译之前,我们需要把message目录编译成一个有效的二进制文件格式,然后以message catalog container注册locales目录。 编译目录,命令如下: msgfmt 程序是 gettext 工具的一部份, 您一定曾经安装 ( 如同msgmerge工具) 过并成功运行过上述命令。 如果您觉得记住po和mo扩展名很难,现在就教您一招:".po" 中的"p" 就如同 "people comprehensible(人能理解)" ,".mo" 中的"m" 就如同 "machine comprehensible(机器能理解)"。 注册locales目录作为翻译container,打开configure.zcml文件,按如下形式注册i18n命名空间: 现在注册目录用: ]]> i18n:registerTranslations 指令能发现目录结构和并且为所有的可用语言提取所有的message的目录。 重要提示:在最后一些步骤期间,message目录的文件名必须声明成域名称!registerTranslations指令将使用文件名来决定域,当然这完全与gettext标准一致。 第VIII步: 尝试翻译 为了测试翻译,请重新启动Zope 3,最好在Mozilla里测试不同的语言,原因在于该浏览器允许您在浏览器里快速更改您接受的语言。具体做法就是:在语言导航参数设置里面更改语言。把German[de]放在列表的最上面。您可以通过类似下面的URL来进行测试预览: 现在您可以看看各个属性名称在德语里如何显示(例如Title已经变成了Titel)。日期格式也变成德语标准格式"天.月.年"以及以24小时格式标注的时间。 第IX步: 在运行中更新翻译 当翻译软件包时,更新翻译就重启Zope 3真的很令人厌烦。就是由于这个原因,系统提供了一个叫"Translation Domain Control" ( ) 的工具。该工具允许您在不重新启动服务器的前提下,在运行中可以更新message目录。您应该看看用于德语的新messageboard域de。 练习 完成所有页面模板的国际化。 就练习1提取新的message字符串,并且把他们与已经存在的翻译合并以及更新英语message目录。