
QR Codes and Non-Latin Text: UTF-8, ECI and Kanji Mode
A cafe prints table cards linking to a plain-text menu in Turkish. On the designer's laptop the preview is perfect. On a guest's phone the same code produces "Kahvaltı Menüsü".
The print is clean. The error correction never fired. The bytes arrived exactly as encoded and were then read with the wrong dictionary.
A QR Code Stores Bytes, Not Letters
The symbology defines four ways to pack data into the grid, and they are chosen for density rather than for language.
- Numeric mode packs three digits into ten bits.
- Alphanumeric mode packs two characters into eleven bits, using a restricted set of digits, uppercase letters, and a handful of symbols.
- Byte mode stores raw eight-bit values, one byte at a time.
- Kanji mode stores Japanese double-byte characters in about thirteen bits each, against the sixteen the same character would consume in byte mode.
Anything outside the numeric and alphanumeric sets lands in byte mode, which is where every non-Latin script ends up. Byte mode is honest about what it is: a container for bytes. It carries no built-in statement that those bytes are Turkish, Greek, or anything else. Density differences between the modes are the reason capacity varies so much by content type, as broken down in how much data a QR code holds.
The Default Assumption Nobody Declared
The specification defines a default interpretation for byte mode built around a single-byte Latin character set. Most modern encoders write UTF-8 regardless, because UTF-8 is what the rest of the web runs on, and most modern decoders detect UTF-8 when the byte pattern looks like valid UTF-8. That detection usually works, and "usually" is the whole problem.
Mojibake appears when multi-byte UTF-8 sequences are read one byte at a time through a single-byte table. A Turkish dotless i or an umlaut turns into two visible characters instead of one. The ı and ü pairs in the menu above are the signature of exactly that mismatch. Cyrillic tends to degrade into strings of Ð and Ñ characters, which is the same fault wearing a different costume.
What ECI Adds
Extended Channel Interpretation is the mechanism the standard provides for removing the guess. An ECI designator is written into the data stream ahead of the payload, carrying an assignment number that identifies the character set the following bytes belong to. UTF-8 has its own number in that registry, 26. A decoder that understands the designator stops guessing and applies the declared set.
Support varies. Some readers honor the designator, some ignore it and fall back to their own detection, and encoder behavior differs too, since not every generator emits a designator at all. You cannot control which reader a stranger's phone is running, so treat ECI as improving the odds rather than as a guarantee.
URLs Sidestep the Whole Problem
URL syntax restricts what may appear in an address to a subset of ASCII. Anything outside it gets percent-encoded, meaning each non-ASCII byte becomes a percent sign and two hex digits. Non-Latin domain names go through a separate ASCII-compatible encoding that turns them into an xn-- prefixed form.
The result is a payload made entirely of characters that survive any character set assumption a decoder might make. This is the strongest practical argument for putting URLs in printed codes rather than raw text, and it applies equally to the routing tricks in multilingual QR experiences.
One caution: copy the encoded form, not the pretty one. Browser address bars often display the human-readable version of a path while holding the encoded version underneath, and pasting what you see rather than what is stored can reintroduce raw bytes into the payload.
Plain Text and Contact Cards Are Where It Breaks
Two payload types account for most real complaints.
A text payload is bytes with no protocol wrapper around them, so there is nothing to declare an encoding and nothing to correct a wrong assumption. Whatever the scanner decides is what the user reads.
A contact card adds a second layer of risk. The QR decoder interprets the bytes, then the phone's contact importer parses the vCard fields and hands them to the address book. The vCard format's handling of character sets has shifted between versions, so an app can decode the bytes correctly and still write a mangled name into the contact record. Names carry the highest risk, since that is where the script-specific characters live: Turkish dotted and dotless i, Azerbaijani schwa, Arabic and Hebrew text that additionally depends on the receiving app for right-to-left display.
Two Phones, Before the Press Run
Encoding faults are cheap to catch and expensive to reprint, so test rather than reason about it.
- Scan with the native camera on an iPhone and on an Android device. These two paths cover most real scans.
- Add at least one third-party scanner app, plus any in-app scanner your audience is likely to use.
- Test the longest string you plan to encode, not a short sample. Truncation and encoding faults both tend to surface at the tail.
- Read the entire result, including the final character. Partial corruption in the middle of a string is easy to skim past.
The Fallback Worth Defaulting To
When a decoder mangles non-Latin text, the fix is to stop asking the decoder to render language at all. Host the text on a page and encode the URL to that page.
The page declares its encoding in the response headers and in the document itself, the browser is a far more capable text renderer than a scanner's result screen, and right-to-left layout becomes CSS rather than a guess. Make the code dynamic and a typo becomes an edit instead of a reprint, and the same symbol can serve a different language depending on who scanned it.
The cost is a network round trip and a dependency on hosting you control. For anything printed at volume in a script other than plain Latin, that is usually the right price.

