How to Develop a Multi-Language Ruby on Rails Application and Take It Global

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.

Article model

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

How to Use the Android NDK When Writing an App in Kotlin

Integrating SiriKit in a Third-Party iOS App