Saltar al contenido principal

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:

  1. Extraction: The system scans the horus-runtime source code for strings marked for translation and extracts them into a master catalog file (.pot file)
  2. Translation: Translators work on language-specific .po files, which contain the original strings and their translations
  3. Compilation: The .po files are compiled into binary .mo files 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:

  1. Scans all Python files in src/horus_runtime/ for _() function calls
  2. Extracts the strings and their source locations (file and line number)
  3. 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 - Spanish
  • fr - French
  • de - German
  • ja - Japanese
  • pt_BR - Brazilian Portuguese (use underscore for regional variants)
  • zh_CN - Simplified Chinese

This command:

  1. Creates the directory structure: src/horus_runtime/locale/es/LC_MESSAGES/
  2. Generates horus_runtime.po by copying the .pot file
  3. Sets the language metadata in the .po header

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:

  1. Compiles all .po files to .mo files (binary format)
  2. Reports any syntax errors (malformed entries, missing translations, etc.)
  3. 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:

  1. Runs babel-extract to regenerate the .pot file from current source code
  2. Runs babel update to merge changes into all existing .po files

The merge process is intelligent:

  • New strings are added with empty msgstr fields (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.