Fluent for Firefox Developers

This tutorial is intended for Firefox engineers already familiar with the previous localization systems offered by Gecko - DTD and StringBundle - and assumes prior experience with those systems.

Using Fluent in Gecko

Fluent is a modern localization system currently being progressively introduced into the Gecko platform with a focus on quality, performance, maintenance and completeness.

In order to ensure that Fluent is ready for engineers to work with, the initial migrations are performed manually with a lot of oversight from the involved stakeholders.

In this initial phase, Firefox Preferences is being migrated as the first target and as a result, the first bindings to be stabilized are for chrome-privileged XUL context.

From there we plan to focus on two areas:

The end goal is replacing all uses of DTD and StringBundle within Firefox’s codebase.

If you want to use Fluent and your code involves one of the areas currently unsupported, we’d like to work with you on getting Fluent ready for your code.

Getting a Review

If you end up working on any patch which touches FTL files, we have a temporary hook in place that will reject your patch unless you get an r+ from one of the following L10n Drivers:

  • Francesco Lodolo (:flod)
  • Zibi Braniecki (:gandalf)
  • Axel Hecht (:pike)
  • Stas Malolepszy (:stas)

Major Benefits

Not only was the previous system designed over 20 years ago using file formats never intended for localization, but also the Web stack which Fluent ties into has completely changed over the same period, and the domain of internationalization got a powerful foundation in the form of Unicode, CLDR and ICU which Fluent tightly interoperates with.

While it is beyond the scope of this document to cover all the benefits of Fluent in detail, below is an attempt to select some most observable changes for each group of consumers.


  • Support for XUL, XHTML, HTML, Web Components, React, JS, Python and Rust
  • Strings are available in a single, unified localization context available for both DOM and runtime code
  • Full internationalization (i18n) support: date and time formatting, number formatting, plurals, genders etc.
  • Strong focus on declarative API via DOM attributes
  • Extensible with custom formatters, Mozilla-specific APIs etc.
  • Separation of concerns: localization details, and the added complexity of some languages, don’t leak onto the source code and are no concern for developers
  • Compound messages link a single translation unit to a single UI element
  • DOM Overlays allow for localization of DOM fragments
  • Simplified build system model
  • No need for pre-processing instructions

Product Quality

  • A robust, multilevel, error fallback system prevents XML errors and runtime errors
  • Simplified l10n API reduces the amount of l10n specific code and resulting bugs
  • Runtime localization allows for dynamic language changes and updates over-the-air
  • DOM Overlays increase localization security

Many other smaller improvements will be noticed by the users of the system over time and, with the new foundation, the Fluent team is currently working on multiple highly requested features which will further improve the experience of developing localizable UIs.

Fluent Translation List - FTL

Fluent introduces a new localization format designed specifically for easy readability and localization features offered by the system.

At first glance the format resembles .properties file. It may look like this:

home-page-header = Home Page

# The label of a button opening a new tab
new-tab-open = Open New Tab

But the FTL file format is significantly more powerful and the additional features quickly add up. In order to familiarize yourself with the basic features, consider reading through the Fluent Syntax Guide to understand a more complex example like:

### These messages correspond to security and privacy user interface.
### Please, choose simple and non-threatening language when localizing
### to help user feel in control when interacting with the UI.

## General Section

-brand-short-name = Firefox
    .gender = masculine

pref-pane =
    .title =
        { PLATFORM() ->
            [windows] Options
           *[other] Preferences
    .accesskey = C

# Variables:
#   $tabCount (Number) - number of container tabs to be closed
containers-disable-alert-ok-button =
    { $tabCount ->
        [one] Close { $tabCount } Container Tab
       *[other] Close { $tabCount } Container Tabs

update-application-info =
    You are using { -brand-short-name } Version: { $version }.
    <span>Please, read the <a>privacy policy</a>.</span>

The above, of course, is a particular selection of complex strings intended to exemplify the new features and concepts introduced by Fluent.

In order to ensure the quality of the output, a lot of new checks and tooling has been added to the build system. Pontoon, the main localization tool used to translate Firefox, has been rebuilding its user experience to support localizers in their work.

Social Contract

Fluent uses the concept of a social contract between developer and localizers. This contract is established by the selection of a unique identifier, called l10n-id, which carries a promise of being used in a particular place to carry a particular meaning.

The use of unique identifiers is not new for Firefox engineers, but it is important to recognize that Fluent formalizes this relationship.


An important part of the contract is that the developer commits to treat the localization output as opaque. That means that no concatenations, replacements or splitting should happen after the translation is completed to generate the desired output.

In return, localizers enter the social contract by promising to provide an accurate and clean translation of the messages that match the request.

In previous localization systems, developers were responsible for differentiating string variants based on a platform via pre-processing instructions, or selecting which strings should be formatted using PluralForms.jsm.

In Fluent, the developer is not to be bothered with inner logic and complexity that the localization will use to construct the response. Whether declensions or other variant selection techniques are used is up to a localizer and their particular translation. From the developer perspective, Fluent returns a final string to be presented to the user, with no l10n logic required in the running code.

Markup Localization

Fluent fully replaces the use of DTD in localization.

To localize an element in Fluent, the developer adds a new message to an FTL file and then has to associate an l10n-id with the element by defining a data-l10n-id attribute:

<h1 data-l10n-id="home-page-header" />

<button data-l10n-id="pref-pane" />

Fluent will take care of the rest, populating the element with the message value in its content and all localizable attributes if defined.

The difference compared to the use of DTD is that the developer provides only a single message to localize the whole element, rather than a separate entity for the value and each of the attributes.

The other change is that the developer can localize a whole fragment of DOM:

<p data-l10n-id="update-application-info" data-l10n-args="{'version': '60.0'}">
  <span class="bold">
    <a href="http://www.mozilla.org/privacy" />
-brand-short-name = Firefox
update-application-info =
    You are using { -brand-short-name } Version: { $version }.
    <span>Please, read the <a>privacy policy</a>.</span>

Fluent will overlay the translation onto the source fragment preserving attributes like class and href from the source and adding translations for the elements inside. The resulting localized content will look like this:

<p data-l10n-id="update-application-info" data-l10n-args="{'version': '60.0'}">
  You are using Firefox Version: 60.0.
  <span class="bold">
    Please, read the <a href="http://www.mozilla.org/privacy">privacy policy</a>.

This operation is sanitized, and Fluent takes care of selecting which elements and attributes can be safely provided by the localization. The list of allowed elements and attributes is maintained by the W3C, and if the developer needs to allow for localization of additional attributes, they can whitelist them using data-l10n-attrs list:

<label data-l10n-id="search-input" data-l10n-attrs="style" />

The above example adds an attribute style to be allowed on this particular label element.

External Arguments

Notice in the previous example the attribute data-l10n-args, which is a JSON object storing variables exposed by the developer to the localizer.

This is the main channel for the developer to provide additional variables to be used in the localization.

Arguments are rarely needed for situations where it’s currently possible to use DTD, since such variables would need to be computed from the code at runtime. It’s worth noting that, when the l10n-args are set in the runtime code, they are in fact encoded as JSON and stored together with l10n-id as an attribute of the element.

Runtime Localization

Fluent fully replaces the use of StringBundle in localization.

In almost every case the JS runtime code will operate on a particular document, either XUL, XHTML or HTML.

If the document has its markup already localized, then Fluent exposes a new attribute on the document element - document.l10n.

This property is an object of type DOMLocalization which maintains the main localization context for this document and exposes it to runtime code as well.

With a focus on declarative localization, the primary method of localization is to alter the localization attributes in the DOM. Fluent provides a method to facilitate this:

document.l10n.setAttributes(element, "new-panel-header");

This will set the data-l10n-id on the element and translate it before the next animation frame.

The reason to use this API over manually setting the attribute is that it also facilitates encoding l10n arguments as JSON:

document.l10n.setAttributes(element "containers-disable-alert-ok-button", {
  tabCount: 5

Non-Markup Localization

In rare cases, when the runtime code needs to retrieve the translation and not apply it onto the DOM, Fluent provides an API to retrieve it:

let [ msg ] = await document.l10n.formatValues([


This model is heavily discouraged and should be used only in cases where the DOM annotation is not possible.


This API is currently only available as asynchronous. In case of Firefox, the only non-DOM localizable calls are used where the output goes to a third-party like Bluetooth, Notifications etc. All those cases should already be asynchronous.


The majority of internationalization issues are implicitly handled by Fluent without any additional requirement. Full Unicode support, bidirectionality, and correct number formatting work without any action required from either developer or localizer.

document.l10n.setAttributes(element, "welcome-message", {
  userName: "اليسع",
  count: 5

A message like this localized to American English will correctly wrap the user name in directionality marks allowing the layout engine to determine how to display the bidirectional text.

On the other hand, the same message localized to Arabic will use the Eastern Arabic numeral for number “5”.

Plural Rules

The most common localization feature is the ability to provide different variants of the same string depending on plural categories.

Fluent replaces the use of the proprietary PluralForms.jsm with a Unicode CLDR standard called Plural Rules.

In order to allow localizers to use it, all the developer has to do is to pass an external argument number:

document.l10n.setAttributes(element, "unread-warning", { unreadCount: 5 });

Localizers can use the argument to build a multi variant message if their language requires that:

unread-warning =
    { $unreadCount ->
        [one] You have { $unreadCount } unread message
       *[other] You have { $unreadCount } unread messages

Fluent guesses that since the variant selection is performed based on a number, its plural category should be retrieved.

If the given translation doesn’t need pluralization for the string (for example Japanese often will not), the localizer can replace it with:

unread-warning = You have { $unreadCount } unread messages

and the message will preserve the social contract.

One additional feature is that the localizer can further improve the message by specifying variants for particular values:

unread-warning =
    { $unreadCount ->
        [0] You have no unread messages
        [1] You have one unread message
       *[other] You have { $unreadCount } unread messages

The advantage here is that per-locale choices don’t leak onto the source code and the developer is not affected.


There is an important distinction between a variant keyed on plural category one and digit 1. Although in English the two are synonymous, in other languages category one may be used for other numbers. For example in Bosnian, category one is used for numbers like 1, 21, 31 and so on, and also for fractional numbers like 0.1.

Partial Arguments

When it comes to formatting data, Fluent allows the developer to provide a set of parameters for the formatter, and the localizer can fine tune some of them. This technique is called partial arguments.

For example, when formatting a date, the developer can just pass a JS Date object, but its default formatting will be pretty expressive. In most cases, the developer may want to use some of the Intl.DateTimeFormat options to select the default representation of the date in string:

document.l10n.setAttributes(element, "welcome-message", {
startDate: FluentDateTime(new Date(), {
    year: "numeric",
    month: "long",
    day: "numeric"
welcome-message = Your session will start date: { $startDate }

In most cases, that will be enough and the date would get formatted in the current Firefox as February 28, 2018.

But if in some other locale the string would get too long, the localizer can fine tune the options as well:

welcome-message = Początek Twojej sesji: { DATETIME($startDate, month: "short") }

This will adjust the length of the month token in the message to short and get formatted in Polish as 28 lut 2018.

At the moment Fluent supports two formatters that match JS Intl API counterparts:

With time more formatters will be added.

Registering New L10n Files

In the previous system, a new localization file had to be registered in order to add it in the jar.mn file for packaging.

Fluent uses a wildcard statement packaging all localization resources into their component’s /localization/ directory.

That means that, if a new file is added to a component of Firefox already covered by Fluent like browser, it’s enough to add the new file to the repository in a path like browser/locales/en-US/browser/component/file.ftl and the toolchain will package it into browser/localization/browser/component/file.ftl.

At runtime Firefox uses a special registry for all localization data. It will register the browser’s /localization/ directory and make all files inside it available to be references.

To make the document localized using Fluent, all the developer has to do is add a single polyfill for the Fluent API to the source and list the resources that will be used:

<link rel="localization" href="branding/brand.ftl"/>
<link rel="localization" href="browser/preferences/preferences.ftl"/>
<script src="chrome://global/content/l10n.js"></script>

For performance reasons the <link/> elements have to be specified above the <script/> and the <script/> itself has to be synchronous in order to ensure that the localization happens before first paint.

This allows Fluent to trigger asynchronous resource loading early enough to perform the initial DOM translation before the initial layout.

The URI provided to the <link/> element are relative paths within the localization system.

Notice that only the registration of the script is synchronous. All the I/O and translation happen asynchronously.

Custom Contexts

The above method creates a single localization context per document. In almost all scenarios that’s sufficient.

In rare edge cases where the developer needs to fetch additional resources, or the same resources in another language, it is possible to create additional contexts manually using Localization class:

const { Localization } =
  ChromeUtils.import("resource://gre/modules/Localization.jsm", {});

const myL10n = new Localization([

let [isDefaultMsg, isNotDefaultMsg] =
  myL10n.formatValues(["is-default", "is-not-default"]);


An example of a use case is the Preferences UI in Firefox which uses the main context to localize the UI but also to build a search index.

It is common to build such search index both in a current language and additionally in English, since a lot of documentation and online help exist only in English.

A developer may create manually a new context with the same resources as the main one, but hardcode it to en-US and then build the search index using both contexts.

Designing Localizable APIs

When designing localizable APIs, the most important rule is to resolve localization as late as possible. That means that instead of resolving strings somewhere deep in the codebase and then passing them on or even caching, it is highly recommended to pass around l10n-id or [l10n-id, l10n-args] pairs until the top-most code resolves them or applies them onto the DOM element.


When writing tests that involve both I18n and L10n, the general rule is that result strings are opaque. That means that the developer should not assume any particular value and should never test against it.

In case of raw i18n the resolvedOptions method on all Intl.* formatters makes it relatively easy. In case of localization, the recommended way is to test that the code sets the right l10n-id/l10n-args attributes like this:


const l10nAttrs = document.l10n.getAttributes(element);

deepEquals(l10nAttrs, {
  id: "my-expected-id",
  args: {
    unreadCount: 5

If the code really has to test for particular values in the localized UI, it is always better to scan for a variable:




Testing against whole values is brittle and will break when we insert Unicode bidirectionality marks into the result string or adapt the output in other ways.

Inner Structure of Fluent

The inner structure of Fluent in Gecko is out of scope of this tutorial, but since the class and file names may show up during debugging or profiling, below is a list of major components, each with a corresponding file in /intl/l10n modules in Gecko.


MessageContext is the lowest level API. It’s fully synchronous, contains a parser for the FTL file format and a resolver for the logic. It is not meant to be used by consumers directly.

In the future we intend to offer this layer for standardization and it may become part of the mozIntl.* or even Intl.* API sets.

That part of the codebase is also the first that we’ll be looking to port to Rust.


Localization is a higher level API which uses MessageContext internally but provides a full layer of compound message formatting and robust error fall-backing.

It is intended for use in runtime code and contains all fundamental localization methods.


DOMLocalization extends Localization with functionality to operate on HTML, XUL and the DOM directly including DOM Overlays and Mutation Observers.


l10n.js is a small runtime code which fetches the <link> elements specified in the document and initializes the main DOMLocalization context on document.l10n.


L10nRegistry is our resource management service. It replaces ChromeRegistry and maintains the state of resources packaged into the build and language packs, providing an asynchronous iterator of MessageContext objects for a given locale set and resources that the Localization class uses.