Read more

Dealing with I18n::InvalidPluralizationData errors

Arne Hartherz
August 30, 2017Software engineer at makandra GmbH

When localizing model attributes via I18n you may run into errors like this:

I18n::InvalidPluralizationData: translation data { ... } can not be used with :count => 1. key 'one' is missing.
Illustration money motivation

Opscomplete powered by makandra brand

Save money by migrating from AWS to our fully managed hosting in Germany.

  • Trusted by over 100 customers
  • Ready to use with Ruby, Node.js, PHP
  • Proactive management by operations experts
Read more Show archive.org snapshot

They seem to appear out of the blue and the error message is more confusing than helpful.

TL;DR A model (e.g. Post) is lacking an attribute (e.g. thread) translation.
Fix it by adding a translation for that model's attribute (attributes.post.thread). The error message reveals the (wrongly) located I18n data (from attributes.thread).

Background

When you run into this, you probably introduced a new model that has attributes like these:

class User < ApplicationRecord
  has_many :posts
end
class Post < ApplicationRecord
  belongs_to :user
  validates :user, presence: true # Default for belongs_to on Rails 5+
end

I18n has the feature of falling back one level to look up translations, if it can't find one. This allows you to define common attributes like updated_at for all models on the activerecord.attributes level (or activemodel.attributes) instead of having the same translation string in each model's attribute block:

# I18n file like en.yml
en:
  activerecord:
    attributes:
      updated_at: Last change
      
      user:
        name: Name
        role: Access level
        # updated_at not needed here, Rails will use the definition from above

Another feature of I18n is optional pluralization Show archive.org snapshot . When calling I18n.translate with a :count option, it will look for nested count instructions:

en:
  thing:
    zero: Things
    one: Thing
    other: Things

The problem

Now imagine creating a post without a user. It is invalid, and Rails will display an error message.

For generating that message, Rails will look up the post's user attribute at the attributes.post level first, and fall back to the generic attributes, thus querying attributes.user. Note that this is not the user attribute, but the user model!

Now it invokes the translation with count: 1. I18n tries to pluralize the derived key (i.e. the user model, and fails. Instead of the pluralization keys one, other etc, it finds name and role.

VoilĂ :

>> Post.new(user: nil).valid?
I18n::InvalidPluralizationData: translation data {:name=>"Name", :role=>"Access level"} can not be used with :count => 1. Key 'one' is missing.

In this example, the validating the user attribute fails and Rails tries to generate localized error messages on the Post instance. The error message generator mistakenly attempts to incorporate the user model's attribute localization data and fails.

Solutions

Option A: No magic (preferred)

Define translations for associations to avoid Rails' fallback to allegedly "generic". In the above example, you need to add the key attributes.post.user. Note that this is all about association names, so you must not use user_id.

# I18n file like en.yml
en:
  activerecord:
    attributes:
      updated_at: Last change

      user:
        name: Name
        role: Access level

      post:
        user: Author

Option B: Use I18n internals

If applicable, you may define a "one" key for each model inside attributes, like so:

# I18n file like en.yml
en:
  activerecord:
    attributes:
      updated_at: Last change

      user:
        one: Author
        name: Name
        role: Access level
      
      post:
        body: Message

While that would be possible, I am not convinced about that approach's readability.

Note that in both cases model_name.human uses the models I18n scope for resolution. You need to define your model names there.

Option C: A little magic

It might be an idea to use a Yaml reference and load all models into each model's attributes list. It will pollute your translation data with unneeded fields, though.

en:
  models: &models
    user:
      one: Author
      other: Authors
    post:
      one: Post
      other: Posts

  attributes: &attributes
    user:
      <<: *models
      name: Name
      role: Access level
    post:
      <<: *models
      body: Message
     
  activerecord:
    models: *models
    attributes: *attributes

  activemodel:
    models: *models
    attributes: *attributes

Hints for debugging

Should you still encounter the error after following the instructions above, you probably did something wrong and want to debug this further.

Place a debugger in activemodel-x.x.x/lib/active_model/errors.rb#generate_message (inside i18n you will find upper level keys only which aren't helpful anymore):

def generate_message(attribute, type = :invalid, options = {})
  # many things happen here

  I18n.translate(key, options)
rescue
  binding.pry
end 

You can then inspect the key attribute which will tell you something like:

activerecord.errors.models.organisation/membership.attributes.user.required

This means the attribute user is missing for organisation/membership and can be solved by adding these lines to your translation files:

en:
  activerecord:
    attributes:
      organisation/membership:
        user:
          one: User
          other: Users
Arne Hartherz
August 30, 2017Software engineer at makandra GmbH
Posted by Arne Hartherz to makandra dev (2017-08-30 09:49)