Як додати 13 мов UI у pet-проект без OpenAI/Google Translate API: Argos Translate + GitHub Actions на практиці
Контекст: у попередньому пості я розповідав про SelenaCore — офлайн-голосовий асистент для розумного будинку на Raspberry Pi / Jetson. Одне з питань, яке постало майже одразу — мультимовність. Українська та англійська були від початку, але self-hosted-проекту, який позиціонується як «локальний і приватний», обмежуватися двома мовами — це compromise. Довелося шукати спосіб додати ще десяток мов без жодного хмарного сервісу та без бюджету на перекладацькі API. Ділюся підходом — знадобиться будь-кому, хто пише self-hosted софт і не хоче платити $20/місяць за DeepL API.
Проблема
До v0.4.0 в SelenaCore було 2 мови UI: українська та англійська. Обидві ручні, src/i18n/locales/{en,uk}.ts, стандартний react-i18next. Додати 10+ мов «по-чесному» — це:
- Платно: DeepL Pro $20/міс, Google Translate API ~$20 за мільйон символів. Помножте на N релізів на рік + регенерацію при зміні строк.
- Компроміс приватності: хмарний переклад бачить усі рядки вашого інтерфейсу, включно з назвами кнопок типу «Активувати камеру спостереження».
- Онлайн-залежність у build-pipeline: немає інтернету → немає релізу.
Для проекту з філософією «все локально, жодного cloud» це неприйнятно. Треба було інше рішення.
Рішення: Argos Translate + CI-генерація локалей
Argos Translate — open-source переклад на базі OpenNMT/CTranslate2. Поставив pip-пакет, скачав моделі en→{target_lang} один раз (~300MB кожна), далі працює офлайн.
Ключова ідея: переклад НЕ в рантаймі на девайсі користувача. Генеруємо всі локалі один раз на CI під час релізу, комітимо результат як статичні JSON-файли в репо. Користувач після git clone отримує вже готові мови без жодного запиту кудись.
# scripts/generate_auto_locales.py (спрощено)
from argostranslate import translate
def generate_locale(source_lang: str, target_lang: str, keys: dict) -> dict:
tl = translate.get_translation_from_codes(source_lang, target_lang)
result = {}
for key, value in keys.items():
result[key] = apply_glossary(tl.translate(value), target_lang)
return result
Усе. 13 цільових мов (pl, cs, de, nl, es, fr, it, pt, tr, ja, zh, ko, hi) за ~3-5 хвилин генерації.
Три проблеми, які знадобилось вирішувати
1. Технічні терміни перекладаються кумедно
Argos, як і будь-який NMT, перекладатиме Wake word як «Слово прокидання», а Provider як «Постачальник». Для UI це ламає UX — користувач не впізнає терміни з документації.
Рішення — glossary перед перекладом. Замінюємо терміни на токени, перекладаємо, повертаємо токени назад. Плюс per-language overrides для випадків, коли хочемо все-таки локалізувати:
{
"keep_original": ["Wake word", "Provider", "SelenaCore", "STT", "TTS", "LLM"],
"per_language_overrides": {
"de": {"Provider": "Anbieter"}
}
}
2. Plural forms для слов’янських мов
Слов’янські мови мають 1 пристрій / 2 пристрої / 5 пристроїв). Argos цього не знає — переклад одного "{{count}} devices" дасть одну форму.
Трюк: підставляємо реальні числа з різних плюральних діапазонів (1, 2, 5), перекладаємо кожне окремо, потім замінюємо число назад на {{count}}. babel.plural каже які форми потрібні для кожної мови. Працює на ~90% випадків, решта — community-override’и.
3. Bootstrap-проблема: що робити, поки моделі Argos не завантажені
Моделі Argos важать ~300MB на мовну пару × багато мов = кілька GB. На Raspberry Pi це неприйнятно. Але на девайсі моделі й не потрібні — вони потрібні тільки на CI, коли генеруються файли.
Це ключове архітектурне рішення: переклад — build-time, не runtime. Результат — статичні JSON’и в src/i18n/locales/auto/*.auto.json, які лежать у git і приходять до користувача разом з кодом. Інтерфейс перемикається моментально, бо i18next просто читає локальний файл через lazy-import.
Автоматизація через GitHub Actions
Workflow тригериться на зміну src/i18n/locales/en.ts у main:
on: push: branches: [main] paths: - 'src/i18n/locales/en.ts'
Далі — встановити Argos (~1 хв), кешувати моделі через actions/cache@v4 (потім вже миттєво), згенерувати, створити auto-PR через peter-evans/create-pull-request@v6. Підписав — з’їжджа у main → нові локалі активні в наступному релізі.
Для публічного OSS-репо GitHub Actions безкоштовні без лімітів, тож цей pipeline нічого не коштує.
Що вийшло в v0.4.0-rc
- 15 мов UI: 2 ручних (uk, en) + 13 авто (pl, cs, de, nl, es, fr, it, pt, tr, ja, zh, ko, hi)
3-tier resolution: manual > community > auto. Якщо хтось відправить PR з{lang}.community.json— його правки перекривають машинний переклад автоматично.- Lazy-load: з 2 мов до 15 initial bundle виріс лише на ~40KB gzip. Кожна мова — окремий async-chunk, вантажиться при зміні мови.
- Safe-harbor recovery: якщо хтось випадково обрав 日本語 і заблукав у японському UI, long-press на логотипі (1.5 сек) відкриває reset-to-EN модалку. Крім того, вкладка Language сама по собі завжди показує дублювання native + English.
Крім i18n зайшли: редизайн нав-бару (Devices/Automations/Voice тепер у топ-левелі навігації, а не в підменю Settings), PIN-gate для Edit-mode дашборда, нова палітра з нейтральним градієнтом замість етнічних шпалер за замовчуванням, та тюнінг intent-pipeline.
Обмеження про які чесно
- Якість машинного перекладу — не 95%. Ближче до 85%. Для UI-лейблів це ок, для емоційного copy (welcome screens, error messages) — гірше. Community PRs від носіїв мови — єдиний спосіб довести до production-quality.
- Arabic (RTL) поки не включений — потребує окремого CSS-рефакторингу, відклав на v0.4.1.
Посилання
- Репо: github.com/dotradepro/SelenaCore
- Сайт: selenehome.tech
- Попередня стаття на DOU: дивитись мій профіль
Буду радий фідбеку, особливо від тих, хто робив подібне. Якщо хтось має досвід з Helsinki-NLP чи NLLB-200 як альтернатив Argos — цікаво порівняти якість для слов’янських мов. І якщо побачите криві переклади у своїй мові — welcome PRs, src/i18n/locales/{lang}.community.json ідеальне місце.
Тестую v0.4.0-rc зараз на Pi 5 8GB + Jetson Orin Nano. Якщо хтось захоче приєднатися до testers — пишіть, дам доступ до RC-білда перед публічним релізом.
1 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарівПрикольно! Треба буде спробувати.LLM-ку генерити локально. Цей варіант теж локально можна оновлювати на рівні з тестами.
Часом лінь бігати додавати переклад до кожної мови, коли додається нове слово чи кнопка чи ще щось в шаблоні.
Раніше думав через