@yaebal/i18n
per-chat locale with ctx.t / ctx.locale / ctx.changeLanguage. missing keys fall back to the default locale, then to the key
itself. powers useTranslation() in the morda jsx layer.
install
pnpm add @yaebal/i18nusage
pass your locale dictionaries to i18n() and install it with .install(). the plugin reads the stored locale for the current chat before your
handlers run, so ctx.t is ready immediately.
import { Bot } from "@yaebal/core";
import { i18n } from "@yaebal/i18n";
const bot = new Bot(process.env.BOT_TOKEN!)
.install(i18n({
defaultLocale: "en",
locales: {
en: {
welcome: "Hello {name}!",
bye: "Goodbye",
},
ru: {
welcome: "Привет {name}!",
bye: "До свидания",
},
},
}));
bot.command("start", async (ctx) => {
await ctx.reply(ctx.t("welcome", { name: ctx.from!.first_name }));
});
bot.command("lang", async (ctx) => {
await ctx.changeLanguage("ru");
await ctx.reply(ctx.t("welcome", { name: ctx.from!.first_name }));
});
bot.start();api
| export | signature | description |
|---|---|---|
i18n | (options: I18nOptions<L>) => Plugin<Context, I18nControls> | creates the i18n plugin |
I18nOptions | interface | options passed to i18n() |
I18nControls | interface | what the plugin adds to the context (t, locale, changeLanguage) |
Dict | Record<string, string> | a single locale's key → template map |
TFn | (key: string, params?: Record<string, unknown>) => string | the type of ctx.t |
I18nOptions<L>
| field | type | required | description |
|---|---|---|---|
defaultLocale | L | yes | locale used when no stored locale exists and for fallback lookups |
locales | Record<L, Dict> | yes | all translation dictionaries keyed by locale code |
storage | StorageAdapter<string> | no | where to persist each chat's locale. defaults to MemoryStorage |
getKey | (ctx: Context) => string | undefined | no | storage key for the update. defaults to ctx.chat?.id?.toString() |
I18nControls (added to ctx)
| property | type | description |
|---|---|---|
t | TFn | translate a key; {placeholder} tokens are replaced from params |
locale | string | the active locale code for this update |
changeLanguage | (locale: string) => Promise<void> | switch locale for this update and persist it; subsequent ctx.t calls in the same handler use the new locale immediately |
persistent storage
the default MemoryStorage is lost on restart. pass any StorageAdapter<string> (same interface as @yaebal/session) to
persist locales across restarts.
import { i18n } from "@yaebal/i18n";
import { type StorageAdapter } from "@yaebal/session";
// any StorageAdapter<string> keeps locale across restarts
class RedisLocaleStorage implements StorageAdapter<string> {
async get(key: string): Promise<string | undefined> { /* ... */ }
async set(key: string, value: string): Promise<void> { /* ... */ }
async delete(key: string): Promise<void> { /* ... */ }
}
bot.install(i18n({
defaultLocale: "en",
locales: { en: { hi: "Hi" }, ru: { hi: "Привет" } },
storage: new RedisLocaleStorage(),
}));per-user locale
override getKey to partition by user instead of chat.
bot.install(i18n({
defaultLocale: "en",
locales: { en: { hi: "Hi" }, ru: { hi: "Привет" } },
// use the user id as the key instead of the chat id
getKey: (ctx) => ctx.from?.id?.toString(),
}));fallback behaviour
// "ru" locale has no "bye" key → falls back to "en"
// "en" has no "missing" key → returns the key itself
ctx.t("bye"); // → "Goodbye" (en fallback)
ctx.t("missing"); // → "missing" (key fallback)interpolation uses
changeLanguage takes effect immediately within the current handler — calls to
updates without a chat (e.g.
{placeholder} syntax, not template
literals. every {key} token in the string is replaced with the matching
entry from params via String(value). changeLanguage takes effect immediately within the current handler — calls to
ctx.t after await ctx.changeLanguage("ru") in the same middleware use
the new locale. the next update loads the persisted locale from storage. updates without a chat (e.g.
poll updates) where getKey returns undefined always use the defaultLocale and
never persist.