🛤️ Ruby on Rails - Internationalization
Related to Ruby Guide and Ruby on Rails Guide.
You set locale in ApplicationController
. You should save the locale and time zone user selects to user or/and cookie. And use some effort to automatically determine e.g. the time zone.
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
around_action :with_locale
around_action :with_time_zone
private
def with_locale
I18n.with_locale(params[:lc] || lc_from_user || lc_from_cookie || 'en') { yield }
end
def lc_from_user
# ...
end
def lc_from_cookie
# ...
end
def with_time_zone
Time.use_zone(params[:tz] || tz_from_user || tz_from_cookie || 'UTC') { yield }
end
def tz_from_user
# ...
end
def tz_from_cookie
# ...
end
end
View lookups will first check if localized version exists. It's rare that you need a totally different layout for a locale but handy when the need comes.
app/views/articles/show.html.erb # used by default
# but if I18n.locale = :de
app/views/articles/show.de.html.erb # is tried first
# same with rescue files
public/500.de.html
Localization are not limited to languages.
# app/controllers/application_controller.rb
before_action :set_expert_locale
def set_expert_locale
I18n.locale = :expert if current_user.expert?
end
# tries to load e.g. app/views/articles/show.expert.html.erb
Use the short form of the I18n
methods. I18n.t
instead of I18n.translate
and I18n.l
instead of I18n.localize
. Leave out the I18n
if possible in the given scope.
I18n.t("store.title") # translation, strings
I18n.l(Time.current) # localization, time and date objects
# usually works:
t "store.title"
l Time.current
You can pass variables to translations. It's a good practice to add variables in translations as some languages might have different semantics e.g. in greeting person's name comes first.
# config/locales/en.yml
en:
greet_username: "%{message}, %{user}!"
# config/locales/pirate.yml
pirate:
greet_username: "%{user}, yarrr, %{message}!"
# app/views/home/index.html.erb
<%= t 'greet_username', user: "Bill", message: "Goodbye" %>
Localize all visible strings. No strings or other locale specific settings should be used in the views, models and controllers. These texts should be moved to the locale files in the config/locales
directory.
Use the activerecord
translation scope for model and attribute translations. Helper methods automatically use these.
en:
activerecord:
errors: # custom validation errors, used with `message: :format_regex`
models:
user:
attributes:
email:
format_regex: has illegal format
models:
user:
zero: No members found
one: Member
other: Members
attributes:
user:
name: Full Name
email: Email
# User.model_name.human => "Member"
# User.model_name.human count: User.count => "Member" if 1
# User.model_name.human count: User.count => "Members" if 2+
# User.model_name.human count: User.count => "No members found" if 0
# User.human_attribute_name(:name) => "Full Name"
# and view helpers are able to find the translations:
<%= form_for(@user) do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<% end %>
Complex translations can be .rb
files. You need to edit that .rb
files are scanned for translations like in the next example.
{
en: {
time: {
formats: {
full: lambda { |time, _| "%H:%M, %A, #{time.day.ordinalize} %B %Y" }
}
}
}
}
# usage:
<%= l Time.current, format: :full %>
Separate translations to directories in config/locales
directory. I personally like to separate them by context e.g. controller. Remember to update application.rb
to crawl subdirectories.
# config/application.rb
locales_path = Rails.root.join("config", "locales", "**", "*.{rb,yml}")
config.i18n.load_path += Dir[locales_path]
locales/
datetime/
en.datetime.yml
en.datetime.rb
errors/
en.errors.yml # common form errors etc.
static_pages/
en.static_pages.views.yml
users/
en.users.models.yml # activerecord scope
en.users.views.yml # views as separate scopes, see next example
en.global.yml
# users/en.users.views.yml
en:
users:
index:
link_to: Members # => for links to this page
back_link_to: Back to Members # => for "back" style links to this page
heading: Members # => should match link_to most of the time
new:
link_to: New Member
heading: Create New Member
show:
action_link_to: Show # => action style link that has context
heading: Member Details
Use lazy lookup for the texts used in views. Helps to keep your translation and view file structures in sync. You can see more examples in the previous example.
en:
users:
show:
link_to: Show
heading: Member Details
# and in views
<%= t ".heading" %> # if in app/views/users/show.html.erb
<%= t "users.show.link_to" %> # full if elsewhere
Prefer the dot-separated keys. Not specifying the :scope
option. The dot-separated call is easier to read, write and trace the hierarchy.
# bad
I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages]
# good
I18n.t "activerecord.errors.messages.record_invalid"
Avoid HTML in translations. Sometimes they might be convenient though. Keys with a _html
suffix and keys named html
are marked as HTML safe, and not sanitized, but potentially contained variables will be sanitized.
# config/locales/en.yml
en:
hello_html: "<b>hello!</b>"
title:
html: "<b>title!</b>"
<%# app/views/home/index.html.erb %>
<%= t "hello_html" %> <%= t "title.html" %>
Use translations for mails. If you don't pass a subject to the mail method, ActionMailer
will try to find it in your translations.
# user_mailer.rb
class UserMailer < ActionMailer::Base
def welcome(user)
# ...
end
end
en:
user_mailer:
welcome:
subject: "Welcome to Rails Guides!"
# user_mailer.rb
class UserMailer < ActionMailer::Base
def welcome(user)
mail(to: user.email, subject: default_i18n_subject(user: user.name))
end
end
en:
user_mailer:
welcome:
subject: "%{user}, welcome to Rails Guides!"
Translate and use the default form errors. Check the full is at error translations snippet.
Learn different variants of Time
. You should be using Time.zone
variants e.g. Time.current
when displaying time to the users, and have all database date times in UTC, as Rails does automatically. Separating user time and system time is a good practice even if your app doesn't support multiple time zones to prevent time zone related bugs in different server environments.
# bad
Time.now # gives date time in system timezone
Time.parse # time to be parsed is in system time zone and returned such
# good
Time.zone.now # gives date time in configured timezone, set with Time.use_zone
Time.current # alias for Time.zone.now, use this
Time.zone.parse # time to be parsed is in configured time zone and returned such
2.hours.ago # result will have configured time zone
time_ago_in_words(2.hours.ago) # => "about 2 hours"
distance_of_time_in_words(2.hours.ago, 2.hours.from_now) # => "about 4 hours"
rake time:zones:all # lists all valid time zones to use in Time.use_zone
# usage:
l Time.zone.parse('2014-04-25 11:30:00')