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.
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
Your file structure will look like this image below:
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
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;
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>
);
}
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',
},
};
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
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>
);
}
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": "شاحنات"
}
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,
},
};
};
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>
);
}
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
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:
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:
yarn add stylis-plugin-rtl stylis
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>
);
}
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;
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>
);
}
Here is a link to the full code on github.
You can follow me on Twitter
Stay safe and Happy codding.
Top comments (4)
Amazing post! Thanks for writing this 💖
This is really good.
You said "yarn add react-rtl" but the correct is "yarn add react-intl", right?
Thank you for this tutorial, really good :)
Realy Amazing post! I have a question. I using "Chakra UI - translate" in me project. I want to add links to some words in the translated texts in my json file. How can I do it?