Adventures with Ruby

Translating ActiveRecord

View Comments

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!

Contents:

  1. Scenario
  2. Setting up
  3. Translating models
  4. Translating attributes
  5. Translating default validations
  6. Interpolation in validations
  7. Model specific messages
  8. Attribute specific messages
  9. Defaults
  10. Using error_messages_for
  11. Conclusion

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
=> #
>> 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
=> #
>> 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!

Written by Iain Hecker

September 2nd, 2008 at 12:45 am

Posted in iain.nl

  • Javix
    What is strange, when I checked it in the console like that:

    Loading development environment (Rails 2.3.4)
    >> PhysicalPerson.human_name
    PhysicalPerson.human_name
    => "Personne physique"
    >>
    everything was correct. So where is the error ?
  • Javix
    If your model is composed of 2 or more words like PhysicalPerson, CustomerService, FinalRelease, etc., when showing error message on validation rails doesn't translate it and put the default english model's human name like instead of (in my case it shoul be in French) 'Personne Physique'. Nevertheless, all the attributes are translated correctly. I don't even know where to look for. With other single-word models (Service, User, etc.) everything is OK. I'm on rails 2.3.4. Any idea ? Thank you.
  • @nasmorn: You're right. I updated the post. Thanks
  • nasmorn
    template: body:
    does not take one: and other:
    You can only give one string. English rails behaves the same way.
    I just checked the source.
  • Simon
    Using 99translations.com helped us to avoid annoying mistakes in YML.
  • @Thomas, no it wasn't included into Rails 2.3, so you'll still have to install the i18n_label plugin
  • Thomas
    Was the plugin merged in Rails 2.3.2? I removed it, and the localization didn't work anymore for the labels.
  • kookimebux
    Hello. And Bye. :)
  • Sam
    New bug: If you have in file “config/fr.yml”
    activerecord: models: attributes: article: title:
    "Le champ Titre”
    price: “Le champ Prix errors: messages: exclusion: “est déjà pris !” notanumber: “n’est pas un nombre”

    And in view: for exclusion message, you have “Le champ Titre est déja pris !”. So, you aren’t message “Le champ Prix n’est pas un nombre” but “Price n’est pas un nombre”. A bug, missing “t” function before actionview ?
  • redrat
    @iain: Thanks! Works as expected!
  • @redrat

    have a look at this plugin: http://github.com/dcrec1/activerecord_i18n_defa...

    it allows you to do:


    nl:
    activerecord:
    attributes:
    _all:
    updated_at: "Bijgewerkt op"
    created_at: "Gemaakt op"
    date: "Datum"
    user:
    date: "Verjaardag"


    (note the underscore before _all)
  • redrat
    Hi,

    just found your blog entry yesterday and it really helped me to get started with the brand new i18n stuff in Rails 2.2. I'm currently converting an application from gettext to i18n. Here's my question:

    Is there a way to specify general translations for some model attributes (like you can give some default translations for error messages)? For example, many of my models use a "date" column and it would be great if I only need to maintain one translation. Something like this would be great (but is not working):

    activerecord => {
    :attributes => {
    :date => "The date" # general translation
    },
    :user => {
    :date => "Birthdate" # model specific translation
    }
    }

    regards,
    red
  • nevermind, it was a problem in the yaml file synthax, thanks :)
  • http://pastie.org/333707

    Sorry about the 3 posts :)

    Thanks again,

    Xav
  • ooops, it stripped the code
    it was
  • Hey Iain,
    I'm still new to rails and trying to get around ti and your plugin seemed very helpful and your post very good tfor my comprehension.
    However, I did install the plugin on my app and I placed the translation of my labels in the activerecord... once I hit the form, I immediately get an error orf this kind:

    ActionView::TemplateError (syntax error on line 33, col 11: ` user:') on line #1 of app/views/users/edit.html.erb:
    1:
    2:
    3:
    4:

    as you can see, I already have an entry in my locale:
    en_US:
    profile:
    title: "Profile"
    activerecord:
    attributes:
    user:
    login: "Login"
    email: "Email"
    name: "Name"

    Any idea would be greatly appreciated... the synthax was working before i installed your plugin and now it doesn't...

    Thanks in advance :)

    Xavier
  • Andrea, jorge:

    The problem is that you didn't translate enough date and time options. You need to have them all in your locale files for date_select to work...

    Have a look at this URL for common locales: http://github.com/svenfuchs/rails-i18n/tree/mas...
  • jorge
    I have the same problem Andrea has, I keep getting ”can’t convert Symbol to String”, which makes the whole localization thing useless... still :(
  • Andrea
    I had 2 problems with my test application:
    1) if the model includes a date field (something like a birthday, rendered in the view with a multiple select) the view breaks ("can't convert Symbol to String" error on line )
    2) error template body seems to accept only one string, not "one" and "other" as in the example.
    Does anyone has a cure for those problems?
  • Hi Iain,

    It would be nice if you placed the whole LOL.yml file somewhere just to help to understand the various scopes that the keys may have, I just spent some time trying to figure out the correct scopes ;D

    Thanks for the great tutorial!
  • Bless up young warrior. This has got to be the best translation tutorial ever to be handed out. Keep it up!
  • @Andreas That breaks the ability to highlight the fields in the form.
  • One work around to the translation of columns in the database is to use Custom validations

    Ex:

    class Comment < ActiveRecord::Base

    belongs_to :post

    def before_validation
    self.author.strip!
    self.author_email.strip!
    end

    private

    #Translates the validates_presence_of into norwegian

    def validate
    errors.add_to_base("Vennligst skriv inn et brukernavn")if self.author.blank?
    errors.add_to_base("Vennligst skriv inn en epost") if self.author_email.blank?
    end
    end
  • Dmitry: Have a look at my new post: http://iain.nl/2008/09/translating-columns/
  • Can you write about how to use translated columns of database in rails? For example we have table named 'blog', and I want to translate it on several languages: fr, en, ru. How to do that?

    BLOG:
    title_en
    title_ru
    title_fr
    text_en
    text_ru
    text_fr
    created_at
    updated_at

    Now I need to use for example russian language, how to do that? In globalize it's automatically select's correct language.
  • Such a complete post about i18n, but still lacking a "but i eated it" joke.
blog comments powered by Disqus