@fluenti/solid
v0.6.3
Published
SolidJS compile-time i18n — Trans/Plural/Select components, I18nProvider, useI18n
Maintainers
Readme
@fluenti/solid
Compile-time i18n for SolidJS -- reactive by design, zero runtime overhead.
Fluenti compiles your messages at build time and pairs them with Solid's fine-grained reactivity. When the locale changes, only the text nodes that depend on it re-render. No virtual DOM diffing, no wasted work.
Features
- Compile-time transforms -- messages are resolved during the build; the runtime ships precompiled functions, not an ICU parser.
- Signal-driven locale --
locale()is a Solid signal; any computation that reads it re-runs automatically. <Trans>,<Plural>,<Select>,<DateTime>,<NumberFormat>-- declarative components that map directly to ICU MessageFormat.t()/d()/n()/msg()-- imperative API for strings, dates, numbers, and lazy message definitions.- Code splitting -- load locale chunks on demand with a single
chunkLoaderoption. - SSR-ready -- first-class SolidStart support with per-request isolation.
Quick Start
1. Install
pnpm add @fluenti/core @fluenti/solid
pnpm add -D @fluenti/cli2. Configure Vite
// vite.config.ts
import solidPlugin from 'vite-plugin-solid'
import fluentiSolid from '@fluenti/solid/vite-plugin'
export default {
plugins: [solidPlugin(), fluentiSolid()],
}3. Wrap your app
// index.tsx
import { render } from 'solid-js/web'
import { I18nProvider } from '@fluenti/solid'
import en from './locales/compiled/en'
import ja from './locales/compiled/ja'
import App from './App'
render(
() => (
<I18nProvider locale="en" fallbackLocale="en" messages={{ en, ja }}>
<App />
</I18nProvider>
),
document.getElementById('root')!,
)4. Translate
import { t, useI18n, Trans, Plural, Select } from '@fluenti/solid'
function Demo(props) {
const { d, n, setLocale } = useI18n()
const name = 'World'
return (
<div>
{/* Tagged template literal */}
<h1>{t`Hello, ${name}!`}</h1>
<Trans>Read the <a href="/docs">documentation</a></Trans>
<Plural value={props.count} one="# item" other="# items" />
<Select value={props.gender} male="He" female="She" other="They" />
<p>{d(new Date(), 'long')}</p>
<p>{n(1234.5, 'currency')}</p>
<button onClick={() => setLocale('ja')}>日本語</button>
</div>
)
}@fluenti/solid/components remains available as an explicit subpath when you want runtime-only component imports or a stricter bundle boundary.
What the compiler does
Write natural-language JSX. The Vite plugin extracts messages, generates deterministic IDs, and replaces the source with precompiled lookups -- all at build time.
// You write:
<Plural value={count} one="# item" other="# items" />
// The compiler emits (conceptually):
t('abc123', { count }) // hash-based lookup, no ICU parsing at runtimeAPI Reference
useI18n()
Returns the reactive i18n context. Works inside any component that is a descendant of <I18nProvider>, or after a top-level createFluenti() call.
const { t, d, n, format, locale, setLocale, isLoading } = useI18n()| Method | Signature | Description |
|--------|-----------|-------------|
| t | (id: string \| MessageDescriptor, values?) => string or t`Hello ${name}` | Dual-mode: function call for catalog lookup, or tagged template literal |
| d | (value: Date \| number, style?) => string | Format a date using named presets or Intl defaults |
| n | (value: number, style?) => string | Format a number using named presets or Intl defaults |
| format | (message: string, values?) => string | Format an ICU message string directly (no catalog lookup) |
| locale | Accessor<string> | Reactive signal for the current locale |
| setLocale | (locale: string) => Promise<void> | Change locale (async when lazy locale loading is enabled) |
| loadMessages | (locale: string, messages) => void | Merge additional messages into a locale catalog at runtime |
| getLocales | () => string[] | List all locales that have loaded messages |
| preloadLocale | (locale: string) => void | Preload a locale chunk in the background without switching |
| isLoading | Accessor<boolean> | Whether a locale chunk is currently being loaded |
| loadedLocales | Accessor<Set<string>> | Set of locales whose messages have been loaded |
createFluenti(config)
Module-level singleton alternative to <I18nProvider>. Call once at startup; useI18n() will find it automatically.
import { createFluenti } from '@fluenti/solid'
const i18n = createFluenti({
locale: 'en',
fallbackLocale: 'en',
messages: { en, ja },
// Optional: post-translation transform, locale change callback, custom formatters
transform: (result, id, locale) => result,
onLocaleChange: (newLocale, prevLocale) => { /* ... */ },
formatters: { /* custom ICU function formatters */ },
})The config accepts all FluentiRuntimeConfigFull options from @fluenti/core, including transform, onLocaleChange, and formatters. See the core README for details.
Components
<Trans> -- Rich text
Render translated content containing inline JSX elements:
<Trans>Click <a href="/next">here</a> to continue</Trans>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| tag | string | 'span' | Wrapper element for multiple children |
<Plural> -- Plural forms
ICU plural rules as a component. Supports string props or rich-text JSX element props:
{/* String props */}
<Plural value={count} zero="No items" one="1 item" other="{count} items" />
{/* Rich text via JSX element props */}
<Plural
value={count}
zero={<>No <strong>items</strong> left</>}
one={<><em>1</em> item remaining</>}
other={<><strong>{count}</strong> items remaining</>}
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | number | -- | The count to pluralize on (required) |
| zero | string \| JSX.Element | -- | Text for zero items |
| one | string \| JSX.Element | -- | Singular form |
| two | string \| JSX.Element | -- | Dual form |
| few | string \| JSX.Element | -- | Few form |
| many | string \| JSX.Element | -- | Many form |
| other | string \| JSX.Element | '' | Default/fallback form |
<Select> -- Option selection
ICU select patterns as a component:
{/* String props */}
<Select value={gender} male="He" female="She" other="They" />
{/* Rich text via options + other */}
<Select
value={gender}
options={{
male: <><strong>He</strong> liked this</>,
female: <><strong>She</strong> liked this</>,
}}
other={<><em>They</em> liked this</>}
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | -- | The value to match (required) |
| options | Record<string, string \| JSX.Element> | -- | Named options map |
| other | string \| JSX.Element | '' | Fallback when no option matches |
<DateTime> -- Date formatting
import { DateTime } from '@fluenti/solid'
<DateTime value={new Date()} format="long" />| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | Date \| number | -- | The date value to format (required) |
| style | string | -- | Named date format style |
<NumberFormat> -- Number formatting
import { NumberFormat } from '@fluenti/solid'
<NumberFormat value={1234.56} format="currency" />| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | number | -- | The number to format (required) |
| style | string | -- | Named number format style |
Utilities
| Export | Description |
|--------|-------------|
| msg | Tag for lazy message definitions outside the component tree |
Code Splitting
Load locale messages on demand instead of bundling everything upfront:
<I18nProvider
locale="en"
messages={{ en }}
lazyLocaleLoading
chunkLoader={(locale) => import(`./locales/compiled/${locale}.js`)}
>
<App />
</I18nProvider>const { setLocale, isLoading, preloadLocale } = useI18n()
// Preload on hover
onMount(() => preloadLocale('ja'))
// Switch locale -- instant if preloaded, async otherwise
await setLocale('ja')SSR with SolidStart
Server-side i18n
Create a server-side i18n instance with per-request locale resolution:
// lib/i18n.server.ts
import { createServerI18n } from '@fluenti/solid/server'
export const { setLocale, getI18n } = createServerI18n({
loadMessages: (locale) => import(`../locales/compiled/${locale}.js`),
fallbackLocale: 'en',
resolveLocale: () => {
// Read locale from cookie, header, or URL
const event = getRequestEvent()
return event?.request.headers.get('accept-language')?.split(',')[0] ?? 'en'
},
})Hydration helper
The getSSRLocaleScript utility injects a tiny inline script that makes the server-detected locale available to the client before hydration, preventing a locale flash:
import { getSSRLocaleScript } from '@fluenti/solid/server'SSR utilities re-exported from @fluenti/solid/server
| Export | Description |
|--------|-------------|
| createServerI18n | Create server-side i18n with lazy message loading |
| detectLocale | Detect locale from headers, cookies, URL path, or query |
| getSSRLocaleScript | Inline script for hydrating the locale on the client |
| getHydratedLocale | Read the locale set by the SSR script on the client |
| isRTL / getDirection | RTL detection helpers |
Documentation
Full docs at fluenti.dev.
