Install and use

Install

Activate your virtual environment, then do:

$ pip install puente

Optional: If you want to extract strings from Django templates, you will also need to install django-babel which has an extractor for Django templates:

$ pip install django-babel

Configure

In settings.py add Puente to INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    'puente',
    # ...
]

In settings.py add puente.ext.i18n as an extension in your Jinja2 template environment configuration. For example, if you were using django-jinja, then it might look like this:

TEMPLATES = [
    {
        'BACKEND': 'django_jinja.backend.Jinja2',
        # ...
        'OPTIONS': {
             # ...
             'autoescape': True,
             'extensions': [
                 # ...
                 'puente.ext.i18n',
                 # ...
             ],
        },
    }
]

Puente configuration goes in the PUENTE setting in your Django settings file. Here’s a minimal example:

PUENTE = {
    'BASE_DIR': BASE_DIR,
    'DOMAIN_METHODS': {
        'django': [
            ('**.py', 'python'),
            ('fjord/**/jinja2/**.html', 'jinja2'),
            ('fjord/**/templates/**.html', 'django'),
        ],
        'djangojs': [
            ('**.js', 'javascript'),
        ]
    }
}

This sets up string extraction for Jinja2 templates using the Jinja2 extractor, Python files using the Python extractor, and Django templates using the Django extractor [1] and puts all those strings in django.pot files.

[1]You need to install django-babel for the Django extractor for it to be available.

Note that BASE_DIR is the path to the project root. It’s in the settings.py file that is generated when you create a new Django project.

BASE_DIR
Type:String
Default:None
Required:Yes

This is the absolute path to the root directory which has locale/ in it. In most cases, it’s probably fine to set it to BASE_DIR which is in the settings.py file that Django generates when you create a new project.

For example:

/home/willkg/
   - fjord/         <-- BASE_DIR
     - .git/
     - locale/
     - fjord/
       - code!!!
     - manage.py
DOMAIN_METHODS
Type:Dict of string to list of (string, string) tuples
Default:None
Required:Yes

Dict of domain name to list of (file matcher, extractor) tuples.

A domain name here is the name that’s used to name the .pot and .po files. For example, if the domain was “django”, then the resulting files would be django.pot and django.po.

The file matcher uses * and ** glob patterns.

The only valid domains are django and djangojs.

Valid extractors include:

  • python for Python files (Babel)
  • javascript for Javascript files (Babel)
  • ignore for files to ignore to alleviate difficulties in file matching (Babel)
  • jinja2 for Jinja2 templates (Jinja2)
  • django for django templates (django-babel) [2]
[2]You need to install django-babel for the Django extractor for it to be available.

You can use extractors provided by other libraries, too. You can also write your own extractors and use a dotted path to the extraction function.

Example of DOMAIN_METHODS:

PUENTE = {
    'DOMAIN_METHODS': {
        'django': [
            ('fjord/**/jinja2/**.html', 'jinja2'),
            ('**.py', 'python')
            ('fjord/**/templates/**.html', 'django'),
        ],
        'djangojs': [
            ('**.js', 'javascript'),
        ]
    }
}

Note

The syntax is an inclusion-style syntax where you specify some group of files to use some extractor.

In some cases, this is very inconvenient because you might need to say something like “use this extractor with all the files with this glob pattern except this one….”.

To exclude files, you create a rule higher up in the list and use the ignore extractor.

For example, to use jinja2 for all files in a directory except ones named whaleshark.html, you’d do something like this:

PUENTE = {
    'DOMAIN_METHODS': {
        'django': [
            ('fjord/**/jinja2/whaleshark.html', 'ignore'),
            ('fjord/**/jinja2/**.html', 'jinja2')
        ]
    }
}

The example is pretty contrived, but hopefully that helps.

KEYWORDS
Type:Dict of keyword to Babel magic
Default:Common gettext indicators
Required:No

Babel has keywords:

https://github.com/python-babel/babel/blob/5116c167/babel/messages/extract.py#L31

Puente adds '_lazy': None to that.

Babel uses the keywords to know what strings to extract and how to extract them.

There’s a puente.utils.generate_keywords function to make it easier to get all the defaults plus the ones you want:

from puente.utils import generate_keywords

PUENTE = {
    'KEYWORDS': generate_keywords({'foo': None})
}
COMMENT_TAGS
Type:List of strings
Default:['Translators:', 'L10n:', 'L10N:', 'l10n:', 'l10N:']
Required:No

The list of prefixes that denote a comment tag intended for the translator.

For example, if you had code like this:

# l10n: This is a menu name.
menu_name = _('File')

Then the comment will get extracted as a translator comment.

Note

Django project uses “Translators:”, so if you use that, you’re closer to vanilla Django.

JINJA2_CONFIG
Type:Dict
Default:Complicated…
Required:Possibly

This has the options to pass to babel_extract.

http://jinja.pocoo.org/docs/dev/integration/#babel-integration

Setting it yourself

Generally, you can add syntax-related options that’d you’d pass in to build a new Jinja2 Environment:

http://jinja.pocoo.org/docs/dev/api/#jinja2.Environment

Additionally, in Jinja2 2.7, they added a silent option which dictates whether the parser fails silently when parsing Jinja2 templates. This commonly happens in two scenarios:

  1. The list of extensions passed isn’t the complete list.
  2. The HTML file isn’t a Jinja2 template.

For debugging purposes, you definitely want silent=False.

Example of JINJA2_CONFIG:

PUENTE = {
    'JINJA2_CONFIG`: {
        'extensions': [
            'jinja2.ext.do',
            'jinja2.ext.loopcontrols',
            'django_jinja.builtins.extensions.CsrfExtension',
            'django_jinja.builtins.extensions.StaticFilesExtension',
            'django_jinja.builtins.extensions.DjangoFiltersExtension',
            'puente.ext.i18n',
        ]
    }
}

Having Puente figure it out for you

If you’re using Jingo or django-jinja, then Puente will try to extract the list of extensions from the relevant settings. If that works for you, then you don’t need to set this.

If Puente is figuring it out, it will automatically add silent=False.

For example, if you’re using django-jinja with these settings:

TEMPLATES = [
    {
        'BACKEND': 'django_jinja.backend.Jinja2',
        # ...
        'OPTIONS': {
             # ...
             'extensions': [
                 # ...
                 'puente.ext.i18n',
                 # ...
             ],
        }
    }
]

Then Puente will build something like this:

PUENTE = {
   # ...
   'JINJA_CONFIG': {
      'extensions': [
          # ...
          'puente.ext.i18n',
          # ...
      ],
      'silent': 'False'
   }
}
PROJECT
Type:String
Default:“PROJECT”
Required:No

The name of this project. This goes in the .pot and .po files and could help translators know which project this file that they’re translating belongs to.

VERSION
Type:String
Default:“1.0”
Required:No

The version of this project. This goes in the .pot and .po files and could help translators know which version of the project this file that they’re translating belongs to.

MSGID_BUGS_ADDRESS
Type:String
Default:“”
Required:No

The email address or url to send bugs related to msgids to. Without this, it’s hard for a translator to know how to report issues back. If they have this, then reporting issues is much easier.

You want good strings, so this is a good thing to set.

For example:

PUENTE = {
    # ...
    'MSGID_BUGS_ADDRESS': 'https://bugzilla.mozilla.org/enter_bug.cgi?project=Input'
}

Templates

We hope you’re using Jinja2’s newstyle gettext and autoescape = True. If that’s the case, then these docs will help:

Further, Puente adds support for pgettext and npgettext in templates:

{{ pgettext("some context", "message string") }}
{{ npgettext("some context", "singular message", "plural message", 5) }}

FIXME: Expand on this and talk about escaping and |safe.

Extract and merge usage

Message extraction

After you’ve configured Puente, you can extract messages like this:

$ ./manage.py extract

This will extract all the strings specified by the DOMAIN_METHODS setting and put them into a <domain>.pot file.

Message merge

After you’ve extracted messages, you’ll want to merge new messages into new or existing locale-specific .po files. You can merge messages like this:

$ ./manage.py merge