Tutorials//10 Min. Lesezeit

SvelteKit i18n: Typsichere mehrsprachige Apps mit Svelte 5 entwickeln

Eray Gündoğmuş
Teilen

SvelteKit i18n: Typsichere mehrsprachige Apps mit Svelte 5 entwickeln

SvelteKits Architektur eignet sich überraschend gut für Internationalisierung. Seine serverseitigen Load-Funktionen laufen vor dem Rendering, was Locale-Erkennung und Nachrichten-Laden zu einem natürlichen Bestandteil des Frameworks macht. URL-basiertes Routing mit Layout-Hierarchien bedeutet, dass Routen mit /en/ oder /de/ präfixiert werden können, ohne gegen das Framework anzukämpfen. Und Svelte 5's Runes — $state, $derived, $effect — ermöglichen reaktives Locale-Wechseln ohne den Boilerplate-Store-Aufwand, der in Svelte 4 nötig war.

Dieser Leitfaden führt durch den Aufbau einer produktionsreifen mehrsprachigen SvelteKit-App mit Svelte 5. Wir behandeln Locale-Erkennung, typsichere Übersetzungsfunktionen, Pluralisierung mit der Intl-API und SEO-Überlegungen. Außerdem betrachten wir ehrlich die Abwägungen zwischen dem Bündeln von Übersetzungsdateien und deren Auslieferung über ein CDN.

Projekt-Setup

Starte mit einem neuen SvelteKit-Projekt (TypeScript-Template):

npx sv create my-i18n-app
cd my-i18n-app
npm install

Für diesen Leitfaden halten wir Abhängigkeiten minimal — die in modernen Browsern eingebauten Intl-APIs übernehmen die Formatierung, und wir schreiben selbst eine dünne Übersetzungsschicht. Falls du ICU-Nachrichtenformat-Unterstützung benötigst, können Bibliotheken wie @messageformat/core eingebunden werden.

Zielverzeichnisstruktur:

src/
  lib/
    i18n/
      index.ts          # Core translation utilities
      types.ts          # Generated/shared types
      locales/
        en.json
        de.json
        fr.json
  hooks.server.ts       # Locale detection
  routes/
    [locale]/
      +layout.server.ts # Load translations
      +layout.svelte    # Provide translations to tree
      +page.svelte

Definiere unterstützte Locales in einer gemeinsamen Konfiguration:

// src/lib/i18n/config.ts
export const SUPPORTED_LOCALES = ['en', 'de', 'fr'] as const;
export type Locale = typeof SUPPORTED_LOCALES[number];
export const DEFAULT_LOCALE: Locale = 'en';

export function isSupportedLocale(locale: string): locale is Locale {
  return SUPPORTED_LOCALES.includes(locale as Locale);
}

Locale-Erkennung und Routing

SvelteKits hooks.server.ts ist der richtige Ort, um die Locale eines Nutzers zu erkennen, bevor eine Route ausgeführt wird. Wir lesen zuerst aus der URL, fallen dann auf den Accept-Language-Header zurück und schließlich auf den Standard.

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { DEFAULT_LOCALE, isSupportedLocale } from '$lib/i18n/config';

export const handle: Handle = async ({ event, resolve }) => {
  // Extract locale from URL path segment
  const pathLocale = event.url.pathname.split('/')[1];

  const locale = isSupportedLocale(pathLocale)
    ? pathLocale
    : detectFromHeader(event.request.headers.get('accept-language'));

  event.locals.locale = locale;

  return resolve(event, {
    transformPageChunk: ({ html }) => html.replace('%lang%', locale),
  });
};

function detectFromHeader(header: string | null): import('$lib/i18n/config').Locale {
  if (!header) return DEFAULT_LOCALE;

  const accepted = header
    .split(',')
    .map((part) => {
      const [lang, q = 'q=1'] = part.trim().split(';');
      return { lang: lang.split('-')[0].trim(), q: parseFloat(q.split('=')[1]) };
    })
    .sort((a, b) => b.q - a.q);

  for (const { lang } of accepted) {
    if (isSupportedLocale(lang)) return lang;
  }

  return DEFAULT_LOCALE;
}

Füge das locale-Feld zum Locals-Typ deiner App hinzu:

// src/app.d.ts
import type { Locale } from '$lib/i18n/config';

declare global {
  namespace App {
    interface Locals {
      locale: Locale;
    }
  }
}

Für URL-basiertes Routing erstelle ein [locale]-Segment auf oberster Ebene und füge einen Params-Matcher zur Validierung hinzu:

// src/params/locale.ts
import { isSupportedLocale } from '$lib/i18n/config';

export function match(param: string): boolean {
  return isSupportedLocale(param);
}

Verweise in deinem Routen-Verzeichnis auf diesen Matcher: src/routes/[locale=locale]/. SvelteKit matched nur Pfade, bei denen das Segment ein gültiger Locale-String ist.

Übersetzungen laden

In +layout.server.ts unterhalb des [locale]-Segments werden die Übersetzungen für die aktuelle Locale geladen. Dies läuft serverseitig vor dem Seitenrendering, sodass kein Aufblitzen von nicht übersetztem Inhalt entsteht.

// src/routes/[locale=locale]/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ params, fetch }) => {
  const locale = params.locale;

  // Option A: Bundled JSON (imported directly)
  const messages = await import(`$lib/i18n/locales/${locale}.json`);

  // Option B: CDN endpoint (no redeploy needed for translation updates)
  // const res = await fetch(`https://cdn.example.com/translations/${locale}.json`);
  // const messages = await res.json();

  return { locale, messages: messages.default };
};

Im Layout-Component werden Übersetzungen für untergeordnete Komponenten zugänglich gemacht:

<!-- src/routes/[locale=locale]/+layout.svelte -->
<script lang="ts">
  import { setContext } from 'svelte';
  import { createTranslator } from '$lib/i18n';
  import type { LayoutData } from './$types';

  const { data }: { data: LayoutData } = $props();

  const t = createTranslator(data.messages);
  setContext('t', t);
  setContext('locale', data.locale);
</script>

<slot />

Übersetzungen in Komponenten mit Svelte 5 Runes verwenden

Mit Svelte 5 können $state und $derived genutzt werden, um Übersetzungen reaktiv zu machen, wenn die Locale wechselt. Hier ist ein Übersetzungsstore-Muster, das sauber mit Runes funktioniert:

// src/lib/i18n/index.ts
import type { Messages } from './types';

export function createTranslator(messages: Messages) {
  return function t(key: keyof Messages, params?: Record<string, string | number>): string {
    const template = messages[key] ?? key;

    if (!params) return template;

    return Object.entries(params).reduce<string>(
      (result, [k, v]) => result.replaceAll(`{${k}}`, String(v)),
      template
    );
  };
}

In einer Komponente den Translator aus dem Context abrufen:

<!-- src/routes/[locale=locale]/+page.svelte -->
<script lang="ts">
  import { getContext } from 'svelte';
  import type { Translator } from '$lib/i18n/types';

  const t = getContext<Translator>('t');
  const locale = getContext<string>('locale');

  // Svelte 5: derived state from context
  let greeting = $derived(t('home.greeting', { name: 'World' }));
</script>

<h1>{greeting}</h1>
<p>{t('home.description')}</p>

Für Locale-Wechsel ohne vollständiges Seitenneulade den Translator reaktiv mit $state aktualisieren:

<script lang="ts">
  import { createTranslator } from '$lib/i18n';
  import type { Messages } from '$lib/i18n/types';

  let messages = $state<Messages>({} as Messages);
  let t = $derived(createTranslator(messages));

  async function switchLocale(newLocale: string) {
    const res = await fetch(`/api/translations/${newLocale}`);
    messages = await res.json();
    // Update URL without reload
    history.pushState({}, '', `/${newLocale}${window.location.pathname.replace(/^\/[^/]+/, '')}`);
  }
</script>

Typsicherheit

Eine typsichere Übersetzungsfunktion erkennt fehlende Schlüssel zur Kompilierzeit und ermöglicht IDE-Autovervollständigung. Typen aus der Basis-Locale-Datei generieren:

// src/lib/i18n/types.ts
// Auto-generated from en.json — do not edit manually
import type en from './locales/en.json';

export type Messages = typeof en;
export type MessageKey = keyof Messages;
export type Translator = (key: MessageKey, params?: Record<string, string | number>) => string;

Dieser Schritt lässt sich automatisieren. Ein einfaches Skript zur Typengenerierung:

// scripts/generate-i18n-types.ts
import { writeFileSync } from 'fs';

const output = `// Auto-generated from en.json — do not edit manually
import type en from './locales/en.json';

export type Messages = typeof en;
export type MessageKey = keyof Messages;
export type Translator = (key: MessageKey, params?: Record<string, string | number>) => string;
`;

writeFileSync('src/lib/i18n/types.ts', output);
console.log('Types generated.');

In den Build-Prozess in package.json einbinden:

{
  "scripts": {
    "generate:i18n": "tsx scripts/generate-i18n-types.ts",
    "prebuild": "npm run generate:i18n",
    "predev": "npm run generate:i18n"
  }
}

Nun erzeugt t('home.greetng') einen TypeScript-Fehler, weil 'home.greetng' in Messages nicht existiert. Tippfehler werden zur Kompilierzeit abgefangen, anstatt von Nutzern in der Produktion entdeckt zu werden.

Dies ist ein Bereich, in dem Plattformen wie Better i18n echten Mehrwert bieten: Sie generieren SDK-Typen automatisch aus deinem Übersetzungsschema, sodass TypeScript-Typen aktualisiert werden, ohne manuelle Skriptläufe, wenn ein Übersetzer einen Schlüssel hinzufügt oder umbenennt.

Wenn du neben Svelte auch React- oder Next.js-Apps entwickelst, gelten dieselben typsicheren Muster — framework-spezifische Implementierungen findest du unter React i18n: The Complete Guide to React Internationalization und Next.js App Router i18n: Server Components and RSC Patterns.

Pluralisierung und Formatierung

JavaScripts eingebaute Intl-API behandelt Pluralisierung korrekt für alle Locales ohne externe Abhängigkeiten:

// src/lib/i18n/format.ts

export function pluralize(
  locale: string,
  count: number,
  forms: Record<Intl.LDMLPluralRule, string>
): string {
  const rule = new Intl.PluralRules(locale).select(count);
  const template = forms[rule] ?? forms.other;
  return template.replace('{count}', String(count));
}

export function formatNumber(locale: string, value: number, options?: Intl.NumberFormatOptions): string {
  return new Intl.NumberFormat(locale, options).format(value);
}

export function formatDate(locale: string, date: Date, options?: Intl.DateTimeFormatOptions): string {
  return new Intl.DateTimeFormat(locale, options).format(date);
}

Verwendung in einer Komponente:

<script lang="ts">
  import { pluralize, formatNumber, formatDate } from '$lib/i18n/format';
  import { getContext } from 'svelte';

  const locale = getContext<string>('locale');

  const itemCount = 3;
  const price = 1499.99;
  const publishedAt = new Date('2024-06-15');

  let itemLabel = $derived(
    pluralize(locale, itemCount, {
      one: '{count} item',
      other: '{count} items',
      zero: 'no items',
      two: '{count} items',
      few: '{count} items',
      many: '{count} items',
    })
  );

  let formattedPrice = $derived(formatNumber(locale, price, { style: 'currency', currency: 'USD' }));
  let formattedDate = $derived(formatDate(locale, publishedAt, { dateStyle: 'long' }));
</script>

<p>{itemLabel}</p>
<p>{formattedPrice}</p>
<p>{formattedDate}</p>

Für die deutsche Locale wird 1499.99 als 1.499,99 $ und das Datum als 15. Juni 2024 dargestellt — ohne zusätzliche Bibliotheken.

SEO für mehrsprachiges SvelteKit

Suchmaschinen benötigen explizite Signale über alternative Sprachversionen deiner Seiten. Füge hreflang-Tags und lokalisierte Metadaten in den <svelte:head> deines Layouts ein. Dies korrekt umzusetzen ist entscheidend für mehrsprachige Rankings — unser vollständiger Leitfaden zu i18n SEO, hreflang-Tags und Locale-URLs behandelt jedes Implementierungsdetail und häufige Fallstricke.

<!-- src/routes/[locale=locale]/+layout.svelte -->
<script lang="ts">
  import { page } from '$app/stores';
  import { SUPPORTED_LOCALES } from '$lib/i18n/config';

  const { data }: { data: LayoutData } = $props();

  // Build alternate URLs for all supported locales
  let alternates = $derived(
    SUPPORTED_LOCALES.map((loc) => ({
      locale: loc,
      url: $page.url.href.replace(`/${data.locale}/`, `/${loc}/`),
    }))
  );
</script>

<svelte:head>
  {#each alternates as { locale, url }}
    <link rel="alternate" hreflang={locale} href={url} />
  {/each}
  <link rel="alternate" hreflang="x-default" href={alternates.find(a => a.locale === 'en')?.url} />
</svelte:head>

<slot />

Für Prerendering SvelteKit so konfigurieren, dass alle Locale-Varianten generiert werden:

// svelte.config.js
const config = {
  kit: {
    prerender: {
      entries: [
        '/en',
        '/de',
        '/fr',
        '/en/about',
        '/de/about',
        '/fr/about',
        // ... or generate dynamically
      ],
    },
  },
};

Für dynamische Prerender-Einträge ein +page.server.ts mit entries-Export verwenden:

// src/routes/[locale=locale]/+page.server.ts
import { SUPPORTED_LOCALES } from '$lib/i18n/config';

export function entries() {
  return SUPPORTED_LOCALES.map((locale) => ({ locale }));
}

CDN-first vs. gebündelte Übersetzungen

Beide Ansätze funktionieren — die richtige Wahl hängt vom Workflow ab.

Gebündelte Übersetzungen (Import aus JSON):

  • Kein zusätzlicher HTTP-Request pro Seitenaufruf
  • Übersetzungen versionsmäßig an das Deployment gebunden
  • Funktioniert offline und ohne externe Abhängigkeiten
  • Ein Tippfehler in einer Übersetzung zu korrigieren erfordert ein erneutes Deployment

CDN-Auslieferung:

  • Übersetzungsaktualisierungen gehen live, ohne die App neu zu deployen
  • Übersetzer können Änderungen unabhängig von Entwicklern veröffentlichen
  • Fügt einen Netzwerkrequest hinzu (durch aggressives Caching gemildert)
  • Erfordert eine Cache-Invalidierungsstrategie

Für die meisten Teams am Anfang ihrer i18n-Reise ist gebündeltes JSON einfacher. Mit wachsendem Übersetzungsteam und zunehmendem Tempo bei Content-Updates wird die Deployment-Kopplung mühsam. Ein Code-Release nur zum Beheben eines deutschen Button-Labels zu veröffentlichen skaliert nicht. i18n in der CI/CD-Pipeline automatisieren ist der natürliche nächste Schritt, wenn der gebündelte Ansatz an seine Grenzen stößt.

Dies ist das Kernproblem, das Plattformen wie Better i18n lösen: Übersetzungsnachrichten liegen auf deren CDN, ausgeliefert über versionierte URLs mit langen Cache-TTLs. Entwickler aktualisieren Code, Übersetzer aktualisieren Übersetzungen — unabhängig voneinander, ohne sich gegenseitig zu blockieren. Die Features-Seite erklärt detailliert, wie das funktioniert, einschließlich der Cache-Invalidierung beim Veröffentlichen von Übersetzungen.

Es gibt auch einen Mittelweg: Übersetzungen zur Build-Zeit von einem CDN abrufen und das Ergebnis in das Deployment-Artefakt bündeln. Schnelle lokale Ladezeiten ohne Locale-Dateien im Repository, aber für Updates ist weiterhin ein Redeploy nötig.

Zusammenfassung

SvelteKit und Svelte 5 bilden eine solide Grundlage für mehrsprachige Apps:

  • Hooks übernehmen die Locale-Erkennung serverseitig, bevor ein Rendering stattfindet
  • Dynamische Routen-Segmente mit Params-Matchern bieten sauberes URL-basiertes Locale-Routing
  • Load-Funktionen holen Übersetzungen einmal pro Layout und reichen sie im Komponentenbaum weiter
  • Svelte 5 Runes ($state, $derived) machen Locale-Wechsel reaktiv ohne manuelle Store-Abonnements
  • TypeScript-Typen, generiert aus der Basis-Locale, fangen fehlende oder falsch geschriebene Schlüssel zur Kompilierzeit ab
  • Intl-APIs behandeln Pluralisierung und Zahlen-/Datumsformatierung korrekt für jede Locale
  • svelte:head hreflang-Tags und vorgerenderte Locale-Routen informieren Suchmaschinen

Die hier beschriebene Architektur deckt die meisten Produktionsanforderungen ab. Die verbleibende Hauptlücke ist operativer Natur: Mit wachsender App erzeugt das Verwalten von Übersetzungsdateien im Repository Reibung zwischen Entwicklern und Übersetzern. CDN-Auslieferung und automatische Typgenerierung sind die zwei Bereiche, in denen zweckgebaute Werkzeuge sich am schnellsten bezahlt machen.

Wenn du eine SvelteKit-App mit einem echten Übersetzungsteam entwickelst, wirf einen Blick auf Better i18n's Svelte-Integration — sie bietet ein typsicheres SDK, CDN-Auslieferung out of the box und einen Git-basierten Review-Workflow, sodass Übersetzungsänderungen denselben Review-Prozess durchlaufen wie Code.

Better i18n ist eine entwicklerfreundliche Lokalisierungsplattform für moderne Frontend-Teams. Typsichere SDKs, Git-basierte Workflows, CDN-Auslieferung und KI-Übersetzung mit Glossar-Durchsetzung — ohne Locale-Dateien in deinem Repository.


Deine App mit better-i18n global machen

better-i18n vereint KI-gestützte Übersetzungen, Git-native Workflows und globale CDN-Auslieferung in einer entwicklerfreundlichen Plattform. Schluss mit Tabellenkalkulationen — fang an, in jeder Sprache auszuliefern.

Kostenlos starten → · Features erkunden · Dokumentation lesen

Comments

Loading comments...