Create a React useTranslation hook to use everywhere-Part 2

Durmuş
5 min readMar 29, 2022

In the first part, we have created the useTranslation hook and added some functionality to it.

Now let’s import it in one of our components and see if it works.

The last version of the hook seems like;

//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 }
};

Usage in Home.jsx;

// src/pages/Home.jsximport useTranslation from "../hooks/useTranslation";export default function Home() {
const { t } = useTranslation();
return (
<div className="App">
<h3>{t("home.title.sub1")}</h3>
</div>
)};

As seen above, we have destructured the “t” function from our hook and called it with the stringified path(with object dot notation) to the desired value of the target language.

As told in the last section, “t” function will split the argument and loop through the selected json file to reach the value of “sub1”.

  • Although translation part works smoothly now, there is one more issue we should address. Because the initial language state is set to “en” in our hook, the language of the whole application will do so in every page refresh. And this will lead to a poor user experience.

To solve this problem, we will create a function to include a logic that will save the user preference to local storage.

export default function setInitialLanguage() {const initialLang = localStorage.getItem("currentLang");if (!initialLang) {localStorage.setItem("currentLang", "en");
return "en";
} else {
return initialLang;
};
};

This function checks the local storage first and if a language is saved, it will assign it otherwise assign a default language, in this case “en”.

Let’s add this to our hook and configure the initial language state.

//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
};
const setInitialLanguage = () => {
const initialLang = localStorage.getItem("currentLang");
if (!initialLang) {
localStorage.setItem("currentLang", "en");
return "en";
} else {
return initialLang;
};
};
export default function useTranslation() {
const [{ language, strings }, setLanguage] = useState({
language: setInitialLanguage(),
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
});
localStorage.setItem("currentLang", newLanguage);
}, [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 }
};

We can further improve this logic to include browser language detection or selection according to client ip and add some other controls to improve security.

// might be also configured according to client ipconst setInitialLanguage = () => {
// declare allowed languages to prevent manipulation from local storage
var allowedLangs = ["tr", "en"];
// check localstorage for current lang
const initialLang = localStorage.getItem("currentLang");
if (!initialLang) {
// if not first;
// detect browser language
let browserLanguage = window.navigator.language || window.navigator.userLanguage;
// assign if there is a valid browser language
if (browserLanguage && allowedLangs.indexOf(browserLanguage) > -1) {
localStorage.setItem("currentLang", browserLanguage);
return browserLanguage;
};
// or assign a default language(en)
localStorage.setItem("currentLang", "en");
return "en";
} else {// set from localStorage if not malformed
if (allowedLangs.indexOf(initialLang) > -1) {
return initialLang;
} else {// if localStorage option is malformed
// set and return the defult language(en)
localStorage.setItem("currentLang", "en");
return "en";
}
};
};

We first added a control to prevent possible malformed input injection from local storage as it can be manipulated easily.

Then, we created a browser language detection system if no user preference is saved.

Lastly, we kept our default language as “en” if none of these conditions are met.

Now that we have created a comprehensive ready-to-use hook, let’s move our helper functions to seperate directory to make it seem cleaner before creating our context Api.

  • Create a folder in the src directory called “utils” and move “getLanguageFile” and “setInitialLanguage” functions to this location. Do not forget to import them properly in our hook later.

Creating the Context Api

Provided that the custom useTranslation hook is ready to use, let’s make use of it in a simple application.

To be able to achieve it, I will create a new context and context provider in the context directory we have created earlier.

//src/context/LanguageContext.jsimport React, { createContext } from "react";
import useTranslation from "../hooks/useTranslation";
export const LanguageContext = createContext(null);export const LanguageProvider = ({ children }) => {
const { t, language, updateLanguage } = useTranslation();
return (
<LanguageContext.Provider value={{ language, updateLanguage, t }}>
{children}
</LanguageContext.Provider>
)
};

After creating the context, I have imported the useTranslation hook and sent its features to our app via context provider.

Now, let’s go to the App.js file and provide our application with these features.

//src/App.jsimport './App.css';
import { LanguageProvider } from "./context/LanguageContext";
import Home from './pages/Home';
export default function App() {
return (
<LanguageProvider>
<Home />
</LanguageProvider>
)
};

From now on, we can change the language from all parts of our app and be sure it will be applied right away.

So let’s create a simple navbar in hour “Home.jsx” and simulate language selection.

//src/pages/Home.jsximport React, { useContext } from 'react';
import { LanguageContext } from "../context/LanguageContext";
import Navbar from '../components/Navbar';
export default function Home() {
const { t } = useContext(LanguageContext);
return (
<div className="App">
<Navbar />
<div className="App-header">
<h1>{t("home.title.main")}</h1>
<h3>{t("home.title.sub1")}</h3>
<h3>{t("home.title.sub2")}</h3>
</div>
</div>
)
};

And here is the navbar component;

//src/components/Navbar.jsximport React, { useContext } from 'react';
import { LanguageContext } from "../context/LanguageContext";
export default function Navbar() {
const { t, language, updateLanguage } = useContext(LanguageContext);
return (
<div
style={{ width: '100%', height: '5rem', backgroundColor: 'grey', display: 'flex', alignItems: 'center', justifyContent: 'space-around' }}>
<div>{t("navbar.about")}</div>
<div>{t("navbar.contact")}</div>
<select value={language} onChange={(e) => updateLanguage(e.target.value)}>
<option value="en">English</option>
<option value="tr">Turkish</option>
</select>
</div>
)
};

When we update the language from navbar component, it will affect the whole application thanks to the context API we have just created.

And because the user preference is saved to local storage, this selection will persist even if the browser is closed and reopened.

In this tutorial, I tried to share how to create a custom useTranlation hook and use it in our application.

I would be more than happy to hear if there are any feedbacks or suggestions. I will provide the link to the Github repo.

You can safely fork it and customize according to your needs.

Happy hacking:)

--

--

Durmuş

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