Skip to content

Working with Translations

Relevant source files

This page explains the internationalization (i18n) system used throughout the PiBot CNC Pendant firmware. It details how to add new translatable strings using the X-macro pattern, the structure of language files, the architecture of the ESP3DTranslationService, and the process for runtime language switching.

The system is designed for high memory efficiency, using memory-mapped LNG1 blobs for active translations and a flash-resident English fallback table, ensuring zero DRAM usage for string storage.


The translation system uses a triple-expansion X-macro pattern. A single definition of a translation string is used to generate the ESP3DLabel enumeration, a lookup map for file-based IDs, and the default English string values stored in flash .rodata.

Sources:main/display/esp3d_translations_list.h#36-47main/display/esp3d_translations_init.cpp#26-37main/modules/translations/esp3d_translation_service.cpp#132-139


The system relies on the ESP3D_TR_DEF(label, id, text) macro. It is expanded in different contexts:

  1. Enum Generation: In esp3d_translations_list.h, it creates the ESP3DLabel enum class used in C++ code. main/display/esp3d_translations_list.h#37-47
  2. English Fallback Table: In esp3d_translations_init.cpp, it populates kEnglishTexts[], a const char* const array stored in flash. main/display/esp3d_translations_init.cpp#31-37
  3. ID Mapping: In esp3d_translations_id_map.h, it creates a mapping between the enum and the stable file_id (represented as l_X) used in language packs. main/modules/translations/esp3d_translation_service.cpp#132-139

To prevent conflicts between core UI elements and specific CNC firmware features, IDs are partitioned:

Sources:main/display/esp3d_translations_init.cpp#31-37tools/language_packs/template.lng#6-9


Language files exist in two forms: raw .lng text files for development and binary LNG1 blobs stored in the ui_resources partition for production.

The source files use an INI-style format with [translations] and [info] sections. tools/language_packs/template.lng#11-196

[translations]
l_0=English
l_1=Settings
l_500=Change Tool
[info]
code=en
name=English (default)

Sources:tools/language_packs/template.lng#3-14tools/language_packs/ui_fr-fr.lng#1-77

For runtime performance, translations are compiled into binary blobs via lang_packs.py. The layout consists of:

  1. Header: 40-byte LangBlobHeader including magic 'LNG1', language code, and entry count. main/modules/translations/esp3d_translation_service.cpp#51-57
  2. Index: Sorted array of LangBlobEntry containing the file_id and string offset. main/modules/translations/esp3d_translation_service.cpp#58-61
  3. Pool: NUL-terminated UTF-8 strings. main/modules/translations/esp3d_translation_service.cpp#48

Sources:main/modules/translations/esp3d_translation_service.cpp#45-65


The primary interface is esp3dTranslationService.translate(ESP3DLabel label, ...). It supports printf-style formatting and returns a pointer directly to the mmapped flash or .rodata when no formatting is required (zero-copy). main/modules/translations/esp3d_translation_service.h#64-67

// Example: Setting a label text with translation
lv_label_set_text(status_label, esp3dTranslationService.translate(ESP3DLabel::loading));

For applications requiring raw strings (e.g., the [ESP460]DUMP command), getRawTranslation() is used to preserve format specifiers. main/modules/translations/esp3d_translation_service.h#71main/core/commands/esp460.cpp#61

The ESP3DTranslationService singleton manages the lifecycle of translation data:

  1. Scanning: getLanguagesList() scans partition slots (identified by ESP3D_LANG_SLOT_IDS_INIT) for available LNG1 blobs. main/modules/translations/esp3d_translation_service.h#81main/modules/translations/esp3d_translation_service.cpp#67-68
  2. Loading: begin() reads the language setting (ESP3DSettingIndex::esp3d_ui_language) and maps the corresponding flash partition blob. main/modules/translations/esp3d_translation_service.cpp#180-205
  3. Lookup: lookup(label) first checks the active binary blob using a binary search; if the label is missing or no blob is loaded, it falls back to esp3d_translations_english(label). main/modules/translations/esp3d_translation_service.cpp#278-305

Sources:main/modules/translations/esp3d_translation_service.cpp#180-205main/modules/translations/esp3d_translation_service.h#64-71


To add a new string to the firmware:

  1. Identify Category: Choose Core (0-499), System (500-999), or Target (1000+).
  2. Edit .inc File: Add a new ESP3D_TR_DEF at the end of the appropriate section.
  • Example: ESP3D_TR_DEF(my_new_status, 550, "Status Active")
  1. Use in UI: Reference the new enum member: ESP3DLabel::my_new_status.
  2. Update Translations: Add l_550=Status Actif to .lng files.
UI ComponentESP3DTranslationService.inc Definition FileDeveloperUI ComponentESP3DTranslationService.inc Definition FileDeveloperExpansion generates ESP3DLabel::my_labelAdd ESP3D_TR_DEF(my_label | 200 | "Hello")translate(ESP3DLabel::my_label)fileIdForLabel(my_label) -> 200Binary Search for ID 200 in LNG1 blobReturn "Bonjour" (e.g. French pack)Return "Hello" (English fallback from .rodata)

Sources:main/display/esp3d_translations_list.h#37-47main/modules/translations/esp3d_translation_service.cpp#132-139main/modules/translations/esp3d_translation_service.cpp#278-305


Sources:main/modules/translations/esp3d_translation_service.cpp#83-120main/display/esp3d_translations_init.cpp#45-51main/core/commands/esp460.cpp#51-77main/display/cnc/fluidnc/screens/esp3d_screen_type.cpp#134-136