Skip to content

Zen and the Art of i18n

March 24, 2020

The G2 main app is, like many, built to utilize the functionality from the Rails i18n gem which is included in rails by default. If you’re reading that with zero clue what this means, “i18n” is shorthand for “internationalization”, and the term is actually i + 18 + n; the 18 referring to the number of letters between the i and the n.

Normally, the main purpose of i18n is to translate text into multiple languages based on users’ locations. But besides localization specifically, we have found many other use cases to help maintain a clean codebase and having a standardized way to add localized strings to views makes for happy engineers.


There are many cases where some translations depend on a count or variable, i.e. telling a user how many answers their question received. A traditional option for dealing with pluralizations might be similar to:

if t(‘question.answers.one_answer’) elsif answers_count == 1: t(‘question.answers.one_answer’) else: t(‘question.answers.multiple_answers’) en: question: answers: zero_answer: You have no answers one_answer: You have 1 answer waiting to be viewed multiple_answers: You have %{count} answers waiting to be viewed

This implementation requires the developer to put logic conditions into the view to determine which translation to render, which is never fun . . . so engineers welcome pluralizations in translations to save them from this.

Rails has the ability to handle translations based on a passed in count attribute to the translation:

t(‘question.answers’, count: answers_count) en: question: answers: one: You have 1 answer waiting to be viewed other: You have %{count} answers waiting to be viewed zero: You have no answers

These two approaches render the same result, but the latter gives the ability to add as little dynamic code as possible, resulting in cleaner code which is easier to maintain and modify. It’s important to note that pluralization can be weird. Some languages have different plural forms for “one” vs “more than one”, some have "one" vs "two" vs "many", and some do not change forms at all.

ActiveRecord scope to translate labels of Models

Another fun feature is being able to use the activerecord scope to translate labels of Models attributes under the `activerecord.attributes` scope.

Using form_with to update a model (in this example a product), the dev simply needs to define the desired label text in our translation file and the forms f.label method will pull in the text.

f.label :name en: activerecord: attributes: product: name: ‘What is the products name?’

Scoping to views

One approach to either formatting or accessing keys is based on how the app’s views are sorted, meaning the app can access the translation based on the directory scope.

‘views/products/show.html’ t(‘.title’) en: products: show: title: Product page title

It is worth mentioning that when working in a codebase that changes frequently, it’s likely views are reorganized, whether it be the renaming of a resource or refactoring into partials. To facilitate easier future work, a developer should bias toward descriptive translation instead of these implicit scoped ones.

Keeping translation files in sync

While using i18n is very helpful and great for quick changes, the yml files can quickly get out of control when there are many teams of developers editing them daily. Changes can get lost, which surfaces the issue of having missing or unused translations hiding, effectively dead code, or files can get messy and unalphabetized, which is key to driving any developer mad.

This is where i18n-tasks comes in.

The command i18n-tasks normalize keeps yml files organized, whilst i18n-tasks unused and i18n-tasks missing uncover unused and missing translations respectively, easing the commit cycle. They can be added to the spec suite to run both locally on commit or manually, and in the test suite in the platform of choice.

Edge cases

There are situations where these might raise false negatives; usually when interpolating into a string to finish the translation scope. This is mainly because the regex used in the lookup can only go so far, when keys are scoped, deeply interpolated or dynamically created in the view, we can occasionally run into false negatives.

Luckily rails allows us to declare these keys as being used, either as comments in the view or in the config file with i18n-tasks-use . We try to avoid excessively doing this so it’s usually a last resort.


If i18n isn’t implemented in your application today, the process to incorporate it might seem intimidating. Gradual adoption, through refactoring or standardizing it going forward, will allow developers to benefit immediately from simpler code in tidier views even without necessarily translating into other languages.

Never miss a post.

Subscribe to keep your fingers on the tech pulse.

By submitting this form, you are agreeing to receive marketing communications from G2.