Skip to content
Fusion

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

Loading diagram...

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):

example/vite.config.ts
		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:

example/src/start.ts
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 typed

Build 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.