Skip to main content

Internationalization (i18n) library for discord.py bots

Project description

discord-i18n

Internationalization (i18n) library for discord.py bots — inspired by ezcord.

  • One YAML (or JSON) file per locale — same layout as ezcord
  • load_languages() helper mirrors ezcord's style
  • Pluralization (one / other / zero)
  • {placeholder} interpolation + {.general} variable expansion
  • Fallback locale chain (en-USen)
  • @use_locale decorator injects a bound Translator into slash-command callbacks
  • I18nCog mixin — built-in self.t() in every cog
  • check_missing_keys() — catch missing translations at startup or in CI
  • localize_commands() — translate command names, descriptions, and options
  • Zero required runtime dependencies — discord.py and PyYAML are optional extras

Installation

pip install discord-i18n          # core only
pip install discord-i18n[yaml]    # + PyYAML (recommended)
pip install discord-i18n[discord] # + discord.py

Quick start

1. Create your locale files

locales/
├── en-US.yaml
├── de.yaml
└── fr.yaml

locales/en-US.yaml (ezcord-style YAML):

ping:
  response: "Pong! Latency: {latency}ms"

greet:
  hello: "Hello, {name}!"

errors:
  cooldown:
    one:   "Please wait {seconds} second."
    other: "Please wait {seconds} seconds."

locales/de.yaml:

ping:
  response: "Pong! Latenz: {latency}ms"

greet:
  hello: "Hallo, {name}!"

errors:
  cooldown:
    one:   "Bitte warte {seconds} Sekunde."
    other: "Bitte warte {seconds} Sekunden."

2. Set up the bot

import discord
from discord import app_commands
from discord_i18n import I18n, load_languages, use_locale

# ezcord-style: load all YAML files from the locales/ folder
i18n = I18n(
    default_locale="en-US",
    localizations=load_languages("locales/"),
)

class MyBot(discord.Client):
    def __init__(self):
        super().__init__(intents=discord.Intents.default())
        self.tree = app_commands.CommandTree(self)

    async def setup_hook(self):
        await self.tree.sync()

bot = MyBot()

3. Use in slash commands

With the @use_locale decorator

from discord_i18n import use_locale, Translator

@bot.tree.command()
@use_locale(i18n)
async def ping(interaction: discord.Interaction, t: Translator):
    latency = round(bot.latency * 1000)
    await interaction.response.send_message(t("ping.response", latency=latency))

Manually

from discord_i18n import locale_from_interaction

@bot.tree.command()
async def greet(interaction: discord.Interaction):
    t = i18n.get_translator(locale_from_interaction(interaction))
    await interaction.response.send_message(t("greet.hello", name=interaction.user.name))

Pluralization

@bot.tree.command()
@use_locale(i18n)
async def cooldown(interaction: discord.Interaction, t: Translator):
    seconds = 5
    msg = t("errors.cooldown", count=seconds, seconds=seconds)
    await interaction.response.send_message(msg)

YAML file structure

Keys can be nested (ezcord style) or dot-separated:

# Nested (ezcord style)
commands:
  ping:
    response: "Pong!"

# Dot-notation also works when calling t()
# i18n.t("commands.ping.response")

Pluralization

Use a mapping with one, other, and optionally zero:

items:
  count:
    zero:  "No items"
    one:   "{count} item"
    other: "{count} items"
t("items.count", count=0)   # → "No items"
t("items.count", count=1)   # → "1 item"
t("items.count", count=5)   # → "5 items"

general section

Values in general are available via {.key} anywhere in the same file — they are automatically expanded when the locale is loaded (no extra call needed):

general:
  support_server: "https://discord.gg/example"
  bot_name: "MyBot"

errors:
  help: "Join our support server: {.support_server}"
  footer: "Powered by {.bot_name}"

The general section stays in the data so you can still look it up; its values are simply pre-expanded into every string that references them.


API reference

load_languages(path)

Loads all .yaml / .yml / .json files from path and returns a {"locale": {...}} dict ready to pass to I18n.

localizations = load_languages("locales/")
# → {"en-US": {...}, "de": {...}, "fr": {...}}

A bare en.yaml is automatically expanded to both en-US and en-GB (same as ezcord).


I18n

I18n(
    default_locale="en-US",       # fallback when no locale is found
    localizations=load_languages("locales/"),  # ezcord-style pre-built dict
    # OR: locales_dir="locales/",  # load from directory (JSON + YAML)
    fallback_locale="en-US",      # secondary fallback
    missing_key_mode="key",       # "key" | "empty" | "raise"
)
Method Description
i18n.t(key, locale=..., count=..., **kwargs) Translate a key
i18n.get_translator(locale) Return a bound Translator callable
i18n.load() Load/reload from locales_dir
i18n.add_locale(locale, data) Register translations programmatically
i18n.available_locales List of loaded locale strings

Translator

A callable returned by i18n.get_translator(locale):

t = i18n.get_translator("de")
t("greet.hello", name="Welt")   # → "Hallo, Welt!"
t("errors.cooldown", count=3, seconds=3)

@use_locale(i18n)

Decorator that resolves the locale from the discord.Interaction and injects a bound Translator as the t parameter:

@tree.command()
@use_locale(i18n)
async def my_cmd(interaction: discord.Interaction, t: Translator):
    await interaction.response.send_message(t("some.key"))

locale_from_interaction(interaction) / locale_from_ctx(ctx)

Extract the locale string from an Interaction or a prefix-command Context.


I18nCog — built-in translation in every Cog

I18nCog is a mixin for discord.ext.commands.Cog that binds an I18n instance once and exposes self.t() everywhere in the cog:

from discord.ext import commands
from discord_i18n import I18n, load_languages, I18nCog

i18n = I18n(default_locale="en-US", localizations=load_languages("locales/"))


class GreetCog(I18nCog, commands.Cog, i18n=i18n):
    def __init__(self, bot):
        self.bot = bot

    @app_commands.command()
    async def greet(self, interaction: discord.Interaction):
        # locale is resolved automatically from the Interaction
        msg = self.t("greet.hello", interaction, name=interaction.user.name)
        await interaction.response.send_message(msg)

You can also set the instance after class definition (useful in large projects):

class GreetCog(I18nCog, commands.Cog):
    ...

GreetCog.set_i18n(i18n)

check_missing_keys — catch missing translations early

Call this on startup or in your test suite to find keys that exist in the default locale but are missing in other languages:

from discord_i18n import check_missing_keys

missing = check_missing_keys(i18n)
# {"de": ["errors.cooldown.zero"], "fr": ["ping.description"]}

if missing:
    print("Missing translations:", missing)

By default a WARNING is also logged for every affected locale. Pass warn=False to suppress logs and only use the return value.


Command localization

Discord lets you translate slash-command names, descriptions, and option names/descriptions per locale. discord-i18n applies these from the same YAML files.

1. Add a commands section to your locale files

# locales/de.yaml
commands:
  ping:
    name: "ping"
    description: "Latenz des Bots prüfen"

  greet:
    name: "begrüßen"
    description: "Eine Begrüßung senden"
    options:
      name:
        name: "name"
        description: "Der zu begrüßende Nutzer"

  # Subcommand group
  admin:
    description: "Admin-Befehle"
    ban:
      description: "Einen Nutzer bannen"
      options:
        user:
          name: "nutzer"
          description: "Der zu bannende Nutzer"

2. Call localize_commands before syncing

from discord_i18n import load_languages, localize_commands

localizations = load_languages("locales/")

class MyBot(discord.Client):
    def __init__(self):
        super().__init__(intents=discord.Intents.default())
        self.tree = app_commands.CommandTree(self)

    async def setup_hook(self):
        localize_commands(self.tree, localizations)  # ← before sync
        await self.tree.sync()

localize_commands sets name_localizations and description_localizations on every command / subcommand / option it finds in the commands block. The default_locale (default "en-US") values are also applied directly to the command object.


Publishing to PyPI

pip install build twine
python -m build
twine upload dist/*

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

discord_i18n-1.0.0.tar.gz (20.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

discord_i18n-1.0.0-py3-none-any.whl (21.5 kB view details)

Uploaded Python 3

File details

Details for the file discord_i18n-1.0.0.tar.gz.

File metadata

  • Download URL: discord_i18n-1.0.0.tar.gz
  • Upload date:
  • Size: 20.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for discord_i18n-1.0.0.tar.gz
Algorithm Hash digest
SHA256 9416bb4c0242accdd4bd0d291ad87d2e1f3cb2b6fec1517130fe57c61cbf2d85
MD5 17f2f7b5bbb1c1ecd2a587ede2bba792
BLAKE2b-256 a2156fb2b8dad0a519fd4af54f5ae0e8efa374ab62094afd76add6108e6f1c09

See more details on using hashes here.

File details

Details for the file discord_i18n-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: discord_i18n-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 21.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for discord_i18n-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 585c8c2954475d68bcd01c13f71b63f050061568d42b91957370ed4ca5162d9b
MD5 8909aadb61508690c7b23557628d259a
BLAKE2b-256 f01c5939eea652887bbc33a4ccfdf8fb4d5b222e178a7a7bed390fbb7e06f641

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page