Многоязычный бот — это не «прогнать тексты через автоперевод и положить в словарь». Это инфраструктурное решение: где хранить переводы, как определять язык пользователя, что делать с плюрализацией, как обновлять тексты без релизов. Разберём по частям.
Где брать язык пользователя
Telegram отдаёт language_code в User объекте — двухбуквенный ISO-639-1 код («ru», «en», «de»). Это подсказка, а не приговор: пользователь может пользоваться ботом на русском, имея английскую раскладку Telegram. Поэтому:
- На старте берём
language_codeкак дефолт. - Сразу даём кнопку «🇷🇺 RU / 🇬🇧 EN» для явного выбора.
- Сохраняем выбор в БД пользователя — это финальный источник истины.
- В настройках всегда позволяем сменить язык.
language_code иногда приходит расширенный («pt-br», «zh-hans») — оставляйте только основной язык, если у вас нет региональных переводов.
Где хранить переводы
Три уровня зрелости:
- Файлы в репозитории. JSON или YAML по языкам:
locales/ru.json,locales/en.json. Подходит для большинства проектов. - gettext (.po/.mo). Стандарт мира перевода, есть инструменты (POEdit, Crowdin, Lokalise). Для команды переводчиков и больших объёмов.
- БД или CMS. Когда тексты меняет маркетинг без релиза. Минус — нужно кэшировать.
Для типового бота на 200–500 строк JSON в репо плюс нормальная структура ключей закрывают потребности.
Структура ключей
Хорошие ключи — иерархические и контекстные. Не «error1», «error2», а lead.error.phone_invalid, menu.tariffs.title. Это спасает от коллизий и помогает переводчику понять контекст.
{
"lead": {
"title": "Оставьте заявку",
"error": {
"phone_invalid": "Проверьте номер телефона"
}
}
}
Не складывайте интерполяции в ключ. Параметры — через шаблон: "greeting": "Привет, {name}!". Переводчик видит структуру и не путается.
Плюрализация
Русский, польский, чешский имеют сложные правила множественных форм. Английский — только две формы (one/other), русский — три (one/few/many).
Стандарт — CLDR Plural Rules. Большинство фреймворков (i18next, gettext, формат Fluent от Mozilla) поддерживают их из коробки.
"orders": {
"one": "У вас {count} заказ",
"few": "У вас {count} заказа",
"many": "У вас {count} заказов"
}
Не пытайтесь «угадать» правила if-else’ами. Используйте библиотеку, иначе на каком-нибудь «21 день» бот скажет «дней» вместо «день».
Fallback
Что делать, если ключа нет в текущем языке? Варианты:
- Вернуть ключ как есть — для разработки видно, что не переведено.
- Откатиться на дефолтный язык (обычно английский или русский) — для продакшена.
- Логировать пропуски — собирать список «надо перевести».
В продакшене стандартная схема: пытаемся найти на языке пользователя, если нет — на дефолте, если нет — пишем ключ и логируем. Это убережёт от пустых сообщений.
Динамический контент
Тексты статичные (меню, кнопки, ошибки) — в файлах переводов. Динамические (названия товаров, описания услуг) — в БД с полем-словарём: title_ru, title_en или JSON-полем title: { ru: "...", en: "..." }.
Контент-менеджер заполняет оба поля при создании. Если переводчика нет — оставляем поле пустым, и при выдаче делаем fallback на основной язык.
Кнопки и команды
Нюанс Telegram: команды бота (/start, /help) переводятся через setMyCommands с параметром language_code. Можно настроить разные команды для русских и английских пользователей.
Кнопки клавиатур и callback’ов — обычные строки в локализованных меню. Используйте callback_data как идентификатор действия (buy_premium), а не как локализованный текст — иначе после смены языка старые кнопки перестанут работать.
Тестирование переводов
Минимальный набор автотестов:
- Все ключи присутствуют во всех языках (lint на parity).
- Плейсхолдеры (
{name}) совпадают между языками. - Длина переведённого текста не выходит за лимиты Telegram (4096 символов сообщение, 64 — кнопка).
- Smoke-тест воронки на каждом языке.
Crowdin/Lokalise валидируют структуру и плейсхолдеры на стороне платформы.
Итого
Многоязычность бота — это структура (файлы или CMS), плюрализация по CLDR, осознанный fallback и явное хранение языка в профиле пользователя. Не путайте language_code Telegram с реальным выбором — давайте кнопку. Начинать с одного языка и закладывать i18n на старте дешевле, чем пытаться добавить второй язык поверх «жёсткой» русской версии.
Частые вопросы
Как определить язык пользователя в Telegram-боте?
Telegram отдаёт language_code в User объекте — двухбуквенный ISO-639-1 код («ru», «en», «de»). Это подсказка, а не приговор: пользователь может пользоваться ботом на русском, имея английскую раскладку Telegram. На старте берём language_code как дефолт. Сразу даём кнопку «🇷🇺 RU / 🇬🇧 EN» для явного выбора. Сохраняем выбор в БД пользователя — это финальный источник истины. В настройках всегда позволяем сменить язык. language_code иногда приходит расширенный («pt-br», «zh-hans») — оставляйте только основной язык, если у вас нет региональных переводов.
Где хранить переводы для многоязычного Telegram-бота?
Три уровня зрелости. Файлы в репозитории — JSON или YAML по языкам (locales/ru.json, locales/en.json), подходит для большинства проектов. Gettext (.po/.mo) — стандарт мира перевода, есть инструменты POEdit, Crowdin, Lokalise; для команды переводчиков и больших объёмов. БД или CMS — когда тексты меняет маркетинг без релиза, минус — нужно кэшировать. Для типового бота на 200–500 строк JSON в репо плюс нормальная структура ключей закрывают потребности. С ростом языков и объёмов имеет смысл переходить на Crowdin/Lokalise.
Как структурировать ключи переводов в боте?
Хорошие ключи — иерархические и контекстные. Не «error1», «error2», а lead.error.phone_invalid, menu.tariffs.title. Это спасает от коллизий и помогает переводчику понять контекст. Не складывайте интерполяции в ключ — параметры через шаблон: «greeting»: «Привет, {name}!». Переводчик видит структуру и не путается. Иерархия по областям продукта: lead, menu, payments, errors, common. В каждой области свой словарь. Это масштабируется и при росте до 1000+ строк, и при добавлении новых языков переводчики работают в привычной структуре.
Как реализовать плюрализацию в Telegram-боте на русском?
Через CLDR Plural Rules. Русский имеет три формы (one/few/many): 1 заказ, 2 заказа, 5 заказов. Английский — две (one/other). Большинство фреймворков (i18next, gettext, формат Fluent от Mozilla) поддерживают CLDR из коробки. Пример: «orders»: { «one»: «У вас {count} заказ», «few»: «У вас {count} заказа», «many»: «У вас {count} заказов» }. Не пытайтесь «угадать» правила if-else’ами. Используйте библиотеку, иначе на каком-нибудь «21 день» бот скажет «дней» вместо «день» — типичный баг ручной плюрализации.
Что делать, если перевод отсутствует в нужном языке?
Стратегия fallback. Что делать если ключа нет в текущем языке? Варианты: вернуть ключ как есть — для разработки видно, что не переведено. Откатиться на дефолтный язык (обычно английский или русский) — для продакшена. Логировать пропуски — собирать список «надо перевести». В продакшене стандартная схема: пытаемся найти на языке пользователя, если нет — на дефолте, если нет — пишем ключ и логируем. Это убережёт от пустых сообщений. Crowdin/Lokalise валидируют структуру и плейсхолдеры на стороне платформы, что дополнительно ловит проблему до прода.
Как переводить команды бота /start, /help в Telegram?
Через setMyCommands с параметром language_code. Можно настроить разные команды для русских и английских пользователей. Кнопки клавиатур и callback’ов — обычные строки в локализованных меню. Используйте callback_data как идентификатор действия (buy_premium), а не как локализованный текст — иначе после смены языка старые кнопки перестанут работать. Минимальный набор автотестов: все ключи присутствуют во всех языках (lint на parity), плейсхолдеры совпадают между языками, длина не выходит за лимиты Telegram (4096 сообщение, 64 кнопка), smoke-тест воронки на каждом языке.