When next-i18next was first released, Next.js only had the Pages Router. The library became the go-to way to add i18n to Next.js apps, wrapping _app with appWithTranslation, calling serverSideTranslations in getStaticProps, and letting Next.js handle locale routing.
Then Next.js introduced the App Router with Server Components, and our advice changed: "You don't need next-i18next anymore for App Router... just use i18next and react-i18next directly." We even published a streamlined setup guide showing how to wire it all up manually.
But honestly? That manual wiring was boilerplate. Every project needed the same middleware, the same getT helper, the same I18nProvider setup. And projects migrating from Pages Router to App Router, or running both routers side by side, had no clean path.
next-i18next v16 fixes all of that.
What's New
-
getT()for Server Components — async, namespace-aware, type-safe -
useT()for Client Components — reads language from URL params automatically -
createProxy()for language detection and routing — edge-safe, zero Node.js dependencies -
I18nProviderfor client hydration — with lazy-loading support for additional namespaces -
basePathscoping — run both App Router and Pages Router in the same app - No-locale-path mode — cookie-based language without URL prefixes
-
Full Pages Router compatibility —
appWithTranslationandserverSideTranslationsvianext-i18next/pages
Already using the manual i18next setup from our 2025 blog post? Migration is straightforward: replace your custom helpers with
next-i18next/serverandnext-i18next/client, and your middleware withcreateProxy().
App Router Setup
1. Install
npm install next-i18next i18next react-i18next
2. Translation Files
Place your translations in your project. The simplest approach uses a resourceLoader with dynamic imports:
app/i18n/locales/en/common.json
app/i18n/locales/en/home.json
app/i18n/locales/de/common.json
app/i18n/locales/de/home.json
3. Configuration
Create i18n.config.ts:
import type { I18nConfig } from 'next-i18next/proxy'
const i18nConfig: I18nConfig = {
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
defaultNS: 'common',
ns: ['common', 'home'],
resourceLoader: (language, namespace) =>
import(`./app/i18n/locales/${language}/${namespace}.json`),
}
export default i18nConfig
The resourceLoader uses dynamic imports to load translation files, they get bundled at build time and loaded on demand. If you prefer, you can also place files in public/locales/ and skip the resourceLoader (the default filesystem loader will pick them up).
4. Proxy (Language Detection & Routing)
Create proxy.ts at your project root. Next.js 16 renamed the old middleware.ts convention to proxy.ts:
import { createProxy } from 'next-i18next/proxy'
import i18nConfig from './i18n.config'
export const proxy = createProxy(i18nConfig)
export const config = {
matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js|site.webmanifest).*)'],
}
The proxy handles:
- Language detection from cookie → Accept-Language header → fallback
- Redirecting bare URLs to locale-prefixed paths (
/about→/en/about) - Setting a custom header for Server Components to read the current language
Still on Next.js 14 or 15? Use createMiddleware from next-i18next/middleware in your middleware.ts: same API, just the old file convention.
5. Root Layout
app/[lng]/layout.tsx:
import { initServerI18next, getT, getResources, generateI18nStaticParams } from 'next-i18next/server'
import { I18nProvider } from 'next-i18next/client'
import i18nConfig from '../../i18n.config'
initServerI18next(i18nConfig)
export async function generateStaticParams() {
return generateI18nStaticParams()
}
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ lng: string }>
}) {
const { lng } = await params
const { i18n } = await getT()
const resources = getResources(i18n)
return (
<html lang={lng}>
<body>
<I18nProvider language={lng} resources={resources}>
{children}
</I18nProvider>
</body>
</html>
)
}
A few things to note:
-
initServerI18next(config)stores the configuration, call it once at module scope -
getResources(i18n)serializes the loaded translations so the client can hydrate without re-fetching -
I18nProvidercreates a client-side i18next instance hydrated with those resources
6. Server Components
import { getT } from 'next-i18next/server'
export default async function Page() {
const { t } = await getT('home')
return <h1>{t('title')}</h1>
}
getT() reads the language from the proxy-set header, loads the requested namespace if needed, and returns a namespace-typed t function plus the resolved lng. No prop drilling, no manual language passing.
For generateMetadata:
export async function generateMetadata() {
const { t } = await getT('home')
return { title: t('meta_title') }
}
For the Trans component in Server Components, use react-i18next/TransWithoutContext and pass both t and i18n:
import { Trans } from 'react-i18next/TransWithoutContext'
import { getT } from 'next-i18next/server'
export default async function Page() {
const { t, i18n } = await getT()
return (
<Trans t={t} i18n={i18n} i18nKey="welcome">
Welcome to <strong>next-i18next</strong>
</Trans>
)
}
7. Client Components
'use client'
import { useT } from 'next-i18next/client'
export default function Counter() {
const { t } = useT('home')
return <button>{t('click_me')}</button>
}
useT reads the language from URL params ([lng] or [locale]) and keeps the i18next instance in sync. In no-locale-path mode (where there are no URL params), it uses the language set by I18nProvider.
No-Locale-Path Mode
Not every project wants /en/about and /de/about. Some prefer clean URLs with cookie-based language:
const i18nConfig: I18nConfig = {
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
localeInPath: false,
resourceLoader: (language, namespace) =>
import(`./app/i18n/locales/${language}/${namespace}.json`),
}
Routes live directly under app/ (no [lng] segment). Language switching uses the useChangeLanguage hook:
'use client'
import { useChangeLanguage } from 'next-i18next/client'
export function LanguageSwitcher() {
const changeLanguage = useChangeLanguage()
return <button onClick={() => changeLanguage('de')}>Deutsch</button>
}
This sets the cookie, updates the i18next instance, and triggers a server re-render — all in one call.
Mixed Router: App Router + Pages Router Together
This is where v16 really shines. Many real-world projects have an existing Pages Router app and want to start building new features with the App Router, without rewriting everything.
The basePath option scopes the proxy to a specific URL prefix:
// i18n.config.ts (App Router)
const i18nConfig: I18nConfig = {
...shared,
basePath: '/app-router',
resourceLoader: (language, namespace) =>
import(`./public/locales/${language}/${namespace}.json`),
}
// proxy.ts
import { createProxy } from 'next-i18next/proxy'
import i18nConfig from './i18n.config'
export const proxy = createProxy(i18nConfig)
export const config = {
matcher: ['/app-router/:path*'],
}
The proxy only handles /app-router/* routes. Pages Router pages at / continue using Next.js built-in i18n routing with appWithTranslation and serverSideTranslations from next-i18next/pages. Both routers share the same translation files from public/locales/.
Pages Router (Unchanged)
If you're on the Pages Router and upgrading from v15, the only change is the import path:
// Before (v15)
import { appWithTranslation, useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// After (v16)
import { appWithTranslation, useTranslation } from 'next-i18next/pages'
import { serverSideTranslations } from 'next-i18next/pages/serverSideTranslations'
Everything else — next-i18next.config.js, getStaticProps, getServerSideProps, custom backends — works exactly the same.
Custom Backends
next-i18next supports any i18next backend plugin. When you provide a custom backend via the use option, the default resource loader is skipped automatically.
On the server side, custom backends work through the shared singleton instance, translations are fetched once and cached:
import { defineConfig } from 'next-i18next'
import HttpBackend from 'i18next-http-backend'
export default defineConfig({
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
use: [HttpBackend],
i18nextOptions: {
backend: {
loadPath: 'https://cdn.example.com/locales/{{lng}}/{{ns}}.json',
},
},
})
On the client side, pass backends through I18nProvider. Since backend classes are functions (not serializable), import them in a 'use client' component:
'use client'
import { I18nProvider } from 'next-i18next/client'
import HttpBackend from 'i18next-http-backend'
import ChainedBackend from 'i18next-chained-backend'
import LocalStorageBackend from 'i18next-localstorage-backend'
export function I18nProviderWithBackend({ children, language, resources, supportedLngs }) {
return (
<I18nProvider
language={language}
resources={resources}
supportedLngs={supportedLngs}
use={[ChainedBackend]}
i18nextOptions={{
backend: {
backends: [LocalStorageBackend, HttpBackend],
backendOptions: [
{ expirationTime: 60 * 60 * 1000 }, // 1 hour localStorage cache
{}, // HttpBackend defaults
],
},
}}
>
{children}
</I18nProvider>
)
}
This gives you server-rendered translations at initial load, with client-side lazy-loading of additional namespaces through the chained backend. The i18next-http-backend example demonstrates this pattern with both App Router and Pages Router in a single project.
A Note on Serverless Environments
On the server, next-i18next uses a module-level singleton i18next instance. Translations are loaded once and reused across all subsequent requests within the same process — great for performance, and custom backends like i18next-http-backend or i18next-locize-backend benefit from this since they don't re-fetch on every request.
However, in serverless environments (Vercel Serverless Functions, AWS Lambda, Google Cloud Functions, etc.), the module-level cache only lives as long as the warm function instance. Each cold start re-initializes the singleton and re-fetches translations.
For serverless deployments, we recommend one of these approaches:
1. Bundle translations at build time (recommended for most projects):
Use resourceLoader with dynamic imports: translations are bundled into the deployment artifact at build time, so there's zero runtime fetching:
export default defineConfig({
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
resourceLoader: (language, namespace) =>
import(`./app/i18n/locales/${language}/${namespace}.json`),
})
2. Download translations in CI/CD:
Download translations from your TMS before building (e.g., via locize-cli or an API). This bundles them with your deployment and avoids runtime HTTP requests entirely.
3. Use HTTP backends with caching:
If you must load translations at runtime, combine i18next-http-backend with i18next-chained-backend and i18next-localstorage-backend on the client side for fast subsequent loads. On the server side, the singleton cache will keep translations warm as long as the function instance is alive.
Avoid relying on HTTP-based backends as the sole translation source in serverless, each cold start adds latency and a potential point of failure.
Bonus
Connect to an awesome translation management system and manage your translations outside of your code.
Let's synchronize the translation files with Locize. This can be done on-demand or on the CI server or before deploying the app.
What to do to reach this step:
- In Locize: signup at https://www.locize.app/register and login
- In Locize: create a new project
- Install the locize-cli (
npm i locize-cli) or use the Locize commands built into i18next-cli (npm i -D i18next-cli) - In Locize: add all your additional languages (this can also be done via API or using the migrate command of the locize-cli)
Syncing translations
With locize-cli:
Use locize download to download the published Locize translations to your local repository before bundling your app. Or use locize sync to synchronize bidirectionally.
With i18next-cli:
i18next-cli is a unified toolchain that goes beyond syncing — it also handles key extraction, type generation, locale synchronization, and linting. Its built-in Locize integration wraps locize-cli commands:
npx i18next-cli locize-download # download translations from Locize
npx i18next-cli locize-sync # upload/sync translations to Locize
npx i18next-cli locize-migrate # migrate local translations to Locize
If your Locize credentials are missing, it will guide you through an interactive setup.
For projects that want to load translations directly from Locize at runtime, use i18next-locize-backend as a custom backend:
import { defineConfig } from 'next-i18next'
import LocizeBackend from 'i18next-locize-backend'
export default defineConfig({
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
use: [LocizeBackend],
i18nextOptions: {
backend: {
projectId: 'your-project-id',
},
},
})
On the server, the singleton instance caches translations, no re-fetching per request. On the client, combine with i18next-chained-backend and i18next-localstorage-backend for offline-first loading with automatic background refresh.
Wrapping Up
next-i18next v16 is a single package that handles every Next.js i18n scenario:
- App Router with Server Components and Client Components
-
Pages Router with the familiar
appWithTranslation/serverSideTranslationsAPI - Mixed setups where both routers coexist
- Custom backends for loading from CDN, API, or Locize
- Locale-in-path and no-locale-path modes
For the complete code, check out the examples on GitHub:
- App Router with locale-in-path
- App Router without locale in path
- Mixed App Router + Pages Router
- Pages Router
- With i18next-http-backend
Happy coding!

Top comments (0)