Skip to main content
01:43 AM
Back to blog
astroi18ntypescriptsvelteislands-architecture

Astro i18n Islands: Passing Locale Across the Client Boundary

A historical i18n bug led this portfolio to pass locale from Astro into Svelte instead of reading browser state during component initialization.

·5 min
Code editor with TypeScript

This portfolio has localized route pairs such as /en/projects and /es/proyectos. Astro knows the locale while generating each page, but a hydrated island can later survive client navigation. That creates a boundary: the initial locale comes from the document, while persistent browser state must respond to route changes.

The Historical Failure

An earlier helper read window.location during Svelte component initialization. That was presented as safe because the component would supposedly skip the browser-dependent code during server rendering. The assumption was wrong: initialization code can run while Astro pre-renders an island, where window does not exist.

A fallback such as typeof window !== 'undefined' ? locale : 'en' avoids the exception but can render Spanish routes with English initial state. It hides the crash instead of defining a reliable data flow.

The repository still contains unused legacy helpers for older React and Svelte components. Their presence is historical residue, not the current public-page architecture.

The Current Contract

Astro derives locale from the route and localizes collection data before rendering the island:

---
const lang = getLangFromUrl(Astro.url);
const entries = await getCollection('projects');
const projects = entries.map((entry) => mapProjectEntry(entry, lang));
---

<StickerAlbum {projects} {lang} client:load />

The Svelte component receives lang as required input. It does not guess the initial locale from a browser global:

<script lang="ts">
  interface Props {
    projects: ProjectForCard[];
    lang: 'en' | 'es';
  }

  let { projects, lang }: Props = $props();
</script>

This keeps generated HTML, hydration, labels, filtering, and date formatting on the same locale from the first render.

Client Navigation Needs a Separate Rule

The retro desktop intentionally keeps some windows open across Astro view transitions. Locale is therefore part of the persistent window contract, not just a page prop. After astro:after-swap, the shell reads the new route, updates localized window content, and preserves only state that is safe to carry between equivalent EN and ES routes.

The centralized route registry supplies those equivalences. Components do not maintain their own about ↔ acerca-de or player ↔ reproductor tables.

The General Lesson

Browser availability and locale identity are different concerns. A lifecycle guard can protect a browser API, but it cannot guarantee correct initial language. The robust order is:

  1. derive locale in Astro;
  2. localize static data before hydration;
  3. pass locale explicitly to interactive islands;
  4. resynchronize persistent state after client navigation;
  5. test EN → ES → EN with the affected UI already open.

The smallest explicit data flow is safer than a convenient hook that infers global state.