Internationalization (i18n) Guide¶
This guide explains how to add translations to MAID, including the translation workflow, PO file format, and CLI tools available for managing translations.
Table of Contents¶
- Overview
- Translation Workflow
- PO File Format
- Adding a New Language
- Translating Strings
- CLI Tools
- In-Game Commands
- Testing Translations
- Best Practices
- For Developers
Overview¶
MAID uses GNU gettext for internationalization. The system supports:
- Player-facing text: Messages, descriptions, UI text
- System messages: Errors, warnings, notifications
- Content pack text: Items, NPCs, room descriptions
Current Status: The i18n framework is fully implemented — locale management,
@languagecommand,TranslatableTextcomponent, PO/MO tooling, and extraction CLI all work. However, core gameplay commands inmaid-stdlib(look,move,get,drop,inventory) andmaid-classic-rpg(combat, magic, crafting) do not yet use_()/gettext(). All user-facing strings in those commands are still hardcoded English. Currently only the@languagecommand and@set(forTranslatableTextfields on rooms/objects) integrate with the i18n system. Wrapping gameplay strings with_()is planned as a future enhancement. The PO file examples below show the intended format once commands are i18n-enabled.
Supported Languages¶
Currently supported languages: - English (en) - Default - Spanish (es) - French (fr) - German (de) - Portuguese (pt) - Japanese (ja) - Chinese Simplified (zh_CN) - Korean (ko)
Directory Structure¶
packages/
maid-engine/
locales/
en/LC_MESSAGES/
maid.po
maid.mo
es/LC_MESSAGES/
maid.po
maid.mo
...
maid-stdlib/
locales/
en/LC_MESSAGES/
stdlib.po
stdlib.mo
...
maid-classic-rpg/
locales/
en/LC_MESSAGES/
classic.po
classic.mo
...
Translation Workflow¶
1. Extract Translatable Strings¶
Run the extraction tool to find all translatable strings:
This scans the codebase for _() and ngettext() calls and updates the POT (template) file.
2. Update Language Files¶
Merge the new strings into existing PO files:
This adds new strings and marks removed strings as obsolete.
3. Translate¶
Edit the PO files to add translations (see PO File Format).
4. Compile¶
Compile PO files to MO (binary) format:
5. Test¶
Test your translations in-game:
PO File Format¶
PO files are plain text files containing translation pairs.
Basic Structure¶
# MAID Translation File
# Copyright (C) 2024 MAID Team
# This file is distributed under the MIT license.
#
msgid ""
msgstr ""
"Project-Id-Version: MAID 0.1.0\n"
"Report-Msgid-Bugs-To: translations@example.com\n"
"POT-Creation-Date: 2024-01-15 10:00+0000\n"
"PO-Revision-Date: 2024-01-15 12:00+0000\n"
"Last-Translator: Jane Doe <jane@example.com>\n"
"Language-Team: Spanish <es@example.com>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/maid_engine/commands/look.py:45
msgid "You see nothing special."
msgstr "No ves nada especial."
#: src/maid_engine/commands/inventory.py:23
msgid "Your inventory is empty."
msgstr "Tu inventario esta vacio."
Entry Components¶
#. Translator comment (from source code)
#: src/file.py:123
#, fuzzy, python-format
msgctxt "inventory"
msgid "You have {count} item."
msgid_plural "You have {count} items."
msgstr[0] "Tienes {count} objeto."
msgstr[1] "Tienes {count} objetos."
| Component | Description |
|---|---|
#. |
Comment from the source code for translators |
#: |
Source file location(s) |
#, |
Flags (fuzzy, python-format, etc.) |
msgctxt |
Context for disambiguation |
msgid |
Original (English) string |
msgid_plural |
Plural form of original |
msgstr |
Translated string |
msgstr[N] |
Plural translations |
Flags¶
| Flag | Meaning |
|---|---|
fuzzy |
Translation needs review (auto-generated or changed source) |
python-format |
Contains Python format placeholders |
no-python-format |
Explicitly not Python format |
Adding a New Language¶
1. Create Language Directory¶
Example for Italian:
This creates:
packages/maid-engine/locales/it/LC_MESSAGES/maid.po
packages/maid-stdlib/locales/it/LC_MESSAGES/stdlib.po
packages/maid-classic-rpg/locales/it/LC_MESSAGES/classic.po
2. Configure Language Metadata¶
Edit the PO file header:
msgid ""
msgstr ""
"Language: it\n"
"Language-Team: Italian <it@example.com>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
3. Plural Forms¶
Different languages have different plural rules. Common examples:
# English, Spanish, Italian, Portuguese (2 forms)
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
# French (2 forms, 0 is singular)
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
# Russian (3 forms)
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
# Japanese, Chinese, Korean (1 form)
"Plural-Forms: nplurals=1; plural=0;\n"
# Polish (3 forms)
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
Translating Strings¶
Simple Strings¶
#: src/maid_stdlib/commands/look.py:45
msgid "You see nothing special."
msgstr "No ves nada especial."
Strings with Placeholders¶
Preserve placeholders exactly:
#: src/maid_stdlib/commands/say.py:23
#, python-format
msgid "{player} says: {message}"
msgstr "{player} dice: {message}"
Important: Do not translate placeholder names!
Plural Strings¶
#: src/maid_stdlib/commands/inventory.py:56
#, python-format
msgid "You have {count} gold coin."
msgid_plural "You have {count} gold coins."
msgstr[0] "Tienes {count} moneda de oro."
msgstr[1] "Tienes {count} monedas de oro."
Context-Dependent Strings¶
The same English word may need different translations:
#: src/maid_stdlib/commands/get.py:34
msgctxt "action"
msgid "get"
msgstr "coger"
#: src/maid_engine/network/http.py:45
msgctxt "http"
msgid "get"
msgstr "obtener"
Multi-Line Strings¶
msgid ""
"This is a long description that spans\n"
"multiple lines. Each line is a separate\n"
"quoted string."
msgstr ""
"Esta es una descripcion larga que abarca\n"
"multiples lineas. Cada linea es una cadena\n"
"citada separada."
Special Characters¶
| Character | Escape |
|---|---|
| Newline | \n |
| Tab | \t |
| Quote | \" |
| Backslash | \\ |
CLI Tools¶
Extract Strings¶
# Extract from all packages
uv run maid i18n extract
# Extract from specific package
uv run maid i18n extract --package maid-stdlib
# Extract with verbose output
uv run maid i18n extract --verbose
Update PO Files¶
# Update all languages
uv run maid i18n update
# Update specific language
uv run maid i18n update --lang es
# Update and remove obsolete entries
uv run maid i18n update --no-obsolete
Compile to MO¶
# Compile all
uv run maid i18n compile
# Compile specific language
uv run maid i18n compile --lang es
# Compile with statistics
uv run maid i18n compile --statistics
Translation Statistics¶
Output:
Translation Statistics:
Language Translated Fuzzy Untranslated Complete
en 500/500 0 0 100%
es 485/500 10 5 97%
fr 450/500 25 25 90%
de 420/500 30 50 84%
ja 380/500 20 100 76%
...
Check for Issues¶
Checks for: - Missing translations - Fuzzy entries - Format string mismatches - Invalid placeholders
Initialize New Language¶
In-Game Commands¶
@language¶
Set your character's language preference.
@language es # Switch to Spanish
@language en # Switch to English
@language list # List available languages
@language # Show current language
Testing with @language¶
# Switch to Spanish
@language es
# Test commands
look
inventory
say Hello, world!
# Check a specific message
@language test "You see nothing special."
Testing Translations¶
Manual Testing¶
- Set language:
@language <code> - Perform actions that trigger the translated text
- Verify output is correct
Automated Testing¶
# In tests
from maid_engine.i18n import get_translator, _
def test_spanish_translation():
translator = get_translator()
translator.set_current_locale("es")
assert _("You see nothing special.") == "No ves nada especial."
def test_plural_translation():
translator = get_translator()
translator.set_current_locale("es")
from maid_engine.i18n import ngettext
assert ngettext(
"You have {count} coin.",
"You have {count} coins.",
1
).format(count=1) == "Tienes 1 moneda."
Pseudo-Localization¶
Test for layout issues with pseudo-localization:
This generates a pseudo-translation that: - Expands text by ~30% (tests truncation) - Adds accents (tests character encoding) - Brackets strings (tests string extraction)
Example:
Best Practices¶
For Translators¶
- Preserve Placeholders
- Keep
{name},{count}, etc. exactly as written -
Do not translate placeholder names
-
Match Tone
- Keep informal/formal tone consistent with the original
-
Match the game's style guide
-
Consider Context
- Read surrounding code comments
-
Test in-game to see how text is displayed
-
Handle Length
- Some languages are longer (German +30%, French +20%)
-
Abbreviate if necessary, but keep meaning
-
Use Correct Plurals
- Follow the plural rules for your language
-
Test with 0, 1, 2, 5, 21, etc.
-
Mark Uncertain Translations
- Add
#, fuzzyflag if unsure - Leave a translator comment
For Developers¶
-
Mark All User-Facing Strings
-
Use Named Placeholders
-
Avoid String Concatenation
-
Provide Context
-
Handle Plurals Correctly
For Developers¶
Using i18n in Code¶
from maid_engine.i18n import _, ngettext, pgettext
# Simple translation
message = _("Welcome to MAID!")
# With placeholders
message = _("Hello, {name}!").format(name=player.name)
# Plurals
message = ngettext(
"You have {n} item.",
"You have {n} items.",
count
).format(n=count)
# With context
message = pgettext("direction", "back") # vs pgettext("body", "back")
Translator Comments¶
# Translators: This appears when the player looks at an empty room
msg = _("You see nothing special.")
# Translators: {name} is the player's name, {item} is the item name
msg = _("{name} picked up {item}.")
Lazy Translation¶
For module-level strings that need to be translated at access time rather than definition time, use a deferred pattern:
from maid_engine.i18n import _
# Define as a callable that defers translation
WELCOME_MESSAGE = lambda: _("Welcome to MAID!") # Translated when called
Note:
lazy_gettextis not currently exported frommaid_engine.i18n. Use the lambda pattern above or call_()at the point of use instead.
Content Pack Translations¶
Content packs should include their own locales/ directories. Register the locale path during pack initialization:
# In content pack __init__.py or on_load()
from pathlib import Path
from maid_engine.i18n import get_translator
translator = get_translator()
translator.load_directory(Path(__file__).parent / "locales", recursive=True)
Note:
load_directory()is accessed via theTranslatorinstance fromget_translator(), not as a top-level function frommaid_engine.i18n.
Contributing Translations¶
Getting Started¶
- Fork the repository
- Find your language in
packages/*/locales/<lang>/ - Edit the
.pofiles - Submit a pull request
Translation Guidelines¶
- Follow the style guide for your language
- Test all translations in-game
- Include screenshots in PR for review
Reporting Issues¶
If you find translation issues: 1. Note the exact English text 2. Note the incorrect translation 3. Suggest the correct translation 4. Open an issue or PR
See Also¶
- GNU gettext Manual
- Building Commands Reference - In-game commands
- Hot Reload Guide - Reload translations during development