Deutsch: http://example.com/de/books
English: http://example.com/en/books
Introduction
No matter how small your application is, it is never too early to think of internationalization, even if you have no immediate plans to broadcast your website in multiple languages. Planning for internationalization early on will save countless hours and headache in the future. Even if your website is only using one language, the process of internationalization will force you to refactor all of your user-specific messages into one place so that they are easy to find and maintain.This post, however, is not about the benefits of internationalization (you can find many yourself with a quick Google search) or how to display your website in multiple languages. I assume that you already know how to do that and will instead delve deeper into how to serve language-based content to your users with intuitive and user-friendly URLs.
Different locale-based URL options
Although I am only discussing one specific method of producing locale-based URLs, it is good to know other alternatives.
Locale as a URL variable: http://example.com/books?locale=en
With this method every URL with have the locale=en
segment to indicate the language. While, it is clear what language is being used, having the same parameter in every URL is a little verbose, not to mention aesthetically unappealing (if you happen to be a pedant). Another disadvantage of this method is that search engines may not realize that a page uses a different language and, as a result, might not index it correctly.
Locale as a subdomain: http://en.example.com/books
This URL certainly looks much more pleasing to the eye and addresses the search engine shortcoming addressed above; however, this method requires extra configuration for every different language. That is, you will have manually add every subdomain to your website and point it correctly to your website, and, depending on how you configure these subdomains, you may have to repoint them all if the IP address of your website happens to change (which, of course, can be addresses by using canonical names). Another possible problem arises from using third-party hosting services, which may not allow you to add subdomains for free.
Locale as a host suffix: http://example.com/en/books
This, in my opinion, is the absolute winner, as it is trivial to configure, does not require any extra configuration if another language needs to be added in the future. Search engines are well aware of this technique, and your URLs stay short and easy to understand.
Session-based locale: http://example.com/books
In this case, there is no way to tell from the URL what language is being used. This method is typically reserved for websites where users have to log in and can then change their language of preference in website settings. Since a user specifically opts for a particular language, there is no reason to make URLs locale-based, and, therefore, this method certainly has its place and should not be discounted.
Creating a small application and setting up some translations
To learn to how to create pretty locale-based URLs, we will build together a tiny book inventory application. Follow my lead.Let's create our application and a
books
resource with a title, one author and a price.
rails new bookapp && cd bookapp
rails generate scaffold book title author price:decimal
rake db:migrate
If we start our application with
rails server
and navigate to http://localhost:3000
we should be able to see an empty list of books. Before we start customizing URLs, let's add some translations to the application so that when the URLs are ready, we will be able to tell the difference.
Create the following internationalization files in /config/locales
:
de.yml
de:
button:
back: 'Zurück'
destroy: 'Zerstören'
edit: 'Bearbeiten'
show: 'Zeigen'
listing: "%{model}liste"
editing: "%{model} bearbeiten"
helpers:
submit:
create: "%{model} erstellen"
submit: "%{model} speichern"
update: "%{model} aktualisieren"
activerecord:
models:
book:
one: 'Buch'
other: 'Bücher'
new: "Neues Buch"
attributes:
book:
title: 'Titel'
author: 'Autor'
price: 'Preis'
en.yml
en:
button:
back: 'Back'
destroy: 'Destroy'
edit: 'Edit'
show: 'Show'
listing: "Listing %{model}"
editing: "Editing %{model}"
helpers:
submit:
create: "Create %{model}"
submit: "Save %{model}"
update: "Update %{model}"
activerecord:
models:
book:
one: 'Book'
other: 'Books'
new: "New Book"
attributes:
book:
title: 'Title'
author: 'Author'
price: 'Price'
Modify the following Book views as follows:
_form.html.erb
<%= form_for(@book) do |f| %>
<div class="field">
<%= f.label :title, Book.human_attribute_name(:title) %><br>
<%= f.text_field :title %>
</div>
<div class="field">
<%= f.label :author, Book.human_attribute_name(:author) %><br>
<%= f.text_field :author %>
</div>
<div class="field">
<%= f.label :price, Book.human_attribute_name(:price) %><br>
<%= f.text_field :price %>
</div>
<div class="actions">
<%= f.submit class: 'button' %>
</div>
<% end %>
edit.html.erb
<h1><%= t('editing', model: Book.model_name.human) %></h1>
<%= render 'form' %>
<%= link_to t('button.back'), books_path, class: 'hollow secondary button' %>
<%= link_to t('button.show'), @book, class: 'button' %>
index.html.erb
<h1><%= t('listing', model: Book.model_name.human(:count => 2)) %></h1>
<table>
<thead>
<tr>
<th><%= Book.human_attribute_name(:title) %></th>
<th><%= Book.human_attribute_name(:author) %></th>
<th><%= Book.human_attribute_name(:price) %></th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @books.each do |book| %>
<tr>
<td><%= book.title %></td>
<td><%= book.author %></td>
<td><%= book.price %></td>
<td><%= link_to t('button.show'), book %></td>
<td><%= link_to t('button.edit'), edit_book_path(book) %></td>
<td><%= link_to t('button.destroy'), book, method: :delete %></td>
</tr>
<% end %>
</tbody>
</table>
<%= link_to t('activerecord.models.book.new'), new_book_path, class: 'button' %>
new.html.erb
<h1><%= t('activerecord.models.book.new') %></h1>
<%= render 'form' %>
<%= link_to t('button.back'), books_path, class: 'hollow secondary button' %>
show.html.erb
<p>
<strong><%= Book.human_attribute_name(:title) %>:</strong>
<%= @book.title %>
</p>
<p>
<strong><%= Book.human_attribute_name(:author) %>:</strong>
<%= @book.author %>
</p>
<p>
<strong><%= Book.human_attribute_name(:price) %>:</strong>
<%= @book.price %>
</p>
<%= link_to t('button.back'), books_path, class: 'hollow secondary button' %>
<%= link_to t('button.edit'), edit_book_path(@book), class: 'button' %>
Now that we've created some translations, we can change the default locale to German in
/config/application.rb
by uncommenting the line:config.i18n.default_locale = :de
If we navigate to
http://localhost:3000/books
, we will see the list of books in German (remember to restart the application to see the changes).
Now we're finally ready to add locales to our URLs.
Setting up locale-based URLs
The first we need to do is enclose all routes that should have the locale prefix insidescope
inside routes.rb
.Rails.application.routes.draw do scope '(:locale)', locale: Rails.configuration.x.locale do resources :books # other locale-based routes defined here end # non-locale-based routes defined here end
The variable
Rails.configuration.x.locale
is currently undefined, but we will use it to keep a list of all supported locales. The best place to define it is in application.rb
:
class Application < Rails::Application
...
config.x.locale = /de|en/
...
end
Now we need to create a
before_action
in application_controller.rb
. We also need to override the default default_url_options
method so that all URLs always contain the locale (note that this will not affect any non-locale-based routes in case you defined some).
class ApplicationController < ActionController::Base protect_from_forgery with: :exception before_action :set_locale private def default_url_options {locale: params[:locale]} end def set_locale I18n.locale = params[:locale] || I18n.default_locale end end
Now we can navigate to
http://localhost:3000/de/books
and http://localhost:3000/en/books
and see the list of books in German and English, respectively.
Detecting default browser language
An even better idea is to serve website content in a user's preferred language by default instead of making users change the language to the one they like best. To achieve this goal, we need only change the default application locale to the user's default browser locale. The resultingapplication_controller.rb
is given below:class ApplicationController < ActionController::Base protect_from_forgery with: :exception before_action :set_locale private def default_url_options {locale: params[:locale]} end def preferred_locale request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first if request.env['HTTP_ACCEPT_LANGUAGE'].present? end def set_locale new_locale = params[:locale] || preferred_locale I18n.locale = if I18n.locale_available? new_locale new_locale else I18n.default_locale end end end
To test whether this worked, change your default browser language from English to German or vice versa, restart the browser and navigate to
http://localhost:3000/books
. You should now see content in two different languages. Note that we've also added a check that verifies that the preferred user language is indeed among the languages available in the application. If that is not the case, we fall back to the default locale.Setting page language in HTML
Search engines and web crawlers need to know which language is used by a particular page of your website. To do that simply make sure that thelang
attribute on the <html>
tag is set to change dynamically:
<html lang="<%= I18n.locale %>">
Creating buttons that change the locale
The only thing left to do is create links that change the language of a website. Fortunately, that is the easiest part.<%= link_to 'Deutsch', params.permit(:locale).merge(locale: :de) %> <%= link_to 'English', params.permit(:locale).merge(locale: :en) %>
These links can be placed on any page of the website (and should probably be present on all pages). They will redirect users to the same page that they are currently on, but the language will be changed.
Conclusion
You are now well on your way to creating user-friendly locale-based URLs that are generic and easily maintainable. The only thing to remember is that when adding new languages it is necessary to add the new locale in theapplication.rb
and restart the application. Everything else should work as is.Happy internationalizing!