Localization

The site is fully localizable. Localization files are not shipped with the code distribution, but are available in separate GitHub repositories. The proper repos 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 data/www-l10n directory, this command will clone the git repo containing the .ftl 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.

Fluent

Bedrock’s Localization (l10n) system is based on Project Fluent. This is a departure from a standard Django project that relies on a gettext work flow of string extraction from template and code files, in that it relies on developers directly editing the default language (English in our case) Fluent files and using the string IDs created there in their templates and views.

The default files for the Fluent system live in the l10n directory in the root of the bedrock project. This directory houses directories for each locale the developers directly implement (mostly simplified English “en”, and “en-US”). The simplified English files are the default fallback for every string ID on the site and should be strings that are plain and easy to understand English, as free from colloquialisms as possible. The translators are able to easily understand the meaning of the string, and can then add their own local flair to the ideas.

Note

Much of this documentation also applies to the use of Fluent with Pocket templates, with the main differences being that:

  • the English Pocket Fluent directory is called l10n-pocket/

  • there are no activation-threshold checks for Pocket templates - we expect all strings to be translated in one go, because they are done via a vendor

.ftl files

When adding translatable strings to the site you start by putting them all into an .ftl file in the l10n/en/ directory with a path that matches or is somehow meaningful for the expected location of the template or view in which they’ll be used. For example, strings for the mozorg/mission.html template would go into the l10n/en/mozorg/mission.ftl file. Locales are activated for a particular .ftl file, not template or URL, so you should use a unique file for most URLs, unless they’re meant to be translated and activated for new locales simultaneously.

You can have shared .ftl files that you can load into any template render, but only the first .ftl file in the list of the ones for a page render will determine whether the page is active for a locale.

Activation of a locale happens automatically once certain rules are met. A developer can mark some string IDs as being “Required”, which means that the file won’t be activated for a locale until that locale has translated all of those required strings. The other rule is a percentage completion rule: a certain percentage (configurable) of the strings IDs in the “en” file must be translated in the file for a locale before it will be marked as active. We’ll get into how exactly this works later.

Translating with .ftl files

The Fluent file syntax is well documented on the Fluent Project’s site. We use “double hash” or “group” comments to indicate strings required for activation. A group comment only ends when another group comment starts however, so you should either group your required strings at the bottom of a file, or also have a “not required” group comment. Here’s an example:

### File for example.html

## Required
example-page-title = The Page Title
# this is a note the applies only to example-page-desc
example-page-desc = This page is a test.

##
example-footer = This string isn't as important

Any group comment (a comment that starts with “##”) that starts with “Required” (case does not matter) will start a required strings block, and any other group comment will end it.

Once you have your strings in your .ftl file you can place them in your template. We’ll use the above .ftl file for a simple Jinja template example:

<!doctype html>
<html>
<head>
    <title>{{ ftl('example-page-title') }}</title>
</head>
<body>
    <h1>{{ ftl('example-page-title') }}</h1>
    <p>{{ ftl('example-page-desc') }}</p>
    <footer>
        <p>{{ ftl('example-footer') }}</p>
    </footer>
</body>
</html>

FTL String IDs

Our convention for string ID creation is the following:

  1. String IDs should be all lower-case alphanumeric characters.

  2. Words should be separated with hyphens.

  3. IDs should be prefixed with the name of the template file (e.g. firefox-new-skyline for firefox-new-skyline.html)

  4. If you need to create a new string for the same place on a page and to transition to it as it is translated, you can add a version suffix to the string ID: e.g. firefox-new-skyline-main-page-title-v2.

  5. The ID should be as descriptive as possible to make sense to the developer, but could be anything as long as it adheres to the rules above.

Using brand names

Common Mozilla brand names are stored in a global brands.ftl file, in the form of terms. Terms are useful for keeping brand names separated from the rest of the translations, so that they can be managed in a consistent way across all translated files, and also updated easily in a global context. In general the brand names in this file remain in English and should not be translated, however locales still have the choice and control to make adjustments should it suit their particular language.

Only our own brands should be managed this way, brands from other companies should not. If you are concerned that the brand is a common word and may be translated, leave a comment for the translators.

Note

We are trying to phase out use of { -brand-name-firefox-browser } please use { -brand-name-firefox } browser.

-brand-name = Firefox

example-about = About { -brand-name }.
example-update-successful = { -brand-name } has been updated.
# "Safari" here refers to the competing web browser
example-compare = How does { -brand-name } compare to Safari?

Important

When adding a new term to brands.ftl, the new term should also be manually added to the mozilla-l10n/www-l10n repo for all locales. The reason for this is that if a term does not exist for a particular locale, then it does not fall back to English like a regular string does. Instead, the term variable name is displayed on the page.

Variables

Single hash comments are applied only to the string immediately following them. They should be used to provide additional context for the translators including:

  1. What the values of variables are.

  2. Context about where string appears on the page if it is not visible or references other elements on the page.

  3. Explanations of English idioms and jargon that may be confusing to non-native speakers.

# Variables:
#   $savings (string) - the percentage saved from the regular price, not including the % Examples: 50, 70
example-bundle-savings = Buy now for { $savings }% off.

# Context: Used as an accessible text alternative for an image
example-bookmark-manager-alt = The bookmark manager window in { -brand-name-firefox }.
# Context: This lists the various websites and magazines who have mentioned Firefox Relay.
# Example: "As seen in: FORBES magainze and LifeHacker"
example-social-proof = As seen in:

example-privacy-on-every = Want privacy on every device?
# "You got it" here is a casual answer to the previous question, "Want privacy on every device?"
example-you-got-it = You got it. Get { -brand-name-firefox } for mobile.
HTML with attributes

When passing HTML tags with attributes into strings for translation, remove as much room for error as possible by putting all the attributes and their values in a single variable. (This is most common with links and their href attributes but we do occasionally pass classes with other tags.)

# Variables:
#   $attrs (attrs) - link to https://www.mozilla.org/about/
example-created = { -brand-name-firefox } was created by <a {$attrs}>{ -brand-name-mozilla }</a>.

# Variables:
#   $class (string) - CSS class used to replace brand name with wordmark logo
example-firefox-relay = Add <span { $class }">{ -brand-name-firefox-relay }</span>
{% set created_attrs = 'href="%s" data-cta-type="link" data-cta-text="created by Mozilla"'|safe|format(url('mozorg.about.index')) %}
<p>{{ ftl('example-created', attrs=created_attrs) }}</p>

{{ ftl('example-firefox-relay', class_name='class="mzp-c-wordmark mzp-t-wordmark-md mzp-t-product-relay"') }}

Obsolete strings

When new strings are added to a page sometimes they update or replace old strings. Obsolete strings & IDs should be removed from ftl files immediately if they are not being used as a fallback. If they are being kept as a fallback they should be removed after 1-2 months.

Fallback

If you need to create a new string for the same place on a page and would like to keep the old one as a fallback, you can add a version suffix to the new string ID: e.g. firefox-new-skyline-main-page-title-v2.

example-block-title-v2 = Security, reliability and speed — on every device, anywhere you go.
# Obsolete string
example-block-title = Security, reliability and speed — from name you can trust.

The ftl helper function has the ability to accept a fallback string ID and is described in the next section.

Remove

If the new string is fundamentally different a new string ID should be created and the old one deleted.

For example, if the page is going from talking about the Google Translate extension to promoting our own Firefox Translate feature the old strings are not appropriate fall backs.

The old strings and IDs should be deleted:

example-translate-title = The To Google Translate extension makes translating the page you’re on easier than ever.
example-translate-content = Google Translate, with over 100 languages* at the ready, is used by millions of people around the world.

The new strings should have different IDs and not be versioned:

example-translate-integrated-title = { -brand-name-firefox } now comes with an integrated translation tool.
example-translate-integrated-content =  Unlike some cloud-based alternatives, { -brand-name-firefox } translates text locally, so the content you’re translating doesn’t leave your machine.

The ftl_has_messages jinja helper would be useful here and is described in the next section.

The ftl helper function

The ftl() function takes a string ID and returns the string in the current language, or simplified english if the string isn’t translated. If you’d like to use a different string ID in the case that the primary one isn’t translated you can specify that like this:

ftl('primary-string-id', fallback='fallback-string-id')

When a fallback is specified it will be used only if the primary isn’t translated in the current locale. English locales (e.g. en-US, en-GB) will never use the fallback and will print the simplified english version of the primary string if not overridden in the more specific locale.

You can also pass in replacement variables into the ftl() function for use with fluent variables. If you had a variable in your fluent file like this:

welcome = Welcome, { $user }!

You could use that in a template like this:

<h2>{{ ftl('welcome', user='Dude') }}<h2>

For our purposes these are mostly useful for things that can change, but which shouldn’t involve retranslation of a string (e.g. URLs or email addresses).

You may also request any other translation of the string (or the original English string of course) regardless of the current locale.

<h2>{{ ftl('welcome', locale='en', user='Dude') }}<h2>

This helper is available in Jinja templates and Python code in views. For use in a view you should always call it in the view itself:

# views.py
from lib.l10n_utils import render
from lib.l10n_utils.fluent import ftl

def about_view(request):
    ftl_files = 'mozorg/about'
    hello_string = ftl('about-hello', ftl_files=ftl_files)
    render(request, 'about.html', {'hello': hello_string}, ftl_files=ftl_files)

If you need to use this string in a view, but define it outside of the view itself, you can use the ftl_lazy variant which will delay evaluation until render time. This is mostly useful for defining messages shared among several views in constants in a views.py or models.py file.

Whether you use this function in a Python view or a Jinja template it will always use the default list of Fluent files defined in the FLUENT_DEFAULT_FILES setting. If you don’t specify any additional Fluent files via the fluent_files keyword argument, then only those default files will be used.

The ftl_has_messages helper function

Another useful template tool is the ftl_has_messages() function. You pass it any number of string IDs and it will return True only if all of those message IDs exist in the current translation. This is useful when you want to add a new block of HTML to a page that is already translated, but don’t want it to appear untranslated on any page.

{% if ftl_has_messages('new-title', 'new-description') %}
  <h3>{{ ftl('new-title') }}</h3>
  <p>{{ ftl('new-description') }}</p>
{% else %}
  <h3>{{ ftl('title') }}</h3>
  <p>{{ ftl('description') }}</p>
{% endif %}

If you’d like to have it return true when any of the given message IDs exist in the translation instead of requiring all of them, you can pass the optional require_all=False parameter and it will do just that.

There is a version of this function for use in views called has_messages. It works exactly the same way but is meant to be used in the view Python code.

# views.py
from lib.l10n_utils import render
from lib.l10n_utils.fluent import ftl, has_messages

def about_view(request):
    ftl_files = 'mozorg/about'
    if has_messages('about-hello-v2', 'about-title-v2',
                    ftl_files=ftl_files):
        hello_string = ftl('about-hello-v2', ftl_files=ftl_files)
        title_string = ftl('about-title-v2', ftl_files=ftl_files)
    else:
        hello_string = ftl('about-hello', ftl_files=ftl_files)
        title_string = ftl('about-title', ftl_files=ftl_files)

    render(request, 'about.html', {'hello': hello_string, 'title': title_string}, ftl_files=ftl_files)

Specifying Fluent files

You have to tell the system which Fluent files to use for a particular template or view. This is done in either the page() helper in a urls.py file, or in the call to l10n_utils.render() in a view.

Using the page() function

If you just need to render a template, which is quite common for bedrock, you will probably just add a line like the following to your urls.py file:

urlpatterns = [
    page('about', 'about.html'),
    page('about/contact', 'about/contact.html'),
]

To tell this page to use the Fluent framework for l10n you just need to tell it which file(s) to use:

urlpatterns = [
    page('about', 'about.html', ftl_files='mozorg/about'),
    page('about/contact', 'about/contact.html', ftl_files=['mozorg/about/contact', 'mozorg/about']),
]

The system uses the first (or only) file in the list to determine which locales are active for that URL. You can pass a string or list of strings to the ftl_files argument. The files you specify can include the .ftl extension or not, and they will be combined with the list of default files which contain strings for global elements like navigation and footer. There will also be files for reusable widgets like the newsletter form, but those should always come last in the list.

Using the class-based view

Bedrock includes a generic class-based view (CBV) that sets up l10n for you. If you need to do anything fancier than just render the page, then you can use this:

from lib.l10n_utils import L10nTemplateView

class AboutView(L10nTemplateView):
    template_name = 'about.html'
    ftl_files = 'mozorg/about'

Using that CBV will do the right things for l10n, and then you can override other useful methods (e.g. get_context_data) to do what you need. Also, if you do need to do anything fancy with the context, and you find that you need to dynamically set the fluent files list, you can easily do so by setting ftl_files in the context instead of the class attribute.

from lib.l10n_utils import L10nTemplateView

class AboutView(L10nTemplateView):
    template_name = 'about.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ftl_files = ['mozorg/about']
        if request.GET.get('fancy'):
            ftl_files.append('fancy')

        ctx['ftl_files'] = ftl_files
        return ctx

A common case is needing to use FTL files when one template is used, but not with another. In this case you would have some logic to decide which template to use in the get_template_names() method. You can set the ftl_files_map class variable to a dict containing a map of template names to the list of FTL files for that template (or a single file name if that’s all you need).

# views.py
from lib.l10n_utils import L10nTemplateView

# class-based view example
class AboutView(L10nTemplateView):
    ftl_files_map = {
        'about_es.html': ['about_es']
        'about_new.html': ['about']
    }

    def get_template_names(self):
        if self.request.locale.startswith('en'):
            template_name = 'about_new.html'
        elif self.request.locale.startswith('es'):
            template_name = 'about_es.html'
        else:
            # FTL system not used
            template_name = 'about.html'

        return [template_name]

If you need for your URL to use multiple Fluent files to determine the full list of active locales, for example when you are redesigning a page and have multiple templates in use for a single URL depending on locale, you can use the activation_files parameter. This should be a list of FTL filenames that should all be used when determining the full list of translations for the URL. Bedrock will gather the full list for each file and combine them into a single list so that the footer language switcher works properly.

Another common case is that you want to keep using an old template for locales that haven’t yet translated the strings for a new one. In that case you can provide an old_template_name to the class and include both that template and template_name in the ftl_files_map. Once you do this the view will use the template in template_name only for requests for an active locale for the FTL files you provided in the map.

from lib.l10n_utils import L10nTemplateView

class AboutView(L10nTemplateView):
    template_name = 'about_new.html'
    old_template_name = "about.html"
    ftl_files_map = {
        "about_new.html": ["about_new", "about_shared"],
        "about.html": ["about", "about_shared"],
    }

In this example when the about_new FTL file is active for a locale, the about_new.html template will be rendered. Otherwise the about.html template would be used.

Using in a view function

Lastly there’s the good old function views. These should use l10n_utils.render directly to render the template with the context. You can use the ftl_files argument with this function as well.

from lib.l10n_utils import render

def about_view(request):
    render(request, 'about.html', {'name': 'Duder'}, ftl_files='mozorg/about')

Fluent File Configuration

In order for a Fluent file to be extracted through automation and sent out for localization, it must first be configured to go through one or more distinct pipelines. This is controlled via a set of configuration files:

  • Vendor, locales translated by an agency, and paid for by Marketing (locales covered by staff are also included in this group).

  • Pontoon, locales translated by Mozilla contributors.

  • Special templates, for locales with dedicated templates that don’t go through the localization process (not currently used).

Each configuration file consists of a pre-defined set of locales for which each group is responsible for translating. The locales defined in each file should not be changed without first consulting the with L10n team, and such changes should not be a regular occurrence.

To establish a localization strategy for a Fluent file, it needs to be included as a path in one or more configuration files. For example:

[[paths]]
    reference = "en/mozorg/mission.ftl"
    l10n = "{locale}/mozorg/mission.ftl"

You can read more about configuration files in the L10n Project Configuration docs.

Important

Path definitions in Fluent configuration files are not source order dependent. A broad definition using a wild card can invalidate all previous path definitions for example. Paths should be defined carefully to avoid exposing .ftl files to unintended locales.

Using a combination of vendor and pontoon configuration offers a flexible but specific set of options to choose from when it comes to defining an l10n strategy for a page. The available choices are:

  1. Staff locales.

  2. Staff + select vendor locales.

  3. Staff + all vendor locales.

  4. Staff + vendor + pontoon.

  5. All pontoon locales (for non-marketing content only).

When choosing an option, it’s important to consider that vendor locales have a cost associated with them, and pontoon leans on the goodwill of our volunteer community. Typically, only non-marketing content should go through Pontoon for all locales. Everything that is marketing related should feature one of the staff/vendor/pontoon configurations.

Fluent File Activation

Fluent files are activated automatically when processed from the l10n team’s repo into our own based on a couple of rules.

  1. If a fluent file has a group of required strings, all of those strings must be present in the translation in order for it to be activated.

  2. A translation must contain a minimum percent of the string IDs from the English file to be activated.

If both of these conditions are met the locale is activated for that particular Fluent file. Any view using that file as its primary (first in the list) file will be available in that locale.

Deactivation

If the automated system activates a locale but we for some reason need to ensure that this page remains unavailable in that locale, we can add this locale to a list of deactivated locales in the metadata file for that FTL file. For example, say we needed to make sure that the mozorg/mission.ftl file remained inactive for German, even though the translation is already done. We would add de to the inactive_locales list in the metadata/mozorg/mission.json file:

{
  "active_locales": [
    "de",
    "fr",
    "en-GB",
    "en-US",
  ],
  "inactive_locales": [
    "de"
  ],
  "percent_required": 85
}

This would ensure that even though de appears in both lists, it will remain deactivated on the site. We could just remove it from the active list, but automation would keep attempting to add it back, so for now this is the best solution we have, and is an indication of the full list of locales that have satisfied the rules.

Alternate Rules

It’s also possible to change the percentage of string completion required for activation on a per-file basis. In the same metadata file as above, if a percent_required key exists in the JSON data (see above) it will be used as the minimum percent of string completion required for that file in order to activate new locales.

Note

Once a locale is activated for a Fluent file it will NOT be automatically deactivated, even if the rules change. If you need to deactivate a locale you should follow the Deactivation instructions.

Activation Status

You can determine and use the activation status of a Fluent file in a view to make some decisions; what template to render for example. The way you would do that is with the ftl_file_is_active function. For example:

# views.py
from lib.l10n_utils import L10nTemplateView
from lib.l10n_utils.fluent import ftl_file_is_active

# class-based view example
class AboutView(L10nTemplateView):
    ftl_files_map = {
        'about.html': ['about']
        'about_new.html': ['about_new', 'about']
    }
    def get_template_names(self):
        if ftl_file_is_active('mozorg/about_new'):
            template_name = 'about_new.html'
        else:
            template_name = 'about.html'

        return [template_name]

# function view example
def about_view(request):
    if ftl_file_is_active('mozorg/about_new'):
        template = 'mozorg/about_new.html'
        ftl_files = ['mozorg/about_new', 'mozorg/about']
    else:
        template = 'about.html'
        ftl_files = ['mozorg/about']

    render(request, template, ftl_files=ftl_files)

Active Locales

To see which locales are active for a particular .ftl file you can either look in the metadata file for that .ftl file, which is the one with the same path but in the metadata folder instead of a locale folder in the www-l10n repository. Or if you’d like something a bit nicer looking and more convenient there is the active_locales management command:

$ ./manage.py l10n_update
$ ./manage.py active_locales mozorg/mission
There are 91 active locales for mozorg/mission.ftl:
- af
- an
- ar
- ast
- az
- be
- bg
- bn
...

You get an alphabetically sorted list of all of the active locales for that .ftl file. You should run ./manage.py l10n_update as shown above for the most accurate and up-to-date results.

String extraction

The string extraction process for both new .ftl content and updates to existing .ftl content is handled through automation. On each commit to main a command is run that looks for changes to the l10n/ and l10n-pocket/ directories. If a change is detected, it will copy those files into a new branch in mozilla-l10n/www-l10n and then a bot will open a pull request containing those changes. Once the pull request has been reviewed and merged by the L10n team, everything is done.

To view the state of the latest automated attempt to open an L10N PR, see:

(We also just try to open L10N PRs every 3 hours, to catch any failed jobs that are triggered by a commit to main)

CSS

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.

All

Locale-specific Templates

While the ftl_has_messages 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.

Note

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.

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.

Adding new L10N integrations

Bedrock, as a platform, can operate in different modes, and it is possible (necessary, even) to support multiple L10N pipelines, so that each mode of operation can have its own distinct Fluent files and translation strategy.

As of Summer 2022, there are two separate L10N integrations within Bedrock:

  • Mozilla.org (“Mozorg mode”)

  • Pocket Marketing Pages (“Pocket mode”)

These integrations are similar in their approach, but not identical in how they run. They use different translations strategies, which requires slightly different data flows.

Moving L10N data (essentially Fluent .ftl files) happens via various automation steps, which aren’t captured here, as they are more about infrastructure and operations. However, what follows outlines the steps needed to add a new L10N integration (for “newintegration”) to Bedrock.

  1. FILE SETUP (Bedrock developer)

Add a directory for the source (en) Fluent strings that will need translation.

Note

For source Fluent files currently…

  • …Mozorg uses ./l10n/

  • …Pocket uses ./l10n-pocket/

Add the following files:

./l10n-newintegration/
./l10n-newintegration/en/  # This is where source Fluent templates go for 'newintegration'
./l10n-newintegration/en/configs/pontoon.toml  # If using community/Pontoon translations at all
./l10n-newintegration/en/configs/vendor.toml  # If using a paid-for translation service such as Smartling
./l10n-newintegration/en/configs/special-templates.toml   # Only needed to exclude certain files from all community AND vendor translation. e.g. we use staff translation only.

./l10n-newintegration/l10n-pontoon.toml  # If using community/Pontoon translations at all
./l10n-newintegration/l10n-vendor.toml  # If using a paid-for translation service such as Smartling

./data/l10n-newintegration-team/  # leave this empty - it will be populated via a git sync using data FROM the l10n team

For the exact content of each .toml or .json file, see the examples in ./l10n/ and ./l10n-pocket/ for inspiration - they’re not too hard to work out. The .toml files outside of /en/ basically point to the ones in /en/configs/ and are a ‘gateway’ through which we spec which config files are relevant to which translation stragegy (community or vendor - or neither if it’s staff-only translation).

  1. REPO SETUP (Bedrock and/or L10N team admin)

You will need to set up one or two new repos, to hold the translation files as part of the pipeline.

i. A repo in where the files are sent to in https://github.com/mozilla-l10n/ for the L10N team’s automation to pick up.

For example, Mozorg uses github.com/mozilla-l10n/www-l10n/ and Pocket uses github.com/mozilla-l10n/www-pocket-l10n/. Your new github.com/mozilla-l10n/www-newintegration-l10n/ repo will be needed regardless of who does the actual translation work.

  1. An optional repo where files are post-processed following translation.

If relevant, this will live in github.com/mozmeao/ - for example github.com/mozmeao/www-newintegration-l10n/

Important

If you are not using Pontoon/community translations, you do NOT need to create this repo. Why? If the translations are done by the community (via Pontoon), there is a possibility that not enough of the strings will be translated in order to render the content in the relevant locale. We run a CI task to determine whether a locale has enough translated strings to be considered ‘active’. At the moment, only Mozorg uses this pattern. The Pocket-mode translations do not have their activation measured because their translations come entirely from a vendor and we expect the Pocket strings to be 100% translated.

  1. CI SETUP (Bedrock dev)

Only relevant if using Pontoon community translations. Details of how MozMarRobot is hooked are best gleaned from looking at https://gitlab.com/mozmeao/www-fluent-update.

In short, once new translations land in the string-source repo (e.g. github.com/mozilla-l10n/www-newintegration-l10n) they are cloned over to the activation-check repo github.com/mozmeao/www-newintegration-l10n/ by CI and later pulled into Bedrock from there.

  1. CONFIGURE SETTINGS (Bedrock dev).

You’ll also have to update settings so that when the site is in ‘newintegration’ mode, it knows which L10N-related local folders and remote repos to use. Look in settings/__init__.py to see what we did for Pocket mode.

You’ll also have to set up new env vars to provide the new repo and filepath settings’ values, which will mean updating github.com/mozmeao/www-config/ and possibly getting new secrets provisioned in Kubernetes if you need to use a separate auth token for github.com/mozilla-l10n/. (You may not.)

Note that if you are not using community/Pontoon translations - and therefore you don’t need to use an intermediary repo to calculate activation status - you can just use the mozilla-l10n/www-newintegration-l10n repo for both outbound and inbound translations - look at the Pocket Mode setting for an example of this.

  1. EXPAND L10N UPDATE SCRIPT (Bedrock dev).

Uploading strings for translation

Uploading `en-locale source strings from Bedrock to the github.com/mozilla-l10n/ repos is handled by bedrock/bin/open-ftl-pr.sh. This file requires no specific code changes to support a new integration as long as you have already set up a SITE_MODE for ‘newintegration’.

However, you do need to add a new entry to bedrock/.gitlab-ci.yml – copy the update-l10n step, in a similar way to how it’s been duplicated for update-pocket-l10n.

Downloading translated strings

Update the configuration dict at the top of bedrock/lib/l10n_utils/management/commands/l10n_update.py so that when that management command is run, it will pull down the appropriate translations for “newintegration”.

Tip: to test drive things, you can fork the real repos and test against your forks by specifying them via local env vars.

  1. VENDOR SETUP (L10N Team)

The vendor (e.g. Smartling) will need to add the new string-source repo (github.com/mozilla-l10n/www-newintegration-l10n) to its configuration. Once this is done new translations from the vendor will be added to that repo, and synced down to Bedrock. This step is out of our hands, but the vendor’s technical contact should be able to make it happen.

  1. PONTOON SETUP (L10N Team)

Details to come for setting up community translations using Pontoon. (Contributions about this aspect are welcome!)