Home › Text Tools › Guides

🔬 In-depth guide

Character Encodings: Why Your Text Turned Into Question Marks

What UTF-8 and Latin-1 actually do to bytes, where mojibake comes from, and why the same file opens correctly in one program and as garbage in another.

There is a specific kind of bad day that starts with a file full of é where every accented letter used to be, or a spreadsheet whose names have all become question marks, or a database column that looks correct in one application and like line noise in another. The word for the mangled result is mojibake, and the cause is almost never corruption. The bytes are usually fine. What went wrong is that somebody decoded them under the wrong assumption.

Understanding why requires separating two things that everyday language runs together: characters, which are what you mean, and bytes, which are what is stored.

A file does not know what encoding it is

This is the fact everything else follows from, and it is genuinely counter-intuitive.

A plain text file contains no field stating its encoding. It is a run of bytes. When a program opens it, that program must decide how to turn those bytes into characters, and it makes that decision from its own defaults, the operating system’s locale, a heuristic sniff of the content, or an out-of-band declaration such as an HTTP header or an XML prologue. Two programs can make different decisions about the same file, and both will happily show you the result of their decision without mentioning that a decision was made.

So “the file is broken” is usually the wrong diagnosis. The file is a sequence of bytes that means one thing under one interpretation and something else under another.

What Latin-1 does, and why it never fails loudly

Latin-1, formally ISO-8859-1 and closely related to the Windows-1252 codepage that most Western European systems actually used, is a single-byte encoding. Every character is exactly one byte, and there are 256 of them. That buys simplicity and costs coverage: you get the unaccented Latin alphabet, digits, punctuation, and a selection of accented letters covering Western European languages, and nothing else. No Greek, no Cyrillic, no Chinese, no emoji.

The critical property, the one that makes it dangerous rather than merely limited, is that a single-byte encoding cannot fail. Any byte at all maps to some character. Feed a Latin-1 decoder a file that was actually UTF-8 and it will not raise an error, because there is no byte sequence it considers invalid. It will simply produce the wrong characters, silently and confidently.

This is where the classic mojibake pattern comes from. In UTF-8, an accented letter is stored as two bytes. A Latin-1 decoder sees two bytes and produces two characters, so one letter becomes a pair of them, and the first is very often the same capital A with a tilde that everybody recognises from broken text. The pattern is so consistent that experienced readers can name the original encoding just by looking at the damage.

What UTF-8 does differently

UTF-8 is variable width. A character occupies one, two, three or four bytes depending on which character it is, and the design has two properties that matter enormously in practice.

The first is that the entire ASCII range, meaning the unaccented English letters, digits and common punctuation, is encoded as a single byte with exactly the same value it had in ASCII. That makes a pure-English UTF-8 file byte-identical to the same file in ASCII or in Latin-1, which is why encoding problems have a habit of staying hidden until the first accented name or curly quotation mark arrives. Every test passed because every test used English.

The second is that multi-byte sequences follow a strict pattern, and not every byte sequence is valid. A UTF-8 decoder can therefore detect that its input was not UTF-8, rather than producing plausible nonsense. That asymmetry is why “read as UTF-8 and see whether it complains” is a reasonable first diagnostic, while the reverse is not.

Where the boundary keeps catching people

The interesting failures are not in files but at the joins between systems, and three come up constantly.

Text on the web has to survive a URL, and a URL is not allowed to contain arbitrary characters. Percent-encoding is the mechanism: unsafe characters are replaced by a percent sign and their byte values in hexadecimal. Note the word bytes. Percent-encoding operates on bytes, not characters, so an accented letter has to be turned into bytes first, and the result differs depending on which encoding did that. This is why the same query string can work against one server and fail against another, and it is the layer our URL encoder and decoder makes visible: seeing the percent sequences is often enough to identify which encoding produced them.

Text passing through a channel that only carries printable characters hits the same boundary from the other side. Base64 exists to solve that, and it converts bytes, not characters. A naive implementation in a browser trips on exactly this: the platform’s built-in function treats each JavaScript string code unit as if it were a single byte, so any character outside the single-byte range either throws an error or silently mangles. Our own Base64 encoder converts the text to its real UTF-8 bytes first and encodes those, which is the difference between a tool that works on English and a tool that works on your actual data.

The third join is the spreadsheet. A CSV exported from a system in one locale and opened in a spreadsheet application configured for another is the single most common source of mojibake in ordinary office work, because neither end declares anything and both ends have opinions.

Case conversion is not as simple as it looks

Encoding is only half the story, because once you have the right characters you still have to operate on them, and character operations are language-dependent in ways that catch people out.

Uppercasing is the obvious example. In most languages the uppercase of the letter i is I, which is the rule almost every piece of software applies. In Turkish it is not: the dotted and dotless i are distinct letters with distinct uppercase forms, and applying the English rule produces a genuinely wrong word. German has a sharp s whose traditional uppercase form is two characters, so uppercasing can change a string’s length. These are not edge cases invented by specification authors; they are ordinary words in ordinary languages.

A case converter is therefore a text tool rather than a byte tool, and worth reaching for rather than reimplementing, particularly for the formats people actually want, such as title case, which has its own conventions about which small words stay lowercase.

Counting is language-dependent too

The same applies to counting, which sounds like it should be the least ambiguous operation available and is not.

“How many characters is this” has at least three defensible answers: how many bytes it occupies, how many code points it contains, and how many things a reader would call characters. They diverge the moment you leave plain English. An accented letter may be one code point or two, depending on whether it was composed or decomposed. A family emoji is a single visual glyph made of several code points joined together. A flag is two.

Our word and character counter counts by code point rather than by storage unit, which is the answer that matches intuition most of the time and is the one most social platforms use for their limits. The important thing is not which definition a tool picks but that it picks one deliberately and tells you, because a length limit enforced under one definition and measured under another is exactly how a form rejects text that looks perfectly short.

The practical rules

Use UTF-8 for everything you control, and declare it wherever a declaration is possible: the HTTP header, the meta tag, the database column and connection, the file export dialog. Most encoding problems are two systems that never agreed, and a declaration is the agreement.

When something does go wrong, resist editing the visible mess. Find the point where the wrong decoding happened and fix it there, because the mojibake is a symptom and the bytes upstream are usually still correct. Fixing the symptom means re-encoding the damage permanently, which turns a reversible problem into an irreversible one.

The tools in this guide, in action

Screenshot of the URL Encode tool with the sample input “https://sysfenix.com/search?q=convert png to jpg…”, Encoding scope set to Component (safest, for query params & path segments)
URL Encode mid-process: the sample input “https://sysfenix.com/search?q=convert png to jpg…”, Encoding scope set to Component (safest, for query params & path segments).
Screenshot of the Base64 Encode tool with the sample input “Everything on SysFenix runs in your browser. You…”
Base64 Encode mid-process: the sample input “Everything on SysFenix runs in your browser. You…”.

Frequently asked questions

Why does the same file look fine in one program and broken in another?

Because a text file does not state its own encoding. It is a sequence of bytes, and every program that opens it has to decide which encoding those bytes are in. One program guesses UTF-8, another defaults to the system codepage, and they disagree. Nothing about the file changed between the two openings; only the interpretation did. This is also why "it works on my machine" is so common with text data: the guess depends on the reader's locale.

Can I recover text that has already turned into mojibake?

Often, yes, if it was only decoded wrongly and not re-saved. Mojibake is usually a reversible mapping: the original bytes were interpreted under the wrong encoding, so re-reading them under the right one restores the text. What is much harder to recover is text that was decoded wrongly and then written out again, because the wrong characters have now been encoded as themselves and the original byte sequence is gone.

Should I add a byte order mark to my files?

Generally not for UTF-8. A BOM is genuinely useful for UTF-16, where it tells a reader which byte comes first, but UTF-8 has no ordering ambiguity to resolve so the mark is only a hint that the file is UTF-8 at all. Plenty of tools handle it badly: it can appear as stray characters at the start of a file, break a shell script's shebang line, or turn the first column name of a CSV into something no importer matches.

Is Base64 an encoding in the same sense as UTF-8?

No, and the shared word causes real confusion. UTF-8 maps characters to bytes. Base64 maps bytes to a restricted set of printable characters, so it sits one level lower and solves a different problem: getting arbitrary binary data through a channel that only reliably carries text. Encoding text as Base64 means first turning the characters into bytes with something like UTF-8, and only then applying Base64 to those bytes.

Tools mentioned in this guide