Как я учил польский, сгорел от Google Translate и случайно написал мобильное приложение с ИИ
Привет, Пикабу. Мне 23 года и недавно я решил начать учить польский. Я с детства учу английский, владею им на хорошем уровне и знаю, насколько мне тяжело дается учеба. Просто зубрить слова и правила - это не мое, становится скучно моментально и, что самое главное, не запоминается практически ничего. Основываясь на этом опыте, я понимал, что мне нужно погружение в контекст. Так как польский (также как и русский) относятся к славянским языкам и в них довольно много похожих по звучанию и значению слов, я стал сразу пытаться начать читать.
И тут начались первые проблемы. На сайтах, на которых есть тексты, адаптированные для уровня А1-А2 читалка без встроенного переводчика. Импортировать тексты в Apple Books это совсем не то и неудобно. Я начал искать альтернативы.
Приложение, которое мне понравилось по функционалу, - это LingQ. У него есть встроенная библиотека текстов, перевод слов по тапу, карточки и, казалось бы, вот он, идеальный вариант. И это он, если бы не цена в 13 фунтов ежемесячно. На хобби тратить столько денег на подписку я посчитал не целесообразно.
Помимо изучения языков, я увлекаюсь программированием на пайтон и вайбкодингом.
Так и родился LinguaVibe.
Что вообще получилось
LinguaVibe - это Flutter-приложение для чтения иностранных текстов. Сейчас поддерживает 5 языков: польский, испанский, немецкий, французский и английский. Интерфейс - на русском.
Основная фича: ты вставляешь текст (или импортируешь статью по URL), читаешь, и когда встречаешь незнакомое слово - тапаешь на него. Приложение отправляет всё предложение в LLaMA через Groq API и получает перевод с учётом контекста. Не просто «слово → перевод», а полноценный разбор: начальная форма, перевод, грамматика и даже пример предложения.
Вторая по важности фича: ты можешь вставить свой ключ Groq API, который получается совершенно бесплатно и получить практически весь функционал приложения совершенно бесплатно, с условием, что у вас установлено приложение с названием из 3 букв.
После этого сохраняешь слово в словарь, а потом учишь его через встроенные карточки с системой интервального повторения (SRS, метод Лейтнера).
Стек и архитектура
Тут всё просто, без enterprise-булщита:
Фронтенд (мобилка):
- Flutter (Dart) - потому что кроссплатформа и горячая перезагрузка
- Provider - стейт-менеджмент, без Riverpod-магии
- Isar - локальная NoSQL база. Быстрая, асинхронная, и главное - у меня отдельная база на каждый язык (`isar_pl`, `isar_es` и т.д.)
- flutter_tts - озвучка слов. Польский, испанский, немецкий - всё произносит
Бэкенд:
- Node.js + Express - лёгкий сервер на соседнем репозитории
- Docker - контейнеризация, потому что я не хочу настраивать сервер руками в 3 часа ночи
- Caddy - reverse proxy с автоматическими HTTPS-сертификатами. Lets Encrypt сам всё делает, красота
AI:
- Groq API - LLaMA 3.1 8B для быстрых переводов по тапу, LLaMA 3.3 70B для генерации примеров и переводов заголовков. Groq быстрый - ответы за 200-300мс, это важно когда ты тапаешь на слова в тексте
Как я это кодил
Буду честен - я вайб-кодил с ИИ-помощниками. Opencode, Cursor, Claude - всё было. Я Python-разработчик, Dart я до этого практически не трогал. Но с ИИ-ассистентами это оказалось не так страшно.
Архитектуру продумал сам: сервисы, репозитории, провайдеры. А детали реализации делал с ИИ. Это реально работает, если ты понимаешь что хочешь получить.
Грабли, на которые я наступил (и закрыл)
Вот тут самое интересное для технарей. Перед релизом пришлось закрывать несколько серьёзных дыр.
1. APP_SECRET_KEY в коде
Изначально ключ бэкенда был захардкожен в `server_config.dart`. Это ужасно - любой может декомпилировать APK и получить ключ.
Решение: перенёс ключ в `--dart-define=APP_SECRET_KEY` при билде. Ключ хранится в `.env` файле (который в `.gitignore`), а билд-скрипт подставляет его через `dart-define`. В коде - `const String.fromEnvironment('APP_SECRET_KEY')`. При декомпиляции ключ не найти в plaintext.
2. SSRF-атаки в веб-парсере
У меня есть фича импорта текстов по URL. Пользователь вставляет ссылку, приложение парсит статью и вытаскивает текст. Проблема: если пользователь вставит `http://localhost:3000/admin` или `http://169.254.169.254/latest/meta-data/` (AWS metadata)?
Решение: в `WebParserService` добавил валидацию URL:
- Проверяю что схема - только `http` или `https`
- Блокирую localhost, 127.0.0.1
- Блокирую приватные сети (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
- Блокирую AWS/GCP metadata (`169.254.169.254`)
// Блокируем SSRF
if (uri.isLoopback || uri.isPrivate) throw Exception('Blocked');
if (uri.host == '169.254.169.254') throw Exception('Blocked');
3. Накрутка лимитов
У меня freemium-модель: бесплатно 10 переводов в день и 3 импорта URL. Premium - безлимит. Но что если пользователь будет спамить запросы?
Решение: на бэкенде - rate limiting по userId. На клиенте - счётчики в SharedPreferences с ежедневным сбросом. Перед каждым запросом - проверка лимита. Если превышен - кидается `SubscriptionTranslationLimitException` и показывается диалог с предложением купить Premium.
4. Вебхуки ЮКассы и идемпотентность
Для Android-оплаты использую ЮКассу. Бэкенд создаёт платёж, пользователь оплачивает в WebView, ЮКасса шлёт вебхук. Проблема: вебхук может прийти дважды (сеть моргнула, таймаут, retry).
Решение: на бэкенде - идемпотентная обработка вебхуков. Каждый платёж имеет уникальный ID, и если вебхук уже обработан - повторный игнорируется. Плюс проверка HMAC-подписи от ЮКассы, чтобы никто не мог подделать вебхук.
5. WebView для платежей
На Android оплата идёт через WebView с ЮКассой. Нужно было убедиться, что пользователь не уйдёт со страницы оплаты и что deep link сработает корректно.
Решение: `webview_flutter` с `NavigationDelegate`. Разрешаю навигацию только на домены `yookassa.ru` и `yandex.ru`. Ловлю deep link `linguavibe://payment-success` через `onNavigationRequest` и `onUrlChange` - как только редирект на наш scheme, закрываем WebView и обновляем статус Premium.
Самый сок в том, что все эти уязвимости (включая защиту от SSRF в парсере и дыру в WebView) мне подсветил не платный аудит и не Claude, а бесплатный Qwen 3.6 в OpenCode, когда я ради прикола скинул ему структуру проекта и спросил: „Где я тут обосрался?“. Нейронка буквально ткнула меня носом в безопасность, расписала, что у меня не так и предложила фиксы.
Что умеет приложение сейчас
Чтение:
- Вставка текста вручную
- Импорт статьи по URL (парсинг с фильтрацией мусора - рекламы, privacy policy и прочего)
- Тап на слово → AI-перевод с контекстом
- TTS-озвучка каждого слова
Словарь:
- Все сохранённые слова с поиском
- Ручное добавление слов (с AI-генерацией перевода и примера)
- Редактирование переводов
Карточки (SRS):
- Метод Лейтнера: 6 коробок с интервалами 5 мин → 4 ч → 1 день → 3 дня → 7 дней → 14 дней
- 3D-анимация переворота карточки
- Настраиваемый лимит слов за сессию (10-100)
- Слова на 5-й коробке считаются освоенными
Настройки:
- Groq API ключ (вставил свой кюч - тогда безлимит)
- Темы: светлая, тёмная, системная
- Стиль плиток: градиент или монохром
- Экспорт/импорт словаря в JSON (Premium)
- Серия дней (streak) - мотивация не бросать
Premium:
- iOS: In-App Purchase, 449 ₽/мес
- Android: ЮКасса, 349 ₽/мес
- Безлимитные переводы и импорты
- Если вставишь свой Groq API ключ - лимиты снимаются и без подписки
Бонус: приложение весит всего 20 МБ в RuStore. Оптимизировал сборку, убрал лишнее, и получилось довольно компактно для Flutter-приложения.
Где скачать и потестить
На данный момент приложение доступно только в RuStore под Android. В будущем планируется релиз под iOS в AppStore.
👉 RuStore
Буду рад любому фидбеку - кидайте в комменты что сломалось, что бесит, чего не хватает. Особенно интересно мнение тех, кто учит языки через чтение.
P.S. Да, я всё ещё учу польский. Да, приложение реально помогает. Нет, я не бросил. Do zobaczenia! 🇵🇱




