use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Locale {
pub language: String,
pub script: Option<String>,
pub region: Option<String>,
pub variants: Vec<String>,
pub extensions: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseError {
Empty,
InvalidSubtag,
}
fn is_alpha(s: &str) -> bool {
s.bytes().all(|b| b.is_ascii_alphabetic())
}
fn is_digit(s: &str) -> bool {
s.bytes().all(|b| b.is_ascii_digit())
}
fn is_alnum(s: &str) -> bool {
s.bytes().all(|b| b.is_ascii_alphanumeric())
}
impl Locale {
pub fn parse(tag: &str) -> Result<Locale, ParseError> {
if tag.is_empty() {
return Err(ParseError::Empty);
}
let mut parts = tag.split(['-', '_']).peekable();
let mut loc = Locale::default();
let lang = parts.next().ok_or(ParseError::Empty)?;
if lang.is_empty() || !((2..=8).contains(&lang.len()) && is_alpha(lang)) {
return Err(ParseError::InvalidSubtag);
}
loc.language = if lang.eq_ignore_ascii_case("und") {
String::new()
} else {
lang.to_ascii_lowercase()
};
if let Some(&s) = parts.peek() {
if s.len() == 4 && is_alpha(s) {
loc.script = Some(titlecase_subtag(s));
parts.next();
}
}
if let Some(&s) = parts.peek() {
if (s.len() == 2 && is_alpha(s)) || (s.len() == 3 && is_digit(s)) {
loc.region = Some(s.to_ascii_uppercase());
parts.next();
}
}
while let Some(&s) = parts.peek() {
let is_variant = ((5..=8).contains(&s.len()) && is_alnum(s))
|| (s.len() == 4 && s.as_bytes()[0].is_ascii_digit() && is_alnum(s));
if !is_variant {
break;
}
loc.variants.push(s.to_ascii_lowercase());
parts.next();
}
let mut current: Option<String> = None;
for p in parts {
let in_private = current.as_deref().is_some_and(|c| c.starts_with('x'));
if p.len() == 1 && p.bytes().next().unwrap().is_ascii_alphanumeric() && !in_private {
if let Some(ext) = current.take() {
if !ext.contains('-') {
return Err(ParseError::InvalidSubtag); }
loc.extensions.push(ext);
}
current = Some(p.to_ascii_lowercase());
} else if let Some(ext) = current.as_mut() {
if p.is_empty() || !p.bytes().all(|b| b.is_ascii_alphanumeric()) {
return Err(ParseError::InvalidSubtag);
}
ext.push('-');
ext.push_str(&p.to_ascii_lowercase());
} else {
return Err(ParseError::InvalidSubtag); }
}
if let Some(ext) = current {
if !ext.contains('-') {
return Err(ParseError::InvalidSubtag);
}
loc.extensions.push(ext);
}
loc.extensions.sort_by(|a, b| {
let key = |s: &String| (s.starts_with("x-"), s.clone());
key(a).cmp(&key(b))
});
Ok(loc)
}
#[must_use]
pub fn maximize(&self) -> Locale {
let lang = if self.language.is_empty() {
"und"
} else {
&self.language
};
let mut candidates: Vec<String> = Vec::new();
if let (Some(s), Some(r)) = (&self.script, &self.region) {
candidates.push(alloc::format!("{lang}-{s}-{r}"));
}
if let Some(r) = &self.region {
candidates.push(alloc::format!("{lang}-{r}"));
}
if let Some(s) = &self.script {
candidates.push(alloc::format!("{lang}-{s}"));
}
candidates.push(String::from(lang));
for key in &candidates {
if let Some(v) = crate::cldr::likely_subtags(key) {
if let Ok(m) = Locale::parse(v) {
return Locale {
language: if self.language.is_empty() {
m.language
} else {
self.language.clone()
},
script: self.script.clone().or(m.script),
region: self.region.clone().or(m.region),
variants: self.variants.clone(),
extensions: self.extensions.clone(),
};
}
}
}
self.clone()
}
#[must_use]
pub fn minimize(&self) -> Locale {
let max = self.maximize();
let lang_only = Locale {
language: self.language.clone(),
..Locale::default()
};
let lang_region = Locale {
language: self.language.clone(),
region: self.region.clone(),
..Locale::default()
};
let lang_script = Locale {
language: self.language.clone(),
script: self.script.clone(),
..Locale::default()
};
for trial in [lang_only, lang_region, lang_script] {
if trial.maximize() == max {
return trial;
}
}
max
}
}
impl core::fmt::Display for Locale {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.language.is_empty() {
f.write_str("und")?;
} else {
f.write_str(&self.language)?;
}
if let Some(s) = &self.script {
write!(f, "-{s}")?;
}
if let Some(r) = &self.region {
write!(f, "-{r}")?;
}
for v in &self.variants {
write!(f, "-{v}")?;
}
for e in &self.extensions {
write!(f, "-{e}")?;
}
Ok(())
}
}
#[must_use]
pub fn negotiate(requested: &[&str], available: &[Locale]) -> Option<usize> {
let maxed: Vec<Locale> = available.iter().map(Locale::maximize).collect();
for req in requested {
let Ok(r) = Locale::parse(req) else { continue };
let r = r.maximize();
for level in 0..3 {
for (i, a) in maxed.iter().enumerate() {
let hit = match level {
0 => a.language == r.language && a.script == r.script && a.region == r.region,
1 => a.language == r.language && a.script == r.script,
_ => a.language == r.language,
};
if hit {
return Some(i);
}
}
}
}
None
}
fn titlecase_subtag(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for (i, b) in s.bytes().enumerate() {
out.push(if i == 0 {
b.to_ascii_uppercase() as char
} else {
b.to_ascii_lowercase() as char
});
}
out
}