Internationalization is the process of designing and developing web applications that can support multiple languages and locales. This is an important consideration for any web application that needs to reach a global audience, as it allows users to interact with the application in their preferred language and format.
Ruby on Rails provides built-in support for I18n, making it easy to create and manage translations for different languages. The humanization helpers of ActiveRecord and ActiveModel I18n, and you can leverage this to make you application support multiple languages, and to also humanize your object names and attributes to generate human readable content and forms.
Take the following two example screens from www.cboflow.com, a Rails application that I maintain. The first is in English, and the second is in Spanish. Using the strategies outlined in this article, I was able to make this application support multiple languages with minimal effort.
I18n and Rails
A new Rails application is configured to use I18n by default.
All of the translations are stored in resource files within the project's config/locales
directory. Each file has to end with a language code, and .yml
. So for example, you might have a file named config/locales/en.yml
, with the following content:
en:
hello: "Hello, world!"
In this folder, you can have as many subfolder as you like, and prefix the files as you wish. Some people like to have one large locale file for each supported language, while others like to have one file per object or view. For example, keeping all content in config/locales/en.yml
, or breaking the content up into several files like config/locales/author.yml
and config/locales/post.yml
The Rails Guide has a lot of information about using this feature.
How I use I18n in my own Rails applications
I try to use the same lingo across my applications.
I will create one locale file for each object that I intent to humanize in a view.
So if I have a Person model, I'll have a person.en.yml
file, and more files for other languages like person.es.yml
.
The default humanization for an attribute name is to the same as String#humanize
, so Person#first_name
will become First name
by default, and if I am happy with that I probably will not make a translation entry.
I also tend to use gems like simple_form to generate my form HTML, and this saves me from having to maintain a lot of view code to outputting translated content onto forms. Also simple_form has it's own i18n convention that compliments the Rails default pretty well.
# person.en.yml
en:
activerecord:
models:
person:
one: Person
other: People
attributes:
person/hispanic:
true: 'Yes'
false: 'No'
errors:
models:
person:
attributes:
birth_date:
must_be_in_the_past: "can't be before today"
must_be_formatted_correctly: "must be formatted like yyyy-mm-dd"
full_name:
blank: "can't be blank"
email:
invalid: "is not valid. It should look like name@domain.com"
phone_number:
invalid: "is not valid. It should look like 123-456-7890"
# person.es.yml
es:
activerecord:
models:
person:
one: "Persona"
other: "Personas"
attributes:
person:
birth_date: "Fecha de nacimiento"
email: "Correo electrónico"
full_name: "Nombre completo"
hispanic: "Hispano"
personal_information: "Información personal"
phone_number: "Número de teléfono"
self_described_gender: "Género autodescrito"
self_described_race: "Raza autodescrita"
person/hispanic:
true: "Sí"
false: "No"
errors:
models:
person:
attributes:
birth_date:
must_be_in_the_past: "no puede ser antes de hoy"
must_be_formatted_correctly: "debe tener el formato yyyy-mm-dd"
full_name:
blank: "no puede estar en blanco"
email:
invalid: "no es válido. Debe parecerse a nombre@dominio.com"
phone_number:
invalid: "no es válido. Debe parecerse a 123-456-7890"
Gems like simple_form will label your forms with the correct languages, but you will have to call the model humanization helpers when outputting other types of content. You can also clean this up with your pwn helpers or presentation objets.
You manually pull translations like this:
Person.human_attribute_name(:first_name) # -> First name
Person.model_name.human # -> Person
Person.model_name.human(count: 2) # -> People
Person.human_attribute_name("hispanic.#{person.hispanic}") %> # -> Yes
Humanizing Context and Service Objects with I18n.
Sometimes it will make sense to humanize one object's names and attributes with a totally new name in different parts of an app. For example, maybe it makes sense on one screen to refer to Person ans Profile, or Household Member.
Sometimes it also makes sense to wrap a sequence of steps into a service object that someone can initiate by filling out a form and clicking a button.
I'll use something like what's below to accomplish that.
Now you can do things like Clients::ChangeProfile.model_name.human
or Clients::ChangeProfile.human_attribute_name(:first_name)
.
module Clients
class ChangeProfile < BaseCommand
attribute :full_name, :string
attribute :birth_date, :date
attribute :email, :string
attribute :phone_number, :string
attribute :hispanic, :boolean, default: false
attribute :race_list, default: []
attribute :self_described_race, :string
attribute :gender_list, default: []
attribute :self_described_gender, :string
attribute :user
validates :user, presence: true
def run
context = {}
validate!
context[:person] = person
# While the implementation is not shown in this code sample, the attributes
# for this ActiveModel based Command/Service Object are written across
# several models within a transaction
save!
result(true, **context)
rescue ActiveRecord::RecordInvalid, ActiveModel::ValidationError
rollup_errors(person) if person
result(false, **context)
end
# overriding model name here, so this command can be bound to a form
# with ActionView and humanized with full I18n support
def self.model_name
ActiveModel::Name.new(self, nil, 'Profile')
end
# other implementation details and methods
# ...
# ...
# ...
end
end
en:
activerecord:
models:
person:
one: Person
other: People
clients/profile:
one: Profile
other: Profiles
household_member:
one: Household Member
other: Household Members
attributes:
person/hispanic:
true: 'Yes'
false: 'No'
errors:
models:
person:
attributes: &person_error_attributes
birth_date:
must_be_in_the_past: "can't be before today"
must_be_formatted_correctly: "must be formatted like yyyy-mm-dd"
full_name:
blank: "can't be blank"
email:
invalid: "is not valid. It should look like name@domain.com"
phone_number:
invalid: "is not valid. It should look like 123-456-7890"
household_member:
attributes:
<<: *person_error_attributes
Navigational links and contextual information
I may place all of my navigational and contextual information into a locale file like what's below.
This enables me to do things like this:
<%= link_to t(:add_action, subject: Person.model_name.human), new_manage_client_care_distribution_path, class: 'btn btn-info' %>
en:
add: Add
add_action: "Add %{subject}"
all: All
back: Back
basic_information: Basic Information
client_profile: Client Profile
confirm: Are you sure?
empty_search: "No %{subject} found"
delete: Delete
edit: Edit
hello: "Hello world"
history_action: "%{subject} History"
insights: Insights
invite_action: "Invite %{subject}"
loading: Loading...
provide_action: "Provide %{subject}"
missing_object: "No %{subject}"
new: New
new_action: "New %{subject}"
register_action: "Register %{subject}"
summary: Summary
view: View
view_action: "View %{subject}"
Conclusion
I18n is powerful and flexible, and really makes it easy to humanize your applications. There are other complexities that I have not covered here, like right-to-left languages, supporting many languages at once, and long form content, but I hope this article has given you a good starting point.
Top comments (0)