use crate::model::RunProperties;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Locale {
#[default]
English,
CommaDecimal,
PointDecimal,
Unrecognised,
}
impl Locale {
pub fn decimal_separator(self) -> char {
match self {
Locale::CommaDecimal => ',',
Locale::English | Locale::PointDecimal | Locale::Unrecognised => '.',
}
}
pub fn spells_numbers(self) -> bool {
match self {
Locale::English | Locale::Unrecognised => true,
Locale::CommaDecimal | Locale::PointDecimal => false,
}
}
pub fn from_cascade<'a>(layers: impl IntoIterator<Item = &'a RunProperties>) -> Self {
layers
.into_iter()
.find_map(|rp| {
rp.lang
.as_ref()
.and_then(|l| l.val.as_deref())
.filter(|tag| !tag.is_empty())
})
.map_or(Locale::English, Locale::from_tag)
}
pub fn from_tag(tag: &str) -> Self {
let primary = tag.split('-').next().unwrap_or("").to_ascii_lowercase();
match primary.as_str() {
"en" => Locale::English,
"af" | "sq" | "hy" | "az" | "be" | "bs" | "bg" | "ca" | "hr" | "cs" | "da" | "nl"
| "et" | "eu" | "fi" | "fo" | "fr" | "gl" | "ka" | "de" | "el" | "hu" | "is" | "id"
| "it" | "kk" | "lb" | "lv" | "lt" | "mk" | "mn" | "nb" | "nn" | "no" | "pl" | "pt"
| "ro" | "ru" | "sr" | "sk" | "sl" | "es" | "sv" | "tr" | "uk" | "vi" => {
Locale::CommaDecimal
}
"am" | "ar" | "bn" | "cy" | "fa" | "fil" | "ga" | "gu" | "he" | "hi" | "iw" | "ja"
| "km" | "kn" | "ko" | "lo" | "ml" | "mr" | "ms" | "mt" | "my" | "ne" | "pa" | "si"
| "sw" | "ta" | "te" | "th" | "tl" | "ur" | "zh" => Locale::PointDecimal,
_ => {
log::warn!(
"w:lang: unhandled language tag {tag:?} (§17.3.2.20) — \
assuming a decimal point and English number words, which \
is what every document got before locale was read"
);
Locale::Unrecognised
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Lang;
fn rp(tag: Option<&str>) -> RunProperties {
RunProperties {
lang: tag.map(|t| Lang {
val: Some(t.to_string()),
east_asia: None,
bidi: None,
}),
..Default::default()
}
}
#[test]
fn english_regions_are_all_english() {
for tag in ["en", "en-US", "en-GB", "en-AU", "EN-us"] {
assert_eq!(Locale::from_tag(tag), Locale::English, "{tag}");
}
}
#[test]
fn the_corpus_languages_classify_as_they_should() {
for tag in ["de-AT", "de-DE", "pl-PL", "it-IT", "ca-ES", "fr-FR"] {
assert_eq!(Locale::from_tag(tag), Locale::CommaDecimal, "{tag}");
}
for tag in ["en-US", "en-GB"] {
assert_eq!(Locale::from_tag(tag), Locale::English, "{tag}");
}
}
#[test]
fn point_decimal_languages_are_not_english() {
for tag in ["ja-JP", "zh-CN", "ko-KR", "he-IL", "th-TH", "hi-IN"] {
assert_eq!(Locale::from_tag(tag), Locale::PointDecimal, "{tag}");
}
}
#[test]
fn an_unknown_region_still_resolves_by_its_primary_subtag() {
assert_eq!(Locale::from_tag("de-LI"), Locale::CommaDecimal);
assert_eq!(Locale::from_tag("en-ZZ"), Locale::English);
}
#[test]
fn an_unknown_primary_subtag_is_unrecognised() {
for tag in ["zz-ZZ", "x-klingon", "qqq"] {
assert_eq!(Locale::from_tag(tag), Locale::Unrecognised, "{tag}");
}
}
#[test]
fn an_unrecognised_tag_answers_exactly_as_english_does() {
assert_eq!(
Locale::Unrecognised.decimal_separator(),
Locale::English.decimal_separator(),
);
assert_eq!(
Locale::Unrecognised.spells_numbers(),
Locale::English.spells_numbers(),
);
}
#[test]
fn only_comma_languages_write_a_comma() {
assert_eq!(Locale::CommaDecimal.decimal_separator(), ',');
assert_eq!(Locale::English.decimal_separator(), '.');
assert_eq!(Locale::PointDecimal.decimal_separator(), '.');
}
#[test]
fn only_english_spells_numbers() {
assert!(Locale::English.spells_numbers());
assert!(!Locale::CommaDecimal.spells_numbers());
assert!(!Locale::PointDecimal.spells_numbers());
}
#[test]
fn the_cascade_takes_the_first_layer_that_sets_a_tag() {
let layers = [rp(None), rp(Some("de-DE")), rp(Some("en-US"))];
assert_eq!(Locale::from_cascade(layers.iter()), Locale::CommaDecimal);
}
#[test]
fn an_empty_tag_falls_through_to_the_next_layer() {
let layers = [rp(Some("")), rp(Some("de-DE"))];
assert_eq!(Locale::from_cascade(layers.iter()), Locale::CommaDecimal);
}
#[test]
fn a_cascade_with_no_tag_at_all_is_english() {
assert_eq!(Locale::from_cascade([rp(None)].iter()), Locale::English);
assert_eq!(Locale::from_cascade(std::iter::empty()), Locale::English);
}
}