Devlog #11 — Dodanie ukraińskiej wersji w 4 dni

Żadnego CMS-a. Żadnego API tłumaczeniowego. Tylko skrypty Python, mnóstwo starannej pracy z hreflang i przełącznik języka zbudowany od zera. Oto jak w jednym sprincie wypuściliśmy pełną ukraińską wersję strony.

293
utworzonych stron ukraińskich
4
dni od startu do wdrożenia
0
wywołań API tłumaczeniowego

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:

  1. 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.
  2. 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.
  3. 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

📅 Dzień 1

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

📅 Dzień 2

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

📅 Dzień 3

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:

  1. Jeśli ścieżka zaczyna się od /uk/ → usuń ten prefiks (przejdź do wersji EN)
  2. 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

📅 Dzień 4

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