Zahlen für sechs Sprachen formatieren: Trennzeichen, Währung und der Weg zurück
Veröffentlicht am 3.10.2025 · 12 Min. Lesezeit · Text- & Sprach-Tools
Daniel Okonkwo — Front-end-Entwickler und Tech-Redakteur bei OneKitly
Web-Performance · Dateiformate
Anhand von 5 Quellen geprüft
Dieselbe Menge schreibt sich im Deutschen, Spanischen und Italienischen 1.234.567,891, im Französischen 1 234 567,891 und im Englischen 1,234,567.891 — und die Leerzeichen der französischen Fassung sind keine, die man tippen kann. Lässt man Intl.NumberFormat auf Node 26 mit ICU 78 laufen, liefert es U+202F NARROW NO-BREAK SPACE als französisches Gruppentrennzeichen und U+00A0 NO-BREAK SPACE als europäisch-portugiesisches, beide unsichtbar und beide tödlich für einen Parser, der ein normales Leerzeichen oder ein Komma erwartet. Drei der sechs Sprachen gruppieren vierstellige Zahlen zudem gar nicht: Spanisch, europäisches Portugiesisch und Italienisch schreiben 1234 ohne Trennzeichen, aber 10.000 mit, weil CLDR ihre minimale Gruppierungsziffernzahl auf zwei setzt. Das Währungslayout teilt sich ebenso: Englisch setzt das Symbol ohne Zwischenraum vor die Ziffern, die anderen fünf hinter die Zahl mit einem geschützten Leerzeichen. Nichts davon macht Number.parseFloat rückgängig: Für die englische Zeichenkette liefert es 1, für die deutsche ein glaubwürdig aussehendes 1,234. Der einzig sichere Weg fragt Intl.NumberFormat.formatToParts, welche Zeichen diese Sprache wirklich verwendet, und entfernt dann das Gruppentrennzeichen, bevor das Dezimaltrennzeichen umgesetzt wird — in dieser Reihenfolge, denn andersherum wird der Wert vertausendfacht.
1.234,56 und 1,234.56 sind dieselbe Zahl, und sie zu verwechseln ändert den Wert, den eine Leserin herausliest. Wir haben Intl.NumberFormat für alle sechs Sprachen der Website laufen lassen und jedes Trennzeichen ausgegeben — auch das unsichtbare, das das Französische verwendet — und dann gemessen, warum parseFloat davon nichts rückgängig machen kann.
Dieselbe Zahl, sechs Schreibweisen
Wir haben einen Wert genommen, 1234567.891, und ihn mit Intl.NumberFormat in den sechs Sprachen formatiert, in denen diese Website erscheint. Englisch ergibt 1,234,567.891. Spanisch, Deutsch und Italienisch ergeben alle 1.234.567,891 — Komma und Punkt haben die Rollen getauscht, sodass jemand mit englischen Gewohnheiten eine tausendfach zu kleine Zahl liest und es nicht merkt. Französisch ergibt 1 234 567,891 und europäisches Portugiesisch ebenfalls, doch mit einem anderen unsichtbaren Zeichen als Abstand.
Das ist kein Darstellungsdetail. Eine Tabellenspalte, die aus einer Konvention in ein System eingefügt wird, das die andere erwartet, ändert stillschweigend jeden ihrer Werte, und der Fehler bleibt unsichtbar, weil beide Schreibweisen gültige Zahlen sind. Die Regel, die man verinnerlichen sollte: Eine formatierte Zahl ist ein Text über einen Wert, in einer bestimmten Sprache, und die Sprache muss mitreisen.
Zwei weitere Konventionen sind wissenswert, weil sie in europäischen Daten auftauchen. Schweizerdeutsch verwendet den Apostroph: de-CH formatiert denselben Wert als 1'234'567.891, mit Punkt als Dezimalzeichen. Und ein bloßes Sprachkürzel ist nicht dasselbe wie Sprache plus Region: Fragt man Intl nach „pt“, kommen brasilianische Konventionen heraus, 1.234.567,891, während „pt-PT“ die europäische Form mit Abständen liefert. Ist dein portugiesischsprachiges Publikum europäisch, ist das bloße Kürzel still und leise falsch.
Das Trennzeichen, das man nicht sieht
Das französische Gruppentrennzeichen ist in den aktuellen CLDR-Daten nicht die Leertaste. Fragt man Intl.NumberFormat nach den Teilen einer französischen Zahl, kommt U+202F NARROW NO-BREAK SPACE zurück. Europäisches Portugiesisch verwendet U+00A0 NO-BREAK SPACE, ein anderes Zeichen anderer Breite. Beide sehen in jedem Editor, jedem Terminal und jedem Browser genau wie ein Leerzeichen aus, und keines ist eines.
Die Folgen sind ganz praktisch. Eine Prüfregel wie /^[\d ,]+$/ weist eine korrekt formatierte französische Zahl zurück, weil das enthaltene Zeichen nicht das Leerzeichen der Klasse ist. Ein Suchen-und-Ersetzen, das Leerzeichen entfernt, lässt das Trennzeichen unangetastet. Ein CSV-Export erzeugt Zellen, die eine Tabellenkalkulation als Text statt als Zahlen liest. Und kopiert man die Zahl von einer Seite in einen Rechner, gibt es einen Fehler, den niemand erklären kann, denn das schuldige Zeichen ist auf beiden Seiten des Einfügens unsichtbar.
Der richtige Zug ist, das Trennzeichen nie zu raten. Intl.NumberFormat(locale).formatToParts(12345.6) liefert ein kleines Feld, in dem ein Eintrag den Typ „group“ und ein anderer den Typ „decimal“ trägt, und ihre Werte sind genau die Zeichen, die diese Sprache heute verwendet. Lies sie zur Laufzeit, und dein Code läuft weiter, wenn CLDR sich ändert — und das tut es: In älteren Daten war das französische Trennzeichen ein gewöhnliches geschütztes Leerzeichen, bevor das schmale es ablöste.
Drei Sprachen gruppieren vierstellige Zahlen nicht
Formatiert man 1234 in den sechs Sprachen, passen die Ergebnisse nicht zusammen. Englisch gibt 1,234, Französisch 1 234 und Deutsch 1.234 — aber Spanisch, europäisches Portugiesisch und Italienisch geben alle 1234, ganz ohne Trennzeichen. Geht man auf 10000, gruppieren alle: 10,000, 10 000, 10.000 beziehungsweise 10.000. Der Umschlag liegt zwischen vier und fünf Ziffern.
Dahinter steht eine CLDR-Einstellung namens minimale Gruppierungsziffern, die angibt, wie viele Ziffern links vom ersten Trennzeichen stehen müssen, damit sich das Gruppieren lohnt. Spanisch, europäisches Portugiesisch und Italienisch setzen sie auf zwei; Englisch, Französisch und Deutsch auf eins. Es gibt sie, weil eine vierstellige Zahl in diesen Sprachen oft eine Jahreszahl oder ein Aktenzeichen ist und ungebrochen besser liest. Braucht man die Gruppierung dennoch — eine Betragstabelle, deren Spalten fluchten müssen —, übergibt man useGrouping: "always", und Spanisch liefert 1.234. Die Gegenoption useGrouping: "min2" lässt Englisch sich wie Spanisch verhalten und 1234 ausgeben.
Währung und Prozent: wohin das Symbol gehört
Für einen Betrag von 1234,50 formatiert Englisch $1,234.50 — Symbol zuerst, kein Abstand, Gruppierung ab Tausend. Die fünf europäischen Sprachen setzen das Symbol alle ans Ende und alle ein geschütztes Leerzeichen davor: Französisch liefert den Betrag mit schmalen geschützten Leerzeichen in den Ziffern, dann ein geschütztes Leerzeichen und das Symbol €; Deutsch liefert 1.234,50, gefolgt von diesem Leerzeichen und dem Symbol; Spanisch, Portugiesisch und Italienisch liefern 1234,50 mit demselben Anhang, ungruppiert wegen der obigen Vierstellenregel.
Dieses geschützte Leerzeichen vor dem Symbol ist ein echtes Zeichen und steht mit Absicht dort: Es verhindert, dass ein Zeilenumbruch den Betrag von seiner Einheit trennt. Es zerlegt auch dieselben naiven Parser wie das Gruppentrennzeichen und lässt Zeichenkettenvergleiche gegen einen handgeschriebenen Erwartungswert in Tests ohne sichtbaren Grund scheitern. Vergleiche formatierte Ausgabe mit formatToParts, nicht per Gleichheit gegen ein selbst getipptes Literal.
Prozent hat seine eigene Falle, und sie betrifft keine Trennzeichen. style: "percent" multipliziert vor dem Formatieren mit 100. Übergibt man 12,34, weil man das Verhältnis schon umgerechnet hat, kommt 1.234 % heraus — eine hundertfach zu große Zahl, klaglos gedruckt. Übergib das rohe Verhältnis, 0,1234, und du erhältst 12,34 %. Auch der Abstand unterscheidet sich: Englisch schreibt 12.3% ohne Lücke, Französisch, Spanisch und Deutsch setzen ein geschütztes Leerzeichen vor das Zeichen, Portugiesisch und Italienisch nicht.
Nachkommastellen, Rundung und die Fallen in den Optionen
Die Vorgabe für eine schlichte Zahl sind höchstens drei Nachkommastellen, sodass 1,23456 als 1,235 erscheint und der Rest verschwindet. Bei einer Währung stammt die Vorgabe aus der Währung selbst: zwei Stellen für Dollar und Euro, null für den Yen, drei für den tunesischen Dinar. Meist ist das genau richtig, und es lohnt zu wissen, dass es geschieht, statt überall zwei anzunehmen.
Die beiden Optionen, die Support-Tickets erzeugen, sind minimumFractionDigits und maximumFractionDigits. Setzt man das Minimum über das Maximum, wirft es einen RangeError, statt zu kappen — immerhin laut. Setzt man nur das Minimum, hebt es stillschweigend das Maximum nach: minimumFractionDigits: 4 auf 1,23456789 druckt 1,2346 und nicht die zwei Stellen, die man von anderswo im Code erwartet haben mag.
Die Rundung hat eine Vorgabe, die man kennen sollte. Intl rundet die Hälfte von null weg, also wird 2,5 zu 3 und -0,5 zu -1 bei null Nachkommastellen; mit roundingMode: "halfEven" wird 2,5 zu 2, was Buchhaltung und Statistik meist wollen. Und Intl ist nicht toFixed: 1,005 auf zwei Stellen ergibt bei Intl 1,01 und bei toFixed 1,00, 2,675 ergibt bei Intl 2,68 und bei toFixed 2,67. Der Unterschied: toFixed rundet den binären Double, dessen Wert minimal unter der geschriebenen Dezimalzahl liegt, während Intl die gemeinte Dezimalzahl rundet.
Kompakte Notation, die eine Übersetzung ist und keine Abkürzung
Mit notation: "compact" wird aus 1234567 im Englischen 1.2M. Die anderen fünf Sprachen nutzen nicht alle M: Französisch gibt den Betrag mit einem M, Spanisch und Portugiesisch ebenfalls, Deutsch gibt Mio. und Italienisch Mln. In der Langform sind die Unterschiede deutlicher: 1.2 million, 1,2 million, 1,2 millones, 1,2 milhões, 1,2 Millionen, 1,2 milioni.
Bei den Tausendern wird es noch bunter. Englisch gibt 1.5K für 1500, Französisch 1,5 k mit kleinem k, Spanisch und Portugiesisch 1,5 mil, Italienisch 1,5K — und Deutsch gibt 1500, unverändert, weil CLDR für deutsche Tausender keine kurze Kompaktform kennt. Nimmt dein Dashboard an, alle Sprachen kürzten gleich, wird die deutsche Spalte breiter als die anderen, und daran lässt sich nichts einstellen.
Warum parseFloat davon nichts rückgängig machen kann
toLocaleString ist eine Einbahnfunktion. Wir haben 1234567,891 in jeder der sechs Sprachen formatiert und das Ergebnis direkt an Number.parseFloat zurückgegeben. Englisch lieferte 1. Französisch lieferte 1. Portugiesisch lieferte 1. Spanisch, Deutsch und Italienisch lieferten 1,234. Keines lieferte den Ausgangswert, und die letzten drei sind die gefährlichen, denn 1,234 ist eine völlig plausible Zahl, die keine Prüfung zurückweist.
Number() ist wenigstens ehrlich: Es liefert für alle sechs NaN, weil keines ein gültiges numerisches Literal ist. Damit ist Number() die bessere Wache, wenn man nur prüft, ob eine Zeichenkette eine nackte Maschinenzahl ist, und damit ist es als Parser für alles, was ein Mensch gelesen hat, unbrauchbar.
Die Reihenfolge der Entfernung wiegt schwerer, als man denkt. Nimm die deutsche Zeichenkette 1.234.567,891. Entferne zuerst die Punkte, wandle dann das Komma in einen Punkt: Ergebnis 1234567.891 — richtig. Wandle zuerst das Komma in einen Punkt und entferne dann die Punkte: Ergebnis 1234567891, tausendfach zu groß und trotzdem eine plausible Ganzzahl. Beides sind Zweizeiler, und nur einer stimmt.
Wir haben die sprachgesteuerte Fassung geschrieben und gegen alle sechs geprüft: Gruppen- und Dezimalzeichen aus formatToParts lesen, jedes Vorkommen des Gruppenzeichens löschen, das Dezimalzeichen durch einen Punkt ersetzen, alles Übrige verwerfen, was weder Ziffer noch Vorzeichen ist, dann umwandeln. Sie hat den Ausgangswert in allen sechs exakt reproduziert und ebenso die sechs Währungszeichenketten samt Symbolen und geschützten Leerzeichen geparst, ohne jeden sprachspezifischen Code.
| Sprache | 1234567,891 | Gruppentrennzeichen | Dezimaltrennzeichen | Betrag, 1234,50 |
|---|---|---|---|---|
| en-US | 1,234,567.891 | Komma | Punkt | $1,234.50 (Symbol zuerst) |
| fr-FR | 1 234 567,891 | U+202F schmales geschütztes Leerzeichen | Komma | 1 234,50 € (Symbol zuletzt) |
| es-ES | 1.234.567,891 | Punkt | Komma | 1234,50 € (keine Gruppierung unter 10.000) |
| pt-PT | 1 234 567,891 | U+00A0 geschütztes Leerzeichen | Komma | 1234,50 € (keine Gruppierung unter 10.000) |
| de-DE | 1.234.567,891 | Punkt | Komma | 1.234,50 € (Symbol zuletzt) |
| it-IT | 1.234.567,891 | Punkt | Komma | 1234,50 € (keine Gruppierung unter 10.000) |
Häufige Fragen
- Welches Trennzeichen verwendet das Französische wirklich für Tausender?
- U+202F NARROW NO-BREAK SPACE, in den zum Redaktionsschluss aktuellen CLDR-Daten — wir haben es direkt aus formatToParts auf Node 26 mit ICU 78 gelesen. Es ist weder die Leertaste noch U+00A0, das europäisches Portugiesisch verwendet. Ältere CLDR-Ausgaben nutzten U+00A0 auch fürs Französische; Code, der eines davon festschreibt, bricht bei einem Laufzeit-Update. Lies das Trennzeichen zur Laufzeit, dann erübrigt sich die Frage.
- Warum wird 1234 im Spanischen und Italienischen ohne Trennzeichen gedruckt?
- Weil CLDR die minimalen Gruppierungsziffern dieser Sprachen auf zwei setzt: Gruppierung beginnt erst, wenn mindestens zwei Ziffern vor dem ersten Trennzeichen stehen. Also wird 1234 schlicht geschrieben und 10.000 gruppiert. Das ist Absicht: Vierstellige Zahlen sind in diesen Sprachen oft Jahreszahlen oder Aktenzeichen. Übergib useGrouping: "always", wenn du das Trennzeichen dennoch brauchst, etwa um eine Zahlenspalte bündig zu halten.
- Kann ich toLocaleString und parseFloat als Paar verwenden?
- Nein, und der Fehlschlag ist leise. Wir haben 1234567,891 in sechs Sprachen formatiert und auf jedes Ergebnis parseFloat angewandt: dreimal kam 1, dreimal 1,234. Keines lieferte das Original. Number() liefert immerhin für alle sechs NaN, scheitert also laut. Formatieren dient der Anzeige, und Parsen braucht einen eigenen, von den Trennzeichen der Sprache gesteuerten Codepfad.
- Sollte ich formatierte oder rohe Zahlen speichern?
- Roh, immer, und formatiere so spät wie möglich. Ein gespeicherter Wert 1234567.891 ist eindeutig, und Arithmetik funktioniert darauf. Eine gespeicherte Zeichenkette 1.234.567,891 trägt eine Sprache mit sich, an die man sich dann erinnern muss, sortiert nicht numerisch und macht aus jeder Rechnung ein Parsen. Die formatierte Form gehört in die Anzeigeschicht, pro Anfrage aus der Sprache der Leserin erzeugt.
- Warum widerspricht toFixed(2) bei 1,005 dem Intl-Ergebnis?
- Weil sie Verschiedenes runden. Der Double-Wert, der 1,005 am nächsten liegt, ist minimal kleiner als 1,005, und toFixed rundet diesen binären Wert, ergibt also 1,00. Intl rundet die gewünschte Dezimalzahl und ergibt 1,01. Dieselbe Spaltung zeigt sich bei 2,675, wo toFixed 2,67 und Intl 2,68 liefert. Geht es um Geld, nimm Intl oder einen exakten Dezimaltyp und mische die beiden nie in einem Bericht.
- Reicht Intl ein bloßer Sprachcode?
- Nicht immer, und Portugiesisch ist das klarste Beispiel. Fragt man Intl nach „pt“, kommen brasilianische Konventionen — ein Punkt für Tausender, 1.234.567,891 —, weil dort die Mehrheit der Sprechenden lebt, während „pt-PT“ die europäische Form mit geschütztem Leerzeichen liefert. Englisch verhält sich umgekehrt genauso bei Datums- und Maßkonventionen. Ist dein Publikum für eine Sprache ein bestimmtes Land, nenne das Land im Sprachkürzel.
Artikel, die dich interessieren könnten
Alle Ratgeber →Ähnliche Tools
Quellen
- Ecma International — ECMA-402: ECMAScript Internationalization API Specification — Intl.NumberFormat
- Unicode Consortium — CLDR — Unicode Common Locale Data Repository (number formats and symbols)
- Unicode Consortium — UTS #35: Unicode Locale Data Markup Language — Part 3, Numbers
- Mozilla — MDN Web Docs — Intl.NumberFormat and formatToParts()
- Mozilla — MDN Web Docs — Number.prototype.toFixed() and Number.parseFloat()
Hast du einen Fehler in diesem Artikel entdeckt?