Polyglot content in a rails app
Most of the apps that we develop are in English. As this is the most universal language in the world, it is normal to use English since it means reaching more people with our app. However, sometimes we also need to reach people that don’t understand English or aren’t well versed in it. Due to this, we will have to translate our app into other languages.
Rails already has a gem that can translate apps by default. I18n is a wonderful gem that will translate anything you want into any language you need. It can even help you set the locale automatically to show your users a certain language based on their location or default country. But this Rails gem does have one small problem: it only works with static content. I18n doesn’t allow the translation of content created by you or your users to other languages. This could go from simple categories or labels for your products up to their descriptions or names.
One way to solve that problem is to use the Mobility gem. This Rails gem allows you to store and retrieve translations as attributes from your models. It enables you to set the attributes to be translated and, if you wish to do so, which language should be used as a default fallback.
After adding the Rails gem to your gemfile, run the generator to create an initializer and a migration for the shared translation tables that are going to be used. To do that you run:
After running the migration you are ready to start translating your stuff. Translating attributes is really easy. All you need to do is add extend Mobility
to the beginning of your file and a line defining what you want to translate, like so:
In this case, you want to translate the product’s name and description. Now, when requesting the product’s name, it will give you the name in a language based on the locale. And if you update the product, that will be based on the locale as well which means if your user is in a Spanish speaking locale, it will define the product’s Spanish name and description.
So far so good. You have different locales and you can add a product name and description to each, which will help you reach a bigger audience. But let’s say that you don’t have a Spanish translation for your product. What then? Are you simply not going to show the product at all, or are you going to show a blank product name and description? Both are bad choices for your app, aren’t they?
Well, great news then. Mobility has just the right feature: you can define fallbacks for each language and for each attribute that you want to translate. To do so, you pass a hash with the fallbacks for each locale in your translated attributes. Here is an example of how you can do it:
In this example, both the Spanish and Portuguese locales will default to English when no translation is found.
Now you may be thinking “This is good and all, but I don’t want to be setting English as a default locale for every language and everything that was translated”. And you would be right. After all, you would just be repeating the default locale everywhere. And this is why I have more good news for you.
Mobility allows you to set up a default locale to translate your entire app. To do that just add config.default_options[:fallbacks] = true
to your mobility.rb
file, and you’ll have all your translations fallback to your default I18n locale, most likely English. If you want to set up specific fallbacks for each locale, slightly change the previous piece of code. Instead of setting the fallback value to true
, set it to the languages you want it to fall back to. Like so: config.default_options[:fallbacks] = { pt: :es, es: :en, en: :es}
. This way you can choose the default fallback for each locale in your app.
We have defined what to show when there is no translation set for a language, and we have set up the order in which we want to translate each language. The thing missing now is to allow users to set up their translations.
Currently, upon adding the translation for a specific language, you have to change the locale to write it up. But that can be really bothersome. If we support a small number of locales in English, Spanish, French, and German, users will need to go to four different locales and insert data to always have a translation in those languages. This is really bothersome. No one wants to load four different pages just to add a few things. So what can we do? A possible solution would be allowing users to set those translations in the form that is used to create or update data. This way everything is in one place, and you don’t have to go to a different page for each locale your application supports. How do we do that? With a combination of I18n and the Mobility gem.
One thing Mobility brings us is locale accessors. These are specific setters and getters for the attribute translations based on locale. This enables you to choose which translation to see or change without having to change the locale to do so. To set this up, all you have to do is add locale_accessors: true
in your model translation line. In our Product it would look like translates :name, locale_accessors: true
and we would be able to have access to the methods name_en
, name_es
and name_pt
that are related to the English, Spanish and Portuguese languages, respectively. Since you might want to use a subset of the available locales, it is possible to set that subset by setting the locales in the locale_accessors
options instead of simply true
. An example would be translates :name, locale_accessors: [:en, :es]
, in order to only have setters and getters for the English and Spanish languages.
So in order to allow users to change this in one page, you only need to add it to the form by adding a new field for each locale and attribute you want to change. It would look something like this:
This seems like a lot of lines just for two attributes and two locales. And if you want to add new locales or attributes, you will have to keep adding them repeatedly to the form.
So how do we do it? A possible solution would be to get the locales from I18n and create the fields we want for each one. Here is how that could be done:
As we can see, now you only need to add two fields: one for the name and another one for the description. This way I18n will be responsible for expanding this fields to name_en
and description_en
and the same thing for other locales. You may have noticed that we have Mobility.normalize_locale(locale)
in there. This is done to make the locales form friendly, for cases like pt-BR
turn into pt-br
.
This creates us the fields we want in the form, however we will have complications in the controller. Since these are different fields, our controller needs to permit them in order for them to be used when creating or updating a record.
As it happens in the form, it would be annoying if we had to add every single combination of attribute and locale to the permitted params. It would become a huge line and it would not look pretty:
Now imagine just having to add to this line every time we added support for a new locale. It would be way too much work. What are our options? A possible solution would be to do the same thing we did for the form. Using the available locales and the attributes, create a list of permitted params. You could do it in the controller, like this:
Now every field to translate is permitted and you don’t have to worry about sending them to the controller.
Having said that, these are just basics for the Mobility gem. It has a lot more to offer and I hope you consider it when looking into translating your app and allowing your users to translate their data to reach new markets.
I hope this gem can be as useful to you as it has been for me. I would love to hear your experiences when translating an app into new languages and regions.
I work at Runtime Revolution, where we focus on delivering and maintaining the best possible products to our clients, while learning the most we can along the way.