Skip to main content
Urbicon UI

Locale Routing

Carry the locale in the URL (/de/blocks/button) and switch it from the built-in LocaleSwitcher. The package deliberately doesn't own routing — it gives you the onLocaleChange seam; SvelteKit's reroute hook does the rest.

Provider & SSR switches the locale in place — no URL change. That is enough for an app whose locale is a user preference. When the locale should be addressable — shareable /de/… links, distinct pages for crawlers, a browser back-button that walks language history — it belongs in the URL. That is a routing concern, and routing is a framework decision, not a component one.

The package owns state, not routes

setLocale mutates the request-scoped locale and fires the provider's onLocaleChange. That callback is the seam: it is where your app decides what a locale switch means — write a cookie, call another i18n, or (here) navigate. The path prefix vs. query-param vs. subdomain choice, with its hreflang and canonical implications, stays yours. This guide wires the recommended path-prefix strategy end to end.

1. Map the URL to a route

SvelteKit's reroute hook runs before handle and turns the visible URL into the route used for matching. Strip the locale segment there, and your route tree never needs a [lang] param — /de/blocks/button and /en/blocks/button both render src/routes/blocks/button.

Loading...
Loading syntax highlighting...

reroute does not change the address bar or event.url — it only picks the route. Because it is cached per URL, keep it pure (no I/O, no Date.now()).

2. Read the locale per request

Since event.url keeps the prefix, the root server load reads the locale straight from it and feeds the provider — SSR and the first client render agree, with no navigator.language guess. A bare path (first visit, or a legacy unprefixed link) has no locale to read, so redirect it to the resolveLocale choice — the single point where cookie and Accept-Language still decide.

Loading...
Loading syntax highlighting...

3. Switch from the LocaleSwitcher

Nothing about the switcher changes — the built-in LocaleSwitcher calls setLocale as always. You only translate the resulting onLocaleChange into a navigation to the prefixed URL. That one handler is the whole routing layer.

Loading...
Loading syntax highlighting...

After the goto lands, the new data.locale flows into the provider as a controlled value — but it already equals the state setLocale set, so no onLocaleChange re-fires. Locale changes triggered by a plain link (not the switcher) do fire it, and resolve to the same URL — the target !== current guard makes that a no-op.

5. hreflang & canonical

Addressable locales exist for crawlers — so tell them. Emit an alternate link per locale and a canonical for the current one. This is the payoff path-prefix routing buys you over an in-place switch.

Loading...
Loading syntax highlighting...

Variants

The recipe above always prefixes, including the default — unambiguous, but it gives up a clean default URL. Two common alternatives, both wired through the same onLocaleChange seam:

Default locale unprefixed

Leave the base locale at the bare path and prefix only the others. Nicer URLs at the cost of one ambiguity: a crawler can't tell an explicit en page from an undecided one, so the canonical tag from the previous step does real work here.

Loading...
Loading syntax highlighting...

Query parameter

If you don't need SEO-distinct pages, ?lang=de is the lightest option: skip reroute entirely, read url.searchParams.get('lang') in the load, and in onLocaleChange goto the same path with the param set. Same seam, no route tree changes — but search engines may treat the variants as one page.