Więcej na rubyonrails.pl: Start | Pobierz | Wdrożenie | Kod (en) | Screencasty | Dokumentacja | Ekosystem | Forum | IRC

Obsługa I18n w Ruby on Rails

Interfejs programistyczny Ruby I18n (skrót od internacjonalizacja) jest dostarczany wraz z Ruby on Rails (od wersji 2.2). Zapewnia łatwy w użyciu i elastyczny framework, dzięki któremu przetłumaczysz swoją aplikację na język inny niż angielski albo przygotujesz dla niej kilka różnych wersji językowych.

Proces “internacjonalizacji” zazwyczaj oznacza wyabstrahowanie wszystkich łańcuchów znaków i innych specyficznych dla regionu elementów aplikacji. Proces “lokalizacji” oznacza dostarczenie tłumaczeń i zlokalizowanych formatów dla tych części. 1

Internacjonalizując aplikację musisz:

Lokalizując aplikację będziesz mógł przede wszystkim:

Niniejszy przewodnik wprowadzi cię w zagadnienia związane z interfejsem programistycznym I18n, między innymi dzięki samouczkowi, pokazującemu jak krok po kroku zinternacjonalizować aplikację Rails.

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>

rails i18n demo untranslated

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.

rails i18n demo translation missing

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ć:

rails i18n demo translated to English

Dopiero gdy zmienisz adres URL na http://localhost:3000?locale=pirate, czyli na taki, który będzie przenosił pirackie (pirate) ustawienia regionalne, zobaczysz:

rails i18n demo translated to pirate

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ć:

rails i18n demo localized time to pirate

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:

10 Autorzy

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.

12 Changelog

Lighthouse ticket