Setup an internationalization (i18n) routing web app with Nextjs and Chakra UI
Tags: typescript, react, internationalization, multilingual
Published: 2020-12-07 23:23:29

Hello folks!! In this article, I will show you how to set up a multilingual web application using [Nextjs](https://nextjs.org/) and [ChakraUI](https://chakra-ui.com/).

## INTRODUCTION

A **Multilingual** web application is an application that provides content in more than one language, for example, English, Arabic, French, etc.

There are businesses benefits in building a multilingual web application, such as expanding the client base and securing sales volume.
 
We will build a demo application to showcase how to render content to left-to-right (LTR) and right-to-left (RTL) languages based on the client locale.

The demo app will look like the image below.
![Screen Shot 2020-12-08 at 6.14.00 PM](https://dev-to-uploads.s3.amazonaws.com/i/v375w9mfyo1pkvqfhzfp.png)
![Screen Shot 2020-12-08 at 6.14.36 PM](https://dev-to-uploads.s3.amazonaws.com/i/s48xiwh1cf45wi7h09ir.png) 
 

This tutorial will span through two steps, which includes:

**Step 1:** Setting up Nextjs, ChakraUI, and other dependencies.

**Step 2:** Setup Internationalization for the application.

Let's get started.

### Step 1: Setting up Nextjs and ChakraUI.

**NextJs** is a React Framework used to build server-side rendered and static web applications. 

To set up NextJs, run this command in your project directory:

```bash
yarn create next-app
yarn add typescript
yarn add -D @types/react @types/react-dom @types/node
```

Your file structure will look like this image below: 
![Screen Shot 2020-12-02 at 10.25.41 AM](https://dev-to-uploads.s3.amazonaws.com/i/4zurs2hdeywavuc84j19.png)

### Setup Chakra UI

**Chakra UI** is a simple, modular and accessible component library that gives you the building blocks you need to build your React applications. [Check out the docs] (https://chakra-ui.com/docs/getting-started).

To setup Chakra UI, install the package and its peer dependencies

```bash
yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion
``` 

To use Chakra UI we need to set up its theme provider.

Open `pages/_app.tsx` and wrap the application with `ChakraProvider` as shown below:

``` tsx
import { ChakraProvider } from "@chakra-ui/react";
import { AppProps } from "next/app";

function MyApp(props: AppProps) {
  const { Component, pageProps } = props;
  return (
    <ChakraProvider>
      <Component {...pageProps} />
    </ChakraProvider>
  );
}

export default MyApp;
```

To demonstrate the feature of Chakra UI, let's build a card component:

```tsx
import React from "react";
import { Box, Text, Container } from "@chakra-ui/react";

export const data = [
  {
    image_url: "https://cutt.ly/ehEjUVT",
    title_en: "Sample shoe",
    title_ar: "حذاء عينة",
    price: 20,
    currency_en: "AED",
    currency_ar: "درهم",
  },
  {
    image_url: "https://cutt.ly/ehEjUVT",
    title_en: "Christmas shoe",
    title_ar: "حذاء عيد الميلاد",
    price: 30,
    currency_en: "AED",
    currency_ar: "درهم",
  },
  {
    image_url: "https://cutt.ly/ehEjUVT",
    title_en: "Sample booth",
    title_ar: "كشك عينة",
    price: 40,
    currency_en: "AED",
    currency_ar: "درهم",
  },
];

type CardPropType = {
  children: React.ReactNode;
};

// product card component
const Card = (props: CardPropType) => {
  const { children } = props;
  return (
    <Box
      borderWidth={1}
      borderTopRightRadius={10}
      maxW={400}
      paddingY={"10px"}
      paddingX={"10px"}
      my={"10px"}
    >
      {children}
    </Box>
  );
};

export default function Home() {
  return (
    <Container>
     {data.map((item, index) => {
       return (
        <Card key={index}>
              <img
                src={item.image_url}
              />
              <Text fontSize="xl">Sample shoe</Text>
              <Text fontSize="xl">
                {currency} {price}
              </Text>
         </Card>
       )
     })
    </Container>
  );
}
``` 
Run your server using the command `yarn dev` to see the changes.

### Step 2: Setup Internationalization

To add multilingual support to NextJs, create a `next.config.js` file in the root of the application with this config:

```tsx
module.exports = {
    i18n: {
        locales: ['en', 'ar'],
        defaultLocale: 'en',
    },
};
```

The `locales` array is used to specify the languages the application support. The `defaultLocale` specify the fallback language.

Create a `_document.tsx` file inside the `pages` directory, this `_document.tsx`  gives us access to the body element which will be used to change the HTML `dir` (direction) and `lang` attributes.

```tsx
import Document, {Html, Head, Main, NextScript, DocumentContext} from 'next/document'

class MyDocument extends Document {
    static async getInitialProps(ctx: DocumentContext) {
        const initialProps = await Document.getInitialProps(ctx);
        return { ...initialProps }
    }

    render() {
        const {locale} = this.props.__NEXT_DATA__ 
        const dir = locale === 'ar' ? 'rtl' : 'ltr';
        return (
            <Html>
                <Head />
                <body dir={dir} lang={locale}>
                <Main />
                <NextScript />
                </body>
            </Html>
        )
    }
}

export default MyDocument
```

### CHANGING CONTENT BASED ON LOCAL.

## The simple approach

A simple way we update the content based on language is to leverage NextJs' `locale` embedded in the `useRouter` hook. 

Let update the product tile in `pages/_index.tsx` file with Arabic text when the locale is `ar`.

```tsx
export default function Home() {
  const router = useRouter();
  const { locale } = router;

  return (
    <Container>
      {data.map((item, index) => {
       return (
        <Card key={index}>
              <img
                src={item.image_url}
              />
              <Text fontSize="xl">
               {locale === 'ar' ? كشك عينة : Sample booth }
              </Text>
              <Text fontSize="xl">
                {currency} {price}
              </Text>
         </Card>
       )
     })
    </Container>
  );
}
```

>To test this, add `/ar` to the route. example:  >http://localhost:3000/ar


## A better approach.

The solution we have currently involved changing the content using a ternary operator, which is efficient when building a page in NextJs.

Another approach is to create a static file containing `ar.json` and `en.json`, and leverage NextJs `getStaticProps` to load the correct file based on locale.

**Step 1 Create a Static file:** 
Create two files `en.json and ar.json` in `public/static` directory.

```json
// en.json
{
  "item_title": "VANS"
}
// ar.json
{
  "item_title": "شاحنات"
}
```

**Step 2 getStaticProps Function:** 
Add a `getStaticProps` function inside `pages/index.tsx` file. 
Here we can read the document using `Node` file system (fs) and return content as a prop to the component, which will also be available in the window object.

```tsx
export const getStaticProps: GetStaticProps = async (ctx) => {
 const { locale } = ctx;
 const dir = path.join(process.cwd(), "public", "static"); 
 const filePath = `${dir}/${locale}.json`;
 const buffer = fs.readFileSync(filePath); 
 const content = JSON.parse(buffer.toString());
 return {
  props: { 
   content,
  },
 };
};
```

At this point, we have access to the content props in `Home` component which returns an object containing the static file for the current locale.

To use this approach, update the `Home` component:
 
```tsx
export default function Home({content}) {
  
return (
    <Container>
      {data.map((item, index) => {
       return (
        <Card key={index}>
              <img
                src={item.image_url}
              />
              <Text fontSize="xl">
               {content.item_title}
              </Text>
              <Text fontSize="xl">
                {currency} {price}
              </Text>
         </Card>
       )
     })
    </Container>
  );
}

```

>To test this, add `/ar` to the route. example:  >http://localhost:3000/ar


### A Robust approach for large applications.

To manage multilingual content for large applications with multiple pages and component, `useContexts` might not be enough, we need a global function we can pass the string id then get the translated value.
 
Create a file `trans.tsx` in the root of the app, then create a `trans` function. 
This `trans` function will leverage a plugin `react-rtl` to transform the content and return the translated value.

Install the plugin:

```sh

yarn add react-rtl

```
 
```tsx
import { createIntl, createIntlCache, IntlCache } from "react-intl";
const cache: IntlCache = createIntlCache();
const intlProv = {};
const content = {};

function getMessages(lang: string) {
  if (!content[lang]) {
     if(typeof window !== "undefined") {
      //@ts-ignore
      content[lang] = window.__NEXT_DATA__?.props.pageProps.content;
      }
   }
  return content[lang];
}

function getIntlProvider(lang: string) {
  if (!intlProv[lang]) {
    intlProv[lang] = createIntl({
     locale: lang,
     messages: getMessages(lang),
     onError: () => {},
    },
    cache // optional
   );
  }
return intlProv[lang];
}

export const trans = (id: string, values?: any) => {
let locale: string;
if(typeof window !== "undefined") {
   //@ts-ignore
   locale = window.__NEXT_DATA__?.locale;
}
 const intl = getIntlProvider(locale);
 return intl.formatMessage({ id }, values);
};
```

We created `getMessages` and `getIntlProvider` functions, let's explain what they do:

**getMessages** function is responsible for getting the content from the window object we saved earlier from our getStaticProps Function.

A **getIntlProvider** function will utilize the react-intl we installed to translate this content from the `getMessages` Function based on the current language.

To use this approach, update the `Home` component:

```tsx

export default function Home({content}) {
  
return (
    <Container>
      {data.map((item, index) => {
       return (
        <Card key={index}>
              <img
                src={item.image_url}
              />
              <Text fontSize="xl">
               {trans('item_title')}
              </Text>
              <Text fontSize="xl">
                {currency} {price}
              </Text>
         </Card>
       )
     })
    </Container>
  );
}

```

>To test this, add `/ar` to the route. example:  >http://localhost:3000/ar


Notice that some styles are not flipped to match Arabic `rtl`, for example, the `borderTopRightRadius` did not change to `borderTopLeftRadius`.

To solve this, because Chakra UI uses emotion, we can add a `stylis` plugin to efficiently transform the styles.

Install the plugin: 

```sh
yarn add stylis-plugin-rtl stylis
``` 

Create a file called `rtl-provider.tsx`. Then create a `RtlProvider` component which will utilize `stylis-plugin-rtl`.

```tsx
import { CacheProvider } from "@emotion/react";
import createCache, { Options } from "@emotion/cache";
import React from "react";
import { useRouter } from "next/router";
import stylisPluginRtl from "stylis-plugin-rtl";

export type LangDirection = "rtl" | "ltr";

type CreateCacheOptions = {
  [K in LangDirection]: Options;
}

const options: CreateCacheOptions = {
  rtl: { key: "ar", stylisPlugins: [stylisPluginRtl as any] },
  ltr: { key: "en" },
};


type RtlProviderProps = {
  children: React.ReactNode;
};

export function RtlProvider(props: RtlProviderProps) {
  const { locale } = useRouter();

  const { children } = props;
  const direction = locale == "ar" ? "rtl" : "ltr";

  return (
    <CacheProvider value={createCache(options[direction])}>
      {children}
    </CacheProvider>
  );
}

```

Navigate to `pages/_app.tsx` file, wrap the `<App/>` component with the `RtlProvider` we created.

```tsx
import { ChakraProvider } from "@chakra-ui/react";
import { AppProps } from "next/app";
import { RtlProvider } from "../rtl-provider";

function MyApp(props: AppProps) {
  const { Component, pageProps } = props;
  return (
    <ChakraProvider>
      <RtlProvider>
        <Component {...pageProps} />
      </RtlProvider>
    </ChakraProvider>
  );
}

export default MyApp;
```

Restart your application server and add the `ar` locale to the route: `http://localhost:3000/ar`. 
Notice that the `borderTopRightRadius` has transformed to `borderTopLeftRadius`.

We can currently switch our application from LTR to RTL based on the locale.

We can spice the code up by adding a button to change the language directly from the route.

```tsx

export default function Home({content}) {
  
return (
    <Container>
       <Button
          bg={"tomato"}
          display={{ base: "none", md: "flex" }}
          onClick={async () => {
            await router.push("/", "/", {
              locale: locale === "en" ? "ar" : "en",
            });
            router.reload();
          }}
        >
          {trans("change_app_language")}
        </Button>
      {data.map((item, index) => {
       return (
        <Card key={index}>
              <img
                src={item.image_url}
              />
              <Text fontSize="xl">
               {trans('item_title')}
              </Text>
              <Text fontSize="xl">
                {currency} {price}
              </Text>
         </Card>
       )
     })
    </Container>
  );
}
``` 

Here is a link to the full code on [github](https://github.com/ugbechike/i18n-app).
You can follow me on [Twitter] (https://twitter.com/iAmCodefreak)

Stay safe and Happy codding.