Punycode Decoder: Convert xn-- (IDN) to Unicode

Encode Unicode text to Punycode (RFC 3492) or decode Punycode back to Unicode.

Docify's Punycode encoder implements RFC 3492 Bootstring to convert Unicode text into ASCII-compatible strings. Example: bücher encodes to bcher-kva. ASCII characters are preserved; non-ASCII code points become base-36 deltas after the last hyphen. This is the raw algorithm without thexn-- prefix — for full domain name processing, use the IDNA tool. Decode accepts an optional xn-- prefix and strips it. Nothing is uploaded.

Unicode text
Encoded output

How it works

  1. Encoding: RFC 3492 Bootstring copies ASCII characters (0–127) literally. If any ASCII was copied, a hyphen delimiter is appended. Non-ASCII code points are then encoded as base-36 deltas.
  2. All-ASCII input: The string hello encodes to hello- (trailing hyphen indicates empty delta section). IDNA ToASCII leaves it unchanged.
  3. Decoding: Strips one optional leading xn-- prefix (any case), then reverses the RFC 3492 algorithm to recover the original Unicode string.
  4. No normalization: Unlike IDNA, this tool does not normalize Unicode. NFC and NFD forms of the same character produce different Punycode strings.

Examples

UnicodePunycode
bücherbcher-kva
münchenmnchen-3ya
中文fiq228c
hellohello-
StraßeStrae-oqa

Punycode vs IDNA

This page is RFC 3492 Bootstring: one Unicode string in, one ASCII string out. It does not add xn--, does not normalize, and does not split on dots. IDNA is the domain layer on top — UTS #46 ToASCII runs NFC and lowercase, splits labels, then RFC 3492 with xn-- on each non-ASCII label. Node's legacy punycode module is deprecated (DEP0040), but punycode.encode and CPython encodings.punycode still write the same RFC 3492 strings this page writes — snippets are in Punycode in Python and Node below. url.domainToASCII is IDNA, not this page. For full domain conversion, use the IDNA converter. For the longer comparison, see Punycode vs IDNA: RFC 3492 and UTS #46.

AspectThis page (RFC 3492)IDNA (UTS #46)
SpecRFC 3492 BootstringUTS #46 ToASCII / ToUnicode (Unicode 18.0.0, nontransitional)
xn-- prefixNot added on encode; decode strips one optional leading prefixPrefixed on each non-ASCII label
Dots / labelsA dot is a regular characterSplits on U+002E, U+3002, U+FF0E, U+FF61
NormalizationNoneNFC, then lowercase
Domain-like inputbücher.de → bcher.de-65aBücher.de → xn--bcher-kva.de
All-ASCII inputhello → hello-example.com unchanged

Encode bücher writes bcher-kva. Decode of xn--bcher-kva strips the prefix and recovers bücher. NFC é (U+00E9) encodes to 9ca; NFD e + U+0301 encodes to e-xbb— this page does not call String.prototype.normalize. Straße encodes to Strae-oqa; IDNA nontransitional keeps ß, so Straße.de becomes xn--strae-oqa.de. CPython encodings.idna is IDNA 2003 and maps ß to ss. This page rejects http(s) URLs — percent-encode a URL on URL encoder. For NFC / NFD, see Unicode normalizer.

Punycode in Python and Node

The snippets below encode bücher with RFC 3492 Bootstring — the same path this page uses. Node punycode.encode and CPython str.encode('punycode') both print bcher-kva. They do not add xn--, do not split on dots, and do not run NFC. For ACE labels and ToASCII, see Punycode vs IDNA: RFC 3492 and UTS #46.

Node.js with punycode.encode

Node still ships node:punycode (also require('punycode')). Node 22 prints DEP0040 and asks for a userland alternative; url.domainToASCII is UTS #46, not this encoder. Decode of xn--bcher-kva here strips the prefix first; punycode.decode does not:

const punycode = require('punycode');

punycode.encode('bücher')
// "bcher-kva"
punycode.decode('bcher-kva')
// "bücher"

punycode.encode('hello')
// "hello-"
punycode.encode('bücher.de')
// "bcher.de-65a"

punycode.decode('xn--bcher-kva')
// "xn--bÊcher"
punycode.toASCII('bücher')
// "xn--bcher-kva"

Python with the punycode codec

CPython's encodings.punycode codec is the stdlib RFC 3492 encoder. Encode a Unicode str to ASCII bytes; decode those bytes back with .decode('punycode'). This is not the idna codec (RFC 3490 IDNA 2003) and not the PyPI idna package:

'bücher'.encode('punycode')
# b'bcher-kva'
b'bcher-kva'.decode('punycode')
# 'bücher'

'hello'.encode('punycode')
# b'hello-'
'bücher.de'.encode('punycode')
# b'bcher.de-65a'

b'xn--bcher-kva'.decode('punycode')
# 'xn--bÊcher'
'bücher'.encode('idna')
# b'xn--bcher-kva'

NFC é (U+00E9) encodes to 9ca here, in Node, and in CPython. NFD e + U+0301 encodes to e-xbb. An empty string encodes to empty in Node and CPython; this page asks for Unicode text. This page rejects http(s) URLs; Node and CPython encode the whole string (https://bcher.de-rzb). Decode of xn--bcher-kva.de fails here and in the snippets because . is not an RFC 3492 digit — punycode.toUnicode and the IDNA converter split labels first. There is no POSIX Punycode encoder. For NFC before encoding, see Unicode normalizer.

FAQ

Does this tool upload my text?
No. All encoding and decoding runs in your browser using JavaScript. Nothing is sent to a server.
What is Punycode used for?
Punycode (RFC 3492) encodes Unicode text into ASCII. It is the core algorithm behind internationalized domain names (IDN), allowing domain names like bücher.de to be represented as xn--bcher-kva.de in DNS.
What is the difference between Punycode and IDNA?
Punycode is just the encoding algorithm. IDNA (Internationalized Domain Names in Applications) adds domain-specific processing: NFC normalization, label splitting on dots, validation, and the xn-- prefix. Use the IDNA tool for full domain name conversion.
Does encoding preserve Unicode normalization?
Yes and no. Punycode encodes the exact code points it receives without normalizing. NFC é (U+00E9) and NFD e+combining-accent encode to different Punycode strings. IDNA normalizes to NFC first.
Why does this tool not add the xn-- prefix?
This is a raw RFC 3492 Punycode encoder. The xn-- prefix is added by IDNA processing, not the Punycode algorithm itself. Decode accepts an optional xn-- prefix and strips it.
Can I encode domain names with dots?
Yes, but the dot is treated as a regular character, not a label separator. bücher.de encodes to bcher.de-65a (one string), not xn--bcher-kva.de (two labels). Use IDNA for proper domain name handling.
What does xn-- mean in a domain?
The xn-- prefix is the ACE (ASCII-Compatible Encoding) prefix defined by IDNA. It signals that the label contains Punycode-encoded Unicode characters. For example, xn--bcher-kva is the ACE form of bücher. The prefix tells DNS resolvers and browsers to decode the label using Punycode.

Related