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')