We are working to improve the internationalization of JShelter. While the webextension API already contains APIs for internationalization, not everything works great. This post is written for webextension developers as well as JShelter developers working with strings presented to our users. Please see our other post if you are looking for ways to translate JShelter.
Translating manifest, CSS files, and JS files
Let us start with the simple and easy items. Adding your translated strings to the
manifest
and
CSS
files is really simple and straightforward. For example, if you want to provide a translatable
description of your extension, you would change your manifest.json
to contain a line like:
{
...
"description": "__MSG_extensionDescription__",
...
}
You add the description as extensionDescription
to your messages.json
:
"extensionDescription": {
"message": "Extension for increasing security and privacy level of the user.",
"description": "Description of the extension."
},
Similarly, you can localize CSS files like:
input:checked + .slider:before {
...
content:"__MSG_ShieldOnSlider__";
...
}
Afterward, you define ShieldOnSlider
, and you are done.
Translations in JavaScript files work a little bit differently, but it is easy to adapt your
JavaScript files. You just use the browser.i18n.getMessage
API. You provide the key in the messages.json
. This time, you can add parameters that can be utilized inside the messages.json
file. For example, you can pass a string that should appear inside the translated string:
browser.i18n.getMessage("defaultLevelSelection", default_level.level_text)
and the message.json
can contain something like:
"defaultLevelSelection": {
"message": "Default level ($levelName$)",
"description": "This text is displayed as the default level in the popup",
"placeholders": {
"levelName": {
"content": "$1",
"description": "Translated name of the default level used by the user",
"example": "Recommended, see the keys JSSL*Name like JSSL2Name"
}
}
},
If you like the placeholders, for example, because you read in the best practices that placeholder substitutions help specify parts that you do not want translated. You are out of luck if you want to add parameters to your manifest or CSS files. Luckily, JShelter does not need parameters in manifest and CSS files, and such a need is rare.
Translating HTML files
Webextensions contain HTML pages. For example, you can configure options_ui
or default_popup
in
manifest.json
. Even so, the internationalization page on MDN is quiet about the internationalization of HTML files. Let us have a look at what others do.
In essence, others add some markup to the HTML file and later process that markup in JS files. For
example, in JShelter, we add data-localize
attribute to each element we want to translate. The attribute
holds the key in the message.json
. For example, JShelter defines:
<label for="nbs-switch" data-localize="networkBoundaryShield">Network Boundary Shield</label>
We added a translation file
i18n_translate_dom.js
to all HTML pages with translatable elements. The script is
simple. It finds elements with the correct attributes and forwards the strings to the
browser.i18n.getMessage
API.
Still, one needs to take care of special sections in the pages, like templates.
The lack of a standard way to cope with HTML translations means that if you go to different webextension, they will likely have a similar script, but the details would be different. That is not optimal.
Language priorities
Webextension manifest file specifies default_locale
as the default language. This language is used
as the last resort to pick untranslated strings. Each language can have variants like en_US
and
en_UK
. Translators can create message.json
for variants and the base language (like en
).
Browsers select translations based on the algorithm documented on MDN. First, they look for the variant, then for the base language, and if they are not successful, they go to the default_locale
. The API returns an empty string if the default language does not contain the key.
Unfortunately, there is no way to tweak the algorithm. For example, some languages are similar. Czech speakers mostly understand Slovaks and vice-versa. However, JShelter cannot tweak the algorithm to look at the Czech translation if a Slovak translation is missing.
Handling plurals
Plurals in English are simple for cardinal numbers. There is just the singular and plural version. However, English has several forms for ordinal numbers, like 1st, 2nd, 3rd, 4th, or 21st. Other languages behave differently. In essence, almost every language has a specific handling of plurals.
Although there is the Intl.PluralRules()
API in JavaScript that is available to webextensions, there is no direct support for plural messages in the browser.i18n
API.
We considered creating several keys for the plural forms. For example, suppose that JShelter needs
to translate a string with message.json
key pluralExample
. We would create a code like:
let pluralCategory = (new Intl.PluralRules()).select(count);
let message = browser.i18n.getMessage("pluralExample" + pluralCategory, count);
At first sight, this is a straightforward solution. However, English defines only categories
"one" and "other." Imagine that the user uses a different locale with the category "few." If JShelter supports that language and that language
defines pluralExamplefew
, great, everything works. But imagine the key pluralExamplefew
is missing for that language. The string selection algorithm would search for pluralExamplefew
in English message.json
. However, that key would not be defined in English. So, the string selection algorithm would yield an empty string.
There are several solutions to the problem:
- Define all variants for the default locale language. We do not like that idea because it would be
confusing for the translators — why is there
pluralExamplefew
and other categories if onlyone
andother
are used in English? They might attempt to remove the unused variants. Moreover, we would unnecessarily overload the translators as they would need to provide the default translation even if that is the same as "other." Finally, translators of other languages would likely be confused and add their translations that would overwhelm them as well. - We could create code that handles the missing translations. For example, the program should check
that
message
is not empty. If empty, it would get the plural category for the English locale and the English translation. We might opt for this path in the future. - There are libraries like webextension-plural
that specialize in this task. However,
webextension-plural
has not been developed for several years.
As JShelter would benefit from plurals only in notifications of Network Boundary Shield that we might be forced to remove in Manifest v3, we decided not to write complex code to handle all exceptions and not to add additional dependencies. We decided to generate messages like "Blocked messages: 5".
Placeholders used in complex messages
Developers should not make assumptions about the composition of the sentences. However, some texts need special rules.
Consider the buttons for adding and removing exceptions for Network Boundary Shield and Fingerprint Detector. For example, the "Enable for the selected domains" button caption. We want to give the user a full and clear explanation; hence, the text is long. But we also want to emphasize the word "Enable." So the button caption uses HTML markup: <strong>Enable</strong> for the selected domains
.
We decided to use placeholders to describe to translators how to handle the translation:
"ButtonEnableForSelectedDomains": {
"message": "<strong>$ENABLE$</strong> $FORTHEDOMAIN$",
"description": "A button caption that can be used generically by JShelter, e.g., in the options; if necessary, edit the structure of the message but make sure to emphasize the enablement. Translate the placeholders.",
"placeholders": {
"enable": {
"content": "Enable",
"description": "Please translate"
},
"forTheDomain": {
"content": "for the selected domains",
"description": "Please translate"
}
}
},
This way, translators are free to change the structure of the message. For example, consider that the translator decides that an appropriate translation to Czech is "Vybrané domény <strong>povol</strong>". The word "enable" is translated as "povol". The translator can generate text like:
"ButtonEnableForSelectedDomains": {
"message": "$SELCTEDDOMAIN$ <strong>$ENABLE$</strong> ",
"placeholders": {
"enable": {
"content": "povol"
},
"selectedDomains": {
"content": "Vybrané domény"
}
}
},
All perfect until we decided to use Weblate to help with the translation, for example, to notify translators about new and changed strings that need translations. According to the docs, Weblate does support Webextension JSON. Weblate manual lists `placeholders as supported. The Weblate UI does not properly display placeholders. Translators do not see the description and example content of the placeholders. They cannot translate the content of the placeholder.
In this case, we could change the definition to something like:
"ButtonEnableForSelectedDomains": {
"message": "<strong>Enable</strong> for the selected domains",
"description": "A button caption that can be used generically by JShelter, e.g., in the options; if necessary, edit the structure of the message but make sure to emphasize the enablement."
},
However, we have other complex cases where dividing the message into placeholders makes sense. For example, we suggest different rules for translating a part of the message, like API names. Hence, we created scripts to help synchronize the strings between the repository and Weblate so that all strings can be translated in Weblate. A developer needs to run the synchronization scripts manually. The expected order is to first propagate changes from Grammarly to main (or other branch), and after that, propagate the changes from that branch in the repository to Weblate.
Additional reading
If you are a JShelter developer or are interested in helping JShelter's internationalization development, please read localization best practices for developers, MDN guide on webextension internationalization, and the i18n API documentation.