Skip to main content

Python Liquid Babel

Liquid Babel is a collection of optional filters and tags that facilitate the internationalization (i18n) and localization (i10n) of Liquid templates.

info

Liquid Babel uses Python Babel. Please refer to the Babel docs for more information about working with message catalogs, locales, currency codes and format strings.

Install

Install Python Liquid Babel using pipenv:

$ pipenv install liquid-babel

Or pip:

$ python -m pip install -U liquid-babel

Filters

Liquid Babel filters are implemented as Python classes. Pass options to a filter constructor, then register the resulting instance with a Liquid Environment by calling its add_filter() method.

from liquid import Environment
from liquid_babel.filters import Currency

env = Environment()
env.add_filter("currency", Currency(
default_locale="en_CA",
default_format="#,##0.00",
))

template = env.from_string("{{ 100457.99 | currency }}")
print(template.render())

Tags

Liquid Babel tags can be registered with an Environment using its add_tag() method. Tags are configured by setting attributes on a subclass. This example changes the name of the default translate tag to trans and disables whitespace normalization in translation messages.

from liquid import Environment
from liquid_babel.tags.translate import TranslateTag

class MyTranslateTag(TranslateTag):
name = "trans"
end = "endtrans"
trim_messages = False

env = Environment()
env.add_tag(MyTranslateTag)

source = """
{%- trans count: worlds | size, you: 'World' -%}
Hello, {{ you }}!
{%- plural -%}
Hello, {{ you }}s!
{%- endtrans -%}
"""

template = env.from_string(source)

# With default NullTranslation
print(template.render())

Translations

New in Liquid Babel version 0.3.0

Liquid Babel includes gettext, ngettext, pgettext and npgettext filter equivalents to the functions found in Python's gettext module. Application developers can choose to use any of these filters, possibly using more user friendly filter names, and/or the more general t (translate) filter.

The t filter can behave like any of the *gettext filters, depending on the arguments it is given. Where the *gettext filters require positional arguments for context, count and plural, t reserves optional count and plural keyword arguments. See the Liquid Babel filter reference for filter configuration and usage examples.

Liquid Babel also offers a {% translate %} tag. This is similar to the {% trans %} tag found in Jinja or the {% blocktranslate %} tag found in Django's template language. Again, application developers can configure and customize the included translate tag to suit an application's needs. See the Liquid Babel tag reference for customization and usage examples.

Message Catalogs

By default, all translation filters and tags will look for a render context variable called translations, which must be an object implementing the Translations protocol. It is the application developer's responsibility to provide a Translations object, being the interface between Liquid and a message catalog.

The Translations protocol is defined as follows. It is simply a subset of the NullTranslations class found in the gettext module.

class Translations(Protocol):
def gettext(self, message: str) -> str:
...

def ngettext(self, singular: str, plural: str, n: int) -> str:
...

def pgettext(self, context: str, message: str) -> str:
...

def npgettext(self, context: str, singular: str, plural: str, n: int) -> str:
...

It could be a GNUTranslations instance, a Babel Translations instance, or any object implementing gettext, ngettext, pgettext and npgettext methods.

Message Variables

Translatable message text can contain placeholders for variables. When using variables in strings to be translated by Liquid Babel filters, variables are defined using percent-style formatting. Only the s modifier is supported and every variable must have a name. In this example you is the variable name.

{{ "Hello, %(you)s!" | t }}
tip

Message variables can be disabled by setting the message_interpolation argument to False when instantiating any of the included translation filters.

Filter keyword arguments are merged with the current render context before being used to replace variables in message text. All variables are converted to their string representation before substitution. Dotted property/attribute access is not supported inside message variables.

{{ "Hello, %(you)s!" | t: you: user.name }}

The translate block tag recognizes simplified Liquid output statements as translation message variables. These variables must be valid identifiers without dotted or bracketed property/attribute access, and no filters.

{% translate %}
Hello, {{ you }}!
{% endtranslate %}

Keyword arguments passed to the translate tag will be merged with the current render context before being used to replace variables in message text. These arguments can use simple, no-argument filters, like size. You should expect a TranslationSyntaxError if unexpected filters or filter arguments are used.

{% translate you: user.name | capitalize, count: users | size %}
Hello, {{ you }}!
{% plural %}
Hello, {{ you }}s!
{% endtranslate %}

Message Extraction

Use the liquid_babel.messages.extract_from_templates() function to build a message catalog from one or more templates. You are then free to make use of Babel's PO file features, or convert the catalog to a more convenient internal representation.

import io

from babel.messages.pofile import write_po

from liquid import Environment
from liquid_babel.filters import register_translation_filters
from liquid_babel.messages import extract_from_templates
from liquid_babel.tags import TranslateTag

env = Environment()
register_translation_filters(env)
env.add_tag(TranslateTag)

source = """
{% # Translators: some comment %}
{{ 'Hello, World!' | t }}
{% comment %}Translators: other comment{% endcomment %}
{% translate count: 2 %}
Hello, {{ you }}!
{% plural %}
Hello, all!
{% endtranslate %}
"""

template = env.from_string(source, name="something.liquid")
catalog = extract_from_templates(template)

buf = io.BytesIO()
write_po(buf, catalog, omit_header=True)
print(buf.getvalue().decode("utf-8"))
output
#. Translators: some comment
#: something.liquid:3
msgid "Hello, World!"
msgstr ""

#. Translators: other comment
#: something.liquid:5
#, python-format
msgid "Hello, %(you)s!"
msgid_plural "Hello, all!"
msgstr[0] ""
msgstr[1] ""

Liquid Babel also includes a Babel compatible extraction method, liquid_babel.messages.extract_liquid(). However, Babel's command-line interface and Setuptools integration are unlikely to be particularly useful to a typical Liquid use case.

If you don't want to work with Babel catalogs or the Babel command-line interface, the lower-level liquid_babel.messages.extract_from_template() yields a (lineno, funcname, message, comments) tuple for each message in a template. Where message could be a string, or a tuple of strings in the case of a pluralizable message.

Translator Comments

When a Liquid comment tag immediately precedes a translatable filter or tag, and the comment starts with a string in comment_tags, that comment will be included as a translator comment with the message. Use the comment_tags argument to extract_liquid(), extract_from_template() or extract_from_template() to change translator comment prefixes. The default is ["Translators:"].

from liquid import Environment
from liquid_babel.filters import register_translation_filters
from liquid_babel.messages import extract_from_templates
from liquid_babel.tags import TranslateTag

env = Environment()
register_translation_filters(env)
env.add_tag(TranslateTag)

source = """
{% # Translators: some comment %}
{{ 'Hello, World!' | t }}
{% comment %}Translators: other comment{% endcomment %}
{% translate count: 2 %}
Hello, {{ you }}!
{% plural %}
Hello, all!
{% endtranslate %}
"""

template = env.from_string(source, name="something.liquid")
catalog = extract_from_templates(template, strip_comment_tags=True)

message = catalog.get("Hello, World!")
print(message.auto_comments[0]) # some comment

Note that Python Liquid's non-standard comment syntax is not supported.