next-intl Documentation

Internationalization (i18n) for Next.js

4,384 stars TypeScript TypeScript 45 files · ~88,516 tokens #date-formatting#i18n#next#react
RAW Doc

File: README.md

Internationalization for Next.js.

Features

Internationalization (i18n) is an essential part of the user experience, therefore next-intl gives you all the parts you need to get language nuances right.

  • 🌟 ICU message syntax: Localize your messages with interpolation, cardinal & ordinal plurals, enum-based label selection and rich text.
  • 📅 Dates, times & numbers: Apply appropriate formatting without worrying about server/client differences like time zones.
  • ✅ Type-safe: Speed up development with autocompletion for message keys and catch typos early with compile-time checks.
  • 💡 Hooks-based API: Learn a single API that can be used across your code base to turn translations into plain strings or rich text.
  • 🚀 Next.js-native and performance-obsessed: App Router, Server Components, static rendering—pick the right tool for the right job, next-intl works everywhere.
  • 🌍 Internationalized routing: Provide unique pathnames per language and optionally localize pathnames for search engine optimization.

→ Read the docs

What does it look like?

jsx
// UserProfile.tsx
import {useTranslations} from 'next-intl';

export default function UserProfile({user}) {
  const t = useTranslations('UserProfile');

  return (
    <section>
      <h1>{t('title', {firstName: user.firstName})}</h1>
      <p>{t('membership', {memberSince: user.memberSince})}</p>
      <p>{t('followers', {count: user.numFollowers})}</p>
    </section>
  );
}
js
// en.json
{
  "UserProfile": {
    "title": "{firstName}'s profile",
    "membership": "Member since {memberSince, date, short}",
    "followers": "{count, plural, ↵
                    =0 {No followers yet} ↵
                    =1 {One follower} ↵
                    other {# followers} ↵
                  }"
  }
}

Lint your messages

As an app grows, messages may drift and become inconsistent.

A companion tool, eloqnt/cli, catches this by analyzing your source code and messages statically:

console
$ eloqnt lint

messages/de.json
│
│  "UserProfile.title": "Dein Profil"
│                        ─┬──────────
│                         ╰─ Inconsistent ICU arguments: missing {firstName} (inconsistent-args)
│
│  "UserProfile.membership": "Member since {memberSince, date, short}"
│  ─┬──────────────────────
│   ╰─ Missing translation for de (missing-translation)

✗ 1 error
! 1 warning

→ Rule details: https://cli.eloqnt.dev/docs/lint-rules/<rule>
→ Run `eloqnt translate` to fill in 1 missing translation

It can optionally also fill in missing translations with eloqnt translate, using your source code as context.



File: AGENTS.md

Agents

Always

  • If running a test with vitest, use "run" to avoid being stuck in watch mode.

When committing

  • Make sure ESLint and Prettier pass on changed files.
  • Make sure all tests pass.

When creating a PR

  • When creating a PR, use conventional commit prefixes for the PR title (e.g. fix: , feat: , ...).

File: CONTRIBUTORS.md

Contributing to `next-intl`

Thank you so much for considering to contribute to next-intl!

This project is a team effort and we rely on individuals working together to build a sustainable and reliable project.

There are several ways you can contribute:

  1. Helping others by answering questions: Developers often seek guidance by posting questions in issues, discussions, and on Stack Overflow. Your responses, based on your experience, are greatly appreciated and help foster a supportive community.
  2. Docs: We focus on documentation as much as we do on the code itself. We strive to create helpful learning resources that efficiently communicate concepts and address common user issues. If you encounter areas that need clarification or have ideas to improve our documentation, we welcome and appreciate your contributions!
  3. Bug fixes: Fixing a bug always starts with creating a regression test that reliably reproduces the broken behavior. This alone is a valuable contribution and can be submitted as a pull request. If you're up for the challenge, feel free to provide the bug fix as well!
  4. New features: We encourage proposing feature ideas as issues prior to starting development. This practice helps to avoid duplicated efforts and allows us to align on the direction before investing significant development time. Note that features for exotic use cases that can already be achieved with the current feature set are unlikely to get merged, as the maintainence of such features over time takes significant effort.
  5. Sponsorship: next-intl is provided free of charge, but the project takes significant time to maintain. Luckily, next-intl receives regular funding from its primary sponsor Crowdin. However, features still need to be prioritized and user support is limited. If you'd like to give back in a financial way, you can consider providing sponsorship for the lead maintainer @amannn.

Open source work should be fun for everyone involved. Let's make sure it stays that way! Our communication style aims to be clear and friendly. We value empathy, respect, and understanding different perspectives.

Thank you for being a part of this project! 🙌

Documentation guidelines

The docs are consulted by developers with different levels of professional experience, different proficiencies of language & reading comprehension and at various points of their app development journey.

We aim to create resources that help developers in the following situations:

  1. Getting started: Developers have limited time and need to get a job done quickly. We try to limit the necessary information to get started, use simple & friendly language and rely on good defaults to support developers. Snippets that can be copy-pasted are frequently used and provide the possibility to include important information as code comments which are typically copied along.
  2. Going deeper: Developers who are already (somewhat) invested into next-intl will consult the docs to learn about advanced use cases. We use precise language so developers trust the library to work as expected, while optionally providing deep dives in expandable areas. We strive to provide relevant information that developers will likely need within the docs, but will use external links to trusted sources like MDN, Wikipedia and the Next.js docs to provide supplemental information for deep dives that aren't the main focus of next-intl.
  3. Fixing a bug: Developers may run into difficult bugs and need all the help they can get. When there are missing pieces within the library, we don't try to hide them, but instead provide documentation for workarounds. Especially in the case of bugs, it's important to provide optional deep dives that include all relevant details. Still, developers typically consult the GitHub issues & discussions of the project, therefore we can provide certain solutions to specific problems there too. Developers will also google their errors, therefore common errors can be addressed with troubleshooting sections that are indexed.

Development

Setup

  1. Make sure you have pnpm installed
  2. Clone the repo
  3. pnpm install
  4. pnpm build-packages

Now, you're all set and you can work on individual packages.

Tests

There are currently two test setups:

  1. Packages use Vitest
  2. Examples use Playwright

In either case, you can focus individual tests during development via it.only.

Linting & formatting

This project uses ESLint for detecting issues in code, as well as Prettier for formatting.

It's recommended to use an editor integration for both, so that autofixable issues are automatically resolved. For editor plugins to work correctly, you should open individual packages you work on as the workspace root (e.g. next-intl/packages/next-intl instead of next-intl).

Alternatively, you can run ESLint and Prettier via the command line:

sh
cd packages/next-intl
pnpm eslint src --fix
pnpm prettier src --write

Pull requests

This repository uses action-semantic-pull-request to ensure that pull request titles match the Conventional Commits spec. This is due to PR titles being used as commit messages to automate releases.

Repository workflows

  • New issues receive the unconfirmed label upon creation and should be regularly triaged. If the issue is actionable, the unconfirmed label should be removed. If this is not the case, the issue will be closed after 30 days.
  • Add the reproduction-missing label to an issue to automatically add a comment and to mark it for being automatically closed in the future in case no reproduction gets added.
  • Add the needs-isolation label to issues that require further isolation since they might contain extraneous code or 3rd party libraries that make it hard to understand if an issue is caused by next-intl.

Releases

Releases are automated via lerna-light. To determine the next version, Conventional Commits is used.

Stable releases

Every commit on main with the following prefixes will trigger a release:

  1. fix: : Patch release
  2. feat: : Minor release

Due to this, it's important to clean up commit messages of merged PRs since the commit title will appear in the changelog. Note that the PR title and description should be cleaned up by the person who initiates the merge since the PR is linked to from the changelog & release and should contain relevant details.

Note that the exclamation mark syntax (!) for indicating breaking changes is currently not supported by Lerna. Instead, a block like BREAKING CHANGE: Dropped support for Node.js 12 should be added to the body of the commit message.

Other prefixes that are allowed and will not create a release are the following:

  1. docs: Documentation-only changes and updated examples
  2. test: Missing tests were added or existing ones corrected
  3. ci: Changes to CI configuration files and scripts
  4. build: Changes that affect the build system or external dependencies
  5. chore: Other changes that don't modify src or test files

Prereleases

Canary versions are automatically published on the canary branch. The prerelease version is determined based on the commit prefix (fix/feat).

When merging from canary to main, a release will be triggered if new commits are added that use one of the release prefixes mentioned above. Note that the canary branch should also be merged to main via a pull request for documentation and review purposes.


File: lerna.json

{
"$schema": "node_modules/@lerna-lite/cli/schemas/lerna-schema.json",
"version": "4.14.9",
"packages": [
"packages/*"
],
"npmClient": "pnpm",
"changelogPreset": "conventional-changelog-conventionalcommits",
"command": {
"publish": {
"removePackageFields": [
"devDependencies",
"prettier"
],
"yes": true
},
"version": {
"changelogIncludeCommitsClientLogin": " – by @%l",
"conventionalCommits": true,
"createRelease": "github",
"syncWorkspaceLock": true
}
}
}


File: package.json

{
"name": "root",
"private": true,
"scripts": {
"release": "lerna publish",
"build-packages": "turbo run build --filter './packages/**' --filter '!./packages/swc-plugin-extractor'"
},
"devDependencies": {
"@lerna-lite/cli": "^4.9.4",
"@lerna-lite/publish": "^4.9.4",
"conventional-changelog-conventionalcommits": "^8.0.0",
"turbo": "^2.4.4"
},
"pnpm": {
"overrides": {
"@babel/parser": "7.21.9"
},
"patchedDependencies": {
"nextra-theme-docs": "patches/nextra-theme-docs.patch"
}
},
"packageManager": "[email protected]"
}


File: pnpm-workspace.yaml

packages:

  • 'packages/*'
  • 'examples/*'
  • 'e2e/*'
  • 'docs'
  • 'tools'

File: turbo.json

{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [
"dist/",
".next/
",
"!.next/cache/",
"build/
",
"target/wasm32-wasip1/release/*.wasm"
]
},
"next-intl#build": {
"dependsOn": ["use-intl#build", "tools#build"],
"outputs": ["dist/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"example-app-router-playground#lint": {
"dependsOn": ["example-app-router-playground#build"]
},
"test": {
"dependsOn": ["build"]
},
"size": {
"dependsOn": ["build"]
}
}
}


File: .prettierrc.json

{
"singleQuote": true,
"bracketSpacing": false,
"trailingComma": "none"
}


File: docs/README.md

docs

Website for next-intl.

You can run it locally like this:

text
pnpm install
pnpm dev

File: docs/package.json

{
"name": "docs",
"private": true,
"scripts": {
"dev": "next dev",
"lint": "eslint src && prettier src --check && node lintLinks.mjs",
"test": "echo 'No tests yet'",
"build": "next build && next-sitemap && node scripts/generate-rss.mjs",
"start": "next start"
},
"dependencies": {
"@docsearch/css": "^3.9.0",
"@docsearch/react": "^3.9.0",
"@headlessui/react": "^2.2.0",
"@heroicons/react": "^2.2.0",
"@vercel/analytics": "1.5.0",
"@vercel/speed-insights": "^1.2.0",
"clsx": "^2.1.1",
"http-status-codes": "^2.3.0",
"next": "^14.2.35",
"nextra": "^3.3.1",
"nextra-theme-docs": "^3.3.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-tweet": "^3.2.2",
"shiki": "^1.22.0",
"tailwindcss": "^3.4.4"
},
"devDependencies": {
"@types/node": "^20.14.5",
"@types/react": "^19.2.1",
"autoprefixer": "^10.4.19",
"eslint": "9.11.1",
"eslint-config-molindo": "^8.0.0",
"fast-glob": "^3.3.3",
"globals": "^16.0.0",
"next-sitemap": "^4.2.3",
"next-validate-link": "^1.5.1",
"prettier": "^3.3.3",
"typescript": "^6.0.0"
},
"prettier": "eslint-config-molindo/.prettierrc.json",
"engines": {
"node": ">=20.0.0"
}
}


File: docs/tsconfig.json

{
"extends": "eslint-config-molindo/tsconfig.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"noUncheckedSideEffectImports": false,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
],
"strict": true,
"target": "ES2017",
"paths": {
"@/": ["./src/"]
}
},
"include": [
"next-env.d.ts",
".d.ts",
"**/
.d.ts",
"/*.mdx",
"
/.ts",
"**/
.tsx",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}


File: docs/src/components/StayUpdated.mdx


Let's keep in touch:


File: docs/src/pages/examples.mdx

import Example from '@/components/Example';

Examples

These examples can provide inspiration for your project, showcasing different setup scenarios.

<Example
featured
image="/template-app-pdp.png"
name="Production-ready templates"
description="Get started quickly with fully-featured project templates, built with internationalization best practices from the ground up."
id="advanced"
sourceLink="https://learn.next-intl.dev/#project-code"
badges={['SEO', 'CMS', 'AI', 'Auth']}
/>


File: docs/src/pages/index.mdx


title: next-intl

import Callout from '@/components/Callout';
import CodeSnippets from '@/components/CodeSnippets';
import FeaturePanel from '@/components/FeaturePanel';
import Hero from '@/components/Hero';
import Link from '@/components/Link';
import LinkButton from '@/components/LinkButton';
import PartnerBanner from '@/components/PartnerBanner';
import Section from '@/components/Section';
import UserTestimonial from '@/components/UserTestimonial';
import UserCompanies from '@/components/UserCompanies';
import Wrapper from '@/components/Wrapper';
import GitHubStarsLinkButton from '@/components/GitHubStarsLinkButton';

<Hero
title="Internationalization for Next.js"
description="Support multiple languages, with your app code becoming simpler instead of complex."
getStarted="Get started"
viewExample="View an example"
announcement={{
href: 'https://learn.next-intl.dev',
label: 'The next-intl course is out now!'
}}
/>

text
<FeaturePanel
  code={<CodeSnippets.icu />}
  title="ICU message syntax"
  description="Localize your messages with interpolation, cardinal & ordinal plurals, enum-based label selection and rich text."
/>
<FeaturePanel
  code={<CodeSnippets.datesTimesNumbers />}
  title="Dates, times, numbers & more"
  description="Apply appropriate formatting without worrying about server/client differences like time zones."
/>
<FeaturePanel
  code={<CodeSnippets.typeSafe />}
  title="Type-safe"
  description="Speed up development with autocompletion for message keys and catch typos early with compile-time checks."
/>
<FeaturePanel
  code={<CodeSnippets.hooks />}
  title="Hooks-based API"
  description="Learn a single API that can be used across your code base to turn translations into plain strings or rich text."
/>
<FeaturePanel
  code={<CodeSnippets.buildOutput />}
  title="Next.js-native and performance-obsessed"
  description="App Router, Server Components, static rendering—pick the right tool for the right job, next-intl works everywhere."
/>
<FeaturePanel
  code={<CodeSnippets.routing />}
  title="Internationalized routing"
  description="Provide unique pathnames per language and optionally localize pathnames for search engine optimization."
/>
text
Using next-intl too? <Link target="_blank" href="https://github.com/amannn/next-intl/issues/138">Let us know!</Link>
text
<h2 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white lg:text-4xl">
    Get started with <code className="nextra-code">next-intl</code>
  </h2>
  
    <LinkButton href="/docs/getting-started" size="lg">
      Read the docs
    </LinkButton>
    <GitHubStarsLinkButton />

File: docs/src/pages/blog/date-formatting-nextjs.mdx


title: Reliable date formatting in Next.js

import Tweet from '@/components/Tweet';
import StayUpdated from '@/components/StayUpdated.mdx';

Reliable date formatting in Next.js

Sep 25, 2024 (updated on Mar 28, 2025) · by Jan Amann

Let's take a look at the following component:

tsx
import {formatDistance} from 'date-fns';

type Props = {
  published: Date;
};

export default function BlogPostPublishedDate({published}: Props) {
  const now = new Date();

  // ... is this ok? 🤔
  return <p>{formatDistance(published, now, {addSuffix: true})}</p>;
}

A quick local test of this component renders the expected result:

text
1 hour ago

So, should we push this to production?

Hmm, wait—something feels a bit off here. Let's take a closer look together.

Environment differences

Since this component neither uses any interactive features of React like useState, nor does it read from server-side data sources, it can be considered a shared component. This means that depending on where the component is being imported from, it may render as either a Server Component or a Client Component.

Let's consider where the component renders in either case:

Type Server Client
Server Component ✅
Client Component ✅ ✅

Now the good news is that if we render this component as a Server Component, there's only one environment to consider: the server. The final markup is generated there, and this is what the user will see.

When it comes to Client Components, the situation is a bit more complex. Since the server and the client likely have a different local time when the component is rendered, we can already anticipate that this component may render inconsistently depending on the environment.

There's some interesting nuance here as well: Since our component qualifies as a shared component, it can run in either environment. Even if the developer originally intended the component to run as a Server Component, it may silently switch to a Client Component if it gets imported into a Client Component in the future—leading to the additional rendering environment to consider.

Hydration mismatches

Let's take a closer look at what happens when BlogPostPublishedDate renders as a Client Component.

In this case, the value for now will always differ between the server and client, due to the latency between these two environments. Depending on factors like caching, the difference may be even significant.

tsx
// Server: "1 hour ago"
formatDistance(published, now, {addSuffix: true})}

// Client: "8 days ago"
formatDistance(published, now, {addSuffix: true})}

React is not particularly happy when it encounters such a situation:

Text content does not match server-rendered HTML

Interestingly, there's a discussion about React patching the Date object in the future, which could potentially help to mitigate this issue.

This is however not the case currently, and there's a also bit more to it—so let's move on for now.

Purity

The crucial part of the component is here:

tsx
const now = new Date();

If you've been using React for a while, you may be familiar with the necessity of components being pure.

Quoting from the React docs, we can keep a component pure by considering these two aspects:

It minds its own business: It does not change any objects or variables that existed before it was called.
Same inputs, same output: Given the same inputs, a pure function should always return the same result.

Since the component is reading from the constantly changing new Date() constructor during rendering, it violates the principle of "same inputs, same output". React components require functional purity to ensure consistent output when being re-rendered (which can happen at any time and often without the user explicitly asking the UI to update).

But is this true for all components? In fact, with the introduction of Server Components, there's a new type of component in town that doesn't have the restriction of "same inputs, same output". Server Components can for instance fetch data, making their output reliant on the state of an external system. This is fine, because Server Components only generate an output once—on the server.

So what does this mean for our component?

Leveraging Server Components

Right, you may have guessed it: We can move the creation of the now variable to a Server Component and pass it down as a prop.

tsx filename="page.tsx"
import BlogPostPublishedDate from './BlogPostPublishedDate';

export default function BlogPostPage() {
  // ✅ Is only called on the server
  const now = new Date();

  const published = ...;

  return <BlogPostPublishedDate now={now} published={published} />;
}

Pages in Next.js render as Server Components by default, so if we don't see a 'use client' directive in this file, we can be sure that the now variable is only created on the server side.

The now prop that we're passing to BlogPostPublishedDate is an instance of Date that can naturally be serialized by React. This means that we can pick it up in our component—both when executing on the server, as well as on the client.

It's worth mentioning that the published date will now update based on the caching rules of the page, therefore if your page renders statically, you might want to consider introducing a revalidate interval.

There's an argument that you might even want to render an updated date on the client side—however, this approach comes with the tradeoff that your final render depends on client-side code to be executed. If this is what makes sense for your use case, you can instead use suppressHydrationWarning to tell React that it's ok for this markup to be updated on the client side.

Are we done yet?

What time is it?

What if we have more components that rely on the current time? We could instantiate the now variable in each component that needs it, but if you consider that even during a single render pass there can be timing differences, this might result in inconsistencies if you're working with dates that require precision.

An option to ensure that a single now value is used across all components that render as part of a single request is to use the cache() function from React:

tsx filename="getnow.ts"
import {cache} from 'react';

// The first component that calls `getNow()` will
// trigger the creation of the `Date` instance.
const getNow = cache(() => new Date());

export default getNow;

… and use it in our page component:

tsx filename="page.tsx"
import getNow from './getNow';

export default function BlogPostPage() {
  // ✅ Will be consistent for the current request,
  // regardless of the timing of different calls
  const now = getNow();
  // ...
}

Now, the first component to call getNow() will trigger the creation of the Date instance. The instance is now bound to the request and will be reused across all subsequent calls to getNow().

Well, are we done now?

Where are we?

We've carefully ensured that our app is free of hydration mismatches and have established consistent time handling across all components. But what happens if we decide one day that we don't want to render a relative time like "2 days ago", but a specific date like "Sep 25, 2024"?

tsx
import {format} from 'date-fns';

type Props = {
  published: Date;
};

export default function BlogPostPublishedDate({published}: Props) {
  // `now` is no longer needed? 🤔
  return <p>{format(published, 'MMM d, yyyy')}</p>;
}

A quick local test shows that everything seems fine, so let's push this to production.

Text content does not match server-rendered HTML

Back to square one.

What's happening here? While our local test worked fine, we're suddenly getting an error in production.

The reason for this is: Time zones.

Handling time zones

While we, as performance-oriented developers, try to serve our app from a location that's close to the user, we can't expect that the server and the client share the same time zone. This means that the call to format can lead to different results on the server and the client.

In our case, this can lead to different dates being displayed. Even more intricate: Only at certain times of the day, where the time zone difference is significant enough between the two environments.

A bug like this can involve quite some detective work. I've learned this first hand, having written more than one lengthy pull request description, containing fixes for such issues in apps I've worked on.

To fix our new bug, the solution is similar to the one we've used for the now variable: We can create a timeZone variable in a Server Component and use that as the source of truth.

tsx filename="page.tsx"
export default function BlogPostPage() {
  // ...

  // Use the time zone of the server
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

  return <BlogPostPublishedDate timeZone={timeZone} published={published} />;
}

To incorporate this into our date formatting, we can use the date-fns-tz package, which wraps date-fns and adds support for formatting dates in a given time zone.

tsx
import {format} from 'date-fns-tz';

type Props = {
  published: Date;
  timeZone: string;
};

export default function BlogPostPublishedDate({published, timeZone}: Props) {
  return <p>{format(published, timeZone, 'MMM d, yyyy')}</p>;
}

Sticking to a single time zone for your app is definitely the easiest solution here. However, in case you'd like to format dates in the user's time zone, a reasonable approach might require having the time zone available on the server side so that it can be used in server-only code.

As browsers don't include the time zone of the user in an HTTP request, there are two primary ways to retrieve it on the server side:

  1. Set a cookie with the browser time zone on the client and retrieve it on the server side using cookies()
  2. Use approximate geographical information from the user's IP address (e.g. x-vercel-ip-timezone). However, it's important to note that this is only an approximation.

Localized date formatting

So far, we've assumed that our app will be used by American English users, with dates being formatted like:

text
Sep 25, 2024

Our situation gets interesting again, once we consider that the date format is not universal. In Great Britain, for instance, the same date might be formatted as "19 Sept 2024", with the day and month being swapped.

In case we want to localize our app to another language, or even support multiple languages, we now need to consider the locale of the user. Simply put, a locale represents the language of the user, optionally combined with additional information like the region (e.g. en-GB represents English as spoken in Great Britain).

To address this new requirement, you might already have a hunch where this is going.

Ensuring consistent date formatting across the server and client requires that we'll create a locale variable in a Server Component and pass it down to relevant components. This can in turn be used by a library like date-fns-tz to format the date accordingly.

tsx
import {format} from 'date-fns-tz';

type Props = {
  published: Date;
  timeZone: string;
  locale: string;
};

export default function BlogPostPublishedDate({
  published,
  timeZone,
  locale
}: Props) {
  return <p>{format(published, timeZone, 'MMM d, yyyy', {locale})}</p>;
}

It's important to pass the locale to all formatting calls now, as this can differ by environment—just like the timeZone and our value for now from earlier.

Can `next-intl` help?

The main problem we've addressed in this post revolves around hydration mismatches that occur when formatting dates across the server and client in Next.js applications. To avoid these errors, we need to ensure that three key environment properties are shared across the entire app:

  1. now: A single, shared timestamp representing the current time
  2. timeZone: Geographical location of the user, affecting date offsets
  3. locale: Language and regional settings for localization

Since you're reading this post on the next-intl blog, you've probably already guessed that we have an opinion on this subject. Note that this is not at all a critizism of libraries like date-fns & friends. On the contrary, I can only recommend these packages.

The challenge we've discussed in this post is rather about the centralization and distribution of environment configuration across a Next.js app, involving interleaved rendering across the server and client that is required for formatting dates consistently. Even when only supporting a single language within your app, this already requires careful consideration.

next-intl uses a centralized i18n/request.ts module that allows to provide request-specific environment configuration like now, timeZone and the locale of the user.

tsx filename="src/i18n/request.ts"
import {getRequestConfig} from 'next-intl/server';

export default getRequestConfig(async () => ({
  // (opt-in to use a shared value across the app)
  now: new Date(),

  // (defaults to the server's time zone)
  timeZone: 'Europe/Berlin',

  // (requires an explicit preference)
  locale: 'en'

  // ...
}));

It's worth noting that a shared value for now is opt-in, as some apps might prefer rendering updated dates on the client side when using relative time formatting or might be fine with slightly diverging dates in favor of granular cache control when using dynamicIO.

Note that, as the name of getRequestConfig implies, the configuration object can be created per request, allowing for dynamic configuration based on a given user's preferences.

This can now be used to format dates in components—at any point of the server-client spectrum:

tsx
import {useFormatter} from 'next-intl';

type Props = {
  published: Date;
};

export default function BlogPostPublishedDate({published}: Props) {
  // ✅ Works in any environment
  const format = useFormatter();

  // "Sep 25, 2024"
  format.dateTime(published);

  // "8 days ago"
  format.relativeTime(published);
}

Behind the scenes, i18n/request.ts is consulted by all server-only code, typically Server Components, but also Server Actions or Route Handlers. In turn, a component called NextIntlClientProvider, commonly placed in the root layout of your app, inherits this configuration and makes it available to all client-side code.

As a result, formatting functions like format.dateTime(…) can seamlessly access the necessary configuration in any environment. This configuration is then passed to native JavaScript APIs like Intl.DateTimeFormat to achieve correct and consistent formatting.

(this post has been updated for [email protected])


Related resources

While the main focus of this post was on date formatting, there are a few related resources that I can recommend if you're interested in digging deeper into the topic:

  1. API and JavaScript Date Gotcha's by Jökull Solberg
  2. The Problem with Time & Timezones by Computerphile
  3. date-fns by Sasha Koss

File: docs/src/pages/blog/index.mdx

import BlogPostLink from '@/components/BlogPostLink';
import StayUpdated from '@/components/StayUpdated.mdx';

next-intl blog


File: docs/src/pages/blog/next-intl-3-0.mdx


title: next-intl 3.0

import PartnerContentLink from '@/components/PartnerContentLink';
import StayUpdated from '@/components/StayUpdated.mdx';

next-intl 3.0

Nov 14, 2023 · by Jan Amann

More than a year ago, on Oct 25, 2022, Next.js 13 was announced with beta support for the App Router and Server Components. Ever since then, next-intl began an exploration on what it means to provide an ideal experience for implementing i18n with the newly added capabilities.

Today, after more than 370 commits and the involvement of over 60 community members, I'm absolutely thrilled to announce next-intl 3.0, which is now App Router-first.

If you're still happy with the Pages Router, rest assured that next-intl is dedicated to support this paradigm for as long as Next.js does. For those who have already migrated to the App Router, this means that you can take full advantage of the new capabilities with next-intl.

New features

  1. Support for React Server Components: The APIs useTranslations, useFormatter, useLocale, useNow and useTimeZone can now be used in Server Components (docs).
  2. New async APIs: To handle i18n in async components, the Metadata API and Route Handlers, the APIs getTranslations, getFormatter, getNow, and getTimeZone have been added (docs).
  3. Middleware for internationalized routing: While Next.js has built-in support for this with the Pages Router, the App Router doesn't include a built-in solution anymore. next-intl now provides a drop-in solution that has you covered (docs).
  4. Internationalized navigation APIs: Similar to the middleware, this provides a drop-in solution that adds internationalization support for Next.js' navigation APIs: Link, useRouter, usePathname and redirect. These APIs allow you to handle locale prefixes behind the scenes and also provide support for localizing pathnames (e.g. /en/about vs. /de/ueber-uns, see the docs).

The latter two have already been added in minor versions, but 3.0 cleans up the API and includes many improvements and bug fixes.

Breaking changes

Users of pre-releases: If you've already tried out pre-release versions, first of all, thank you so much! Second: Some APIs saw iterations over the pre-release period, please carefully review the breaking changes below, even if you're already using some of the new APIs. Also note the change from getTranslator to getTranslations to provide better ergonomics for working with async Server Components.

Updated setup

next-intl now requires two additional setup steps when you're using the App Router:

  1. The i18n.ts module provides configuration for Server Components
  2. next-intl/plugin needs to be added to link your i18n.ts module to next-intl

New navigation APIs for the App Router

With v2.14, the navigation APIs useRouter, usePathname and Link were added to next-intl that enabled you to use the APIs you're used to from Next.js while automatically considering a locale behind the scenes.

With 3.0, we're cleaning up these APIs by moving them to a shared namespace as well as introducing type-safety for the locale prop that can be passed to these APIs.

diff
- import Link from 'next-intl/link';
- import {useRouter, usePathname} from 'next-intl/client';
- import {redirect} from 'next-intl/server';

+ import {createSharedPathnamesNavigation} from 'next-intl/navigation';
+
+ const locales = ['en', 'de'] as const;
+ const {Link, useRouter, usePathname, redirect} = createSharedPathnamesNavigation({locales});

Typically, you'll want to call this factory function in a central place in your app where you can easily import from (see the navigation docs).

These changes bring the existing APIs in line with the new createLocalizedPathnamesNavigation API that allows you to localize pathnames:

tsx filename="navigation.ts"
import {
  createLocalizedPathnamesNavigation,
  Pathnames
} from 'next-intl/navigation';

export const locales = ['en', 'de'] as const;

// The `pathnames` object holds pairs of internal
// and external paths, separated by locale.
export const pathnames = {
  // If all locales use the same pathname, a
  // single external path can be provided.
  '/': '/',
  '/blog': '/blog',

  // If locales use different paths, you can
  // specify each external path per locale.
  '/about': {
    en: '/about',
    de: '/ueber-uns'
  }
} satisfies Pathnames<typeof locales>;

export const {Link, redirect, usePathname, useRouter} =
  createLocalizedPathnamesNavigation({locales, pathnames});

By using a similar API, you can upgrade from shared pathnames to localized pathnames by replacing the factory function.

Switching the middleware default of `localePrefix` to `always`

Previously, the localePrefix of the middleware defaulted to as-needed, meaning that a locale prefix was only added for non-default locales.

This default has now been changed to always since this has two advantages:

  1. We can recommend a safer default matcher that needs no extra treatment for pathnames with dots (e.g. /users/jane.doe)
  2. It avoids an edge case of Link where we include a prefix for the default locale on the server side but patch this on the client side by removing the prefix (certain SEO tools might report a hint that a link points to a redirect in this case).

If you want to stay on the as-needed strategy, you can configure this option in the middleware.

Static rendering of Server Components

With the newly introduced Server Components support comes a temporary workaround for static rendering.

If you call APIs from next-intl in Server Components, the page will by default opt into dynamic rendering. This is a limitation that we aim to remove in the future, but as a stopgap solution, we've added the unstable_setRequestLocale API so that you can keep your pages fully static.

Note that if you're using next-intl exclusively in Client Components, this doesn't apply to your app and static rendering will continue to work as it did before.

Changes to [`NextIntlClientProvider`](/docs/usage/configuration#nextintlclientprovider)

First, the import for this component has changed:

diff
- import {NextIntlClientProvider} from 'next-intl/client';
+ import {NextIntlClientProvider} from 'next-intl';

Depending on if you're using NextIntlClientProvider with or without the App Router, there's been another change to consider that was implemented in order to avoid hydration mismatches across the server and client.

Using NextIntlClientProvider without the App Router

If you're using NextIntlClientProvider without the App Router (e.g. with the Pages Router), you need to define the locale prop now explicitly (e.g. via useRouter().locale). Furthermore, a warning has been added if a timeZone hasn't been defined.

Using NextIntlClientProvider with the App Router

If you're using NextIntlClientProvider with the App Router, there has been a slight change to the semantics. If you're rendering NextIntlClientProvider within a Server Component, the component will pass defaults for locale, now and timeZone to the client side.

If you're already providing these options, then you're set.

If you're not, be aware that this will by default opt the corresponding page into dynamic rendering. If your app relies on static rendering, you can avoid this by passing all of the mentioned options:

tsx
<NextIntlClientProvider
  messages={messages}
  // By providing these props explicitly,
  // the provider can render statically.
  timeZone="Europe/Vienna"
  now={new Date()}
  locale={locale}
>
  ...
</NextIntlClientProvider>

Alternatively, you can explicitly enable static rendering—see the previous section.

Other notable changes

  1. next-intl now uses exports in package.json to clearly define which modules are exported. This should not affect you, unless you've previously imported undocumented internals.
  2. NextIntlProvider has been removed in favor of NextIntlClientProvider
  3. The middleware now needs to be imported from next-intl/middleware instead of next-intl/server (deprecated since v2.14).
  4. next@^13.4 is now required for the RSC APIs. Next.js 10–12 is still supported for the Pages Router integration via use-intl (see also: Support for legacy Next.js versions).
  5. useMessages now has a non-nullable return type for easier consumption and will throw if no messages are configured.
  6. createTranslator(…).rich now returns a ReactNode. Previously, this was somewhat confusing, since t.rich accepted and returned either React elements or strings depending on if you retrieve the fuction via useTranslations or createTranslator. Now, an explicit t.markup function has been added to generate markup strings like '**Hello**' outside of React components.
  7. useIntl has been replaced with useFormatter (deprecated since v2.11).
  8. createIntl has been replaced with createFormatter (deprecated since v2.11).
  9. useLocale now requires next@>=13.3.0. If you're on an older version of Next.js and you're using the Pages Router, please use useRouter().locale instead.

Upgrade now

text
npm install next-intl@latest

Thank you

This release was truly a team effort and couldn't be nearly where it is today without the involvement of the community.

Thank you so much for:

  • All the encouraging words along the way
  • Testing out prereleases
  • Providing feedback
  • Contributing code
  • Questioning ideas
  • Helping each other

I had the pleasure to get in touch with so many of you along the way and I'm incredibly grateful for the willingness to help and support each other in our community.

Participants who joined the conversation in [pull request #149](https://github.com/amannn/next-intl/pull/149).

A special thank you goes to Crowdin, being the primary sponsor for next-intl and enabling me to regularly dedicate time for this project.

—Jan

(this post has been updated from an initial announcement for the 3.0 release candidate)


File: docs/src/pages/blog/next-intl-3-22.mdx


title: next-intl 3.22 subtitle: Incrementally moving forward

import StayUpdated from '@/components/StayUpdated.mdx';

next-intl 3.22: Incrementally moving forward

Oct 21, 2024 · by Jan Amann

Over the past few months, a number of minor releases have been rolled out, each providing incremental improvements to next-intl. And while each of these releases has been an improvement on its own, they've also been part of a larger progression towards a more unified and streamlined API surface in general.

Today, another minor release was published that marks the final step of this progression: [email protected].

While this release is fully backwards-compatible, it includes some modern alternatives to existing APIs. Therefore, it's a good time to take a moment to review the changes and consider migrating where relevant.

Recent improvements:

  1. defineRouting: Type-safe, centralized routing configuration (introduced in v3.18)
  2. i18n/request.ts: Streamlined file organization (introduced in v3.19)
  3. await requestLocale: Preparation for Next.js 15 (introduced in v3.22)
  4. createNavigation: Streamlined navigation APIs (introduced in v3.22)
  5. setRequestLocale: Static rendering (marked as stable in v3.22)
  6. defaultTranslationValues: Deprecated in favor of a user-land pattern (introduced in v3.22)

Let's take a look at these changes in more detail.

`defineRouting` [#define-routing]

Previously, configuration that was shared among the middleware and the navigation APIs required special care to be kept in sync. With the introduction of defineRouting, this configuration can now be centralized in a type-safe manner:

tsx filename="i18n/routing.ts"
import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['en-US', 'en-GB'],
  defaultLocale: 'en-US',
  localePrefix: {
    mode: 'always',
    prefixes: {
      'en-US': '/us',
      'en-GB': '/uk'
    }
  },
  pathnames: {
    '/': '/',
    '/organization': {
      'en-US': '/organization',
      'en-GB': '/organisation'
    }
  }
});

This routing config is typically defined in a central place like i18n/routing.ts and can be used where previously individual configuration was required:

tsx filename="middleware.ts"
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

export default createMiddleware(routing);

// ...

If you've used defineRouting previously to v3.22 and have provided middleware options as a second argument to createMiddleware, you can now pass these to defineRouting instead.

The docs have been consistently updated to reflect these changes and also suggest the creation of navigation APIs directly in i18n/routing.ts for simplicity. If you prefer to keep your navigation APIs separately, that's of course fine as well.

`i18n/request.ts` [#i18n-request]

If you're using locale-based routing, you'll typically end up with two configuration files for next-intl:

  1. Routing configuration (defined via defineRouting)
  2. Request configuration (defined via getRequestConfig)

While (2) was historically suggested at i18n.ts, the default is now changing to i18n/request.ts in order to streamline the organization of files and to communicate the purpose of this file more clearly:

text
└── src
    └── i18n
        ├── routing.ts (1)
        └── request.ts (2)

Due to this, i18n/request.ts is now considered the new default and is suggested throughout the docs.

If you prefer to use a custom location instead, you can do so by providing it in next.config.mjs:

tsx filename="next.config.mjs"
import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin('./somewhere/else/request.ts');

// ...

`await requestLocale` in `getRequestConfig` [#await-request-locale]

Next.js 15 is on the horizon and introduces a change for request APIs to turn async. In preparation for this change, the locale that is passed to getRequestConfig has been replaced by requestLocale with some minor differences:

  1. requestLocale needs to be awaited
  2. The result can be undefined, therefore requiring a fallback
  3. The locale should now also be returned from getRequestConfig
diff
+ import {routing} from './i18n/routing';

export default getRequestConfig(async ({
-  locale
+  requestLocale
}) => {
+  // This typically corresponds to the `[locale]` segment
+  let locale = await requestLocale;

-  // Validate that the incoming `locale` parameter is valid
-  if (!routing.locales.includes(locale as any)) notFound();
+  // Ensure that the incoming locale is valid
+  if (!locale || !routing.locales.includes(locale as any)) {
+    locale = routing.defaultLocale;
+  }

  return {
+    locale,
    // ...
  };
});

While slightly more verbose, this change allows you to return a fallback locale for edge cases where the middleware can't provide a locale (e.g. a global country selection page at / or a global 404 page).

Also note that it's considered good practice to return a valid locale here in case any unknown values are encountered in the [locale] segment. However, you may wish to add validation in a central place like your root layout to conditionally call notFound() in this case.

`createNavigation` [#create-navigation]

The newly added createNavigation function supersedes these previously provided APIs:

  1. createSharedPathnamesNavigation
  2. createLocalizedPathnamesNavigation

This new function is a reimplementation of the existing navigation functionality and unifies the API for both use cases, while also fixing a few quirks of the previous APIs. Additionally, this implementation has been updated in anticipation of Next.js 15 to run without warnings.

Usage

tsx filename="i18n/routing.ts"
import {createNavigation} from 'next-intl/navigation';
import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting(/* ... */);

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

Migrating to createNavigation

createNavigation is generally considered a drop-in replacement, but a few changes might be necessary:

  1. createNavigation is expected to receive your complete routing configuration. Ideally, you define this via the defineRouting function and pass the result to createNavigation. The one edge case is if you're locales aren't known at build time.
  2. If you've used createLocalizedPathnamesNavigation and have composed the Link with its href prop, you should no longer provide the generic Pathname type argument.
diff
- ComponentProps<typeof Link<Pathname>>
+ ComponentProps<typeof Link>
  1. If you've used redirect, you now have to provide an explicit locale (even if it's just the current locale). The previously passed href (whether it was a string or an object) now needs to be wrapped in an object and assigned to the href prop. This change was necessary in preparation for Next.js 15.
tsx
// Retrieving the current locale
// ... in regular components:
const locale = useLocale();
// ... in async components:
const locale = await getLocale();
diff
- redirect('/about')
+ redirect({href: '/about', locale})

- redirect({pathname: '/users/[id]', params: {id: 2}})
+ redirect({href: {pathname: '/users/[id]', params: {id: 2}}, locale})
  1. If you've used getPathname and have previously manually prepended a locale prefix, you should no longer do so—getPathname now takes care of this depending on your routing strategy.
diff
- '/'+ locale + getPathname(/* ... */)
+ getPathname(/* ... */);
  1. If you're using a combination of localePrefix: 'as-needed' & domains and you're using getPathname, you now need to provide a domain argument (see Special case: Using domains with localePrefix: 'as-needed')

`setRequestLocale` marked as stable [#set-request-locale]

In case you rely on static rendering, you might have used the unstable_setRequestLocale API before. This function has now been marked as stable since it will likely remain required for the foreseeable future.

diff
- import {unstable_setRequestLocale} from 'next-intl/server';
+ import {setRequestLocale} from 'next-intl/server';

Close to a year ago, I opened discussion #58862 in the Next.js repository. This was an attempt at starting a conversation about how Next.js could provide a way to access a user locale in Server Components without a tradeoff in ergonomics or rendering implications. While the issue has gained in popularity and currently ranks as #2 of the top upvoted discussions of the past year, I've unfortunately not been able to get a response from the Next.js team on this topic so far. Based on my understanding, it's certainly not an easy problem to solve, but I'd be more than happy to collaborate on this if I can.

While I'm still optimistic that we can make the setRequestLocale API obsolete at some point in the future, the "unstable" prefix doesn't seem to be justified anymore—especially since the API has been known to work reliably since its introduction.

`defaultTranslationValues` (deprecated) [#default-translation-values]

defaultTranslationValues allow you to share global values to be used in messages across your app. The most common case are shared rich text elements (e.g. b: (chunks) => **{chunks}**).

However, over time this feature has shown drawbacks:

  1. We can't serialize them automatically across the RSC boundary (see #611)
  2. They get in the way of type-safe arguments (see #410)

Due to this, the feature will be deprecated and the docs now suggest a better alternative:

tsx
import {useTranslations} from 'next-intl';
import RichText from '@/components/RichText';

function AboutPage() {
  const t = useTranslations('AboutPage');
  return <RichText>{(tags) => t.rich('description', tags)}</RichText>;
}

What's next?

These release notes were originally planned to be posted as part of the next major version. However, I'm really happy that it was possible to roll out all of these changes incrementally and without breaking changes.

All of these changes were inspired by many conversations with users and contributors of next-intl. It's quite a privileged position to be in, being able to receive feedback from so many of you, and ensuring the spectrum of i18n use cases can be served by a lean, cohesive package. A big thank you also goes out to everyone who helped to test pre-releases of this version.

With these changes out of the way, users can upgrade to modern APIs at their own pace in the v3 range. That being said, the 4.0 release is already in the works, but mostly aims to clean up deprecated functionality to ensure next-intl remains minimalistic.

—Jan


File: docs/src/pages/blog/next-intl-4-0.mdx


title: next-intl 4.0

import PartnerContentLink from '@/components/PartnerContentLink';
import StayUpdated from '@/components/StayUpdated.mdx';

next-intl 4.0

Mar 12, 2025 · by Jan Amann

After a year of feature development, this release focuses on streamlining the API surface while maintaining the core architecture of next-intl. With many improvements already released in previous minor versions, this update introduces several enhancements that will improve your development experience and make working with internationalization even more seamless.

Here's what's new in [email protected]:

  1. Revamped augmented types
  2. Strictly-typed locale
  3. Strictly-typed ICU arguments
  4. GDPR compliance
  5. Modernized build output
  6. Improved inheritance in NextIntlClientProvider
  7. Stricter config for domains
  8. Preparation for upcoming Next.js features

Please also have a look at the other changes listed below before you upgrade.

Revamped augmented types

After type-safe Formats was added in [email protected], it became clear that a new API was needed that centralizes the registration of augmented types.

With [email protected], both Messages as well as Formats can now be registered under a single type that is scoped to next-intl and no longer affects the global scope:

tsx
// global.ts

import {formats} from '@/i18n/request';
import en from './messages/en.json';

declare module 'next-intl' {
  interface AppConfig {
    Messages: typeof en;
    Formats: typeof formats;
  }
}

See the updated TypeScript augmentation guide.

Strictly-typed locale

Building on the new type augmentation mechanism, [email protected] now allows you to strictly type locales across your app:

tsx
// global.ts

import {routing} from '@/i18n/routing';

declare module 'next-intl' {
  interface AppConfig {
    // ...
    Locale: (typeof routing.locales)[number];
  }
}

By doing so, APIs like useLocale() or <Link /> that either return or receive a locale will now pick up your app-specific Locale type, improving type safety across your app.

To simplify narrowing of string-based locales, a hasLocale function has been added. This can for example be used in i18n/request.ts to return a valid locale:

tsx
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async ({requestLocale}) => {
  // Typically corresponds to the `[locale]` segment
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

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

Furthermore, the Locale type can be imported into your app code in case you're passing a locale to another function and want to ensure type safety:

tsx
import {Locale} from 'next-intl';

async function getPosts(locale: Locale) {
  // ...
}

Note that strictly-typing the Locale is optional and can be used as desired in case you wish to have additional guardrails in your app.

Strictly-typed ICU arguments

How type-safe can your app be?

The quest to bring type safety to the last corner of next-intl has led me down a rabbit hole with the discovery of an ICU parser by Marco Schumacher—written entirely in types. Marco kindly published his implementation for usage in next-intl, with me only adding support for rich tags on top.

Check it out:

tsx
// "Hello {name}"
t('message', {});
//           ^? {name: string}

// "It's {today, date, long}"
t('message', {});
//           ^? {today: Date}

// "Page {page, number} out of {total, number}"
t('message', {});
//           ^? {page: number, total: number}

// "You have {count, plural, =0 {no followers yet} one {one follower} other {# followers}}."
t('message', {});
//           ^? {count: number}

// "Country: {country, select, US {United States} CA {Canada} other {Other}}"
t('message', {});
//           ^? {country: 'US' | 'CA' | (string & {})}

// "Please refer to the <link>guidelines</link>."
t.rich('message', {});
//                ^? {link: (chunks: ReactNode) => ReactNode}

With this type inference in place, you can now use autocompletion in your IDE to get suggestions for the available arguments of a given ICU message and catch potential errors early.

This also addresses one of my favorite pet peeves:

tsx
t('followers', {count: 30000});
json
// ✖️ Would be: "30000 followers"
"{count} followers"

// ✅ Valid: "30,000 followers"
"{count, number} followers"

Due to a current limitation in TypeScript, this feature is opt-in for now. Please refer to the strict arguments docs to learn how to enable it.

GDPR compliance [#gdpr-compliance]

In order to comply with the current GDPR regulations, the following changes have been made and are relevant to you if you're using the next-intl middleware with locale-based routing:

  1. The locale cookie now defaults to a session cookie that expires when the browser is closed.
  2. The locale cookie is now only set when a user switches to a locale that doesn't match the accept-language header.

If you want to increase the cookie expiration, e.g. because you're informing users about the usage of cookies or if GDPR doesn't apply to your app, you can use the maxAge attribute to do so:

tsx
// i18n/routing.tsx

import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting({
  // ...

  localeCookie: {
    // Expire in one year
    maxAge: 60 * 60 * 24 * 365
  }
});

Since the cookie is now only available after a locale switch, make sure to not rely on it always being present. E.g. if you need access to the user's locale in a Route Handler, a reliable option is to provide the locale as a search param (e.g. /api/posts/12?locale=en).

As part of this change, disabling a cookie now requires you to set localeCookie: false in your routing configuration. Previously, localeDetection: false ambiguously also disabled the cookie from being set, but since a separate localeCookie option was introduced recently, this should now be used instead.

Learn more in the locale cookie docs.

Modernized build output

The build output of next-intl has been modernized and now leverages the following optimizations:

  1. ESM-only: To enable enhanced tree-shaking and align with the modern JavaScript ecosystem, next-intl is now ESM-only. The only exception is next-intl/plugin which is published both as CommonJS as well as ESM, due to next.config.js still being popular.
  2. Modern JSX transform: The peer dependency for React has been bumped to v17 in order to use the more efficient, modern JSX transform.
  3. Modern syntax: Syntax is now compiled down to the Browserslist defaults query, which is a shortcut for ">0.5%, last 2 versions, Firefox ESR, not dead"—a baseline that is considered a reasonable target for modern apps.

If you're using next-intl with Jest or Vitest, please also refer to the new testing docs.

With these changes, the bundle size of next-intl has been reduced by ~7% (PR #1470).

Improved inheritance of `NextIntlClientProvider` [#nextintlclientprovider-inheritance]

Previously, NextIntlClientProvider would conservatively inherit only a subset from i18n/request.ts.

To improve the getting started experience, the provider by default now also inherits:

Due to this, you can now remove these props from NextIntlClientProvider if you've previously passed them manually:

diff
<NextIntlClientProvider
-  messages={messages}
-  formats={formats}
>
  ...
</NextIntlClientProvider>

If you don't want to inherit these props and stick to the previous behavior, you can opt-out:

tsx
<NextIntlClientProvider
  // Don't pass these to the client
  messages={null}
  formats={null}
>
  ...
</NextIntlClientProvider>

With this, NextIntlClientProvider now inherits all of your configuration, with the minor exception of error handling functions. Since functions are not serializable, they cannot be passed across the server/client boundary. However, an alternative for this is also on the horizon.

To make it easier to work with error handling functions on the client side, NextIntlClientProvider can now also be used in a nested fashion and will inherit the configuration from a parent provider (PR #1413).

Stricter config for `domains` [#domains-config]

So far, when using domains in combination with localePrefix: 'as-needed', next-intl had to make some tradeoffs to avoid reading the current host of the incoming request in components.

Now, by introducing two new constraints, next-intl can avoid these tradeoffs altogether:

  1. A locale can now only be used for a single domain
  2. Each domain now must specify its locales

The result is a simplified, more intuitive model that works as expected for this popular use case.

If you previously used locales across multiple domains, you now have to be more specific—typically by introducing a regional variant for a base language. You can additionally customize the prefixes if desired.

Example:

tsx
import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['sv-SE', 'en-SE', 'no-NO', 'en-NO'],
  defaultLocale: 'en-SE',
  localePrefix: {
    mode: 'as-needed',
    prefixes: {
      'en-SE': '/en',
      'en-NO': '/en'
    }
  },
  domains: [
    {
      domain: 'example.se',
      defaultLocale: 'sv-SE',
      locales: ['sv-SE', 'en-SE']
    },
    {
      domain: 'example.no',
      defaultLocale: 'no-NO',
      locales: ['no-NO', 'en-NO']
    }
  ]
});

This will create the following structure:

  • example.se: sv-SE
  • example.se/en: en-SE
  • example.no: no-NO
  • example.no/en: en-NO

Learn more in the updated docs for domains.

Preparation for upcoming Next.js features [#nextjs-future]

To ensure that the sails of next-intl are set for a steady course in the upcoming future, I've investigated the implications of upcoming Next.js features like ppr, dynamicIO and rootParams for next-intl.

This led to three minor changes:

  1. If you don't already have a NextIntlClientProvider in your app that wraps all Client Components that use next-intl, you now have to add one (see PR #1541 for details).
  2. If you're using format.relativeTime in Client Components, you may need to provide the now argument explicitly now (see PR #1536 for details).
  3. If you're using locale-based routing, make sure you've updated to await requestLocale that was introduced in [email protected]. The previously deprecated locale argument will serve an edge case in the future once rootParams is a thing (see PR #1625 for details).

While the mentioned Next.js features are still under development and may change, these changes seem reasonable to me in any case—and ideally will be all that's necessary to adapt for next-intl to get the most out of these upcoming capabilities.

I'm particularly excited about the announcement of rootParams, as it seems like this will finally fill in the missing piece that enables apps with locale-based routing to support static rendering without workarounds like setRequestLocale. I hope to have more to share on this soon!

Other changes

  1. Return type-safe messages from useMessages and getMessages (see PR #1489)
  2. Require locale to be returned from getRequestConfig (see PR #1486)
  3. Allow to declare pathnames partially for convenience (see PR #1743)
  4. Disallow passing null, undefined or boolean as an ICU argument (see PR #1561)
  5. Bump minimum required TypeScript version to 5 for projects using TypeScript (see PR #1481)
  6. Return x-default alternate link also for sub pages when using localePrefix: 'always' and update middleware matcher suggestion to /((?!api|_next|_vercel|.*\\..*).*) (see PR #1720)
  7. Remove deprecated APIs (see PR #1479)
  8. Remove deprecated APIs pt. 2 (see PR #1482)

Upgrade now

For a smooth upgrade, please initially upgrade to the latest v3.x version and check for deprecation warnings.

Afterwards, you can upgrade by running:

text
npm install next-intl@4

I'd love to hear about your experiences with [email protected]! Join the conversation in the discussions.

Thank you!

I want to sincerely thank everyone who has helped to make next-intl what it is today.

A special thank you goes to Crowdin, the sponsor partner of next-intl, enabling me to regularly work on this project and provide it as a free and open-source library for everyone.

—Jan

(this post has been updated from an initial announcement for the 4.0 release candidate)

PS: Have you heard that learn.next-intl.dev is coming?


File: docs/src/pages/blog/nextjs-root-params.mdx


title: 'Using next/root-params in Next.js 16.3'

import StayUpdated from '@/components/StayUpdated.mdx';
import CourseBanner from '@/components/CourseBanner';
import BlogVimeoVideo from '@/components/BlogVimeoVideo';
import Details from '@/components/Details';

Using `next/root-params` in Next.js 16.3

Aug 4, 2026 · by Jan Amann

Next.js v16.3 was just released and comes with a new feature: next/root-params.

This new API fills in the missing piece that allows apps that use top-level dynamic segments like [locale] to read segment values deeply in Server Components:

tsx
import {locale} from 'next/root-params';

async function Component() {
  // The ability to read params deeply in
  // Server Components ... finally!
  const curLocale = await locale();
}

This addition is a game-changer for next-intl.

While the library previously relied on workarounds to provide a locale to Server Components, this API now provides native support in Next.js for this use case, allowing the library to integrate much tighter with Next.js.

Practically, for users of next-intl this means:

  1. Being able to support static rendering of apps with locale-based routing without setRequestLocale
  2. Improved integration with Next.js cache mechanisms like cacheComponents

But first, let's have a look at how this API works in practice.

Introduction to root layouts

text
Prefer to watch a video?

  

→ Continue reading at [static rendering](#static-rendering)

Previously, Next.js required a root layout to be present at app/layout.tsx—the root of your app.

Now, you can move a root layout to a nested segment, even if it's a dynamic one:

text
src
└── app
    └── [locale]
        ├── layout.tsx (root layout)
        └── page.tsx

In this extended definition, a root layout now is any layout that has no other layouts located above it.

In contrast, layouts that do have other layout ancestors are regular layouts:

text
src
└── app
    └── [locale]
        ├── layout.tsx (root layout)
        ├── (...)
        └── news
            ├── layout.tsx (regular layout)
            └── page.tsx

With the addition of next/root-params, you can now read param values of a root layout in all Server Components that render within it:

tsx filename=src/components/localeswitcher.tsx
import {locale} from 'next/root-params';

export async function LocaleSwitcher() {
  // Read the value of `[locale]`
  const curLocale = await locale();

  // ...
}

Multiple root layouts

Here's where it gets interesting: With route groups, you can provide another layout for pages that are not located in the [locale] segment:

text
src
└── app
    ├── [locale]
    │   ├── layout.tsx
    │   └── page.tsx
    └── (unlocalized)
        ├── layout.tsx
        └── page.tsx

The layout at [locale]/layout.tsx as well as the layout at (unlocalized)/layout.tsx both have no other layouts located above them, therefore both qualify as root layouts.

Due to this, in this case the returned value of next/root-params will depend on where the component that calls the function is being rendered from.

If you call next/root-params in shared code that is used by both layouts, this allows for a pattern like this:

tsx filename="src/utils/getlocale.tsx"
import {locale} from 'next/root-params';

export default async function getLocale() {
  // Try to read the locale in case we're in `[locale]/layout.tsx`
  let curLocale = await locale();

  // If we're in `(unlocalized)/layout.tsx`, let's use a fallback
  if (!curLocale) {
    curLocale = 'en';
  }

  return curLocale;
}

With this, you can use the getLocale function across your codebase to read the current locale without having to worry about where it's being called from.

In an internationalized app, this can for example be useful to implement a country selection page at the root where you have to rely on a default locale. Once the user is within the [locale] segment, this param value can be used instead for localizing page content.

Static rendering

In case we know the values for the [locale] segment ahead of time, we can provide them to Next.js by using the generateStaticParams function to enable static rendering:

tsx filename="src/app/[locale]/layout.tsx"
const locales = ['en', 'de'];

// Pre-render all available locales at build time
export function generateStaticParams() {
  return locales.map((locale) => ({locale}));
}

// ...

Note that dynamicParams = false doesn't work with Cache Components, therefore if you want to treat your locales array as exhaustive, you might be better off adding runtime validation for the [locale] segment and call notFound() to guard against unknown locales (see below).

Leveraging `next/root-params` in `next-intl`

So, how can you use this in next-intl?

Similarly to how we've defined the getLocale function above, we do in fact already have a central place that is called by all server-side functions that require the current locale of the user: i18n/request.ts.

So let's use next/root-params here:

tsx filename="src/i18n/request.ts" {1,8}
import * as rootParams from 'next/root-params';
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';
import {notFound} from 'next/navigation';

export default getRequestConfig(async () => {
  const paramValue = await rootParams.locale();

  let locale;
  if (hasLocale(routing.locales, paramValue)) {
    locale = paramValue;
  } else {
    // Runtime validation for unknown locales
    notFound();
  }

  return {
    locale
    // ...
  };
});

That's it—with a single change to i18n/request.ts you can start using next/root-params!


One caveat though: next/root-params currently doesn't work in Route Handlers or Server Actions. Next.js plans to add support for these in a future release.

You can work around that though by passing an explicit locale param at call sites, e.g. via bind:

tsx {3}
async function action(locale: string) {
  'use server';
  const t = await getTranslations({locale, namespace: 'ContactForm'});
  // ...
}

… and then incorporate the override in getRequestConfig:

tsx filename="i18n/request.ts"
// ...

export default getRequestConfig(async ({locale}) => {
  // Only read from `next/root-params` if no explicit
  // override is provided by the caller
  if (!locale) {
    const paramValue = await rootParams.locale();
    if (hasLocale(routing.locales, paramValue)) {
      locale = paramValue;
    } else {
      notFound();
    }
  }

  return {
    locale
    // ...
  };
});

Time for spring cleaning

With this change, you can now simplify your codebase in various ways:

Remove a pass-through root layout

For certain patterns like global 404 pages, you might have used a pass-through root layout so far:

tsx filename="src/app/layout.tsx"
export default function RootLayout({children}: LayoutProps<'/'>) {
  return children;
}

This needs to be removed now as otherwise this will qualify as a root layout instead of the one defined at src/app/[locale]/layout.tsx.

Instead, you can use global-not-found for this now.

Avoid reading the `[locale]` segment

Since next-intl provides the current locale via useLocale and getLocale, you can seamlessly read the locale from these APIs instead of params now:

diff filename="src/app/[locale]/layout.tsx"
+ import {getLocale} from 'next-intl/server';

export default async function RootLayout({
  children,
-  params
}: LayoutProps<'/[locale]'>) {
-  const {locale} = await params;
+  const locale = await getLocale();

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

If you've augmented the Locale type you'll now also benefit from type safety here:

tsx
// ✅ 'en' | 'de'
const locale = await getLocale();

Behind the scenes, if you call useLocale or getLocale in a Server Component, your i18n/request.ts config will be consulted, potentially using a fallback that you have defined.

Remove manual locale overrides [#locale-override]

If you're using async APIs like getTranslations, you might have previously passed the locale manually, typically to enable static rendering in the Metadata API.

Now, you can remove this and rely on the locale that is returned from i18n/request.ts:

diff filename="src/app/[locale]/page.tsx"
export async function generateMetadata(
-  {params}: PageProps<'/[locale]'>
) {
-  const {locale} = await params;
-  const t = await getTranslations({locale, namespace: 'HomePage'});
+  const t = await getTranslations('HomePage');

  // ...
}

The cases where you still require a locale override are:

  1. When you're using functions from next-intl in Route Handlers or Server Actions, which next/root-params doesn't support yet
  2. If your UI renders messages from multiple locales in parallel (uncommon)

Static rendering

If you've previously used setRequestLocale to enable static rendering, you can now remove it:

diff filename="src/[locale]/page.tsx"
- import {setRequestLocale} from 'next-intl/server';

- export default function Page({params}: PageProps<'/[locale]'>) {
-   setRequestLocale(params.locale);
+ export default function Page() {
  // ...
}

Note that generateStaticParams is naturally still required though.

Custom routing setups

next-intl provides mechanisms like localePrefix (and esp. prefixes) that allow you to customize your routing configuration. However, some apps might require further customization that goes beyond the capabilities that next-intl provides out of the box.

With the introduction of next/root-params, it's now easier than ever to implement custom routing setups, while still being able to use core functionality from next-intl like useTranslations.

Example:

text
app/
└── [tenant]
      ├── layout.tsx
      └── page.tsx
tsx filename="src/i18n/request.ts"
import * as rootParams from 'next/root-params';
import {getRequestConfig} from 'next-intl/server';
import {fetchTenant} from '@/services/tenant';

export default getRequestConfig(async () => {
  const tenantId = await rootParams.tenant();
  const tenant = await fetchTenant(tenantId);
  const locale = tenant.locale;

  return {
    locale
    // ...
  };
});

In this case, you can consider implementing your own middleware and navigation APIs, if relevant.

Try `next/root-params` today!

If you're giving next/root-params a go with next-intl, let me know how it works for you by joining the discussion here: Experiences with next/root-params.

I'm curious to hear how it simplifies your codebase!

—Jan


File: docs/src/pages/blog/precompilation.mdx


title: 'Ahead-of-time compilation for next-intl'

import StayUpdated from '@/components/StayUpdated.mdx';

Ahead-of-time compilation for next-intl

Jan 28, 2026 · by Jan Amann

tl;dr: By flipping a single precompile flag in your next.config.ts, you can immediately drop ~9KB of compressed JavaScript from your bundle size and also improve the runtime performance of your app.

The cost of adding next-intl with a single client-side translation to an app now amounts to ~4KB of compressed JavaScript.

How we got here

When React Server Components were announced, I was pretty excited.

I immediately saw potential for next-intl to improve performance for its users by moving more work to the server—leaving the client with less JavaScript to parse and evaluate.

But I also have to admit, I got a bit carried away. While for very interactive apps this was never really an option, when working on apps with very sensitive performance requirements, I'd try to almost dogmatically avoid client-side translations entirely.

And while you can get pretty far with this approach, it can require quite a few tricks like leaning heavily into donut components and other fancy patterns.

It almost felt like a game of "the floor is lava", trying to find clever tricks to avoid touching the client with translations and ultimately avoid shipping an ICU parser to the client. I know some of you did the same.

A different perspective

About three years ago, I started a conversation with Jan Nicklas about how we could best leverage the new Server Components paradigm for internationalization.

At some point, he shared an idea with me that he'd implement in a prototype: icu-to-json, a fresh take on removing the ICU parser from the runtime. I always wanted to integrate this into next-intl, but for the longest time, it seemed like a big ask architecturally.

But then, about two months ago, I shipped the first implementation of useExtracted, which happened to provide just the infrastructure that finally unlocked this feature.

The loader I implemented for processing .po files and later for custom formats looked like a perfect fit to precompile messages ahead of time.

So it was time to get to work.

Challenges with precompilation

When compiling a simple message like Hello {name}!, you might get the following AST as a result:

json
[
  {"type": 0, "value": "Hello "},
  {"type": 1, "value": "name"},
  {"type": 0, "value": "!"}
]

In this case, 0 represents a string node and 1 represents an argument.

But while the compilation work is already done, the size of the AST data structure is significantly larger than our initial message. So even though this can avoid work at runtime, it adds significant weight to the bundle.


Another approach is to compile messages into function modules, like this:

tsx filename="messages/en.js"
function hello(name) {
  return `Hello ${name}!`;
}

The problem with this approach, however, is that such functions cannot be serialized across the RSC bridge when passed to Client Components. Libraries that use this approach therefore resort to importing the generated function into components to avoid crossing the bridge.

And in turn, this results in all locale-specific variants of a message being bundled with your code, thereby defeating the possibility of splitting messages by locale.

next-intl is used by websites like Ethereum.org, which currently ships in 67 languages. So the function-based approach is just not an option here.

Minified ASTs

Architecturally, precompiled ASTs are a better fit for next-intl—but they were too large.

The core idea of Jan Nicklas's prototype was to minify ASTs, largely avoiding object properties in favor of arrays with positional entries. I took his idea and was able to squeeze the size even further.

So here's what it looks like:

tsx
compile('Hello {name}!');

… becomes:

json
["Hello ", ["name"], "!"]

… and from there, we can evaluate the AST with a minimal runtime:

tsx
// "Hello World!"
format(compiled, 'en', {name: 'World'});

And the best part is that this doesn't have any overhead at all for plain strings:

text
"Welcome!" → "Welcome!"

Based on my experience, such plain strings often account for the majority of messages that typical apps ship with, so this is a big win.

That said, this approach scales up to more complex messages and supports the full range of ICU features that you know from next-intl:

tsx
compile(
  'You have {count, plural, =0 {no followers yet} one {one follower} other {# followers}}.'
);

… becomes:

text
["You have ", ["count", 2, {
  "=0": "no followers yet",
  "one": "one follower",
  "other": [0, " followers"]
}], "."]

Also, rich text is of course supported:

tsx
compile('Hello <b>World</b>');

… becomes:

json
["Hello ", ["b", "World"]]

Minimal runtime

So what is this format function?

tsx
format(compiled, 'en', {count: 2});

It's a minimal runtime that efficiently evaluates the optimized AST and calls into native Intl APIs to format dates, numbers, and more.

All that, in ~650 bytes (compressed).

So if you make the switch to ahead-of-time compilation, this will immediately drop ~9KB of compressed JavaScript from both your server and client bundle. The cost of adding next-intl with a single client-side translation to an app now amounts to ~4KB of compressed JavaScript.

Additionally, formatting of messages will now be significantly faster, as the parsing overhead is gone from the runtime and evaluation of the optimized AST is very cheap.

Turn on precompilation today

The good news is that all of this is built right into next-intl, and there's no new API to learn.

Just flip a switch:

tsx filename="next.config.ts" {9-10}
import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin({
  experimental: {
    messages: {
      path: './messages',
      format: 'json',

      // Enable precompilation
      precompile: true
    }
  }
});

export default withNextIntl();

If you haven't seen the messages config option yet, it was previously shipped as part of useExtracted and is now relevant for precompilation as well.

But that's it. Your existing calls to useTranslations and useExtracted will automatically benefit from this optimization—no code changes needed.

One gotcha though

One aspect to call out here is that t.raw is not supported with precompilation.

As messages are parsed at build time, we can't know if you're planning to call t.raw on them later, so this feature isn't supported with precompiled messages.

But let's take a step back here.

Historically, t.raw was added to support raw HTML content in your messages. However, time has shown that this is cumbersome in practice for long-form content and that there are better alternatives:

  1. MDX for local content: For imprint pages and similar, grouping your localized content into files like content.en.mdx and content.es.mdx is significantly easier to manage.
  2. CMS for remote content: Content management systems typically ship with a portable format that allows you to express rich text in an HTML-agnostic way, enabling you to use the same content for mobile apps and more (see, for example, Sanity's Portable Text).

The other use case that t.raw was traditionally (ab)used for is handling arrays of messages. The recommended pattern for this has always been to use individual messages for each string (see arrays of messages). This pattern also has the benefit of being statically analyzable.

Related to this, the recently introduced useExtracted doesn't support t.raw either, since it doesn't fit this paradigm in the first place.

Because of this, it's recommended to migrate to one of the mentioned alternatives if you'd like to benefit from ahead-of-time compilation. If you're heavily using t.raw, you can of course also decide to leave the optimization off for now.

Potentially in a future release, t.raw might be deprecated entirely due to the downsides this API has—but this is still up for discussion.

Ready to try it?

If you're excited about ahead-of-time compilation as well, I'd really love to hear your feedback.

Give it a try in your app with [email protected] and share your feedback with me. Please note that this feature is currently considered experimental, so changes are expected.

As a closing note, the final frontier for next-intl performance optimization is automatic tree shaking of messages. I hope to have more to share on this later this year!

— Jan

PS: If you're interested in more technical details, I've written an RFC that describes the design decisions and tradeoffs for this feature in more detail.


File: docs/src/pages/blog/translations-outside-of-react-components.mdx


title: How (not) to use translations outside of React components

import StayUpdated from '@/components/StayUpdated.mdx';

How (not) to use translations outside of React components

Apr 21, 2023 · by Jan Amann

Have you ever wondered why next-intl doesn’t provide an API to consume translations outside of React components?

The traditional way to internationalize your app with next-intl is to use the useTranslations hook:

tsx
import {useTranslations} from 'next-intl';

function About() {
  const t = useTranslations('About');
  return <h1>{t('title')}</h1>;
}

Why is it not possible to format messages e.g. in utility functions? Is there something missing here?

This may seem like an unnecessary limitation, but the absence of this feature is intentional and aims to encourage the use of proven patterns that avoid potential issues—especially if they are easy to overlook.

Example: Formatting error messages

Let's assume you have a FeedbackForm component that posts user feedback to a backend endpoint. Unfortunately, the server occasionally returns a 504 status code due to the high volume of feedback. To improve the user experience, you would like to implement automatic retries and provide appropriate feedback to the user.

Here’s a naive approach. Can you spot an issue with this implementation?

tsx
import {useTranslations, useNow} from 'next-intl';
import {addMinutes} from 'date-fns';

function sendFeedback() {
  // ❌ Bad implementation: Returns formatted messages
  API.sendFeedback().catch((error) => {
    // In case of a gateway timeout, notify the
    // user that we'll try again in 5 minutes
    if (error.status === 504) {
      // (let's assume `t` is defined here for the sake of the example)
      return t('timeout', {nextAttempt: addMinutes(new Date(), 5)});
    }
  });
}

function FeedbackForm({user}) {
  const t = useTranslations('Form');
  const [errorMessage, setErrorMessage] = useState();

  function onSubmit() {
    sendFeedback().catch((errorMessage) => {
      setErrorMessage(errorMessage);
    });
  }

  return (
    <form onSubmit={onSubmit}>
      {errorMessage != null && <p>{errorMessage}</p>}
      ...
    </form>
  );
}

Have you found an issue?

Let's have a look together:

  1. The nextAttempt value is interpolated into the message in a utility function that is called by an event handler. There's no way how we can keep the remaining time updated as we're nearing the retry timeout.
  2. If the user changes the language, the error message will remain in the previously selected language, leading to a jarring user experience.

A better way: Formatting during render

To avoid these issues, we can format messages during the rendering phase of React, turning data structures into human readable strings.

tsx
import {useTranslations, useNow} from 'next-intl';
import {addMinutes} from 'date-fns';

function FeedbackForm({user}) {
  const t = useTranslations('Form');
  const [retry, setRetry] = useState();
  const now = useNow({
    // Update every minute
    updateInterval: 1000 * 60
  });

  function onSubmit() {
    // ✅ Good implementation: Store data structures in state
    API.sendFeedback().catch((error) => {
      if (error.status === 504) {
        setRetry(addMinutes(now, 5));
      }
    });
  }

  return (
    <form onSubmit={onSubmit}>
      {retry != null && <p>{t('timeout', {nextAttempt: nextAttempt - now})}</p>}
      ...
    </form>
  );
}

Now, we can offer a better user experience by interactively counting down the time to the next attempt.

Additionally, this approach is more robust to possibly unexpected states, like the user changing the language while the timeout message is being displayed.

The exception that proves the rule

If you’re working with Next.js, you might want to translate i18n messages in API routes, Route Handlers or the Metadata API.

next-intl/server provides a set of awaitable versions of the functions that you usually call as hooks from within components. These are agnostic from React and can be used for these cases.

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

// The `locale` is received from Next.js via `params`
const locale = params.locale;

// This creates the same function that is returned by `useTranslations`.
const t = await getTranslations({locale});

// Result: "Hello world!"
t('hello', {name: 'world'});

This seems familiar

If you’ve been working with React for a longer time, you might have experienced the change from component{DidMount,DidUpdate,WillUnmount} to useEffect. The reason why useEffect is superior is because it nudges the developer into a direction where the app is always in sync and by doing this, a whole array of potential issues just magically disappear.

By limiting ourselves to only format messages during render, we're in a similar situation: The rendered output of translated messages is always in sync with app state and we can rely on the app being consistent.

Related: "How can I reuse messages?" in the structuring messages docs


File: docs/src/pages/blog/use-extracted.mdx


title: 'useExtracted' subtitle: The Tailwind of i18n?

import StayUpdated from '@/components/StayUpdated.mdx';
import PartnerContentLink from '@/components/PartnerContentLink';
import BlogVideo from '@/components/BlogVideo';

useExtracted: The Tailwind of i18n?

Nov 7, 2025 · by Jan Amann

For quite some time, I used to be a Tailwind skeptic. But one day, I decided to give it five minutes — and I was hooked. Its core idea is just so irresistibly pragmatic. Once you've tried it, you just can't go back.

It made me wonder: "What would the Tailwind for i18n look like?"

Now first of all, next-intl is doing very well and has recently crossed 1M weekly downloads (thank you everyone!). So it seems like there are plenty of people who are quite happy with how it works today. And its core APIs like useTranslations are surely here to stay.

But will we continue to write code the same way tomorrow, as we do today?

It seems evident that we will not. AI agents are increasingly making their way into our workflows, and innovations like Cursor and Claude Code are becoming more powerful by the day.

So what makes a library suitable for AI-first development? Probably, that it was already a great option for humans in the first place. Tailwind wasn't "built for AI", it was a fantastic idea for how to make styling easy for humans. But now, it's become the default for agentic tools when it comes to styling.

So what makes Tailwind Tailwind?

Design principles

If we consider the design of Tailwind, we can see that an i18n solution that follows the same principles might look something like this:

  1. Colocation: Similar to how Tailwind avoids the need to manage separate stylesheets, there should not be a need for manually managing JSON message catalogs when adding, updating or removing messages. Message catalogs can however act as a compile target.
  2. Local reasoning: Generative AI is very good at Tailwind since it only requires very small context windows. Having to read entire message catalogs leads to context pollution and should therefore be avoided (at least not without tool calls).
  3. No naming of things: Not having to come up with names is a major productivity boost, therefore manual keys should be avoided as much as possible.
  4. Purging: When code is quickly changed, there shouldn't be any dead code left behind. Similar to how Tailwind can purge unused styles, we should purge unused messages automatically.
  5. Minification: Tailwind class names have a tiny bundle footprint. In the same way, messages should also use minified keys that ensure bundles are as small as possible.
  6. Prototype-friendly, production-ready: Tailwind looks exactly the same, regardless of whether it's used for a quick prototype or a production app. In the same way, there should be a single API that avoids upfront structural decisions related to the project's size and complexity.
  7. Refactoring-friendly: Moving code across components is seamless with Tailwind, this should be the case for your messages as well.

While next-intl has answers to some of these questions, ultimately the truth is that there's currently potential left on the table. Therefore, after publishing an RFC about two months ago—today, I'm incredibly excited to share what I believe could be the answer to this:

tsx
import {useExtracted} from 'next-intl';

function InlineMessages() {
  const t = useExtracted();
  return <h1>{t('Look ma, no keys!')}</h1>;
}

`useExtracted` in action

I already made you read far too much, so here's a demo to show you how it works.

Let's start with an app that has next-intl installed, and we'll make a previously static label eligible for translation to other languages:

No naming of keys, no CLI to invoke, just next dev as you're used to.

And you get an always-in-sync JSON catalog for free.

Everything you like about `useTranslations`, but without the keys

If you've internationalized an app before, you know that we need to translate more than just plain strings.

So let's use some ICU features:

Yep, TypeScript will automatically validate that you're using the number formatter in your messages when needed—all without additional setup. By doing this, we can ensure that Intl.NumberFormat will be used to turn a raw number into a readable string that uses locale-sensitive formatting.

Also t.rich is of course supported:

tsx
t.rich('Please refer to the <link>guidelines</link>.', {
  link: (chunks) => <Link href="/guidelines">{chunks}</Link>
});

And yes, there's also an awaitable version for Server Components and friends:

tsx filename="page.tsx"
import {getExtracted} from 'next-intl/server';

export default async function ProfilePage() {
  const user = await fetchUser();
  const t = await getExtracted();

  return (
    <PageLayout title={t('Hello {name}!', {name: user.name})}>
      <UserDetails user={user} />
    </PageLayout>
  );
}

Ok, now let's take a step back. Components that use inline messages appear to be easy to generate for AIs, all without having to load lengthy message catalogs into their precious context window.

But what about using AI to translate your messages?

Context is key

While providing context was always important for translators, it seems like in the AI era, doing this in an easy-to-digest way that's based on text is even more important.

If you've previously used hand-crafted keys like auth.login.title, you already did your part to provide some context that clarifies the intent of the message.

But what if our translations look like this:

json
{
  "0MXX5B": "Welcome back!"
}

Not so easy.

But again, making something easy for AIs was probably always about making it easy for humans in the first place. And we already found a solution to this—wait for it—30 years ago!

GNU gettext introduced .po files for message catalogs, which look like this:

po
#. Greeting shown to user when logging back in
#: src/app/(auth)/login/page.tsx
msgid "0MXX5B"
msgstr "Welcome back!"

Everything is there: An optional description, a filename reference, an ID and the label itself.

So this is what you'll now be able to use with next-intl as well:

Note how enabling the .po formatter also activates a Turbopack loader that will parse your locale catalogs as plain JavaScript objects for you, making them easy to consume in your app. It's really just a compile target.

That being said, if JSON is your jam, that's totally fine too. And soon, there will also be support for custom formatters that will allow you to incorporate file references and descriptions in whatever format you prefer.

Now we're ready to translate

With this, we're in a much better position to get accurate and user-friendly translations.

So first of all, let's add a new locale:

Nope, the video wasn't cut. Neither was there a paste from the clipboard. When you add a new locale, next-intl will automatically populate the catalog with empty entries for all messages that you support.

So from here, you can start translating your messages however you want, either with an editor, or in the easiest case by using an AI-based translation service like Crowdin:

More on this in the localization management docs.

What sorcery is this?

Behind the scenes, useExtracted integrates with Next.js at two touch points:

1. Turbopack loader for extraction

The core piece is a loader that will be called for source files containing useExtracted calls.

Note that only Next.js 16+ is supported, since it introduced an optimization for Turbopack that allows to peek inside files to check if they even use the useExtracted hook before actually processing the file.

If useExtracted is found, then SWC—the Rust-based compiler that powers Next.js—will parse the file, followed by a JavaScript transformer that will compile the file to a useTranslations call:

tsx
import {useTranslations} from 'next-intl';

function InlineMessages() {
  const t = useTranslations();

  // Pick up the minified key
  return <h1>{t('dPSc42')}</h1>;
}

Additionally, in case there are changes to the messages previously known for this file, the loader will also emit an updated messages catalog for your source locale, and also your target locales will be kept in sync. By doing this, Turbopack HMR will reflect the changes instantly in your running app.

If no messages were changed as part of a save, the extraction part will be skipped altogether.

2. Turbopack loader for catalogs

To support catalog formats like .po (and custom ones in the future), a second Turbopack loader will be enabled that efficiently reads your catalogs and returns them as plain JavaScript objects:

tsx filename="i18n/request.ts"
import {getRequestConfig} from 'next-intl/server';

export default getRequestConfig(async () => {
  const locale = 'en';

  // E.g. `{"NhX4DJ": "Hello"}`
  const messages = (await import(`../../messages/${locale}.po`)).default;

  // ...
});

Oh and yes, Webpack is also supported.

Ready to try it?

I could probably go on, e.g. about how useExtracted also works out-of-the-box in test environments without any compilation at all, but I think I've already made my point.

If you're excited about this as well, I'd really love to hear your feedback.

Give the demo app a try and share your feedback with me. If you're an early adopter, you can already try this feature in your app with [email protected], but please note that it's currently considered experimental, so changes should be expected.

Also, it's worth mentioning again that the useTranslations API that you might be using today is not going anywhere. If you're already happy with it, then by all means keep using it. Personally, I think useExtracted has a lot of potential, but only time will tell if this is true.

As the feature stabilizes, you'll of course be the first to hear about best practices and in-depth tutorials if you're a member of 🌐 learn.next-intl.dev.

— Jan

PS: A special thank you goes to projects & companies like gettext, Lingui, FormatJS, WordPress, and Zendesk for their pioneering work in the space of message extraction. I'd also like to thank Jan Nicklas for his work on next-yak, pushing the boundaries of Turbopack and sharing his insights.

Further reading:


File: docs/src/pages/docs/design-principles.mdx

import PartnerContentLink from '@/components/PartnerContentLink';

Design principles

This page provides a deep dive about the design principles that next-intl is based on.

While this page doesn't need to be read in order to use the library effectively in a project, it can provide you with a better understanding of the philosophy behind the library and can help you evaluate if next-intl is a good fit for your project.

This page also links to planned improvements in order to act as a transparent reference for enhancements to expect from next-intl in the future.

Holistic

Internationalization clearly requires flexibility from your codebase. However, even implementing a single language properly can already be a challenge by itself.

Using dynamic text labels is the most obvious aspect of internationalization, but supporting a language well also includes many other aspects:

  1. Pluralization rules: While a language like English has only two plural forms (singular and plural), other languages have up to six different forms.
  2. Date and time formatting: Different languages have different conventions for formatting dates and times. Even the year displayed can vary from country to country; for example, Thailand uses the Buddhist calendar, which is 543 years ahead of the Gregorian one.
  3. Number formatting: Formatting conventions for numbers vary across different languages. For instance, when comparing English and German, the separators for thousands and decimals are flipped.
  4. List formatting: Formatting lists like "HTML, CSS, and JavaScript" is not only a matter of assembling strings in the right order, but also of using language-specific conjunctions and punctuation marks.
  5. Text direction: While most languages are written from left to right, some languages like Arabic are written from right to left and require a mirrored layout.

On top of this come typical app problems like:

  1. Rich text formatting: Many apps need to support some way of rich text, e.g. to embed links into text labels ("Learn more in the rich text docs.").
  2. Time zones: Displaying dates requires consistent handling of time zones across the server and client, potentially even customized based on a preference of the user.
  3. Relative time formatting: Displaying relative times like "5 minutes ago" or "in 2 hours" requires special care to get the formatting right, and also requires a consistent reference value for the current time. Potentially, you also need a mechanism to update the displayed time regularly.
  4. Localized URLs: URLs should be localized to match the user's language preference (e.g. /en/about-us for English and /es/sobre-nosotros for Spanish). Implementation-wise this requires mapping incoming requests to the right pages and providing streamlined APIs for developers to link between pages in a locale-agnostic way.
  5. Language negotiation: The user's language preference should be detected based on browser settings, but should also be manually configurable—even if a user hasn't signed in.
  6. SEO: Search engines need to be informed about localized versions of your pages in order to present the best-matching content to your users.
  7. Country-specifics: If you provide services or products in different countries, you might need to consider aspects like different currencies and units of measurement.
  8. Environment differences: Your components might execute on the server, the client, or both. To achieve consistent rendering across different environments, you need to ensure your whole app has unified access to properties like the user locale, time zone and a reference value for the current time.

Considering this list, it might be apparent how large the problem space is and how interconnected different pieces of internationalization are.

next-intl takes a holistic approach to internationalization that nudges you towards best practices for handling languages and country-specific conventions. This way, you have more time to focus on what makes your app unique.

→ Planned enhancements

Ergonomic

The bold claim of this library is that your app code will become simpler instead of complex when you implement internationalization.

If you consider the holistic picture of what internationalization entails, it's clear that a majority of your components will be involved with internationalization in one way or another. Due to this, next-intl makes it a priority to provide convenient APIs that ensure you feel productive and in control. As a developer, you should be able to focus solely on solving practical problems, with internationalization seamlessly integrated as a side effect of your efforts.

Once internationalization is set up, adding a new language is in the simplest case only a matter of adding a new JSON file with translations—everything else is being taken care of by next-intl.

→ Planned enhancements

Standards-based

With the introduction of the ECMAScript Internationalization API, JavaScript has gotten very capable in the recent years in regard to formatting of dates, times, numbers, lists and pluralization. next-intl builds on top of this API and provides an ergonomic interface to work with these features, while considering your app-specific configuration & needs.

For text formatting, next-intl is based on International Components for Unicode (ICU). ICU is a mature and widely used standard for internationalization that is supported by many programming languages and frameworks. next-intl uses the ICU message syntax for defining text labels, which allows to express complex formatting requirements like interpolating variables and pluralization in a concise and readable way—also for non-developers like translators.

By being based on standards, next-intl ensures that your internationalization code is future-proof and feels familiar to developers who have existing experience with internationalization. Additionally, relying on standards ensures that next-intl integrates well with translation management systems like Crowdin.

next-intl uses a nested style to provide structure to messages, allowing to express hierarchies of messages without redundancy. By supporting only a single style, we can offer advanced features that rely on these assumptions like type-safety for messages. If you're coming from a different style, you can consider migrating to the nested style (see "Can I use a different style for structuring my messages?" in the structuring messages docs).

As standards can change, next-intl is expected to keep up with the latest developments in the ECMAScript standard (e.g. Temporal and Intl.MessageFormat).

→ Planned enhancements

Compatible

While next-intl is designed to tackle internationalization in a holistic way, it's important to consider that internationalization might require integration with other tools and services, enabling you to use the best tools for the job and to collaborate with non-developers.

Typical apps require some of the following integrations:

Translation Management Systems (TMS)

These are typically used to manage translations and to collaborate with translators. Services like Crowdin provide a wide range of features, allowing translators to work in a web-based interface on translations, while providing different mechanisms to sync translations with your app.

next-intl integrates well with these services as it uses ICU message syntax for defining text labels, which is a widely supported standard. The recommended way to store messages is in JSON files that are structured by locale since this is a popular format that can be imported into a TMS. While it's recommended to have at least the messages for the default locale available locally (e.g. for type-safe messages), you can also load messages dynamically, e.g. from a CDN that your TMS provides.

Content Management Systems (CMS)

If your app uses content from a CMS, you can use this alongside next-intl. For instance, you might want to manage content like blog posts or marketing copy in a CMS, while managing UI labels in next-intl.

A CMS typically provides a way to define content for multiple languages, and to fetch this content via a REST API. By using the negotiated app locale that is returned from next-intl via getLocale and passing it to requests to your CMS API, you can retrieve content for the user's preferred language.

Backend data

Similar to a CMS, you might have data in a backend service or database that needs to be queried based on the language and country of the user. For instance, you might want to show different prices & currencies for different countries, or you might want to show localized product names. By using the negotiated app locale from next-intl for requests to your backend, you can ensure localization reaches all parts of your app.

Other libraries

Next.js has a rich ecosystem of libraries that can be used alongside next-intl, e.g. to handle authentication. By providing documentation and examples for common integrations, next-intl ensures that you'll have a frictionless experience when integrating with other libraries.

→ Planned enhancements

Performance-obsessed

next-intl was designed with high-traffic sites in mind that need to deliver a fast and reliable user experience and has proven to work on complex e-commerce pages with outstanding Core Web Vitals.

To achieve this, next-intl primarily relies on these techniques currently:

  1. RSC-first: By integrating deeply with React Server Components, we can offload work to a build step or a capable server, therefore reducing the runtime footprint of your app on the client side.
  2. Splitting of messages: By splitting messages by locale, and optionally also by server, client and component, we can reduce the amount of messages that are sent to the client. This is especially important for apps that support many languages and have a large amount of messages.
  3. Caching: Parsing of messages, as well as the instantiation of Intl constructors is cached across your app, therefore reducing the amount of necessary computation.
  4. Shortcuts: Better than caching the result of repetitive work is not doing it in the first place (e.g. by detecting plain messages that don't require parsing).

→ Planned enhancements

Next.js-first

Next.js comes with a lot of bells and whistles, but at the same time there are a number of aspects that need to be handled carefully to enable a reliable internationalization integration. next-intl, as the name implies, is primarily designed to work well with Next.js. Rather than trying to be a one-size-fits-all solution, next-intl integrates with Next.js as deeply as necessary and makes it a priority to stay on top of the latest developments in the Next.js ecosystem.

That being said, next-intl has a Next.js-agnostic core that can be used in any React app, or even in plain JavaScript: use-intl. This core library contains most features of next-intl, but lacks Next.js-specific integrations like routing APIs. The goal of this library is to make it possible to use familiar APIs in other parts of your stack (e.g. React Native) and, in case you ever feel like Next.js is not the right fit for your project anymore, to provide a straightforward migration path.

Migration-friendly

We've all been there, technology moves on, and sometimes you need to move on as well. next-intl is designed to be a good citizen in your codebase, and to make it possible to migrate away from certain parts of your stack in case this becomes necessary.

If you ever feel like Next.js or next-intl is not the right fit for your project anymore, you have multiple options here:

  1. Moving away from Next.js: If you decide to migrate away from Next.js, you can continue to use the core library use-intl in any React app, e.g. allowing you to reuse existing components in a React Router app.
  2. Moving away from next-intl: If you find that next-intl doesn't fit your needs anymore, you'll have to adapt app code that references the library, but you can still reuse your standards-based ICU messages and replace formatting APIs e.g. with direct calls to the ECMAScript Internationalization API.

We won't hold you back, but if you like, we can stay friends.

That being said, we're doing our best to make next-intl a great fit for your project. If you ever feel like something is not working as expected or if you have ideas for improvements, please let us know on the issue tracker and we'll do our best to help you out.


Woah, this was a long read. Did you really read all of this? next-intl was created out of a lot of curiosity and passion for internationalization, seems like we share that! We're always curious to hear how next-intl is working out for you. If you have any feedback or questions, please don't hesitate to reach out!


File: docs/src/pages/docs/environments.mdx

import Card from '@/components/Card';
import Cards from '@/components/Cards';

Environments

The next-intl APIs are available in the following environments:

While modern browsers and server runtimes typically support all necessary JavaScript APIs that are required for next-intl, you can double check the runtime requirements.


File: docs/src/pages/docs/getting-started.mdx

import Image from 'next/image';
import Callout from '@/components/Callout';
import Chip from '@/components/Chip';
import Card from '@/components/Card';
import Cards from '@/components/Cards';
import CourseBanner from '@/components/CourseBanner';

Next.js internationalization (i18n)

next-intl is an internationalization toolkit for Next.js that helps you:

  1. Render localized translations
  2. Format dates, numbers, and more
  3. Handle internationalized routing

… to provide the essential foundation for building localized apps.

Learn more about what you can build with next-intl by exploring the example apps.


File: docs/src/pages/docs/routing.mdx

import Card from '@/components/Card';
import Cards from '@/components/Cards';
import {CourseVideoBanner} from '@/components/CourseBanner';

Routing

next-intl integrates with Next.js' routing system in two places:

  1. Proxy / middleware: Negotiates the locale and handles redirects & rewrites (e.g. / → /en)
  2. Navigation APIs: Lightweight wrappers around Next.js' navigation APIs like <Link />

This enables you to express your app in terms of APIs like <Link href="/about">, while aspects like the locale and user-facing pathnames are automatically handled behind the scenes (e.g. /de/über-uns).

Prefer to watch a video?


File: docs/src/pages/docs/usage.mdx

import Callout from '@/components/Callout';
import Card from '@/components/Card';
import Cards from '@/components/Cards';

Usage guide

This guide explains how next-intl can be used in React components.

Note that while it might be helpful to get an overview of the capabilities of next-intl, you don't have to read the whole usage guide upfront. Rather, you can come back to it as necessary.

Let's get started:


File: docs/src/pages/docs/workflows.mdx

import Card from '@/components/Card';
import Cards from '@/components/Cards';
import {CourseVideoBanner} from '@/components/CourseBanner';

Workflows & integrations

To get the most out of next-intl, you can choose from these integrations to improve your workflow.

Prefer to watch a video?



File: docs/src/pages/docs/environments/actions-metadata-route-handlers.mdx

import Callout from '@/components/Callout';
import Details from '@/components/Details';
import {CourseVideoBanner} from '@/components/CourseBanner';

Server Actions, Metadata & Route Handlers

There are a few places in Next.js apps where you can apply internationalization outside of React components:

  1. Metadata API
  2. Server Actions
  3. Open Graph images
  4. Manifest
  5. Sitemap
  6. Route Handlers

next-intl/server provides a set of awaitable functions that can be used in these cases.

Metadata API

To internationalize metadata like the page title, you can use functionality from next-intl in the generateMetadata function that can be exported from pages and layouts.

tsx filename="layout.tsx"
import {getTranslations} from 'next-intl/server';

export async function generateMetadata() {
  const t = await getTranslations('Metadata');

  return {
    title: t('title')
  };
}

Server Actions

Server Actions provide a mechanism to execute server-side code that is invoked by the client. In case you're returning user-facing messages, you can use next-intl to localize them based on the user's locale.

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

async function loginAction(locale: string, data: FormData) {
  'use server';

  const t = await getTranslations({namespace: 'LoginForm', locale});
  const areCredentialsValid = /* ... */;
  if (!areCredentialsValid) {
    return {error: t('invalidCredentials')};
  }
}

Note that when you're displaying messages generated in Server Actions to the user, you should consider the case if the user can switch the locale while the message is displayed to ensure that the UI is localized consistently.

If you're using a [locale] segment as part of your routing strategy then this is handled automatically. If you're not, you might want to clear the message manually, e.g. by resetting the state of the respective component via key={locale}.

When using Zod for validation, how can I localize error messages?

Zod allows you to provide contextual error maps that can be used to customize error messages per parse invocation. Since the locale is specific to a particular request, this mechanism comes in handy to turn structured errors from zod into localized messages:

tsx
import {getTranslations} from 'next-intl/server';
import {loginUser} from '@/services/session';
import {z} from 'zod';

const loginFormSchema = z.object({
  email: z.string().email(),
  password: z.string().min(1)
});

// ...

async function loginAction(locale: string, data: FormData) {
  'use server';

  const t = await getTranslations({namespace: 'LoginForm', locale});
  const values = Object.fromEntries(data);

  const result = loginFormSchema.safeParse(values, {
    error(issue) {
      if (issue.path) {
        const path = issue.path.join('.');
        return {
          email: t('invalidEmail'),
          password: t('invalidPassword')
        }[path];
      }
    }
  });

  // ...
}

This approach keeps the schema locale-agnostic and allows you to tailor the wording to each field.

If you'd instead like to share error messages across multiple schemas, you can move the mapping into a reusable helper:

tsx filename="getvalidationerrormap.ts"
import {getTranslations} from 'next-intl/server';

export default async function getValidationErrorMap() {
  const t = await getTranslations('Validation');

  return function errorMap(issue) {
    switch (issue.code) {
      case 'too_small':
        return t('tooSmall', {minimum: Number(issue.minimum)});
      case 'invalid_format':
        return t('invalidFormat');
      default:
        return undefined;
    }
  };
}
tsx
async function loginAction(data: FormData) {
  'use server';

  const values = Object.fromEntries(data);
  const result = loginFormSchema.safeParse(values, {
    error: await getValidationErrorMap()
  });

  // ...
}

Open Graph images

If you're programmatically generating Open Graph images, you can call functions from next-intl in the exported component:

tsx filename="app/[locale]/opengraph-image.tsx"
import {ImageResponse} from 'next/og';
import {getTranslations} from 'next-intl/server';

export default async function OpenGraphImage({params}) {
  const {locale} = await params;
  const t = await getTranslations({locale, namespace: 'OpenGraphImage'});
  return new ImageResponse(<div style={{fontSize: 128}}>{t('title')}</div>);
}

Next.js will create a public route based on the segment where opengraph-image.tsx is placed, e.g.:

text
http://localhost:3000/en/opengraph-image?f87b2d56cee109c7

However, if you're using locale-based routing and you've customized the localePrefix setting, this route might not be accessible since Next.js doesn't know about potential rewrites of the middleware.

If this applies to your app, you can adapt your matcher to bypass requests to the opengraph-image.tsx file:

tsx filename="proxy.ts"
// ...

export const config = {
  matcher: [
    // Skip all paths that should not be internationalized
    '/((?!api|_next|_vercel|.*/opengraph-image|.*\\..*).*)'

    // ...
  ]
};

Manifest

Since the manifest file needs to be placed in the root of the app folder (outside the [locale] dynamic segment), you need to provide a locale explicitly since next-intl can't infer it from the pathname:

tsx filename="app/manifest.ts"
import {MetadataRoute} from 'next';
import {getTranslations} from 'next-intl/server';

export default async function manifest(): Promise<MetadataRoute.Manifest> {
  // Pick a locale that is representative of the app
  const locale = 'en';

  const t = await getTranslations({
    namespace: 'Manifest',
    locale
  });

  return {
    name: t('name'),
    start_url: '/',
    theme_color: '#101E33'
  };
}

Sitemap

If you're using a sitemap to inform search engines about all pages of your site, you can attach locale-specific alternate entries to every URL to indicate that a particular page is available in multiple languages or regions.

Note that by default, next-intl returns the link response header to instruct search engines that a page is available in multiple languages. While this sufficiently links localized pages for search engines, you may choose to provide this information in a sitemap instead if you have more specific requirements (e.g. when using CMS-driven URLs like /news/[articleSlug] where articleSlug depends on the locale).

Next.js supports providing alternate URLs per language via the alternates entry which can be combined with getPathname to construct URLs for each locale:

tsx
import {MetadataRoute} from 'next';
import {getPathname} from '@/i18n/navigation';

const host = 'https://acme.com';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  return [
    {
      url: host,
      lastModified: new Date(),
      alternates: {
        languages: {
          es: host + (await getPathname({locale: 'es', href: '/'})),
          de: host + (await getPathname({locale: 'de', href: '/'}))
        }
      }
    }
  ];
}

Learn more:

Route Handlers

You can use next-intl in Route Handlers too. The locale can either be received from a search param, a layout segment or by parsing the accept-language header of the request.

tsx filename="app/api/hello/route.tsx"
import {NextResponse} from 'next/server';
import {hasLocale} from 'next-intl';
import {getTranslations} from 'next-intl/server';
import {routing} from '@/i18n/routing';

export async function GET(request) {
  // Example: Receive the `locale` via a search param
  const {searchParams} = new URL(request.url);
  const locale = searchParams.get('locale');
  if (!hasLocale(routing.locales, locale)) {
    return NextResponse.json({error: 'Invalid locale'}, {status: 400});
  }

  const t = await getTranslations({locale, namespace: 'Hello'});
  return NextResponse.json({title: t('title')});
}

File: docs/src/pages/docs/environments/core-library.mdx

import Details from '@/components/Details';

Core library

While next-intl is primarily intended to be used in Next.js apps, the core is agnostic and can be used independently—either in React apps or any other JavaScript environment.

React apps

next-intl internally uses a library called use-intl. This core library contains most features you're familiar with from next-intl, but lacks the following Next.js-specific features:

  1. Routing APIs
  2. Awaitable APIs for Server Actions, Metadata and Route Handlers
  3. Server Components integration along with i18n/request.ts

In case Server Components establish themselves in React apps outside of Next.js, the support for Server Components might be moved to the core library in the future.

In contrast, use-intl contains all APIs that are necessary for handling i18n in regular React apps:

This allows you to use the same APIs that you know from next-intl in other environments:

  1. React Apps (example)
  2. React Native
  3. Testing environments like Jest (example)
  4. Storybook

Basic usage:

tsx
import {IntlProvider, useTranslations} from 'use-intl';

// You can get the messages from anywhere you like. You can also
// fetch them from within a component and then render the provider
// along with your app once you have the messages.
const messages = {
  App: {
    hello: 'Hello {username}!'
  }
};

function Root() {
  return (
    <IntlProvider messages={messages} locale="en">
      <App user={{name: 'Jane'}} />
    </IntlProvider>
  );
}

function App({user}) {
  const t = useTranslations('App');
  return <h1>{t('hello', {username: user.name})}</h1>;
}

Can I build shared components that work in both Next.js and plain React apps?

Yes, shared components that render in either environment are supported.

In this case, it can be beneficial to import from next-intl for the relevant components, to be able to benefit from the Server
Components integration
in case you render them in Next.js. In other environments, imports to next-intl will work as well, but will require IntlProvider to be present.

Non-React apps

Besides the React-specific APIs, use-intl also exports two low-level functions that can be used in any JavaScript environment:

  1. createTranslator for translating messages
  2. createFormatter for formatting numbers, dates & times and lists

These APIs receive all relevant configuration directly and don't rely on global configuration.

You can use these APIs as follows:

tsx
import {createTranslator, createFormatter} from 'use-intl/core';

const messages = {
  basic: 'Hello {name}!',
  rich: 'Hello <b>{name}</b>!'
};

// This creates the same function that is returned by `useTranslations`.
// Since there's no provider, you can pass all the properties you'd
// usually pass to the provider directly here.
const t = createTranslator({locale: 'en', messages});

// Result: "Hello world!"
t('basic', {name: 'world'});

// To generate HTML markup, you can consider using the `markup`
// function which in contrast to `t.rich` returns a markup string.
t.markup('rich', {
  name: 'world',
  b: (chunks) => `<b>${chunks}</b>`
});

// Creates the same object that is returned by `useFormatter`.
const format = createFormatter({locale: 'en'});

// Result: "Oct 17, 2022"
format.dateTime(new Date(2022, 9, 17), {dateStyle: 'medium'});

File: docs/src/pages/docs/environments/error-files.mdx

import Callout from '@/components/Callout';

Internationalization in Next.js error files

The Next.js App Router's file convention provides two files that can be used for error handling:

  1. not-found.js
  2. error.js

This page provides practical guides for these cases.

Tip: You can have a look at the App Router example to explore a working app with error handling.

`not-found.js`

This section is only relevant if you're using [locale-based routing](/docs/routing).

Next.js renders the closest not-found page when a route segment calls the notFound function. We can use this mechanism to provide a localized 404 page by adding a not-found file within the [locale] folder.

tsx filename="app/[locale]/not-found.tsx"
import {useTranslations} from 'next-intl';

export default function NotFoundPage() {
  const t = useTranslations('NotFoundPage');
  return <h1>{t('title')}</h1>;
}

Note however that Next.js will only render this page when the notFound function is called from within a route, not for all unknown routes in general.

Catching unknown routes

To catch unknown routes too, you can define a catch-all route that explicitly calls the notFound function.

tsx filename="app/[locale]/[...rest]/page.tsx"
import {notFound} from 'next/navigation';

export default function CatchAllPage() {
  notFound();
}

After this change, all requests that are matched within the [locale] segment will render the not-found page when an unknown route is encountered (e.g. /en/unknown).

Catching non-localized requests

When the user requests a route that is not matched by the next-intl middleware, there's no locale associated with the request (depending on your matcher config, e.g. /unknown.txt might not be matched).

You can add a global-not-found page to handle these cases too:

tsx filename="app/global-not-found.tsx"
import Error from 'next/error';

export default function GlobalNotFound() {
  return (
    <html lang="en">
      <body>
        <Error statusCode={404} />
      </body>
    </html>
  );
}

For the 404 page to render, we need to call the notFound function when an unknown value for the [locale] segment is encountered—typically in i18n/request.ts.

`error.js`

When an error file is defined, Next.js creates an error boundary within your layout that wraps pages accordingly to catch runtime errors:

tsx
<RootLayout>
  <ErrorBoundary fallback={<Error />}>
    <Page />
  </ErrorBoundary>
</RootLayout>
Schematic component hierarchy that Next.js creates internally.

Since the error file must be defined as a Client Component, you have to use NextIntlClientProvider to provide messages in case it renders:

tsx filename="layout.tsx"
import pick from 'lodash/pick';
import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';

export default async function RootLayout(/* ... */) {
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider
          locale={locale}
          // Make sure to provide at least the messages for `Error`
          messages={pick(messages, 'Error')}
        >
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

Once NextIntlClientProvider is in place, you can use functionality from next-intl in the error file:

tsx filename="error.tsx"
'use client';

import {useTranslations} from 'next-intl';

export default function Error({error, reset}) {
  const t = useTranslations('Error');

  return (
    <div>
      <h1>{t('title')}</h1>
      <button onClick={reset}>{t('retry')}</button>
    </div>
  );
}

Note that error.tsx is loaded right after your app has initialized. If your app is performance-sensitive and you want to avoid loading translation functionality from next-intl as part of this bundle, you can export a lazy reference from your error file:

tsx filename="error.tsx"
'use client';

import {lazy} from 'react';

// Move error content to a separate chunk and load it only when needed
export default lazy(() => import('./Error'));

File: docs/src/pages/docs/environments/runtime-requirements.mdx

import Callout from '@/components/Callout';

Runtime requirements

Browser

The source code of next-intl is compiled for the same browsers that Next.js supports.

Based on the features you're using, you have to make sure your target browsers support the following APIs:

If you target a browser that doesn't support all the required APIs, consider using polyfills.

Cloudflare provides a polyfill service that you can use to load the necessary polyfills for a given locale.

Example:

js filename="intlpolyfills.tsx"
import {useLocale} from 'next-intl';
import Script from 'next/script';

function IntlPolyfills() {
  const locale = useLocale();

  const polyfills = [
    'Intl',
    'Intl.Locale',
    'Intl.DateTimeFormat',
    `Intl.DateTimeFormat.~locale.${locale}`,
    `Intl.NumberFormat`,
    `Intl.NumberFormat.~locale.${locale}`,
    'Intl.PluralRules',
    `Intl.PluralRules.~locale.${locale}`,
    'Intl.RelativeTimeFormat',
    `Intl.RelativeTimeFormat.~locale.${locale}`,
    'Intl.ListFormat',
    `Intl.ListFormat.~locale.${locale}`,
    'Intl.DisplayNames',
    `Intl.DisplayNames.~locale.${locale}`
  ];

  return (
    <Script
      strategy="beforeInteractive"
      src={
        'https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=' +
        polyfills.join(',')
      }
    />
  );
}
Note that the polyfill service doesn't support every locale. You can find a list of the available polyfills in the [`polyfill-service` repository](https://github.com/cdnjs/polyfill-service/tree/main/polyfill-libraries/3.101.0/polyfills/__dist) (e.g. search for `Intl.DateTimeFormat.~locale.de-AT`).

Node.js

The minimum version to support all relevant Intl APIs is Node.js 13. Starting from this version, all required APIs are available.


File: docs/src/pages/docs/environments/server-client-components.mdx

import Callout from '@/components/Callout';
import Details from '@/components/Details';

Internationalization of Server & Client Components

React Server Components allow you to implement components that remain server-side only if they don’t require React’s interactive features, such as useState and useEffect.

This applies to handling internationalization too.

tsx filename="page.tsx"
import {useTranslations} from 'next-intl';

// Since this component doesn't use any interactive features
// from React, it can be run as a Server Component.

export default function HomePage() {
  const t = useTranslations('HomePage');
  return <h1>{t('title')}</h1>;
}

Moving internationalization to the server side unlocks new levels of performance, leaving the client side for interactive features.

Benefits of server-side internationalization:

  1. Your messages never leave the server and don't need to be passed to the client side
  2. Library code for internationalization doesn't need to be loaded on the client side
  3. No need to split your messages, e.g. based on routes or components
  4. No runtime cost on the client side

Using internationalization in Server Components

Server Components can be declared in two ways:

  1. Async components
  2. Non-async, regular components

In a typical app, you'll likely find both types of components. next-intl provides corresponding APIs that work for the given component type.

Async components

These are primarily concerned with fetching data and cannot use hooks. Due to this, next-intl provides a set of awaitable versions of the functions that you usually call as hooks from within components.

tsx filename="page.tsx"
import {getTranslations} from 'next-intl/server';

export default async function ProfilePage() {
  const user = await fetchUser();
  const t = await getTranslations('ProfilePage');

  return (
    <PageLayout title={t('title', {username: user.name})}>
      <UserDetails user={user} />
    </PageLayout>
  );
}

These functions are available:

  • getTranslations
  • getFormatter
  • getNow
  • getTimeZone
  • getMessages
  • getLocale

Non-async components [#shared-components]

Components that aren't declared with the async keyword and don't use interactive features like useState, are referred to as shared components. These can render either as a Server or Client Component, depending on where they are imported from.

In Next.js, Server Components are the default, and therefore shared components will typically execute as Server Components:

tsx filename="userdetails.tsx"
import {useTranslations} from 'next-intl';

export default function UserDetails({user}) {
  const t = useTranslations('UserProfile');

  // This component will execute as a Server Component by default.
  // However, if it is imported from a Client Component, it will
  // execute as a Client Component.
  return (
    <section>
      <h2>{t('title')}</h2>
      <p>{t('followers', {count: user.numFollowers})}</p>
    </section>
  );
}

If you import useTranslations, useFormatter, useLocale, useNow and useTimeZone from a shared component, next-intl will automatically provide an implementation that works best for the environment this component executes in (server or client).

How does the Server Components integration work?

next-intl uses react-server conditional exports to load code that is optimized for the usage in Server or Client Components. While configuration for hooks like useTranslations is read via useContext on the client side, on the server side it is loaded via i18n/request.ts.

Hooks are currently primarily known for being used in Client Components since they are typically stateful or don't apply to a server environment. However, hooks like useId can be used in Server Components too. Similarly, next-intl provides a hooks-based API that looks identical, regardless of if it's used in a Server or Client Component.

The one restriction that currently comes with this pattern is that hooks cannot be called from async components. next-intl therefore provides a separate set of awaitable APIs for this use case.

Should I use async or non-async functions for my components?

If you implement components that qualify as shared components, it can be beneficial to implement them as non-async functions. This allows to use these components either in a server or client environment, making them really flexible. Even if you don't intend to to ever run a particular component on the client side, this compatibility can still be helpful, e.g. for simplified testing.

However, there's no need to dogmatically use non-async functions exclusively for handling internationalization—use what fits your app best.

Regarding performance, async functions and hooks can be used interchangeably. The configuration from i18n/request.ts is only loaded once upon first usage and both implementations use request-based caching internally where relevant. The only minor difference is that async functions have the benefit that rendering can be resumed right after an async function has been invoked. In contrast, in case a hook call triggers the initialization in i18n/request.ts, the component will suspend until the config is resolved and will re-render subsequently, possibly re-executing component logic prior to the hook call. However, once config has been resolved as part of a request, hooks will execute synchronously without suspending, resulting in less overhead in comparison to async functions since rendering can be resumed without having to wait for the microtask queue to flush (see resuming a suspended component by replaying its execution in the corresponding React RFC).

Using internationalization in Client Components

Depending on your situation, you may need to handle internationalization in Client Components. Providing all messages to the client side is the easiest way to get started, therefore next-intl automatically does this when you render NextIntlClientProvider. This is a reasonable approach for many apps.

However, you can be more selective about which messages are passed to the client side if you're interested in optimizing the performance of your app:

tsx filename="layout.tsx"
<NextIntlClientProvider
  // Don't pass any messages to the client
  messages={null}
>
  ...
</NextIntlClientProvider>

You can still add another instance of NextIntlClientProvider for certain parts of your app in case you want to pass messages to the client there.

There are several options for using translations from next-intl in Client Components, listed here in order of enabling the best performance:

Option 1: Passing translated labels to Client Components

The preferred approach is to pass the processed labels as props or children from a Server Component.

tsx filename="faqentry.tsx" {11-13}
import {useTranslations} from 'next-intl';
import Expandable from './Expandable'; // A Client Component
import FAQContent from './FAQContent';

export default function FAQEntry() {
  // Call `useTranslations` in a Server Component ...
  const t = useTranslations('FAQEntry');

  // ... and pass translated content to a Client Component
  return (
    <Expandable title={t('title')}>
      <FAQContent content={t('description')} />
    </Expandable>
  );
}
tsx filename="expandable.tsx" {5,14-15}
'use client';

import {useState} from 'react';

function Expandable({title, children}) {
  const [expanded, setExpanded] = useState(false);

  function onToggle() {
    setExpanded(!expanded);
  }

  return (
    <div>
      <button onClick={onToggle}>{title}</button>
      {expanded && <div>{children}</div>}
    </div>
  );
}

By doing this, we can use interactive features from React like useState on translated content, even though the translation only runs on the server side.

Learn more in the Next.js docs: Passing Server Components to Client Components as Props

Example: How can I implement a locale switcher?

If you implement a locale switcher as an interactive select, you can keep internationalization on the server side by rendering the labels from a Server Component and only marking the select element as a Client Component.

tsx filename="localeswitcher.tsx"
import {useLocale, useTranslations} from 'next-intl';
import {locales} from '@/config';

// A Client Component that registers an event listener for
// the `change` event of the select, uses `useRouter`
// to change the locale and uses `useTransition` to display
// a loading state during the transition.
import LocaleSwitcherSelect from './LocaleSwitcherSelect';

export default function LocaleSwitcher() {
  const t = useTranslations('LocaleSwitcher');
  const locale = useLocale();

  return (
    <LocaleSwitcherSelect defaultValue={locale} label={t('label')}>
      {locales.map((cur) => (
        <option key={cur} value={cur}>
          {t('locale', {locale: cur})}
        </option>
      ))}
    </LocaleSwitcherSelect>
  );
}

Example implementation (demo)

See also: useRouter

Example: How can I implement a form?

Forms need client-side state for showing loading indicators and validation errors.

To keep internationalization on the server side, it can be helpful to structure your components in a way where the interactive parts are moved out to leaf components instead of marking the whole form with 'use client';.

Example:

tsx filename="app/register/page.tsx"
import {useTranslations} from 'next-intl';

// A Client Component, so that `useActionState` can be used
// to potentially display errors received after submission.
import RegisterForm from './RegisterForm';

// A Client Component, so that `useFormStatus` can be used
// to disable the input field during submission.
import FormField from './FormField';

// A Client Component, so that `useFormStatus` can be used
// to disable the submit button during submission.
import FormSubmitButton from './FormSubmitButton';

export default function RegisterPage() {
  const t = useTranslations('RegisterPage');

  function registerAction() {
    'use server';
    // ...
  }

  return (
    <RegisterForm action={registerAction}>
      <FormField label={t('firstName')} name="firstName" />
      <FormField label={t('lastName')} name="lastName" />
      <FormField label={t('email')} name="email" />
      <FormField label={t('password')} name="password" />
      <FormSubmitButton label={t('submit')} />
    </RegisterForm>
  );
}

Option 2: Moving state to the server side

You might run into cases where you have dynamic state, such as pagination, that should be reflected in translated messages.

tsx filename="pagination.tsx"
function Pagination({curPage, totalPages}) {
  const t = useTranslations('Pagination');
  return <p>{t('info', {curPage, totalPages})}</p>;
}

You can still manage your translations on the server side by using:

  1. Page or search params
  2. Cookies
  3. Database state

In particular, page and search params are often a great option because they offer additional benefits such as preserving the state of the app when the URL is shared, as well as integration with the browser history.

There's an article on Smashing Magazine about [using `next-intl` in Server Components](https://www.smashingmagazine.com/2023/03/internationalization-nextjs-13-react-server-components) which explores the usage of search params through a real-world example (specifically the section about [adding interactivity](https://www.smashingmagazine.com/2023/03/internationalization-nextjs-13-react-server-components/#adding-interactivity-dynamic-ordering-of-photos)).

Option 3: Providing individual messages

If you need to incorporate dynamic state into components that cannot be moved to the server side, you can wrap these components with NextIntlClientProvider and provide the relevant messages.

tsx filename="counter.tsx"
import pick from 'lodash/pick';
import {NextIntlClientProvider, useMessages} from 'next-intl';
import ClientCounter from './ClientCounter';

export default function Counter() {
  // Receive messages provided in `i18n/request.ts` …
  const messages = useMessages();

  return (
    <NextIntlClientProvider
      messages={
        // … and provide the relevant messages
        pick(messages, 'ClientCounter')
      }
    >
      <ClientCounter />
    </NextIntlClientProvider>
  );
}

How can I know which messages I need to provide to the client side?

Currently, the messages you select for being passed to the client side need to be picked based on knowledge about the implementation of the wrapped components.

An automatic, compiler-driven approach is being evaluated in next-intl#1.

Option 4: Providing all messages

If you're building a highly dynamic app where most components use React's interactive features, you may prefer to make all messages available to Client Components—this is the default behavior of next-intl.

tsx filename="layout.tsx" /nextintlclientprovider/
import {NextIntlClientProvider} from 'next-intl';

export default async function RootLayout(/* ... */) {
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider>...</NextIntlClientProvider>
      </body>
    </html>
  );
}

How does loading messages on the client side relate to performance?

Depending on the requirements for your app, you might want to monitor your Core Web Vitals to ensure your app meets your performance goals.

If you pass messages to NextIntlClientProvider, Next.js will emit them during the streaming render to the markup of the page so that they can be used by Client Components. This can contribute to the total blocking time, which in turn can relate to the interaction to next paint metric. If you're seeking to improve these metrics in your app, you can be more selective about which messages are passed to the client side.

However, as the general rule for optimization goes: Always measure before you optimize. If your app already performs well, there's no need for optimization.

There are currently two research areas that aim to maximize the performance of using messages on the client side:

  1. Automatic tree shaking of messages
  2. Ahead-of-time compilation of messages

The goal of these are to optimize the patterns you're already using with next-intl, enabling best-in-class performance for your app without changes to your code.

Troubleshooting

"Failed to call `useTranslations` because the context from `NextIntlClientProvider` was not found." [#missing-context]

You might encounter this error or a similar one referencing useFormatter while working on your app.

This can happen because:

  1. You're intentionally calling the hook from a Client Component, but NextIntlClientProvider is not present as an ancestor in the component tree. If this is the case, you can wrap your component in NextIntlClientProvider to resolve this error.
  2. The component that calls the hook accidentally ended up in a client-side module graph, but you expected it to render as a Server Component. If this is the case, try to pass this component via children to the Client Component instead.

"Functions cannot be passed directly to Client Components because they're not serializable." [#non-serializable-props]

You might encounter this error when you try to pass a non-serializable prop to NextIntlClientProvider.

The component accepts the following props that are not serializable:

  1. onError
  2. getMessageFallback

To configure these, you can wrap NextIntlClientProvider with another component that is marked with 'use client' and defines the relevant props.

See: How can I provide non-serializable props like onError to NextIntlClientProvider?


File: docs/src/pages/docs/environments/testing.mdx

import Callout from '@/components/Callout';
import Details from '@/components/Details';

Testing

Components that use next-intl can be used in unit tests:

tsx
import {render} from '@testing-library/react';
import {NextIntlClientProvider} from 'next-intl';
import {expect, it} from 'vitest';
import messages from '../../messages/en.json';
import UserProfile from './UserProfile';

it('renders', () => {
  render(
    <NextIntlClientProvider locale="en" messages={messages}>
      <UserProfile />
    </NextIntlClientProvider>
  );
});

To avoid having to mock server components, it can be beneficial to define components as non-async functions, allowing you to flexibly render them in any environment.

Vitest

next-intl is bundled as ESM-only, which requires the usage of explicit file extensions internally. However, in order to avoid a deoptimization in Next.js, next-intl currently has to import from next/navigation instead of next/navigation.js.

Vitest correctly assumes a file extension though, therefore for the time being, if you're using createNavigation, you need to ask Vitest to process imports within next-intl:

tsx filename="vitest.config.mts"
import {defineConfig} from 'vitest/config';

export default defineConfig({
  test: {
    server: {
      deps: {
        // https://github.com/vercel/next.js/issues/77200
        inline: ['next-intl']
      }
    }
  }
});

Jest

Since Jest doesn't have built-in ESM support, you need to instruct Jest to transform imports from next-intl:

tsx filename="jest.config.js"
const nextJest = require('next/jest');

const createJestConfig = nextJest({dir: './'});

module.exports = async () => ({
  ...(await createJestConfig({
    testEnvironment: 'jsdom',
    rootDir: 'src'
  })()),
  // https://github.com/vercel/next.js/issues/40183
  transformIgnorePatterns: ['node_modules/(?!next-intl)/']
});

File: docs/src/pages/docs/getting-started/app-router.mdx

import Card from '@/components/Card';
import Cards from '@/components/Cards';
import {Tabs} from 'nextra/components';
import Callout from '@/components/Callout';
import Steps from '@/components/Steps';
import Details from '@/components/Details';
import CourseBanner, {CourseVideoBanner} from '@/components/CourseBanner';

Next.js App Router internationalization (i18n)

Prefer to watch a video?

Getting started

If you haven't done so already, create a Next.js app that uses the App Router and run:

sh
npm install next-intl

Now, we're going to create the following file structure:

text
├── messages
│   ├── en.json
│   └── ...
├── next.config.ts
└── src
    ├── i18n
    │   └── request.ts
    └── app
        ├── layout.tsx
        └── page.tsx

Let's set up the files:

`messages/en.json` [#messages]

Messages represent the translations that are available per language and can be provided either locally or loaded from a remote data source.

The simplest option is to add JSON files in your local project folder:

json filename="messages/en.json"
{
  "HomePage": {
    "title": "Hello world!"
  }
}

`i18n/request.ts` [#i18n-request]

next-intl creates a request-scoped configuration object, which you can use to provide messages and other options based on the user's locale to Server Components.

tsx filename="src/i18n/request.ts"
import {getRequestConfig} from 'next-intl/server';

export default getRequestConfig(async () => {
  // Static for now, we'll change this later
  const locale = 'en';

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

Can I move this file somewhere else?

This file is supported out-of-the-box as ./i18n/request.ts both in the src folder as well as in the project root with the extensions .ts, .tsx, .js and .jsx.

If you prefer to move this file somewhere else, you can optionally provide a path to the plugin:

js filename="next.config.ts"
const withNextIntl = createNextIntlPlugin(
  // Specify a custom path here
  './somewhere/else/request.ts'
);

`next.config.ts` [#next-config]

Now, set up the plugin which links your i18n/request.ts file to next-intl.

<Tabs items={['next.config.ts', 'next.config.js']}>
<Tabs.Tab>

js filename="next.config.ts"
import {NextConfig} from 'next';
import createNextIntlPlugin from 'next-intl/plugin';

const nextConfig: NextConfig = {};

const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);

</Tabs.Tab>
<Tabs.Tab>

js filename="next.config.js"
const createNextIntlPlugin = require('next-intl/plugin');

const withNextIntl = createNextIntlPlugin();

/** @type {import('next').NextConfig} */
const nextConfig = {};

module.exports = withNextIntl(nextConfig);

</Tabs.Tab>

`app/layout.tsx` [#layout]

To make your request configuration available to Client Components, you can wrap the children in your root layout with NextIntlClientProvider.

tsx filename="app/layout.tsx" {11}
import {NextIntlClientProvider} from 'next-intl';

type Props = {
  children: React.ReactNode;
};

export default async function RootLayout({children}: Props) {
  return (
    <html>
      <body>
        <NextIntlClientProvider>{children}</NextIntlClientProvider>
      </body>
    </html>
  );
}

`app/page.tsx` [#page]

Use translations in your page components or anywhere else!

tsx filename="app/page.tsx"
import {useTranslations} from 'next-intl';

export default function HomePage() {
  const t = useTranslations('HomePage');
  return <h1>{t('title')}</h1>;
}

In case of async components, you can use the awaitable getTranslations function instead:

tsx filename="app/page.tsx"
import {getTranslations} from 'next-intl/server';

export default async function HomePage() {
  const t = await getTranslations('HomePage');
  return <h1>{t('title')}</h1>;
}

Next steps

Locale-based routing

If you’d like to use unique pathnames for every language that your app supports (e.g. /en/about or example.de/über-uns), you can continue to set up a top-level [locale] segment for your app.

Provide a locale

If your app doesn't require unique pathnames per locale, you can provide a locale to next-intl based on user preferences or other application logic.

The simplest option is to use a cookie:

tsx filename="src/i18n/request.ts"
import {cookies} from 'next/headers';
import {getRequestConfig} from 'next-intl/server';

export default getRequestConfig(async () => {
  const store = await cookies();
  const locale = store.get('locale')?.value || 'en';

  return {
    locale
    // ...
  };
});

Internationalization isn't just translating words

next-intl provides the essential foundation for internationalization in Next.js apps. It handles aspects like translations, date and number formatting, as well as internationalized routing.

However, building for a global audience spans a wider range of topics:

  • Choosing the right architecture and routing strategy for your app

  • Integrating with backend services or a CMS

  • Leveraging generative AI for content localization

  • Streamlining your development workflow with TypeScript and IDE tooling

  • Collaborating with your team using a translation management system

  • Understanding all the pieces that contribute to a truly localized experience

  • Mastering SEO for multilingual apps to reach global audiences


File: docs/src/pages/docs/getting-started/pages-router.mdx

import {Tabs} from 'nextra/components';
import Callout from '@/components/Callout';

Next.js Pages Router internationalization (i18n)

While it's recommended to use next-intl with the App Router, the Pages Router is still fully supported.

<Tabs items={['Multiple languages', 'Single language']}>
<Tabs.Tab>

  1. npm install next-intl
  2. Set up internationalized routing
  3. Add the provider in _app.tsx
jsx filename="_app.tsx" /nextintlclientprovider/
import {NextIntlClientProvider} from 'next-intl';
import {useRouter} from 'next/router';

export default function App({Component, pageProps}) {
  const router = useRouter();

  return (
    <NextIntlClientProvider
      locale={router.locale}
      timeZone="Europe/Vienna"
      messages={pageProps.messages}
    >
      <Component {...pageProps} />
    </NextIntlClientProvider>
  );
}
  1. Provide messages on a page-level
js filename="pages/index.tsx"
export async function getStaticProps(context) {
  return {
    props: {
      // You can get the messages from anywhere you like. The recommended
      // pattern is to put them in JSON files separated by locale and read
      // the desired one based on the `locale` received from Next.js.
      messages: (await import(`../../messages/${context.locale}.json`)).default
    }
  };
}
  1. Use translations in your components!

</Tabs.Tab>
<Tabs.Tab>
Even if you only support a single language, next-intl can still be helpful to separate your labels and formatting concerns from your application logic.

  1. npm install next-intl

  2. Add the provider in _app.tsx

jsx filename="_app.tsx" /nextintlclientprovider/
import {NextIntlClientProvider} from 'next-intl';

export default function App({Component, pageProps}) {
  return (
    <NextIntlClientProvider
      locale="en"
      timeZone="Europe/Vienna"
      messages={pageProps.messages}
    >
      <Component {...pageProps} />
    </NextIntlClientProvider>
  );
}
  1. Provide messages on a page-level
js filename="pages/index.tsx"
export async function getStaticProps() {
  const locale = 'en';

  return {
    props: {
      // You can get the messages from anywhere you like. The recommended pattern
      // is to put them in JSON files separated by locale (e.g. `en.json`).
      messages: (await import(`../../messages/${locale}.json`)).default
    }
  };
}
  1. Use translations in your components!

</Tabs.Tab>

Next steps:

  • Exploring `next-intl`? Check out the [usage guide](/docs/usage).
  • Ran into an issue? Have a look at the [Pages Router example](/examples#pages-router) to explore a working app.
  • Are you transitioning from the Pages to the App Router? Check out [the migration example](/examples#app-router-migration).

  • File: docs/src/pages/docs/routing/configuration.mdx

    import Details from '@/components/Details';
    import Card from '@/components/Card';
    import Cards from '@/components/Cards';
    import Callout from '@/components/Callout';
    import {CourseVideoBanner} from '@/components/CourseBanner';

    Routing configuration

    `defineRouting`

    The routing configuration that is shared between the middleware and the navigation APIs can be defined with the defineRouting function.

    tsx filename="src/i18n/routing.ts"
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      // A list of all locales that are supported
      locales: ['en', 'de'],
    
      // Used when no locale matches
      defaultLocale: 'en'
    });

    Depending on your routing needs, you may wish to consider further settings—see below.

    What if the locales aren't known at build time?

    In case you're building an app where locales can be added and removed at runtime, you can provide the routing configuration for the middleware dynamically per request.

    To create the corresponding navigation APIs, you can omit the locales argument from createNavigation in this case.

    Still, in case you're defining other routing config, make sure to keep them in sync between the middleware and the navigation APIs.

    `localePrefix`

    By default, the pathnames of your app will be available under a prefix that matches your directory structure (e.g. /en/about → app/[locale]/about/page.tsx). You can however adapt the routing to optionally remove the prefix or customize it per locale by configuring the localePrefix setting.

    Learn more:

    `localePrefix: 'always'` (default) [#locale-prefix-always]

    By default, pathnames always start with the locale (e.g. /en/about).

    tsx filename="routing.ts" {5}
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      // ...
      localePrefix: 'always'
    });

    `localePrefix: 'as-needed'` [#locale-prefix-as-needed]

    If you want to use no prefix for the default locale (e.g. /about) while keeping it for other locales (e.g. /de/about), you can configure your routing accordingly:

    tsx filename="routing.ts" {5}
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      // ...
      localePrefix: 'as-needed'
    });

    Note that:

    1. If you use this routing strategy, make sure that your matcher detects unprefixed pathnames.
    2. The middleware will by default set a cookie to remember the user's locale preference. If no explicit locale prefix is present in the pathname, then locale detection will potentially redirect users to the latest locale that was matched based on the cookie value (e.g. / → /de).
    3. If a superfluous locale prefix like /en/about is requested, the middleware will automatically redirect to the unprefixed version /about. This can be helpful in case you're redirecting from another locale and you want to update a potential cookie value first (e.g. <Link /> relies on this mechanism).

    `localePrefix: 'never'` [#locale-prefix-never]

    If you'd like to provide a locale to next-intl, e.g. based on user settings, you can consider not using locale-based routing in the first place.

    However, you can also configure the middleware to never show a locale prefix in the URL, which can be helpful in the following cases:

    1. You want to use domain-based routing and there's only one locale per domain
    2. You want to use a cookie to determine the locale while enabling static rendering
    tsx filename="routing.ts" {5}
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      // ...
      localePrefix: 'never'
    });

    In this case, requests for all locales will be rewritten to have the locale only prefixed internally. You still need to place all your pages inside a [locale] folder for the routes to be able to receive the locale param.

    Note that:

    1. If you use this routing strategy, make sure that your matcher detects unprefixed pathnames.
    2. Alternate links are disabled in this mode since URLs might not be unique per locale. Due to this, consider including these yourself, or set up a sitemap that links localized pages via alternates.
    3. You can consider increasing the maxAge attribute of the locale cookie to a longer duration to remember the user's preference across sessions.

    `prefixes` [#locale-prefix-prefixes]

    If you'd like to customize the user-facing prefix, you can provide a locale-based mapping:

    tsx filename="routing.ts"
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      locales: ['en-US', 'de-AT', 'zh'],
      defaultLocale: 'en-US',
      localePrefix: {
        mode: 'always',
        prefixes: {
          'en-US': '/us',
          'de-AT': '/eu/at'
          // (/zh will be used as-is)
        }
      }
    });

    Note that:

    1. You should adapt your matcher to match the custom prefixes.
    2. Custom prefixes are only visible to the user and rewritten internally to the corresponding locale. Therefore, the [locale] segment corresponds to the locale, not the prefix.

    Can I read the matched prefix in my app?

    Since the custom prefix is rewritten to the locale internally, you can't access the prefix directly. However, you can extract details like the region from the locale:

    tsx
    import {useLocale} from 'next-intl';
    
    function Component() {
      // Assuming the locale is 'en-US'
      const locale = useLocale();
    
      // Extracts the "US" region
      const {region} = new Intl.Locale(locale);
    }

    The region must be a valid ISO 3166-1 alpha-2 country code or a UN M49 region code. When passed to Intl.Locale, the region code is treated as case-insensitive and normalized to uppercase. You can also combine languages with regions where the language is not natively spoken (e.g. en-AT describes English as used in Austria).

    Apart from the region, a locale can encode further properties, like the numbering system.

    If you'd like to encode custom information in the locale, you can use arbitrary private extensions, denoted by the -x- prefix (e.g. en-US-x-usd). The Intl.Locale constructor ignores private extensions, but you can extract them from the locale string manually.

    `pathnames` [#pathnames]

    Prefer to watch a video?

    Many apps choose to localize pathnames, especially when search engine optimization is relevant.

    Example:

    • /en/about
    • /de/über-uns

    Since you typically want to define these routes only once internally, you can use the next-intl middleware to rewrite such incoming requests to shared pathnames.

    tsx filename="routing.ts"
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      locales: ['en-US', 'en-UK', 'de'],
      defaultLocale: 'en-US',
    
      // The `pathnames` object holds pairs of internal and
      // external paths. Based on the locale, the external
      // paths are rewritten to the shared, internal ones.
      pathnames: {
        // If all locales use the same pathname, a single
        // external path can be used for all locales
        '/': '/',
        '/blog': '/blog',
    
        // If locales use different paths, you can
        // specify the relevant external pathnames
        '/services': {
          de: '/leistungen'
        },
    
        // Encoding of non-ASCII characters is handled
        // automatically where relevant
        '/about': {
          de: '/über-uns'
        },
    
        // Dynamic params are supported via square brackets
        '/news/[articleSlug]': {
          de: '/neuigkeiten/[articleSlug]'
        },
    
        // Static pathnames that overlap with dynamic segments
        // will be prioritized over the dynamic segment
        '/news/just-in': {
          de: '/neuigkeiten/aktuell'
        },
    
        // Also (optional) catch-all segments are supported
        '/categories/[...slug]': {
          de: '/kategorien/[...slug]'
        }
      }
    });

    Localized pathnames map to a single internal pathname that is created via the file-system based routing in Next.js. In the example above, /de/über-uns will be handled by the page at /[locale]/about/page.tsx.

    How can I localize CMS-driven URLs?

    If you have a route like /news/[articleSlug], you may want to localize your pathnames like this:

    text
    /en/news/launch-of-new-product
    /de/neuigkeiten/produktneuheit

    In this case, you can use pathnames to localize the static part of the pathname:

    tsx
    '/news/[articleSlug]': {
      de: '/neuigkeiten/[articleSlug]'
    }

    … and use a localized slug from your CMS for the dynamic part.

    When doing this, make sure your locale switcher and alternate links are also aware of your CMS-driven URLs so they point to the correct localized pathnames.

    Learn more:

    How can I revalidate localized pathnames?

    Depending on if a route is generated statically (at build time) or dynamically (at runtime), revalidatePath needs to be called either for the localized or the internal pathname.

    Consider this example:

    text
    app
    └── [locale]
        └── news
            └── [slug]

    … with this routing configuration:

    tsx filename="routing.ts"
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      locales: ['en', 'de'],
      defaultLocale: 'en',
      pathnames: {
        '/news/[slug]': {
          en: '/news/[slug]',
          de: '/neuigkeiten/[slug]'
        }
      }
    });

    Depending on whether some-article was included in generateStaticParams or not, you can revalidate the route like this:

    tsx
    // Statically generated at build time
    revalidatePath('/de/news/some-article');
    
    // Dynamically generated at runtime:
    revalidatePath('/de/neuigkeiten/some-article');

    When in doubt, you can revalidate with revalidateTag instead.

    See also: vercel/next.js#59825

    `domains`

    Prefer to watch a video?

    If you want to serve your localized content based on different domains, you can provide a list of mappings between domains and locales via the domains setting.

    Examples:

    • us.example.com: en-US
    • ca.example.com: en-CA
    • ca.example.com/fr: fr-CA
    • fr.example.com: fr-FR

    In many cases, domains are combined with a localePrefix setting to achieve results as shown above. Also custom prefixes can be used to customize the user-facing prefix per locale.

    tsx filename="routing.ts"
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      locales: ['en-US', 'en-CA', 'fr-CA', 'fr-FR'],
      defaultLocale: 'en-US',
      domains: [
        {
          domain: 'us.example.com',
          defaultLocale: 'en-US',
          locales: ['en-US']
        },
        {
          domain: 'ca.example.com',
          defaultLocale: 'en-CA',
          locales: ['en-CA', 'fr-CA']
        },
        {
          domain: 'fr.example.com',
          defaultLocale: 'fr-FR',
          locales: ['fr-FR']
        }
      ],
      localePrefix: {
        mode: 'as-needed',
        prefixes: {
          // Cleaner prefix for `ca.example.com/fr`
          'fr-CA': '/fr'
        }
      }
    });

    Locales are required to be unique across domains, therefore regional variants are typically used to avoid conflicts. Note however that you don't necessarily need to provide messages for each locale if the overall language is sufficient for your use case.

    If no domain matches, the middleware will fall back to default locale matching based on prefixes (e.g. on localhost, for local development).

    Optionally, each domain can override the overall localePrefix setting:

    tsx filename="routing.ts" {6,12,18,24}
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      locales: ['en-US', 'en-CA', 'fr-CA', 'fr-FR'],
      defaultLocale: 'en-US',
      localePrefix: 'as-needed',
      domains: [
        {
          domain: 'us.example.com',
          defaultLocale: 'en-US',
          locales: ['en-US'],
          localePrefix: 'never'
        },
        {
          domain: 'ca.example.com',
          defaultLocale: 'en-CA',
          locales: ['en-CA', 'fr-CA'],
          localePrefix: 'always'
        },
        {
          domain: 'fr.example.com',
          defaultLocale: 'fr-FR',
          locales: ['fr-FR']
          // Uses the global setting
        }
      ]
    });

    See also: Domain-based routing

    `localeDetection` [#locale-detection]

    The middleware will detect a matching locale based on your routing configuration & the incoming request and will either pass the request through for a matching locale or redirect to one that matches.

    If you want to rely entirely on the URL to resolve the locale, you can set the localeDetection property to false. This will disable locale detection based on the accept-language header and a potentially existing cookie value from a previous visit.

    tsx filename="routing.ts" {5}
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      // ...
      localeDetection: false
    });

    In this case, only the locale prefix and a potentially matching domain are used to determine the locale.

    If you don't rely on a locale cookie being set, you can consider turning it off.

    If a user changes the locale to a value that doesn't match the accept-language header, next-intl will set a session cookie called NEXT_LOCALE that contains the most recently detected locale. This is used to remember the user's locale preference for subsequent requests.

    By default, the cookie will be configured with the following attributes:

    1. sameSite: This value is set to lax so that the cookie can be set when coming from an external site.
    2. path: This value is not set by default, but will use the value of your basePath if configured.

    If you have more specific requirements, you can adjust these settings accordingly:

    tsx filename="routing.ts"
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      // ...
    
      // Will be merged with the defaults
      localeCookie: {
        // Custom cookie name
        name: 'USER_LOCALE',
        // Expire in one year
        maxAge: 60 * 60 * 24 * 365
      }
    });

    … or turn the cookie off entirely:

    tsx filename="routing.ts"
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      // ...
    
      localeCookie: false
    });

    Which maxAge value should I consider for GDPR compliance?

    To be compliant out of the box, next-intl does not set the max-age value of the cookie, making it a session cookie that expires when a browser is closed.

    Please note that legal requirements may vary by region, so it's advisable to verify them independently. While we strive to keep this information as up-to-date as possible, we cannot guarantee its accuracy.

    Learn more:

    The middleware automatically sets the link header to inform search engines that your content is available in different languages. Note that this automatically integrates with your routing strategy and will generate the correct links based on your configuration.

    However, there are cases where you may want to provide these links yourself:

    1. You have pages that are only available for certain locales
    2. You're using an external system like a CMS to manage pathnames of your pages

    In this case, you can opt-out of this behavior by setting alternateLinks to false:

    tsx filename="routing.ts"
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      // ...
    
      alternateLinks: false
    });

    If you want to provide alternate links yourself, you can do so for example with a sitemap or by using the Metadata API.

    Learn more:

    Which alternate links are included?

    Using the middleware defaults, the link header of a response for / will look like this:

    text
    link: <https://example.com/en>; rel="alternate"; hreflang="en",
          <https://example.com/de>; rel="alternate"; hreflang="de",
          <https://example.com/>; rel="alternate"; hreflang="x-default"

    The x-default entry is included to point to a variant that can be used if no other language matches the user's browser setting. This special entry is reserved for language selection & detection, in our case issuing a 307 redirect to the best matching locale.

    To provide fully specified URLs, the domain is read from the x-forwarded-host header, with a fallback to host (hosting platforms typically provide these headers out of the box).

    Your middleware configuration, including options like domains, pathnames and basePath, is automatically incorporated.

    Can I customize the alternate links?

    If you need to customize the alternate links, you can either turn them off and provide your own implementation, or if you only need to make minor adaptions, you can compose the middleware and add your custom logic after the middleware has run:

    tsx filename="middleware.ts"
    import createMiddleware from 'next-intl/middleware';
    import LinkHeader from 'http-link-header';
    import {NextRequest} from 'next/server';
    import {routing} from './i18n/routing';
    
    const handleI18nRouting = createMiddleware(routing);
    
    export default async function middleware(request: NextRequest) {
      const response = handleI18nRouting(request);
    
      // Example: Remove the `x-default` entry
      const link = LinkHeader.parse(response.headers.get('link'));
      link.refs = link.refs.filter((entry) => entry.hreflang !== 'x-default');
      response.headers.set('link', link.toString());
    
      return response;
    }

    `next.config.ts` [#next-config]

    Apart from your routing configuration, next-intl will also incorporate settings from next.config.ts.

    `basePath`

    The middleware as well as the navigation APIs will automatically consider a basePath that you might have configured in your Next.js config.

    The only exception is the getPathname function, which will return the bare pathname without a base path. Due to this, you can prefix the returned pathname manually if you need to.

    When using a base path, you should make sure that your matcher handles an explicit root:

    tsx filename="proxy.ts"
    export const config = {
      // The `matcher` is relative to the `basePath`
      matcher: [
        // This entry handles the root of the base
        // path and should always be included
        '/'
    
        // ... other matcher config
      ]
    };

    `trailingSlash`

    If you have trailingSlash set to true in your Next.js config, this setting will be taken into account by the middleware and the navigation APIs.

    Note that if you're using pathnames, your internal and external pathnames can be defined either with or without a trailing slash as they will be normalized internally.


    File: docs/src/pages/docs/routing/middleware.mdx

    import Callout from '@/components/Callout';
    import Details from '@/components/Details';
    import {CourseVideoBanner} from '@/components/CourseBanner';

    Proxy / middleware

    The next-intl middleware can be created via createMiddleware.

    It receives a routing configuration and takes care of:

    1. Locale negotiation
    2. Applying relevant redirects & rewrites
    3. Providing alternate links for search engines

    Example:

    tsx filename="proxy.ts"
    import createMiddleware from 'next-intl/middleware';
    import {routing} from './i18n/routing';
    
    export default createMiddleware(routing);
    
    export const config = {
      // Match all pathnames except for
      // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
      // - … the ones containing a dot (e.g. `favicon.ico`)
      matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
    };

    Note: proxy.ts was called middleware.ts up until Next.js 16.

    Locale detection [#locale-detection]

    The locale is negotiated based on your routing configuration, taking into account your settings for localePrefix, domains, localeDetection, and localeCookie.

    Prefix-based routing (default) [#location-detection-prefix]

    Prefer to watch a video?

    By default, prefix-based routing is used to determine the locale of a request.

    In this case, the locale is detected based on these priorities:

    1. A locale prefix is present in the pathname (e.g. /en/about)
    2. A cookie is present that contains a previously detected locale
    3. A locale can be matched based on the accept-language header
    4. As a last resort, the defaultLocale is used

    To change the locale, users can visit a prefixed route. This will take precedence over a previously matched locale that is saved in a cookie or the accept-language header and will update a previous cookie value.

    Example workflow:

    1. A user requests / and based on the accept-language header, the en locale is matched.
    2. The user is redirected to /en.
    3. The app renders <Link locale="de" href="/">Switch to German</Link> to allow the user to change the locale to de.
    4. When the user clicks on the link, a request to /de is initiated.
    5. The middleware will add a cookie to remember the preference for the de locale.
    6. The user later requests / again and the middleware will redirect to /de based on the cookie.

    Which algorithm is used to match the accept-language header against the available locales?

    To determine the best-matching locale based on the available options from your app, the middleware uses the "best fit" algorithm of @formatjs/intl-localematcher. This algorithm is expected to provide better results than the more conservative "lookup" algorithm that is specified in RFC 4647.

    To illustrate this with an example, let's consider your app supports these locales:

    1. en-US
    2. de-DE

    The "lookup" algorithm works by progressively removing subtags from the user's accept-language header until a match is found. This means that if the user's browser sends the accept-language header en-GB, the "lookup" algorithm will not find a match, resulting in the default locale being used.

    In contrast, the "best fit" algorithm compares a distance between the user's accept-language header and the available locales, while taking into consideration regional information. Due to this, the "best fit" algorithm is able to match en-US as the best-matching locale in this case.

    Domain-based routing [#location-detection-domain]

    Prefer to watch a video?

    If you're using the domains setting, the middleware will match the request against the available domains to determine the best-matching locale. To retrieve the domain, the host is read from the x-forwarded-host header, with a fallback to host (hosting platforms typically provide these headers out of the box).

    The locale is detected based on these priorities:

    1. A locale prefix is present in the pathname (e.g. ca.example.com/fr)
    2. A locale is stored in a cookie and is supported on the domain
    3. A locale that the domain supports is matched based on the accept-language header
    4. As a fallback, the defaultLocale of the domain is used

    Since the middleware is aware of all your domains, if a domain receives a request for a locale that is not supported (e.g. en.example.com/fr), it will redirect to an alternative domain that does support the locale.

    Example workflow:

    1. The user requests us.example.com and based on the defaultLocale of this domain, the en locale is matched.
    2. The app renders <Link locale="fr" href="/">Switch to French</Link> to allow the user to change the locale to fr.
    3. When the link is clicked, a request to us.example.com/fr is initiated.
    4. The middleware recognizes that the user wants to switch to another domain and responds with a redirect to ca.example.com/fr.

    Matcher config

    The middleware is intended to only run on pages, not on arbitrary files that you serve independently of the user locale (e.g. /favicon.ico).

    A popular strategy is to match all routes that don't start with certain segments (e.g. /_next) and also none that include a dot (.) since these typically indicate static files. However, if you have some routes where a dot is expected (e.g. /users/jane.doe), you should explicitly provide a matcher for these.

    tsx filename="proxy.ts"
    export const config = {
      // Matcher entries are linked with a logical "or", therefore
      // if one of them matches, the middleware will be invoked.
      matcher: [
        // Match all pathnames except for
        // - … if they start with `/api`, `/_next` or `/_vercel`
        // - … the ones containing a dot (e.g. `favicon.ico`)
        '/((?!api|_next|_vercel|.*\\..*).*)',
    
        // However, match all pathnames within `/users`, optionally with a locale prefix
        '/([\\w-]+)?/users/(.+)'
      ]
    };

    {/* Keep this in sync with packages/next-intl/test/middleware/middleware.test.tsx */}

    Note that some third-party providers like Vercel Analytics typically use internal endpoints that are then rewritten to an external URL (e.g. /_vercel/insights/view). Make sure to exclude such requests from your middleware matcher so they aren't rewritten by accident.

    Composing other middlewares

    By calling createMiddleware, you'll receive a function of the following type:

    tsx
    function middleware(request: NextRequest): NextResponse;

    If you need to incorporate additional behavior, you can either modify the request before the next-intl middleware receives it, modify the response or even create the middleware based on dynamic configuration.

    tsx filename="proxy.ts"
    import createMiddleware from 'next-intl/middleware';
    import {NextRequest} from 'next/server';
    
    export default async function proxy(request: NextRequest) {
      // Step 1: Use the incoming request (example)
      const defaultLocale = request.headers.get('x-your-custom-locale') || 'en';
    
      // Step 2: Create and call the next-intl middleware (example)
      const handleI18nRouting = createMiddleware({
        locales: ['en', 'de'],
        defaultLocale
      });
      const response = handleI18nRouting(request);
    
      // Step 3: Alter the response (example)
      response.headers.set('x-your-custom-locale', defaultLocale);
    
      return response;
    }
    
    export const config = {
      // Match only internationalized pathnames
      matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
    };

    Example: Additional rewrites

    If you need to handle rewrites apart from the ones provided by next-intl, you can call NextResponse.rewrite() conditionally after the next-intl middleware has run.

    This example rewrites requests for /[locale]/profile to /[locale]/profile/new if a special cookie is set.

    tsx filename="proxy.ts"
    import createMiddleware from 'next-intl/middleware';
    import {NextRequest, NextResponse} from 'next/server';
    import {routing} from './i18n/routing';
    
    const handleI18nRouting = createMiddleware(routing);
    
    export default async function proxy(request: NextRequest) {
      let response = handleI18nRouting(request);
    
      // Additional rewrite when NEW_PROFILE cookie is set
      if (response.ok) {
        // (not for errors or redirects)
        const [, locale, ...rest] = new URL(
          response.headers.get('x-middleware-rewrite') || request.url
        ).pathname.split('/');
        const pathname = '/' + rest.join('/');
    
        if (
          pathname === '/profile' &&
          request.cookies.get('NEW_PROFILE')?.value === 'true'
        ) {
          response = NextResponse.rewrite(
            new URL(`/${locale}/profile/new`, request.url),
            {headers: response.headers}
          );
        }
      }
    
      return response;
    }
    
    export const config = {
      // Match only internationalized pathnames
      matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
    };

    You may wish to customize this based on your routing configuration and use case.

    Starter templates (auth, saas, tenants)

    🌐 learn.next-intl.dev provides additional starter templates with middleware composition.

    Including:

    • app-router-auth with Better Auth: Auth-protected app with locale from user settings.
    • app-router-saas with Better Auth: Locale prefixes on public routes with a login to a protected app.
    • app-router-tenants: An advanced composition pattern that routes between multiple tenants and a landing page while supporting multiple locales.

    Source code

    Usage without proxy / middleware (static export)

    If you're using the static export feature from Next.js (output: 'export'), a proxy / middleware will not run. You can use prefix-based routing nonetheless to internationalize your app, but a few tradeoffs apply.

    Static export limitations:

    1. Using a locale prefix is required (same as localePrefix: 'always')
    2. The locale can't be negotiated on the server (same as localeDetection: false)
    3. You can't use pathnames, as these require server-side rewrites
    4. Static rendering is required

    Additionally, other limitations as documented by Next.js will apply too.

    If you choose this approach, you might want to enable a redirect at the root of your app:

    tsx filename="app/page.tsx"
    import {redirect} from 'next/navigation';
    
    // Redirect the user to the default locale when `/` is requested
    export default function RootPage() {
      redirect('/en');
    }

    If you add such a root page at app/page.tsx, you need to add a root layout at app/layout.tsx as well, even if it's just passing children through:

    tsx filename="app/layout.tsx"
    export default function RootLayout({children}) {
      return children;
    }

    Troubleshooting

    "The proxy / middleware doesn't run for a particular page." [#middleware-not-running]

    To resolve this, make sure that:

    1. The proxy / middleware is set up in the correct file (e.g. src/proxy.ts).
    2. Your matcher correctly matches all routes of your application, including dynamic segments with potentially unexpected characters like dots (e.g. /users/jane.doe).
    3. In case you're composing other middlewares, ensure that the middleware is called correctly.
    4. In case you require static rendering, make sure to follow the static rendering guide instead of relying on hacks like force-static.

    "My page content isn't localized despite the pathname containing a locale prefix." [#content-not-localized]

    This is very likely the result of your proxy / middleware not running on the request. As a result, a potential fallback from i18n/request.ts might be applied.

    "Unable to find `next-intl` locale because the proxy / middleware didn't run on this request and no `locale` was returned in `getRequestConfig`." [#unable-to-find-locale]

    If the middleware is not expected to run on this request (e.g. because you're using a setup without locale-based routing, you should explicitly return a locale from getRequestConfig to recover from this error.

    If the middleware is expected to run, verify that your middleware is set up correctly.

    Note that next-intl will invoke the notFound() function to abort the render if no locale is available after getRequestConfig has run. You should consider adding a not-found page due to this.


    File: docs/src/pages/docs/routing/navigation.mdx

    import Callout from '@/components/Callout';
    import Details from '@/components/Details';
    import {CourseVideoBanner} from '@/components/CourseBanner';

    Prefer to watch a video?

    next-intl provides lightweight wrappers around Next.js' navigation APIs like <Link /> and useRouter that automatically handle the user locale and pathnames behind the scenes.

    To create these APIs, you can call the createNavigation function with your routing configuration:

    tsx filename="navigation.ts"
    import {createNavigation} from 'next-intl/navigation';
    import {routing} from './routing';
    
    export const {Link, redirect, usePathname, useRouter, getPathname} =
      createNavigation(routing);

    This function is typically called in a central module like src/i18n/navigation.ts in order to provide easy access to navigation APIs in your components.

    What if the locales aren't known at build time?

    In case you're building an app where locales can be added and removed at runtime, createNavigation can be called without the locales argument, therefore allowing any string that is encountered at runtime to be a valid locale. In this case, you'd not use the defineRouting function.

    tsx filename="navigation.ts"
    import {createNavigation} from 'next-intl/navigation';
    
    export const {Link, redirect, usePathname, useRouter, getPathname} =
      createNavigation({
        // ... potentially other routing
        // config, but no `locales` ...
      });

    Note however that the locales argument for the middleware is still mandatory. If you need to fetch the available locales at runtime, you can provide the routing configuration for the middleware dynamically per request.

    APIs

    The created navigation APIs are thin wrappers around the equivalents from Next.js and mostly adhere to the same function signatures. Your routing configuration and the user's locale are automatically incorporated.

    If you're using the pathnames setting in your routing configuration, the internal pathnames that are accepted for href arguments will be strictly typed and localized to the given locale.

    This component wraps next/link and localizes the pathname as necessary.

    tsx
    import {Link} from '@/i18n/navigation';
    
    // When the user is on `/en`, the link will point to `/en/about`
    <Link href="/about">About</Link>
    
    // Search params can be added via `query`
    <Link href={{pathname: "/users", query: {sortBy: 'name'}}}>Users</Link>
    
    // You can override the `locale` to switch to another language
    // (this will set the `hreflang` attribute on the anchor tag)
    <Link href="/" locale="de">Switch to German</Link>

    Depending on if you're using the pathnames setting, dynamic params can either be passed as:

    tsx
    // 1. A final string (when not using `pathnames`)
    <Link href="/users/12">Susan</Link>
    
    // 2. An object (when using `pathnames`)
    <Link href={{
      pathname: '/users/[userId]',
      params: {userId: '5'}
    }}>
      Susan
    </Link>

    How can I render a navigation link?

    The useSelectedLayoutSegment hook from Next.js allows you to detect if a given child segment is active from within the parent layout. Since this returns an internal pathname, it can be matched against an href that you can pass to Link.

    tsx filename="navigationlink.tsx"
    'use client';
    
    import {useSelectedLayoutSegment} from 'next/navigation';
    import {ComponentProps} from 'react';
    import {Link} from '@/i18n/navigation';
    
    export default function NavigationLink({
      href,
      ...rest
    }: ComponentProps<typeof Link>) {
      const selectedLayoutSegment = useSelectedLayoutSegment();
      const pathname = selectedLayoutSegment ? `/${selectedLayoutSegment}` : '/';
      const isActive = pathname === href;
    
      return (
        <Link
          aria-current={isActive ? 'page' : undefined}
          href={href}
          style={{fontWeight: isActive ? 'bold' : 'normal'}}
          {...rest}
        />
      );
    }
    tsx
    <nav>
      <NavigationLink href="/">{t('home')}</NavigationLink>
      <NavigationLink href="/about">{t('about')}</NavigationLink>
      <NavigationLink href="/blog">{t('blog')}</NavigationLink>
    </nav>

    See also the Next.js docs on creating an active link component.

    How can I compose the link with its href prop?

    If you need to create a component that receives an href prop that is forwarded to Link internally, you can compose the props from Link with the ComponentProps type:

    tsx filename="styledlink.tsx"
    import {ComponentProps} from 'react';
    import {Link} from '@/i18n/navigation';
    
    type Props = ComponentProps<typeof Link> & {
      color: 'blue' | 'red';
    };
    
    export default function StyledLink({color, href, ...rest}: Props) {
      return <Link href={href} style={{color}} {...rest} />;
    }

    In case you're using the pathnames setting, the href prop of the wrapping component will now be strictly typed based on your routing configuration.

    Why does <Link /> always set a locale prefix when using the locale prop?

    If you're providing a locale prop, typically to change the locale of the target page, you might notice that the link's href will always include a locale prefix, even if you're using a localePrefix setting other than always.

    Example:

    If you're using localePrefix: 'as-needed' and en is your default locale, then the href for this link will still be /en/about:

    tsx
    // Links to `/en/about`
    <Link href="/about" locale="en">
      About
    </Link>

    The reason for this is that a potential cookie may need to be updated before the user can visit the unprefixed route at /about. The prefixed pathname will take care of this and will subsequently redirect to the unprefixed route. This behavior is necessary because links might be interacted with before your page is hydrated and client-side code would have a chance to update the cookie.

    If you'd like to avoid this behavior, you can instead use useRouter to switch the locale, which can rely on updating the cookie on the client side before navigating to the target page.

    How can I link to unknown routes when using the pathnames setting?

    In this case, the navigation APIs are strictly typed and only allow routes specified in the pathnames config. If you need to link to unknown routes in certain places, you can disable the type checking on a case-by-case basis:

    tsx
    // @ts-expect-error
    <Link href="/unknown">...</Link>

    Unknown routes will be passed through as-is, but will receive relevant locale prefixes in case of absolute pathnames.

    How does prefetching of localized links work?

    <Link /> from next-intl inherits the default prefetch behavior from next/link.

    The one exception to this is when you've set the locale prop. In this case, the link will not be prefetched, because this would otherwise result in prematurely overwriting the locale cookie as part of the prefetch request.

    `useRouter`

    If you need to navigate programmatically, e.g. in an event handler, next-intl provides a convience API that wraps useRouter from Next.js and localizes the pathname accordingly.

    tsx
    'use client';
    
    import {useRouter} from '@/i18n/navigation';
    
    const router = useRouter();
    
    // When the user is on `/en`, the router will navigate to `/en/about`
    router.push('/about');
    
    // Search params can be added via `query`
    router.push({
      pathname: '/users',
      query: {sortBy: 'name'}
    });
    
    // You can override the `locale` to switch to another language
    router.replace('/about', {locale: 'de'});

    Depending on if you're using the pathnames setting, dynamic params can either be passed as:

    tsx
    // 1. A final string (when not using `pathnames`)
    router.push('/users/12');
    
    // 2. An object (when using `pathnames`)
    router.push({
      pathname: '/users/[userId]',
      params: {userId: '5'}
    });

    How can I change the locale for the current page?

    By combining usePathname with useRouter, you can change the locale for the current page programmatically by navigating to the same pathname, while overriding the locale.

    Depending on if you're using the pathnames setting, you optionally have to forward params to potentially resolve an internal pathname.

    tsx
    'use client';
    
    import {usePathname, useRouter} from '@/i18n/navigation';
    import {useParams} from 'next/navigation';
    
    const pathname = usePathname();
    const router = useRouter();
    
    // Without `pathnames`: Pass the current `pathname`
    router.replace(pathname, {locale: 'de'});
    
    // With `pathnames`: Pass `params` as well
    const params = useParams();
    router.replace(
      // @ts-expect-error -- TypeScript will validate that only known `params`
      // are used in combination with a given `pathname`. Since the two will
      // always match for the current route, we can skip runtime checks.
      {pathname, params},
      {locale: 'de'}
    );

    Learn more:

    `usePathname`

    To retrieve the current pathname without a potential locale prefix, you can call usePathname.

    tsx
    'use client';
    
    import {usePathname} from '@/i18n/navigation';
    
    // When the user is on `/en`, this will be `/`
    const pathname = usePathname();

    Note that if you're using the pathnames setting, the returned pathname will correspond to an internal pathname template (dynamic params will not be replaced by their values).

    tsx
    // When the user is on `/de/über-uns`, this will be `/about`
    const pathname = usePathname();
    
    // When the user is on `/de/neuigkeiten/produktneuheit`,
    // this will be `/news/[articleSlug]`
    const pathname = usePathname();

    `redirect`

    If you want to interrupt the render and redirect to another page, you can invoke the redirect function. This wraps redirect from Next.js and localizes the pathname as necessary.

    Note that a locale prop is always required, even if you're just passing the current locale.

    tsx
    import {redirect} from '@/i18n/navigation';
    
    // Redirects to `/en/login`
    redirect({href: '/login', locale: 'en'});
    
    // Search params can be added via `query`
    redirect({href: '/users', query: {sortBy: 'name'}, locale: 'en'});

    Depending on if you're using the pathnames setting, dynamic params can either be passed as:

    tsx
    // 1. A final string (when not using `pathnames`)
    redirect({href: '/users/12', locale: 'en'});
    
    // 2. An object (when using `pathnames`)
    redirect({
      href: {
        pathname: '/users/[userId]',
        params: {userId: '5'}
      },
      locale: 'en'
    });

    When using a localePrefix setting other than always, you can enforce a locale prefix by setting the forcePrefix option to true. This is useful when changing the user's locale and you need to update the locale cookie first:

    tsx
    // Will initially redirect to `/en/about` to update the locale
    // cookie, regardless of your `localePrefix` setting
    redirect({href: '/about', locale: 'en', forcePrefix: true});
    [`permanentRedirect`](https://nextjs.org/docs/app/api-reference/functions/permanentRedirect) is supported too.

    Why does TypeScript not narrow types correctly after calling redirect?

    TypeScript currently has a limitation with control flow analysis, which results in not being able to narrow types correctly after calling redirect as well as detecting unreachable code:

    tsx
    import {redirect} from '@/i18n/navigation';
    
    function UserProfile({userId}: {userId?: string}) {
      if (!userId) {
        redirect({href: '/login', locale: 'en'});
      }
    
      // `userId` should be narrowed to `string` here,
      // but TypeScript doesn't analyze this correctly
    }

    To work around this limitation, you can return the call to the redirect function:

    tsx
    if (!userId) {
      return redirect({href: '/login', locale: 'en'});
    }
    
    // ✅ `userId` is narrowed to `string` here

    `getPathname`

    If you need to construct a particular pathname based on a locale, you can call the getPathname function.

    tsx
    import {getPathname} from '@/i18n/navigation';
    
    // Will return `/en/about`
    const pathname = getPathname({
      locale: 'en',
      href: '/about'
    });
    
    // Search params can be added via `query`
    const pathname = getPathname({
      locale: 'en',
      href: {
        pathname: '/users',
        query: {sortBy: 'name'}
      }
    });

    Depending on if you're using the pathnames setting, dynamic params can either be passed as:

    tsx
    // 1. A final string (when not using `pathnames`)
    const pathname = getPathname({
      locale: 'en',
      href: '/users/12'
    });
    
    // 2. An object (when using `pathnames`)
    const pathname = getPathname({
      locale: 'en',
      href: {
        pathname: '/users/[userId]',
        params: {userId: '5'}
      }
    });

    Use cases:


    File: docs/src/pages/docs/routing/setup.mdx

    import {Tabs} from 'nextra/components';
    import Callout from '@/components/Callout';
    import Steps from '@/components/Steps';
    import Details from '@/components/Details';
    import {CourseVideoBanner} from '@/components/CourseBanner';

    Setup locale-based routing

    Prefer to watch a video?

    In order to use unique pathnames for every language that your app supports, next-intl can be used to handle the following routing setups:

    1. Prefix-based routing (e.g. /en/about)
    2. Domain-based routing (e.g. en.example.com/about)

    In either case, next-intl integrates with the App Router by using a top-level [locale] dynamic segment that can be used to provide content in different languages.

    Initial setup

    To get started with locale-based routing, we'll set up the following files:

    `src/i18n/routing.ts` [#i18n-routing]

    We'll use routing.ts as a central place to define our routing configuration:

    ts filename="src/i18n/routing.ts"
    import {defineRouting} from 'next-intl/routing';
    
    export const routing = defineRouting({
      // A list of all locales that are supported
      locales: ['en', 'de'],
    
      // Used when no locale matches
      defaultLocale: 'en'
    });

    Depending on your requirements, you may wish to customize your routing configuration later—but let's finish with the setup first.

    `src/proxy.ts` [#proxy]

    Once we have our routing configuration in place, we can use it to set up a proxy:

    tsx filename="src/proxy.ts"
    import createMiddleware from 'next-intl/middleware';
    import {routing} from './i18n/routing';
    
    export default createMiddleware(routing);
    
    export const config = {
      // Match all pathnames except for
      // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
      // - … the ones containing a dot (e.g. `favicon.ico`)
      matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
    };

    Note: proxy.ts was called middleware.ts up until Next.js 16.

    How can I match pathnames that contain dots like /users/jane.doe?

    If you have pathnames where dots are expected, you can match them with explicit entries:

    tsx filename="src/proxy.ts" {10,11}
    // ...
    
    export const config = {
      matcher: [
        // Match all pathnames except for
        // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
        // - … the ones containing a dot (e.g. `favicon.ico`)
        '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
    
        // Match all pathnames within `{/:locale}/users`
        '/([\\w-]+)?/users/(.+)'
      ]
    };

    This will match e.g. /users/jane.doe, also optionally with a locale prefix.

    `src/i18n/navigation.ts` [#i18n-navigation]

    Additionally, we can use our routing configuration to set up the navigation APIs:

    ts filename="src/i18n/navigation.ts"
    import {createNavigation} from 'next-intl/navigation';
    import {routing} from './routing';
    
    // Lightweight wrappers around Next.js' navigation
    // APIs that consider the routing configuration
    export const {Link, redirect, usePathname, useRouter, getPathname} =
      createNavigation(routing);

    `src/i18n/request.ts` [#i18n-request]

    Now, we can read the matched locale from next/root-params in our request configuration:

    tsx filename="src/i18n/request.ts"
    import * as rootParams from 'next/root-params';
    import {notFound} from 'next/navigation';
    import {getRequestConfig} from 'next-intl/server';
    import {hasLocale} from 'next-intl';
    import {routing} from './routing';
    
    export default getRequestConfig(async ({locale}) => {
      if (!locale) {
        const paramValue = await rootParams.locale();
        if (hasLocale(routing.locales, paramValue)) {
          locale = paramValue;
        } else {
          notFound();
        }
      }
    
      return {
        locale
        // ...
      };
    });

    The locale argument that is passed to getRequestConfig can be used for edge cases where you need to override the current locale.

    Note: next/root-params is available by default in Next.js 16.3 and later. In earlier versions, it needs to be enabled via experimental.rootParams in your Next.js config.

    `src/app/[locale]/layout.tsx` [#layout]

    To complete our setup, we'll move all of our existing layouts and pages into the [locale] segment:

    text
    src
    └── app
        └── [locale]
            ├── layout.tsx
            ├── page.tsx
            └── ...

    That's all it takes!

    The locale that was matched is now correctly incorporated in all functionality from next-intl:

    tsx filename="app/[locale]/layout.tsx"
    import {useTranslations} from 'next-intl';
    
    export default function Page() {
      // ✅ Uses the locale from `i18n/request.ts`
      const t = useTranslations();
    
      // ...
    }

    From here, you can configure your routing to cater to your specific needs.

    In case you ran into an issue, have a look at the App Router example to explore a working app.

    Static rendering

    With `next/root-params`

    When you follow the setup instructions with next/root-params above, your app is automatically eligible for static rendering. Since we are using a dynamic route segment for the [locale] param, we need to use generateStaticParams so that the routes can be rendered at build time.

    Depending on your needs, you can add generateStaticParams either to a layout or pages:

    1. Layout: Enables static rendering for all pages within this layout (e.g. app/[locale]/layout.tsx)
    2. Individual pages: Enables static rendering for a specific page (e.g. app/[locale]/page.tsx)

    Example:

    tsx
    import {routing} from '@/i18n/routing';
    
    export function generateStaticParams() {
      return routing.locales.map((locale) => ({locale}));
    }

    Do I need to return all locales from generateStaticParams?

    If you prefer to only render certain locales at build time, or none at all, you can be selective about the ones you return from generateStaticParams (see Static Rendering in the Next.js docs).

    With `setRequestLocale` (legacy)

    setRequestLocale is a legacy API that was added until next/root-params was introduced. It is still supported for backwards compatibility, but it is recommended to use next/root-params instead.

    When using locale-based routing, next-intl will currently opt into dynamic rendering when APIs like useTranslations are used in Server Components. This is a limitation that we aim to remove in the future, but as a stopgap solution, next-intl provides a temporary API that can be used to enable static rendering.

    Add `generateStaticParams`

    Since we are using a dynamic route segment for the [locale] param, we need to use generateStaticParams so that the routes can be rendered at build time.

    Depending on your needs, you can add generateStaticParams either to a layout or pages:

    1. Layout: Enables static rendering for all pages within this layout (e.g. app/[locale]/layout.tsx)
    2. Individual pages: Enables static rendering for a specific page (e.g. app/[locale]/page.tsx)

    Example:

    tsx
    import {routing} from '@/i18n/routing';
    
    export function generateStaticParams() {
      return routing.locales.map((locale) => ({locale}));
    }

    Do I need to return all locales from generateStaticParams?

    If you prefer to only render certain locales at build time, or none at all, you can be selective about the ones you return from generateStaticParams (see Static Rendering in the Next.js docs).

    Add `setRequestLocale` to all relevant layouts and pages

    next-intl provides an API that can be used to distribute the locale that is received via params in layouts and pages for usage in all Server Components that are rendered as part of the request.

    tsx filename="app/[locale]/layout.tsx"
    import {setRequestLocale} from 'next-intl/server';
    import {hasLocale} from 'next-intl';
    import {notFound} from 'next/navigation';
    import {routing} from '@/i18n/routing';
    
    type Props = {
      children: React.ReactNode;
      params: Promise<{locale: string}>;
    };
    
    export default async function LocaleLayout({children, params}: Props) {
      const {locale} = await params;
      if (!hasLocale(routing.locales, locale)) {
        notFound();
      }
    
      // Enable static rendering
      setRequestLocale(locale);
    
      return (
        // ...
      );
    }
    tsx filename="app/[locale]/page.tsx"
    import {use} from 'react';
    import {setRequestLocale} from 'next-intl/server';
    import {useTranslations} from 'next-intl';
    
    export default function IndexPage({params}) {
      const {locale} = use(params);
    
      // Enable static rendering
      setRequestLocale(locale);
    
      // Once the request locale is set, you
      // can call hooks from `next-intl`
      const t = useTranslations('IndexPage');
    
      return (
        // ...
      );
    }

    Keep in mind that:

    1. The locale that you pass to setRequestLocale should be validated (e.g. in your root layout).
    2. You need to call this function in every page and every layout that you intend to enable static rendering for since Next.js can render layouts and pages independently.
    3. setRequestLocale needs to be called before you invoke any functions from next-intl like useTranslations or getMessages.

    How does setRequestLocale work?

    next-intl uses cache() to create a mutable store that holds the current locale. By calling setRequestLocale, the current locale will be written to the store, making it available to all APIs that require the locale.

    Note that the store is scoped to a request and therefore doesn't affect other requests that might be handled in parallel while a given request resolves asynchronously.

    Why is this API necessary?

    Next.js currently doesn't provide an API to read route params like locale at arbitrary places in Server Components (see vercel/next.js#58862). The locale is fundamental to all APIs provided by next-intl, therefore passing this as a prop throughout the tree doesn't stand out as particularly ergonomic.

    Due to this, next-intl uses its middleware to attach an x-next-intl-locale header to the incoming request, holding the negotiated locale as a value. This technique allows the locale to be read at arbitrary places via headers().get('x-next-intl-locale').

    However, the usage of headers opts the route into dynamic rendering.

    By using setRequestLocale, you can provide the locale that is received in layouts and pages via params to next-intl. All APIs from next-intl can now read from this value instead of the header, enabling static rendering.

    Use the `locale` param in metadata

    In addition to the rendering of your pages, also page metadata needs to qualify for static rendering.

    To achieve this, you can forward the locale that you receive from Next.js via params to the awaitable functions from next-intl.

    tsx filename="page.tsx"
    import {getTranslations} from 'next-intl/server';
    
    export async function generateMetadata({params}) {
      const {locale} = await params;
      const t = await getTranslations({locale, namespace: 'Metadata'});
    
      return {
        title: t('title')
      };
    }