When I took over that seven-year-old system, all I had to change was one line of UI copy: add a parenthetical note after "out of stock". The file was message_zh.properties, 1846 lines and 326 keys, the whole screen full of \uXXXX, and searching for the word stock hit nothing. I spent twenty minutes comparing lines to find the right one, then fumbled \u5de5 with a digit missing and the build failed with illegal escape character. That detour taught me one thing: those \u strings are not mojibake; they are the same Chinese characters written another way.
1. A code point is an ID number; \uXXXX is just its hexadecimal spelling
Unicode assigns every character an ID number called a code point, independent of bytes and of the language you write in. \u4e2d means "the code point of this character, written 4E2D in hex." The same character zhong can appear in several forms.
| Character | Code point | Unicode escape | HTML numeric entity | Decimal code point |
|---|---|---|---|---|
| zhong | U+4E2D | \u4e2d | 中 | 20013 |
| wen | U+6587 | \u6587 | 有 | 25991 |
| gong | U+5DE5 | \u5de5 | 巧 | 24039 |
| ju | U+5177 | \u5177 | 具 | 20855 |
Two ranges are worth remembering: the Basic Multilingual Plane runs from U+0000 to U+FFFF, 65536 code points in total, and most common symbols and Chinese characters fall inside it; the main CJK block runs from U+4E00 to U+9FFF, 20992 code points. Rare characters and emoji above U+FFFF cannot be expressed with a single four-digit escape and need a surrogate pair: U+1F600 has to be written as the two parts \ud83d\ude00.
2. Five things that look like mojibake but live on different layers
Mixing these layers up is the fastest way to corrupt data.
| Looks like | What it really is | Which layer |
|---|---|---|
\u4e2d\u6587 | Escaped spelling of character code points | Source code / JSON string layer |
中 | The same code point as a decimal entity | HTML markup layer |
%E4%B8%AD | The three UTF-8 bytes E4 B8 AD of one Chinese character, percent-encoded | URL transport layer |
D6 D0 | The two GBK bytes of the same character | Byte encoding layer |
䏿–‡ | UTF-8 bytes displayed as Latin-1 | Already broken: a display accident |
The last row is true mojibake: the bytes were interpreted wrongly, and no "convert back to Chinese" button can rescue it; you have to go to the source and fix the decoding. Likewise, the classic replacement-character mess comes from reading EF BF BD replacement characters again as GBK. I have seen people feed %u4E2D to decodeURIComponent and get nothing but URI malformed; that is the awkward legacy of escape(), belonging to neither world.
3. How I finally changed that line
- Locate it. I ran the whole file through Unicode Converter to get a plain Chinese version, searched that result for the word stock, took the key name, and went back to the original file to edit that line. I did not touch a single character of the original, to avoid invisible differences from a full round trip.
- Confirm the form. A properties file is read line by line, so the escape uses a single backslash,
\u5de5; if the same text has to sit inside a Java source string, it becomes\\u5de5. One backslash too few or too many and the compiler just throws an illegal escape error. - When generating the replacement line, I used the non-ASCII-only mode, so letters, digits and half-width punctuation stay as they are and the diff shows only the characters I actually changed; converting the whole selection turns every line into escapes and reviewers cannot see what changed.
- When I need a form a page can use directly, I switch the same text to decimal mode and get entities such as
中.
For the percent layer I use other tools: URL Encoder and Decoder works on byte sequences, so %E4%B8%AD is its business; Text Encoder handles full-width versus half-width forms and ASCII values. The three cannot replace one another; they sit on different layers.
4. A few numbers you can copy directly
- The same two-character word: 2 characters, 6 bytes in UTF-8, 4 bytes in GBK, and 12 ASCII characters when written as
\u4e2d\u6587; one escaped Chinese character always takes 6 characters. - Anywhere a quota is counted in characters, this hurts: signatures, SMS templates, config values. Full escaping multiplies the length by six, and a cut in the middle leaves a fragment like
\u4e, which is a syntax error. - The characters you cannot see:
\u3000is the full-width space (12288),\u00a0is the no-break space (160), and\u201c/\u201dare curly Chinese quotation marks (8220 / 8221). Copy text from a document or a chat window and nine times out of ten it carries them; restore to Chinese before find-and-replace, or the operation is meaningless. - Do not lose track of the backslash layers: a properties file uses a single backslash,
\u5de5; inside a Java source string it becomes\\u5de5; put that text into JSON and another layer is peeled on. Each layer adds one escape, and mistakes are usually a missing one somewhere in between.
5. Common questions
Q: What is the 4e2d in \u4e2d?
It is the Unicode code point of the character zhong, 4E2D in hex, which is 20013 in decimal, so the HTML entity is written 中. Both point to the same character; the only difference is hex versus decimal.
Q: Does writing escapes instead of Chinese characters change how the code runs?
After parsing they are exactly equivalent; it is the same character. The difference is on the human side: an escaped form defeats full-text search, code review and copy checks, and it assumes the file itself can only safely hold ASCII.
Q: Why did older Java config files have to be escaped?
Before JDK 9, PropertyResourceBundle always read these files as ISO-8859-1, so Chinese had to be stored as \uXXXX escapes, and early JDKs shipped a dedicated converter called native2ascii, later deprecated. That explains why the file in my hands was full of escapes: it was a legacy constraint, not corrupt data.
Q: Why do character counts never match when copy contains emoji?
Because characters above the BMP take two UTF-16 code units: '\ud83d\ude00'.length is 2, so one smiley counts as two characters in any length check. Count by code point with something like codePointAt, and in source you can also write \u{1f600}, supported since ES6. As for the Invalid Unicode escape sequence error, it usually comes from a truncated escape such as a lone \u4e that cannot fill four hex digits.
Q: Can converting hundreds of lines at once break something?
The conversion itself is lossless; the risk is overwriting the original. My habit is to use the converted copy only for reading, never for saving, make the actual edits in the original file, and check with a diff before committing that it still has 1846 lines and only the target keys changed.
That line, out of stock with a note about changing it in settings, went in with the original single-backslash escape and compiled. Looking back, the valuable part of those twenty minutes was not finding a converter but figuring out which layer I was dealing with: escapes and entities are two spellings of one code point and convert losslessly; the percent layer belongs to URLs; and once bytes have been decoded wrongly, as with 䏿–‡, only fixing the decoding at both ends helps. No convert-to-Chinese button can save it.