Lazy Load Localisation with React-I18next

In the last tutorial, we have looked at how to set up basic react-i18next for translation, but it has a major drawback for large React applications which is needed to load the huge junk of language files upfront where most of the translated texts may not be needed in the initial page.

Thus, we need to explore a better way of handling large text files so that we can improve page load time.

The simplest solution is to separate the text file to smaller pieces and lazy load them when needed. Luckily, there is a library i18next-http-backend that able to help us achieve this easily.

Setup i18next-http-backend

Let’s use the following sandbox as the starter for this tutorial. In this page, we have 2 tabs that contain the short description of Luke Skywalker and Han Solo as well as the languages selections.

You notice that we have placed all the text translation in the single file, it’s okay for now as we only have a short description of 2 Star Wars’ characters. However, there will be a disaster if we have the full details of all Star Wars’ characters.

One way of splitting the text translation for this scenario will be based on the tab, which can be referred to as a component or a page in a real-world application.

To achieve this, first, let’s start by installing the i18next-http-backend package and hook it to the i18next.config.ts.

// Import i18next-http-backend
import HttpApi from "i18next-http-backend";

i18n
  .use(HttpApi) // Initialize here
  .init({
    // Other configs
    backend: {
      loadPath: "/locales/{{lng}}.json" // The URL to download the locales
    }
  });

For more backend options available, please check out the documentation.

Next, we can copy the text files from ./src/locales to ./public/locales.

Since we are going to load the texts remotely, then we can remove the resource in the config too.

// Remove import of locales locally
import en from "./locales/en.json";
import zh from "./locales/zh.json";

i18n
  .init({
    // Remove resources
    resources: {
      en: {
        translation: en
      },
      zh: {
        translation: zh
      }
    },
});

If you run the application now, you probably something the error like this.

No worries, this is normal. Since we are loading the texts remotely, it will take some times to download the text. Thus, it requires us to show some fallback UI, for example, a loading spinner or skeleton loading screen and etc.

To fix this, we can wrap the App.tsx with React.Suspense and show the fallback UI. React.Suspense also very useful for React code-splitting too if you never heard of it.

// App.tsx
render(
  <React.Suspense fallback="Loading...">
    <App />
  </React.Suspense>,
  rootElement
);

In your production application, you may consider putting the text files in CDN to improve the performance.

Introducing Namespace

We are now loading the texts from servers or somewhere, but we still have not split the texts into multiple files. This is where we can leverage namespace for the translation. As previously mentioned, we can split the texts based on the tab, so let’s do that now.

// ./public/locales/app/en.json
{
  "title.language": "Select your language",
  "title.tab1": "Luke Skywalker",
  "title.tab2": "Han Solo",
}

// ./public/locales/app/zh.json
{
  "title.language": "选择你的语言",
  "title.tab1": "路克·天行者",
  "title.tab2": "韓·蘇羅",
}

// ./public/locales/tab1/en.json
{
  "description": "Luke Skywalker was a Tatooine farmboy who rose from humble beginnings to become one of the greatest Jedi the galaxy has ever known. Along with his friends Princess Leia and Han Solo, Luke battled the evil Empire, discovered the truth of his parentage, and ended the tyranny of the Sith"
}

// ./public/locales/tab1/zh.json
{
  "description": "路克·天行者是一名為乔治·卢卡斯导演的著名科幻电影《星球大战》正傳三部曲中的主要人物。由馬克·漢米爾飾演。為反抗軍同盟與銀河帝國抗爭的重要角色,同時也是反抗軍領袖莉亞公主的雙胞胎哥哥、走私客韓蘇洛的朋友、絕地大師歐比王·「班」·肯諾比及尤達大師的學徒以及墮落原力黑暗面的絕地安納金·天行者(黑武士)與前納布星女王佩咪·艾米達拉的兒子"
}

// ./public/locales/tab2/en.json
{
  "description": "Smuggler. Scoundrel. Hero. Han Solo, captain of the Millennium Falcon, was one of the great leaders of the Rebel Alliance. He and his co-pilot Chewbacca came to believe in the cause of galactic freedom, joining Luke Skywalker and Princess Leia Organa in the fight against the Empire."
}

// ./public/locales/tab2/zh.json
{
  "description": "走私者。恶棍。英雄。千年猎鹰队的船长汉·索罗(Han Solo)是反叛联盟的伟大领导人之一。他和他的副驾驶丘巴卡(Chewbacca)相信银河自由,并与卢克·天行者(Luke Skywalker)和莱娅·奥加纳公主(Leia Organa)一起抗击帝国"
}

Here, we have created 3 namespaces: app, tab1 and tab2. We also move the description text to respective text files. In this case, the descriptions will only be loaded when the tab switching.

Next, we need to change the way how we load the locales.

// i18next.config.ts

i18n
  // Other codes
  .init({
    backend: {
      loadPath: "/locales/{{ns}}/{{lng}}.json" // Change this to follow the correct path
    }
  });

In addition, we will need to tell i18next to use the namespace and update the translation key.

// App.tsx
const { t } = useTranslation('app');
// t('tab1.title') -> t('title.tab1')
// t('tab2.title') -> t('title.tab2')

// tab1.tsx
const { t } = useTranslation('tab1');
// t('tab1.description') -> t('description')

// tab2.tsx
const { t } = useTranslation('tab2');
// t('tab2.description') -> t('description')

Let’s see the result now

If you open with the new tab and open the inspector, you should able to see new en.json is loaded when you switch tab.

Nice! It’s work as expected, our application can now lazy load the translation text files and the performance has been improved.

I hope you find this tutorial useful and you can apply to your React application.

Leave a Reply