Paraglide JS + Svelte 5 Best Practices
Behandle die Locale als Request-/Build-Status, der durch einen Compiler aufgelöst wird, nicht als reaktiven App-Status – überlasse Paraglide die vollständige Dokumentnavigation für Locale-Wechsel und Svelte 5 Runes alles andere. Paraglide JS ist SvelteKits offizielle i18n-Integration: Es kompiliert messages/{locale}.json in tree-shakable, vollständig typisierte ESM-Funktionen und ermöglicht so bis zu ~70 % kleinere i18n-Bundles als Laufzeitbibliotheken (≈47 KB vs. ≈205 KB im veröffentlichten Benchmark mit 5 Locales / 200 Nachrichten), ohne asynchrone Waterfalls und ohne dass ein i18n-Provider in Svelte-5-Komponenten eingebunden werden muss.
1. Entscheidungstabelle für den Schnellzugriff
| Entscheidung | Empfohlene Standardeinstellung | Warum |
|---|---|---|
| Installationspfad | npx sv add paraglide="languageTags:en,de" in einer neuen SvelteKit-App | Richtet Vite-Plugin, Hooks, app.html-Attribute und .gitignore in einem Schritt ein |
| Paket | @inlang/paraglide-js v2.x | Der v1-Adapter @inlang/paraglide-js-adapter-sveltekit ist veraltet – verwende stattdessen das Kernpaket direkt |
| Strategie (SSR + lokalisierte URLs) | ["url", "cookie", "baseLocale"] | URL ist die SEO-Quelle der Wahrheit; Cookie speichert die Benutzerauswahl sichtbar für den Server; baseLocale wird immer zuletzt aufgelöst |
| Strategie (gespeicherte Überschreibung hinzufügen) | ["localStorage", "cookie", "preferredLanguage", "url", "baseLocale"] | localStorage allein ist für die erste SSR-Anfrage unsichtbar; Cookie überbrückt Server/Client |
| Locale-Wechsel-Verhalten | Standardmäßig setLocale(locale) (vollständige Dokumentnavigation) | Framework-Reaktivität rendert nicht <html lang>, dir, <title> oder lokalisierte URLs neu; Navigation schon |
| AsyncLocalStorage | Aktiviert lassen (Standard) | Vercel Edge und Cloudflare Workers (Node-Kompatibilität) unterstützen es; Deaktivieren riskiert, dass die Locale zwischen gleichzeitigen Requests ausläuft |
| TS-Deklarationen | emitTsDeclarations: true (TypeScript ≥ 5.6) | Hält Svelte- und TS-Sprachserver synchron, wenn sich Schlüssel während der Entwicklung ändern |
| Nachrichtenschlüssel | Flache, stabile, lesbare Zufallsschlüssel (calm_green_otter) | Offizielle Empfehlung; verschachtelte Schlüssel funktionieren, aber flache Schlüssel werden bevorzugt |
| Vorabrendern | export const prerender = true + entries() pro Locale | SSG benötigt für jede Locale eine eindeutige URL-Datei |
| Reaktivität für Locale | Standardmäßig keine in Komponenten | getLocale() ist Request-bezogen; ein neues Dokument wird pro Locale gerendert, daher müssen Runes es nicht verfolgen |
2. Einrichtung & Konfiguration (Paraglide 2.x + SvelteKit + Svelte 5)
2.1 Schnellster Weg
npx sv create my-app
npx sv add paraglide="languageTags:en,de,demo:yes"
Das Svelte-CLI-Add-on konfiguriert das Inlang-Projekt, das Vite-Plugin, die SvelteKit handle/reroute-Hooks, %lang%/%dir% in app.html sowie eine optionale Demoseite.
2.2 Manuelle Einrichtung (vollständiger Boilerplate)
Installiere und richte das Projekt ein, verdrahte dann vier Dateien:
vite.config.ts – Plugin mit expliziter Strategie:
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { paraglideVitePlugin } from '@inlang/paraglide-js';
export default defineConfig({
plugins: [
sveltekit(),
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/lib/paraglide',
emitTsDeclarations: true,
strategy: ['url', 'cookie', 'baseLocale'],
}),
],
});
emitTsDeclarations: true generiert .d.ts-Dateien neu, während du Nachrichten bearbeitest, sodass der Svelte- und der TypeScript-Server synchron bleiben; es erfordert TypeScript 5.6+ (setze false für den schnelleren JSDoc-Inferenzpfad).
src/app.html – Ersetze die hartcodierte Sprache:
<!doctype html>
<html lang="%lang%" dir="%dir%">
src/hooks.server.ts – Server-Middleware mit Request-bezogener Locale:
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
import { getTextDirection } from '$lib/paraglide/runtime';
const paraglideHandle: Handle = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => {
event.request = localizedRequest;
return resolve(event, {
transformPageChunk: ({ html }) =>
html.replace('%lang%', locale).replace('%dir%', getTextDirection(locale)),
});
});
export const handle: Handle = paraglideHandle;
Die Middleware erkennt die Locale (Strategien in der angegebenen Reihenfolge), leitet Dokumentanfragen um, wenn die URL nicht mit der erkannten Locale übereinstimmt, delokalisiert /de/about → /about, bevor SvelteKit es sieht, und verpackt die Anfrage in AsyncLocalStorage, sodass getLocale() die richtige Locale pro gleichzeitiger Anfrage zurückgibt.
src/hooks.ts – Routing-Grenze (muss hooks.ts sein, nicht hooks.server.ts):
import type { Reroute } from '@sveltejs/kit';
import { deLocalizeUrl } from '$lib/paraglide/runtime';
export const reroute: Reroute = (request) => deLocalizeUrl(request.url).pathname;
// project.inlang/paraglide.config.ts
import { defineConfig } from '@inlang/paraglide-js';
export default defineConfig({
outdir: './src/lib/paraglide',
strategy: ['url', 'cookie', 'baseLocale'],
urlPatterns: [
{ pattern: '/', localized: [['en', '/']] },
{
pattern: '/:path(.*)?',
localized: [
['en', '/en/:path(.*)?'],
['de', '/de/:path(.*)?'],
],
},
],
});
Rangfolge: CLI-Flags / Plugin-Optionen > Konfigurationsdatei > integrierte Standardeinstellungen. Der Watch-Modus lädt die Konfiguration heiß neu.
3. Kernnutzungsmuster (Nachrichten, Locale, Routing)
3.1 Nachrichten
import { m } from '$lib/paraglide/messages';
import type { LocalizedString } from '$lib/paraglide/runtime';
m.hello_world(); // "Hallo Welt!"
m.greeting({ name: 'Ada' }); // "Hallo Ada!" – Parameter sind typisiert
m.greeting({ name: 'Ada' }, { locale: 'de' }); // Erzwingt Locale für einen Aufruf
const title: LocalizedString = m.welcome_title(); // Markensicherer Zeichenkettentyp
Nachrichten leben in messages/{locale}.json; Locales werden in project.inlang/settings.json deklariert (baseLocale + locales). Bevorzuge flache, stabile Schlüssel; verschachtelte Schlüssel werden über m['user.profile.title']() unterstützt. Der dynamische Schlüsselzugriff verhindert Tree-Shaking – ordne stattdessen dynamische IDs expliziten Nachrichtenreferenzen zu.
3.2 Locale abrufen und setzen
import { getLocale, locales, setLocale, getTextDirection } from '$lib/paraglide/runtime';
getLocale(); // z. B. "de" – Request-bezogen auf dem Server
getTextDirection(); // "ltr" | "rtl" für die aktuelle Locale
setLocale('de'); // Vollständige Dokumentnavigation zur lokalisierten URL
setLocale() aktualisiert Ihre konfigurierten Strategien und führt dann eine vollständige Dokumentnavigation durch – zur lokalisierten URL, wenn die url-Strategie aktiv ist, andernfalls ein Neuladen. Dies ist beabsichtigt: <html lang>, dir, <title>, Metadaten und die lokalisierte URL sind Dokumentzustände, die die Framework-Reaktivität nicht aktualisieren kann.
Der Notausstieg setLocale(locale, { reload: false }) existiert nur für vollständig clientgerenderte Oberflächen (eingebettete Widgets, Erweiterungsseiten), die nicht wiederherstellbaren In-Memory-Status bewahren müssen. Verwende ihn niemals für gewöhnliche Locale-Auswahlen, auf SSR/SSG-Dokumenten oder auf Routen, deren Strategie url enthält – er lässt die alte URL und Shell an Ort und Stelle, sodass getLocale() weiterhin die alte Locale auflöst.
3.3 Lokalisierte URLs und Links
import { localizeHref, deLocalizeUrl, localizeUrl } from '$lib/paraglide/runtime';
localizeHref('/blog'); // "/de/blog" für aktuelle Locale
localizeHref('/blog', { locale: 'de' }); // Explizit
localizeUrl('https://example.com/about', { locale: 'de' }).href; // Vollständige URL-Form
URL-Muster verwenden die standardmäßige URLPattern-Syntax; die Reihenfolge ist wichtig – setze spezifische Muster vor Platzhalter und teste sie auf urlpattern.com.
3.4 Svelte 5 Locale-Wechsler (idiomatisch)
<script lang="ts">
import { page } from '$app/state'; // Svelte 5: Objekt, kein Store
import { resolve } from '$app/paths';
import { locales, localizeHref } from '$lib/paraglide/runtime';
</script>
<nav aria-label="Sprachen">
{#each locales as locale (locale)}
<a
href={resolve(localizeHref(page.url.pathname, { locale }))}
data-sveltekit-reload
aria-current={page.params.locale === locale ? 'page' : undefined}
>
{locale}
</a>
{/each}
</nav>
Zwei Svelte-5-Details sind hier wichtig: $app/state gibt page als reaktives Objekt frei (der $page-Store und $app/stores sind das Legacy-Muster), und data-sveltekit-reload auf sprachübergreifenden Links hält die lokalisierte URL, das servergerenderte Dokument und die Laufzeit-Locale synchron – auch erforderlich für vorgerendertes Crawling.
4. Rich-Text mit Svelte 5 Snippets (ParaglideMessage)
Markup ermöglicht es Übersetzern, die Struktur zu kontrollieren, während deine App das Rendering steuert. Tag-Namen sind app-definiert – nichts wird automatisch auf HTML abgebildet.
// messages/en.json
{
"cta": "{#link to=|/docs|}Lies die Doku{/link}",
"welcome": "{#b}Hallo {name}{/b}{#icon/}"
}
<script lang="ts">
import { ParaglideMessage } from '@inlang/paraglide-js-svelte';
import { m } from '$lib/paraglide/messages';
const name = $props().name; // oder beliebiges $state
</script>
<ParaglideMessage message={m.welcome} inputs={{ name }}>
{#snippet b({ children })}
<strong>{@render children?.()}</strong>
{/snippet}
{#snippet icon({ attributes })}
<Icon variant={attributes?.variant} />
{/snippet}
</ParaglideMessage>
<ParaglideMessage> verwendet message.parts(), wenn Markup vorhanden ist, und fällt auf message() für Klartext zurück; die {#snippet tag({ children, options, attributes })}-Syntax ist genau das Svelte-5-Snippet-Muster – keine Slots, kein <slot>-Legacy. Beachte die Literalsyntax to=|/docs| (durch Pipe getrennt) und dass m.welcome() allein den markierungslosen Klartext zurückgibt.
5. Svelte 5 Runes-Integration: Was reaktiv ist (und was nicht)
Das zentrale mentale Modell: Paraglide besitzt die Locale als Dokument-/Request-Status; Svelte 5 Runes besitzen den UI-Status. Da Locale-Wechsel ein neues Dokument ausliefern, brauchst du selten Runes, um getLocale() zu verfolgen. Muster, die in der Praxis vorkommen:
Lokalisierungsabhängige abgeleitete Werte innerhalb eines einzelnen Dokuments (z. B. eine reine Client-Oberfläche mit reload: false oder ein Vorschaufenster, das mehrere Locales explizit rendert):
<script lang="ts">
import { getLocale, locales } from '$lib/paraglide/runtime';
import { m } from '$lib/paraglide/messages';
let { locale = getLocale() } = $props(); // Typisierte Props
// $derived wertet neu aus, wenn sich die `locale`-Prop ändert – nicht, wenn
// sich Paraglides interne Locale ändert (was ohne Navigation nicht passiert):
const greeting = $derived(m.greeting({ name: 'Ada' }, { locale }));
const isRtl = $derived(getTextDirection(locale) === 'rtl');
</script>
Das Weitergeben der erzwungenen Locale nach unten anstelle des Lesens des globalen Status ist das Runen-freundliche Muster: Die { locale }-Optionen bei jedem Nachrichtenaufruf machen Komponenten zu reinen Funktionen von Props, was genau der Art und Weise entspricht, wie Svelte 5 den Statusfluss haben möchte. Reserviere $effect für echte Seiteneffekte (z. B. Persistieren einer Vorschau-Locale) – weise niemals einem $derived zu und schreibe kein $state von innerhalb eines $effect, um Locale zu "pushen"; übergib stattdessen Props.
Globaler Client-Status (z. B. "zuletzt angezeigte Locale" für ein In-App-Übersetzungstool):
// src/lib/locale-preview.svelte.ts – .svelte.ts-Dateien unterstützen Runes auf Modulebene
export const preview = $state({ locale: 'en' as string | undefined });
$state auf Modulebene in .svelte.ts-Dateien wird von allen Importierenden gemeinsam genutzt – in Ordnung für appweite Singletons, aber denk daran, dass dies nicht die produktive Locale ist (das ist die Strategiekette + Dokumentnavigation).
6. SSR, Hydration und SSG
6.1 SSR-Regeln
- Eine
handle-Kettenreihenfolge: SetzeparaglideHandlean die erste Stelle (oder vor Auth-Handler, die einen nach Locale indiziertenevent.localsbenötigen), damit nachgelagerte Hooks die delokalisierte Anfrage sehen. - Hinter einem TLS-terminierenden Proxy übergib
effectiveRequestUrl, sodass URL-Matching und Weiterleitungen den browserzugewandten Ursprung verwenden. - Lasse
AsyncLocalStorageaktiviert; es machtgetLocale()unter gleichzeitigen Anfragen und Streaming SSR sicher. Der FallbackdisableAsyncLocalStorageist nur für Laufzeiten ohnenode:async_hooksund garantierter Request-Isolation gedacht – die Verwendung an anderer Stelle kann dazu führen, dass die Locale über verschiedene Benutzer hinweg ausläuft.
6.2 Hydrationskorrektheit
Server und Client müssen die Locale in derselben Strategiereihenfolge auflösen. Der klassische Fehler: Der Server löst aus der URL auf, aber die Client-Strategie liest zuerst localStorage, das zur SSR-Zeit nicht existiert. Behebe dies, indem du eine sichtbare cookie-Strategie für den Server einfügst, wenn eine persistierte Auswahl die erste Antwort prägen muss.
6.3 SSG (adapter-static / prerender)
Vorabrendern führt die Middleware niemals aus – setze die Locale während des Builds pro Seite:
// src/routes/[locale]/+layout.ts
import { setLocale, assertIsLocale } from '$lib/paraglide/runtime';
import type { LayoutLoad } from './$types';
export const prerender = true;
export function load({ params }: { params: { locale: string } }) {
setLocale(assertIsLocale(params.locale));
}
// src/routes/[locale]/+page.ts
import { locales } from '$lib/paraglide/runtime';
export const prerender = true;
export function entries() {
return locales.map((locale) => ({ locale }));
}
SSG erfordert, dass jede Locale ein URL-Präfix trägt (konfiguriere urlPatterns, sodass /en/… und /de/… beide existieren), sonst kann SvelteKit keine separaten Dateien pro Locale ausgeben. Für den SPA-Fallback des statischen Adapters setze kit.paths.relative = false, um lokalisierte Asset-404-Fehler zu vermeiden, und verwende generateStaticLocalizedUrls() für Sitemaps.
7. Leistung & Tree-Shaking
-
Rufe Nachrichtenfunktionen direkt auf.
m.hello()kompiliert zu einer kleinen ESM-Funktion; dein Bundler verwirft unbenutzte Nachrichten und ihre Übersetzungen automatisch. Die Bundle-Größe bleibt flach, während der Katalog wächst (Benchmark: 47 KB, unabhängig davon, ob das Projekt 200 oder 1.000 Nachrichten enthält). Verwende niemalsm['hello']()oderm[someKey]()– dynamischer Zugriff zwingt den Bundler, alle Nachrichten zu behalten, da er nicht statisch erkennen kann, welche Schlüssel verwendet werden. -
**--
// Fortsetzung folgt