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 (node_modules/.fluenti/server.js) that bundles createServerI18n, message imports, and a FluentProvider component — aliased as @fluenti/next/__generated
  2. Registers a webpack loader that transforms t`...` tagged templates and t() calls into __i18n.t() lookups, auto-detecting server vs client context
  3. Adds a resolve alias so @fluenti/next/__generated points to the generated module

The loader detects context automatically:

  • Files with 'use client' → client mode (reads from globalThis.__fluenti_i18n, set by I18nProvider)
  • Files with 'use server' or inside app/ → server mode (reads from React.cache()-scoped store)

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?reads locale cookieModule path that default-exports () => string | Promise<string>
serverModulestring?auto-generatedCustom server module path (skips generation)
serverModuleOutDirstring?node_modules/.fluentiWhere to write generated files
dateFormatsDateFormatOptions?Custom date format presets
numberFormatsNumberFormatOptions?Custom number format presets
fallbackChainRecord<string, string[]>?Locale fallback chains

withFluenti() generates a FluentProvider component that wraps both the client I18nProvider and server locale setup:

src/app/layout.tsx
import { cookies } from 'next/headers'
import { getDirection } from '@fluenti/core'
// @ts-expect-error — generated at build time by withFluenti()
import { FluentProvider } from '@fluenti/next/__generated'
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>
<FluentProvider locale={locale}>
{children}
</FluentProvider>
</body>
</html>
)
}

Client components use t`...` for translations. The withFluenti() plugin injects the macro automatically:

src/app/page.tsx
'use client'
import { useRouter } from 'next/navigation'
import { useI18n } from '@fluenti/react'
export default function Home() {
const { setLocale, preloadLocale } = useI18n()
const router = useRouter()
const switchLocale = async (loc: string) => {
document.cookie = `locale=${loc};path=/;max-age=31536000`
await setLocale(loc)
router.refresh()
}
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. The withFluenti() plugin detects the component type and applies the correct server-side transform:

src/app/about/page.tsx
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() reads the locale from a locale cookie. To resolve the locale from a database, JWT, or external API, pass a module path:

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'
},
})

You can use both patterns together. The FluentProvider in your layout handles both:

app/layout.tsx
import { cookies } from 'next/headers'
import { getDirection } from '@fluenti/core'
// @ts-expect-error — generated at build time
import { FluentProvider } from '@fluenti/next/__generated'
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>
<FluentProvider locale={locale}>
{children}
</FluentProvider>
</body>
</html>
)
}
  • Server Components use t`...` (auto-transformed) or import from @/lib/i18n.server
  • Client Components use t`...` (auto-transformed) or import from @fluenti/react

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

The t`...` macro works in 'use server' files — the plugin detects the directive and injects the server-side i18n accessor automatically:

app/actions.ts
'use server'
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` }
// ...
}

No imports needed — withFluenti() handles everything.

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')
}