23.04.2019
Autorzy:

Tłumaczenia i lokalizacje w Symfony 3.4 i 4.0

Tłumaczenie strony to temat bardzo popularny i lekki, doskonale pasujący na wprowadzenie w Symfony. W niniejszym artykule postaram się odrobinę usystematyzować całe zagadnienie – skupimy się na Symfony w wersji 3.4 i 4.0. Zamieszczone kawałki kodu były przetestowane na obu wersjach Symfony, ale aplikacja, która powstała w trakcie pisania artykułu używa Symfony 4.0. 


Klasyczne podejście do problemu 

Bardzo często tworząc projekt nie zwracamy uwagi na jego przyszłą internacjonalizacje, aż do pewnego dnia, gdy okazuje się, że druga wersja językowa jest już potrzeba. W przypadku projektów stworzonych zgodnie ze sztuką, niezależnie od wybranych technologii, tłumaczenie nie powinno być tu dużym problemem. Jednak czy na pewno? Wszelkie wiadomości, etykiety, tytuły i komentarze występujące w naszych szablonach powinny być łatwo przetłumaczalne, niezależnie od tego, czy tłumaczenia były planowane, czy nie. Takie dane, będące tylko i wyłącznie stałymi, powinny być zgrupowane razem, w jednym, bądź kilku plikach – np. w pliku tłumaczeń.

Co jednak z naszą bazą danych? Wprowadzone do niej produkty, kategorie czy artykuły też powinny być jakoś uwzględnione w naszych tłumaczeniach. Jaką drogę tutaj należy obrać? Na każdą tabelę z treścią, która ma być tłumaczona stworzyć drugą z samymi tłumaczeniami? Może użyć jednej tabeli, która będzie tłumaczyć wszystko? Może rozwiążemy ten problem za pomocą jakiegoś duplikowania rekordów w naszych tabelach? Zastanówmy się nad tym w kolejnych krokach. 

Tłumaczenie a lokalizacja

Najpierw zadajmy sobie pytanie: Czego oczekujemy? Tłumaczenia? Lokalizacji? Przecież to nie to samo. Lokalizacja stanowi szersze zagadnienie, w zależności od wersji strony musimy nie tylko przetłumaczyć teksty, ale też często zmienić format wyświetlanych dat, w przypadku sklepu zmienić walutę lub dokonać jakiejś innej zmiany w naszym projekcie. 

Przygotowanie 

Nim zaczniemy wdrażać różne wersje językowe naszego projektu, musimy upewnić się, czy rozpoczynając nad nimi pracę zachowywaliśmy rozsądny porządek. Wszelkie nasze stałe – zarówno te numeryczne, te które niedługo mają stać się ciągami tłumaczonymi na różnorakie języki, jak i wszystko inne co ma ustaloną wartość, powinno być w jakiś sposób pogrupowane i co najważniejsze – odseparowane od reszty kodu. 

Tak jak oczywiste powinno być dla nas trzymanie dyrektyw CSS w szablonach stylów, nie zaś osadzonych w treści HTML, podobnie powinno być ze wszystkimi etykietami wplecionymi w nasze szablony. Grupowanie ich razem, choćby w plikach tłumaczeń, nie powinno być dla nas ani trochę dziwne . Co jednak z naszymi komunikatami wyjątków? Czy o nich pamiętaliśmy, czy zgrupowaliśmy je razem? To też są stałe

Oprócz tłumaczeń, istnieje też problem lokalizacji. Musimy pamiętać o wyświetlaniu dat, czasem uwzględnić jakąś walutę lub jednostki miary. Tutaj też będzie nam potrzebny porządek. Upewnijmy się, że posiadamy tylko jedną usługę, która przetwarza obiekty DateTime i odpowiada za ich formatowanie. Jeżeli nasz projekt jest pokryty sensownie napisanymi testami, to jest duża szansa, że obiekt ten nie jest zmienną globalną, singletonem, czy inną statyczną chimerą, lecz poprawnie wstrzykiwanym serwisem. 

Zakładając, że już posiadamy projekt w Symfony i uporaliśmy się z naszym bałaganem w stałych i w duplikowaniu różnych odpowiedzialności, to sprawa tłumaczeń i lokalizacji powinna być dość łatwa. Zacznijmy od ustalenia, jakie wersje językowe są dostępne na naszej stronie. Aby było ciekawiej proponuję dwie wersje językowe: polską i angielską oraz trzy lokalizacje: Polska, Wielka Brytania i Stany Zjednoczone.

Daje nam to 3 dostępne localeen_USen_GB i pl_PL. Warto tutaj zwrócić uwagę na format locale. Zastosowano format zgodny ze standardami ISO 639-1 i ISO 3166-1 alpha-2. Pierwszy człon odpowiada za język, drugi za kraj. Czasem jednak nasze locale zawierać będzie tylko język. Korzystając z zewnętrznych bibliotek i bundli, musimy przygotować się na to, że twórcy wykorzystywanego przez nas kodu nie zawsze przestrzegają normy i standardy. Wtedy nasze locale może nie być zgodne z tymi standardami. Pokusa prostego rozwiązania, może nas skusić do dostosowania naszego projektu do błędów innych. Nie powinniśmy tego robić, ale jak wiadomo – każdy czasem lubi pójść na skróty. 

Czas wybrać domyślne locale. W pliku app/config/config.yml lub parameters.yml (lub pliku config/services.yaml w przypadku Symfony 4) dodajmy nowy parametr: 

Będzie on odpowiadał za domyślną lokalizacje strony.

Kolejnym krokiem jest dodanie wykorzystywania naszego nowo dodanego parametru. Do pliku app/config/config.yml (w przypadku Symfony 4 będzie to plik: config/packages/translation.yaml):

Te dwa ustawienia odpowiadają za domyślną lokalizację.

Pamiętajcie proszę, że w przypadku Symfony 4, translator nie będzie domyślnie dostępny, potrzebne będzie wywołanie: composer require translator

Organizacja podstawowych tłumaczeń

Ustawiliśmy domyślny translator, czas wybrać format tłumaczeń i zapełniać nimi odpowiednie pliki. W kwestii formatu wybieram dla nas pliki yaml. Teraz przygotujemy jakąś spójną koncepcje ich wypełniania. Jakakolwiek by ona nie była, będzie dobra, jeżeli będzie spójna i wszyscy członkowie projektu będą wiedzieć gdzie szukać i dodawać nowe tłumaczenia. 

Jako mój przykład takiej organizacji mogę zaproponować: 

 Ta koncepcja zakłada podzielenie tłumaczeń ze względu na funkcjonalności naszego projektu. Zakładam tutaj sortowanie tłumaczeń alfabetycznie, po pełnym kluczu tłumaczenia, pomoże to uniknąć przypadkowego dodanie podobnych tłumaczeń. Sugeruję stworzenie zbioru jakiś generycznych tłumaczeń, wspólnych dla różnych funkcjonalności projektu. Nie zamierzam też w żaden sposób sugerować wam co z punktu widzenia tłumaczeń należy uznać za odrębną funkcjonalność, zaś co jedynie za część innej. Nie znam waszych systemów. 

Bardzo jednak uczulam na to, aby nazwy tłumaczeń i ich wartości były ze sobą spójne w sensie domenowym. Starajcie się, aby w tłumaczeniach i kodzie pojawiały się takie same nazwy jak w specyfikacji dostarczanej przez wasz biznes. Może was to ustrzec przed pewnymi nieporozumieniami i wieloma liniami niepotrzebnego kodu. 

Wykorzystanie translatora 

Ustawiliśmy translator, wypełniliśmy plik z tłumaczeniami, czas aby nasz projekt zaczął wykorzystywać te tłumaczenia. Możemy już wstrzyknąć usługę translatora do każdej naszej klasy i cieszyć się ładnie tłumaczonym tekstem. Czy jednak na pewno wszędzie? Warto wyznaczyć sobie pewne granice, gdzie translator będzie nam niezbędny, bo wszędzie indziej lepiej aby go nie było. 

Osobiście postrzegam translator jako coś bardzo mocno związanego z widokiem. Chciałbym, aby mój translator był używany tylko i wyłącznie w szablonach twig. Nie zawsze będzie to jednak możliwe. Jeżeli nasza aplikacja wystawia jakiekolwiek API, choćby dla najprostszego ajaxa, niezbędne może się okazać czasem wykorzystać translator poza szablonem.

Kontroler? Może jednak lepiej nie, preferowałbym używać go w jakiś handlerach i niekiedy w event listnerach, ale ze względów wydajnościowych, tam też lepiej nie. 

Zbędne tłumaczenia 

Wraz z upływem czasu tłumaczeń będzie coraz więcej, czasem niektóre będą wychodzić z użycia, nie zawsze jednak będą one usuwane z plików tłumaczeń. Prędzej, lub jeszcze prędzej, czeka nas bałagan w tych plikach. Z pomocą jednak przychodzi Symfony. W dokumentacji Symfony znajdziecie przykład komendy:

php bin/console debug:translation en-EN AppBundle 

lub w przypadku Symfony 4: 

php bin/console debug:translation en-EN

W kwestii porządkowania naszych tłumaczeń może nam to pomóc, ale nie oczekujcie zbyt wiele. Im bardziej skomplikowany jest wasz projekt, a co za tym idzie im więcej błędów się spodziewacie w waszych tłumaczeniach, tym więcej fałszywych alarmów znajdziecie na wyjściu tejże komendy. 

Warto też zwrócić uwagę na to, że w naszym przypadku aplikacja zakłada dwie lokalizacje anglojęzyczne. Jeżeli nasz projekt będzie wystarczająco prosty, może się okazać, że zawartość pliku messages.en_US.yaml i messages.en_GB.yaml będzie taka sama, sugeruje wtedy zamiast tworzyć dwa pliki zastosować dowiązanie symboliczne. 

Przełącznik języka

Kolejnym zagadnieniem z którym się zmierzymy jest przełącznik języka, czy też raczej lokalizacji. Najbardziej klasyczna wersja zakłada trzymanie informacji o lokalizacji w sesji i wpięciu listenera pod wydarzenie onKernelRequest. Pamiętajmy jednak, że dodajemy kod, który będzie wykonywany przy każdym requeście. Jeżeli zależy nam na wydajności, to może to być dla nas bardzo kosztowne.

Przykładowy listener może wyglądać następująco:

Zaś w naszym services.yml umieścimy: 

Upewnijcie się, że opcje autowire i autoconfigure są ustawione poprawnie.

Musimy jeszcze utworzyć akcje kontrolera odpowiedzialną za obsługę zmiany locale

W powyższym przykładzie lokalizację zapisujemy w cookie. Najprostszy przełącznik lokalizacji wyglądałby w następujący sposób: 

Moglibyśmy się pokusić o przekazanie aktualnego adresu do naszej akcji kontrolera: 

Wtedy nasza akcja kontrolera mogłaby nas przenosić do tego adresu, zamiast do strony.

Nasze locale możemy przechowywać też w bazie danych, osobno dla każdego użytkownika. Stosowne przerobienie kontrolera nie powinno stanowić większego problemu.

Liczba mnoga 

Niebanalnym problemem jest poprawne dobranie formy tłumaczenia w zależności od mnogości argumentu. Dobrym przypadkiem może być dla nas język polski. Wyobraźmy sobie listę wpisów, na której może być zero, jeden, dwa lub pięć elementów. Wiadomość o liczebności w każdym przypadku będzie inna. Tak więc mamy: 

> Nie znaleziono wyników

> Znaleziono 1 wynik 

> Znaleziono 2 wyniki 

> Znaleziono 5 wyników

W różnych językach panują różne zasady, na szczęście w Symfony jest dostępny mechanizm transchoice.

Aby przetłumaczyć taki komunikat wystarczy w szablonie użyć prostego: 

Dodatkowo musimy dodać stosowne tłumaczenia w którym dokładnie określimy zakresy wartości dla jakich ma być wybierana stosowna wartość: 

To bardzo proste rozwiązanie prawie rozwiązuje problem liczby mnogiej. Wnikliwy czytelnik jednak zauważy że takie rozwiązanie nie zawsze zadziała poprawnie. Już w przypadku, gdy wyświetlane będą 22 wyniki wyświetlany komunikat będzie błędny: *Znaleziono 22 wyników*. 

Aby w poprawny sposób wyświetlić ten komunikat musimy do metody transchoice przekazać nie ilość rekordów, lecz liczbę, która wskaże na to, którego tłumaczenia należy użyć oraz samą ilość jako dodatkowy parametr. Nasze tłumaczenie powinno wyglądać w następujący sposób:

Zaś stosownie od ilości wyników musimy zastosować odpowiednią formę:

Aby poprawnie wybrać tłumaczenie, potrzebna będzie jeszcze odrobina logiki która się tym zajmie. Niezależnie gdzie umieścimy tę logikę, musimy pamiętać że dla różnych języków będzie ona różna. Metoda sprawdzająca, którą formę należy użyć mogłaby wyglądać następująco: 

Formatowanie dat 

W naszym projekcie przydatne może okazać się poprawne formatowanie i wyświetlanie dat. Warto zwrócić uwagę na to, że w Stanach Zjednoczonych powszechnie używa się formatu: n/j/y, w Wielkiej Brytanii d M Y, w Polsce zaś d.m.y. Stworzymy więc w naszym projekcie serwis odpowiedzialny za formatowanie dat. 

Tłumaczenie wyjątków 

Kolejnym elementem podlegającym tłumaczeniu są wyjątki. PHP, Symfony i Doctrine zapewnią nam wiele różnych wyjątków. Zazwyczaj tylko niektóre powinny trafić do użytkownika. Treść wyjątku o problemie w połączeniu z bazą danych z nazwą bazy danych, nazwą użytkownika i jego hasłem nie powinny w żadnym wypadku zostać bezmyślnie wyświetlane użytkownikowi. Przy filtrowaniu, które wyjątki będziemy wyświetlać, a które nie, proponuję zastosować zasadę białej listy.

Utwórzmy własny rodzaj wyjątku i wyświetlajmy użytkownikowi tylko te wyjątki:

Wszędzie w naszym kodzie gdzie wyrzucamy jakiś wyjątek, którego treść w przypadku błędu miałaby zostać wyświetlona użytkownikowi, będziemy stosować nasz ParametrizedException. Konstruktor naszego wyjątku został zmodyfikowany, przyjmuje on tablicę parametrów, które zostaną przekazane do tłumaczenia. Przykładowy kod kontrolera, który obsługiwałby taki wyjątek mógłby wyglądać następująco: 

Niektóre wyjątki chcielibyśmy zalogować, warto wtedy aby treści logowały się w domyślnym języku. Nasz kod obsługi wyjątku wyglądałby wtedy tak:

Zaś metoda odpowiedzialna za tłumaczenie na domyślny język: 

Tłumaczenia bazy danych 

Ostatnim zagadnieniem, które chciałbym poruszyć są tłumaczenia treści z naszej bazy danych. To jaką drogę powinniśmy obrać zależy od specyfiki naszego projektu. Jeżeli nasz projekt przypomina bardziej bloga, w którym istnieją artykuły w różnych językach, gdzie nie każdy artykuł jest tłumaczony na inne języki, sugerowałbym dodać do naszej tabeli z treściami kolumnę odpowiedzialną za język. Jeżeli trzymamy w bazie danych jakieś proste słowniki, na przykład kategorie lub tagi, można zastosować rozszerzenie do Doctrine: Doctrine2 behavioral extensions. Daje ono nam możliwość tłumaczenia kolumn encji w locie. Nie polecam tego rozwiązania, nie będzie ono najszybsze w działaniu, może jednak okazać się dla nas wystarczające. 

W przypadku niedużych tabeli przechowujących tagi lub statusy, które nie są edytowane przez użytkownika, może warto zastosować się, czy nie warto byłoby przenieść przechowywanie tłumaczonych nazw do projektu, czy to przez przechowywanie w bazie danych tylko wpisów tłumaczeń, czy też przez dodanie wszystkich wartości tych obiektów do kodu. 

Z kolei w przypadku danych które może edytować użytkownik, może być potrzebne stworzenie dodatkowej pośredniej tabeli na tłumaczenia, lub jeżeli z góry znamy ilość wszystkich języków dla których ma działać portal i jest ona niewielka, może wystarczy nam dodać kilka kolumn do naszej tabeli na odpowiednie tłumaczenia. Niekiedy treści podstron różnych lokalizacji jednego serwisu nie posiadają wzajemnie swoich odpowiedników. Czasem nakład obliczeniowy wynikający z różnych lokalizacji strony może negatywnie wpływać na nasz serwis. Warto się wtedy zastanowić, czy przypadkiem dla nowej lokalizacji, lub wersji językowej nie warto byłoby postawić odpowiednio skonfigurowaną kopie systemu.

Podsumowanie 

Pobieżnie przeglądnęliśmy kilka zagadnień powiązanych z lokalizacją, aby ostatecznie usłyszeć radę o uciecze od problemu przez duplikację systemu. Nasze projekty są różne i wymagają różnych rozwiązań. Możliwe, że czasem zastosujemy i takie rozwiązanie. Wszystko zależy od Ciebie i Twojego projektu.


W Unity Group dzielimy się wiedzą w trakcie spotkań wewnętrznych, takich jak Unity Tech Talks*, czy Coders. Jeśli chcesz do nas dołączyć jako programista PHP, zapraszamy do zapoznania się z ofertami pracy:

  1. PHP Developer (nowa platforma sklepowa Spryker)
  2. PHP Developer (rozwiązania Product Information Management)

* – prowadzimy też otwartą serię Open UTT, zawsze możesz wpaść nas poznać stacjonarnie.

Sprawdzaj nasz kalendarz wydarzeń albo śledź nas w social media!

Nasi eksperci
/ Dzielą się wiedzą

PIM in Marketplace Platform
16.04.2024

Rola systemów PIM na platformach marketplace

E-Commerce

Dobrej jakości, kompletne informacje produktowe pozwalają sprzedawcom w odpowiedni sposób zaprezentować asortyment, a klientom znaleźć dokładnie to, czego szukają. Najpopularniejsze platformy marketplace działają niemalże jak wyszukiwarki, pozwalając konsumentom na zaawansowane filtrowanie ofert za pomocą wielu słów kluczowych i różnych...

11.04.2024

Nowy model cenowy MuleSoft / Niższy próg wejścia 

Integracja systemów

MuleSoft to lider integracji aplikacji i systemów, ułatwiający firmom tworzenie złożonych rozwiązań informatycznych, czerpiącym ze wszystkich dostępnych zasobów informatycznych. Producent oferuje także reużywalne API i konfiguracje umożliwiające proste łączenie zróżnicowanych systemów. Ostatnio MuleSoft wprowadził znaczące zmiany w...

09.04.2024

Recommerce / Znaczenie handlu wtórnego na rynku detalicznym

E-Commerce

W ciągu ostatniego roku lub dwóch recommerce zyskał ogromną popularność. W związku z trudną sytuacją ekonomiczną konsumenci chętnie odsprzedają swoje rzeczy i kupują używane produkty, aby zaoszczędzić pieniądze. To z kolei wpływa na sytuację w sprzedaży bezpośredniej, dodatkowo zachęcając sprzedawców detalicznych do wejścia na rynek...

Ekspercka wiedza
dla Twojego biznesu

Jak widać, przez lata zdobyliśmy ogromną wiedzę - i uwielbiamy się nią dzielić! Porozmawiajmy o tym, jak możemy Ci pomóc.

Napisz do nas

<dialogue.opened>