This library implements BCP 47 in Clojure and ClojureScript: the language tags of RFC 5646 and their matching by RFC 4647. Use it to pick the language of a response from an Accept-Language header, or to check the language of a document.
This library was developed with assistance from frontier LLMs.
The library needs Clojure 1.11+ and Java 11+. Add the latest release from
Clojars to the :deps of
your deps.edn:
dk.simongray/langtag-clj {:mvn/version "0.2.1"}
For changes that aren't released yet, use the SHA of the latest commit on
master instead. In shadow-cljs a Git dependency only works through
deps.edn, so set :deps true in your shadow-cljs.edn too.
Parse a tag with langtag/parse:
(require '[dk.simongray.langtag :as langtag])
(langtag/parse "zh-yue-Hant-HK-u-co-pinyin-x-private")
;; => {:language "zh" :extlangs ["yue"] :script "Hant" :region "HK"
;; :extensions {"u" ["co" "pinyin"]} :private-use ["private"]}
The map only has the subtags that the tag has, in canonical case. A
grandfathered tag such as i-klingon parses as
{:grandfathered "i-klingon"}. To write a map back as text, or to put a
tag in canonical case, use langtag/tag:
(langtag/tag "EN-gb")
;; => "en-GB"
(langtag/tag {:language "sr" :script "latn" :region "rs"})
;; => "sr-Latn-RS"
For a tag that isn't well formed, both functions return nil. To find out
why, use langtag/error:
(langtag/error "de-419-DE")
;; => {:reason :unexpected-subtag :subtag "DE" :index 2}
To check a tag, use langtag/well-formed?. Besides the grammar, it checks
the two rules of RFC 5646 that need no registry: no variant or extension
may appear twice. To check its subtags as well, see
the registry.
Lookup picks the one tag of yours that best matches the user's preferences, e.g. for the language of a page:
(langtag/lookup "da-DK, en;q=0.8" ["da" "en"])
;; => "da"
(langtag/lookup "fr-CA" ["da" "en"])
;; => nil
The first argument is a language priority list: an Accept-Language value,
or a coll of ranges in order of priority such as ["da" "en"]. Lookup
falls back from da-DK to da before it tries the next range. When nothing
matches, the default is up to you, e.g. (or (langtag/lookup ...) "en").
Filtering returns every tag that matches, in order of priority.
Basic filtering
matches the start of a tag, while
extended filtering
also skips subtags in between, such as a script. Its ranges can start
with * for any language, e.g. *-CH.
(langtag/basic-filter "de-DE" ["de-DE-1996" "de-Deva" "de-Latn-DE"])
;; => ["de-DE-1996"]
(langtag/extended-filter "de-DE" ["de-DE-1996" "de-Deva" "de-Latn-DE"])
;; => ["de-DE-1996" "de-Latn-DE"]
The matching functions return the tags as you gave them. To match other things than tags, e.g. the labels of a word, pass a function that returns the tag of each (or nil when it has none):
(langtag/lookup "da-DK, en;q=0.8" :lang [{:text "dog" :lang "en"}
{:text "hund" :lang "da"}])
;; => {:text "hund" :lang "da"}
To see the priority list itself, use langtag/priority-list:
(langtag/priority-list "da, en-gb;q=0.8, en;q=0.7")
;; => [{:range "da" :weight 1.0} {:range "en-gb" :weight 0.8}
;; {:range "en" :weight 0.7}]
A weight of 0 rules a language out. A tag has the weight of the most
specific range that matches it, as in HTTP/1.1
(RFC 2616), so
"en, en-GB;q=0" accepts all English but British English.
The root namespace works from the syntax of a tag alone. The registry
namespace adds what needs the
IANA Language Subtag Registry:
whether a tag is valid, i.e. whether the registry has each of its
subtags, and the canonical form of a tag.
(require '[dk.simongray.langtag.registry :as registry])
(registry/error "en-UK")
;; => {:reason :unregistered-subtag :subtag "UK" :index 1}
(registry/canonical-form "iw-IL")
;; => "he-IL"
(registry/extlang-form "yue-HK")
;; => "zh-yue-HK"
The canonical form replaces deprecated tags and subtags with the ones that the registry prefers. The extlang form also puts a language such as Cantonese (yue) after its macrolanguage (zh), as the registry allows for a few of them.
The namespace has its own lookup, basic-filter and extended-filter,
which compare ranges and tags in extlang form. So a browser that asks for
iw gets your he page, and one that asks for yue-HK falls back to zh:
(registry/lookup "yue-HK, en;q=0.8" ["en" "zh"])
;; => "zh"
The library carries the registry of registry/file-date, and each update
of the registry means a new release.
The registry has more than the library uses yet:
The parser keeps the subtags of the -u- (RFC 6067) and -t- (RFC 6497) extensions as plain subtags. Their keys come from CLDR rather than the registry, so reading them may come later.
clojure -X:test # the tests on the JVM
npm install # once, for the Node tests
clojure -M:cljs compile test # the tests in Node
clojure -M:bench -m dk.simongray.langtag.bench # benchmarks on the JVM
clojure -M:cljs release bench && node target/bench.js # benchmarks in Node
dev/fetch.sh && clojure -X:tables # a newer registry
IANA keeps no older copies of the registry, so the repository keeps the one in dev-resources that the table in resources is made from. After an update, commit both.
Besides the examples of RFC 5646 and RFC 4647, the tests compare random tags and priority lists with the JDK, and the registry namespace with each record of the registry. The docstring of jdk_test.clj lists where the JDK differs.
The public namespaces are the root and registry. The internal ones are
under impl:
impl.syntax, which parses and writes tagsimpl.matching, which reads priority lists and matches tagsimpl.registry, which reads the registry's table and puts tags in
canonical formThe langtag-clj project is licensed under the MIT licence.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |