从PYZ虚拟目录中获取Gettext消息目录以用于GtkBuilder小部件

26

是否有一种已经确定的方法可以将gettext locale/xy/LC_MESSAGES/*嵌入到PYZ bundle中?具体来说,是让Gtks自动小部件翻译从ZIP存档文件中获取它们。

对于其他嵌入式资源,pkgutil.get_detainspect/get_source就足够好了。但是系统和Python gettext APIs依赖于提供一个普通的localedir;没有资源或字符串等。

因此,我无法设计出可行或实用的解决方法:

  1. 虚拟的gvfs/gio路径
    现在使用archive://file%3A%2F%2Fmypkg.pyz%2Fmessages%2F IRI可以作为从zip文件中直接读取其他文件的替代方法。但是glibs g_dgettext仍然只是系统库的一个薄包装器。因此,任何这样的URL都不能用作localedir

  2. 部分解压缩zip
    我想这就是PyInstaller的工作方式。但是,将某些东西打包成.pyz应用程序,每次调用时都要进行预提取,当然有点荒谬。

  3. 用户界面gettext .mo/.po提取
    现在手动阅读消息目录或只使用平凡的字典可能是一个选择。但仅适用于应用程序内字符串。这又不是让Gtk/GtkBuilder隐式地捕捉它们的方法。
    因此,我必须手动遍历整个小部件树,标签、文本、内部小部件、标记文本等。这是可能的,但是不太好。

  4. FUSE装载
    这将是非常不稳定的。但当然,可以通过gvfs-mount等访问zip内容。只是似乎会消耗一定的内存。我怀疑在两个应用程序实例运行或以前未清理终止时,它是否会保持可靠。(我不知道,由于系统库(如gettext)在脆弱的zip fuses点上跌倒..)

  5. Gtk信号/事件进行翻译(?)
    我找不到关于这个的任何信息,所以我相当确定在Gtk/PyGtk/GI中没有小部件翻译的替代机制。Gtk/Builder期望并与gettext绑定

也许有更可靠的方法吗?


1
修正了那个注释。我最初在 glib 中寻找解决方法(包括 gio 路径等),因此怀疑可能有一个解决方法。但是 GtkBuilder UI 文件是一个更好的提示。可能更简单的方法是寻找替代方案来预翻译其文本节点,而不是寻找小部件钩子。 - mario
有没有一种已经确定的方法可以将gettext locale/xy/LC_MESSAGES/*嵌入到PYZ bundle中?“已确定的方法”是什么意思?您可以选择嵌入或不嵌入。如果我理解正确,您想将gettext语言环境目录包含在应用程序pyz bundle中。对吗? - Nizam Mohamed
嘿,我只是开玩笑。顺便问一下,将文本翻译为 gtk.Button(_("Welcome to Python!") 中的 "Welcome to Python!" 对你来说足够了吗?Gtk+ 是可能的,但 Glade 不行,因为后者是 C 扩展。 - Nizam Mohamed
嗨,我有一个解决方案,如果你感兴趣,请在评论中回答我的问题。 - Nizam Mohamed
这意味着那些你传递给GtkBuilder的Glade xml文件吗?如果是,请发一些示例代码,我认为这是可能的。 - Nizam Mohamed
显示剩余7条评论
1个回答

4
这是我的 Glade/GtkBuilder/Gtk 应用示例。我定义了一个函数 xml_gettext,它透明地翻译 Glade XML 文件并将其作为字符串传递给 gtk.Builder 实例。
import mygettext as gettext
import os
import sys

import gtk
from gtk import glade

glade_xml = '''<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <!-- interface-requires gtk+ 3.0 -->
  <object class="GtkWindow" id="window1">
    <property name="can_focus">False</property>
    <signal name="delete-event" handler="onDeleteWindow" swapped="no"/>
    <child>
      <object class="GtkButton" id="button1">
        <property name="label" translatable="yes">Welcome to Python!</property>
        <property name="use_action_appearance">False</property>
        <property name="visible">True</property>
        <property name="can_focus">True</property>
        <property name="receives_default">True</property>
        <property name="use_action_appearance">False</property>
        <signal name="pressed" handler="onButtonPressed" swapped="no"/>
      </object>
    </child>
  </object>
</interface>'''

class Handler:
    def onDeleteWindow(self, *args):
        gtk.main_quit(*args)

    def onButtonPressed(self, button):
       print('locale: {}\nLANGUAGE: {}'.format(
              gettext.find('myapp','locale'),os.environ['LANGUAGE']))

def main():
    builder = gtk.Builder()
    translated_xml = gettext.xml_gettext(glade_xml)
    builder.add_from_string(translated_xml)
    builder.connect_signals(Handler())

    window = builder.get_object("window1")
    window.show_all()

    gtk.main()

if __name__ == '__main__':
    main()  

我已将我的本地目录存档为locale.zip,并将其包含在pyz捆绑包中。
这是locale.zip的内容。

(u'/locale/fr_FR/LC_MESSAGES/myapp.mo',
 u'/locale/en_US/LC_MESSAGES/myapp.mo',
 u'/locale/en_IN/LC_MESSAGES/myapp.mo')

为了将locale.zip作为文件系统,我使用fs中的ZipFS。

幸运的是,Python的gettext不是GNU gettext。 gettext是纯Python编写的,它不使用GNU gettext,但模仿它。 gettext有两个核心函数findtranslation。我在一个名为mygettext的单独模块中重新定义了这两个函数,以便它们使用ZipFS中的文件。

gettext使用os.pathos.path.existsopen查找文件并打开它们,我用fs模块中的等效函数替换它们。

这是我的应用程序内容。

pyzzer.pyz -i glade_v1.pyz  
# A zipped Python application
# Built with pyzzer

Archive contents:
  glade_dist/glade_example.py
  glade_dist/locale.zip
  glade_dist/__init__.py
  glade_dist/mygettext.py
  __main__.py

因为pyz文件有文本,通常是一个shebang,在打开pyz文件的二进制模式后,我会跳过这一行。应用程序中其他想要使用gettext.gettext函数的模块,应该从mygettext中导入zfs_gettext并将其设置为_的别名。
下面是mygettext.py的内容。
from errno import ENOENT
from gettext import _expand_lang, _translations, _default_localedir
from gettext import GNUTranslations, NullTranslations
import gettext
import copy
import os
import sys
from xml.etree import ElementTree as ET
import zipfile

import fs
from fs.zipfs import ZipFS


zfs = None
if zipfile.is_zipfile(sys.argv[0]):
    try:
        myself = open(sys.argv[0],'rb')
        next(myself)
        zfs = ZipFS(ZipFS(myself,'r').open('glade_dist/locale.zip','rb'))
    except:
        pass
else:
    try:
        zfs = ZipFS('locale.zip','r')
    except:
        pass
if zfs:
    os.path = fs.path
    os.path.exists = zfs.exists
    open = zfs.open

def find(domain, localedir=None, languages=None, all=0):

    # Get some reasonable defaults for arguments that were not supplied
    if localedir is None:
        localedir = _default_localedir
    if languages is None:
        languages = []
        for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
            val = os.environ.get(envar)
            if val:
                languages = val.split(':')
                break
                                                                                     if 'C' not in languages:
            languages.append('C')
    # now normalize and expand the languages
    nelangs = []
    for lang in languages:
        for nelang in _expand_lang(lang):
            if nelang not in nelangs:
                nelangs.append(nelang)
    # select a language
    if all:
        result = []
    else:
        result = None
    for lang in nelangs:
        if lang == 'C':
            break
        mofile = os.path.join(localedir, lang, 'LC_MESSAGES', '%s.mo' % domain)
        mofile_lp = os.path.join("/usr/share/locale-langpack", lang,
                               'LC_MESSAGES', '%s.mo' % domain)

        # first look into the standard locale dir, then into the 
        # langpack locale dir

        # standard mo file
        if os.path.exists(mofile):
            if all:
                result.append(mofile)
            else:
                return mofile

        # langpack mofile -> use it
        if os.path.exists(mofile_lp): 
            if all:
                result.append(mofile_lp)
            else:
               return mofile

        # langpack mofile -> use it
        if os.path.exists(mofile_lp): 
            if all:
                result.append(mofile_lp)
            else:
                return mofile_lp

    return result

def translation(domain, localedir=None, languages=None,
                class_=None, fallback=False, codeset=None):
    if class_ is None:
        class_ = GNUTranslations
    mofiles = find(domain, localedir, languages, all=1)
    if not mofiles:
        if fallback:
            return NullTranslations()
        raise IOError(ENOENT, 'No translation file found for domain', domain)
    # Avoid opening, reading, and parsing the .mo file after it's been done
    # once.
    result = None
    for mofile in mofiles:
        key = (class_, os.path.abspath(mofile))
        t = _translations.get(key)
        if t is None:
            with open(mofile, 'rb') as fp:
                t = _translations.setdefault(key, class_(fp))
        # Copy the translation object to allow setting fallbacks and
        # output charset. All other instance data is shared with the
        # cached object.
        t = copy.copy(t)
        if codeset:
            t.set_output_charset(codeset)
        if result is None:
            result = t
        else:
            result.add_fallback(t)
    return result

def xml_gettext(xml_str):
    root = ET.fromstring(xml_str)
    labels = root.findall('.//*[@name="label"][@translatable="yes"]')
    for label in labels:
        label.text = _(label.text)
    return ET.tostring(root)

gettext.find = find
gettext.translation = translation
_ = zfs_gettext = gettext.gettext

gettext.bindtextdomain('myapp','locale')
gettext.textdomain('myapp')

以下两个不应该被调用,因为glade没有使用Python的gettext
glade.bindtextdomain('myapp','locale')
glade.textdomain('myapp')

1
哇,好多代码。还在仔细阅读中...看起来非常实用。而且似乎对其他用户也很有用。我要进行测试。--(已经看到答案通知了。所以请减少评论提醒!) - mario
嘿,马里奥,你非常酷!非常感谢你。愉快编码! - Nizam Mohamed

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接