Skip to content
fluenti

React SPA

Fluenti works with any React SPA built on Vite. See the Quick Start for initial setup.

Wrap your app with I18nProvider:

src/main.tsx
import { createRoot } from 'react-dom/client'
import { I18nProvider } from '@fluenti/react'
import { App } from './App'
import en from './locales/compiled/en'
import zhCN from './locales/compiled/zh-CN'
function Root() {
return (
<I18nProvider
locale="en"
fallbackLocale="en"
messages={{ en, 'zh-CN': zhCN }}
>
<App />
</I18nProvider>
)
}
createRoot(document.getElementById('root')!).render(<Root />)

The t`...` tagged template is a compiler macro — no import needed:

function Hero() {
const name = 'World'
return <h1>{t`Hello, ${name}!`}</h1>
}

For dynamic keys or non-component contexts:

import { useI18n } from '@fluenti/react'
function Greeting() {
const { i18n } = useI18n()
return <p>{i18n.t('Welcome to my app')}</p>
}
import { Trans, Plural } from '@fluenti/react'
<Trans>Click <a href="/next">here</a> to continue.</Trans>
<Plural value={count} zero="No items" one="# item" other="# items" />

Use <Select> for categorical values like gender, role, or status:

import { Select } from '@fluenti/react'
function Notification({ gender }: { gender: string }) {
return (
<Select
value={gender}
male="He updated his profile"
female="She updated her profile"
other="They updated their profile"
/>
)
}

Any additional props beyond value and other are treated as case options — use whatever keys match your data.

Format dates and numbers with locale-aware components:

import { DateTime, NumberFormat } from '@fluenti/react'
function OrderSummary({ date, total }: { date: Date; total: number }) {
return (
<div>
<p>Order placed: <DateTime value={date} style="long" /></p>
<p>Total: <NumberFormat value={total} style="currency" /></p>
</div>
)
}

Format styles ("long", "currency", etc.) are defined via the dateFormats and numberFormats props on <I18nProvider>.

The useI18n() hook provides the full i18n API:

import { useI18n } from '@fluenti/react'
function Dashboard() {
const { i18n, locale, setLocale, isLoading, loadedLocales, preloadLocale } = useI18n()
return (
<div>
<p>{i18n.t('Welcome back, {name}!', { name: 'Alice' })}</p>
<p>{i18n.t('You have {count, plural, one {# notification} other {# notifications}}', { count: 5 })}</p>
<p>Today: {i18n.d(new Date(), 'long')}</p>
<p>Balance: {i18n.n(1234.5, 'currency')}</p>
</div>
)
}
PropertyTypeDescription
i18nFluentInstanceExtendedCore instance with t(), d(), n(), format()
localestringCurrent active locale
setLocale(locale) => Promise<void>Switch locale (loads messages if needed)
isLoadingbooleantrue while a locale is being loaded
loadedLocalesstring[]Locales with messages already in memory
preloadLocale(locale) => Promise<void>Load a locale without switching to it

Translate a message. Uses an O(1) hash lookup — safe to call directly in JSX without useMemo:

i18n.t('Hello') // simple
i18n.t('Hello, {name}!', { name: 'World' }) // ICU interpolation

Format a date using Intl.DateTimeFormat with an optional named style:

i18n.d(new Date()) // default format
i18n.d(new Date(), 'long') // named preset from dateFormats config

Format a number using Intl.NumberFormat with an optional named style:

i18n.n(1234.5) // default format
i18n.n(1234.5, 'currency') // named preset from numberFormats config

Direct ICU interpolation without catalog lookup — useful for one-off formatting:

i18n.format('{count, plural, one {# item} other {# items}}', { count: 3 })

Use msg to define translatable messages outside the component tree (routes, constants, config objects). The message is resolved at render time, so it always reflects the current locale:

import { msg } from '@fluenti/react'
import { useI18n } from '@fluenti/react'
// Define at module level — no i18n context needed
const NAV_ITEMS = [
{ path: '/', label: msg`Home` },
{ path: '/about', label: msg`About` },
{ path: '/contact', label: msg`Contact` },
]
// Resolve at render time
function Nav() {
const { i18n } = useI18n()
return (
<nav>
{NAV_ITEMS.map((item) => (
<a key={item.path} href={item.path}>
{i18n.t(item.label)}
</a>
))}
</nav>
)
}

Load locale messages on demand instead of bundling them all upfront:

<I18nProvider
locale="en"
fallbackLocale="en"
messages={{ en }}
loadMessages={(locale) => import(`./locales/compiled/${locale}.js`)}
>
<App />
</I18nProvider>

Show a loading indicator while messages are being fetched:

function LocaleSwitcher() {
const { locale, setLocale, isLoading } = useI18n()
return (
<select
value={locale}
onChange={(e) => setLocale(e.target.value)}
disabled={isLoading}
>
<option value="en">English</option>
<option value="ja">日本語</option>
<option value="zh-CN">中文</option>
</select>
)
}

Preload a locale before the user switches, eliminating perceived latency:

function LocaleButton({ code, label }: { code: string; label: string }) {
const { setLocale, preloadLocale } = useI18n()
return (
<button
onMouseEnter={() => preloadLocale(code)}
onClick={() => setLocale(code)}
>
{label}
</button>
)
}

Handle untranslated messages with the missing prop — useful for development debugging or fallback logic:

<I18nProvider
locale="ja"
fallbackLocale="en"
messages={{ en, ja }}
missing={(locale, id) => {
console.warn(`[i18n] Missing "${id}" for locale "${locale}"`)
return undefined // fall through to fallbackLocale
}}
>
<App />
</I18nProvider>

Return a string to use as the translation, or undefined to fall through to the fallback locale.

  • Uses <I18nProvider> instead of a plugin
  • Access i18n via useI18n() hook — returns { i18n, locale, setLocale, ... }
  • Call i18n.t() directly in JSX — no useMemo needed, it’s an O(1) hash lookup
  • No v-t directive — use t`...` or <Trans> component

@fluenti/react works with any React-based framework: