How a Single ß Can Break Everything
Vladimir Jovanović
Trusted expertise in
AI-powered communication
The trusted AI assistant for communication and productivity
Trusted by 40M+ people and 50K+ organizations
Works where you do
across 500,000+
web and desktop apps
Grammarly + Coda + Superhuman
Coda, the maker of powerful AI productivity tools, and Superhuman, the AI-native email app, are now a part of Grammarly.��Together, we aim to revolutionize how teams operate—enabling them to stay focused, aligned, and achieve results quickly.
Story time
Story time
Changing requirements
Changing requirements
Changing requirements
Unicode
Unicode � ☐
Unicode � ☐
The Dark Ages Before Unicode
The Dark Ages Before Unicode
Unicode's Big Idea - Code Points
Unicode's Big Idea - Code Points
From Code Points to Bytes
From Code Points to Bytes
UTF-8 - The Web's Choice
UTF-8 - The Web's Choice
UTF-8 - The Web's Choice
UTF-16 - Android's Internal Choice
UTF-16 - Android's Internal Choice
UTF-16 - Android's Internal Choice
UTF-16 - Android's Internal Choice
UTF-8 vs UTF-16 - A Character Comparison
Character | Code Point | UTF-8 Bytes | UTF-16 Units | Description |
A | U+0041 | 41 | 00 41 | Basic ASCII |
Ω | U+03A9 | CE A9 | 03 A9 | Greek letter |
中 | U+4E2D | E4 B8 AD | 4E 2D | Chinese character |
🚀 | U+1F680 | F0 9F 9A 80 | D8 3D DE 80 | Rocket emoji |
UTF-8 vs UTF-16 - A Character Comparison
Character | Code Point | UTF-8 Bytes | UTF-16 Units | Description |
A | U+0041 | 41 | 00 41 | Basic ASCII |
Ω | U+03A9 | CE A9 | 03 A9 | Greek letter |
中 | U+4E2D | E4 B8 AD | 4E 2D | Chinese character |
🚀 | U+1F680 | F0 9F 9A 80 | D8 3D DE 80 | Rocket emoji |
Astral planes
Plane | Range | Name | Description |
0 | U+0000–U+FFFF | Basic Multilingual Plane (BMP) | Most common characters: Latin, Cyrillic, Arabic, Hebrew, CJK (common), symbols, punctuation, emoji (some) |
1 | U+10000–U+1FFFF | Supplementary Multilingual Plane (SMP) | Emoji, historic scripts (Linear B, Old Italic, etc.), musical notation, ancient languages |
2 | U+20000–U+2FFFF | Supplementary Ideographic Plane (SIP) | Rare and historic CJK ideographs |
3 | U+30000–U+3FFFF | Tertiary Ideographic Plane (TIP) | More rare CJK ideographs, added in Unicode 13+ |
4-13 | U+40000–U+DFFFF | (Unassigned / Reserved) | Reserved for future use |
14 | U+E0000–U+EFFFF | Supplementary Special-purpose Plane (SSP) | Special characters like language tags, variation selectors |
15 | U+F0000–U+FFFFF | Private Use Area-A | For private use; not assigned by Unicode Consortium |
16 | U+100000–U+10FFFF | Private Use Area-B | For private use; not assigned by Unicode Consortium |
|
Astral planes
Plane | Range | Name | Description |
0 | U+0000–U+FFFF | Basic Multilingual Plane (BMP) | Most common characters: Latin, Cyrillic, Arabic, Hebrew, CJK (common), symbols, punctuation, emoji (some) |
1 | U+10000–U+1FFFF | Supplementary Multilingual Plane (SMP) | Emoji, historic scripts (Linear B, Old Italic, etc.), musical notation, ancient languages |
2 | U+20000–U+2FFFF | Supplementary Ideographic Plane (SIP) | Rare and historic CJK ideographs |
3 | U+30000–U+3FFFF | Tertiary Ideographic Plane (TIP) | More rare CJK ideographs, added in Unicode 13+ |
4-13 | U+40000–U+DFFFF | (Unassigned / Reserved) | Reserved for future use |
14 | U+E0000–U+EFFFF | Supplementary Special-purpose Plane (SSP) | Special characters like language tags, variation selectors |
15 | U+F0000–U+FFFFF | Private Use Area-A | For private use; not assigned by Unicode Consortium |
16 | U+100000–U+10FFFF | Private Use Area-B | For private use; not assigned by Unicode Consortium |
|
Unicode Normalization
Unicode Normalization
"🧑🧑🧒🧒".length == ?
"🧑🧑🧒🧒".length == 11
Why is "🧑🧑🧒🧒".length == 11?
🧑
🧑
🧒
🧒
=
🧑🧑🧒🧒
Why is "🧑🧑🧒🧒".length == 11?
Emoji | Name | Code Points | UTF-16 units |
🧑 | Adult emoji | U+1F9D1 | 2 |
| Zero Width Joiner | U+200D | 1 |
🧑 | Adult emoji | U+1F9D1 | 2 |
| Zero Width Joiner | U+200D | 1 |
🧒 | Child emoji | U+1F9D2 | 2 |
| Zero Width Joiner | U+200D | 1 |
🧒 | Child emoji | U+1F9D2 | 2 |
| | | 11 |
Emoji Modifiers & Variation Selectors
Emoji | Code Points | Description |
👍 | U+1F44D | Thumbs up |
👍🏿 | U+1F44D + U+1F3FF | Thumbs up + dark skin tone modifier |
❤ | U+2764 | Default heart (platform varies) |
❤︎ | U+2764 + U+FE0E | Text-style heart |
❤️ | U+2764 + U+FE0F | Emoji-style heart |
👩⚕️ | U+1F469 + U+200D + U+2695 + U+FE0F | Woman + ZWJ + Medical symbol + Variation Selector |
Emoji Modifiers & Variation Selectors
Emoji | Code Points | Description |
👍 | U+1F44D | Thumbs up |
👍🏿 | U+1F44D + U+1F3FF | Thumbs up + dark skin tone modifier |
❤ | U+2764 | Default heart (platform varies) |
❤︎ | U+2764 + U+FE0E | Text-style heart |
❤️ | U+2764 + U+FE0F | Emoji-style heart |
👩⚕️ | U+1F469 + U+200D + U+2695 + U+FE0F | Woman + ZWJ + Medical symbol + Variation Selector |
Counting Grapheme Clusters
Counting Grapheme Clusters
class GetGraphemeClusterCount {
operator fun invoke(text: String): Int {
val boundary = BreakIterator.getCharacterInstance()
boundary.setText(text)
var count = 0
var start = boundary.first()
while (start != BreakIterator.DONE) {
val end = boundary.next()
if (end != BreakIterator.DONE) {
count++
}
start = end
}
return count
}
}
Be Careful With Regex
Using Regex Correctly
Getting first grapheme cluster
private fun getFirstGraphemeCluster(text: String): String {
if (text.isEmpty()) return ""
val boundary = BreakIterator.getCharacterInstance()
boundary.setText(text)
val start = boundary.first()
val end = boundary.next()
return if (start != BreakIterator.DONE && end != BreakIterator.DONE) {
text.substring(start, end)
} else {
""
}
}
Back to the story
Back to the story
fun getInitials(username: String): String {
val words = username.splitIntoWords()
return when {
words.isEmpty() -> ""
words.size == 1 -> getFirstGraphemeCluster(words[0])
else -> getFirstGraphemeCluster(words[0]) +� getFirstGraphemeCluster(words[1])
}
}
Back to the story
fun getInitials(username: String): String {
val words = username.splitIntoWords()
return when {
words.isEmpty() -> ""
words.size == 1 -> getFirstGraphemeCluster(words[0])
else -> getFirstGraphemeCluster(words[0]) +� getFirstGraphemeCluster(words[1])
}.uppercase()
}
Things go bad 😳
require(getGraphemeClusterCount(initials) <= 2) {
"Profile image cannot display more than 2 grapheme clusters: '$initials'"
}
Profile image cannot display more than 2 grapheme clusters:
Things go bad 😳
require(getGraphemeClusterCount(initials) <= 2) {
"Profile image cannot display more than 2 grapheme clusters: '$initials'"
}
Profile image cannot display more than 2 grapheme clusters: SSH
Back to getInitials
fun getInitials(username: String): String {
val words = username.splitIntoWords()
return when {
words.isEmpty() -> ""
words.size == 1 -> getFirstGraphemeCluster(words[0])
else -> getFirstGraphemeCluster(words[0]) +� getFirstGraphemeCluster(words[1])
}.uppercase()
}
uppercase method
ßmart hacker
ßmart hacker
Everyone Gets Tripped Up by Unicode
Everyone Gets Tripped Up by Unicode
T̸̛̙̞͙͔̪͕̝̀͐̀̍͠͝͝h̵̨͍̻̺̝̓̈́̏̋͊̔͋a̸̢̧̨̼̜̖̗̖̠͇͎͐̾͐͛͊̆̽͗̓̈͠ń̴̨̝͇̦̝̺͙̉̽k̸̹̣̯̠̖͔̺̻̱̘͎̍͗̓͊͋̀ ̶̛͓͕̻͈̺̍̋y̵̮̪͕̖̺̭̓͊̇̏̓̉͌͋ͅơ̵̢͖̗̟̪̯͚̤̞̗̈́̆͐̿͛͆͑́̓u̵̯̳̭͚͖͇͐̀͐̍̈́̆͛͒̑̑͋̔̕͠͠!̸̧̳̩̅͝
Vladimir Jovanović�https://vladimirj.dev� @vladimirj.dev
Questions�