We recently came across the need to start translating Givey so we thought about how to do
it and the best way to go about it. Let’s talk about Ruby on Rails and locales.
Since we use Ruby on Rails, it’s a simple affair to add translations using I18n. We can
just create different YAML files named after their respective locale (es.yml, en.yml) etc and be done with it.
Switching locales
Next came being able to switch to different locales. There are a few of ways to go about this, so I won’t list them all.
Potentially we could:
- Use a subdomain for different languages
- Use a prefix for endpoints (givey.com/es/explore)
- Use a query string (givey.com?locale=es)
Subdomains weren’t really considered as we saw no real reason to use them since the app would
be exactly the same and not separate, which is mainly why you would use subdomains.
So it came to a choice of prefixed routes or query strings. Prefixing routes would require modifying our routes configuration to make it work. Not impossible, but not as easy as…
Query strings. Nice and simple. However, these aren’t persisted through different requests. We could do
something like appending the query string each time we navigate or make a request, but that’s pushing it a bit.
Not to mention that we need to check if there is already a query string and so on.
Persisting locale
But, how do we persist this so we always know the locale? Simple, we used a session. If the query string was present,
we change the locale to the value of params[:locale]
I18n.locale = session[:locale] || default_locale if params[:locale] && params[:locale].to_sym.in?(I18n.available_locales) I18n.locale = session[:locale] = params[:locale] end
This sets the locale to that stored in the session, or uses the default locale if the session is empty.
It then assigns the session if the query string is present and its value is an available locale.
This also means we can create a little widget on the page to switch locales easily.
Now we can do something like
<%= I18n.t(:hello_world) %>
which will be translated in to the current locale.
Translations in JavaScript
However, we can’t do that with JavaScript (we don’t use erb.js files, just regular JavaScript). So, the next
step was to figure out how to expose the translations to our JavaScript.
We came across a few Ruby gems for doing just this. However, they were either unmaintained or had a few more
dependencies than we would have liked.
We came up with 3 different ways of doing this ourselves.
- Output a JSON list of translations in the DOM and save to a JavaScript variable
- Create a rake task to build JS files of translations
- Create an endpoint to return the locale data JSON style
With option A, given that we know the locale, we could do something such as
<script> var translations = "#{I18n.backend.send(:translations)[I18n.locale.to_sym].to_json}" </script>
This is a simple method and it means we can do something like translations[‘hello_world’] in our JavaScript.
The issue we saw with this, however, is that this will very quickly clog up the DOM and could produce a bottleneck in the
rendering. These locale files could potentially be hundreds of lines long. So we quickly brushed that idea aside.
Option B would see a rake task (triggered before deploy) which would take each locale and create a JavaScript file.
Something like es.js would contain pretty much the same as above, assigning the locale data to a variable or global
object.
The issue with this is that all of these locales would need to be included in the asset pipeline. Meaning we would be
loading every locale file in to memory, even if it’s not used.
Therefore, we went with option C. We created a public API endpoint which returns a JSON list of the locale data.
With this, we created a little JavaScript module to load this data in to it. This means we can have callbacks for when
the locale data is actually loaded.
We also created several helper methods for translating in JavaScript. Namely translate and t, keeping it the same
as the Rails I18n method names.
We did have a small issue with key names however. I18n allows nesting for locale data:
es: hello: world: 'Hello world'
In Ruby terms, we can just do I18n.t(‘hello.world’) to get the translation. So how do we go about this in JavaScript?
Well, we stayed with passing the keys as a period delimited string, so how do we use that to get the proper key in the JSON?
Well, we used a reducer:
return key.split('.').reduce(self.index, self.translations)
And with that, we now have Givey translated fully on the client!
How do you switch locales?
The ability to switch to a different locale is hidden behind a feature flag so you won’t be able to use it yet. However, we are in the process of getting things translated and it should be enabled soon.