Trilingual URL routing with Astro i18n slug mapping
12 min read

Astro i18n ohne Plugin: Dreisprachige Websites mit Slug-Mapping

#Astro #i18n #Routing

Wer eine Website in mehreren Sprachen betreiben will, greift schnell zu einem i18n-Plugin. Astro-i18next, Paraglide, astro-i18n-aut — die Optionen sind zahlreich. Aber jedes Plugin bringt Abhängigkeiten mit, diktiert eine Ordnerstruktur und macht Dinge, die du vielleicht gar nicht brauchst. Was, wenn du die volle Kontrolle behalten willst? Was, wenn du übersetzte URLs brauchst — /ueber-mich auf Deutsch, /about auf Englisch, /sobre-mi auf Spanisch — und keine schwarze Box dazwischen?

Genau das habe ich auf dieser Website umgesetzt: dreisprachig, mit Astros eingebautem i18n-Routing und einer einzigen Slug-Tabelle. Kein Plugin, keine Magie, vollständig nachvollziehbar. In diesem Tutorial zeige ich dir Schritt für Schritt, wie das funktioniert.

Astro bietet eingebaute i18n-Routing-Unterstützung, mit der du Inhalte in mehreren Sprachen bereitstellen kannst — mit voller Kontrolle über URL-Muster und Fallback-Logik.

Astro Docs Offizielle Dokumentation

Das Problem mit i18n-Plugins

Plugins für Internationalisierung lösen ein reales Problem, aber sie tun es oft mit Annahmen, die nicht zu jedem Projekt passen. Manche generieren automatisch Ordner, andere verlangen spezielle Dateinamenskonventionen. Die meisten übersetzen keine URLs — du bekommst /en/ueber-mich statt /en/about. Und wenn du einen Fehler beheben musst, stehst du vor abstrahiertem Code, den du nicht geschrieben hast.

Astros eingebautes i18n-Routing gibt dir die Grundlage: locale-basierte Präfixe, Default-Locale-Erkennung und Sprachverhandlung. Was fehlt, ist die Slug-Übersetzung — und genau hier kommt unsere eigene Lösung ins Spiel.

3
Sprachen
0
Plugins
1
Slug-Tabelle

Die Architektur im Überblick

Das Konzept ist einfach: Deutsch ist die Default-Locale und bekommt keinen URL-Präfix. Englisch bekommt /en/, Spanisch /es/. Jede Seite hat in jeder Sprache einen semantisch passenden Slug — keine id-Parameter, keine UUID-Pfade, keine Sprachpräfixe vor deutschen Slugs.

Route-Auflösungsfluss der i18n-Konfiguration
Astro-Konfiguration

In astro.config.mjs definierst du die drei Locales und setzt prefixDefaultLocale auf false. Damit hat Deutsch keinen Präfix — /kontakt statt /de/kontakt. Englisch und Spanisch bekommen automatisch /en/ und /es/ als Präfix. Das ist die einzige Konfiguration, die Astro selbst braucht.

Slug-Mapping-Tabelle mit dreisprachiger Zuordnung
Slug-Tabelle

Eine zentrale TypeScript-Datei (src/i18n/utils.ts) enthält eine Tabelle, die jeden Seitenpfad auf seine drei Sprachvarianten mappt. /kontakt wird zu /contact (EN) und /contacto (ES). Eine Reverse-Lookup-Map ermöglicht die Auflösung in beide Richtungen — von jedem Slug zu jedem Ziel-Slug.

Sprachumschalter mit Auflösungslogik
Helper-Funktionen

Zwei Kernfunktionen nutzen die Tabelle: getLocalePath() generiert locale-bewusste Pfade für Navigation und Links. getLocalePathFromCurrent() nimmt die aktuelle URL und generiert den äquivalenten Pfad in einer anderen Sprache — perfekt für den Sprachumschalter.

Übersetzungs-JSON-Dateien mit Schlüsselzuordnung
Übersetzungs-JSONs

Drei JSON-Dateien (de.json, en.json, es.json) enthalten alle UI-Texte als flache Key-Value-Paare. Die Funktion t(locale, key) gibt den übersetzten String zurück, mit automatischem Fallback auf Deutsch. Einfach, typsicher und ohne Laufzeit-Overhead.

Die i18n-Konfiguration in Astro

Der Ausgangspunkt ist die astro.config.mjs. Hier sagst du Astro, welche Sprachen deine Seite unterstützt und wie das Routing funktioniert.

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://arnoldwender.com',
  output: 'static',

  // i18n — DE (default, kein Präfix), EN und ES mit Präfix
  i18n: {
    defaultLocale: 'de',
    locales: ['de', 'en', 'es'],
    routing: {
      prefixDefaultLocale: false,
    },
  },
});

Durch diese drei Zeilen Konfiguration weiß Astro, dass:

  • Seiten in src/pages/ zur deutschen Version gehören (kein Präfix)
  • Seiten in src/pages/en/ die englische Version sind (Präfix /en/)
  • Seiten in src/pages/es/ die spanische Version sind (Präfix /es/)
  • Astro.currentLocale in jeder Seite automatisch die aktuelle Sprache liefert

Was Astro nicht tut: Es übersetzt keine Slugs. /en/kontakt ist für Astro ein gültiger englischer Pfad — aber semantisch unsinnig. Hier beginnt unsere eigene Arbeit.

Die Slug-Tabelle

Das Herzstück der Lösung ist eine einfache Tabelle, die jeden Pfad auf seine Sprachvarianten mappt. Sie lebt in src/i18n/utils.ts und hat bewusst keine Abhängigkeiten.

// src/i18n/utils.ts — Dreisprachige Slug-Zuordnung

const slugTable: { de: string; en: string; es: string }[] = [
  { de: '/kontakt',          en: '/contact',        es: '/contacto' },
  { de: '/ueber-mich',       en: '/about',          es: '/sobre-mi' },
  { de: '/preise',           en: '/pricing',        es: '/precios' },
  { de: '/datenschutz',      en: '/privacy',        es: '/privacidad' },
  { de: '/barrierefreiheit', en: '/accessibility',  es: '/accesibilidad' },
  { de: '/impressum',        en: '/impressum',      es: '/aviso-legal' },
  { de: '/blog',             en: '/blog',           es: '/blog' },
  { de: '/leistungen',       en: '/services',       es: '/servicios' },
  { de: '/faq',              en: '/faq',            es: '/faq' },
  { de: '/links',            en: '/links',          es: '/enlaces' },
  { de: '/stack',            en: '/stack',           es: '/stack' },
  { de: '/sitemap',          en: '/sitemap',        es: '/mapa-del-sitio' },
];
// Reverse Lookup — jeder Slug einer beliebigen Sprache → vollständiger Eintrag

const slugToEntry = new Map<string, { de: string; en: string; es: string }>();

for (const entry of slugTable) {
  slugToEntry.set(entry.de, entry);
  slugToEntry.set(entry.en, entry);
  slugToEntry.set(entry.es, entry);
}

// Beispiel: slugToEntry.get('/contact')
// → { de: '/kontakt', en: '/contact', es: '/contacto' }
//
// slugToEntry.get('/contacto')
// → { de: '/kontakt', en: '/contact', es: '/contacto' }

Die Tabelle ist bewusst flach gehalten. Jeder Eintrag ist ein Objekt mit drei Strings — keine verschachtelten Strukturen, keine IDs, keine Metadaten. Das macht sie leicht zu lesen, leicht zu erweitern und leicht zu debuggen.

Die slugToEntry-Map ist der entscheidende Trick: Sie registriert jeden Slug jeder Sprache als Schlüssel und verweist auf den gleichen Eintrag. So kannst du von /contact (EN), /contacto (ES) oder /kontakt (DE) aus immer den Eintrag finden und den Slug jeder anderen Sprache nachschlagen. Die Auflösung funktioniert in alle Richtungen.

Der getLocalePath()-Helper

Die erste Kernfunktion generiert locale-bewusste Pfade. Sie wird in der Navigation, in internen Links und überall dort verwendet, wo du einen Pfad für eine bestimmte Sprache brauchst.

/**
 * Locale-bewussten Pfad mit Slug-Übersetzung generieren.
 * DE (default) bekommt keinen Präfix und verwendet deutsche Slugs.
 * EN/ES werden mit Locale-Code prefixed und verwenden lokalisierte Slugs.
 * Akzeptiert Slugs aus JEDER Sprache — löst auf die Zielsprache auf.
 */
export function getLocalePath(locale: string | undefined, path: string): string {
  const lang = (locale || 'de') as Locale;
  const entry = slugToEntry.get(path);

  if (lang === 'de') {
    // DE (default) — kein Präfix, deutschen Slug zurückgeben
    return entry ? entry.de : path;
  }

  // EN/ES — Locale-Code als Präfix, sprachspezifischen Slug verwenden
  const targetSlug = entry ? entry[lang] : path;
  return `/${lang}${targetSlug}`;
}
---
// In einer beliebigen Astro-Komponente
import { getLocalePath } from '../i18n/utils';

const locale = Astro.currentLocale || 'de';
---

<!-- Navigation mit übersetzten Slugs -->
<nav>
  <a href={getLocalePath(locale, '/kontakt')}>
    {t(locale, 'nav.contact')}
  </a>
  <a href={getLocalePath(locale, '/ueber-mich')}>
    {t(locale, 'nav.about')}
  </a>
  <a href={getLocalePath(locale, '/leistungen')}>
    {t(locale, 'nav.services')}
  </a>
</nav>

<!-- Ergebnisse nach Locale:
  DE: /kontakt, /ueber-mich, /leistungen
  EN: /en/contact, /en/about, /en/services
  ES: /es/contacto, /es/sobre-mi, /es/servicios
-->

Der entscheidende Punkt: Du kannst den Input-Pfad in jeder Sprache angeben. Ob du /kontakt, /contact oder /contacto übergibst — die Funktion findet den Eintrag und gibt den richtigen Slug für die Zielsprache zurück. Das macht die Navigation-Komponenten sprachagnostisch: Du schreibst sie einmal mit deutschen Slugs, und die Funktion übersetzt automatisch.

Der Sprachumschalter

Der Sprachumschalter ist das Stück, das alles zusammenbringt. Er muss die aktuelle URL nehmen, herausfinden welche Seite der Nutzer gerade sieht, und den entsprechenden Pfad in der Zielsprache generieren.

/**
 * Pfad normalisieren — Trailing Slashes entfernen (außer Root /).
 * Stellt konsistentes Slug-Lookup sicher, unabhängig vom Dev-Server-Verhalten.
 */
function normalizePath(pathname: string): string {
  return pathname === '/' ? '/' : pathname.replace(/\/+$/, '');
}

/**
 * Einen vollständigen Pfadnamen in den äquivalenten Pfad der Zielsprache übersetzen.
 * Wird vom LanguageSwitcher verwendet, um korrekte Cross-Locale-Links zu bauen.
 */
export function getLocalePathFromCurrent(
  targetLocale: string,
  currentPathname: string
): string {
  // Locale-Präfix und Trailing Slashes entfernen → sauberer Basispfad
  const stripped = currentPathname.replace(/^\/(en|es)(\/|$)/, '/') || '/';
  const basePath = normalizePath(stripped);

  const entry = slugToEntry.get(basePath);
  const lang = targetLocale as Locale;

  if (lang === 'de') {
    // DE (default) — kein Präfix, deutschen Slug zurückgeben
    return entry ? entry.de : basePath;
  }

  // EN/ES — Locale-Code als Präfix, sprachspezifischen Slug verwenden
  const targetSlug = entry ? entry[lang] : basePath;
  return `/${lang}${targetSlug === '/' ? '' : targetSlug}` || `/${lang}`;
}
---
// LanguageSwitcher.astro (vereinfacht)
import Icon from '../atoms/Icon.astro';
import { getLocalePathFromCurrent } from '../../i18n/utils';

const currentLocale = Astro.currentLocale || 'de';
const pathname = Astro.url.pathname;

const locales = [
  { code: 'de', label: 'Deutsch', flag: 'DE' },
  { code: 'en', label: 'English', flag: 'EN' },
  { code: 'es', label: 'Español', flag: 'ES' },
];

function getLocalizedPath(locale: string): string {
  return getLocalePathFromCurrent(locale, pathname);
}
---

<div class="lang-switcher">
  <button aria-expanded="false" aria-haspopup="listbox">
    <Icon name="globe" size={16} />
    <span>{currentLocale.toUpperCase()}</span>
  </button>

  <ul role="listbox" aria-label="Sprache wählen">
    {locales.map((locale) => (
      <li role="option" aria-selected={locale.code === currentLocale}>
        <a href={getLocalizedPath(locale.code)} hreflang={locale.code}>
          <span>{locale.flag}</span>
          <span>{locale.label}</span>
        </a>
      </li>
    ))}
  </ul>
</div>

Die Funktion getLocalePathFromCurrent() arbeitet in drei Schritten:

  1. Präfix entfernen: Der Regex /^\/(en|es)(\/|$)/ entfernt /en/ oder /es/ vom Anfang der URL. Was übrig bleibt, ist der rohe Slug — egal in welcher Sprache er gerade ist.

  2. Slug nachschlagen: Der gereinigte Pfad wird in der slugToEntry-Map gesucht. Wenn /contact übrig bleibt, findet die Map den Eintrag mit allen drei Sprachversionen.

  3. Zielsprache zusammensetzen: Für Deutsch gibt es keinen Präfix, für EN/ES wird der Locale-Code vorangestellt.

Die Übersetzungs-JSONs

Neben den Slugs brauchen wir auch übersetzte UI-Texte: Navigationslabels, Button-Texte, Formularbeschriftungen, Footer-Links. Diese leben in drei JSON-Dateien mit identischen Schlüsseln.

{
  "nav.home": "Startseite",
  "nav.about": "Über mich",
  "nav.contact": "Kontakt",
  "nav.services": "Leistungen",
  "nav.blog": "Blog",
  "nav.faq": "FAQ",
  "hero.cta.primary": "Kontakt aufnehmen",
  "hero.cta.secondary": "Portfolio ansehen",
  "footer.copyright": "Alle Rechte vorbehalten.",
  "cookie.message": "Nur essenzielle Cookies — kein Tracking.",
  "cookie.accept": "Akzeptieren"
}
{
  "nav.home": "Home",
  "nav.about": "About",
  "nav.contact": "Contact",
  "nav.services": "Services",
  "nav.blog": "Blog",
  "nav.faq": "FAQ",
  "hero.cta.primary": "Get in Touch",
  "hero.cta.secondary": "View Portfolio",
  "footer.copyright": "All rights reserved.",
  "cookie.message": "Only essential cookies — no tracking.",
  "cookie.accept": "Accept"
}
{
  "nav.home": "Inicio",
  "nav.about": "Sobre mí",
  "nav.contact": "Contacto",
  "nav.services": "Servicios",
  "nav.blog": "Blog",
  "nav.faq": "FAQ",
  "hero.cta.primary": "Contacto",
  "hero.cta.secondary": "Ver Portfolio",
  "footer.copyright": "Todos los derechos reservados.",
  "cookie.message": "Solo cookies esenciales — sin rastreo.",
  "cookie.accept": "Aceptar"
}

Der t()-Helper ist bewusst minimal:

import en from './locales/en.json';
import de from './locales/de.json';
import es from './locales/es.json';

type Locale = 'de' | 'en' | 'es';

const translations: Record<Locale, Record<string, string>> = { de, en, es };

/**
 * Übersetzten String nach Key für die gegebene Locale holen.
 * Fallback auf Deutsch (default), wenn der Key in der Zielsprache fehlt.
 */
export function t(locale: string | undefined, key: string): string {
  const lang = (locale || 'de') as Locale;
  return translations[lang]?.[key] || translations.de[key] || key;
}

Keine Verschachtelung, keine Namensräume, keine Interpolation. Ein flaches Key-Value-Objekt pro Sprache und eine Funktion, die den Wert zurückgibt. Der Fallback auf Deutsch stellt sicher, dass fehlende Übersetzungen niemals einen leeren String ergeben — du siehst immer mindestens den deutschen Text, und wenn auch der fehlt, den Key selbst. Das ist beim Debugging Gold wert.

Die Dateistruktur

Hier eine Übersicht, wie alle Teile zusammenpassen:

Architektur-Übersicht

  1. astro.config.mjs

    Definiert defaultLocale: "de", locales: ["de", "en", "es"] und prefixDefaultLocale: false. Das ist die gesamte Framework-Konfiguration — drei Zeilen, die Astro sagen, wie Routen zu Sprachen zugeordnet werden.

  2. src/i18n/utils.ts

    Enthält die Slug-Tabelle, die Reverse-Lookup-Map, getLocalePath(), getLocalePathFromCurrent(), t() und getTranslations(). Eine einzige Datei mit ~110 Zeilen, die das gesamte i18n-System antreibt.

  3. src/i18n/locales/*.json

    Drei JSON-Dateien (de.json, en.json, es.json) mit identischen Keys und sprachspezifischen Werten. Enthalten alle UI-Texte: Navigation, Buttons, Formulare, Fehlermeldungen, SEO-Metadaten.

  4. src/pages/en/*.astro & src/pages/es/*.astro

    Für jede Seite gibt es eine englische und spanische Version unter dem jeweiligen Locale-Ordner. Die Dateinamen entsprechen den übersetzten Slugs: pages/en/about.astro wird zu /en/about, pages/es/sobre-mi.astro wird zu /es/sobre-mi.

  5. Komponenten & Layouts

    Header, Footer und Navigation verwenden getLocalePath() und t() mit Astro.currentLocale, um Links und Labels sprachabhängig zu rendern. Keine Duplizierung — eine Komponente für alle drei Sprachen.

src/
├── i18n/
│   ├── utils.ts              ← Slug-Tabelle + Helper-Funktionen
│   └── locales/
│       ├── de.json            ← Deutsche UI-Texte
│       ├── en.json            ← Englische UI-Texte
│       └── es.json            ← Spanische UI-Texte
├── pages/
│   ├── index.astro            ← DE Homepage (/)
│   ├── kontakt.astro          ← DE Kontakt (/kontakt)
│   ├── ueber-mich.astro       ← DE Über mich (/ueber-mich)
│   ├── leistungen.astro       ← DE Leistungen (/leistungen)
│   ├── en/
│   │   ├── index.astro        ← EN Homepage (/en/)
│   │   ├── contact.astro      ← EN Contact (/en/contact)
│   │   ├── about.astro        ← EN About (/en/about)
│   │   └── services.astro     ← EN Services (/en/services)
│   └── es/
│       ├── index.astro        ← ES Homepage (/es/)
│       ├── contacto.astro     ← ES Contacto (/es/contacto)
│       ├── sobre-mi.astro     ← ES Sobre mí (/es/sobre-mi)
│       └── servicios.astro    ← ES Servicios (/es/servicios)
├── components/
│   └── molecules/
│       └── LanguageSwitcher.astro  ← Nutzt getLocalePathFromCurrent()
└── layouts/
    └── BaseLayout.astro       ← Nutzt t() und getLocalePath()

Plugin-basiert vs. eigene Lösung

Aspekt Plugin-basiert Eigene Slug-Tabelle Recommended
Bundle-Größe Zusätzliche JS-Abhängigkeit Null Laufzeit-Overhead
URL-Kontrolle Plugin entscheidet über Pfadformat Volle Kontrolle über jeden Slug
Flexibilität An Plugin-API gebunden Beliebig erweiterbar
Setup-Aufwand npm install + Konfiguration Eine Datei mit ~110 Zeilen
Slug-Übersetzung Oft nicht unterstützt Kernfeature
Debugging Plugin-Internals verstehen Eigener Code, volle Transparenz
Updates Abhängig von Plugin-Maintainern Keine externen Abhängigkeiten
TypeScript Variiert je nach Plugin Native TypeScript-Integration

SEO-Aspekte: hreflang und Canonical

Übersetzte URLs sind nur die halbe Miete. Suchmaschinen müssen wissen, welche Seiten zusammengehören. Dafür gibt es hreflang-Tags im <head>:

<!-- Auf der deutschen Seite /kontakt -->
<link rel="alternate" hreflang="de" href="https://arnoldwender.com/kontakt" />
<link rel="alternate" hreflang="en" href="https://arnoldwender.com/en/contact" />
<link rel="alternate" hreflang="es" href="https://arnoldwender.com/es/contacto" />
<link rel="alternate" hreflang="x-default" href="https://arnoldwender.com/kontakt" />
<link rel="canonical" href="https://arnoldwender.com/kontakt" />
---
// In SEO.astro oder BaseLayout.astro
import { getLocalePathFromCurrent, locales, defaultLocale } from '../i18n/utils';

const currentPath = Astro.url.pathname;
const siteUrl = 'https://arnoldwender.com';
---

{locales.map((locale) => (
  <link
    rel="alternate"
    hreflang={locale}
    href={`${siteUrl}${getLocalePathFromCurrent(locale, currentPath)}`}
  />
))}
<link
  rel="alternate"
  hreflang="x-default"
  href={`${siteUrl}${getLocalePathFromCurrent(defaultLocale, currentPath)}`}
/>

Jede Sprachversion einer Seite verweist auf alle anderen Versionen. Der x-default-Tag zeigt auf die deutsche Version als Fallback. So weiß Google, dass /kontakt, /en/contact und /es/contacto Übersetzungen der gleichen Seite sind — und zeigt dem Nutzer die richtige Version in den Suchergebnissen. Wie du zusätzlich mit JSON-LD Structured Data die Sichtbarkeit in Google weiter steigerst, erfährst du im SEO-Artikel.

Best Practices

Default-Locale zuerst

Baue immer erst die deutsche (Default-) Version komplett auf. Die EN/ES-Versionen sind dann Kopien mit übersetzten Inhalten und angepassten Slugs. So hast du ein funktionierendes Produkt, bevor du internationalisierst.

Slug-Konsistenz

Halte die Slug-Tabelle immer aktuell. Jede neue Seite braucht einen Eintrag in allen drei Sprachen. Mache das zur Routine: Seite erstellen → Slug-Tabelle ergänzen → Dateien in en/ und es/ anlegen.

Fallback-Strategie

Die t()-Funktion fällt automatisch auf Deutsch zurück. Aber Seiten brauchen explizite Fallbacks: Wenn eine ES-Version fehlt, leite auf DE weiter. Niemals eine 404 zeigen, nur weil eine Übersetzung fehlt.

SEO mit hreflang

Jede Seite muss hreflang-Tags für alle Sprachversionen enthalten — inklusive x-default. Vergiss nicht die Canonical-URL: Sie zeigt immer auf die aktuelle Sprachversion, nicht auf die Default-Locale.

Content Collections

Blog-Posts leben in locale-spezifischen Ordnern: blog/de/, blog/en/, blog/es/. Jeder Post hat ein locale-Feld im Frontmatter. So können Listings und Tags sprachspezifisch gefiltert werden.

Typensicherheit

Definiere den Locale-Typ explizit: type Locale = "de" | "en" | "es". Verwende ihn überall statt string. So fängt TypeScript Tippfehler in Locale-Parametern zur Compile-Zeit ab.

Ablauf bei einer neuen Seite

Deutsche Seite erstellen

Neue Datei in src/pages/ anlegen, z. B. leistungen.astro. Layout, Inhalte und SEO-Meta-Tags setzen. Testen.

Slug-Tabelle ergänzen

Neuen Eintrag in der slugTable hinzufügen: { de: "/leistungen", en: "/services", es: "/servicios" }. Die Reverse-Lookup-Map wird automatisch aktualisiert.

Englische Version anlegen

Datei in src/pages/en/services.astro erstellen. Den Dateinamen an den englischen Slug anpassen. Inhalte übersetzen, Locale auf "en" setzen.

Spanische Version anlegen

Datei in src/pages/es/servicios.astro erstellen. Inhalte ins Spanische übersetzen, Locale auf "es" setzen. Sprachumschalter testen — alle drei Links müssen funktionieren.

Navigation & hreflang aktualisieren

Falls die Seite in der Navigation erscheint, Übersetzungskeys in den drei JSON-Dateien ergänzen. hreflang-Tags werden automatisch generiert, wenn die SEO-Komponente getLocalePathFromCurrent() verwendet.

Häufig gestellte Fragen

Frequently Asked Questions

Zusammenfassung

Astros eingebautes i18n-Routing gibt dir das Fundament: locale-basierte Ordner, automatische Spracherkennung und saubere URL-Präfixe. Was fehlt — übersetzte Slugs, ein bidirektionaler Sprachumschalter und UI-Übersetzungen — lässt sich mit einer einzigen TypeScript-Datei und drei JSON-Dateien ergänzen. Kein Plugin, kein Build-Overhead, volle Kontrolle.

Die Slug-Tabelle ist das Herzstück: Sie mappt jeden Pfad auf seine drei Sprachvarianten und ermöglicht Cross-Locale-Auflösung in alle Richtungen. Die Helper-Funktionen bauen darauf auf und liefern fertige Pfade für Navigation, Links und den Sprachumschalter. Und die t()-Funktion sorgt dafür, dass jeder UI-Text in der richtigen Sprache angezeigt wird.

Das Ergebnis: Eine dreisprachige Website mit sauberen, SEO-freundlichen URLs, einem funktionierenden Sprachumschalter und exakt null zusätzlichen Abhängigkeiten. Alles in ~110 Zeilen TypeScript.