In Rails world, when it comes to translating data stored in database, most people use Globalize which defines itself as the “Rails I18n de-facto standard library for ActiveRecord model/data translation”.
I used it many times and let be honest, I don’t like it, at all. It isn’t a bad or buggy gem but its inner design doesn’t fit my needs and expectations.
How Globalize works
Globalize relies on having a table dedicated to store translations for a model attributes. So you will have to add one table per model that needs translations.
When you retrieve an object with translated attributes through ActiveRecord, Globalize creates a SQL query which retrieves the actual data of the object and does a join on the related translation table to get according translations.
It can become quickly pretty heavy for the database and results in slow requests. I often found myself having big performance issues just because of this automatic joins when I was retrieving an ActiveRecord object and its relationships.
The main advantage of this solution — and I think that’s why Globalize team did it this way — is that it works whatever the database you use. It uses standard SQL only.
Toward a better solution
In my day to day work with Rails, I mostly use PostgreSQL and sometimes MySQL. I don’t have the need to be compatible with all the databases available out there. With that in mind I searched for another solution to store the translations, one that would be more efficient for my use cases.
The first thing that came to my mind was to use hstore to store the translations. That is a nice solution and there’s already some gems available to do that but it works with PostgreSQL only.
I searched for another solution and found out that I could use JSON datatype which is available for both MySQL and PostgreSQL.
This is a huge improvement over Globalize since translations can be stored in the same table. Rails introduced JSON datatype support in 4.2. It’s as easy as manipulating a plain old hash. Rails automatically does the conversion job for you.
No more joins needed, no more separated translation tables!
That sounds pretty good, it feels like a more natural and powerful solution to me. We still have the same features and flexibility that provides Globalize.
Example of using JSON to store translations
Adding translation columns to the table
First, for each translatable attribute of our model we need to create a JSON column:
Using the newly added JSON attributes
Now that we’ve created our
News model and the according migration,
we can use it to store our translations:
Improve translatable attributes interface
You must think “Dude that’s a pretty roots solution…”. Yes but this is a proof-of-concept. We now know that it works and we can build a better interface based on this.
Sure you’ll want something fancier, for instance you would want to have a method that returns the data for the current locale without having to pass it explicitly.
You’ll also want to return the data for the default locale if there’s no translation for the given locale.
A minimal implementation could look like this:
Then we use it in our models:
With very little amount of code we already have a much better interface to use our translation system!
You could also want to be able to find a record based on its translated content without having to build the JSON by hand.
There’s already a Gem to keep your hands clean
I’m not the first one to think of this solution and someone has already wrote a gem that package all this with a nice interface.
You really should give it a try.
Don’t follow, think by yourself
The main message behind this blog post is that you should not follow the mass blindly and always think twice when you’re choosing a library you’re going to depend on in your application.
Adding a dependency in your app can lead to huge impact on performance and on how you’ll design it. Always try to think about available alternatives and the real advantages of using a potentially heavy library over your own light implementation.