Create a React useTranslation hook to use everywhere-Part 1

Durmuş
6 min readMar 29, 2022
translation

Recently, we have been assigned to rewrite our Node-React project with a modern design and most up-to-date practices. When we took the job we got both excited and nervous. Because, we would gain a lot of experience if we could succeed it. On the other hand, it was a huge project to start from scratch and there was a lot to consider in terms of npm packages we had used so far.

After restructuring the backbone of both server and client side, our intention was to move forward with the minimum number of packages as possible as we can. Because we had had really bad experiences with their management in our previous version.

We did a lot of research, put a lot of thought into which libraries are strictly required in our backend and frontend.

And when it was time to add language support, things became a little more challenging. It was my job to find a solution for this one. And all the tutorials I read was showing just one single direction: The famous i18next.

Because we had an oath:) not to use packages unless we have to, I started to search deeper and deeper. Actually, there were some articles about creating a custom useTranslation hook. But almost all of them lacked a comprehensive solution for a larger and scalable web application.

In the end, I could manage to create a full-fledged custom useTranslation hook with the support of my teammates. And because I believe the importance of sharing knowledge, I decided to write a tutorial explaining all the steps and share it.

As it will be a full-featured project, I will break it into two parts to make each part clear and simple enough to grasp.

In the first part, I will focus on creating the hook itself as well as establishing the project struture. In the last part, I will add some extra features to our hook and show you how to use it in your application with context API.

Enough talk, right? Let’s jump right in.

First of all, open a new terminal in your preferred directory and create a brand-new react app with the super-handy npx create-react-app <name> command.

npx create-react-app react-translation-hook

Then, open the newly-created project with your code editor, toggle terminal and run;

npm start

Go to address http://localhost:3000/ in your browser. If you see that star-like react icon spinning, we are good to go:)

First let’s set the grounds for our project architecture;

  1. Create a new folder in your src directory called “hooks” and a file inside named “useTranslation.js”.

Maybe you are curious about naming convention. So just to make it clear; components which return jsx are named with pascal case whereas hooks which return objects, arrays or variables are named with camel case convention and starts with a special prefix : “use”.

2. Create another folder called “languages” in src directory. Then, depending on your language preferences, create two seperate folders giving their international abbreviations as their names inside the newly-created languages directory.

In my case I will use “en” for English and “tr” for Turkish.

And finally inside each one of these folders, create a json file called “strings.json”.

3. Create a folder called context and put a “LanguageContext.js” file inside to set up our context API later.

4. And finally create the last two folders in src directory which will be named as “pages” and “components”.

Create a “Home.jsx” file inside your “pages” folder and a “Navbar.jsx” file in your “components” folder.

After all, our folder structure should be as follows:

Let’s create the basic version of our hook and improve it;

  1. First, we will create the useTranslation hook and configure it to return a “language” state, a “setLanguage” function for now.
//src/hooks/useTranslation.jsimport { useState } from 'react';
export default function useTranslation() {
const [language, setLanguage] = useState("en");
return { language, setLanguage }
};

Above is the very basic version of our hook. Now, lets address the issues one by one and configure our code accordingly.

  • We have to find a way to access the key value pairs of the selected language, so that our hook can translate and return it to the users. And this should be handled in accordance with language selection. We keep the language preference in state because we want the whole process to restart&rerender after it changes.

With these being said, we might fill json files for preferred languages and configure our code as;

//src/hooks/useTranslation.jsimport { useState, useEffect } from 'react';const getLanguageFile = async({ language }) => {
const module = await import(`../languages/${language}/strings.json`);
return module.default
};
export default function useTranslation() {
const [{ language, strings }, setLanguage] = useState({
language: "en",
strings: {}
});
const updateLanguage = async (newLanguage) => {
const fetchedStrings = await getLanguageFile({ language: newLanguage });
setLanguage({
language: newLanguage,
strings: fetchedStrings
});
});
useEffect(() => {
updateLanguage(language)
}, [language]);
return { language, updateLanguage }
};

I have created a simple async function to fetch language strings and expanded the language state to include these strings. I have also improved my language setter function to handle more than just setting the language.

This version might work but as you can guess there might be some performance issues.

  • To prevent unnecessary async calls to our “getLanguageFile” function, we might want to wrap it with useCallback, and add a simple control inside updateLanguage function to prevent extra rerenders.
//src/hooks/useTranslation.jsimport { useState, useEffect, useRef, useCallback } from 'react';const getLanguageFile = async({ language }) => {
const module = await import(`../languages/${language}/strings.json`);
return module.default
};
export default function useTranslation() {
const [{ language, strings }, setLanguage] = useState({
language: "en",
strings: {}
});
const isJsonFetched = useRef(false);const updateLanguage = useCallback(
async (newLanguage) => {
if (isJsonFetched.current && newLanguage === language) return;
const fetchedStrings = await getLanguageFile({ language: newLanguage });
isJsonFetched.current = true;
setLanguage({
language: newLanguage,
strings: fetchedStrings
});
}, [language]);
useEffect(() => {
updateLanguage(language)
}, [language, updateLanguage])
return { language, updateLanguage }
};

In addidition to useCallback, I have used a simple flag created with useRef and an equation control between the language in state and the language to be changed to prevent unnecessary rerenders.

  • Now that we are able to set language and import its json file, the last step is to create a translation function which will handle the actual translation. It will be super simple and will just find the desired value of the target language by looping over the whole json file.

I will name it as “t” to make it cleaner while calling it.

const t = (translation) => {
// example argument
'home.title.main'
// split keys and assign to an array
var keys = translation.split(".");
var result = strings;
// loop through to unnest the desired value
for (let i = 0; i < keys.length; i++) {
result = result[keys[i]]
if (!result) break;
};
// return value or chained keys if malformed
return result || translation
};

The function accepts a string which includes a path to the desired value created using object dot notation.

Here is the last version of our useTranslation hook.

//src/hooks/useTranslation.jsimport { useState, useEffect, useRef, useCallback } from 'react';
const getLanguageFile = async({ language }) => {
const module = await import(`../languages/${language}/strings.json`);
return module.default
};
export default function useTranslation() {
const [{ language, strings }, setLanguage] = useState({
language: "en",
strings: {}
});
const isJsonFetched = useRef(false);const updateLanguage = useCallback(
async (newLanguage) => {
if (isJsonFetched.current && newLanguage === language) return;
const fetchedStrings = await getLanguageFile({ language: newLanguage });
isJsonFetched.current = true;
setLanguage({
language: newLanguage,
strings: fetchedStrings
});
}, [language]);
const t = (translation) => {
var keys = translation.split(".");
var result = strings;
for (let i = 0; i < keys.length; i++) {
result = result[keys[i]]
if (!result) break;
};
return result || translation
};
useEffect(() => {
updateLanguage(language)
}, [language, updateLanguage])
return { t, language, updateLanguage }
};

In the next part, we will improve our hook by adding an initial language detection feature and learn how to use this hook in our components.

Go to the the next part…

--

--

Durmuş

I'm an enthusiastic Web developer. I like learning new technologies and teaching them to other people.