Migrating to Rails 2.3 and Globalize2 from Rails 2.1 and Globalize1
The great migration Exodus
Here’s the deal: my good old pet-project, Bitspudlo.com e-store engine (accidentally I also run this store, but that’s offtopic) is still running on Rails 2.1 due to Globalize1 being compatible with rails up to this version — with Globalize for Rails 2.1 being extremely hackish and half-official, if you consider Globalize website.
I didn’t feel well with that. So, since Globalize2 project started, I’ve been observing it almost since it was announced. For some time it still didn’t do the trick for me (despite being written really cleanly and without brutal hacks, thanks to Rails i18n API since 2.2), especially in terms of migrating data from Globalize1.
Last weekend was the day I said “enough” after seeing that Globalize2 matured pretty well and being fed up with my pet Rails project not being “edge” enough. Seriously, I don’t feel very well with hackish solutions and Globalize1 sometimes didn’t work as it should have.
There’s actually a pretty good guide on migrating to Rails 2.3 by Peter Marklund, so that was my starting point. The upside of my migration was that I was starting from 2.1 (alas with code written in the dark ages of 1.2.3 – 1.2.6), the downside was the huge migration step ahead of me in terms of migrating from Globalize1 to Globalize2.
So I started with a bang: change Rails version in config/environment.rb, remove vendor/plugins/globalize, install Globalize2 from Github and get dirty trying to get the app to work (thank git for branching!).
The important things come first: model translations. Globalize1 was designed here in a way that texts in “default” language were kept in given model table itself and only translations (deviations) to non-default languages were kept in huge GlobalizeTranslation model with STI onto ModelTranslation. Now, Globalize2, on the other hand, didn’t use the original table at all — all the translatable text fields were kept in model_translations table, regardless of default language. And of course Globalize2 overrided the field and field= methods for translated fields.
My migrations looked exactly like in Globalize2 readme:
class CreateProductTranslationTable < ActiveRecord::Migration def self.up Product.create_translation_table! :title => :string, :description => :text end def self.down Product.drop_translation_table! end end
Fortunately all the data for default language was still available through attributes hash, so this is the migration script I came up with after some fiddling: available as gist on Github
Feel free to play with this script (hey, it worked for me!), but remember to backup your data beforehand.
Then I had to sweep through controllers (actually only ApplicationController filters and one action responsible for setting up a different locale) to change from Globalize1 Locale to I18n.locale (and of course using now symbols instead of old string)
class ApplicationController < ActionController::Base before_filter :set_locale def set_locale default_locale = 'pl-PL' locale = params[:locale] || default_locale session[:locale] = locale I18n.locale = locale.to_sym end end
(I’ve ommited the code for autodetecting locale based on browser settings / request header etc.)
Then I had to run through helpers and logic responsible for displaying prices in different currencies. I’m not going to paste code here as I feel it’s pretty hackish and still looking for a way to elegantly refactor it. Fortunately despite its hackishness it was still pretty quickly updated and started to work.
Not much to say here: I got a chance for some pretty nice refactoring of code that (for instance) was sending e-mails with ActionMailer that had titles depending on language. Previously it was extremely fugly, checking for locale in if statement and setting e-mail’s @title depending on the locale. I made it use new and shiny Rails I18n API with some interpolation and that pretty much did the trick. I would fall in love with Rails I18n API if I hadn’t already while working with it at job for client projects.
Localizing templates / views
Now this got funny.
Globalize1 had a facility that let developer give the template a .en.html.erb extension and have it rendered for en-language locales with falling back to .html.erb for other locales or when .en. version was not found. Globalize2 focuses on extending Rails I18n API and providing nice and clean model translations, but ommited the issue of translating views.
Theoretically speaking it’s because Rails actually does exactly that, but with a twist: I was using both en-US and en-GB locales with Globalize1 to switch currencies based on region. And Rails localized-templates solution doesn’t support fallbacks to generalized locale, so it’s been looking for .en-US.html.erb template (for instance) and, when not found, falling back to .html.erb completely ignoring .en.html.erb one.
That’s partially because locale fallbacks are actually something introduced by Globalize2 and Rails doesn’t know anything about it.
That’s when I found Jose Valim’s Localized Templates plugin which proposed a pretty different solution (cleaner one: subdirectories for locales), but still didn’t have locale fallbacks. Jose turned out to be a very friendly guy and suggested I take a look at his plugin’s code to modify it, so I did and 15 minutes later I had a Localized Templates fork using Globalize2 locale fallbacks that did the trick for me.
The only thing left was moving the actual localized templates from views/path/template.en.html.erb to views/en/path/template.html.erb, but I wanted to do it anyway to have a good separated file structure.
If you want to have both fallbacks and the template.locale.html.erb naming scheme, have a look at this plugin: the code is really clean, nice and easy to hack with.
Is that all?
Some refactoring I did in the meantime (e.g. passing locale in path and relying on session only as fallback) fortunately made all the transition pretty easy anyway. I still have to throw out ActiveScaffold (it doesn’t play well with Haml as far as I see from backtrace) and replace it with normal, decent CRUD, but that was something I’ve been planning nonetheless.