Как сделать Telegram-бота на русском, узбекском и английском
Практика4 минРедакция Innovialabs
Коротко
Берите language_code только как подсказку, сразу давайте выбрать язык и храните выбор в базе, все тексты держите в файлах gettext или Fluent, а описания команд задавайте для каждого языка через setMyCommands.
Короткий ответ
Мультиязычный бот собирается из четырёх частей:
- Определение языка — при первом
/startберитеlanguage_codeпользователя как значение по умолчанию. - Явный выбор — сразу предлагайте сменить язык кнопками и сохраняйте выбор в базе.
- Файлы переводов — все тексты выносите из кода в gettext (
.po) или Fluent (.ftl). - Локализованные команды — описания команд и самого бота задаются отдельно для каждого языка через Bot API.
Шаг 1. Язык из language_code
В каждом апдейте Telegram передаёт поле from.language_code — язык интерфейса приложения у пользователя: ru, en, uz, иногда с регионом, например en-GB. Учитывайте три вещи:
- поле необязательное и может отсутствовать;
- оно показывает язык приложения, а не язык, на котором человеку удобно читать: многие в Узбекистане держат Telegram на русском или английском;
- значит, это только подсказка, а не окончательное решение.
Порядок выбора языка: сохранённый выбор → language_code, если язык поддерживается → язык по умолчанию.
Шаг 2. Выбор языка пользователем
При первом запуске покажите приветствие и inline-кнопки «Русский», «O‘zbekcha», «English». Подписи кнопок пишите на самом языке: человек найдёт свой вариант, даже если остальной текст ему непонятен. Добавьте команду /language и пункт в настройках, чтобы язык можно было сменить в любой момент.
Выбор храните в базе вместе с профилем пользователя, а не в памяти процесса — иначе после перезапуска бот «забудет» язык. Пример middleware для aiogram 3:
from aiogram.utils.i18n import I18n, I18nMiddleware
class UserLocaleMiddleware(I18nMiddleware):
async def get_locale(self, event, data) -> str:
user = data.get("event_from_user")
if user is None:
return self.i18n.default_locale
saved = await get_saved_locale(user.id) # чтение из вашей БД
if saved:
return saved
code = (user.language_code or "").split("-")[0]
if code in self.i18n.available_locales:
return code
return self.i18n.default_locale
i18n = I18n(path="locales", default_locale="ru", domain="messages")
UserLocaleMiddleware(i18n).setup(dp)
Сразу после выбора ответьте уже на новом языке — например, внутри with i18n.use_locale(code):.
Шаг 3. Файлы переводов: gettext или Fluent
| gettext (.po / .mo) | Fluent (.ftl) | |
|---|---|---|
| Формат | исходная строка и её перевод | ключ и сообщение |
| Множественное число | правила в заголовке .po, функция ngettext | варианты по категориям CLDR прямо в сообщении |
| Инструменты | Babel, Poedit, большинство сервисов переводов | меньше инструментов, файлы проще читать |
| В aiogram | встроенный модуль aiogram.utils.i18n | через сторонние пакеты |
Цикл работы с gettext через Babel:
pybabel extract -k _ -k __ -o locales/messages.pot .
pybabel init -i locales/messages.pot -d locales -D messages -l uz
pybabel compile -d locales -D messages
После изменений в коде вместо init используйте pybabel update. В Fluent множественное число описывается в самом сообщении:
cart-items = { $count ->
[one] В корзине { $count } товар
[few] В корзине { $count } товара
*[many] В корзине { $count } товаров
}
Это важно именно для нашей тройки: в русском три формы, в английском две, а в узбекском существительное после числа не меняется («5 ta mahsulot»).
Правила, которые экономят время:
- не склеивайте фразы из кусков — используйте плейсхолдеры, порядок слов в языках разный;
- давайте переводчику контекст: «Открыть» в меню и в статусе заказа могут переводиться по-разному;
- не переводите
callback_data— это технические идентификаторы; - для reply-кнопок сравнивайте текст через ленивый перевод, например
F.text == __("Каталог"), а лучше используйте inline-кнопки сcallback_data.
Шаг 4. Локализованные команды
Метод setMyCommands принимает language_code. Telegram показывает список команд для языка интерфейса пользователя, а если такого нет — список по умолчанию. Так же работают setMyName, setMyDescription и setMyShortDescription.
from aiogram.types import BotCommand
COMMANDS = {
"ru": [BotCommand(command="start", description="Начать"),
BotCommand(command="language", description="Сменить язык")],
"uz": [BotCommand(command="start", description="Boshlash"),
BotCommand(command="language", description="Tilni o‘zgartirish")],
"en": [BotCommand(command="start", description="Start"),
BotCommand(command="language", description="Change language")],
}
async def setup_commands(bot):
await bot.set_my_commands(COMMANDS["en"]) # список по умолчанию
for lang, commands in COMMANDS.items():
await bot.set_my_commands(commands, language_code=lang)
Если пользователь выбрал в боте язык, отличный от языка приложения, задайте ему команды персонально через scope=BotCommandScopeChat(chat_id=...).
Частые ошибки
- тексты прямо в хендлерах — добавление языка превращается в поиск по всему коду;
- кнопки не проверены на всех языках — длинные подписи обрезаются;
- даты, суммы и телефоны форматируются одинаково для всех — используйте форматирование по локали, например через Babel;
- уведомления менеджерам уходят на языке клиента, а не получателя.