Девлог #20 — Автоматизація конвеєра i18n EN→UK: Python, машинний переклад та паралельні дерева

Вручну перекладати 200+ сторінок симуляцій українською було неможливо масштабувати. Цей пост описує конвеєр автоматизації, який ми побудували: порівняння HTML-структур, машинний переклад на рівні сегментів, робочий процес пост-редагування, впровадження hreflang та паралельне дерево каталогів /uk/, що дзеркалить кожну англомовну сторінку симуляції.

Проблема: два паралельні дерева контенту

mysimulator.uk обслуговує два повних дерева контенту: англійське (корінь) та українське (/uk/). Кожна симуляція має відповідну українську сторінку. Коли ми додаємо нову симуляцію або оновлюємо контент на англійській стороні, українська версія має залишатися синхронною — інакше сайт стає неузгодженим для українських користувачів.

Спочатку це був ручний процес. Він працював для перших 30 симуляцій. До 80-ї симуляції це стало вузьким місцем. До 150-ї — це стало неможливим. Рішення: конвеєр на Python, що виявляє зміни в англійському HTML, витягує текстові вузли для перекладу, перекладає їх і записує назад структуровану українську HTML-розмітку.

Огляд конвеєра

1
Виявлення відмінностей

Порівняння англійського index.html з наявним /uk/index.html шляхом хешування текстового вмісту кожного елемента, що підлягає перекладу. У чергу на переклад ставляться лише змінені сегменти — незмінений текст залишається як є, щоб уникнути повторного перекладу відредагованого людиною тексту.

2
Парсинг HTML і вилучення тексту

BeautifulSoup 4 парсить вихідний HTML. Текстові вузли всередині h1, h2, h3, p, li, meta[name=description], title та атрибутів alt витягуються у вигляді плоского списку сегментів. Структура зберігається за допомогою XPath-подібних ідентифікаторів слотів.

3
Машинний переклад

Сегменти пакетуються і надсилаються до API перекладу групами по 50. Технічні терміни — назви симуляцій, назви алгоритмів, власні назви, одиниці виміру — захищаються за допомогою глосарію та XML-плейсхолдерів, щоб вони проходили без перекладу.

4
Черга перевірки пост-редагування

Перекладені сегменти з оцінкою впевненості нижче 0.85 записуються у файл review_queue.json. Людина- рецензент може відкрити цей файл, виправити записи й повторно запустити конвеєр. Перевірені сегменти кешуються у translation_memory.json, тож вони ніколи не перекладаються повторно.

5
Реконструкція HTML

Перекладені сегменти вставляються назад у структуру HTML з використанням ідентифікаторів слотів із кроку 2. Атрибути URL (href, src, action) переписуються, щоб коректно вирішувати відносні шляхи в /uk/slug/index.html.

6
Впровадження hreflang і canonical

Скрипт вставляє коректні теги <link rel="canonical"> та <link rel="alternate" hreflang> для англійської та української версій, вказуючи кожну одну на іншу.

# translate_sims.py — спрощений основний цикл
from bs4 import BeautifulSoup
import json, hashlib, pathlib

TRANS_TAGS = {'h1', 'h2', 'h3', 'p', 'li', 'title'}

def extract_segments(html_path: pathlib.Path) -> list[dict]:
    soup = BeautifulSoup(html_path.read_text('utf-8'), 'html.parser')
    segments = []
    for i, tag in enumerate(soup.find_all(TRANS_TAGS)):
        text = tag.get_text(strip=True)
        if len(text) > 3:
            segments.append({
                'id': f'{tag.name}_{i}',
                'text': text,
                'hash': hashlib.sha1(text.encode()).hexdigest()[:8],
            })
    return segments

def apply_translations(source_html: str, translations: dict) -> str:
    soup = BeautifulSoup(source_html, 'html.parser')
    for i, tag in enumerate(soup.find_all(TRANS_TAGS)):
        key = f'{tag.name}_{i}'
        if key in translations:
            tag.clear()
            tag.append(translations[key])
    return str(soup)

Робота з технічними термінами: захист глосарію

Фізичні та математичні терміни погано перекладаються — «Навʼє-Стокс», «Рунге-Кутта», «leaky integrate-and-fire» — усі мають проходити без змін. Наш підхід із глосарієм огортає захищені терміни у XML-подібні плейсхолдер-теги перед перекладом:

# Вхід:  "The Runge-Kutta RK4 integrator solves stiff ODEs."
# Після захисту:
#   "The <P1/> integrator solves stiff ODEs."
#   protected_map = {'P1': 'Runge-Kutta RK4'}
# Після MT:
#   "Інтегратор <P1/> розв'язує жорсткі ЗДР."
# Після відновлення:
#   "Інтегратор Runge-Kutta RK4 розв'язує жорсткі ЗДР."

Перевірка синхронізації в CI

CI-процес запускається при кожному push у main. Він порівнює маніфест хешів англійських сторінок з українським маніфестом і звітує про будь-які сторінки, що вийшли з синхронізації:

# CI workflow: i18n-sync-check (фрагмент)
jobs:
  sync-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check EN/UK sync
        run: python _check_sync.py --report --fail-on-drift

Засвоєні уроки: найскладнішою частиною був не сам переклад — а точне збереження структури HTML. BeautifulSoup при серіалізації дерева іноді згортає самозакривні теги та непомітно змінює порядок атрибутів. Ми перейшли на стратегію реконструкції на основі слотів (замість мутації всередині дерева), щоб отримати байт-стабільний вивід.