Using gettext in compiled Qt ui files with PyQt

by mandel on March 14th, 2011

In some cases you might find yourself in the situation of wanting to use gettext in a PyQt project in which you have .ui files generated using QtDesigner.

For those kind of situations is a good idea to extend the uic compiler form PyQt. he following example shows how to do so in a distutils command.

class QtBuild(build_extra.build_extra):
    """Build PyQt (.ui) files and resources."""
 
    description = "build PyQt GUIs (.ui)."
 
    def compile_ui(self, ui_file, py_file=None):
        """Compile the .ui files to python modules."""
        # Search for pyuic4 in python bin dir, then in the $Path.
        if py_file is None:
            # go from the ui_file in the data folder to the
            # python file in the qt moodule
            py_file = os.path.split(ui_file)[1]
            py_file = os.path.splitext(py_file)[0] + '_ui.py'
            py_file = os.path.join('package', 'qt', py_file)
        # we indeed want to catch Exception, is ugle but w need it
        # pylint: disable=W0703
        try:
            # import the uic compiler from pyqt and generate the 
            # .py files something similar could be done with pyside
            # but that is left as an exercise for the reader.
            from PyQt4 import uic
            fp = open(py_file, 'w')
            uic.compileUi(ui_file, fp)
            fp.close()
            log.info('Compiled %s into %s', ui_file, py_file)
        except Exception, e:
            self.warn('Unable to compile user interface %s: %s',
                           py_file, e)
            if not os.path.exists(py_file) or
                                            not file(py_file).read():
                raise SystemExit(1)
            return
        # pylint: enable=W0703
 
    def run(self):
        """Execute the command."""
        self._wrapuic()
        basepath = os.path.join('data',  'qt')
        for dirpath, _, filenames in os.walk(basepath):
            for filename in filenames:
                if filename.endswith('.ui'):
                    self.compile_ui(os.path.join(dirpath, filename))
 
    # pylint: disable=E1002
    _wrappeduic = False
    @classmethod
    def _wrapuic(cls):
        """Wrap uic to use gettext's _() in place of tr()"""
        if cls._wrappeduic:
            return
 
        from PyQt4.uic.Compiler import compiler, qtproxies, indenter
 
        # pylint: disable=C0103
        class _UICompiler(compiler.UICompiler):
            """Speciallized compiler for qt .ui files."""
            def createToplevelWidget(self, classname, widgetname):
                o = indenter.getIndenter()
                o.level = 0
                o.write('from module.with.gettext.setup import _')
                return super(_UICompiler, self).createToplevelWidget(
                                   classname, widgetname)
        compiler.UICompiler = _UICompiler
 
        class _i18n_string(qtproxies.i18n_string):
            """Provide a translated text."""
 
            def __str__(self):
                return "_('%s')" % self.string.encode(
                                                'string-escape')
 
        qtproxies.i18n_string = _i18n_string
 
        cls._wrappeduic = True
        # pylint: enable=C0103
    # pylint: enable=E1002

The above should be doable with PySide, but that is left as an exercise for the reader.

  • http://mandel.themacaque.com mandel

    Recently I received an email asking me for a more detailed explanation of the post, what better to add it as a comment so that others can find it. From my email:

    The trick here is based on the fact that we are extending the uic compiler to modify the way it generates the python code. The idea is very simple. Lets imaging that you could tell the uic compiler to use gettext rather than the normal Qt translation methods. That way instead of using the Qt method you would call those from gettext. Lets take a look at what I mean:

    Qt: tr(‘my translated string’)
    gettext: _(‘my translated string’)

    As you can see the actual differences between the code are minimum, so this can clearly be done automatically. In PyQt4 and PySide (although I ahve not tested it) you can extend the python compiler class to modify its behaviour the way you want, and that is precisely what the blog pots talks about. Lets take a look step by step at this process:

    The first thing we would like to do is to create an extension of the compiler class, that we can do as follows:

    from PyQt4.uic.Compiler import compiler, qtproxies, indenter

    # pylint: disable=C0103
    class _UICompiler(compiler.UICompiler):
    “”"Speciallized compiler for qt .ui files.”"”

    within this child class we are going to override the method that creates the top level widget, this allows us to add imports to the python code so that we can import the gettext method.

    def createToplevelWidget(self, classname, widgetname):
    o = indenter.getIndenter()
    o.level = 0
    o.write(‘from module.with.gettext.setup import _’)
    return super(_UICompiler, self).createToplevelWidget(
    classname, widgetname)

    where ‘from module.with.gettext.setup import _’ represents the module in your python application in which you have done the setup of gettext. Of course you do not have to do that in a diff module, but I like to keep everything in the same place. This compiler class will already provide a compiled .uic code with the extra import, but ofcourse that is not everything. The next step is to tell the compiler to use gettext for the translations. To do that we extend a new class that is sued by the compiler class which is the

    qtproxies.i18n_string

    which as the name states takes care of the translations:

    class _i18n_string(qtproxies.i18n_string):
    “”"Provide a translated text.”"”

    def __str__(self):
    return “_(‘%s’)” % self.string.encode(
    ‘string-escape’)

    There we are using ‘_’ which is the name of the function used by gettext for translations, and what it does is to ensure that all translatable string in you app are translated in such a way.

    To ensure that the uic modules uses or extended classes we just have to import:

    from PyQt4.uic.Compiler import compiler, qtproxies, indenter

    And set the correct classes, one for the compiler:

    compiler.UICompiler = _UICompiler

    and for the translating class:

    qtproxies.i18n_string = _i18n_string

    All this in my example is done in a class method which extends all the required classes and sets them up. Once you have all this set you can call the uic compilation from your setup.py, for example:

    from PyQt4 import uic
    fp = open(py_file, ‘w’)
    uic.compileUi(ui_file, fp)
    fp.close()

    I hope the explanamtion makes more sense. I suppose that you are not the only one that have not understood the example fully so I’ll put this answer in the post comments.

    I hope it helps and let me know if you need any other help.

  • arequate

    Thanks for explaining the general idea! The following code is a simple wrapper for pyuic4 that uses your technique.

    ——————————————————————————————-
    ### import and wrap compiler.UICompiler and
    from PyQt4.uic.Compiler import compiler, qtproxies, indenter

    # pylint: disable=C0103
    class _UICompiler(compiler.UICompiler):
    “”"Speciallized compiler for qt .ui files.”"”

    def createToplevelWidget(self, classname, widgetname):
    o = indenter.getIndenter()
    o.level = 0
    o.write(“import gettextn_ = gettext.translation(‘%s’, fallback=True).ugettextn” % PO_FILE_NAME)
    return super(_UICompiler, self).createToplevelWidget(classname, widgetname)

    compiler.UICompiler = _UICompiler

    ### wrap qtproxies.i18n_string
    class _i18n_string(qtproxies.i18n_string):
    “”"Provide a translated text.”"”

    def __str__(self):
    return “_(‘%s’)” % self.string.encode(‘string-escape’)

    qtproxies.i18n_string = _i18n_string

    ### run /usr/bin/pyuic4
    # there’s no main function, so just import the module
    import PyQt4.uic.pyuic

    ——————————————————————————————-