Inhaltsverzeichnis
Unicode und Zeichenkodierung: Ein Entwickler-Leitfaden zu i18n
Bevor ein einziges Wort übersetzt werden kann, bevor ein Locale gewechselt wird, bevor Pluralisierungsregeln angewendet werden – muss Text korrekt gespeichert, übertragen und dargestellt werden. Dies ist der Bereich der Zeichenkodierung, und ein Fehler dabei erzeugt das unleserliche Zeichendurcheinander, das Entwickler als "Mojibake" bezeichnen.
Unicode hat das grundlegende Problem gelöst, alle Schriftsysteme der Welt in einem einzigen, universellen Standard darzustellen. Doch zu verstehen, wie Unicode funktioniert und wie seine verschiedenen Kodierungen (UTF-8, UTF-16, UTF-32) mit Ihrem Software-Stack interagieren, ist unverzichtbares Wissen für jeden Entwickler, der internationale Software erstellt.
Die Welt vor Unicode: Warum Kodierung wichtig ist
Vor Unicode hatte jedes Land und jede Sprache einen eigenen Kodierungsstandard. ASCII verarbeitete Englisch (128 Zeichen). Latin-1 (ISO 8859-1) fügte westeuropäische Zeichen hinzu. Windows-1252 war eine Microsoft-Variante von Latin-1. Shift-JIS kodierte Japanisch. GB2312 kodierte Chinesisch. KOI8-R kodierte russisches Kyrillisch.
Das Problem: Diese Kodierungen waren untereinander inkompatibel. Ein Dokument, das in Shift-JIS kodiert und als Latin-1 angezeigt wurde, produzierte unlesbaren Text. Eine Datenbank, die Zeichenketten in Windows-1252 speicherte und in UTF-8 anzeigte, verstümmelte Zeichen mit Akzenten. Systeme, die Daten über Kodierungsgrenzen hinweg transportierten – insbesondere E-Mail und das frühe Web – erzeugten ständig Mojibake.
Unicode wurde entwickelt, um dieses Problem durch eine einzige universelle Kodierung zu lösen, die alle Schriftsysteme umfasst.
Unicode: Der Standard erklärt
Unicode ist ein Zeichensatz-Standard – er weist jedem Zeichen in jedem Schriftsystem eine eindeutige Nummer ("Codepoint" genannt) zu. Der Unicode-Standard umfasst:
- Lateinische Schriftsysteme (Englisch, Französisch, Deutsch, Spanisch usw.)
- Kyrillisch (Russisch, Ukrainisch, Bulgarisch usw.)
- Arabisch und Hebräisch (RTL-Schriftsysteme)
- CJK (Chinesisch, Japanisch, Koreanisch) – über 90.000 Ideografien
- Devanagari (Hindi, Sanskrit)
- Thailändisch, Tibetisch, Khmer, Birmanisch
- Äthiopisch (Amharisch)
- Mathematische und wissenschaftliche Symbole
- Emoji (ja, Emojis sind Unicode-Zeichen)
Der gesamte Unicode-Coderaum enthält 1.114.112 mögliche Codepoints (von U+0000 bis U+10FFFF), von denen derzeit etwa 150.000 zugewiesen sind.
Codepoints vs. Zeichen
Ein Unicode-Codepoint ist eine Zahl von U+0000 bis U+10FFFF. Der Buchstabe "A" ist U+0041. Der Buchstabe "é" ist U+00E9. Das chinesische Zeichen 中 ist U+4E2D. Das Emoji 🌍 ist U+1F30D.
Ein "Zeichen", wie Benutzer es wahrnehmen, ist jedoch nicht immer ein einzelner Codepoint. Unicode verfügt über kombinierende Zeichen – separate Codepoints, die das vorangehende Zeichen modifizieren:
- "é" kann als einzelner Codepoint U+00E9 dargestellt werden (vorkombiniert)
- Oder als "e" (U+0065) + kombinierender Akut-Akzent (U+0301) = é (dekomprimiert)
Diese beiden Darstellungen sind visuell identisch, aber unterschiedliche Bytesequenzen. Das ist wichtig für:
- Zeichenkettenvergleich (sind diese zwei Zeichenketten gleich?)
- Zeichenkettenlänge (wie viele Zeichen?)
- Zeichenkettenindizierung (welches Zeichen steht an Position 3?)
Unicode-Normalisierung standardisiert diese Darstellungen. NFC (kanonische Zerlegung, dann kanonische Zusammensetzung) ist die gebräuchlichste Form für die Web-Nutzung. NFD zerlegt alles in Basis- + kombinierende Sequenzen.
Surrogate Pairs
Das ursprüngliche Unicode-Design zielte auf 65.536 Codepoints (16-Bit) ab und umfasste die "Basic Multilingual Plane" (BMP). Zeichen außerhalb der BMP – darunter viele CJK-Ideografien, historische Schriftsysteme und Emojis – erfordern Codepoints oberhalb von U+FFFF.
In UTF-16 werden Zeichen außerhalb der BMP als "Surrogate Pairs" kodiert – zwei 16-Bit-Einheiten, die zusammenarbeiten, um ein Zeichen zu kodieren. Dies ist eine häufige Fehlerquelle in JavaScript, das intern UTF-16 verwendet:
const emoji = '🌍'; // U+1F30D, außerhalb der BMP
// Falsch: behandelt Surrogate Pairs als separate Zeichen
emoji.length; // 2 (nicht 1!)
emoji[0]; // '\uD83C' (High Surrogate, nicht das Emoji)
emoji[1]; // '\uDF0D' (Low Surrogate, nicht das Emoji)
// Korrekt: Array.from oder den String-Iterator verwenden
Array.from(emoji).length; // 1
[...emoji].length; // 1
// Korrekt: codePointAt für den vollständigen Codepoint
emoji.codePointAt(0); // 127757 (0x1F30D)
// Korrekt: for...of iteriert nach Codepoint, nicht nach Code-Einheit
for (const char of emoji) {
console.log(char); // '🌍'
}
Graphem-Cluster
Selbst Codepoints entsprechen nicht immer dem, was Benutzer als "Zeichen" betrachten. Ein Graphem-Cluster ist eine Folge von Codepoints, die als eine einzelne visuelle Einheit dargestellt wird:
- Ein Grundbuchstabe + kombinierende diakritische Zeichen:
ê=e+̂ - Ein Emoji mit Modifikator:
👍🏾(Daumen hoch + Hautton-Modifikator) = 2 Codepoints, 1 Graphem - Ein Familien-Emoji:
👨👩👧👦= 4 Personen-Emojis verbunden durch Zero Width Joiner (ZWJ) = 1 Graphem, 11 Codepoints, 22 UTF-16-Code-Einheiten
Für Zeichenkettenoperationen, bei denen der Benutzer "ein Zeichen" wahrnehmen würde, möchten Sie Graphem-Cluster verwenden:
// Intl.Segmenter (moderne API) für Graphem-Segmentierung
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const text = '👨👩👧👦';
const graphemes = [...segmenter.segment(text)];
graphemes.length; // 1 (ein visuelles Zeichen)
UTF-8, UTF-16 und UTF-32: Die Kodierungen
Unicode ist ein Zeichensatz. UTF-8, UTF-16 und UTF-32 sind Kodierungen, die festlegen, wie Unicode-Codepoints als Bytes dargestellt werden.
UTF-8
UTF-8 ist eine Kodierung mit variabler Länge, die 1–4 Bytes pro Codepoint verwendet:
| Codepoints | Bytes | Hinweis |
|---|---|---|
| U+0000 – U+007F | 1 Byte | ASCII-kompatibel |
| U+0080 – U+07FF | 2 Bytes | Erweitertes Latein, IPA, Hebräisch, Arabisch |
| U+0800 – U+FFFF | 3 Bytes | Die meisten CJK |
| U+10000 – U+10FFFF | 4 Bytes | Ergänzend (Emoji, seltenes CJK) |
Vorteile:
- ASCII-kompatibel: ASCII-Dateien sind gültiges UTF-8
- Speichereffizient für englischen/lateinischen Text (1 Byte pro Zeichen)
- Selbst-synchronisierend: Jedes Byte kann als Start- oder Fortsetzungsbyte identifiziert werden
- Keine Byte-Reihenfolge-Probleme
Nachteile:
- Variable Länge macht O(1)-Direktzugriff nach Codepoint unmöglich (linearer Scan erforderlich)
- CJK-Text benötigt 3 Bytes pro Zeichen (gegenüber 2 Bytes in UTF-16)
UTF-8 ist die dominierende Kodierung im Web: HTTP-Header, HTML-Dateien, JSON und die meisten APIs verwenden UTF-8. Wenn Sie Web-Software entwickeln, ist UTF-8 Ihr Standard.
UTF-16
UTF-16 verwendet 2 oder 4 Bytes pro Codepoint:
- BMP-Zeichen: 2 Bytes
- Ergänzende Zeichen: 4 Bytes (Surrogate Pairs)
Verwendet von: Windows-APIs, Java-String-Typ, JavaScript-Engines intern, .NET-String-Typ
Byte Order Mark (BOM): UTF-16-Dateien beginnen oft mit einem BOM (U+FEFF), um die Byte-Reihenfolge anzugeben (Big-Endian oder Little-Endian). UTF-16BE und UTF-16LE sind die beiden Varianten.
UTF-32
UTF-32 verwendet genau 4 Bytes pro Codepoint – feste Breite. Einfach für Code, der O(1)-Direktzugriff nach Codepoint benötigt, verbraucht aber 4-mal mehr Speicher als ASCII-Text.
Verwendet von: Python 3 intern (auf einigen Plattformen), einige Unix/Linux-APIs
Häufige Kodierungsfehler und wie man sie behebt
Der Fehler „é" erscheint als „é"
Dies ist der klassische Fall von UTF-8, das als Latin-1 interpretiert wird. Die UTF-8-Kodierung von "é" (U+00E9) sind die zwei Bytes 0xC3 0xA9. Als Latin-1 interpretiert: Ã (0xC3) und © (0xA9).
Lösung: Stellen Sie sicher, dass die gesamte Datenpipeline konsistent UTF-8 verwendet. Überprüfen Sie den Zeichensatz Ihrer Datenbankverbindung (charset=utf8mb4 in MySQL), Ihre HTTP-Antwort-Header (Content-Type: text/html; charset=UTF-8), Ihren Dateilesecode und alle Datenexporte/-importe.
Das MySQL-Problem utf8 vs. utf8mb4
MySQL's utf8-Zeichensatz speichert nur 3-Byte-UTF-8-Sequenzen – er kann keine Emojis oder ergänzende CJK-Zeichen speichern, die 4-Byte-UTF-8-Sequenzen erfordern.
Lösung: Verwenden Sie in MySQL immer utf8mb4 für vollständige Unicode-Unterstützung:
CREATE TABLE content ( body TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ); -- Oder auf Verbindungsebene festlegen: SET NAMES utf8mb4;
Fehler bei der Zeichenkettenlänge
# Python 3: len() gibt die Anzahl der Codepoints zurück, nicht die Bytes oder Grapheme
text = "é" # ein Graphem, ein Codepoint
len(text) # 1 ✓
text = "e\u0301" # ein Graphem, zwei Codepoints (e + kombinierender Akut)
len(text) # 2 ✗ (Benutzer sieht ein Zeichen, Python sieht zwei)
# Für die Graphemanzahl in Python die grapheme-Bibliothek verwenden:
import grapheme
grapheme.length("e\u0301") # 1 ✓
// JavaScript: length gibt die Anzahl der UTF-16-Code-Einheiten zurück
"🌍".length // 2 (Emoji belegt 2 UTF-16-Einheiten)
// Für die Anzahl der Codepoints:
[..."🌍"].length // 1 ✓
// Für die Graphemanzahl:
const s = new Intl.Segmenter();
[...s.segment("🌍")].length // 1 ✓
Sortierung und Kollation
Das Sortieren von Zeichenketten in Unicode ist nicht dasselbe wie das Sortieren nach Bytewert. „ä" sollte im Deutschen nahe bei „a" sortiert werden, aber im Schwedischen nach „z". „ch" wurde im traditionellen Spanisch als eine einzelne Einheit sortiert. Der Unicode-Kollationsalgorithmus (CLDR) definiert sprachspezifische Sortierregeln.
// Falsch: sortiert nach Bytewert ['ä', 'z', 'a'].sort() // ['a', 'z', 'ä'] (falsch für die meisten Locales) // Korrekt: locale-bewusste Sortierung ['ä', 'z', 'a'].sort((a, b) => a.localeCompare(b, 'de')); // ['a', 'ä', 'z'] ✓ (korrekt für Deutsch) ['ä', 'z', 'a'].sort((a, b) => a.localeCompare(b, 'sv')); // ['a', 'z', 'ä'] ✓ (korrekt für Schwedisch)
Groß-/Kleinschreibung und das türkische i
Die türkische Sprache hat ein gepunktetes „İ" und ein punktloses „ı". Im Türkischen ist der Kleinbuchstabe „I" gleich „ı" (nicht „i"), und der Großbuchstabe „i" ist „İ" (nicht „I"). Die Verwendung von locale-unbewusster Groß-/Kleinschreibungskonvertierung beschädigt türkische Zeichenketten:
// Falsch: locale-unbewusst
"Istanbul".toLowerCase() // "istanbul" (Englisch)
"istanbul".toUpperCase() // "ISTANBUL" (Englisch)
// Korrekt: locale-bewusst
"Istanbul".toLocaleLowerCase('tr') // "istanbul" (hier gleich)
"istanbul".toLocaleUpperCase('tr') // "İSTANBUL" (gepunktetes İ)
// Türkischer I-Fehler:
"I".toLowerCase() // "i" (falsch im Türkischen)
"I".toLocaleLowerCase('tr') // "ı" (korrektes punktloses ı)
Reguläre Ausdrücke und Unicode
JavaScript-Regex arbeitet standardmäßig auf UTF-16-Code-Einheiten, nicht auf Codepoints:
// Falsch: . entspricht einer UTF-16-Code-Einheit, nicht einem Codepoint
/^.$/.test('🌍') // false (Emoji besteht aus 2 Code-Einheiten)
// Korrekt: u-Flag für Unicode-bewusste Regex verwenden
/^.$/u.test('🌍') // true ✓
// Außerdem: Unicode-Eigenschafts-Escapes mit u-Flag
/\p{Script=Arabic}/u.test('مرحبا') // true ✓
/\p{Emoji}/u.test('🌍') // true ✓
Datenbankkonfiguration für Unicode
PostgreSQL
PostgreSQL unterstützt Unicode nativ in allen modernen Versionen. Verwenden Sie TEXT-Spalten (ohne Längenbeschränkung) anstelle von VARCHAR(n), das Zeichen über Versionen hinweg unterschiedlich zählt. Die Standardkodierung für neue PostgreSQL-Datenbanken sollte UTF8 sein.
MySQL / MariaDB
Wie oben erwähnt, verwenden Sie immer utf8mb4 mit der Kollation utf8mb4_unicode_ci:
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
SQLite
SQLite speichert Text standardmäßig in UTF-8. Keine spezielle Konfiguration erforderlich.
Redis und Caches
Redis speichert Zeichenketten als Bytes – es ist kodierungsunabhängig. Stellen Sie sicher, dass Ihr Client-Code UTF-8 konsistent kodiert und dekodiert.
Zusammenfassung: Unicode i18n-Checkliste
- Alle Dateien, Datenbanken und APIs verwenden UTF-8-Kodierung
- MySQL-Datenbanken verwenden
utf8mb4, nichtutf8 - HTTP-Antworten deklarieren
charset=utf-8im Content-Type - JavaScript-Zeichenkettenoperationen verwenden das
u-Flag in Regex - Zeichenkettenlängenberechnungen berücksichtigen Zeichen mit mehreren Code-Einheiten, wenn benutzerseitig sichtbar
- Groß-/Kleinschreibungskonvertierung verwendet wo nötig locale-bewusste Methoden
- Sortierung verwendet
localeComparemit dem entsprechenden Locale - BOM wird aus UTF-8-Dateien entfernt, wo nicht erwartet
- Unicode-Normalisierung wird vor dem Zeichenkettenvergleich angewendet
Weitere Informationen darüber, wie diese technischen Grundlagen mit echten Lokalisierungs-Workflows zusammenhängen, finden Sie unter Grundlagen der Lokalisierung und Internationalisierung und Software-Lokalisierung.
Machen Sie Ihre App mit better-i18n global
better-i18n kombiniert KI-gestützte Übersetzungen, Git-native Workflows und globale CDN-Auslieferung in einer entwicklerzentrierten Plattform. Hören Sie auf, Tabellen zu verwalten, und beginnen Sie, in jeder Sprache zu veröffentlichen.
Kostenlos starten → · Funktionen entdecken · Dokumentation lesen