The site is fully localizable. Localization files are not shipped with the code distribution, but are available in a separate GitHub repository. The proper repo can be cloned and kept up-to-date using the l10n_update management command:

$ ./manage.py l10n_update

If you don’t already have a locale directory, this command will clone the git repo containing the translation files (either the dev or prod files depending on your DEV setting). If the folder is already present, it will update the repository to the latest version.

.lang files

Bedrock supports a workflow similar to gettext. You extract all the strings from the codebase, then merge them into each locale to get them translated.

The files containing the strings are called “.lang files” and end with a .lang extension.

To extract all the strings from the codebase, run:

$ ./manage.py l10n_extract

If you’d only like to extract strings from certain files, you may optionally list them on the command line:

$ ./manage.py l10n_extract bedrock/mozorg/templates/mozorg/contribute.html

Command line glob matching will work as well if you want all of the HTML files in a directory, for example:

$ ./manage.py l10n_extract bedrock/mozorg/templates/mozorg/*.html

That will use gettext to get all the needed localizations from Python and HTML files, and will convert the result into a series of .lang files inside locale/templates. This directory represents the “reference” set of strings to be translated, and you are free to modify or split up .lang files here as needed (just make sure they are being referenced correctly, from the code, see Which .lang file should it use?).

Once you have extracted .lang files locally, they can then be added via pull request to the mozilla.org l10n repository for translation. You can read the full documentation for more information on the extraction workflow.

Translating with .lang files

To translate a string from a .lang file, simply use the gettext interface.

In a jinja2 template:

<div>{{ _('Hello, how are you?') }}<div>

<div>{{ _('<a href="%s">Click here</a>')|format('http://mozilla.org/') }}</div>

<div>{{ _('<a href="%(url)s">Click here</a>')|format(url='http://mozilla.org/') }}</div>

Note the usage of variable substitution in the latter examples. It is important not to hardcode URLs or other parameters in the string. jinja’s format filter lets us apply variables outsite of the string.

You can provide a one-line comment to the translators like this:

{# L10n: "like" as in "similar to", not "is fond of" #}
{{ _('Like this:') }}

The comment will be included in the .lang files above the string to be translated.

In a Python file, use lib.l10n_utils.dotlang._ or lib.l10n_utils.dotlang._lazy, like this:

from lib.l10n_utils.dotlang import _lazy as _

sometext = _('Foo about bar.')

You can provide a one-line comment to the translators like this:

# L10n: "like" as in "similar to", not "is fond of"
sometext = _('Like this:')

The comment will be included in the .lang files above the string to be translated.

There’s another way to translate content within jinja2 templates. If you need a big chunk of content translated, you can put it all inside a trans block.

{% trans %}
  <div>Hello, how are you</div>
{% endtrans %}

{% trans url='http://mozilla.org' %}
  <div><a href="{{ url }}">Click here</a></div>
{% endtrans %}

Note that it also allows variable substitution by passing variables into the block and using template variables to apply them.

Which .lang file should it use?

Translated strings are split across several .lang files to make it easier to manage separate projects and pages. So how does the system know which one to use when translating a particular string?

  • All translations from Python files are put into main.lang. This should be a very limited set of strings and most likely should be available to all pages.
  • Templates always load main.lang and download_button.lang.
  • Additionally, each template has its own .lang file, so a template at mozorg/firefox.html would use the .lang file at <locale>/mozorg/firefox.lang.
  • Templates can override which .lang files are loaded. The above global ones are always loaded, but instead of loading <locale>/mozorg/firefox.lang, the template can specify a list of additional lang files to load with a template block:
{% add_lang_files "foo" "bar" %}

That will make the page load foo.lang and bar.lang in addition to main.lang and download_button.lang.

When strings are extracted from a template, they are added to the template-specific .lang file. If the template explicitly specifies .lang files like above, it will add the strings to the first .lang file specified, so extracted strings from the above template would go into foo.lang.

You can similarly specify extra .lang files in your Python source as well. Simply add a module-level constant in the file named LANG_FILES. The value should be either a string, or a list of strings, similar to the add_lang_files tag above.

# forms.py

from lib.l10n_utils.dotlang import _

LANG_FILES = ['foo', 'bar']

sometext = _('Foo about bar.')

This file’s strings would be extracted to foo.lang, and the lang files foo.lang, bar.lang, main.lang and download_button.lang would be searched for matches in that order.

l10n blocks

Bedrock also has a block-based translation system that works like the {% block %} template tag, and marks large sections of translatable content. This should not be used very often; lang files are the preferred way to translate content. However, there may be times when you want to control a large section of a page and customize it without caring very much about future updates to the English page.

A Localizers’ guide to l10n blocks

Let’s look at how we would translate an example file from English to German.

The English source template, created by a developer, lives under apps/appname/templates/appname/example.html and looks like this:

{% extends "base-pebbles.html" %}

{% block content %}
  <img src="someimage.jpg">

  {% l10n foo, 20110801 %}
  <h1>Hello world!</h1>
  {% endl10n %}


  {% l10n bar, 20110801 %}
  <p>This is an example!</p>
  {% endl10n %}
{% endblock %}

The l10n blocks mark content that should be localized. Realistically, the content in these blocks would be much larger. For a short string like above, please use lang files. We’ll use this trivial code for our example though.

The l10n blocks are named and tagged with a date (in ISO format). The date indicates the time that this content was updated and needs to be translated. If you are changing trivial things, you shouldn’t update it. The point of l10n blocks is that localizers completely customize the content, so they don’t care about small updates. However, you may add something important that needs to be added in the localized blocks; hence, you should update the date in that case.

When the command ./manage.py l10n_extract is run, it generates the corresponding files in the locale folder (see below for more info on this command).

The German version of this template is created at locale/de/templates/appname/example.html. The contents of it are:

{% extends "appname/example.html" %}

{% l10n foo %}
<h1>Hello world!</h1>
{% endl10n %}

{% l10n bar %}
<p>This is an example!</p>
{% endl10n %}

This file is an actual template for the site. It extends the main template and contains a list of l10n blocks which override the content on the page.

The localizer just needs to translate the content in the l10n blocks.

When the reference template is updated with new content and the date is updated on an l10n block, the generated l10n file will simply add the new content. It will look like this:

{% extends "appname/example.html" %}

{% l10n foo %}
<h1>This is an English string that needs translating.</h1>
{% was %}
<h1>Dies ist ein English string wurde nicht.</h1>
{% endl10n %}

{% l10n bar %}
<p>This is an example!</p>
{% endl10n %}

Note the was block in foo. The old translated content is in there, and the new content is above it. The was content is always shown on the site, so the old translation still shows up. The localizer needs to update the translated content and remove the was block.

Generating the locale files

$ ./manage.py l10n_check

This command will check which blocks need to be translated and update the locale templates with needed translations. It will copy the English blocks into the locale files if a translation is needed.

You can specify a list of locales to update:

$ ./manage.py l10n_check fr
$ ./manage.py l10n_check fr de es


When dealing with currency, make a separate gettext wrapper, placing the amount inside a variable. You should also include a comment describing the intent. For example:

{# L10n: Inserts a sum in US dollars, e.g. '$100'. Adapt the string in your translation for your locale conventions if needed, ex: %(sum)s US$ #}
{{ _('$%(sum)s')|format(sum='15') }}


If a localized page needs some locale-specific style tweaks, you can add the style rules to the page’s stylesheet like this:

html[lang="it"] #features li {
  font-size: 20px;

html[dir="rtl"] #features {
  float: right;

If a locale needs site-wide style tweaks, font settings in particular, you can add the rules to /media/css/l10n/{{LANG}}/intl.css. Pages on Bedrock automatically includes the CSS in the base templates with the l10n_css helper function. The CSS may also be loaded directly from other Mozilla sites with such a URL: //mozorg.cdn.mozilla.net/media/css/l10n/{{LANG}}/intl.css.

Open Sans, the default font on mozilla.org, doesn’t offer non-Latin glyphs. intl.css can have @font-face rules to define locale-specific fonts using custom font families as below:

  • X-LocaleSpecific-Light: Used in combination with Open Sans Light. The font can come in 2 weights: normal and optionally bold
  • X-LocaleSpecific: Used in combination with Open Sans Regular. The font can come in 2 weights: normal and optionally bold
  • X-LocaleSpecific-Extrabold: Used in combination with Open Sans Extrabold. The font weight is 800 only

Here’s an example of intl.css:

@font-face {
  font-family: X-LocaleSpecific-Light;
  font-weight: normal;
  font-display: swap;
  src: local(mplus-2p-light), local(Meiryo);

@font-face {
  font-family: X-LocaleSpecific-Light;
  font-weight: bold;
  font-display: swap;
  src: local(mplus-2p-medium), local(Meiryo-Bold);

@font-face {
  font-family: X-LocaleSpecific;
  font-weight: normal;
  font-display: swap;
  src: local(mplus-2p-regular), local(Meiryo);

@font-face {
  font-family: X-LocaleSpecific;
  font-weight: bold;
  font-display: swap;
  src: local(mplus-2p-bold), local(Meiryo-Bold);

@font-face {
  font-family: X-LocaleSpecific-Extrabold;
  font-weight: 800;
  font-display: swap;
  src: local(mplus-2p-black), local(Meiryo-Bold);

Localizers can specify locale-specific fonts in one of the following ways:

  • Choose best-looking fonts widely used on major platforms, and specify those with the src: local(name) syntax
  • Find a best-looking free Web font, add the font files to /media/fonts/, and specify those with the src: url(path) syntax
  • Create a custom Web font to complement missing glyphs in Open Sans, add the font files to /media/fonts/l10n/, and specify those with the src: url(path) syntax. M+ 2c offers various international glyphs and looks similar to Open Sans, while Noto Sans is good for the bold and italic variants. You can create subsets of these alternative fonts in the WOFF and WOFF2 formats using a tool found on the Web. See Bug 1360812 for the Fulah (ff) locale’s example

Developers should use the .open-sans mixin instead of font-family: 'Open Sans' to specify the default font family in CSS. This mixin has both Open Sans and X-LocaleSpecific so locale-specific fonts, if defined, will be applied to localized pages. The variant mixins, .open-sans-light and .open-sans-extrabold, are also available.

Staging Copy Changes

The need will often arise to push a copy change to production before the new copy has been translated for all locales. To prevent locales not yet translated from displaying English text, you can use the l10n_has_tag template function. For example, if the string “Firefox benefits” needs to be changed to “Firefox features”:

{% if l10n_has_tag('firefox_products_headline_spring_2016') %}
  <h1>{{ _('Firefox features') }}</h1>
{% else %}
  <h1>{{ _('Firefox benefits') }}</h1>
{% endif %}

This function will check the .lang file(s) of the current page for the tag firefox_products_headline_spring_2016. If it exists, the translation for “Firefox features” will be displayed. If not, the pre-existing translation for “Firefox benefits” will be displayed.

When using l10n_has_tag, be sure to coordinate with the localization team to decide on a good tag name. Always use underscores instead of hyphens if you need to visually separate words.

Locale-specific Templates

While the l10n_has_tag template function is great in small doses, it doesn’t scale particularly well. A template filled with conditional copy can be difficult to comprehend, particularly when the conditional copy has associated CSS and/or JavaScript.

In instances where a large amount of a template’s copy needs to be changed, or when a template has messaging targeting one particular locale, creating a locale-specific template may be a good choice.

Locale-specific templates function simply by naming convention. For example, to create a version of /firefox/new.html specifically for the de locale, you would create a new template named /firefox/new.de.html. This template can either extend /firefox/new.html and override only certain blocks, or be entirely unique.

When a request is made for a particular page, bedrock’s rendering function automatically checks for a locale-specific template, and, if one exists, will render it instead of the originally specified (locale-agnostic) template.


Creating a locale-specific template for en-US was not possible when this feature was introduced, but it is now. So you can create your en-US-only template and the rest of the locales will continue to use the default.


Note that the presence of an L10n template (e.g. locale/de/templates/firefox/new.html) will take precedence over a locale-specific template in bedrock.

Specifying Active Locales in Views

Normally we rely on activation tags in our translation files (.lang files) to determine in which languages a page will be available. This will almost always be what we want for a page. But sometimes we need to explicitly state the locales available for a page. The impressum page for example is only available in German and the template itself has German hard-coded into it since we don’t need it to be translated into any other languages. In cases like these we can send a list of locale codes with the template context and it will be the final list. This can be accomplished in a few ways depending on how the view is coded.

For a plain view function, you can simply pass a list of locale codes to l10n_utils.render in the context using the name active_locales. This will be the full list of available translations. Use add_active_locales if you want to add languages to the existing list:

def french_and_german_only(request):
    return l10n_utils.render(request, 'home.html', {'active_locales': ['de', 'fr'])

If you don’t need a custom view and are just using the page() helper function in your urls.py file, then you can similarly pass in a list:

page('about', 'about.html', active_locales=['en-US', 'es-ES']),

Or if your view is even more fancy and you’re using a Class-Based-View that inherits from LangFilesMixin (which it must if you want it to be translated) then you can specify the list as part of the view Class definition:

class MyView(LangFilesMixin, View):
    active_locales = ['zh-CN', 'hi-IN']

Or in the urls.py when using a CBV:

url(r'about/$', MyView.as_view(active_locales=['de', 'fr'])),

The main thing to keep in mind is that if you specify active_locales that will be the full list of localizations available for that page. If you’d like to add to the existing list of locales generated from the lang files then you can use the add_active_locales name in all of the same ways as active_locales above. It’s a list of locale codes that will be added to the list already available. This is useful in situations where we would have needed the l10n team to create an empty .lang file with an active tag in it because we have a locale-specific-template with text in the language hard-coded into the template and therefore do not otherwise need a .lang file.


In local development environments and on demo servers all l10n_has_tag calls evaluate to true. If the content has not been translated it will display the English strings.

To test l10n locally you can set DEV=False in your .env file.

If you are running your local server you will need to restart it after altering your .env file.