Framework I18n zawiera wszystko, co niezbędne do przetłumaczenia aplikacji Railsowej. Ale oczywiście możesz użyć każdego innego pluginu lub rozszerzenia, aby pozyskać dodatkowe funkcjonalności lub elementy. Aby dowiedzieć się więcej, sprawdź hasło I18n na Wikipedii.
1 Jak działa gem I18n?
Internacjonalizacja to złożony problem. Języki naturalne niezwykle się od siebie różnią (np. mają różne reguły dotyczące tworzenia liczby mnogiej), bardzo trudno stworzyć narzędzie, które rozwiązałoby jednocześnie wszystkie problemy związane z lokalizacją. Z tego powodu interfejs programistyczny I18n skupia się na:
- udostępnieniu już na wstępie wsparcia dla angielskiego i języków do niego podobnych,
- ułatwieniu dostosowania i rozbudowywania aplikacji dla innych języków.
Wszystkie statyczne łańcuchy znaków w frameworku Rails – np. komunikaty walidacji modułu Active Record, formaty daty i czasu - zostały zinternacjonalizowane, więc tłumaczenie aplikacji Railsowej polega na nadpisaniu ich.
1.1 Ogólna struktura biblioteki
Interfejs programistyczny I18n dzieli się na dwie części:
- publiczny interfejs programowania aplikacji frameworku I18n –- moduł ruby z publicznymi metodami i definicjami, określającymi sposób działania bliblioteki
- domyślny stan końcowy procesu (backend_), który jest celowo nazywany prostym stanem końcowym (_Simple backend),implementujący powyższe metody
Jako użytkownik będziesz mieć dostęp tylko do publicznych metod modułu I18n, ale wiedza o możliwościach stanu końcowego jest przydatna.
Jest możliwym (lub wręcz pożądanym), aby zamienić dostarczony prosty stan końcowy na lepszy, który będzie mógł przechowywać przetłumaczone dane w relacyjnej bazie danych, słowniku GetText lub innym tego typu. Więcej informacji znajdziesz w części Używanie różnych stanów końcowych.
1.2 Publiczny interfejs programowania aplikacji I18n
Najważniejszymi metodami interfejsu programistycznego I18n są:
translate # przeszukaj tłumaczenia tekstów
localize # lokalizuj obiekty daty i czasu do formatów regionalnych
Posiadają one aliasy #t i #l dzięki czemu możesz użyć ich w następujący sposób:
I18n.t 'store.title'
I18n.l Time.now
Istnieją również metody czytające i przypisujące wartości atrybutom:
load_path # wskaż swoje własne pliki tłumaczeń
locale # pobierz i ustaw aktualne ustawienia regionalne
default_locale # pobierz i ustaw domyślne ustawienia regionalne
exception_handler # użyj innego exception_handler
backend # użyj innego stanu końcowego procesu
Teraz trochę praktyki: w następnych rozdziałach pokażemy, jak przetłumaczyć prostą aplikacje railsów.
2 Konfiguracja (przygotowanie) aplikacji do internacjonalizacji
Aby aplikacja zadziałała ze wsparciem interfejsu programistycznego I18n, wystarczy parę prostych czynności.
2.1 Konfiguracja modułu I18n
Zgodnie ze strategią convention over configuration, Railsy skonfigurują aplikacji rozsądne ustawienia domyślne. Jeśli jednak z jakiś względów chcesz je zmienić, możesz bardzo łatwo jest je nadpisać.
Railsy dodają automatycznie wszystkie pliki .rb and .yml z katalogu config/locales do ścieżki ładowania tłumaczeń(translations load path).
Zajrzyj do znajdującego się w tym katalogu pliku ustawień regionalnych en.yml, który zawiera przykładową parę tłumaczonych łańcuchów znaków:
en:
hello: "Hello world"
To oznacza, że w ustawieniach regionalnych :en klucz hello zostanie zamieniony na łańcuch znaków Hello world. Każdy string wewnątrz Railsów może być internacjonalizowany w ten sposób, zobacz na przykład komunikaty walidacji modułu Active Record w pliku activerecord/lib/active_record/locale/en.yml albo formaty czasu i daty w pliku activesupport/lib/active_support/locale/en.yml. Możesz użyć formatu YAML bądź też standardowych tablic asocjacyjnych Ruby, aby przechowywać tłumaczenia w domyślnym (prostym) stanie końcowym procesu.
Biblioteka modułu I18n domyślnie korzysta z angielskich ustawień regionalnych, dlatego jeśli ich nie zmienisz, przy wyszukiwaniu tłumaczeń będzie używane :en.
Biblioteka I18n podchodzi pragmatycznie do kluczy regionalnych (po małej dyskusji), dlatego zawiera tylko język, czyli na przykład :en, :pl, a nie region, jak w tradycyjnym zapisie oddzielającym “język” od “regionu” bądź “dialektu”: :en-us lub :en-uk. Wiele międzynarodowych aplikacji używa tylko elementu “języka” jako ustawień regionalnych, np., :pl, :th or :es (dla polskich, tajlandzkich, hiszpańskich). Jednak wewnątrz niektórych grup językowych zdarzają się różnice regionalne, które mogą okazać się bardzo istotne. Przykładowo, dla ustawień regionalnych :en-us jako symbol waluty powinien występować $, a dla :en-uk walutą jest £. Możesz separować region i inne ustawienia regionalne w ten sposób. Musisz tylko pamiętać, by umieścić słowniku np. :en-uk dla Wielkiej Brytanii. W implementacji pomogą ci różne "pluginy do Rails I18n ":http://rails-i18n.org/wiki, takie jak chociażby Globalize2.
Ścieżka ładowania tłumaczeń (i18n.load_path) to po prostu tablica ścieżek do twoich plików tłumaczeń. Pliki te zostaną załadowane automatycznie i będą dostępne w aplikacji. Możesz wybrać taki katalog i schemat nazywania plików tłumaczeń, jaki najbardziej ci odpowiada.
Stan końcowy procesu załaduje te tłumaczenia dynamicznie, gdy translacja będzie wyszukiwana po raz pierwszy. Jest to możliwe dzięki temu, że da się zamienić stan końcowy procesu nawet po wskazaniu ścieżki do plików tłumaczeń.
Domyślne pliki environment.rb zawierają instrukcję, tłumaczącą jak dodać ustawienia regionalne z innych katalogów i jak zmienić domyślne ustawienia. W tym celu wystarczy, że usuniesz komentarze i wyedytujesz specjalne linie.
# Masz możliwość zmiany frameworku internacjonalizacji
# Możesz ustawić inną lokalizację domyślną (standardową jest :en) albo dodać więcej ścieżek wczytywania.
# Wszystkie pliki z config/locales/*.rb,yml zostaną dodane automatycznie.
# config.i18n.load_path << Dir[File.join(RAILS_ROOT, 'my', 'locales', '*.{rb,yml}')]
# config.i18n.default_locale = :de
2.2 Opcjonalnie: niestandardowe ustawienia konfiguracji I18n
W ramach uzupełnienia warto dodać, że jeśli z jakiegoś powodu nie chcesz użyć pliku environment.rb, możesz zawsze wykonać wszystko sam.
Ścieżkę do tłumaczeń możesz ustawić w dowolnym miejscu aplikacji –- upewnij się tylko, że zostanie określona, nim rozpocznie się ich wyszukiwanie. Możliwe że będziesz chciał też zmienić domyślne ustawienia regionalne. Najprościej zrobić to wpisując ten kod:
# W config/initializers/locale.rb
# Wskaż bibliotece modułu i18n, gdzie ma szukać ścieżek do tłumaczeń
I18n.load_path << Dir[ File.join(RAILS_ROOT, 'lib', 'locale',
'*.{rb,yml}') ]
# Zmień domyślne ustawienia regionalne na inne niż :en
I18n.default_locale = :pt
2.3 Konfigurowanie i przekazywanie ustawień regionalnych
Jeżeli chcesz przetłumaczyć aplikację Railsową na język różny od domyślnego angielskiego, musisz zmienić wartość I18n.default_locale w pliku environment.rb. możesz też (tak jak w powyższym przykładzie) wykorzystać do tego initializer. Wartość ustawiona w ten sposób nie zmieni się podczas kolejnych żądań.
Inaczej postępuje się w przypadku aplikacji, które są tłumaczone na wiele języków. W nich ustawienia regionalne muszą być konfigurowane i przekazywane między żądaniami.
Być może naszła cię teraz ochota, żeby przechowywać ustawienia regionalne w sesji lub pliku cookie. Nie rób tego. Ustawienia regionalne powinny być transparentne, co więcej, powinny stanowić część adresu URL. W ten sposób nie naruszysz podstawowego przyzwyczajenia użytkowników Internetu: jeśli wysyłasz przyjacielowi adres jakiejś strony, powinien on zobaczyć dokładnie tę samą stronę, taką samą zawartość. W ten sposób twoje aplikacje będą działały kojąco (sprytnie określa to angielski termin RESTful). Więcej o tej regule znajdziesz w artykułach Stefan Tilkov’a. Oczywiście istnieją pewne wyjątki od tej zasady, ale o nich za chwilę.
Konfiguracja naprawdę jest prosta. Wystarczy w kontrolerze aplikacji (ApplicationController) określić ustawienia regionalne korzystając z filtra wstępnego (before_filter). Robi się to w ten sposób:
before_filter :set_locale
def set_locale
# jeżeli params[:locale] jest puste, zostanie użyta wartość I18n.default_locale
I18n.locale = params[:locale]
end
Sposób ten wymaga, aby ustawienia regionalne były przekazywane jako parametr w adresie URL, jak tutaj: http://example.com/books?locale=pl (taki standard przyjmuje np. Google). zatem http://localhost:3000?locale=pl załaduje ustawienia dla Polski, a http://localhost:3000?locale=de dla Niemiec, itd. Jeśli już teraz chciałbyś wypróbować jak to działa (póki co — wpisując ręcznie ustawienia regionalne do adresu URL i odświeżając stronę) możesz pominąć następną sekcję i przejść od razu do części “Zinternacjonalizuj swoją aplikację”.
Oczywiście nie musisz dopisywać ręcznie ustawień regionalnych do każdego wykorzystywanego w aplikacji adresu. Co więcej, adresy URL mogą wyglądać różnie (nie tylko standardowo jak http://example.com/pl/books, ale i http://example.com/pl/books). Dowiesz się teraz więcej o wszystkich tych możliwościach.
2.4 Konfigurowanie ustawień regionalnych na podstawie nazwy domeny
Istnieje możliwość powiązania ustawień regionalnych z nazwą domeny, w której działa aplikacja. Na przykład, chcemy aby dla www.example.com wczytywały się ustawienia angielskie (albo domyślne), a dla www.example.es hiszpańskie. Jak widać, ustawienia regionalne zależą od nazwy domeny najwyższego poziomu. Ma to kilka zalet:
- Ustawienia regionalne są oczywistą częścią adresu URL.
- Ludzie intuicyjnie wiedzą, w jakim języku zostanie wyświetlona strona.
- Implementacja tej metody w Railsach jest banalna.
- Wyszukiwarki działają lepiej, gdy zawartość w różnych językach znajduje się na różnych pośrednio łączonych domenach.
Implementacja tej metody poprzez ApplicationController jest następująca:
before_filter :set_locale
def set_locale
I18n.locale = extract_locale_from_uri
end
# Pobierz ustawienia regionalne z domeny najwyższego poziomu lub zwróć nil jeśli dany język nie jest obsługiwany
# Wpisz coś podobnego do:
# 127.0.0.1 application.com
# 127.0.0.1 application.it
# 127.0.0.1 application.pl
# W pliku /etc/hosts aby wypróbować (lokalnie) ten sposób
def extract_locale_from_tld
parsed_locale = request.host.split('.').last
I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil
end
Analogicznie konfiguruje się ustawienia regionalne, opierając się na subdomenie:
# Skonfiguruj ustawienia regionalne na podstawie żądania subdomeny (np. http://it.application.local:3000)
# Wpisz coś podobnego do:
# 127.0.0.1 gr.application.local
# W pliku /etc/hosts aby wypróbować (lokalnie) ten sposób
def extract_locale_from_subdomain
parsed_locale = request.subdomains.first
I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil
end
W aplikacjach z menu, pozwalającym na wybór języka, dobrze byłoby móc zamieścić coś podobnego do tego:
link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['REQUEST_URI']}")
zakładając przy tym, że będzie możliwość przypisania APP_CONFIG[:deutsch_website_url] pewnej wartości, np. http://www.application.de.
Choć ten sposób nie posiada żadnej wymienionych wcześniej zalet, możesz nie móc lub nie chcieć umieszczać różnych wersji językowych na różnych domenach. Dlatego też najbardziej oczywistym rozwiązaniem pozostaje załączenie ustawień regionalnych jako parametru w URL (lub ścieżce żądania).
2.5 Konfigurowanie ustawień regionalnych na podstawie parametrów adresu URL
Najbardziej standardowym sposobem na konfiguracji (i przekazywania) ustawień regionalnych jest załączanie w adresie url parametrów, tak jak to zrobiliśmy z filtrem wstępnym dla I18n.locale = params[:locale] w pierwszym przykładzie. W tym przypadku pasują nam adresy podobne do www.example.com/books?locale=ja lub www.example.com/ja/books.
To rozwiązanie ma prawie takie same zalety, jak określanie ustawień regionalnych na podstawie nazwy domeny: jest zgodne z intuicyjnym postrzeganiem adresów stron i współgra z resztą Internetu. Jednak w jego implementację trzeba włożyć nieco więcej wysiłku.
Dalej nie będzie sprawiało większych trudności pobieranie parametrów i określanie na ich podstawie ustawień regionalnych. Dopiero dołączanie ich do każdego adresu URL i w ten sposób przekazywanie między żądaniami okaże się bardziej problematyczne. Dodanie wyraźnej opcji do każdego adresu URL (np. link_to( books_url(:locale => I18n.locale))+) byłoby nudne i zapewne niemożliwe.
W ApplicationController#default_url_options, znajduje się infrastruktura służąca do “centralizowania dynamicznych decyzji dotyczących adresów URL”, która jest używana bardzo precyzyjnie: poprzez implementacje/nadpisanie tej metody ustawia się wartości domyślne dla url_for i zależne nich helpery.
W kontrolerze aplikacji możemy załączyć coś takiego:
# app/controllers/application_controller.rb
def default_url_options(options={})
logger.debug "default_url_options is passed options: #{options.inspect}\n"
{ :locale => I18n.locale }
end
Każdy helper zależny od url_for ((np. helpery dla nazwanych ścieżek takie jak root_path lub root_url, dla ścieżek zasobów jak chociażby books_path lub books_url, itd.) automatycznie będzie zawierał w łańcuchu zapytania ustawienia regionalne, tak jak tu: http://localhost:3001/?locale=ja.
To rozwiązanie może być satysfakcjonujące. Daje ono efekt czytelności adresów URL, jednak głównie wtedy, kiedy ustawienia regionu są doczepione do końca każdego adresu URL. Bo z architektonicznego punktu widzenia, ustawienia regionalne znajdują się wyżej w hierarchii niż inne części domeny i adres URL powinien to odzwierciedlać.
Najlepiej byłoby, gdyby adresy URL wyglądały podobnie do tych: www.example.com/en/books (ustawienia angielskie) i www.example.com/pl/books (ustawienia polskie). Osiągniemy to, nadpisując strategię default_url_options". Aby to zrobić, wystarczy ustawić trasy z opcją path_prefix, o tak:
# config/routes.rb
map.resources :books, :path_prefix => '/:locale'
Teraz, gdy wywołasz metodę books_path powinieneś dostać "/en/books" (przy standardowych ustawieniach regionalnych). Adres URL o postaci http://localhost:3001/pl/books powinien wczytać ustawienia dla Polski, a wywołanie books_path zwróci "/pl/books" (ponieważ zmieniły się ustawienia lokalne).
Oczywiście, musisz zwrócić szczególną uwagę na źródłowy (root) adres aplikacji (zazwyczaj jest to “homepage” lub “dashboard”). Adres http://localhost:3001/pl nie zadziała automatycznie, ponieważ deklaracja map.root :controller => "dashboard" w pliku routes.rb nie bierze pod uwagę ustawień lokalnych. I słusznie — istnieje tylko jedno źródło adresu.
Prawdopodobnie będziesz zmuszony zmapować adresy URL w ten sposób:
# config/routes.rb
map.dashboard '/:locale', :controller => "dashboard"
Przyjrzyj się dokładnie kolejności tras, nie mogą wzajemnie się wykluczać. (Powinieneś umieścić je bezpośrednio przed deklaracją map.root.)
Obecnie to rozwiązanie ma jedną (raczej) dużą wadę. Z powodu implementacji default_url_options, musisz wyraźnie przekazywać opcję :id, tak jak tutaj: link_to 'Show', book_url(:id => book)). Nie będziesz też mógł korzystać z Railsowej magii w kodzie, np. z link_to 'Show', book. Jeśli stanowi to dla Ciebie problem, przyjrzyj się dokładnie dwóm rozszerzeniom, które w tej sytuacji ułatwią pracę z trasami: routing_filter Sven’a Fuchs’a i translate_routes Raula Murciano. Zajrzyj też na stronę: Jak odkodować aktualne ustawienia regionalne z adresu URL poprzez I18n na Wikipedii.
2.6 Konfigurowanie ustawień regionalnych na podstawie informacji dostarczonych przez klienta
Tylko w wyjątkowych przypadkach, określanie ustawień regionalnych na podstawie informacji uzyskanych od klienta będzie uzasadnione. Informacje te można pozyskać chociażby sprawdzając preferowany przez użytkownika język (ustawiony dla przeglądarki), mogą też bazować na powiązaniu IP z regionem. Co więcej, użytkownik może sam wybrać odpowiednie dla niego ustawienia regionalne w aplikacji i zachować je w swoim profilu. To rozwiązanie pasuje bardziej do aplikacji i usług jedynie bazujących na sieci Web, nie do witryn — zajrzyj do części poświęconej sesjom, plikom cookie i “kojącej” architekturze.
2.6.1 Używanie Accept-Language
Jednym ze źródeł dostarczonych przez użytkownika informacji może być nagłówek http Accept-Language. Ludzie będą mogli ustawić go w ich przeglądarkach lub innych klientach (takich jak chociażby Curl).
Prosta implementacja nagłówka Accept-Language:
def set_locale
logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}"
I18n.locale = extract_locale_from_accept_language_header
logger.debug "* Locale set to '#{I18n.locale}'"
end
private
def extract_locale_from_accept_language_header
request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
end
Oczywiście w środowisku produkcyjnym będziesz potrzebował wydajniejszego kodu. Możesz użyć pluginu, np. przygotowanego przez Iain’a Hecker’a http_accept_language albo nawet zrobionego przez Ryan’a Tomayko rack middleware locale.
2.6.2 Uzywanie bazy danych GeoIP lub podobnej
Innym sposobem jest wykorzystanie bazy danych, która będzie mapowała IP użytkownika na nazwę jego regionu tak jak np. GeoIP Lite Country. Mechanizm działania będzie podobny jak w kodzie powyżej –- będzi do bazy zapytanie o IP użytkownika, a następnie wyszukać preferowane ustawienia regionalne dla wynikowego państwa/regionu/miasta.
2.6.3 Profil użytkownika
Możesz również umożliwić użytkownikom Twojej aplikacji ustawianie (i zmienianie) regionu poprzez interfejs Twojej aplikacji. Znowu mechanizm tego rozwiązania jest zbliżony do tego, który pokazaliśmy wyżej — prawdopodobnie użytkownicy Twojej aplikacji będą wybierać lokalizację z rozwijanej listy. Następnie będzie ona przechowywana w bazie danych (razem z innymi informacjami dotyczącymi ich profili). Wtedy ustalisz ustawienia regionalne zgodne z tą wartością.
3 Internacjonalizacja aplikacji
Brawo! Zainicjalizowałeś wsparcie modułu I18n w Twojej aplikacji i wskazałeś, które ustawienia regionalne powinny być w niej użyte oraz określiłeś, jak je zachować między żądaniami. Teraz wreszcie zajmiemy się czymś naprawdę interesującym.
Teraz zinternacjonalizujemy naszą aplikację — wyabstrahujemy te części, które są specyficzne dla różnych języków, a potem zlokalizujemy je, czyli przygotujemy dla nich tłumaczenia.
Najprawdopodobniej masz w swojej aplikacji coś podobnego do:
# config/routes.rb
ActionController::Routing::Routes.draw do |map|
map.root :controller => 'home', :action => 'index'
end
# app/controllers/home_controller.rb
class HomeController < ApplicationController
def index
flash[:notice] = "Hello flash!"
end
end
# app/views/home/index.html.erb
<h1>Hello world!</h1>
<p><%= flash[:notice] %></p>
3.1 Dodawanie tłumaczeń
Oczywiście te dwa łańcuchy znaków są lokalizowane dla języka angielskiego. Żeby zinternacjonalizować kod zamień te łańcuchy znaków poprzez wywołanie helpera #t z sensownym dla tłumaczenia kluczem:
# app/controllers/home_controller.rb
class HomeController < ApplicationController
def index
flash[:notice] = t(:hello_flash)
end
end
# app/views/home/index.html.erb
<h1><%=t :hello_world %></h1>
<p><%= flash[:notice] %></p>
Gdy zrenderujesz ten widok, zobaczysz komunikat błędu, mówiący o tym, że brakuje tłumaczenia dla kluczy :hello_world i :hello_flash.
Railsy dodają do widoków helpera t (od ang. translate = tłumaczenie). Wychwyci on brakujące tłumaczenia i wymieni komunikat błędu na <span class="translation_missing">.
Teraz dodamy brakujące tłumaczenia (czyli “zlokalizujemy” je):
# config/locales/en.yml
en:
hello_world: Hello World
hello_flash: Hello Flash
# config/locales/pirate.yml
pirate:
hello_world: Ahoy World
hello_flash: Ahoy Flash
Gotowe. Ponieważ nie zmieniłeś default_locale, I18n dalej będzie posługiwał się angielskim. Twoja aplikacja powinna teraz wyświetlić:
Dopiero gdy zmienisz adres URL na http://localhost:3000?locale=pirate, czyli na taki, który będzie przenosił pirackie (pirate) ustawienia regionalne, zobaczysz:
Po dodaniu nowych plików ustawień regionalnych musisz zrestartować serwer.
Możesz użyć YAML (.yml) lub plików Ruby (.rb) do przechowywania Twoich tłumaczeń. Większość programistów pracujących na Railsach preferuje YAML. Jednak ma on jedną dużą wadę. YAML jest bardzo czuły na białe spacje i znaki specjalne. Dlatego aplikacja może załadować Twoje słowniki niepoprawnie. Pliki Ruby zawieszają działanie aplikacji natychmiast po wystąpieniu błędu. Więc jeśli korzystając ze słowników YAML, napotkasz na jakieś “dziwne błędy”, spróbuj przetestować kod z tych słowników w pliku Ruby.
3.2 Dodawanie formatów daty/czasu
Poprzez dodanie do widoku datownika, zaprezentujemy lokalizowanie daty oraz czasu. Aby zlokalizować format czasu przekaż obiekt Time do I18n.l lub (lepiej) użyj helpera #l. Format wybierzesz przekazując opcję :format — domyślnie używanym formatem jest :default.
# app/views/home/index.html.erb
<h1><%=t :hello_world %></h1>
<p><%= flash[:notice] %></p
<p><%= l Time.now, :format => :short %></p>
W pirackich tłumaczeniach plików dodamy inny format czasu (obecnie jest tam zgodny z domyślnym angielskim standardem):
# config/locales/pirate.yml
pirate:
time:
formats:
short: "arrrround %H'ish"
Powinieneś teraz zobaczyć:
Aby stan końcowy procesu zadziałał tak jak chcesz, zapewne będziesz musiał dodać parę innych formatów daty/czasu. Oczywiście istnieje spora szansa, że ktoś już przed Tobą wykonał całą robotę, tłumacząc wszystkie standardowe elementy Railsów. Poszukaj w repozytorium Rails-i18n na Github archiwum zawierającego różne pliki regionalne. Gdy umieścisz plik (bądź pliki) tego typu w katalogu config/locales/, stanie się on automatycznie gotowy do odczytu.
3.3 Zlokalizowane widoki
Rails 2.3 wprowadza kolejny bardzo wygodny element: zlokalizowane widoki (szablony). Załóżmy, że masz w swojej aplikacji kontroler książek (BooksController). Indeks akcji renderuje zawartość do szablonu app/views/books/index.html.erb . Kiedy umieścisz w tym samym katalogu zlokalizowany wariant szablonu: index.es.html.erb, Railsy będą renderowały zawartość do tego nowego szablonu (oczywiście wtedy, kiedy ustawienia regionalne są określone jako :es). Dla standardowych ustawień regionalnych będzie użyty domyślny widok index.html.erb. (Przyszłe wersje Railsów być może uczynią tę magiczną lokalizację składnikiem public, itd.)
Możesz skorzystać z tej własności, np. gdy pracujesz z duża ilością statycznego tekstu, który będziesz przechowywać w YAML lub słownikach Ruby. Pamiętaj jednak, że jeśli zechcesz potem dokonać jakiś zmian w “głównym” szablonie, będziesz musiał uwzględnić je też we wszystkich zlokalizowanych szablonach.
3.4 Organizacja plików regionalnych
Jeśli używasz dostarczanego przez bibliotekę I18n standardowego prostego przechowywania (SimpleStore), wówczas słowniki są przechowywane w prostych plikach tekstowych na dysku. Wstawienie wszystkich tłumaczeń do jednego pliku może okazać się niewykonalne. Dlatego je trzymać w wielu plikach, uporządkowanych zgodnie z ustaloną przez Ciebie hierarchią.
Przykładowo, katalog config/locales może wyglądać tak:
|-defaults |---es.rb |---en.rb |-models |---book |-----es.rb |-----en.rb |-views |---defaults |-----es.rb |-----en.rb |---books |-----es.rb |-----en.rb |---users |-----es.rb |-----en.rb |---navigation |-----es.rb |-----en.rb
W ten sposób możesz separować modele i atrybuty modeli od tekstów wewnątrz widoków, a następnie te wszystkie części od “elementów standardowych” (takich jak chociażby formaty daty i czasu). Inne sposoby przechowywania dla biblioteki i18n mogą dostarczać różnych środków dla takiej separacji.
Standardowy mechanizm ładowania ustawień lokalnych nie wczytuje plików regionalnych ze słowników, które zagnieżdżone. Żeby nasz przykład zadziałał musimy wyraźnie powiedzieć Railsom, że mają szukać głębiej:
# config/environment.rb
config.i18n.load_path += Dir[File.join(RAILS_ROOT, 'config', 'locales', '**', '*.{rb,yml}')]
Listę narzędzi do zarządzania translacjami znajdziesz po hasłem Rails i18n Wiki.
4 Przegląd funkcji interfejsu programistycznego I18n
Powinieneś już wiedzieć, w jaki sposób używać biblioteki i18n oraz znać wszystkie istotne aspekty internacjonalizowania prostej aplikacji Railsowej. W następnych rozdziałach przyjrzymy się tym zagadnieniom jeszcze dokładniej
Omówimy następujące problemy:
- wyszukiwanie tłumaczeń
- wstawianie danych do tłumaczeń
- stosowanie w tłumaczeniach liczby mnogiej
- lokalizowanie dat, numerów, walut, itd.
4.1 Wyszukiwanie tłumaczeń
4.1.1 Podstawowe wyszukiwanie, zakresy i zagnieżdżone klucze
Tłumaczenia wyszukuje się po kluczach, które mogą być zarówno symbolami, jak i łańcuchami znaków. Dlatego też poniższe wywołania są równoważne:
I18n.t :message
I18n.t 'message'
Metoda translate posiada również opcję :scope. Może ona zawierać jeden lub więcej dodatkowych kluczy, które zostaną użyte do sprecyzowania “przestrzeni nazw” lub zasięgu klucza translacji:
I18n.t :invalid, :scope => [:activerecord, :errors, :messages]
Powyższy kod nakazuje wyszukanie spośród komunikatów błędów modułu Active Record komunikatu :invalid.
Ponadto, klucz i zakresy mogą być zapisane w formie oddzielanych kropkami kluczy, jak w przykładzie:
I18n.translate :"activerecord.errors.messages.invalid"
Właśnie dlatego te wywołania są równoważne:
I18n.t 'activerecord.errors.messages.invalid'
I18n.t 'errors.messages.invalid', :scope => :active_record
I18n.t :invalid, :scope => 'activerecord.errors.messages'
I18n.t :invalid, :scope => [:activerecord, :errors, :messages]
4.1.2 Ustawienia domyślne
Ustawienie opcji :default sprawi, że jej wartość będzie ona zwrócona za każdym razem, gdy brakuje tłumaczenia:
I18n.t :missing, :default => 'Not here'
# => 'Not here' // 'Nie tu'
Gdy pod opcją :default kryje się odwołanie do symbolu, zostanie on potraktowany jako klucz i przetłumaczony. Można określić jednocześnie więcej niż jedną wartość domyślną. Wtedy zostanie zwrócona pierwsza z nich, która nie jest pusta.
Dla przykładu: poniższy kod próbuje przetłumaczyć klucz :missing, a następnie klucz :also_missing. Ponieważ obydwa nie zostały określone, zostanie zwrócony łańcuch “Not here” (ang. Nie tutaj):
I18n.t :missing, :default => [:also_missing, 'Not here']
# => 'Not here' // 'Nie tutaj'
4.1.3 Szukanie masowe i przeszukiwanie przestrzeni nazw
Aby równocześnie wyszukać wiele tłumaczeń w tablicy kluczy:
I18n.t [:odd, :even], :scope => 'activerecord.errors.messages'
# => ["must be odd", "must be even"] //["musi być nieparzyste", "musi być parzyste"]
Co więcej, klucz może być przetłumaczony do (teoretycznie zagnieżdżonej) tablicy asocjacyjnej. Np., by zebrać wszystkie komunikaty błędów modułu Active Record w tablicy asocjacyjnej:
I18n.t 'activerecord.errors.messages'
# => { :inclusion => "is not included in the list", :exclusion => ... } // { :inclusion => "nie znajduje się na liście", :exclusion => ... }
4.1.4 “Leniwe” wyszukiwanie
Railsy 2.3 implementują wygodną metodę, służącą do wyszukiwania ustawień regionalnych wewnątrz widoków. Gdy masz taki słownik:
es:
books:
index:
title: "Título"
Możesz poszukać wartości books.index.title wewnątrz szablonu app/views/books/index.html.erb w następujący sposób (zwróć uwagę na kropkę):
<%= t '.title' %>
4.2 Interpolowanie danych
Często okazuje się, że do tłumaczeń trzeba wstawić wartości zmiennych. Moduł I18n pomoże Ci w tym.
Za wyjątkiem :default i :scope wszystkie opcje przekazywane do #translate będą wstawiane do tłumaczeń:
I18n.backend.store_translations :en, :thanks => 'Thanks {{name}}!'
I18n.translate :thanks, :name => 'Jeremy'
# => 'Thanks Jeremy!'
Jeśli tłumaczenie korzysta z :default lub :scope jak ze zmiennej interpolowanej, stosowany jest wyjątek I18n::ReservedInterpolationKey. Jeżeli tłumaczenie oczekuje wstawienia zmiennej, która nie została przekazana do #translate, używany jest wyjątek I18n::MissingInterpolationArgument.
4.3 Tworzenie liczby mnogiej
W angielskim bardzo łatwo zapisać dany łańcuch znaków w liczbie pojedynczej i mnogiej — “1 message” and “2 messages”. Pozostałe języki (polski, japoński, rosyjski, itd.) mogą mieć zupełnie inne gramatyki, uwzględniające więcej lub mniej form liczby mnogiej. Z tego powodu interfejs programistyczny I18n prezentuje bardzo elastyczne podejście do tego problemu.
Interpolowana zmienna :count pełni tutaj szczególną rolę. Nie tylko zostaje wstawiona do tłumaczenia, ale i na jej podstawie wybiera się odpowiednią formę liczby mnogiej (zgodną z zasadami tworzenia liczby mnogiej zdefiniowanymi przez CLDR):
I18n.backend.store_translations :en, :inbox => {
:one => '1 message',
:other => '{{count}} messages'
}
I18n.translate :inbox, :count => 2
# => '2 messages'
Algorytm tworzenia liczby mnogiej dla :en jest prosty:
entry[count == 1 ? 0 : 1]
W tym przykładzie tłumaczenie znaczone jako :one dotyczy liczby pojedynczej, zaś reszta (w tym 0) jest traktowana jako liczba mnoga.
Gdy wyszukiwanie klucza nie zwróci tablicy asocjacyjnej odpowiedniej dla tworzenia liczby mnogiej, zastosowany będzie wyjątek 18n:InvalidPluralizationData.
4.4 Określanie i przekazywanie ustawień regionalnych
Ustawienia regionalne mogą być określane pseudoglobalnie poprzez I18n.locale (w formie Thread.current, czyli np. Time.zone) Można je też w formie opcji przekazywać do #translate i #localize.
Jeśli żadne ustawienia regionalne nie zostały przekazane, używa się tych z I18n.locale:
I18n.locale = :de
I18n.t :foo
I18n.l Time.now
Przykład wyraźnego przekazywania ustawień regionalnych:
I18n.t :foo, :locale => :de
I18n.l Time.now, :locale => :de
Dla I18n.locale domyślną wartością jest ta, którą zawiera I18n.default_locale, czyli standardowo :en. Domyślne ustawienia lokalne zmienia się w ten sposób:
I18n.default_locale = :de
5 Jak przechowywać tłumaczenia?
Dostarczony prosty stan końcowy dopuszcza przechowywanie tłumaczeń zarówno w Ruby, jak i w formacie YAML. 2
Przykładowa tablica asocjacyjna Ruby, dostarczająca tłumaczeń:
{
:pt => {
:foo => {
:bar => "baz"
}
}
}
Równoważny jej plik YAML wygląda tak:
pt:
foo:
bar: baz
Jak widać w obydwu przypadkach ustawienia regionalne są kluczem nadrzędnym. :foo to klucz przestrzeni nazw, a :bar jest kluczem tłumaczenia “baz”.
A teraz bardziej “rzeczywisty” przykład, plik w formacie YAML en.ymlyml z modułu ActiveSupport:
en:
date:
formats:
default: "%Y-%m-%d"
short: "%b %d"
long: "%B %d, %Y"
Wszystkie poniższe wyszukiwania są równoważne i zwrócą datę w formacie :short, czyli "%B %d":
I18n.t 'date.formats.short'
I18n.t 'formats.short', :scope => :date
I18n.t :short, :scope => 'date.formats'
I18n.t :short, :scope => [:date, :formats]
Zazwyczaj rekomendujemy używanie formatu YAML do przechowywania translacji. Może się jednak zdarzyć, że będziesz potrzebował przechowywać lambdy Ruby jako część danych regionalnych, na przykład daty specjalnej.
5.1 Tłumaczenia dla modeli modułu Active Record
Do transparentnego wyszukiwania tłumaczeń modelu i nazw atrybutów możesz użyć metod Model.human_name i Model.human_attribute_name(attribute).
Na przykład, gdy dodasz poniższe translacje:
en:
activerecord:
models:
user: Dude
attributes:
user:
login: "Handle"
# atrybut "login" będzie tłumaczony jako "Handle"
User.human_name zwróci “Dude” i User.human_attribute_name("login") zwróci “Handle”.
5.1.1 Zakresy komunikatu błędu
Komunikaty błędów walidacji modułu Active Record tłumaczy się bardzo prosto. Moduł Active Record zawiera specjalne miejsce, w którym możesz umieścić tłumaczenia komunikatów. Dzięki temu możliwe staje się dostarczanie różnych komunikatów i tłumaczeń dla modeli, atrybutów i/lub walidacji. Pod uwagę bierze się także dziedziczenie pojedynczej tablicy.
To skuteczne narzędzie, które pozwala elastycznie dopasowywać komunikaty do potrzeb aplikacji.
Przyjrzyj się teraz modelowi User z walidacją nazwy atrybutu validates_presence_of:
class User < ActiveRecord::Base
validates_presence_of :name
end
W tym przykładzie kluczem dla komunikatu błędu jest :blank. Moduł Active Record poszuka go w przestrzeni nazw:
activerecord.errors.models.[model_name].attributes.[attribute_name]
activerecord.errors.models.[model_name]
activerecord.errors.messages
W naszym przykładzie będzie sprawdzał poniższe klucze i zwróci pierwszy rezultat:
activerecord.errors.models.user.attributes.name.blank
activerecord.errors.models.user.blank
activerecord.errors.messages.blank
W przypadku gdy modele korzystają dodatkowo z dziedziczenia wyszukiwane są komunikaty dla klas, z których dane modele dziedziczą.
Dla przykładu model Admin dziedziczy z User:
class Admin < User
validates_presence_of :name
end
Dlatego moduł Active Record będzie szukał komunikatów w tej kolejności:
activerecord.errors.models.admin.attributes.title.blank
activerecord.errors.models.admin.blank
activerecord.errors.models.user.attributes.title.blank
activerecord.errors.models.user.blank
activerecord.errors.messages.blank
W ten sposób można przygotować specjalne tłumaczenia dla wszelakich komunikatów błędów i różnych punktów łańcucha dziedziczenia modeli oraz dla atrybutów, modeli lub domyślnych zakresów.
5.1.2 Wstawianie danych do komunikatów błędów
Przetłumaczona nazwa modelu, atrybutu oraz jego wartość zawsze mogą zostać interpolowane.
Dla przykładu, zamiast standardowego komunikatu błędu "can not be blank" (ang. nie może być pusty), możesz użyć nazwy atrybutu: "Please fill in your {{attribute}}" (ang.Proszę, podaj wartość atrybutu {{attribute}}).
- count, tam, gdzie jest dostępne, może być użyte do tworzenia liczby mnogiej:
walidacja | z opcją | komunikat | interpolacja |
---|---|---|---|
validates_confirmation_of | – | :confirmation | - |
validates_acceptance_of | – | :accepted | - |
validates_presence_of | – | :blank | - |
validates_length_of | :within, :in | :too_short | count |
validates_length_of | :within, :in | :too_long | count |
validates_length_of | :is | :wrong_length | count |
validates_length_of | :minimum | :too_short | count |
validates_length_of | :maximum | :too_long | count |
validates_uniqueness_of | – | :taken | - |
validates_format_of | – | :invalid | - |
validates_inclusion_of | – | :inclusion | - |
validates_exclusion_of | – | :exclusion | - |
validates_associated | – | :invalid | - |
validates_numericality_of | – | :not_a_number | - |
validates_numericality_of | :greater_than | :greater_than | count |
validates_numericality_of | :greater_than_or_equal_to | :greater_than_or_equal_to | count |
validates_numericality_of | :equal_to | :equal_to | count |
validates_numericality_of | :less_than | :less_than | count |
validates_numericality_of | :less_than_or_equal_to | :less_than_or_equal_to | count |
validates_numericality_of | :odd | :odd | - |
validates_numericality_of | :even | :even | - |
5.1.3 Tłumaczenia dla helpera error_messages_for modułu Active Record
Również do helpera error_messages_for modułu Active Record można dołączyć tłumaczenia.
Domyślnie Railsy dostarczają następujących tłumaczeń:
en:
activerecord:
errors:
template:
header:
one: "1 error prohibited this {{model}} from being saved"
other: "{{count}} errors prohibited this {{model}} from being saved"
body: "There were problems with the following fields:"
5.2 Przegląd innych wbudowanych metod interfejsu programistycznego I18n
Railsy używają niezmiennych (fixed) łańcuchów znaków i innych lokalizacji, takich jak łańcuchy ustalające format lub inne informacje związane z formatem w kilku helperach. Teraz powiemy o nich parę słów.
5.2.1 Metody helpera modułu ActionView
- distance_of_time_in_words tłumaczy, tworzy liczbę mnogą, a także wstawia liczbę sekund, minut, godzin, itd. Zobacz więcej: datetime.distance_in_words.
- datetime_select i select_month używają przetłumaczonych nazw miesięcy do wypełnienia znacznika select. Zobacz więcej: date.month_names. Ponadto datetime_select poszukuje opcji określającej kolejność w date.order (o ile nie zostanie ona wcześniej w wyraźny sposób przekazana). Wszystkie helpery związane z wyborem daty tłumaczą aktualny czas, posługując się translacjami z zakresu datetime.prompts (oczywiście robią to, tylko jeśli te tłumacznenia są odpowiednie).
- Helpery number_to_currency, number_with_precision, number_to_percentage, number_with_delimiter i number_to_human_size korzystają z ustawień formatu dla numerów zlokalizowanego w zakresie number scope.
5.2.2 Metody modułu Active Record
- human_name i human_attribute_name tłumaczą nazwy modeli oraz nazwy atrybutów, o ile są one dostępne w zakresie activerecord.models. Wspierają też tłumaczenia dla dziedziczonych nazw klas (wyjaśniliśmy ten mechanizm już wcześniej, w części “Zakresy komunikatów błędów”).
- ActiveRecord::Errors#generate_message (korzysta z niego walidacja modułu Active Record, ale może być też wywołany ręcznie) używa human_name i human_attribute_name (zobacz wyżej). Ponadto tłumaczy komunikaty błędów i wspiera tłumaczenia dla dziedziczonych nazw klas (więcej na ten temat w części “Zakresy komunikatów błędów”).
- ActiveRecord::Errors#full_messages dopisuje nazwę atrybutu do początku komunikatu błędu, używając separatora z activerecord.errors.format.separator (domyślnie jest nim ' ').
5.2.3 Metody modułu ActiveSupport
- Array#to_sentence używa ustawień formatu, jeśli znajdzie je w zakresie support.array.
6 Dostosowywanie ustawień I18n
6.1 Używanie różnych stanów końcowych
Dostarczany prosty stan końcowy wykonuje tylko “najprostszą rzecz, która pozwoli zadziałać” w Ruby on Rails (z kilku powodów) 3 … Co oznacza, że gwarantuje jedynie zadziałanie dla angielskiego i języków bardzo do niego podobnych. Co więcej, jest on w stanie jedynie czytać tłumaczenia, a nie będzie umiał przechowywać ich dynamicznie w żadnym formacie.
Na szczęście możesz łatwo pozbyć się tych ograniczeń. Moduł I18n pozwala w szybko wymienić prosty stan końcowy na taki, który będzie lepiej dostosowany do Twoich potrzeb. Przykładowo, istnieje możliwość zamiany na stan końcowy Globalize’s Static:
I18n.backend = Globalize::Backend::Static.new
6.2 Używanie różnych procedur obsługi wyjątków:
Interfejs programistyczny I18n definiuje następujące wyjątki, które będą używane przez stany końcowe, gdy wydarzy się coś niespodziewanego:
MissingTranslationData # nie znaleziono tłumaczenia dla wskazanego klucza
InvalidLocale # ustawienia regionalne zapisane w I18n.locale są nieprawidłowe (np. nil)
InvalidPluralizationData # przekazano opcję count, ale danych nie da się przetłumaczyć w liczbie mnogiej
MissingInterpolationArgument # tłumaczenie oczekuje interpolowanego argumentu, ale nie został on dostarczony
ReservedInterpolationKey # tłumaczenie zawiera zarezerwowaną nazwę zmiennej interpolowanej (np. jedną z tych: scope, default)
UnknownFileType # stan końcowy nie wie, jak obsłużyć dołączony w I18n.load_path typ pliku
Moduł I18n wychwyci wszystkie te wyjątki, kiedy tylko pojawią się w stanie końcowym i przekaże je do metody default_exception_handler. Metoda ta jeszcze raz zastosuje wszystkie wyjątki, prócz wyjątków MissingTranslationData. Wykrycie wyjątku MissingTranslationData powoduje zwrócenie komunikatu “błędu wyjątku”, zawierającego brakujący klucz/zakres.
Działa to w ten sposób ponieważ w fazie programowania chcemy, aby widoki renderowały się, nawet jeśli brakuje tłumaczenia.
Ale w innej sytuacji możesz chcieć zmodyfikować to zachowanie. Na przykład standardowa obsługa wyjątków nie pozwala na proste wychwycenie brakujących tłumaczeń podczas automatycznych testów. W takim przypadku należy zmienić procedurę obsługi wyjątków. Nowa procedura musi być metodą modułu I18n:
module I18n
def just_raise_that_exception(*args)
raise args.first
end
end
I18n.exception_handler = :just_raise_that_exception
W ten sposób zostaną ponownie wzięte pod uwagę wszystkie wyjątki, w tym również MissingTranslationData.
Kolejną sytuacją, w której zmiana standardowego zachowania jest potrzebna wiąże się z helperem TranslationHelper, który udostępnia metodę #t (jak również #translate). Kiedy w tym kontekście pojawia się wyjątek MissingTranslationData helper wstawia komunikat do znacznika span z klasą CSS translation_missing.
Aby to zrobić (niezależnie od ustawień obsługi wyjątków) helper wymusza na I18n#translate zastosowanie wszystkich wyjątków, poprzez ustalenie opcji :raise:
I18n.t :foo, :raise => true # zawsze ponownie wywołuj wyjątki ze stanu końcowego
7 Konkluzja
Teraz masz już ogólny zarys tego, jak działa moduł I18n i jesteś gotowy, by przetłumaczyć swój projekt.
Jeśli czegoś jeszcze brakuje Ci w tym podręczniku lub jeśli zauważyłeś błędy, powiadom nas o tym poprzez nasz raportownik błędów. Jeżeli chcesz przedyskutować konkretne fragmenty albo masz jakieś pytania –- zapisz się na naszą listę mailingową..
8 Wsparcie dla interfejsu programistycznego I18n Railsów
Gem I18n został wprowadzony w wersji 2.2 Ruby on Rails i wciąż ewoluuje. Projekt zgodny jest z dobrą Railsową tradycją: najpierw rozwija się rozwiązania w pluginach i prawdziwych aplikacjach, następnie wybiera się najlepsze oraz najbardziej przydatne funkcjonalności i dopiero te włącza się do Ruby on Rails.
Dlatego też zachęcamy do eksperymentowania z nowymi pomysłami oraz elementami w pluginach i innych bibliotekach, a także do udostępniania ich całej społeczności. (Nie zapomnij pochwalić się swoimi dokonaniami na naszej liście dyskusyjnej!).
Jeśli zauważysz, że w naszym repozytorium przykładowych tłumaczeń brakuje Twojego języka, prosimy, sforkuj repozytorium, dopisz dane i wyślij je nam.
9 Źródła:
- rails-i18n.org – Strona domowa projektu rails-i18n. Wiele przydatnych informacji znajdziesz też na Wikipedii.
- rails-i18n Google group – Lista dyskusyjna projektu.
- Github: rails-i18n – CRepozytorium kodu dla projektu rails-i18n. Jest tam wiele przykładowych tłumaczeń, które mogą Ci się przydać.
- Lighthouse: rails-i18n – Raportownik błędów dla projektu rails-i18n.
- Github: i18n – Repozytorium kodu dla gemu i18n.
- Lighthouse: i18n – Raportownik błędów dla gemu i18n.
10 Autorzy
- Sven Fuchs (główny autor)
- Karel Minařík
Jeśli ten przewodnik był dla Ciebie użyteczny, zarekomenduj jego autorów na workingwithrails.
11 Przypisy
1 Lub, cytując Wikipedię: “Internacjonalizacja jest procesem konstruowania oprogramowania, które może być dostosowane do wielu języków i regionów, bez zmian projektowych. Lokalizacja jest procesem przystosowywania oprogramowania do danego regionu lub języka, poprzez dodawanie specyficznych regionalnych składników i tłumaczenie tekstów.”
2 Inne stany końcowe mogą zezwolić na użycie tych formatów lub zażądać ich, np. stan końcowy GetText pozwala na czytanie plików GetText.
3 Jednym tych z powodów jest to, że nie chcemy niepotrzebnych ładowań dla aplikacji, które nie korzystają z żadnych z udostępnianych przez moduł I18n możliwości. Dlatego staramy się, aby domyślna biblioteka modułu dla angielskiego była jak najprostsza. Kolejnym powodem jest to, że nie ma jednego rozwiązania, które automatycznie rozwiąże wszystkie problemy związane z różnymi językami. Dlatego najsensowniejszym wyjściem z sytuacji jest umożliwienie łatwej zmiany początkowej implementacji. Dzięki temu staje się też łatwiejsze eksperymentowanie z własnymi elementami i rozszerzeniami.