Skip to content

Localization

Gopher speaks English and German. It has two mechanisms, because a manifest and a panel do not read text in the same way.

The panel: catalogs in lib/i18n/

FileWhat it holds
lib/i18n/en.tsThe English catalog. It is the reference for the other locales.
lib/i18n/de.tsThe German catalog.
lib/i18n/catalog.tsThe types. Catalog comes from the English catalog.
lib/i18n/index.tsThe locale of the user, t(), and the plural forms.
components/i18n.tsxI18nProvider and useI18n().
components/LanguagePicker.tsxThe control in the header of the panel.

A key is flat and has dots, for example status.signed-out.title. The dots make groups for a reader; the code does not walk them as a path. A value is a string, or a set of plural forms that Intl.PluralRules selects from. A {name} mark in a value is a placeholder. t() puts a value in its place, and it gives a number to Intl.NumberFormat first.

A component calls useI18n() and never reads a catalog. Thus one context value re-renders the whole panel when the user changes the language.

The completeness test is the compiler. lib/i18n/de.ts declares its constant as Catalog, and Catalog comes from the keys of the English catalog. Thus npm run compile fails if a translation has no value for a key, invents a key, or gives a string where a plural is necessary. The compiler cannot test what a machine runs. tests/i18n.test.ts covers that instead: which locale detectLocale reads from its sources, the guards, and how t() and the plural forms put values into the text.

The manifest: public/_locales/

A manifest cannot read a TypeScript file. browser.i18n is the only mechanism that can localize a manifest. Five strings live apart from the rest because of this. They are the name, the short name, and the description, plus the titles of the icon and the keyboard command. The default_locale key is en (see permissions and configuration).

Which language the panel shows

The setting is auto, en, or de, and it is in browser.storage.local with the other panel state. auto reads two sources, in this order:

  1. browser.i18n.getUILanguage(), the language of the browser interface. This is the same source that selects the text of the manifest, thus the name of the extension and the panel agree.
  2. navigator.languages, the content languages of the user. Gopher reads this source only when the first source names a language that Gopher does not have. A person can have an English interface and ask each page for German, and that is a statement about the language that the person reads.

Only the first subtag decides, thus de-AT gets the German catalog. If no source names a language that Gopher has, the answer is English.

Two limits, and neither has a repair

  • The text of the manifest obeys the browser, not the panel. The browser reads _locales/ before the extension starts, and browser.i18n has no way to select a different language. Thus a German panel in an English browser keeps an English name in the list of extensions. The panel is the surface that the user reads, thus the panel gets the choice.
  • The first frame can be English. browser.storage.local has no synchronous read. The first render uses the language of the browser, and the panel renders again when storage answers. This is visible only to a user who selected a language that is not the language of the browser. The alternative is an empty first frame for every user.

To add a language

  1. Copy lib/i18n/en.ts to lib/i18n/{tag}.ts and translate each value. Declare the constant as Catalog. The compiler then names each key that is missing.
  2. Add the tag to LOCALES and the catalog to CATALOGS in lib/i18n/index.ts.
  3. Add the name of the language, written in that language, to LOCALE_NAMES.
  4. Add public/_locales/{tag}/messages.json with the five manifest strings.
  5. Run npm run compile and npm run build.

A language with more plural categories than English (Polish, Russian, Arabic) needs no change to the English catalog. PluralMessage, in lib/i18n/catalog.ts, marks one and other as necessary, and the other categories of the CLDR as optional.

What is not in a catalog

The name “Gopher” is a product name and does not change (see principles). The message field of a failure (lib/messages.ts) is for a developer who reads the console; the user reads the status.* text of the banner.