I'm a Floridian web engineer who planted his roots in Paris. Some of my favorite past-times are analyzing and optimizing development processes, building solid APIs, mentoring junior devs, and long walks on the beach.

Internationalize your rails app

I18n - Expected usage after setup

If everything has been properly put into place, then we expect to be able to simply use the t method in our views and have them render in different languages:

<h1><%= t('some.string.value') %></h1>

/// Should render:
<h1>My Value</h1>
<h1>Mi Valor</h1>
<h1>Ma Valeur</h1>

Stateless requests

If we don't know too much about the inner workings a few of the first roadblocks will be dealing with moving between requests. How to we store the locale between requests? How can we store anything between requests?

Cookies

I've actually used this for a simple wordpress site I had. The user carries the cookie with them, so once they choose their locale, it stays with them. It is dirt simple to set up, but there are a few drawbacks.

  1. You can't share locales when sharing a link, since the locale is attached to a user, if you have a spanish user sharing a link, the locale will change back to the default when the link is followed by another user. This is because the locale is tied to the user's browser.
  2. Signing out loses the user's locale setting or at least complicates keeping it
  3. Storing the locale in a cookie will make it harder to reason about. This seems like a minor thing, but when bugs come up dealing with a locale, you will be happier having it in directly in the URL.

URLs

Storing your locale information in the URL is probably the most straight forward. It takes a little more plumbing to implement because since it will be stateless, you'll have to ensure that every link contains the information.

However, it is explicit as it is visible in the URL. It is shareable and debuggable for the same reason.

Where to put my locale

Rails has a few things to say about this but essentially you have a few different possibilities for keeping the locale in the URL.

  1. Subdomains(http://fr.wollydo.com/machins) - can be beneficial if you want to think of each locale as a different site, and it is more cost effective than a TLD
  2. Top Level domains(http://wollydo.fr/machins) - are another way to designate locales as different sites however, you need to buy each domain.
  3. URL parameter(http://wollydo.fr/machins?locale=nl) - a little planned way to keep the locale in the url.
  4. URL path(http://wollydo.fr/fr/machins) - this treats locales as the most important identifier of the site, by putting it first in the path. It is a much more permenant decision than a simple parameter because you have modified your path structure.

I am going to assume that you think that the last option is the best. The rails guide actually has quite a bit of helpful information if you want to look into the other locale options.

Life of a rails request

Here is the life of a rails request for an app that has a machins resource.

Simplified life of a rails request

With this simplified view of the path of a request, we can see that to handle communicating the locale by URLs, we will have to generate a URL that contains the locale, resolve the locale in the routes, set the locale, and call the i18n.t method in our views. Another thing which is not immediately visible by the request path alone is that we will want our user to be able to select a locale.

Generate the locale

The following creates a select box to generate our locale. It uses a method that we will create later called url_for_locale which will modify the rails url_for to reload the current page while passing a new locale.


<label><%= t('change_locale') %></label>
<select class='language_switcher'>
  <% I18n.available_locales.each do |locale| %>
    <option
      value='<%= url_for_locale(locale) %>'
      selected='<%= locale == I18n.locale %>'>
      <%= locale %>
    </option>
  <% end %>
</select>

# We use a simple jQuery method to reload the page on click of the locale in the select box.
$('select.language_switcher')
  .change(function(element){
    window.location.href = $(this).val();
});

As mentioned earlier, Rails comes with a method called url_for we are going to wrap this in our new method url_for_locale to redirect to the current page with the new locale.

Scope incoming locale

Your route file is where we are going to scope our locale. If I think about the solution that I really want, it is one where my site has all of the normal URLs that go to the default locale and then any available secondary locales will be visible in the URL.

http://sillypants.com/machin      # default
http://sillypants.com/fr/machin   # french

This seems like it would be 2x the work in the routes file as you would have to re-define the routes but there is really a simple solution. You define your default routes in a method and then use call it directly in the routes file

# route definitions in routes.rb
def site_endpoints
  namespace :admin do
  resources :posts
  resources :images
end

  resources :posts, only: [:index]
end

scope '/(:locale)', constraints: AvailableLocalesConstraint.new do
  site_endpoints
end
site_endpoints

You may have noticed that I am instantiating a classAvailableLocalesConstraint. This is a standard way to scope more complex matching rules for scopes. I don't often need to use something this powerful but here I find it useful to have this level of expression. I am using this because I need something to only match if there is a locale in the params and it is one of the locales I have translations for, otherwise the constraint will fail and the routes file will match further down.

class AvailableLocalesConstraint
  def matches?(request)
    return unless request.params[:locale]

    locale = request.params[:locale].to_sym
    I18n.available_locales.include? locale
  end
end

Set locale based on the params

Now we know that whatever is in the params[:locale] should be a valid locale. Technically, it could also be nil. In order to set i18n.locale we can set a few things in the ApplicationController.

You'll notice that I have also overridden default_url_options, this allows me to reset the locale in all the path_for, url_for calls so that I don't have to specify the locale in things like form submits and any links in the page, either the user has specified a change or it will keep the locale automatically.

before_action :set_locale

def set_locale
  I18n.locale == params[:locale] || I18n.default_locale
end

def default_url_options(options={})
  { locale: I18n.locale == I18n.default_locale ? nil : I18n.locale }
end