Internationalization (i18n)

Internationalization (i18n)

The SDK carries the translation engine, the generator tooling, and the base
catalog of SDK-owned strings. An app brings its own catalog. At build time
the catalogs merge into a single enum and one set of string tables per
firmware image.

Layout

PocketMage_SDK/
  pocketmage_i18n/
    pocketmage_i18n.h / .cpp          # runtime engine (I18n class)
    languages/                        # base catalog (SDK-owned strings)
      en/pocketmage_i18n.po
      fr/pocketmage_i18n.po
      fr/pocketmage_i18n.aliases
      es/...  de/...
    tools/
      gen_i18n.py                     # merger, generator, and build hook
    tests/
      test_gen_i18n.py

An app adds one catalog directory of its own:

<app>/src/i18n/
  en/pocketmage_i18n.po
  fr/pocketmage_i18n.po
  fr/pocketmage_i18n.aliases
  ...

A catalog directory has one subdirectory per language, named by the language
code. en is required and defines that catalog’s string order.

Catalog format

Standard gettext .po. msgctxt is the StringID enum name; msgid is the
English text; msgstr is the translation. The en file carries a
# Languages: directive in its leading comment block:

# PocketMage OS English Translations
# Languages: en fr es de

msgctxt "STR_SYS_HELLO"
msgid "Hello"
msgstr "Hello"

That directive, in the base catalog, is where the language list and order
come from. Each catalog’s en file defines the order of the strings that
catalog contributes; the catalog’s other languages must list the same
msgctxts, with the same msgids, in the same order.

A language missing from a catalog falls back to that catalog’s English text,
so an English-only app catalog is valid without translating anything.

Aliases

pocketmage_i18n.aliases maps folded, lowercase user input to a StringID.
Optional, only defined for non-English languages:

annuler -> STR_KB_APP_CANCEL

Folding strips diacritics (Paramètres -> parametres). Aliases from all
catalogs concatenate per language; a key that folds to the same value twice
is a build error.

Ownership

Owner Namespaces
SDK base catalog STR_KB_* (incl. STR_KB_APP_*, STR_KB_USB_*, STR_KB_INIT_*), STR_GRID_*, STR_SD_*, STR_WIFI_*, STR_SYS_*, STR_TOUCHPAD_*, STR_OLED_*, STR_FRAME(S)_*, STR_INFO/GOOD/SAVED_*, STR_DAY_*, STR_MONTH_*
App catalog STR_TERM_*, STR_ONBOARD_*, STR_SSH_*, STR_SETTINGS_*, STR_CAL_*, STR_UTILS_*, STR_LOCK_*, STR_HOME_*, STR_TASKS_*, STR_FILEWIZ_*, STR_APPLOADER_*, STR_COMM_*, STR_USB_*, STR_LEX_*, STR_JOURNAL_*, STR_TXT_*, STR_GOTO_*, STR_SAVE_*

msgctxt names are globally unique across the merged catalogs; reusing one
the SDK already defines is a build error. SDK code only references SDK
namespaces, so merging an app catalog never changes how SDK code behaves.

Ownership follows the consumer: base if SDK code references the string, app
otherwise. That keeps a bare SDK checkout building standalone (its base
catalog contains every string its own modules use), and the merge appends the
app catalog after the base, so app strings never move base enum values. The
indexed helper ranges all sit in the base; their relative order is fixed and
never perturbed by an app catalog.

Generator

tools/gen_i18n.py merges the catalogs in command-line order and writes
pocketmage_i18n_gen.h and pocketmage_i18n_gen.cpp:

python3 pocketmage_i18n/tools/gen_i18n.py \
    --catalog pocketmage_i18n/languages \
    --catalog ../app/src/i18n \
    --out-dir <build-dir>

--catalog is repeatable; the first catalog is the base. With no arguments,
the tool generates the SDK’s own base catalog into pocketmage_i18n/, so a
bare SDK checkout builds standalone.

The merge keeps base strings first and appends each later catalog in turn.
Every enum value equals its index in the merged order, so the layout is
stable: adding an app catalog never changes SDK string values.

Validation

The generator exits non-zero and aborts the build on any of:

  • a missing # Languages: directive in the base en file, or en not first;
  • a msgctxt that is not [A-Z][A-Z0-9_]*;
  • a duplicate msgctxt within a catalog or across catalogs;
  • a non-en file whose msgctxt set, order, or msgids differ from that
    catalog’s en;
  • a duplicated folded alias;
  • the indexed helper ranges missing, inverted, overlapping, or out of order.

Indexed helper ranges

Four ranges are indexed by the runtime helpers (appName(), dayName(),
monthName(), kbAppName()). They must stay contiguous blocks, in this
order:

Range Members
STR_KB_APP_CANCEL … STR_KB_APP_LOADER app list in the keyboard app switcher
STR_GRID_TXT … STR_GRID_LOADER app list in the launcher grid
STR_DAY_SUNDAY … STR_DAY_SATURDAY weekdays
STR_MONTH_JAN … STR_MONTH_ERR months (ERR is the last member)

Their heads are emitted with explicit = N enum values as documentation of
intent; the values always equal the index in the merged order.

Build integration

The generator doubles as the build hook. Point extra_scripts at it in the
app’s platformio.ini; it reads its settings from project options (or
POCKETMAGE_I18N_CATALOGS / POCKETMAGE_I18N_OUT_DIR, or the SDK base
catalog as a last resort):

extra_scripts = pre:lib/PocketMage_SDK/pocketmage_i18n/tools/gen_i18n.py

custom_i18n_catalogs =
    lib/PocketMage_SDK/pocketmage_i18n/languages
    src/i18n
custom_i18n_out_dir = .pio/build/${PIOENV}/i18n

As a hook it regenerates only when a .po/.aliases file is newer than the
generated tables, so incremental builds stay cheap.

custom_i18n_out_dir must be ahead of any stale copy on the include path.
The engine includes "pocketmage_i18n_gen.h"; the generated files are build
artifacts, never committed.

Adding a string or a language

  • String: add the msgctxt/msgid/msgstr to the owning catalog’s en
    file and to each language file, in the same position relative to that
    catalog’s other strings. App strings go in the app catalog.
  • Language: append the code to the base en file’s # Languages:
    directive and add that language’s directory to each catalog that translates
    it. Never reorder existing languages: the index is the value stored in NVS.

Testing

python3 -m unittest discover -s pocketmage_i18n/tests

The suite covers catalog merging, language fallback, alias merging, and every
validation failure above, using small in-test catalogs.