123456789_123456789_123456789_123456789_123456789_

DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.

Internationalization (I18n)

In this guide you will learn how to translate your application into a single non-English language or provide multi-language support.

After reading this guide, you will know:


Internationalization and Localization

Internationalization (I18n) is the process of preparing your application to support multiple languages and regional formats. In practice, this usually means abstracting strings and other locale-specific elements, such as date or currency formats, out of your application.

Localization (L10n) is the process of providing translations and locale-specific formats for those abstracted pieces.

To internationalize a Rails application, you typically need to:

To localize your application, you typically need to:

How I18n in Ruby on Rails Works

Rails ships with the Ruby i18n gem, which provides the underlying internationalization framework. In this guide, the I18n API refers to the interface your Rails application uses to work with that framework, such as I18n.t, I18n.l, and Rails translation helpers. As part of this system, static strings in Rails itself, such as Active Record validation messages and time and date formats, are already internationalized.

Rails includes core I18n functionality by default. Additional gems, such as rails-i18n, can provide extra locale data and other extensions, but they are not required for basic I18n support.

NOTE: The I18n API is primarily intended for translating user-facing text within the application. Translating model content itself (for example, blog posts stored in the database) requires a separate approach.

The Ruby I18n gem is split into two parts:

As a user you should only access the public methods on the I18n module, but it is useful to know about the capabilities of the backend.

INFO: It is possible to swap the shipped Simple backend with a more powerful one, which would store translation data in a relational database, GetText dictionary, or similar. See section Using alternate backends below.

The most important methods of the I18n API are:

The I18n module also exposes configuration through attribute readers and writers:

I18n.load_path << Rails.root.join("config", "locales", "es.yml")
I18n.locale = :es
I18n.default_locale = :en
I18n.available_locales = [:en, :es]
I18n.enforce_available_locales = true
I18n.exception_handler = MyExceptionHandler.new
I18n.backend = I18n::Backend::Simple.new

Internationalizing an Application

In this section, we'll internationalize a basic Rails application.

Creating Locale Dictionaries

Rails applications contain a config/locales directory with a en.yml locale file by default. This file is a YAML dictionary that contains translations for the English locale.

The default en.yml locale in this directory contains a sample translation:

en:
  hello: "Hello world"

Looking up the translation for hello in the :en locale returns "Hello world":

I18n.t("hello")
# => "Hello world"

The I18n library uses English as the default locale, so when a locale is not explicitly set, :en will be used for looking up translations. To learn how to change the current locale, see Managing the Locale across Requests.

NOTE: Many international applications use only the "language" element of a locale such as :cs, :th, or :es (for Czech, Thai, and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the :"en-US" locale you would have $ as a currency symbol, while in :"en-GB", you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English

Organization of Locale Files

When using the default Simple backend shipped with the i18n library, dictionaries are stored in plain-text files on disk. Putting translations for all parts of your application in one file per locale can become hard to manage, so it often helps to organize locale files in a hierarchy.

The hierarchy shown below is only an example. Choose a structure that makes sense for your application, and split files further when a single locale file becomes too large or hard to navigate.

For example, your config/locales directory could look like this:

|-defaults
|---es.yml
|---en.yml
|-models
|---book
|-----es.yml
|-----en.yml
|-views
|---defaults
|-----es.yml
|-----en.yml
|---books
|-----es.yml
|-----en.yml
|---users
|-----es.yml
|-----en.yml
|---navigation
|-----es.yml
|-----en.yml

This way, the model and model attribute names, text inside views, and global defaults such as date and time formats are all kept separate. Other stores for the i18n library could provide different means of separation.

Using Translations in Views and Controllers

Given the following example, we have a HomeController with an index action that sets a flash message and renders the app/views/home/index.html.erb template:

# config/routes.rb
Rails.application.routes.draw do
  root to: "home#index"
end
# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    flash[:notice] = "Hello Flash"
  end
end
<!-- app/views/home/index.html.erb -->
<h1>Hello World</h1>
<p><%= flash[:notice] %></p>

To internationalize this code, replace the strings with calls to Rails' #t helper using translation keys. The heading is translated in the view, while the flash message is translated where it is set in the controller:

# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    flash[:notice] = t("home_flash")
  end
end
<!-- app/views/home/index.html.erb -->
<h1><%= t("home_title") %></h1>
<p><%= flash[:notice] %></p>

Now, when this view is rendered, it will show an error message indicating that the translations for the keys "home_title" and "home_flash" are missing.

<h1>
  <span class="translation_missing" title="translation missing: en.home_title">
    Home Title
  </span>
</h1>
<p>Translation missing: en.home_flash</p>

Now add the translations to your locale files:

# config/locales/en.yml
en:
  home_title: Hello world!
  home_flash: Hello flash!
# config/locales/es.yml
es:
  home_title: ¡Hola mundo!
  home_flash: ¡Hola flash!

With the default locale, the page renders the English strings.

Hello world!
Hello flash!

If the default locale is set to the Spanish locale:

# config/application.rb
config.i18n.default_locale = :es

the response renders the Spanish strings:

¡Hola mundo!
¡Hola flash!

NOTE: You need to restart the server when you add new locale files.

Passing Variables to Translations

One key consideration for successfully internationalizing an application is to avoid making incorrect assumptions about grammar rules when abstracting localized code. Grammar rules that seem fundamental in one locale may not hold true in another.

Improper abstraction is shown in the following example, where assumptions are made about the ordering of the different parts of the translation.

<!-- app/views/products/show.html.erb -->
<%= "#{t('currency')}#{@product.price}" %>
# config/locales/nl.yml
nl:
  currency: "€ "
# config/locales/es.yml
es:
  currency: "€ "

If the product’s price is 100, Dutch might want "€ 100" while Spanish might want "100 €", but this abstraction produces "€ 100" for both locales.

To create proper abstraction, the I18n gem ships with a feature called variable interpolation that allows you to use variables in translation definitions and pass the values for these variables to the translation method.

Proper abstraction is shown in the following example:

<!-- app/views/products/show.html.erb -->
<%= t('product_price', price: @product.price) %>
# config/locales/nl.yml
nl:
  product_price: "€ %{price}"
# config/locales/es.yml
es:
  product_price: "%{price} €"

All grammatical and punctuation decisions are made in the definition itself, so the abstraction can give a proper translation.

I18n.t("product_price", price: 100, locale: :nl)
# => "€ 100"

I18n.t("product_price", price: 100, locale: :es)
# => "100 €"

INFO: Rails provides helpers such as number_to_currency for localizing numbers and currency values.

NOTE: The default and scope keywords are reserved and can't be used as variable names. If used, an I18n::ReservedInterpolationKey exception is raised. If a translation expects an interpolation variable, but this has not been passed to #translate, an I18n::MissingInterpolationArgument exception is raised.

Adding Date/Time Formats

To localize the time format, you can pass the Time object to I18n.l or, preferably, use Rails' #l helper. You can pick a format by passing the :format option.

<!-- app/views/home/index.html.erb -->
<h1><%= t("home_title") %></h1>
<p><%= flash[:notice] %></p>
<p><%= l(Time.now, format: :short) %></p>

For example, you can define a custom short time format in a locale file:

# config/locales/es.yml
es:
  time:
    formats:
      short: "%H:%M"

Then l(Time.current, format: :short, locale: :es) uses that format for the Spanish locale.

Rails also provides relative_time_in_words for a localized past/future phrase rather than a formatted timestamp:

<p><%= relative_time_in_words(3.minutes.from_now) %></p>
<p><%= relative_time_in_words(15.seconds.ago, include_seconds: true) %></p>

This renders phrases such as:

in 3 minutes
less than 20 seconds ago

By default, this looks up translations in the datetime.relative scope and combines them with the same localized distance strings used by distance_of_time_in_words.

TIP: You may need to add additional date and time formats for the I18n backend to behave as expected. However, translations for Rails’ default formats are often already available for many locales. See the rails-i18n repository for an archive of various locale files. Placing these files in config/locales/ makes them automatically available to your application.

Localized Views

In some cases, translating individual strings is not the best fit. If an entire template differs by locale, Rails can render a localized view instead.

For example, consider a BooksController with an index action that renders the app/views/books/index.html.erb template. If a localized variant of this template, index.es.html.erb, is added to the same directory, Rails will render that template when the locale is set to :es. When the locale is set to the default locale, the generic index.html.erb template will be used.

<!-- app/views/books/index.html.erb -->
<h1>Books</h1>
<!-- app/views/books/index.es.html.erb -->
<h1>Libros</h1>

You can make use of this feature when working with a large amount of static content, which would be clumsy to put inside YAML or Ruby dictionaries. Bear in mind, though, that any change you make to the template must be propagated to all localized variants.

Inflection Rules for Other Locales

Rails allows you to define inflection rules such as singularization and pluralization for locales other than English. These rules can be defined for multiple locales in config/initializers/inflections.rb. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit.

Rails already includes default English inflection rules, like the pluralization of "person" to "people":

"person".pluralize
# => "people"

You can add custom pluralization rules for Portuguese, like below:

# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:pt_br) do |inflect|
  inflect.irregular "aluguel", "aluguéis"
end

Then inflector-based helpers can use those rules for that locale:

"aluguel".pluralize(:pt_br)
# => "aluguéis"

When regional locales are used together with I18n.fallbacks, inflection rules can also come from a fallback locale. For example, en-GB can reuse :en inflections when that fallback is mapped explicitly.

config.i18n.fallbacks = { "en-GB": :en }

Managing the Locale across Requests

To support multiple locales, applications should set the locale at the beginning of each request and ensure it stays consistent for the duration of that request.

The default locale is used for all translations unless I18n.locale= or I18n.with_locale is used. In a controller, the safest way to do this is with I18n.with_locale, typically in an around_action, so the locale is applied only for that request.

I18n.locale can leak into subsequent requests served by the same thread/process if it is not consistently set in every controller. For example, executing I18n.locale = :es in one POST request will have effects for all later requests to controllers that don't set the locale, but only in that particular thread/process. For that reason, instead of I18n.locale = you can use I18n.with_locale which does not have this leak issue.

The locale can be set using one of many different approaches, depending on where your application stores or derives that information.

Setting the Locale from a Request Parameter

A commonly used technique to set the locale is to include it in request parameters.

For example, you can set the locale in a concern that is included in ApplicationController:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Locale
end
# app/controllers/concerns/locale.rb
module Locale
  extend ActiveSupport::Concern

  included do
    around_action :switch_locale
  end

  def switch_locale(&action)
    locale = params[:locale] || I18n.default_locale
    I18n.with_locale(locale, &action)
  end
end

This example uses a URL query parameter to set the locale, such as http://example.com/books?locale=pt. With this approach, http://localhost:3000?locale=pt renders the Portuguese localization, while http://localhost:3000?locale=de loads a German localization.

Setting the Locale from URL Params

The previous section showed how to read the locale from params[:locale]. In practice, that is only half of the solution. If the locale comes from the URL, your application also needs to include it in generated URLs so the choice is preserved from one request to the next.

For example, you may want URLs such as www.example.com/books?locale=ja or www.example.com/ja/books.

This approach has many of the same advantages as using a parameter for the locale:

It does require a little more setup, though.

Preserving the Locale in Query Parameters

With this approach, we need to ensure that every generated link continues to include the locale. Adding locale: I18n.locale manually to every call to link_to or every route helper would be tedious and easy to miss.

Rails provides a central place for this through default_url_options. By defining it, you can tell url_for and the route helpers built on top of it to automatically include the current locale.

For example, you can add this to your ApplicationController:

# app/controllers/application_controller.rb
def default_url_options
  { locale: I18n.locale }
end

Every helper method that uses url_for (for example, named route helpers such as root_path and root_url, or resource route helpers such as books_path and books_url) will now automatically include the locale in the query string, such as http://localhost:3001/?locale=ja.

You may find this sufficient. However, query parameters can make URLs less readable, and the locale is often conceptually higher-level than the rest of the path. In many applications, it makes more sense for the locale to appear in the path itself.

Using a Locale Scope in the Path

You may prefer URLs like http://www.example.com/en/books for English and http://www.example.com/nl/books for Dutch. You can do this with the same default_url_options approach, combined with a scope in your routes:

# config/routes.rb
scope "/:locale" do
  resources :books
end

Now books_path will return "/en/books" when the current locale is English. Visiting http://localhost:3001/nl/books should set the locale to Dutch, and subsequent calls to books_path during that request will return "/nl/books".

WARNING: Since the return value of default_url_options is cached per request, the URLs in a locale selector cannot be generated invoking helpers in a loop that sets the corresponding I18n.locale in each iteration. Instead, leave I18n.locale untouched, and pass an explicit :locale option to the helper, or edit request.original_fullpath.

If you don't want to force the use of a locale in your routes you can use an optional path scope (denoted by the parentheses):

# config/routes.rb
scope "(:locale)", locale: /en|nl/ do
  resources :books
end

With this approach you will not get a Routing Error when accessing resources such as http://localhost:3001/books without a locale. This is useful for when you want to use the default locale when one is not specified.

You need to take special care of the root URL (usually "homepage" or "dashboard") of your application. A URL like http://localhost:3001/nl will not work automatically, because the root to: "dashboard#index" declaration in your routes.rb doesn't take locale into account.

You would need to map URLs like these:

# config/routes.rb
get "/:locale" => "dashboard#index"

WARNING: Be mindful of the order of your routes so that route declarations do not unintentionally match other routes.

For applications with more complex path-based locale requirements, locale extraction can be handled at the Rack middleware layer before the request reaches Rails routing. This keeps locale handling separate from the routing layer and is typically only needed when rewriting request paths or mounting the application under a locale-specific prefix.

Setting the Locale from the Domain Name

You can also set the locale from the domain or subdomain where your application runs.

In order to set the locale from the domain name, you need to have a separate domain for each locale. For example, www.example.com can load the English (or the default) locale, and www.example.es the Spanish locale. Setting the locale from the domain name has several advantages:

You can implement it like this in your ApplicationController:

# app/controllers/concerns/locale.rb
around_action :switch_locale

def switch_locale(&action)
  locale = extract_locale_from_domain || I18n.default_locale
  I18n.with_locale(locale, &action)
end

# Get locale from top-level domain or return nil if such locale is not available.
# You can add the following in your /etc/hosts file to try this out locally:
#   127.0.0.1 application.com
#   127.0.0.1 application.it
#   127.0.0.1 application.pl
def extract_locale_from_domain
  parsed_locale = request.host.split(".").last
  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end

You can also set the locale from the subdomain in a very similar way. This is often easier to configure and operate than a separate top-level domain for each locale.

# Get locale code from request subdomain (like http://it.application.local:3000)
# You can add the following in your /etc/hosts file to try this out locally:
#   127.0.0.1 it.application.local
#
# Additionally, you need to add the following configuration to your config/environments/development.rb:
#   config.hosts << 'it.application.local:3000'
def extract_locale_from_subdomain
  parsed_locale = request.subdomains.first
  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end

If your application includes a locale switching menu, you would then have something like this in it:

link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['PATH_INFO']}")

assuming you would set APP_CONFIG[:deutsch_website_url] to some value like http://www.application.de.

This solution has the advantages mentioned above, however, you may not be able to or may not want to provide different localizations ("language versions") on different domains. In those cases, you can include the locale in the query parameters or request path, as shown above.

NOTE: Controller-based locale extraction works well for simple applications. For production applications that derive the locale from host or path information, Rack middleware can extract the locale before routing and store it in the Rack environment. See Rails on Rack for more about Rack middleware in Rails applications.

Setting the Locale from User Preferences

An application with authenticated users may allow users to set a locale preference through the application's interface. With this approach, a user's selected locale is persisted in the database and used to set the locale for authenticated requests by that user.

When using a stored preference, ensure that the value is included in I18n.available_locales before applying it.

# app/controllers/concerns/locale.rb
around_action :switch_locale

def switch_locale(&action)
  locale = Current.user&.locale
  locale = I18n.default_locale unless I18n.available_locales.map(&:to_s).include?(locale)

  I18n.with_locale(locale, &action)
end

If your application also allows the locale to be set from the URL or another request-based source, decide which value takes precedence. In many applications, an explicit locale in the URL should override the user's stored preference.

Choosing an Implied Locale

When a request does not explicitly specify a locale, an application may try to infer one. This is best treated as a fallback: an explicit locale from the URL, domain, or user preference should usually take precedence.

Inferring Locale from the Language Header

The Accept-Language HTTP header indicates the languages the client prefers for the response. Browsers typically set this header based on the user's language preferences, so it is often the best first choice when inferring a locale.

A simple implementation might look like this:

# app/controllers/concerns/locale.rb
def switch_locale(&action)
  logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}"
  locale = extract_locale_from_accept_language_header || I18n.default_locale
  logger.debug "* Locale set to '#{locale}'"
  I18n.with_locale(locale, &action)
end

private
def extract_locale_from_accept_language_header
  request.env["HTTP_ACCEPT_LANGUAGE"]&.scan(/^[a-z]{2}/)&.first
end

This example is intentionally simple. In practice, more robust parsing is usually necessary because the header can contain multiple languages, region subtags such as en-GB, and quality values that indicate preference order.

Libraries such as accept_language or the locale Rack middleware can help implement this more reliably.

Inferring the Locale from IP Geolocation

The IP address of the client making the request can be used to infer the client's region and, in some cases, suggest a locale. Services such as GeoLite2 Country or gems like geocoder can be used to implement this approach.

In practice, this is a less reliable signal than using the Accept-Language header. A user's network location does not necessarily match their preferred language, and IP-based lookups may be affected by VPNs, mobile carriers, proxies, or corporate networks.

Storing the Locale from the Session or Cookies

Sessions or cookies can be useful for remembering a user's previous locale choice, especially when no explicit locale is present in the URL or another request-based source. In many applications, they are best treated as a fallback rather than the canonical source of the locale.

The locale is often best kept transparent and reflected in the URL, so that a shared link shows the same content and language to everyone who opens it. This also aligns with common RESTful expectations.

Overview of the I18n API Features

The following sections cover the I18n API in more depth, using examples with both I18n.translate and the translate view helper method.

Looking up Translations

Basic Lookup, Scopes, and Nested Keys

Translations are looked up by keys. String keys are a good default because they work naturally with keys such as "books.index.title":

I18n.t "books.index.title"

The translate method also takes a :scope option which can contain one or more additional keys that will be used to specify a "namespace" or scope for a translation key:

I18n.t "record_invalid", scope: "activerecord.errors.messages"
# => "Validation failed: Name can't be blank"

This looks up the "record_invalid" message in the Active Record error messages.

Additionally, both the key and scopes can be specified as dot-separated keys as in:

I18n.translate "activerecord.errors.messages.record_invalid"
# => "Validation failed: Name can't be blank"

Thus the following calls are equivalent:

I18n.t "activerecord.errors.messages.record_invalid"
# => "Validation failed: Name can't be blank"
I18n.t "errors.messages.record_invalid", scope: "activerecord"
# => "Validation failed: Name can't be blank"
I18n.t "record_invalid", scope: "activerecord.errors.messages"
# => "Validation failed: Name can't be blank"
I18n.t "record_invalid", scope: ["activerecord", "errors", "messages"]
# => "Validation failed: Name can't be blank"

Defaults

When a :default option is given, its value will be returned if the translation is missing:

I18n.t "missing", default: "Not here"
# => 'Not here'

If the :default value is a string key, it will be used as a key and translated.

One can also provide multiple values as default. The first one that results in a value will be returned. The example below first tries to translate the key "missing" and then the key "also_missing". Since both do not yield a result, the string "Not here" will be returned:

I18n.t "missing", default: ["also_missing", "Not here"]
# => 'Not here'

Bulk and Namespace Lookup

To look up multiple translations at once, an array of keys can be passed:

I18n.t ["odd", "even"], scope: "errors.messages"
# => ["must be odd", "must be even"]

Also, a key can translate to a (potentially nested) hash of grouped translations. For example, one can receive all Active Record error messages as a Hash:

I18n.t "errors.messages"
# => {:inclusion=>"is not included in the list", :exclusion=> ... }

If you want to perform interpolation on a bulk hash of translations, you need to pass deep_interpolation: true as a parameter. When you have the following dictionary:

en:
  welcome:
    title: "Welcome!"
    content: "Welcome to the %{app_name}"

then the nested interpolation will be ignored without the setting:

I18n.t "welcome", app_name: "book store"
# => {:title=>"Welcome!", :content=>"Welcome to the %{app_name}"}

I18n.t "welcome", deep_interpolation: true, app_name: "book store"
# => {:title=>"Welcome!", :content=>"Welcome to the book store"}

"Lazy" Lookup

Rails implements a convenient way to look up the locale inside views. When you have the following dictionary:

es:
  books:
    index:
      title: "Título"

you can look up the books.index.title value inside app/views/books/index.html.erb template like this (note the dot):

<%= t ".title" %>

NOTE: Automatic translation scoping by partial is only available from the translate view helper method.

"Lazy" lookup can also be used in controllers:

en:
  books:
    create:
      success: Book created!

This is useful for setting flash messages:

class BooksController < ApplicationController
  def create
    # ...
    redirect_to books_url, notice: t(".success")
  end
end

Pluralization

In many languages, including English, there are only two forms, a singular and a plural, for a given string, e.g. "1 message" and "2 messages".

Other languages (Arabic, Japanese, Russian and many more) have different grammars that have additional or fewer plural forms. Thus, the I18n API provides a flexible pluralization feature.

The :count interpolation variable has a special role in that it both is interpolated to the translation and used to pick a pluralization from the translations according to the rules defined in the pluralization backend. By default, only the English pluralization rules are applied.

I18n.backend.store_translations :en, inbox: {
  zero: "no messages", # optional
  one: "one message",
  other: "%{count} messages"
}
I18n.translate "inbox", count: 2
# => '2 messages'

I18n.translate "inbox", count: 1
# => 'one message'

I18n.translate "inbox", count: 0
# => 'no messages'

The algorithm for pluralizations in :en is:

lookup_key = :zero if count == 0 && entry.has_key?(:zero)
lookup_key ||= count == 1 ? :one : :other
entry[lookup_key]

The translation denoted as :one is regarded as singular, and the :other is used as plural. If the count is zero, and a :zero entry is present, then it will be used instead of :other.

If the lookup for the key does not return a Hash suitable for pluralization, an I18n::InvalidPluralizationData exception is raised.

Locale-specific Rules

The I18n gem provides a Pluralization backend that can be used to enable locale-specific rules. Include it to the Simple backend, then add the localized pluralization algorithms to translation store, as i18n.plural.rule.

I18n::Backend::Simple.include(I18n::Backend::Pluralization)
I18n.backend.store_translations :pt, i18n: {
  plural: { rule: lambda { |n| [0, 1].include?(n) ? :one : :other } }
}
I18n.backend.store_translations :pt, apples: {
  one: "one or none",
  other: "more than one"
}

I18n.t "apples", count: 0, locale: :pt
# => 'one or none'

Alternatively, the separate gem rails-i18n can be used to provide a fuller set of locale-specific pluralization rules.

Setting and Passing a Locale

The locale can be either set pseudo-globally to I18n.locale (which uses Thread.current in the same way as, for example, Time.zone) or can be passed as an option to #translate and #localize.

WARNING: The i18n gem stores I18n.locale using Thread.current. Applications serving multiple requests from the same thread should set the locale explicitly around each request, for example with I18n.with_locale.

If no locale is passed, I18n.locale is used:

I18n.locale = :de
I18n.t "foo"    # Looks up de.foo
I18n.l Time.now # Looks up de.time.formats.default

Explicitly passing a locale:

I18n.t "foo", locale: :br    # Looks up br.foo
I18n.l Time.now, locale: :es # Looks up es.time.formats.default

The I18n.locale defaults to I18n.default_locale which defaults to :en. The default locale can be set like this:

I18n.default_locale = :de

Using Safe HTML Translations

Keys with a '_html' suffix and keys named 'html' are marked as HTML safe. When you use them in views the HTML will not be escaped.

# config/locales/en.yml
en:
  welcome: <b>welcome!</b>
  hello_html: <b>hello!</b>
  title:
    html: <b>title!</b>
<!-- app/views/home/index.html.erb -->
<%= t('welcome') %>
<%= raw t('welcome') %>
<%= t('hello_html') %>
<%= t('title.html') %>

This renders:

&lt;b&gt;welcome!&lt;/b&gt;
<b>welcome!</b>
<b>hello!</b>
<b>title!</b>

Interpolation escapes as needed though. For example, given:

en:
  welcome_html: "<b>Welcome %{username}!</b>"

you can safely pass the username as set by the user:

<%# This is safe, it is going to be escaped if needed. %>
<%= t('welcome_html', username: @current_user.username) %>

Safe strings on the other hand are interpolated verbatim.

NOTE: Automatic conversion to HTML safe translate text is only available from the translate (or t) helper method. This works in views and controllers.

Built-in Rails Translations

Rails uses I18n in several framework features beyond direct calls to I18n.t. These sections cover the translation conventions used by Active Record, Action Mailer, form helpers, and other Rails helpers.

Active Record Models

You can use the methods Model.model_name.human and Model.human_attribute_name(attribute) to transparently look up translations for your model and attribute names.

For example, when you add the following translations:

en:
  activerecord:
    models:
      user: Customer
    attributes:
      user:
        login: "Handle"
      # will translate User attribute "login" as "Handle"

Then User.model_name.human will return "Customer" and User.human_attribute_name("login") will return "Handle".

User.model_name.human
# => "Customer"

User.human_attribute_name("login")
# => "Handle"

You can also set a plural form for model names:

en:
  activerecord:
    models:
      user:
        one: Customer
        other: Customers

Then, User.model_name.human(count: 2) will return "Customers". With count: 1 or without params, it will return "Customer".

User.model_name.human(count: 2)
# => "Customers"

User.model_name.human
# => "Customer"

In the event you need to access nested attributes within a given model, you should nest these under model/attribute at the model level of your translation file:

en:
  activerecord:
    attributes:
      user/role:
        admin: "Admin"
        contributor: "Contributor"

Then User.human_attribute_name("role.admin") will return "Admin".

User.human_attribute_name("role.admin")
# => "Admin"

NOTE: If you are using a class which includes ActiveModel and does not inherit from ::ActiveRecord::Base, replace activerecord with activemodel in the above key paths.

Error Message Scopes

Active Record validation error messages can also be translated easily. Active Record gives you a couple of namespaces where you can place your message translations in order to provide different messages and translation for certain models, attributes, and/or validations. It also transparently takes single table inheritance into account.

This gives you a way to flexibly adjust your messages to your application's needs.

Consider a User model with a validation for the name attribute:

class User < ApplicationRecord
  validates :name, presence: true
end

The key for the error message in this case is :blank. Thus, in our example it will try the following keys in this order and return the first result:

activerecord.errors.models.user.attributes.name.blank
activerecord.errors.models.user.blank
activerecord.errors.messages.blank
errors.attributes.name.blank
errors.messages.blank

To explain it more abstractly, it returns the first key that matches in the order of the following list.

activerecord.errors.models.[model_name].attributes.[attribute_name].[key]
activerecord.errors.models.[model_name].[key]
activerecord.errors.messages.[key]
errors.attributes.[attribute_name].[key]
errors.messages.[key]

Additionally, when your models are using inheritance then the messages are looked up in the inheritance chain.

For example, you might have an Admin model inheriting from User:

class Admin < User
  validates :name, presence: true
end

Then Active Record will look for messages in this order:

activerecord.errors.models.admin.attributes.name.blank
activerecord.errors.models.admin.blank
activerecord.errors.models.user.attributes.name.blank
activerecord.errors.models.user.blank
activerecord.errors.messages.blank
errors.attributes.name.blank
errors.messages.blank

This way you can provide special translations for various error messages at different points in your model's inheritance chain and in the attributes, models, or default scopes.

Error Message Interpolation

The translated model name, translated attribute name, and value are always available for interpolation as model, attribute and value respectively.

For example, instead of the default error message "can't be blank" you could use the attribute name like this : "Please fill in your %{attribute}". count, where available, can be used for pluralization if present:

validation with option message interpolation
confirmation - :confirmation attribute
acceptance - :accepted -
presence - :blank -
absence - :present -
length :within, :in :too_short count
length :within, :in :too_long count
length :is :wrong_length count
length :minimum :too_short count
length :maximum :too_long count
uniqueness - :taken -
format - :invalid -
inclusion - :inclusion -
exclusion - :exclusion -
associated - :invalid -
non-optional association - :required -
numericality - :not_a_number -
numericality :greater_than :greater_than count
numericality :greater_than_or_equal_to :greater_than_or_equal_to count
numericality :equal_to :equal_to count
numericality :less_than :less_than count
numericality :less_than_or_equal_to :less_than_or_equal_to count
numericality :other_than :other_than count
numericality :only_integer :not_an_integer -
numericality :in :in count
numericality :odd :odd -
numericality :even :even -
comparison :greater_than :greater_than count
comparison :greater_than_or_equal_to :greater_than_or_equal_to count
comparison :equal_to :equal_to count
comparison :less_than :less_than count
comparison :less_than_or_equal_to :less_than_or_equal_to count
comparison :other_than :other_than count

Action Mailer E-Mail Subjects

If you don't pass a subject to the mail method, Action Mailer will try to find it in your translations. The performed lookup will use the pattern <mailer_scope>.<action_name>.subject to construct the key.

# user_mailer.rb
class UserMailer < ActionMailer::Base
  def welcome(user)
    #...
  end
end
en:
  user_mailer:
    welcome:
      subject: "Welcome to Rails Guides!"

To send parameters to interpolation use the default_i18n_subject method on the mailer:

# 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!"

Form Helpers

Form helpers use model and attribute translations when they build labels and error messages. For example, form_with calls human_attribute_name for label text when you do not pass an explicit label:

<%= form_with model: @user do |form| %>
  <%= form.label :login %>
  <%= form.text_field :login %>
<% end %>

With the following translations:

en:
  activerecord:
    attributes:
      user:
        login: "Handle"

the label renders as:

<label for="user_login">Handle</label>

Action View Helper Methods

Several Action View helpers use locale data for dates, times, numbers, and currency formatting.

Active Model Methods

Active Model uses translations for human-readable model names, attribute names, and validation error messages.

Active Support Methods

Active Support also includes helpers that use locale-specific formatting rules.

Advanced Configuration and Setup

Configuring the load paths and default locale

Rails automatically adds translation files from config/locales to the translations load path. By default, this includes .rb and .yml files, and for many applications that is all you need.

Rails' own translations are organized the same way. See, for example, the Active Model validation messages in activemodel/lib/active_model/locale/en.yml or the date and time formats in activesupport/lib/active_support/locale/en.yml. With the default Simple backend, you can store translations in YAML files or in plain Ruby hashes.

The translations load path (I18n.load_path) is the list of translation files Rails loads automatically. You can customize it if you want a different directory structure or naming scheme.

NOTE: The backend lazy-loads these translations the first time a translation is looked up. You can still swap the backend implementation later if needed.

To add more translation files or change the default locale, configure config.i18n in config/application.rb:

config.i18n.load_path += Dir[Rails.root.join("my", "locales", "*.{rb,yml}")]
config.i18n.default_locale = :de

The load path must be configured before any translations are looked up.

If you need to work directly with i18n instead, for example in an initializer, you can do that as well:

# config/initializers/locale.rb

# Where the I18n library should search for translation files
I18n.load_path += Dir[Rails.root.join("lib", "locale", "*.{rb,yml}")]

# Permitted locales available for the application
I18n.available_locales = [:en, :pt]

# Set default locale to something other than :en
I18n.default_locale = :pt

In general, prefer config.i18n.* in application configuration. Appending directly to I18n.load_path will not override translations from external gems in the same way.

You can use YAML (.yml) or plain Ruby (.rb) files to store translations in the Simple backend. YAML is the format most Rails applications use, but it is sensitive to whitespace and special characters, so syntax mistakes can prevent a locale file from loading correctly.

Ruby locale files can be useful when you want syntax errors to fail fast, or when you need values such as lambdas in your locale data.

If your translations are stored in YAML files, certain keys must be escaped. They are:

Examples:

# config/locales/en.yml
en:
  success:
    'true':  'True!'
    'on':    'On!'
    'false': 'False!'
  failure:
    true:    'True!'
    off:     'Off!'
    false:   'False!'
I18n.t "success.true"  # => 'True!'
I18n.t "success.on"    # => 'On!'
I18n.t "success.false" # => 'False!'
I18n.t "failure.false" # => Translation Missing
I18n.t "failure.off"   # => Translation Missing
I18n.t "failure.true"  # => Translation Missing

Storing Custom Translations

The Simple backend shipped with Active Support allows you to store translations in both plain Ruby and YAML format. Other backends may allow or require different formats.

For example a Ruby Hash providing translations can look like this:

{
  pt: {
    foo: {
      bar: "baz"
    }
  }
}

The equivalent YAML file would look like this:

pt:
  foo:
    bar: baz

As you see, in both cases the top level key is the locale. "foo" is a namespace key and "bar" is the key for the translation "baz".

Here is a "real" example from the Active Support en.yml translations YAML file:

en:
  date:
    formats:
      default: "%Y-%m-%d"
      short: "%b %d"
      long: "%B %d, %Y"

So, all of the following equivalent lookups will return the :short date format "%b %d":

I18n.t "date.formats.short"                # => "%b %d"
I18n.t "formats.short", scope: "date"      # => "%b %d"
I18n.t "short", scope: "date.formats"      # => "%b %d"
I18n.t "short", scope: ["date", "formats"] # => "%b %d"

Generally we recommend using YAML as a format for storing translations. There are cases, though, where you want to store Ruby lambdas as part of your locale data, e.g. for special date formats.

Using Alternate Backends

For several reasons the Simple backend shipped with Active Support only does the "simplest thing that could possibly work" for Ruby on Rails. It is guaranteed to work for English and, as a side effect, for languages that are very similar to English. It is also only capable of reading translations and cannot dynamically store them in another format.

That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs, by passing a backend instance to the I18n.backend= setter.

For example, you can replace the Simple backend with the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends.

With the Chain backend, you could use the Active Record backend and fall back to the (default) Simple backend:

I18n.backend = I18n::Backend::Chain.new(
  I18n::Backend::ActiveRecord.new,
  I18n.backend
)

In this configuration, lookups first check the Active Record backend and then fall back to the locale files loaded by the Simple backend:

I18n.t("welcome")
# Looks for "welcome" in the Active Record backend first,
# then in YAML/Ruby locale files.

This can be useful when you want translators or administrators to update some translations at runtime while still keeping default translations in version-controlled locale files.

Handling I18n Exceptions

The I18n API defines the following exceptions that will be raised by backends when the corresponding unexpected conditions occur:

Exception Reason
I18n::MissingTranslationData no translation was found for the requested key
I18n::InvalidLocale the locale set to I18n.locale is invalid (e.g. nil)
I18n::InvalidPluralizationData a count option was passed but the translation data is not suitable for pluralization
I18n::MissingInterpolationArgument the translation expects an interpolation argument that has not been passed
I18n::ReservedInterpolationKey the translation contains a reserved interpolation variable name (i.e. one of: scope, default)
I18n::UnknownFileType the backend does not know how to handle a file type that was added to I18n.load_path

Handling Missing Translations

If config.i18n.raise_on_missing_translations is false (the default in all environments), the missing translation message is returned instead. It includes the missing key and scope so you can find and fix the problem.

If config.i18n.raise_on_missing_translations is true, missing translations raise errors instead of returning the usual "Translation missing: ..." message. If the value is :strict, model translations also raise. It is a good idea to enable this in your test environment so you can catch missing translations early.

For example:

I18n.t("missing")
# => "Translation missing: en.missing"

I18n.t!("missing")
# => I18n::MissingTranslationData: Translation missing: en.missing

When config.i18n.raise_on_missing_translations is true, the same lookup raises:

I18n.t("missing")
# => I18n::MissingTranslationData: Translation missing: en.missing

I18n.t("inbox", locale: nil)
# => I18n::InvalidLocale

I18n.t("product_price", locale: :nl)
# => I18n::MissingInterpolationArgument

If you want to customize this behavior further, set config.i18n.raise_on_missing_translations = false and implement an I18n.exception_handler. The custom exception handler can be a proc or a class with a call method:

# config/initializers/i18n.rb
module I18n
  class RaiseExceptForSpecificKeyExceptionHandler
    def call(exception, locale, key, options)
      if key == "special.key"
        "translation missing!" # return this, don't raise it
      elsif exception.is_a?(MissingTranslation)
        raise exception.to_exception
      else
        raise exception
      end
    end
  end
end

I18n.exception_handler = I18n::RaiseExceptForSpecificKeyExceptionHandler.new

This would raise all exceptions the same way the default handler would, except for I18n.t("special.key").

The translate view helper also routes missing translations through I18n.exception_handler, so a custom handler applies there as well.