i18n

How translation of the user interface works across all clients

Table of Contents

Overview

Every piece of user-interface text in Pankosmia is looked up by a key in one shared translation tree, so all clients (dashboard, settings, content handlers…) follow the same language. A key has three parts joined by colons: <category>:<section>:<name>, for example pages:core-settings:title. The value behind a key holds one translation per language code (en, fr, es, id…). When a key is not found, the key itself is displayed.

Structure of the translation tree

Category Content
pages One section per client, named after the client id, filled from the i18n object of its pankosmia_metadata.json. Example: pages:core-settings:title.
library Strings of shared libraries such as the RCL. Example: library:pankosmia-rcl:required_field.
components Strings of shared interface parts (header, framework messages…). Example: components:framework:no_entry_if_offline.
branding Identity of the product. Example: branding:software:name.
scripture Names of books and versifications.
munchers, crunchers, flavors, server, fonts Other categories of the template, used by specific content handlers and server features.

Where the strings come from

  • The template: the base file i18n.json of resource-core/templates. The desktop app build copies it to lib/templates/i18n.json. It holds library, components, branding, scripture…
  • Each client: the i18n object of its pankosmia_metadata.json. A key title in the client core-settings becomes pages:core-settings:title.
  • The product: i18nPatch.json and i18n-overrides.json in the globalBuildResources of the desktop app (see below).

How a client loads the translations

When a client starts, the SpSpa component of the RCL requests /api/i18n/flat from the server and keeps the result in the i18n context. The response is a flat map of key to text, for the language chosen by the user in the Settings client (stored through /api/settings/languages). Components read it with i18nRef.current and look up a key with doI18n.

Using translations in code

Both functions come from pankosmia-lib/i18n. doI18n(key, source) returns the text, or the key itself when it is missing. doInterpolatedI18n(key, values, source) fills the placeholders %1%, %2%… of a text with an array of strings.

import { useContext } from "react";
import { i18nContext } from "pankosmia-rcl";
import { doI18n, doInterpolatedI18n } from "pankosmia-lib/i18n";

const { i18nRef } = useContext(i18nContext);

doI18n("pages:core-settings:title", i18nRef.current);

// "greeting": { "en": "Hello %1%, you have %2% books" }
doInterpolatedI18n(
  "pages:my-client:greeting",
  ["Ada", "3"],
  i18nRef.current,
);

doInterpolatedI18n is stricter than doI18n: it throws when the key is missing, when a value is not a string, when the number of values differs from the number of placeholders, or when a placeholder is malformed.

Adding a string to a client

  1. Add the key to the i18n object of the client’s pankosmia_metadata.json, with at least the en translation.
  2. Use it in the code as pages:<client id>:<key>.
  3. Rebuild the client. Other languages can be added in the file or with the Localization Editor.
{
  "id": "my-client",
  "i18n": {
    "title": { "en": "My client", "fr": "Mon client" }
  }
}

Localization Editor

The client core-client-i18n-editor (id i18n-editor) edits the whole translation tree. It reads it from /api/i18n/raw and saves it with a POST to /api/i18n. It only lets you edit text values and add or remove language codes. It requires debug mode (require.debug is true), so it is listed in the Debug section of the menu.

Product-level customization

A desktop app can adjust texts without touching the clients, with two files in its globalBuildResources:

  • i18nPatch.json: applied at build time by buildCommon.js on top of templates/i18n.json. Every entry must already exist in the template, otherwise the build fails. In practice it only holds the product name (branding.software.name.en), which the app_setup script keeps equal to APP_NAME.
  • i18n-overrides.json: copied to app_resources/product/i18n-overrides.json. It has the same purpose, with the shape { "pages": { } } (empty by default).