React Server Components
React Server Components render on the server and never ship JavaScript to the client. Fluenti supports RSC via the t`...` tagged template (when using withFluenti()) or createServerI18n from @fluenti/react/server.
For a general overview of Next.js integration, see the Next.js guide.
Recommended: t`...` with withFluenti()
Section titled “Recommended: t`...` with withFluenti()”When using withFluenti() in your next.config.ts, Server Components can use the same t`...` syntax as Client Components. The plugin detects the component type and applies the correct transform:
// app/page.tsx (Server Component — no 'use client')export default async function Page() { return ( <div> <h1>{t`Welcome to our app`}</h1> <p>{t`We have been building great things.`}</p> </div> )}No imports needed — withFluenti() handles the setup automatically.
Advanced: createServerI18n
Section titled “Advanced: createServerI18n”For advanced patterns (custom locale resolution, server-bound components, streaming), use createServerI18n from @fluenti/react/server:
How It Works
Section titled “How It Works”setLocale(locale)— stores the locale in a request-scoped async context (Node.jsAsyncLocalStorage)getI18n()— reads the locale from the async context and returns a bound i18n instance- Pre-bound components (
Trans,Plural, etc.) — automatically resolve their locale from the async context
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' },})Request Scope — How It Works Under the Hood
Section titled “Request Scope — How It Works Under the Hood”React Server Components cannot use useContext because they render on the server without a persistent component tree. Fluenti uses React.cache() to provide per-request isolation instead.
The problem
Section titled “The problem”In traditional React, providers share state via Context. But RSC renders are stateless — there is no component tree to hold context. Each request needs its own isolated i18n instance with the correct locale and messages.
The solution: React.cache()
Section titled “The solution: React.cache()”React.cache() creates a memoized function whose cache is scoped to a single server request. Fluenti uses this to create a request-local store:
// Simplified from @fluenti/react/serverconst getRequestStore = cache(() => ({ locale: null, // set by setLocale() instance: null, // cached i18n instance}))When setLocale('ja') is called in a layout, it writes to this request-scoped store. All subsequent getI18n() calls within the same request read from the same store — regardless of which component calls them.
Request flow
Section titled “Request flow”Request 1 (page render) Request 2 (Server Action)──────────────────────── ─────────────────────────Layout action handler └─ setLocale('ja') └─ getI18n() └─ Page ├─ store.locale == null └─ getI18n() │ (setLocale was never called) ├─ store.locale = 'ja' ├─ resolveLocale() → 'ja' └─ returns i18n(ja) └─ returns i18n(ja)Why Server Actions need resolveLocale
Section titled “Why Server Actions need resolveLocale”Server Actions are independent HTTP requests — they don’t go through the layout, so setLocale() is never called. The React.cache() store starts empty. Without resolveLocale, getI18n() would throw.
By providing resolveLocale (e.g., reading from a cookie), the action can auto-detect the locale:
const { getI18n } = createServerI18n({ loadMessages: (locale) => import(`../locales/compiled/${locale}.js`), resolveLocale: async () => { const { cookies } = await import('next/headers') return (await cookies()).get('locale')?.value ?? 'en' },})Suspense and streaming fallback
Section titled “Suspense and streaming fallback”When a component renders inside a <Suspense> boundary during streaming, the React.cache() store may not be populated yet. Fluenti handles this with a module-level fallback (_lastInstance) that caches the most recently created i18n instance. This ensures streamed components can still resolve translations synchronously without throwing.
Usage in Server Components
Section titled “Usage in Server Components”Translation function
Section titled “Translation function”import { getI18n } from '@/lib/i18n.server'
export default async function Page() { const { t } = await getI18n() return <h1>{t('Welcome to our app')}</h1>}Rich text with Trans
Section titled “Rich text with Trans”import { Trans } from '@/lib/i18n.server'
export default async function Page() { return <Trans>Read the <a href="/docs">documentation</a> to get started.</Trans>}Plurals
Section titled “Plurals”import { Plural } from '@/lib/i18n.server'
export default async function Page({ count }) { return <Plural value={count} one="# result found" other="# results found" />}Date and number formatting
Section titled “Date and number formatting”import { DateTime, NumberFormat } from '@/lib/i18n.server'
export default async function Page() { return ( <div> <DateTime value={new Date()} style="long" /> <NumberFormat value={1234.5} style="currency" /> </div> )}resolveLocale for Server Actions
Section titled “resolveLocale for Server Actions”Server Actions are separate requests that don’t go through the layout’s setLocale() call. The resolveLocale function is called automatically to determine the active locale.
Using t`...` (recommended)
Section titled “Using t`...` (recommended)”When using withFluenti(), the t`...` macro works directly in 'use server' files — the plugin detects the directive and injects the server-side i18n accessor:
'use server'
export async function searchAction(query: string) { if (!query) return { error: t`Please enter a search term` } // ...}No imports needed — withFluenti() handles the transform automatically.
Using getI18n() manually
Section titled “Using getI18n() manually”For dynamic message keys or when using createServerI18n directly:
'use server'import { getI18n } from '@/lib/i18n.server'
export async function searchAction(query: string) { const { t } = await getI18n() // resolveLocale auto-detects from cookie if (!query) return { error: t('Please enter a search term') } // ...}Custom Locale Resolution
Section titled “Custom Locale Resolution”By default, withFluenti() reads the locale from a locale cookie. If you store locale preferences in a database, pass a resolveLocale module path:
import { withFluenti } from '@fluenti/next'
export default withFluenti({ resolveLocale: './lib/resolve-locale',})({ reactStrictMode: true })The module must default-export an async function that returns the locale string.
Example: Database User Preference
Section titled “Example: Database User Preference”A common pattern is to store each user’s preferred locale in the database and fall back to a cookie for anonymous visitors.
Assuming a users table with a locale column (e.g., Prisma / Drizzle):
model User { id String @id @default(cuid()) email String @unique locale String? // null = use default // ...}Write the resolver:
import { cookies } from 'next/headers'import { auth } from './auth'import { db } from './db'
export default async function resolveLocale(): Promise<string> { // 1. Logged-in user → read from DB const session = await auth() if (session?.user) { const user = await db.user.findUnique({ where: { id: session.user.id }, select: { locale: true }, }) if (user?.locale) return user.locale }
// 2. Anonymous visitor → read from cookie const cookieStore = await cookies() return cookieStore.get('locale')?.value ?? 'en'}In the layout, the same resolver logic determines the locale for page renders:
import { cookies } from 'next/headers'import { getDirection } from '@fluenti/core'// @ts-expect-error — generated at build timeimport { FluentProvider } from '@fluenti/next/__generated'import resolveLocale from '@/lib/resolve-locale'
export default async function RootLayout({ children }: { children: React.ReactNode }) { const locale = await resolveLocale() return ( <html lang={locale} dir={getDirection(locale)}> <body> <FluentProvider locale={locale}>{children}</FluentProvider> </body> </html> )}Saving locale to the database
Section titled “Saving locale to the database”Use a Server Action to persist the user’s choice:
'use server'import { auth } from '@/lib/auth'import { db } from '@/lib/db'import { cookies } from 'next/headers'
export async function setUserLocale(locale: string) { // Persist for logged-in users const session = await auth() if (session?.user) { await db.user.update({ where: { id: session.user.id }, data: { locale }, }) }
// Always set cookie as fallback (anonymous + SSR pre-auth) const cookieStore = await cookies() cookieStore.set('locale', locale, { path: '/', maxAge: 60 * 60 * 24 * 365 })}Call it from a Client Component:
'use client'import { useRouter } from 'next/navigation'import { useI18n } from '@fluenti/react'import { setUserLocale } from '@/app/actions'
export function LocaleSwitcher() { const { locale, setLocale, preloadLocale } = useI18n() const router = useRouter()
const switchTo = async (newLocale: string) => { await setUserLocale(newLocale) // persist to DB + cookie await setLocale(newLocale) // update client state router.refresh() // re-render server layout }
return ( <div> <button onMouseEnter={() => preloadLocale('en')} onClick={() => switchTo('en')}> English </button> <button onMouseEnter={() => preloadLocale('ja')} onClick={() => switchTo('ja')}> 日本語 </button> </div> )}Why this works across all contexts
Section titled “Why this works across all contexts”| Context | How locale is resolved |
|---|---|
| Page render (layout) | resolveLocale() called directly in layout |
| Server Component (RSC) | Reads from React.cache() store set by layout |
| Server Action | resolveLocale() called automatically by withFluenti() (layout doesn’t run) |
| Client Component | Reads from I18nProvider context (set by FluentProvider) |
Streaming & Suspense
Section titled “Streaming & Suspense”RSC components work with Next.js streaming. Each component independently resolves its locale and messages:
import { Suspense } from 'react'
export default async function Page() { return ( <div> <h1>{t`Dashboard`}</h1> <Suspense fallback={<p>Loading...</p>}> <AsyncWidget /> </Suspense> </div> )}