Internationalized routes

Two common i18n routing styles, and how svelte-crumbs handles each: locale-prefixed URLs handled outside the router (Paraglide & friends), and locale segments modelled as an optional [[lang]] route parameter.

Locale-prefix apps (Paraglide & friends)

Libraries like Paraglide serve /sv/products/42 from the route /products/[id] — the locale prefix exists in the URL but not in the route id, so the pathname and the route no longer line up segment by segment. Pass transformPath to strip the prefix before matching:

<script lang="ts">
  import { getCrumbs } from 'svelte-crumbs';
  import { deLocalizeHref, localizeHref } from '$lib/paraglide/runtime';

  const crumbs = $derived(
    await getCrumbs({ transformPath: ({ pathname }) => deLocalizeHref(pathname) })
  );
</script>

{#each crumbs as crumb, i (crumb.url)}
  {#if i > 0}<span aria-hidden="true">/</span>{/if}
  <a href={localizeHref(crumb.url)}>{crumb.label}</a>
{/each}

Matching now runs entirely in de-localized route space. That has two consequences worth knowing:

  • Resolvers see de-localized paths — the page snapshot passed to each resolver is rebuilt around the transformed pathname, and each crumb.url is a de-localized prefix like /products/42.
  • Re-localize at render — since crumb.url has no locale prefix, wrap it when rendering the link: localizeHref(crumb.url). That keeps visitors on their language when they click a crumb.

transformPath gets the same error isolation as resolvers: if it throws, the error is logged and the untransformed pathname is used, so a broken transform degrades the trail instead of breaking the page. A falsy return value ('') is normalized to '/', and a missing leading slash is added for you.

Optional-param routing with [[lang]]

If the locale lives in your route tree instead — /i18n/[[lang]] — no options are needed: optional parameters work out of the box. The walk knows a [[lang]] segment consumes zero or one pathname segments (it checks page.params), so both /i18n and /i18n/sv produce a correct trail. The resolver reads page.params.lang to pick a translated label:

<script module lang="ts">
	const labels: Record<string, string> = {
	  en: 'Greetings',
	  sv: 'Hälsningar'
	};
	
	export const breadcrumb: BreadcrumbMeta = async (page) => ({
	  label: labels[page.params.lang ?? 'en'] ?? labels.en
	});
</script>

Here crumb.url is the concrete pathname prefix — locale segment included — so no re-localization step is needed when rendering. Try it on the live demo page: switch between the default language and Swedish and watch the crumb in the top bar change.