Internationalization
Fluixi ships built-in i18n. createI18n gives you a reactive, SSR-correct translator:
import { createI18n } from '@fluixi/core/i18n';
const i18n = createI18n({
locale: 'en',
locales: ['en', 'fr'] as const,
messages: { en: { hello: 'Hello, {name}' }, fr: { hello: 'Bonjour, {name}' } },
});
export const { t, locale, setLocale } = i18n;
t('hello', { name: 'Ada' }); // "Hello, Ada"
t reads the locale reactively, so a translated string inside a template updates when the
locale changes — no re-mount, no reload.
Message keys
Keys can be flat or nested, and both resolve through the same dotted path:
messages: {
en: {
'nav.dashboard': 'Dashboard', // flat, dotted
nav: { settings: 'Settings' }, // nested
},
}
t('nav.dashboard');
t('nav.settings');
Flat keys are checked first, so a dotted key wins over a nested path of the same name. That matters when merging a translator's flat export into a hand-written nested tree.
t('…') autocompletes the keys from your messages, so a typo is a type error rather than a
missing string at runtime.
Interpolation, plurals and formats
{name} placeholders are filled from the second argument. Pluralization uses
Intl.PluralRules; numbers and dates use Intl.NumberFormat / Intl.DateTimeFormat, so
the rules come from the platform rather than from a table that has to be maintained.
Switching locale
setLocale('fr');
On the client that sets a signal and persists the choice, so it survives a reload. On the server it sets the locale for the current request only and seeds it into the page.
SSR and why it does not leak
This is the part worth understanding. On the server the active locale lives in the request context, not in a module-level signal. Two requests being handled concurrently in the same process therefore cannot see each other's locale — the failure mode where one user's page renders in another user's language simply cannot arise.
On the client there is one user, so the locale is an ordinary reactive signal.
The server-resolved locale is serialized into the page and read back before the first client render, so hydration matches what was rendered — no flash of the wrong language, no hydration mismatch.
Auto-discovery
Put translations in src/i18n/<locale>.json and @fluixi/start exposes them as
virtual:fluixi-i18n — typed off the JSON, so keys flow straight into createI18n
without a hand-written type.
import messages from 'virtual:fluixi-i18n';
const i18n = createI18n({ locale: 'en', locales: ['en', 'fr'] as const, messages });
Adding a locale is then adding a file.
Choosing the initial locale
detectLocale picks from what the request offers — the Accept-Language header, a
persisted cookie — narrowed to the locales you actually ship, with your default as the
fallback.
That wraps up the v1 guide. Explore the reactive core live in the Playground.