Internationalization (i18n)
Horus Runtime provides a complete internationalization system that allows plugins, workflows, and UI components to be localized into multiple languages. The system is built on GNU gettext, the industry-standard translation framework, and uses pybabel for managing translation workflows.
How Translation Works
The i18n system operates in three phases:
- Extraction: The system scans the
horus-runtimesource code for strings marked for translation and extracts them into a master catalog file (.potfile) - Translation: Translators work on language-specific
.pofiles, which contain the original strings and their translations - Compilation: The
.pofiles are compiled into binary.mofiles that the runtime loads at execution time
When your code requests a translation, the runtime looks up the string in the compiled catalog for the current locale and returns the appropriate translation. If no translation exists, the original string is returned as a fallback.
Using Translations in Your Code
Basic Translation
Import the translation function and use it to mark strings that should be translated:
from horus_runtime.i18n import tr as _
# Simple string translation
greeting = _("Hello, world!")
error_message = _("File not found")
status = _("Processing complete")
The _() function is the standard gettext convention. It marks the string for extraction and performs the translation lookup at runtime. Use it for any user-facing text: messages, error descriptions, etc.
Pluralization
Many languages have complex plural rules. English has two forms (singular/plural), but other languages may have three, four, or even more. The i18n system handles this automatically:
from horus_runtime.i18n import tr as _
# Provide both singular and plural forms
count = 5
message = _("{n} file", "{n} files", n=count)
# Result: "5 files"
count = 1
message = _("{n} file", "{n} files", n=count)
# Result: "1 file"
# Works with any countable noun
tasks = _("{n} task completed", "{n} tasks completed", n=total)
errors = _("{n} error found", "{n} errors found", n=error_count)
The system automatically selects the correct plural form based on the language's rules. For example, Polish has different forms for 1, 2-4, and 5+. You provide the English forms, and translators provide all necessary forms for their language.
Variable Substitution
Embed variables into translated strings using named placeholders:
from horus_runtime.i18n import tr as _
# Single variable
username = "Alice"
message = _("Welcome, {user}!", user=username)
# Result: "Welcome, Alice!"
# Multiple variables
file_name = "database.db"
file_size = "2.3 MB"
status = _("Downloaded {name} ({size})", name=file_name, size=file_size)
# Result: "Downloaded database.db (2.3 MB)"
# Combining plurals and substitution
items = 42
category = "documents"
summary = _("Found {n} {type}", "Found {n} {type}s", n=items, type=category)
# Result: "Found 42 documents"
Named placeholders ensure translators can reorder variables to match their language's grammar. For example, English says "Welcome, Alice" but another language might require "Alice, welcome".
Context-Specific Translations
Some words translate differently depending on context. For example, "Open" can mean "open a file" or "not closed":
from horus_runtime.i18n import tr as _
# Use descriptive strings that provide context
button_label = _("Open file") # Better than just _("Open")
status = _("File is open") # Different context, different translation
# For very short strings, add context in comments
# Translators: This is a button label for opening files
action = _("Open")
The clearer your original strings, the better the translations will be.
Babel Configuration
Babel is a Python library that automates the extraction, merging, and compilation of translation files. The Horus Runtime uses a babel.cfg configuration file that tells Babel where to find translatable strings and how to extract them.
Initial Setup
Before extracting translations, the locale directory structure must exist:
mkdir -p src/horus_runtime/locale/
This creates the root directory where all translation files will be stored. Each language will get its own subdirectory following the gettext convention: locale/LANG/LC_MESSAGES/.
Extracting Translatable Strings
When you add new _() calls to your code, you need to extract them into the master catalog:
make babel-extract
This command does the following:
- Scans all Python files in
src/horus_runtime/for_()function calls - Extracts the strings and their source locations (file and line number)
- Creates or updates
src/horus_runtime/locale/messages.pot
The .pot file (Portable Object Template) is the master catalog. It contains all extractable strings in your source code with no translations—just the original English text. This file is never edited manually; it's regenerated from source code.
After extraction, the .pot file looks like this:
#: src/horus_runtime/workflow.py:45
msgid "Workflow started"
msgstr ""
#: src/horus_runtime/workflow.py:67
#, python-format
msgid "{n} task"
msgid_plural "{n} tasks"
msgstr[0] ""
msgstr[1] ""
Each entry shows where the string was found in the source code (#: comment), the original string (msgid), and empty translation slots (msgstr).
Adding a New Language
Adding a language creates a language-specific catalog based on the master .pot file.
Step 1: Create Language Files
make babel-add LANG=es
Replace es with the appropriate ISO 639-1 language code:
es- Spanishfr- Frenchde- Germanja- Japanesept_BR- Brazilian Portuguese (use underscore for regional variants)zh_CN- Simplified Chinese
This command:
- Creates the directory structure:
src/horus_runtime/locale/es/LC_MESSAGES/ - Generates
horus_runtime.poby copying the.potfile - Sets the language metadata in the
.poheader
The LC_MESSAGES directory name is a gettext convention. All applications using gettext expect translations in this exact directory name.
Step 2: Edit Translations
Open the generated .po file at src/horus_runtime/locale/es/LC_MESSAGES/horus_runtime.po. You'll see entries like this:
#: src/horus_runtime/workflow.py:45
msgid "Workflow started"
msgstr ""
Fill in the msgstr field with the translation:
#: src/horus_runtime/workflow.py:45
msgid "Workflow started"
msgstr "Flujo de trabajo iniciado"
For plurals, fill in all plural forms required by the language:
#: src/horus_runtime/workflow.py:67
#, python-format
msgid "{n} task"
msgid_plural "{n} tasks"
msgstr[0] "{n} tarea"
msgstr[1] "{n} tareas"
The .po file header contains important metadata:
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
The Plural-Forms field defines the plural rule for the language. Spanish has two forms: one for n=1, another for everything else. This rule is language-specific and must be correct for pluralization to work.
Step 3: Validate Translations
After editing, verify that your translations are syntactically correct:
make babel-check
This command:
- Compiles all
.pofiles to.mofiles (binary format) - Reports any syntax errors (malformed entries, missing translations, etc.)
- Shows statistics: how many strings are translated vs. untranslated
If compilation succeeds, the .mo files are created in the same directory as the .po files. These binary files are what the runtime actually loads—they're optimized for fast lookups at runtime.
Updating Existing Translations
As you develop, you'll add new strings, modify existing ones, or remove obsolete ones. The translation files need to stay synchronized with the source code.
Step 1: Extract and Update
make babel-refresh
This command does two things:
- Runs
babel-extractto regenerate the.potfile from current source code - Runs
babel updateto merge changes into all existing.pofiles
The merge process is intelligent:
- New strings are added with empty
msgstrfields (need translation) - Modified strings are marked as "fuzzy" (need review)
- Unchanged strings keep their existing translations
- Removed strings are commented out (in case you need to reference them)
Step 2: Review Changes
Open each .po file and look for:
New entries (empty translations):
msgid "New feature enabled"
msgstr ""
Provide the translation.
Fuzzy entries (marked with #, fuzzy):
#, fuzzy
msgid "File saved successfully"
msgstr "Archivo guardado"
The "fuzzy" flag indicates the original string changed slightly, and the existing translation might need adjustment. Review the translation, update it if necessary, and remove the #, fuzzy comment.
Obsolete entries (commented out):
#~ msgid "Old message"
#~ msgstr "Mensaje antiguo"
These can be deleted or kept for reference.
Step 3: Compile
After updating translations, recompile:
make babel-check
This verifies syntax and regenerates the .mo files that the runtime will load.
Translation Management Commands
The Makefile provides these commands for managing translations:
make babel-extract
Extracts translatable strings from the source code into the master .pot file. Does not modify existing .po files.
make babel-update
Updates all language .po files using the current .pot template. Merges new and changed strings into each .po file without marking modified entries as fuzzy.
make babel-refresh
Runs both babel-extract and babel-update to regenerate the .pot file and update all .po files in one step.
make babel-check
Validates all .po files for missing or fuzzy translations. Fails if any untranslated or fuzzy strings are found. Compiles all .po files to .mo files if validation passes.
make babel-add LANG=xx
Initializes a new language catalog for the given language code (replace xx with the ISO code). Creates the necessary directory and .po file for the new language.
Directory Structure
The complete translation directory structure:
src/horus_runtime/
locale/
messages.pot # Master template (auto-generated)
es/
LC_MESSAGES/
horus_runtime.po # Spanish translations
horus_runtime.mo # Compiled binary
fr/
LC_MESSAGES/
horus_runtime.po # French translations
horus_runtime.mo # Compiled binary
Test with Long Translations
Some languages (German, Finnish) produce translations 30-50% longer than English. Make sure the UI can handle longer text without breaking layouts.