08 Jul 2022 ~ 5 min read

Code Splitting i18n Locales Using Dynamic Imports

Implementing internationalisation could be a daunting task for your frontend projects, but luckily tools like i18n make our lives easier by offering useful APIs that help us move forward quickly.

Disclaimer: This article expects you to be already aware of some basic concepts around react, localization, webpack, and i18n.

TLDR;— Get straight to the code react-i18n-codesplitting-locales.

Photo by Tim Mossholder on UnsplashPhoto by Tim Mossholder on Unsplash

General Architecture

In a modern frontend application, we ideally have a core CSV from which we generate our locale.json files for each language we support. Assuming we support English, French and Japanese, then we would have three locale.json respectively.

If you are using a module bundler like webpack, the rudimentary implementation of the internationalization would involve importing all the locale.json files and bundling them all together in one bundle.

General ArchitectureGeneral Architecture

The problem with this architecture is that your main.js bundle would have all the JSON files bundled together even when you do not need them.

Imagine your app is being loaded for a single language, the JSON files for the other two languages are also part of your main.js bundle, although this might not be a problem for smaller applications, as your applications scale and the number of languages you support increases, then this rudimentary implementation would slowly eat up into your performance budgets.

Hence, a better solution would be to load only the required locale files or load them on demand. And this can be achieved via a concept called Code-splitting.

Code-splitting

Code-splitting in a nutshell is the process of splitting up your code into smaller bundles based on certain criteria that consequently result in better load times.

Most modern bundlers like webpack, come with their own configurations for code-splitting during build-time. There are multiple ways in which you can enforce your webpack configuration to split your code, but in our case, we are going to use dynamic imports.

The following code-block when bundled with a modern bundler like webpack would generate a chunk for all the files in the locales directory and load them on demand based on the computed value of the variable langKey.

async function loadResources(langKey) {
  const resources = await import(`./locales/${langKey}.json`);

  // do something with the resource.
}
loadResources().then(() => console.log("resource loaded dynamically"));

In this way, our architecture remains the same but the bundler would take care of splitting our application code into smaller chunks.

Using Dynamic Imports With React-i18n

Before we get into the demo of the actual code-base, let's look at how to implement the dynamic imports with react-i18n. The official documentation recommends using a backend plugin, however, it can also be done easily with webpack as it supports dynamic imports out of the box.

When we use dynamic imports to load the file, we should be aware of race conditions between loading the resources and calling the useTranslation hook.

The i18n init method should have been complete before we execute the useTranslation hook’s methods (eg: t(‘key’)).

We can use React.lazy in combination with Suspense to await the dynamic imports before actually using the useTranslation here.

Alternatively, we can also use the I18nProvider to pass on the value of the i18n object, with Suspense to await the loading of the resources as shown here.

If the useTranslation hook is called earlier before the resources are available, you might end up with a warning of this sort —

Warning React-i18nWarning React-i18n

Demo

I bootstrapped my application quickly using this boilerplate that I built, based on the article I wrote here.

With Code-splitting

Clone and open this codebase in your favourite editor, and follow the instructions on the read me to run the project. The code is currently code-split, how do you know?

Once you install and run the project using —

npm run install
npm run serve
// visit http://localhost:3000/webpack-dev-server

And you should see the following information about the current dev-server's dist directory:

Webpack Dev-server Build DirectoryWebpack Dev-server Build Directory

And when you load the application initially and also switch the languages you should be able to see these chunks (src_locales_ja_json.js etc.,) downloaded from the network tab.

When we visit http://localhost:3000/?language=ja, we only load the language JSON information meant for Japanese and not English and French.

This is essentially code-splitting based on dynamic imports in action.

Network Tab for Chunk LoadingNetwork Tab for Chunk Loading

Now that we see that the code-splitting is in action, let's see how the project fairs without code-splitting.

Without Code-splitting

Let's go ahead and modify our i18n.ts file with the following code —

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en from "./locales/en.json";
import fr from "./locales/fr.json";
import ja from "./locales/ja.json";

const LANG_KEY: string =
  new URLSearchParams(window?.location?.search).get("language") || "en";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resources: any = { en, fr, ja };

export const init = () => {
  i18n.use(initReactI18next).init({
    resources,
    lng: LANG_KEY,
  });
};

export default i18n;

And visiting http://localhost:3000/webpack-dev-server would give us the following result —

Assets without code-splittingAssets without code-splitting

Here we can see that all our imported locale files have been bundled along with the main.js bundle, consequently increasing the bundle size of the same.