This is how ActiveRecord's I18n integration works in Rails 2.2

This article is quite old.Time flies when you're having fun. I've been writing for my blog for a long time. Stuff changes fast, especially in the Ruby world. That's why I've put this warning on old posts. The article might still be valid, though.

Translating ActiveRecord

Updated October 10th, 2008 to be up to date with Rails 2.2 RC1 release.

With Rails 2.2 releasing any day now, I want to show you how to translate ActiveRecord related stuff. It is quite easy, once you know where to keep your translations. Here is a complete guide to using all built in translation methods!

Scenario

Suppose we're building a forum. A forum has several types (e.g. admin) of users and suppose we want to make the most important users into separate models using Single Table Inheritance (sti). This gives us the most complete scenario in showing off all translations:

class User < ActiveRecord::Base
  validates_presence_of :name, :email, :encrypted_password, :salt
  validates_uniqueness_of :email, :message => :already_registered
end

class Admin < User
  validate :only_men_can_be_admin
private
  def only_men_can_be_admin
    errors.add(:gender, :chauvinistic, :default => "This is a chauvinistic error message") unless gender == 'm'
  end
end

Setting up

Make sure you're running Rails 2.2 or Rails edge (rake rails:freeze:edge)

Now let's translate all this into LOLCAT, just for fun. We need a directory to place the locale files:

mkdir app/locales

And we need to load all files as soon as the application starts. So we make an initializer:

# config/initializers/load_translations.rb
%w{yml rb}.each do |type|
  I18n.load_path += Dir.glob("#{RAILS_ROOT}/app/locales/**/*.#{type}")
end
I18n.default_locale = 'LOL'

This approach is recommended, because loading files is not something you want to do during the request, when it should already be available. Setting you're locale like this is probably not recommended, but it's easy, if you're just using one language.

Translating models

Next, we're going to make some simple translation files. All ActiveRecord translations need to be in the activerecord scope. So when starting your locale file, it starts with the locale name, followed by the scope.

LOL:
  activerecord:
    models:
      user: kitteh
      admin: Ceiling cat

Let's try this out in script/console

User.human_name
# => "kitteh"
Admin.human_name
# => "Ceiling cat"

It's nice to know that the method human_name is used by error messages in validations too. But we'll come to that in just a second.

If you didn't specify the translation of admin, it would have used the translation of user, because it inherited it.

Translating attributes

We could append to the same file, but I choose to make a new file, because it keeps this post clean and it's a bit easier to see how the scoping works.

LOL:
  activerecord:
    attributes:
      user:
        name: naem
        email: emale

And let's try this again:

User.human_attribute_name("name")
# => "naem"
Admin.human_attribute_name("email")
# => "emale"

Once again, you can see that single table inheritance helps us with this.

Both human_name and human_attribute cannot really fail, because if no translation has been specified, it would return the normal humanized version. So if you're making an English site, you don't really need to translate models and attributes.

Translating default validations

Let's translate a few default messages:

LOL:
  activerecord:
    errors:
      messages:
        blank: "can not has nottin"
u = User.new
# => #<User id: nil, etc>
u.valid?
# => false
u.errors.on(:name)
# => "can not has nottin"

Interpolation in validations

You have more freedom in your validation messages now. With every message you can interpolate the translated name of the model, the attribute and the value. The variable 'count' is also available where applicable (e.g. validates_length_of)

LOL:
  activerecord:
    errors:
      messages:
        already_registered: "u already is {{model}}"
u.errors.on(:email)
# => "u already is kitteh"

Remember to put quotes around the translation key in yaml, because it'll fail without it, when using the interpolation brackets.

Model specific messages

A message specified in the activerecord.errors.models scope overrides the translation of this kind of message for the entire model.

LOL:
  activerecord:
    errors:
      messages:
        blank: "can not has nottin"
      models:
        admin:
          blank: "want!"
u.errors.on(:name)
# => "can has nottin"
a = Admin.new
# => #<Admin id: nil, etc>
a.valid?
# => false
a.errors.on(:salt)
# => "want!"

Attribute specific messages

Any translation in the activerecord.errors.models.model_name.attributes scope overrides model specific attribute- and default messages.

LOL:
  activerecord:
    errors:
      models:
        admin:
          blank: "want!"
          attributes:
            salt:
              blank: "is needed for cheezburger"
a.errors.on(:name)
# => "want!"
a.errors.on(:salt)
# => "is needed for cheezburger"

Defaults

When you specify a symbol as the default option, it will be translated like a normal error message, just like you've seen with :already_registered. When default hasn't been found, it'll try looking up the normal key you have given. With :already_registered, that key has already been set by Rails, because we're using validates_uniqueness_of.

When you specify a string as default value, it'll use this when no translations have otherwise been found.

a.gender = 'f'
# => "f"
a.valid?
# => false
a.errors.on(:gender)
# => "This is a chauvinistic error message"

Using error_messages_for

When you want to display the error messages in a model in a view, most people will user error_messages_for. These messages are also translated. The message has a header and a single line, saying how many errors there are. Here are the default English translations of these messages. I will leave it up to you to translate it to LOLCAT. Win a lifetime supply of cheezburgerz* with this mini-competition ;)

en-US:
  activerecord:
    errors:
      template:
        header:
          one: "1 error prohibited this {{model}} from being saved"
          other: "{{count}} errors prohibited this {{model}} from being saved"
        body: "There were problems with the following fields:"

There is one slight problem with the messages it displays. error_messages_for uses the errors.full_messages in it's list. This means that the attribute names will be put before it. Of course these will be translated with human_attribute_name, but it might not always be desirable. In other languages than English it's sometimes hard to formulate a nice error message with the attribute name at the beginning. This will have to be fixed in later Rails versions.

Conclusion

I hope you'll agree with me that these translation options for ActiveRecord are really nice! This is what we have been waiting for. Too bad I was a bit too late with my adjustments, so form labels don't translate by default. I did build it, but Rails was already feature frozen by then. I will probably post a plugin that adds this functionality. Same goes for a i18n version of scaffold.

Please keep coming back to my site, or add the RSS feed to your favorite reader.

Of course, stay in touch with the i18n mailinglist. A lot of people are putting a lot of effort into the project. New plugins and gems solving problems problems rapidly. I18n is one of the more difficult things to do, so if you have a special insight in a language, please contribute!

Happy devving!

PS. Damn! I wish I was in Berlin right now!

* invisible cheezburgerz only

Update

This post is old. Still, after 2 years, it still accounts for 10% of my daily traffic. How cool is that? Seriously though, read the official Rails guide on this subject, for up to date information!

comments powered byDisqus