ruk·si

🛤️ Ruby on Rails
Internationalization

Updated at 2015-09-05 22:02

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')

Sources