use std::{fmt::Display, ops::Deref, str::FromStr};
use std::sync::Mutex;
pub(crate) static SYSTEM_LANGUAGE: Mutex<LanguageTag> = Mutex::new(LanguageTag::US_ENGLISH);
pub fn system_language() -> LanguageTag {
#[expect(
clippy::missing_panics_doc,
reason = "Only `get` and `set` ever lock the mutex, and they can't panic so the mutex can't be poisoned"
)]
let lock = SYSTEM_LANGUAGE.lock().unwrap();
let value = *lock;
drop(lock);
value
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct LanguageTag {
tag: [u8; 5],
}
impl LanguageTag {
pub(crate) const US_ENGLISH: LanguageTag = LanguageTag::new_panics("en-US");
pub(crate) const BR_PORTUGUESE: LanguageTag = LanguageTag::new_panics("pt-BR");
pub fn new(tag: impl AsRef<str>) -> Option<LanguageTag> {
let tag = tag.as_ref();
if tag.len() != 5 {
return None;
}
if !tag
.chars()
.take(2)
.chain(tag.chars().skip(3))
.all(|c| c.is_ascii_alphabetic())
{
return None;
}
if tag.chars().nth(2) != Some('-') {
return None;
}
Some(unsafe { Self::new_unchecked(tag) })
}
#[must_use]
pub(crate) const fn new_panics(tag: &str) -> LanguageTag {
let bytes = tag.as_bytes();
assert!(bytes.len() == 5);
assert!(
bytes[0].is_ascii_alphabetic()
&& bytes[1].is_ascii_alphabetic()
&& bytes[3].is_ascii_alphabetic()
&& bytes[4].is_ascii_alphabetic()
);
assert!(bytes[2] == b'-');
unsafe { LanguageTag::new_unchecked(tag) }
}
const unsafe fn new_unchecked(tag: &str) -> LanguageTag {
let bytes = tag.as_bytes();
let tag = [
bytes[0].to_ascii_lowercase(),
bytes[1].to_ascii_lowercase(),
bytes[2],
bytes[3].to_ascii_uppercase(),
bytes[4].to_ascii_uppercase(),
];
LanguageTag { tag }
}
#[must_use]
pub const fn english() -> Self {
Self::US_ENGLISH
}
#[must_use]
pub const fn portuguese() -> Self {
Self::BR_PORTUGUESE
}
}
impl FromStr for LanguageTag {
type Err = LanguageTagParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
LanguageTag::new(s).ok_or(LanguageTagParseError)
}
}
impl Deref for LanguageTag {
type Target = str;
fn deref(&self) -> &Self::Target {
unsafe { str::from_utf8_unchecked(&self.tag) }
}
}
impl Display for LanguageTag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.deref().fmt(f)
}
}
#[derive(Debug)]
pub struct LanguageTagParseError;
impl Display for LanguageTagParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid language tag")
}
}
impl std::error::Error for LanguageTagParseError {}
#[cfg(test)]
mod tests {
use super::LanguageTag;
#[test]
fn valid_tags_are_allowed() {
assert!(LanguageTag::new("en-US").is_some());
assert!(LanguageTag::new("pt-BR").is_some());
}
#[test]
fn case_conversion() {
assert_eq!(&*LanguageTag::new("en-us").unwrap(), "en-US");
assert_eq!(&*LanguageTag::new("EN-us").unwrap(), "en-US");
assert_eq!(&*LanguageTag::new("EN-US").unwrap(), "en-US");
assert_eq!(&*LanguageTag::new("en-US").unwrap(), "en-US");
}
}