Skip to main content

use_language/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6/// A normalized 2-letter or 3-letter language subtag.
7#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct LanguageCode {
9    value: String,
10}
11
12impl LanguageCode {
13    /// Parses and normalizes a language subtag.
14    #[must_use]
15    pub fn new(input: &str) -> Option<Self> {
16        parse_language_code(input)
17    }
18
19    /// Returns the normalized language subtag.
20    #[must_use]
21    pub fn as_str(&self) -> &str {
22        &self.value
23    }
24
25    /// Consumes the language code and returns the normalized string.
26    #[must_use]
27    pub fn into_string(self) -> String {
28        self.value
29    }
30}
31
32impl AsRef<str> for LanguageCode {
33    fn as_ref(&self) -> &str {
34        self.as_str()
35    }
36}
37
38impl fmt::Display for LanguageCode {
39    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40        formatter.write_str(self.as_str())
41    }
42}
43
44/// Parses a language subtag and normalizes it to lowercase.
45#[must_use]
46pub fn parse_language_code(input: &str) -> Option<LanguageCode> {
47    normalize_language_code(input).map(|value| LanguageCode { value })
48}
49
50/// Returns `true` when the input is a simple 2-letter or 3-letter language subtag.
51#[must_use]
52pub fn is_language_code(input: &str) -> bool {
53    normalize_language_code(input).is_some()
54}
55
56/// Normalizes a simple language subtag to lowercase.
57#[must_use]
58pub fn normalize_language_code(input: &str) -> Option<String> {
59    let trimmed = input.trim();
60    if !matches!(trimmed.len(), 2 | 3) || !trimmed.bytes().all(|byte| byte.is_ascii_alphabetic()) {
61        return None;
62    }
63
64    Some(trimmed.to_ascii_lowercase())
65}
66
67#[cfg(test)]
68mod tests {
69    use super::{LanguageCode, is_language_code, normalize_language_code, parse_language_code};
70
71    #[test]
72    fn accepts_common_language_examples() {
73        for language in ["en", "es", "fr", "de", "zh", "ar", "ja"] {
74            assert!(is_language_code(language));
75            assert_eq!(parse_language_code(language).unwrap().as_str(), language);
76        }
77    }
78
79    #[test]
80    fn normalizes_to_lowercase() {
81        assert_eq!(normalize_language_code("EN"), Some("en".to_string()));
82        assert_eq!(LanguageCode::new(" ZHO ").unwrap().as_str(), "zho");
83    }
84
85    #[test]
86    fn rejects_invalid_language_shapes() {
87        for language in ["", "e", "engb", "en-US", "e1", "en_", "中文"] {
88            assert!(!is_language_code(language));
89            assert!(parse_language_code(language).is_none());
90        }
91    }
92}