Working with Locales and Time Zones in Rails 🌏
Rails handles internationalization (i18n) really well, and you can set the default locale and time zone for your application very simply:
# config/application.rb
module I18nTest
class Application < Rails::Application
config.i18n.default_locale = 'en'
config.time_zone = 'London'
end
end
Setting the locale from the URL
First you need to wrap your applications routes in a scope defining the :locale
parameter. Your application URLs will then look like /en/controller/action
with the first segment telling Rails which locale it should use. The following regular expression just places a constraint on the locale parameter so that it must match one of the locales the application knows about.
# config/routes.rb
scope ':locale', locale: /#{I18n.available_locales.join("|")}/ do
# application routes...
end
# Catch all requests without a locale and redirect to the default...
match '*path', to: redirect("/#{I18n.default_locale}/%{path}"), constraints: lambda { |req| !req.path.starts_with? "/#{I18n.default_locale}/" }
match '', to: redirect("/#{I18n.default_locale}")
Contrary to the Rails guides examples, I prefer to use the around_action
hook with I18n.with_locale
class method to set the locale. Even though we may not be running in a multi-threaded environment, I feel it is more responsible way to handle the locale on a per-request basis like this.
We also override the #default_url_options
method so the locale is automatically set when we use any of the *_url
or *_path
helper methods in our controller or views.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
around_action :with_locale
private
def with_locale
I18n.with_locale(params[:locale]) { yield }
end
def default_url_options(options = {})
{ locale: I18n.locale }
end
end
And there we have it, the application can respond with the correct content based on the URL. To make use of this in your application use  the t
and l
helpers, these can be used in your controllers and views.
Using i18n in your application
I recommend using them for everything, it will save a lot of pain if you ever need to handle multiple locales in the future; or you might just want to change the format of your dates.
First in Rails 4.1 you’ll want to raise_on_missing_translations
so Rails will shout at you if any translations are missing.
# config/environments/{test,development}.rb
Rails.application.configure do |config|
config.action_view.raise_on_missing_translations = true
end
The t
or translate
helper accepts a simple dot.notation
string to specify the translation you are trying to access (there are short hand options available but check the Rails documentation for more information). You can also pass in a hash to replace named keys in the translation string:
# config/locales/en.yml
en:
dot:
notation: "Hello, my name is %{name}"
# Example
t('dot.notation', name: 'Pete') #=> Hello, my name is Pete
The l
or localize
helper doesn’t get as much attention as it deserves, it’s very useful for localising dates, numbers, money and so on.
# config/locales/en.yml
en:
time:
formats:
short: '%d %b %H:%M'
# Example
l(Time.current, format: :short) #=> 25 Apr 11:40
Note: Rails only provides the US English locale by default, but you can get hold of you locale from the rails-i18n gem.
Find missing or unused i18n translations
As your application grows you’ll find the translation files can quickly become large difficult to keep track of everything in them. Thankfully i18n-tasks can ease the pain and help you track down missing or unused translations (credit to Jessie Young for letting me know about this one):
i18n-tasks health
Check out the readme for i18n-tasks for more details on using the tool, along with the configuration options available.
Setting the time zone
It’s recommended to use the around_action
hook and Time.use_zone
as Time.zone
is known to leak into other threads. In the example below the time zone is stored as an attribute on the user; a list of valid time zone values can be found by running rake time:zones:all
.
Note: The Time.use_zone
method is a core extension provided by ActiveSupport.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
around_action :with_time_zone, if: :current_user
private
def with_time_zone
Time.use_zone(current_user.time_zone) { yield }
end
end
Working with time zones can be tricky, Rails helps you in some places and then leaves you hanging in others (for good reason, but it can still catch you out). Let’s examine some of the gotchas:
Working with ActiveRecord attributes and time zones
Good news! ActiveRecord helps you out by converting all values to UTC for storing in the database. When it fetches the record the value is converted to the expect time zone for that request (either from config.time_zone
or if you’re using Time.use_zone
).
Working with the Time
class
If you are using the Time
class it’s important to specify the time zone you want to use. The easiest way to do this is to include the zone
with Time.zone
; this will use the expected time zone. If you just use Time
then it will use the servers system time zone. For example Rails provides some helpers:
Time.current # Rails helper for Time.zone.now
Time.zone.parse('2014-04-25 11:30:00')
Method calls like 2.hours.ago
use the time zone you’ve configured so these are safe to use as well. Even if you’re not building an application that cares about time zones at the moment, it could save you some pain in the future to use the time zone safe methods.