Dlaczego ukraiński?
Ten projekt służy studentom nauk ścisłych na całym świecie. Po angielskim największą niedostatecznie obsłużoną grupą — pod względem materiałów edukacyjnych STEM w ojczystym języku — okazał się ukraiński. Decyzja miała też wymiar osobisty: opiekunowie projektu są Ukraińcami.
Pytanie nie brzmiało czy to zrobić, tylko jak zarządzać 293 plikami HTML na statycznej stronie bez narzędzi budowania, nie zamieniając przy tym utrzymania wersji angielskich w koszmar.
Decyzja architektoniczna
Rozważano trzy podejścia:
-
Wykrywanie języka po stronie serwera —
przekierowanie na podstawie
Accept-Language. Odrzucone: strona to czysty statyczny HTML, serwowany jako pliki statyczne z naszego CDN. Żadnej logiki serwerowej. - JavaScript i18n na jednej stronie (i18next itp.) — jeden plik HTML, JS podmienia teksty. Odrzucone: fatalne dla SEO, wymaga paczki JS na każdej stronie, a każda symulacja i tak już ładuje Three.js — dodanie biblioteki i18n do 225 stron symulacji było nie do przyjęcia.
-
Równoległe drzewo plików statycznych pod
/uk/— dokładne lustro angielskiej struktury z przetłumaczonym HTML. Każda strona ma własny adres URL, własne metadane, własny hreflang wskazujący z powrotem na angielski kanoniczny adres. To właśnie zbudowaliśmy.
Kompromis: równoległe drzewa podwajają liczbę plików. 293 strony ukraińskie na dodatek do 326 angielskich = 619 łącznie. Ale adresy URL są stabilne, SEO jest czyste i nie ma żadnych zależności JS na potrzeby tłumaczenia. Warto.
Dzień 1 — Szkielet struktury plików
Pierwszym zadaniem było napisanie skryptu Python, który duplikował
każdy folder symulacji pod /uk/ i korygował wszystkie
ścieżki względne. Trudna część: ścieżki takie jak
../../shared/theme.css (2 poziomy wyżej od
/categories/) zamieniają się w
../../../../shared/theme.css (4 poziomy wyżej od
/uk/categories/). Pomyłka tutaj cicho psuje każdy plik
stylów.
import os, re, shutil
def fix_paths(html: str, extra_depth: int) -> str:
prefix = '../' * extra_depth
# Fix relative src/href that start with ../../ but not //
return re.sub(
r'((?:src|href)=["\'])(?!https?://|//|#)(\.\./)',
lambda m: m.group(1) + prefix + m.group(2),
html
)
Dzień 2 — Przebieg tłumaczenia
Każda strona symulacji ma trzy kluczowe bloki tekstu do
przetłumaczenia: <title> strony, meta description
oraz panel opisu symulacji (sekcja „Co to pokazuje / Jak używać /
Czy wiesz, że?"). Wszystko inne — sterowanie interfejsem, etykiety
osi, fragmenty kodu — pozostaje po angielsku.
Tłumaczenie wykonano ręcznie z pomocą narzędzi językowych do korekty, stawiając na naturalną polszczyznę zamiast tłumaczenia dosłownego. Terminologia naukowa jest zgodna z ukraińskim standardem akademickim tam, gdzie istnieją ustalone odpowiedniki, a w pozostałych przypadkach termin angielski podawany jest w nawiasie.
Wymóg hreflang
Każda strona EN musiała zyskać
<link rel="alternate" hreflang="uk"> wskazujący
na swój ukraiński odpowiednik. Każda strona UK potrzebowała
wzajemnego linku do wersji EN. A obie potrzebowały
hreflang="x-default" wskazującego na angielski
kanoniczny adres. Bez wzajemnej pary Google całkowicie ignoruje
hreflang.
<!-- In /boids/index.html (English) -->
<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/">
<!-- In /uk/boids/index.html (Ukrainian) -->
<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/">
Skrypt walidujący sprawdzał każdą stronę wyrażeniem regularnym: wszystkie trzy tagi hreflang muszą istnieć i być spójne ze ścieżką pliku. Żadna strona nie została opublikowana, dopóki audyt nie pokazał 0 błędów.
Dzień 3 — Komponent przełącznika języka
Przełącznik języka to pojedynczy tag <a>
wstrzykiwany przez components.js do paska nawigacji.
Odczytuje bieżący adres URL i tworzy adres URL alternatywnego
języka w następujący sposób:
-
Jeśli ścieżka zaczyna się od
/uk/→ usuń ten prefiks (przejdź do wersji EN) -
W przeciwnym razie → dodaj prefiks
/uk/(przejdź do wersji UK)
function buildLangUrl(base) {
const path = window.location.pathname;
if (path.startsWith('/uk/')) {
return base + path.slice(3); // strip /uk prefix
}
return base + 'uk' + path; // add /uk prefix
}
Jest to zawodne dokładnie w jednym przypadku: stron, które istnieją po angielsku, ale jeszcze nie po ukraińsku. Przełącznik i tak się pokazuje, prowadząc do 404. Krok wstępnego przetwarzania generuje manifest JSON wszystkich istniejących ścieżek UK, a przełącznik sprawdza go przed pokazaniem przycisku.
Dzień 4 — Mapa strony, Robots, wdrożenie
Mapa strony musiała zawierać zarówno adresy URL EN, jak i UK, z
odpowiednimi alternatywami hreflang <xhtml:link>
w każdym wpisie. Skrypt Python generujący sitemap.xml
zaktualizowano tak, by przechodził jednocześnie przez drzewa EN i
UK i generował poprawne alternatywy.
Finalne wdrożenie dodało 293 nowe pliki i zmodyfikowało 326 istniejących plików EN (aby dodać hreflang). Łączny diff: ~1 500 zmienionych plików w repozytorium. Serwer WWW obsłużył je bez żadnej zmiany konfiguracji — statyczny HTML po prostu działa.
Co nas zaskoczyło
-
Audyt hreflang zajął więcej czasu niż samo tłumaczenie.
Drobne niespójności — ukośnik na końcu jednego linku, ale nie
drugiego, rozbieżność
/uk/kontra/uk— cicho psują interpretację Google. Skrypt walidatora wykrył 47 takich błędów przed wdrożeniem. - Ukraińska terminologia STEM okazała się bogatsza, niż się spodziewaliśmy. Dla większości pojęć z fizyki i matematyki istnieją ustalone ukraińskie terminy. Trudność polegała na konsekwencji — używaniu tego samego terminu na każdej stronie, a nie tego, który akurat przyszedł do głowy jako pierwszy.
- Organiczny ruch ukraiński pojawił się w ciągu 2 tygodni od startu. Żadnej promocji — tylko czysty hreflang, poprawne kanoniczne adresy i treści niedostępne nigdzie indziej po ukraińsku.