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"
Localized links
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
langattribute → 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 .