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.
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 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.
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.