Brakujące reguły pluralizacji w Rails

Opublikowane przez Jarosław Zabiełło Mon, 02 Aug 2010 21:39:00 GMT

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ążek
Na 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)

Tagi , , , ,  | 9 comments

Comments

  1. Avatar Arkadiusz Holko powiedział about 12 hours later:

    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

  2. Avatar Jarosław Zabiełło powiedział about 16 hours later:

    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.rb coś 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

     I18n.t(:'i18n.plural.rule', :locale => :pl)
    

    wyrzuca błąd ArgumentError: wrong number of arguments (2 for 1) dla pierwszej linijki pliku config/pluralize.rb

  3. Avatar Arkadiusz Holko powiedział about 20 hours later:

    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.

  4. Avatar Jarosław Zabiełło powiedział about 20 hours later:

    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/)

    I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
    

    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.

  5. Avatar piter powiedział 24 days later:

    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

  6. Avatar Piotr Kubowicz powiedział about 1 month later:

    Linia 15, “lt” powinno być symbolem (:lt), inaczej leci:

    undefined local variable or method `lt’

  7. Avatar Jaroslaw Zabiello powiedział 3 months later:

    Kompletny przykład dla Rails 3.0.3 z 11 regułami pluralizacji:

    https://github.com/hipertracker/rails3-i18n-pluralization-example

  8. Avatar ja powiedział over 2 years later:

    Od 3 lat żadnej nowej notki, czyżby autorowi się znudziły railsy? :-)

  9. Avatar Jarosław Zabiełło powiedział over 2 years later:

    Raczej brak czasu + dodatkowe problemy techniczne z aplikacją bloga.

(leave url/email »)

   Pomoc języka formatowania Obejrzyj komentarz