File: README.md
next-i18next
The easiest way to translate your Next.js apps.
Supports the App Router (Server Components, Client Components, middleware), the Pages Router, and mixed setups where both routers coexist.
If you already know i18next: next-i18next v16 is a thin layer on top of i18next and react-i18next that handles the Next.js-specific wiring — middleware, server/client split, resource hydration — so you don't have to.
What's new in v16
- App Router support:
getT()for Server Components,useT()for Client Components,createProxy()for language detection and routing - Locale-in-path (
/en/about) and no-locale-path (cookie-based) modes - Mixed Router: Use both App Router and Pages Router in the same app with
basePathscoping - Custom Backends:
i18next-http-backend,i18next-locize-backend,i18next-chained-backend, etc. - Edge-safe Proxy: Zero Node.js dependencies in the proxy/middleware path
- Pages Router: Existing
appWithTranslation/serverSideTranslationsAPI preserved undernext-i18next/pages
Advice:
If you don't like to manage your translation files manually or are simply looking for a better management solution, take a look at i18next-locize-backend. The i18next backend plugin for 🌐 Locize ☁️ — built by the same team behind next-i18next, with CDN delivery (works great on Vercel/serverless), AI translation, and no redeploys for copy changes.
Starting from a Next.js app with hardcoded strings? Run npx i18next-cli localize — one command that wraps strings in t(), extracts keys, connects to Locize and AI-translates your app (review the diff for server components). See the launch post.
Table of Contents
- App Router Setup
- No-Locale-Path Mode
- ICU and MF2 with the App Router
- Mixed Router Setup
- Pages Router Setup
- Custom i18next Backends
- API Reference
- Examples
- Migration from v15
App Router Setup
1. Install
npm install next-i18next i18next react-i18next2. Translation files
Place JSON translation files in your project. There are two common patterns:
In public/locales/ (served statically, works with default config — local/traditional hosting only):
public/locales/en/common.json
public/locales/en/home.json
public/locales/de/common.json
public/locales/de/home.jsonServerless platforms (Vercel, AWS Lambda, etc.): Files in
public/are served via CDN but are not available on the filesystem at runtime. UseresourceLoaderwith dynamic imports instead (see below).
In app/i18n/locales/ (bundled via dynamic imports, requires resourceLoader — works everywhere including serverless):
app/i18n/locales/en/common.json
app/i18n/locales/de/common.json3. Configuration
Create a config file (e.g., i18n.config.ts):
import type { I18nConfig } from 'next-i18next/proxy'
const i18nConfig: I18nConfig = {
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
defaultNS: 'common',
ns: ['common', 'home'],
// Recommended: works on all platforms including Vercel/serverless
resourceLoader: (language, namespace) =>
import(`./app/i18n/locales/${language}/${namespace}.json`),
}
export default i18nConfigThe resourceLoader uses dynamic import() which the bundler can trace, ensuring translation files are included in the serverless function bundle. If you prefer to keep translations in public/locales/ and are not deploying to a serverless platform, you can omit resourceLoader — next-i18next will read from the filesystem at runtime.
Tip: Import
I18nConfigfromnext-i18next/proxy(not fromnext-i18next) to keep the config file Edge-safe.
Dev tip — hot-reloading translations: set
reloadOnPrerender: process.env.NODE_ENV === 'development'in your config to refetch translations on every render in dev so edits to locale files appear without restartingnext dev. The flag is automatically a no-op in production, so it is safe to keep in your committed config — custom backends (HTTP, locize, chained) won't be hit per-request in production builds.Caveat with
import()-basedresourceLoader: dynamicimport()of JSON is cached at the bundler level and is not reliably re-invalidated by Turbopack/Webpack HMR after the first edit, so hot-reload can stall after one change. For full hot-reload during development, gate your loader so dev usesfs.readFileand production keeps bundler-traceableimport():tsconst resourceLoader: I18nConfig['resourceLoader'] = process.env.NODE_ENV === 'development' ? async (lng, ns) => { const fs = await import('fs/promises') const path = await import('path') const content = await fs.readFile( path.resolve(process.cwd(), `app/i18n/locales/${lng}/${ns}.json`), 'utf-8' ) return JSON.parse(content) } : (lng, ns) => import(`./app/i18n/locales/${lng}/${ns}.json`)Pages Router and the App Router default backend already use
fsand are unaffected.
4. Proxy
Create proxy.ts at your project root (Next.js 16+ replaces middleware.ts with 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).*)'],
}Note:
createMiddlewarefromnext-i18next/middlewareis still available for projects on Next.js < 16.
The proxy:
- Detects language from cookie > Accept-Language header > fallback
- Redirects bare URLs to locale-prefixed paths (e.g.,
/about->/en/about) - Sets a custom header (
x-i18next-current-language) for Server Components - Persists the language in a cookie, but only when it changed: a cookie written by the proxy counts as a modified cookie for Next, and a modified cookie makes every Server Action revalidate and refetch the page. Set
persistCookie: falseif another system owns that cookie, andcookieOptionsto scope it (e.g.{ domain: '.example.com', secure: true })
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>
)
}Key points:
initServerI18next(config)— call once at module scope in the root layoutgetResources(i18n)— serializes loaded translations for client hydrationI18nProvider— wraps children so client components can useuseT()
Initialization without ordering assumptions
initServerI18next(config) is a side effect: it has to run in the process before the first getT(). Module scope of the root layout works because Next loads the root layout before anything below it, but nothing enforces it if a second copy of next-i18next/server calls getT() first, for example from a Route Handler, which is bundled in its own layer. If you would rather not depend on evaluation order at all, bind the config once and import from that module everywhere:
// i18n.server.ts
import { createServerI18next } from 'next-i18next/server'
import i18nConfig from './i18n.config'
export const { getT, getResources, generateI18nStaticParams } = createServerI18next(i18nConfig)// any Server Component, layout or generateMetadata
import { getT } from '@/i18n.server'Every caller then imports the module that holds the config, so there is no initialization step and no order to get right. Call createServerI18next once at module scope: each call owns its own shared i18next instance.
6. Server Components
// app/[lng]/page.tsx
import { getT } from 'next-i18next/server'
export default async function Home() {
const { t } = await getT('home')
return <h1>{t('title')}</h1>
}
export async function generateMetadata() {
const { t } = await getT('home')
return { title: t('meta_title') }
}On Next.js 16.3+ getT() resolves the language from the [lng] root param (via next/root-params) before falling back to the proxy header and cookie. Reading a root param does not opt the route out of static prerendering, so getT() is safe in prerendered routes (including with cacheComponents: true). Route Handlers and Server Actions have no root params — pass { lng } explicitly there.
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 works in both locale-in-path (/en/about) and no-locale-path modes. I18nProvider follows the [lng] or [locale] route param when there is one; useT itself does not subscribe to the URL, so rewriting it (history.replaceState, ?page=) does not re-render its callers.
For the Trans component in Client Components:
'use client'
import { Trans, useT } from 'next-i18next/client'
export default function Greeting() {
const { t } = useT()
return <Trans t={t} i18nKey="greeting">Hello <strong>world</strong></Trans>
}8. Language Switching (locale-in-path)
When the locale is part of the URL path (e.g., /en/about → /de/about), switch languages by navigating to the new locale prefix:
'use client'
import { usePathname, useRouter } from 'next/navigation'
export function LanguageSwitcher({ supportedLngs }: { supportedLngs: string[] }) {
const pathname = usePathname()
const router = useRouter()
const switchLocale = (locale: string) => {
const segments = pathname.split('/')
segments[1] = locale
router.push(segments.join('/'))
}
return (
<div>
{supportedLngs.map((lng) => (
<button key={lng} onClick={() => switchLocale(lng)}>{lng}</button>
))}
</div>
)
}For the no-locale-path mode (cookie-based), see useChangeLanguage below.
Hide Default Locale
If you want clean URLs for the default language while keeping locale prefixes for other languages, set hideDefaultLocale: true:
const i18nConfig: I18nConfig = {
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
hideDefaultLocale: true,
}In this mode:
/aboutserves the default language (English) — no prefix needed/de/aboutserves German — non-default locales keep their prefix/en/aboutautomatically redirects to/about(canonical clean URL)- The
[lng]folder structure stays the same — the proxy rewrites internally
No-Locale-Path Mode
If you prefer clean URLs without a locale prefix for all languages (e.g., /about instead of /en/about), set localeInPath: false:
const i18nConfig: I18nConfig = {
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
localeInPath: false,
resourceLoader: (language, namespace) =>
import(`./app/i18n/locales/${language}/${namespace}.json`),
}In this mode:
- Routes live directly under
app/(no[lng]segment) - The middleware detects language from cookies / Accept-Language, sets the header, but does not redirect
- Server Components use
getT()as usual (language is read from the header) - Client Components use
useT()as usual (language comes fromI18nProvider) - Use
useChangeLanguage()for language switching (updates cookie + triggers server re-render):
'use client'
import { useChangeLanguage } from 'next-i18next/client'
export function LanguageSwitcher() {
const changeLanguage = useChangeLanguage()
return (
<div>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('de')}>Deutsch</button>
</div>
)
}The root layout reads the language from getT() instead of URL params:
// app/layout.tsx (no [lng] segment)
export default async function RootLayout({ children }) {
const { i18n, lng } = await getT()
const resources = getResources(i18n)
return (
<I18nProvider language={lng} resources={resources}>
<html lang={lng}>
<body>{children}</body>
</html>
</I18nProvider>
)
}See examples/app-router-no-locale-path for a complete example.
Internal Locale Mode (clean URLs + static prerendering)
localeInPath: false gives clean URLs, but the language then comes from headers()/cookies(), which makes every route dynamic. If you need clean URLs and static prerendering per locale (e.g. with Next.js Cache Components, cacheComponents: true), set localeInPath: 'internal':
const i18nConfig: I18nConfig = {
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
localeInPath: 'internal',
localeParamName: 'locale', // only if your segment is app/[locale] instead of app/[lng]
}In this mode:
- Routes live under
app/[lng]/exactly like the default mode, sogenerateI18nStaticParams()prerenders every locale - The proxy detects the language (cookie > Accept-Language > fallback) and rewrites
/aboutto/de/aboutinternally — the public URL never shows a locale - Explicit locale paths (
/de/about) are served as-is, like in the default mode — the proxy never redirects them, so<Link>prefetches cannot flip the language cookie. Your own links should be clean (/about) getT()reads the language from the[lng]root param (Next.js 16.3+), so it does not opt the route out of prerendering- Use
useChangeLanguage()for language switching, as in no-locale-path mode hideDefaultLocalehas no effect here
Loading Languages On Demand
By default the shared server instance preloads every supported language at startup, so getResources(i18n) can serialize any of them. With many locales and namespaces that is a lot of resident memory. Pass i18nextOptions: { preload: [] } to load each language on its first request instead:
const i18nConfig: I18nConfig = {
supportedLngs: ['en', 'de', 'fr', 'it'],
fallbackLng: 'en',
i18nextOptions: { preload: [] },
}The store then only holds languages that have been requested so far, so pass [lng, fallbackLng] to getResources rather than relying on everything being present.
On the client the language then changes in two steps: useChangeLanguage writes the cookie and calls router.refresh(), and I18nProvider switches as soon as that re-render delivers the new language's resources. Client Components keep working without any extra setup — pass supportedLngs to I18nProvider only if you want to restrict which languages it accepts (unset, it accepts any language the server sends).
ICU and MF2 with the App Router
To write your translations in ICU MessageFormat (i18next-icu) or in Unicode MessageFormat 2 (i18next-mf2) instead of the i18next JSON format, register the plugin in the server config and import I18nProvider from next-i18next/icu (or next-i18next/mf2). Both plugins replace i18next's own plurals, context keys and {{interpolation}}; Using ICU or MF2 lists what keeps working and what does not.
Install
npm install i18next-icu intl-messageformat
# or, for MF2
npm install i18next-mf2Both are optional peer dependencies of next-i18next. Apps that never import next-i18next/icu or next-i18next/mf2 do not resolve or bundle them.
Configuration
Add the plugin to use, so getT() formats the messages on the server:
// i18n.config.ts
import type { I18nConfig } from 'next-i18next/proxy'
import ICU from 'i18next-icu'
const i18nConfig: I18nConfig = {
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
resourceLoader: (language, namespace) =>
import(`./app/i18n/locales/${language}/${namespace}.json`),
use: [ICU],
}
export default i18nConfigapp/i18n/locales/en/common.json:
{
"photos": "You have {count, plural, =0 {no photos} one {# photo} other {# photos}}.",
"order": "{status, select, pending {Your order is being prepared.} shipped {Your order is on its way.} other {Your order status is unknown.}}",
"welcome": "Welcome back, <0>{name}</0>!"
}In the root layout, import I18nProvider from next-i18next/icu instead of next-i18next/client. It takes the same props and registers i18next-icu on the client instance:
// app/[lng]/layout.tsx
import { initServerI18next, getT, getResources, generateI18nStaticParams } from 'next-i18next/server'
import { I18nProvider } from 'next-i18next/icu' // or 'next-i18next/mf2'
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()
return (
<html lang={lng}>
<body>
<I18nProvider language={lng} resources={getResources(i18n)}>
{children}
</I18nProvider>
</body>
</html>
)
}A Server Component can not pass a plugin class to a Client Component as a prop: `` in a layout fails the build with "Functions cannot be passed directly to Client Components". next-i18next/icu is a Client module that imports the plugin itself, so no 'use client' wrapper of your own is needed. useT, useChangeLanguage and Trans still come from next-i18next/client.
Notes:
- The client instance does not read the server config. Plugin options in
i18nextOptions.i18nFormat(for examplebidiIsolation: 'none'for MF2) have to be passed toI18nProvideras well, via itsi18nextOptionsprop, or the server and client render different text. - The proxy imports the same config, so the plugin ends up in the proxy bundle too (about 40 KB for i18next-icu). To keep it out, leave
useout of the shared config and add it where the server is initialized:initServerI18next({ ...i18nConfig, use: [ICU] }). - For MF2, put
MF2fromi18next-mf2intouseand importI18nProviderfromnext-i18next/mf2. Messages are written in MF2 syntax, e.g..input {$count :number} .match $count one {{You have {$count} photo.}} * {{You have {$count} photos.}}.
Server Components
// app/[lng]/page.tsx
import { getT } from 'next-i18next/server'
import { IcuTransWithoutContext } from 'react-i18next/IcuTransWithoutContext'
export default async function Page() {
const { t, i18n } = await getT()
return (
<main>
<p>{t('photos', { count: 5 })}</p>
<p>{t('order', { status: 'shipped' })}</p>
<p>
<IcuTransWithoutContext
i18nKey='welcome'
defaultTranslation='Welcome back, <0>{name}</0>!'
content={[{ type: 'strong' }]}
values={{ name: 'Ada' }}
t={t}
i18n={i18n}
/>
</p>
</main>
)
}IcuTransWithoutContext (react-i18next 17.0.21 or later) uses no React context, so it renders in Server Components. Pass the t from getT(), it is bound to the request's language. Trans from react-i18next/TransWithoutContext works as well, with values and components:
<Trans t={t} i18n={i18n} i18nKey='welcome' values={{ name: 'Ada' }} components={[<strong key='0' />]} />In Server Components, import both from these entry points rather than from the react-i18next root, which also loads React context.
Client Components
'use client'
import { useState } from 'react'
import { IcuTrans } from 'react-i18next'
import { useT } from 'next-i18next/client'
export function Photos() {
const { t } = useT()
const [count, setCount] = useState(0)
return (
<>
<p>{t('photos', { count })}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<IcuTrans
i18nKey='welcome'
defaultTranslation='Welcome back, <0>{name}</0>!'
content={[{ type: 'strong' }]}
values={{ name: 'Ada' }}
/>
</>
)
}TypeScript
With i18next 26.5.0 or later, set parseInterpolation: 'icu' (or 'mf2') and the options of t from getT and useT, and the values of IcuTrans and IcuTransWithoutContext (react-i18next 17.1.0 or later), are type-checked against the arguments of each message. The messages need literal types, which npx i18next-cli types generates (see i18next-cli below); with typeof a JSON import every message is a string and nothing is checked:
// @types/i18next.d.ts
import 'i18next'
import type Resources from './resources' // generated by `npx i18next-cli types`
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'common'
resources: Resources
parseInterpolation: 'icu' // or 'mf2'
}
}// photos: 'You have {count, plural, =0 {no photos} one {# photo} other {# photos}}.'
const { t } = await getT()
t('photos', { count: 5 }) // ok
t('photos', { count: 'five' }) // type error: count takes a number
t('photos') // type error: 'pass these arguments as t() options': { count: number; }With i18next 26.2.0 to 26.4.x, set parseInterpolation: false instead: the default type extractor reads {{variables}}, and ICU arguments nested in a plural (one {{count} row}) and MF2 variants (* {{You have {$count} photos.}}) contain double braces that are not i18next variables, so these t calls would not compile. Keys are still checked, the values you pass to t are not.
i18next-cli
i18next-cli 1.76.0 or later knows both formats. Set extract.messageFormat to 'icu' or 'mf2' in your i18next.config.ts:
import { defineConfig } from 'i18next-cli'
export default defineConfig({
locales: ['en', 'de'],
extract: {
input: ['app/**/*.{ts,tsx}'],
output: 'app/i18n/locales/{{language}}/{{namespace}}.json',
messageFormat: 'icu', // or 'mf2'
},
})extract then writes one key per message instead of _one/_other suffix keys and extracts IcuTrans and IcuTransWithoutContext, lint checks the syntax, the argument names and the plural categories of every language, and types writes the resources file with literal message types and parseInterpolation: 'icu' or 'mf2' (i18next-cli 1.77.0 with i18next 26.5.0 or later, else false) into a newly generated i18next.d.ts.
See examples/app-router-icu for a complete app with plurals, a select and rich text in Server and Client Components.
Mixed Router Setup (App Router + Pages Router)
For projects that use both routers, next-i18next supports a basePath option that scopes the App Router middleware to a specific URL prefix while the Pages Router uses Next.js built-in i18n routing for everything else.
Configuration
Create a shared config file for common settings:
// i18n.shared.js
module.exports = {
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
defaultNS: 'common',
ns: ['common', 'footer'],
}App Router config with basePath:
// i18n.config.ts
import type { I18nConfig } from 'next-i18next/proxy'
const shared = require('./i18n.shared.js')
const i18nConfig: I18nConfig = {
...shared,
basePath: '/app-router',
resourceLoader: (language, namespace) =>
import(`./public/locales/${language}/${namespace}.json`),
}
export default i18nConfigPages Router config:
// next-i18next.config.js
const shared = require('./i18n.shared.js')
module.exports = {
i18n: {
defaultLocale: shared.fallbackLng,
locales: shared.supportedLngs,
},
localePath:
typeof window === 'undefined'
? require('path').resolve('./public/locales')
: '/locales',
}Proxy/Middleware
With basePath: '/app-router', createProxy automatically:
- Skips any request not under
/app-router/...(letting Pages Router handle those) - Redirects
/app-router/pageto/app-router/en/page - Sets the language header for Server Components under that prefix
// 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*'],
}next.config.js
Include the Pages Router i18n config so Next.js handles locale routing for Pages:
const { i18n } = require('./next-i18next.config.js')
module.exports = {
i18n,
reactStrictMode: true,
}Directory structure
app/app-router/[locale]/layout.tsx -- App Router layout
app/app-router/[locale]/page.tsx -- App Router pages
pages/_app.tsx -- appWithTranslation
pages/index.tsx -- Pages Router pages
public/locales/en/common.json -- shared translation filesApp Router pages import from next-i18next/server and next-i18next/client.
Pages Router pages import from next-i18next/pages and next-i18next/pages/serverSideTranslations.
See examples/mixed-routers for a complete example.
Pages Router Setup
For projects using only the Pages Router, the familiar v15 API is available under next-i18next/pages:
// next-i18next.config.js
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'de'],
},
}// next.config.js
const { i18n } = require('./next-i18next.config.js')
module.exports = { i18n }// pages/_app.tsx
import { appWithTranslation } from 'next-i18next/pages'
const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />
export default appWithTranslation(MyApp)// pages/index.tsx
import { serverSideTranslations } from 'next-i18next/pages/serverSideTranslations'
import { useTranslation } from 'next-i18next/pages'
export const getStaticProps = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
})
export default function Home() {
const { t } = useTranslation('common')
return <h1>{t('title')}</h1>
}See examples/pages-router-simple, examples/pages-router-ssg, and examples/pages-router-auto-static-optimize for more.
Custom i18next Backends
next-i18next supports any i18next backend plugin for loading translations from an API, CDN, or services like Locize.
When a custom backend is provided via use, next-i18next will not add its default resource loader, giving you full control.
i18next-http-backend
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',
},
},
})i18next-locize-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',
apiKey: 'your-api-key', // only needed for saving missing keys
},
},
})i18next-chained-backend
For client-side caching with a remote fallback:
import { defineConfig } from 'next-i18next'
import ChainedBackend from 'i18next-chained-backend'
import HttpBackend from 'i18next-http-backend'
import LocalStorageBackend from 'i18next-localstorage-backend'
export default defineConfig({
supportedLngs: ['en', 'de'],
fallbackLng: 'en',
use: [ChainedBackend],
i18nextOptions: {
backend: {
backends: [LocalStorageBackend, HttpBackend],
backendOptions: [
{ expirationTime: 7 * 24 * 60 * 60 * 1000 },
{ loadPath: '/locales/{{lng}}/{{ns}}.json' },
],
},
},
})For the client-side I18nProvider, pass custom backend plugins via the use prop:
<I18nProvider
language={lng}
resources={resources}
use={[HttpBackend]}
i18nextOptions={{
backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
}}
>
{children}
</I18nProvider>Backends passed here are browser-only. I18nProvider is a Client Component, but the App Router also renders it on the server, once per request, so a backend attached there would run in Node on every render: fetching per render, and, for backends that refresh on a timer (i18next-locize-backend defaults reloadInterval to 1 hour whenever window is undefined), leaving a live timer behind on each one. next-i18next therefore skips backend plugins during the server pass and renders from resources; the browser instance keeps the backend and does the fetching. Non-backend plugins (detectors, post-processors) are applied in both passes.
If you want Client Components to load namespaces that are not in resources on the server as well, so only the namespaces a route renders end up in the HTML and the rest load on demand, opt in with ssrBackend. It applies the backends from use during the server pass too. Combine it with react.useSuspense: true, so a missing namespace suspends into the nearest <Suspense> boundary instead of rendering keys, and both passes render the same text:
'use client'
import { Suspense } from 'react'
import { I18nProvider } from 'next-i18next/client'
import resourcesToBackend from 'i18next-resources-to-backend'
const backend = resourcesToBackend((lng, ns) => import(`../i18n/locales/${lng}/${ns}.json`))
export function Providers({ children, language, resources }) {
return (
<I18nProvider
language={language}
resources={resources} // app-shell namespaces only, e.g. getResources(i18n, ['common'], [lng])
use={[backend]}
ssrBackend
i18nextOptions={{ react: { useSuspense: true } }}
>
<Suspense fallback={<Skeleton />}>{children}</Suspense>
</I18nProvider>
)
}Use ssrBackend only with backends that have no timers and no relative URLs, such as i18next-resources-to-backend with dynamic imports. HTTP and locize backends must stay browser-only for the reason above.
Server-side caching
On the server, next-i18next uses a module-level singleton i18next instance:
- Translations are loaded once and reused across all subsequent requests
- Custom backends benefit from this — no re-fetching per request
- Additional namespaces are loaded on demand and cached
In serverless environments (Lambda, Vercel Serverless, etc.), the cache only lives as long as the warm function instance. For serverless, prefer:
- Bundling translations at build time via
resourcesorresourceLoaderwith dynamic imports - Downloading translations in CI/CD via Locize CLI
API Reference
`next-i18next` (root export)
| Export | Description |
|---|---|
defineConfig(config) |
Type-safe config helper (identity function) |
normalizeConfig(config) |
Fill in defaults and validate config |
I18nConfig |
Config type |
`next-i18next/proxy` (also available as `next-i18next/middleware`)
| Export | Description |
|---|---|
createProxy(config) |
Returns a Next.js proxy function (for proxy.ts) |
createMiddleware(config) |
Alias for createProxy (for middleware.ts on Next.js < 16) |
defineConfig, normalizeConfig, I18nConfig |
Re-exported for Edge-safe config usage |
`next-i18next/server`
| Export | Description |
|---|---|
initServerI18next(config) |
Initialize server config (call once at module scope, before the first getT()) |
createServerI18next(config) |
Returns { getT, getResources, generateI18nStaticParams } bound to config. No initServerI18next and no evaluation order to get right; see Initialization without ordering assumptions |
getT(ns?, options?) |
Get { t, i18n } for Server Components. Options: { lng?, keyPrefix? } |
getResources(i18n, namespaces?, languages?) |
Extract loaded resources for client hydration. The shared instance preloads every supported language by default, so pass languages (e.g. [lng, fallbackLng]) to keep the serialized payload small. Include the fallback language, or keys missing from the current one have nothing to fall back to on the client |
generateI18nStaticParams() |
Returns [{ lng: 'en' }, { lng: 'de' }, ...] for generateStaticParams, keyed by localeParamName (generateI18nStaticParams<'locale'>() narrows the type) |
`next-i18next/client`
| Export | Description |
|---|---|
I18nProvider |
Client-side provider wrapping I18nextProvider |
useT(ns?, options?) |
Translation hook for Client Components (works in all modes) |
useChangeLanguage(cookieName?, cookieOptions?) |
Language switcher hook for no-locale-path and internal mode; cookieOptions takes the same attributes as the config option |
Trans |
Re-exported from react-i18next |
`next-i18next/icu` and `next-i18next/mf2`
| Export | Description |
|---|---|
I18nProvider |
I18nProvider from next-i18next/client with i18next-icu (/icu) or i18next-mf2 (/mf2) registered on the client instance. Same props. See ICU and MF2 with the App Router |
`next-i18next/pages`
| Export | Description |
|---|---|
appWithTranslation |
HOC for _app |
useTranslation |
Translation hook (re-exported from react-i18next) |
Trans, I18nContext, withTranslation |
Re-exported from react-i18next |
`next-i18next/pages/serverSideTranslations`
| Export | Description |
|---|---|
serverSideTranslations(locale, ns?, config?, extraLocales?) |
Load translations for getStaticProps / getServerSideProps |
Config Options
| Option | Default | Description |
|---|---|---|
supportedLngs |
required | Array of supported language codes |
fallbackLng |
required | Default language |
defaultNS |
'common' |
Default namespace |
ns |
[defaultNS] |
All known namespaces |
localeInPath |
true |
true: locale prefix in the URL. false: clean URLs, language from cookie/header (routes are dynamic). 'internal': clean URLs, locale segment only in the internal rewrite (routes stay prerenderable) |
localeParamName |
'lng' |
Name of the locale route segment (app/[lng]). Used by generateI18nStaticParams and root-param language detection |
hideDefaultLocale |
false |
When true (with localeInPath: true), the default language has no URL prefix |
localePath |
'/locales' |
Path to locale files relative to /public |
localeStructure |
'{{lng}}/{{ns}}' |
Locale file directory structure |
localeExtension |
'json' |
Locale file extension |
resources |
— | Pre-loaded resources (skips dynamic loading) |
resourceLoader |
— | Custom async loader (lng, ns) => Promise<object> |
basePath |
— | Route-folder prefix the proxy handles, e.g. '/app-router' for app/app-router/[lng]/… in mixed-router setups. Not needed for Next's own basePath from next.config, which the proxy handles automatically |
cookieName |
'i18next' |
Cookie name for language persistence |
headerName |
'x-i18next-current-language' |
Header name for server-side language passing |
cookieMaxAge |
31536000 (1 year) |
Cookie max age in seconds |
persistCookie |
true |
Whether the proxy writes the language cookie. false reads it but never writes it, for apps where another system owns the cookie |
cookieOptions |
{} |
Extra cookie attributes (domain, secure, sameSite, path, maxAge) applied on top of { path: '/', maxAge: cookieMaxAge, sameSite: 'lax' } |
ignoredPaths |
['/api', '/_next', '/static'] |
Paths the proxy/middleware should skip |
use |
[] |
Extra i18next plugins |
i18nextOptions |
{} |
Additional i18next init options |
nonExplicitSupportedLngs |
false |
Match 'en' to 'en-US' etc. |
Examples
| Example | Description |
|---|---|
app-router-simple |
App Router with locale-in-path (/en/...) |
app-router-no-locale-path |
App Router with cookie-based language (no locale in URL) |
app-router-icu |
App Router with ICU MessageFormat: plurals, select and rich text in Server and Client Components |
mixed-routers |
App Router + Pages Router in the same project |
pages-router-simple |
Pages Router with getStaticProps / getServerSideProps |
pages-router-ssg |
Pages Router with static export (output: 'export') |
pages-router-auto-static-optimize |
Pages Router with client-side loading via chained backend |
Migration from v15
App Router users
If you were using i18next and react-i18next directly (as recommended in v15):
npm install next-i18next@16- Create an
i18n.config.tswith your languages and namespaces - Replace your custom proxy/middleware with
createProxy(config)inproxy.ts - Replace your custom
getT/ translation init withinitServerI18next+getTfromnext-i18next/server - Replace your custom
I18nProviderwith the one fromnext-i18next/client - Replace
useTranslationwithuseTfromnext-i18next/clientin client components
Pages Router users
- Update imports from
next-i18nexttonext-i18next/pages - Update
serverSideTranslationsimport tonext-i18next/pages/serverSideTranslations - Update type imports:
import type { TFunction, WithTranslation, I18n } from 'next-i18next/pages' - Everything else works the same
Contributors
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
Gold Sponsors
localization as a service - Locize
Needing a translation management? Want to edit your translations with an InContext Editor? Use the original provided to you by the maintainers of i18next!
Now with a Free plan for small projects! Perfect for hobbyists or getting started.
By using Locize you directly support the future of i18next and next-i18next.
File: babel.config.json
{
"env": {
"test": {
"presets": [
"@babel/preset-typescript",
[
"@babel/env",
{
"targets": {
"node": "current"
},
"modules": "commonjs"
}
],
["@babel/preset-react"]
],
"plugins": [
[
"add-module-exports",
{
"addDefaultProperty": true
}
]
]
}
}
}
File: CHANGELOG.md
16.4.1
- Docs: typed ICU and MF2 arguments - the README section "ICU and MF2 with the App Router" now describes
parseInterpolation: 'icu'/'mf2'(i18next 26.5.0): the options oftfromgetTanduseTand thevaluesofIcuTransandIcuTransWithoutContext(react-i18next 17.1.0) are type-checked against the message's arguments, with the resources file fromnpx i18next-cli typesfor literal message types.parseInterpolation: falsestays the setting for i18next 26.2.0 to 26.4.x. No code changes:getTanduseTreturn i18next'sTFunction, so they get the checks as is. - Example
app-router-icuusesparseInterpolation: 'icu'with ani18next.config.tsand a generated@types/resources.d.ts.
16.4.0
- App Router: ICU and MF2 without a
'use client'wrapper -next-i18next/icuandnext-i18next/mf2export anI18nProviderwith the same props as the one fromnext-i18next/client, with i18next-icu or i18next-mf2 registered on the client instance. Render it from the root layout and keepuse: [ICU](or[MF2]) in the server config. A Server Component can not pass a plugin class to a Client Component, so `` in a layout failed the build with "Functions cannot be passed directly to Client Components", and ICU needed a hand-written'use client'wrapper around the provider. Both plugins are optional peer dependencies: apps that never import these entry points do not resolve or bundle them. New README section "ICU and MF2 with the App Router" (setup,getT/useT, rich text withIcuTransWithoutContextfrom react-i18next 17.0.21 in Server Components,parseInterpolation: falsefor TypeScript,extract.messageFormatin i18next-cli 1.76.0) and a new example,app-router-icu.
16.3.2
- App Router:
useTno longer re-renders on every URL change — it calleduseParams()to sync the language from a[lng]/[locale]segment, and that subscribes the caller to the router's params, which Next replaces on every URL change, includinghistory.replaceState/pushStateand search-param updates. Every component usinguseTre-rendered each time, even inlocaleInPath: falsemode where there is no such segment. The sync now lives in a single component insideI18nProviderthat renders nothing, anduseTis a typeduseTranslation. #2349 - App Router:
useTno longer breaksnext buildwithcacheComponents: true— that sync component only mounts in the browser, souseParams()is no longer called during server rendering. AuseTcaller on a route whose params are not all covered bygenerateStaticParamsused to fail the prerender with "useParams()in a Client Component outside of<Suspense>"; it now renders into the static shell. #2349
16.3.1
- App Router: changing the language on the client works with
i18nextOptions: { preload: [] }— with languages loaded on demand the server ships only the ones it has loaded, andI18nProviderpinnedsupportedLngsto whateverresourceshappened to contain, so i18next resolved every other language back to the fallback and client components stayed in the old language.supportedLngsis no longer derived fromresources(pass the prop to restrict it), and each newresourcesprop is merged into the client instance —router.refresh()delivers the language the server just loaded — re-resolving the language when its bundles only just arrived.useChangeLanguageleaves that switch to the provider when the store holds nothing for the target language yet, soi18n.resolvedLanguageno longer lags a switch behind in client components (react-i18next 17.0.14 fixes the same staleness at its source, for the case where an app switches the language itself). #2348
16.3.0
- App Router:
createServerI18next(config)— returns{ getT, getResources, generateI18nStaticParams }bound to the config, so the server API no longer depends oninitServerI18nexthaving run first in the same module instance. Create it once in ani18n.server.tsand import from there: every caller then imports the module that holds the config, and neither module evaluation order nor a second bundling layer (Route Handlers live in their own) can reachgetTuninitialized.initServerI18nextand the module-level functions keep working. #2347 - App Router: default server state is keyed on
globalThis— a second copy ofnext-i18next/serverin the same process (another bundling layer, or the CJS build next to the ESM one) now sees the sameinitServerI18nextcall instead of throwing. The "not initialized" error also names the real requirement (before the firstgetT()in the process) and points atcreateServerI18next. #2347
16.2.0
- App Router: the proxy only writes the language cookie when the language changed — every response used to re-set the cookie: internal-mode rewrites on every request, the default and
hideDefaultLocaleredirects, and the referer branch on every navigation from a locale-prefixed page. Next merges a cookie set in middleware into the request's mutable cookies and treats any modified cookie as a revalidation, so every Server Action (its POST carries the page as referer) made the client router drop its cache and refetch the page, resetting remounted forms along the way. The write is now skipped when the incoming cookie already holds the resolved language. Trade-off: the cookie'smaxAgeis refreshed only on change. #2346 - App Router:
persistCookieandcookieOptions—persistCookie: falsemakes the proxy read the language cookie without ever writing it, for apps where another system owns it.cookieOptions(domain,secure,sameSite,path,maxAge) is applied on top of the defaults when the proxy does write, anduseChangeLanguage(cookieName?, cookieOptions?)accepts the same options so both writers agree on the cookie's scope. #2346
16.1.1
- App Router: proxy rewrites and redirects keep Next's
basePath— targets were built withnew URL(path, req.url), which drops the base path that Next keeps insidereq.nextUrl, so withbasePath: '/nxt'innext.configevery locale redirect, and every rewrite inhideDefaultLocaleand'internal'mode, landed outside the app and 404'd. Targets are now built fromreq.nextUrl.clone(), which also preservestrailingSlash, and the referer-based cookie persistence strips the base path before looking for a locale. ThebasePathoption in the i18n config is unrelated: it is the route-folder prefix for mixed-router setups (app/app-router/[lng]/…). #2345 - App Router:
ssrBackendprop onI18nProvider— opt in to applying the backends fromuseduring the server render pass as well. Withi18nextOptions: { react: { useSuspense: true } }a namespace missing fromresourcessuspends into the nearest<Suspense>boundary and streams in, so only the namespaces a route renders need to be in the HTML and the rest load on demand without hydration mismatches. Only for timer-free backends such asi18next-resources-to-backendwith dynamic imports; the 16.0.11 reasoning still applies to http/locize backends. #2345
16.1.0
- App Router:
localeInPath: 'internal'— a third routing mode for clean URLs and static prerendering per locale. Routes stay underapp/[lng]/, but the proxy rewrites/aboutto/{lng}/aboutinternally (cookie → Accept-Language → fallback), so the public URL never shows a locale while every locale is still prerendered viagenerateStaticParams. Explicit locale paths (/de/about) are served as-is, never redirected, so<Link>prefetches cannot flip the language cookie; switch languages withuseChangeLanguage(). This is the mode for Next.js Cache Components (cacheComponents: true), wherelocaleInPath: falsecannot be used because readingheaders()/cookies()makes every route dynamic. #2345 - App Router:
getT()resolves the language fromnext/root-paramsfirst (Next.js 16.3+) — root params are part of the route key, so unlikeheaders()/cookies()they do not opt a route out of prerendering.await getT('home')in a prerendered[lng]route no longer needs{ lng }threaded through every call site. Falls back to header/cookie detection on older Next.js, in Route Handlers and Server Actions (which have no root params), or when the root layout has no locale segment. #2345 - App Router: languages load on demand —
getT()now callsloadLanguages(lng)and checkshasLoadedNamespace(ns, { lng })for the requested language instead of the instance's own. The default still preloads every supported language; seti18nextOptions: { preload: [] }to load each language on its first request, keeping memory proportional to the locales actually served. #2345 - App Router:
localeParamNameoption — name of the locale route segment (default'lng').generateI18nStaticParams()keys its params by it, so anapp/[locale]segment now prerenders (previously the returned{ lng }silently matched nothing); root-param detection uses the same name.generateI18nStaticParams<'locale'>()narrows the return type. #2345
16.0.11
- App Router: backends are no longer attached during SSR —
I18nProvideris a Client Component, but the App Router also renders it on the server, once per request, and itsuseStateinitializer therefore built a throwaway i18next instance in Node on every render. With a backend passed viause, that instance got a Node-side backend, and backends that refresh on a timer never let it go:i18next-locize-backenddefaultsreloadIntervalto 1 hour wheneverwindowis undefined, so every render left behind a live timer that kept refetching its namespaces forever. In production this showed up as a request floor that ratcheted upwards between deploys (measured on one app: ~90 requests/hour climbing to ~8,500 before a pod restart, ~190k downloads/day against a ~10k baseline). Backend plugins, including the built-in fetch loader, are now applied only in the browser, andpartialBundledLanguagesis off during the server pass, so SSR renders purely fromresources. Non-backend plugins (detectors, post-processors) still apply in both passes. getResourcesaccepts a language filter —getResources(i18n, namespaces?, languages?). The shared server instance preloads every supported language, so the default return value serialized all of them into the HTML; on a four-language app that was ~440 KB per page. Pass[lng, fallbackLng]to ship only what the client renders.
16.0.10
- Pages Router: restore
saveMissingvia.finally— the #2344 fix re-enabledsaveMissingin a.thenon thechangeLanguagepromise; now uses.finallyso a hypothetically rejected promise cannot leavesaveMissingdisabled for the rest of the session (defensive only: current i18next never rejects that promise)
16.0.9
- Pages Router: no more
missingKeyHandlerfalse positives on locale change —appWithTranslationreuses one i18next instance, so a client-side locale change renders at least once with the destination's resources loaded but the previous language still active (thechangeLanguagecall runs in a layout effect). WithsaveMissing: true, keys absent from the previous language were reported as missing during that transitional render even though they exist in the destination locale.saveMissingis now suspended while the language handoff is pending and restored oncechangeLanguagecompletes. #2344
16.0.8
- Pages Router: self-diagnosing
serverSideTranslationserror — the "Initial locale argument was not passed into serverSideTranslations" error now names its two common causes (missingi18nsection innext.config.js; pages rendered outside Next.js locale routing such as custom 404/500 oroutput: 'export') and shows the explicit-locale fix inline. - CI: weekly Next.js compatibility workflow — builds and e2e-tests all example apps against
next@latestandnext@canaryon a weekly schedule (plus manual dispatch), so upstream Next.js regressions like the 13.5.4 bundling breakage (#2214) surface within days instead of via user reports.
16.0.7
- Pages Router: fix duplicate
i18nextin client bundles — v16 shipped the Pages Router as CJS-only, which forced bundlers to loadi18nextvia itsrequirecondition (i18next/dist/cjs) while user code loaded it via theimportcondition (i18next/dist/esm), producing two copies of i18next in the same bundle. The Pages Router now builds dual ESM + CJS outputs, and the./pagesand./pages/serverSideTranslationsexports declare matchingimport/requireconditions. The server-only filesystem branch ofcreateConfighas been extracted into a separateserverSideConfigmodule (injected via hook byserverSideTranslations) socreateConfigno longer pulls Node built-ins into client bundles. Note:appWithTranslationno longer re-runs the server-side validation/preload during SSR — that work happens once, up-front, insideserverSideTranslations(in practice a no-op sinceappWithTranslationuses the browser client with pre-loaded resources). #2342
16.0.6
- Pages Router:
reloadOnPrerendernow actually reloads — the option was previously callingreloadResources()on the browser-side i18next instance returned byappWithTranslation(which has no FS backend and is oftennullduringgetStaticProps/getServerSideProps). It now reloads on the disk-backed node-side instance, scoped to exactly the locales × namespaces being shipped, so edits to locale files appear without restartingnext dev. Gated behindNODE_ENV !== 'production'to keep custom HTTP/locize/chained backends from being refetched on every prerender call. #2123 - App Router:
reloadOnPrerenderis now wired up — previously declared in the config types but never consumed. Same dev-only semantics as Pages Router: refetches translations from the configured backend (default FS oruse-provided custom backend) before each render in development, deduplicated acrossgetTcalls within a single render via React'scache(). No effect in production builds. Note: when using a dynamicimport()-basedresourceLoader, hot-reload is bundler-dependent and may stall after the first edit because Turbopack/Webpack cache resolved JSON modules across HMR cycles — see the README "Dev tip" for the dev/prod-split pattern that gives full hot-reload.
16.0.5
- Pages Router: export missing types from
next-i18next/pages—TFunction,I18n,WithTranslation,WithTranslationHocType, andUseTranslationtypes are now properly re-exported, matching the v15 API surface #2339
16.0.4
- TypeScript compatibility with i18next v26 — fixed type error in Pages Router
createConfigwhere i18next v26's readonlypreloadtype was incompatible with the internal config type
16.0.3
- New
hideDefaultLocaleoption (App Router) — when enabled, the default language is served without a URL prefix (/aboutinstead of/en/about). Non-default locales keep their prefix (/de/about). Explicit default-locale paths (/en/about) are automatically redirected to the clean URL. Works withbasePathtoo. #2338
16.0.2
- Serverless/Vercel: better error when
public/locales/is not available at runtime — the default filesystem backend now catches read failures and shows a clear error message explaining the serverless limitation with aresourceLoadercode example #2337 partialBundledLanguages+resources— when bothresourcesandpartialBundledLanguages: trueare provided, the default backend is now correctly kept so it can load additional namespaces not included in the pre-loaded resources
16.0.1
- App Router:
config.resourcesnow works on the server —resourceswas typed and documented but ignored at runtime byinitServerI18next/getT. The resource backend is now skipped whenresourcesis provided, and the pre-loaded translations are passed directly toi18next.init(). #2336
16.0.0
BREAKING CHANGES
- Import paths changed for Pages Router:
next-i18next→next-i18next/pages,next-i18next/serverSideTranslations→next-i18next/pages/serverSideTranslations - Root export is now App Router: The default
next-i18nextimport now exports App Router utilities (defineConfig,normalizeConfig,I18nConfig). Pages Router users must update tonext-i18next/pages. - Removed
i18next-fs-backenddependency: Pages Router server-side loading now usesi18next-resources-to-backendwithfs.readFileSyncinternally. No change needed for users — the behavior is identical.
New Features
- App Router support — first-class support for Next.js App Router with Server Components and Client Components
getT(ns?, options?)— async translation function for Server Components, layouts, andgenerateMetadata. Returns namespace-typed{ t, i18n, lng }.useT(ns?, options?)— translation hook for Client Components. Reads language from[lng]or[locale]URL params automatically.I18nProvider— client-side provider for hydrating server-loaded translations. Supports custom backends viauseprop.initServerI18next(config)— one-time server configuration setupgetResources(i18n, namespaces?)— extract loaded resources for client hydrationgenerateI18nStaticParams()— helper forgenerateStaticParams
- Proxy support (Next.js 16+) —
createProxy()fromnext-i18next/proxyfor the newproxy.tsfile convention.createMiddleware()fromnext-i18next/middlewareremains available for Next.js 14/15.- Edge-safe language detection (cookie → Accept-Language → fallback)
- Locale-in-path routing with automatic redirects
- Custom header for Server Component language detection
basePathoption — scope the proxy to a URL prefix (e.g.,/app-router) for mixed App Router + Pages Router setups- No-locale-path mode —
localeInPath: falsefor cookie-based language without URL prefixes. UseuseChangeLanguage()hook for language switching. defineConfig()— type-safe configuration helper- Custom backend support — pass any i18next backend plugin (http-backend, locize-backend, chained-backend) via the
useconfig option. The default resource loader is skipped automatically when a custom backend is provided. resourceLoaderoption — custom async loader function(lng, ns) => Promise<object>for dynamic imports or custom loading logic- Server-side singleton caching — translations loaded once and reused across requests. Custom backends benefit from this — no re-fetching per request.
nonExplicitSupportedLngs— match'en'to'en-US'etc. in language detection
Pages Router
- All existing v15 APIs preserved under
next-i18next/pagesandnext-i18next/pages/serverSideTranslations appWithTranslation,useTranslation,Trans,serverSideTranslations— unchanged behavior- Defensive
.filter(Boolean)onconfig.usearrays to handle CJS/ESM interop edge cases with Turbopack
15.4.3
- types: fix: WithTranslation type from next-i18next is not generic (TS error) #2329
15.4.2
- types: re-add @types/hoist-non-react-statics to dependencies #2316
15.4.1
- Move @types/hoist-non-react-statics to devDependencies #2310
15.4.0
- support i18next v24
15.3.1
- update some i18next dependencies to address #2288
15.3.0
- Only overwrite ns config if it provided #2270
15.2.0
- add possibility to pass resources directly via config and set localePath to null
15.1.2
- fix: Install error with react-i18next v14 #2248
15.1.1
- optimize/fix last change for turbo
15.1.0
- try to fix for turbo #2222
15.0.0
- refactor: reuse existing i18next instance #2226
-> If you use client side pages (not lazy loading translations), like described here, make sure you set thepartialBundledLanguagesoption to true, like here.
14.0.3
- fix: correct namespacesRequired type in serverSideTranslations #2203
14.0.2
- try to fix namespacesRequired in serverSideTranslations is not type-safe anymore #2201
14.0.0
- requires i18next >= v23.0.1
- requires react-i18next >= v13.0.0
Breaking changes
i18next 23.0.0 dropped support for older browsers.
From nextjs 13, you can use the transpilePackages
to avoid issues.
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['i18next'],
}
module.exports = nextConfig13.3.0
- using a custom backend on server side should also lazy load the passed namespaces
13.2.2
- pageProps may be undefined on strange setups #2109"
13.2.1
- types: fix serverSideTranslations args #2104"
13.2.0
- types: Update serverSideTranslation args type #2097"
13.1.6
- fix: allow user provided affixes to be used without providing localeStructure #2100"
13.1.5
13.1.4
- revert #2084
13.1.3
- another code snippet for #2084 to try to fix for dynamic pages
13.1.2
- different code snippet for #2084 to try to fix for dynamic pages
13.1.1
- log config path if no config found
13.1.0
- feat(server-side): custom default config path #2084
13.0.3
- Error if custom localeStructure provided, but no ns option defined.
13.0.2
Fix
- Types: DefaultNamespace import, see #2061.
13.0.1
Fix
- Fix missing
i18n.localeDetectionin UserConfig, see #2057. - Update examples to latest i18next in #2058
Caution
If you're experiencing typecheck errors regarding keys: ensure i18next is at least ^22.4.3 in your
package.json (then run install), see #2058.
13.0.0
The v13.0.0 release is a major version to improve stability and general experience.
It comes with 2 easy changes related to installation. Existing code shouldn't be impacted.
Details can be found in the UPGRADING.md document.
Breaking changes
react-i18next and i18next
have been moved to peer-dependencies. They must be installed
in your app (#1966)bash# Add react-i18next > 12.0.0 and i18next > 22.0.4 to your app dependencies npm install react-i18next i18next --save # NPM yarn add react-i18next i18next # Yarn pnpm add react-i18next i18next --save # PNPMThis might solve issues with duplicates and multiple i18n context instances.
If you encounter any issue, please read the Troubleshoot doc
before posting an issue.Types augmentations are now handled by i18next instead of react-i18next (#1997).
See the upgrade document here.
New
- Support for NextJs 13 (excluding new experimental layout / rsc)
- Upgrade to i18next v22 and react-i18next v12, see #1966
- Support for node 18 lts #2017
Fix
- Fix types for appWithTranslation #1987
New minimum versions
We've dropped support for nextjs < 12.0.0 / react < 17.0.2 (#1983)
and node < 14 (#1974).
12.1.0
- fix: appWithTranslation re-renders _app when the locale is changed (#1954)
- feat: introduce onPreInitI18next option (#1960)
12.0.1
- fix: fallbackLng if namespaces are undefined (#1943 closes #1941)
12.0.0
Why a major version?
The following changes could lead to more languages being loaded, which could increase the page size.
- feat: improve fallback language handling (#1927)
- feat: add support for nonExplicitSupportedLngs (#1930)
11.3.0
- feat: introduce extraLocales (#1916)
11.2.2
- fix: pass namespaces to the client also for custom backends (#1913)
11.2.1
- fix: pass namespaces to the client (#1912 closes #1839)
11.2.0
- feat: support nested namespace structure (#1911)
11.1.1
- fix: remove postinstall script
11.1.0
- first release with new project ownership
- update most dependencies
- update docs and example
- feat: support default locale by ignoring it (#1679)
11.0.0
Features:
- Allow client side translation loading (8132efd)
Documentation:
- Link to
react-i18nextconfig options (422a0f3)
File: CODE_OF_CONDUCT.md
Contributor Covenant Code of Conduct
Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances - Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a
professional setting
Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
File: CONTRIBUTING.md
Contributing
Setup local dev environment
- Clone the repo
- Ensure you have a node version >=20, npm > 8
Working on the code
Run npm install
npm installUseful commands:
| Name | Description |
|---|---|
npm run lint |
Run eslint over the codebase. |
npm run typecheck |
Run typescript typechecking. |
npm run test |
Run unit tests. |
npm run build |
Run a build (creates dist files in ./dist folder) |
npm run check-dist |
Check dist files for ecmascript compliance (requires build). |
npm run check-size |
Ensure dist files are under a certain size threshold (requires build). |
npm run check-size:why |
Open treemap of dist files (requires build) |
npm run clean |
Clean all dist files. |
E2E testing
First ensure the examples/simple app is installed.
npm run install:example:simpleTo run the e2e test suite
npm run build && npm run move-build-to-examples
npm run build:examples
npm run test:e2eWorking on examples
Examples nextjs apps live in the examples/ folder.
npm run install:examples
npm run build:examplesPRs
All PRs must pass all checks before they will be considered for review.
Notes
Please be sure to comply with our Developer's Certificate of Origin
File: package.json
{
"name": "next-i18next",
"version": "16.4.1",
"repository": {
"type": "git",
"url": "https://github.com/i18next/next-i18next.git"
},
"author": "i18next",
"funding": [
{
"type": "individual",
"url": "https://www.locize.com/i18next"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
},
{
"type": "individual",
"url": "https://www.locize.com"
}
],
"main": "dist/appRouter/index.cjs",
"module": "dist/appRouter/index.mjs",
"types": "dist/appRouter/index.d.cts",
"exports": {
".": {
"import": {
"types": "./dist/appRouter/index.d.mts",
"default": "./dist/appRouter/index.mjs"
},
"require": {
"types": "./dist/appRouter/index.d.cts",
"default": "./dist/appRouter/index.cjs"
}
},
"./middleware": {
"import": {
"types": "./dist/appRouter/proxy/index.d.mts",
"default": "./dist/appRouter/proxy/index.mjs"
},
"require": {
"types": "./dist/appRouter/proxy/index.d.cts",
"default": "./dist/appRouter/proxy/index.cjs"
}
},
"./proxy": {
"import": {
"types": "./dist/appRouter/proxy/index.d.mts",
"default": "./dist/appRouter/proxy/index.mjs"
},
"require": {
"types": "./dist/appRouter/proxy/index.d.cts",
"default": "./dist/appRouter/proxy/index.cjs"
}
},
"./server": {
"import": {
"types": "./dist/appRouter/server.d.mts",
"default": "./dist/appRouter/server.mjs"
},
"require": {
"types": "./dist/appRouter/server.d.cts",
"default": "./dist/appRouter/server.cjs"
}
},
"./client": {
"import": {
"types": "./dist/appRouter/client.d.mts",
"default": "./dist/appRouter/client.mjs"
},
"require": {
"types": "./dist/appRouter/client.d.cts",
"default": "./dist/appRouter/client.cjs"
}
},
"./icu": {
"import": {
"types": "./dist/appRouter/icu.d.mts",
"default": "./dist/appRouter/icu.mjs"
},
"require": {
"types": "./dist/appRouter/icu.d.cts",
"default": "./dist/appRouter/icu.cjs"
}
},
"./mf2": {
"import": {
"types": "./dist/appRouter/mf2.d.mts",
"default": "./dist/appRouter/mf2.mjs"
},
"require": {
"types": "./dist/appRouter/mf2.d.cts",
"default": "./dist/appRouter/mf2.cjs"
}
},
"./pages": {
"import": {
"types": "./dist/pagesRouter/index.d.mts",
"default": "./dist/pagesRouter/index.mjs"
},
"require": {
"types": "./dist/pagesRouter/index.d.cts",
"default": "./dist/pagesRouter/index.cjs"
}
},
"./pages/serverSideTranslations": {
"import": {
"types": "./dist/pagesRouter/serverSideTranslations.d.mts",
"default": "./dist/pagesRouter/serverSideTranslations.mjs"
},
"require": {
"types": "./dist/pagesRouter/serverSideTranslations.d.cts",
"default": "./dist/pagesRouter/serverSideTranslations.cjs"
}
},
"./package.json": "./package.json"
},
"license": "MIT",
"files": [
"dist"
],
"engines": {
"node": ">=20"
},
"description": "The easiest way to translate your NextJs apps.",
"keywords": [
"react",
"i18next",
"nextjs",
"next",
"translation",
"localisation",
"localization",
"locale",
"app-router",
"pages-router",
"server-components"
],
"scripts": {
"prettier": "prettier --ignore-path .gitignore --write .",
"lint": "eslint src",
"clean": "rm -rf ./examples/pages-router-simple/.next ./examples/pages-router-ssg/.next ./examples/pages-router-auto-static-optimize/.next ./examples/app-router-simple/.next ./examples/app-router-no-locale-path/.next ./examples/mixed-routers/.next ./examples/app-router-icu/.next ./dist && mkdir dist",
"build": "npm run clean && tsdown",
"build:examples": "for d in examples//; do (cd "$d" && npm run build) || exit 1; done",
"build:example:pages-router-simple": "cd examples/pages-router-simple && npm run build",
"build:example:pages-router-ssg": "cd examples/pages-router-ssg && npm run build",
"build:example:pages-router-auto-static-optimize": "cd examples/pages-router-auto-static-optimize && npm run build",
"build:example:app-router-simple": "cd examples/app-router-simple && npm run build",
"build:example:app-router-no-locale-path": "cd examples/app-router-no-locale-path && npm run build",
"build:example:mixed-routers": "cd examples/mixed-routers && npm run build",
"build:example:app-router-icu": "cd examples/app-router-icu && npm run build",
"typecheck": "tsc --project ./tsconfig.pagesRouter.json --noEmit && tsc --project ./tsconfig.appRouter.json --noEmit",
"typecheck:example:pages-router-simple": "cd examples/pages-router-simple && npm run typecheck",
"test": "npm run lint && NODE_ENV=test jest --maxWorkers=1 --silent",
"install:examples": "for d in examples//; do (cd "$d" && npm install) || exit 1; done",
"install:example:pages-router-simple": "cd examples/pages-router-simple && npm install",
"install:example:pages-router-ssg": "cd examples/pages-router-ssg && npm install",
"install:example:pages-router-auto-static-optimize": "cd examples/pages-router-auto-static-optimize && npm install",
"install:example:app-router-simple": "cd examples/app-router-simple && npm install",
"install:example:app-router-no-locale-path": "cd examples/app-router-no-locale-path && npm install",
"install:example:mixed-routers": "cd examples/mixed-routers && npm install",
"install:example:app-router-icu": "cd examples/app-router-icu && npm install",
"copy-build-to-example": "rimraf $EXAMPLE/node_modules/next-i18next && mkdir -p $EXAMPLE/node_modules/next-i18next && cp -r dist package.json $EXAMPLE/node_modules/next-i18next",
"move-build-to-examples": "EXAMPLE=examples/pages-router-simple npm run copy-build-to-example && EXAMPLE=examples/pages-router-ssg npm run copy-build-to-example && EXAMPLE=examples/pages-router-auto-static-optimize npm run copy-build-to-example && EXAMPLE=examples/app-router-simple npm run copy-build-to-example && EXAMPLE=examples/app-router-no-locale-path npm run copy-build-to-example && EXAMPLE=examples/mixed-routers npm run copy-build-to-example && EXAMPLE=examples/app-router-icu npm run copy-build-to-example",
"test:e2e": "start-server-and-test 'cd examples/pages-router-simple && npm run start' http://127.0.0.1:3000 cy:run",
"cy:run": "cypress run",
"example": "npm run build && EXAMPLE=examples/mixed-routers npm run copy-build-to-example && cd examples/mixed-routers && npm run dev",
"example:prod": "npm run build && EXAMPLE=examples/mixed-routers npm run copy-build-to-example && cd examples/mixed-routers && npm run build && npm run start",
"example:pages-router-simple": "npm run build && EXAMPLE=examples/pages-router-simple npm run copy-build-to-example && cd examples/pages-router-simple && npm run dev",
"example:pages-router-simple:prod": "npm run build && EXAMPLE=examples/pages-router-simple npm run copy-build-to-example && cd examples/pages-router-simple && npm run build && npm run start",
"example:pages-router-ssg": "npm run build && EXAMPLE=examples/pages-router-ssg npm run copy-build-to-example && cd examples/pages-router-ssg && npm run dev",
"example:pages-router-ssg:prod": "npm run build && EXAMPLE=examples/pages-router-ssg npm run copy-build-to-example && cd examples/pages-router-ssg && npm run build && npm run start",
"example:pages-router-auto-static-optimize": "npm run build && EXAMPLE=examples/pages-router-auto-static-optimize npm run copy-build-to-example && cd examples/pages-router-auto-static-optimize && npm run dev",
"example:pages-router-auto-static-optimize:prod": "npm run build && EXAMPLE=examples/pages-router-auto-static-optimize npm run copy-build-to-example && cd examples/pages-router-auto-static-optimize && npm run build && npm run start",
"example:app-router-simple": "npm run build && EXAMPLE=examples/app-router-simple npm run copy-build-to-example && cd examples/app-router-simple && npm run dev",
"example:app-router-simple:prod": "npm run build && EXAMPLE=examples/app-router-simple npm run copy-build-to-example && cd examples/app-router-simple && npm run build && npm run start",
"example:app-router-no-locale-path": "npm run build && EXAMPLE=examples/app-router-no-locale-path npm run copy-build-to-example && cd examples/app-router-no-locale-path && npm run dev",
"example:app-router-no-locale-path:prod": "npm run build && EXAMPLE=examples/app-router-no-locale-path npm run copy-build-to-example && cd examples/app-router-no-locale-path && npm run build && npm run start",
"example:app-router-icu": "npm run build && EXAMPLE=examples/app-router-icu npm run copy-build-to-example && cd examples/app-router-icu && npm run dev",
"example:app-router-icu:prod": "npm run build && EXAMPLE=examples/app-router-icu npm run copy-build-to-example && cd examples/app-router-icu && npm run build && npm run start",
"prepublishOnly": "npm run build",
"preversion": "npm run test && npm run build && git push",
"postversion": "git push && git push --tags && npm run release",
"release": "gh-release",
"nuke:install": "rimraf '/node_modules' '/package-lock.json'",
"prepare": "husky"
},
"devDependencies": {
"@babel/core": "7.29.7",
"@babel/preset-env": "7.29.7",
"@babel/preset-react": "7.29.7",
"@babel/preset-typescript": "7.29.7",
"@eslint/js": "^9.39.5",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@types/jest": "^30.0.0",
"@types/node": "^25.9.5",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@types/testing-library__cypress": "^5.0.13",
"babel-plugin-add-module-exports": "1.0.4",
"cypress": "^15.21.0",
"eslint": "^9.39.5",
"gh-release": "8.1.0",
"globals": "17.11.0",
"husky": "^9.1.7",
"i18next": "^26.4.0",
"i18next-icu": "2.6.0",
"i18next-mf2": "2.1.0",
"intl-messageformat": "12.1.3",
"jest": "^30.4.2",
"jest-environment-jsdom": "^30.4.1",
"neostandard": "^0.13.0",
"next": "^16.3.1",
"prettier": "3.9.6",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.14",
"rimraf": "^6.1.3",
"start-server-and-test": "^2.1.5",
"tsdown": "0.21.4",
"typescript": "^5.9.3"
},
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.7",
"hoist-non-react-statics": "^3.3.2",
"i18next-resources-to-backend": "^1.2.3"
},
"peerDependencies": {
"i18next": ">= 23.7.13",
"i18next-icu": ">= 2.0.0",
"i18next-mf2": ">= 1.0.0",
"next": ">= 14.1.0",
"react": ">= 18.0.0",
"react-i18next": ">= 14.0.0"
},
"peerDependenciesMeta": {
"i18next-icu": {
"optional": true
},
"i18next-mf2": {
"optional": true
}
}
}
File: TROUBLESHOOT.md
Troubleshoot
Issues
Need to pass in an i18next instance
The next-i18next / react-i18next packages are relying on a react context to
share data. Due to the nature of nextjs/ssr/react, the variety of package managers,
their configurations and versions, you might encounter the following warning:
You will need to pass in an i18next instance by using initReactI18nextTo fix:
- Ensure no duplicate versions of
ì18nextand/orreact-i18nextco-exists. See debug installation. - Optionally adapt your setup to explicitly pass the config recommendations.
Can't find next-i18next.config.js
In monorepo or package managers creating symlinks in node_modules (ie: pnpm) you
might experience.
(...)next-i18next/dist/commonjs/serverSideTranslations.js
Critical dependency: the request of a dependency is an expression
Cannot find module '(...)app/next-i18next.config.js'You can solve it by explicitly passing the config.
Recipes
How to explicitly pass the config
By default, next-i18next will automatically load a next-i18n.config.js located in the project root
directory. In monorepos / workspace enabled projects the detection of the current project directory
might fail (depending on where the package manager actually hoist the next-i18next package). If you're impacted
a good way to circumvent the issue is to pass the config explicitly. It also allows
to choose a different extension (.mjs, .cjs).
This generally fix can't find next-i18next.config.js (and eventual need to pass in an i18next instance).
in _app.tsx
tsx// _app.tsx import type { AppProps } from 'next/app' import { appWithTranslation } from 'next-i18next' import nextI18NextConfig from '../next-i18next.config' const MyApp = ({ Component, pageProps }: AppProps) => ( <Component {...pageProps} /> ) export default appWithTranslation(MyApp, nextI18NextConfig)For
getServerSideProps()andgetStaticProps()the recommended approach is
to wrap our serverSideTranslations() in a separate file where you can inject the
configuration.typescript// ie: ./lib/i18n/getServerTranslations.ts import type { Namespace } from 'i18next'; import type { SSRConfig, UserConfig } from 'next-i18next' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import nextI18nextConfig from '../../next-i18next.config' type ArrayElementOrSelf<T> = T extends Array<infer U> ? U[] : T[]; export const getServerTranslations = async ( locale: string, namespacesRequired?: ArrayElementOrSelf<Namespace> | undefined, configOverride?: UserConfig, extraLocales?: string[] | false ): Promise<SSRConfig> => { const config = configOverride ?? nextI18nextConfig return serverSideTranslations(locale, namespacesRequired, config, extraLocales) }And use it instead of
serverSideTranslations:typescriptimport { getServerTranslations } from '@/lib/i18n'; const i18nNamespaces = ['demo']; export default function DemoRoute( props: InferGetStaticPropsType<typeof getStaticProps> ) { // ... } export const getStaticProps: GetStaticProps<Props> = async (context) => { const { locale } = context; return { props: { ...(await getServerTranslations(locale, i18nNamespaces)), }, }; };
How to debug installation
Since v13.0.0, i18next and react-i18next
must be explicitly installed in your app dependencies as well (peer-deps of next-i8next). Check first they are well present
in your package.json ("i18next": "^22.0.6"and"react-i18next": "^12.0.0")Recent versions of pnpm (>=7.29) / yarn (>=3) / npm (>=8) should be able to dedupe installed versions for you.
In case of issue, try to dedupe manuallyPM Check Fix (only on semver) yarn 1 npx -y yarn-deduplicate --listnpx -y yarn-deduplicateyarn 2+ yarn dedupe --listyarn dedupepnpm < 7.23.0 npx -y pnpm-deduplicate --listnpx -y pnpm-deduplicatepnpm >= 7.23.0 not available pnpm dedupenpm 8 not available npm dedupeYou can also list duplicate with
npm why -r next-i18next i18next,pnpm why -r next-i18next i18next
oryarn why -R next-i18next && yarn why -R i18next.PNPM < 7.29.0 might have issues with peer deduplication in monorepo. This will be addressed in v8, but
starting with 7.29.0 you can already set these settings in.npmrc.text# https://pnpm.io/next/npmrc use-lockfile-v6=true # https://github.com/pnpm/pnpm/releases/tag/v7.29.0 dedupe-peer-dependents=true resolve-peers-from-workspace-root=true
After fixing potential duplicates, run an installation (or update). A new lock file should be generated.
File: tsconfig.appRouter.json
{
"compilerOptions": {
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"lib": ["esnext", "dom"],
"target": "es2020",
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist/appRouter",
"rootDir": "src/appRouter",
"noEmit": false
},
"include": ["src/appRouter//*"],
"exclude": ["src/appRouter//.test."]
}
File: tsconfig.json
{
"files": [],
"references": [
{ "path": "./tsconfig.pagesRouter.json" },
{ "path": "./tsconfig.appRouter.json" }
]
}
File: tsconfig.pagesRouter.json
{
"compilerOptions": {
"esModuleInterop": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"lib": ["esnext", "dom"],
"target": "es2020",
"module": "commonjs",
"moduleResolution": "node",
"jsx": "react"
},
"include": ["src/pagesRouter//*", "node_modules/next/types/global.d.ts"],
"exclude": ["src/pagesRouter//.test."]
}
File: cypress/tsconfig.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"types": ["cypress", "@types/testing-library__cypress"]
},
"include": ["**/*.ts"]
}
File: examples/app-router-icu/README.md
next-i18next App Router example with ICU MessageFormat
This example shows how to write translations in ICU MessageFormat with next-i18next and the Next.js App Router, using i18next-icu.
It demonstrates:
use: [ICU]ini18n.config.ts, sogetT()formats ICU messages in Server ComponentsI18nProviderfromnext-i18next/icuin the root layout, souseT()formats them in Client Components, without a'use client'wrapper of your own- a plural with an exact
=0case and aselect, on the server and on the client - rich text with
IcuTransWithoutContextfromreact-i18next/IcuTransWithoutContextin a Server Component, andIcuTransin a Client Component parseInterpolation: 'icu'in@types/i18next.d.ts(i18next 26.5.0 or later), sot()options andIcuTransvalues are type-checked against the ICU arguments of each message;@types/resources.d.tsholds the messages as literal types and is generated withnpx i18next-cli typesfromi18next.config.ts
For MF2, install i18next-mf2 instead, put MF2 into use and import I18nProvider from next-i18next/mf2.
See ICU and MF2 with the App Router and Using ICU or MF2 for details.
Getting started
npm install
npm run devFile: examples/app-router-icu/package.json
{
"name": "next-i18next-app-router-icu-example",
"version": "1.0.0",
"license": "MIT",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start"
},
"dependencies": {
"i18next": "^26.5.0",
"i18next-icu": "^2.6.0",
"intl-messageformat": "^12.1.3",
"next": "^16.3.1",
"next-i18next": "^16.4.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.1.0"
},
"devDependencies": {
"@types/node": "^25.9.5",
"@types/react": "^19.2.18",
"typescript": "^5.9.3"
}
}
File: examples/app-router-icu/tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
".next/types//*.ts",
"/.ts",
"**/.tsx",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
File: examples/app-router-icu/app/i18n/locales/de/common.json
{
"title": "next-i18next mit ICU MessageFormat",
"photos": "Du hast {count, plural, =0 {keine Fotos} one {# Foto} other {# Fotos}}.",
"order": "{status, select, pending {Deine Bestellung wird vorbereitet.} shipped {Deine Bestellung ist unterwegs.} other {Der Status deiner Bestellung ist unbekannt.}}",
"welcome": "Willkommen zurück, <0>{name}</0>!",
"cart": "{count, plural, =0 {Dein Warenkorb ist leer} one {# Artikel im Warenkorb} other {# Artikel im Warenkorb}}",
"add": "Artikel hinzufügen",
"remove": "Artikel entfernen",
"rendered": "Gerendert von einer <0>{where}</0> Component.",
"switch": "Sprache wechseln:"
}
File: examples/app-router-icu/app/i18n/locales/en/common.json
{
"title": "next-i18next with ICU MessageFormat",
"photos": "You have {count, plural, =0 {no photos} one {# photo} other {# photos}}.",
"order": "{status, select, pending {Your order is being prepared.} shipped {Your order is on its way.} other {Your order status is unknown.}}",
"welcome": "Welcome back, <0>{name}</0>!",
"cart": "{count, plural, =0 {Your cart is empty} one {# item in your cart} other {# items in your cart}}",
"add": "Add item",
"remove": "Remove item",
"rendered": "Rendered by a <0>{where}</0> Component.",
"switch": "Switch language:"
}
File: examples/app-router-no-locale-path/README.md
next-i18next App Router example (no locale in path)
This example shows how to use next-i18next v16 with the Next.js App Router without a locale prefix in the URL (cookie-based language detection).
URLs stay clean (/about instead of /en/about), and language switching is handled via useChangeLanguage().
It demonstrates:
- Server Components using
getT()fromnext-i18next/server - Client Components using
useT()fromnext-i18next/client - Cookie-based language detection via
createProxy()inproxy.js(no URL redirects) useChangeLanguage()hook for switching languagesI18nProviderfor client-side hydration
Getting started
npm install
npm run devBlog post
This example was originally based on this blog post and discussion.
File: examples/app-router-no-locale-path/package.json
{
"name": "next-app-dir-i18next-no-locale-path-example",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"build": "next build",
"dev": "next dev",
"lint": "eslint .",
"start": "next start",
"migrateToLocize": "locize migrate --project-id=01b2e5e8-6243-47d1-b36f-963dbb8bcae3 --api-key=c9805cd1-fd50-4d98-967a-383b39066de8 --cdn-type pro --path=./app/i18n/locales",
"downloadLocales": "locize download --project-id=01b2e5e8-6243-47d1-b36f-963dbb8bcae3 --cdn-type pro --ver=latest --clean=true --path=./app/i18n/locales",
"syncLocales": "locize sync --project-id=01b2e5e8-6243-47d1-b36f-963dbb8bcae3 --cdn-type pro --api-key=bd8cb856-d6b3-4c82-a456-862c729a1674 --ver=latest --path=./app/i18n/locales --dry=true"
},
"dependencies": {
"i18next": "^26.4.0",
"next": "^16.3.1",
"next-i18next": "^16.0.10",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.12"
},
"devDependencies": {
"eslint": "^9.39.5",
"eslint-config-next": "^16.3.1",
"locize-cli": "^12.5.1"
}
}
File: examples/app-router-no-locale-path/app/i18n/locales/de/client-page.json
{
"back-to-home": "Zurück zur Hauptseite",
"counter_one": "eines ausgewählt",
"counter_other": "{{count}} ausgewählt",
"counter_zero": "keines ausgewählt",
"h1": "Eine client Seite, um das client-seitige i18n zu demonstrieren",
"title": "Client Seite",
"to-second-client-page": "zur zweiten client Seite"
}
File: examples/app-router-no-locale-path/app/i18n/locales/de/footer.json
{
"description": "Dies ist eine Nicht-Seitenkomponente, die einen eigenen Namespace erfordert",
"helpLocize": "Wenn Sie <1>Locize</1> einsetzen, unterstützen Sie direkt die Zukunft von <3>i18next</3>.",
"languageSwitcher": "Wechseln von <1>{{lng}}</1> nach: "
}
File: examples/app-router-no-locale-path/app/i18n/locales/de/second-client-page.json
{
"back-to-home": "Zurück zur Hauptseite",
"h1": "Eine zweite client Seite, um das client-seitige i18n zu demonstrieren",
"title": "Zweite client Seite"
}
File: examples/app-router-no-locale-path/app/i18n/locales/de/second-page.json
{
"back-to-home": "Zurück zur Hauptseite",
"h1": "Eine zweite Seite, um das Routing zu demonstrieren",
"title": "Zweite Seite"
}
File: examples/app-router-no-locale-path/app/i18n/locales/de/translation.json
{
"h1": "Ein einfaches Beispiel",
"title": "Hauptseite",
"to-client-page": "Zur client seitigen Seite",
"to-second-page": "Zur zweiten Seite",
"welcome": "Willkommen zu Next.js 16 <1>mit dem App Router</1> und next-i18next",
"blog": {
"text": "Schau dir den entsprechenden <1>Blogbeitrag</1> dieses Beispiels an.",
"link": "https://www.locize.com/blog/next-i18next-v16"
}
}
File: examples/app-router-no-locale-path/app/i18n/locales/en/client-page.json
{
"back-to-home": "Back to home",
"counter_one": "one selected",
"counter_other": "{{count}} selected",
"counter_zero": "none selected",
"h1": "A client page, to demonstrate client side i18n",
"title": "Client page",
"to-second-client-page": "to second client page"
}
File: examples/app-router-no-locale-path/app/i18n/locales/en/footer.json
{
"description": "This is a non-page component that requires its own namespace",
"helpLocize": "With using <1>Locize</1> you directly support the future of <3>i18next</3>.",
"languageSwitcher": "Switch from <1>{{lng}}</1> to: "
}
File: examples/app-router-no-locale-path/app/i18n/locales/en/second-client-page.json
{
"back-to-home": "Back to home",
"h1": "A second client page, to demonstrate client side i18n",
"title": "Second client page"
}
File: examples/app-router-no-locale-path/app/i18n/locales/en/second-page.json
{
"back-to-home": "Back to home",
"h1": "A second page, to demonstrate routing",
"title": "Second page"
}
File: examples/app-router-no-locale-path/app/i18n/locales/en/translation.json
{
"h1": "A simple example",
"title": "Home",
"to-client-page": "To client page",
"to-second-page": "To second page",
"welcome": "Welcome to Next.js 16 <1>with the App Router</1> and next-i18next",
"blog": {
"text": "Check out the corresponding <1>blog post</1> describing this example.",
"link": "https://www.locize.com/blog/next-i18next-v16"
}
}
File: examples/app-router-no-locale-path/app/i18n/locales/it/client-page.json
{
"back-to-home": "Tornare alla pagina principale",
"counter_many": "selezionati {{count}}",
"counter_one": "uno selezionato",
"counter_other": "{{count}} selezionati",
"counter_zero": "nessuno selezionato",
"h1": "Una pagina client, per dimostrare il i18n nel client",
"title": "Pagina client",
"to-second-client-page": "alla seconda pagina client"
}
File: examples/app-router-no-locale-path/app/i18n/locales/it/footer.json
{
"description": "Questo è un componente non di pagina che richiede il proprio spazio dei nomi",
"helpLocize": "Con l'utilizzare <1>Locize</1> sostieni direttamente il futuro di <3>i18next</3>.",
"languageSwitcher": "Passa da <1>{{lng}}</1> a: "
}
File: examples/app-router-no-locale-path/app/i18n/locales/it/second-client-page.json
{
"back-to-home": "Tornare alla pagina principale",
"h1": "Una seconda pagina client, per dimostrare il i18n nel client",
"title": "Seconda pagina client"
}
File: examples/app-router-no-locale-path/app/i18n/locales/it/second-page.json
{
"back-to-home": "Tornare alla pagina principale",
"h1": "Una seconda pagina, per dimostrare il routing",
"title": "Seconda pagina"
}
File: examples/app-router-no-locale-path/app/i18n/locales/it/translation.json
{
"h1": "Un semplice esempio",
"title": "Casa",
"to-client-page": "Alla pagina client",
"to-second-page": "Alla seconda pagina",
"welcome": "Benvenuti a Next.js 16 <1>con l'App Router</1> e next-i18next",
"blog": {
"text": "Scopri il corrispondente <1>articolo</1> che descrive questo esempio.",
"link": "https://www.locize.com/blog/next-i18next-v16"
}
}
File: examples/app-router-simple/README.md
next-i18next App Router example (locale-in-path)
This example shows how to use next-i18next v16 with the Next.js App Router and locale-in-path routing (/en/about, /de/about).
It demonstrates:
- Server Components using
getT()fromnext-i18next/server - Client Components using
useT()fromnext-i18next/client - Language detection and routing via
createProxy()inproxy.ts I18nProviderfor client-side hydrationgenerateStaticParamsfor static generation of all localesTranscomponent usage in both server and client components
Getting started
npm install
npm run devBlog post
There's also a blog post describing the i18n approach in more detail.
File: examples/app-router-simple/package.json
{
"name": "next-app-dir-i18next-example-ts",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"build": "next build",
"dev": "next dev",
"dev-turbo": "next dev --turbo",
"lint": "eslint .",
"start": "next start",
"migrateToLocize": "locize migrate --project-id=01b2e5e8-6243-47d1-b36f-963dbb8bcae3 --api-key=c9805cd1-fd50-4d98-967a-383b39066de8 --cdn-type pro --path=./app/i18n/locales",
"downloadLocales": "locize download --project-id=01b2e5e8-6243-47d1-b36f-963dbb8bcae3 --cdn-type pro --ver=latest --clean=true --path=./app/i18n/locales && npm run i18next-resources-for-ts",
"syncLocales": "locize sync --project-id=01b2e5e8-6243-47d1-b36f-963dbb8bcae3 --api-key=bd8cb856-d6b3-4c82-a456-862c729a1674 --cdn-type pro --ver=latest --path=./app/i18n/locales --dry=true",
"i18next-resources-for-ts": "i18next-resources-for-ts interface -i ./app/i18n/locales/en -o ./@types/resources.d.ts",
"ts": "next build && tsc --noEmit && rm tsconfig.tsbuildinfo"
},
"dependencies": {
"i18next": "^26.4.0",
"next": "^16.3.1",
"next-i18next": "^16.0.10",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.12"
},
"devDependencies": {
"@types/node": "^25.9.5",
"@types/react": "^19.2.18",
"eslint": "^9.39.5",
"eslint-config-next": "^16.3.1",
"i18next-resources-for-ts": "^2.1.0",
"locize-cli": "^12.5.1",
"typescript": "^5.9.3"
}
}
File: examples/app-router-simple/tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
".next/types//*.ts",
"/.ts",
"**/.tsx",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
File: examples/app-router-simple/app/i18n/locales/de/client-page.json
{
"back-to-home": "Zurück zur Hauptseite",
"counter_one": "eines ausgewählt",
"counter_other": "{{count}} ausgewählt",
"counter_zero": "keines ausgewählt",
"h1": "Eine client Seite, um das client-seitige i18n zu demonstrieren",
"title": "Client Seite",
"to-second-client-page": "zur zweiten client Seite"
}
File: examples/app-router-simple/app/i18n/locales/de/footer.json
{
"description": "Dies ist eine Nicht-Seitenkomponente, die einen eigenen Namespace erfordert",
"helpLocize": "Wenn Sie <1>Locize</1> einsetzen, unterstützen Sie direkt die Zukunft von <3>i18next</3>.",
"languageSwitcher": "Wechseln von <1>{{lng}}</1> nach: "
}
File: examples/app-router-simple/app/i18n/locales/de/second-client-page.json
{
"back-to-home": "Zurück zur Hauptseite",
"h1": "Eine zweite client Seite, um das client-seitige i18n zu demonstrieren",
"title": "Zweite client Seite",
"to-client-page": "zur client Seite"
}
File: examples/app-router-simple/app/i18n/locales/de/second-page.json
{
"back-to-home": "Zurück zur Hauptseite",
"h1": "Eine zweite Seite, um das Routing zu demonstrieren",
"title": "Zweite Seite"
}
File: examples/app-router-simple/app/i18n/locales/de/translation.json
{
"h1": "Ein einfaches Beispiel",
"title": "Hauptseite",
"to-client-page": "Zur client seitigen Seite",
"to-second-page": "Zur zweiten Seite",
"welcome": "Willkommen zu Next.js 16 <1>mit dem App Router</1> und next-i18next",
"blog": {
"text": "Schau dir den entsprechenden <1>Blogbeitrag</1> dieses Beispiels an.",
"link": "https://www.locize.com/blog/next-i18next-v16"
}
}
File: examples/app-router-simple/app/i18n/locales/en/client-page.json
{
"back-to-home": "Back to home",
"counter_one": "one selected",
"counter_other": "{{count}} selected",
"counter_zero": "none selected",
"h1": "A client page, to demonstrate client side i18n",
"title": "Client page",
"to-second-client-page": "to second client page"
}
File: examples/app-router-simple/app/i18n/locales/en/footer.json
{
"description": "This is a non-page component that requires its own namespace",
"helpLocize": "With using <1>Locize</1> you directly support the future of <3>i18next</3>.",
"languageSwitcher": "Switch from <1>{{lng}}</1> to: "
}
File: examples/app-router-simple/app/i18n/locales/en/second-client-page.json
{
"back-to-home": "Back to home",
"h1": "A second client page, to demonstrate client side i18n",
"title": "Second client page",
"to-client-page": "to client page"
}
File: examples/app-router-simple/app/i18n/locales/en/second-page.json
{
"back-to-home": "Back to home",
"h1": "A second page, to demonstrate routing",
"title": "Second page"
}
File: examples/app-router-simple/app/i18n/locales/en/translation.json
{
"h1": "A simple example",
"title": "Home",
"to-client-page": "To client page",
"to-second-page": "To second page",
"welcome": "Welcome to Next.js 16 <1>with the App Router</1> and next-i18next",
"blog": {
"text": "Check out the corresponding <1>blog post</1> describing this example.",
"link": "https://www.locize.com/blog/next-i18next-v16"
}
}
File: examples/app-router-simple/app/i18n/locales/it/client-page.json
{
"back-to-home": "Tornare alla pagina principale",
"counter_many": "selezionati {{count}}",
"counter_one": "uno selezionato",
"counter_other": "{{count}} selezionati",
"counter_zero": "nessuno selezionato",
"h1": "Una pagina client, per dimostrare il i18n nel client",
"title": "Pagina client",
"to-second-client-page": "alla seconda pagina client"
}
File: examples/app-router-simple/app/i18n/locales/it/footer.json
{
"description": "Questo è un componente non di pagina che richiede il proprio spazio dei nomi",
"helpLocize": "Con l'utilizzare <1>Locize</1> sostieni direttamente il futuro di <3>i18next</3>.",
"languageSwitcher": "Passa da <1>{{lng}}</1> a: "
}
File: examples/app-router-simple/app/i18n/locales/it/second-client-page.json
{
"back-to-home": "Tornare alla pagina principale",
"h1": "Una seconda pagina client, per dimostrare il i18n nel client",
"title": "Seconda pagina client",
"to-client-page": "alla pagina client"
}
File: examples/app-router-simple/app/i18n/locales/it/second-page.json
{
"back-to-home": "Tornare alla pagina principale",
"h1": "Una seconda pagina, per dimostrare il routing",
"title": "Seconda pagina"
}
File: examples/app-router-simple/app/i18n/locales/it/translation.json
{
"h1": "Un semplice esempio",
"title": "Casa",
"to-client-page": "Alla pagina client",
"to-second-page": "Alla seconda pagina",
"welcome": "Benvenuti a Next.js 16 <1>con l'App Router</1> e next-i18next",
"blog": {
"text": "Scopri il corrispondente <1>articolo</1> che descrive questo esempio.",
"link": "https://www.locize.com/blog/next-i18next-v16"
}
}
File: examples/mixed-routers/package.json
{
"name": "next-i18next-example-mixed-routers",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start -p ${PORT:=3000}",
"typecheck": "tsc --project ./tsconfig.json --noEmit"
},
"dependencies": {
"i18next": "^26.4.0",
"next": "^16.3.1",
"next-i18next": "^16.4.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.12"
},
"devDependencies": {
"@types/node": "^25.9.5",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"typescript": "^5.9.3"
}
}
File: examples/mixed-routers/tsconfig.json
{
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [{ "name": "next" }]
},
"include": [
"next-env.d.ts",
".next/types//*.ts",
"/.ts",
"**/.tsx",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
File: examples/mixed-routers/vercel.json
{
"github": {
"silent": true
}
}
File: examples/mixed-routers/public/locales/de/common.json
{
"h1": "next-i18next",
"description": "Der einfachste Weg, deine Next.js Apps zu übersetzen.",
"change-locale": "Sprache wechseln zu "{{changeTo}}"",
"to-second-page": "Zur zweiten Seite",
"to-app-router": "Zum App Router Bereich",
"to-pages-router": "Zum Pages Router Bereich",
"title": "Hauptseite | next-i18next",
"routers.title": "App Router und Pages Router",
"routers.text": "Seit v16 unterstützt next-i18next den Next.js App Router: getT in Server Components, useT in Client Components und createProxy für proxy.ts. Diese Website nutzt beide Router nebeneinander; diese Seite läuft mit dem Pages Router.",
"icu.question": "Verwendest du ICU MessageFormat oder MF2?",
"icu.answer": "Seit v16.4.0 importierst du I18nProvider aus next-i18next/icu oder next-i18next/mf2 und behältst deine ICU- oder MF2-Texte, in Server und Client Components.",
"icu.cta": "Zum app-router-icu Beispiel",
"icu.link": "https://github.com/i18next/next-i18next/tree/master/examples/app-router-icu",
"blog.v16.question": "Was ist neu in next-i18next v16?",
"blog.v16.answer": "Schau dir <1>den Blogbeitrag</1> für die ganze Geschichte an.",
"blog.v16.link": "https://www.locize.com/blog/next-i18next-v16",
"blog.appDir.question": "Verwendest du den Next.js App Router?",
"blog.appDir.answer": "Dann schau dir <1>diesen Blogbeitrag</1> an.",
"blog.appDir.link": "https://www.locize.com/blog/i18n-next-app-router",
"blog.optimized.question": "Möchtest du einige Superkräfte entfesseln, um für alle Seiten optimierte Übersetzungen zu haben?",
"blog.optimized.answer": "Dann schaue dir vielleicht <1>diesen Blogbeitrag</1> an.",
"blog.optimized.link": "https://www.locize.com/blog/next-i18next",
"blog.ssg.question": "Möchtest du SSG (next export) verwenden?",
"blog.ssg.answer": "Dann schaue dir vielleicht <1>diesen Blogbeitrag</1> an.",
"blog.ssg.link": "https://www.locize.com/blog/next-i18n-static",
"blog.mcp.question": "Verwendest du KI-Coding-Assistenten wie Claude oder Cursor?",
"blog.mcp.answer": "Schau dir <1>diesen Blogbeitrag</1> an, um mehr zu erfahren.",
"blog.mcp.link": "https://www.locize.com/blog/mcp-server"
}
File: examples/mixed-routers/public/locales/de/footer.json
{
"description": "Dies ist eine Nicht-Seitenkomponente, die einen eigenen Namespace erfordert",
"helpLocize": "Wenn Sie <1>Locize</1> einsetzen, unterstützen Sie direkt die Zukunft von <3>i18next</3>."
}
File: examples/mixed-routers/public/locales/de/second-page.json
{
"h1": "Eine zweite Seite, um das Routing zu demonstrieren",
"back-to-home": "Zurück zur Hauptseite",
"title": "Zweite Seite | next-i18next"
}
File: examples/mixed-routers/public/locales/en/common.json
{
"h1": "next-i18next",
"description": "The easiest way to translate your Next.js apps.",
"change-locale": "Change locale to "{{changeTo}}"",
"to-second-page": "To second page",
"to-app-router": "Go to App Router section",
"to-pages-router": "Go to Pages Router section",
"title": "Home | next-i18next",
"routers.title": "App Router and Pages Router",
"routers.text": "Since v16, next-i18next supports the Next.js App Router: getT in Server Components, useT in Client Components and createProxy for proxy.ts. This site runs both routers side by side; this page uses the Pages Router.",
"icu.question": "Using ICU MessageFormat or MF2?",
"icu.answer": "Since v16.4.0, import I18nProvider from next-i18next/icu or next-i18next/mf2 and keep your ICU or MF2 messages, in Server and Client Components.",
"icu.cta": "See the app-router-icu example",
"icu.link": "https://github.com/i18next/next-i18next/tree/master/examples/app-router-icu",
"blog.v16.question": "What's new in next-i18next v16?",
"blog.v16.answer": "Check out <1>the blog post</1> for the full story.",
"blog.v16.link": "https://www.locize.com/blog/next-i18next-v16",
"blog.appDir.question": "Are you using the Next.js App Router?",
"blog.appDir.answer": "Then check out <1>this blog post</1>.",
"blog.appDir.link": "https://www.locize.com/blog/i18n-next-app-router",
"blog.optimized.question": "Do you like to unleash some super powers to have all side optimized translations?",
"blog.optimized.answer": "Then you may have a look at <1>this blog post</1>.",
"blog.optimized.link": "https://www.locize.com/blog/next-i18next",
"blog.ssg.question": "Do you want to use SSG (next export)?",
"blog.ssg.answer": "Then you may have a look at <1>this blog post</1>.",
"blog.ssg.link": "https://www.locize.com/blog/next-i18n-static",
"blog.mcp.question": "Using AI coding assistants like Claude or Cursor?",
"blog.mcp.answer": "Check out <1>this blog post</1> to learn more.",
"blog.mcp.link": "https://www.locize.com/blog/mcp-server"
}
File: examples/mixed-routers/public/locales/en/footer.json
{
"description": "This is a non-page component that requires its own namespace",
"helpLocize": "With using <1>Locize</1> you directly support the future of <3>i18next</3>."
}
File: examples/mixed-routers/public/locales/en/second-page.json
{
"h1": "A second page, to demonstrate routing",
"back-to-home": "Back to home",
"title": "Second page | next-i18next"
}
File: examples/pages-router-auto-static-optimize/package.json
{
"name": "next-i18next-example-auto-static-optimize",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start -p ${PORT:=3000}",
"typecheck": "tsc --project ./tsconfig.json --noEmit",
"clean": "rimraf .next",
"nuke:install": "rimraf ./node_modules ./package-lock.json",
"toc": "i18next-resources-for-ts toc -i ./public/locales/en -o ./@types/resources.ts",
"merge": "i18next-resources-for-ts merge -i ./public/locales/en -o ./@types/resources.json",
"interface": "i18next-resources-for-ts interface -i ./public/locales/en -o ./@types/resources.d.ts"
},
"dependencies": {
"i18next": "^26.4.0",
"i18next-chained-backend": "^5.0.6",
"i18next-http-backend": "^3.0.6",
"i18next-localstorage-backend": "^4.3.2",
"next": "^16.3.1",
"next-i18next": "^16.0.10",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.12"
},
"devDependencies": {
"@types/node": "^25.9.5",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"i18next-resources-for-ts": "^2.1.0",
"picocolors": "^1.1.1",
"rimraf": "^6.1.3",
"typescript": "^5.9.3"
}
}
File: examples/pages-router-auto-static-optimize/tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "esnext",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"baseUrl": ".",
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true
},
"include": [
"next-env.d.ts",
"/*.ts",
"/.tsx",
"**/.js",
"/*.jsx",
"/.cjs",
"**/.mjs"
],
"exclude": [
"/node_modules",
"/.*/"
]
}
File: examples/pages-router-auto-static-optimize/vercel.json
{
"github": {
"silent": true
}
}
File: examples/pages-router-auto-static-optimize/public/locales/de/common.json
{
"h1": "Ein einfaches Beispiel",
"change-locale": "Sprache wechseln zu "{{changeTo}}"",
"to-second-page": "Zur zweiten Seite",
"to-auto-static-page": "To auto static page (de)",
"error-with-status": "Auf dem Server ist ein Fehler ({{statusCode}}) aufgetreten",
"error-without-status": "Auf dem Server ist ein Fehler aufgetreten",
"title": "Hauptseite | next-i18next",
"blog": {
"appDir": {
"question": "Verwendest du das neue Next.js 13/14 mit app directory?",
"answer": "Dann schau dir <1>diesen Blogbeitrag</1> an.",
"link": "https://www.locize.com/blog/next-app-dir-i18n/"
},
"optimized": {
"question": "Möchtest du einige Superkräfte entfesseln, um für alle Seiten optimierte Übersetzungen zu haben?",
"answer": "Dann schaue dir vielleicht <1>diesen Blogbeitrag</1> an.",
"link": "https://www.locize.com/blog/next-i18next-de/"
},
"ssg": {
"question": "Möchtest du SSG (next export) verwenden?",
"answer": "Dann schaue dir vielleicht <1>diesen Blogbeitrag</1> an.",
"link": "https://www.locize.com/blog/next-i18n-statisch/"
}
}
}
File: examples/pages-router-auto-static-optimize/public/locales/de/footer.json
{
"description": "Dies ist eine Nicht-Seitenkomponente, die einen eigenen Namespace erfordert",
"helpLocize": "Wenn Sie <1>Locize</1> einsetzen, unterstützen Sie direkt die Zukunft von <3>i18next</3>."
}
File: examples/pages-router-auto-static-optimize/public/locales/de/second-page.json
{
"h1": "Eine zweite Seite, um das Routing zu demonstrieren",
"back-to-home": "Zurück zur Hauptseite",
"title": "Zweite Seite | next-i18next"
}
File: examples/pages-router-auto-static-optimize/public/locales/de/staticpage.json
{ "hi": "hello_de" }
File: examples/pages-router-auto-static-optimize/public/locales/en/common.json
{
"h1": "A simple example",
"change-locale": "Change locale to "{{changeTo}}"",
"to-second-page": "To second page",
"to-auto-static-page": "To auto static page (en)",
"error-with-status": "A {{statusCode}} error occurred on server",
"error-without-status": "An error occurred on the server",
"title": "Home | next-i18next",
"blog": {
"appDir": {
"question": "Are you using the new Next.js 13/14 app directory?",
"answer": "Then check out <1>this blog post</1>.",
"link": "https://www.locize.com/blog/next-app-dir-i18n/"
},
"optimized": {
"question": "Do you like to unleash some super powers to have all side optimized translations?",
"answer": "Then you may have a look at <1>this blog post</1>.",
"link": "https://www.locize.com/blog/next-i18next/"
},
"ssg": {
"question": "Do you want to use SSG (next export)?",
"answer": "Then you may have a look at <1>this blog post</1>.",
"link": "https://www.locize.com/blog/next-i18n-static/"
}
}
}
File: examples/pages-router-auto-static-optimize/public/locales/en/footer.json
{
"description": "This is a non-page component that requires its own namespace",
"helpLocize": "With using <1>Locize</1> you directly support the future of <3>i18next</3>."
}
File: examples/pages-router-auto-static-optimize/public/locales/en/second-page.json
{
"h1": "A second page, to demonstrate routing",
"back-to-home": "Back to home",
"title": "Second page | next-i18next"
}
File: examples/pages-router-auto-static-optimize/public/locales/en/staticpage.json
{ "hi": "hello_en" }
File: examples/pages-router-simple/package.json
{
"name": "next-i18next-example-simple",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start -p ${PORT:=3000}",
"typecheck": "tsc --project ./tsconfig.json --noEmit",
"clean": "rimraf .next",
"nuke:install": "rimraf ./node_modules ./package-lock.json",
"toc": "i18next-resources-for-ts toc -i ./public/locales/en -o ./@types/resources.ts",
"merge": "i18next-resources-for-ts merge -i ./public/locales/en -o ./@types/resources.json",
"interface": "i18next-resources-for-ts interface -i ./public/locales/en -o ./@types/resources.d.ts"
},
"dependencies": {
"i18next": "^26.4.0",
"next": "^16.3.1",
"next-i18next": "^16.0.10",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.12"
},
"devDependencies": {
"@types/node": "^25.9.5",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"i18next-resources-for-ts": "^2.1.0",
"picocolors": "^1.1.1",
"rimraf": "^6.1.3",
"typescript": "^5.9.3"
}
}
File: examples/pages-router-simple/tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "esnext",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"baseUrl": ".",
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true
},
"include": [
"next-env.d.ts",
"/*.ts",
"/.tsx",
"**/.js",
"/*.jsx",
"/.cjs",
"**/.mjs"
],
"exclude": [
"/node_modules",
"/.*/"
]
}
File: examples/pages-router-simple/vercel.json
{
"github": {
"silent": true
}
}
File: examples/pages-router-simple/public/locales/de/common.json
{
"h1": "Ein einfaches Beispiel",
"change-locale": "Sprache wechseln zu "{{changeTo}}"",
"to-second-page": "Zur zweiten Seite",
"error-with-status": "Auf dem Server ist ein Fehler ({{statusCode}}) aufgetreten",
"error-without-status": "Auf dem Server ist ein Fehler aufgetreten",
"title": "Hauptseite | next-i18next",
"blog": {
"v16": {
"question": "Was ist neu in next-i18next v16?",
"answer": "Schau dir <1>den Blogbeitrag</1> für die ganze Geschichte an.",
"link": "https://www.locize.com/blog/next-i18next-v16"
},
"appDir": {
"question": "Verwendest du den Next.js App Router?",
"answer": "Dann schau dir <1>diesen Blogbeitrag</1> an.",
"link": "https://www.locize.com/blog/i18n-next-app-router"
},
"optimized": {
"question": "Möchtest du einige Superkräfte entfesseln, um für alle Seiten optimierte Übersetzungen zu haben?",
"answer": "Dann schaue dir vielleicht <1>diesen Blogbeitrag</1> an.",
"link": "https://www.locize.com/blog/next-i18next"
},
"ssg": {
"question": "Möchtest du SSG (next export) verwenden?",
"answer": "Dann schaue dir vielleicht <1>diesen Blogbeitrag</1> an.",
"link": "https://www.locize.com/blog/next-i18n-static"
}
}
}
File: examples/pages-router-simple/public/locales/de/footer.json
{
"description": "Dies ist eine Nicht-Seitenkomponente, die einen eigenen Namespace erfordert",
"helpLocize": "Wenn Sie <1>Locize</1> einsetzen, unterstützen Sie direkt die Zukunft von <3>i18next</3>."
}
File: examples/pages-router-simple/public/locales/de/second-page.json
{
"h1": "Eine zweite Seite, um das Routing zu demonstrieren",
"back-to-home": "Zurück zur Hauptseite",
"title": "Zweite Seite | next-i18next"
}
File: examples/pages-router-simple/public/locales/en/common.json
{
"h1": "A simple example",
"change-locale": "Change locale to "{{changeTo}}"",
"to-second-page": "To second page",
"error-with-status": "A {{statusCode}} error occurred on server",
"error-without-status": "An error occurred on the server",
"title": "Home | next-i18next",
"blog": {
"v16": {
"question": "What's new in next-i18next v16?",
"answer": "Check out <1>the blog post</1> for the full story.",
"link": "https://www.locize.com/blog/next-i18next-v16"
},
"appDir": {
"question": "Are you using the Next.js App Router?",
"answer": "Then check out <1>this blog post</1>.",
"link": "https://www.locize.com/blog/i18n-next-app-router"
},
"optimized": {
"question": "Do you like to unleash some super powers to have all side optimized translations?",
"answer": "Then you may have a look at <1>this blog post</1>.",
"link": "https://www.locize.com/blog/next-i18next"
},
"ssg": {
"question": "Do you want to use SSG (next export)?",
"answer": "Then you may have a look at <1>this blog post</1>.",
"link": "https://www.locize.com/blog/next-i18n-static"
}
}
}
File: examples/pages-router-simple/public/locales/en/footer.json
{
"description": "This is a non-page component that requires its own namespace",
"helpLocize": "With using <1>Locize</1> you directly support the future of <3>i18next</3>."
}
File: examples/pages-router-simple/public/locales/en/second-page.json
{
"h1": "A second page, to demonstrate routing",
"back-to-home": "Back to home",
"title": "Second page | next-i18next"
}
File: examples/pages-router-ssg/README.md
Static internationalized (i18n) next.js website with the help of [next-i18next](https://github.com/i18next/next-i18next) and [next-language-detector](https://github.com/i18next/next-language-detector)
What is this?
This is a simple example of how to use next-i18next with Next.js for a complete static website and optional use of Locize to get translations up and running quickly and easily.
For more info...
You may have arrived here from the Next.js repository, from next-i18next repository or the react-i18next repository. Either way, for more documentation on:
- next-i18next, please visit the main README
- Next.js, please visit the website
- Locize in combination with Next.js, please visit the main README
A step by step guide can be found in this tutorial.
optional [Locize](https://www.locize.com?utm_source=next_i18next_pages_router_ssg_readme&utm_medium=github&utm_campaign=examples_pages_router_ssg_readme) usage (not mandatory)
Before "deploying" your app, you can run the downloadLocales script (or similar), which will use the cli to download the translations from locize into the appropriate folder next-i18next is looking in to (i.e. ./public/locales).
Verify yourself
To prove that this works, pull this example to your local machine and run:
npm i
npm run build
npm run serveand open your browser at http://localhost:8080
You can now deploy the out folder to any static webserver, like GitHub pages.
File: examples/pages-router-ssg/package.json
{
"name": "next-i18next-example-ssg",
"version": "13.0.0",
"license": "MIT",
"scripts": {
"404": "cp out/404/index.html out/404.html",
"dev": "next dev",
"build": "next build",
"postbuild": "npm run 404",
"server": "next start",
"start": "npm run dev",
"lint": "eslint .",
"test": "npm run lint",
"serve": "http-server out -p 8080",
"downloadLocales": "locize download --project-id=0842ada9-1d1d-4d48-ab27-08f6a132f558 --cdn-type pro --ver=latest --clean=true --path=./public/locales",
"clean": "rimraf .next out"
},
"dependencies": {
"i18next": "^26.4.0",
"next": "^16.3.1",
"next-i18next": "^16.0.10",
"next-language-detector": "^1.1.1",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.12"
},
"devDependencies": {
"eslint": "^9.39.5",
"eslint-config-next": "^16.3.1",
"http-server": "^14.1.1",
"locize-cli": "^12.5.1",
"rimraf": "^6.1.3"
}
}
File: examples/pages-router-ssg/public/locales/de/404.json
{
"h1": "Die von Ihnen gesuchte Seite ist nicht verfügbar oder gehört nicht zu dieser Website!",
"title": "Seite nicht gefunden"
}
File: examples/pages-router-ssg/public/locales/de/common.json
{
"back-to-home": "Zurück zur Hauptseite",
"error-with-status": "Auf dem Server ist ein Fehler ({{statusCode}}) aufgetreten",
"error-without-status": "Auf dem Server ist ein Fehler aufgetreten",
"h1": "Ein einfaches Beispiel",
"title": "Hauptseite | next-i18next",
"to-second-page": "Zur zweiten Seite"
}
File: examples/pages-router-ssg/public/locales/de/footer.json
{
"change-locale": "Sprache wechseln",
"description": "Dies ist eine Nicht-Seitenkomponente, die einen eigenen Namespace erfordert"
}
File: examples/pages-router-ssg/public/locales/de/second-page.json
{
"h1": "Eine zweite Seite, um das Routing zu demonstrieren",
"title": "Zweite Seite | next-i18next"
}
File: examples/pages-router-ssg/public/locales/en/404.json
{
"h1": "The page you are looking for is not available or doesn't belong to this website!",
"title": "Page not found"
}
File: examples/pages-router-ssg/public/locales/en/common.json
{
"back-to-home": "Back to home",
"error-with-status": "A {{statusCode}} error occurred on server",
"error-without-status": "An error occurred on the server",
"h1": "A simple example",
"title": "Home | next-i18next",
"to-second-page": "To second page"
}
File: examples/pages-router-ssg/public/locales/en/footer.json
{
"change-locale": "Change locale",
"description": "This is a non-page component that requires its own namespace"
}
File: examples/pages-router-ssg/public/locales/en/second-page.json
{
"h1": "A second page, to demonstrate routing",
"title": "Second page | next-i18next"
}
File: examples/pages-router-ssg/public/locales/it/404.json
{
"h1": "La pagina che stai cercando non è disponibile o non appartiene a questo sito!",
"title": "Pagina non trovata"
}
File: examples/pages-router-ssg/public/locales/it/common.json
{
"back-to-home": "Torna alla pagina principale",
"error-with-status": "Sul server si verificato un errore ({{statusCode}})",
"error-without-status": "Sul server si verificato un errore",
"h1": "Un semplice esempio",
"title": "Pagina principale | next-i18next",
"to-second-page": "Vai alla seconda pagina"
}
File: examples/pages-router-ssg/public/locales/it/footer.json
{
"change-locale": "Cambia lingua",
"description": "Questo è un componente non di pagina che richiede il proprio namespace"
}
File: examples/pages-router-ssg/public/locales/it/second-page.json
{
"h1": "Una seconda pagina per dimostrare il routing",
"title": "Seconda pagina | next-i18next"
}
File: .github/stale.yml
Number of days of inactivity before an issue becomes stale
daysUntilStale: 7
Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
Issues with these labels will never be considered stale
exemptLabels:
- 'discussion'
- 'feature request'
- 'bug'
- 'breaking change'
- 'doc'
- 'issue'
- 'help wanted'
- 'good first issue'
- 'pr hold'
Label to use when marking an issue as stale
staleLabel: stale
Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
File: .github/workflows/ci.yml
name: CI
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- ''
paths-ignore:
- 'docs/'
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
node-version: [24.x, 22.x]
steps:
- uses: actions/checkout@v6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: 📥 Install
run: npm install
- name: Install Examples
run: npm run install:examples
- name: Typecheck
run: npm run typecheck
- name: Unit tests
run: npm test
- name: Build next-i18next
run: npm run build
# Copy built dist + package.json into each example's node_modules/next-i18next
# so examples use the current build instead of the published npm version.
- name: Replace examples install with repo build
run: npm run move-build-to-examples
- name: Build examples
run: npm run build:examples
- name: E2E tests
run: npm run test:e2e
- name: Typecheck pages-router-simple example
run: npm run typecheck:example:pages-router-simpleFile: .github/workflows/next-compat.yml
name: Next.js compatibility (latest + canary)
Catches upstream Next.js regressions (like the 13.5.4 bundling breakage, #2214)
within days instead of via user reports: builds and e2e-tests the example apps
against next@latest and next@canary on a weekly schedule.
on:
schedule:
- cron: '0 6 * * 1' # Mondays 06:00 UTC
workflow_dispatch:
permissions:
contents: read
jobs:
examples:
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
next-version: [latest, canary]
steps:
- uses: actions/checkout@v6
- name: Use Node.js 24.x
uses: actions/setup-node@v6
with:
node-version: 24.x
- name: 📥 Install
run: npm install
- name: Install Examples
run: npm run install:examples
- name: Override next with ${{ matrix.next-version }} (root + examples)
run: |
npm install --no-save "next@${{ matrix.next-version }}"
for dir in examples/*/; do
(cd "$dir" && npm install --no-save --no-audit --no-fund "next@${{ matrix.next-version }}")
done
echo "Resolved versions:"
node -p "require('next/package.json').version"
for dir in examples/*/; do
node -p "'$dir: ' + require('./' + '$dir' + 'node_modules/next/package.json').version"
done
- name: Typecheck
run: npm run typecheck
- name: Unit tests
run: npm test
- name: Build next-i18next
run: npm run build
- name: Replace examples install with repo build
run: npm run move-build-to-examples
- name: Build examples
run: npm run build:examples
- name: E2E tests
run: npm run test:e2e