next-i18next Documentation

The easiest way to translate your NextJs apps.

6,166 stars TypeScript TypeScript 5 files · ~35,748 tokens #i18next#next#nextjs#now#react#react-i18next#reactjs#ssr
RAW Doc

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 basePath scoping
  • 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 / serverSideTranslations API preserved under next-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

1. Install

bash
npm install next-i18next i18next react-i18next

2. 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):

text
public/locales/en/common.json
public/locales/en/home.json
public/locales/de/common.json
public/locales/de/home.json

Serverless platforms (Vercel, AWS Lambda, etc.): Files in public/ are served via CDN but are not available on the filesystem at runtime. Use resourceLoader with dynamic imports instead (see below).

In app/i18n/locales/ (bundled via dynamic imports, requires resourceLoader — works everywhere including serverless):

text
app/i18n/locales/en/common.json
app/i18n/locales/de/common.json

3. Configuration

Create a config file (e.g., i18n.config.ts):

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 i18nConfig

The 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 I18nConfig from next-i18next/proxy (not from next-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 restarting next 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()-based resourceLoader: dynamic import() 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 uses fs.readFile and production keeps bundler-traceable import():

ts
const 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 fs and are unaffected.

4. Proxy

Create proxy.ts at your project root (Next.js 16+ replaces middleware.ts with proxy.ts):

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: createMiddleware from next-i18next/middleware is 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: false if another system owns that cookie, and cookieOptions to scope it (e.g. { domain: '.example.com', secure: true })

5. Root Layout

tsx
// 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 layout
  • getResources(i18n) — serializes loaded translations for client hydration
  • I18nProvider — wraps children so client components can use useT()

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:

ts
// i18n.server.ts
import { createServerI18next } from 'next-i18next/server'
import i18nConfig from './i18n.config'

export const { getT, getResources, generateI18nStaticParams } = createServerI18next(i18nConfig)
tsx
// 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

tsx
// 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:

tsx
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

tsx
'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:

tsx
'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:

tsx
'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:

ts
const i18nConfig: I18nConfig = {
  supportedLngs: ['en', 'de'],
  fallbackLng: 'en',
  hideDefaultLocale: true,
}

In this mode:

  • /about serves the default language (English) — no prefix needed
  • /de/about serves German — non-default locales keep their prefix
  • /en/about automatically 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:

ts
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 from I18nProvider)
  • Use useChangeLanguage() for language switching (updates cookie + triggers server re-render):
tsx
'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:

tsx
// 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':

ts
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, so generateI18nStaticParams() prerenders every locale
  • The proxy detects the language (cookie > Accept-Language > fallback) and rewrites /about to /de/about internally — 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
  • hideDefaultLocale has 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:

ts
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

bash
npm install i18next-icu intl-messageformat
# or, for MF2
npm install i18next-mf2

Both 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:

ts
// 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 i18nConfig

app/i18n/locales/en/common.json:

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:

tsx
// 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 example bidiIsolation: 'none' for MF2) have to be passed to I18nProvider as well, via its i18nextOptions prop, 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 use out of the shared config and add it where the server is initialized: initServerI18next({ ...i18nConfig, use: [ICU] }).
  • For MF2, put MF2 from i18next-mf2 into use and import I18nProvider from next-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

tsx
// 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:

tsx
<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

tsx
'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:

ts
// @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'
  }
}
tsx
// 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:

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:

js
// i18n.shared.js
module.exports = {
  supportedLngs: ['en', 'de'],
  fallbackLng: 'en',
  defaultNS: 'common',
  ns: ['common', 'footer'],
}

App Router config with basePath:

ts
// 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 i18nConfig

Pages Router config:

js
// 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/page to /app-router/en/page
  • Sets the language header for Server Components under that prefix
ts
// 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:

js
const { i18n } = require('./next-i18next.config.js')

module.exports = {
  i18n,
  reactStrictMode: true,
}

Directory structure

text
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 files

App 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:

js
// next-i18next.config.js
module.exports = {
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'de'],
  },
}
js
// next.config.js
const { i18n } = require('./next-i18next.config.js')
module.exports = { i18n }
tsx
// pages/_app.tsx
import { appWithTranslation } from 'next-i18next/pages'

const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />
export default appWithTranslation(MyApp)
tsx
// 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

ts
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

ts
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:

ts
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:

tsx
<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:

tsx
'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:

  1. Bundling translations at build time via resources or resourceLoader with dynamic imports
  2. 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):

  1. npm install next-i18next@16
  2. Create an i18n.config.ts with your languages and namespaces
  3. Replace your custom proxy/middleware with createProxy(config) in proxy.ts
  4. Replace your custom getT / translation init with initServerI18next + getT from next-i18next/server
  5. Replace your custom I18nProvider with the one from next-i18next/client
  6. Replace useTranslation with useT from next-i18next/client in client components

Pages Router users

  1. Update imports from next-i18next to next-i18next/pages
  2. Update serverSideTranslations import to next-i18next/pages/serverSideTranslations
  3. Update type imports: import type { TFunction, WithTranslation, I18n } from 'next-i18next/pages'
  4. Everything else works the same

Contributors

Thanks goes to these wonderful people (emoji key):

Rob Capellini💻 ⚠️ Alexander Kachkaev📢 💬 🤔 💻 ⚠️ Mathias Wøbbe💻 🤔 ⚠️ Lucas Feliciano🤔 👀 Ryan Leung💻 Nathan Friemel💻 📖 💡 🤔 Isaac Hinman💻 📖 🤔 🚧
Adriano Raiano💻 📖 🤔 🚧 Felix Mosheev💬 💻 ⚠️ Sébastien Vanvelthem💻 📖

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 of t from getT and useT and the values of IcuTrans and IcuTransWithoutContext (react-i18next 17.1.0) are type-checked against the message's arguments, with the resources file from npx i18next-cli types for literal message types. parseInterpolation: false stays the setting for i18next 26.2.0 to 26.4.x. No code changes: getT and useT return i18next's TFunction, so they get the checks as is.
  • Example app-router-icu uses parseInterpolation: 'icu' with an i18next.config.ts and a generated @types/resources.d.ts.

16.4.0

  • App Router: ICU and MF2 without a 'use client' wrapper - next-i18next/icu and next-i18next/mf2 export an I18nProvider with the same props as the one from next-i18next/client, with i18next-icu or i18next-mf2 registered on the client instance. Render it from the root layout and keep use: [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 with IcuTransWithoutContext from react-i18next 17.0.21 in Server Components, parseInterpolation: false for TypeScript, extract.messageFormat in i18next-cli 1.76.0) and a new example, app-router-icu.

16.3.2

  • App Router: useT no longer re-renders on every URL change — it called useParams() 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, including history.replaceState/pushState and search-param updates. Every component using useT re-rendered each time, even in localeInPath: false mode where there is no such segment. The sync now lives in a single component inside I18nProvider that renders nothing, and useT is a typed useTranslation. #2349
  • App Router: useT no longer breaks next build with cacheComponents: true — that sync component only mounts in the browser, so useParams() is no longer called during server rendering. A useT caller on a route whose params are not all covered by generateStaticParams used 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, and I18nProvider pinned supportedLngs to whatever resources happened to contain, so i18next resolved every other language back to the fallback and client components stayed in the old language. supportedLngs is no longer derived from resources (pass the prop to restrict it), and each new resources prop 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. useChangeLanguage leaves that switch to the provider when the store holds nothing for the target language yet, so i18n.resolvedLanguage no 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 on initServerI18next having run first in the same module instance. Create it once in an i18n.server.ts and 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 reach getT uninitialized. initServerI18next and the module-level functions keep working. #2347
  • App Router: default server state is keyed on globalThis — a second copy of next-i18next/server in the same process (another bundling layer, or the CJS build next to the ESM one) now sees the same initServerI18next call instead of throwing. The "not initialized" error also names the real requirement (before the first getT() in the process) and points at createServerI18next. #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 hideDefaultLocale redirects, 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's maxAge is refreshed only on change. #2346
  • App Router: persistCookie and cookieOptions — persistCookie: false makes 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, and useChangeLanguage(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 with new URL(path, req.url), which drops the base path that Next keeps inside req.nextUrl, so with basePath: '/nxt' in next.config every locale redirect, and every rewrite in hideDefaultLocale and 'internal' mode, landed outside the app and 404'd. Targets are now built from req.nextUrl.clone(), which also preserves trailingSlash, and the referer-based cookie persistence strips the base path before looking for a locale. The basePath option in the i18n config is unrelated: it is the route-folder prefix for mixed-router setups (app/app-router/[lng]/…). #2345
  • App Router: ssrBackend prop on I18nProvider — opt in to applying the backends from use during the server render pass as well. With i18nextOptions: { react: { useSuspense: true } } a namespace missing from resources suspends 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 as i18next-resources-to-backend with 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 under app/[lng]/, but the proxy rewrites /about to /{lng}/about internally (cookie → Accept-Language → fallback), so the public URL never shows a locale while every locale is still prerendered via generateStaticParams. Explicit locale paths (/de/about) are served as-is, never redirected, so <Link> prefetches cannot flip the language cookie; switch languages with useChangeLanguage(). This is the mode for Next.js Cache Components (cacheComponents: true), where localeInPath: false cannot be used because reading headers()/cookies() makes every route dynamic. #2345
  • App Router: getT() resolves the language from next/root-params first (Next.js 16.3+) — root params are part of the route key, so unlike headers()/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 calls loadLanguages(lng) and checks hasLoadedNamespace(ns, { lng }) for the requested language instead of the instance's own. The default still preloads every supported language; set i18nextOptions: { preload: [] } to load each language on its first request, keeping memory proportional to the locales actually served. #2345
  • App Router: localeParamName option — name of the locale route segment (default 'lng'). generateI18nStaticParams() keys its params by it, so an app/[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 — I18nProvider is a Client Component, but the App Router also renders it on the server, once per request, and its useState initializer therefore built a throwaway i18next instance in Node on every render. With a backend passed via use, that instance got a Node-side backend, and backends that refresh on a timer never let it go: i18next-locize-backend defaults reloadInterval to 1 hour whenever window is 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, and partialBundledLanguages is off during the server pass, so SSR renders purely from resources. Non-backend plugins (detectors, post-processors) still apply in both passes.
  • getResources accepts 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 saveMissing via .finally — the #2344 fix re-enabled saveMissing in a .then on the changeLanguage promise; now uses .finally so a hypothetically rejected promise cannot leave saveMissing disabled for the rest of the session (defensive only: current i18next never rejects that promise)

16.0.9

  • Pages Router: no more missingKeyHandler false positives on locale change — appWithTranslation reuses 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 (the changeLanguage call runs in a layout effect). With saveMissing: true, keys absent from the previous language were reported as missing during that transitional render even though they exist in the destination locale. saveMissing is now suspended while the language handoff is pending and restored once changeLanguage completes. #2344

16.0.8

  • Pages Router: self-diagnosing serverSideTranslations error — the "Initial locale argument was not passed into serverSideTranslations" error now names its two common causes (missing i18n section in next.config.js; pages rendered outside Next.js locale routing such as custom 404/500 or output: 'export') and shows the explicit-locale fix inline.
  • CI: weekly Next.js compatibility workflow — builds and e2e-tests all example apps against next@latest and next@canary on 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 i18next in client bundles — v16 shipped the Pages Router as CJS-only, which forced bundlers to load i18next via its require condition (i18next/dist/cjs) while user code loaded it via the import condition (i18next/dist/esm), producing two copies of i18next in the same bundle. The Pages Router now builds dual ESM + CJS outputs, and the ./pages and ./pages/serverSideTranslations exports declare matching import/require conditions. The server-only filesystem branch of createConfig has been extracted into a separate serverSideConfig module (injected via hook by serverSideTranslations) so createConfig no longer pulls Node built-ins into client bundles. Note: appWithTranslation no longer re-runs the server-side validation/preload during SSR — that work happens once, up-front, inside serverSideTranslations (in practice a no-op since appWithTranslation uses the browser client with pre-loaded resources). #2342

16.0.6

  • Pages Router: reloadOnPrerender now actually reloads — the option was previously calling reloadResources() on the browser-side i18next instance returned by appWithTranslation (which has no FS backend and is often null during getStaticProps/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 restarting next dev. Gated behind NODE_ENV !== 'production' to keep custom HTTP/locize/chained backends from being refetched on every prerender call. #2123
  • App Router: reloadOnPrerender is 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 or use-provided custom backend) before each render in development, deduplicated across getT calls within a single render via React's cache(). No effect in production builds. Note: when using a dynamic import()-based resourceLoader, 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, and UseTranslation types 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 createConfig where i18next v26's readonly preload type was incompatible with the internal config type

16.0.3

  • New hideDefaultLocale option (App Router) — when enabled, the default language is served without a URL prefix (/about instead 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 with basePath too. #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 a resourceLoader code example #2337
  • partialBundledLanguages + resources — when both resources and partialBundledLanguages: true are 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.resources now works on the server — resources was typed and documented but ignored at runtime by initServerI18next / getT. The resource backend is now skipped when resources is provided, and the pre-loaded translations are passed directly to i18next.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-i18next import now exports App Router utilities (defineConfig, normalizeConfig, I18nConfig). Pages Router users must update to next-i18next/pages.
  • Removed i18next-fs-backend dependency: Pages Router server-side loading now uses i18next-resources-to-backend with fs.readFileSync internally. 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, and generateMetadata. 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 via use prop.
    • initServerI18next(config) — one-time server configuration setup
    • getResources(i18n, namespaces?) — extract loaded resources for client hydration
    • generateI18nStaticParams() — helper for generateStaticParams
  • Proxy support (Next.js 16+) — createProxy() from next-i18next/proxy for the new proxy.ts file convention. createMiddleware() from next-i18next/middleware remains 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
  • basePath option — scope the proxy to a URL prefix (e.g., /app-router) for mixed App Router + Pages Router setups
  • No-locale-path mode — localeInPath: false for cookie-based language without URL prefixes. Use useChangeLanguage() 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 use config option. The default resource loader is skipped automatically when a custom backend is provided.
  • resourceLoader option — 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/pages and next-i18next/pages/serverSideTranslations
  • appWithTranslation, useTranslation, Trans, serverSideTranslations — unchanged behavior
  • Defensive .filter(Boolean) on config.use arrays 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 the partialBundledLanguages option 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.

js
/** @type {import('next').NextConfig} */
const nextConfig = {
  transpilePackages: ['i18next'],
}
module.exports = nextConfig

13.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

  • #2089, more stable impelementation of "feat(server-side): custom default config path #2084"

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.localeDetection in 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     # PNPM

    This 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-i18next config 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

  1. Clone the repo
  2. Ensure you have a node version >=20, npm > 8

Working on the code

Run npm install

bash
npm install

Useful 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.

bash
npm run install:example:simple

To run the e2e test suite

bash
npm run build && npm run move-build-to-examples
npm run build:examples
npm run test:e2e

Working on examples

Examples nextjs apps live in the examples/ folder.

bash
npm run install:examples
npm run build:examples

PRs

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:

text
You will need to pass in an i18next instance by using initReactI18next

To fix:

  1. Ensure no duplicate versions of ì18next and/or react-i18next co-exists. See debug installation.
  2. 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.

text
(...)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).

  1. 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)
  2. For getServerSideProps() and getStaticProps() 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:

    typescript
    import { 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

  1. 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")

  2. 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 manually

    PM Check Fix (only on semver)
    yarn 1 npx -y yarn-deduplicate --list npx -y yarn-deduplicate
    yarn 2+ yarn dedupe --list yarn dedupe
    pnpm < 7.23.0 npx -y pnpm-deduplicate --list npx -y pnpm-deduplicate
    pnpm >= 7.23.0 not available pnpm dedupe
    npm 8 not available npm dedupe

    You can also list duplicate with npm why -r next-i18next i18next, pnpm why -r next-i18next i18next
    or yarn why -R next-i18next && yarn why -R i18next.

  3. 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] in i18n.config.ts, so getT() formats ICU messages in Server Components
  • I18nProvider from next-i18next/icu in the root layout, so useT() formats them in Client Components, without a 'use client' wrapper of your own
  • a plural with an exact =0 case and a select, on the server and on the client
  • rich text with IcuTransWithoutContext from react-i18next/IcuTransWithoutContext in a Server Component, and IcuTrans in a Client Component
  • parseInterpolation: 'icu' in @types/i18next.d.ts (i18next 26.5.0 or later), so t() options and IcuTrans values are type-checked against the ICU arguments of each message; @types/resources.d.ts holds the messages as literal types and is generated with npx i18next-cli types from i18next.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

bash
npm install
npm run dev

File: 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() from next-i18next/server
  • Client Components using useT() from next-i18next/client
  • Cookie-based language detection via createProxy() in proxy.js (no URL redirects)
  • useChangeLanguage() hook for switching languages
  • I18nProvider for client-side hydration

Getting started

bash
npm install
npm run dev

Blog 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"
}


{
"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"
}


{
"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"
}


{
"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() from next-i18next/server
  • Client Components using useT() from next-i18next/client
  • Language detection and routing via createProxy() in proxy.ts
  • I18nProvider for client-side hydration
  • generateStaticParams for static generation of all locales
  • Trans component usage in both server and client components

Getting started

bash
npm install
npm run dev

Blog 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"
}


{
"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"
}


{
"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"
}


{
"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"
}


{
"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"
}


{
"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/"
}
}
}


{
"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/"
}
}
}


{
"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"
}
}
}


{
"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"
}
}
}


{
"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:

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:

sh
npm i
npm run build
npm run serve

and 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"
}


{
"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"
}


{
"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"
}


{
"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

text
- 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-simple

File: .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

text
- 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