next-intl guide for Next.js: Practical i18n architecture with SimpleLocalize

Kinga Pomykała
Kinga Pomykała
Last updated: June 22, 20267 min read
next-intl guide for Next.js: Practical i18n architecture with SimpleLocalize

If you are building with Next.js App Router, next-intl is one of the strongest options for internationalization.

This guide covers current setup patterns from next-intl docs, production trade-offs, and a concrete translation workflow with SimpleLocalize.

For the broader architecture, key strategy, and localization workflows across frameworks, see the complete technical guide to internationalization and software localization.

If you are choosing between libraries, read react-i18next vs next-intl: Which should you use for React localization?.

What next-intl gives you

next-intl is designed for Next.js and supports:

  • App Router and Server Components
  • ICU messages for plurals, interpolation, and rich text
  • Locale-aware routing (path prefix and domain-based)
  • Navigation wrappers (Link, useRouter, redirect) that include locale context
  • Optional TypeScript augmentation for typed locales, keys, and formats

For up-to-date reference, use the official docs:

When next-intl is a great fit

next-intl works very well when:

  • Your app is Next.js App Router first
  • You want locale-aware URLs for indexing and sharing (/en/pricing, /de/pricing)
  • You need server-side translations in layouts, metadata, and server components
  • You want one i18n system for UI labels plus numbers, dates, and lists
  • Your team prefers message files in Git plus CI-based translation sync

Typical use cases

  • SEO-focused marketing pages with localized pathnames
  • SaaS dashboards with locale switchers and saved user locale preference
  • E-commerce checkout flows with price/date formatting and plural rules
  • Product sites that need domain-based locale routing by market

When another approach can be better

next-intl can be a weaker fit in these cases:

  • You are not on Next.js
  • You need one i18n library across web, React Native, Electron, and non-Next.js apps
  • You require runtime translation delivery as your default model across many frontends

Also note static export constraints. If your Next.js app uses output: 'export', proxy/middleware does not run. next-intl still works, but with limits documented by the project:

  • Locale prefix is required
  • Server locale negotiation is unavailable
  • Pathname localization config is unavailable

If your product depends heavily on runtime message updates without redeploys, compare file-based delivery with CDN-based delivery patterns and choose per route type.

Current setup pattern (App Router + locale routing)

The setup below follows next-intl docs for locale-based routing.

1. Define routing

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

export const routing = defineRouting({
  locales: ['en', 'de', 'fr', 'pl'],
  defaultLocale: 'en'
});

2. Add proxy (Next.js 16+) or middleware (Next.js 15 and lower)

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

export default createMiddleware(routing);

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

3. Create localized navigation wrappers

// src/i18n/navigation.ts
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';

export const {Link, redirect, usePathname, useRouter, getPathname} = createNavigation(routing);

4. Configure request-scoped locale and messages

// src/i18n/request.ts
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async ({requestLocale}) => {
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

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

5. Use [locale] segment and enable static params

// src/app/[locale]/layout.tsx
import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { setRequestLocale, getMessages } from 'next-intl/server'; // added getMessages
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';

// ... (generateStaticParams)

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

  if (!hasLocale(routing.locales, locale)) {
    notFound();
  }

  setRequestLocale(locale);
  const messages = await getMessages(); // Fetch messages for client components

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

Translation usage examples

Client component

'use client';

import {useTranslations} from 'next-intl';

export function UpgradeBanner({plan}: {plan: 'free' | 'pro'}) {
  const t = useTranslations('UpgradeBanner');

  return (
    <p>
      {t('cta', {plan})}
    </p>
  );
}
{
  "UpgradeBanner": {
    "cta": "Move to {plan, select, free {Free} pro {Pro} other {Free}} today"
  }
}

Server component

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

export default async function BillingSummary({
  total
}: {
  total: number;
}) {
  const t = await getTranslations('Billing');

  return <h2>{t('summary', {total})}</h2>;
}
{
  "Billing": {
    "summary": "Total due: {total, number, ::currency/USD}"
  }
}

Plurals and time formatting

{
  "Inbox": {
    "messages": "{count, plural, =0 {No messages} one {# message} other {# messages}}",
    "lastSync": "Last sync: {updatedAt, date, medium}"
  }
}
import {useTranslations} from 'next-intl';

export function InboxHeader({count, updatedAt}: {count: number; updatedAt: Date}) {
  const t = useTranslations('Inbox');

  return (
    <>
      <h1>{t('messages', {count})}</h1>
      <small>{t('lastSync', {updatedAt})}</small>
    </>
  );
}

Architecture flow for next-intl + SimpleLocalize

Here is the practical flow many Next.js teams use:

  • Git Repo (source of truth for messages and reviewable changes)
  • SimpleLocalize CLI (upload and download in local dev and CI)
  • SimpleLocalize Translation Platform (translators, reviewers, AI suggestions)
  • CI build (translations bundled and production-ready)

Implementation with SimpleLocalize

Most teams using next-intl keep message files in Git and sync them with a translation platform. This gives clear version history, predictable builds, and easier rollback.

Why this model performs so well in Next.js

For Next.js Server Components, CLI-based JSON sync is often a better default than dynamic translation API fetching per request:

  • Messages are bundled at build time or read from local disk, so rendering does not wait on an external translation API call.
  • Lower request-time I/O helps keep TTFB fast.
  • App Router rendering stays stable under traffic spikes because message loading is local and predictable.
  • Builds are reproducible. You know exactly which translation set went to production.

Dynamic loading still has its place for selected routes that need immediate content changes, but for most UI strings in Server Components, file-based sync is the best baseline.

1. Store source messages

/messages
  en.json
  de.json
  fr.json
  pl.json

2. Configure SimpleLocalize CLI

# simplelocalize.yml
apiKey: YOUR_PROJECT_API_KEY

uploadFormat: single-language-json
uploadLanguageKey: en
uploadPath: ./messages/en.json
uploadOptions:
  - REPLACE_TRANSLATION_IF_FOUND

downloadFormat: single-language-json
downloadLanguageKey: ['de', 'fr', 'pl']
downloadPath: ./messages/{lang}.json

Omit downloadLanguageKey to pull all languages. Use downloadPath with {lang} placeholder to save each language in its own file.

3. Add scripts

{
  "scripts": {
    "i18n:upload": "simplelocalize upload",
    "i18n:download": "simplelocalize download"
  }
}

4. Use CI to pull latest translations

# Example GitHub Actions step
- name: Download translations
  run: npx simplelocalize download
  env:
    SIMPLELOCALIZE_API_KEY: ${{ secrets.SIMPLELOCALIZE_API_KEY }}

5. Optional runtime loading pattern

If you want message updates without redeploy, load messages remotely in i18n/request.ts and apply caching rules. This can be useful for selected content-heavy sections, while core UI keeps file-based messages.

SimpleLocalize docs:

Common implementation mistakes

Some common mistakes to avoid:

  • Mixing non-localized imports (next/link) with localized navigation wrappers in locale-aware routes
  • Keeping hardcoded user-facing strings in server actions or metadata
  • Skipping locale validation and rendering invalid locale segments
  • Missing matcher coverage for routes with dots in path segments
  • Adding many tiny message namespaces too early and making extraction harder

Decision guide

Pick next-intl if your architecture is Next.js-centric and you want strong App Router integration.

Pick react-i18next if you need one shared i18n runtime across multiple React environments.

For many teams, the best process is simple:

  1. Keep message source files in repo
  2. Sync with SimpleLocalize in CI
  3. Add runtime message fetching only where deployment frequency is a real issue

This gives predictable releases and still leaves room for faster content updates where needed.

Kinga Pomykała
Kinga Pomykała
Content creator of SimpleLocalize

Get started with SimpleLocalize

  • All-in-one localization platform
  • Web-based translation editor for your team
  • Auto-translation, QA-checks, AI and more
  • See how easily you can start localizing your product.
  • Powerful API, hosting, integrations and developer tools
  • Unmatched customer support
Start for free
No credit card required5-minute setup
"The product
and support
are fantastic."
Laars Buur|CTO
"The support is
blazing fast,
thank you Jakub!"
Stefan|Developer
"Interface that
makes any dev
feel at home!"
Dario De Cianni|CTO
"Excellent app,
saves my time
and money"
Dmitry Melnik|Developer