Real fun: Get on Rails with Globalize (take #2)

posted: June 13th, 2006 · by: Sven

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

This article is a bit outdated and has been replaced by an entire series in the meantime: “Get on Rails with Globalize: a comprehensive writeup in 6 parts”.

This is the second version of my writeup about Globalize. My first attemp was too much a literal protocol of my actual steps – which haven’t actually mixed that well with a newbie intro to Globalize. So simply let’s start over.

I initially really shyed away from translating a pretty simple application just because of my previous experiences with t10n/i18n libraries that were nothing else but a big pain and hassle …

Ever felt the same? Then here’s some good news for yor. If you’re going to add multi-language support to a Ruby on Rails application I suggest you give Globalize a serious try.

The docs are a bit spare so at first it seemed to me that things don’t work as announced (or: at all). But once I understood how Globalize works everything went like a charm and was incredibly simple to use (at least for the very basic stuff I’ve tried so far).

So, I’ll put together how things worked for me.

Preliminaries

You’ll need to have a working Rails app up and running. That is, if you want to start from scratch first make sure that your conf/database.yml knows about your database, valid credentials etc. and everything else is nice.

Install and setup Globalize

Then make your way to your Rails app directory and install the plugin:

script/plugin install http://svn.globalize-rails.org/svn/
globalize/globalize/branches/for-1.1

Now just do:

rake globalize:setup

... and you’re already done with the setup!

If you’re curious, check your database! You’ll find three new globalize_* tables with 239 countries, 186 languages and 3420 pre-translated strings… including several languages I’d bet you’ve never heard of like Avar, Manx or Wolof!

Whoops! These folks don’t really expect my application to be translated to their native language over there in the Northeast-Caucasian Mountains, do they??

Configure Globalize

Now, that’s it. You’re already globalized.

There’s nothing more to do than set the “base_language” for your Globalize instance but it’s easy to miss this step I think. At least it happened to me. This should work:

Rails::Initializer.run do |config|
  # ...
end
include Globalize # put that thing here
Locale.set_base_language('en-US') # and here :)

If you’re used to locales with underscores (like I was) and miss this you’ll get an exception: ‘en_US’ won’t work. (I’ll yet have to lookup some RFCs or specs to find out what’s the difference.)

As far as I understand the meaning of the base_language that you set here, this is what Globalize treats as the “primary” language – which means that content stored your model tables is stored in the primary language whereas translations are stored in the globalize_translations table.

Having written this I remembered having read about that in the mailinglist and found the following quote from Joshua Harvey:

“Base language entries are stored in the parent table itself, as opposed to the globalize_translations table. So if you have a table called ‘products’, and your base language is English, the English product name would be stored in products.name, whereas the German translation would be stored in globalize_translations.

It’s important to never change the base language once you’ve starting populating the database.”

I think this states it more clearly.

(I yet wonder what’s the right choice or best practice for me here. From the standpoint of developing an ubiqous Domain Language an English base_language would be first choice of course. I therefor chose that option.

On the other hand … from the standpoint of an agile process of getting real with an application that for the most forseeable time will deliver localized content in German (and probably only some content in other languages) this would be a waste of ressources, wouldn’t it? I’ll have to find out more on this.)

Check it out!

Already impatient to check out some working stuff? I were, too …

There’s a nice example in the <a href=”http://www.globalize-rails.org/wiki/pages/example”>Globalize wiki using Rails Unit tests and fixtures.

I probably should have tried that but I didn’t. I’ve globalized an existing application that I had been working on before, so I decided to just open up a controller and write something to the screen.

The shortest statement I’ve found was:

def index
  Locale.set("es-ES")
  render :text => Time.now.localize("%d %B %Y")
end

This prints:

07 Junio 2006

Wew, hey! This means Globalize already works.

Of course my next step was to try and change the locale to “de-DE”. Which resulted in:

07 June 2006

Wtf!? Too bad. :( Would have been too nice if this worked out of the box, wouldn’t it?

But this forced me to read a bit more than I had yet. It took me a while to read through the Globalize wiki, inspect some code, query the database tables … to finally find out that there simply is no default translation provided for “June” in German (which of course isn’t that unusual at all).

Instead, there’s been a row in the translations table pointing to a “German” language row and containing a NULL value! How this?

That’s a feature :) This might not be obvious in the first place but Globalize adds NULL values to the translations table when it sees a string that’s not yet translated. While you’re developing your application Globalize will collect the strings that need to be translated. You can then write a Rails controller afterwards and add the translations.

Ok, I changed that NULL value to the German translation “Juni” that I’ve missed before. That worked. I’ve got the expected result now:

07 Juni 2006

I wondered how the heck I am supposed to add this stuff to the database from Ruby? In the Globalize wiki I found something like this:

Locale.set_translation('Welcome', Language.pick('de-DE'), 
  'Willkommen')

That looks pretty nice. I added a similar line to my controller and inspected the new records that had been created in my globalize_translations table.

I opened up a template and (having the Language.locale set to ‘de-DE’ in the controller index method for now) added the line:

<%= 'Welcome'.t -%>

Expected result: “Willkommen”. Actual result: the same. Great. :)

I changed the locale to ‘en-US’ and got the expected result “Welcome”. I noticed that this string is recieved from the original string in the template because there’s no translation for “Welcome” in the database of course.

Now, that’s way cool. Think about that for a moment.

Globalize lets you append .t to any string which then will transparently lookup and find available translations for it. In case there’s no translation available it will simply return the original string (which I believe is the default behaviour most t10n/i18n tools show).

Just add hot water …

Now, that’s about date formats and strings in my templates so far. What about my models?

Globalize comes with the capability to transparently translate any attribute of your ActiveRecords Models … all you have to do is add a “translates” directive like this:

def article
  translates :title
end

As soon as there actually is a translation for the “title” attribute known to Globalize it will be present for the language you have set. You can make Globalize know about a translation by simply saving the model like so:

Locale.set('en-US')
@article = Article.create!(:title => 'Welcome to Globalize!')

Locale.set('de-DE')
@article = Article.find(:first)
@article.title = 'Willkommen zu Globalize!'
@article.save

(I think instead of Article.find(:first) or something like this you could also do @article.reload.)

Enthused by my first experiences I applied this stuff to my experimental voting application. As far as I can tell it’s really done in an instant.

I collected the hardcoded strings from the templates and let Globalize add translations for it. I simply did this from an extra method in the controller for now using above mentioned Locale.set_translation method. With portability in mind this should better be done by migrations I think? I’ll have to figure out though how exactly to do so. And for more food for thought:there’s also a pretty interesting Howto about “Adding a Translation View” in the wiki.

Of course there’s much, much more to Globalize. E.g. there’s support for localized date and number formatting, “multiple plurals” (which some languages have) etc. and you can use the “slash trick” to insert data into your strings, like so:

"Hey, %s! You're globalized." / 'Sven'

... will result in “Hallo, Sven! Du bist Globalized.” (at least it will if you have added this translation of course).

So …

I’m really fascinated about how easy and quickly I’ve been able to get this stuff up and running. Of course – you’re used to this kind of experience like this using Rails, it’s just it’s unique featurewise selling-point or something. But I’ve been honestly surprised.

That’s definitely fun :)

Leave a comment

24 Comments

  1. Chris said June 13th, 2006 at 03:23 AM  

    Just testing if comments are working now :)

  2. Sven said June 13th, 2006 at 03:25 AM  

    They do.

    But you need to hit reload/F5 after submitting ... I'll have to fix that.

  3. Josh H. said June 13th, 2006 at 03:59 AM  

    Another excellent write-up, Sven. Thanks!

  4. olivier said June 13th, 2006 at 08:38 PM  

    shouldn't

    <% 'Welcome'.t -%>

    better work as

    <%= 'Welcome'.t -%> in the template ?

  5. olivier said June 13th, 2006 at 08:57 PM  

    Also danke shön

  6. Sven said June 13th, 2006 at 09:27 PM  

    Thanks for the kind words, Josh!

    Olivier, of course you're right. I've corrected that. Thanks :)

  7. Gabriel said June 15th, 2006 at 01:39 PM  

    I couldn't install globalize with the instructions you gave. The URL didn't work, it seems that : http://svn.globalize-rails.org/svn/globalize/ globalize/trunk works. Also I had to install SVN client from here http://subversion.tigris.org/project_packages.html or the installation won't work.

  8. Sven said June 15th, 2006 at 07:50 PM  

    Hi Gabriel!

    Thanks for the heads-up!

    I believe that the URL http://svn.globalize-rails.org/svn/globalize/globalize/branches/for-1.1/ (which really does exist, you can check that by simply pointing your browser to it) points to a branch that is (like the name says) "for Rails 1.1"

    Actually I had installed the trunk version at first. I got lots of strange errors when it came to save an object to the database etc. with the trunk version running on Rails 1.1 - they IMO simply don't work together (and aren't supposed to do so).

    I haven't been aware that one has to install SVN in order to get the "script/plugin install" command working. But this could be true of course - and is probably what you've solved by installing a SVN client?

  9. Matho said June 16th, 2006 at 02:44 AM  

    Hi, Sven!

    Running InstantRails on a XP-Pro I had tried your installation procedure and came up to the same decision as Gabriel, but tried the TortoiseSVN instead. It 'exported' the structure/files to the apps directories as expected.

    It can be found at www.tortoisesvn.org

    Here the 'URL of Repository' used was:

    http://svn.globalize-rails.org/svn/globalize/globalize/trunk

    and the 'Export directory':

    C:\InstantRails\rails_apps\cookbook\vendor\plugins

    which in this case had to be generated.

    rake globalize:setup installed the three tables in the database and took about a minute to run.

    After taking a look into the tables generated, finding quite a lot of NULL pointers and missing a lot of words, I wonder how I gonna´ have my templates translated, keeping in mind, that there's no 'grammar' at all.

  10. Sven said June 16th, 2006 at 04:57 PM  

    Hi Matho!

    Thanks for sharing this.

    Not sure what you mean by "no grammar at all" ... but I think the all-over process of getting an app translated is pretty much the same like with a traditional (like gettext based) solution: Build your app. When "finished" replace all strings by keys and translate them. (Often the tricky thing is to know, when you're "finished". ;-)

    In Globalize you're keys will show up in the database tables with the NULL records you mention. You can either add the translations directly to the database (using the db maintenance tool you're used to) by replacing the NULL values. Or you can do it from Ruby by using Locale.set_translation(...) like mentioned in the article.

    That is, you'd replace a string 'Leave a comment' by <%= 'Leave a comment'.t %>. After having shown up the template once, the key 'Leave a comment' will be present in the database table and you can insert your translation - either by manually editing the record set or by doing something like Locale.set_translation('Leave a comment', Language.pick('de-DE'), 'Einen Kommentar schreiben') ...

  11. Trejkaz said June 21st, 2006 at 06:40 AM  

    Actually, there is one more step. Because there is now the possibility of user-provided translations containing user data, you need to HTML escape the resulting string or you leave yourself open to cross-site scripting attacks and other problems.

    In other words, 'Leave a comment' should actually become:

    <%=h 'Leave a comment'.t %>

  12. Sven said June 22nd, 2006 at 01:13 PM  

    Hi Trejkaz,

    thanks for pointing this out!

    Hmm, yes. Ok, I think there are several usecases where you won't want the end-user to translate an application or website. So I think, this is a special case.

    On the other hand one should of course be aware of this from start for security reasons.

  13. tonio said July 23rd, 2006 at 07:15 PM  

    For windows boxes, I think you have to use ruby script/plugin install http://svn.globalize-rails.org/svn/globalize/globalize/branches/for-1.1

    svn client is not necessary in this case

  14. where to translate ? said July 26th, 2006 at 01:26 PM  

    hi Sven, I post on rails list a question about what you said:

    "I simply did this from an extra method in the controller for now using above mentioned Locale.set_translation method. With portability in mind this should better be done by migrations I think?"

    => So you create an extra method and add all your translation inside: Locale.set_translation('Welcome', Language.pick('de-DE'), 'Willkommen') Locale.set_translation('Welcome', Language.pick('de-DE'), 'Willkommen') .....

    1/ When do you call this method ?

    2/ You talk about using migration ... any idea to do it ?

    thanks for your great tutorial I try to translate a website using globalize and I am not sure or to do it yet ;-)

    arnaud

  15. MyKey_ said August 6th, 2006 at 11:31 PM  

    Using Globalize the ObjectMethod save (ActiveRecord::Base) returns false whenever I use translates. This is a problem because scaffold MyObject creates a MyObjectController with the following code:

    def create
      @myobject = MyObject.new(params[:myobject])
      if @my_object.save
        flash[:notice] = 'MyObject was successfully created.'
        redirect_to :action => 'list'
      else
        render :action => 'new'
      end
    end
    
    The problem is, that save calls create_or_update and returns the result. But Globalize overrides this method, not giving a damn about the original return value. Therefore I suggest to alter the method create_or_update in vendor/plugins/trunk/lib/globalize/localization/db_translate to something like this:
    def create_or_update
      retval = globalizeoldcreate_or_update
      update_translation if Locale.active
      retval
    end
    

  16. Eduard said August 23rd, 2006 at 01:29 PM  

    What about more dynamic values? <%="%s has %d apples and %s has two" / "Goran" /5 /"Mihai"%> generates 3 strings into the DB and, if you replace Goran with Sven, another 2 strings are inserted into DB.

    how this problem can be solved?

  17. Tom Green said September 1st, 2006 at 05:58 PM  

    Hello to everyone,

    I'm trying to use the globalize plug in for rails, which I managed to get up and run...

    Two questions though give me a strange feeling I hope someone can help me with... I stuck already for quite some time :-(

    My base language is set to Locale.setbaselanguage('en-EN') in environments.rb.

    Using script/console and checking with: Locale.base_language.id => 1819.

    I also verified within the application that the base language is set to English.

    Unfortunately I find, that every string I want to translate (e.g. with:<%= 'string to translate'.t %> or in the controller with "modelvariable.attribute.t" ) appears in the table globalizetranslate for the base language too.

    For instance: Select languageid FROM globalizetranslations WHERE tr_key ='string to translate'; => 1556 AND 1819!

    I thought the strings for the baselanguage would *not* appear in the globalizetranslations table.. Or I'm mistaken?

    The second question I do have is: When I'm removing a "translates :attribute" statement in a model I can't access the value anymore on the console or in the controllers. What else do I have to do to get the original value of the model?

    Help that helps me solving my trouble is very much appreciated.

    Thanks

    Tom Green

  18. Sven said September 2nd, 2006 at 09:19 PM  

    Tonio,

    ruby script/plugin install http://svn.globalize-rails.org/svn/globalize/ globalize/branches/for-1.1

    does actually work on any system, I think.

    Thanks for pointing that out. I'll change that in some upcoming next version of this write-up.

  19. Sven said September 2nd, 2006 at 09:41 PM  

    Arnaud,

    1) yep, I added an extra method to add the translations as an array. The method would iterate that array and add the translations. I worked through my application searching the templates for strings that needed to be translated, replaced them by 'something'.t or so in the templates and added the translations to the array. I called that method as often as I wanted (I think I called it from irb) while working through the app.

    2) I tried adding the translation data through a migration after writing this article - worked fine. I think, I used csv migrations for it, but that's not necessary. One thing I'm not quite sure about is what a reasonable workflow would be here. I'd think translation should be done as late as possible to avoid ongoing changes at multiple places (templates, migrations, ...).

    I think I'll check that out and cover migrations in an upcoming next version of the article.

  20. Sven said September 2nd, 2006 at 09:47 PM  

    MyKey_,

    thanks for the headsup. But ... are you sure that's the case with the for-1.1 branch? I distantly remember being bitten by something like this when I was running the trunk version of Globalize accidentally.

  21. Sven said September 2nd, 2006 at 10:00 PM  

    Eduard,

    to be honest right now I can't remember how I got this working but I think it worked. I'll check that out and try to cover it in an upcoming rewrite of this article. In the meanwhile you'll probably want to ask about this on the Globalize mailinglist? http://www.globalize-rails.org/wiki/pages/MailingList

    Have you tried to use an array as a "divisor"?

  22. Sven said September 2nd, 2006 at 10:09 PM  

    Tom,

    1) I believe that it is correct when there are entries for both the baselanguage and the translated language showing up in that db table. At least that's what I'm seeing in my tables, too. (Probably the reasoning behind that is to give you the opportunity to check for all strings that'd need translation in one go before an additional language has been added?)

    I'll try to find out more about this and cover that in a rewrite of this article. Thanks for asking this!

    2) I'm not quite sure if I understand what you mean by "access the original value". Do you possibly need to reload the class within IRB after having changed the code?

  23. Tom Green said September 3rd, 2006 at 07:35 PM  

    Hello Sven,

    thanks a lot for answering my questions!!!

    It's good to know, that this happens to your globalize_translations table too. Still though, I'm a but surprised that these entries are made... but hey... if it's a "feature" I'm fine with it :-)

    I was just about to explain "the original values" problem in more detail--- but before a re-check worked out differently: I couldn't reproduce it. Never mind.. :-) I'm sure that I had re-loaded the console and browser.

    Uh, and I'm came across the same question that you had, whether to use English or German as a base-language. I've decided for English, since most visitors of the page I'm working on wanna have it in German. That forces me to translate each item :-) Making sure I'm having it in English too..

    So thanks again for your page and answers!

    Tom

  24. Antony said September 28th, 2006 at 01:49 AM  

    Great tutorial, thank you!

Leave a comment

Name required
E-Mail and Website optional

If you can read this, you don't use a typical webbrowser that plays nice with CSS.
Please do not fill in anything here!

Hint: You're going to comment a Globalize-related article. If you are going to post a bugreport or ask a usage-related question you might want to consider subscribing to the Globalize users mailinglist instead and post over there. In any case, your comment is appreciated and welcome here, too :-)

Hint: Markdown will be applied to your comment. If you post any code, be sure to escape underscores (like so: \_) if you do not want them to be converted to an <em>phasis.

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