2015-04-16 9 views
26

Gibt es einen etablierten Ansatz zum Einbetten von Gettext locale/xy/LC_MESSAGES/* in eine PYZ bundle? Speziell, um Gtks automatische Widget-Übersetzung zu haben, holen Sie sich das innerhalb des ZIP-Archivs. Für andere eingebettete Ressourcen pkgutil.get_deta oder inspect/get_source funktionieren gut genug Aber System und Python gettext APIs sind abhängig von bindtextdomain geliefert werden eine einfache alte localedir;Gettext-Nachrichtenkataloge aus dem virtuellen Verzeichnis innerhalb von PYZ für GtkBuilder-Widgets

  1. Virtuelle gvfs/gio Pfade
    Jetzt mit archive://file%3A%2F%2Fmypkg.pyz%2Fmessages%2F IRIs eine Alternative wäre, zu lesen: keine Ressourcen oder Strings usw.

    So konnte ich nicht eine praktikable oder auch nur entfernt praktische Abhilfe ersinnen andere Dateien direkt von einem Zip. Aber glibs g_dgettext ist immer noch nur ein dünner Wrapper um das System lib. Und daher können solche URLs nicht als localedir verwendet werden.

  2. teilweise Extraktion des zip
    Das ist, wie PyInstaller funktioniert, denke ich. Aber es ist natürlich etwas lächerlich, etwas als .pyz Anwendung zu bündeln, nur um es bei jedem Aufruf vorextrahiert zu haben.

  3. Userland getText .mo/.po Extraktion
    nun die Nachrichtenkataloge manuell zu lesen oder einfach nur mit trivial dicts würde stattdessen eine Option sein. Aber nur für In-Application-Strings. Das ist wiederum keine Möglichkeit, Gtk/GtkBuilder implizit abholen zu lassen.
    Also musste ich den gesamten Widget-Baum manuell durchlaufen, Labels, Text, innere Widgets, Markup-Text, etc. Möglich, aber meh.

  4. FUSE Montage
    Dies würde superflaky sein. Aber natürlich könnte der Inhalt der zip zugegriffen werden gvfs-mount usw. Es scheint nur wie ein bestimmter Speicher Schwein. Und ich bezweifle, dass es mit z. zwei App-Instanzen werden ausgeführt oder eine vorherige unsauber beendet. (Wie weiß nich, wegen einer Systembibliothek, wie gettext, Stolpern über einen fragilen Zip Sicherung Punkt ..)

  5. Gtk Signal/Ereignis für die Übersetzung (?)
    Ich habe Hocke about this gefunden, so dass ich Ich bin mir ziemlich sicher, dass es keinen alternativen Mechanismus für Widget-Übersetzungen in Gtk/PyGtk/GI gibt. Gtk/Builder erwartet und ist tied to gettext.

Gibt es einen zuverlässigen Ansatz vielleicht?

+0

Ich muss zugeben, ich verstehe nicht, was du meinst von "Glib ist eng an gettext angekettet". Das sind nur ein paar Makros, in GLib zwingt Sie gar nichts, sie zu benutzen oder gettext zu benutzen. – jku

+0

Gut natürlich. Das ist auch, was die glib-Dokumentation behauptet ("erzwingt keine bestimmte Lokalisierungsmethode .."). Im Kontext von Gtk ist es einfach nicht so machbar. (Selbst wenn eine Neukompilierung eine Option wäre, würden die Makroschemata beispielsweise eine ICU-Substitution nicht leicht erlauben). Wenn Sie also alles außer gettext verwenden, müssen Sie alle Gtk-Widgets iterativ übersetzen. – mario

+0

Wenn Ihr Problem darin besteht, dass GtkBuilder XML-Datei-Lokalisierung an gettext gebunden ist (was für mich ein gültiges Problem scheint), sollten Sie dies klar sagen. Wenn Sie implizieren, dass GLib (oder Gtk Application Code) Übersetzung irgendwie an gettext gebunden ist, macht es die Frage schwerer zu verstehen: Ich schätze, ich war nicht die Einzige, die sich am Kopf kratzte und dachte: "Diese Dinge haben nichts mit der Übersetzung zu tun ist tatsächlich gemacht: GTK Widgets kümmern sich nicht um die Übersetzung, die sie erwarten, um die übersetzte Zeichenkette "... – jku

Antwort

4

Dies ist mein Beispiel Glade/GtkBuilder/Gtk-Anwendung. Ich habe eine Funktion xml_gettext definiert, die glade xml-Dateien transparent übersetzt und an gtk.Builder Instanz als Zeichenfolge übergibt.

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

Ich habe meine locale Verzeichnisse in locale.zip archiviert, die im pyz Bündel enthalten ist.
Dies ist Inhalt 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') 

Um die locale.zip als Dateisystem mache ich ZipFS von fs verwenden.

Zum Glück ist Python nicht GNU gettext. ist reines Python, es verwendet GNU gettext nicht, sondern imitiert es. hat zwei Kernfunktionen find und translation. Ich habe diese zwei in einem separaten Modul mit dem Namen mygettext neu definiert, um sie Dateien aus der ZipFS verwenden zu lassen.

gettext verwendet os.path, os.path.exists und open Dateien zu finden und öffnen Sie sie, die ich ersetzen die entsprechenden diejenigen fs Modul bilden.

Dies ist Inhalt meiner Anwendung.

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 

Da pyz Dateien Text, in der Regel eine shebang, vorangestellt, springe ich diese Zeile nach der pyz Datei im Binär-Modus zu öffnen. Andere Module in der Anwendung, die die gettext.gettext-Funktion verwenden möchten, sollten stattdessen zfs_gettext von mygettext importieren und es zu einem Alias ​​zu _ machen.

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

Die folgenden beiden nicht genannt werden sollte, weil glade Python nicht gettext verwenden.

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

Wow, viel Code. Liest es immer noch durch ... Sieht ziemlich praktisch aus. Und außerdem scheint es auch für andere Benutzer nützlich zu sein. Ich werde es testen. - (Schon die Antwortbenachrichtigung gesehen. Also bitte auf den Kommentar pingen!) – mario

+0

Hey mario, du bist super cool! Vielen Dank. Glückliche Kodierung! –