Зберігаємо нерви розробників: історія створення i18n SaaS платформи
Локалізація продуктів — це вічний головний біль для розробників. Кожен, хто хоч раз підтримував мультиязичність у веб-застосунках, знає цю історію: гігантські безформні JSON-файли, конфлікти при злитті гілок (git merge conflicts) у перекладах, ручні копіпасти гугл-транслейту та вічні прохання маркетологів або копірайтерів «підправити текст у кнопочці прямо на продакшені».
Ми вирішили створити свій інструмент — i18n SaaS — систему для централізованого керування перекладами з автоматизацією через ШІ (Gemini).
У цій статті я поділюся технічними рішеннями, граблями та архітектурними інсайтами, які ми отримали в процесі розробки Go-бекенду та Nuxt
Сам сервіс доступний за посиланням: 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 запускав повний реактивний перерахунок всього дерева компонентів таблиці, пошукових фільтрів та статусів. Клавіатурне введення гальмувало на
Як вирішили: Ми ізолювали введення символів у буфер редагованої комірки:
- Замість прямого зв’язку з глобальним станом, ми створили локальну реактивну змінну
localEditingValue, яка прив’язана лише до активного поля вводу. - При введенні тексту оновлюється лише ця єдина локальна змінна — це відбувається миттєво (0 мс лагу).
- Глобальний стан проєкту оновлюється лише один раз — коли користувач завершує редагування і виходить з поля (подія
@blurабо натискання Enter). - Замість фіксованих інпутів використали
<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:
- Намалювали красиву модалку з тарифами Pro та Business.
- При натисканні на тариф з’являється форма: «Наразі сервіс у приватній беті. Залиште свій email, і ми надішлемо вам промокод на знижку 50% відразу після відкриття оплат!».
- Дані відправляються на ендпоінт
/api/leads.
Це дозволило перевірити реальну конверсію відвідувачів у покупців. Спойлер: ми вже отримали перші ліди в базу, хоча трафіку майже не було! Тепер ми точно знаємо, що інтерес є, і маємо перших потенційних клієнтів.
📈 Результати та висновки
Розробка такого інструменту дозволила нам на практиці вирішити реальні проблеми продуктивності веб-додатків:
- Навчилися вичавлювати максимум швидкості з Firestore без зайвих фінансових витрат.
- Переконалися, що робота з великими динамічними таблицями у Vue вимагає ретельної ізоляції локальних станів.
- Перевірили гіпотезу платоспроможності клієнтів ще до написання коду білінгу.
Будемо раді почути ваші думки про архітектуру та ідею проєкту! Як ви зазвичай вирішуєте питання локалізації у своїх командах? Напишіть у коментарях!
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів