i18n的实现

============
 i18n的实现
============

:作者: limodou
:联系: [email protected]
:日期: $Date$
:版本: $Id: i18n.txt 42 2005-09-28 05:19:21Z limodou $
:主页: http://wiki.woodpecker.org.cn/moin/NewEdit
:BLOG: http://www.donews.net/limodou
:版权: GPL

.. contents::


一、通常的实现方法
-----------------

什么是i18n,它是国际化的简称(Internationalization,去掉开始的I和最后的N,中间一共18个字符)。对于它的实现,
在 Python 的文档关于 gettext 中有详细的描述。同时我在wxPyWiki上找到关于wxPyCookbook上关于国际化实现的教程,
我便按文档和教程开始了我的实验。

例子我使用教程上提供的,不过我进行了修改。一是增加了中文,去掉了西班牙语和法语的处理。二是将某些信息放在另一
个模块中,测试跨模块的实现。同时在这个模块中将进行国际化处理的内容放在了列表中,如::

	message = [_('English'), _('Chinese')]

下面我描述一个通常的方法。

1. 在主模块中导入gettext模块,它需要放在进行国际化处理的语句、模块导入之前。如::

	gettext.install('i18ntest', './locale', unicode=True)

   三个参数的意思分别为:

   * 作用域名,用于限定翻译文件的主名 
   * 路径,存放翻译文件的路径 
   * unicode,是否使用unicode(如果你的应用程序是unicode的,则此处应为True)

   上述的指令将安装一个缺省的国际化处理类。在我们需要安装某种特定的国际化处理类时,我们可以::

	gettext.translation('i18ntest', './locale', languages=['en']).install(True)

   这样将安装指定的翻译文件。前两个参数同gettext.install,第三个参数指明语言的种类。gettext.translation将
   返回一个新的对象。执行它的install函数将安装支持指定语言的国际化处理功能。install中的参为是否使用unicode。

2. 在所有需要进行国际化处理的字符串上加上_(),如:English和Chinese需要国际化处理,那么要转化为:_('English')
   和_('Chinese')。这一步工作可能会比较麻烦。

   经过上述的处理,你的程序虽然还没有真正的翻译文件但仍然是可以运行的。

3. 使用pygettext.py进行字符串的抽取。pygettext.py是在python的安装包中自带的一个工具,它位于tools/i18n目录
   中,同时还有一个叫msgfmt.py的程序,是用来将翻译文件转换成gettext可识别的二进制文件。命令行参数如下::

	Python pygettext.py 文件名

   文件名可以同时有多个。执行python pygettext.py --help 可以看它支持哪些选项。如果没有指定输出文件名,则缺
   省会生成一个名为messages.pot的文件。它就是最原始的用来翻译成其它语言的文件。打开它你会看到可能如下的内容::

	# SOME DESCRIPTIVE TITLE.
	# Copyright (C) YEAR ORGANIZATION
	# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
	#
	msgid ""
	msgstr ""
	"Project-Id-Version: PACKAGE VERSION/n"
	"POT-Creation-Date: Mon Jun 14 13:28:26 2004/n"
	"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE/n"
	"Last-Translator: FULL NAME </n">EMAIL@ADDRESS>/n"
	"Language-Team: LANGUAGE </n">[email protected]>/n"
	"MIME-Version: 1.0/n"
	"Content-Type: text/plain; charset=CHARSET/n"
	"Content-Transfer-Encoding: ENCODING/n"
	"Generated-By: pygettext.py 1.5/n"
	
	#: i18ntest.py:28 i18ntest.py:76
	msgid "MiniApp"
	msgstr ""
	
	#: i18ntest.py:39 i18ntest.py:85
	msgid "E&xit"
	msgstr ""

   一个messages.pot由以下内容组成:

   * '#'开始的注释行 
   * 空行
   * msgid行,表明一个翻译项,可以有多行。如果为多行,则形如::

		 msgid ""
		 "This is a multiline/n"
		 "test"
		 如果msgid内容为空,则表示msgstr是文档的信息 
		
   * msgstr行,表明一个译文,可以为多行,格式同msgid。如果msgid为空,则为文档信息。在文档信息中可以录入关
     于此译文的一些情况,同时最重要的就是charset域,用于指明此文档的编码信息,也就是保存此文档时所用的编码。
     建议对中文使用utf-8编码。
	
   那么我们得到这个pot文件后,先将其保存为不同的版本,以便下面的翻译。如:messages_cn.po。翻译时,只考虑
   msgid不为空的地方,将译文写在msgstr处即可。

4. 使用msgfmt.py将po文件转换为二进制的mo文件。命令行如下::

	python msgfmt.py messages_cn.po

   这样将生成一个名为messages_cn.mo的二进制文件。

5. 安装mo文件。其实就是将mo文件放在正确的位置。gettext要求翻译文件组织成如下目录::

	./locale/en/LC_MESSAGES 
	./locale/cn/LC_MESSAGES 

   locale是在gettext.translation中指定的。en, cn是语言分类。LC_MESSAGES是要求的。同时mo文件应改为与作用域
   名相同,如i18ntest.mo。

   经过以上的步骤,一个支持国际化处理的程序可用了。

   在wxPyWiki教程中使用了mki18n.py的程序。但我使用了python自带的工具,与教程有所区别。

二、遇到的问题
--------------

采用通常的方法存在一些不方便的地方,感觉有以下几点:

1. pygettext.py可以一次读取多个文件,但当文件太多时,在命令行下不容易放下。不支持目录和文件列表的处理。 
2. 当我的程序修改后,需要翻译的地方也发生了变化,如果将新的变化与原来已经翻译好的内容合并? 
3. local/en/LC_MESSAGE这种目录组织有些麻烦。

三、解决方法
------------

对于第一个问题,我的解决方法是修改pygettext.py源程序。我没有实现目录的处理,实现的是文件列表的处理。首先将
需要进行处理的 Python 源文件名生成一个文件,一个文件占一行。然后在pygettext.py中增加处理文件列表的命令行选
项。修改后的文件如果有感兴趣的可以下载最新的 NewEdit.py 源代码(在本文发表时,只可以从cvs下载)。

对于第二个问题,我的解决方法是编写一个合并程序。它首先将已经翻译好的文件读取出来,按关键字保存到一个字典中。
再读取最新生成的翻译文件,对翻译文件中的每一个翻译项,如果字典中不存在,则直接将此项插入到字典中;如果存在,
并且译文不为空的话,则替换原译文。处理完新的翻译文件后,再去除字典中的关键字在新的翻译文件中不存在的项。这一
点可能要注意,因为我每次生成的新的翻译文件都是一个全集,因此我采用这种方法是为了去除无用的翻译项。如果新的翻
译文件不是全集,应该把去除字典关键字的操作去掉。但如果要去除无用的翻译项只能由人工来完成了。此程序也在 NewEdit 源代码中。

对于第三个问题,我查阅了gettext.py源代码。原因就出在find()函数。它最后需要生成一个mo文件名,代码如下::

	mofile = os.path.join(localedir, lang, 'LC_MESSAGES', '%s.mo' % domain)

可以看到一个mo文件是由目录名、语言的种类、LC_MESSAGES、和作用域名组成的。形如::

	localedir/lang/LC_MESSAGES/domain.mo

因此,需要象这样建立相应的目录,并将mo文件放到相应的目录下。

我的想法是:将所有翻译文件放在同一目录(如果项目很大,涉及到许多程序的翻译就另当别论),
翻译文件为:主名_地区编码.mo。为什么不用语言编码呢?因为象中文有简体中文和繁体中文的区别,语言编码都是zh,
这样根本区分不出来。如果用地区编码则可以,分别为cn和tw。

因此为了实现此功能,我根据gettext编写了自已的i18n模式,在实现了gettext功能的基础上还满足了我的要求。同时此
模块还可以根据操作缺省的locale自动调用相应的翻译文件。此文件同在 NewEdit 源代码中。

这样,在 NewEdit 中与i18n有关的内容有:

* pygettext.py(修改) 
* msgfmt.py 
* i18n.py(实现gettext功能) 
* mergepo.py(合并po语言文件)

四、NewEdit中的具体使用
----------------------

在NewEdit的启动程序--NewEdit.py中靠近前面的地方加入::

	1    sys.path.append('./modules')
	2    import i18n
	3    i18n = i18n.I18n('newedit', './lang', unicode=True)
	4    try:
	5        import Lang
	6    except:
	7        pass
	8    else:
	9        i18n.install(Lang.language)

i18n是放在modules目录中的。第三行为生成一个i18n对象,它的调用参数同gettext.install。这样运行到第3行,将会
自动调用缺省翻译文件。缺省翻译文件为:取系统缺省locale,得到地区代码,判断与地区代码相匹配的翻译文件是否存
在,如果存在则装入;如果不存在,则不使用任何翻译文件,这样系统没有国际化的处理。

第5行是导入Lang模块。它其实是我使用了一个小花招。用户可以指定语言的种类,如果指定完毕,则会在modules目录下
生成一个Lang.py文件,其内容为:language="语言"。也就是将用户选定的语言地区代码保存到这个文件中,以便下一次
启动时调用相应的语言文件。因此,NewEdit 在用户修改了语言之后,只有重启新的语言设置才会生效。

导入i18n只需要在启动模块中导入即可,其它模块不用导入。

然后在所有可以翻译的地方加入_()(好累的工作)。

接着生成了一个文件清单。

调用pygettext.py生成语言文件newedit_cn.po。

对newedit_cn.po进行翻译。

调用msgfmt.py将newedit_cn.po编译成mo文件。

将newedit_cn.mo文件拷贝到lang目录下。

启动 NewEdit ,OK,基本上没问题了。

如果对源程序有修改,则再调用pygettext.py生成新的翻译文件messages.pot。调用mergepo.py(合并语言文件)将新旧
进行融合。再生成mo文件,拷贝到lang目录下,如此循环。

但在 NewEdit 中还存在一个问题:如何处理xml的资源文件。

在 NewEdit 中,象查找、查找并替换、包括文本、反包括文本这几项功能的对话框都是使用了xml的资源文件,因此无法
象一般的源程序一样加入_()的处理。我的方法就是生成不同语言的xml资源文件,在需要使用资源文件时,根据程序选定
的语言,调用相应的资源文件。因为资源文件是xml文件,因此文本的编码应与xml的encoding声明相一致。对于中文的翻
译文件,建议使用utf-8编码。因为utf-8是xml缺省使用的编码。


附:gettext的实现原理
---------------------

只是本人的理解!

gettext的功能其实挺简单:定义_()函数并安装,根据传入的翻译项从mo文件中取得译文,并根据uniocde标识进行应用
的unicode编码处理。

一个gettext对象提供了两个基本的获得译文的函数:gettext和ugettext。一个用来获得未进行编码转换的译文,一个用
来获得转成unicode的译文。根据unicode的标志,这两个函数之一将被引用到_()函数上。如果未安装任何译文,则缺省返
回是只是翻译项,即原样返回。因此不安装任何译文,gettext一样是可以用的。

如何安装_()。

::

	import __buildin__
	__buildin__.__dict__['_']=unicode and self.ugettext or self.gettext

这样就将_()函数变成一个内置函数了,在其它的后续的模块中就可以直接使用它了。因此,使用gettext时,安装要很靠
前,并且只需要在启动模块中装入即可。

后记:为什么 NewEdit 不动态更新界面

既然实现了国际化处理,如果用户改变了使用的语言,如果整个应用立即改变不是更好吗?当然是很吸引人,但对于 NewEdit 
存在问题。一方面所有界面与国际化处理相关的地方,都要用代码去实现更新过程,工作量大。另外,除非测试,一个用户
不会总是在各种语言之间切换来切换去,一般都是固定使用某种语言,因此,在重新启动后启用新的语言设置是可以接受的
方案。还有一个重要的原因就是:使用_()这种方式只适合函数调用方式,对于全局变量或模块变量无能为力。为什么呢?比如::

	message = _('Chinese')

这样定义了一个变量。如果它是存在一个模块中,作为模块的全局变量的话,当导入这个模块时,这个语句会执行。并且导
入过程只会执行一次。这样当语句执行完毕后message就是具体的值,而不再是message=_('Chinese')这种语句了。这样,
当我们改变了语言,message只会是上一个语言的译文,不会变成新的译文。这一点真是很难解决。而为什么在函数调用时
可以呢?严格来说应该是这样的形式可以::

	call(_('Chinese'))

这是因为,当调用函数时参数是动态计算的,因此每次调用call时都会重新计算参数的值,因此这种方式可以的。总的来
说,如果在执行时可以每次调用_()函数的话,这样就可以实现动态语言切换。如果不行,则使用静态语言切换。因此 NewEdit 
使用了静态语言的切换。

i18n的一个小问题
----------------

不仅是我发现,别的使用 NewEdit 的人也发现,有时在 Shell 窗口中执行过一些语句后,再回到文档窗口中时,随着鼠
标的移动文本自动会选中,甚至按键都不起作用。这个问题很有趣,为什么执行过Shell命令就不行了呢?后来我发现,这
是因为i18n中使用的_()函数与Shell中的_变量冲突所致。在Shell中,执行完一条命令后,命令的结果就保存在_变量中。
但i18n把这个'_'使用为一个内置的函数,因此造成两者的冲突。于是我不得不将 NewEdit 中的所有_()函数全部改成了
tr()(自定义的),这下子才解决这个问题。从而可以说明,PyShell(使用它生成的Shell窗口)的变量空间与NewEdit是有
重叠的。可能也没有办法,因为i18n中的_()函数是通过将其置入内置__builtin__中的,而__builtin__是全局共享的,
因此对它有改变势必会影响到别的模块。

还好这个问题是可以解决的。但PyShell好象不支持中文。本来想多实现一些功能,但却带来一些负作用,小心为妙呀。

`[返回]`_

.. _`[返回]`: technical.htm

你可能感兴趣的:(i18n)