The WebExtensions API has a rather handy module available for internationalizing add-ons — i18n. In this article we'll explore its features and provide a practical example of how it works. The WebExtensions i18n system is similar to common JavaScript libraries for i18n such as i18n.js.
Note: The example WebExtension featured in this article — notify-link-clicks-i18n — is available on Github. Follow along with the source code as you go through the sections below.
Anatomy of an internationalized WebExtension
An internationalized WebExtension can contain the same features as any other WebExtension — background scripts, content scripts, etc. — but it also has some extra parts to allow it to switch between different locales. These are summarized in the following directory tree:
- webextension-root-directory/
- _locales
- en
- messages.json
- English messages (strings)
- messages.json
- de
- messages.json
- German messages (strings)
- messages.json
- etc.
- en
- manifest.json
- locale-dependent metadata
- javascript.js
- JavaScript for retrieving browser locale, locale-specific messages, etc.
- styles.css
- locale-dependent css
- _locales
Let's explore each of the new features in turn — each of the below sections represents a step to follow when internationalizing your WebExtension.
Providing localized strings in _locales
Every i18n system requires the provision of strings translated into all the different locales you want to support. In WebExtensions, these are contained within a directory called _locales
, placed inside the extension root. Each individual locale has its strings (called messages in WebExtensions) contained within a file called messages.json
, which is placed inside a subdirectory of _locales
, named using the language subtag for that locale's language.
So for example, in our sample app we have directories for "en" (English), "de" (German), "nl" (Dutch), and "ja" (Japanese). Each one of these has a messages.json
file inside it.
Let's now look at the structure of one of these files (_locales/en/messages.json):
{ "extensionName": { "message": "Notify link clicks i18n", "description": "Name of the extension." }, "extensionDescription": { "message": "Shows a notification when the user clicks on links.", "description": "Description of the extension." }, "notificationTitle": { "message": "Click notification", "description": "Title of the click notification." }, "notificationContent": { "message": "You clicked $1.", "description": "Tells the user which link they clicked: the $1 placeholder is the url." } }
This file is standard JSON — each one of its members is an object with a name, which contains a message
and a description
. All of these items are strings; $1
is a placeholder, which is replaced with a substring at the time the notificationContent
member is called by the WebExtension. You'll learn how to do this in the {{anch("Retrieving message strings from JavaScript")}} section.
Note: You can find much more information about the contents of messages,json
files in our Locale-Specific Message reference.
Internationalizing manifest.json
There are a couple of different tasks to carry out to internationalize your manifest.json.
Retrieving localized strings in manifests
Your manifest.json includes strings that are displayed to the user, such as the add-on's name and description. If you internationalize these strings and put the appropriate translations of them in messages.json, then the correct translation of the string will be displayed to the user, based on the current locale, like so.
To internationalize strings, specify them like this:
"name": "__MSG_extensionName__", "description": "__MSG_extensionDescription__",
Here, we are retrieving message strings dependant on the browser's locale, rather than just including static strings.
To call a message string like this, you need to specify it like this:
- Two underscores, followed by
- The string "MSG", followed by
- One underscore, followed by
- The name of the message you want to call as defined in
messages.json
, followed by - Two underscores
__MSG_ + messageName + __
Specifying a default locale
Another field you should specify in your manifest.json is default_locale:
"default_locale": "en"
This specifies a default locale to use if the extension doesn't include a localized string for the browser's current locale. Any message strings that are not available in the browser locale are taken from the default locale instead. There are some more details to be aware of in terms of how the browser selects strings — see {{anch("Localized string selection")}}.
Locale-dependent CSS
Note that you can also retrieve localized strings from CSS files in the extension. For example, you might want to construct a locale-dependent CSS rule, like this:
header { background-image: url(../images/__MSG_extensionName__/header.png); }
This is useful, although you might be better off handling such a situation using {{anch("Predefined messages")}}.
Retrieving message strings from JavaScript
So, you've got your message strings set up, and your manifest. Now you just need to start calling your message strings from JavaScript so your extension can talk the right language as much as possible. The actual i18n API is pretty simple, containing just four main methods:
- You'll probably use {{WebExtAPIRef("i18n.getMessage()")}} most often — this is the method you use to retrieve a specific language string, as mentioned above. We'll see specific usage examples of this below.
- The {{WebExtAPIRef("i18n.getAcceptLanguages()")}} and {{WebExtAPIRef("i18n.getUILanguage()")}} methods could be used if you needed to customize the UI depending on the locale — perhaps you might want to show preferences specific to the users' preferred languages higher up in a prefs list, or display cultural information relevant only to a certain language, or format displayed dates appropriately according to the browser locale.
- The {{WebExtAPIRef("i18n.detectLanguage()")}} method could be used to detect the language of user-submitted content, and format it appropriately.
In our notify-link-clicks-i18n example, the background script contains the following lines:
var title = chrome.i18n.getMessage("notificationTitle"); var content = chrome.i18n.getMessage("notificationContent", message.url);
The first one just retrieves the notificationTitle message
field from the available messages.json
file most appropriate for the browser's current locale. The second one is similar, but it is being passed a URL as a second parameter. What gives? This is how you specify the content to replace the $1
placeholder we see in the notificationContent message
field. For example, the original notificationContent
message string in the en/messages.json
file is
You clicked $1.
Let's say the URL clicked on is https://developer.mozilla.org
. After the {{WebExtAPIRef("i18n.getMessage()")}} call, the contents of the second parameter replace the $1
placeholder, so the final string stored in the content
variable is
You clicked https://developer.mozilla.org.
Note: If you have multiple placeholders, you can provide them inside an array that is given to {{WebExtAPIRef("i18n.getMessage()")}} as the second parameter — [a, b, c]
will substitute $1
, $2
, and $3
, and so on.
Localized string selection
Locales can be specified using only a language code, like fr
or en
, or they may be further qualified with a region code, like en-US
or en-GB
, which describes a regional variant of the same basic language. When you ask the i18n system for a string, it will select a string using the following algorithm:
- if there is a
messages.json
file for the exact current locale, and it contains the string, return it. - Otherwise, if the current locale is qualified with a region (e.g.
en-US
) and there is amessages.json
file for the regionless version of that locale (e.g.en
), and that file contains the string, return it. - Otherwise, if there is a
messages.json
file for thedefault_locale
defined in themanifest.json
, and it contains the string, return it. - Otherwise return an empty string.
Take the following example:
- webextension-root-directory/
- _locales
- en-GB
- messages.json
{ "colorLocalised": { "message": "colour", "description": "Color." }, ... }
- messages.json
{ "colorLocalised": { "message": "color", "description": "Color." }, ... }
- messages.json
- fr
- messages.json
{ "colorLocalised": { "message": "couleur", "description": "Color." }, ...}
- messages.json
- en-GB
- _locales
Suppose the default_locale
is set to fr
, and the browser's current locale is en-GB
:
- If the add-on calls
getMessage("colorLocalised")
, it will return "colour". - If "colorLocalised" were not present in
en-GB
, thengetMessage("colorLocalised")
, would return "color", not "couleur".
Other points to note:
- Any number of consecutive dollar signs appearing in strings are replaced by the same number of dollar signs minus one. So, $$ > $, $$$ > $$, etc.
- When the locale file is read, tokens matching
/\$([a-z0-9_@]+)\$/i
are replaced with the matching value from the string's "replacements" object. These substitutions happen prior to processing any/\$\d/
tokens in the message. - When a locale string is used, tokens matching
/\$\d+/
are replaced with the replacements passed togetMessage()
. getMessage()
won't process calls with more than 9 placeholders/substitutions.
Predefined messages
The i18n module provides us with some predefined messages, which we can call in the same way as we saw earlier in {{anch("Calling message strings from manifests and extension CSS")}}. For example:
__MSG_extensionName__
Predefined messages use exactly the same syntax, except with @@
before the message name, for example
__MSG_@@ui_locale__
The following table shows the different available predefined messages:
Message name | Description |
---|---|
@@extension_id |
The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message. Note: You can't use this message in a manifest file. |
@@ui_locale |
The current locale; you might use this string to construct locale-specific URLs. |
@@bidi_dir |
The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Arabic. |
@@bidi_reversed_dir |
If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it's "ltr". |
@@bidi_start_edge |
If the @@bidi_dir is "ltr", then this is "left"; otherwise, it's "right". |
@@bidi_end_edge |
If the @@bidi_dir is "ltr", then this is "right"; otherwise, it's "left". |
Going back to our earlier example, it would make more sense to write it like this:
header { background-image: url(../images/__MSG_@@ui_locale__/header.png); }
Now we can just store our local specific images in directories that match the different locales we are supporting — en, de, etc. — which makes a lot more sense.
Let's look at an example of using @@bidi_*
messages in a CSS file:
body { direction: __MSG_@@bidi_dir__; } div#header { margin-bottom: 1.05em; overflow: hidden; padding-bottom: 1.5em; padding-__MSG_@@bidi_start_edge__: 0; padding-__MSG_@@bidi_end_edge__: 1.5em; position: relative; }
For left-to-right languages such as English, the CSS declarations involving the predefined messages above would translate to the following final code lines:
direction: ltr; padding-left: 0; padding-right: 1.5em;
For a right-to-left language like Arabic, you'd get:
direction: rtl; padding-right: 0; padding-left: 1.5em;
Testing out your WebExtension
Starting in Firefox 45, you can install WebExtensions temporarily from disk — see Loading from disk. Do this, and then try testing out our notify-link-clicks-i18n WebExtension. Go to one of your favourite websites and click a link to see if a notification appears reporting the URL of the clicked link.
Next, change Firefox's locale to one supported in the extension that you want to test.
- Open "about:config" in Firefox, and search for the
general.useragent.locale
preference. - Double click on the preference (or press Return/Enter) to select it, enter the language code for the locale you want to test, then click "OK" (or press Return/Enter). For example in our example extension, "en" (English), "de" (German), "nl" (Dutch), and "ja" (Japanese) are supported.
- Restart your browser to complete the change.
Note: This works to change the browser's locale, even if you haven't got the language pack installed for that language. You'll just the browser UI in your default language if this is the case.
Load the extension temporarily from disk again, then test your new locale:
- Visit "about:addons" again — you should now see the add-on listed, with its icon, plus name and description in the chosen language.
- Test your WebExtension again. In our example, you'd go to another website and click a link, to see if the notification now appears in the chosen language.
{{EmbedYouTube("R7--fp5pPGg")}}