i18n-Formatierung: Datum, Zahlen & Währung mit Intl und i18next (Guide 2026)
Übersetzung deckt die Wörter ab. Locale-bewusste Formatierung deckt alles Numerische ab, und genau dort fällt handgestricktes i18n leise auseinander: 1,234.56 ist im Deutschen 1.234,56 und im Französischen 1 234 567,89, derselbe Tag ist je nach Locale 7/7/2026, 7.7.2026 oder 2026/7/7, und Yen-Beträge haben gar keine Nachkommastellen. Die Verkettung '$' + price.toFixed(2) ist im größten Teil der Welt ein Bug.
Die gute Nachricht: Jede dieser Regeln wird bereits mit dem Browser ausgeliefert, und i18next stellt sie direkt in Ihren Übersetzungsstrings bereit. Dieser Guide behandelt die Intl-APIs, die die Arbeit machen, und die i18next-Verdrahtung darüber.
Inhaltsverzeichnis
- Warum manuelles Formatieren scheitert
- Der Intl-Werkzeugkasten
- Zahlen, Prozent & Kompaktschreibweise
- Währungen
- Datum, Uhrzeit & relative Zeitangaben
- Formatierung in i18next-Übersetzungen
- Eigene Formatierer
- Häufige Fehler
- Formatierte Strings über Sprachen hinweg verwalten
- Häufig gestellte Fragen
Warum manuelles Formatieren scheitert
Nehmen Sie eine Zahl, 1234567.89, und rendern Sie sie so, wie fünf reale Locales es erwarten:
| Locale | Darstellung |
|---|---|
en-US (Englisch, USA) | 1,234,567.89 |
de-DE (Deutsch) | 1.234.567,89 |
fr-FR (Französisch) | 1 234 567,89 |
de-CH (Deutsch, Schweiz) | 1'234'567.89 |
en-IN (Englisch, Indien) | 12,34,567.89 |
Die deutsche Darstellung vertauscht die Bedeutung von . und , gegenüber dem Englischen. Französisch gruppiert mit (schmalen geschützten) Leerzeichen. Indien gruppiert in Lakh und Crore, sogar die Gruppengrößen unterscheiden sich also. Datumsangaben haben dasselbe Problem (Monat/Tag/Jahr vs. Tag.Monat.Jahr vs. Jahr/Monat/Tag), und Währungen legen Symbolposition und währungsspezifische Dezimalregeln obendrauf.
Nichts davon sollte man pro Sprache von Hand codieren. Wie die Pluralregeln ist all das im Unicode CLDR definiert, und es steckt bereits in Ihrer Laufzeitumgebung.
Der Intl-Werkzeugkasten
Die ECMAScript Internationalization API (Intl) wird mit jedem modernen Browser und Node.js ausgeliefert, CLDR-Daten inklusive, es gibt also keine Locale-Bundles zu installieren:
| API | Was sie formatiert | i18next-Formatname |
|---|---|---|
Intl.NumberFormat | Zahlen, Prozent, Einheiten, Kompaktschreibweise, Währung | number, currency |
Intl.DateTimeFormat | Datum und Uhrzeit | datetime |
Intl.RelativeTimeFormat | „vor 3 Tagen", „in 2 Stunden" | relativetime |
Intl.ListFormat | „a, b und c" | list |
Intl.PluralRules | Pluralkategorien | treibt die Suffixe _one / _other an |
Die ersten vier sind dieser Artikel. Die letzte ist ein eigenes Thema und wird im oben verlinkten Pluralisierungs-Guide behandelt.
Zahlen, Prozent & Kompaktschreibweise
Intl.NumberFormat behandelt Trennzeichen, Prozent, physikalische Einheiten und Kompaktschreibweise („Social-Media-Notation"), pro Locale:
new Intl.NumberFormat('en-US').format(1234567.89) // "1,234,567.89"
new Intl.NumberFormat('de-DE').format(1234567.89) // "1.234.567,89"
new Intl.NumberFormat('en', { style: 'percent' }).format(0.256) // "26%"
new Intl.NumberFormat('de', { style: 'percent' }).format(0.256) // "26 %"
new Intl.NumberFormat('en', { notation: 'compact' }).format(1234567) // "1.2M"
new Intl.NumberFormat('de', { notation: 'compact' }).format(1234567) // "1,2 Mio."
new Intl.NumberFormat('en', { style: 'unit', unit: 'kilometer-per-hour' }).format(88) // "88 km/h"Beachten Sie die Details, die niemand von Hand codieren würde: Das deutsche Prozentzeichen bekommt ein (geschütztes) Leerzeichen davor, und das Kompakt-Suffix ist ein locale-spezifisches Wort, nicht immer ein Buchstabe.
Währungen
Währungsformatierung ist Zahlenformatierung plus drei locale-abhängige Entscheidungen: welches Symbol, wo es steht und wie viele Nachkommastellen die Währung hat.
const price = 1234.5
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price)
// "$1,234.50"
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price)
// "1.234,50 €"
new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(price)
// "¥1,235"Die Yen-Zeile ist die, die Leute überrascht: JPY hat keine Untereinheiten, also rundet Intl auf ganze Yen. price.toFixed(2) hätte ¥1234.50 ausgegeben, was es nicht gibt.
Eine Regel zum Verinnerlichen: Die Währung kommt aus der Transaktion, die Formatierung aus der Locale des Nutzers. Ein deutscher Nutzer, der in Dollar kauft, sieht Dollar, deutsch formatiert:
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'USD' }).format(price)
// "1.234,50 $"Leiten Sie den Währungscode niemals aus der UI-Sprache ab.
Datum, Uhrzeit & relative Zeitangaben
Intl.DateTimeFormat übernimmt Reihenfolge, Trennzeichen und die Monats-/Wochentagsnamen. Die Presets dateStyle / timeStyle wählen ein sinnvolles Layout pro Locale:
const d = new Date('2026-07-07T15:30:00Z')
new Intl.DateTimeFormat('en-US').format(d) // "7/7/2026"
new Intl.DateTimeFormat('de-DE').format(d) // "7.7.2026"
new Intl.DateTimeFormat('ja-JP').format(d) // "2026/7/7"
new Intl.DateTimeFormat('en-US', { dateStyle: 'full' }).format(d)
// "Tuesday, July 7, 2026"
new Intl.DateTimeFormat('de-DE', { dateStyle: 'long', timeStyle: 'short', timeZone: 'Europe/Zurich' }).format(d)
// "7. Juli 2026 um 17:30"Formatierung konvertiert keine Zeitzonen; ohne explizite timeZone-Option wird in der Zone der Laufzeitumgebung gerendert.
Für Strings wie „vor 3 Tagen" / „in 2 Stunden" nimmt Intl.RelativeTimeFormat einen vorzeichenbehafteten Versatz plus eine Einheit:
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
rtf.format(-1, 'day') // "yesterday"
rtf.format(3, 'hour') // "in 3 hours"
new Intl.RelativeTimeFormat('de', { numeric: 'auto' }).format(-1, 'day') // "gestern"Und Intl.ListFormat verbindet Wortlisten mit den richtigen Trennzeichen und der richtigen Konjunktion pro Sprache:
new Intl.ListFormat('en').format(['HTML', 'CSS', 'JavaScript']) // "HTML, CSS, and JavaScript"
new Intl.ListFormat('de').format(['HTML', 'CSS', 'JavaScript']) // "HTML, CSS und JavaScript"Formatierung in i18next-Übersetzungen
Intl selbst aufzurufen funktioniert, zerlegt einen Satz aber in code-formatierte und übersetzte Fragmente. i18next löst das: Seit v21.3 sind die Intl-Formatierer in die Interpolation eingebaut, der Platzhalter trägt also sein Format:
{
"downloads_one": "{{count, number}} download",
"downloads_other": "{{count, number}} downloads",
"price": "On sale for {{val, currency(USD)}}",
"updated": "Updated {{val, relativetime}}",
"released": "Released on {{val, datetime}}",
"authors": "By {{val, list}}"
}t('downloads', { count: 1234567 }) // "1,234,567 downloads"
t('price', { val: 42.5 }) // "On sale for $42.50"
t('updated', { val: -3 }) // "Updated 3 days ago"
t('released', { val: new Date() }) // "Released on 7/7/2026"
t('authors', { val: ['Nina', 'Marco', 'Aisha'] }) // "By Nina, Marco, and Aisha"i18next übergibt der richtigen Intl-API automatisch die aktive Sprache, und aktuelle Versionen cachen die zugrunde liegenden Formatierer-Instanzen. Der downloads-Schlüssel zeigt, wie Formatierung mit den Plural-Suffixen _one / _other zusammenspielt: count wählt die Pluralform und wird lokalisiert gerendert. Und da das gewöhnliche t()-Interpolation ist, funktioniert es unverändert in react-i18next, next-i18next und jedem anderen Binding.
Formatoptionen stehen entweder inline im Schlüssel oder pro Aufruf via formatParams:
{
"revenue": "Revenue: {{val, currency}}",
"score": "{{val, number(minimumFractionDigits: 2)}} points"
}t('revenue', {
val: 12345.67,
formatParams: { val: { currency: 'EUR' } }
})
// active language en → "Revenue: €12,345.67"
// active language de → "Umsatz: 12.345,67 €"Sowohl der String als auch die Formatierung folgen der aktiven Sprache; in Ihrer Komponente ändert sich nichts. Halten Sie die Ressourcen als reines .json (Format-Referenz), und beachten Sie: Das hier ist Formatierung auf Interpolationsebene. Wie sich ganze Message-Formate vergleichen (ICU, Fluent, gettext, …), zeigt I18N in the Multiverse of Formats.
Eigene Formatierer
Alles, was Intl nicht abdeckt, können Sie selbst registrieren und wie ein eingebautes Format verwenden:
i18next.services.formatter.add('filesize', (value, lng) => {
const mb = value / (1024 * 1024)
return `${new Intl.NumberFormat(lng, { maximumFractionDigits: 1 }).format(mb)} MB`
}){ "attachment": "Attachment size: {{val, filesize}}" }Wenn die Setup-Arbeit pro Locale anfällt (wie das Konstruieren dieses Intl.NumberFormat), bieten neuere i18next-Versionen zusätzlich formatter.addCached, das den Formatierer einmal pro Sprache aufbaut und wiederverwendet. Projekte, die vor i18next v21 gestartet sind, konfigurieren womöglich noch die Legacy-Funktion interpolation.format(value, format, lng); sie funktioniert weiterhin, aber die eingebauten Formate decken inzwischen das meiste ab, wofür sie verwendet wurde (typischerweise Moment.js- oder Numeral.js-Wrapper).
Häufige Fehler
- Symbole verketten.
'$' + price.toFixed(2)macht Symbol, Position, Trennzeichen und Dezimalstellen falsch (JPY hat keine). Verwenden Siestyle: 'currency', immer. - Kein Locale-Argument.
toLocaleString()odernew Intl.NumberFormat()ohne Locale verwendet die Locale der Laufzeitumgebung, nicht die Sprache Ihres Nutzers, eine klassische Quelle für Server/Client-Hydration-Abweichungen. Die i18next-Formate übergeben immer die aktive Sprache. - Währung aus der UI-Sprache ableiten. Währung ist Transaktionsdatum. Formatieren Sie
USDfür einen deutschen Nutzer mit deutschen Trennzeichen; wechseln Sie nicht stillschweigend zuEUR. - Formate in Übersetzungen tippen. Wenn Übersetzer Muster im Stil von
1.234,56in Strings schreiben, riskiert jede Textänderung einen Formatfehler. Lassen Sie den rohen Platzhalter ({{val, number}}) im String und überlassen Sie den Rest CLDR. - Formatierung mit Zeitzonen verwechseln.
Intl.DateTimeFormatrendert in der Zone der Laufzeitumgebung, sofern Sie keintimeZoneübergeben. Formatierung lokalisiert die Darstellung, sie verstellt nicht die Uhr. - Fehlende ICU-Daten auf dem Server. Offizielle Node.js-Builds liefern seit v13 volle ICU-Daten mit, aber eigene oder
small-icu-Builds fallen still auf englische Formate zurück, ein Bug, den man nur serverseitig sieht.
Formatierte Strings über Sprachen hinweg verwalten
Die Formatierung selbst ist geschenkt, CLDR macht die Arbeit. Was in der Praxis bricht, sind die Platzhalter. Ein Übersetzer, der {{val, currency}} als {{val currency}} neu tippt oder das Wort relativetime ins Französische übersetzt, macht genau eine Sprache in Produktion kaputt, und kein Test gegen die englischen Strings wird das erwischen.
Genau hier verdient sich ein Translation-Management-System seinen Platz. Locize ist um das i18next-Message-Format herum gebaut:
- Platzhalter wie
{{val, datetime}}bleiben im Editor sichtbar; Übersetzer verschieben sie an die richtige Position, statt sie neu zu tippen. - Maschinelle und KI-Übersetzung ist i18next-format-bewusst, Platzhalter überstehen die Übersetzungsrunde also unversehrt.
- Schlüssel, die via saveMissing hinzukommen, bringen ihre Platzhalter mit und sind übersetzungsbereit.
Holen Sie die Formatierung aus Intl, halten Sie die Strings in JSON, und eine Locale hinzuzufügen wird zur Übersetzungsaufgabe statt zur Engineering-Aufgabe, was das eigentliche Ziel von i18n ist.
Häufig gestellte Fragen
Seit i18next v21.3 ist die Formatierung eingebaut: Schreiben Sie {{val, number}}, {{val, datetime}}, {{val, currency(USD)}}, {{val, relativetime}} oder {{val, list}} direkt in den Übersetzungsstring. i18next ruft die passende Intl-API (Intl.NumberFormat, Intl.DateTimeFormat, …) mit der aktiven Sprache auf, sodass die Ausgabe automatisch der Locale folgt.
Verwenden Sie {{val, currency(USD)}} im Übersetzungsstring, oder halten Sie den String generisch ({{val, currency}}) und übergeben Sie den Währungscode beim Aufruf via formatParams. Der Währungscode sollte aus Ihren Transaktionsdaten kommen, nicht aus der UI-Sprache: Ein deutscher Nutzer kann in USD zahlen, was als 1.234,50 $ gerendert wird, mit deutschen Trennzeichen und Dollarzeichen.
Intl ist die ECMAScript Internationalization API, die in jedem modernen Browser und in Node.js eingebaut ist. Sie liefert locale-bewusste Formatierer auf Basis von CLDR-Daten: Intl.NumberFormat (Zahlen, Währung, Prozent, Einheiten), Intl.DateTimeFormat (Datum und Uhrzeit), Intl.RelativeTimeFormat („vor 3 Tagen"), Intl.ListFormat (Wortlisten) und Intl.PluralRules (Pluralkategorien). Keine zusätzliche Bibliothek, keine Locale-Dateien im Bundle.
Für die Ausgabe-Formatierung: meist nein. Intl.DateTimeFormat und Intl.NumberFormat decken die locale-bewusste Darstellung ab, ohne Locale-Bundles auszuliefern. Bibliotheken wie date-fns oder Day.js bleiben für Datumsarithmetik und Parsing nützlich, und Moment.js ist im Wartungsmodus; die eigene Dokumentation empfiehlt Intl-basierte Alternativen.
Die häufigste Ursache ist ein Aufruf von toLocaleString() oder new Intl.NumberFormat() ohne Locale-Argument: Dann wird die Locale der Laufzeitumgebung verwendet, nicht die vom Nutzer gewählte Sprache, was auch Server/Client-Hydration-Abweichungen verursacht. Die eingebauten i18next-Formatierer übergeben immer die aktive Sprache. Prüfen Sie außerdem, ob der Wert wirklich eine Zahl bzw. ein Date-Objekt ist und kein String.
Intl.RelativeTimeFormat formatiert einen numerischen Versatz plus eine Einheit: format(-3, "day") ergibt „vor 3 Tagen", format(2, "hour") ergibt „in 2 Stunden", und die Option numeric: "auto" macht aus einem Versatz von 1 Tag „gestern" bzw. „morgen". In i18next schreiben Sie {{val, relativetime}} und übergeben den Versatz; die Berechnung des Versatzes aus einem Zeitstempel bleibt in Ihrem Code.
Ja. Registrieren Sie es mit i18next.services.formatter.add("myformat", (value, lng, options) => …) und verwenden Sie {{val, myformat}} in Übersetzungsstrings. Für Setup-Arbeit pro Locale gibt es formatter.addCached, das den Formatierer einmal pro Sprache aufbaut und wiederverwendet, dasselbe Caching, das i18next für seine eingebauten Intl-Formate nutzt.
Nein. Trennzeichen, Symbolposition und Datumsreihenfolge kommen pro Locale aus CLDR via Intl, niemand muss sie eintippen. Übersetzer verschieben nur Platzhalter wie {{val, datetime}} an die richtige Stelle im Satz. Ein Translation-Management-System wie Locize hält diese Platzhalter sichtbar und übersetzt maschinell um sie herum, ohne sie zu beschädigen.