Internationalization
The example app is localized with Paraglide JS (English + Swedish). The split is deliberate: the consumer owns the locales and the strings; fusion-ui stays presentational and takes every label as a prop. No fusion-* package imports Paraglide.
Who owns what
fusion-ui components expose their copy as props with English defaults — e.g.
AuthForm's labels, UserMenu's logoutLabel, ColorSchemeToggle's label.
The app passes Paraglide messages into those props, so changing locale is just the
app handing different strings down. The library never needs to know a translation
layer exists.
Setup
project.inlang/settings.json declares the locales; the strings live in
messages/{locale}.json:
{
"baseLocale": "sv",
"locales": ["sv", "en"],
"plugin.inlang.messageFormat": { "pathPattern": "./messages/{locale}.json" }
}The Vite plugin compiles them into src/paraglide/ — gitignored, regenerated on
build/dev (and by the pretypecheck script for a standalone typecheck):
paraglideVitePlugin({
project: "./project.inlang",
outdir: "./src/paraglide",
strategy: ["cookie", "baseLocale"],
cookieName: "PARAGLIDE_LOCALE",
}),The cookie-first strategy keeps SSR and the client deterministic: there is no first-paint locale guessing, so no hydration mismatch.
With no PARAGLIDE_LOCALE cookie, the strategy falls back to baseLocale — which is
sv, so a first-time visitor gets the Swedish UI, and English is one click away
in the header switcher. That UI default lines up with the proactive agent's
output language: the agent follows the app.defaultLanguage setting (also sv), so its
findings and chat replies are Swedish too, as is the seed data (the Tikab projects,
the discipline names). To make English the default instead, flip baseLocale
back to en (and reorder locales).
SSR: locale per request
getLocale() has to return the request's locale while the page renders on the
server. A request middleware runs the whole render inside Paraglide's
AsyncLocalStorage context — and imports the generated server entry lazily so
node:async_hooks never reaches the client bundle:
const localeMiddleware = createMiddleware({ type: "request" }).server(async ({ request, next }) => {
const { paraglideMiddleware } = await import("#/paraglide/server.js");
return paraglideMiddleware(request, async () => (await next()).response);
});__root.tsx then renders <html lang={getLocale()}>. The header switcher calls
setLocale, which writes the cookie and reloads, so the server re-renders in the
new language.
Using messages
import { m } from "#/paraglide/messages.js";
<PageHeader title={m.directory_title()} />;
m.hello_greeting({ name: user.name }); // params are typedBuild table column headers (and any other label list) inside the component, not at module scope — module scope would freeze the locale at import time and break SSR, where modules load outside any request's locale context.