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 ofresourceIDs
and agenerateBundles
function that returns a generator overFluentBundles
.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 currentFluentBundle
, then no fallbacking will occur. If a translation is missing, the localization object will retrieve the next bundle from thegenerateBundles
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 ofresourceIDs
and agenerateBundles
function that returns a generator overFluentBundles
.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
anddata-l10n-args
attributes on a DOMelement
.DOMLocalization
makes use of mutation observers to detect change todata-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 viasetAttributes
) over manually retrieving translations withformat
. 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 DOMelement
.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 thisDOMLocalization
.Additionally, if this
DOMLocalization
has an observer, start observing the newroot
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 thisDOMLocalization
.Additionally, if this
DOMLocalization
has an observer, stop observingroot
.Returns
true
if the root was the last one managed by thisDOMLocalization
.
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
anddata-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
anddata-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.
-
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. -
Nested markup is not allowed in either text-level elements or functional elements.
-
Text content from the translation currently replaces the entire children subtree of functional elements passed into the translation.
-
There's no way to allow additional elements to be defined in translations without putting them in the source HTML as functional elements.