# Step-by-Step Guide to Implementing i18n in Next.js with next-intl

> Learn how to add internationalization to your Next.js App Router project using next-intl. Complete walkthrough from installation to production deployment.
- **Author**: Mohit Sapat
- **Published**: 2026-08-10
- **URL**: https://www.babelize.co/blogs/implement-i18n-nextjs-next-intl

---

<p class="lead">
    Adding internationalization to a Next.js application used to involve complex configuration, third-party backends, and wrestling with server-side rendering quirks. With <strong>next-intl</strong> and the App Router, that story has changed completely. This guide walks you through every step of implementing i18n in a Next.js 14+ project -- from installation to a production-ready multilingual app.
</p>

<h2>Why next-intl?</h2>

<p>
    <strong>next-intl</strong> is purpose-built for Next.js. Unlike framework-agnostic alternatives that require additional wrappers and workarounds, next-intl integrates with the App Router, React Server Components, Middleware, and Server Actions natively. This means faster page loads, smaller client bundles, and a developer experience that feels like a natural extension of Next.js itself. Still deciding? Our <a href="/blogs/next-intl-vs-react-i18next/">next-intl vs react-i18next comparison</a> covers when the alternative is the better fit.
</p>

<p>By the end of this guide, you'll have a Next.js app that serves content in multiple languages, detects user locale preferences, and handles fallback gracefully -- all in under 100 lines of configuration.</p>

<h2>Step 1: Install next-intl</h2>

<p>Start by installing the package in your Next.js project:</p>

<pre><code>npm install next-intl</code></pre>

<p>That's it -- no additional plugins, no backends, no complex dependency trees. The library ships with everything you need for both server and client-side translations.</p>

<h2>Step 2: Create Translation Files</h2>

<p>Translation files are plain JSON files organized by locale (see <a href="/blogs/json-translation-files-explained/">JSON translation files explained</a> for naming and structure conventions). Create a <code>messages/</code> directory at your project root:</p>

<pre><code>messages/
├── en.json
├── fr.json
├── es.json</code></pre>

<p>Each file contains key-value pairs for that locale. Here's a minimal <code>en.json</code>:</p>

<pre><code>{
  "HomePage": {
    "title": "Welcome to Our Platform",
    "description": "Build multilingual apps with confidence.",
    "cta": "Get Started"
  },
  "Navigation": {
    "home": "Home",
    "about": "About",
    "contact": "Contact Us"
  }
}</code></pre>

<p>And the corresponding <code>fr.json</code>:</p>

<pre><code>{
  "HomePage": {
    "title": "Bienvenue sur Notre Plateforme",
    "description": "Créez des applications multilingues en toute confiance.",
    "cta": "Commencer"
  },
  "Navigation": {
    "home": "Accueil",
    "about": "À propos",
    "contact": "Contactez-Nous"
  }
}</code></pre>

<p><strong>Tip:</strong> Use a nested structure to group translations by page or component. This keeps files organized as your application grows, and next-intl's type generation preserves the nesting for autocomplete.</p>

<h2>Step 3: Configure Middleware</h2>

<p>Create <code>src/middleware.ts</code> to handle locale detection and URL rewriting:</p>

<pre><code>import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";

export default createMiddleware(routing);

export const config = {
  matcher: ["/", "/(en|fr|es)/:path*"],
};</code></pre>

<p>Next, create <code>src/i18n/routing.ts</code> to define your supported locales:</p>

<pre><code>import { defineRouting } from "next-intl/routing";

export const routing = defineRouting({
  locales: ["en", "fr", "es"],
  defaultLocale: "en",
  localePrefix: "as-needed",
});</code></pre>

<p>The middleware does three things automatically:</p>
<ul>
    <li><strong>Detects</strong> the user's preferred locale from the URL or <code>Accept-Language</code> header</li>
    <li><strong>Redirects</strong> un-prefixed URLs to the correct locale (e.g., <code>/about</code> becomes <code>/en/about</code>)</li>
    <li><strong>Rewrites</strong> the URL internally so your server components receive the locale via <code>params.locale</code></li>
</ul>

<h2>Step 4: Create i18n Request Config</h2>

<p>Create <code>src/i18n/request.ts</code> to load the correct translation messages for each request:</p>

<pre><code>import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;

  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale;
  }

  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default,
  };
});</code></pre>

<p>This function runs for every request, loads the appropriate JSON file, and makes the messages available throughout your component tree. The fallback to the default locale ensures your app never breaks if a user requests an unsupported language.</p>

<h2>Step 5: Wire Up the Plugin</h2>

<p>Add next-intl to your <code>next.config.ts</code>:</p>

<pre><code>import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";

const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");

const nextConfig: NextConfig = {};

export default withNextIntl(nextConfig);</code></pre>

<p>This plugin ensures the request configuration is picked up during the build and that the i18n routing works correctly across all environments.</p>

<h2>Step 6: Wrap Your Layout</h2>

<p>Update your root layout at <code>src/app/[locale]/layout.tsx</code>:</p>

<pre><code>import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise&lt;{ locale: string }&gt;;
}) {
  const { locale } = await params;

  if (!routing.locales.includes(locale as any)) {
    notFound();
  }

  const messages = await getMessages();

  return (
    &lt;html lang={locale}&gt;
      &lt;body&gt;
        &lt;NextIntlClientProvider messages={messages}&gt;
          {children}
        &lt;/NextIntlClientProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>

<p>The <code>NextIntlClientProvider</code> makes translations available to all components within the tree. The <code>notFound()</code> call handles invalid locale URLs gracefully.</p>

<h2>Step 7: Using Translations in Components</h2>

<h3>Server Components</h3>

<p>Use <code>getTranslations()</code> for server components. These run entirely on the server with zero client-side JavaScript:</p>

<pre><code>import { getTranslations } from "next-intl/server";

export default async function HomePage() {
  const t = await getTranslations("HomePage");

  return (
    &lt;main&gt;
      &lt;h1&gt;{t("title")}&lt;/h1&gt;
      &lt;p&gt;{t("description")}&lt;/p&gt;
      &lt;button&gt;{t("cta")}&lt;/button&gt;
    &lt;/main&gt;
  );
}</code></pre>

<h3>Client Components</h3>

<p>For interactive components, use the <code>useTranslations()</code> hook:</p>

<pre><code>"use client";

import { useTranslations } from "next-intl";

export function Navigation() {
  const t = useTranslations("Navigation");

  return (
    &lt;nav&gt;
      &lt;a href="/"&gt;{t("home")}&lt;/a&gt;
      &lt;a href="/about"&gt;{t("about")}&lt;/a&gt;
      &lt;a href="/contact"&gt;{t("contact")}&lt;/a&gt;
    &lt;/nav&gt;
  );
}</code></pre>

<p>The key distinction: <code>getTranslations()</code> is an async function for Server Components, while <code>useTranslations()</code> is a synchronous hook for Client Components. next-intl handles the boundary seamlessly.</p>

<h2>Step 8: Dynamic Values and Pluralization</h2>

<p>next-intl supports ICU message syntax for dynamic content:</p>

<pre><code>{
  "Cart": {
    "itemCount": "You have {count, plural, =0 {no items} =1 {one item} other {# items}} in your cart.",
    "welcome": "Hello, {name}!"
  }
}</code></pre>

<p>Usage in components:</p>

<pre><code>t("Cart.itemCount", { count: 3 });  // "You have 3 items in your cart."
t("Cart.welcome", { name: "Marie" });  // "Hello, Marie!"</code></pre>

<p>The ICU syntax handles pluralization, gender, and complex formatting rules across all supported locales automatically. Our <a href="/blogs/icu-messageformat-plurals-guide/">ICU MessageFormat guide</a> covers plural categories, <code>select</code>, and nesting in depth.</p>

<h2>Step 9: Language Switcher</h2>

<p>Build a locale switcher using next-intl's navigation utilities:</p>

<pre><code>"use client";

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

const languages = [
  { code: "en", label: "English" },
  { code: "fr", label: "Français" },
  { code: "es", label: "Español" },
] as const;

export function LanguageSwitcher() {
  const pathname = usePathname();
  const router = useRouter();
  const locale = useLocale();

  return (
    &lt;select
      value={locale}
      onChange={(e) =&gt; router.push(pathname, { locale: e.target.value })}
    &gt;
      {languages.map(({ code, label }) =&gt; (
        &lt;option key={code} value={code}&gt;
          {label}
        &lt;/option&gt;
      ))}
    &lt;/select&gt;
  );
}</code></pre>

<p>The <code>usePathname</code> and <code>useRouter</code> from <code>@/i18n/navigation</code> automatically handle locale prefixes in URLs, so navigation between locales is seamless.</p>

<h2>Step 10: Typed Translations</h2>

<p>next-intl gets its types straight from your source-language messages file, so there is no separate generation step. Declare them once in a <code>global.d.ts</code> file at the project root:</p>

<pre><code>import messages from "./messages/en.json";

declare module "next-intl" {
  interface AppConfig {
    Messages: typeof messages;
  }
}</code></pre>

<p>Make sure <code>"resolveJsonModule": true</code> is set in your <code>tsconfig.json</code> so the JSON import type-checks.</p>

<p>Now your IDE will autocomplete translation keys, catch typos at compile time, and show inline documentation for every message in your translation files.</p>

<h2>Common Gotchas and Solutions</h2>

<ul>
    <li><strong>Client component hydration mismatch:</strong> Ensure <code>NextIntlClientProvider</code> wraps your entire app in the root layout, not in individual pages.</li>
    <li><strong>404 on locale pages:</strong> Double-check that your middleware matcher includes all locale prefixes and that <code>routing.locales</code> matches your folder structure.</li>
    <li><strong>Missing translations:</strong> Use the <code>onError</code> callback in your request config to log missing keys during development without crashing. If messages come from a translation API at request time, plan for outages with <a href="/blogs/translation-api-failure-handling/">timeouts, cached fallbacks, and retries</a>.</li>
    <li><strong>SEO considerations:</strong> Add <code>hreflang</code> tags and canonical URLs for each locale to avoid duplicate content issues. The <a href="/blogs/hreflang-multilingual-seo-guide/">hreflang and multilingual SEO guide</a> shows how to generate them in Next.js.</li>
    <li><strong>Translation management:</strong> As your app grows to 10+ languages, manually editing JSON files becomes tedious. Use automated localization tools like <strong>Babelize</strong> to sync translations, manage glossaries, and keep all locale files in sync without manual copy-pasting.</li>
</ul>

<h2>Production Checklist</h2>

<ul>
    <li>All translation keys exist in every locale JSON file</li>
    <li>RTL layouts tested if supporting Arabic, Hebrew, or other RTL scripts</li>
    <li>Dynamic content (dates, numbers, currencies) uses <code>Intl</code> formatting</li>
    <li><code>hreflang</code> tags are present on every page</li>
    <li>Language switcher preserves current page, not just redirects to home</li>
    <li>404 and error pages are also localized</li>
    <li>Translation files are excluded from client bundles (next-intl handles this automatically)</li>
</ul>

<h2>Next Steps</h2>

<p>At this point, you have a fully functional multilingual Next.js application. From here, consider:</p>

<ul>
    <li><strong>Automating translations</strong> -- Babelize can translate your JSON files in bulk, keep them in sync as you iterate, and even create GitHub PRs with the updated locale files. See the <a href="https://docs.babelize.co/sdk/nextjs" target="_blank" rel="noopener noreferrer">Babelize Next.js docs</a> to wire it into this setup.</li>
    <li><strong>Adding more locales</strong> -- next-intl scales effortlessly. Just add another JSON file and update your <code>routing.ts</code>.</li>
    <li><strong>Advanced patterns</strong> -- Explore <code>next-intl</code>'s support for rich text translations, ICU message formatting, and per-component message splitting.</li>
</ul>

<p>The hardest part of i18n isn't the library -- it's maintaining translations as your product evolves. next-intl eliminates the technical complexity. Tools like Babelize eliminate the operational complexity.</p>
---
- [All articles](https://www.babelize.co/blogs)