nahuel.luca()

How use internacionalization in NextJS

Having a multilingual website is important because it broadens your reach and potential audience. It allows you to reach people who speak different languages, which increases business opportunities, improves the user experience by providing content in their native language and helps build a global and multicultural brand image. In addition, it can improve search engine rankings by providing relevant content in different languages.

What are we going to do?

We will build a basic app with Nextjs and use middleware, params and usePathname() to manage our languages in the application.

Starting

We will create a typical app in nextjs, in my case I will use TailwindCSS and shadcn/ui for the styles. After creating the app we will change the folder structures inside the app folder we will put the [lang] folder and inside this new folder everything that contains our app. This is the structure of the project

app/[lang]/page.tsx
app/[lang]/layout.tsx
app/[lang]/without-Internationalization-link/page.tsx
app/[lang]/example-param/page.tsx

Creating the language configuration and our middleware

// i18-config.ts
export const i18n = {
  defaultLocale: "en",
  locales: ["en", "es"],
} as const;

export type Locale = (typeof i18n)["locales"][number];
//middleware.ts

import { NextRequest, NextResponse } from "next/server";
import { match as matchLocale } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";

import { i18n } from "./i18n-config";

function getLocale(request: NextRequest): string | undefined {
  // Negotiator expects plain object so we need to transform headers
  // console.log(request.nextUrl);
  const negotiatorHeaders: Record<string, string> = {};

  request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));

  // @ts-ignore locales are readonly
  const locales: string[] = i18n.locales;

  // Use negotiator and intl-localematcher to get best locale
  let languages = new Negotiator({ headers: negotiatorHeaders }).languages(
    locales
  );

  const locale = matchLocale(languages, locales, i18n.defaultLocale);

  return locale;
}

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;

  const pathnameIsMissingLocale = i18n.locales.every(
    (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  );

  if (pathnameIsMissingLocale) {
    const locale = getLocale(request);

    return NextResponse.redirect(
      new URL(
        `/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`,
        request.url
      )
    );
  }
}

export const config = {
  matcher: "/((?!api|static|.*\\..*|_next).*)",
};

So what's going on here?

With getLocale() we are getting from the headers our location and then with matchLocale we check if it matches our languages in the app. Then in the middleware with pathnameIsMissingLocale we check if we have our locale in the URL, if not we make a redirect to a URL with the locale

Excellent, we have almost everything ready

Now we will create the dictionaries folder that will contain our json with its version in en and in es

// dictionaries/en/home.json
{
  "title": "Hello World",
  "subtitle": "This is a test of application internationalization with NextJS"
}

and

// dictionaries/es/home.json
{
   "title": "Hola Mundo",
  "subtitle": "Esto es una prueba de internacionalización de la aplicación con NextJS"
}

now we will create a function to obtain the dictionaries in utils/dictionaries.ts

import "server-only";
import { Locale } from "@/i18n-config";

interface DictionaryPaths {
  [section: string]: {
    [locale in Locale]: () => Promise<any>;
  };
}

const dictionaryPaths: DictionaryPaths = {
  home: {
    en: () =>
      import("../dictionaries/en/home.json").then((module) => module.default),
    es: () =>
      import("../dictionaries/es/home.json").then((module) => module.default),
  },
};

export const getDictionary = async (
  section: string,
  locale: Locale
): Promise<any> => {
  const sectionDictionary = dictionaryPaths[section];

  if (!sectionDictionary) {
    throw new Error(`Invalid dictionary section: ${section}`);
  }

  const getLocaleDictionary = sectionDictionary[locale];

  if (!getLocaleDictionary) {
    throw new Error(`Invalid locale for section ${section}: ${locale}`);
  }

  return getLocaleDictionary();
};

Great now to use it we will only access from our path to the lang parameter and pass it to our function to get the content in the correct language.

export default async function Home({ params }: { params: { lang: Locale } }) {
  const dictionary = await getDictionary("home", params.lang);
  return (
    <main className="">
      <div className="space-y-2">
        <h1 className="font-bold text-2xl">{dictionary.title}</h1>
        <h4 className="font-semibold text-xl text-muted-foreground">
          {dictionary.subtitle}
        </h4>
      </div>
      <p className="mt-5">{dictionary.content}</p>

      <div className="mt-10">
        <h2 className="text-lg font-medium">{dictionary.swithLanguage}</h2>
        <Button asChild>
          <Link href="/without-Internationalization-link">
            {dictionary.link}
          </Link>
        </Button>
      </div>
    </main>
  );
}

One more thing...

You will notice that when using the NextJS Link component the param lang does not persist, this is a problem because if the user changes the language to a different one than the one detected in the header when using Link the URL will return to the same lang that contains the header initially.

To solve this I found 2 ways. One is to pass by props the lang parameter that can only be obtained in the routes in this way:

export default function WithLangParam({
  params,
}: {
  params: { lang: Locale };
}) {
  return (
    <div>
      With lang param
      <Link href={`/${params.lang}/without-Internationalization-link`}>Go</Link>
    </div>
  );
}

The second way is to create a client-side component and use usePathname() to get the current language and put it in the url like this:

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";

export function getLocalePath(pathName: string) {
  if (pathName.startsWith("/es")) {
    return "es/";
  } else {
    return "en/";
  }
}

export default function InternationalizationLink({
  href,
  children,
  className,
  target,
  onClick,
}: {
  href: string;
  children: React.ReactNode;
  className?: string;
  target?: string;
  onClick?: () => void;
}) {
  const pathName = usePathname();

  return (
    <Link
      className={className}
      href={`/${getLocalePath(pathName)}/${href}`}
      target={target}
      onClick={onClick}
    >
      {children}
    </Link>
  );
}

To end

I hope this guide has helped you in some way and if you want I have a live example for you to try here. Thanks for reading.