How to Structure and Scale Frontend Translations (with practical examples)

Reginaldo Junior
BetaPage
Published in
8 min readSep 30, 2019

Hello folks,

This time I am here to show you something that almost no one talks about: how to structure and scale frontend translations?

There are tons and tons of content about how to translate text and how to use pluralization and other fancy resources but no one never talks about how should you structure these translation files? I mean, if you work with more people on your project, storing all the translation strings into one big, ugly JSON file is not an option, why not? excellent question, come with me to find out.

The first thing you may be asking yourself is “why not to store all the strings in one file as all the packages show on the docs? is this guy mad?”, well, maybe I am, but what I'm gonna tell you is that if you store all your translation strings in one file and use a VCS (which I hope you do, for the sake of your team), chances are that you are struggling with code conflicts every single time you try to merge two branches, and why is that? simple, because everyone is editing one single large translation file, and that makes it impossible to maintain the file integrity across all developers.

Let’s illustrate the problem before we go into the solution, imagine that you’ve assigned two of your developers to create a user profile application, so we do have information about the user, and the profile, we will be using the i18next translation structure on the examples here, but you can adapt this to your translation solution, so the basic structure of our translation file will be something like:

This is all it requires to store our translation strings in i18next, and that’s okay to keep in one single file, things start to become complicated when the boss decides to add the create posts ability to the users, and also, to follow people they like, so we have two tasks, for two developers to do.

The developer in charge of creating the posts feature will modify the translation file in his branch, so his file will be something like:

And the developer in charge of the follow people feature will modify his file on his branch to something like:

And the time has come, when you have to merge both branches into the master branch, so you first merge the posts feature, and everything goes just fine, but then, when you try to merge the follow feature branch, you will face the following conflict:

This is what happens when two people edit the same file and try to merge them, and it’s not a big deal to fix the conflict and continue working, so the file would be merged like this:

That was pretty easy, right? but this was an easy task, with just two developers, imagine that your team is bigger, or that you have more tasks to merge together, this translation file will have conflicts on every single pull request, just because everyone is messing around with different versions of the file.

Now that I've presented you the problem, here goes the solution

Don’t keep all the translation into one single file.

And that’s the end of the article, thanks for reading.

Okay, you caught me, that’s not the end, but we will get there, so, how do we split the translation files into other files so no one screws other people files?

If you are using create-react-app, webpack, or some other bundler that allows you to read project files dynamically, then you have a very easy way to do this, and I'll show you exactly what you have to do, but first, I'll show you how your folder structure will look like:

So let’s explain this structure:

  • Translations: this folder will hold all of your translation files
  • index.js: this file will be in charge of auto-import all of the translation files and merge them properly into one big javascript object to make it available in the i18next instance.
  • en-US and pt-BR: these are language folders, as the objective here is to allow modularization, all the folders have the same files
  • follow.js, general.js, post.js and user.js: these are translation files, we will store our locale strings on them, split on their entity, for example for posts feature we have created a posts.js file or the follow.js file for the follow people feature.

And how these files look like?

So, as you can see, the files only store the information they need to, and that way, our users would never need to touch the same file at the same time.

Now it’s time to merge all of these files together into one large object and provide it to the i18next instance, this process can be replicated using any translation provider, just adapt it to your needs.

The i18next requires the language resources to be provided in the following structure:

[language]: {
[namespace]: {
[key]: "translation"
}
}

for example:

'en-US': {
translation: {
user_name: 'user name'
}
}

And currently our code is split into multiple files, so how do we merge them together? require.context is the key!

required.context allows you to dynamically import files on your application, so the first thing we are going to create on our translations/index.js file is to create the function that will get the files:

And what the require.context does receive as param ? first the folder we are looking for, then a flag to indicate whether we are using subdirectories on the search or not, in this case, we do, and last but not least, the regExp for the files we are looking for, in this case, any file that ends with .js

So, if you execute this function you will have as a result what we call a context

you can read more about that here.

With this content we can now list the files, and also import them, for now, let’s just check if everything is working fine:

the result of context.keys() should be:

./en-US/follow.js
./en-US/general.js
./en-US/post.js
./en-US/user.js
./index.js
./pt-BR/follow.js
./pt-BR/general.js
./pt-BR/post.js
./pt-BR/user.js

The .keys() method returns all the file names that match our regExp, so now, what we need to do? We need to filter our files so that we don’t accidentally import the index.js file (that would cause an error since we would be importing the same file again and again), let’s do this:

Now the files variable hold a list of files without the index.js file, and that’s exactly what we need, now we just need to import the content of the files and merge them together, your code should be like:

Okay this code may be a bit scary, but we will break it down, first we reduce the file names we have already filtered before, and then we import all the file translations using:

const fileTranslations = context(fileName)

After that we get the language from the file, usually, this will be the first block on the filename, so for a file ./pt-BR/user.js the language will be pt-BR, so we just remove the ./ and the .js which leaves us with pt-BR/user, now the only thing we need to do is to split this into an array on every occurrence of / so we will have an array like [“pt-BR”, “user”]

Thas what we do here

const [language] = fileName.replace('./', '').replace('.js', '').split('/');

After that, we only need to get the current language translation blocks

const languageTranslations = get(translations, language, {});

And merge the translations together

return {
...translations,
[language]: {
...merge(languageTranslations, fileTranslations)
}
}

Now you may notice that we used two helper functions from our friend lodash that is not required, but I strongly recommend you to use it because it’s a lot easier to deeply merge two objects using merge function than rolling your own custom made solution.

Well, at the end of this code you will have the original object you worked with, but split into modules and that allows you to scale up your team without worry about stupid conflicts just because everyone is editing the same file.

Now we can import our translation resources using i18next and react that would be like:

And then you could use it on your component like:

That’s all folks, this was a really long article and I hope you’ve enjoyed it, any questions please let me know.

Oh, one last thing, this solution seems a bit opinionated about which tech you use for translation (i18next in this case), but if you do pay attention to the concepts and the basic idea, you can use it with any translation service on your frontend.

Source code: https://github.com/BRKsReginaldo/translation-example

If you liked this content you may also like my other article: Redux without Switch Cases (and some other tips)

--

--

Reginaldo Junior
BetaPage

I can lose everything, but I'll never lose what I've learned on the path till here