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

posted: January 14th, 2007 · by: Sven

in: Globalization, Programming · tagged as: , , , , , , ·  21 comments »

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

Leave a comment

21 Comments

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

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

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

    • tr_key is set to VARCHAR 255 in database, so every string longer than that doesn’t really get translated (it’s inserted in the database again and again).
    • Our base_language is set to es-ES, when I load my homepage for the first time and I set the locale to es-ES globalize stores in database all the strings in that page. ¿Why? I don’t need that, I only need that Globalize stores strings to be transalated in languages different from my base_language. I won’t translate strings from my base_language to my base_language again (sorry for my english).
    • When I translate some strings it works fine, but after a couple of reloadings globalize stores some translated strings to be translated again. For example: We have this string in our template: “Hola, %s” We set the english translation to: “Hello, %s” The translation works as spected, but after one page reload globalize stores “Hello, %s” to be translated. It seems that it only happens with strings that contain %s, %d…

    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 :-(

  4. 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. :-)

  5. 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])

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

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

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

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

  10. Sven said February 16th, 2008 at 02:36 PM  

    Thanks, Steffen! I’ve fixed that.

  11. 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 :)

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

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

    2. <%= “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!

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

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

  15. QQQ said February 7th, 2011 at 06:36 PM  

    Finally we kissed and the passion scale went sky high and I knew I was onto a good thing - sex was a certainty free porn videos. She never hesitated when I began to fondle her breasts and she willingly exposed them for me mobile porn. They were firm and I suspected a breast enhancement but said nothing - they still felt good and I was enjoying them and gradually working my way further south free porn tube. She was a step ahead of me and before I could completely undress her she moved on me atk hairy and I was suddenly having my pants pulled down and I was enjoying one of he best cock sucking hairy pussy experiences I had ever had. ABB728019394

  16. Yeng2 said February 24th, 2011 at 06:54 AM  

    I18n abutment in Ruby on Rails was alien in the absolution 2.2 and is still evolving. pass4sure 70-563 The activity follows the acceptable Ruby on Rails development attitude of evolving solutions in plugins and absolute applications first, pass4sure EX0-104 and alone again cherry picking the best-of-bread of a lot of broadly advantageous appearance for admittance in the core. pass4sure mb2-633 Thanks for giving me an amazing post, its great time to read your post. pass4sure 70-502 I’ve got some more interesting topic for discussion. So keep it up.

  17. chat said March 31st, 2011 at 08:08 PM  

    The following cleaned up the issue:

    Dependencies.loadoncepaths -= Dependencies.loadoncepaths.select{|path| \ path =~ %r(^#{File.dirname(FILE)}) }

  18. side sleeper pillow said April 22nd, 2011 at 06:57 AM  

    it that true?

  19. okey oyunu said May 12th, 2011 at 04:13 PM  

    Thanks for this article. Tüm dünya artik okey oyunu oynuyor. Yillardir bir çok oyun programi olmasina ragmen, içlerinden en güzeli olarak nitelendirebilecegimiz tek bir site göze çarpmaktadir. Diger tüm okey oyunu programlarinin aksine ücretsiz olmasi ve 3 boyutlu olarak hizmet vermesi mükemmel bir gelismedir. Sizlerde www.okey-oyunu.com adresinden bu essiz okey oyununu indirebilirsiniz. Kullanimi çok basit ve Türkçe dil seçenegi ile kolaylikla oyuna baslayabilirsiniz. Ister kendi ülkenizden, isterseniz dünyanin tüm farkli bölgelerinden dilediginiz oyun odalarini seçerek, oyuna hemen baslayabilirsiniz. Okey oyunu oynamak için artik arkadas bile aramaniza gerek kalmadan, bilgisayarinizdan 100 binlerce üye ile online olarak okey oyununu oynamanin zevkine varabilirsiniz.

  20. porno said May 22nd, 2011 at 01:28 PM  

    I do agree with all of the ideas you have presented in your post. They’re really convincing and will definitely work. Still, the posts are too short for newbies. Could you please extend them a bit from next time? Thanks for the post.

  21. porno said May 22nd, 2011 at 01:59 PM  

    good comment. thanks you friends.

    I’ve surfed the net more than three hours today, however, I haven’t found such useful information. Thanks a lot, it is really useful to me

Sorry, comments are closed for this article.

artweb design
Sven Fuchs
Grünberger Str. 65
10245 Berlin, Germany


http://www.artweb-design.de

Fon +49 (30) 47 98 69 96
Fax +49 (30) 47 98 69 97