Бот-переводчик и словарь

Учим «Цыплёнка-помощника» переводить любое сообщение на другой язык — берём текст у пользователя, отправляем его в API перевода через aiohttp и возвращаем готовый перевод прямо в чат.

API перевода — это чужой сервер в интернете, который умеет одно дело, но делает его отлично: ты присылаешь ему фразу и говоришь, на какой язык перевести, а он возвращает перевод. Твой бот тут как посредник: берёт текст у друга, бежит к этому серверу, получает ответ и приносит обратно.

В прошлом уроке про запросы к API через aiohttp мы научили «Цыплёнка-помощника» ходить за данными на чужие серверы и разбирать их ответ в формате JSON. Сегодня применим это умение к по-настоящему полезной штуке — переводчику. К концу урока твой бот будет принимать любое сообщение, переводить его и даже спрашивать кнопками, на какой язык переводить.

Зачем боту уметь переводить

Представь: ты в Discord-сервере по любимой игре, и там полно ребят со всего мира. Кто-то скидывает гайд на английском, кто-то ругается на испанском в голосовом, а ты хочешь понять, что вообще происходит. Открывать каждый раз отдельную вкладку с переводчиком — лень. А что, если прямо в Telegram написать боту фразу, и он мгновенно вернёт перевод?

Или другая ситуация: вы с друзьями ведёте клановый чат, и туда забрёл игрок, который пишет только на турецком. Пересылаешь его сообщение «Цыплёнку-помощнику» — и тут же читаешь по-русски. Удобно, быстро, и не надо никуда уходить из мессенджера. А ещё бот-переводчик отлично выручает с домашкой по английскому: списываешь незнакомое слово из учебника, кидаешь боту — и сразу видишь перевод, не листая толстый словарь.

И вот что приятно: всю «магию» перевода делает не ты. Тебе не нужно знать пять языков, не нужно хранить гигантский словарь у себя в коде. В интернете уже есть готовые сервисы, которые переводят миллионы фраз каждый день, — а наша задача всего лишь научить бота вежливо к ним обращаться и красиво показывать ответ. По сути, мы строим маленький мостик между другом в чате и большим умным сервером где-то далеко.

Вот к чему мы придём. Бот, который переводит любой присланный текст:

Ты: How do I get to the next level?

🐤 Перевод (en → ru):
Как мне перейти на следующий уровень?

Результат: пользователь шлёт боту фразу на английском, бот отправляет её в API перевода, получает русский вариант и присылает его обратно, подписав, с какого языка на какой переводил.

Как бот разговаривает с чужим сервером

Вспомни аналогию из прошлого урока: бот — это официант. Ты (пользователь) сидишь за столиком и говоришь официанту, что хочешь. Официант идёт на кухню (чужой сервер), передаёт заказ, ждёт, забирает готовое блюдо и приносит тебе. Бот сам ничего не переводит — он просто бегает между тобой и «кухней перевода».

Кухня в нашем случае — это API перевода: сервер, который принимает HTTP-запрос с текстом и языком, а возвращает перевод. Мы возьмём бесплатный публичный сервис, у которого простой адрес и понятный ответ. Запрос к нему — это обычная ссылка с параметрами, примерно такая:

https://api.mymemory.translated.net/get?q=Hello&langpair=en|ru

Разберём, что тут к чему:

  • https://api.mymemory.translated.net/get — адрес «окошка», куда мы стучимся за переводом.
  • q=Hello — параметр q (от query, «запрос»): сам текст, который надо перевести.
  • langpair=en|ru — пара языков: с английского (en) на русский (ru). Коды языков — это стандартные двухбуквенные обозначения: en, ru, de (немецкий), fr (французский), es (испанский).

Если открыть такую ссылку в браузере, сервер ответит не красивой страничкой, а текстом в формате JSON — это способ записи данных, где всё лежит в фигурных скобках по парам «ключ: значение». Ответ выглядит примерно так:

{
  "responseData": {
    "translatedText": "Привет",
    "match": 1
  },
  "responseStatus": 200
}

Нам из всего этого нужен только один кусочек — responseDatatranslatedText. В Python после разбора JSON мы достанем его так: data["responseData"]["translatedText"]. Это как открыть коробку responseData и вытащить из неё то, что лежит под ярлычком translatedText.

Тренируемся доставать перевод из ответа

Прежде чем лезть в код бота, потренируемся на чистом Python разбирать такой ответ. Это обычная работа со словарём, без всякого интернета — можешь запустить прямо здесь:

import json

# так выглядит ответ сервера в виде строки
raw = '{"responseData": {"translatedText": "Привет", "match": 1}, "responseStatus": 200}'

data = json.loads(raw)            # строку JSON превращаем в словарь Python
perevod = data["responseData"]["translatedText"]

print("Достали перевод:", perevod)
print("Тип результата:", type(perevod).__name__)

Вывод:

Достали перевод: Привет
Тип результата: str

Видишь? Сначала json.loads() превращает текст ответа в обычный словарь, а дальше мы ходим по ключам, как по полочкам, и достаём нужное. Ровно это будет делать бот — только строку ему отдаст не переменная, а сервер.

Пример 1. Простой бот-переводчик

Теперь главное. Соберём хэндлер, который ловит любое текстовое сообщение, отправляет его в API перевода через aiohttp и присылает перевод. Это кусок, который добавляется к нашему bot.py рядом с уже знакомыми bot и dp:

import aiohttp
from aiogram import F
from aiogram.types import Message

TRANSLATE_URL = "https://api.mymemory.translated.net/get"

@dp.message(F.text)
async def translate_text(message: Message):
    text = message.text

    params = {"q": text, "langpair": "en|ru"}

    async with aiohttp.ClientSession() as session:
        async with session.get(TRANSLATE_URL, params=params) as resp:
            data = await resp.json()

    perevod = data["responseData"]["translatedText"]

    await message.answer(
        f"🐤 Перевод (en → ru):\n{perevod}"
    )

Результат: когда пользователь пишет боту английскую фразу, бот отправляет её на сервер перевода, дожидается ответа и присылает русский перевод с подписью «Перевод (en → ru)».

Разберём по шагам, что тут происходит:

  1. @dp.message(F.text) — этот хэндлер срабатывает на любое сообщение, в котором есть текст. F.text — «фильтр по полю text»: реагируем только на текстовые сообщения, а не на стикеры или фото.
  2. params = {"q": text, "langpair": "en|ru"} — складываем параметры запроса в обычный словарь. aiohttp сам аккуратно приклеит их к адресу и закодирует пробелы и спецсимволы — нам не надо вручную клеить ссылку.
  3. async with aiohttp.ClientSession() as session — открываем сессию: это как открыть браузер, через который пойдут запросы. async with гарантирует, что сессия закроется сама, даже если что-то пойдёт не так.
  4. session.get(TRANSLATE_URL, params=params) — отправляем GET-запрос на сервер перевода с нашими параметрами.
  5. await resp.json() — просим aiohttp разобрать ответ как JSON и сразу превратить его в словарь Python. Слово await значит «подожди, пока ответ придёт, и не блокируй остальных пользователей».
  6. data["responseData"]["translatedText"] — достаём из словаря сам перевод, как мы тренировались выше.

Обрати внимание на async и await: пока бот ждёт ответа от сервера перевода (а это может быть полсекунды-секунда), он не «замирает». Другие пользователи в это время спокойно получают свои ответы. Это и есть та самая асинхронность — официант не стоит столбом у кухни, а успевает обслужить другие столики, пока готовится твоё блюдо.

Пример 2. Даём пользователю выбрать язык

Переводить всегда только с английского на русский — скучновато. Дадим пользователю выбор: пусть нажмёт кнопку и сам решит, на какой язык переводить. Inline-кнопки и callback мы разбирали в модуле про интерфейс — здесь применим их к переводчику.

Идея такая: пользователь присылает текст, бот показывает кнопки с языками, пользователь жмёт нужную — и только тогда бот переводит. Чтобы не потерять исходный текст между сообщением и нажатием кнопки, спрячем его прямо в данные кнопки (в callback_data).

Сначала — клавиатура с языками. Соберём её в отдельной функции, чтобы не загромождать хэндлер:

from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup

def language_keyboard():
    keyboard = InlineKeyboardMarkup(inline_keyboard=[
        [
            InlineKeyboardButton(text="🇬🇧 English", callback_data="to:en"),
            InlineKeyboardButton(text="🇷🇺 Русский", callback_data="to:ru"),
        ],
        [
            InlineKeyboardButton(text="🇩🇪 Deutsch", callback_data="to:de"),
            InlineKeyboardButton(text="🇫🇷 Français", callback_data="to:fr"),
        ],
    ])
    return keyboard

Результат: функция собирает табличку из четырёх кнопок-флагов (английский, русский, немецкий, французский) в два ряда. У каждой кнопки в callback_data зашит код языка вида to:en.

Теперь сам сценарий. Первый хэндлер ловит текст и показывает кнопки, второй — срабатывает на нажатие и переводит:

from aiogram import F
from aiogram.types import Message, CallbackQuery

# временное хранилище последнего текста для каждого пользователя
last_text = {}

@dp.message(F.text)
async def ask_language(message: Message):
    last_text[message.from_user.id] = message.text
    await message.answer(
        "На какой язык перевести?",
        reply_markup=language_keyboard(),
    )

@dp.callback_query(F.data.startswith("to:"))
async def do_translate(callback: CallbackQuery):
    target = callback.data.split(":")[1]      # "to:de" -> "de"
    text = last_text.get(callback.from_user.id)

    if text is None:
        await callback.answer("Сначала пришли текст!")
        return

    params = {"q": text, "langpair": f"autodetect|{target}"}

    async with aiohttp.ClientSession() as session:
        async with session.get(TRANSLATE_URL, params=params) as resp:
            data = await resp.json()

    perevod = data["responseData"]["translatedText"]

    await callback.message.answer(f"🐤 Перевод:\n{perevod}")
    await callback.answer()

Результат: пользователь шлёт фразу, бот спрашивает кнопками язык; после нажатия флага бот переводит на выбранный язык (исходный язык определяется автоматически) и присылает результат. Если пользователь нажал кнопку, ничего предварительно не написав, бот мягко напомнит сначала прислать текст.

Что здесь важно понять:

  • last_text[message.from_user.id] = message.text — запоминаем текст по id пользователя в обычном словаре. Так бот не перепутает запросы разных людей: у каждого свой «последний текст».
  • callback.data.split(":")[1] — разбираем callback_data. Строку "to:de" режем по двоеточию и берём вторую часть — код языка de.
  • "langpair": f"autodetect|{target}" — слово autodetect говорит серверу «сам пойми, на каком языке исходник», а перевести просим на выбранный target.
  • await callback.answer() в самом конце — обязательно. Без него у пользователя кнопка будет «думать» с крутящимся кружком. Этот вызов гасит часики на кнопке.

Маленькая честность: словарь last_text в памяти — решение временное, ровно как переменные пропадали в модуле про базу данных. После перезапуска бота он очистится. Для учебного бота это нормально, а в «боевом» эти данные обычно держат в FSM или в базе.

Пример 3. Делаем перевод надёжным

Пока что наши хэндлеры жили в идеальном мире, где сервер всегда отвечает и всегда отдаёт перевод. В реальности так не бывает: интернет моргнёт, сервис перегрузится, пользователь пришлёт пустую строку или книжку на двести страниц. Если бот к этому не готов, он просто упадёт с ошибкой — и замолчит для всех, пока ты его не перезапустишь.

Сделаем «взрослую» версию хэндлера, которая держит удар. Добавим три вещи: проверку длины текста до отправки, таймаут на запрос и обёртку try/except на случай, если что-то всё-таки сломается:

import asyncio
import aiohttp
from aiogram import F
from aiogram.types import Message

MAX_LEN = 500
TIMEOUT = aiohttp.ClientTimeout(total=10)  # ждём ответ не дольше 10 секунд

@dp.message(F.text)
async def safe_translate(message: Message):
    text = message.text.strip()

    if not text:
        await message.answer("🐤 Пришли мне текст для перевода.")
        return

    if len(text) > MAX_LEN:
        await message.answer(
            f"🐤 Слишком длинно — до {MAX_LEN} символов, пожалуйста."
        )
        return

    params = {"q": text, "langpair": "en|ru"}

    try:
        async with aiohttp.ClientSession(timeout=TIMEOUT) as session:
            async with session.get(TRANSLATE_URL, params=params) as resp:
                data = await resp.json()
    except (aiohttp.ClientError, asyncio.TimeoutError):
        await message.answer("🐤 Сервис перевода не отвечает, попробуй позже.")
        return

    response_data = data.get("responseData")
    if not response_data or not response_data.get("translatedText"):
        await message.answer("🐤 Не получилось перевести. Попробуй другую фразу.")
        return

    perevod = response_data["translatedText"]
    await message.answer(f"🐤 Перевод:\n{perevod}")

Результат: бот сначала проверит, что текст не пустой и не слишком длинный; затем отправит запрос с таймаутом в 10 секунд. Если сервер не ответит или вернёт ответ без перевода, бот не упадёт, а вежливо сообщит о проблеме. Только если всё хорошо — пришлёт перевод.

Сравни с первой версией — кода стало больше, но каждый кусок делает понятную работу:

  • message.text.strip() — обрезаем пробелы по краям; if not text ловит и пустую строку, и сообщение из одних пробелов.
  • len(text) > MAX_LEN — отсекаем слишком длинный текст до похода на сервер. Зачем грузить сервер тем, что он всё равно отвергнет.
  • aiohttp.ClientTimeout(total=10) и session(timeout=...) — говорим «жди ответ максимум 10 секунд». Не пришёл — считаем, что сервис недоступен.
  • try/except (aiohttp.ClientError, asyncio.TimeoutError) — ловим обе беды: и обрыв сети, и истёкший таймаут. Внутри except мягко извиняемся перед пользователем.
  • data.get("responseData") вместо data["responseData"] — метод .get() вернёт None, если ключа нет, а не уронит бота с KeyError. Это защита от «кривого» ответа.

Запомни эту структуру — проверка ввода, таймаут, try/except, проверка ответа — она пригодится для любого общения с чужими серверами, не только для перевода. Погода, новости, мемы, курс валют: код снаружи разный, а каркас надёжности один и тот же.

Частые ошибки и подводные камни

1. Забыл await перед запросом или resp.json()

Самая частая ошибка с асинхронным кодом. Пишешь data = resp.json() без await — и в data оказывается не словарь, а странный объект-«обещание» (корутина), из которого ничего не достать. Правило: перед любым обращением к серверу и перед .json() ставь await. Ждать — это нормально, для того и асинхронность.

2. Лезешь в ответ по ключу, которого нет

Ты ждёшь data["responseData"]["translatedText"], а сервер вернул ошибку — и такого ключа в ответе нет. Бот падает с KeyError. Всегда держи в голове, что сервер может ответить не тем, чего ты ждёшь. Перед тем как лезть вглубь, проверяй: if "responseData" in data: — а уже потом доставай перевод. Лучше показать «не получилось перевести», чем уронить бота.

3. Отправляешь пустую строку или слишком длинный текст

Бесплатные API перевода часто ограничивают длину текста (например, до 500 символов) и обижаются на пустые запросы. Если переслать боту огромную простыню, сервер вернёт ошибку вместо перевода. Проверяй длину до отправки: if len(text) > 500: — и вежливо попроси текст покороче.

4. Забыл callback.answer() после нажатия кнопки

Если в хэндлере inline-кнопки не вызвать callback.answer(), у пользователя на кнопке бесконечно крутится индикатор загрузки, будто бот завис. Telegram ждёт этот «ответ-квитанцию». Возьми за привычку: в конце любого callback-хэндлера — await callback.answer().

5. Не учитываешь, что сервер может тормозить или не ответить

Чужой сервер — не твой: он может быть перегружен, ответить через 10 секунд или вовсе отвалиться. Если не задать таймаут, бот будет терпеливо ждать вечно. На запрос полезно ставить ограничение по времени и оборачивать вызов в try/except, чтобы при сбое сети бот сказал «сервис недоступен, попробуй позже», а не молчал и не падал.

Мини-практика: команда /tr и счётчик переводов

Теперь твой ход. Доработай «Цыплёнка-помощника»-переводчика:

  • Сделай так, чтобы бот переводил не любое сообщение подряд, а только после команды /tr с текстом: /tr How are you. Аргументы команды мы разбирали в модуле про команды — текст после /tr и есть то, что надо перевести.
  • Перед отправкой на сервер проверяй: если текста после /tr нет — попроси его прислать; если он длиннее 500 символов — попроси покороче.
  • Заведи счётчик: бот считает, сколько переводов он уже сделал, и по команде /stats отвечает «🐤 Я перевёл уже N фраз!».

Подсказки: текст после команды можно взять из message.text, отрезав сам /tr (например, через message.text.split(maxsplit=1)). Счётчик на первых порах держи в обычной переменной, а если хочешь, чтобы он переживал перезапуск, — вспомни прошлый модуль и сохрани число в SQLite. И обязательно оберни запрос к серверу в try/except, чтобы бот не падал, если перевод не удался.

Итоги

Сегодня «Цыплёнок-помощник» научился по-настоящему полезному фокусу — переводить тексты, забирая перевод с чужого сервера. Что теперь в твоём арсенале:

  • как устроен запрос к API перевода: адрес, параметр с текстом и пара языков langpair;
  • как отправить запрос через aiohttp внутри хэндлера и не заморозить бота благодаря async/await;
  • как достать перевод из JSON-ответа по ключам responseDatatranslatedText;
  • как дать пользователю выбор языка через inline-кнопки и передать код языка в callback_data;
  • главные грабли: пропущенный await, отсутствующий ключ в ответе, длинный текст, забытый callback.answer() и зависший сервер.

Главная мысль урока: твой бот не обязан уметь всё сам. В интернете полно серверов, которые что-то отлично делают, — а бот может стать удобным мостиком между другом в чате и этими сервисами. Сегодня это перевод, а механика та же, что для погоды, новостей или курса валют.

В следующем уроке мы продолжим тему интеграций и научим «Цыплёнка-помощника» работать с картинками и медиа из внешних сервисов — например, приносить случайный мем или картинку по запросу. До встречи!

Проверьте себя
1. Что делает бот-переводчик, когда получает текст от пользователя?
AСам переводит текст по встроенному словарю aiogram
BОтправляет текст на чужой сервер (API перевода), получает ответ и возвращает перевод пользователю
CСохраняет текст в базу и переводит его раз в сутки
DПересылает сообщение в BotFather для перевода
2. Зачем перед resp.json() ставят await?
Aawait ускоряет разбор JSON в несколько раз
BБез await словарь будет отсортирован по алфавиту
CПотому что получение и разбор ответа — асинхронные операции; без await в переменной окажется корутина, а не словарь с данными
Dawait нужен только для отправки сообщений, а не для запросов
3. Из какого места JSON-ответа MyMemory достаётся сам перевод?
Adata["translatedText"]
Bdata["responseData"]["translatedText"]
Cdata["perevod"]
Ddata["responseStatus"]["text"]
4. Зачем код языка прячут в callback_data inline-кнопки, например to:de?
AЧтобы кнопка светилась нужным цветом
BЧтобы при нажатии бот получил вместе с сигналом и информацию о выбранном языке и понял, на что переводить
Ccallback_data обязателен только для reply-клавиатур
DЧтобы Telegram сам выполнил перевод
5. Что произойдёт, если в callback-хэндлере не вызвать callback.answer()?
AБот сразу упадёт с ошибкой
BПеревод придёт дважды
CУ пользователя на кнопке будет бесконечно крутиться индикатор загрузки, будто бот завис
DКнопка автоматически удалится
6. Почему перед отправкой текста на API стоит проверять его длину?
AДлинный текст переводится на другой язык
Baiohttp не умеет отправлять больше 10 символов
CБесплатные API перевода часто ограничивают длину запроса, и слишком длинный текст вернёт ошибку вместо перевода
DTelegram запрещает сообщения длиннее 500 символов