The punycoder package provides high-performance Unicode
and Punycode encoding/decoding for internationalized domain names
(IDNs). It addresses critical gaps in R’s URL processing capabilities by
offering reliable, fast conversion between Unicode and ASCII
representations of domain names.
International domain names containing Unicode characters (like café.com or москва.рф) need to be converted to ASCII format for use in many network protocols and systems. Existing R packages have limitations:
punycoder provides:
library(punycoder)
# Encode Unicode domains to ASCII
puny_encode("café.com")
# Returns: "xn--caf-dma.com"
puny_encode("москва.рф")
# Returns: "xn--80adxhks.xn--p1ai"
# Decode ASCII domains back to Unicode
puny_decode("xn--caf-dma.com")
# Returns: "café.com"
# Vectorized operations
domains <- c("café.com", "москва.рф", "北京.中国")
encoded <- puny_encode(domains)
print(encoded)# Check if domain is already punycode
is_punycode("xn--caf-dma.com") # TRUE
is_punycode("café.com") # FALSE
# Check if domain contains Unicode characters
is_idn("café.com") # TRUE
is_idn("example.com") # FALSE
# Comprehensive domain validation
result <- validate_domain(c("café.com", "invalid..domain", "valid.org"))
print(result)puny_encode() is the raw RFC 3492 transform: it
Punycode-encodes the labels you hand it, and nothing more.
host_normalize() is the higher-level operation. It applies
the UTS #46 canonical-host pipeline — character mapping, case folding,
NFC normalization, label validation — and then encodes, so it
answers “what is the canonical ASCII form of this host?” rather than
“what is the Punycode of this string?”.
library(punycoder)
# Mapped, case-folded, validated, then encoded
host_normalize("Café.Example.COM")
#> [1] "xn--caf-dma.example.com"
# The codec transforms what it is given, and leaves case alone
puny_encode("Café.Example.COM")
#> [1] "xn--Caf-dma.Example.COM"host_normalize() does not follow the strict/non-strict
switch described above. It is a separate contract: a host that fails UTS
#46 validation is reported as NA and never raises an error,
so callers can layer their own policy on top.
Normalization is pinned to a named profile.
normalization_profile_info() returns that identity as a
one-row data frame, which is what you record alongside results you
intend to reproduce later:
A build of punycoder ships a set of vendored Unicode
table versions, one of which is the pinned default:
Any shipped version can be selected per call. The difference between versions is confined to code points assigned in the newer one — a character that does not exist yet in the older tables is disallowed there, so the host is invalid:
host <- paste0(intToUtf8(0xA7CF), ".example")
host_normalize(host, unicode_version = "16.0.0")
#> [1] NA
host_normalize(host, unicode_version = "17.0.0")
#> [1] "xn--078a.example"Selecting a non-default version is reflected in the profile token, so the identity you record always describes the normalization that actually ran:
normalization_profile_info(unicode_version = "16.0.0")$profile
#> [1] "uts46-nontransitional-std3-v2+unicode-16.0.0"The argument defaults to NULL, which means the pinned
version — deliberately not “the newest available”, so adding a table set
in a later release cannot silently change results under existing code.
Naming a version the build does not ship is an error listing what is
available, never a quiet fall back to the pin.
See ?host_normalize for the relaxable validation flags
and the full profile parameters.
The package provides robust error handling with informative messages:
# Strict validation (default)
try({
puny_encode(c("valid.com", "")) # Empty string causes error
})
# Non-strict mode returns NA for invalid input
result <- puny_encode(c("valid.com", ""), strict = FALSE)
print(result)
# Validation provides detailed error information
validation <- validate_domain(c("valid.com", "invalid..domain", ""))
print(validation)The package is designed for high-performance processing:
You can configure package behavior using R options:
punycoder is designed to integrate well with other R
packages:
# With data.table
library(data.table)
dt <- data.table(
original = c("café.com", "москва.рф"),
encoded = puny_encode(c("café.com", "москва.рф"))
)
# With dplyr
library(dplyr)
domains_df <- data.frame(
unicode_domain = c("café.com", "москва.рф")
) |>
mutate(
ascii_domain = puny_encode(unicode_domain),
is_international = is_idn(unicode_domain)
)help(package = "punycoder")The package uses a C++ backend with Rcpp for performance, and follows
RFC 3492 standards for punycode implementation. When
libidn2 is available at build time, punycoder
uses it behind the same R-level API and falls back to the built-in
implementation otherwise.
punycoder is used as the Punycode and IDNA engine by two
sibling packages:
punycoder for host
canonicalization before PSL matching. Reach for it when you need eTLD or
registrable-domain queries.punycoder and pslr to handle the complete
URL processing pipeline.punycoder descends from the earlier R hrbrmstr/punycode
package and implements published standards directly: RFC 3492
(Punycode), the IDNA2008 RFCs, Unicode UTS #46, and UAX #15
normalization, validated against the Unicode Consortium’s IdnaTestV2
conformance data. It is built on Rcpp with an optional GNU
libidn2 backend.
The full list of credits — prior art, dependencies, the standards
this code implements, and the data sources it serves — is in ACKNOWLEDGMENTS.md.