IDNA ToASCII / ToUnicode

Convert internationalized domain names between Unicode and ASCII-Compatible Encoding (ACE) using UTS #46.

Docify's IDNA converter implements UTS #46 (Unicode Technical Standard #46) to convert internationalized domain names. ToASCII encodes Unicode domains to ASCII-Compatible Encoding (ACE) with xn-- prefixes: Bücher.de becomes xn--bcher-kva.de. ToUnicode decodes back to readable form. The tool uses nontransitional processing, NFC normalization, and splits on multiple dot characters (U+002E, U+3002, U+FF0E, U+FF61). Nothing is uploaded.

Unicode domain
ToASCII output

How it works

  1. ToASCII (Encode) processes the domain through UTS #46: trim whitespace, normalize to NFC, convert to lowercase, then split on dot characters. Each non-ASCII label is encoded with RFC 3492 Punycode and prefixed with xn--.
  2. ToUnicode (Decode) reverses the process: labels with xn-- prefixes are decoded from Punycode back to Unicode. Case is normalized to lowercase.
  3. Nontransitional mode preserves characters like German ß (sharp S) rather than mapping them to ASCII equivalents. This matches modern browser behavior.
  4. Limitations: This is not a full IDNA validator. It does not implement the complete UTS #46 Mapping Table, CheckBidi, CheckJoiners, or DNS lookups.

Examples

UnicodeACE (ToASCII)
Bücher.dexn--bcher-kva.de
Straße.dexn--strae-oqa.de
中文.中国xn--fiq228c.xn--fiqs8s
münchen.dexn--mnchen-3ya.de
example.comexample.com (unchanged)

IDNA in Python and Node

The snippets below convert Bücher.de to ACE and back. Node url.domainToASCII and url.domainToUnicode are WHATWG / ICU UTS #46 — the same family this page implements (nontransitional). CPython str.encode('idna') is RFC 3490 IDNA 2003, not UTS #46. Both print xn--bcher-kva.de on this label. For Punycode without the xn-- prefix or label split, see Punycode vs IDNA: RFC 3492 and UTS #46.

Node.js with url.domainToASCII

Node 10+ ships these helpers on node:url (also as import { domainToASCII, domainToUnicode } from 'node:url'). They take a host, not a URL. new URL applies the same conversion to the hostname when you already have a scheme:

const { domainToASCII, domainToUnicode } = require('url');

domainToASCII('Bücher.de')
// "xn--bcher-kva.de"
domainToUnicode('xn--bcher-kva.de')
// "bücher.de"

new URL('https://Bücher.de').hostname
// "xn--bcher-kva.de"

Python with the idna codec

CPython's encodings.idna codec is the stdlib IDNA 2003 encoder. Encode a Unicode domain to ACE bytes; decode ACE bytes back with .decode('idna'). This is not the PyPI idna package (IDNA 2008):

'Bücher.de'.encode('idna')
# b'xn--bcher-kva.de'
'xn--bcher-kva.de'.encode('ascii').decode('idna')
# 'bücher.de'

This page and Node keep German ß: Straße.de becomes xn--strae-oqa.de. CPython maps ß to ss (IDNA 2003), so the same input becomes strasse.de, and decoding xn--strae-oqa.de raises UnicodeError. This page rejects http(s) URLs. Node domainToASCII on a full URL returns an empty string; CPython treats the whole string as one domain. This page trims whitespace and lowercases. Node rejects leading or trailing spaces (empty string). CPython does not trim, and ASCII-only labels keep their case (SS.de stays SS.de). U+3002, U+FF0E, and U+FF61 are label separators here, in Node, and in the CPython codec. For NFC before encoding, see Unicode normalize. Sibling raw Punycode (no xn--, no label split) is Punycode encoder. GNU idn2 samples are in IDNA in the shell below.

IDNA in the shell

There is no POSIX IDNA converter. GNU idn2 (Libidn2, not the older idn from libidn) is a command-line IDNA2008 lookup. Since 2.0.5 the default is Unicode TR46 nontransitional processing — the same family this page and Node url.domainToASCII use. On the documented domain Bücher.de, idn2 --quiet writes the same ACE Convert writes: xn--bcher-kva.de. Decode is -d / --decode. The locale must be UTF-8; LC_ALL=C fails with could not convert string to UTF-8. Debian idn2 and Homebrew brew install libidn2 ship the CLI. idn2 is not POSIX. For the Node and CPython snippets, see IDNA in Python and Node above.

GNU idn2 (Linux)

Confirmed here with GNU idn2 2.3.7 (Libidn2). Default lookup is the same as -N / --tr46nt. --quiet is silent operation (this version already prints only the result plus a trailing newline; this page does not append that newline). Use printf, not echo:

# ToASCII (same ACE as this page on this domain)
printf '%s' 'Bücher.de' | idn2 --quiet
# xn--bcher-kva.de

idn2 --quiet 'Bücher.de'
# xn--bcher-kva.de

# ToUnicode of an ACE label
printf '%s' 'xn--bcher-kva.de' | idn2 --quiet -d
# bücher.de

# Nontransitional ß (this page, Node, default idn2)
idn2 --quiet 'Straße.de'
# xn--strae-oqa.de

Transitional -T / --tr46t maps German ß to ss, so Straße.de becomes strasse.de. This page and Node keep xn--strae-oqa.de. That transitional mapping matches CPython encodings.idna (IDNA 2003), not this converter. --no-tr46 rejects uppercase Bücher.de and Straße.de with string contains a disallowed character. idn2 does not trim: Bücher.de fails with A-label roundtrip failed; this page trims. A string that starts with http:// or https:// fails the same way; this page throws a host-only error. A trailing dot writes xn--bcher-kva.de.; this page throws Empty DNS label. Empty stdin exits 0 with no output; Convert stays disabled until there is text. -d on a Unicode domain (not ACE) echoes the input without lowercasing — idn2 --quiet -d 'Bücher.de' writes Bücher.de; ToUnicode here lowercases to bücher.de. -r / --register is IDNA2008 registration, not lookup — straße.de fails with register: string contains a disallowed character. Older idn (libidn) is IDNA 2003, like the CPython codec. curl %{url.host} is not an IDNA or WHATWG parser. Raw RFC 3492 without xn-- is Punycode encoder. For Punycode vs IDNA, see Punycode vs IDNA: RFC 3492 and UTS #46.

FAQ

Does this tool upload my domain name?
No. All encoding and decoding runs in your browser using JavaScript. The page never sends your input to a server.
What is IDNA and why do I need it?
IDNA (Internationalized Domain Names in Applications) lets you use non-ASCII characters like Bücher.de in domain names. DNS only supports ASCII, so IDNA converts Unicode labels to ASCII-Compatible Encoding (ACE) with an xn-- prefix.
What standard does this tool follow?
This tool implements UTS #46 (Unicode Technical Standard #46) with nontransitional processing. It uses Unicode 18.0.0, NFC normalization, and RFC 3492 Punycode encoding.
How does IDNA handle German ß (sharp S)?
In nontransitional mode, ß stays as ß (not mapped to ss). So Straße.de becomes xn--strae-oqa.de, preserving the sharp S rather than converting it to xn--strasse-...
What is the difference between IDNA and Punycode?
Punycode (RFC 3492) is just the encoding algorithm. IDNA adds domain-specific processing: NFC normalization, label splitting on dots, validation rules, and the xn-- prefix for each non-ASCII label.
Can I paste a full URL?
No. This tool expects domain names only. Strings starting with http:// or https:// are rejected. For full URLs, use the URL Parser tool which applies IDNA to the hostname automatically.

Related