What should a JShelter developer know about internationalization?

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 only one and other 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.