Overview

Fluent is a family of localization specifications, implementations and good practices developed by Mozilla.

This documentation is intended to be a starting point for anyone looking to understand and learn to use Fluent DOM, which is a set of abstractions that allow for localizing content in HTML pages as they relate to the Document Object Model (DOM).


Example

We can specify translations in Fluent (.ftl) files for different languages. Here we specify translations for en-US and es-MX.

en-US/main.ftl

hello = Hello { $name }.

es-MX/main.ftl

hello = Hola { $name }.

We can then use our Fluent translations to localize the content of elements in the HTML. In this example <h1> element will be translated by Fluent to say "Hola Erik.", because we specify the defaultLanguage to be es-MX, which matches our translation above.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8' />
    <meta name='defaultLanguage' content='es-MX' />
    <meta name='availableLanguages' content='en-US, es-MX' />
    <link name='localization' content='./localization/{locale}/main.ftl' />
    <script type='module' src='index.js'></script>
  </head>
  <body>
    <h1 data-l10n-id='hello' data-l10n-args='{ "name": "Erik" }'>Localize me!</h1>
  </body>
</html>

The following sections will explain the concepts behind Fluent DOM, as well as present a walkthrough tutorial on how to set up a project from scratch that uses Fluent DOM to localize HTML content.

Background

Fluent aims to provide useful abstractions over the entire process of localization. It is helpful to view the process as divided into three layers: low level, mid level and high level.

Low Level: Bundle Generation

At the lowest level of abstraction, Fluent expects to operate on bundles of resources.

In this context, a resource is a Fluent Translation List (.ftl) file that contains translations of localized messages.

A bundle can be seen as a collection of resources that provide a set of translations for a given locale.

The process of fetching resources and collecting them into bundles is an incredibly complex space that may have different desired implementations for different systems, which is why it deserves its own layer of abstraction.

Mid Level: The Localization Class

Sitting on top of the abstracted low-level mechanism to fetch bundles of localization resources is the Localization class.

The separation of abstraction between the low-level and mid-level allows the Localization API to be agnostic of how the resources are retrieved. The resources themselves could be sitting on the file system, fetched asynchronously over a network, or something else entirely.

Once the Localization class is given bundles to work with, it provides an API to translate localized messages based on the data contained within the bundles.

High Level: DOM Localization and Friends

Sitting on top of the abstracted low-level and mid-level mechanisms to retrieve resources and format localized messages is the DOMLocalization class.

In this abstraction layer, the DOMLocalization provides an API that works natively with HTML elements and fragments. It allows you to declaratively localize content by adding attributes directly into HTML.

Importantly, DOMLocalization is only one of many high-level abstractions that can sit on top of the low-level and mid-level layers. Other high-level abstractions such as Fluent React can just as well sit on top of the previous layers, providing a clean API to work with ReactJS

Fluent DOM

Fluent DOM is an abstraction that is specialized for localizing the Document Object Model (DOM).

There are two primary concepts to understand when working with Fluent DOM:

L10n ID — The identifiers by which Fluent DOM will recognize elements to localize.

L10n Args — The arguments that may be passed into the localized content to affect the final translation.

L10n ID

The L10n ID is an identifier that you assign to a DOM element to be localized.

For example, a welcome message may have a regular id of welcome and a data-l10n-id of hello. The regular id behaves as normal, a way to query for this element, such as with getElementById(). The data-l10n-id is intended to match a Fluent translation that will be used to localize this element.

<p id='welcome' data-l10n-id='hello'></p>

We define our L10n IDs in Fluent (.ftl) files for each locale that we want to provide a translation. In the example above, the data-l10n-id is hello, so in the Fluent files we would define translations for the hello L10n ID.

For the en locale you might have:

en/main.ftl

hello = Hello

And for the es locale you might have:

es/main.ftl

hello = Hola

The data-l10n-id is like a social contract between the element and its localized content translation. Each ID is unique to a single translation. If translated content needs to be updated, then a new data-l10n-id must be provided. For example, if we want to change our en message from "Hello" to "Hello there," we need to replace the previous L10n ID hello with a new one, such as hello-there.

This new ID and content would then require a re-translation in every other locale, such as es in the example above. Additionally, HTML elements will need to use this new ID as well.

en/main.ftl

hello-there = Hello there

This new ID and content would then need to get re-translated for every other locale, such as es in the example above, and the HTML elements will also need to receive the new ID as well.

index.html

<p id='welcome' data-l10n-id='hello-there'></p>

L10n Args

data-l10n-args provides a way to supply arguments to your localized content.

To build upon the data-l10n-id example of a welcome message, perhaps we want to be able to provide a name of the person we are welcoming. To do this, we will need to add a data-l10n-args tag in addition to the data-l10n-id.

<p id='welcome'
  data-l10n-id='hello'
  data-l10n-args='{"name": "world"}'>
</p>

And in the Fluent file, we will be able to use the argument within our localized content:

# $name - The name of the person to welcome.
hello = Hello { $name }

In the example above, passing in data-l10n-args='{"name:" "world"}' would result in the final translation of Hello world.


Limitations

At this time, Fluent DOM is only able to accept the following types as data-l10n-args object's properties.

  • numbers
  • strings
  • bool (converted to "true" or "false" string)
  • null (treated as non-existent property)

It technically also supports dates, since dates can be represented as a number, however there is an open bug (Bug1611754) to support dates in a more straightforward way.

Getting started with Fluent DOM

This is a small, step-by-step tutorial showing a simple way to get started with Fluent in a web project built from scratch using Fluent.js

We will also be using Node Package Manager (npm) and Parcel in this tutorial.

Fluent.js

Fluent.js is a JavaScript implementation of Project Fluent, a localization framework designed to unleash the expressive power of the natural language.

Fluent.js allows interaction with data-l10n-id and data-l10n-args through the DOMLocalization class, which is derived from the Localization base class.

The Localization base class provides the mid-level APIs for interacting with Fluent. High-level abstractions such as Fluent DOM (for localizing HTML pages) and Fluent React (for react projects) are built on top of this Localization object's abstractions.

Localization

The Localization class is a central high-level API for using Fluent.

Functionality


constructor(resourceIds, generateBundles)

Creates a new Localization object given a list of resourceIDs and a generateBundles function that returns a generator over FluentBundles.

The resourceIds are the paths to the fluent files that are used to generate bundles.

The generateBundles function's generator behavior acts as a fallbacking strategy for the availability of fluent resources. Fallbacking allows for translations from a different language to be used if the translation is not available in the desired language. For example, if the Spanish translation is missing, English text could be displayed instead.

When localizing content in the ideal scenario, the generateBundles function will only ever have to produce one bundle. This would mean that the first bundle had translations for every requested localization.

If the current bundle in unable to produce a translation, then the generator's next bundle will be retrieved in an attempt to find a translation, and so on.


addResourceIds(resourceIds, eager = false)

Adds resource IDs to this localization object. Accepts a boolean for whether to eagerly or lazily apply the changes with a default value of false (for lazy).


removeResourceIds(resourceIds)

Removes resource IDs from this localization object. This operation is never eager.


formatValues(keys)

Retrives translations corresponding to the passed keys. Keys must be {id, args} objects.

If all keys have available translations within the current FluentBundle, then no fallbacking will occur. If a translation is missing, the localization object will retrieve the next bundle from the generateBundles function and so on.

docL10n.formatValues([
  {id: 'hello', args: { name: 'Mary' }},
  {id: 'hello', args: { name: 'John' }},
  {id: 'welcome'}
]).then(console.log)

formatValue(id, args)

Note:

Use this sparingly for one-off messages which do not need to be retranslated when the user changes their language preferences, e.g. in notifications.

Retrieves the translation corresponding to the id.

If passed, args is a simple hash object with a list of variables that will be interpolated in the value of the translation.

Returns a promise resolving to the translation string.

docL10n.formatValue(
  'hello', { name: 'world' }
).then(console.log);

DOMLocalization

The DOMLocalization class is an extension of the Localization class and is responsible for fetching resources and formatting translations for DOM elements.

Functionality


Includes all the functionality from Localization


constructor(resourceIds, generateBundles)

Creates a new DOMLocalization object given a list of resourceIDs and a generateBundles function that returns a generator over FluentBundles.

The resourceIds are the paths to the fluent files that are used to generate bundles.

The generateBundles function's generator behavior acts as a fallbacking strategy for the availability of fluent resources. Fallbacking allows for translations from a different language to be used if the translation is not available in the desired language. For example, if the Spanish translation is missing, English text could be displayed instead.

The generateBundles function's generator behavior acts as a fallbacking strategy for the availability of fluent resources.

When localizing content in the ideal scenario, the generateBundles function will only ever have to produce one bundle. This would mean that the first bundle had translations for every requested localization.

If the current bundle in unable to produce a translation, then the generator's next bundle will be retrieved in an attempt to find a translation, and so on.


setAttributes(element, id, args)

Set the data-l10n-id and data-l10n-args attributes on a DOM element. DOMLocalization makes use of mutation observers to detect change to data-l10n-* attributes and translate elements asynchronously. setAttributes is a convenience method which allows to translate DOM elements declaratively.

You should always prefer to use data-l10n-id on elements (statically in HTML or dynamically via setAttributes) over manually retrieving translations with format. The use of attributes ensures that the elements can be retranslated when the user changes their language preferences.

localization.setAttributes(
  document.querySelector('#welcome'), 'hello', { name: 'world' }
);

This will set the following attributes on the #welcome element. The MutationObserver will pick up this change and will localize the element asynchronously.

<p id='welcome'
  data-l10n-id='hello'
  data-l10n-args='{"name": "world"}'>
</p>

getAttributes(element)

Get the data-l10n-* attributes from a DOM element.

localization.setAttributes(
  document.querySelector('#welcome'), 'hello', { name: 'world' }
)
localization.getAttributes(
  document.querySelector('#welcome')
);
// -> { id: 'hello', args: { name: 'world' } }

connectRoot(root)

Add new root to the list of roots managed by this DOMLocalization.

Additionally, if this DOMLocalization has an observer, start observing the new root in order to translate mutations in it.

The new root may not overlap with an existing root.


disconnectRoot(root)

Remove root from the list of roots managed by this DOMLocalization.

Additionally, if this DOMLocalization has an observer, stop observing root.

Returns true if the root was the last one managed by this DOMLocalization.


translateRoots()

Translate all roots associated with this DOMLocalization.


translateFragment(fragment)

Translate a DOM element or fragment asynchronously using this DOMLocalization object.

Manually trigger the translation (or re-translation) of a DOM fragment. Use the data-l10n-id and data-l10n-args attributes to mark up the DOM with information about which translations to use.

Returns a Promise that gets resolved once the translation is complete.


translateElements(elements)

Translate a list of DOM elements asynchronously using this DOMLocalization object.

Manually trigger the translation (or re-translation) of a list of elements. Use the data-l10n-id and data-l10n-args attributes to mark up the DOM with information about which translations to use.

Returns a Promise that gets resolved once the translation is complete.

Getting started with Fluent DOM

This is a small, step-by-step tutorial showing a simple way to get started with Fluent in a web project built from scratch using Fluent.js.

We will build up to localizing a <h1> element using the following syntax:

src/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8' />
    <meta name='defaultLanguage' content='es-MX' />
    <meta name='availableLanguages' content='en-US, es-MX' />
    <link name='localization' content='./localization/{locale}/main.ftl' />
    <script type='module' src='index.js'></script>
  </head>
  <body>
    <h1 data-l10n-id='hello'>Localize me!</h1>
  </body>
</html>

The tutorial will show how to set up your localization resources for your desired locales, and how to retrieve values from those resources to localize content.

We will be using Node Package Manager (npm) and Parcel in this tutorial.

Installing NPM

To install Node Package Manager (npm), we will be using Node Version Manager (nvm).

Navigate to the NVM repository and follow the instructions for the Install & Upate Script section.

Note that their examples pipe to bash. If you are using another shell, such as zsh, you will need to change this to match your desired shell.

After running their install script, you should be able to close and re-open your terminal and verify that it was installed correctly by checking the version.

terminal

nvm --version
0.39.1

Next, we will install npm through nvm.

terminal

nvm install node

Now we can verify that npm is installed.

terminal

npm --version
8.3.0

In the next section we will build a basic skeleton of a project for using Fluent DOM.

Setting up a project

This tutorial will walk you through setting up a basic project with Node Package Manager (npm) and Parcel to use Fluent DOM.

If you don't have npm installed, please refer to the previous section on installing npm.


First let's create a new directory called fluent-playground.

terminal

mkdir fluent-playground
cd fluent-playground

Next we will create a new project using npm

terminal

npm init

You should now have a package.json file that looks something like this:

package.json

{
  "name": "fluent-playground",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Next we will need to add our src directory and some basic index.html and index.js files.

terminal

mkdir src
touch src/index.html
touch src/index.js
fluent-playground
├── src
│  ├── index.html
│  └── index.js
└── package.json

Populate your index.html with some minimal content:

src/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8' />
    <script type='module' src='index.js'></script>
  </head>
  <body>
    <h1 id='welcome'>Localize me!</h1>
  </body>
</html>

Now we need to add the Parcel bundler to run our project.

terminal

npm install parcel

You should now see that parcel has been added as a dependency in package.json

package.json

"dependencies": {
  "parcel": "^2.0.1",
}

Next we need to tell npm to use Parcel on the start command by adding the following line to the scripts section in package.json.

package.json

"scripts": {
  "start": "parcel src/index.html",
  "test": "echo \"Error: no test specified\" && exit 1"
},

Verify that everything is working by running

terminal

npm start

which should serve your html page on localhost. You should see the <h1> element that says Localize me!.


In the next section we will create some Fluent (.ftl) files and cover how to get parcel to add them as assets to your project.

Adding fluent files

This tutorial will walk you through getting Parcel to add Fluent (.ftl) files as assets to your project.

If you don't have a working Parcel project, please refer to the previous section.


Now that we have a working Parcel setup, we need to add some fluent files. We will store the files in a directory called static in our project root. Within the static directory we will create localization/{locale}/main.ftl, where each {locale} will be a specific locale.

In this tutorial, we will only create paths for the en-US and es-MX locales.

terminal

mkdir -p static/localization/en-US
touch static/localization/en-US/main.ftl
mkdir static/localization/es-MX
touch static/localization/es-MX/main.ftl

tree view

fluent-playground
├── src
│  ├── index.html
│  └── index.js
├── static
│  └── localization
│     ├── en-US
│     │  └── main.ftl
│     └── es-MX
│        └── main.ftl
├── package-lock.json
└── package.json

Then we will populate our main.ftl files with our localized message using hello as our data-l10n-id.

static/localization/en-US/main.ftl

hello = Hello!

static/localization/es-MX/main.ftl

hello = ¡Hola!

Next we will need to install a Parcel plugin to copy over our static files to our project directory, then verify that it is added to your package.json.

terminal

npm install parcel-reporter-static-files-copy

package.json

"dependencies": {
  "parcel-bundler": "^1.12.5",
  "parcel-reporter-static-files-copy": "^1.3.4"
}

Finally, in order to enable the static file copy, we need to create a .parcelrc file and add a few lines to it.

terminal

touch .parcelrc

.parcelrc

{
  "extends": ["@parcel/config-default"],
  "reporters": ["...", "parcel-reporter-static-files-copy"]
}

Now when we run npm start, Parcel should build our dist directory with our assets included.

tree view

fluent-playground
├── dist
│  ├── localization
│  │  ├── en-US
│  │  │  └── main.ftl
│  │  └── es-MX
│  │     └── main.ftl
│  ├── index.html
│  ├── src.e31bb0bc.js
│  └── src.e31bb0bc.js.map
...

In the next section we will cover how to use Fluent DOM from JavaScript to localize our message.

Using Fluent

This tutorial will show how to use fluent to localize the message in our <h1> element.

If you don't have a Parcel project set up with fluent files, please refer to the previous sections.


First, we need to add and import our Fluent dependencies.

For this tutorial we will install fluent/bundle and fluent/dom via the following command:

terminal

npm install @fluent/bundle @fluent/dom

And verify that the dependencies are added to your package.json.

package.json

"dependencies": {
  "@fluent/bundle": "^0.17.0",
  "@fluent/dom": "^0.8.0",
  ...
}

Then we need to import some items from these dependencies in index.js.

src/index.js

import { DOMLocalization } from "@fluent/dom";
import { FluentBundle, FluentResource } from "@fluent/bundle";

Next, we need to add the metadata to the HTML regarding which languages we have available.

Add the following lines to the <head> section of your index.html file.

src/index.html

    <meta name='defaultLanguage' content='en-US' />
    <meta name='availableLanguages' content='en-US, es-MX' />

Now we are going to add a JavaScript function to index.js to retrieve this meta data.

src/index.js

function getMeta(elem) {
  return {
    available: elem.querySelector('meta[name="availableLanguages"]')
      .getAttribute("content")
      .split(",").map(s => s.trim()),
    default: elem.querySelector('meta[name="defaultLanguage"]')
      .getAttribute("content"),
  };
}

Now that we are able to retrieve the metadata for which locales are supported, we need a way to resolve the paths to the Fluent resources that we created for those locales.

We are going to add the following line to the <head> section of the index.html file:

src/index.html

<link name='localization' content='./localization/{locale}/main.ftl' />

src/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8' />
    <meta name='defaultLanguage' content='es-MX' />
    <meta name='availableLanguages' content='en-US, es-MX' />
    <link name='localization' content='./localization/{locale}/main.ftl' />
    <script type='module' src='index.js'></script>
  </head>
  <body>
    <h1 id='welcome'>Localize me!</h1>
  </body>
</html>

We will now need an asynchronous JavaScript function to fetch a FluentResource from a given path, replacing the {locale} template with an actual given locale.

src/index.js

async function fetchResource(locale, resourceId) {
  const url = resourceId.replace("{locale}", locale);
  const response = await fetch(url);
  const text = await response.text();
  return new FluentResource(text);
}

Once we are able to fetch a resource, we will want to be able to turn that resource into a FluentBundle.

src/index.js

async function createBundle(locale, resourceId) {
    let resource = await fetchResource(locale, resourceId);
    let bundle = new FluentBundle(locale);
    let errors = bundle.addResource(resource);
    if (errors.length) {
      // Syntax errors are per-message and don't break the whole resource
    }
    return bundle;
}

Finally, we will want a top-level generateBundles function (remember, this is what DOMLocalization is initialized with) to provide a generator over the available fluent bundles for our supported locales.

src/index.js

async function* generateBundles(resourceIds) {
  const defaultLanguage = getMeta(document.head).default;
  yield await createBundle(defaultLanguage, resourceIds[0]);

  const availableLanguages = getMeta(document.head).available;
  for (const locale of availableLanguages) {
    yield await createBundle(locale, resourceIds[0]);
  }
}

This function will first grab the bundle for the default language, and then go through the list of available languages. Yes, there is a redundancy here where the default language is also in the list of available languages. For simplicity, we'll leave it this way.


Out final steps are to grab the translatable <h1> element, our template path for the resourceIDs, and construct our DOMLocalization object to perform the translations.

src/index.js

const welcome = document.getElementById("welcome");
console.log(welcome);

const resources = document.querySelector("link[name=localization]");
const resourcePathTemplate = resources.attributes.content.nodeValue;
console.log(resourcePathTemplate);

const l10n = new DOMLocalization([resourcePathTemplate], generateBundles);
l10n.connectRoot(document.documentElement);
l10n.setAttributes(welcome, "hello");
l10n.translateRoots();

With this final step, you should be able to invoke

terminal

npm start

and now see Hello!, as specified in our Fluent file for en-US. You should also be able to change the default language in index.html to es-MX and see ¡Hola!.


Congratulations! You've just set up a project from scratch that uses Fluent DOM to localize HTML elements.


In the next section we will cover how to use data-l10n-id directly to translate DOM elements without doing it manually from JavaScript.

Using L10n ID

This section assumes that you have followed the tutorial up through the Using FLuent section, and that you have a working HTML page that localizes the following content.

src/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8' />
    <meta name='defaultLanguage' content='es-MX' />
    <meta name='availableLanguages' content='en-US, es-MX' />
    <link name='localization' content='./localization/{locale}/main.ftl' />
    <script type='module' src='index.js'></script>
  </head>
  <body>
    <h1 id='welcome'>Localize me!</h1>
  </body>
</html>

In our current implementation we are using JavaScript to query for the <h1> element through its identifier (id='welcome'). Then we are using our DOMLocalization object to set this element's attributes to hello to match the data-l10n-id that is specified in our Fluent (.ftl) files.

src/index.js

const welcome = document.getElementById("welcome");
...
l10n.setAttributes(welcome, "hello");

This works fine, but one of the nice things about using Fluent DOM is the abstraction around using data-l10n-id directly in your HTML. Our DOMLocalization object can automatically observe the document and localize the elements who have matching L10n IDs without having to manually query for them in JavaScript.

To make this change, we are going to remove the id='welcome' from our <h1> element in index.html, and add data-l10n-id='hello'.

src/index.html

<h1 id='welcome' data-l10n-id='hello'>Localize me!</h1>

Then we are going to remove the lines in JavaScript that manually query for and modify the <h1> element.

src/index.js

//const welcome = document.getElementById("welcome");
//console.log(welcome);
//
const resources = document.querySelector("link[name=localization]");
const resourcePathTemplate = resources.attributes.content.nodeValue;
console.log(resourcePathTemplate);

const l10n = new DOMLocalization([resourcePathTemplate], generateBundles);
l10n.connectRoot(document.documentElement);
//l10n.setAttributes(welcome, "hello");
l10n.translateRoots();

In this case, we fetch our resources, create our DOMLocalization object, connect it to the document, and then invoke translateRoots(). The DOMLocalization object will automatically see that our <h1> element has a data-l10n-id='hello', which matches the ID in our Fluent (.ftl) files, and make the appropriate translation.


In the next section we will cover how to pass arguments to our localized content using data-l10n-args.

Using L10n Args

This section assumes that you have followed the tutorial up through the Using L10n ID section, and that you have a working HTML page that localizes the following content.

src/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8' />
    <meta name='defaultLanguage' content='es-MX' />
    <meta name='availableLanguages' content='en-US, es-MX' />
    <link name='localization' content='./localization/{locale}/main.ftl' />
    <script type='module' src='index.js'></script>
  </head>
  <body>
    <h1 id='welcome' data-l10n-id='hello'>Localize me!</h1>
  </body>
</html>

We are now going to allow our localized welcome message to take in a variable as an argument.

In each of the two fluent files, we are going to add a comment describing the variable in addition to adding the variable into our localized messages.

static/localization/en-US/main.ftl

# Variables:
#  $name (String): the name of the person to greet.
hello = Hello { $name }!

static/localization/en-US/main.ftl

# Variables:
#  $name (String): the name of the person to greet.
hello = ¡Hola { $name }!

Now, when we run the program, we will still see our localized message for our locale, but since Fluent doesn't have any default value for this variable, it will just passthrough the variable name.

¡Hola ⁨{$name}⁩!

This is intended behavior if Fluent is unable to find any values to match the variable for this message.

To fix this, we can add a default value in the HTML via the data-l10n-args attribute:

src/index.html

<h1 id='welcome' data-l10n-id='hello' data-l10n-args='{"name": "world"}'>Localize me!</h1>

Now, when our DOMLocalization is translating this element, it will see the default argument for $name and provide the value "world" to the localized message.

When we run the program again, we should now see:

¡Hola ⁨world⁩!

We can also pass in a new variable to the localized message via JavaScript.

To pass in a new variable we will call the setAttributes() function to pass in a new argument to the element before we call translateRoots().

src/index.js

const element = document.getElementById('welcome');
l10n.setAttributes(
  element, 'hello', { name: 'Erik' }
);
l10n.translateRoots();

Now, when we run the program, we should see that our element was localized with the new, non-default argument.

¡Hola ⁨Erik⁩!

Congratulations! You are now localizing an HTML page utilizing Fluent DOM with data-l10n-id and data-l10n-args.

DOM Overlays

At the moment, this page is taken verbatim from https://github.com/projectfluent/fluent.js/wiki/DOM-Overlays

When localizing an HTML document you're likely to encounter two scenarios where the DOM elements may appear in translations:

  • Text-level elements. Localizers may want to use some HTML markup to make the translation correct according to the rules of grammar and spelling. For instance, <em> may be used for words borrowed from foreign languages, <sup> may be used for ordinals or abbreviations, etc.

  • Functional elements. Developers may want to pass elements as arguments to the translation and create rich language-agnostic UIs. For instance, they may define an <img> element which should be placed inline inside of the translation. Each language will need to decide where exactly the <img> should go.

fluent-dom features a secure overlay logic which:

  • allows localizers to use some safe text-level markup in translations, and
  • allows developers to pass functional elements as arguments to translations.

Text-Level Elements

Text-level elements such as <em>, <strong> and <sup>, are considered safe and are in fact often required to build correct translations. fluent-dom always allows these elements to be present in translations. No further nesting is allowed and all attributes defined on these text-level elements are sanitized using a well-defined list of safe attributes.

ordinals = 1<sup>st</sup>, 2<sup>nd</sup>, 3<sup>rd</sup>, and so on.

The list of text-level elements is defined by the HTML specification in §4.5. Text-level semantics. fluent-dom allows almost all of them by default, with the exception of <a> (which doesn't make much sense without href) and <ruby>, <rt> and <rp> (which require nesting).

Functional Elements

Developers may also put child elements (both text-level and others) inside of the element which is the target of the translation. These child elements must be annotated with the data-l10n-name attribute. fluent-dom will look for corresponding child elements defined by the translation and clone them into the final translated result. The cloning preserves functional attributes defined in the source (href, class, src) but destroys any event listeners attached to the child element. Safe attributes found on the matching child element from the translation will be copied to the resulting child.

<img> Example

<p data-l10n-id="hello-icon">
    <img data-l10n-name="world" src="world.png">
</p>
hello-icon = Hello, <img data-l10n-name="world" alt="world">!

Result:

<p data-l10n-id="hello-icon">
    Hello, <img data-l10n-name="world" alt="world" src="world.png">!
</p>

<a> Example

<span data-l10n-id="privacy-note">
  <a data-l10n-name="priv" href="https://www.mozilla.org/privacy" />
</span>
privacy-note = Read our <a data-l10n-name="priv" title="Privacy Policy">privacy policy</a>.

will produce:

<span data-l10n-id="privacy-note">
  Read our <a data-l10n-name="priv" href="https://www.mozilla.org/privacy" title="Privacy Policy">privacy policy</a>.
</span>

Safety

The DOM Overlays logic sanitizes all elements found in the translation before inserting them into the DOM. Only safe text-level elements are allowed by default. Their attributes are also sanitized to only allow safe translatable attributes like title or alt.

When non-text-level functional elements are used in the translation, like <img>, fluent-dom will only try to match them against the source HTML if both the type and the value of the data-l10n-name attribute match the corresponding element defined in the source.

This design secures fluent-dom against potentially malicious translations. At the same time it empowers the localizers and the developers to make the localized user experience more complete.

Whitelisting additional attributes

Developers can allow additional attributes to be set by the translation via the data-l10n-attrs attribute. Its value should be a comma-separated list of attribute names.

<span data-l10n-id="privacy-note">
  <a href="https://www.mozilla.org/privacy" data-l10n-attrs="style" />
</span>
privacy-note = Read our <a title="Privacy Policy" style="font-weight: bold">privacy policy</a>.

will produce:

<span data-l10n-id="privacy-note">
  Read our <a href="https://www.mozilla.org/privacy" data-l10n-attrs="style" title="Privacy Policy" style="font-weight: bold">privacy policy</a>.
</span>

Please note that using data-l10n-attrs shifts the responsibility for the safety of the widget onto the developer.

Limitations

The implementation of DOM overlays as of fluent-dom 0.2.0 is subject to the following limitations. We plan to address some of them in the future revisions of the feature.

  1. At the moment the identity of functional elements passed into the translation via data-l10n-name is not preserved. It's thus not possible to add event listeners to them.

  2. Nested markup is not allowed in either text-level elements or functional elements.

  3. Text content from the translation currently replaces the entire children subtree of functional elements passed into the translation.

  4. There's no way to allow additional elements to be defined in translations without putting them in the source HTML as functional elements.