bcp-47
Parse and stringify BCP 47 language tags into structured, spec-compliant schema objects.
Repository Health
Technical Analysis
bcp-47 is a zero-heavyweight-dependency JavaScript library that parses BCP 47 (RFC 5646) language tags — the standard behind HTML lang attributes, HTTP Accept-Language headers, and Unicode locale identifiers — into a fully-typed schema object, and stringifies schema objects back into valid tags. It implements the complete BCP 47 grammar: extended language subtags, ISO 15924 scripts, ISO 3166-1/UN M49 regions, variants, singleton extensions, private-use subtags, and the fixed lists of grandfathered irregular and regular tags.
The parser supports a forgiving mode that returns partial results with warning callbacks instead of throwing, plus a default normalization mode that maps legacy forms (like i-klingon) to their modern equivalents. Written by Titus Wormer as part of the wooorm/unified family of text-processing utilities, it ships ESM-only with full TypeScript types and depends only on his own tiny is-alphabetical/is-alphanumerical/is-decimal character-class checkers.
What You Get
- A
parse(tag, options)function that turns any BCP 47 string into a structured schema with language, script, region, variants, extensions, and private-use fields - A
stringify(schema)function that serializes a schema object back into a canonical BCP 47 tag string - Forgiving-mode parsing that returns partial matches with warning callbacks instead of throwing on malformed tags
- Automatic normalization of legacy/irregular tags (e.g.
i-klingontotlh) per the BCP 47 registry - Full TypeScript type definitions for
Schema,Extension,Options, andWarning
Common Use Cases
- Validating and normalizing user-supplied
Accept-Languageheader values in an HTTP server - Extracting the script/region/variant components of a document’s
langattribute for i18n routing - Linting or canonicalizing locale tags stored in a CMS or translation management system
- Building higher-level locale-matching or negotiation logic on top of a reliable BCP 47 parser
Under The Hood
Architecture
The core is two small files plus two static lookup tables: parse.js implements a hand-written character scanner over the lowercased tag string, threading a single index cursor through sequential grammar productions — language, then extended language subtags, script, region, variants, extensions, and private-use — with an early-exit fail() closure that either returns partial results (forgiving mode) or an empty schema. stringify.js is a pure inverse function that reads the same schema shape and joins its parts back into a tag string. regular.js and normal.js hold the fixed lists of grandfathered/legacy tags used for normalization. There is no class hierarchy, no external state, and no I/O — the whole library is stateless value transformation, so a change to the core parsing loop stays fully self-contained and is exercised directly by the fixtures under test/fixtures.
Tech Stack
Plain ESM JavaScript (type: module) authored with JSDoc and compiled to a generated index.d.ts for full TypeScript types, with type-coverage enforcing 100% strict coverage. It targets Node.js 14.14+/16.0+ as well as Deno and browsers via esm.sh. Runtime dependencies are limited to three single-purpose wooorm packages — is-alphabetical, is-alphanumerical, and is-decimal. Build and quality tooling includes tsc for type-checking and declaration generation, xo for linting, prettier for formatting, remark-cli with remark-preset-wooorm for linting the README itself, c8 for coverage, and GitHub Actions running against both the current Node.js LTS and latest releases.
Code Quality
Tests live in test/parse.js and test/stringify.js using node:test and node:assert/strict, plus a data-driven test/fixtures.js that iterates real-world tag fixtures under test/fixtures. c8 enforces 100% coverage (c8 --100) as part of the test script. Error handling is explicit: parse() throws synchronously on null/undefined input, and internal failures route through a single fail() closure that either invokes a caller-supplied warning callback or returns an empty/partial schema — nothing is silently swallowed. Naming is terse but consistent with the author’s other packages, and the whole codebase is typed via JSDoc with strict type-coverage enforcement in CI.
API Design
The public surface is exactly two named exports, parse and stringify, built around a Schema object designed to round-trip losslessly between them. Defaults favor safety (forgiving: false, normalize: true) while still exposing an escape hatch — the warning callback — for callers who want detail on why a tag failed rather than a bare success/failure signal. There’s no configuration object to construct and no class to instantiate; getting started is a single import and a single function call, consistent with the author’s broader pattern of small, single-purpose unified-ecosystem packages.