Web Development

How do I encode international characters in URLs?

Master encoding international characters in URLs, from UTF-8 encoding to percent-encoding, with practical examples and implementation strategies.

By Inventive HQ Team

Non-ASCII characters in URLs are encoded two different ways depending on where they appear: the domain name is converted to ASCII with IDN Punycode (the xn-- form), while the path, query string, and fragment are UTF-8 encoded and then percent-encoded (%HH per byte). A URL is technically restricted to a small ASCII set by RFC 3986, so https://münchen.de/café never travels over the wire as-is — it becomes https://xn--mnchen-3ya.de/caf%C3%A9. Your browser displays the readable form but sends the encoded one.

That's the summary an AI overview gives you. What it can't give you is the part that actually trips people up: which mechanism applies to which part of the URL, why you must never percent-encode a hostname, and how the readable form (an IRI, RFC 3987) relates to the wire form. The table below is the map to keep.

Which part of the URL uses which encoding

Non-ASCII handling is not one rule — it's a different rule per URL component. This is the single table worth memorizing:

URL partExample (readable)How non-ASCII is handledEncoded (on the wire)
SchemehttpsAlways ASCII — nothing to dohttps
Host / domainmünchen.deIDN → Punycode (ToASCII), xn-- prefixxn--mnchen-3ya.de
Path segment/café/menüPercent-encode UTF-8 bytes (/ stays a separator)/caf%C3%A9/men%C3%BC
Query string?q=größePercent-encode UTF-8 bytes (reserved chars too)?q=gr%C3%B6%C3%9Fe
Fragment#résuméPercent-encode UTF-8 bytes#r%C3%A9sum%C3%A9

The takeaway: the domain is a special case. DNS cannot carry a % sign, so hostnames use Punycode; everything after the host uses percent-encoded UTF-8. Applying encodeURIComponent (or quote, or rawurlencode) to a whole URL including the host is a bug.

How each part of a URL encodes non-ASCII characters The readable address https://münchen.de/café?q=größe transforming into its on-the-wire form, with the host converted to Punycode xn--mnchen-3ya.de and the path and query percent-encoded as UTF-8 bytes. One URL, two encodings: Punycode for the host, percent-encoding for the rest

WHAT YOU READ (IRI) https:// münchen.de / café ?q= größe

IDN / Punycode percent-encode UTF-8

WHAT TRAVELS (URI) https:// xn--mnchen-3ya.de / caf%C3%A9 ?q= gr%C3%B6%C3%9Fe

DNS can't carry a % sign, so the host uses xn-- Punycode; the path and query use %HH per UTF-8 byte.

Understanding Character Encoding in URLs

URLs present a unique challenge when dealing with international (non-ASCII) characters. While modern web applications support text in virtually any language, URLs are technically defined to contain only ASCII characters. This creates a gap that must be bridged through proper encoding. Understanding how to encode international characters in URLs is essential for building truly global applications.

The process of encoding international characters in URLs involves two steps: character encoding (usually UTF-8) and then percent-encoding (also called URL encoding). These steps must happen in the correct order, and understanding why each is necessary is fundamental to implementing this correctly.

The Two-Step Encoding Process

The first step in encoding international characters is character encoding. Before any character can be percent-encoded, it must first be represented as bytes using a specific character encoding standard. UTF-8 (Unicode Transformation Format - 8-bit) is the de facto standard for this purpose and is strongly recommended for all modern applications.

UTF-8 represents characters as variable-length byte sequences. ASCII characters (0-127) are represented as single bytes, which means they don't change during UTF-8 encoding. However, international characters require multiple bytes. For example:

  • The Euro symbol (€) is encoded as three bytes in UTF-8: E2 82 AC
  • The Chinese character (中) is encoded as three bytes: E4 B8 AD
  • The Cyrillic character (я) is encoded as two bytes: D1 8F
  • The Arabic character (ع) is encoded as two bytes: D8 B9

The second step is percent-encoding, where each byte is represented as a percent sign followed by its two-digit hexadecimal value. So the Euro symbol, after UTF-8 encoding to E2 82 AC, becomes "%E2%82%AC" in a URL.

Encode and decode any string against this exact pipeline below — it applies UTF-8 followed by percent-encoding, the same as encodeURIComponent:

Loading interactive tool...

Practical Examples of International Character Encoding

Let's walk through concrete examples to illustrate the encoding process. If you want to create a URL with a German filename containing umlauts, like "münchen.pdf", here's what happens:

  1. The string "münchen" contains the character "ü"
  2. UTF-8 encoding converts "ü" to the bytes C3 BC (in hexadecimal)
  3. Percent-encoding converts C3 BC to "%C3%BC"
  4. The full URL becomes: https://example.com/files/m%C3%BCnchen.pdf

For a Japanese example, if you want to encode "日本語.txt" (Japanese language):

  1. "日" in UTF-8 is E6 97 A5
  2. "本" in UTF-8 is E6 9C AC
  3. "語" in UTF-8 is E8 AA 9E
  4. The full encoded filename becomes: "%E6%97%A5%E6%9C%AC%E8%AA%9E.txt"
  5. The complete URL: https://example.com/files/%E6%97%A5%E6%9C%AC%E8%AA%9E.txt
Advertisement

Language-Specific Implementation

Different programming languages handle international character encoding with varying levels of convenience.

JavaScript Implementation:

const text = "München café naïve";
const encoded = encodeURIComponent(text);
// Result: "M%C3%BCnchen%20caf%C3%A9%20na%C3%AFve"

// For complete URIs where you want to preserve slashes
const uri = "https://example.com/München/café/";
const encodedUri = encodeURI(uri);
// Result: "https://example.com/M%C3%BCnchen/caf%C3%A9/"

JavaScript's encodeURIComponent() function automatically handles UTF-8 encoding and percent-encoding, making it convenient for most use cases. The encodeURI() variant preserves structural characters like slashes.

Python Implementation:

from urllib.parse import quote, quote_plus

text = "München café naïve"
# For path components
encoded_path = quote(text)
# Result: "M%C3%BCnchen%20caf%C3%A9%20na%C3%AFve"

# For query strings (uses + for spaces)
encoded_query = quote_plus(text)
# Result: "M%C3%BCnchen+caf%C3%A9+na%C3%AFve"

Python's urllib.parse module provides functions that handle the complete encoding pipeline. By default, they use UTF-8, which is the right choice for international characters.

PHP Implementation:

$text = "München café naïve";
// Using rawurlencode (RFC 3986 compliant)
$encoded = rawurlencode($text);
// Result: "M%C3%BCnchen%20caf%C3%A9%20na%C3%AFve"

// Using urlencode (application/x-www-form-urlencoded)
$form_encoded = urlencode($text);
// Result: "M%C3%BCnchen+caf%C3%A9+na%C3%AFve"

PHP distinguishes between urlencode() and rawurlencode(). The former uses "+" for spaces (legacy HTML form format), while the latter uses "%20" (RFC 3986 compliant).

Java Implementation:

import java.net.URLEncoder;
import java.net.URLDecoder;

String text = "München café naïve";
String encoded = URLEncoder.encode(text, "UTF-8");
// Result: "M%C3%BCnchen+caf%C3%A9+na%C3%AFve"

// Decode it back
String decoded = URLDecoder.decode(encoded, "UTF-8");

Java's URLEncoder defaults to UTF-8, making it straightforward to handle international characters correctly.

Handling Different URL Components

The way you encode international characters might differ depending on the URL component where they appear.

In Path Segments: Path segments should use percent-encoding consistently. The forward slash (/) should remain unencoded as it's a path separator, but all other special characters should be encoded. For example: https://example.com/café/menü/coffee becomes https://example.com/caf%C3%A9/men%C3%BC/coffee

In Query String Parameters: Query parameters are typically encoded more aggressively. Both reserved characters (like & and =) and international characters should be percent-encoded. For example: https://example.com/search?q=café+français becomes https://example.com/search?q=caf%C3%A9+fran%C3%A7ais

In Fragment Identifiers: Fragments (the part after #) are also subject to encoding rules. International characters should be percent-encoded similarly to query strings.

Character Encoding Selection and UTF-8 Dominance

While multiple character encoding standards exist (ISO-8859-1, Big5, Shift_JIS), UTF-8 has become the universal standard for web applications for several important reasons:

  1. Unicode Support: UTF-8 can represent any Unicode character, making it suitable for any language
  2. Backward Compatibility: ASCII characters are represented identically in UTF-8, ensuring compatibility with legacy systems
  3. Web Standards: HTML5 requires UTF-8, and modern web standards assume UTF-8
  4. Efficiency: UTF-8 uses variable-length encoding, so common ASCII characters take minimal space
  5. Browser Support: All modern browsers handle UTF-8 URL encoding correctly

When implementing international character support, always specify UTF-8 as your character encoding. If you inherit code that uses other encodings, consider migrating to UTF-8 for consistency.

International Domain Names (IDNs)

A special case in URL encoding involves international domain names. While domain names might contain international characters, they don't use percent-encoding. Instead, they use Punycode encoding, which converts international characters to ASCII-compatible encoding (ACE).

For example, "münchen.de" becomes "xn--mnchen-3ya.de" through Punycode conversion. The xn-- prefix is the ACE (ASCII-Compatible Encoding) marker: when a resolver or browser sees a label starting with xn--, it knows the remainder is a Punycode-encoded Unicode string and can decode it back for display. This is different from percent-encoding and is handled by domain name systems separately from URL path encoding.

When working with IDNs:

  • The browser typically displays the international form in the address bar
  • The DNS system uses the Punycode form internally
  • Your URL encoding functions don't need to handle domain name encoding
  • Only the path and query string portions need percent-encoding

The IRI: the readable form of the whole address

There is a formal name for the human-readable version of a URL that still contains Unicode: an IRI (Internationalized Resource Identifier), defined in RFC 3987 in 2005. https://münchen.de/café?q=größe is an IRI; the fully encoded https://xn--mnchen-3ya.de/caf%C3%A9?q=gr%C3%B6%C3%9Fe is the equivalent URI. The mapping is exactly the two rules from the table above: Punycode the host, percent-encode the UTF-8 bytes of the path, query, and fragment. The IRI is what a person reads and what a browser shows; the URI is what actually travels over the network. They are treated as the same address.

Security note: IDN homograph attacks

Internationalized domains introduce a spoofing risk worth knowing about. In an IDN homograph (homoglyph) attack, an attacker registers a domain using Unicode characters that look identical to Latin ones — a Cyrillic "а" (U+0430) in place of the Latin "a", for instance — so the address visually reads as a trusted brand while resolving to an entirely different Punycode label. Because аpple.com (Cyrillic first letter) and apple.com (all-Latin) are distinct domains, the look-alike can host a convincing phishing page. Modern browsers defend against this by falling back to displaying the raw xn-- Punycode form in the address bar whenever a label mixes scripts or matches a known confusable pattern, which makes the deception visible. If you ever see an unexpected xn-- in a link you thought was a familiar site, treat it as a red flag.

Common Pitfalls and Solutions

Pitfall 1: Assuming your text editor or source code encoding will handle it automatically Never assume this. Always explicitly specify UTF-8 encoding when reading or processing text that might contain international characters.

Pitfall 2: Encoding before you're sure about the character encoding of the source If you're receiving text from an external source, verify its encoding first. Many legacy systems use ISO-8859-1 or other non-UTF-8 encodings. Convert to UTF-8 before percent-encoding.

Pitfall 3: Mixing percent-encoded and non-encoded international characters Be consistent. Either encode all non-ASCII characters or encode none. Mixing them in the same URL creates confusion and potential issues.

Pitfall 4: Not considering URL normalization URLs can be normalized in different ways. Some normalization processes might change how international characters are represented. Be aware of this if you're comparing or storing URLs.

Testing International Character Encoding

When testing URL encoding for international characters, create test cases that include:

  1. Latin characters with diacritics: café, naïve, Zürich
  2. Non-Latin scripts: 中文 (Chinese), العربية (Arabic), Русский (Russian)
  3. Mixed scripts: A string combining multiple languages
  4. Characters that need multiple bytes: Emoji (though emoji support in URLs is limited)
  5. Complete URLs with multiple components: Path, query string, and fragment with international characters

Test that:

  • Encoding produces correct percent-encoded output
  • Decoding recovers the original characters
  • URLs work correctly when transmitted over HTTP
  • Server-side code correctly receives and processes the decoded values

Best Practices for International URL Encoding

  1. Always use UTF-8: Make UTF-8 your default character encoding for all applications
  2. Use standard library functions: Don't write custom encoding logic; use tested, standard functions
  3. Encode at the right point: Encode when building the URL, not before
  4. Document encoding assumptions: Make it clear in your code and documentation that UTF-8 is used
  5. Test with real international characters: Don't just test with ASCII
  6. Handle decoding on the server: Always verify that your server correctly decodes international characters from URLs
  7. Be consistent: Apply the same encoding strategy throughout your application

Conclusion

Encoding international characters in URLs is a fundamental requirement for global web applications. By understanding the two-step process of UTF-8 character encoding followed by percent-encoding, and by using standard library functions specific to your programming language, you can reliably handle any international character. UTF-8 has become the de facto standard precisely because it solves this problem elegantly, making it the obvious choice for any modern application. With these practices in place, your URLs will work correctly with any language and script the world's users speak.

Frequently Asked Questions

How do you put non-ASCII characters in a URL?

You never put the raw non-ASCII character in the URL that travels over the wire — you encode it. Which method depends on where it sits. In the host (domain) name, the label is converted to ASCII with IDN Punycode, producing an xn-- form such as xn--mnchen-3ya.de. Everywhere else — path, query string, and fragment — the character is first turned into UTF-8 bytes and each byte is then percent-encoded as %HH. So München.de/café becomes xn--mnchen-3ya.de/caf%C3%A9. Browsers show you the pretty version but send the encoded version.

Why can't URLs contain non-ASCII characters directly?

The URI standard (RFC 3986) defines a URL as a sequence of characters drawn only from a limited ASCII set — letters, digits, and a handful of punctuation marks. That restriction exists so a URL survives being typed, printed, copied, and passed through decades-old systems that assume ASCII. Anything outside that set, including every accented letter and every non-Latin script, has to be represented using the ASCII-safe encodings: Punycode for the domain and percent-encoded UTF-8 for the rest.

What is the difference between Punycode and percent-encoding?

They solve the same problem in two different places. Punycode (used by Internationalized Domain Names) re-encodes an entire Unicode domain label into an ASCII string prefixed with xn--, because DNS itself only understands the letter-digit-hyphen set and cannot carry a % sign. Percent-encoding takes the UTF-8 bytes of a character and writes each byte as %HH; it is used in the path, query, and fragment, which do allow the % character. You will frequently see both in one address: the host is Punycode, the path is percent-encoded.

What does xn-- mean in a domain name?

xn-- is the ACE (ASCII-Compatible Encoding) prefix that flags a domain label as Punycode. When a resolver or browser sees a label starting with xn--, it knows the rest is a Punycode-encoded Unicode string and can decode it back for display. The prefix was chosen because xn-- is extremely unlikely to begin a normal domain name. Example: münchen.de is stored and resolved as xn--mnchen-3ya.de.

Do domain names use percent-encoding?

No. This is the single most common mistake. Domain names are encoded with IDN Punycode, not percent-encoding, because the DNS protocol cannot carry a % character in a hostname. Your URL-encoding function (encodeURIComponent, quote, rawurlencode) should only ever be applied to the path, query, and fragment — never to the host. If you percent-encode a hostname you will get a domain that does not resolve.

What is an IRI (RFC 3987)?

An IRI (Internationalized Resource Identifier), defined in RFC 3987 in 2005, is the human-readable superset of a URI that is allowed to contain Unicode characters directly — for example https://müller.example/café. To turn an IRI into a plain URI that can travel over the network, you Punycode the host and percent-encode the UTF-8 bytes of everything else. The IRI is what people read; the URI is what the wire carries. The two are treated as equivalent addresses.

What is an IDN homograph attack?

A homograph (or homoglyph) attack registers a domain using Unicode characters that look identical to Latin ones — for example a Cyrillic "а" in place of the Latin "a" — so the address appears to be a trusted brand. Because the underlying Punycode is different, it is a completely different domain. Modern browsers defend against this by displaying the raw xn-- Punycode form instead of the pretty Unicode whenever a label mixes scripts or looks suspicious, so a spoofed "аpple.com" shows as its xn-- form in the address bar.

How do I encode a URL with Chinese characters or emoji?

Use your language's standard URL-encoding function, which handles UTF-8 plus percent-encoding for you: encodeURIComponent in JavaScript, urllib.parse.quote in Python, rawurlencode in PHP. The Chinese string 中文 becomes %E4%B8%AD%E6%96%87 (three UTF-8 bytes each). Emoji work the same way byte-for-byte, though some systems still handle four-byte sequences poorly, so test them explicitly rather than assuming they round-trip.

URL encodinginternational charactersUTF-8internationalizationweb standards