Skip to content

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

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:

uv run maid i18n extract

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:

uv run maid i18n update

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:

uv run maid i18n compile

5. Test

Test your translations in-game:

@language es
look around

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

uv run maid i18n init <language_code>

Example for Italian:

uv run maid i18n init it

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

uv run maid i18n stats

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

uv run maid i18n check

Checks for: - Missing translations - Fuzzy entries - Format string mismatches - Invalid placeholders

Initialize New Language

uv run maid i18n init <lang_code>

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

  1. Set language: @language <code>
  2. Perform actions that trigger the translated text
  3. 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:

uv run maid i18n pseudo

This generates a pseudo-translation that: - Expands text by ~30% (tests truncation) - Adds accents (tests character encoding) - Brackets strings (tests string extraction)

Example:

"Hello" -> "[Helloooo]"


Best Practices

For Translators

  1. Preserve Placeholders
  2. Keep {name}, {count}, etc. exactly as written
  3. Do not translate placeholder names

  4. Match Tone

  5. Keep informal/formal tone consistent with the original
  6. Match the game's style guide

  7. Consider Context

  8. Read surrounding code comments
  9. Test in-game to see how text is displayed

  10. Handle Length

  11. Some languages are longer (German +30%, French +20%)
  12. Abbreviate if necessary, but keep meaning

  13. Use Correct Plurals

  14. Follow the plural rules for your language
  15. Test with 0, 1, 2, 5, 21, etc.

  16. Mark Uncertain Translations

  17. Add #, fuzzy flag if unsure
  18. Leave a translator comment

For Developers

  1. Mark All User-Facing Strings

    from maid_engine.i18n import _
    
    # Good
    msg = _("You see nothing special.")
    
    # Bad - hardcoded
    msg = "You see nothing special."
    

  2. Use Named Placeholders

    # Good - clear for translators
    _("You gave {item} to {recipient}.")
    
    # Bad - ambiguous
    _("You gave {} to {}.")
    

  3. Avoid String Concatenation

    # Good
    _("The {color} sword is sharp.")
    
    # Bad - word order varies by language
    _("The ") + color + _(" sword is sharp.")
    

  4. Provide Context

    # Good - context for "back"
    pgettext("navigation", "back")
    pgettext("body_part", "back")
    
    # Bad - ambiguous
    _("back")
    

  5. Handle Plurals Correctly

    from maid_engine.i18n import ngettext
    
    # Good
    msg = ngettext(
        "You found {n} coin.",
        "You found {n} coins.",
        count
    ).format(n=count)
    
    # Bad
    msg = _("You found {} coin(s).").format(count)
    


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

  1. Fork the repository
  2. Find your language in packages/*/locale/<lang>/
  3. Edit the .po files
  4. 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