Skip to content
fluenti

SSR & Hydration

  1. No shared global state — Every SSR request gets its own Fluenti instance
  2. Hydration consistency — Client always reads the server-determined locale from payload
  3. Path-first detection — In SSG/ISR, the URL path is the only reliable locale source

When a request arrives at your server, Fluenti determines which locale to render by inspecting multiple sources in a defined priority order. The detectLocale function from @fluenti/core checks each source sequentially and returns the first valid match.

SourceHow it worksWhen to use
CookieReads a locale preference stored in a browser cookiePersistent user choice across sessions
Query parameterChecks ?lang=ja or similar URL parameterTemporary overrides, preview links
URL pathExtracts locale from path prefix (e.g. /ja/about)SEO-friendly routing with locale in URL
Accept-Language headerParses the browser’s language preference headerFirst-visit detection for new users
FallbackReturns the configured default localeSafety net when no source matches

The default priority chain is:

cookie -> query -> path -> Accept-Language -> fallback

Each source is checked in order. The first match that appears in your available locales list wins. If no source produces a match, the fallback locale is returned.

import { detectLocale } from '@fluenti/core'
const locale = detectLocale({
cookie: req.headers.cookie, // e.g. "fluenti_locale=ja"
query: url.searchParams.get('lang'), // e.g. "ja"
path: url.pathname, // e.g. "/ja/about"
headers: req.headers, // Accept-Language header
available: ['en', 'ja', 'zh-CN'],
fallback: 'en',
})

The Accept-Language header is parsed into an ordered list by quality value. For example:

Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,ja;q=0.7

This produces the preference list ['en-US', 'en', 'zh-CN', 'ja'], which is negotiated against your available locales. The first match wins.

When rendering HTML on the server, you need to communicate the detected locale to the client so it initializes with the same value. Fluenti provides a script injection helper for this.

  1. Server: getSSRLocaleScript(locale) generates a <script> tag that sets window.__FLUENTI_LOCALE__ to the detected locale.
  2. HTML: The script tag is injected into <head> as part of the SSR response.
  3. Client: getHydratedLocale(fallback) reads the window variable and returns the locale, falling back to the provided default.
import { getSSRLocaleScript, getHydratedLocale } from '@fluenti/core'
// Server: inject into HTML <head>
const script = getSSRLocaleScript('ja')
// -> '<script>window.__FLUENTI_LOCALE__="ja"</script>'
// Client: read it back
const locale = getHydratedLocale('en')
// -> 'ja' (from the injected script)

The locale value is escaped before injection. Characters like <, >, &, ", and ' are converted to Unicode escapes (\u003c, \u003e, etc.) to prevent script injection. The locale is also validated as a BCP 47 code and capped at 255 characters.

When running multiple Fluenti instances (e.g. micro-frontends), use a custom window variable key to avoid collisions:

// Server
const script = getSSRLocaleScript('ja', { key: '__MY_APP_LOCALE__' })
// -> '<script>window.__MY_APP_LOCALE__="ja"</script>'
// Client — key must match
const locale = getHydratedLocale('en', { key: '__MY_APP_LOCALE__' })

The custom key must be a valid JavaScript identifier (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/).

Regardless of framework, always set the lang and dir attributes on the <html> element. This is critical for accessibility (screen readers) and SEO:

import { getDirection } from '@fluenti/core'
const dir = getDirection(locale) // 'ltr' or 'rtl'
// <html lang="ja" dir="ltr">

After a user explicitly switches locale, store the preference in a cookie so detectLocale picks it up on the next server request.

When the user switches locale, set a cookie from the client:

function switchLocale(newLocale: string) {
// Set cookie (accessible by both server and client)
document.cookie = `fluenti_locale=${newLocale}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`
// Then trigger navigation or locale change
}
SettingRecommendationWhy
path/Cookie available on all routes
max-age1 year (31536000)Persistent preference
SameSiteLaxAvailable on navigation but blocks CSRF
SecureSet in productionOnly sent over HTTPS
HttpOnlyDo not setClient JS needs to read/write it for locale switching

Hydration mismatches occur when the server renders HTML with one locale but the client initializes with a different one. Here are the most common causes and solutions.

CauseSolution
Client reads locale from localStorage instead of server payloadUse cookies or the hydration script, never localStorage for initial locale
Server detects locale from Accept-Language but client defaults to 'en'Inject the server locale via getSSRLocaleScript or framework payload
CDN caches a single locale variant for all usersAdd Vary: Accept-Language header or use path-based locales
Client-side router changes locale before hydration completesDelay locale changes until after hydration (onMounted / useEffect)

The client must always initialize with the exact locale the server used. There are two reliable ways to communicate this:

  1. Hydration script (getSSRLocaleScript / getHydratedLocale) — for manual SSR setups
  2. Framework payload — Nuxt stores it in nuxtApp.payload, Next.js passes it via URL params or cookies

Next.js App Router uses React.cache() for per-request isolation. The withFluenti() wrapper generates a server module with locale detection, I18nProvider, and RSC support built in.

app/[locale]/layout.tsx
import { setLocale, I18nProvider } from '@fluenti/next'
export default async function RootLayout({
params,
children,
}: {
params: Promise<{ locale: string }>
children: React.ReactNode
}) {
const { locale } = await params
return (
<html lang={locale}>
<body>
<I18nProvider locale={locale}>{children}</I18nProvider>
</body>
</html>
)
}

The generated server module includes automatic resolveLocale for Server Actions, which checks the referer URL path, the locale cookie, and the Accept-Language header in that order.

See Next.js guide and Server Components.

In production SSR, multiple requests are processed concurrently. If locale state is stored in a module-level variable, one request can overwrite another’s locale. Fluenti solves this with per-request isolation:

FrameworkMechanism
Vue / NuxtAsyncLocalStorage via withLocale()
SolidStartAsyncLocalStorage via withLocale()
Next.js (RSC)React.cache() — automatic per-request scoping
React Router SSRAsyncLocalStorage via withLocale()

For a deep dive on isolation patterns, see Server Context Isolation.

UtilityDescription
detectLocale(options)Detect locale from cookie, query, path, or Accept-Language header
getSSRLocaleScript(locale, options?)Generate a <script> tag to inject locale into HTML (XSS-safe). Pass { key } for custom variable
getHydratedLocale(fallback?, options?)Read the injected locale on the client. Pass { key } to match custom variable
getDirection(locale)Return 'rtl' or 'ltr'
isRTL(locale)Check if a locale is right-to-left