use alloc::{string::String, vec::Vec};
use core::{
fmt::{self, Display, Formatter},
num::NonZeroU8,
str::FromStr,
};
use crate::{Error, Result};
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct Language {
lang: [NonZeroU8; 2],
country: Option<[NonZeroU8; 2]>,
}
impl Default for Language {
fn default() -> Self {
Self::from_str("en/US")
.expect("this is an internal bug (failed to parse en/US)")
}
}
impl FromStr for Language {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let lang = s.split_terminator('.').next().unwrap_or_default();
if lang.is_empty() {
return Err(Error::empty_record());
}
let mut parts = lang.split(SEPARATORS);
let lang = parts
.next()
.ok_or_else(|| Error::with_invalid_data("No lang"))?
.as_bytes();
let country = parts.next().unwrap_or("\0\0").as_bytes();
if parts.next().is_some() {
return Err(Error::with_invalid_data("Invalid locale"));
} else if lang.len() != 2 {
return Err(Error::with_invalid_data("Invalid length lang code"));
} else if country.len() != 2 {
return Err(Error::with_invalid_data(
"Invalid length country code",
));
}
let Some(lang) = NonZeroU8::new(lang[0]).zip(NonZeroU8::new(lang[1]))
else {
return Err(Error::with_invalid_data("Lang code contains NUL"));
};
let lang = [lang.0, lang.1];
if (country[0] == 0 || country[1] == 0)
&& (country[0] != 0 || country[1] != 0)
{
return Err(Error::with_invalid_data("Country code contains NUL"));
}
let country = NonZeroU8::new(country[0])
.zip(NonZeroU8::new(country[1]))
.map(|country| [country.0, country.1]);
if !(lang[0].get().is_ascii_lowercase()
&& lang[1].get().is_ascii_lowercase())
{
return Err(Error::with_invalid_data(
"Lang code not ascii lowercase",
));
}
if let Some(ref country) = country {
if !(country[0].get().is_ascii_uppercase()
&& country[1].get().is_ascii_uppercase())
{
return Err(Error::with_invalid_data(
"Country code not ascii uppercase",
));
}
}
Ok(Self { lang, country })
}
}
impl Display for Language {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&String::from_utf8_lossy(&[
self.lang[0].get(),
self.lang[1].get(),
]))?;
let Some(country) = self.country.as_ref() else {
return Ok(());
};
f.write_str("/")?;
f.write_str(&String::from_utf8_lossy(&[
country[0].get(),
country[1].get(),
]))
}
}
impl PartialEq<Language> for str {
fn eq(&self, lang: &Language) -> bool {
lang_str_eq(lang, self)
}
}
impl PartialEq<Language> for &str {
fn eq(&self, lang: &Language) -> bool {
lang_str_eq(lang, self)
}
}
impl PartialEq<Language> for String {
fn eq(&self, lang: &Language) -> bool {
lang_str_eq(lang, self)
}
}
impl PartialEq<String> for Language {
fn eq(&self, string: &String) -> bool {
lang_str_eq(self, string)
}
}
impl PartialEq<str> for Language {
fn eq(&self, string: &str) -> bool {
lang_str_eq(self, string)
}
}
impl PartialEq<&str> for Language {
fn eq(&self, string: &&str) -> bool {
lang_str_eq(self, string)
}
}
#[derive(Debug, Clone, Default)]
pub struct LanguagePreferences {
pub(crate) fallbacks: Vec<Language>,
pub(crate) collation: Option<Language>,
pub(crate) char_classes: Option<Language>,
pub(crate) monetary: Option<Language>,
pub(crate) messages: Option<Language>,
pub(crate) numeric: Option<Language>,
pub(crate) time: Option<Language>,
}
impl Display for LanguagePreferences {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let langs: [(&str, Vec<Language>); 6] = [
("Collation", self.collation_langs().collect()),
("CharClasses", self.char_class_langs().collect()),
("Monetary", self.monetary_langs().collect()),
("Messages", self.message_langs().collect()),
("Numeric", self.numeric_langs().collect()),
("Time", self.time_langs().collect()),
];
for (i, (name, langs)) in langs.iter().enumerate() {
if i != 0 {
f.write_str(",")?;
}
write!(f, "{name}=")?;
for (j, lang) in langs.iter().enumerate() {
if j != 0 {
f.write_str(":")?;
}
write!(f, "{lang}")?;
}
}
Ok(())
}
}
impl LanguagePreferences {
fn chain_fallbacks<'a>(
&'a self,
l: &Option<Language>,
) -> impl Iterator<Item = Language> + 'a {
let lang_without_country = if let Some(ref lang) = l {
lang.country.is_some().then_some(Language {
lang: lang.lang,
country: None,
})
} else {
None
};
(*l).into_iter()
.chain(lang_without_country)
.chain(self.fallbacks.iter().cloned())
}
pub fn collation_langs(&self) -> impl Iterator<Item = Language> + '_ {
self.chain_fallbacks(&self.collation)
}
pub fn char_class_langs(&self) -> impl Iterator<Item = Language> + '_ {
self.chain_fallbacks(&self.char_classes)
}
pub fn monetary_langs(&self) -> impl Iterator<Item = Language> + '_ {
self.chain_fallbacks(&self.monetary)
}
pub fn message_langs(&self) -> impl Iterator<Item = Language> + '_ {
self.chain_fallbacks(&self.messages)
}
pub fn numeric_langs(&self) -> impl Iterator<Item = Language> + '_ {
self.chain_fallbacks(&self.numeric)
}
pub fn time_langs(&self) -> impl Iterator<Item = Language> + '_ {
self.chain_fallbacks(&self.time)
}
pub(crate) fn add_stripped_fallbacks(mut self) -> Self {
let mut no_country_langs = Vec::new();
for lang in self.fallbacks.iter() {
if lang.country.is_some() {
no_country_langs.push(Language {
lang: lang.lang,
country: None,
});
} else {
let Some(i) = no_country_langs.iter().position(|x| x == lang)
else {
continue;
};
no_country_langs.remove(i);
}
}
self.fallbacks.extend(no_country_langs);
self
}
}
fn lang_str_eq(language: &Language, string: &str) -> bool {
let mut iter = string.split(SEPARATORS);
let string_lang = iter.next().map(|s| s.as_bytes());
let string_country = iter.next().map(|s| s.as_bytes());
let end = iter.next();
let lang = [language.lang[0].get(), language.lang[1].get()];
let Some(country) = language.country.as_ref() else {
return end.is_none()
&& string_lang == Some(&lang)
&& string_country.is_none();
};
let country = [country[0].get(), country[1].get()];
end.is_none()
&& string_lang == Some(&lang)
&& string_country == Some(&country)
}
const SEPARATORS: &[char] = &['_', '-', '/'];