async-zulip-bot-sdk

Internationalization (i18n)

The async-zulip-bot-sdk includes a lightweight, JSON-based internationalization system that allows you to easily support multiple languages in your bots.

Overview

The i18n system:

File Structure

Translations are organized as follows:

project_root/
├── bot_sdk/
│   └── i18n/
│       ├── en.json        # SDK defaults (English)
│       └── zh.json        # SDK defaults (Chinese)
│
└── bots/
    ├── my_bot/
    │   ├── __init__.py
    │   ├── bot.yaml       # Bot config with language: zh
    │   └── i18n/          # Bot-specific translations (optional)
    │       ├── en.json
    │       └── zh.json
    │
    └── other_bot/
        ├── __init__.py
        ├── bot.yaml       # language: en
        └── i18n/
            └── zh.json

Configuration

Per-bot language is set in bot.yaml:

# bots/my_bot/bot.yaml
owner_user_id: 123
language: zh
role_levels:
  user: 1
  admin: 50
  bot_owner: 200
settings: {}

Usage

In BaseBot subclasses

from bot_sdk import BaseBot, Message, CommandSpec, CommandArgument

class MyBot(BaseBot):
    def register_commands(self):
        # Descriptions can be translated at registration time
        self.command_parser.register_spec(
            CommandSpec(
                name="hello",
                description=self.tr("Greet a user"),  # Uses i18n
                args=[
                    CommandArgument(
                        name="name",
                        type=str,
                        required=True,
                        description=self.tr("The name to greet")
                    ),
                ],
                handler=self.handle_hello,
            )
        )

    async def handle_hello(self, invocation, message, bot):
        name = invocation.args.get("name")
        # Translate user-facing messages
        reply = self.tr("Hello {name}!", name=name)
        await self.send_reply(message, reply)

    async def on_message(self, message: Message) -> None:
        # Translate any user-visible text
        await self.send_reply(
            message,
            self.tr("Thank you for your message!")
        )

Command handler pattern

class MyBot(BaseBot):
    async def handle_command(self, invocation, message, bot):
        try:
            # Validate and process
            result = self.do_something(invocation.args)
        except ValueError as e:
            # Error messages translated
            await self.send_reply(
                message,
                "❌ " + self.tr("Invalid input: {error}", error=str(e))
            )
            return

        # Success message translated
        await self.send_reply(
            message,
            "✅ " + self.tr("Operation successful!")
        )

Creating Translation Files

JSON format

Translations are simple key-value JSON files:

{
  "Hello {name}!": "¡Hola {name}!",
  "Greet a user": "Saludar a un usuario",
  "The name to greet": "El nombre para saludar",
  "Invalid input: {error}": "Entrada inválida: {error}",
  "Operation successful!": "¡Operación exitosa!",
  "Thank you for your message!": "¡Gracias por tu mensaje!"
}

Tips

Bot-specific example

For bots/my_bot/i18n/es.json:

{
  "Greet a user": "Saludar a un usuario",
  "The name to greet": "El nombre para saludar",
  "Hello {name}!": "¡Hola {name}!",
  "Thank you for your message!": "¡Gracias por tu mensaje!"
}

Built-in translations

The SDK provides default translations for built-in commands and UI strings:

English (bot_sdk/i18n/en.json):

Chinese (bot_sdk/i18n/zh.json):

You can override any of these in your bot’s own i18n/{language}.json file.

The I18n class

For advanced use cases, you can instantiate I18n directly:

from bot_sdk.i18n import I18n

# Load Chinese translations, searching multiple paths
i18n = I18n(
    language="zh",
    search_paths=[
        "bots/my_bot/i18n",  # Bot-specific translations
        "bot_sdk/i18n"        # SDK defaults
    ],
    default_language="en"     # Fallback to English
)

# Translate a string
greeting = i18n.translate("Hello {name}!", name="Alice")
print(greeting)  # Output: 你好 Alice!

The build_i18n_for_bot helper

BaseBot uses a helper function to automatically locate and load translations:

from bot_sdk.i18n import build_i18n_for_bot

# Called automatically by BaseBot._init_i18n()
i18n = build_i18n_for_bot(
    language="zh",
    bot_module_name="bots.my_bot"  # Automatically finds bot directory
)

This function:

  1. Resolves the bot module path
  2. Searches <bot_dir>/i18n for translations
  3. Falls back to bot_sdk/i18n
  4. Returns a ready-to-use I18n instance

Hot reload

The built-in !reload command (admin-only) reinitializes the i18n system without restarting the bot:

User: !reload
Bot: ✅ Configuration and translations reloaded.

This is useful when:

CommandParser and i18n

CommandParser automatically receives a translator function from BaseBot:

# In BaseBot.__init__:
self.command_parser = CommandParser(
    prefixes=self.command_prefixes,
    enable_mentions=self.enable_mention_commands,
    auto_help=self.auto_help_command,
    translator=lambda s: self.tr(s),  # Pass bot's tr method
)

This means:

Best practices

  1. Translate user-visible text only

    • Use i18n for messages users will see
    • Keep logs, debug messages, and developer-facing text in English
  2. Use consistent key naming

    • Use the English text as the key
    • Avoid changing keys; instead, add new entries
  3. Provide fallbacks

    • Always include English translations in SDK i18n files
    • This ensures bot continues working even if a translation is missing
  4. Test multi-language support

    • Test your bot with language: en, language: zh, etc.
    • Verify placeholders substitute correctly
    • Check that !reload properly switches languages
  5. Organize bot-specific translations

    • Create bots/my_bot/i18n/ directory with en.json and other language files
    • Keep bot-specific translations separate from SDK defaults

Example: Multi-language bot

from bot_sdk import BaseBot, Message, CommandSpec, CommandArgument, run_bot

class MultiLangBot(BaseBot):
    def register_commands(self):
        self.command_parser.register_spec(
            CommandSpec(
                name="fortune",
                description=self.tr("Tell a fortune"),
                handler=self.handle_fortune,
            )
        )

    async def handle_fortune(self, invocation, message, bot):
        fortunes = {
            "en": "Today is a good day!",
            "zh": "今天是个好日子!",
        }
        fortune = fortunes.get(self.language, "Today is a good day!")
        await self.send_reply(message, fortune)

    async def on_message(self, message: Message) -> None:
        await self.send_reply(
            message,
            self.tr("Thanks for the message, {name}!", name=message.sender_full_name)
        )

if __name__ == "__main__":
    run_bot(MultiLangBot)

With bot.yaml set to language: zh, this bot will respond in Chinese. Use !reload to switch languages dynamically.

Troubleshooting

Translation not appearing?

Placeholders not substituting?

Want to add a new language?