Usage¶
Using Fluent in a Django project requires understanding a number of concepts and APIs, in addition to understanding the Fluent syntax. This guide outlines the main things you need.
Terminology¶
Internationalization and localization (i18n and l10n) tools usually distinguish between ‘languages’ and ‘locales’. ‘Locale’ is a broader term than includes other cultural/regional differences, such as how numbers and dates are represented.
Since they go together, Fluent not only addresses language translation, it also integrates locale support. If a message contains a number substitution, when different locales are active the number formatting will match the language automatically. For this reason the django-ftl docs generally do not make a big distinction between these terms, but tend to use ‘locale’ (which includes language).
Django’s i18n docs
distinguish between ‘locale name’ (which look like it
, en_US
etc) and
‘language code’ (which look like it
, en-us
). In reality there is a lot
of overlap between these. Most modern systems (e.g. unicode CLDR) use BCP 47 language tags, which are the same as
‘language codes’. They in fact represent locales as well as languages, and have
a mechanism for incorporating more specific locale information.
Fluent and django-ftl use BCP 47 language tags in all their APIs (more information below).
FTL files and layout¶
Fluent translations are placed in Fluent Translation List files, with the suffix
.ftl
. For them to be found by django-ftl, you need to use the following
conventions, which align with the conventions used across other tools that use
Fluent (such as Pontoon).
For the sake of this guide, we will assume you are writing a Django app
(reusable or non-reusable) called myapp
- that is, it forms a Python
top-level module/package called myapp
. You will need to replace myapp
with the actual name of your app.
You will need your directory layout to match the following example:
myapp/
__init__.py
ftl_bundles.py
locales/
en/
myapp/
main.ftl
de/
myapp/
main.ftl
That is:
Within your
myapp
package directory, create alocales
directory. In a typical Django app, thislocales
directory exists at the same level as your app-specifictemplates
,templatetags
,static
etc. directories.For each locale you support, within that folder create a directory with the locale name. The example above shows English and German. Locale names should be in BCP 47 format.
It is recommended that you follow the capitalization convention in BCP 47, which is:
Lower case for the language code
Title case for script code
Upper case for region code
e.g. en, en-GB, en-US, de-DE, zh-Hans-CN
django-ftl does not enforce this convention - it will find locale files if different capitalization is used. However, if multiple directories exist for the same locale, differing only by case (e.g.
EN-US
anden-US
), and their contents are not the same, then your FTL files will probably not be found correctly.Finally, django-ftl will also find the FTL files if you name the directories in Unix “locale name” convention with underscores e.g.
en_GB
, although for the sake of consistency and other tools this is also not recommended.Within each specific locale directory, create another directory with the name of your app. This is necessary to give a separate namespace for your FTL files, so that they don’t clash with the FTL files that might be provided by other Django apps. By doing it this way, you can reference FTL files from other apps in your app — this is very similar to how templates and static files work in Django.
Within that
myapp
directory, you can add any number of further sub-directories, and can split your FTL files up into as many files as you want. For the remainder of this guide we will assume a singlemyapp/main.ftl
file for each locale.
The contents of these files must be valid Fluent syntax. For the sake of this
guide, we will assume myapp
has an ‘events’ page which greets the user, and
informs them how many new events have happened on the site since their last
visit. It might have an English myapp/main.ftl
file that looks like this:
events-title = MyApp Events!
events-greeting = Hello, { $username }
events-new-events-info = { $count ->
[0] There have been no new events since your last event.
[1] There has been one new event since your last visit.
*[other] There have been { $count } new events since your last visit.
}
In this .ftl
file, events-title
, events-greeting
and
events-new-events-info
are Fluent message IDs. Note that we have used
events-
as an adhoc namespace for this ‘events’ page, to avoid name clashes
with other messages from our app. It’s recommended to use a prefix like this for
different pages or components in your app.
Bundles¶
To use .ftl
files with django-ftl, you must first define a
Bundle
. They represent a collection of .ftl
files that you want to use, and are responsible for finding and loading these
files. The definition of a Bundle
can go anywhere in your project, but we
recommend the convention of creating a ftl_bundles.py
file inside your
Python myapp
package, i.e. a myapp.ftl_bundles
module.
Our ftl_bundles.py
file will look like this:
from django_ftl.bundles import Bundle
main = Bundle(['myapp/main.ftl'])
Bundle
takes a single required positional argument
which is a list of FTL files. See Bundle
API docs
for other arguments.
Activating a locale/language¶
The most direct way to activate a specific language/locale is to use
django_ftl.activate()
:
from django_ftl import activate
activate("en-US")
The argument can be any BCP 47 language tag, or a “language priority list” (a prioritized, comma separated list of language tags). For example:
"en-US, en, fr"
It is recommended that the value passed in should be validated by your own code. Normally it will come from a list of options that you have given to a user (see Setting the user language preference below).
As soon as you activate a language, all Bundle
objects will switch to using
that language, for the current thread only. (Before activating, they will use
your LANGUAGE_CODE
setting as a default if require_activate=False
, and
this is also used as a fallback in the case of missing FTL files or messages).
Please note that activate
is stateful, meaning it is essentially a global
(thread local) variable that is preserved between requests. This introduces the
possibility that one user’s request changes the behavior of subsequent requests
made by a completely different user. This problem can also affect test isolation
in automated tests. The best way to avoid these problems is to use
django_ftl.override()
instead:
from django_ftl import override
with override("en-US"):
pass # Code that uses this language
Alternatively, ensure that django_ftl.deactivate()
is called at the end of a
request.
Using middleware¶
The way you choose to activate a given language will depend on your exact setup.
django-ftl
comes with a few middleware that may help you automatically
activate a locale for every request.
If you were using Django’s built-in i18n solution previously, or are still using
it for some parts of your app, you may also be using
django.middleware.locale.LocaleMiddleware.
If that is the case, and if you want to continue using LocaleMiddleware
, the
easiest solution is to add
"django_ftl.middleware.activate_from_request_language_code"
after it in your
MIDDLEWARE
setting:
MIDDLEWARE = [
...
"django.middleware.locale.LocaleMiddleware",
"django_ftl.middleware.activate_from_request_language_code"
...
]
This is a very simple middleware that simply looks at request.LANGUAGE_CODE
(which has been set by django.middleware.locale.LocaleMiddleware
) and
activates that language for django-ftl.
Instead of these two, you could also use
"django_ftl.middleware.activate_from_request_session"
by adding it to your
MIDDLEWARE
(somewhere after the session middleware). This middleware looks
for a language set in request.session
, as set by the set_language
view
that Django provides (see set_language docs),
and uses this value, falling back to settings.LANGUAGE_CODE
if it is not
found. It also sets request.LANGUAGE_CODE
to the same value, similar to how
django.middleware.locale.LocaleMiddleware
behaves.
Both of these provided middleware use override
to set the locale, not
activate
, as per the advice above, for better request and test isolation.
You are not limited to these middleware, or to using Django’s set_language
view — these are provided as shortcuts and examples. In some cases it will be
best to write your own, using the middleware source code
as a starting point.
Outside of the request-response cycle¶
If you need to generate localized text from code running outside of the request-response cycle (e.g. cron jobs or asynchronous tasks), you will not be able to use middleware, and will need some other way to determine the locale to use. This might involve:
a field on a model (e.g.
User
class) to store the locale preference.for asynchronous tasks such as Celery, you could pass the locale as an argument. For Celery, signals such as task-prerun might be useful.
Once you have determined the locale you need, use django_ftl.activate()
or
django_ftl.override()
to activate it.
Using bundles from Python¶
After you have activated a locale, to obtain a translation you call the
Bundle
format()
method, passing in a
message ID and an optional dictionary of arguments:
>>> from myapp.ftl_bundles import main as ftl_bundle
>>> ftl_bundle.format('events-title')
'MyApp Events!'
>>> ftl_bundle.format('events-greeting', {'username': 'boaty mcboatface'})
'Hello, \u2068boaty mcboatface\u2069'
The \u2068
and \u2069
characters are unicode bidi isolation characters that are
inserted by Fluent to ensure that the layout of text behaves correctly in case
substitutions are in a different script to the surrounding text.
That’s it for the basic case. See format()
for
further info about passing numbers and datetimes, and about how errors are
handled.
Lazy translations¶
Sometimes you need to translate a string lazily. This happens when you have a
string that is defined at module load time (see the Django lazy translation
docs
for more info). For this situation, you can use
format_lazy()
instead of format
. It takes
the same parameters, but doesn’t generate the translation until the value is
used in a string context, such as in template rendering.
For example, the verbose_name
and help_text
attributes of a model field
could be done this way:
from django.db import models
from myapp.ftl_bundles import main as ftl_bundle
class Kitten(models.Model):
name = models.CharField(
ftl_bundle.format_lazy('kitten-name'),
help_text=ftl_bundle.format_lazy('kitten-name.help-text'))
# kittens.ftl
kitten-name = name
.help-text = Use most recent name if there have been are multiple.
Note that here we have used attributes to combine the two related pieces of text into a single message
If you do not use format_lazy
, then the verbose_name
and help_text
attributes will end up always having the text translated into the default
language.
As a more effective way to prevent this from happening, you can also pass
require_activate=True
parameter to Bundle
. As
long as there is no activate
call at module level in your project, this will
cause the Bundle
to raise an exception if you attempt to use the format
method at module level.
Note
If you pass require_activate=True
, you may have trouble with some
features like Django migrations which will attempt to serialize model
and field definitions, which forces lazy strings to be evaluated.
You can work around this problem by putting the following code in
your ftl_bundles.py
files:
import sys
import os.path
from django_ftl import activate
if any(os.path.split(arg)[-1] == 'manage.py' for arg in sys.argv) and 'makemigrations' in sys.argv:
activate('en')
Aliases¶
If you are using the format
and format_lazy
functions a lot, you can
save on typing by defining some appropriate aliases for your bundle methods at
the top of a module - for example:
from myapp.ftl_bundles import main as ftl_bundle
ftl = ftl_bundle.format
ftl_lazy = ftl_bundle.format_lazy
Then use ftl
and ftl_lazy
just as you would use ftl_bundle.format
and ftl_bundle.format_lazy
.
Using bundles from Django templates¶
To use django-ftl template tags in a project, django_ftl
must be added to
your INSTALLED_APPS
like this:
INSTALLED_APPS = (
...
'django_ftl.apps.DjangoFtlConfig',
...
)
Put {% load ftl %}
at the top of your template to load the template tag
library. It provides 3 template tags, at least one of which you will need:
ftlconf
¶
This is used to set up the configuration needed by ftlmsg
, namely the
bundle
to be used. It should be used once near the top of a template (before
any translations are needed), and should be used in the situation where most of
the template will use the same bundle. For setting the configuration for just
part of a template, use withftl
.
The bundle
argument is either a bundle object (passed in via the template
context), or a string that is a dotted path to a bundle.
(An optional mode
may also be passed, which is currently limited to a single
string value 'server'
which is also the default value, so it is currently
not very useful! In the future further options may be added, mainly with the
idea of enabling client-side rendering of the messages.)
Example:
{% load ftl %}
{% ftlconf bundle='myapp.ftl_bundles.main' %}
Example where we pass in the bundle object from the view:
# myapp.views
from myapp.ftl_bundles import main as main_bundle
def my_view(request):
# ...
return render(request, 'myapp/mypage.html',
{'ftl_bundle': main_bundle})
{# myapp/events.html #}
{% load ftl %}
{% ftlconf bundle=ftl_bundle %}
withftl
¶
withftl
is similar to ftlconf
in that its purpose is to set
configuration data for generating messages. It differs in that:
It sets the data only for the contained template nodes, up to a closing
endwithftl
node, which is required.It also takes a
language
parameter that can be used to override the language, in addition to thebundle
andmode
parameters thatftlconf
take. This should be a string in BCP 47 format.
Multiple nested withftl
tags can be used, and they can be nested into a
template that has ftlconf
at the top, and their scope will be limited to the
contained template nodes as you would expect.
Example:
{% load ftl %}
{% withftl bundle='myapp.ftl_bundles.main' %}
{% ftlmsg 'events-title' %}
{% endwithftl %}
{% withftl bundle='myapp.ftl_bundles.other' language='fr' %}
{% ftlmsg 'other-message' %}
{% endwithftl %}
As with ftlconf
, the parameters do not have to be just literal strings, they
can refer to values in the context as most template tags can. You must supply
one or more of mode
, bundle
or language
.
ftlmsg
¶
Finally, to actually render a message, you need to use ftlmsg
. It takes one
required parameter, the message ID, and any number of keyword arguments, which
correspond to the parameters you would pass in the arguments dictionary when
calling format()
in Python code.
Example:
{% load ftl %}
{% ftlconf bundle='myapp.ftl_bundles.main' %}
<body>
<h1>{% ftlmsg 'events-title' %}</h1>
<p>{% ftlmsg 'events-greeting' username=request.user.username %}</p>
</body>
Alternative configuration¶
In some cases, use of ftlconf
or withftl
in templates can be tedious and
you may want to specify configuration of mode/bundle using a more global method.
An alternative is to set some configuration variables in the template context. You can do this using some manual method, or using a context processor. The variables you need to set are given by the constants below:
django_ftl.templatetags.ftl.MODE_VAR_NAME
for mode.django_ftl.templatetags.ftl.BUNDLE_VAR_NAME
for the bundle.
For example, the following is a context processor that will return the required
configuration for the ftlmsg
template tag.
import django_ftl.templatetags.ftl
from my_app.ftl_bundles import main
def ftl(request):
return {
django_ftl.templatetags.ftl.MODE_VAR_NAME: 'server',
django_ftl.templatetags.ftl.BUNDLE_VAR_NAME: main,
}
This could be configured to be used always via your TEMPLATES
context_processors
setting, or invoked manually and merged into a context dictionary.
HTML escaping¶
If your messages are plain text, and you use Django templates, then messages will be HTML-escaped by Django’s automatic escaping mechanism as normal, as there is nothing more to worry about. If you need to include HTML fragments in the messages (e.g. to make some text bold or into a link), read on.
django-ftl plugs in to fluent_compiler
’s escaping mechanism and provides an
escaper out of the box that allows you to handle HTML embedded in your messages.
To use it, give your message IDs the suffix -html
. For example:
welcome-message-html = Welcome { $name }, you look <i>wonderful</i> today.
In this example, $name
will have HTML escaping applied as you expect and
need, while the <i>wonderful</i>
markup will be left as it is. The whole
message will be returned as a Django SafeText
instance so that further HTML
escaping will not be applied.
It is recommended not to use -html
unless you need it, because that will
limit the use of a message to HTML contexts, and it also requires translators to
write correct HTML (for example, with ampersands written as &
).
Note that there are rules regarding how messages with different escapers can be used. For example:
-brand = Ali & Alisha's ice cream
-brand-html = Ali & Alisha's <b>cool</b> ice cream
The -brand
term can be used from any other message, and from a …-html
message it will be correctly escaped. The -brand-html
term, however, can
only be used from other …-html
messages.
Template considerations¶
A very common mistake in i18n is forgetting to set the lang
tag on HTML
content. In the normal case, each base template that contains an <html>
tag
needs to be modified to add the lang
attribute - assuming you’ve used
middleware as described above this could be as simple as:
<html lang="{{ request.LANGUAGE_CODE }}">
See w3c docs on the lang attribute for more information.
Setting the user language preference¶
How you want to set and store the user’s language preference will depend on your application. For example, you can set it in a cookie, in the session, or store it as a user preference.
Django has a built-in set_language
view that you can use with django-ftl -
see the set_language docs.
(It is designed to work with Django’s built-in i18n solution but works just as
well with django-ftl). It saves a user’s language preference into the session
(or a cookie if you are not using sessions), which you can then use later in a
middleware or view, for example.
Auto-reloading¶
By default, django-ftl loads and caches all FTL files on first usage. In development, this can be annoying as changes are not reflected unless you restart the development server. To solve this, django-ftl comes with an auto-reloading mechanism for development mode. To use it, you must install pyinotify:
$ pip install pyinotify
By default, if you have DEBUG = True
in your settings (which is normally the
case for development mode), the reloader will be used and any changes to FTL
files references from bundles will be detected and picked up immediately.
You can also control this manually with your FTL
settings in
settings.py
:
FTL = {
'AUTO_RELOAD_BUNDLES': True
}
Also, you can configure this behavior via the
Bundle
constructor.