Brakujące reguły pluralizacji w Rails
Opublikowane przez Jarosław Zabiełło
Połączenie sił wbudowanego mechanizmu I18n oraz pluginu Globalize daje całkiem duże możliwości do budowania wielojęzycznych aplikacji w Rails. Globalize służy do tłumaczenia danych trzymanych w bazie. Zaś moduł I18n do typowych prac lokalizacyjnych związanych z interfejsem aplikacji (włącznie ze wszystkimi komunikatami wyświetlanymi przez sam framework Ruby on Rails). Jedynym problemem jaki spotkałem jest brak reguł pluralizacji dla języków innych od języka angielskiego.
Jak wiadomo, jęz. angielskim istnieją dwie formy: liczba pojedyńcza i mnoga (1 book, 2 or more books). Ale już w języku polskim – trzy (1 książka, 2,3,4 książki, 5 i więcej książek). To co mnie ostatnio zdziwiło podczas prac z Rails 3 to kompletny brak wbudowanych reguł pluralizacji dla języków innych od angielskiego. Fakt, że Rails mechanizm I18n opiera na gemie i18n, i to w sumie wina tego gemu że posiada zdefiniowaną metodę pluralizacji tylko dla języka angielskiego. Jest też napisane, że można to rozszerzyć o dodatkową logikę, ale nigdzie nie podano szczegółowych informacji o tym jak to zrobić.
Po przebadaniu źródeł (i raczej bezskutecznym poszukiwaniu rozwiązania w internecie) napisalem kod który rozszerza Rails o dodatkowe reguły pluralizacji. Poniższy kod można dodać gdzieś do inicjalizatorów, np. do pliku config/initializers/pluralization.rb. Reguły pluralizacji zostały skopiowane z Gettexta.
# config/initializers/pluralization.rb
module I18n::Backend::Pluralization
# rules taken from : http://www.gnu.org/software/hello/manual/gettext/Plural-forms.html
def pluralize(locale, entry, n)
return entry unless entry.is_a?(Hash) && n
if n == 0 && entry.has_key?(:zero)
key = :zero
else
key = case locale
when :pl # Polish
n==1 ? :one : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? :few : :other
when :cs, :sk # Czech, Slovak
n==1 ? :one : (n>=2 && n<=4) ? :few : :other
when lt # Lithuanian
n%10==1 && n%100!=11 ? :one : n%10>=2 && (n%100<10 || n%100>=20) ? :few : :other
when :lv # Latvian
n%10==1 && n%100!=11 ? :one : n != 0 ? :few : :other
when :ru, :uk, :sr, :hr # Russian, Ukrainian, Serbian, Croatian
n%10==1 && n%100!=11 ? :one : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? :few : :other
when :sl # Slovenian
n%100==1 ? :one : n%100==2 ? :few : n%100==3 || n%100==4 ? :many : :other
when :ro # Romanian
n==1 ? :one : (n==0 || (n%100 > 0 && n%100 < 20)) ? :few : :other
when :gd # Gaeilge
n==1 ? :one : n==2 ? :two : :other;
# add another language if you like...
else
n==1 ? :one : :other # default :en
end
end
raise InvalidPluralizationData.new(entry, n) unless entry.has_key?(key)
entry[key]
end
end
I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
Teraz wystarczy w którymś z plików lokalizacyjnych, np. config/locale/pl.yaml dodać
pl:
:book
zero: brak książek
one: 1 książka
few: %{count} książki
other: %{count} książek
i wywołać to w szablonie
<% (0..5).each do |i| %> <%= t(:book, :count => i) %>, < % end %>
To samo można uzyskać w konsoli Rails:
tmpl = {
:zero => 'brak książek',
:one => '1 książka',
:few => '%{count} książki',
:other => '%{count} książek'
}
I18n.locale = :pl
I18n.backend.store_translations :pl, :book => tmpl
(0..5).each{|i| puts I18n.t(:book, :count => i)}
Wynik:
brak książek 1 książka 2 książki 3 książki 4 książki 5 książekNa koniec jeszcze jedna ciekawostka związana z interpolacją zmiennych. Jest to przykład z konkretnego kodu jaki ostatnio pisałem. Wpierw plik z tłumaczeniem:
# plik config/locales/controllers/search/pl.yml
pl:
controllers:
search:
paging: "wyświetl co %{step} wersetów"
Jak widać z komentarza, plik znajduje się głębiej w strukturze katalogu. Aby Rails ładował wszystkie pliki *.yml i *.rb z dowolnego podkatalogu w config/locales, należy do pliku config/application.rb dodać wpis:
# inside class Application < Rails::Application block
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
Sam szablon (używany tu jest format Haml) wygląda następująco:
%div
- html = capture do
%select#step{:name => 'step'}
- [10, 20, 50, 100, 200, 500].each do |step|
%option{:value => step}= step
%div= raw t(:'controllers.search.paging', :step => html)
Za pomocą funkcji capture cały kod HTM znajdujący się w zasięgu bloku kodu zostaje przypisany do zmiennej i następnie jako parametr do I18n. Jak wiadomo, Rails 3 wyświetlane wartości w szablonach przepuszcza przez znany z Rails 2.x helper h. Koniecznie jest zatem dodanie na początku metody raw aby Rails 3 nie modyfikował HTML’a.
UPDATE
Twórcy gemu i18n wyszli najwyraźniej z założenia, że każdy musi sobie samemu dodać reguły pluralizacji dla pozostałych języków. A zatem krok po kroku, jeszcze raz, tym razem wg zasad twórców.
Jeśli aplikacja ma obsługiwać wiele języków, najwygodniej, jest stworzyć po jednym pliku YAML na język w ramach wydzielonego podkatalogu (wsadzenie wszystkich reguł do jednego pliku nie działa, framework ogólnie nie lubi jak w pliku jest więcej niż jeden główny klucz)
config
locales
pluralization
pl.rb
ru.rb
....
Aby wszyskie pliki lokalizacyne były ładowane z dowolnego podkatalogu wewnątrz config/locales/ trzeba dodać do config/application.rb poniższy wpis:
# config/application.rb
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
Reguły lokalizacji dla przykładowych języków:
# config/locales/pluralization/pl.rb
key = lambda{|n| n==1 ? :one : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? :few : :other}
{:pl => {:i18n => {:plural => {:keys => [:one, :few, :other], :rule => key}}}}
# config/locales/pluralization/ru.rb
key = lambda{|n| n%10==1 && n%100!=11 ? :one : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? :few : :other}
{:ru => {:i18n => {:plural => {:keys => [:one, :few, :other], :rule => key}}}}
Na końcu należy załadować moduł gemu i18n który załaduje obsługę dla powyższych plików:
# config/initializers/pluralization.rb I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)



Hej, rozwiązanie, które podałeś jest już zaimplementowane w gemie I18n :) http://github.com/svenfuchs/i18n/blob/master/lib/i18n/backend/pluralization.rb
Arkadiusz: właśnie że to nie jest zaimplementowane. Reguły pluralizacji (przynajmniej tyle ile się da) powinny być wbudowane domyślnie w i18n, a nie zostawione innym do implementacji w każdym projekcie Rails. Po drugie nie jest jasne jak używać tego ich klucza :’i18n.plural.rule’. Próbowałem dodać do
config/locales/pluralization.rbcoś takiego:rule = lambda{|n| n==1 ? :one : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? :few : :other } {:pl => {:i18n => {:plural => {:rule => rule}}}}Ale próba wywołania
wyrzuca błąd ArgumentError: wrong number of arguments (2 for 1) dla pierwszej linijki pliku config/pluralize.rb
Mój config wygląda następująco: http://gist.github.com/506770 Działanie identyczne do rozwiązania zaprezentowanego przez Ciebie :). Zgadzam się, że powinno to być domyślnie wbudowane w gema i18n.
Dobra, rozgryzłem to. Można użyć wpisu w pliku YAML (tak jak podałem w komentarzu wyżej) ale aby to działało, trzeba wywołać gdzieś (np. w config/initializers/)
W każdym razie reguły pluralizacji i tak trzeba dodać samemu. Moim zdaniem powinny być dodane dostępnie. To tak jakby użytkownicy Gettext’a musieli sobie sami dodawać takie reguły pluralizacji w każdym programie zamiast mieć je już zdefiniowane. Zaraz zaktualizuję tekst.
W twoim skrypcie pluralization.rb powinno być:
raise I18n::InvalidPluralizationData.new(entry, n) unless entry.has_key?(key)
w twojej wersji dostaniesz uninitialized constant I18n::Backend::Pluralization::InvalidPluralizationData
Linia 15, “lt” powinno być symbolem (:lt), inaczej leci:
undefined local variable or method `lt’
Kompletny przykład dla Rails 3.0.3 z 11 regułami pluralizacji:
https://github.com/hipertracker/rails3-i18n-pluralization-example