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.
Recommended Setup with withFluenti
Section titled “Recommended Setup with withFluenti”The withFluenti() wrapper in next.config.ts is the recommended way to set up Fluenti in Next.js:
import { withFluenti } from '@fluenti/next'
export default withFluenti()({ reactStrictMode: true,})What it does
Section titled “What it does”withFluenti() performs three things at build time:
- Generates a server module (
.fluenti/server.js) that bundlescreateServerI18n, message imports, and aI18nProvidercomponent — aliased as@fluenti/next - Registers a webpack loader that rewrites direct-import authoring APIs in supported client and server scopes
- Adds a resolve alias so
@fluenti/nextpoints to the generated module
The loader no longer injects globals or proxy accessors. Unbound t identifiers are left untouched.
Config options
Section titled “Config options”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 })| Option | Type | Default | Description |
|---|---|---|---|
locales | string[]? | from config file | Override locale list |
defaultLocale | string? | from config file | Override default/fallback locale |
compiledDir | string? | from config file | Path to compiled message catalogs |
resolveLocale | string? | multi-layer fallback | Module path that default-exports () => string | Promise<string> |
cookieName | string? | 'locale' | Cookie name used for locale detection |
serverModule | string? | auto-generated | Custom server module path (skips generation) |
serverModuleOutDir | string? | .fluenti | Where to write generated files |
dateFormats | DateFormatOptions? | — | Custom date format presets |
numberFormats | NumberFormatOptions? | — | Custom number format presets |
fallbackChain | Record<string, string[]>? | — | Locale fallback chains |
I18nProvider
Section titled “I18nProvider”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.
Path-based routing (recommended)
Section titled “Path-based routing (recommended)”Use [locale] segments so the server always knows the correct locale from the URL. This prevents hydration mismatches and ensures correct SSR/SEO:
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:
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).*)'] }Cookie-based routing (alternative)
Section titled “Cookie-based routing (alternative)”If path-based routing is not possible, you can read the locale from a cookie:
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> )}generateMetadata
Section titled “generateMetadata”Next.js conventional exports like generateMetadata are automatically recognized as server-eligible and promoted to async by the compiler. No special setup is needed:
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
Section titled “Client Components”Client components use t`...` via import { t } from '@fluenti/react':
'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> )}React Server Components (RSC)
Section titled “React Server Components (RSC)”Server Components use the same t`...` syntax via import { t } from '@fluenti/react':
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:
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> )}Custom Locale Resolution
Section titled “Custom Locale Resolution”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:
export default withFluenti({ resolveLocale: './lib/resolve-locale',})({ reactStrictMode: true })The module must default-export an async function returning the locale string:
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.
Generated Server Module
Section titled “Generated Server Module”For full control, you can bypass withFluenti() generation and create your own server module with createServerI18n:
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' },})Compiled Output
Section titled “Compiled Output”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 initaddscompileOutDirto.gitignoreautomatically- In dev mode,
withFluenti()watches source files and re-compiles on changes - For production, run
fluenti compilebeforenext build(or rely onbuildAutoCompile: true, the default)
Hybrid: Client + Server in the same app
Section titled “Hybrid: Client + Server in the same app”You can use both patterns together. The I18nProvider in your [locale] layout handles both:
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, orawait getI18n()from a custom server module for advanced cases - Client Components use
t`...`via@fluenti/react, oruseI18n().t()for runtime lookup
Server Actions
Section titled “Server Actions”Server Actions are independent requests that skip the layout’s setLocale() call. The resolveLocale function is called automatically to determine the active locale.
Using t`...` (recommended)
Section titled “Using t`...` (recommended)”Direct-import t works in 'use server' files:
'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` } // ...}Using getI18n() manually
Section titled “Using getI18n() manually”For cases where you need the full i18n instance (e.g. dynamic message keys):
'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')}