homeASCIIcasts

338: Globalize3 

(view original Railscast)

Below is a page from a blogging application that shows a number of Article records. This application supports internationalization and allows the user to change the language that’s displayed via links at the top of the page.

Our blogging application with the localizable text displayed in Wookieespeak.

Clicking the “Wookieespeak” link changes the title at the top of the page but each article’s content is still displayed in English. How do we change the content from the database so that’s it’s displayed in the user’s preferred language?

Internationalization in Rails applications is usually done with YAML files. Under the /config/locales directory are one or more files that contain the translated texts for each language that the application supports. This approach was covered in detail in episode 138 and while it works well for static text it won’t help us display text from the database in different languages. To do this we need to store each translation in a database table and the Globalize3 gem makes doing this much easier. Globalize3 creates a separate table to store translations for each model that we use it with and it will switch to the proper translation depending on the user’s selected locale. To use this gem in our application we’ll need to add it to the gemfile and then run bundle to install it.

/Gemfile

gem 'globalize3'

Now we can go to the model we want to translate and use the translates method to specify the names of the columns we want to be available in multiple languages, in our case the name and content columns in Article.

/app/models/article.rb

class Article < ActiveRecord::Base
  translates :name, :content
end

We need to create a database table to store the translations. The gem’s README file says that we can use create_translation_table! inside the same migration where we create the table for the model but as we already have a articles table with data in it we’ll use an alternative approach that uses a separate migration and which allows us to migrate the existing data. First we’ll create a migration called create_article_translations in our application.

$ rails g migration create_article_translations

We’ll modify this migration based on what the README file tells us. We need to call create_translations_table! on the model we want to add translations to and pass in the names of the columns that we want to be translatable.

/db/migrations/291204100000_create_article_translations.rb

class CreateArticleTranslations < ActiveRecord::Migration
  def up
    Article.create_translation_table!({
      name: :string,
      content: :text
    }, {
      migrate_data: true
    })
  end
  def down
    Article.drop_translation_table! migrate_data: true
  end
end

The data from these two columns will now be stored not in the articles table but instead in a new article_translations table. We’ll need to migrate the database for these changes to take effect.

$ rake db:migrate

We can demonstrate how this works in the Rails console. If we check the current locale we’ll see that it’s at its default of English. Getting the first article’s name will fetch it from our new article_translations table.

1.9.3-p125 :001 > I18n.locale
 => :en 
1.9.3-p125 :002 > Article.first.name
  Article Load (0.2ms)  SELECT "articles".* FROM "articles" LIMIT 1
  Article::Translation Load (0.2ms)  SELECT "article_translations".* FROM "article_translations" WHERE "article_translations"."article_id" = 1
 => "Superman"

If we change the locale to Wookieespeak and try to fetch the first article’s name we’ll get nil as the translated text doesn’t exist.

1.9.3-p125 :003 > I18n.locale = :wk
 => :wk 
1.9.3-p125 :004 > Article.first.name
  Article Load (0.3ms)  SELECT "articles".* FROM "articles" LIMIT 1
  Article::Translation Load (0.2ms)  SELECT "article_translations".* FROM "article_translations" WHERE "article_translations"."article_id" = 1
  Article::Translation Load (0.3ms)  SELECT "article_translations".* FROM "article_translations" WHERE "article_translations"."article_id" = 1 AND "article_translations"."locale" = 'wk' LIMIT 1
 => nil

Globalize3 overrides both the getter and setter behaviour for the columns and scopes it to the current language. If we update the first article’s name while our locale is set to wk it will insert a record into the article_translations table for that locale and when we fetch the name again we’ll see the value we entered.

1.9.3-p125 :005 > Article.first.update_attribute(:name, "Ahhyya")
1.9.3-p125 :006 > Article.first.name
  Article Load (0.3ms)  SELECT "articles".* FROM "articles" LIMIT 1
  Article::Translation Load (0.2ms)  SELECT "article_translations".* FROM "article_translations" WHERE "article_translations"."article_id" = 1
 => "Ahhyya"

Changing the locale back to English will show the English name again.

1.9.3-p125 :007 > I18n.locale = :en
 => :en 
1.9.3-p125 :008 > Article.first.name
  Article Load (0.2ms)  SELECT "articles".* FROM "articles" LIMIT 1
  Article::Translation Load (0.2ms)  SELECT "article_translations".* FROM "article_translations" WHERE "article_translations"."article_id" = 1
 => "Superman"

Now we can see this in action on our site. We’ll need to restart the server for the changes to be picked up but after we do the English version of the page should look the same as that data has been migrated over. If we look the page in Wookieespeak, though, the page will be almost blank.

The titles and descriptions for most of the article are missing as translations for them don’t exist.

The first article here has the name that we set in the console but all the other attributes that we set up to be translatable will be blank. We can, however, now edit one of these articles and set the translated text.

Articles can now be edited in both supported languages.

This article is now available in two languages and when we switch between them we’ll see it displayed in the selected language. If we go back to the edit form the text fields will show the text for the selected language and we can click the links and swap between them to update the article in either language. If we change an attribute which isn’t set up for translation such as the author name it will be updated for all all languages as its value will still be stored in the articles table, not the article_translations table.

Using a Fallback Language

Translating all the database records into both languages can be quite a task. As we saw before nil values will be returned for those attributes that haven’t been translated into the current language but we can provide a fallback language that will be shown when a translation doesn’t exist in the current language. To do this we just need to modify our application’s config file and add an i18n.fallbacks option.

/config/application.rb

config.i18n.fallbacks = true

With this in place any translations that aren’t specified in the current language will fall back to the default locale, in this case English. We’ll need to restart our application for this change to be picked up but when we reload the page the translations that are missing in Wookieespeak are shown in English instead.

Articles without a Wookieespeak translation now fallback to being displayed in English.