Next.js App Router with react-intl (Tutorial)
A complete guide for Next.js i18n with the App Router
(6 minute read)
Last updated: May 2026
Available Next.js i18n Tutorials:
Which library should I choose? Compare them: next-i18next vs next-intl, react-i18next vs react-intl, next-i18next vs react-intl
1. Install the libraries
React Intl formats your messages, and next-i18n-router adds language detection and locale URLs. i18nexus manages and AI translates the strings.
Use Next.js 16.3+ with the App Router, TypeScript, and Node.js 22.13+ or 24 LTS. The paths below use no src directory and the default @/* alias.
npx create-next-app@latest my-app
cd my-app
npm install react-intl@^12 next-i18n-router2. Let i18nexus handle your translations
Write your copy once. i18nexus stores your source strings, AI translates them into your project languages, and gives your team a place to review the results. The CLI brings those translations into your app.
Already have an i18nexus project?
Use its project API key instead of creating another project with init. Install @i18nexus/cli as a dev dependency, add the key to .env, and run npx i18nexus pull. Follow the CLI guide to add development and build sync. The library, languages, namespaces, and output path must match the imports in this tutorial.
To create a new i18nexus project, run this from your app directory:
npx @i18nexus/cli@latest initFor this example, use these choices:
- Sign in or create an account in the browser opened by the CLI.
- Use
react-intlas your library. Installed libraries are detected automatically. - For a new app, choose that you do not have translation JSON yet. Set English (
en) as the base language and German (de) as an additional language. - Use one namespace:
common. - Accept automatic sync in
package.json. This installs the CLI locally and adds development and build sync scripts.
Already have translation JSON?
The CLI can detect and import it into your new i18nexus project. Keep your existing language codes and namespaces, and adjust the examples below to match.
If you keep a custom translation folder, the CLI includes it in your sync scripts. For manual pulls, include that same path: npx i18nexus pull --path ./your-folder.
When setup finishes, add the project API key printed by the CLI to .env in the app root. Add .env to .gitignore:
.envI18NEXUS_API_KEY="your_project_api_key"Add your first two strings
Open the dashboard link printed by the CLI and add these English strings in the common namespace. The key is what your code uses; the value is what your users read.
welcome: Welcome to my app!greeting:Hello, {name}!
Save each string, then expand its row to see the translations. You can review and edit them in the same place as your source text.
Prefer working with an AI coding assistant? Choose MCP setup during init, then ask your assistant to create these same strings through i18nexus. The MCP guide explains that workflow. The dashboard remains available for you and your team.
Give AI translation a moment to finish, then download the generated JSON before adding the imports in the next step:
npx i18nexus pullYou should now have messages/en/common.json and its German equivalent. These are generated files: make future string changes in i18nexus and let the CLI sync them.
3. Add locale routing
Move the root layout and page into app/[locale], with no app/layout.tsx above it. If you keep globals.css, move it alongside the layout or adjust its import.
i18nConfig.tsimport type {Config} from 'next-i18n-router/dist/types';
const i18nConfig: Config = {
locales: ['en', 'de'],
defaultLocale: 'en'
};
export default i18nConfig;
proxy.tsimport {i18nRouter} from 'next-i18n-router';
import type {NextRequest} from 'next/server';
import i18nConfig from './i18nConfig';
export function proxy(request: NextRequest) {
return i18nRouter(request, i18nConfig);
}
export const config = {
matcher: '/((?!api|_next|.*\\..*).*)'
};
app/locale.tsimport {locale} from 'next/root-params';
import {notFound} from 'next/navigation';
import i18nConfig from '@/i18nConfig';
export default async function getLocale() {
const currentLocale = await locale();
if (!i18nConfig.locales.includes(currentLocale)) notFound();
return currentLocale;
}
English uses /; German uses /de. The proxy detects the visitor's language and remembers their choice. Root params let nested Server Components read the matched locale without passing it through props.
4. Load messages for server and client components
app/intl.tsimport {cache} from 'react';
import {createIntl, createIntlCache} from 'react-intl/server';
import getLocale from './locale';
const formatterCache = createIntlCache();
export const getIntl = cache(async () => {
const locale = await getLocale();
const messages = (await import(`@/messages/${locale}/common.json`)).default;
return createIntl({locale, defaultLocale: 'en', messages}, formatterCache);
});
components/IntlProvider.tsx'use client';
import {IntlProvider as Provider} from 'react-intl';
import type {ComponentProps} from 'react';
export default function IntlProvider(props: ComponentProps<typeof Provider>) {
return <Provider {...props} />;
}
app/[locale]/layout.tsximport type {ReactNode} from 'react';
import {getIntl} from '@/app/intl';
import IntlProvider from '@/components/IntlProvider';
import i18nConfig from '@/i18nConfig';
export function generateStaticParams() {
return i18nConfig.locales.map(locale => ({locale}));
}
export default async function RootLayout({children}: {children: ReactNode}) {
const intl = await getIntl();
return (
<html lang={intl.locale}>
<body>
<IntlProvider locale={intl.locale} defaultLocale="en" messages={intl.messages}>
{children}
</IntlProvider>
</body>
</html>
);
}
Server Components import the formatter from react-intl/server. The client provider receives only serializable messages and locale settings. The shared helper can be called from any nested Server Component.
5. Display your strings
app/[locale]/page.tsximport {getIntl} from '@/app/intl';
import Greeting from '@/components/Greeting';
import LanguageSwitcher from '@/components/LanguageSwitcher';
export default async function Home() {
const intl = await getIntl();
return (
<main>
<h1>{intl.formatMessage({id: 'welcome'})}</h1>
<Greeting />
<LanguageSwitcher />
</main>
);
}
components/Greeting.tsx'use client';
import {FormattedMessage} from 'react-intl';
export default function Greeting() {
return <p><FormattedMessage id="greeting" values={{name: 'Sam'}} /></p>;
}
6. Change languages
components/LanguageSwitcher.tsx'use client';
import {usePathname} from 'next/navigation';
import {useIntl} from 'react-intl';
import i18nConfig from '@/i18nConfig';
export default function LanguageSwitcher() {
const {locale} = useIntl();
const pathname = usePathname();
function changeLanguage(newLocale: string) {
document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=2592000;SameSite=Lax`;
if (i18nConfig.noPrefix) {
window.location.reload();
return;
}
const hasPrefix = locale !== i18nConfig.defaultLocale || i18nConfig.prefixDefault;
const path = hasPrefix ? pathname.slice(`/${locale}`.length) : pathname;
window.location.assign(
`/${newLocale}${path === '/' ? '' : path}` +
window.location.search + window.location.hash
);
}
return (
<select aria-label="Language" value={locale} onChange={e => changeLanguage(e.target.value)}>
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
);
}
A full navigation runs the proxy even if Next.js has prefetched the destination. Including the new locale also lets the proxy update its cookie and redirect /en to /. The switcher preserves the current page, query, and fragment.
When adding links, include the current locale for non-default languages: for example, /de/about instead of /about. See the complete working example for localized page links.
Run npm run dev and try the language switcher. The welcome message and greeting should change together:
Keep translations in sync
The sync script added by the CLI runs i18nexus listen alongside your app. It pulls on startup and updates the generated JSON when strings or translations change in i18nexus. Refresh the page if your app still shows a cached translation.
For deployment, set I18NEXUS_API_KEY in your build environment and run npm run build. The CLI-added prebuild script pulls the latest JSON before compilation. Install dev dependencies in the build stage so the CLI is available. Updates to bundled translations require a new build.
These helpers use root params during Server Component rendering. For Route Handlers and Server Actions, pass the locale explicitly to a separate translation loader.
Keep adding source strings in the dashboard or through the i18nexus MCP. Your team can review and refine the AI translations in the same project. React Intl documentation has the details for larger apps.
Level up your localization
It only takes a few minutes to streamline your translations forever.
Get started