Gettext message catalogues from virtual dir within PYZ for GtkBuilder widgets Gettext message catalogues from virtual dir within PYZ for GtkBuilder widgets python python

Gettext message catalogues from virtual dir within PYZ for GtkBuilder widgets


This my example Glade/GtkBuilder/Gtk application. I've defined a function xml_gettext which transparently translates glade xml files and passes to gtk.Builder instance as a string.

import mygettext as gettextimport osimport sysimport gtkfrom gtk import gladeglade_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()  

I've archived my locale directories into locale.zip which is included in the pyz bundle.
This is contents of 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')

To make the locale.zip as a filesystem I use ZipFS from fs.

Fortunately Python gettext is not GNU gettext. gettext is pure Python it doesn't use GNU gettext but mimics it. gettext has two core functions find and translation. I've redefined these two in a seperate module named mygettext to make them use files from the ZipFS.

gettext uses os.path ,os.path.exists and open to find files and open them which I replace with the equivalent ones form fs module.

This is contents of my application.

pyzzer.pyz -i glade_v1.pyz  # A zipped Python application# Built with pyzzerArchive contents:  glade_dist/glade_example.py  glade_dist/locale.zip  glade_dist/__init__.py  glade_dist/mygettext.py  __main__.py

Because pyz files have text, usually a shebang, prepended to it, I skip this line after opening the pyz file in binary mode. Other modules in the application that want to use the gettext.gettext function, should import zfs_gettext instead from mygettext and make it an alias to _.

Here goes mygettext.py.

from errno import ENOENTfrom gettext import _expand_lang, _translations, _default_localedirfrom gettext import GNUTranslations, NullTranslationsimport gettextimport copyimport osimport sysfrom xml.etree import ElementTree as ETimport zipfileimport fsfrom fs.zipfs import ZipFSzfs = Noneif 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:        passelse:    try:        zfs = ZipFS('locale.zip','r')    except:        passif zfs:    os.path = fs.path    os.path.exists = zfs.exists    open = zfs.opendef 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 resultdef 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 resultdef 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 = findgettext.translation = translation_ = zfs_gettext = gettext.gettextgettext.bindtextdomain('myapp','locale')gettext.textdomain('myapp')

The following two shouldn't be called because glade doesn't use Python gettext.

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