Skip to content
fluenti

Vue Manual SSR

If you’re using Vue without Nuxt, you can set up SSR manually using the server utilities from @fluenti/vue/server. For Nuxt projects, see the Nuxt SSR guide. For general SSR concepts, see SSR & Hydration.

Create your server-side i18n singleton in a shared module. This provides setLocale, getI18n, and withLocale — all backed by AsyncLocalStorage for per-request isolation:

server/i18n.ts
import { createServerI18n } from '@fluenti/vue/server'
export const { setLocale, getI18n, withLocale } = createServerI18n({
loadMessages: (locale) => import(`../locales/compiled/${locale}.ts`),
fallbackLocale: 'en',
})

Messages are loaded lazily on first use and cached across requests. The loadMessages function is called once per locale for the lifetime of the server process.

OptionTypeDescription
loadMessages(locale: string) => Promise<Messages>(required) Async loader for compiled catalogs
fallbackLocalestringLocale to fall back to when a key is missing
resolveLocale() => string | Promise<string>Auto-detect locale when setLocale() was not called
fallbackChainRecord<string, string[]>Custom fallback chains per locale
dateFormatsDateFormatOptionsNamed date format styles
numberFormatsNumberFormatOptionsNamed number format styles
missing(locale, id) => string | undefinedHandler for missing translation keys

Use detectLocale from @fluenti/core to determine the locale from cookies, headers, or query parameters:

import { detectLocale } from '@fluenti/vue/server'
const locale = detectLocale({
cookie: getCookie(event, 'lang'),
headers: event.headers,
available: ['en', 'zh-CN', 'ja'],
fallback: 'en',
})

detectLocale checks sources in priority order: cookie, Accept-Language header, then fallback.

In production, your server handles multiple requests concurrently. Use withLocale to scope the locale to each request, preventing cross-request leakage:

server/middleware/i18n.ts
import { detectLocale } from '@fluenti/vue/server'
import { withLocale } from '../i18n'
app.use(async (req, res, next) => {
const locale = detectLocale({
cookie: req.cookies.lang,
headers: req.headers,
available: ['en', 'zh-CN', 'ja'],
fallback: 'en',
})
await withLocale(locale, () => next())
})

withLocale uses AsyncLocalStorage internally. All calls to getI18n() within the callback resolve to an i18n instance scoped to that locale — even across async boundaries.

For simpler setups (single-request-at-a-time or middleware that does not support withLocale), use the direct setLocale + getI18n pattern:

setLocale(locale)
const i18n = await getI18n()
// i18n.t, i18n.d, i18n.n are all available

Create a fresh createFluenti() instance per request and install it on the Vue app:

import { createFluenti } from '@fluenti/vue'
const i18n = createFluenti({ locale, messages: allMessages })
app.use(i18n)

Inject the server-detected locale into the HTML so the client can hydrate with the same locale:

// Server: inject into HTML <head>
import { getSSRLocaleScript } from '@fluenti/vue/server'
const script = getSSRLocaleScript(locale)
// → <script>window.__FLUENTI_LOCALE__="zh-CN"</script>
// Client: read it back
import { getHydratedLocale } from '@fluenti/core'
import { createFluenti } from '@fluenti/vue'
const locale = getHydratedLocale('en') // 'en' is the fallback
const i18n = createFluenti({ locale, messages: allMessages })
app.use(i18n)

This ensures the client starts with the exact locale the server rendered, avoiding hydration mismatches.

import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
import { detectLocale, getSSRLocaleScript } from '@fluenti/vue/server'
import { createFluenti } from '@fluenti/vue'
import { withLocale, getI18n } from './server/i18n'
const server = express()
server.get('*', async (req, res) => {
const locale = detectLocale({
cookie: req.cookies.lang,
headers: req.headers,
available: ['en', 'zh-CN', 'ja'],
fallback: 'en',
})
await withLocale(locale, async () => {
const app = createSSRApp(App)
const i18n = createFluenti({ locale, messages: await loadMessages(locale) })
app.use(i18n)
const html = await renderToString(app)
const localeScript = getSSRLocaleScript(locale)
res.send(`
<!DOCTYPE html>
<html lang="${locale}">
<head>${localeScript}</head>
<body><div id="app">${html}</div></body>
</html>
`)
})
})
  1. No shared global state — Create a new createFluenti() instance per request on the server
  2. Use withLocale for concurrent SSR to prevent cross-request locale leakage
  3. Hydration consistency — Use getSSRLocaleScript + getHydratedLocale to ensure the client starts with the same locale the server rendered
  4. Cookie-based detection — Use cookies (not localStorage) so the server can detect locale on the first request
  5. Message cachingcreateServerI18n caches loaded messages across requests, so each locale is loaded only once