Skip to main content

Overview

The wedding website supports three languages: Spanish (default), English, and German. The i18n system is built with a simple, lightweight approach using JSON translation files and utility functions.

Architecture

The i18n system is located in src/i18n/ and consists of:

Translation Files

JSON files for each language (es.json, en.json, de.json)

Utility Functions

Helper functions for language detection and translation access

Translation Files

Each language has its own JSON file with identical structure:
{
  "header": {
    "es": "Español",
    "en": "English",
    "de": "Deutsch"
  },
  "menu": {
    "title": "Menú",
    "eventos": "Eventos",
    "playlist": "Playlist",
    "hospedaje": "Hospedaje",
    "contacto": "Información de Contacto",
    "asistencia": "Confirmación de Asistencia"
  },
  "principal": {
    "fecha": "22 · OCTUBRE · 2026",
    "lugar": "Cartagena de Indias, Colombia"
  },
  "form": {
    "titulo": "Confirma tu asistencia",
    "nombre": "Nombre y apellidos",
    "enviar": "Enviar",
    "msg_success": "¡Confirmación enviada! Gracias 🎉"
  }
}

Core Functions

The src/i18n/index.ts file exports two key functions:
src/i18n/index.ts
import es from './es.json';
import en from './en.json';
import de from './de.json';

const translations = { es, en, de };

export type Lang = 'es' | 'en' | 'de';

// Detect language from URL path
export function getLang(url: URL): Lang {
  const path = url.pathname.split('/')[1];
  if (path === 'en' || path === 'de') return path;
  return 'es'; // Default to Spanish
}

// Get translations for a specific language
export function useTranslations(lang: Lang) {
  return translations[lang];
}

getLang()

Detects the current language from the URL pathname:
  • Extracts the first path segment from the URL
  • Returns 'en' if path is /en or /en/...
  • Returns 'de' if path is /de or /de/...
  • Returns 'es' (Spanish) as the default for / or any other path

useTranslations()

Returns the translation object for a given language:
const t = useTranslations('en');
console.log(t.form.titulo);  // "Confirm your attendance"
console.log(t.menu.eventos); // "Events"

Usage in Components

Every component that displays text should follow this pattern:
1

Import the i18n utilities

---
import { useTranslations, type Lang } from '../i18n/index.ts';
2

Define Props interface with lang

interface Props {
  lang?: Lang;
}
const { lang = 'es' } = Astro.props;
3

Get translations object

const t = useTranslations(lang);
4

Use translations in markup

<h2>{t.form.titulo}</h2>
<button>{t.form.enviar}</button>

Complete Example

src/components/SectionForm.astro
---
import { useTranslations, type Lang } from '../i18n/index.ts';

interface Props {
  lang?: Lang;
}
const { lang = 'es' } = Astro.props;
const t = useTranslations(lang);
---

<div id="asistencia">
  <h2 class="font-['Amoresa'] text-center">
    {t.form.titulo}
  </h2>
  <p class="text-center">
    {t.form.subtitulo}
  </p>
  
  <input 
    type="text" 
    placeholder={t.form.nombre} 
  />
  
  <button 
    data-msg-success={t.form.msg_success}
    data-msg-error={t.form.msg_error}>
    {t.form.enviar}
  </button>
</div>

Usage in Pages

Pages detect the language from the URL and pass it to components:
---
// Spanish (default) - no lang detection needed
import Layout from '../layouts/Layout.astro';
import Header from '../components/Header.astro';
import SectionPrincipal from '../components/SectionPrincipal.astro';
---

<Layout title="Nuestra Historia - Alejandra & Alexander">
  <Header />
  <main>
    <SectionPrincipal />
  </main>
</Layout>

Language Switcher

The header component implements language switching:
src/components/Header.astro
---
import { useTranslations, type Lang } from '../i18n/index.ts';

interface Props {
  lang?: Lang;
}
const { lang = 'es' } = Astro.props;
const t = useTranslations(lang);
---

<header>
  <div class="flex items-center space-x-2">
    <a href="/" 
       class:list={[{ 'font-bold': lang === 'es' }]}>
      { t.header.es }
    </a>
    <span>·</span>
    <a href="/en" 
       class:list={[{ 'font-bold': lang === 'en' }]}>
      { t.header.en }
    </a>
    <span>·</span>
    <a href="/de" 
       class:list={[{ 'font-bold': lang === 'de' }]}>
      { t.header.de }
    </a>
  </div>
</header>
The class:list directive conditionally applies font-bold to highlight the active language.

Using Translations in Client Scripts

For client-side JavaScript, pass translations via data attributes:
Component with client-side messages
---
const t = useTranslations(lang);
---

<button 
  id="submit-btn"
  data-msg-success={t.form.msg_success}
  data-msg-error={t.form.msg_error}
  data-msg-required={t.form.msg_required}>
  {t.form.enviar}
</button>

<script>
  const btn = document.getElementById('submit-btn') as HTMLButtonElement;
  
  // Access translations from data attributes
  const msgSuccess = btn?.dataset.msgSuccess ?? 'Success!';
  const msgError = btn?.dataset.msgError ?? 'Error occurred';
  const msgRequired = btn?.dataset.msgRequired ?? 'Please fill all fields';
  
  btn?.addEventListener('click', async () => {
    try {
      // ... form submission logic
      alert(msgSuccess);
    } catch (e) {
      alert(msgError);
    }
  });
</script>

Adding New Languages

1

Create translation file

Add a new JSON file in src/i18n/ (e.g., fr.json) with the same structure as existing files.
2

Update index.ts

src/i18n/index.ts
import es from './es.json';
import en from './en.json';
import de from './de.json';
import fr from './fr.json'; // Add import

const translations = { es, en, de, fr }; // Add to object

export type Lang = 'es' | 'en' | 'de' | 'fr'; // Add to type

export function getLang(url: URL): Lang {
  const path = url.pathname.split('/')[1];
  if (path === 'en' || path === 'de' || path === 'fr') return path;
  return 'es';
}
3

Create page directory

Create src/pages/fr/index.astro following the pattern of /en and /de pages.
4

Update header component

Add the new language link to the language switcher in Header.astro.

Translation Key Structure

Translations are organized by section:
SectionDescriptionExample Keys
headerLanguage nameses, en, de
menuNavigation menutitle, eventos, playlist
principalHero sectionfecha, lugar
historiaStory sectiontitulo, p1, p2, p3
preweddingPre-wedding eventtitulo, lugar, fecha, hora
celebracionWedding timelineevento1_hora, evento1_desc
dresscodeAttire guidelinestitulo, mujer, hombre
playlistMusic sectiontitulo, subtitulo, boton
hospedajeAccommodationstitulo, hotel_nombre
formRSVP formtitulo, nombre, enviar, msg_success
contactoContact infotitulo, planner_titulo, contactar
footerFooter creditscreditos

Best Practices

Type Safety: Always use the Lang type for language parameters to ensure compile-time checking.
Default Language: Always provide a default value of 'es' for the lang prop to ensure components work standalone.
Consistent Keys: When adding new translations, ensure all language files have the exact same key structure.
HTML in Translations: The footer demonstrates embedding HTML in translations using &amp; entities and <a> tags.

Build docs developers (and LLMs) love