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;