Building a Multilingual application in Nextjs 14

In this challenge we will look into integrating a language switcher feature on the homepage, allowing users to select their preferred language (English, Spanish, or French).

May 2, 20247 min read

What is a multilingual website?

A multilingual website offers content in more than one language. It's designed to cater to audiences from different linguistic backgrounds, allowing users to select their preferred language, read, and interact with the website's content.

By targeting user requirements through language-specific content, businesses can create a more inclusive and accessible online presence that satisfies user requirements and drives business growth forward.

Next.js's built-in i18n routing vs the next-intl library

Next.js's built-in i18n routing and the next-intl library supports the management of locales by providing support for translating content into multiple languages. Developers can define a list of supported languages and specify the default language for their application.

Despite their similarities, they also serve different purposes:

Built-in i18n Routing in Next.js

  1. Next.js provides native support for internationalized routing.
  2. With Next.js's i18n routing, you can set a default locale and domain-specific locales. Next handles the routing based on the user's preferred language.

next-intl Library

  1. The next-intl library provides additional features beyond the basic routing such as translation, message formatting, and date formatting.
  2. Next-intl provides control over how messages are formatted and displayed in different languages allowing for flexibility.
  3. It provides locale management including utilities for managing locales and dynamically switching between languages at runtime.

The choice between the two depends on the project's complexity and user requirements. In this article, we will use the next-intl library for our project.

Try Kodaschool for free

Click below to sign up and get access to free web, android and iOs challenges.

Sign Up


Getting started

We will create a Next.js app that uses the App router.

npx create-next-app@latest

next-intl integrates with the App Router by using a [locale] used to provide content in different languages. e.g./en, /en/about, etc.

Next, run the following command to install next-intl in your project.

npm install next-intl

Project structure

The folder structure should look like this:

├── messages (1)
│   ├── en.json
│   └── ...
├── next.config.mjs (2)
└── src
    ├── i18n.ts (3)
    ├── middleware.ts (4)
    └── app
        └── [locale]
            ├── layout.tsx (5)
            └── page.tsx (6)

Messages

We will create a messages folder at the root of our project and create different locales. In our case, we require support for English, Spanish, and French languages. We will create three different JSON files to support these locales.

Here is an example of the en.json file which provides support for English locale.

{
  "IndexPage": {
    "title": "Hello world!"
  },
  "Navigation":{
     "about" :"About us",
    "pricing":"Pricing",
     "features":"Features",
     "contact":"Contact",
     "btn":"Getting started"
  },
  "HomePage":{
   "titleOne":"Empower your transactions,",
   "titleTwo":"Unleash the possibilities",
   "desc":"With Karpay, bid farewell to traditional hassles and experience hassle free global money trnasfers welcoming confdence and simplicity",
   "btn":"Get started it is free"
  },
   "NotFoundPage":{
     "title":"Something went wrong!"
  }

}

next.config.mjs

The next step is to configure a plugin that creates an alias. This alias will allow you to provide your internationalization (i18n) configuration to a Server Component in your project.

import createNextIntlPlugin from 'next-intl/plugin';
 
const withNextIntl = createNextIntlPlugin();
 
/** @type {import('next').NextConfig} */
const nextConfig = {};
 
export default withNextIntl(nextConfig);

i18n

next-intl creates a configuration once per request. You can provide messages and other options depending on the user's locale.

import { notFound } from "next/navigation";
import { getRequestConfig } from "next-intl/server";

// Can be imported from a shared config
const locales = ["en", "fr", "es"];

export default getRequestConfig(async ({ locale }) => {
  // Validate that the incoming `locale` parameter is valid
  if (!locales.includes(locale as any)) notFound();

  return {
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});
  • Function Purpose: This function is used to dynamically configure internationalization settings based on each incoming request.
  • Locale Validation: If a locale is not included in the locales array, notFound() is called to return a 404 error. This prevents users from accessing unsupported locales.
  • Dynamic Message Loading: The function then dynamically imports a JSON file based on the locale. This JSON file contains localized messages for the application, structured as ../messages/${locale}.json.
  • The await import(...): This allows the loading of localization messages needed based on the selected locale optimizing the performance of the application.

middleware.ts

The middleware matches a locale for the request and, handles redirects and rewrites accordingly.

import createMiddleware from "next-intl/middleware";

export default createMiddleware({
  // A list of all locales that are supported
  locales: ["en", "fr", "es"],

  // Used when no locale matches
  defaultLocale: "en",
});

export const config = {
  // Match only internationalized pathnames
  matcher: ["/", "/(fr|en|es)/:path*"],
};

The createMiddleware function is called with a configuration object. This configuration includes:

  • locales: An array of strings representing the locales that your application supports. In this case, it supports English (en), French (fr), and Spanish (es).
  • defaultLocale: This indicates the default locale to use if the incoming request does not match any of the supported locales or if the locale cannot be determined from the request.

app/[locale]/layout.tsx

The locale determined by the middleware is accessible through the locale parameter. It enables the configuration of the document language based on the locale.

With this, you can incorporate translations within your page components.

import Header from "../components/header";
import { Inter } from "next/font/google";
import "./globals.css";

interface RootLayoutProps {
  children: React.ReactNode;
  params: {
    locale: string;
  };
}
const inter = Inter({ subsets: ["latin"] });

export default function RootLayout({
  children,
  params,
}: Readonly<RootLayoutProps>) {
  return (
    <html lang={params.locale}>
      <body className={inter.className}>
        <div className="flex flex-col min-h-screen mx-auto bg-secondary">
          <Header />
          <div className="flex-grow mt-5">{children}</div>
        </div>
      </body>
    </html>
  );
}

The param indicates the current language for the user's session. For example, en-US for English, es for Spanish, and fr-FR for French. It's essential for determining which translation settings and locale-specific content to present to the user.

app/[locale]/page.tsx

Use translations in your components or anywhere else in your app tree. We will use useTransalation in the HomePage component and the header component.

The t() method is accessed through the useTranslation hook from the next-intl hook. It fetches the translated strings based on the selected locale.

For Example:

  • t("titleOne") and t("titleTwo"): These calls retrieve translated titles from the translation files for display on the homepage.

Here we also import a utility function - (highlightWords) for text manipulation.

To illustrate how using translation works, let's integrate translation capabilities into the Homepga component and the Header component.

Homepage component

import { useTranslations } from "next-intl";
import { highlightWords } from "../utilis";
import { FaArrowRightLong } from "react-icons/fa6";

export default function Home() {
  const t = useTranslations("HomePage");
  const titleOne = t("titleOne");
  const titleTwo = t("titleTwo");
  const wordOne = titleOne.split(" ");
  const wordTwo = titleTwo.split(" ");

  return (
    <main className="my-24">
      <div className="text-center w-full m-auto">
        <p className="text-6xl text-white leading-10 font-bold">
          {highlightWords(wordOne)}
        </p>
        <p className="text-6xl leading-10 text-white font-bold my-4">
          {highlightWords(wordTwo)}
        </p>
        <p className="w-[40%] text-white m-auto my-2">{t("desc")}</p>
        <button className="rounded-3xl bg-primary p-3 text-white justify-center m-auto my-4 gap-2 flex h-auto items-center">
          {t("btn")}
          <FaArrowRightLong />
        </button>
      </div>
    </main>
  );
}

The Header Component

We will create a separate language switcher component and import it here.

import { useTranslations } from "next-intl";
import React from "react";
import Link from "next/link";
import Logo from "../../../public/images/logo.png";
import LanguageSwitcher from "./language-switcher";
const Header = () => {
  const t = useTranslations("Navigation");
  return (
    <nav className="flex container h-16 items-center justify-between">
      <Link href="/">
        <h2 className="text-white">KaraPay</h2>
      </Link>
      <ul className="flex gap-8 text-white">
        <li>{t("about")}</li>
        <li>{t("pricing")}</li>
        <li>{t("features")}</li>
        <li>{t("contact")}</li>
      </ul>
      <div className="flex gap-4 h-auto items-center">
        <button className="rounded-3xl bg-primary px-4 py-2 text-white ">
          {t("btn")}
        </button>
        <LanguageSwitcher />
      </div>
    </nav>
  );
};

export default Header;

The language switcher

This component provides an interface for dynamically switching the locale of the application.

  • The useTransition hook provides a start transition function that handles updates that should be deferred until the next browser render.
  • The useRouteer hook changes the URL based on the selected language. For example, navigating to http://localhost:3000/en displays the English version of the Homepage section.
  • The useLoacle retrieves the active locale which sets the default value of the select dropdown.
  • The handleSelect function captures the new language selected by the user and is triggered by the onChange event on the <select> element.
"use client";
import { TbWorldDownload } from "react-icons/tb";
import { useRouter } from "next/navigation";
import { useLocale } from "next-intl";
import React, { useTransition, ChangeEvent } from "react";

const LanguageSwitcher = () => {
  const [isPending, startTransition] = useTransition();
  const router = useRouter();
  const localeActive = useLocale();
  const handleSelect = (e: ChangeEvent<HTMLSelectElement>) => {
    const nextLocale = e.target.value;
    startTransition(() => {
      router.replace(`/${nextLocale}`);
    });
  };
  return (
    <div className="relative">
      <TbWorldDownload className="absolute left-2 z-20 top-2" />
      <select
        defaultValue={localeActive}
        className="bg-gray-50 border border-none text-gray-900 text-sm rounded-lg block w-full py-2 px-8 "
        onChange={handleSelect}
      >
        <option value="en">English</option>
        <option value="es">Spanish</option>
        <option value="fr">French</option>
      </select>
    </div>
  );
};

export default LanguageSwitcher;

Translating the plain text based on the selected locale.

ImageImage

Conclusion

The challenge involves adding multilingual functionality to the existing homepage of the Karapay website. Users should be able to switch between English, Spanish, and French versions seamlessly, with all textual content updated accordingly. Completing this challenge will empower the website to cater to a wider global audience and provide a more inclusive user experience.

Github Repo: https://github.com/kodaschool/Multilingual-website-kodaschool/

Mary Maina

About Mary Maina

I am a frontend devloper