In this article we're collecting best practices for writing localizable Gaia apps. If you are doing development work on the Gaia project, these should be observed as much as possible. They are also useful to those creating their own Firefox OS apps.
Note: There is another good document that you should take the time to read: Localization content best practices. This is more generic (not Firefox OS-specific), and covers best practices for making content strings as localizable as possible, whereas the following is more about implementing those strings in your code.
UI Localization
The best way to write localizable code is to move as much of l10n logic to declarative HTML as possible. You should always try to mark up your HTML Elements with data-l10n-id
and data-l10n-args
and just set/remove/update those using JavaScript if needed. You also don't need to put the original content in HTML anymore.
Using a declarative API
Below is an example of a well localized UI with some JavaScript-driven localization. The code is not racy, requires no guards, will work in any locale and react properly to language changes. Notice that the HTML doesn't have any English content in it. L10n.js uses its own fallback mechanism and any content defined in the source HTML will be replaced anyways on runtime.
<h1 data-l10n-id="appName" /> <h2 data-l10n-id="summary" /> <article> <p id="author" /> <button id="actionButton" /> </article>
The best way to localize UI elements from JavaScript is to set the data-l10n-id
attribute on an element:
appNameHeadingElem.setAttribute('data-l10n-id', 'appName');
actionButtonElem.setAttribute('data-l10n-id', newArticle ? 'saveBtnLabel' : 'updateBtnLabel'); navigator.mozL10n.setAttributes(authorElem, 'articleAuthor', { 'name': 'John Smith' });
appName = My App saveBtnLabel = Save updateBtnLabel = Update articleAuthor = The author of this article is {{ name }}
L10n.js has a MutationObserver
set that will react to this change and localize the element. You can also set data-l10n-args
, as shown here:
var elem = document.getElementById('myelement'); elem.setAttribute('data-l10n-id', 'label1'); elem.setAttribute('data-l10n-args', JSON.stringify({'name': 'John'}));
Although it may be tempting to use
for HTMLElement.dataset
l10nId
or l10nArgs
, we do not recommend doing this. Not only is setAttribute()
faster, but it also is a more future-proof approach because the data-
prefix is used only temporarily and once we move forward with WebAPI standardization effort, data-l10n-id
will be replaced with a l10n-id
attribute.
Argument substitution
Localized strings can be made to contain placeholders: that is, strings passed in at runtime that should not be localized. For example, suppose the localized string is intended to greet the user: "Hello, Bob!". The app.properties files might look like this:
// en/app.properties greeting=Hello {{person}}!
// fr/app.properties greeting=Bonjour {{person}} !
JavaScript code can then pass these placeholders into webl10n.formatValue()
as properties of a JSON object:
var user = "Bob"; navigator.mozL10n.formatValue("greeting", {"person" : user}).then((string) => { alert(string); // -> "Hello Bob!" if device language is en // -> "Bonjour Bob !" if device language is fr });
Pluralization
The getting started article describes how a translator can handle pluralization by supplying different forms of a string. For example:
tomatoCount={[ plural(n) ]} tomatoCount[zero]=Vous n'avez pas de tomates :(. tomatoCount[one]=Vous avez une tomate. tomatoCount[other] = Vouz avez {{n}} tomates.
This file will return different values for tomatoCount depending on the value passed in as n:
navigator.mozL10n.formatValue("tomatoCount", {"n" : count.value}).then((string) => { alert(string); // "Vous avez une tomate." });
Removing localization
If you need to stop the node from being translated/retranslated, you need to remove l10nId from the element.
document.getElementById('node').removeAttribute('data-l10n-id');
One limitation of the current approach is that we do not have a good way to clean up the node, so we rely on users manually removing the translation of the value and localized attributes:
document.getElementById('node').textContent = null; document.getElementById('node').removeAttribute('placeholder');
In the future we hope to be able to automatically remove translation of the value and attributes when l10nId is unset.
Do not set l10n-id on elements with child elements
One of the important considerations is that when you set l10n-id
on an element, L10n.js takes over the rendering of that element, so any child nodes will be overwritten with localized strings. In the future we may even get something similar to the Shadow DOM, to render localized versions of an element.
If you need to localize DOM Fragment, see below.
Do not use mozL10n.get
One of the commonly used anti-patterns from older l10n paradigms is a synchronous method used to retrieve l10n strings from a bundle. We recommend avoiding using mozL10n.get
since it is synchronous and requires you to guard your code with mozL10n.once()
or mozL10n.ready()
; it also doesn't work with retranslation. The method is deprecated and will be removed soon.
Instead of writing this:
// BAD var elem1 = document.createElement('p'); elem1.textContent = navigator.mozL10n.get('helloMsg'); var elem2 = document.createElement('input'); elem2.placeholder = navigator.mozL10n.get('msgPlaceholder'); var elem3 = document.createElement('button'); elem3.ariaLabel = navigator.mozL10n.get('volumeLabel'); // .properties helloMsg = Hello World msgPlaceholder = Enter password volumeLabel = Switch volume
Use this:
// GOOD var elem1 = document.createElement('p'); elem1.setAttribute('data-l10n-id', 'helloMsg'); var elem2 = document.createElement('input'); elem2.setAttribute('data-l10n-id', 'passwordInput'); var elem3 = document.createElement('button'); elem3.setAttribute('data-l10n-id', 'volumeButton'); // .properties helloMsg = Hello World passwordInput.placeholder = Enter password volumeButton.ariaLabel = Switch volume
In rare cases when you can't use setAttribute nor mozL10n.setAttributes, you can use a non-racy asynchronous L10n.formatValue()
method.
mozL10n.ready will cause memory leaks
When you provide a callback function to mozL10n.ready()
, the function won't get properly garbage collected if it is not explicitly removed. More importantly, if it is a method that is bound to an object, the object instance won't go away. See bug 1135256 for more details. For example, if you have a JavaScript object representing a list item widget and attach a method as a callback with mozL10n.ready()
, the JavaScript object will never get garbage collected. A pattern that could help mitigate this issue, is to have a singleton listener in your script that handles locale changes for all object instances with a WeakMap, here is a partial example:
var instances = new WeakMap(); function MyThing(container) { // used for finding this instance in the DOM container.classList.add('my-thing'); instances.set(container, this); if (navigator.mozL10n.readyState === 'complete') { this.localize(); } } MyThing.prototype.localize = function() { /* ... */ }; navigator.mozL10n.ready(function() { // Look for all the containers of this object type, and retrieve instances. for (var container of document.querySelectorAll('.my-thing')) { var obj = instances.get(container); if (obj) obj.localize(); } });
Passing strings outside of the app
Sometimes your strings are intended to be submitted to another API instead of being displayed to the user. Examples of such scenarios are Bluetooth API, alert()
, confirm()
etc.
In scenarios like that, you should use an asynchronous and forward compatible mozL10n.formatValue
method which returns a promise with the value.
Instead of writing this:
// BAD
alert(navigator.mozL10n.get('myL10nId', {var1: "value"}));
Use this:
// GOOD navigator.mozL10n.formatValue('myL10nId', {var1: "value"}).then((string) => { alert(string); });
One particular scenario is Notifications API, which has it's own NotificationsHelper described below.
It's also important to remember that the paradigm is to carry l10nIds around the app, and only resolve them in the View code right before displaying. So instead of:
// BAD function sendText(msg) { navigator.mozMobileMessage.send(number, msg); } var msgs = { confirmation: navigator.mozL10n.get('confirmationMessage'), }; sendText(msgs.confirmation);
use this:
// GOOD function sendText(l10nId) { navigator.mozL10n.formatValue(l10nId).then(msg => { navigator.mozMobileMessage.send(number, msg); }); } var msgs = { confirmation: 'confirmationMessage' }; sendText(msgs.confirmation);
For more details on how to handle scenarios where not all cases are to be localized and handling localizable API, please see the Writing APIs that operate on L10nIDs section of this article.
Localizing DOM Fragments
mozL10n uses an approach called DOM Overlays that allows for overlaying localization strings with HTML. If you want to make this code localizable:
<p>See our <a href="https://www.mozilla.org">website</a> for more information!</p>
Write it this way:
.properties: seeLink = See our <a>website</a> for more information! html: <p data-l10n-id="seeLink"><a href="https://www.mozilla.org" class="external big"></a></p>
Date/Time Formatting
For Date/Time and also Number and Currency formatting we're using Intl API. An example:
// simple version for when performance is not crucial:, element.textContent = (new Date()).toLocaleString(navigator.languages, { month: 'numeric', day: '2-digit', year: 'short', });
If you want to localize time, you should use shared/js/date_time_helper.js
to get navigator.mozHour12 and then:
element.textContent = (new Date()).toLocaleString(navigator.languages, { hour12: navigator.mozHour12, hour: 'numeric', minute: 'numeric', second: 'numeric' });
navigator.mozHour12
will return proper true
, false
or undefined
, where undefined will result in Intl API picking the default setting for the given language.
If you need to format a lot of elements, then it is significantly more performant to create one formatter and use it on all elements:
var formatter = Intl.DateTimeFormatter(navigator.languages, { year: 'short', day: 'numeric', month: 'numeric' }); for (var i = 0; i < messages.length; i++) { message.element.textContent = formatter.format(message.date); }
The only thing you need to remember is that if you cache the formatter you need to set event listeners on languageschange
event and if your formatter formats hour then also timeformatchange
event to reset the formatters and reformat the elements.
For relative dates, we currently have mozL10n.DateTimeFormat.relativeDate(date)
API which returns a promise.
Notice: mozL10n.DateTimeFormat.localeFormat
and mozL10n.DateTimeFormat.fromNow
are deprecated and will be removed soon.
How to write code that operates on user-provided strings or l10nIDs
In this example, we may have a song title that is not localizable, or a string "Unknown Track" that comes from our localization resources.
There are two patterns to approach this:
Pattern 1
<h1 id="titleElement />
function updateScreen(track) { var titleElement = document.getElementById('titleElement'); if (track.title) { navigator.mozL10n.setAttributes(titleElement, 'trackTitle', { 'title': track.title }); } else { navigator.mozL10n.setAttributes(titleElement, 'trackTitleUnknown'); } }
trackTitle = {{ title }} trackTitleUnknown = Unknown Track
Pattern 2
<h1 id="titleElement />
function updateScreen(track) { var titleElement = document.getElementById('titleElement'); if (track.title) { titleElement.removeAttribute('data-l10n-id'); titleElement.textContent = track.title; } else { titleElement.setAttribute('data-l10n-id', 'trackTitleUnknown'); } }
trackTitleUnknown = Unknown Track
These approaches are similar, but in the future Pattern 1 may become the default as we move toward fully localizable HTML trees.
In both patterns notice that we do not set l10n-id
or l10n-args
in the HTML because we will only set the value when we first load the track in JavaScript, so setting it in HTML is a waste of resources (l10n.js will attempt to translate it if you add it there).
It's also important to notice that we currently don't do any cleanup magic when you remove these attributes, so you need to clean it up yourself. This may change in the future.
Writing code that iterates over many l10n strings
If you have multiple strings (for example error codes), it may be tempting to use mozL10n.get
to test if there is a translation for the string and if not set some generic response. That's not a good pattern because first it uses mozL10n.get
, and second it confuses missing strings with strings that should not be there, creating hard to reproduce bugs and edge cases.
Instead you should create a list of localized strings and test against it, like this:
var l10nCodes = [ 'ERROR_MISSING', 'ERROR_UNKNOWN', 'ERROR_TIMEOUT' ]; if (l10nCodes.indexOf(code) !== -1) { elem.setAttribute('data-l10n-id', code); } else { elem.setAttribute('data-l10n-id', 'ERROR_UNKNOWN'); }
Writing APIs that operate on L10nIDs
One of the more interesting cases to deal with is when you write an API that is supposed to receive L10nIDs. In the simplest case, it looks like this:
function updateTitle(titleL10nId) { document.getElementById('titleElement').setAttribute('data-l10n-id', titleL10nId); }
but if you have a case where you may also need l10nArgs and/or plain strings for cases like the above example, or even HTML fragment to inject (in the future this will be replaced by DOM Overlays), the full pattern we recommend is this:
// titleL10n may be: // a string -> l10nId // an object -> {id: l10nId, args: l10nArgs} // an object -> {raw: string} // an object -> {html: string} function updateTitle(titleL10n) { if (typeof(titleL10n) === 'string') { elem.textContent = ''; // not needed if you're not adding line 25-29 [1] elem.setAttribute('data-l10n-id', titleL10n); return; } if (titleL10n.id) { elem.textContent = ''; // not needed if you're not adding line 25-29 [1] navigator.mozL10n.setAttributes(elem, titleL10n.id, titleL10n.args); return; } if (titleL10n.raw) { elem.removeAttribute('data-l10n-id'); elem.textContent = titleL10n.raw; return; } if (titleL10n.html) { elem.removeAttribute('data-l10n-id'); elem.innerHTML = titleL10n.html; return; } }
[1] If your code supports HTML fragments and a flow in which first the node may receive {html: string}
and later must be switched to l10nId based translation, you need to clean textContent. Otherwise L10n.js will complain about setting l10nId on a node with children nodes. See "Untranslation" section in this article for more background.
Of course, you don't have to support cases you don't need. The HTML case is rarely needed, and l10nArgs or raw may not be needed either. But following this schema for l10n parameters allows you to get it working and later extend the supported cases without altering the API.
One of the consequences of our current limitations regarding unsetting l10nId is that we don't have a clean way to clear the DOM Fragment from the old translation. If your API has potential to have
Testing
When writing tests, we discourage testing the values of the nodes. It makes the test useless in other locales, it will not work with asynchronous translation, and in the future it won't work if we change how we present localization of DOM Elements.
Gaia provides a shared mock_l10n that you should use.
The best strategy is to isolate your test to make sure it sets the proper l10n-id
and l10n-args
attributes:
assert.equal(elem.getAttribute('data-l10n-id'), 'myExpectedId');
or:
var l10nAttrs = navigator.mozL10n.getAttributes(elem); assert.equal(l10nAttrs.id, 'myExpectedId'); assert.deepEqual(l10nAttrs.args, {'name': 'John'});
mock_l10n also provides an assertL10n helper function:
l10nAssert(element, 'l10nId');
l10nAssert(element2, { raw: 'string' });
l10nAssert(element3, {
id: 'l10nId',
});
l10nAssert(element4, {
id: 'l10nId',
args: {
user: 'John'
}
});
l10nAssert(element5, { html: "<span>Foo</span>" });
Notification API
So, you want to send a Notification. The W3C API expects you to pass title and body as a string. Gaia offers you a NotificationHelper that works with mozL10n.
Instead of:
var title = navigator.mozL10n.get('notification_title'); var body = navigator.mozL10n.get('notification_body', {user: "John"}); var notification = new Notification(title, { body: body, }); // set onclick handler for the notification notification.onclick = myCallback;
you should write:
NotificationHelper.send('notification_title', { bodyL10n: {id: 'notification_body', args: {user: "John"}} }).then(function(notification) { notification.addEventListener('click', myCallback); });
NotificationHelper uses handles title and bodyL10n the same way as described in "Writing APIs that operate on L10nIDs" section of this article, so you can pass a string or an object.