Saturday, August 10, 2013

"Branching" Translations

There will come a time when the website being created needs to be translated to different languages. What if we needed different translations while we are on the same language setting? What happens next?

This is where playing with APIs beyond their defaults comes into play. What I did is basically use a different compiled binary translation file for the GNUTranslations class to use. I am probably one of those devious devs in my circle whose never handled (or survived in) a Django project before, so bear with me. The software stack used in this project is:
  • Twisted
  • Rammi
  • Babel
  • Jinja2
  • txTemplate

Basic Idea

So the core idea here is that we use a different *.mo file depending on the kind of translation that we need for a particular resource / template / mode. Turns out to be simple idea, eh?

Disclaimer

Code snippets posted here are not suitable for direct copy paste as a whole bunch of them are not actually tested (well, they're created on the fly) and only serve the purpose of giving the reader -- that is you, the idea on its implementation.

Current Working Directory

I will be issuing CLI commands in my project root directory which basically contains:
  • (d) locale    # contains translation-related files
  • (d) resources # contains the python code
  • (d) templates # contains the jinja2 templates
  • (f) babel.cfg
Needless to say, I used Babel to extract the messages marked for translation all throughout the project. Now, let's go over how I leveraged Babel to complete this task (it is also, by the way, our star tool in this article).

Babel

Installing Babel in your virtualenv gives you the ability to invoke the `pybabel` command which demands usage of its subcommands, namely:
  • extract
  • update
  • compile    
Note that they need to be invoked in that order

Invoking pybabel extract will cause pybabel to dig through the files of the project in search for messages marked for translation and then generate a *.pot (*.po template) file. This command requires a mapping file which is basically a configuration file which allows us to specify which file it should search or which directory. A sample for this is:
    
    [extractors]
    jinja2 = jinja2.ext.babel_extract
    
    
    [python: **.py]
    encoding = utf-8
    
    [jinja2: **/templates/**.html]
    encoding = utf-8

This causes the pybabel extract command to recursively search all folders for *.py files and then recursively search the templates for any *.html file. Next, I do the pybabel extract command twice:
    
    # format
    pybabel extract --sort-by-file --no-wrap --mapping=<mapping-file> --output=<path-to-*.pot-file>
    
    # example
    pybabel extract --sort-by-file --no-wrap --mapping=babel.cfg --output=locale/messages.pot
    pybabel extract --sort-by-file --no-wrap --mapping=babel.cfg --output=locale/messages-alternate.pot

That now yields two *.pot files which we can use with the pybabel update command, which generates/updates the *.po file. Again, we do this for each *.pot file that we have.

    # format
    pybabel update --input-file=<path-to-*.pot-file> --output-dir=<base-locale-directory> --domain=<name-of-*.po-file>
    
    # example
    pybabel update --input-file=locale/messages.pot --output-dir=locale --domain=messages
    pybabel update --input-file=locale/messages-alternate.pot --output-dir=locale --domain=messages-alternate

The --domain option specifies the name of both the *.po file and the *.mo file (to be discussed later) which will be used by the GNUTranslations instance in our code later. Alright! So, do some translations in the *.po file and when you're done, time to do the pybabel compile command to generate our binary *.mo files.

    # format
    pybabel compile --directory=<base-locale-directory> --domain=<name-of-*.mo-file> -f
    
    # example
    pybabel compile --directory=locale --domain=messages -f
    pybabel compile --directory=locale --domain=messages-alternate -f

The --domain option here also pretty much doubles as a way to select which *.po file is going to be compiled and what the file name of the *.mo file will be.

Python

Now that you have the *.mo files, what happens next? What to do in the code? Basically, simply use the *.mo file by instantiating another GNUTranslations class. I am using Babel, but there should not be that much difference as the Babel API also extends from the GNUTranslations class

    # resources/translations.py
    # Load default translations
    import locale
    
    from babel.support import Translations
    
    os.environ['LANGUAGE'] = 'en_US.UTF-8'
    locale.setlocale(locale.LC_MESSAGES, 'en_US.UTF-8')
    
    # load 'messages.mo'
    trans_default = Translations().load(dirname='locale')
    
    # load 'messages-alternate.mo'
    trans_altern8 = Translations().load(dirname='locale', domain='messages-alternate.mo')
    
    
    # resources/some-other-file.py
    # Now, it's all a matter of using the tranlations
    from resources.translations import (
        trans_default,
        trans_altern8
    )
    
    trans_default.ugettext('Translate Me')
    trans_atlern8.ugettext('Translate Me')

Jinja2

We have the translations instances. We can use it in the python code. How about in our Jinja2 templates? Unfortunately, we have to create a separate Environment instance:
    
    # resources/tranlations.py
    from jinja2 import Environment, FileSystemLoader
    
    env_opts = {
        'loader'    : FileSystemLoader('templates'),
        'extensions': [
            'jinja2.ext.i18n'
        ]
    }
    env_default = Environment(**env_opts)
    env_default.install_gettext_translations(trans_default, newstyle=True)
    
    env_altern8 = Environment(**env_opts)
    env_altern8.install_gettext_translations(trans_altern8, newstyle=True)

Jinja2 uses ugettext by default.

Back to Python - Rammi

To hook it up in the code.

    # resources/resources.py
    from rammi.resources import BaseResource
    from txtemplate import Jinja2TemplateLoader
    
    from resources import translations
    
    
    class IndexResource(BaseResource):
        def __init__(self, mode=0, *args, **kwargs):
            BaseResource.__init__(self, *args, **kwargs)
            
            if mode:
                # Tranlations
                self._ = translations.trans_default.ugettext
                
                # Template Renderers
                self.templates.environment = translations.env_default
            else:
                # Tranlations
                self._ = translations.trans_altern8.ugettext
                
                # Template Renderers
                self.templates.environment = translations.env_altern8

Rammi should be able to use the templates with the customized Jinja environment for your translations usage and then at the same time, you should be using self._ instead of the plain ugettext function for translations inside your Resource, especially ones whose translations branch out according to its mode

No comments:

Post a Comment