Problem: dwa równoległe drzewa treści
mysimulator.uk obsługuje dwa pełne drzewa treści:
angielskie (korzeń) i ukraińskie
(/uk/). Każda symulacja ma odpowiadającą jej stronę
ukraińską. Gdy dodajemy nową symulację lub aktualizujemy treść po
stronie angielskiej, wersja ukraińska musi pozostać zsynchronizowana
— inaczej strona staje się niespójna dla ukraińskich użytkowników.
Początkowo był to proces ręczny. Działał dla pierwszych 30 symulacji. Przy symulacji nr 80 stał się wąskim gardłem. Przy 150 — był już nie do utrzymania. Rozwiązaniem był potok w Pythonie, który wykrywa zmiany w angielskim HTML, wyodrębnia węzły tekstowe do przetłumaczenia, tłumaczy je i zapisuje z powrotem ustrukturyzowany ukraiński HTML.
Przegląd potoku
Porównanie angielskiego index.html z istniejącym
/uk/index.html poprzez haszowanie treści
tekstowej każdego elementu podlegającego tłumaczeniu. Do
kolejki tłumaczenia trafiają tylko zmienione segmenty —
niezmieniony tekst pozostaje bez zmian, aby uniknąć ponownego
tłumaczenia tekstu poprawionego przez człowieka.
BeautifulSoup 4 parsuje źródłowy HTML. Węzły
tekstowe wewnątrz h1, h2,
h3, p, li,
meta[name=description], title oraz
atrybutów alt są wyodrębniane jako płaska lista
segmentów. Struktura jest zachowywana za pomocą
identyfikatorów slotów przypominających XPath.
Segmenty są grupowane i wysyłane do API tłumaczenia w partiach po 50. Terminy techniczne — nazwy symulacji, nazwy algorytmów, nazwy własne, jednostki — są chronione za pomocą glosariusza i placeholderów w stylu XML, dzięki czemu przechodzą bez tłumaczenia.
Przetłumaczone segmenty z wynikiem pewności poniżej 0.85 są
zapisywane do pliku review_queue.json. Recenzent
może otworzyć ten plik, poprawić wpisy i ponownie uruchomić
potok. Sprawdzone segmenty są buforowane w
translation_memory.json, więc nigdy nie są
tłumaczone ponownie.
Przetłumaczone segmenty są wstawiane z powrotem do struktury
HTML z użyciem identyfikatorów slotów z kroku 2. Atrybuty URL
(href, src, action)
są przepisywane, aby poprawnie rozwiązywać ścieżki względne
pod /uk/slug/index.html.
Skrypt wstawia poprawne tagi
<link rel="canonical"> oraz
<link rel="alternate" hreflang> zarówno dla
wersji angielskiej, jak i ukraińskiej, wskazując wzajemnie na
siebie.
# translate_sims.py — uproszczona główna pętla
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)
Obsługa terminów technicznych: ochrona glosariusza
Terminy fizyczne i matematyczne nie tłumaczą się dobrze — „Navier-Stokes”, „Runge-Kutta”, „leaky integrate-and-fire” — wszystkie muszą przejść bez zmian. Nasze podejście z glosariuszem owija chronione terminy w tagi placeholderów w stylu XML przed tłumaczeniem:
# Wejście: "The Runge-Kutta RK4 integrator solves stiff ODEs."
# Po ochronie:
# "The <P1/> integrator solves stiff ODEs."
# protected_map = {'P1': 'Runge-Kutta RK4'}
# Po MT:
# "Integrator <P1/> rozwiązuje sztywne równania różniczkowe zwyczajne."
# Po przywróceniu:
# "Integrator Runge-Kutta RK4 rozwiązuje sztywne równania różniczkowe zwyczajne."
Sprawdzanie synchronizacji w CI
Proces CI uruchamia się przy każdym pushu do main.
Porównuje manifest haszy stron angielskich z manifestem ukraińskim
i zgłasza wszystkie strony, które wypadły z synchronizacji:
# CI workflow: i18n-sync-check (fragment)
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
Wnioski: najtrudniejszą częścią nie było samo
tłumaczenie — było nią wierne zachowanie struktury HTML.
BeautifulSoup podczas serializacji drzewa czasami
zwija samo-zamykające się tagi i niepostrzeżenie zmienia kolejność
atrybutów. Przeszliśmy na strategię rekonstrukcji opartej na
slotach (zamiast mutacji wewnątrz drzewa), aby uzyskać
bajtowo-stabilny wynik.