Idiom: a new internationalisation library for Elixir

There's a lot of untapped potential in how we internationalise our products. Now, there's a new library to help out.

There was a time when my team was tasked with updating the translations in our app. The request was to update a few strings according to new legal requirements. At the time, our workflow included updating the translations in an external editor, exporting them to the file format of each codebase, adding a commit with the updated file, and submitting a new version to the App and Play Store - way too many manual steps that made this a very unpopular task to pick up. It was a complete pain in the ass.

This is one situation where I felt like our tooling setup really failed us. There are so many little things we could have done: automatically synchronise to our repository, for example. Still, there is one limitation that cannot be overcome when bundling our translation files with our releases: having to build a new release and, for mobile applications, going through the annoying review process of the Stores.

Over time, the idea of over-the-air localisation became more and more lovely to me. Then, I ended up working in the localisation industry, and realised that what I wanted wasn’t possible in Elixir at the moment. There is a long-standing pull request for gettext which adds support for runtime translations, but gettext still isn’t the fully featured library I would like to see: there is no support for fallback locales and the way plurals are used is not immediately obvious especially for languages with more than two plural forms.

Enter Idiom

Let me introduce Idiom to you - and let’s not beat around the bush for too long, so here’s an excerpt from the README:

# Set the locale
Idiom.put_locale("en-US")

t("landing.welcome")

# With natural language key
t("Hello Idiom!")

# With interpolation
t("Good morning, {{name}}. We hope you are having a great day.", %{name: "Marco"})

# With plural and interpolation
t("You need to buy {{count}} cheese cakes", count: 1)

# With namespace
t("Create your account", namespace: "signup")
Idiom.put_namespace("signup")
t("Create your account")

# With explicit locale
t("Create your account", to: "fr")

# With fallback locale
t("Create your account", to: "fr", fallback: "en")
t("Create your account", to: "fr", fallback: ["en-US", "en"])

# With fallback key
t(["Create your account", "Register"], to: "fr")

# Everything everywhere all at once
t("Thank you {{name}}, your purchase of {{count}} items was successful", %{name: "Marco"}, to: "fr", namespace: "signup", count: 2, fallback: "en")

Now that you know the entire API (that was quick, I know), let’s talk about some of the cool things that are hopefully going to make you not hate localisation.

Automatic locale hierarchy

Most languages have some sort of variations between regions. If you’re American, you’re gonna take the elevator, whereas the British among us would properly be more comfortable with a lift. Then there’s also regional differences in spelling (@behaviour, anyone?). The base language and a majority of the messages will stay the same, though. In order to reduce duplication, Idiom allows you to define your translations like this:

{
	"en": {
		"default": {
			"Create your account": "Create your account"
		}
	},
	"en-US": {
		"default": {
			"Take the elevator": "Take the elevator"
		}
	},
	"en-GB": {
		"default": {
			"Take the elevator": "Take the lift"
		}
	}
}

The "Create your account" key stays the same in every region of the English language, so we can define it one level up under the en locale. The regional differences can then be defined under the en-US and en-GB locales.
Now, when you call t("Create your account", to: "en-GB"), Idiom will also automatically try to resolve the key in the en locale if it’s not found inside en-GB by creating a resolution hierarchy internally. That way, you can define regional differences without duplicating every single key.

Fallback locales

Ever have a language that is not yet fully there translation-wise, missing a key here and there, but still ready enough to be made available for the users? Know that the user speaks another language? Add it to the resolution hierarchy as fallback language, and it’ll transparently show the message in that language. This is also quite handy when you are targeting users from multi-lingual countries. For some Swiss people, for example, you might get away with something like t("Hello!", to: "de", fallback: ["fr", "it"]). Others might rip your head off, though, so make sure to finish those German translations!

Over-the-air localisation

Finally. The reason I actually wrote this thing.
Right now, Idiom supports two backends out of the box: Phrase and Lokalise. They are both fantastic tools to manage your translations, and I highly recommend either of them. Both also support bundling your translations as over-the-air package to be downloaded from their CDNs, and Idiom can continuously download them and keep your copy fresh.

Let’s look at that, shall we?

config :idiom,
  default_locale: "en",
  default_namespace: "default",
  backend: Idiom.Backend.Phrase
  # backend: Idiom.Backend.Lokalise

config :idiom, Idiom.Backend.Phrase,
  locales: ["de-DE", "en-US"],
  distribution_id: "54070a20cb50153126f891eaee37123a",
  distribution_secret: "K14wARUvEikIj_9-HlcuZc0uFLG1w_OfUviNi5mDpsQ",
  otp_app: :hello_it_is_me_your_otp_app

config :idiom, Idiom.Backend.Lokalise,
  project_id: "8858941664f84165efb1d8.83865528",
  api_token: "47022866020464a1fdd9cfe66438025cbx87",
  otp_app: :hello_it_is_me_your_otp_app

Select a backend, add your tokens, and let Idiom start is as part of its Supervisor. It’s that easy! Now, your team writing copy can just update things themselves and publish a new distribution on their timeline without requiring any engineering input.

Of course, if you are not ready to jump to an OTA solution, Idiom still supports the local file system as source. In fact, I’d highly recommend having some version of your localisation efforts locally in case the provider you are using is unavailable. When you are ready to move, though, Idiom will already support you in that endeavour - and, if it’s with a provider that’s not Phrase or Lokalise, let me know or open a PR/publish your own on Hex!

What’s next?

Not much, at the moment! I’d like to add some more test cases around RTL languages and different scripts, but writing them has turned out to be pretty difficult since I don’t speak one myself. Since Elixir is UTF-8 by default, though, I am reasonably confident about Idiom already supporting those. Still, test cases are always nice, so if you feel like your language would fit the above description, please get in touch or open a PR!

Other than that, I am just waiting for the feedback to come in as I release this to the public. The good, the bad, the “how the hell did you miss this bug?” - they’re all welcome, seriously!

I don’t expect any huge changes to the API and behaviour at this point, but the version will probably stay at 0.x for a while to leave the door open for more sweeping changes in case y’all think it’s unusable. Fingers crossed!


Thanks to the team behind gettext for creating the current de-facto localisation library in Elixir, the i18next team for giving me a ton of inspiration around the API, and my team at work that got an early look at this and gave some valuable feedback.