Бот-переводчик и словарь
Учим «Цыплёнка-помощника» переводить любое сообщение на другой язык — берём текст у пользователя, отправляем его в 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
}Нам из всего этого нужен только один кусочек — responseData → translatedText. В 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)».
Разберём по шагам, что тут происходит:
@dp.message(F.text)— этот хэндлер срабатывает на любое сообщение, в котором есть текст.F.text— «фильтр по полю text»: реагируем только на текстовые сообщения, а не на стикеры или фото.params = {"q": text, "langpair": "en|ru"}— складываем параметры запроса в обычный словарь.aiohttpсам аккуратно приклеит их к адресу и закодирует пробелы и спецсимволы — нам не надо вручную клеить ссылку.async with aiohttp.ClientSession() as session— открываем сессию: это как открыть браузер, через который пойдут запросы.async withгарантирует, что сессия закроется сама, даже если что-то пойдёт не так.session.get(TRANSLATE_URL, params=params)— отправляем GET-запрос на сервер перевода с нашими параметрами.await resp.json()— просимaiohttpразобрать ответ как JSON и сразу превратить его в словарь Python. Словоawaitзначит «подожди, пока ответ придёт, и не блокируй остальных пользователей».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-ответа по ключам
responseData→translatedText; - как дать пользователю выбор языка через inline-кнопки и передать код языка в
callback_data; - главные грабли: пропущенный
await, отсутствующий ключ в ответе, длинный текст, забытыйcallback.answer()и зависший сервер.
Главная мысль урока: твой бот не обязан уметь всё сам. В интернете полно серверов, которые что-то отлично делают, — а бот может стать удобным мостиком между другом в чате и этими сервисами. Сегодня это перевод, а механика та же, что для погоды, новостей или курса валют.
В следующем уроке мы продолжим тему интеграций и научим «Цыплёнка-помощника» работать с картинками и медиа из внешних сервисов — например, приносить случайный мем или картинку по запросу. До встречи!