Eine fehlende Übersetzung
ist ein Typfehler.
Übersetzungsdateien driften, weil sie niemand prüft — ein Key wird im Englischen umbenannt, der deutsche Katalog behält den alten, und ein Besucher sieht einen rohen Key oder einen englischen String. Hier ist der zweite Katalog gegen den ersten typisiert: die Lücke kippt deinen Build statt deinen Nutzer.
// app.config.ts — the whole wiring
export default {
locales: ['en', 'de'] as const,
defaultLocale: 'en' as const,
}
// src/locales/de.ts — typed against en: a missing key fails typecheck
export default defineLocale<typeof en>()({
'nav.pricing': 'Preise',
})Was du aus einem Feld bekommst.
Ein Config-Feld verdrahtet das Ganze
Deklariere `locales` und `defaultLocale` in der app.config.ts, und das Framework legt einen Provider um deine App, lädt den passenden Katalog und macht die Übersetzungs-Hooks app-weit nutzbar. Kein Provider zum Mounten, kein Loader zum Schreiben, keine Bootstrap-Datei zum Synchronhalten.
Kataloge, die gegeneinander typechecken
Die Default-Locale definiert die Key-Menge; jeder andere Katalog ist dagegen typisiert. Füg einen Key auf Englisch hinzu, und die deutsche Datei kompiliert nicht mehr, bis sie ihn auch hat — der einzige Moment, in dem ihn wirklich jemand ergänzt.
Die URL-Strategie folgt dem Render-Modus
Eine statisch gerenderte Seite kann zur Build-Zeit kein Cookie lesen, sie bekommt also URL-präfigierte Locales (/de/…); eine interaktive App kann es, sie bekommt ein Cookie und keinen URL-Lärm. Beides steckt in den Templates — gewählt danach, was die Seite ist, nicht nach einer projektweiten Vermutung.
Auflösung, die den Besucher respektiert
Eine explizite Wahl gewinnt, dann die gespeicherte Präferenz, dann Accept-Language, dann dein Default. Wer Deutsch gewählt hat, bleibt über einen Reload hinweg auf Deutsch; wer nie gewählt hat, bekommt die Browser-Präferenz statt der Sprache deiner Zentrale.
Übersetzte Metadaten, nicht nur Fließtext
Route-Metadaten werden mit der aktiven Locale berechnet — Titel, Descriptions, Canonicals und OG-Tags sind also in dem HTML übersetzt, das ein Crawler liest. Genau die Hälfte von i18n, die eine reine Client-Bibliothek strukturell nicht leisten kann.
Daten, Zahlen und Plurale von der Plattform
Die Formatierung reitet auf Intl statt auf einer mitgelieferten Locale-Datenbank — ein Datum wird so geschrieben, wie diese Sprache Daten schreibt, Pluralregeln sind die der Sprache selbst, und es gehen keine Megabytes CLDR an den Browser.
Warum das Typsystem die Kataloge hält.
Nichts anderes bemerkt einen fehlenden Key rechtzeitig
Ein Runtime-Fallback verdeckt die Lücke: die Seite rendert, der Key löst zu Englisch oder zu sich selbst auf, und der Fehler erreicht einen Nutzer, bevor er einen Entwickler erreicht. Eine Lint-Regel sieht die Datei, aber nicht die Absicht. Die Sekundärkataloge gegen den Primärkatalog zu typisieren verlegt die Entdeckung an die einzige Stelle, an der das Beheben trivial ist — den Moment, in dem du den Key angelegt hast. Dieselbe Logik nutzt das Framework für Schemas und Procedures: leg den Vertrag dorthin, wo der Compiler ihn halten kann.
Eine fehlende Übersetzung, beide Wege.
// en.json
{ "cart.items": "You have %d items" }
// de.json — somebody will add it later
{}
t('cart.itmes', { count: 3 })
// ^ typo. No error. Renders the key, or the English, or nothing —
// whichever your library calls graceful. A user finds it.// locales/en.ts
export const en = { 'cart.items': 'You have {count, plural, one {# item} other {# items}}' }
// locales/de.ts — a missing key is a TYPE ERROR, not a fallback
export const de = { 'cart.items': 'Du hast {count, plural, one {# Artikel} other {# Artikel}}' }
t('cart.items', { count: 3 })
// ^ the key is checked, and so is `count` — a message whose
// placeholders differ between languages does not compile.Ein Fallback ist ein Design, das einer deutschen Besucherin Englisch ausliefert und das für erledigt hält. Typisierte Kataloge verschieben diese Entdeckung in den Build: ein Key, den eine Sprache hat und die andere nicht, ist ein Typfehler — und eine Nachricht, deren Platzhalter sich zwischen beiden unterscheiden, ebenso.
Internationalisierung im Detail.
Warum driften Übersetzungsdateien, und was hält das hier auf?
Weil sie niemand prüft. Ein Key wird im Englischen umbenannt, der deutsche Katalog behält den alten, und die Lücke zeigt sich als roher Key oder englischer String vor einem deutschen Besucher — meist von einem Kunden gemeldet statt im Review gefunden.
Hier definiert die Default-Locale die Key-Menge, und jeder andere Katalog ist dagegen typisiert. Einen Key auf Englisch hinzuzufügen lässt die deutsche Datei nicht mehr kompilieren, bis sie ihn auch hat — der einzige Moment, in dem ihn wirklich jemand ergänzt. Ein Runtime-Fallback würde genau diesen Moment verdecken.
Es ist dasselbe Prinzip, das der Rest des Frameworks auf Schemas und Procedures anwendet: leg den Vertrag dorthin, wo der Compiler ihn halten kann, damit der Fehler beim Tastendruck passiert statt in Produktion.
Wie viel muss ich verdrahten?
Deklarier `locales` und `defaultLocale` in der App-Config. Das Framework legt einen Provider um die App, lädt den passenden Katalog und macht die Übersetzungs-Hooks app-weit nutzbar — kein Provider zum Mounten, kein Loader zum Schreiben, keine Bootstrap-Datei zum Nachziehen.
Eine weitere Sprache ist ein Code in dieser Liste plus eine Katalogdatei. Weil der Katalog gegen den Default typisiert ist, sagt dir der Compiler dann genau, was fehlt, statt dass du zwei Objekte per Auge vergleichst.
Jedes Frontend-Template kommt von Anfang an zweisprachig, die Verdrahtung wird also vorgeführt statt beschrieben — inklusive Locale-Switcher und der Strategie, die zum Render-Modus der Seite passt.
Warum hängt die URL-Strategie am Render-Modus?
Weil eine statisch gerenderte Seite zur Build-Zeit kein Cookie lesen kann. Sie wird einmal pro Locale erzeugt, die Locale muss also in der URL stehen — `/de/…` — damit die richtige Variante als Datei existiert und indexierbar ist.
Eine interaktive Seite kann ein Cookie lesen, sie braucht den URL-Lärm also nicht: ein authentifiziertes Dashboard behält einen sauberen Pfad und wechselt die Sprache an Ort und Stelle. Beiden eine Strategie aufzuzwingen heißt entweder nicht indexierbare statische Seiten oder unnötig überladene URLs.
Die Auflösung respektiert den Besucher in definierter Reihenfolge: eine ausdrückliche Wahl gewinnt, dann die gespeicherte Präferenz, dann Accept-Language, dann dein Default. Wer Deutsch gewählt hat, bleibt über einen Reload hinweg dabei; wer nie gewählt hat, bekommt die Browser-Präferenz statt der Sprache deiner Zentrale.
Was ist mit dem, was Suchmaschinen lesen?
Route-Metadaten werden mit der aktiven Locale berechnet — Titel, Descriptions, Canonicals, OG-Tags und strukturierte Daten sind also im HTML selbst übersetzt, in der Version, die ein Crawler indexiert. Eine reine Client-Übersetzungsbibliothek kann diese Hälfte strukturell nicht leisten, weil der Crawler die Seite bereits gelesen hat.
Die strukturierten Daten müssen außerdem die richtige Sprache deklarieren, und das geht leicht schief: ein hartkodierter Wert heißt, dass jede übersetzte Seite Such- und Antwortmaschinen sagt, sie sei englisch. Diese Site hatte genau diesen Defekt, gefunden durch Lesen des gerenderten deutschen HTML statt des Codes.
Formatierung reitet auf dem Intl der Plattform statt auf einer mitgelieferten Locale-Datenbank — Daten werden so geschrieben, wie diese Sprache Daten schreibt, Pluralregeln sind die der Sprache, und es gehen keine Megabytes Locale-Daten an den Browser.
Was das Config-Feld verdrahtet.
| Aspekt | Wie er behandelt wird |
|---|---|
| Provider | Automatisch aus den deklarierten Locales gelegt — nichts zu mounten, keine Bootstrap-Datei. |
| Kataloge | Gegen die Default-Locale typisiert — ein fehlender Key kippt den Build statt den Besucher. |
| URL-Strategie | URL-präfigiert, wo die Seite statisch gerendert wird, Cookie-basiert, wo sie interaktiv ist. |
| Auflösung | Ausdrückliche Wahl, dann gespeicherte Präferenz, dann Accept-Language, dann dein Default. |
| Metadaten | Titel, Canonicals, OG-Tags und strukturierte Daten pro Route mit der aktiven Locale berechnet. |
| Formatierung | Daten, Zahlen und Plurale über Intl — kein gebündeltes CLDR an den Browser. |
Häufige Fragen
Was passiert bei einer fehlenden Übersetzung?
Der Build scheitert. Kataloge sind gegen die Default-Locale typisiert, ein fehlender Key ist also ein Compile-Fehler statt eines Runtime-Fallbacks — und das ist der Punkt, denn ein Fallback verdeckt die Lücke, bis ein Nutzer sie sieht.
Soll ich URL-Präfixe oder ein Cookie nehmen?
Das folgt dem Render-Modus, nicht dem Geschmack. Eine statisch gerenderte Seite kann zur Build-Zeit kein Cookie lesen, sie braucht also `/de/…` als indexierbare Datei; eine interaktive Seite kann es und behält eine saubere URL. Die Templates liefern beides, pro Seite gewählt.
Sind übersetzte Seiten indexierbar?
Ja — Metadaten werden pro Route mit der aktiven Locale berechnet, Head-Tags, Canonicals und strukturierte Daten sind also im HTML übersetzt, das ein Crawler liest. Prüfenswert im gerenderten Output statt nur im Code: ein hartkodierter Sprachwert in strukturierten Daten ist ein leicht ausgelieferter Defekt.
Wie groß ist das Client-Bundle?
Formatierung nutzt das Intl der Plattform statt einer gebündelten Locale-Datenbank, Pluralregeln und Datumsformate kosten auf der Leitung also nichts. Was du ausliefest, ist dein eigener Katalog für die aktive Locale.
Kann ich später eine dritte Sprache ergänzen?
Füg den Code der Config-Liste hinzu und eine Katalogdatei. Weil Kataloge gegen den Default typisiert sind, zählt der Compiler dann genau auf, was fehlt, statt dich zwei Objekte per Auge vergleichen zu lassen.
Was i18n berührt.
Öffne das Framework. Schau es dir selbst an.
Jede Primitive auf dieser Seite ist heute im Framework. Klone den Starter, lass `voltro dev` laufen, in zwei Minuten ist es auf dem Bildschirm.