diff --git a/src/content/accept_language.rs b/src/content/accept_language.rs
new file mode 100644
index 00000000..e6937db9
--- /dev/null
+++ b/src/content/accept_language.rs
@@ -0,0 +1,240 @@
+//! Client header advertising which languages the client is able to understand.
+
+use crate::content::LanguageProposal;
+use crate::headers::{Header, HeaderValue, Headers, ACCEPT_LANGUAGE};
+
+use std::fmt::{self, Debug, Write};
+use std::slice;
+
+/// Client header advertising which languages the client is able to understand.
+pub struct AcceptLanguage {
+ wildcard: bool,
+ entries: Vec<LanguageProposal>,
+}
+
+impl AcceptLanguage {
+ /// Create a new instance of `AcceptLanguage`.
+ pub fn new() -> Self {
+ Self {
+ entries: vec![],
+ wildcard: false,
+ }
+ }
+
+ /// Create an instance of `AcceptLanguage` from a `Headers` instance.
+ pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
+ let mut entries = vec![];
+ let headers = match headers.as_ref().get(ACCEPT_LANGUAGE) {
+ Some(headers) => headers,
+ None => return Ok(None),
+ };
+
+ let mut wildcard = false;
+
+ for value in headers {
+ for part in value.as_str().trim().split(',') {
+ let part = part.trim();
+
+ if part.is_empty() {
+ continue;
+ } else if part == "*" {
+ wildcard = true;
+ continue;
+ }
+
+ let entry = LanguageProposal::from_str(part)?;
+ entries.push(entry);
+ }
+ }
+
+ Ok(Some(Self { wildcard, entries }))
+ }
+
+ /// Push a directive into the list of entries.
+ pub fn push(&mut self, prop: impl Into<LanguageProposal>) {
+ self.entries.push(prop.into())
+ }
+
+ /// Returns `true` if a wildcard directive was passed.
+ pub fn wildcard(&self) -> bool {
+ self.wildcard
+ }
+
+ /// Set the wildcard directive.
+ pub fn set_wildcard(&mut self, wildcard: bool) {
+ self.wildcard = wildcard
+ }
+
+ /// An iterator visiting all entries.
+ pub fn iter(&self) -> Iter<'_> {
+ Iter {
+ inner: self.entries.iter(),
+ }
+ }
+
+ /// An iterator visiting all entries.
+ pub fn iter_mut(&mut self) -> IterMut<'_> {
+ IterMut {
+ inner: self.entries.iter_mut(),
+ }
+ }
+}
+
+impl Header for AcceptLanguage {
+ fn header_name(&self) -> crate::headers::HeaderName {
+ ACCEPT_LANGUAGE
+ }
+
+ fn header_value(&self) -> crate::headers::HeaderValue {
+ let mut output = String::new();
+ for (n, directive) in self.entries.iter().enumerate() {
+ let directive: HeaderValue = directive.clone().into();
+ match n {
+ 0 => write!(output, "{}", directive).unwrap(),
+ _ => write!(output, ", {}", directive).unwrap(),
+ };
+ }
+
+ if self.wildcard {
+ match output.len() {
+ 0 => write!(output, "*").unwrap(),
+ _ => write!(output, ", *").unwrap(),
+ };
+ }
+
+ // SAFETY: the internal string is validated to be ASCII.
+ unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
+ }
+}
+
+/// A borrowing iterator over entries in `AcceptLanguage`.
+#[derive(Debug)]
+pub struct IntoIter {
+ inner: std::vec::IntoIter<LanguageProposal>,
+}
+
+impl Iterator for IntoIter {
+ type Item = LanguageProposal;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.inner.next()
+ }
+
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.inner.size_hint()
+ }
+}
+
+/// A lending iterator over entries in `AcceptLanguage`.
+#[derive(Debug)]
+pub struct Iter<'a> {
+ inner: slice::Iter<'a, LanguageProposal>,
+}
+
+impl<'a> Iterator for Iter<'a> {
+ type Item = &'a LanguageProposal;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.inner.next()
+ }
+
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.inner.size_hint()
+ }
+}
+
+/// A mutable iterator over entries in `AcceptLanguage`.
+#[derive(Debug)]
+pub struct IterMut<'a> {
+ inner: slice::IterMut<'a, LanguageProposal>,
+}
+
+impl<'a> Iterator for IterMut<'a> {
+ type Item = &'a mut LanguageProposal;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.inner.next()
+ }
+
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.inner.size_hint()
+ }
+}
+
+impl Debug for AcceptLanguage {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut list = f.debug_list();
+ for directive in &self.entries {
+ list.entry(directive);
+ }
+ list.finish()
+ }
+}
+
+impl IntoIterator for AcceptLanguage {
+ type Item = LanguageProposal;
+ type IntoIter = IntoIter;
+
+ #[inline]
+ fn into_iter(self) -> Self::IntoIter {
+ IntoIter {
+ inner: self.entries.into_iter(),
+ }
+ }
+}
+
+impl<'a> IntoIterator for &'a AcceptLanguage {
+ type Item = &'a LanguageProposal;
+ type IntoIter = Iter<'a>;
+
+ #[inline]
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a mut AcceptLanguage {
+ type Item = &'a mut LanguageProposal;
+ type IntoIter = IterMut<'a>;
+
+ #[inline]
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter_mut()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::Response;
+
+ #[test]
+ fn smoke() -> crate::Result<()> {
+ let lang = LanguageProposal::new("en-CA", Some(1.0)).unwrap();
+ let mut accept = AcceptLanguage::new();
+ accept.push(lang.clone());
+
+ let mut headers = Response::new(200);
+ accept.apply_header(&mut headers);
+
+ let accept = AcceptLanguage::from_headers(headers)?.unwrap();
+ assert_eq!(accept.iter().next().unwrap(), &lang);
+ Ok(())
+ }
+
+ #[test]
+ fn wildcard() -> crate::Result<()> {
+ let mut accept = AcceptLanguage::new();
+ accept.set_wildcard(true);
+
+ let mut headers = Response::new(200);
+ accept.apply_header(&mut headers);
+
+ let accept = AcceptLanguage::from_headers(headers)?.unwrap();
+ assert!(accept.wildcard());
+ Ok(())
+ }
+}
diff --git a/src/content/language_range_proposal.rs b/src/content/language_range_proposal.rs
new file mode 100644
index 00000000..1b4b85c7
--- /dev/null
+++ b/src/content/language_range_proposal.rs
@@ -0,0 +1,125 @@
+use crate::ensure;
+use crate::headers::HeaderValue;
+use crate::language::LanguageRange;
+use crate::utils::parse_weight;
+
+use std::cmp::{Ordering, PartialEq};
+use std::ops::{Deref, DerefMut};
+use std::str::FromStr;
+
+/// A proposed `LanguageRange` in `AcceptLanguage`.
+#[derive(Debug, Clone, PartialEq)]
+pub struct LanguageProposal {
+ /// The proposed language.
+ pub(crate) language: LanguageRange,
+
+ /// The weight of the proposal.
+ ///
+ /// This is a number between 0.0 and 1.0, and is max 3 decimal points.
+ weight: Option<f32>,
+}
+
+impl LanguageProposal {
+ /// Create a new instance of `LanguageProposal`.
+ pub fn new(language: impl Into<LanguageRange>, weight: Option<f32>) -> crate::Result<Self> {
+ if let Some(weight) = weight {
+ ensure!(
+ weight.is_sign_positive() && weight <= 1.0,
+ "LanguageProposal should have a weight between 0.0 and 1.0"
+ )
+ }
+
+ Ok(Self {
+ language: language.into(),
+ weight,
+ })
+ }
+
+ /// Get the proposed language.
+ pub fn language_range(&self) -> &LanguageRange {
+ &self.language
+ }
+
+ /// Get the weight of the proposal.
+ pub fn weight(&self) -> Option<f32> {
+ self.weight
+ }
+
+ pub(crate) fn from_str(s: &str) -> crate::Result<Self> {
+ let mut parts = s.split(';');
+ let language = LanguageRange::from_str(parts.next().unwrap())?;
+ let weight = parts.next().map(parse_weight).transpose()?;
+ Ok(Self::new(language, weight)?)
+ }
+}
+
+impl From<LanguageRange> for LanguageProposal {
+ fn from(language: LanguageRange) -> Self {
+ Self {
+ language,
+ weight: None,
+ }
+ }
+}
+
+impl PartialEq<LanguageRange> for LanguageProposal {
+ fn eq(&self, other: &LanguageRange) -> bool {
+ self.language == *other
+ }
+}
+
+impl PartialEq<LanguageRange> for &LanguageProposal {
+ fn eq(&self, other: &LanguageRange) -> bool {
+ self.language == *other
+ }
+}
+
+impl Deref for LanguageProposal {
+ type Target = LanguageRange;
+ fn deref(&self) -> &Self::Target {
+ &self.language
+ }
+}
+
+impl DerefMut for LanguageProposal {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.language
+ }
+}
+
+impl PartialOrd for LanguageProposal {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ match (self.weight, other.weight) {
+ (Some(left), Some(right)) => left.partial_cmp(&right),
+ (Some(_), None) => Some(Ordering::Greater),
+ (None, Some(_)) => Some(Ordering::Less),
+ (None, None) => None,
+ }
+ }
+}
+
+impl From<LanguageProposal> for HeaderValue {
+ fn from(entry: LanguageProposal) -> HeaderValue {
+ let s = match entry.weight {
+ Some(weight) => format!("{};q={:.3}", entry.language, weight),
+ None => entry.language.to_string(),
+ };
+ unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn smoke() {
+ let _ = LanguageProposal::new("en", Some(1.0)).unwrap();
+ }
+
+ #[test]
+ fn error_code_500() {
+ let err = LanguageProposal::new("en", Some(1.1)).unwrap_err();
+ assert_eq!(err.status(), 500);
+ }
+}
diff --git a/src/content/mod.rs b/src/content/mod.rs
index aff9fb4b..fb5fd5b4 100644
--- a/src/content/mod.rs
+++ b/src/content/mod.rs
@@ -33,6 +33,7 @@
pub mod accept;
pub mod accept_encoding;
+pub mod accept_language;
pub mod content_encoding;
mod content_length;
@@ -40,6 +41,7 @@ mod content_location;
mod content_type;
mod encoding;
mod encoding_proposal;
+mod language_range_proposal;
mod media_type_proposal;
#[doc(inline)]
@@ -47,10 +49,13 @@ pub use accept::Accept;
#[doc(inline)]
pub use accept_encoding::AcceptEncoding;
#[doc(inline)]
+pub use accept_language::AcceptLanguage;
+#[doc(inline)]
pub use content_encoding::ContentEncoding;
pub use content_length::ContentLength;
pub use content_location::ContentLocation;
pub use content_type::ContentType;
pub use encoding::Encoding;
pub use encoding_proposal::EncodingProposal;
+pub use language_range_proposal::LanguageProposal;
pub use media_type_proposal::MediaTypeProposal;
diff --git a/src/language/mod.rs b/src/language/mod.rs
new file mode 100644
index 00000000..45bcd045
--- /dev/null
+++ b/src/language/mod.rs
@@ -0,0 +1,172 @@
+//! RFC 4647 Language Ranges.
+//!
+//! [Read more](https://datatracker.ietf.org/doc/html/rfc4647)
+
+mod parse;
+
+use crate::headers::HeaderValue;
+use std::{
+ borrow::Cow,
+ fmt::{self, Display},
+ slice,
+ str::FromStr,
+};
+
+/// An RFC 4647 language range.
+#[derive(Debug, Clone, PartialEq)]
+pub struct LanguageRange {
+ pub(crate) subtags: Vec<Cow<'static, str>>,
+}
+
+impl LanguageRange {
+ /// An iterator visiting all entries.
+ pub fn iter(&self) -> Iter<'_> {
+ Iter {
+ inner: self.subtags.iter(),
+ }
+ }
+
+ /// An iterator visiting all entries.
+ pub fn iter_mut(&mut self) -> IterMut<'_> {
+ IterMut {
+ inner: self.subtags.iter_mut(),
+ }
+ }
+}
+
+impl Display for LanguageRange {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut tags = self.subtags.iter();
+ if let Some(tag) = tags.next() {
+ write!(f, "{}", tag)?;
+
+ for tag in tags {
+ write!(f, "-{}", tag)?;
+ }
+ }
+ Ok(())
+ }
+}
+
+/// A borrowing iterator over entries in `LanguageRange`.
+#[derive(Debug)]
+pub struct IntoIter {
+ inner: std::vec::IntoIter<Cow<'static, str>>,
+}
+
+impl Iterator for IntoIter {
+ type Item = Cow<'static, str>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.inner.next()
+ }
+
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.inner.size_hint()
+ }
+}
+
+/// A lending iterator over entries in `LanguageRange`.
+#[derive(Debug)]
+pub struct Iter<'a> {
+ inner: slice::Iter<'a, Cow<'static, str>>,
+}
+
+impl<'a> Iterator for Iter<'a> {
+ type Item = &'a Cow<'static, str>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.inner.next()
+ }
+
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.inner.size_hint()
+ }
+}
+
+/// A mutable iterator over entries in `LanguageRange`.
+#[derive(Debug)]
+pub struct IterMut<'a> {
+ inner: slice::IterMut<'a, Cow<'static, str>>,
+}
+
+impl<'a> Iterator for IterMut<'a> {
+ type Item = &'a mut Cow<'static, str>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.inner.next()
+ }
+
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.inner.size_hint()
+ }
+}
+
+impl IntoIterator for LanguageRange {
+ type Item = Cow<'static, str>;
+ type IntoIter = IntoIter;
+
+ #[inline]
+ fn into_iter(self) -> Self::IntoIter {
+ IntoIter {
+ inner: self.subtags.into_iter(),
+ }
+ }
+}
+
+impl<'a> IntoIterator for &'a LanguageRange {
+ type Item = &'a Cow<'static, str>;
+ type IntoIter = Iter<'a>;
+
+ #[inline]
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a mut LanguageRange {
+ type Item = &'a mut Cow<'static, str>;
+ type IntoIter = IterMut<'a>;
+
+ #[inline]
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter_mut()
+ }
+}
+
+impl From<LanguageRange> for HeaderValue {
+ fn from(language: LanguageRange) -> Self {
+ let s = language.to_string();
+ unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) }
+ }
+}
+
+impl FromStr for LanguageRange {
+ type Err = crate::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ parse::parse(s)
+ }
+}
+
+impl<'a> From<&'a str> for LanguageRange {
+ fn from(value: &'a str) -> Self {
+ Self::from_str(value).unwrap()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_iter() -> crate::Result<()> {
+ let range: LanguageRange = "en-CA".parse().unwrap();
+ let subtags: Vec<_> = range.iter().collect();
+ assert_eq!(&subtags, &["en", "CA"]);
+ Ok(())
+ }
+}
diff --git a/src/language/parse.rs b/src/language/parse.rs
new file mode 100644
index 00000000..78c5c7e1
--- /dev/null
+++ b/src/language/parse.rs
@@ -0,0 +1,59 @@
+use std::borrow::Cow;
+
+use super::LanguageRange;
+
+fn split_tag(input: &str) -> Option<(&str, &str)> {
+ match input.find('-') {
+ Some(pos) if pos <= 8 => {
+ let (tag, rest) = input.split_at(pos);
+ Some((tag, &rest[1..]))
+ }
+ Some(_) => None,
+ None => (input.len() <= 8).then(|| (input, "")),
+ }
+}
+
+// language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*"
+// alphanum = ALPHA / DIGIT
+pub(crate) fn parse(input: &str) -> crate::Result<LanguageRange> {
+ let mut tags = Vec::new();
+
+ let (tag, mut input) = split_tag(input).ok_or_else(|| crate::format_err!("WIP error"))?;
+ crate::ensure!(!tag.is_empty(), "Language tag should not be empty");
+ crate::ensure!(
+ tag.bytes()
+ .all(|b| (b'a'..=b'z').contains(&b) || (b'A'..=b'Z').contains(&b)),
+ "Language tag should be alpha"
+ );
+ tags.push(Cow::from(tag.to_string()));
+
+ while !input.is_empty() {
+ let (tag, rest) = split_tag(input).ok_or_else(|| crate::format_err!("WIP error"))?;
+ crate::ensure!(!tag.is_empty(), "Language tag should not be empty");
+ crate::ensure!(
+ tag.bytes().all(|b| (b'a'..=b'z').contains(&b)
+ || (b'A'..=b'Z').contains(&b)
+ || (b'0'..=b'9').contains(&b)),
+ "Language tag should be alpha numeric"
+ );
+ tags.push(Cow::from(tag.to_string()));
+ input = rest;
+ }
+
+ Ok(LanguageRange { subtags: tags })
+}
+
+#[test]
+fn test() {
+ let range = parse("en").unwrap();
+ assert_eq!(&range.subtags, &["en"]);
+
+ let range = parse("en-CA").unwrap();
+ assert_eq!(&range.subtags, &["en", "CA"]);
+
+ let range = parse("zh-Hant-CN-x-private1-private2").unwrap();
+ assert_eq!(
+ &range.subtags,
+ &["zh", "Hant", "CN", "x", "private1", "private2"]
+ );
+}
diff --git a/src/lib.rs b/src/lib.rs
index c8821a41..1cf9825b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -123,6 +123,7 @@ pub mod cache;
pub mod conditional;
pub mod content;
pub mod headers;
+pub mod language;
pub mod mime;
pub mod other;
pub mod proxies;