DEV Community

Cover image for Setup an internationalization (i18n) routing web app with Nextjs and Chakra UI
Ugbechike
Ugbechike

Posted on • Updated on

Setup an internationalization (i18n) routing web app with Nextjs and Chakra UI

Hello folks!! In this article, I will show you how to set up a multilingual web application using Nextjs and ChakraUI.

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
Screen Shot 2020-12-08 at 6.14.36 PM

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:

yarn create next-app
yarn add typescript
yarn add -D @types/react @types/react-dom @types/node
Enter fullscreen mode Exit fullscreen mode

Your file structure will look like this image below:
Screen Shot 2020-12-02 at 10.25.41 AM

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.

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

yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

module.exports = {
    i18n: {
        locales: ['en', 'ar'],
        defaultLocale: 'en',
    },
};
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

// en.json
{
  "item_title": "VANS"
}
// ar.json
{
  "item_title": "شاحنات"
}
Enter fullscreen mode Exit fullscreen mode

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.

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,
  },
 };
};
Enter fullscreen mode Exit fullscreen mode

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:

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

Enter fullscreen mode Exit fullscreen mode

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:


yarn add react-rtl

Enter fullscreen mode Exit fullscreen mode
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);
};
Enter fullscreen mode Exit fullscreen mode

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:


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

Enter fullscreen mode Exit fullscreen mode

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:

yarn add stylis-plugin-rtl stylis
Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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.


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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here is a link to the full code on github.
You can follow me on Twitter

Stay safe and Happy codding.

Top comments (3)

Collapse
 
segunadebayo profile image
Segun Adebayo

Amazing post! Thanks for writing this 💖

Collapse
 
ecj222 profile image
Enoch Chejieh

This is really good.

Collapse
 
laurindo profile image
Daniel Laurindo

You said "yarn add react-rtl" but the correct is "yarn add react-intl", right?
Thank you for this tutorial, really good :)