Internationalise a React or Next.js Site

Keep every string in one JSON message file per language, give each language its own URL such as /de/about, and format dates, prices and plurals with the browser's built-in Intl APIs. In the Next.js App Router, next-intl wires these together: a [locale] route segment, a proxy that picks the language, and useTranslations in components.

The parts of a translated site
PartWhat to use
Stringsmessages/en.json, messages/de.json and so on
URLsOne path prefix per language: /en/..., /de/...
Language detectionnext-intl's createMiddleware in proxy.ts (middleware.ts before Next.js 16)
Dates, numbers, moneyIntl.DateTimeFormat and Intl.NumberFormat
PluralsICU message syntax or Intl.PluralRules
Search engineshreflang links for every language version, plus x-default

Updated

Translation is more than swapping words. Word order changes, plural rules differ, Arabic reads right to left and every country writes dates its own way. Try the switcher below to see one small product card change language, then build the same thing in your app with the setup files and the hreflang generator.

Try it: one card in five languages

Switch language and change the item count. Strings come from a message object, and the price, date and plural form come from the browser's Intl APIs.

hreflang generator

Enter your domain, a page path and your language codes to get the link tags for the head, or the Next.js alternates object.

next-intl setup for the App Router

Install with npm install next-intl. These files follow the structure in next-intl's own getting started guide for locale-based routing.

messages/en.json and messages/de.json

One file per language, same keys in each. ICU syntax handles plurals inside the message.

// messages/en.json
{
  "Cart": {
    "title": "Your cart",
    "items": "{count, plural, =0 {No items} one {# item} other {# items}}"
  }
}

// messages/de.json
{
  "Cart": {
    "title": "Ihr Warenkorb",
    "items": "{count, plural, =0 {Keine Artikel} one {# Artikel} other {# Artikel}}"
  }
}

src/i18n/routing.ts and src/proxy.ts

Routing lists your locales once. The proxy (called middleware.ts before Next.js 16) redirects visitors to a language prefix.

// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";

export const routing = defineRouting({
  locales: ["en", "de", "ar"],
  defaultLocale: "en",
});

// src/proxy.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";

export default createMiddleware(routing);

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

src/i18n/request.ts

Reads the locale from the [locale] segment, rejects unknown ones and loads that language's messages.

import * as rootParams from "next/root-params";
import { notFound } from "next/navigation";
import { getRequestConfig } from "next-intl/server";
import { hasLocale } from "next-intl";
import { routing } from "./routing";

export default getRequestConfig(async ({ locale }) => {
  if (!locale) {
    const value = await rootParams.locale();
    if (hasLocale(routing.locales, value)) locale = value;
    else notFound();
  }
  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default,
  };
});

next.config.ts, layout and a page

The plugin links request.ts. Move your pages under app/[locale]/ and read messages with useTranslations. next/root-params is on by default from Next.js 16.3; earlier versions need experimental.rootParams.

// next.config.ts
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";

const nextConfig: NextConfig = {};
export default createNextIntlPlugin()(nextConfig);

// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { routing } from "@/i18n/routing";

export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}

export default async function LocaleLayout({ children, params }) {
  const { locale } = await params;
  return (
    <html lang={locale} dir={locale === "ar" ? "rtl" : "ltr"}>
      <body><NextIntlClientProvider>{children}</NextIntlClientProvider></body>
    </html>
  );
}

// src/app/[locale]/cart/page.tsx
import { useTranslations } from "next-intl";

export default function CartPage() {
  const t = useTranslations("Cart");
  return <h1>{t("title")} ({t("items", { count: 3 })})</h1>;
}

Formatting without a library

Intl in plain JavaScript

Works in any React app, translated or not.

const price = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(1234.5);
// "1.234,50 €"

const date = new Intl.DateTimeFormat("ar-EG", { dateStyle: "long" }).format(new Date(2026, 9, 14));

const rule = new Intl.PluralRules("ar-EG").select(3);
// "few": Arabic has six plural categories, English has two for cardinals

hreflang rules that search engines check

Getting language annotations right
RuleExample
Use full absolute URLshttps://example.com/de/about, not /de/about
Each version lists itself and all othersThe German page links to en, de, ar and x-default
Language is ISO 639-1, optional region ISO 3166-1 alpha-2de, en-GB, pt-BR
Never a region alonebe is Belarusian, not Belgium; use nl-BE or fr-BE
x-default for everyone elsePoints at a language picker or your main version

Messages, not strings in components

Every piece of text a person sees should come from a message file keyed by meaning, such as Cart.checkoutButton. Developers edit English, translators edit the other files, and nobody hunts through components for hard-coded copy.

Namespacing by component keeps files readable. next-intl's useTranslations('Cart') then gives you t('checkoutButton') inside that component only.

Let Intl do the formatting

Intl.NumberFormat writes 1234.5 euros as 1.234,50 € for a German reader, with the symbol and separators placed the way that locale expects. Intl.DateTimeFormat does the same for dates, and Intl.PluralRules tells you whether a number takes the one, few, many or other form in a given language.

These APIs ship in every modern browser and in Node, so formatting adds no weight to your bundle and stays correct for languages you have never studied.

Right-to-left languages

Arabic, Hebrew, Persian and Urdu read right to left. Setting dir="rtl" on the html element mirrors text alignment and the order of inline content, but fixed left and right values in CSS stay put.

Use logical properties such as margin-inline-start and padding-inline-end, or Tailwind's ms- and pe- utilities, so spacing flips automatically with the direction.

How it works

  1. Pick a language in the demo. The card swaps its messages, formats price and date for that locale, and flips direction for Arabic.
  2. Change the item count to watch each language choose its own plural form.
  3. Copy the next-intl setup files into a Next.js App Router project.
  4. Enter your domain and languages in the hreflang generator and paste the tags, or the metadata object, into your layout.

Tips

Frequently asked questions

Which i18n library should I use with React?

For the Next.js App Router, next-intl is purpose built and supports Server Components. For a plain React app without Next.js, pick a library that stores messages as JSON, so moving to another one later stays manageable.

Should languages live on subfolders, subdomains or separate domains?

Subfolders such as example.com/de/ are the simplest to run, because one site and one domain carry everything. next-intl also supports domain-based routing if you need de.example.com or a country domain.

How do I add hreflang in Next.js?

Return alternates.languages from metadata or generateMetadata, with one entry per language URL. Next.js prints a link rel="alternate" hreflang tag for each. Every language version must list all the others and itself, or search engines may ignore the set.

Can I machine translate my site?

You can use machine translation as a first draft, but have a fluent speaker review it, especially headlines, prices and legal text. Thin or unreviewed translations can confuse readers and weaken trust.

Related: Multilingual website guide, Next.js middleware examples, JavaScript date formatting, Sitemap and robots.txt examples, Infinite scroll in React.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap