Internationalization in Next 14 without using third-party libraries

There are many third-party internationalization libraries you can use in the Next.js 14 project. You will be able to encounter hydration failure in some of them while using them. I would like to tell you about the structure I use in my own projects. You do not need to install any third party libraries in this structure. There is no complex code structure. You can add as many languages as you want. You can run these languages on both the server side and the client side without any problems.

You can visit the address below for the source code

Github repository

First, let's create a new next js project

npx create-next-app@latest newproject

Then, let's create a directory named "lang" in the main directory of the project and create the relevant files in it.

├── lang
│ ├── locales
│ │ ├── de.json
│ │ ├── en.json
│ ├── clientSide.tsx
│ ├── serverSide.tsx
│ ├── languages.tsx

We use the following codes to import language files on the server side.

//serverSide.tsx
import "server-only";
import { cookies } from "next/headers";
import en from "./locales/en.json";
import de from "./locales/de.json";
import { Languages, Locale } from "./languages";

const dictionaries = {
  en,
  de,
};

export const getLang = () => {
  const hasLang = Languages.locales.some((item) => item === getCookie("lang"));
  //@ts-ignore
  const lang: Locale = hasLang ? getCookie("lang") : Languages.defaultLocale;
  return lang;
};

export const getTranslate = () => dictionaries[getLang()];

We use the following codes to import language files on the client side.

//clientSide.tsx
"use client";
import { getCookie } from "cookies-next";
import en from "./locales/en.json";
import de from "./locales/de.json";
import { Languages, Locale } from "./languages";

const dictionaries = {
  en,
  de,
};

export const getLang = () => {
  const hasLang = Languages.locales.some((item) => item === getCookie("lang"));
  //@ts-ignore
  const lang: Locale = hasLang ? getCookie("lang") : Languages.defaultLocale;
  return lang;
};

export const getTranslate = (lang: Locale) => dictionaries[lang];

The getLang function found here allows us to find out what the active language is on the client side.

On the server side, we can learn the active language both with the getLang function and via params.

We define all our languages in the "languages.tsx" file below.

//languages.tsx
export const Languages = {
  defaultLocale: "en",
  locales: ["en", "de"],
} as const;

export type Locale = (typeof Languages)["locales"][number];

Finally, when we enter the address of our website, we create our "middleware.ts" file, which will direct us to the relevant language address.

//middleware.ts
import { NextResponse } from "next/server";

import { Languages, Locale } from "@/lang/languages";

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

const cookieName = "lang";

export function middleware(req: any) {
  let lang: Locale = Languages.defaultLocale;
  if (req.cookies.has(cookieName)) {
    const hasLang = Languages.locales.some(
      (item) => item === req.cookies.get(cookieName)
    );
    lang = hasLang ? req.cookies.get(cookieName) : Languages.defaultLocale;
  }

  if (
    !Languages.locales.some((loc) =>
      req.nextUrl.pathname.startsWith(`/${loc}`)
    ) &&
    !req.nextUrl.pathname.startsWith("/_next") &&
    !req.nextUrl.pathname.startsWith("/api/") &&
    !req.nextUrl.pathname.startsWith("/images/")
  ) {
    return NextResponse.redirect(
      new URL(`/${lang}${req.nextUrl.pathname}`, req.url)
    );
  }

  if (req.headers.has("referer")) {
    const response = NextResponse.next();
    return response;
  }

  return NextResponse.next();
}

In this file, we also specify the folders that will be exempt from the language url.

You can visit the address below for the source code

Github repository

You can test that the project works flawlessly on both the server side and the client side.