As your business scales, it may reach the international level. In order to cater to the needs of a global audience and to provide your customers with user-friendly service, your Rails app will need to support multiple languages, currencies, and date and time formats.
App internationalization is an old practice among developers, and there are different ways to deal with it. Working with clients whose businesses are sometimes international, we often deal with translating web apps and websites into different languages. In this article, we share our experience working with app internationalization.
Types of web content
Typically, a web app consists of static and dynamic content. Static content is a content delivered to users without having to be modified or generated, meaning it doesn’t depend on user input or preferences. Static web pages have fixed code and their content doesn’t change unless it’s manually edited by a web developer. A great example of a static web page is any of our blog post pages, where the content (the article text and all media) remains the same until we manually change it.
Dynamic content (also known as adaptive content), in contrast, is web content that’s generated at the moment a user requests a web page and on the basis of their interactions with a web app as well as their preferences. Dynamic content is adapted based on data such as location and device type to deliver the most relevant and satisfying user experience. The brightest example of dynamic content is Uber’s blog. It decides which articles to show based on a user’s location.
Knowing what types of content exist will help us to better understand how internationalization and localization are applied. Now let’s get down to business and try to translate your web app.
Localizing static content
For localizing static content, we’ll use the built-in Ruby I18n gem, which is a standard framework for translating content that’s supported in Ruby on Rails 2.2 and higher and used both to translate web apps into different languages, in order to extend cultural or linguistic market.
Localizing a Rails app with the Ruby I18n gem means defining translated values for static strings in the Rails framework in different languages.
Step 1. Set default configurations
This step involves setting a default locale and translation load paths as well as defining available locales in config/initializers/locales.rb
.
The default locale refers to the locale that’s set automatically unless a different locale is detected. English is the default locale in the l18n gem.
In the following example, we set German as the default locale and enumerate available locales to make the web app available in German, English, and Portuguese. We also define the translation load path, which is the path to the folder where all locales are stored.
# Where the I18n library should search for translation files
I18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]
# Set default locale to something other than :en
I18n.default_locale = :de
# Whitelist locales available for the application
I18n.available_locales = [:de, :en, :pt]
Step 2. Determining locales
Now let’s move directly to determining a locale. You probably want your app to support multiple locales – such as:
de
,
:
en
,
and:
pt
– to reach different regions.
For this, you should set the locale at the beginning of each direct request to your web app or website so that all strings are translated using the desired locale during the lifetime of that request. In plain English, doing this will make your app show your users content in the language associated with the locale they’ve chosen.
You can choose between the following approaches for setting locales:
1. Set a locale from the domain name. Here you can set the locale from a top-level domain name (https://application.de
), domain name (https://german-domain.de
), or subdomain name (https://de.application.com
).
Using a top-level domain name for setting a locale has several benefits. Following this approach, you’ll get a URL for your web app that looks like this:
https://application.de
https://application.pt
https://application.co.uk
The locale is an obvious component of the web app’s URL. Moreover, it’s better for SEO when content in different languages resides in separate but linked domains. This approach involves writing the following lines of code in ApplicationController
:
class ApplicationController < ActionController::Base
before_action :set_locale
private
def set_locale
I18n.locale = extract_locale_from_tld || I18n.default_locale
end
def extract_locale_from_tld
parsed_locale = request.host.split('.').last
I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end
end
Setting the locale from the domain name also improves SEO. In addition, this option enables you to directly translate the hostname to the user’s native language. By using this approach, you’ll get URLs like this:
https://german-domain.net
https://english-domain.co.uk
https://portuguese-domain.net
To do this, first store application hosts in the config/application.rb
file:
module ApplicationName
class Application < Rails::Application
config.hosts = {
pt: ENV.fetch('HOST_PT', 'portuguese-domain.net'),
de: ENV.fetch('HOST_DE', 'german-domain.de'),
en: ENV.fetch('HOST_EN', 'english-domain.co.uk')
}
end
end
Then, set the locale according to the hostname in ApplicationController
:
class ApplicationController < ActionController::Base
before_action :set_locale_from_domain
private
def set_locale_from_domain
detected_locale = Rails.application.config.hosts.key(request.host)
I18n.locale = detected_locale.present? ? detected_locale : I18n.default_locale
end
end
The locale is set in a similar manner when based on a subdomain name:
class ApplicationController < ActionController::Base
before_action :set_locale_from_subdomain
private
def set_locale_from_subdomain
detected_locale = request.subdomains.first
I18n.locale = I18n.available_locales.map(&:to_s).include?(detected_locale) ? detected_locale : I18n.default_locale
end
end
As a result, you’ll get URLs like this:
https://pt.application.com
https://de.application.com
https://en.application.com
2. Set a locale using URL parameters
When setting a locale from URL parameters, the path to your service will look something like this:
https://example.com/cats?locale=pt
Though setting a locale using URL parameters has the same benefits as setting a locale from a website domain name (it’s RESTful and complies with the rest of the internet), it requires more effort. First, add the following code to ApplicationController
:
class ApplicationController < ActionController::Base
before_action :set_locale
private
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
end
Here’s where the tricky part begins. The chosen locale must persist across the requests. How we can deal with this?
This problem can be solved by adding link_to root_url(locale: I18n.locale)
after each request. This would be inconvenient, however, so you need to find another solution. As an alternative, you can add the default_url_options
method, which sets default parameters for the url_for
method and other methods that rely on it:
def default_url_options
{ locale: I18n.locale }
end
Thanks to this method, route helpers will automatically include the ?locale
part to your web app’s URL.
3. Store a locale in the path
When storing a locale in the path, the URL of your app will look something like this:
https://www.application.com/de/cats (uploads the German locale)
https://www.application.com/fr/cats (uploads the French locale)
For this, just add a few lines of code into config/routes.rb
:
scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do
resources :cats
end
Step 3. Enabling automatic locale detection
There are two possible ways to get a locale implicitly.
- Set a locale based on the user’s location
You can easily perform user’s locale detection from geolocation with geocoder gem. After that, the app will set the language that’s defined for this area. Geocoder adds location and safe_location
methods to the standard Rack:
:Request
object so you can easily look up the location of any HTTP request by IP address.
# returns Geocoder::Result object
result = request.location
We don’t recommend relying on this approach, though, since it’s considered quite inaccurate. For instance, suppose a user from Spain goes on vacation to Germany. When they enter your website while on vacation, they probably want to see it in Spanish and not in German, since they may not know German.
- Get required data from the HTTP Accept-Language header
Part of an HTTP request, the HTTP Accept-Language
header contains data about a user’s browser language preferences. This information can be sent to the app server so that it can determine the locale. Let’s get back to the example with a tourist from Spain. Your website can easily display the Spanish version to this tourist while they’re in Germany by relying on their browser language preferences (assuming they’re set to Spanish).
If you want your app to automatically detect a locale using information from the Accept-Language
header, use this code:
def browser_locale(request)
locales = request.env['HTTP_ACCEPT_LANGUAGE'] || ""
locales.scan(/[a-z]{2}(?=;)/).find do |locale|
I18n.available_locales.include?(locale.to_sym)
end
end
Step 4. Organizing translation files
All translations of static strings can be stored in YAML (.yml) or plain Ruby (.rb) files. Commonly, Ruby developers use YAML for this purpose. But this option has one huge drawback: *.yml
files are sensitive to white space and special characters. Pay close attention to them or you’ll face problems with loading dictionaries.
Yalantis has also prepared some useful tips on how to localize an app.
Tip #1. Store each locale in a separate file. For instance, here we have two separate files with English and German translations stored in config/locales
.
# config/locales/en.yml
en:
help: ‘Help’
# config/locales/de.yml
de:
help: ‘Hilfe’
To quickly find translations, it’s better to split your static string content into different files stored in separate folders. For instance:
|-defaults
|---de.rb
|---en.rb
|-models
|---book
|-----es.rb
|-----en.rb
|-views
|---defaults
|-----es.rb
|-----en.rb
|---books
|-----es.rb
|-----en.rb
|---users
|-----es.rb
|-----en.rb
|---navigation
|-----es.rb
|-----en.rb
Tip #2. After you’ve finished organizing files, look inside those files and put translation keys in order. First and foremost, we recommend giving clear names to your translation keys so it’s easy to understand their purpose. For example:
passwords:
edit:
change_my_password: Change my password
Tip #3. Don’t store all translation keys at the same level of nesting. This is a bad solution since similar keys will get messed up. For instance, you’d better not write code like this:
en:
submit: Submit
log_out: Log Out
blog: Blog
errors: Errors were found:
Instead, it’s good solution to organize information in the following way:
en:
forms:
submit: Submit
errors: Errors were found
main_menu:
log_out: Log Out
blog: Blog
Tip #4. Use variables to interpolate keys with values. For example, to allow notifications_count
to accept a variable, use the following approach:
inbox:
zero: 'no notifications'
one: 'one notification'
few: "%{count} notifications"
many: "%{count} notifications"
other: '%{count} notifications'
And use it in the following way:
t(:inbox, count: 2) # => '2 notifications'
t(:inbox, count: 1) # => 'one notification'
t(:inbox, count: 0) # => 'no notifications'
Tip #5. Mind that the time format is different in different regions. Some regions use a 24-hour clock instead of 12-hour. The same situation with date format. For instance, DD/MM/YYYY format is used in most Asian countries, Russia, and some European countries, while in the US the MM/DD/YYYY format is used.
en:
date:
formats:
default: "%Y-%m-%d"
short: "%b %d"
long: "%B %d, %Y"
time:
formats:
default: "%H:%M:%S"
short: "%H:%M"
long: "%H:%M:%S, %d %B"
To localize a date or time, use the localize
method (aliased as l):
l(post.created_at, format: :long)
Tip #6. Use lazy lookups — short versions of commands that let you write less code inside controllers and views.
For instance, if you’re translating the app/views/cats/show.html.erb
view, there’s no need to write the full path to the translation key. Instead, you can write:
<%= t('cats.show.title') %>
# Change to
<%= t(:title) %>
Before using lazy lookups, however, make sure you store translation keys with the following nesting:
en:
cats:
show:
title: Cats show page title
Tip #7. All static strings should be translated. So don’t forget to translate enums and model attributes:
en:
activerecord:
attributes:
user:
first_name: First Name
gender:
female: Female
male: Male
as well as error messages:
activerecord.errors.models.[model_name].attributes.[attribute_name].[error_type]
activerecord.errors.models.[model_name].[error_type]
activerecord.errors.messages.[error_type]
errors.attributes.[attribute_name].[error_type]
errors.messages.[error_type]
Tip #8. Bear in mind that machine translation may have imperfections, which is why it’s sometimes better to work with experts who can localize your web app. You can use third-party services like PhraseApp, Translation.io, LocaleApp, and Translation Exchange to have native speakers translate your static strings.
Localizing dynamic content
Localizing of dynamic content is important since it helps you deliver a personalized user experience. The Globalize gem is a perfect solution for localizing dynamic content. You can find detailed documentation of this gem in the Github repository. There are a few key points from this document to be pointed out.
Yalantis has experience localizing a project with dynamic content, so let’s use this example to show the localization process.
Our task was to localize the content on a travel platform. In the admin panel, a publisher can choose and populate input fields in English, French, or German. Depending on the locale, the web app shows content in English, French, or German. We used the Article model with a Title field, and publishers need to fill in this title so that the app can display the relevant title on each version of the website.
The localization process included the following steps:
1. Add the ‘globalize’ gem line to the Gemfile and run bundle install:
gem 'globalize', '~> 5.0.0'
2. Enumerate the attributes to be translated:
class Product < ActiveRecord::Base
translates :name, :description, fallbacks_for_empty_translations: true
end
3. Add migration to TranslateProducts
to generate the translation tables that will store product translations:
class TranslateProducts < ActiveRecord::Migration[5.0]
def self.up
Product.create_translation_table!
end
def self.down
Product.drop_translation_table!
end
end
After that, we get access to translated attributes:
I18n.locale = :en
product.name # => My product name
I18n.locale = :fr
product.name # => Mon nom de produit
Translating routes
If you have a web app with multi-language support, you may want to translate routes as well. What does that mean?
Imagine you have a web app in English and German with a Help page. Without translated routes, the URLs will look like this:
english-version.com/help
german-version.de/help
But after we translate routes, we’ll get translated pages:
english-domain.com/help
german-domain.de/hilfe
First, if you want a single Rails app to serve multiple domains in different languages, you need to configure NGINX like this:
server {
listen 80;
server_name www.french-domain.com www.germany-domain.com;
root /webapps/mycook/public;
}
You can change URLs to display whatever language is requested using the route_translator gem. This gem allows you to manage translations of your app routes with a simple dictionary format. With routes_translator, you can configure localized hosts, locale detection, route namespaces, and so on. The gem has well-written official guides and documentation. We’ll consider only the initial steps of localizing routes:
1. Add the gem to your Gemfile and run bundle install:
gem 'route_translator'
2. Put the groups of routes to be translated inside a localized block:
Rails.application.routes.draw do
namespace :admin do
resources :cats
end
localized do
resources :cats
end
end
3. Add translations to the locale files (en.yml, fr.yml):
en:
routes:
cats: cats
new: new
fr:
routes:
cats: chats
new: nouveau
Voila, you have localized routes!
cats_fr GET /fr/chats(.:format) cats#index {:locale=>"fr"}
cats_en GET /cats(.:format) cats#index {:locale=>"en"}
If your routes have slugs like /products/259-green-shoe
, you might want them to be converted to slugs such as /produits/259-chaussures-
vertes
. For this purpose, use friendly_id-globalize. This gem lets you use Globalize to translate slugs in your models. Using this gem is as easy as pie.
1. Add the gem to the Gemfile:
gem friendly_id-globalize
2. Run a command from the terminal in the project directory to generate migration:
rails generate friendly_id_globalize
3. List slugs that need to be localized:
class Product < ActiveRecord::Base
translates :name, :description, :slug
extend FriendlyId
friendly_id :name, use: :globalize
end
Now you can find models by slug!
I18n.locale = :fr
Product.find("259-chaussures-vertes")
I18n.locale = :en
Product.find("259-green-shoe")
The bottom line
As you can see, there are technical solutions that can help you take your project global and localize its static and dynamic content in a few simple steps. In this article, we’ve overviewed the process of localizing web applications built with Ruby on Rails using native tools offered by the platform and applying open-source libraries provided by the Rails community. If you have any questions or want to create an app supporting multiple languages, get in touch and we’ll find the solution together.
Ten articles before and after
Testing Web Software: Unit Testing Methods
Real-Time Features: Best Use Cases and Reason To Implement Them In Your App
Golang and Node.js Comparison: Scalability, Performance, and Tools
How to Load Test an API to Ensure Your App Works Smoothly
Which Payment Gateway Integration to Choose for Your App
When and Why Angular Is a Good Technical Solution for Your Project
API Versioning: Which Gem to Pick for API Versioning on Ruby on Rails
What You Can Create with FFmpeg Filters and Frei0r Plugin Effects