i18n. How we deal with translations

Bogdan Plieshka
Zattoo’s Tech Blog
7 min readFeb 18, 2021

Zattoo is a multicultural product, made by people talking different languages for people watching TV in different languages. Translations cover the application interface, stream audio, and subtitles. This article focuses on how we manage, consume, process, and optimize interface languages.

Manage translations

We have multiple projects supporting multiple languages accumulating thousands of translation strings that are constantly being added, changed, and removed. This work involves professional translators and product teams needing to efficiently collaborate in some space.

There are plenty of services in the market that can help to solve this: Lokalise, Crowdin, Phrase, Transifex, Smartling, and many more.

For more than a decade, we have collaborated with webtranslate.it (WTI).

It might not be the fanciest service on the market, but it’s a simple and stable solution with excellent and responsive support.

Receiving translations

From the development side, our goal in working with the translation platform is pretty straightforward and can be simplified to one routine: we want to define some phrases and receive all possible translations for them.

Configuration

However, just saying we want to receive translations for a project is not enough because we are working with multiple projects, and one project could depend on another or use some common translations with other projects.

We express relationships between our projects in the configuration:

where every object refers to a unique project and contains:

  • name as a back-reference to project id
  • key as a private key to work translations platform API
  • wtiID as public WTI platform project id used to build deep links to translations (might be obsolete in other platforms)
  • depends as a list of project dependencies

Once we are working with the App project, we expect to receive Common and App translations

Pull translations

To pull sources, we use a Node.js script that uses the node-fetch package and fetches translations in JSON format via the translations platform REST API.

When we need to update translations while working on a project or to schedule updates in CI, we can run this seeding script:

> npm run i18n:install

That will take care of not just receiving raw source but also of further transformation and optimization steps.

Seeding

Normalize translations

Depending on the translation platform, we can receive translation sources in many different formats. With WTI, we ask for JSON and receive an abstract tree:

While the schema is complete, it’s not really a format we want to work with. Traversing a deeply nested tree is an expensive operation, so we flatten it to key-value pairs to work with translations more efficiently.

Untranslated

Our translations are done by a team of translators spread around the world, and living in different time-zones, so it takes some time to get them done. Some translations will be left void until the translators complete them.

However, our development needs to continue and not wait until translations are ready. That leads to the question of how we should proceed with development if some translations are missing? Leave it void or put a placeholder?

Fallback

We can fall back to English used by default in our specifications.

Collecting untranslated metrics

Having an untranslated fallback is totally fine for development but unacceptable for delivery to the end-user.

Therefore, while we are doing fallback to the default language, we collect which translations are actually missing.

These stats are handled by git together with the project, so later we can use this information for release acceptance.

Lint translations

Not all translation platforms are bullet-proof, and translators can make mistakes, which is fine. But it’s not fine to be unaware of it, so we lint our translations and verify that translation strings are consistent.

Store translations

Once we are sure our translation is normalized, completed, and validated, we can store them under git together with the project.

So we have translations we are ready to work with, but how to access them?

Accessor functions

To access them, we generate JavaScript accessor functions

that use the `translate` selector, which takes the proper translation value depending on the currently used language.

These functions contain tips about the original translation key they use, the original English text behind it, and a deep-link to the exact page in the translation platform.

You might think it’s strange to generate JavaScript, but this approach has a big advantage. It gives us full control of the translations.

We know which translations are not used and can eliminate them as dead code. This is very interesting because we build the project for multiple tenants using a different batch of features.

They’re also type-safe, so there’s no chance of providing wrong data to the translation function with parameters.

Bundle

We are done with fetching and seeding and can develop a product. However, we are far from the finish because this is not exactly what we want to distribute to our users.

Tenants

As you might know, Zattoo is not just a popular streaming service provider reachable by the zattoo.com address.

It is also the platform for many tenants seeking the best experience for their users.

Every tenant has a unique set of presets including different content, features, themes, and configurations. And every application we distribute is individually bundled with customer choices. For translations these choices include next:

Overrides

Any tenant can override any translation with one that better suits their business.

So we accept overrides and replace matching translations accordingly.

Non-supported languages

Some of the tenants are big and cover multiple countries, and they might be interested in all languages that we provide. Some of the tenants are more local and interested only in a subset of languages based on the content they provide.

So there is a filtration step that assures no unused languages are delivered to users.

Formality

Some tenants prefer speaking formally to their users, and some prefer informal language.

So we filter not-used formality.

Namespace

As you might remember, one project might receive translations from multiple sources, but in the end, we are interested in providing a single bundle of translations per language. But if we merge sources, we might have naming collisions and unwanted overrides, so we prefix our keys with the project id in advance.

Once it’s done, our translations bundle is ready to be distributed, and we serve them as static .json files. The application fetches one of them before the first start and takes it from the cache for subsequent starts, thanks to Service Worker, which allows us to avoid network requests.

Production Optimization

One last thing. As you might notice, our key-value pairs bundle looks quite big. It contains repeatable prefixes and the keys themselves are needed only for humans. What if we could throw out keys and leave only values?

And this is exactly what we did, with the help of a home-written Webpack Plugin running on assembling a production-like environment.

Translation bundle size was reduced by more than 60%.

In Summary

Our flow might look complex at first glance, but comparing it to the complexity we serve dealing with multiple projects and tenants, it might not be that scary. In the end, everything that was described here can be visualized in one block-scheme pipeline.

--

--

Bogdan Plieshka
Zattoo’s Tech Blog

Engineering Manager at Zattoo | React Berlin Meetup Organizer | Crafting symbols for over a decade 👨‍💻✨