Rails I18n caveats and tips
A while ago I had the pleasure of translating one of our Rails products to different languages. Rails’ internationalization (I18n) feature still is a new kid on block and at the time documentation was scarce and scattered over different places. So after spending way too many hours setting it up, but mostly replacing strings and moving their translation to the separate YAML files, I have gained a fairly thorough insight into Rails’ I18n implementation, how to use it and what to look out for. It’s the latter which I would like to share some examples of. It might save you the ages I had to spend. (Or a fraction of it; you will still have to find and replace all strings, sorry.)
Some of them may already be obvious to you, others might be new; here is what I’ll talk about:
Translating error messages
One who’s new to Rails I18n might be tempted to use I18n.translate anywhere a string needs to be localized. That’s okay in a lot of cases. The one case where it’s not however, is hard to catch by means of tests. That makes this an even more important caveat. I am talking about class definitions, those of models in particular.
In the production environment, models are only loaded once. If the class definition contains a call to I18n.translate that will be evaluated right then, when the class is loaded for the first time, using the locale that is set at that specific time. Then when the locale changes, this particular translation is not re-evaluated and will therefor still appear in the previous language. As in the development environment models are reloaded before each request, you won’t notice it until you deploy your application.
A common scenario where this might go wrong is when specifying custom errors messages for model validations. Have a look at the following example of this.
class Monkey < ActiveRecord::Base
validates_presence_of :name, :message => I18n.t(:name_required)
end
Luckily, it’s not hard to prevent this kind of errors from happening; By default Rails looks in a special place for error messages, a specific scope inside of your locale file to be more specific. This is where it looks for the missing attribute error, from top to bottom:
activerecord.errors.models.monkey.attributes.name.blank
activerecord.errors.models.monkey.blank
activerecord.errors.messages.blank
So if you need a general translation for missing attributes one of your locale files should look like this:
nl:
activerecord:
errors:
messages:
blank: "moet worden ingevuld"
And if you need a specific error message for a monkey’s name that is missing, it should contain:
nl:
activerecord:
errors:
models:
monkey:
attributes:
name:
blank: "moet worden ingevuld (bijv . Abu of Kingkong)"
With this in place you can leave out the :message option for validates_presence_of, and avoid the usage of I18n.translate.
Error messages for custom validations
One way of localizing an error message for a custom validation is to look up the translation by its full scope:
class Monkey < ActiveRecord::Base
validate require_monkey_friendly_zoo
protected
def require_monkey_friendly_zoo
errors.add(:zoo, I18n.t("activerecord.errors.models.monkey.attributes.zoo.unfriendly")) unless zoo.monkey_friendly?
end
end
This doesn’t pose the problem I discussed above, because the instance method in which I18n.t is called is evaluated at runtime. But obviously this approach can be quite tedious, and next to that you can’t automatically make use of certain placeholders such as {{attribute}} and {{model}} as you can with regular error messages. Rails provides a handy method, errors.generate_message, that you can use here instead. It will keep your code readable and give you automatic localization and all options you’d have with regular error messages.
errors.add(:zoo, errors.generate_message(:zoo, :unfriendly)) unless zoo.monkey_friendly?
Missing translations
This is probably the most common error: Forgetting to add a translation to one or more of the locale files. There are a couple of ways to prevent this.
First of all we can tell Rails to raise an exception whenever it encounters a call to t() and it can’t find the requested key in the locale file, instead of showing a string like “translation missing: en, zoos, show, monkey_friendly”. Of course you don’t want to throw around exceptions like these in production environment, but if you set this up for the test environment you will be able to catch missing translations by running your test suite (assuming your tests cover all views). You can do this by adding the following code to test_helper.rb:
module I18n
def self.just_raise(*args)
raise args.first
end
end
I18n.exception_handler = :just_raise
As most likely you won’t run all of the tests with all of the available locales, this is only going to catch missing translations for just one language. So additionally we should check whether all of the locale files contain the same translation keys. A way to do this is by writing a test that compares the structure of the translations hashes.
Put this Hash extension in a file inside the /lib folder:
class Hash
# Compares two hashes bases on only their keys.
# Recursively matches nested hashes too.
def =~(other)
(self.keys == other.keys) && all?{ |k, v| !v.is_a?(Hash) || other[k] =~ v }
end
end
Then, for just two languages the test could look like this:
test "Language files should contain the same keys" do
en = YAML.load_file(File.join(RAILS_ROOT, 'lib', 'languages', 'en.yml'))['en']
nl = YAML.load_file(File.join(RAILS_ROOT, 'lib', 'languages', 'nl.yml'))['nl']
assert en =~ nl
end
(For large locale files it can be a pain to find the actual differences, so you might want to sophisticate this a little to provide some useful feedback.)
Form labels
Model attributes’ translations can be stored in the scope activerecord.attributes.[model].[attribute], but it seems that the label helper methods don’t use it. Save the following code to a file in the /lib folder to make all label helpers automatically look up translations:
module ActionView::Helpers::FormHelper
# Addition to make the +label+ method use ActiveRecord::Base#human_attribute_name
# in order to invoke i18n.
def label_with_human_name(object_name, method, text = nil, options = {})
unless text
klass = object_name.to_s.classify.constantize rescue nil
text = klass.human_attribute_name(method.to_s) if klass
end
label_without_human_name(object_name, method, text, options)
end
alias_method_chain :label, :human_name
end
Hopefully this was helpful to you. More information on I18n can be found at the Rails Guides I18n page.

Update: Instead of
you can now simply use: