Title: What should a JShelter developer know about internationalization?
Date: 2023-08-31 15:00
Series: i18n
series_index: 2

We are working to improve the internationalization of JShelter. While the webextension API already contains [APIs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n) 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](/i18n/) if you are looking for ways to translate JShelter.

[TOC]

### Translating manifest, CSS files, and JS files

Let us start with the simple and easy items. Adding your translated strings to the
[manifest](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization#internationalizing_manifest.json)
and
[CSS](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization#locale-dependent_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:

```JSON
{
    ...
    "description": "__MSG_extensionDescription__",
    ...
}
```

You add the description as `extensionDescription` to your `messages.json`:

```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:

```CSS
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](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n/getMessage). 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:

```JS
browser.i18n.getMessage("defaultLevelSelection", default_level.level_text)
```

and the `message.json` can contain something like:

```JSON
	"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](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization#hardcoded_substitution).
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](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization) is quiet about the internationalization of HTML files. Let us have a [look at what others do](https://stackoverflow.com/questions/25467009/internationalization-of-html-pages-for-my-google-chrome-extension).

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:

```HTML
<label for="nbs-switch" data-localize="networkBoundaryShield">Network Boundary Shield</label>
```

We added a [translation file](https://codeberg.org/JShelter/webextension/src/commit/bff8ce9c69ca28c1952898125983429c1f7f8a32/common/i18n_translate_dom.js#L43)
`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](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n/getMessage).

Still, one needs to take care of special sections in the pages, like [templates](https://codeberg.org/JShelter/webextension/src/commit/bff8ce9c69ca28c1952898125983429c1f7f8a32/common/i18n_translate_dom.js#L43).

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](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization#localized_string_selection). 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](https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html). In
essence, almost every language has a specific handling of plurals.

Although there is the `Intl.PluralRules()` [API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) 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:

```JS
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](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization#localized_string_selection) 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](https://github.com/joelspadin/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](https://mozilla-l10n.github.io/documentation/localization/dev_best_practices.html#splitting-and-composing-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:

```JSON
	"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
&lt;strong&gt;povol&lt;/strong&gt;". The word "enable" is translated as "povol". The translator can generate
text like:

```JSON
	"ButtonEnableForSelectedDomains": {
		"message": "$SELCTEDDOMAIN$ <strong>$ENABLE$</strong> ",
		"placeholders": {
			"enable": {
				"content": "povol"
			},
			"selectedDomains": {
				"content": "Vybrané domény"
			}
		}
	},
```

All perfect until we decided to use [Weblate](https://hosted.weblate.org/projects/jshelter/webextension/) to help with the translation, for example, to notify translators about new and changed strings that need translations. According to the [docs](https://docs.weblate.org/en/latest/formats/webextension.html), Weblate does support Webextension JSON. [Weblate manual](https://readthedocs.org/projects/weblate/downloads/pdf/weblate-3.9.1/) lists `placeholders as supported. The Weblate UI does not properly display placeholders. Translators [do not see the description](https://github.com/WeblateOrg/weblate/issues/3398) 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:

```JSON
	"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](https://codeberg.org/JShelter/webextension/src/branch/main/tools/i18n) 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](https://mozilla-l10n.github.io/documentation/localization/dev_best_practices.html), [MDN guide on webextension internationalization](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization), and the [i18n API documentation](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n).
