Skip to main content
Borrissol serves its content in four languages — Catalan (ca), Spanish (es), English (en), and French (fr) — without duplicating any page files. Catalan is the site’s default locale and lives at the root URL with no prefix (e.g. /, /gallery, /blog). Every other language gets its own two-letter prefix (/es, /en, /fr). The entire approach is powered by Astro 6’s built-in i18n routing combined with a single translation catalog in src/i18n/ui.ts and a set of utility functions in src/i18n/utils.ts.

Routing architecture

Astro’s i18n config in astro.config.mjs declares the four locales and sets prefixDefaultLocale: false so Catalan URLs stay clean:
astro.config.mjs
Each translated page lives under src/pages/[lang]/ (e.g. src/pages/[lang]/index.astro). That file calls getStaticPaths() to emit one static route per non-default locale. The Catalan version of every page has a twin file directly in src/pages/ (e.g. src/pages/index.astro) that handles the root route. This means there is exactly one .astro file per page, not one per language.

The translation catalog (src/i18n/ui.ts)

All copy for every language lives in one file. There are no per-language JSON files to keep in sync.
src/i18n/ui.ts
The ui object holds every translation key for Spanish, English, and French. Catalan translations are inlined directly into components through the fallback mechanism (the default locale’s keys are used when a language’s entry is missing). The notFound object is intentionally separated from ui. The 404 page is built once as a static HTML file in the default locale, so it serializes all four languages and picks the correct one at runtime from the URL:
src/i18n/ui.ts

Utility functions (src/i18n/utils.ts)

Four helpers cover every i18n use case across the codebase. A typical component setup looks like this:
getAlternatePath does a URL-prefix swap instead of a lookup table, so it works for every route automatically — including future ones:
The localeMap in utils.ts maps short codes to BCP 47 tags for Open Graph and the HTML lang attribute:

Adding a new translation key

1

Open `src/i18n/ui.ts`

Add the key to all four language objects inside the ui export. Catalan is the fallback, so if you add it only to ca other locales will silently display the Catalan text — always add to all four.
2

Use `t('your.key')` in the component

useTranslations falls back first to the default locale’s value, then to the raw key string. A missing translation will never crash the build, but it will surface Catalan text in other locales — treat missing keys as bugs.

Hreflang tags

Every page gets <link rel="alternate" hreflang="…"> tags in its <head> via Seo.astro. The sitemap generated by @astrojs/sitemap also emits hreflang cross-references inside the sitemap XML, reinforcing the signal for search engines:
astro.config.mjs

301 redirects

The redirects block in astro.config.mjs handles two categories of legacy or incorrect URLs: Legacy /ca/* URLs — Catalan was historically served under a /ca prefix. Those paths no longer exist; all are redirected to the root equivalent:
astro.config.mjs
Non-Catalan blog URLs — The blog is intentionally Catalan-only (authored exclusively by the workshop founder in her native language). Visitors who follow a translated blog link are redirected to the Catalan blog rather than seeing a 404:
astro.config.mjs
Astro requires redirect destinations to match real routes, which is why each path is listed explicitly rather than using a wildcard pattern.

Language switcher

The language switcher is part of Navbar.astro. It uses getAlternatePath to compute the target URL for each locale and preserves the URL hash via client-side JavaScript, so a visitor on /en#workshops lands on /es#workshops after switching to Spanish — not just /es.
URL slugs are always in English, lowercase, and single-word (/gallery, /press, /blog) regardless of the display language. Translated labels (nav.gallery → “Galeria” / “Galería” / “Gallery” / “Galerie”) are in ui.ts; the routes are not. This keeps hreflang clean and avoids duplicated slug logic.