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:
- the session override returned by your
fromresolver, - the sender’s Telegram
language_code, - 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
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 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 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,
)
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()