#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Tag {
pub(super) language: String,
script: Option<String>,
region: Option<String>,
}
impl Tag {
pub(super) fn parse(name: &str) -> Option<Self> {
let base = name.split(['.', '@']).next().unwrap_or_default();
let mut parts = base.split(['_', '-']).filter(|part| !part.is_empty()).map(str::to_ascii_lowercase);
let language = parts.next()?;
let is_language = (2..=3).contains(&language.len()) && language.chars().all(|c| c.is_ascii_lowercase());
if !is_language || language == "c" {
return None;
}
let (mut script, mut region) = (None, None);
for part in parts {
let letters = part.chars().all(|c| c.is_ascii_lowercase());
match part.len() {
4 if letters && script.is_none() => script = Some(part),
2 if letters && region.is_none() => region = Some(part),
3 if part.chars().all(|c| c.is_ascii_digit()) && region.is_none() => region = Some(part),
_ => {}
}
}
Some(Self { language, script, region })
}
pub(super) fn full(&self) -> String {
[Some(&self.language), self.script.as_ref(), self.region.as_ref()]
.into_iter()
.flatten()
.map(String::as_str)
.collect::<Vec<_>>()
.join("-")
}
pub(super) fn script(&self) -> Option<&str> {
if let Some(script) = &self.script {
return Some(script);
}
match (self.language.as_str(), self.region.as_deref()?) {
("zh", "cn" | "sg" | "my") => Some("hans"),
("zh", "tw" | "hk" | "mo") => Some("hant"),
_ => None,
}
}
pub(super) fn region(&self) -> Option<&str> {
self.region.as_deref()
}
}
pub(super) fn language_of(code: &str) -> String {
code.split(['-', '_']).next().unwrap_or(code).to_ascii_lowercase()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_system_names_and_file_codes() {
let tag = Tag::parse("pt_BR.UTF-8").expect("a language");
assert_eq!((tag.language.as_str(), tag.region.as_deref(), tag.full()), ("pt", Some("br"), "pt-br".to_owned()));
let tag = Tag::parse("zh_Hans_CN").expect("a language");
assert_eq!((tag.script(), tag.full()), (Some("hans"), "zh-hans-cn".to_owned()));
assert_eq!(Tag::parse("zh_TW").and_then(|tag| tag.script().map(str::to_owned)).as_deref(), Some("hant"));
assert_eq!(Tag::parse("de_CH.UTF-8@euro").map(|tag| tag.full()).as_deref(), Some("de-ch"));
assert_eq!(Tag::parse("es-419").and_then(|tag| tag.region).as_deref(), Some("419"));
for none in ["C", "C.UTF-8", "POSIX", "", "english", "1a"] {
assert_eq!(Tag::parse(none), None, "{none}");
}
}
#[test]
fn a_code_s_language_is_its_first_part() {
assert_eq!(language_of("pt-BR"), "pt");
assert_eq!(language_of("zh-Hans"), "zh");
assert_eq!(language_of("TR"), "tr");
}
}