Search code examples
ruby-on-railsinternationalizationrails-i18npluralize

Rails I18n pluralization without an explicit count number


Rails provides a built-in pluralization method for String, e.g., "apple".pluralize returns "apples" and "apple".pluralize(1) does "apple" (see this question). However, it works only for English. Is there any easy way to make it work for another language?

Specifically, I want to make it work in Rails 7 for Japanese (ja), in which there is no distinction between the singular and plural forms. So, basically, I want to make pluralization disabled when the (user's) locale I18n.locale is "ja". However, "林檎".pluralize(locale: :ja) returns "林檎s", which is obviously wrong.

The way that works in conjunction with the standard I18n translation framework of Rails would be the best.


Solution

  • Maybe the easiest, but non-DRY way

    In Rails, String#pluralize is (I understand) based on ActiveSupport::Inflector.pluralize. Although it only supports English, it does not change the String for the other languages (locales), which is what you want:

    I18n.backend.store_translations :en, Article: "Article"
    I18n.backend.store_translations :ja, Article: "記事"
    
    I18n.t(:Article, locale: :en).pluralize(1, :en) #=> "Article"
    I18n.t(:Article, locale: :en).pluralize(2, :en) #=> "Articles"
    I18n.t(:Article, locale: :ja).pluralize(1, :ja) #=> "記事"
    I18n.t(:Article, locale: :ja).pluralize(2, :ja) #=> "記事"
    
     # Wrong examples
    I18n.t(:Article, locale: :ja).pluralize              #=> "記事s"
    I18n.t(:Article, locale: :ja).pluralize(locale: :ja) #=> "記事s"
    

    To specify any language but English, the first argument seems mandatory (which is, IMO, both unintuitive and lengthy).

    A confusing thing is that the locale is not the keyword argument for String#pluralize, unlike ActionController::Base.helpers.pluralize, but the optional main argument! If you specify only the locale with a colon in the form of the keyword argument (as in the last line in the example above), the result will be surprising.

    For the languages that dintinguish singular and plural forms but in a different rule from English, like "un animal" and "des animaux" in French, you can specify your own inflection rule according to the Rails doc.

    This is, however, not ideal in the sense this breaks the DRY principle, in which the locale must be specified twice.

    Explicitly specifying the plural form

    You can achieve a similar thing by using Rails' count option in the I18n framework:

    I18n.backend.store_translations :en, Article: {
      one:   "Article",
      other: "Articles"
    } # not using "%{count} Articles"
    I18n.backend.store_translations :ja, Article: "記事"
      # no distinction betwwen singular and plural for ja
    
    I18n.t(:Article, locale: :en, count: 3) #=> "Articles"
    I18n.t(:Article, locale: :ja, count: 3) #=> "記事"
    I18n.t(:Article, locale: :en, count: 1) #=> "Article"
    I18n.t(:Article, locale: :ja, count: 1) #=> "記事"
    
     # Wrong example
    I18n.t(:Article, locale: :en) #=> {:one=>"Article", :other=>"Articles"}
    I18n.t(:Article, locale: :ja) #=> "記事"  # this works, though.
    

    This method does follow the DRY principle.

    However, there are a couple of major downsides. First, I feel, to be honest, it is daft to define the plural forms for all the (perhaps hundreds of?) English words for which you define translations, considering Rails knows how to pluralize those English words.

    Second, once you have specified a set of translations like these, you must always call them with the count option.

    In addition, the point of the count option in I18n.t is, arguably, you don't have to write the count number twice by including %{count} in the definition. In the example above, I delibrately exclude %{count} so that you can and do get the result without the count number as the OP asks for. However, this method kind of defeats the purpose I18n.t (I18n.translate) is designed for, as you would still have to explicitly specify where and how the number is placed like,

    sprintf "%d %s", cnt, I18n.t(:Article, locale: I18n.locale, count: cnt)
    

    This downside is particularly relevant for some languages, which place the number AFTER the main word in some contexts. For example, the American English expression of a day and month "November 7", as opposed to "7 November" (in British English), is an example. Or, the standard currency expressions vary from language to language, like "£4" (en-gb), 10.50 € (fr), 300円 (ja).

    An alternative method

    You can use ActionController::Base.helpers.pluralize like the following example, in combination with your own translations. Although this is not what the OP asks for (that is, to get a plural form without a count number), I here include this for the sake of completeness.

    I18n.backend.store_translations :en, Article: "Article"
    I18n.backend.store_translations :ja, Article: "記事"
    
    helper.pluralize(1, I18n.t(:Article, locale: :en), locale: :en) #=> "1 Article"
    helper.pluralize(2, I18n.t(:Article, locale: :en), locale: :en) #=> "2 Articles"
    helper.pluralize(1, I18n.t(:Article, locale: :ja), locale: :ja) #=> "1 記事"
    helper.pluralize(2, I18n.t(:Article, locale: :ja), locale: :ja) #=> "2 記事"
    
     # Wrong example
    helper.pluralize(2, I18n.t(:Article, locale: :ja))              #=> "2 記事s"
    

    A major difference is that the number is forcibly included in the returned String.

    The format is also different and is more lengthy, breaking the DRY principle.