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
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
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:
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).
Telegram language en-US -> locale "en"
Telegram language id -> locale "id"
Missing locale -> default localeLoad from JSON Files
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:
{
"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.
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' }));
});