Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/constanza101/borrissol/llms.txt

Use this file to discover all available pages before exploring further.

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
i18n: {
  defaultLocale: 'ca',
  locales: ['ca', 'es', 'en', 'fr'],
  routing: {
    prefixDefaultLocale: false,
  },
},
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.
// src/pages/[lang]/index.astro  (simplified)
import { nonDefaultLangPaths } from '../../i18n/utils';

export function getStaticPaths() {
  return nonDefaultLangPaths(); // → [{ params: { lang: 'es' } }, { params: { lang: 'en' } }, { params: { lang: 'fr' } }]
}

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
export const languages = {
  ca: 'CAT',
  es: 'ES',
  en: 'EN',
  fr: 'FR',
} as const;

export const defaultLang = 'ca' as const;
export type Lang = keyof typeof languages;
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
export const notFound: Record<Lang, Record<string, string>> = {
  ca: { title: "S'ha descosit un fil", home: "Torna a l'inici", /* … */ },
  es: { title: 'Se ha soltado un hilo',  home: 'Volver al inicio', /* … */ },
  en: { title: 'A thread came loose',    home: 'Back to home',     /* … */ },
  fr: { title: "Un fil s'est défait",    home: "Retour à l'accueil", /* … */ },
};

Utility functions (src/i18n/utils.ts)

Four helpers cover every i18n use case across the codebase.
FunctionPurpose
getLangFromUrl(url)Extracts locale from the URL path; falls back to defaultLang
useTranslations(lang)Returns a t(key) function bound to the given locale
localizedPath(lang, path)Prefixes a path for non-default locales
getAlternatePath(url, targetLang)Computes the equivalent URL in another locale
nonDefaultLangPaths()Returns getStaticPaths entries for all non-default locales
A typical component setup looks like this:
import { getLangFromUrl, useTranslations, localizedPath } from '../../i18n/utils';

const lang = getLangFromUrl(Astro.url);
const t    = useTranslations(lang);

// t('nav.services') → 'Services' (EN) | 'Serveis' (CA) | 'Servicios' (ES) | 'Services' (FR)
// localizedPath('es', '/gallery') → '/es/gallery'
// localizedPath('ca', '/gallery') → '/gallery'
getAlternatePath does a URL-prefix swap instead of a lookup table, so it works for every route automatically — including future ones:
// /es/gallery + 'en'  → '/en/gallery'
// /es/gallery + 'ca'  → '/gallery'
// /gallery    + 'fr'  → '/fr/gallery'
getAlternatePath(Astro.url, 'en');
The localeMap in utils.ts maps short codes to BCP 47 tags for Open Graph and the HTML lang attribute:
export const localeMap: Record<Lang, string> = {
  es: 'es_ES',
  en: 'en_US',
  ca: 'ca_ES',
  fr: 'fr_FR',
};

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.
// ui.ts — add to each language block:
'offer.new.label': 'CAT text here',   // ca (inline in component — see below)
// es:
'offer.new.label': 'ES text here',
// en:
'offer.new.label': 'EN text here',
// fr:
'offer.new.label': 'FR text here',
2

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

const lang = getLangFromUrl(Astro.url);
const t    = useTranslations(lang);

// In the template:
// {t('offer.new.label')}
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
sitemap({
  i18n: {
    defaultLocale: 'ca',
    locales: {
      ca: 'ca-ES',
      es: 'es-ES',
      en: 'en-US',
      fr: 'fr-FR',
    },
  },
  filter: (page) => !page.includes('/ca'),
}),

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
'/ca':             { status: 301, destination: '/' },
'/ca/gallery':     { status: 301, destination: '/gallery' },
'/ca/press':       { status: 301, destination: '/press' },
'/ca/blog':        { status: 301, destination: '/blog' },
'/ca/blog/[slug]': { status: 301, destination: '/blog/[slug]' },
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
'/es/blog':         { status: 301, destination: '/blog' },
'/es/blog/[slug]':  { status: 301, destination: '/blog/[slug]' },
'/en/blog':         { status: 301, destination: '/blog' },
'/en/blog/[slug]':  { status: 301, destination: '/blog/[slug]' },
'/fr/blog':         { status: 301, destination: '/blog' },
'/fr/blog/[slug]':  { status: 301, destination: '/blog/[slug]' },
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.

Build docs developers (and LLMs) love