【问题标题】:Gettext message catalogues from virtual dir within PYZ for GtkBuilder widgets为 GtkBuilder 小部件从 PYZ 中的虚拟目录获取文本消息目录
【发布时间】:2015-04-16 21:24:25
【问题描述】:

是否有将 gettext locale/xy/LC_MESSAGES/* 嵌入PYZ bundle 的既定方法?特别是让 Gtks 自动小部件翻译从 ZIP 存档中提取它们。

对于其他嵌入式资源pkgutil.get_deta 或inspect/get_source 工作得很好。但是系统和 Python gettext APIs 依赖于bindtextdomain 提供一个普通的旧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 提取
    现在可以选择手动读出消息目录或仅使用琐碎的 dicts。但仅适用于应用程序中的字符串。这又是没有办法让 Gtk/GtkBuilder 隐式拾取它们。
    因此我不得不手动遍历整个小部件树、标签、文本、内部小部件、markup_text 等。可能,但是 meh。

  4. 保险丝安装
    这将是超级片状的。但当然,可以访问 zip 内容gvfs-mount 等。这似乎是某种内存占用。而且我怀疑它是否会保持可靠,例如两个应用程序实例正在运行,或者之前的一个不干净的终止。 (我不知道,由于系统库,如 gettext,绊倒了一个脆弱的 zip 熔断点..)

  5. 用于翻译的 Gtk 信号/事件(?)
    我找到了squat about this,所以我有点确定在Gtk/PyGtk/GI 中没有用于小部件翻译的替代机制。 Gtk/Builder 期望并且是tied togettext。

也许有更可靠的方法?

【问题讨论】:

  • 我不得不承认我不明白你所说的“Glib 与 gettext 紧密相连”是什么意思。这些只是几个方便的宏,GLib 中绝对没有任何东西强迫你使用它们或 gettext。
  • 当然可以。这也是 glib 文档所断言的(“不强制任何特定的本地化方法......”)。在 Gtk 的上下文中,它只是不太可行。 (即使重新编译是一种选择,宏方案也不允许例如 ICU 轻松替换)。因此,如果您使用的不是 gettext,那么您就会陷入迭代翻译所有 Gtk 小部件的困境。
  • 如果您的问题是 GtkBuilder xml 文件本地化与 gettext 相关联(这对我来说似乎是一个有效的问题),您应该清楚地说出来。当您暗示 GLib(或 Gtk 应用程序代码)翻译以某种方式与 gettext 相关联时,这会使问题更难理解:我猜我不是唯一一个挠头想“这些东西与如何翻译无关的人”实际上是:GTK 小部件不关心翻译,他们只是希望得到翻译后的字符串”...
  • 修复了该注释。我最初寻找 glib 寻找解决方法(gio 路径和所有),所以怀疑那里有解决方法。但是 GtkBuilder UI 文件是一个更好的提示。找到预翻译其文本节点的替代方法可能比寻找小部件挂钩更简单。
  • 是否有在 PYZ 包中嵌入 gettext locale/xy/LC_MESSAGES/* 的既定方法?,“既定方法”是什么意思?你可以或不能嵌入。如果我理解正确,您希望将 gettext 语言环境目录包含到应用程序 pyz 包中。对吗?

标签: python pygtk gettext glib pyz


【解决方案1】:

这是我的示例 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有两个核心功能find和translation。我在名为mygettext 的单独模块中重新定义了这两个,以使它们使用来自ZipFS 的文件。

gettext 使用 os.path 、os.path.exists 和 open 来查找文件并打开它们,我将它们替换为 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')

【讨论】:

  • 哇,很多代码。还在通读……看起来很实用。而且似乎对其他用户也很有用。我要测试一下。 --(已经看到回复通知了。所以请放宽评论ping!)
  • 嘿马里奥,你太酷了!非常感谢。编码愉快!
猜你喜欢
  • 2021-08-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-11
  • 1970-01-01
  • 2011-01-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多