94

I want to be able to translate pluralized strings in i18n in rails. A string can be :

You have 2 kids

or

You have 1 kid

I know that I can use pluralize helper method, but I want to embed this in i18n translations so that I don't have to mess up with my views at any point in the future. I read that :count is somehow used in translations for plural, but I can't find any real resources on how it gets implemented.

Notice that I know that I can pass a variable in a translation string. I also tried something like :

<%= t 'misc.kids', :kids_num => pluralize(1, 'kid') %>

Which works fine, but has a fundamental problem of the same idea. I need to specify the string 'kid' in the pluralize helper. I don't want to do that because it will lead to view problems in the future. Instead I want to keep everything in the translation and nothing in the view.

How can I do that ?

You Nguyen
  • 9,961
  • 4
  • 26
  • 52
Spyros
  • 46,820
  • 25
  • 86
  • 129
  • 2
    Note that the "interpolator" and quotes `"#{....}"` are not necessary in the code above. – Zabba May 29 '11 at 05:40
  • 1
    you have a wrong approach because you are assuming that the plurals for other languages are working like in English. See [my answer](http://stackoverflow.com/questions/6166064/i18n-pluralization/6166232#6166232) for a correct approach. – sorin May 29 '11 at 06:50
  • Sorin, thank you for your answer, i just don't want to use gettext for this one. I think Zabba's solution is great for my needs with i18n. – Spyros May 29 '11 at 06:53
  • Rails 3 handles more robustly using CLDR and 'count' interpolation variable: http://guides.rubyonrails.org/i18n.html#pluralization – Luke W May 23 '12 at 19:02
  • Years later, but you can also use a translation on the string 'kid' - so you have: `<%= t 'misc.kids', :kids_num => pluralize(1, t('kid')) %>`. Maybe this didn't work in 2011(!) but it sure does now on Rails 5.2.2 – Jarvis Johnson Apr 07 '19 at 02:42
  • No, Jarvis, this doesn't do what you think it does. – Clemens Kofler Aug 10 '20 at 14:58

8 Answers8

186

Try this:

en.yml :

en:
  misc:
    kids:
      zero: no kids
      one: 1 kid
      other: %{count} kids

In a view:

You have <%= t('misc.kids', :count => 4) %>

Updated answer for languages with multiple pluralization (tested with Rails 3.0.7):

File config/initializers/pluralization.rb:

require "i18n/backend/pluralization" 
I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)

File config/locales/plurals.rb:

{:ru => 
  { :i18n => 
    { :plural => 
      { :keys => [:one, :few, :other],
        :rule => lambda { |n| 
          if n == 1
            :one
          else
            if [2, 3, 4].include?(n % 10) && 
               ![12, 13, 14].include?(n % 100) && 
               ![22, 23, 24].include?(n % 100)

              :few 
            else
              :other 
            end
          end
        } 
      } 
    } 
  } 
}

#More rules in this file: https://github.com/svenfuchs/i18n/blob/master/test/test_data/locales/plurals.rb
#(copy the file into `config/locales`)

File config/locales/en.yml:

en:
  kids:
    zero: en_zero
    one: en_one
    other: en_other

File config/locales/ru.yml:

ru:
  kids:
    zero: ru_zero
    one: ru_one
    few: ru_few
    other: ru_other

Test:

$ rails c
>> I18n.translate :kids, :count => 1
=> "en_one"
>> I18n.translate :kids, :count => 3
=> "en_other"
>> I18n.locale = :ru
=> :ru
>> I18n.translate :kids, :count => 1
=> "ru_one"
>> I18n.translate :kids, :count => 3
=> "ru_few"  #works! yay! 
>> I18n.translate :kids, :count => 5
=> "ru_other"  #works! yay! 
Zabba
  • 64,285
  • 47
  • 179
  • 207
  • Sorry but this just doesn't work with lots of languages. Pluralization is really complex, see http://translate.sourceforge.net/wiki/l10n/pluralforms Due to this I think that my answer is more appropriate. – sorin May 29 '11 at 06:55
  • 1
    @sorin, updated my answer to use multiple pluralization rules. – Zabba May 29 '11 at 17:10
  • 6
    It's ok, but now you have a new fulltime job, to maintain the pluralization dictionary!. – sorin May 29 '11 at 17:18
  • This is great! To make `%{count}` to work I had to use quotes around the whole block ie. `one: "%{count} kid"` – firedev Sep 26 '12 at 03:41
  • Just in case anyone runs into the same problem: httpd needs to be restarted in order for these changes to take effect. Silly me. – themarketka Jan 03 '13 at 08:58
  • 1
    @ThePablick, yes, since the files in the '/initializer' directory are loaded only once - at http server startup. – Zabba Jan 03 '13 at 17:15
  • I just wonder why `pluralize()` doesn’t seem to do this, or more of, where I’m failing at... – Smar Jun 09 '16 at 12:02
  • Your pluralization rules for Russian are wrong, see the [other answer](https://stackoverflow.com/a/26979816/52499). – x-yuri Feb 08 '22 at 14:55
40

I hope Russian-speaking Ruby on Rails programmers could find this. Just want to share my own very precise Russian pluralization formula. It based on Unicode Specs. Here is contents of config/locales/plurals.rb file only, everything else should be done as same as in answer above.

{:ru => 
  { :i18n => 
    { :plural => 
      { :keys => [:zero, :one, :few, :many],
        :rule => lambda { |n| 
          if n == 0
            :zero
          elsif
            ( ( n % 10 ) == 1 ) && ( ( n % 100 != 11 ) )
            # 1, 21, 31, 41, 51, 61...
            :one
          elsif
            ( [2, 3, 4].include?(n % 10) \
            && ![12, 13, 14].include?(n % 100) )
            # 2-4, 22-24, 32-34...
            :few
          elsif ( (n % 10) == 0 || \
            ![5, 6, 7, 8, 9].include?(n % 10) || \
            ![11, 12, 13, 14].include?(n % 100) )
            # 0, 5-20, 25-30, 35-40...
            :many
          end
        } 
      } 
    } 
  } 
}

Native speakers may enjoy cases such as 111 and 121. And here the test results:

  • zero: 0 запросов/куриц/яблок
  • one: 1 запрос/курица/яблоко
  • few: 3 запроса/курицы/яблока
  • many: 5 запросов/куриц/яблок
  • one: 101 запрос/курица/яблоко
  • few: 102 запроса/курицы/яблока
  • many: 105 запросов/куриц/яблок
  • many: 111 запросов/куриц/яблок
  • many: 119 запросов/куриц/яблок
  • one: 121 запрос/курица/яблоко
  • few: 122 запроса/курицы/яблока
  • many: 125 запросов/куриц/яблок

Thanks for initial answer!

sashaegorov
  • 1,821
  • 20
  • 26
  • 1
    The other answer you referred to, placed this in a different file. So with that approach your content should go to `config/locales/plurals.rb` rather than `config/initializers/pluralization.rb` – silverdr Aug 17 '16 at 11:31
  • @silverdr I've fixed file name in answer. Thanks for the tip! – sashaegorov Aug 30 '16 at 15:59
  • I wonder what `v` means on the unicode site... Also, the last `elsif` branch can be replaced with `else`, can't it? – x-yuri Feb 08 '22 at 14:52
  • By the way, in the `i18n` gem they have [the rules](https://github.com/ruby-i18n/i18n/blob/00fc8100135878af7b5cc05aa2213844dcbe4e1b/test/test_data/locales/plurals.rb#L83), but in the test data. – x-yuri Feb 08 '22 at 15:03
11

First, remember that number of plural forms depends on language, for English there are two, for Romanian there are 3 and for Arabic there are 6 !.

If you want to be able to properly use plural forms you have to use gettext.

For Ruby and rails you should check this http://www.yotabanana.com/hiki/ruby-gettext-howto-rails.html

sorin
  • 161,544
  • 178
  • 535
  • 806
  • 4
    Sorin, this is what I was also thinking but this seems to be solved by following the CLDR format (http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). Am I wrong? – Nikos D Nov 03 '11 at 13:34
  • There's also 1st, 2nd, 3rd, 4th, 11th, 12th and 13th but 21st, 22nd, 23rd and so on. – gnasher729 Jul 20 '15 at 16:24
9

English

It just works out of the box

en.yml:

en:
  kid:
    zero: 'no kids'
    one: '1 kid'
    other: '%{count} kids'

Usage (you can skip I18n in a view file, of course):

> I18n.t :kid, count: 1
 => "1 kid"

> I18n.t :kid, count: 3
 => "3 kids"

Russian (and other languages with multiple plural forms)

Install rails-18n gem and add translations to your .yml files as in the example:

ru.yml:

ru:
  kid:
    zero: 'нет детей'
    one: '%{count} ребенок'
    few: '%{count} ребенка'
    many: '%{count} детей'
    other: 'дети'

Usage:

> I18n.t :kid, count: 0
 => "нет детей"

> I18n.t :kid, count: 1
 => "1 ребенок"

> I18n.t :kid, count: 3
 => "3 ребенка"

> I18n.t :kid, count: 5
 => "5 детей"

> I18n.t :kid, count: 21
 => "21 ребенок"

> I18n.t :kid, count: 114
 => "114 детей"

> I18n.t :kid, count: ''
 => "дети"
installero
  • 9,096
  • 3
  • 39
  • 43
8

Rails 3 handles this robustly with CLDR consideration and count interpolation variable. See http://guides.rubyonrails.org/i18n.html#pluralization

# in view
t('actors', :count => @movie.actors.size)

# locales file, i.e. config/locales/en.yml
en:
  actors:
    one: Actor
    other: Actors
Luke W
  • 8,276
  • 5
  • 44
  • 36
5

There is actually an alternative to the cumbersome i18n approach. The solution is called Tr8n.

Your above code would simply be:

 <%= tr("You have {num || kid}", num: 1) %>

That's it. No need to extract your keys from your code and maintain them in resource bundles, no need to implement pluralization rules for each language. Tr8n comes with numeric context rules for all language. It also comes with gender rules, list rules and language cases.

The full definition of the above translation key would actually look like this:

 <%= tr("You have {num:number || one: kid, other: kids}", num: 1) %>

But since we want to save space and time, num is automatically mapped to numeric rules and there is no need to provide all options for the rule values. Tr8n comes with pluralizers and inflectors that will do the work for you on the fly.

The translation for your key in Russian, would simply be:

 "У вас есть {num || ребенок, ребенка, детей}"

By the way, your translation would be inaccurate in languages that have gender specific rules. For example, in Hebrew, you would actually have to specify at least 2 translations for your example, as "You" would be different based on the gender of the viewing user. Tr8n handles it very well. Here is a transliteration of Hebrew translations:

 "Yesh leha yeled ahad" with {context: {viewing_user: male, num: one}}
 "Yesh leha {num} yeladim" with {context: {viewing_user: male, num: other}}
 "Yesh lah yeled ahad" with {context: {viewing_user: female, num: one}}
 "Yesh lah {num} yeladim" with {context: {viewing_user: female, num: other}}

So your single English key, in this case, needs 4 translations. All translations are done in context - you don't have to break the sentence. Tr8n has a mechanism to map one key to multiple translations based on the language and context - all done on the fly.

One last thing. What if you had to make the count part bold? It would simply be:

<%= tr("You have [bold: {num || kid}]", num: 1, bold: "<strong>{$0}</strong>") %>

Just in case you want to redefine your "bold" later - it would be very easy - you won't have to go through all your YAML files and change them - you just do it in one place.

To learn more, please take a look here:

https://github.com/tr8n/tr8n_rails_clientsdk

Disclosure: I am the developer and the maintainer of Tr8n framework and all its libraries.

0

About Redmine. If you copy pluralization file rules in config/locales/ as plurals.rb and other not same as locale name (ru.rb, pl.rb .. etc) these not work. You must rename file rules to 'locale'.rb or change method in file /lib/redmine/i18n.rb

def init_translations(locale)
  locale = locale.to_s
  paths = ::I18n.load_path.select {|path| File.basename(path, '.*') == locale}
  load_translations(paths)
  translations[locale] ||= {}
end

and if you have older redmine, add

module Implementation
        include ::I18n::Backend::Base
        **include ::I18n::Backend::Pluralization**
0

I've found a very good resource with description of locales http://translate.sourceforge.net/wiki/l10n/pluralforms ( alt link ).

For example, for ukrainian, russian, belarusian and few other languages the expression will look like this plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);

cuddlemeister
  • 1,586
  • 12
  • 15