Skip to content

Internationalization (I18n)

I18n workflow

Localize bot replies by detecting locale, loading dictionaries, and keeping translations close to handlers.

Locale detection

Read Telegram language metadata and fall back to a default locale.

Translation helper

Use `ctx.i18n` from handlers after middleware registration.

Template variables

Interpolate values into translated strings.

The built-in I18n helper stores dictionaries in memory and injects ctx.i18n.t() into handlers.

Quick Start

ts
import { Bot, I18n } from 'vibegram';

const bot = new Bot(process.env.BOT_TOKEN!);
const i18n = new I18n('en');

i18n.loadLocale('en', {
    welcome: 'Welcome {name}!',
    help: 'Available commands: /start, /help',
});

i18n.loadLocale('id', {
    welcome: 'Selamat datang {name}!',
    help: 'Perintah tersedia: /start, /help',
});

bot.use(i18n.middleware());

Using Translations

ts
bot.command('start', async ctx => {
    const text = ctx.i18n!.t('welcome', {
        name: ctx.from?.first_name ?? 'Guest',
    });

    await ctx.reply(text);
});

bot.command('help', ctx => ctx.reply(ctx.i18n!.t('help')));

If a key is missing, t() returns the key itself.

Template Variables

Use {variable} placeholders:

ts
i18n.loadLocale('en', {
    order_confirmed: 'Order #{id} confirmed. Total: ${amount}.',
});

ctx.i18n!.t('order_confirmed', { id: '1234', amount: '99.99' });
// "Order #1234 confirmed. Total: $99.99."

Placeholders are simple string substitutions. Escape values yourself before placing them inside formatted HTML or Markdown messages.

How Language Detection Works

The middleware reads ctx.from?.language_code, keeps the first two characters, and falls back to the default locale passed to new I18n(defaultLang).

text
Telegram language en-US -> locale "en"
Telegram language id    -> locale "id"
Missing locale          -> default locale

Load from JSON Files

ts
import { readFileSync } from 'node:fs';

function loadLocaleFile(lang: string) {
    return JSON.parse(readFileSync(`./locales/${lang}.json`, 'utf8'));
}

i18n.loadLocale('en', loadLocaleFile('en'));
i18n.loadLocale('id', loadLocaleFile('id'));

Example locales/en.json:

json
{
    "welcome": "Welcome {name}!",
    "choose_menu": "Choose a menu:",
    "generic_error": "Something went wrong. Please try again later."
}

Manual Language Override

Store the user's language choice in session and override ctx.i18n after the built-in i18n middleware runs.

ts
bot.use(session({ initial: () => ({ lang: undefined as string | undefined }) }));
bot.use(i18n.middleware());

bot.use(async (ctx, next) => {
    const lang = ctx.session?.lang;
    if (lang) {
        ctx.i18n = {
            locale: lang,
            t: (key, placeholders) => i18n.t(lang, key, placeholders),
        };
    }

    await next();
});

bot.action(/^lang_(\w+)$/, async ctx => {
    const lang = ctx.match?.[1];
    if (!lang) return;

    ctx.session.lang = lang;
    await ctx.answerCbQuery('Language updated');
    await ctx.reply(i18n.t(lang, 'welcome', { name: ctx.from?.first_name ?? 'Guest' }));
});

Released under the ISC License.