Globalize's advanced features - Get on Rails with Globalize! Part 3 of 8
posted: January 14th, 2007 · by: Sven
In Part 2 of this series you’ve seen:
- how to setup your application to use Unicode
- how to select and keep the current user’s locale
- how to translate entire templates (instead of individual strings)
- how to translate Rails ActiveRecord messages
In this article we’ll talk about some of the more advanced features Globalize comes with like:
- Abstracting ViewTranslations (sprintf-like usage)
- Singular and (multiple) plural ViewTranslations
- Globalize’s Currency class
- Piggybacking translations of associated models
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
jlstar said March 2nd, 2007 at 11:24 AM ¶
I appreciated much these articles and I encourage you has to continue the last. However I had a small problem with :
Locale.set "de-DE" p.name => "Der Pate" p.categories => "Filme" p.save
doesn't work but :
Locale.set "de-DE" p = Product.new :name => "Der Pate" p.category = Category.new :name => "Filme" p.save
work fine ... i understand this in file "dbtranslationtest.rb"
enjoy
Sven said March 2nd, 2007 at 02:43 PM ¶
Hi jlstar!
Umm, yes :) This is meant to be one piece of code, so that the variable p from three lines above can be reused (in fact, that's part of what I'm trying to demonstrate).
Just think of the text "Also, let's add German translations:" as a Ruby comment. I probably should actually change that formatting to make that more clear.
Thanks for the hint!
And yes, I'm going to post the next installment of this series shortly.
demimismo said March 22nd, 2007 at 10:39 AM ¶
Hi!
First of all I must say that i’m a newbie to Rails, so I think that all my problems with Globalize begins with that.
We are building an application with globalize. We only needed to translate strings in the frontend (in our templates) so we are usign t() in all the strings and the hack to pass it multiple arguments.
We use Rails 1.2, (tested all flavors: 1.2.0, 1.2.1 and 1.2.2)
Problems that we have:
Ofcourse I have the right version of Globalize (branch for 1.2), but I have tested other versions (1.1 and trunk). We have tested under windows, MacOs and Linux.
I don’t understand what’s the problem, I have lost many many many time testing and debugging because I thought it may be a bug in my software, but now I have to change to Gettext because I’m late to my deadline :-(
Sven said March 22nd, 2007 at 02:08 PM ¶
Hi demimismo,
first of all, thanks for reporting your problems that detailed.
I’d very much appreciate if you could spend the time to register to the Globalize Mailinglist so that we can try to work things out there.
I’m sure you tested to omit the multiple-arguments patch and tried if the same problems continued to occure.
The first both problems you point out have to do with the understanding of the stings in your templates/views being conceptualized as keys in Globalize (and I think that’s mostly common ground in I18n solutions). Therefor (and for performance reasons, of course) the keys are stored as VARCHAR (and not TEXT), so their length is limited. I think it’s worth considering to add some kind of validation to Globalize here.
Because the strings in your views are seen as keys, it’s natural to track them in the database by inserting a record for each of them. Also, if you think about it, it might be desirable to actually re-“translate” them for the baselanguage, too.
As for your third problem I honestly don’t understand it :) After you’ve inserted the translation Globalize will insert another, identical row to the database? My first guess here would be to check out if this behaviour continues to occur after removing the multiple-arguments patch.
In any case, please consider registering to the Globalize mailinglist as that’s a far better place to find support. :-)
Tim said August 7th, 2007 at 11:56 AM ¶
For using Currencies with the globalize plugin i’d like to add a bit more explanation to the setup in the models. I at first thought that the line: composedof :price, :classname => “Globalize::Currency”, :mapping => [ %w(price cents) ] could be used for all attributes, if I just adjusted the attribute name ‘:price’ of the example to my attribute name. I was wrong. ‘composed_of’ is a built in Rails method, and care should be taken when composing the :mapping array. In the example is maps the (table) field ‘price’ to the variable ‘cents’ (used in the Currency model). When your field/attribute name is not ‘price’ you should therefor also adjust this in the mapping. (composedof :totalprice, :classname => “Globalize::Currency”, :mapping => [ %w(totalprice cents) ])
(Price being such a natural name to use with currencies, I usumed it to a standard of Currency [like ‘cents’ is anyway])
Sven said August 11th, 2007 at 06:27 PM ¶
Thanks for pointing this out, Tim!
I should add something like this to the article asap, but I’m a bit short of time right now. Please let me know if you have any specific suggestions on how to change the text.
Mitja said January 23rd, 2008 at 01:31 PM ¶
Hello, great tutorial!
I am very surprised on Slovenian example. Not many authors use it as an example :)
Anayway, just a little correction. It is not 0 mesta, but 0 mest :)
Thanks for this great tutorial! Mitja
Sven said January 25th, 2008 at 02:21 PM ¶
Hey Mitja!
Many thanks for that correction :) I’ve changed that bit. Is that correct now?
Actually I’ve collected some examples from various websites and am (unfortunately) by no means capable of speaking Slovenian.
Steffen said February 6th, 2008 at 01:28 PM ¶
Just a short notice: the link to the follow-up article isn’t working. Still, this series is a nice write-up. Thanks for the information.
Sven said February 16th, 2008 at 02:36 PM ¶
Thanks, Steffen! I’ve fixed that.
Mitja said February 20th, 2008 at 02:12 PM ¶
Hey Sven,
it is correct now, yes :) Our language is quite complicated and i understand you know nothing about it. It is a great one for making examples though :)
Thanks for this correction :)
Judith Meyer said April 4th, 2008 at 10:42 AM ¶
Thanks for the overview! Globalize is really neat. Just two issues, both related to dates:
Why do I still have to specify the date format when localizing it? That kind of defeats the purpose, because different languages use vastly different formats and I just want to use the most common one for each language. I noticed the empty column “dateformat” in the globalizecountries table. Is this going to be used for this issue? Can I contribute? I’m a comp linguistics student.
<%= “Created at %d” / post.created_at.localize(“%B %d %Y”) %> doesn’t work, the %d isn’t replaced, yet it’s crucial that the date is in the natural position in this phrase. What should I be using here?
Thank you very much for your help!
karouf said April 24th, 2008 at 09:18 PM ¶
Judith,
post.created_at.localize(“%B %d %Y”) being a string, I think you’d rather be using %s instead of %d
Mam Andersson said June 6th, 2008 at 02:50 PM ¶
Any ideas on how to translate the foreign key of an association? Lets say an object should have different associations in the different languages. Like in your above example but the Product have different categories for the different languages.
/mam