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 baseenfile, orennot first; - a
msgctxtthat is not[A-Z][A-Z0-9_]*; - a duplicate
msgctxtwithin a catalog or across catalogs; - a non-
enfile whosemsgctxtset, order, ormsgids differ from that
catalog’sen; - 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/msgstrto the owning catalog’sen
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
enfile’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.