Od sierpnia każda wersja językowa serwisu na moim silniku była osobną instancją: polska pod jednym adresem, angielska pod subdomeną, każda z własnym repozytorium, bazą i wdrożeniem. Dziś to się skończyło. Dwa serwisy zostały połączone, a ta strona dostała wersję angielską od razu w nowym układzie — pod /en, w tym samym wdrożeniu.
Dlaczego
Osobne instancje były łatwe do postawienia i drogie w utrzymaniu. Każda poprawka treści szła dwa razy: dwa commity, dwa wdrożenia, dwie weryfikacje. Reguła „wersja angielska w tej samej sesji” była pilnowana ręcznie, czyli czasem nie była. Statystyki leżały w dwóch bazach i raport trzeba było sklejać. Jedna z wersji wdrażała się ręcznie, bo automatyczne budowanie wymagało kliknięcia w panelu. A czytelnik zalogowany po polsku po przejściu na angielski był dla serwisu kimś obcym, bo sesja siedzi na hoście.
Co to daje
Jeden push wdraża obie wersje. Jedna baza trzyma oba języki — każdy rekord dotyczący strony ma kolumnę z językiem, a rzeczy dotyczące materiału (oceny, komentarze, głosy) i tak od początku były kluczowane kluczem wpisu ponad językiem. Jedna domena to jedna tożsamość przeglądarki, jedna sesja i jedna mapa strony dla wyszukiwarki. Zamiast pięciu wdrożeń do pilnowania są trzy. Stare adresy angielskie przekierowują regułą 301 na nowe, ze ścieżką i parametrami.
Jak
O języku wpisu decyduje folder: wpisy polskie leżą tam, gdzie leżały, angielskie w src/content/en/posts. O języku strony decyduje adres — przedrostek /en wynika z adresu wersji zapisanego w jej konfiguracji, więc nie ma osobnego pola, które mogłoby się z nim rozjechać.
Trudniejsza część była gdzie indziej. Funkcja zwracająca konfigurację woła się w setkach miejsc i do tej pory zawsze zwracała to samo. Teraz musi wiedzieć, w której wersji jest żądanie. Zmienna globalna odpadła od razu: jeden izolat Workera obsługuje żądania równolegle, a dwa renderowania przeplatają się na każdym await. Język jest więc stanem żądania — na serwerze niesie go AsyncLocalStorage, w przeglądarce przedrostek w pasku adresu.
strona, trasa serwerowa przedrostek ścieżki — i tylko on
server function nagłówek z przeglądarki (wspólny adres bez przedrostka)
przeglądarka location.pathname
mapa strony, powiadomienia withLang(kod, fn) — po kolei dla każdej wersjiPierwszy pomysł na przedrostek, basepath routera, nie przeżył zderzenia z TanStack Start: serwer nadpisuje go przy każdym żądaniu wartością z czasu budowania. Przeżył rewrite — router widzi adresy bez przedrostka, przeglądarka z przedrostkiem. Sprawdziliśmy to prototypem, zanim powstało cokolwiek więcej.
Baza dostała nową tabelę odsłon z kolumną języka i rejestr migracji danych. Stara tabela została nietknięta. Dane starych baz angielskich przeniósł skrypt, który zatrzymuje się przed pierwszym zapisem, gdy trafi na tabelę, której nie zna. Każda migracja najpierw przeszła próbę generalną na kopii bazy produkcyjnej, na tymczasowej bazie usuniętej zaraz potem.
Co poszło nie tak
Popołudnie skończyło się czterema wydaniami poprawkowymi. Trzy wynikały ze scalenia i każde uczy czegoś innego.
Migracja uzupełniała język w starych zdarzeniach, zanim kolumnę założył moduł, który ją dokłada — ale dopiero przy pierwszym zapisie zdarzenia. Próba generalna tego nie złapała, bo skrypt przenoszący dane założył kolumnę wcześniej. Test sprawdził kolejność, która działa, i przemilczał tę, która zdarzyła się naprawdę. Od tej pory migracja sama zakłada każdą kolumnę, której dotyka.
Nagrania z magazynu obiektów pod /en odpowiadały 404. Router zdejmował przedrostek z tego, co widzi, ale trasa mediów czytała klucz z samego adresu żądania. Lokalnie nie było tego widać, bo lokalny magazyn jest pusty.
Najbardziej pouczające: przełącznik języka działał w jedną stronę. Na angielski przechodził, z powrotem na polski — nie. Zapasowe źródło języka dla server functions, nagłówek Referer, rozstrzygało także zwykłe strony: kliknięcie „Polski” na angielskiej stronie niosło w nagłówku adres angielskiej i serwer odsyłał z powrotem. Testy szły curlem, który tego nagłówka nie wysyła. Wyszło przy ręcznym klikaniu na produkcji.
Wniosek
Każda z tych usterek była tym samym założeniem — jedno wdrożenie, jeden język — ukrytym w innym miejscu kodu. Żadnej nie znalazłby test pojedynczego żądania, bo pojedyncze żądanie zawsze wie, w jakim jest języku. Znalazły je dopiero rzeczy, które robi człowiek: klik, przejście, powrót. Od dziś przełącznik testuję klikaniem, kilka razy z rzędu, w obie strony.
Stan na koniec dnia: silnik ma 132 wydania w 38 dni, trzy wdrożenia i sześć wersji językowych. Pełny opis, z odrzuconymi wariantami i listą pułapek, jest w dokumentacji silnika — przyda się przy następnym takim połączeniu.