Подтвердить что ты не робот

Каталоги каталогов Gettext из виртуального каталога в PYZ для виджетов GtkBuilder

Существует ли установленный подход для вставки gettext locale/xy/LC_MESSAGES/* в пакет PYZ? В частности, для перевода Gotks-автоматического виджета из архива ZIP.

Для других встроенных ресурсов pkgutil.get_deta или inspect/get_source работает достаточно хорошо. Но системные и Python API-интерфейсы gettext зависят от bindtextdomain, поставляемого простой старой localedir; нет ресурсов или строк и т.д.

Таким образом, я не мог придумать практичный или даже удаленный практический способ:

  • Виртуальные пути gvfs/gio
    Теперь использование archive://file%3A%2F%2Fmypkg.pyz%2Fmessages%2F IRI будет альтернативой для чтения других файлов непосредственно из zip. Но glibs g_dgettext - это всего лишь тонкая оболочка вокруг системы lib. И поэтому любые такие URL-адреса не могут использоваться как localedir.

  • Частичное извлечение zip
    Я думаю, что PyInstaller работает. Но это, конечно, несколько смешно, чтобы связать что-то как .pyz-приложение, только чтобы оно было перенаправлено на каждый вызов.

  • Userland gettext .mo/.po извлечение
    Теперь чтение каталогов сообщений вручную или просто использование тривиальных dicts вместо этого будет вариантом. Но только для строк в приложении. Это опять же не означает, что Gtk/GtkBuilder забирает их неявно.
    Таким образом, мне пришлось вручную перемещать все дерево виджетов, ярлыки, текст, внутренние виджеты, markup_text и т.д. Возможно, но meh.

  • Установка FUSE
    Это было бы неплохо. Но, конечно, можно было получить доступ к zip-содержимому gvfs-mount и т.д. Похоже, что это определенная память. И я сомневаюсь, что он остался бы надежным, например. два экземпляра приложения, или предыдущее, нечисто закончившееся. (Как и dunno, из-за системной библиотеки, такой как gettext, спотыкается о хрупкую точку плавкого перехода zip.)

  • Сигнал/событие Gtk для перевода (?)
    Я нашел squat об этом, поэтому я определенно не вижу альтернативного механизма для перевода виджета в Gtk/PyGtk/GI. Gtk/Builder ожидает и привязан к gettext.

Возможно ли более надежный подход?

4b9b3361

Ответ 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 как файловую систему, я использую ZipFS из fs.

К счастью, 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, должны импортировать zfs_gettext вместо mygettext и сделать его псевдоним _.

Здесь 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 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')