|$ curl https://forge-ai.dev/api/markdown?path=docs/nextjs/internationalization
$cat docs/next.js-—-internationalization-(i18n).md
updated Last week·18 min read·published

Next.js — Internationalization (i18n)

Next.jsIntermediate🎯Free Tools
Introduction

Internationalization (i18n) makes your application usable across languages and regions. Next.js with next-intl provides a complete solution for translated content, localized routing, and locale-aware formatting. The library integrates with both Server and Client Components.

Setup with next-intl
terminal
Bash
1npm install next-intl
2
3# Project structure:
4# messages/en.json, messages/es.json, messages/fr.json
5# src/i18n/routing.ts, src/i18n/request.ts, src/i18n/navigation.ts
6# src/middleware.ts
src/i18n/routing.ts
TypeScript
1// src/i18n/routing.ts
2import { defineRouting } from "next-intl/routing";
3
4export const routing = defineRouting({
5 locales: ["en", "es", "fr", "de"],
6 defaultLocale: "en",
7 pathnames: {
8 "/about": {
9 en: "/about",
10 es: "/sobre",
11 fr: "/a-propos",
12 de: "/ueber-uns",
13 },
14 "/blog": {
15 en: "/blog",
16 es: "/blog",
17 fr: "/blogue",
18 de: "/blog",
19 },
20 },
21});
22
23// src/i18n/request.ts
24import { getRequestConfig } from "next-intl/server";
25import { routing } from "./routing";
26
27export default getRequestConfig(async ({ requestLocale }) => {
28 let locale = await requestLocale;
29 if (!locale || !routing.locales.includes(locale as any)) {
30 locale = routing.defaultLocale;
31 }
32 return {
33 locale,
34 messages: (await import("../../messages/" + locale + ".json")).default,
35 };
36});
37
38// src/i18n/navigation.ts
39import { createNavigation } from "next-intl/navigation";
40import { routing } from "./routing";
41
42export const { Link, redirect, usePathname, useRouter } =
43 createNavigation(routing);
Message Files
messages/en.json
JSON
1{
2 "common": {
3 "welcome": "Welcome to our app",
4 "loading": "Loading...",
5 "error": "Something went wrong",
6 "save": "Save",
7 "cancel": "Cancel"
8 },
9 "home": {
10 "title": "Home",
11 "description": "This is the home page",
12 "features": {
13 "title": "Features",
14 "fast": "Lightning fast",
15 "secure": "Secure by default"
16 }
17 },
18 "blog": {
19 "title": "Blog",
20 "readMore": "Read more",
21 "publishedOn": "Published on {date}",
22 "author": "By {name}",
23 "comments": "{count, plural, =0 {No comments} one {1 comment} other {{count} comments}}"
24 },
25 "profile": {
26 "greeting": "Hello, {name}!",
27 "items": "{count, plural, =0 {You have no items} one {You have 1 item} other {You have {count} items}}"
28 }
29}
messages/es.json
JSON
1{
2 "common": {
3 "welcome": "Bienvenido a nuestra aplicacion",
4 "loading": "Cargando...",
5 "error": "Algo salio mal",
6 "save": "Guardar",
7 "cancel": "Cancelar"
8 },
9 "home": {
10 "title": "Inicio",
11 "description": "Esta es la pagina de inicio",
12 "features": {
13 "title": "Caracteristicas",
14 "fast": "Ultras rapido",
15 "secure": "Seguro por defecto"
16 }
17 }
18}

info

Use ICU message format for plurals, gender, and interpolation. The {count, plural, ...} syntax handles singular/plural forms across languages.
Server & Client Components
components.tsx
TSX
1// Server Component — use getTranslations
2import { getTranslations } from "next-intl/server";
3
4export default async function HomePage() {
5 const t = await getTranslations("home");
6
7 return (
8 <div>
9 <h1>{t("title")}</h1>
10 <p>{t("description")}</p>
11 <p>{t("features.fast")}</p>
12 <p>{t("features.secure")}</p>
13 </div>
14 );
15}
16
17// Client Component — use useTranslations
18"use client";
19import { useTranslations, useLocale } from "next-intl";
20
21export function BlogPost({ post }: { post: Post }) {
22 const t = useTranslations("blog");
23 const locale = useLocale();
24
25 return (
26 <article>
27 <h2>{post.title}</h2>
28 <p>{t("author", { name: post.author })}</p>
29 <p>{t("publishedOn", { date: post.createdAt.toLocaleDateString(locale) })}</p>
30 <p>{t("comments", { count: post.comments.length })}</p>
31 </article>
32 );
33}
34
35// Language switcher
36"use client";
37import { useLocale } from "next-intl";
38import { useRouter, usePathname } from "@/i18n/navigation";
39
40export function LanguageSwitcher() {
41 const locale = useLocale();
42 const router = useRouter();
43 const pathname = usePathname();
44
45 function switchLocale(newLocale: string) {
46 router.replace(pathname, { locale: newLocale });
47 }
48
49 return (
50 <select value={locale} onChange={(e) => switchLocale(e.target.value)}>
51 <option value="en">English</option>
52 <option value="es">Espanol</option>
53 <option value="fr">Francais</option>
54 <option value="de">Deutsch</option>
55 </select>
56 );
57}
Date & Number Formatting
formatting.tsx
TSX
1"use client";
2import { useFormatter, useNow, useTimeZone } from "next-intl";
3
4export function LocalizedContent() {
5 const format = useFormatter();
6 const now = useNow();
7 const timeZone = useTimeZone();
8
9 return (
10 <div>
11 {/* Number formatting */}
12 <p>{format.number(1234567.89, { style: "currency", currency: "USD" })}</p>
13 {/* en: $1,234,567.89 | es: 1.234.567,89 $ | de: 1.234.567,89 $ */}
14
15 <p>{format.number(0.856, { style: "percent" })}</p>
16 {/* en: 85.6% | es: 85,6% */}
17
18 <p>{format.number(1234567, { notation: "compact" })}</p>
19 {/* en: 1.2M | es: 1,2M */}
20
21 {/* Date formatting */}
22 <p>{format.dateTime(now, { dateStyle: "full" })}</p>
23 {/* en: Monday, July 14, 2026 | es: lunes, 14 de julio de 2026 */}
24
25 <p>{format.dateTime(now, { timeStyle: "short" })}</p>
26 {/* en: 2:30 PM | es: 14:30 */}
27
28 {/* Relative time */}
29 <p>{format.relativeTime(now, new Date("2026-07-10"))}</p>
30 {/* "4 days ago" */}
31
32 {/* Time zone */}
33 <p>Current timezone: {timeZone}</p>
34
35 {/* Plural rules */}
36 <p>{format.plural(0, { one: "# item", other: "# items" })}</p>
37 <p>{format.plural(5, { one: "# item", other: "# items" })}</p>
38 </div>
39 );
40}
41
42// Server-side formatting
43import { getFormatter, getTranslations } from "next-intl/server";
44
45export default async function ServerFormatted() {
46 const format = await getFormatter();
47 const t = await getTranslations("common");
48
49 const price = format.number(29.99, { style: "currency", currency: "EUR" });
50 const date = format.dateTime(new Date(), { dateStyle: "long" });
51
52 return (
53 <div>
54 <p>{t("welcome")}</p>
55 <p>Price: {price}</p>
56 <p>Date: {date}</p>
57 </div>
58 );
59}
Routing Strategies
routing.ts
TypeScript
1// Strategy 1: Locale prefix (default)
2// /en/about, /es/about, /fr/about
3
4// Strategy 2: No prefix (locale from cookie/header)
5// /about (locale determined by Accept-Language or cookie)
6
7// Strategy 3: Domain-based
8// en.example.com/about
9// es.example.com/about
10
11// middleware.ts — handle locale detection
12import createMiddleware from "next-intl/middleware";
13import { routing } from "./i18n/routing";
14
15export default createMiddleware(routing);
16
17export const config = {
18 matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
19};
20
21// next.config.js — add next-intl plugin
22const createNextIntlPlugin = require("next-intl/plugin");
23const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
24
25/** @type {import('next').NextConfig} */
26const nextConfig = {};
27module.exports = withNextIntl(nextConfig);
28
29// Locale detection priority:
30// 1. Cookie (user's previous choice)
31// 2. Accept-Language header (browser preference)
32// 3. Default locale fallback

info

Use the defaultLocale setting as a fallback. Always set a cookie when the user switches locale so their preference persists across visits.
Best Practices

1. Use getTranslations() in Server Components and useTranslations() in Client Components.

2. Organize message files by feature, not by language. Keep keys consistent across locales.

3. Use ICU message format for plurals and interpolation — it handles edge cases that string concatenation cannot.

4. Use format.dateTime() and format.number() instead of raw Date.toLocaleDateString() for consistent locale-aware formatting.

5. Use the Link component from next-intl/navigation — it automatically prefixes routes with the current locale.

6. Set hrefLang in your metadata for SEO — search engines use it to serve the correct language version.

$Blueprint — Engineering Documentation·Section ID: NEXT-I18N·Revision: 1.0