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.