{"id":6851,"date":"2025-10-20T06:00:00","date_gmt":"2025-10-20T06:00:00","guid":{"rendered":"https:\/\/poeditor.com\/blog\/?p=6851"},"modified":"2026-04-20T12:51:04","modified_gmt":"2026-04-20T12:51:04","slug":"next-js-i18n","status":"publish","type":"post","link":"https:\/\/poeditor.com\/blog\/next-js-i18n\/","title":{"rendered":"Next.js i18n with next-intl: A comprehensive guide"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"510\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n-1024x510.png\" alt=\"\" class=\"wp-image-6871\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n-1024x510.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n-300x149.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n-768x382.png 768w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n-1536x765.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n.png 1776w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>Next.js is one of the popular ways to ship <a href=\"\/blog\/react-localization-i18next\/\">React<\/a> apps today. It\u2019s fast, it scales, and it\u2019s straightforward. A lot of us use it for documentation sites, dashboards, storefronts, and marketing pages because it covers both developer ergonomics and production needs.<\/p>\n\n\n\n<p>When you\u2019re building for more than one market, it\u2019s expected that your app speaks the user\u2019s language. That means you need to implement internationalization (i18n) and localization (l10n).<\/p>\n\n\n\n<p>In this guide, we\u2019ll set up i18n in a Next.js app using next-intl. We\u2019ll explain how to set it up for both the App and Pages Router.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Getting started<\/h2>\n\n\n\n<p>To follow this guide, you need a Next.js app. If you don\u2019t have one yet, create it with:<\/p>\n\n\n\n<p><code>npx create-next-app@latest [project-name]<\/code><\/p>\n\n\n\n<p>During the prompts, you can pick <strong>App Router<\/strong> or <strong>Pages Router<\/strong> depending on your preference. If you already have an app, just note which router you\u2019re using.<\/p>\n\n\n\n<p>Once the project is up, install <a href=\"https:\/\/www.npmjs.com\/package\/next-intl\" rel=\"nofollow\"><code>next-intl<\/code><\/a>:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td>npm i next-intl<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p><code>npm i next-intl<\/code><\/p>\n\n\n\n<p>You may ask, why <a href=\"https:\/\/next-intl.dev\/\" rel=\"nofollow\">next-intl<\/a>? Fair question. Next.js already has solid i18n routing, and <code>next-intl<\/code> sits nicely on top of it. It covers the essentials you actually need such as interpolation, pluralization, arrays, and more using ICU messages. It also works with both App Router and Pages Router, so you\u2019re not boxed in.<\/p>\n\n\n\n<p>With <code>next-intl<\/code> installed, we\u2019re good to go.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Set up i18n for App Router (step-by-step)<\/h2>\n\n\n\n<p>For internationalization to work in your app, the first thing to do is add the <code>next-intl<\/code> plugin to Next.js. Open the <code>next.config.ts<\/code> file and replace it with this or adjust it to use this:<\/p>\n\n\n\n<p><code>\/\/ next.config.ts<br>import createNextIntlPlugin from 'next-intl\/plugin';<br>const withNextIntl = createNextIntlPlugin();<br><br>\/** @type {import('next').NextConfig} *\/<br>const nextConfig = {};<br><br>export default withNextIntl(nextConfig);<\/code><\/p>\n\n\n\n<p>What this does is that it wraps your Next.js config so <code>next-intl<\/code> can plug into the build and server runtime. Under the hood, the plugin looks for a \u201crequest config\u201d (we\u2019ll add it next) to know which messages file to load for each locale.<\/p>\n\n\n\n<p>The next step is to declare your locales and set up a couple of helpers for navigation. For separation of concerns, it makes sense to keep i18n stuff in its own folder, so create <code>src\/i18n\/<\/code> and then add <code>routing.ts<\/code>:<\/p>\n\n\n\n<p><code>\/\/ src\/i18n\/routing.ts<br>import {createSharedPathnamesNavigation} from 'next-intl\/navigation';<br><br>export const locales = ['en', 'de'] as const;<br>export type Locale = (typeof locales)[number];<br><br>export const defaultLocale: Locale = 'en';<br><br>\/**<br>* These helpers are drop-in replacements for Next's Link\/useRouter etc,<br>* but they keep the locale in the URL automatically.<br>*\/<br>export const {Link, redirect, usePathname, useRouter} =<br>&nbsp; createSharedPathnamesNavigation({<br>&nbsp; &nbsp; locales,<br>&nbsp; &nbsp; \/\/ 'always' -&gt; URLs are always prefixed (\/en, \/de)<br>&nbsp; &nbsp; \/\/ If you prefer \/ (no prefix) for default locale, use 'as-needed'<br>&nbsp; &nbsp; localePrefix: 'always'<br>&nbsp; });<\/code><\/p>\n\n\n\n<p>This helps you declare supported languages and a default in one place. From here on, the locale-aware <code>Link<\/code> and router hooks keep <code>\/en<\/code> or <code>\/de<\/code> in your paths automatically.<\/p>\n\n\n\n<p>Now, you need to tell <code>next-intl<\/code> how to load messages for each request. In the same <code>src\/i18n\/<\/code> folder, create <code>request.ts<\/code>:<\/p>\n\n\n\n<p><code>\/\/ src\/i18n\/request.ts<br>import {getRequestConfig} from 'next-intl\/server';<br>import {locales, defaultLocale, type Locale} from '.\/routing';<\/code><br><br><code>\/**<br>* This runs on the server for each request.<br>* It validates the incoming locale and dynamically loads the right JSON file.<br>*\/<br>export default getRequestConfig(async ({locale}) =&gt; {<br>&nbsp; const isSupported = locales.includes(locale as Locale);<br>&nbsp; const resolvedLocale = (isSupported ? locale : defaultLocale) as Locale;<br><br>&nbsp; const messages = (await import(`..\/..\/messages\/${resolvedLocale}.json`)).default;<br><br>&nbsp; return {<br>&nbsp; &nbsp; locale: resolvedLocale,<br>&nbsp; &nbsp; messages<br>&nbsp; };<br>});<\/code><\/p>\n\n\n\n<p>What this does is that the plugin you added in <code>next.config.ts<\/code> calls this to figure out which JSON to load based on the URL. It\u2019s dynamic, so you only pull in what you actually need.<\/p>\n\n\n\n<p>As the final core file needed for the setup, you need a small <a href=\"https:\/\/nextjs.org\/docs\/app\/api-reference\/file-conventions\/middleware\" rel=\"nofollow\">middleware<\/a> to keep URLs locale-aware and to bump <code>\/<\/code> to your default locale (<code>\/en<\/code> in this example). Create <code>src\/middleware.ts<\/code>:<\/p>\n\n\n\n<p><code>\/\/ src\/middleware.ts<br>import createMiddleware from 'next-intl\/middleware';<br>import {locales, defaultLocale} from '.\/i18n\/routing';<br><br>export default createMiddleware({<br>&nbsp; locales,<br>&nbsp; defaultLocale,<br>&nbsp; localePrefix: 'always',<br>&nbsp; \/\/ Keep behavior deterministic (no automatic browser-language redirects).<br>&nbsp; localeDetection: false<br>});<br><br>export const config = {<br>&nbsp; \/**<br>&nbsp; * Apply middleware to the homepage and any path that already has a locale prefix.<br>&nbsp; * Adjust the matcher if you add more locales.<br>&nbsp; *\/<br>&nbsp; matcher: ['\/', '\/(en|de)\/:path*']<br>};<\/code><\/p>\n\n\n\n<p>This makes sure every route is prefixed with a locale (e.g., <code>\/en<\/code>, <code>\/de<\/code>). Visiting \/ redirects to <code>\/${defaultLocale}<\/code>, so \/ \u2192 <code>\/en<\/code>. Browser-language detection is turned off to avoid surprise redirects during development. You can enable it if that\u2019s your preference.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Add your translation files<\/h3>\n\n\n\n<p>Localization works by resolving keys to messages per locale. The clean way to manage this is to keep one JSON per language. At your project root, create a <code>messages\/<\/code> folder and add two files with the same keys in both.<\/p>\n\n\n\n<p class=\"has-black-color has-text-color has-link-color wp-elements-bbc7b33aac2e72db313e52cfe40d269f\"><code>messages\/en.json:<\/code><\/p>\n\n\n\n<p><code>{<br>\"Home\": {<br>\"title\": \"Welcome\",<br>\"greeting\": \"Welcome back, {name}!\"<br>}<br>}<\/code><\/p>\n\n\n\n<p class=\"has-black-color has-text-color has-link-color wp-elements-cf7a382f692ab6bb34ac2a5c25c829a3\"><code>messages\/de.json<\/code><\/p>\n\n\n\n<p><code>{<br>\"Home\": {<br>\"title\": \"Willkommen\",<br>\"greeting\": \"Willkommen zur\u00fcck, {name}!\"<br>}<br>}<\/code><\/p>\n\n\n\n<p>Keep the keys identical across locales (<code>Home.title<\/code>, <code>Home.greeting<\/code>, etc.). This is how tools (and people) keep translations in sync. We\u2019ll connect this to <a href=\"https:\/\/poeditor.com\/\">POEditor<\/a> later. For now, local JSON keeps the flow simple.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Update app structure<\/h3>\n\n\n\n<p>Next.js App Router uses a segment for the locale, so we\u2019ll route everything through <code>\/[locale]\/\u2026<\/code>. First, let\u2019s make the root page nudge users to the default locale. Open <code>src\/app\/page.tsx<\/code> and add:<\/p>\n\n\n\n<p><code>import { redirect } from 'next\/navigation';<br><br>\/\/ This page only renders when the app is built statically (output: 'export')<br>export default function RootPage() {<br>redirect('\/en');<br>}<\/code><\/p>\n\n\n\n<p>Now create a <code>[locale]<\/code> folder under <code>src\/app\/<\/code> and add a <code>layout.tsx<\/code>. This layout is a <a href=\"https:\/\/nextjs.org\/docs\/app\/getting-started\/server-and-client-components\" rel=\"nofollow\">Server Component<\/a> that validates the locale, loads the correct messages, and provides them to everything below:<br><br><code>\/\/ src\/app\/[locale]\/layout.tsx<br>import {NextIntlClientProvider} from 'next-intl';<br>import {notFound} from 'next\/navigation';<br>import {locales, type Locale} from '@\/i18n\/routing';<br><\/code><br><code>export default async function LocaleLayout({<br>&nbsp; children,<br>  params: {locale}<br>}: {<br>&nbsp; children: React.ReactNode;<br>&nbsp; params: {locale: string};<br>}) {<br>&nbsp; \/\/ Validate the URL param<br>&nbsp; if (!locales.includes(locale as Locale)) notFound();<br> <\/code><br><code>\/\/ Load messages for this locale<br>&nbsp; const messages = (await import(`..\/..\/..\/messages\/${locale}.json`)).default;<br><\/code><\/p>\n\n\n\n<p><code>return (<br>&nbsp; &nbsp; &lt;html lang={locale}&gt;<br>&nbsp; &nbsp; &nbsp; &lt;body&gt;<br>&nbsp; &nbsp; &nbsp; &nbsp; {\/* Make messages available to all client components below *\/}<br>&nbsp; &nbsp; &nbsp; &nbsp; &lt;NextIntlClientProvider locale={locale} messages={messages}&gt;<br>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {children}<br>&nbsp; &nbsp; &nbsp; &nbsp; &lt;\/NextIntlClientProvider&gt;<br>&nbsp; &nbsp; &nbsp; &lt;\/body&gt;<br>&nbsp; &nbsp; &lt;\/html&gt;<br>&nbsp; );<br>}<\/code><\/p>\n\n\n\n<p>What happens in the code above is that <code>&lt;html lang={locale}&gt;<\/code> helps browsers, assistive tech, and SEO understand the language. <code>NextIntlClientProvider<\/code> exposes messages to client components via hooks.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Render a page that uses translations<\/h3>\n\n\n\n<p>Inside the same <code>[locale]<\/code> folder, add page.tsx. This one is a client component so we can use the <code>useTranslations<\/code> hook:<\/p>\n\n\n\n<p><code>\/\/ src\/app\/[locale]\/page.tsx<br>'use client';<br><br>import {useTranslations} from 'next-intl';<br><br>export default function HomePage() {<br>&nbsp; const t = useTranslations('Home');<br>&nbsp; const name = 'Jane';<\/code><\/p>\n\n\n\n<p><code><br>return (<br>&nbsp; &nbsp; &lt;main style={{padding: 24}}&gt;<br>&nbsp; &nbsp; &nbsp; &lt;h1&gt;{t('title')}&lt;\/h1&gt;<br>&nbsp; &nbsp; &nbsp; &lt;p&gt;{t('greeting', {name})}&lt;\/p&gt;<br>&nbsp; &nbsp; &lt;\/main&gt;<br>&nbsp; );<br>}<\/code><\/p>\n\n\n\n<p>That\u2019s it! Keys from <code>messages\/{locale}.json<\/code> flow into the component, and next-intl handles formatting and interpolation. Hooks like <code>useTranslations<\/code> run in client components, the layout stays server-side.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Add a language switcher<\/h3>\n\n\n\n<p>Before we jump to the Pages Router, let\u2019s make switching languages easy. Create <code>src\/components\/Navigation.tsx<\/code>:<\/p>\n\n\n\n<p><code>\/\/ src\/components\/Navigation.tsx<br>'use client';<br><br>import {Link, usePathname} from '@\/i18n\/routing';<br><br>export default function Navigation() {<br>&nbsp; const pathname = usePathname();<br><br>&nbsp; return (<br>&nbsp; &nbsp; &lt;nav style={{display: 'flex', gap: 12, padding: 16}}&gt;<br>&nbsp; &nbsp; &nbsp; {\/* Link\/Path helpers keep the current path but swap the locale *\/}<br>&nbsp; &nbsp; &nbsp; &lt;Link href={pathname} locale=\"en\"&gt;EN&lt;\/Link&gt;<br>&nbsp; &nbsp; &nbsp; &lt;Link href={pathname} locale=\"de\"&gt;DE&lt;\/Link&gt;<br>&nbsp; &nbsp; &lt;\/nav&gt;<br>&nbsp; );<br>}<\/code><\/p>\n\n\n\n<p>Drop <code>&lt;Navigation \/&gt;<\/code> anywhere under <code>[locale]\/layout.tsx<\/code> or inside your page. Now you can hop between <code>\/en<\/code> and <code>\/de<\/code> without losing the current route.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"315\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-1024x315.png\" alt=\"\" class=\"wp-image-6860\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-1024x315.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-300x92.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-768x237.png 768w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-1536x473.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image.png 1552w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>You can access the complete project in this <a href=\"https:\/\/github.com\/POEditor\/next-internationalization\" rel=\"nofollow\">GitHub repository<\/a> to compare the setup. Here is what the final file structure should look like:<\/p>\n\n\n\n<p><code>your-app\/<br>\u251c\u2500 next.config.ts<br>\u251c\u2500 messages\/<br>\u2502&nbsp; \u251c\u2500 en.json<br>\u2502&nbsp; \u2514\u2500 de.json<br>\u251c\u2500 src\/<br>\u2502&nbsp; \u251c\u2500 i18n\/<br>\u2502&nbsp; \u2502&nbsp; \u251c\u2500 request.ts<br>\u2502&nbsp; \u2502&nbsp; \u2514\u2500 routing.ts<br>\u2502&nbsp; \u251c\u2500 middleware.ts<br>\u2502&nbsp; \u251c\u2500 app\/<br>\u2502&nbsp; \u2502&nbsp; \u251c\u2500 page.tsx&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # (optional) redirects \"\/\" \u2192 \"\/en\"<br>\u2502&nbsp; \u2502&nbsp; \u2514\u2500 [locale]\/<br>\u2502&nbsp; \u2502 &nbsp; &nbsp; \u251c\u2500 layout.tsx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # loads messages + wraps provider<br>\u2502&nbsp; \u2502 &nbsp; &nbsp; \u2514\u2500 page.tsx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # uses translations in a client component<br>\u2502&nbsp; \u2514\u2500 components\/<br>\u2502 &nbsp; &nbsp; \u2514\u2500 Navigation.tsx&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # language switcher (EN\/DE)<br>\u2514\u2500 package.json<br><\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Set up i18n for Pages Router (step-by-step)<\/h2>\n\n\n\n<p>If you\u2019re on the Pages Router, the flow is a little different from App Router. We won\u2019t use a <code>[locale]<\/code> segment, instead we\u2019ll lean on <a href=\"https:\/\/nextjs.org\/docs\/pages\/guides\/internationalization\" rel=\"nofollow\">Next.js built-in i18n routing<\/a> (<code>\/en<\/code>, <code>\/de<\/code>, etc.), connect it with <code>next-intl<\/code> once in <code>_app.tsx<\/code>, and load messages per page with <code>getStaticProps<\/code> (or <code>getServerSideProps<\/code> if you prefer <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/rendering\/server-side-rendering\" rel=\"nofollow\">SSR<\/a>).<\/p>\n\n\n\n<p>To get started, you need to tell Next.js which locales you support and wrap your config with the <code>next-intl<\/code> plugin. Open <code>next.config.ts<\/code> and adjust it:<\/p>\n\n\n\n<p><code>\/\/ next.config.ts<br>import createNextIntlPlugin from 'next-intl\/plugin';<br><br>const withNextIntl = createNextIntlPlugin();<br><br>\/** @type {import('next').NextConfig} *\/<br>const nextConfig = {<br>&nbsp; \/\/ Next.js built-in i18n does the URL routing for Pages Router<br>&nbsp; i18n: {<br>&nbsp; &nbsp; locales: ['en', 'de'],<br>&nbsp; &nbsp; defaultLocale: 'en',<br>&nbsp; &nbsp; \/\/ You can flip this to true later if you want auto language detection<br>&nbsp; &nbsp; localeDetection: false<br>&nbsp; }<br>};<br><br>export default withNextIntl(nextConfig);<\/code><\/p>\n\n\n\n<p>Next, create a \u201crequest config\u201d so the plugin knows where to fetch messages. Make a folder <code>src\/i18n\/<\/code> and add <code>request.ts<\/code>:<\/p>\n\n\n\n<p><code>\/\/ src\/i18n\/request.ts<br>import {getRequestConfig} from 'next-intl\/server';<br><\/code><\/p>\n\n\n\n<p><code>export default getRequestConfig(async ({locale}) =&gt; {<br>\/\/ Guard against unexpected locales; fall back to 'en'<br>const supported = ['en', 'de'] as const;<br>const isSupported = (supported as readonly string[]).includes(locale);<br>const resolved = isSupported ? locale : 'en';<br><\/code><\/p>\n\n\n\n<p><code>const messages = (await import(..\/..\/messages\/${resolved}.json)).default;<br>return {locale: resolved, messages};<br>});<\/code><\/p>\n\n\n\n<p><br>What this does is that whenever a request comes in for <code>\/de\/...<\/code>, the plugin calls this, we validate the locale, and we import the right JSON file. It\u2019s dynamic, so you only load what you need.<\/p>\n\n\n\n<p>Now add your translation files. Just like we did for App Router, keep one JSON per locale with the same keys.<\/p>\n\n\n\n<p><code>\/messages<br>&nbsp; \u251c\u2500 en.json<br>&nbsp; \u2514\u2500 de.json<\/code><\/p>\n\n\n\n<p>Next, add the provider once at the top of your app so every page can read translations. Create <code>src\/pages\/_app.tsx<\/code>:<\/p>\n\n\n\n<p><code>\/\/ src\/pages\/_app.tsx<br>import type {AppProps} from 'next\/app';<br>import {NextIntlProvider} from 'next-intl';<br><br>type Messages = Record&lt;string, unknown&gt;;<br><br>export default function MyApp({<br>&nbsp; Component,<br>&nbsp; pageProps<br>}: AppProps &amp; {pageProps: {messages?: Messages; locale?: string}}) {<br>&nbsp; const {messages, locale} = pageProps;<br><br>&nbsp; \/\/ In dev you might navigate to a page before messages are loaded; guard it.<br>&nbsp; if (!messages || !locale) {<br>&nbsp; &nbsp; return &lt;Component {...pageProps} \/&gt;;<br>&nbsp; }<br><br>&nbsp; return (<br>&nbsp; &nbsp; &lt;NextIntlProvider messages={messages} locale={locale}&gt;<br>&nbsp; &nbsp; &nbsp; &lt;Component {...pageProps} \/&gt;<br>&nbsp; &nbsp; &lt;\/NextIntlProvider&gt;<br>&nbsp; );<br>}<\/code><\/p>\n\n\n\n<p>This is important because <code>_app.tsx<\/code> is the Pages Router\u2019s global shell. Wrapping with <code>NextIntlProvider<\/code> here means any component can grab translations with <code>useTranslations<\/code>.<\/p>\n\n\n\n<p>Now load messages per page. For the homepage, create <code>src\/pages\/index.tsx<\/code>:<\/p>\n\n\n\n<p><code>\/\/ src\/pages\/index.tsx<br>import {useTranslations} from 'next-intl';<br>import type {GetStaticPropsContext, InferGetStaticPropsType} from 'next';<br><br>export default function HomePage(<br>&nbsp; _props: InferGetStaticPropsType&lt;typeof getStaticProps&gt;<br>) {<br>&nbsp; const t = useTranslations('Home');<br>&nbsp; const name = 'Jane';<br>&nbsp; const count = 3;<br><br>&nbsp; return (<br>&nbsp; &nbsp; &lt;main style={{padding: 24}}&gt;<br>&nbsp; &nbsp; &nbsp; &lt;h1&gt;{t('title')}&lt;\/h1&gt;<br>&nbsp; &nbsp; &nbsp; &lt;p&gt;{t('greeting', {name})}&lt;\/p&gt;<br>&nbsp; &nbsp; &nbsp; &lt;p&gt;{t('inbox', {count})}&lt;\/p&gt;<br>&nbsp; &nbsp; &lt;\/main&gt;<br>&nbsp; );<br>}<br><br>\/\/ SSG example; use getServerSideProps if your content is request-time<br>export async function getStaticProps({locale}: GetStaticPropsContext) {<br>&nbsp; const messages = (await import(`..\/..\/messages\/${locale}.json`)).default;<br>&nbsp; return {<br>&nbsp; &nbsp; props: {<br>&nbsp; &nbsp; &nbsp; messages,<br>&nbsp; &nbsp; &nbsp; locale<br>&nbsp; &nbsp; }<br>&nbsp; };<br>}<\/code><\/p>\n\n\n\n<p>What\u2019s happening here is that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>getStaticProps<\/code> runs once per locale when you build (<code>next build<\/code>). Because we set i18n.locales in <code>next.config.ts<\/code>, Next.js will call this for <code>en<\/code> and <code>de<\/code>.<\/li>\n\n\n\n<li>Whatever we return in props becomes <code>pageProps<\/code> in <code>_app.tsx<\/code>, which the provider uses.<\/li>\n\n\n\n<li>If this page were dynamic (say <code>pages\/blog\/[slug].tsx<\/code>) and you\u2019re statically generating, you\u2019d also add a <code>getStaticPaths<\/code> that returns paths for each slug for each locale. For purely server-rendered pages, swap to <code>getServerSideProps<\/code> and the pattern stays the same.<\/li>\n<\/ul>\n\n\n\n<p>Finally, let\u2019s add a simple language switcher. With Pages Router, you can either set <code>locale<\/code> on <code>&lt;Link\/&gt;<\/code> or call <code>router.push<\/code> with a <code>locale<\/code> option. Here\u2019s a component that keeps the current path and flips locales:<\/p>\n\n\n\n<p><code>\/\/ src\/components\/LanguageSwitch.tsx<br>import Link from 'next\/link';<br>import {useRouter} from 'next\/router';<br><br>export default function LanguageSwitch() {<br>&nbsp; const {asPath, locale} = useRouter();<br><br>&nbsp; \/\/ Simple toggle; in production you might list all supported locales<br>&nbsp; const next = locale === 'en' ? 'de' : 'en';<br><br>&nbsp; return (<br>&nbsp; &nbsp; &lt;nav style={{display: 'flex', gap: 12, padding: 16}}&gt;<br>&nbsp; &nbsp; &nbsp; &lt;Link href={asPath} locale=\"en\"&gt;EN&lt;\/Link&gt;<br>&nbsp; &nbsp; &nbsp; &lt;Link href={asPath} locale=\"de\"&gt;DE&lt;\/Link&gt;<br>&nbsp; &nbsp; &nbsp; {\/* Or a single toggle link *\/}<br>&nbsp; &nbsp; &nbsp; &lt;Link href={asPath} locale={next}&gt;Switch to {next.toUpperCase()}&lt;\/Link&gt;<br>&nbsp; &nbsp; &lt;\/nav&gt;<br>&nbsp; );<br>}<\/code><\/p>\n\n\n\n<p>You can now add the <code>&lt;LanguageSwitch \/&gt;<\/code> component into a page or a layout component you render on every page.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"449\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-1-1024x449.png\" alt=\"\" class=\"wp-image-6861\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-1-1024x449.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-1-300x131.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-1-768x336.png 768w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-1-1536x673.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-1.png 1600w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p><br>You can access the complete project in this <a href=\"https:\/\/github.com\/POEditor\/next-internationalization\" rel=\"nofollow\">GitHub repository<\/a> to compare the setup. Here\u2019s the final structure:<\/p>\n\n\n\n<p><code>your-app\/<br>\u251c\u2500 next.config.ts<br>\u251c\u2500 messages\/<br>\u2502&nbsp; \u251c\u2500 en.json<br>\u2502&nbsp; \u2514\u2500 de.json<br>\u251c\u2500 src\/<br>\u2502&nbsp; \u251c\u2500 i18n\/<br>\u2502&nbsp; \u2502&nbsp; \u2514\u2500 request.ts&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # tells next-intl how to load messages<br>\u2502&nbsp; \u251c\u2500 pages\/<br>\u2502&nbsp; \u2502&nbsp; \u251c\u2500 _app.tsx&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # wraps app with NextIntlProvider<br>\u2502&nbsp; \u2502&nbsp; \u2514\u2500 index.tsx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # loads messages via getStaticProps<br>\u2502&nbsp; \u2514\u2500 components\/<br>\u2502 &nbsp; &nbsp; \u2514\u2500 LanguageSwitch.tsx&nbsp; &nbsp; &nbsp; &nbsp; # locale-aware links for Pages Router<br>\u2514\u2500 package.json<\/code><\/p>\n\n\n\n<p>Now that we\u2019ve set up i18n for both the App and Pages Router, it\u2019s time to use it. In real apps you won\u2019t just render static strings, you\u2019ll pass values into messages, handle singular\/plural cases properly, and format dates, times, and numbers the way people expect.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Interpolation and pluralization<\/h2>\n\n\n\n<p>Interpolation is just \u201cput a variable inside a sentence.\u201d You declare placeholders in your JSON and pass the values when you call <code>t()<\/code>.<\/p>\n\n\n\n<p>To better understand this, add a message with a placeholder to your English file. Open <code>messages\/en.json<\/code> and add:<\/p>\n\n\n\n<p><code>{<br>\"Profile\": {<br>\"greeting\": \"Welcome back, {name}!\"<br>}<br>}<\/code><\/p>\n\n\n\n<p>Then add the same key to your other locale(s). For German, open <code>messages\/de.json<\/code>:<\/p>\n\n\n\n<p><code>{<br>&nbsp; \"Profile\": {<br>&nbsp; &nbsp; \"greeting\": \"Willkommen zur\u00fcck, {name}!\"<br>&nbsp; }<br>}<\/code><\/p>\n\n\n\n<p>Notice the placeholder is the same in both (<code>{name}<\/code>). Now, you can use it in your component.<\/p>\n\n\n\n<p><code>import {useTranslations} from 'next-intl';<br><br>export default function HomePage() {<br>&nbsp; const t = useTranslations('Profile'); \/\/ points at \"Profile\" in your JSON<br><br>&nbsp; \/\/ This is the dynamic value you want to insert<br>&nbsp; const name = 'Jane';<br><br>&nbsp; \/\/ Call t('greeting', {name: &lt;value&gt;}) to fill {name}<br>&nbsp; return &lt;p&gt;{t('greeting', {name})}&lt;\/p&gt;;<br>}<\/code><\/p>\n\n\n\n<p>Notice the placeholder is the same in both (<code>{name}<\/code>). Now, you can use it in your component. It\u2019s also important to know that you can pass any value, not just strings. Numbers, booleans, even preformatted strings are fine:<\/p>\n\n\n\n<p>t(&#8216;Profile.greeting&#8217;, {name: user.displayName || &#8216;Guest&#8217;});<\/p>\n\n\n\n<p>If you accidentally pass <code>{username: 'Jane'}<\/code> but your message uses <code>{name}<\/code>, nothing will be inserted. The keys must match.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Pluralization<\/h3>\n\n\n\n<p>Now let\u2019s handle the very common \u201c0 vs 1 vs many\u201d problem. We\u2019ll do this with <a href=\"\/blog\/icu-message-format\/\">ICU<\/a> plurals so you don\u2019t write <code>if (count === 1) \u2026<\/code> in your components.<\/p>\n\n\n\n<p>Start by declaring a pluralized message in English. Open <code>messages\/en.json<\/code> and add a key with a <code>{count, plural, \u2026}<\/code> block. I like to keep these under a small namespace, e.g. <code>\"Inbox\"<\/code>:<\/p>\n\n\n\n<p><code>{<br>&nbsp; \"Inbox\": {<br>&nbsp; &nbsp; \"messages\": \"{count, plural, =0{No messages} one{One message} other{{count} messages}}\"<br>&nbsp; }<br>}<\/code><\/p>\n\n\n\n<p>The string above is an ICU plural. It starts with <code>{count, plural, \u2026}<\/code> where count is the value you\u2019ll pass from code, and plural tells ICU to pick text based on that number. Inside, you define branches: you can match exact numbers (like <code>=0<\/code> for \u201cNo messages\u201d) and use category names (like <code>one<\/code> and <code>other<\/code>) so languages can apply their own rules. The <code>other<\/code> branch is required and acts as the fallback when nothing else matches.<\/p>\n\n\n\n<p>Now add the same key path in your other locale(s). For German:<\/p>\n\n\n\n<p><code>{<br>&nbsp; \"Inbox\": {<br>&nbsp; &nbsp; \"messages\": \"{count, plural, =0{Keine Nachrichten} one{Eine Nachricht} other{# Nachrichten}}\"<br>&nbsp; }<br>}<\/code><\/p>\n\n\n\n<p>Next, use it in your component:<\/p>\n\n\n\n<p><code>import {useTranslations} from 'next-intl';<\/code><\/p>\n\n\n\n<p><code>import {useState} from 'react';<br><br>export default function InboxDemo() {<br>&nbsp; const t = useTranslations('Inbox'); \/\/ points at the \"Inbox\" object in JSON<br>&nbsp; const [count, setCount] = useState(0);<br><br>&nbsp; return (<br>&nbsp; &nbsp; &lt;main style={{padding: 24}}&gt;<br>&nbsp; &nbsp; &nbsp; &lt;p&gt;{t('messages', {count})}&lt;\/p&gt;<br><br>&nbsp; &nbsp; &nbsp; &lt;div style={{display: 'flex', gap: 8, marginTop: 12}}&gt;<br>&nbsp; &nbsp; &nbsp; &nbsp; &lt;button onClick={() =&gt; setCount(0)}&gt;0&lt;\/button&gt;<br>&nbsp; &nbsp; &nbsp; &nbsp; &lt;button onClick={() =&gt; setCount(1)}&gt;1&lt;\/button&gt;<br>&nbsp; &nbsp; &nbsp; &nbsp; &lt;button onClick={() =&gt; setCount(5)}&gt;5&lt;\/button&gt;<br>&nbsp; &nbsp; &nbsp; &lt;\/div&gt;<br>&nbsp; &nbsp; &lt;\/main&gt;<br>&nbsp; );<br>}<\/code><\/p>\n\n\n\n<p>You can literally click through 0 \u2192 1 \u2192 5 and watch the sentence switch branches. When you flip the site to <code>\/de<\/code>, the message and number formatting change.<\/p>\n\n\n\n<p><strong>Quick tip:<\/strong> If you ever need ordinals (1st, 2nd, 3rd\u2026), use <code>selectordinal<\/code>:<\/p>\n\n\n\n<p><code>{<br>&nbsp; \"Profile\": {<br>&nbsp; &nbsp; \"anniversary\": \"It's your {year, selectordinal, one{#st} two{#nd} few{#rd} other{#th}} year!\"<br>&nbsp; }<br>}<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Formatting dates, times, and numbers<\/h2>\n\n\n\n<p>Localization isn\u2019t just about translating words. You also want dates, times, and numbers to look right for whoever\u2019s reading. With <code>next-intl<\/code> you can do this directly inside your messages (my default), or in code when you\u2019re formatting values first and then dropping them into the UI.<\/p>\n\n\n\n<p>Let\u2019s start in messages. Add a few examples to your English file:<\/p>\n\n\n\n<p><code>\/\/ messages\/en.json<br>{<br>\"Stats\": {<br>\"today\": \"Today's date is {d, date, medium}.\",<br>\"price\": \"Price: {p, number, ::currency\/USD}\",<br>\"percent\": \"Completed: {v, number, ::percent}\",<br>\"customDate\": \"Published on {d, date, ::yyyyMMMdd}\"<br>}<br>}<\/code><\/p>\n\n\n\n<p>Use them in your component like this:<\/p>\n\n\n\n<p><code>import {useTranslations} from 'next-intl';<\/code><\/p>\n\n\n\n<p><code>export default function StatsBlock() {<br>const t = useTranslations('Stats');<\/code><\/p>\n\n\n\n<p><code>return (<br>&lt;&gt;<\/code><\/p>\n\n\n\n<p><code>{t('today', {d: new Date()})}<\/code><\/p>\n\n\n\n<p><code>{t('price', {p: 29.99})}<\/code><\/p>\n\n\n\n<p><code>{t('percent', {v: 0.42})}<\/code><\/p>\n\n\n\n<p><code>{t('customDate', {d: new Date()})}<br><\/code><br><code>);<br>}<\/code><\/p>\n\n\n\n<p>What\u2019s happening is that <code>{d, date, medium}<\/code> prints a nice locale-aware date (so it changes automatically between, say, English and French). <code>{p, number, ::currency\/USD}<\/code> renders currency with the right symbol and separators for the current locale while keeping USD. <code>{v, number, ::percent}<\/code> expects a fraction, so <code>0.42<\/code> becomes <code>42%<\/code>. And <code>::yyyyMMMdd<\/code> is a quick custom format (\u201cskeleton\u201d) without writing extra code.<\/p>\n\n\n\n<p>If you prefer to format values in code first use the formatter hook:<\/p>\n\n\n\n<p><code>import {useFormatter} from 'next-intl';<\/code><\/p>\n\n\n\n<p><code>export default function FancyFormats() {<br>const f = useFormatter();<\/code><\/p>\n\n\n\n<p><code>const price = f.number(2999.5, {style: 'currency', currency: 'EUR'});<br>const when = f.dateTime(new Date(), {dateStyle: 'long', timeStyle:<\/code><\/p>\n\n\n\n<p><code>'short'});<br>&nbsp; const ago &nbsp; = f.relativeTime(-90, 'minute'); \/\/ \"1 hour ago\"<\/code><br><br><code>return (<br>&nbsp; &nbsp; &lt;&gt;<br>&nbsp; &nbsp; &nbsp; &lt;p&gt;{price}&lt;\/p&gt;<br>&nbsp; &nbsp; &nbsp; &lt;p&gt;{when}&lt;\/p&gt;<br>&nbsp; &nbsp; &nbsp; &lt;p&gt;{ago}&lt;\/p&gt;<br>&nbsp; &nbsp; &lt;\/&gt;<br>&nbsp; );<br>}<\/code><\/p>\n\n\n\n<p>The whole idea is that if the value is part of a sentence, format it in the message so translators see the full context and if you\u2019re composing values first and then dropping them into the UI, use <code>useFormatter<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Managing translations with POEditor<\/h2>\n\n\n\n<p>Local JSON is fine while you\u2019re building, but once you have more than one language or more than one person, you want a place to manage strings without digging through files. That\u2019s what POEditor gives you.<\/p>\n\n\n\n<p>The flow is simple, you push your source JSON (usually English) to POEditor, translate there, then pull down JSON per locale and drop it back into <code>\/messages<\/code>.<\/p>\n\n\n\n<p>To get started, export your JSON translation, then import them to your POEditor project.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/screenshot-articol-1024x576.jpg\" alt=\"import terms poeditor\" class=\"wp-image-6866\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/screenshot-articol-1024x576.jpg 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/screenshot-articol-300x169.jpg 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/screenshot-articol-768x432.jpg 768w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/screenshot-articol-1536x864.jpg 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/screenshot-articol.jpg 1550w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>You can do this manually through the dashboard as shown above, or automate it with the <a href=\"\/docs\/api\">POEditor API<\/a>. Here\u2019s what an upload request looks like using <code>curl<\/code>:<\/p>\n\n\n\n<p><code>curl -X POST https:\/\/api.poeditor.com\/v2\/projects\/upload \\<br>&nbsp; -F api_token=\"YOUR_API_TOKEN\" \\<br>&nbsp; -F id=\"YOUR_PROJECT_ID\" \\<br>&nbsp; -F updating=\"terms_translations\" \\<br>&nbsp; -F file=@\"messages\/en.json\"<\/code><\/p>\n\n\n\n<p>This imports your translation keys and values into POEditor so you can start translating immediately. See the <a href=\"\/docs\/api#projects_upload\">upload docs<\/a> for everything you can tweak.<\/p>\n\n\n\n<p>Once your translations are done, you can <a href=\"\/docs\/api#projects_export\">export updated files<\/a> using their API too. For example, to download the French version:<\/p>\n\n\n\n<p><code>curl -X POST https:\/\/api.poeditor.com\/v2\/projects\/export \\<br>&nbsp; -d api_token=\"YOUR_API_TOKEN\" \\<br>&nbsp; -d id=\"YOUR_PROJECT_ID\" \\<br>&nbsp; -d language=\"fr\" \\<br>  -d type=\"json\"<\/code><\/p>\n\n\n\n<p>That returns a temporary URL. Download it and save as messages\/fr.json (or whatever locale you\u2019re adding). You can automate this step as part of a deploy or CI workflow by building it in with a simple script.<\/p>\n\n\n\n<p>You should also note that <a href=\"\/kb\/icu-message-syntax-plurals\">POEditor imports ICU messages<\/a> as-is and shows translators a small helper for plurals.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"447\" src=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-2-1024x447.png\" alt=\"\" class=\"wp-image-6867\" srcset=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-2-1024x447.png 1024w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-2-300x131.png 300w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-2-768x335.png 768w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-2-1536x670.png 1536w, https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/image-2.png 1579w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>So the same strings you wrote for <code>next-intl<\/code> go straight in with no special casing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping up<\/h2>\n\n\n\n<p>Localization always starts small, but it never stays that way. With <code>next-intl<\/code>, you\u2019ve got a clean path in App Router and Pages Router.<\/p>\n\n\n\n<p>And when you\u2019re ready to collaborate, POEditor is a translation management tool that helps you stay organized and collaborate easily.<\/p>\n\n\n\n<p>That\u2019s it! You\u2019ve got everything you need to ship a Next.js app that feels native.<\/p>\n\n\n<div class=\"call-action my-4 d-flex justify-content-between align-items-md-center gap-4 flex-column flex-lg-row\"><div><h3 class=\"fs-4\">Ready to power up localization?<\/h3><span class=\"fs-6\">Subscribe to the POEditor platform today!<\/span><\/div><a class=\"btn btn-b-primary d-flex align-items-center justify-content-center px-4 py-3 flex-shrink-0\" \n\t\t\t\t\thref=\"https:\/\/poeditor.com\/pricing\/?utm_source=blog&#038;utm_medium=btn&#038;utm_campaign=cta_pricing\">See pricing<\/a><\/div>\n\n\n\n<p><br><br><\/p>\n\n\n\n<p><br><br><\/p>\n\n\n\n<p><br><br><br><\/p>\n\n\n\n<p><br><br><br><br><\/p>\n\n\n\n<p><br><\/p>\n\n\n\n<p><br><br><\/p>\n\n\n\n<p><br><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Next.js is one of the popular ways to ship React apps today. It\u2019s fast, it scales, and it\u2019s straightforward. A lot of us use it for documentation sites, dashboards, storefronts, and marketing pages because it covers both developer ergonomics and production needs. When you\u2019re building for more than one market, it\u2019s expected that your app [&hellip;]<\/p>\n","protected":false},"author":9,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-6851","post","type-post","status-publish","format-standard","hentry","category-tutorials"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.4 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Next.js i18n with next-intl: A comprehensive guide - POEditor Blog<\/title>\n<meta name=\"description\" content=\"Next.js i18n made easy. Learn how to add internationalization, localization, and translations to your Next.js project for a global audience.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/poeditor.com\/blog\/next-js-i18n\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Next.js i18n with next-intl: A comprehensive guide - POEditor Blog\" \/>\n<meta property=\"og:description\" content=\"Next.js i18n made easy. Learn how to add internationalization, localization, and translations to your Next.js project for a global audience.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/poeditor.com\/blog\/next-js-i18n\/\" \/>\n<meta property=\"og:site_name\" content=\"POEditor Blog\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/POEditor\" \/>\n<meta property=\"article:published_time\" content=\"2025-10-20T06:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-04-20T12:51:04+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1776\" \/>\n\t<meta property=\"og:image:height\" content=\"884\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Joel Olawanle\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@poeditor\" \/>\n<meta name=\"twitter:site\" content=\"@poeditor\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Joel Olawanle\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"10 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/\"},\"author\":{\"name\":\"Joel Olawanle\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/person\\\/7c32041b74b3e09061cabde6e194862b\"},\"headline\":\"Next.js i18n with next-intl: A comprehensive guide\",\"datePublished\":\"2025-10-20T06:00:00+00:00\",\"dateModified\":\"2026-04-20T12:51:04+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/\"},\"wordCount\":1987,\"publisher\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2025\\\/10\\\/next.js-i18n-1024x510.png\",\"articleSection\":[\"Tutorials\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/\",\"name\":\"Next.js i18n with next-intl: A comprehensive guide - POEditor Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2025\\\/10\\\/next.js-i18n-1024x510.png\",\"datePublished\":\"2025-10-20T06:00:00+00:00\",\"dateModified\":\"2026-04-20T12:51:04+00:00\",\"description\":\"Next.js i18n made easy. Learn how to add internationalization, localization, and translations to your Next.js project for a global audience.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/#primaryimage\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2025\\\/10\\\/next.js-i18n.png\",\"contentUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2025\\\/10\\\/next.js-i18n.png\",\"width\":1776,\"height\":884,\"caption\":\"next.js i18n\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/next-js-i18n\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Next.js i18n with next-intl: A comprehensive guide\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/\",\"name\":\"POEditor Blog\",\"description\":\"All about translation and localization management\",\"publisher\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#organization\",\"name\":\"POEditor\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/logo_head_512_transparent.png\",\"contentUrl\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/wp-content\\\/uploads\\\/2019\\\/11\\\/logo_head_512_transparent.png\",\"width\":512,\"height\":512,\"caption\":\"POEditor\"},\"image\":{\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/POEditor\",\"https:\\\/\\\/x.com\\\/poeditor\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/poeditor\\\/\",\"https:\\\/\\\/www.youtube.com\\\/channel\\\/UCXAk1u8N49VRMAqNneENCFA\"]},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/#\\\/schema\\\/person\\\/7c32041b74b3e09061cabde6e194862b\",\"name\":\"Joel Olawanle\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/30f391dce10587a611158f3f46b79cabfc72e00d891bf7ae84dd80cec0a6d71b?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/30f391dce10587a611158f3f46b79cabfc72e00d891bf7ae84dd80cec0a6d71b?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/30f391dce10587a611158f3f46b79cabfc72e00d891bf7ae84dd80cec0a6d71b?s=96&d=mm&r=g\",\"caption\":\"Joel Olawanle\"},\"url\":\"https:\\\/\\\/poeditor.com\\\/blog\\\/author\\\/joel-olawanle\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Next.js i18n with next-intl: A comprehensive guide - POEditor Blog","description":"Next.js i18n made easy. Learn how to add internationalization, localization, and translations to your Next.js project for a global audience.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/poeditor.com\/blog\/next-js-i18n\/","og_locale":"en_US","og_type":"article","og_title":"Next.js i18n with next-intl: A comprehensive guide - POEditor Blog","og_description":"Next.js i18n made easy. Learn how to add internationalization, localization, and translations to your Next.js project for a global audience.","og_url":"https:\/\/poeditor.com\/blog\/next-js-i18n\/","og_site_name":"POEditor Blog","article_publisher":"https:\/\/www.facebook.com\/POEditor","article_published_time":"2025-10-20T06:00:00+00:00","article_modified_time":"2026-04-20T12:51:04+00:00","og_image":[{"width":1776,"height":884,"url":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n.png","type":"image\/png"}],"author":"Joel Olawanle","twitter_card":"summary_large_image","twitter_creator":"@poeditor","twitter_site":"@poeditor","twitter_misc":{"Written by":"Joel Olawanle","Est. reading time":"10 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/poeditor.com\/blog\/next-js-i18n\/#article","isPartOf":{"@id":"https:\/\/poeditor.com\/blog\/next-js-i18n\/"},"author":{"name":"Joel Olawanle","@id":"https:\/\/poeditor.com\/blog\/#\/schema\/person\/7c32041b74b3e09061cabde6e194862b"},"headline":"Next.js i18n with next-intl: A comprehensive guide","datePublished":"2025-10-20T06:00:00+00:00","dateModified":"2026-04-20T12:51:04+00:00","mainEntityOfPage":{"@id":"https:\/\/poeditor.com\/blog\/next-js-i18n\/"},"wordCount":1987,"publisher":{"@id":"https:\/\/poeditor.com\/blog\/#organization"},"image":{"@id":"https:\/\/poeditor.com\/blog\/next-js-i18n\/#primaryimage"},"thumbnailUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n-1024x510.png","articleSection":["Tutorials"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/poeditor.com\/blog\/next-js-i18n\/","url":"https:\/\/poeditor.com\/blog\/next-js-i18n\/","name":"Next.js i18n with next-intl: A comprehensive guide - POEditor Blog","isPartOf":{"@id":"https:\/\/poeditor.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/poeditor.com\/blog\/next-js-i18n\/#primaryimage"},"image":{"@id":"https:\/\/poeditor.com\/blog\/next-js-i18n\/#primaryimage"},"thumbnailUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n-1024x510.png","datePublished":"2025-10-20T06:00:00+00:00","dateModified":"2026-04-20T12:51:04+00:00","description":"Next.js i18n made easy. Learn how to add internationalization, localization, and translations to your Next.js project for a global audience.","breadcrumb":{"@id":"https:\/\/poeditor.com\/blog\/next-js-i18n\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/poeditor.com\/blog\/next-js-i18n\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/poeditor.com\/blog\/next-js-i18n\/#primaryimage","url":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n.png","contentUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2025\/10\/next.js-i18n.png","width":1776,"height":884,"caption":"next.js i18n"},{"@type":"BreadcrumbList","@id":"https:\/\/poeditor.com\/blog\/next-js-i18n\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/poeditor.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Next.js i18n with next-intl: A comprehensive guide"}]},{"@type":"WebSite","@id":"https:\/\/poeditor.com\/blog\/#website","url":"https:\/\/poeditor.com\/blog\/","name":"POEditor Blog","description":"All about translation and localization management","publisher":{"@id":"https:\/\/poeditor.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/poeditor.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/poeditor.com\/blog\/#organization","name":"POEditor","url":"https:\/\/poeditor.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/poeditor.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2019\/11\/logo_head_512_transparent.png","contentUrl":"https:\/\/poeditor.com\/blog\/wp-content\/uploads\/2019\/11\/logo_head_512_transparent.png","width":512,"height":512,"caption":"POEditor"},"image":{"@id":"https:\/\/poeditor.com\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/POEditor","https:\/\/x.com\/poeditor","https:\/\/www.linkedin.com\/company\/poeditor\/","https:\/\/www.youtube.com\/channel\/UCXAk1u8N49VRMAqNneENCFA"]},{"@type":"Person","@id":"https:\/\/poeditor.com\/blog\/#\/schema\/person\/7c32041b74b3e09061cabde6e194862b","name":"Joel Olawanle","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/30f391dce10587a611158f3f46b79cabfc72e00d891bf7ae84dd80cec0a6d71b?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/30f391dce10587a611158f3f46b79cabfc72e00d891bf7ae84dd80cec0a6d71b?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/30f391dce10587a611158f3f46b79cabfc72e00d891bf7ae84dd80cec0a6d71b?s=96&d=mm&r=g","caption":"Joel Olawanle"},"url":"https:\/\/poeditor.com\/blog\/author\/joel-olawanle\/"}]}},"_links":{"self":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts\/6851","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/users\/9"}],"replies":[{"embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/comments?post=6851"}],"version-history":[{"count":17,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts\/6851\/revisions"}],"predecessor-version":[{"id":6942,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/posts\/6851\/revisions\/6942"}],"wp:attachment":[{"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/media?parent=6851"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/categories?post=6851"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/poeditor.com\/blog\/wp-json\/wp\/v2\/tags?post=6851"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}