About this entry
You’re currently reading the article “Model Translation: Globalizing a Ruby on Rails Application by Adding Multilingual Support to the Models through Globalize's External Table or the Internal Storage Mechanism.”
- Published:
- December 17th 08:10 PM
- Updated:
- August 17th 07:53 PM
- Sections:
- Ruby on Rails
Model Translation: Globalizing a Ruby on Rails Application by Adding Multilingual Support to the Models through Globalize's External Table or the Internal Storage Mechanism
Using the globalize plug-in for Ruby on Rails out-of-the-box makes it easy to add support for various languages. It can handle the management of translated terms and translation of models (displaying the content coming from the database in the selected language). This article gives a brief introduction about to how to implement model translation with globalize in an actual application.
Liked it? !
Index
- Everybody on the Same Page
- How Model Translations Work
- Globalize's External Table
- Views that Make Model Translations Clear
- Trying it Out
- Internal Storage Mechanism
- Coming Soon
Everybody on the Same Page
In a previous article I explained how to add multilingual support to the views of our basic RESTful social_graph application. Here I will extend the multilingual support to include model translations. Please give it a quick glimpse before continuing with these articles, if you haven't done so already.
How Model Translations Work
Model translations means translating the specific fields of of the tables within the database into the appropriate language. There are two approaches: 1) to have the translated terms stored in Globalize's external table, it being the same table where globalize stores the view translations, and 2) the internal storage mechanism where translated terms are stored in their respective tables.
Globalize's External Table
Without doubt this is the easiest approach. Unfortunately, the current version as of the writing of this article (revision 181) is not yet compatible with Rails 2.0. Following Artem Vasiliev suggestion, replace the attributes_with_quotes method in social_graph\vendor\plugins\globalize\lib\globalize\localization\db_translate.rb to:
def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true)
if Locale.base?
quoted = attributes.inject({}) do |quoted, (name, value)|
if column = column_for_attribute(name)
quoted[name] = quote_value(value, column) unless !include_primary_key && column.primary
end
quoted
end
else
quoted = attributes.inject({}) do |quoted, (name, value)|
if !self.class.globalize_facets_hash.has_key?(name) && column = column_for_attribute(name)
quoted[name] = quote_value(value, column) unless !include_primary_key && column.primary
end
quoted
end
end
include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
end
To avoid the "undefined method `determine_deprecated_finder'" error replace also in social_graph\vendor\plugins\globalize\lib\globalize\localization\db_translate.rb the method called method_missing with:
def method_missing(method_id, *arguments)
if match = /find_(all_by|by)_([_a-zA-Z]\w*)/.match(method_id.to_s)
finder = determine_finder(match)
facets = extract_attribute_names_from_match(match)
super unless all_attributes_exists?(facets)
#Overrride facets to use appropriate attribute name for current locale
facets.collect! {|attr_name| respond_to?(:globalize_facets) && globalize_facets.include?(attr_name.intern) ? localized_facet(attr_name) : attr_name}
attributes = construct_attributes_from_arguments(facets, arguments)
case extra_options = arguments[facets.size]
when nil
options = { :conditions => attributes }
set_readonly_option!(options)
ActiveSupport::Deprecation.silence { send(finder, options) }
when Hash
finder_options = extra_options.merge(:conditions => attributes)
validate_find_options(finder_options)
set_readonly_option!(finder_options)
if extra_options[:conditions]
with_scope(:find => { :conditions => extra_options[:conditions] }) do
ActiveSupport::Deprecation.silence { send(finder, finder_options) }
end
else
ActiveSupport::Deprecation.silence { send(finder, finder_options) }
end
end
elsif match = /find_or_(initialize|create)_by_([_a-zA-Z]\w*)/.match(method_id.to_s)
instantiator = determine_instantiator(match)
facets = extract_attribute_names_from_match(match)
super unless all_attributes_exists?(facets)
attributes = construct_attributes_from_arguments(facets, arguments)
options = { :conditions => attributes }
set_readonly_option!(options)
find_initial(options) || send(instantiator, attributes)
else
super
end
end
Suppose we want to translate the title attribute within the relations table. You simply need to make a call to the translates method specifying which attributes you want to have translated. The social_graph/app/models/relation.rb will look like this:
class Relation < ActiveRecord::Base
translates :title, :base_as_default => true
has_many :acquaintances
end
Yes! It's that simple. If you are in English, the title of the relation will draw from the English, if you are in Spanish, the title of the relation will draw from the Spanish (if it is not present, it will use the English since we set :base_as_default to true).
So do the following experiment to get a taste of how it works before continuing. Open you browser here:
http://amontano-laptop:3000/relations/1/edit
Now switch to Spanish clicking on the Español link in the upper left corner. It will look the same and the title still displays "Friend" in English. Now change it in the form to "Amigo" (without the quotes) and update it. Now click on the Back link to return to the relations/index view. If you are in Spanish mode it will display "Amigo", if you are in English mode it will display "Friend" in the table. Notice how transparent it is since we are invoking the same title attribute. A small caveat is that the relation should pre-exist in English before adding the Spanish.
Views that Make Model Translations Clear
This kind of edit works, but it is pretty confusing! We should add the following two changes: the New Relation link should only be available in the base language and the edit view should be clearer on what we are exactly editing.
So my suggestion is as follows. When the base language is selected (English in this case), then the edit view should allow you to edit all your fields in the base language (translated and untranslated). If a language other than the base language is selected, then the translated fields should first be displayed in English and then within a form with a clear "Save translation" message for the submit button. Fortunately it is easy to evaluate the if the selected language is the base language using the Globalize::Locale.base? method and it is easy the switch languages using the Globalize::Locale.switch_locale method. Just remember to reload the variable within the switch_locale block and afterwards, to that the proper language is reloaded. social_graph/app/views/relations/edit.rhtml will look like this:
<h1><%= 'Editing relation'.t %></h1>
<%= error_messages_for :relation %>
<% if Globalize::Locale.base?
form_for(:relation, :url => relation_path(@relation), :html => { :method => :put }) do |f| %>
<p>
<b><%= 'Title'.t %></b><br />
<%= f.text_field :title %>
</p>
<p>
<b><%= 'Description'.t %></b><br />
<%= f.text_area :description %>
</p>
<p>
<%= submit_tag 'Update'.t %>
</p>
<% end
else %>
<h2><%= Globalize::Locale.base_language.native_name %></h2>
<% Globalize::Locale.switch_locale(LANGUAGES[Globalize::Locale.base_language.code][:locale]) do
@relation.reload %>
<p>
<b><%= 'Title'.t %></b><br/>
<%= @relation.title %>
</p>
<% end
@relation.reload %>
<h2><%= Globalize::Locale.language.native_name %></h2>
<% form_for(:relation, :url => relation_path(@relation), :html => { :method => :put }) do |f| %>
<p>
<b><%= 'Title'.t %></b><br />
<%= f.text_field :title %>
</p>
<p>
<%= submit_tag 'Save translation'.t %>
</p>
<% end
end %>
<%= link_to 'Show'.t, relation_path(@relation) %> |
<%= link_to 'Back'.t, relations_path %>
A similar Locale.base? can be made to hide the link to a new relation when a language other than the base language is selected and to display edit or translate depending on the language. social_graph/app/views/relations/index.rhtml will look like this:
<h1><%= 'Listing relations'.t %></h1>
<table>
<tr>
<th><%= 'Title'.t %></th>
<th><%= 'Description'.t %></th>
</tr>
<% for relation in @relations %>
<tr>
<td><%=h relation.title %></td>
<td><%=h relation.description %></td>
<td><%= link_to 'Show'.t, relation_path(relation) %></td>
<td><%= link_to Globalize::Locale.base? ? 'Edit'.t : 'Translate'.t, edit_relation_path(relation) %></td>
<td><%= link_to 'Destroy'.t, relation_path(relation), :confirm => 'Are you sure?', :method => :delete %></td>
</tr>
<% end %>
</table>
<br />
<% if Globalize::Locale.base? %>
<%= link_to 'New relation', new_relation_path %>
<% end %>
Trying it Out
Open your browser here:
http://amontano-laptop:3000/relations
Switch the language to Spanish and click on translate for each relation to add each translation. Afterwards you might want to visit:
http://amontano-laptop:3000/translations
to add the new messages for the view translations that were used in the relations/edit and relations/index.
Now go back to:
http://amontano-laptop:3000/acquaintances/new
and you will notice that the drop-down list for relations is already displaying the proper translations.
Internal Storage Mechanism
Storing the model translations in Globalize's external table works great for simple sql queries, but more complex ones will fail (for instance if :includes is used). A more flexible alternative that will compartmentalize better the translations for each table is the internal storage mechanism, where translations are stored in an extra field within the same table. The name of the field containing the translation will start with the original field's name followed by an underscore and the language's code. So in our sample, we would add the extra field with the following command-line call to create the migration:
ruby script/generate migration add_title_es_to_relation title_es:string
Notice that I am following the naming conventions for the new "sexy migrations" in Rails 2.0. If you open social_graph/db/migrate/004_add_title_es_to_relation.rb, you will notice that it will do exactly what we want: add a column called title_es to the relations table. Cool, isn't it!
Now run the migration:
rake db:migrate
Now we change we have to specify in the model that we are using the internal storage mechanism by adding self.keep_translations_in_model = true to social_graph\app\models\relation.rb:
class Relation < ActiveRecord::Base
self.keep_translations_in_model = true
translates :title, :base_as_default => true
has_many :acquaintances
end
Follow the above steps to re-populate the translations for the relation model, and you are all set! The translation for title is automatically stored in title_es being both saved and loaded in a completely transparent manner.
Coming Soon
- Support for languages using complex scripts, such as Chinese, Tibetan, and Dzongkha. Globalize is great, but it falls short when the languages you want to support require specific fonts and font sizes or use Unicode ranges that may not be interpreted correctly by the browser. In this article I will show how to extend globalize to easily support these special cases.

4 comments
Jump to comment form | comments rss [?]