← Home

Globalize's advanced features - Get on Rails with Globalize! Part 3 of 8

Abstracting ViewTranslations: the way of the slashes.

As soon as you're starting to do some interactive stuff - let's say you're going to call user Bob by his name: "Welcome back, Bob, please check your mailbox!" - you'll probably very soon get annoyed of ... code like this:

<%= "Welcome back, ".t + name +
  ", please check your mailbox!".t %>.

Ugly, hm? Moreover there probably soon will arise problems with different word orders in different languages, like when "Welcome back, ".t + name would probably need to be name + ", welcome back".t in another language for some grammatical reason. As soon as you have more than just a few strings and languages to manage this kind of stuff can get really nasty.

Globalize offers a pretty nice solution here: ViewTranslations (i.e. strings that are translated through the method .t) may be used as a "format" string just as you know them from sprintf. The allowed specifiers are %s for strings and %d for numbers.

So you could do:

<%= "Welcome back, %s, please check your mail!".t(nil, name) %>

... instead of that monstrous concatenation above.

But better yet, Globalize overloads the / operator for Strings and allows you to this:

<%= "Welcome back, %s, please check your mail!" / name %>

Quite nice, isn't it?

There's only one argument supported currently. That means you can not do: "Welcome, %s! You have %d unread messages." / [name, count].

That looks quite like a limitation, doesn't it? Well, if you really need this feature (and after you've read the following section and know what you're doing), you'll probably want to look at the "multiple arguments to fetch" monkey-patch on the Globalize wiki (scroll down to the bottom of the page). This will add the ability to hand more than one value over to your ViewTranslations.

Singular and (multiple) plural ViewTranslations

Why would we want multiple plural forms for a ViewTranslation? Well, as the sharp reader you are you surely noticed that flaw above where "You have %d unread messages" would result in bad English when Bob has exactly 1 message (not "messages") in his inbox.

Depending on the value of count we'd need three different strings:

"You have [0] unread messages."
"You have [1] unread message."
"You have [2..n] unread messages."

For many languages these three cases are sufficient, though in French you'd use the same form for the first case (zero messages) like for the second (1 message). But there are languages that are even more complex:

"In many languages, including a number of Indo-European languages, there is also a dual number (used for indicating two objects). Some other grammatical numbers present in various languages include nullar (for no objects), trial (for three objects) and paucal (for a few objects). In languages with dual, trial, or paucal numbers, plural refers to numbers higher than those (i.e. more than two, more than three, or many). [...] Languages having only a singular and plural form may still differ in their treatment of zero. For example, in English, German, Dutch, Italian, Spanish and European Portuguese, the plural form is used for zero or more than one, and the singular for one thing only. By contrast, in French and Brazilian Portuguese, the singular form is used for zero. Some languages, such as Latvian, have a special form (the nullar) for zero."
http://en.wikipedia.org/wiki/Plural

To make this (virtually very) long story short, Globalize is here to help you with this stuff and allows you to specify zero, singular and (one or many) plural translations - just as required by the target language. You can specify these translations like this:

Locale.set_translation(key, [singular, plural_1, ... plural_n],
  zero)

For example: In Slovenian you'll find the following declension for the word "mesto" which means "city":

singular: mesto
dual: mesti
paucal (3-4): mesta
plural (5-10): mest

Now, let's see how Globalize copes with this.

>> Locale.set('sl-SI')
>> Locale.set_translation('%d city', ['%d mesto', '%d mesti',
  '%d mesta', '%d mest'], '%d mest')
>> (0..5).each { |i| puts "%d city" / i }
0 mest
1 mesto
2 mesti
3 mesta
4 mesta
5 mest

And that's the correct result. Globalize recognizes the Slovenian dual and its two plural cases.

Actually, when you look at the globalize_language table for the Slovenian entry you'll see that Slovenian plurals are even more fun :-). The pluralization field of that row holds the expression:

c % 10 == 1 && c % 100 != 11 ? 1 : c % 10 >= 2 && c % 10 <= 4 &&
  (c % 100 < 10 || c % 100 >= 20) ? 2 : 3

... which will be evaluated to select the correct plural form.

Another example for complex plural forms is Polish (which can be found in the GNU Gettext Manual):

"In Polish we use e.g. plik (file) this way:
1 plik
2,3,4 pliki
5-21 plikòw
22-24 pliki
25-31 plikòw
and so on."

We can translate this to Globalize like this:

>> Locale.set 'pl-PL'
>> Locale.set_translation('%d file', ['%d plik', '%d pliki',
   '%d plików'], '%d plikòw')
>> [0,1,2,3,5,21,22,25].each { |i| puts "%d file" / i }
0 plikòw
1 plik
2 pliki
3 pliki
5 plików
21 plików
22 pliki
25 plików

Again, Globalize already knows about the unusual distribution of the two different plurals: in this case this is the expression:

c == 1 ? 1 : c % 10 >= 2 && c % 10 <= 4 && (c % 100 <10 ||
  c % 100 >= 20) ? 2 : 3

What happens here under the hood is that each Language comes with a pluralization expression like the one above (though, obviously, for most languages a less complex formula is sufficient).

This expression yields to an index i for any given number n that's provided through the "string" / n syntax. The index i refers to the set of plural forms that you provide when you set a translation through Locale.set_translation(key, [singular, plural_1, ... plural_n], zero). 0 will refer to the zero form, 1 to the singular form, 2 to plural_1 and so on.

Globalize's Currency class

Globalize comes with a dedicated Currency class that you can "use for representing money values in your ActiveRecord models. It stores values as integers internally and in the database, to safeguard precision and rounding. More importantly for globalization freaks, it prints out the amount correctly in the current locale, via the handy format method. Try it!" (from the Globalize api docs on Currency).

Let's say we need to define a simple Product class. We can then use Globalize's Currency class to delegate the handling of the price.

class Product <ActiveRecord::Base
  composed_of :price, :class_name => "Globalize::Currency",
    :mapping => [ %w(price cents) ]
end

Now we can create a product:

p = Product.new
p.price = Currency.new(12345678)
p.price.to_s
# "12,345.78"

... and use the Currency delegate to display the price in a localized format. In Germany currency value will be formatted like this:

Locale.set("de-DE")
p.price.to_s
# "12.345,78 ?"
p.price.format(:code => true)
# "12.345,78 EUR"

While in Swiss you'd use:

Locale.set("de-CH")
p.price.to_s
# "SFr. 123'456.78"
p.price.format :code => true
# "123'456.78 CHF"

Again, this formatting information comes from the database tables that Globalize comes with - this time it's defined in the globalize_countries table.

FYI: Like similar classes that essentially represent immutable and exchangeable values Globalize::Currency implements the ValueObject pattern:

"Examples of value objects are things like numbers, dates, monies and strings. Usually, they are small objects which are used quite widely. Their identity is based on their state rather than on their object identity. This way, you can have multiple copies of the same conceptual value object. So I can have multiple copies of an object that represents the date 16 Jan 1998. Any of these copies will be equal to each other. For a small object such as this, it is often easier to create new ones and move them around rather than rely on a single object to represent the date."

Piggybacking translations of associated models

When you have your models associated with other models you'll often want to save some performance by eagerly loading them in a single database call instead of one per object.

Hence, "there's a piggyback feature for associations. So, Product.find(:all, :include_translated => :manufacturer) is one DB call, but gives you product.manufacturer_name in your current language." (from the Globalize wiki)

In the api docs they tell us: ":include_translated works as follows: any model specified in the :include_translated option will be eagerly loaded and added to the current model as attributes, prefixed with the name of the associated model. This is often referred to as 'piggybacking'."

The api docs example uses models like the following ones. Let's assume that you've already set them up correctly:

class Product <ActiveRecord::Base
  belongs_to :category
  translates :name
end

class Category <ActiveRecord::Base
  has_many :products
  translates :name
end

Now let's make sure that there's a single page belonging to a category:

Product.destroy_all
Category.destroy_all

Locale.set "en-US"
p = Product.new :name => "The Godfather"
p.category = Category.new :name => "Movies"
p.save

Also, let's add German translations:

Locale.set "de-DE"
p.name => "Der Pate"
p.categories => "Filme"
p.save

Now we can access the product and category in one go using Globalize's piggybacking and get the translated properties:

Locale.set "us-US"
Product.find :first, include_translated => :category
# <Page:0x2466344 @original_language=English, @attributes={
  "category_name"=>"Movies", "title"=>"The Godfather",
  "id"=>"1", "category_id"=>"1"}>

Locale.set "de-DE"
Product.find :first, include_translated => :category
# <Page:0x2466344 @original_language=German, @attributes={
  "category_name"=>"Filme", "title"=>"Der Pate",
  "id"=>"1", "category_id"=>"1"}>

... which are the expected results. :-)

So much for this time ...

In Part 4 of this series we'll talk about how to "Pimp up Globalize - extensions and patches".

On my list there are currently the following topics:

  • Web-based management of your translations
  • Liquid Concept's "Globalize extension"
  • Scaping an application for strings to translate
  • Automatic translation through Bablefish