Learn moreKontent.ai Horizons is back! Secure your free spot today.
Petal
How to improve your project with multilingual capabilities

How to unlock multilingual capabilities of your React website

At first, we create our app in English. We face adding multilingual support when the project grows or needs to serve people from countries that have multiple official languages. How can you tackle this with React and headless CMS?


Ivan KiráľSep 6, 2022

You started small – you have a headless CMS as your content hub and your React website as a single channel. As time passed, you found out that you needed more channels and languages to reach new markets and be more accessible. This article will help you set up your React site to be multilingual.

Project Overview

The application we will create will use React in combination with Typescript. I recommend using Create React App with a Typescript template as it sets up some of the dependencies automatically. If you want to do it manually, bear in mind we’ll use React hooks and, therefore, in React in at least version of 16.8. We will also need the following libraries to manage the React application successfully: 

npm i  @kontent-ai/delivery-sdk react-router-dom@6 react-intl  universal-cookie --save

If you are already familiar with React Router, the new version comes with a few changes, such as that routes are selected based on the best match instead of being traversed in order. For a detailed change log, see React Router | Upgrading from v5.

Using headless content storage

In the example, we’ll fetch content from the headless CMS Kontent.ai. It should contain a couple of movies and also some other pages such as About us and Home. Our project will be a simple app consisting of a few pages – Home, About, Movies, and Movie detail. The URLs will be analogical except for the Home page whose URL will be simple "/" and Movie, which we will identify as /movies/:codename.

The URL structure will slightly change when we add another language. The most typical approach is to use language prefixes, so our URLs will then look like this: /en/about-us. Before we continue, create a new project in Kontent.ai and note the project ID and Management API key from the project settings. 

Kontent.ai


Next, import the following package using the Kontent.ai Template Manager. After the import finishes, you should see the content items in your project.

You can download the zip via this link.

Kontent.ai

Note: Acknowledge that your language prefixes should be the same as the localization codenames of your project.

Add a new language to content

Let’s start with adding a new language to our content. In Kontent.ai, head to the Project Settings and  Localization. Add a new language and set fallback to the default language. I added a new language and called it sk. I also have changed the name and codename of the default language to en to keep language prefixes and codenames intact. Now you are ready to create new language variants of your content items. It’s common that some content items don’t have language variants in all languages. For those cases, Kontent.ai features language fallbacks that deliver language variants of other languages in a defined order. Find out more here.

Kontent.ai

React application

Now we are ready to start working on our React application. Let’s see how our application will be structured:

- src
| - components
  | - LocalizedApp.tsx
| - localization
  | - en.json
  | - sk.json
| - utils
  | - clients.ts
| - views
  | - Home.tsx
  | - About.tsx
  | - Movies.tsx
  | - Movie.tsx
- App.tsx
- index.tsx

Fetching content from the CMS

To obtain data from Kontent.ai, we will use the Kontent.ai Delivery SDK. For starters, we will initialize a client which will be used for querying data. Create a /utils folder, and inside it, create a client.ts file with the code below. Remember to replace <YOUR_PROJECT_ID> with your actual project ID that you can find in Project settings in Kontent.ai.

import {createDeliveryClient} from "@kentico/kontent-delivery";

export const deliveryClient = createDeliveryClient({
    projectId: <YOUR_PROJECT_ID>
});

Prepare our local content 

Apart from fetching our content from Kontent.ai, for some texts, it’s more beneficial to store them locally. It is typical for the data you want to have always ready and don’t want to wait till they are fetched. One of the cases for this scenario is navigation link titles which we’ll keep in JSON files – en.json and sk.json. Keep in mind that React Intl support only key:value hierarchy, meaning you can’t use nested objects. Although, you might use a separator such as '_' to give your labels a taste of hierarchy:

// en.json
"header_home_link_title": "Home",
"header_about_link_title": "About us",
"header_movies_link_title": "Movies"

// sk.json
"header_home_link_title": "Domov",
"header_about_link_title": "O nas",
"header_movies_link_title": "Filmy"

Initialize routing

Now the fun begins! Firstly, we will add support for routing in the index.tsx which you can find at the top level of our files structure. As mentioned before, we support multilanguage routes using prefixes, therefore every route that has one of our supported languages in route in format /en/... should navigate us to a component LocalizedApp which is a semi-step that gets us to the application which will view our pages. The plain URL / will navigate us to the language prefix stored in cookies. If no cookie for language is set yet, then it will use the English language by default. Every other route will fall under the 404 page.

const cookies = new Cookies(document.cookie);
const lang = cookies.get('lang') ?? 'en';

ReactDOM.render(
    <React.StrictMode>
<BrowserRouter>
            <Routes>
                {["en", "sk"].map((value) => (
                    <Route
                        key={value}
                        path={`/${value.toLowerCase()}/*`}
                        element={<LocalizedApp lang={value} />}
                    />
                ))}
                <Route path="/" element={<Navigate to={`/${lang.toLowerCase()}`} />} />
                <Route path="*" element={<Navigate to={`/${lang.toLowerCase()}/404`} />} />
            </Routes>
</BrowserRouter>

</React.StrictMode>,
    document.getElementById('root')
);

Initializing React Intl in LocalizedApp

Our next challenge is to tell components which language is currently being used. We will use the React Intl IntlProvider component for that. Let’s create a /LocalizedApp.tsx component, which will wrap our app in IntlProvider. This allows every component of the app to access the necessary data – locale, defaultLocale, and messages. In the previous code file, we have already passed a lang variable to our new component LocalizedApp. We can now obtain it and pass it to IntlProvider also with messages from the JSON file and the default language option. In the useEffect block, we will save our current language into a cookie to make it available for subsequent requests. 

import messages_en from "../localization/en.json";
import messages_sk from "../localization/sk.json";
...

export const messages: { [index: string]: any } = {
    'en': messages_en,
    'sk': messages_sk
}

interface LocalizedAppProps{
    lang: string
}

export const LocalizedApp: React.FC<LocalizedAppProps> = ({lang}) => {
    const cookies = useMemo(() => new Cookies(document.cookie), []);

    useEffect(() => {
        cookies.set('lang', lang, { path: '/' });
    }, [lang, cookies])

    return (
        <IntlProvider
            locale={lang}
            messages={messages[lang]}
            defaultLocale={"en"}
        >
            <App/>
        </IntlProvider>
    )
}

Routing and local translations of our app

Great job there! Now we can create links and routes to navigate users around. Let’s look at our simple navigation header first in app.tsx. We get access to formatMessage function via useIntl() hook. Then we use this function to tell React Intl to translate our link titles. It automatically takes provided JSON file from IntlProvider and gives us the right text according to the specified id. Then we specify the routes that our app can navigate through. Notice that the Route for the Movie component consists of /movies/:codename. The suffix tells us that there is a variable codename, which can be then obtained via hook from the URL.

function App() {
    const { formatMessage } = useIntl();

  return (
    <div className="App">
        <div>
          <ul>
            <li>
                <Link to={``}>{formatMessage({id:"Header.homeLinkTitle"})}</Link>
            </li>
            <li>
              <Link to={`${formatMessage({id:"Routing.about-us"})}`}>{formatMessage({id:"Header.aboutLinkTitle"})}</Link>
            </li>
            <li>
              <Link to={`movies`}>{formatMessage({id:"Header.movieLinkTitle"})}</Link>
            </li>
          </ul>

            <Routes>
                <Route path="" element={<Home />} />
                <Route path="/about-us" element={<About />} />
                <Route path="/movies" element={<Movies />} />
                <Route path={"/movies/:codename"} element={<Movie />} />
                <Route path={"*"} element={<Navigate to={"/404"} />}/>
                <Route path="404" element={<NotFound />}/>
            </Routes>
        </div>
    </div>
  );
}

Fetching translated content from Kontent.ai

Now it’s time to fetch data from Kontent.ai and display it. We will first process the movie component. Let’s create a /views folder and a /views/Movie.tsx file inside it. Firstly, we define a Movie type according to our content type in Kontent.ai. We obtain our locale via useIntl() and the codename of the movie via useParams() hook, which lets us obtain data from the URL. Then, we prepare a function that uses our previously defined Kontent.ai delivery client to fetch a movie with the given codename. We also need to provide a language parameter, so that the CMS knows which language version we want. For that, we use .languageParameter(locale). Then we call the function inside of useEffect, so it is executed anytime the codename or locale gets changed. Lastly, we can easily render our data in the return function. 

Kontent.ai
export type Movie = IContentItem<{
    title: Elements.TextElement;
    description: Elements.TextElement;
}>;

export const Movie: React.FC = () => {
    const [movie, setMovie] = useState<Movie|undefined>();
    const { locale } = useIntl();
    const { codename } = useParams();

    useEffect(() => {
        getMovie().then(val => {
            setMovie(val?.data.item)
        })
    }, [codename, locale])

    const getMovie = async () => {
        if (codename === undefined){
            return
        }
        return await deliveryClient.item<Movie>(codename).languageParameter(locale).toPromise();
    }

    return(
        <div>
            <p>{movie?.elements.title.value}</p>
            <p>{movie?.elements.description.value}</p>
        </div>
    )
}

Note: Analogically, you can create all the other views.

Handling 404

To handle 404 errors, it might seem like a good idea to have one /404 page for all languages. In our example, we decided to go in a way that every language has its own route – e.g., /en/404. The reason is that it’s better for SEO as it is recommended to use different URLs for different languages. 

To sum up...

We created a new sample app and installed the necessary dependencies for internationalization and content fetching. We adjusted the implementation to support multiple languages, used the Kontent.ai delivery SDK to fetch appropriate content, and finally added multiple 404 pages to handle non-existing pages. We have discussed how to make a basic react app with routing and multilanguage support using Kontent.ai. If you are more interested in Kontent.ai, you can check React Sample App that can show you more of the integration between React and Kontent.ai.

Written by

Ivan Kiráľ

As a developer advocate, I take care of the developer community. I learn new technologies and apply my knowledge in our open source projects.

More articles from Ivan