In Ruby on Rails 3, non-database functionality was moved from ActiveRecord to ActiveModel. This is great since it makes it easy to use all the ActiveRecord niceness we’ve come to love for non-ActiveRecord models.
My favorite bit of functionality moved to ActiveModel is localization. (Well, validations are actually my favorite, but localization is a close second.) Rails has robust localization support though the Rails Internationalization (I18n) API. Hopefully you’re already using it, if not for creating multi-lingual sites, then at least for keeping custom attribute and model names consistent between labels, errors messages, etc.
The Rails guide does a great job of walking us through the basics of localization and using it with an ActiveRecord object, but falls short when looking at ActiveModel localization. So let’s dive in…
Let’s say we have the following namespaced model:
[ruby]
module PetShop
class Puppy
include ActiveModel::Conversion
include ActiveModel::AttributeMethods
include ActiveModel::Validations
attr_accessor :name, :price, :image_url
end
end
[/ruby]
As you can see, we’ve already included some ActiveModel modules. Conversion gives us useful methods including to_param and to_key. AttributeMethods gives us lots of accessor and attribute related goodness. And Validations gives us, well, validations.
Let's look at the attributes. The "name" and "price" attributes will humanize nicely to "Name" and "Price" but I want to refer to "image_url" as "Puppy Pic", not the default humanization "Image Url". This looks like a job for localization!
First, we need to extend ActiveModel::Naming and ActiveModel::Translation. This gives us the model and attribute localization methods that we long for. But why extend rather than include? We’re dealing with class methods here, not instance methods!
[ruby]
module PetShop
class Puppy
include ActiveModel::Conversion
include ActiveModel::AttributeMethods
include ActiveModel::Validations
extend ActiveModel::Naming
extend ActiveModel::Translation
attr_accessor :name, :price, :breed, :image_url
end
end
[/ruby]
Great! So now we’re ready to use all the human_attribute_name, model_name power we can handle, right? Next stop, our locales file (e.g. en.yml). For a non-namespaced, ActiveModel object updating the human name of "image_url" to "Puppy Pic" is simple and well documented:
[ruby]
en:
activerecord:
attributes:
puppy:
image_url: Puppy Pic
[/ruby]
But that’s not what we have. How do we handle namespaced ActiveModel objects? Poking around the human_attribute_name function source code reveals this:
[ruby]
if namespace
lookup_ancestors.each do |klass|
defaults << :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
end
defaults << :"#{self.i18n_scope}.attributes.#{namespace}.#{attribute}" end
[/ruby]
So there it is, plain as code. Not so plain? No matter, that’s what the Rails Console is for. If we take a look at our object from the console:
[powershell gutter="false"] >> PetStore::Puppy.i18n_scope
:activemodel
>> PetStore::Puppy.model_name.i18n_key
:"petstore/puppy"
[/powershell]
And here, namespace refers to the attribute namespace, not our module name. Refer to the human_attribute_name source if you don’t believe me. But trust me when I say it’s nil in our example.
So, armed with this knowledge, what should our en.yml look like?
[ruby]
en:
activemodel:
attributes:
petstore/puppy:
image_url: Puppy Pic
[/ruby]
So there it is. Now enjoy all the localization fun that Rails offers, even with puny ActiveModel classes.