Девлог #11 — Додавання української за 4 дні

Жодної CMS. Жодного API перекладу. Лише Python-скрипти, багато ретельної роботи з hreflang та перемикач мов, створений з нуля. Ось як ми випустили повну українську версію сайту за один спринт.

293
Створено українських сторінок
4
дні від старту до релізу
0
викликів API перекладу

Чому українська?

Цей проєкт обслуговує студентів природничих наук по всьому світу. Після англійської найбільшою аудиторією, яка недостатньо забезпечена ресурсами STEM-освіти рідною мовою, виявилась українська. Це рішення було й особистим: супроводжувачі проєкту — українці.

Питання полягало не в тому, чи варто це робити, а в тому, як керувати 293 HTML-файлами на статичному сайті без інструментів збірки, не перетворюючи підтримку англійських версій на кошмар.

Архітектурне рішення

Розглядалися три підходи:

  1. Визначення мови на стороні сервера — перенаправлення на основі Accept-Language. Відхилено: сайт — це чистий статичний HTML, що подається як статичні файли з нашого CDN. Жодної серверної логіки.
  2. JavaScript i18n на одній сторінці (i18next тощо) — один HTML-файл, JS підміняє рядки. Відхилено: жахливо для SEO, вимагає JS-бандла на кожній сторінці, а кожна симуляція вже завантажує Three.js — додавання бібліотеки i18n до 225 сторінок симуляцій було неприйнятним.
  3. Паралельне дерево статичних файлів у /uk/ — точне дзеркало англійської структури з перекладеним HTML. Кожна сторінка має власну URL-адресу, власні метадані, власний hreflang, що вказує назад на англійський канонічний URL. Саме це ми й побудували.

Компроміс: паралельні дерева подвоюють кількість файлів. 293 українські сторінки на додачу до 326 англійських = 619 загалом. Але URL-адреси стабільні, SEO чисте, і немає жодних JS-залежностей для перекладу. Того варте.

День 1 — Побудова структури файлів

📅 День 1

Першим завданням було написати Python-скрипт, який дублював кожну папку симуляції під /uk/ і коригував усі відносні шляхи. Складна частина: шляхи на кшталт ../../shared/theme.css (2 рівні вгору від /categories/) перетворюються на ../../../../shared/theme.css (4 рівні вгору від /uk/categories/). Помилка тут непомітно ламає кожен файл стилів.

import os, re, shutil

def fix_paths(html: str, extra_depth: int) -> str:
    prefix = '../' * extra_depth
    # Виправляємо відносні src/href, що починаються з ../../, але не з //
    return re.sub(
        r'((?:src|href)=["\'])(?!https?://|//|#)(\.\./)',
        lambda m: m.group(1) + prefix + m.group(2),
        html
    )

День 2 — Робочий процес перекладу

📅 День 2

Кожна сторінка симуляції має три ключові текстові блоки для перекладу: <title> сторінки, meta description та панель опису симуляції (розділ «Що це показує / Як користуватися / Чи знали ви?»). Усе інше — елементи керування інтерфейсом, підписи осей, фрагменти коду — залишається англійською.

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

Вимога щодо hreflang

Кожна англійська сторінка мала отримати <link rel="alternate" hreflang="uk">, що вказує на її український відповідник. Кожна українська сторінка потребувала зворотного посилання на англійську версію. І обидві потребували hreflang="x-default", що вказує на англійський канонічний URL. Без зворотної пари Google повністю ігнорує hreflang.

<!-- У /boids/index.html (англійська) -->
<link rel="alternate" hreflang="en" href="https://www.mysimulator.uk/algorithms/boids/">
<link rel="alternate" hreflang="uk" href="https://www.mysimulator.uk/uk/algorithms/boids/">
<link rel="alternate" hreflang="x-default" href="https://www.mysimulator.uk/algorithms/boids/">

<!-- У /uk/boids/index.html (українська) -->
<link rel="alternate" hreflang="en" href="https://www.mysimulator.uk/algorithms/boids/">
<link rel="alternate" hreflang="uk" href="https://www.mysimulator.uk/uk/algorithms/boids/">
<link rel="alternate" hreflang="x-default" href="https://www.mysimulator.uk/algorithms/boids/">

Скрипт валідації перевіряв кожну сторінку за регулярним виразом: усі три теги hreflang мають існувати та відповідати шляху файлу. Жодна сторінка не публікувалася, доки аудит не показував 0 помилок.

День 3 — Компонент перемикача мов

📅 День 3

Перемикач мов — це єдиний тег <a>, що вставляється components.js у навігаційну панель. Він зчитує поточний URL і формує URL альтернативної мови так:

  1. Якщо шлях починається з /uk/ → видалити його (перейти до англійської версії)
  2. Інакше → додати /uk/ на початок (перейти до української версії)
function buildLangUrl(base) {
  const path = window.location.pathname;
  if (path.startsWith('/uk/')) {
    return base + path.slice(3);   // прибираємо префікс /uk
  }
  return base + 'uk' + path;       // додаємо префікс /uk
}

Це крихко рівно в одному випадку: сторінки, які існують англійською, але ще не мають української версії. Перемикач все одно показується, посилаючись на 404. Крок попередньої обробки генерує JSON-маніфест усіх наявних українських шляхів, і перемикач перевіряє його перед показом кнопки.

День 4 — Мапа сайту, Robots, Розгортання

📅 День 4

Мапа сайту мала містити як англійські, так і українські URL-адреси з відповідними альтернативами hreflang <xhtml:link> у кожному записі. Python-скрипт, що генерує sitemap.xml, оновили, щоб він обходив дерева EN та UK разом і видавав правильні альтернативи.

Фінальне розгортання додало 293 нові файли та змінило 326 наявних англійських файлів (щоб додати hreflang). Загальний diff: ~1 500 змінених файлів у репозиторії. Веб-сервер обслуговував їх без жодних змін конфігурації — статичний HTML просто працює.

Що нас здивувало