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
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/
locale/
en/LC_MESSAGES/
maid.po
maid.mo
es/LC_MESSAGES/
maid.po
maid.mo
...
maid-stdlib/
locale/
en/LC_MESSAGES/
stdlib.po
stdlib.mo
...
maid-classic-rpg/
locale/
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/locale/it/LC_MESSAGES/maid.po
packages/maid-stdlib/locale/it/LC_MESSAGES/stdlib.po
packages/maid-classic-rpg/locale/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 set_locale, _
def test_spanish_translation():
set_locale("es")
assert _("You see nothing special.") == "No ves nada especial."
def test_plural_translation():
set_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:
from maid_engine.i18n import lazy_gettext as _l
WELCOME_MESSAGE = _l("Welcome to MAID!") # Translated when used
Content Pack Translations¶
Content packs should include their own locale directories:
# In content pack __init__.py
from maid_engine.i18n import add_locale_path
add_locale_path(Path(__file__).parent / "locale")
Contributing Translations¶
Getting Started¶
- Fork the repository
- Find your language in
packages/*/locale/<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