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:
- How I18n works in Ruby on
Rails - How to configure, set, and switch locales in a
Railsapplication - How to organize translations and use the
I18nAPI in views, controllers, and models - How to use
I18nto translate Active Record errors or Action Mailer E-mail subjects - How to customize backends and handle translation-related exceptions
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:
- ensure your application is configured for internationalization (I18n)
- tell
Railswhere to find translation files - set, preserve, and switch the locale for each request
To localize your application, you typically need to:
- provide translations for application text
- add or customize locale-specific formats, such as dates, times, and currencies
- organize and maintain translation data
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:
- The public API of the
I18nframework - a Ruby module with public methods that define how the library works. - A default backend (named Simple backend) that implements these methods.
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:
translate, which looks up translations for a given key and locale. It is aliased ast, so you can useI18n.t("store.title"). Additionally, this helper catches missing translations and wraps the resulting error message in a<span class="translation_missing">.localize, which localizesDateandTimeobjects to local formats. It is aliased asl, so you can useI18n.l(Time.now).
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
- United Kingdom" locale in a
:"en-GB"dictionary.
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
|---
|-----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:
- the locale is visible in the URL
- the URL can be shared
- search engines can distinguish between language-specific pages
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
{ 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:
- The locale is an obvious part of the URL.
- People intuitively grasp in which language the content will be displayed.
- Search engines like that content in different languages lives at different, inter-linked domains.
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:
<b>welcome!</b>
<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..blank
errors.attributes.name.blank
errors..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..[key]
errors.attributes.[attribute_name].[key]
errors..[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..blank
errors.attributes.name.blank
errors..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.
-
distance_of_time_in_wordstranslates and pluralizes its result and interpolates the number of seconds, minutes, hours, and so on.```ruby distance_of_time_in_words(Time.current, Time.current + 3.minutes) # => "3 minutes" ``` It uses translations from the `datetime.distance_in_words` scope. -
relative_time_in_wordsbuilds ondistance_of_time_in_wordsand adds a localized prefix/suffix depending on whether the time is in the past or future.```ruby relative_time_in_words(3.minutes.from_now) # => "in 3 minutes" relative_time_in_words(3.minutes.ago) # => "3 minutes ago" ``` It uses translations from the `datetime.relative` scope. -
datetime_selectandselect_monthuse translated month names for populating the resulting select tag.```erb <%= select_month(Date.new(2024, 3, 1)) %> ``` The rendered `<option>` labels come from translated month names in the `date.month_names` scope. `datetime_select` also looks up the order option from `date.order` (unless you pass the option explicitly). All date selection helpers translate the prompt using the translations in the `datetime.prompts` scope if applicable. -
The
number_to_currency,number_with_precision,number_to_percentage,number_with_delimiter, andnumber_to_human_sizehelpers use the number format settings located in thenumberscope.```ruby number_to_currency(10) # => "$10.00" ```
Active Model Methods
Active Model uses translations for human-readable model names, attribute names, and validation error messages.
-
model_name.humanandhuman_attribute_nameuse translations for model names and attribute names if available in theactiverecord.modelsandactiverecord.attributesscopes.```ruby User.model_name.human # => "Customer" ``` They also support translations for inherited class names (e.g. for use with STI) as explained above in "Error message scopes". -
ActiveModel::Errors#generate_message (which is used by Active Model validations but may also be used manually) uses
model_name.humanandhuman_attribute_name.```ruby user.errors.generate_message(:name, :blank) # => "can't be blank" ``` It also translates the error and supports translations for inherited class names as explained above in "Error message scopes". -
ActiveModel::Error#full_message and ActiveModel::Errors#full_messages prepend the attribute name to the error message using a format looked up from
errors.format(default:"%{attribute} %{message}").```ruby user.errors.full_messages # => ["Name can't be blank"] ``` To customize the default format, override it in the app's locale files. To customize the format per model or per attribute, see [`config.active_model.i18n_customize_full_message`][].
Active Support Methods
Active Support also includes helpers that use locale-specific formatting rules.
-
Array#to_sentence uses format settings from the
support.arrayscope.```ruby ["apples", "oranges", "pears"].to_sentence # => "apples, oranges, and pears" ```
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:
- true, on, yes
- false, off, no
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, )
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.