telega_i18n

Internationalization (i18n) for the Telega Telegram Bot Library.

Translations live in a Catalog — one flat table of dotted keys per locale. Load them from TOML or JSON, install the middleware to resolve the active locale per update, then call t inside handlers.

Quick start

locales/en.toml:

greeting = "Hello, {name}!"

[cart]
title = "Your cart"

locales/ru.toml:

greeting = "Привет, {name}!"

[cart]
title = "Ваша корзина"
import gleam/option.{None}
import telega/router
import telega_i18n

pub fn build_router(catalog) {
  // `catalog` loaded once at startup, e.g. with `load_toml_dir`.
  router.new("bot")
  |> router.use_middleware(telega_i18n.middleware(
    catalog:,
    // Optional per-user override stored in the session. Return `None`
    // to fall back to the user's Telegram `language_code`.
    from: fn(_session) { None },
  ))
  |> router.on_command("start", greet)
}

fn greet(ctx, _command) {
  let msg = telega_i18n.t(ctx, "greeting", [#("name", "Lucy")])
  // -> "Hello, Lucy!" or "Привет, Lucy!" depending on the user's locale
  reply.with_text(ctx, msg)
}

Locale resolution

For every update the middleware picks the first available of:

  1. the session override returned by your from resolver,
  2. the sender’s Telegram language_code,
  3. the catalog’s default locale.

The resolved locale is stored in the process dictionary of the chat instance handling the update, so t needs only the key.

Fallback chains

Lookups walk a chain: the active locale, its base language ("en-US""en"), any explicit with_fallback entries, and finally the default locale. A missing key returns the key itself, so nothing ever crashes on a typo.

Pluralization

tn selects a CLDR plural category (one/few/many/other) and looks up "<key>.<category>". The count is injected as {count} automatically. English and Russian rules are built in.

[items]
one = "{count} item"
other = "{count} items"
telega_i18n.tn(ctx, "items", 5, [])
// -> "5 items"

Types

A collection of translations keyed by locale. Build it with new and one of the loaders, then hand it to middleware.

pub opaque type Catalog

Errors raised while loading translations.

pub type I18nError {
  ParseError(message: String)
  FileError(message: String)
}

Constructors

  • ParseError(message: String)

    The TOML/JSON content could not be parsed.

  • FileError(message: String)

    A file or directory could not be read.

Values

pub fn add_json(
  catalog catalog: Catalog,
  locale locale: String,
  content content: String,
) -> Result(Catalog, I18nError)

Parse a JSON document and merge it into the catalog under locale. Nested objects become dotted keys.

pub fn add_locale(
  catalog catalog: Catalog,
  locale locale: String,
  translations translations: dict.Dict(String, String),
) -> Catalog

Add (or merge into) a locale from an already-flattened map of dotted keys to templates. Later entries win on conflict.

pub fn add_toml(
  catalog catalog: Catalog,
  locale locale: String,
  content content: String,
) -> Result(Catalog, I18nError)

Parse a TOML document and merge it into the catalog under locale. Nested tables become dotted keys ([cart] title = "..."cart.title).

pub fn current_locale() -> option.Option(String)

The locale active in the current process, if enter (or the middleware) has run.

pub fn default_locale(catalog: Catalog) -> String

The catalog’s default locale.

pub fn enter(
  catalog catalog: Catalog,
  locale locale: String,
) -> Nil

Store the active catalog and locale for the current process. The middleware calls this before each handler; call it yourself only if you resolve locales outside the router.

pub fn leave() -> Nil

Clear the i18n state for the current process.

pub fn load_json_dir(
  catalog catalog: Catalog,
  dir dir: String,
) -> Result(Catalog, I18nError)

Load every *.json file in dir as a locale named after the file (en.json"en"), merging them into catalog.

pub fn load_toml_dir(
  catalog catalog: Catalog,
  dir dir: String,
) -> Result(Catalog, I18nError)

Load every *.toml file in dir as a locale named after the file (en.toml"en"), merging them into catalog.

pub fn locales(catalog: Catalog) -> List(String)

The list of locales the catalog knows about.

pub fn middleware(
  catalog catalog: Catalog,
  from from: fn(session) -> option.Option(String),
) -> fn(
  fn(bot.Context(session, error, dependencies), update.Update) -> Result(
    bot.Context(session, error, dependencies),
    error,
  ),
) -> fn(bot.Context(session, error, dependencies), update.Update) -> Result(
  bot.Context(session, error, dependencies),
  error,
)

Router middleware that resolves the active locale for every update and makes it available to t/tn.

from reads an optional per-user override out of the session (e.g. a language the user chose in settings); return None to fall back to the sender’s Telegram language_code and then the catalog default.

pub fn new(default_locale default_locale: String) -> Catalog

Create an empty catalog with the given default locale. The default is the last link of every fallback chain.

pub fn plural_category(locale: String, count: Int) -> String

Return the CLDR plural category ("one", "few", "many", "other") for count in locale. Russian and English have dedicated rules; every other locale uses the English rule.

pub fn resolve_locale(
  catalog catalog: Catalog,
  session session_locale: option.Option(String),
  update update_locale: option.Option(String),
) -> String

Resolve the active locale from a session override and the sender’s Telegram language_code, falling back to the catalog default.

pub fn t(
  ctx ctx: bot.Context(session, error, dependencies),
  key key: String,
  args args: List(#(String, String)),
) -> String

Translate key for the locale active in the current handler, interpolating {placeholder} values from args. Requires the middleware (or a manual enter); otherwise returns the key unchanged.

pub fn tn(
  ctx ctx: bot.Context(session, error, dependencies),
  key key: String,
  count count: Int,
  args args: List(#(String, String)),
) -> String

Pluralizing variant of t. See translate_count.

pub fn translate(
  catalog catalog: Catalog,
  locale locale: String,
  key key: String,
  args args: List(#(String, String)),
) -> String

Translate key in an explicit locale, interpolating {placeholder} values from args. Missing keys return the key unchanged.

Prefer t inside handlers; this is the pure building block, handy for tests and locale-agnostic call sites.

pub fn translate_count(
  catalog catalog: Catalog,
  locale locale: String,
  key key: String,
  count count: Int,
  args args: List(#(String, String)),
) -> String

Pluralizing variant of translate. Picks the CLDR category for count, looks up "<key>.<category>" (falling back to "<key>.other"), and injects count as {count}.

pub fn translate_current(
  key key: String,
  args args: List(#(String, String)),
) -> String

Translate using the active process locale without a context. t delegates here.

pub fn user_language_code(
  raw: types.Update,
) -> option.Option(String)

Extract the sender’s language_code from a raw update, if present.

pub fn with_command_translations(
  builder: telega.TelegaBuilder(session, error, dependencies),
  catalog catalog: Catalog,
  prefix prefix: String,
) -> telega.TelegaBuilder(session, error, dependencies)

Wire localized command descriptions from this catalog into a telega builder. Implies telega.with_auto_commands: the bot publishes its commands on start (default language first, then one setMyCommands(language_code:) per catalog locale).

Each command’s description is looked up at prefix <> command — command "start" with prefix: "commands." reads catalog key "commands.start", honoring the catalog’s fallback chains. A missing key keeps the description the command was registered with in the router.

let catalog =
  i18n.new("en")
  |> i18n.add_toml("en", en_toml)
  |> i18n.add_toml("ru", ru_toml)

telega.new_for_polling(api_client:)
|> telega.with_router(router)
|> i18n.with_command_translations(catalog, prefix: "commands.")
|> telega.init_for_polling()
pub fn with_fallback(
  catalog catalog: Catalog,
  locale locale: String,
  chain chain: List(String),
) -> Catalog

Register an explicit fallback chain for a locale. These locales are tried (in order) after the active locale and its base language but before the default locale.

Search Document