Зберігаємо нерви розробників: історія створення i18n SaaS платформи

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Локалізація продуктів — це вічний головний біль для розробників. Кожен, хто хоч раз підтримував мультиязичність у веб-застосунках, знає цю історію: гігантські безформні JSON-файли, конфлікти при злитті гілок (git merge conflicts) у перекладах, ручні копіпасти гугл-транслейту та вічні прохання маркетологів або копірайтерів «підправити текст у кнопочці прямо на продакшені».

Ми вирішили створити свій інструмент — i18n SaaS — систему для централізованого керування перекладами з автоматизацією через ШІ (Gemini).

У цій статті я поділюся технічними рішеннями, граблями та архітектурними інсайтами, які ми отримали в процесі розробки Go-бекенду та Nuxt 3-фронтенду. Зокрема, розповім, як ми оптимізували Firestore, щоб не вилетіти в трубу за лімітами запитів, та як боролися з лагами інтерфейсу під час рендерингу великих таблиць у Vue.

Сам сервіс доступний за посиланням: i18n-saas.com (будемо вдячні за фідбек та тестування!).

🏗️ Технологічний стек

Ми обрали легкий, швидкий та масштабований стек:

  • Бекенд: Go (модульний моноліт на базі роутера go-chi/v5).
  • База даних: Google Firestore (обрана за швидкість старту, інтеграцію з Firebase Auth та гнучкість документо-орієнтованої структури).
  • Фронтенд: Nuxt 3 (SSR/SPA режим) + Vuetify 3 (UI бібліотека) + vue-i18n для локалізації.
  • ШІ-модуль: Google Gemini API (gemini-2.5-flash з ланцюжком автоматичного перемикання на gemini-2.5-pro та gemini-2.0-flash у разі збоїв).

🛠️ Технічні виклики та рішення

1. Оптимізація Firestore: зниження витрат та запитів на 98%

Firestore — це круто, але ціноутворення побудоване на кількості операцій читання/запису. Спочатку наша архітектура грішила класичним антипаттерном: при імпорті чи збереженні перекладів ми робили перевірки та запити до бази в циклі for. Якщо користувач імпортував файл на 500 ключів, ми отримували 500 індивідуальних запитів на перевірку та 500 запитів на запис. Це повільно (через мережевий оверхед) і дорого.

Як вирішили: Переписали логіку на пакетні операції (Batched Writes) та масове отримання документів (GetAll):

  • Тепер при збереженні чи імпорті ми робимо один запит fb.DB.GetAll, щоб завантажити стан документів у пам’ять.
  • Записи групуються в пачки (batches) по 400 операцій (Firestore дозволяє максимум 500 в одній транзакції) і записуються одним мережевим запитом.
// Приклад групування транзакцій у Go
batch := fb.DB.Batch()
count := 0
for _, translation := range list {
tRef := fb.DB.Collection("translations«).Doc(translation.ID)
batch.Set(tRef, translation)
count++
if count >= 400 {
_, err := batch.Commit(ctx)
if err != nil {
return err
}
batch = fb.DB.Batch()
count = 0
}
}
if count > 0 {
_, err = batch.Commit(ctx)
}

Це зменшило кількість мережевих запитів під час масових операцій у сотні разів і знизило навантаження на базу практично до нуля.

2. Захист від DDOS-запитів клієнтів: In-Memory Cache для Public API

Сайт надає клієнтський API ендпоінт /api/public/{token}/{locale}.json, з якого сторонні додатки користувачів стягують актуальні переклади прямо в рантаймі. Якщо клієнтський розробник зробить помилку (наприклад, циклічний запит без кешування у своєму додатку), його сайт почне «довбати» наш сервер тисячами запитів на секунду.

Для захисту ми впровадили швидке потокобезпечне кешування в оперативній пам’яті (In-Memory Cache) на стороні Go:

  • Замість читання бази на кожен запит, ми кешуємо результат у пам’яті бекенду.
  • Кожен проєкт має мітку часу останньої зміни (UpdatedAt).
  • Запит перевіряє тільки швидку мітку часу в пам’яті. Якщо змін не було — повертається кешований JSON або статус 304 Not Modified.
  • Кеш інвалідується моментально тільки тоді, коли користувач вносить зміни в панелі керування перекладами.

3. Боротьба з лагами Vue: оптимізація рендерингу великих таблиць

На сторінці перекладу проєкту ми маємо велику інтерактивну таблицю: сотні ключів, колонка для базової мови та окрема колонка для кожної цільової мови. Спочатку кожне поле вводу (input) було зв’язане через v-model безпосередньо з глибоким об’єктом у загальному масиві перекладів. Це призвело до катастрофічних лагів при введенні тексту: Коли користувач натискав одну літеру, Vue запускав повний реактивний перерахунок всього дерева компонентів таблиці, пошукових фільтрів та статусів. Клавіатурне введення гальмувало на 200–500 мс.

Як вирішили: Ми ізолювали введення символів у буфер редагованої комірки:

  1. Замість прямого зв’язку з глобальним станом, ми створили локальну реактивну змінну localEditingValue, яка прив’язана лише до активного поля вводу.
  2. При введенні тексту оновлюється лише ця єдина локальна змінна — це відбувається миттєво (0 мс лагу).
  3. Глобальний стан проєкту оновлюється лише один раз — коли користувач завершує редагування і виходить з поля (подія @blur або натискання Enter).
  4. Замість фіксованих інпутів використали <textarea> з сучасною CSS-властивістю field-sizing: content, що змушує поле автоматично підлаштовувати висоту під текст без стрибків інтерфейсу та без JS-скриптів вимірювання висоти.

<!— Оптимізована комірка таблиці —>
<template>
<textarea
v-model="localEditingValue"
@blur="blurCellAndSave"
@keydown.enter.prevent="$event.target.blur()"
class="minimal-cell-input«
/>
</template>

4. Гнучкий ШІ-перекладач на базі Gemini API

Ми хотіли дати користувачам можливість перекладати весь проєкт на 10 мов одним кліком. Для цього було створено bulk-перекладач:

  • Робиться запит до Gemini з чітким контекстним промптом: модель виступає як професійний локалізатор, який розуміє контекст інтерфейсу (наприклад, кнопка це чи довгий опис).
  • Для надійності побудовано ланцюжок відказостійкості (fallback chain): якщо gemini-2.5-flash видає помилку лімітів чи збій мережі, система автоматично перемикається на gemini-2.5-pro, а далі на gemini-2.0-flash або зовнішній переклад, роблячи ретраї з експоненційною затримкою.

🧪 Тестування попиту без платіжки: Fake Door Test

Коли технічна частина була готова, постало питання: чи готові люди платити за Pro та Business плани? Замість того, щоб витрачати тижні на інтеграцію Stripe/LiqPay, налаштування податків, підписок тощо, ми пішли шляхом Fake Door Testing:

  1. Намалювали красиву модалку з тарифами Pro та Business.
  2. При натисканні на тариф з’являється форма: «Наразі сервіс у приватній беті. Залиште свій email, і ми надішлемо вам промокод на знижку 50% відразу після відкриття оплат!».
  3. Дані відправляються на ендпоінт /api/leads.

Це дозволило перевірити реальну конверсію відвідувачів у покупців. Спойлер: ми вже отримали перші ліди в базу, хоча трафіку майже не було! Тепер ми точно знаємо, що інтерес є, і маємо перших потенційних клієнтів.

📈 Результати та висновки

Розробка такого інструменту дозволила нам на практиці вирішити реальні проблеми продуктивності веб-додатків:

  • Навчилися вичавлювати максимум швидкості з Firestore без зайвих фінансових витрат.
  • Переконалися, що робота з великими динамічними таблицями у Vue вимагає ретельної ізоляції локальних станів.
  • Перевірили гіпотезу платоспроможності клієнтів ще до написання коду білінгу.

Будемо раді почути ваші думки про архітектуру та ідею проєкту! Як ви зазвичай вирішуєте питання локалізації у своїх командах? Напишіть у коментарях!

👍ПодобаєтьсяСподобалось3
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі