i18n cheatsheet. Library: next-intl.

Install

npm i next-intl

File structure

app/
└── [locale]/
    ├── layout.tsx
    ├── page.tsx
    └── ...
middleware.ts
i18n/
├── request.ts
└── locales/
    ├── en.json
    └── es.json

i18n/request.ts

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

export default getRequestConfig(async ({ locale }) => ({
  messages: (await import(`./locales/${locale}.json`)).default,
}));

next.config.js

const withNextIntl = require("next-intl/plugin")("./i18n/request.ts");
module.exports = withNextIntl({});

middleware.ts

import createMiddleware from "next-intl/middleware";

export default createMiddleware({
  locales: ["en", "es"],
  defaultLocale: "en",
  localePrefix: "as-needed",   // "/about" or "/es/about"
});

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

Layout

// app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";

export default async function Layout({ children, params }: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  const messages = await getMessages();
  
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

Translation files

// i18n/locales/en.json
{
  "home": {
    "greeting": "Hello, {name}!",
    "items": "{count, plural, one {one item} other {# items}}"
  }
}
// i18n/locales/es.json
{
  "home": {
    "greeting": "¡Hola, {name}!",
    "items": "{count, plural, one {un artículo} other {# artículos}}"
  }
}

Usage in server component

import { getTranslations } from "next-intl/server";

export default async function Page() {
  const t = await getTranslations("home");
  return <h1>{t("greeting", { name: "World" })}</h1>;
}

Usage in client component

"use client";
import { useTranslations } from "next-intl";

export function Greeting() {
  const t = useTranslations("home");
  return <p>{t("greeting", { name: "World" })}</p>;
}

Plurals

t("items", { count: 5 });    // "5 items"
t("items", { count: 1 });    // "one item"

ICU MessageFormat under the hood.

Dates and numbers

import { useFormatter } from "next-intl";

const f = useFormatter();
f.dateTime(new Date(), { dateStyle: "long" });       // "July 4, 2026"
f.number(1234.5, { style: "currency", currency: "USD" });   // "$1,234.50"
f.relativeTime(new Date(Date.now() - 3600 * 1000));   // "1 hour ago"
import { Link } from "@/i18n/routing";

<Link href="/about">About</Link>       // automatically prefixed by locale

Get/set locale

import { getLocale } from "next-intl/server";

const locale = await getLocale();
import { useLocale } from "next-intl";

const locale = useLocale();

Switching locale

"use client";
import { useRouter, usePathname } from "@/i18n/routing";
import { useLocale } from "next-intl";

export function LocaleSwitcher() {
  const router = useRouter();
  const pathname = usePathname();
  const locale = useLocale();
  
  return (
    <select
      value={locale}
      onChange={(e) => router.push(pathname, { locale: e.target.value })}
    >
      <option value="en">English</option>
      <option value="es">Español</option>
    </select>
  );
}

Metadata per locale

import { getTranslations } from "next-intl/server";

export async function generateMetadata({ params }) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: "meta" });
  return {
    title: t("title"),
    description: t("description"),
  };
}

Typed translations

// global.d.ts
import type messages from "./i18n/locales/en.json";

declare global {
  type Messages = typeof messages;
}

declare module "next-intl" {
  interface AppConfig {
    Messages: Messages;
  }
}

Now t("home.greeting") is type-checked.

Right-to-left (RTL)

<html lang={locale} dir={locale === "ar" || locale === "he" ? "rtl" : "ltr"}>

Hardcoded vs translated

What needs translation:

  • User-facing copy.
  • Error messages shown to users.
  • Email templates.

What doesn’t:

  • Console logs.
  • Internal IDs.
  • API responses (usually).

Common mistakes

  • Translations file inconsistent keys between locales — runtime crash.
  • Plurals without ICU syntax → wrong grammar.
  • Server fetching with locale not in URL → wrong language returned.
  • Storing translations in DB without TTL/cache.
  • Forgetting lang attribute → SEO and screen readers hit.

Read this next

If you want my next-intl + RTL setup, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .