Skip to content
fluenti

Next.js

Fluenti integrates with Next.js App Router via @fluenti/next, supporting both Client Components and React Server Components (RSC). For general SSR concepts, see the SSR & Hydration guide.

The withFluenti() wrapper in next.config.ts is the recommended way to set up Fluenti in Next.js:

next.config.ts
import { withFluenti } from '@fluenti/next'
export default withFluenti()({
reactStrictMode: true,
})

withFluenti() performs three things at build time:

  1. Generates a server module (.fluenti/server.js) that bundles createServerI18n, message imports, and a I18nProvider component — aliased as @fluenti/next
  2. Registers a webpack loader that rewrites direct-import authoring APIs in supported client and server scopes
  3. Adds a resolve alias so @fluenti/next points to the generated module

The loader no longer injects globals or proxy accessors. Unbound t identifiers are left untouched.

All options are optional — defaults are read from fluenti.config.ts:

export default withFluenti({
locales: ['en', 'ja', 'zh-CN'],
defaultLocale: 'en',
resolveLocale: './lib/resolve-locale',
})({ reactStrictMode: true })
OptionTypeDefaultDescription
localesstring[]?from config fileOverride locale list
defaultLocalestring?from config fileOverride default/fallback locale
compiledDirstring?from config filePath to compiled message catalogs
resolveLocalestring?multi-layer fallbackModule path that default-exports () => string | Promise<string>
cookieNamestring?'locale'Cookie name used for locale detection
serverModulestring?auto-generatedCustom server module path (skips generation)
serverModuleOutDirstring?.fluentiWhere to write generated files
dateFormatsDateFormatOptions?Custom date format presets
numberFormatsNumberFormatOptions?Custom number format presets
fallbackChainRecord<string, string[]>?Locale fallback chains

withFluenti() generates a I18nProvider async server component that handles both server and client i18n setup. It initializes the server-side locale (via React.cache), loads messages, and wraps children in a client-side provider with statically imported catalogs. That client provider currently bundles the configured locale catalogs up front so function-based compiled messages can cross the RSC boundary safely.

Use [locale] segments so the server always knows the correct locale from the URL. This prevents hydration mismatches and ensures correct SSR/SEO:

src/app/[locale]/layout.tsx
import { getDirection } from '@fluenti/core'
import { I18nProvider } from '@fluenti/next'
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ locale: string }>
}) {
const { locale } = await params
return (
<div dir={getDirection(locale)}>
<I18nProvider locale={locale}>{children}</I18nProvider>
</div>
)
}

Add middleware to redirect bare paths to the default locale:

src/middleware.ts
import { NextResponse, type NextRequest } from 'next/server'
const locales = ['en', 'zh-CN', 'ja']
const defaultLocale = 'en'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const hasLocale = locales.some(l => pathname.startsWith(`/${l}/`) || pathname === `/${l}`)
if (hasLocale) return
const locale = request.cookies.get('locale')?.value ?? defaultLocale
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url))
}
export const config = { matcher: ['/((?!_next|api|favicon).*)'] }

If path-based routing is not possible, you can read the locale from a cookie:

src/app/layout.tsx
import { cookies } from 'next/headers'
import { getDirection } from '@fluenti/core'
import { I18nProvider } from '@fluenti/next'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const locale = (await cookies()).get('locale')?.value ?? 'en'
return (
<html lang={locale} dir={getDirection(locale)}>
<body>
<I18nProvider locale={locale}>{children}</I18nProvider>
</body>
</html>
)
}

Next.js conventional exports like generateMetadata are automatically recognized as server-eligible and promoted to async by the compiler. No special setup is needed:

src/app/[locale]/page.tsx
import { t } from '@fluenti/react'
export function generateMetadata() {
return {
title: t`My Page`,
description: t`Welcome to my page`,
}
}

This also works for generateStaticParams, generateViewport, generateSitemaps, and generateImageMetadata.

Client components use t`...` via import { t } from '@fluenti/react':

src/components/LocaleSwitcher.tsx
'use client'
import { useRouter, usePathname } from 'next/navigation'
import { t, useI18n } from '@fluenti/react'
export function LocaleSwitcher() {
const { locale, setLocale, preloadLocale } = useI18n()
const router = useRouter()
const pathname = usePathname()
const pathWithoutLocale = pathname.replace(/^\/[^/]+/, '') || '/'
const switchLocale = async (loc: string) => {
document.cookie = `locale=${loc};path=/;max-age=31536000`
await setLocale(loc)
router.push(`/${loc}${pathWithoutLocale}`)
}
return (
<div>
<h1>{t`Welcome to my app`}</h1>
<button
onMouseEnter={() => preloadLocale('zh-CN')}
onClick={() => switchLocale('zh-CN')}
>中文</button>
</div>
)
}

Server Components use the same t`...` syntax via import { t } from '@fluenti/react':

src/app/about/page.tsx
import { t } from '@fluenti/react'
export default async function AboutPage() {
return (
<div>
<h1>{t`About us`}</h1>
<p>{t`We build great software.`}</p>
</div>
)
}

For advanced RSC patterns, you can also use getI18n() and the server-side components directly:

src/app/dashboard/page.tsx
import { getI18n, Trans, Plural } from '@/lib/i18n.server'
export default async function DashboardPage() {
const { t } = await getI18n()
return (
<div>
<h1>{t('Dashboard')}</h1>
<Trans>Read the <a href="/docs">documentation</a>.</Trans>
<Plural value={5} one="# item" other="# items" />
</div>
)
}

By default, withFluenti() resolves the locale using a multi-layer fallback: middleware-injected x-fluenti-locale header → cookie → Accept-Language header → default locale. For Server Actions or custom auth flows, you can override this with your own resolver module:

next.config.ts
export default withFluenti({
resolveLocale: './lib/resolve-locale',
})({ reactStrictMode: true })

The module must default-export an async function returning the locale string:

lib/resolve-locale.ts
import { cookies } from 'next/headers'
import { auth } from './auth'
export default async function resolveLocale(): Promise<string> {
const session = await auth()
if (session?.user?.locale) return session.user.locale
const cookieStore = await cookies()
return cookieStore.get('locale')?.value ?? 'en'
}

This is critical for Server Actions, which skip the layout and rely on resolveLocale. See Custom Locale Resolution for a full example.

For full control, you can bypass withFluenti() generation and create your own server module with createServerI18n:

lib/i18n.server.ts
import { createServerI18n } from '@fluenti/react/server'
export const { setLocale, getI18n, Trans, Plural, DateTime, NumberFormat } = createServerI18n({
loadMessages: (locale) => import(`../locales/compiled/${locale}.js`),
fallbackLocale: 'en',
resolveLocale: async () => {
const { cookies } = await import('next/headers')
return (await cookies()).get('locale')?.value ?? 'en'
},
})

The fluenti compile command (or withFluenti()’s auto-compile in dev mode) writes compiled message modules to the compileOutDir path from fluenti.config.ts (default: ./src/locales/compiled/).

  • These files are auto-generated and should not be edited manually
  • fluenti init adds compileOutDir to .gitignore automatically
  • In dev mode, withFluenti() watches source files and re-compiles on changes
  • For production, run fluenti compile before next build (or rely on buildAutoCompile: true, the default)

You can use both patterns together. The I18nProvider in your [locale] layout handles both:

app/[locale]/layout.tsx
import { getDirection } from '@fluenti/core'
import { I18nProvider } from '@fluenti/next'
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ locale: string }>
}) {
const { locale } = await params
return (
<div dir={getDirection(locale)}>
<I18nProvider locale={locale}>{children}</I18nProvider>
</div>
)
}
  • Server Components use t`...` via @fluenti/react, or await getI18n() from a custom server module for advanced cases
  • Client Components use t`...` via @fluenti/react, or useI18n().t() for runtime lookup

Server Actions are independent requests that skip the layout’s setLocale() call. The resolveLocale function is called automatically to determine the active locale.

Direct-import t works in 'use server' files:

app/actions.ts
'use server'
import { t } from '@fluenti/react'
export async function greetAction() {
return t`Hello from server action`
}
export async function searchAction(query: string) {
if (!query) return { error: t`Please enter a search term` }
// ...
}

For cases where you need the full i18n instance (e.g. dynamic message keys):

app/actions.ts
'use server'
import { getI18n } from '@/lib/i18n.server'
export async function greetAction() {
const { t } = await getI18n() // resolveLocale called automatically
return t('Hello from server action')
}