Flask-BabelEx

Flask-BabelEx is an extension to Flask that adds i18n and l10n support to any Flask application with the help of babel, pytz and speaklater. It has builtin support for date formatting with timezone support as well as a very simple and friendly interface to gettext translations.

Installation

Install the extension with one of the following commands:

$ easy_install Flask-BabelEx

or alternatively if you have pip installed:

$ pip install Flask-BabelEx

Please note that Flask-BabelEx requires Jinja 2.5. If you are using an older version you will have to upgrade or disable the Jinja support.

Configuration

To get started all you need to do is to instanciate a Babel object after configuring the application:

from flask import Flask
from flask.ext.babelex import Babel

app = Flask(__name__)
app.config.from_pyfile('mysettings.cfg')
babel = Babel(app)

The babel object itself can be used to configure the babel support further. Babel has two configuration values that can be used to change some internal defaults:

BABEL_DEFAULT_LOCALE The default locale to use if no locale selector is registered. This defaults to 'en'.
BABEL_DEFAULT_TIMEZONE The timezone to use for user facing dates. This defaults to 'UTC' which also is the timezone your application must use internally.

For more complex applications you might want to have multiple applications for different users which is where selector functions come in handy. The first time the babel extension needs the locale (language code) of the current user it will call a localeselector() function, and the first time the timezone is needed it will call a timezoneselector() function.

If any of these methods return None the extension will automatically fall back to what’s in the config. Furthermore for efficiency that function is called only once and the return value then cached. If you need to switch the language between a request, you can refresh() the cache.

Example selector functions:

from flask import g, request

@babel.localeselector
def get_locale():
    # if a user is logged in, use the locale from the user settings
    user = getattr(g, 'user', None)
    if user is not None:
        return user.locale
    # otherwise try to guess the language from the user accept
    # header the browser transmits.  We support de/fr/en in this
    # example.  The best match wins.
    return request.accept_languages.best_match(['de', 'fr', 'en'])

@babel.timezoneselector
def get_timezone():
    user = getattr(g, 'user', None)
    if user is not None:
        return user.timezone

The example above assumes that the current user is stored on the flask.g object.

Formatting Dates

To format dates you can use the format_datetime(), format_date(), format_time() and format_timedelta() functions. They all accept a datetime.datetime (or datetime.date, datetime.time and datetime.timedelta) object as first parameter and then optionally a format string. The application should use naive datetime objects internally that use UTC as timezone. On formatting it will automatically convert into the user’s timezone in case it differs from UTC.

To play with the date formatting from the console, you can use the test_request_context() method:

>>> app.test_request_context().push()

Here some examples:

>>> from flask.ext.babelex import format_datetime
>>> from datetime import datetime
>>> format_datetime(datetime(1987, 3, 5, 17, 12))
u'Mar 5, 1987 5:12:00 PM'
>>> format_datetime(datetime(1987, 3, 5, 17, 12), 'full')
u'Thursday, March 5, 1987 5:12:00 PM World (GMT) Time'
>>> format_datetime(datetime(1987, 3, 5, 17, 12), 'short')
u'3/5/87 5:12 PM'
>>> format_datetime(datetime(1987, 3, 5, 17, 12), 'dd mm yyy')
u'05 12 1987'
>>> format_datetime(datetime(1987, 3, 5, 17, 12), 'dd mm yyyy')
u'05 12 1987'

And again with a different language:

>>> app.config['BABEL_DEFAULT_LOCALE'] = 'de'
>>> from flask.ext.babelex import refresh; refresh()
>>> format_datetime(datetime(1987, 3, 5, 17, 12), 'EEEE, d. MMMM yyyy H:mm')
u'Donnerstag, 5. M\xe4rz 1987 17:12'

For more format examples head over to the babel documentation.

Using Translations

The other big part next to date formatting are translations. For that, Flask uses gettext together with Babel. The idea of gettext is that you can mark certain strings as translatable and a tool will pick all those app, collect them in a separate file for you to translate. At runtime the original strings (which should be English) will be replaced by the language you selected.

There are two functions responsible for translating: gettext() and ngettext(). The first to translate singular strings and the second to translate strings that might become plural. Here some examples:

from flask.ext.babelex import gettext, ngettext

gettext(u'A simple string')
gettext(u'Value: %(value)s', value=42)
ngettext(u'%(num)s Apple', u'%(num)s Apples', number_of_apples)

Additionally if you want to use constant strings somewhere in your application and define them outside of a request, you can use a lazy strings. Lazy strings will not be evaluated until they are actually used. To use such a lazy string, use the lazy_gettext() function:

from flask.ext.babelex import lazy_gettext

class MyForm(formlibrary.FormBase):
    success_message = lazy_gettext(u'The form was successfully saved.')

So how does Flask-BabelEx find the translations? Well first you have to create some. Here is how you do it:

Translating Applications

First you need to mark all the strings you want to translate in your application with gettext() or ngettext(). After that, it’s time to create a .pot file. A .pot file contains all the strings and is the template for a .po file which contains the translated strings. Babel can do all that for you.

First of all you have to get into the folder where you have your application and create a mapping file. For typical Flask applications, this is what you want in there:

[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

Save it as babel.cfg or something similar next to your application. Then it’s time to run the pybabel command that comes with Babel to extract your strings:

$ pybabel extract -F babel.cfg -o messages.pot .

If you are using the lazy_gettext() function you should tell pybabel that it should also look for such function calls:

$ pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot .

This will use the mapping from the babel.cfg file and store the generated template in messages.pot. Now we can create the first translation. For example to translate to German use this command:

$ pybabel init -i messages.pot -d translations -l de

-d translations tells pybabel to store the translations in this folder. This is where Flask-BabelEx will look for translations. Put it next to your template folder.

Now edit the translations/de/LC_MESSAGES/messages.po file as needed. Check out some gettext tutorials if you feel lost.

To compile the translations for use, pybabel helps again:

$ pybabel compile -d translations

What if the strings change? Create a new messages.pot like above and then let pybabel merge the changes:

$ pybabel update -i messages.pot -d translations

Afterwards some strings might be marked as fuzzy (where it tried to figure out if a translation matched a changed key). If you have fuzzy entries, make sure to check them by hand and remove the fuzzy flag before compiling.

Flask-BabelEx looks for message catalogs in translations directory which should be located under Flask application directory. Default domain is “messages”.

For example, if you want to have translations for German, Spanish and French, directory structure should look like this:

translations/de/LC_MESSAGES/messages.mo translations/sp/LC_MESSAGES/messages.mo translations/fr/LC_MESSAGES/messages.mo

Translation Domains

By default, Flask-BabelEx will use “messages” domain, which will make it use translations from the messages.mo file. It is not very convenient for third-party Flask extensions, which might want to localize themselves without requiring user to merge their translations into “messages” domain.

Flask-BabelEx allows extension developers to specify which translation domain to use:

from flask.ext.babelex import Domain

mydomain = Domain(domain='myext')

mydomain.lazy_gettext('Hello World!')

Domain contains all gettext-related methods (gettext(), ngettext(), etc).

In previous example, localizations will be read from the myext.mo files, but they have to be located in translations directory under users Flask application. If extension is distributed with the localizations, it is possible to specify their location:

from flask.ext.babelex import Domain

from flask.ext.myext import translations
mydomain = Domain(translations.__path__[0])

mydomain will look for translations in extension directory with default (messages) domain.

It is also possible to change the translation domain used by default, either for each app or per request.

To set the Domain that will be used in an app, pass it to Babel on initialization:

from flask import Flask
from flask.ext.babelex import Babel, Domain

app = Flask(__name__)
domain = Domain(domain='myext')
babel = Babel(app, default_domain=domain)

Translations will then come from the myext.mo files by default.

To change the default domain in a request context, call the as_default() method from within the request context:

from flask import Flask
from flask.ext.babelex import Babel, Domain, gettext

app = Flask(__name__)
domain = Domain(domain='myext')
babel = Babel(app)

@app.route('/path')
def demopage():
    domain.as_default()

    return gettext('Hello World!')

Hello World! will get translated using the myext.mo files, but other requests will use the default messages.mo. Note that a Babel must be initialized for the app for translations to work at all.

Troubleshooting

On Snow Leopard pybabel will most likely fail with an exception. If this happens, check if this command outputs UTF-8:

$ echo $LC_CTYPE
UTF-8

This is a OS X bug unfortunately. To fix it, put the following lines into your ~/.profile file:

export LC_CTYPE=en_US.utf-8

Then restart your terminal.

API

This part of the documentation documents each and every public class or function from Flask-BabelEx.

Configuration

class flask.ext.babelex.Babel(app=None, default_locale='en', default_timezone='UTC', date_formats=None, configure_jinja=True, default_domain=None)

Central controller class that can be used to configure how Flask-Babel behaves. Each application that wants to use Flask-Babel has to create, or run init_app() on, an instance of this class after the configuration was initialized.

default_locale

The default locale from the configuration as instance of a babel.Locale object.

default_timezone

The default timezone from the configuration as instance of a pytz.timezone object.

init_app(app)

Set up this instance for use with app, if no app was passed to the constructor.

list_translations()

Returns a list of all the locales translations exist for. The list returned will be filled with actual locale objects and not just strings.

New in version 0.6.

load_locale(locale)

Load locale by name and cache it. Returns instance of a babel.Locale object.

localeselector(f)

Registers a callback function for locale selection. The default behaves as if a function was registered that returns None all the time. If None is returned, the locale falls back to the one from the configuration.

This has to return the locale as string (eg: 'de_AT', ‘’en_US‘’)

timezoneselector(f)

Registers a callback function for timezone selection. The default behaves as if a function was registered that returns None all the time. If None is returned, the timezone falls back to the one from the configuration.

This has to return the timezone as string (eg: 'Europe/Vienna')

Context Functions

flask.ext.babelex.get_locale()

Returns the locale that should be used for this request as babel.Locale object. This returns None if used outside of a request. If flask-babel was not attached to the Flask application, will return ‘en’ locale.

flask.ext.babelex.get_timezone()

Returns the timezone that should be used for this request as pytz.timezone object. This returns None if used outside of a request. If flask-babel was not attached to application, will return UTC timezone object.

Translation domains

class flask.ext.babelex.Domain(dirname=None, domain='messages')

Localization domain. By default will use look for tranlations in Flask application directory and “messages” domain - all message catalogs should be called messages.mo.

as_default()

Set this domain as default for the current request

get_translations()

Returns the correct gettext translations that should be used for this request. This will never fail and return a dummy translation object if used outside of the request or if a translation cannot be found.

get_translations_cache(ctx)

Returns dictionary-like object for translation caching

get_translations_path(ctx)

Returns translations directory path. Override if you want to implement custom behavior.

gettext(string, **variables)

Translates a string with the current locale and passes in the given keyword arguments as mapping to a string formatting string.

gettext(u'Hello World!')
gettext(u'Hello %(name)s!', name='World')
lazy_gettext(string, **variables)

Like gettext() but the string returned is lazy which means it will be translated when it is used as an actual string.

Example:

hello = lazy_gettext(u'Hello World')

@app.route('/')
def index():
    return unicode(hello)
lazy_pgettext(context, string, **variables)

Like pgettext() but the string returned is lazy which means it will be translated when it is used as an actual string.

New in version 0.7.

ngettext(singular, plural, num, **variables)

Translates a string with the current locale and passes in the given keyword arguments as mapping to a string formatting string. The num parameter is used to dispatch between singular and various plural forms of the message. It is available in the format string as %(num)d or %(num)s. The source language should be English or a similar language which only has one plural form.

ngettext(u'%(num)d Apple', u'%(num)d Apples', num=len(apples))
npgettext(context, singular, plural, num, **variables)

Like ngettext() but with a context.

New in version 0.7.

pgettext(context, string, **variables)

Like gettext() but with a context.

New in version 0.7.

Datetime Functions

flask.ext.babelex.to_user_timezone(datetime)

Convert a datetime object to the user’s timezone. This automatically happens on all date formatting unless rebasing is disabled. If you need to convert a datetime.datetime object at any time to the user’s timezone (as returned by get_timezone() this function can be used).

flask.ext.babelex.to_utc(datetime)

Convert a datetime object to UTC and drop tzinfo. This is the opposite operation to to_user_timezone().

flask.ext.babelex.format_datetime(datetime=None, format=None, rebase=True)

Return a date formatted according to the given pattern. If no datetime object is passed, the current time is assumed. By default rebasing happens which causes the object to be converted to the users’s timezone (as returned by to_user_timezone()). This function formats both date and time.

The format parameter can either be 'short', 'medium', 'long' or 'full' (in which cause the language’s default for that setting is used, or the default from the Babel.date_formats mapping is used) or a format string as documented by Babel.

This function is also available in the template context as filter named datetimeformat.

flask.ext.babelex.format_date(date=None, format=None, rebase=True)

Return a date formatted according to the given pattern. If no datetime or date object is passed, the current time is assumed. By default rebasing happens which causes the object to be converted to the users’s timezone (as returned by to_user_timezone()). This function only formats the date part of a datetime object.

The format parameter can either be 'short', 'medium', 'long' or 'full' (in which cause the language’s default for that setting is used, or the default from the Babel.date_formats mapping is used) or a format string as documented by Babel.

This function is also available in the template context as filter named dateformat.

flask.ext.babelex.format_time(time=None, format=None, rebase=True)

Return a time formatted according to the given pattern. If no datetime object is passed, the current time is assumed. By default rebasing happens which causes the object to be converted to the users’s timezone (as returned by to_user_timezone()). This function formats both date and time.

The format parameter can either be 'short', 'medium', 'long' or 'full' (in which cause the language’s default for that setting is used, or the default from the Babel.date_formats mapping is used) or a format string as documented by Babel.

This function is also available in the template context as filter named timeformat.

flask.ext.babelex.format_timedelta(datetime_or_timedelta, granularity='second')

Format the elapsed time from the given date to now or the given timedelta. This currently requires an unreleased development version of Babel.

This function is also available in the template context as filter named timedeltaformat.

Gettext Functions

flask.ext.babelex.gettext(*args, **kwargs)
flask.ext.babelex.ngettext(*args, **kwargs)
flask.ext.babelex.pgettext(*args, **kwargs)
flask.ext.babelex.npgettext(*args, **kwargs)
flask.ext.babelex.lazy_gettext(*args, **kwargs)
flask.ext.babelex.lazy_pgettext(*args, **kwargs)

Low-Level API

flask.ext.babelex.refresh()

Refreshes the cached timezones and locale information. This can be used to switch a translation between a request and if you want the changes to take place immediately, not just with the next request:

user.timezone = request.form['timezone']
user.locale = request.form['locale']
refresh()
flash(gettext('Language was changed'))

Without that refresh, the flash() function would probably return English text and a now German page.

Fork me on GitHub