sofie_values/web/
locale.rs1use sofie_core::{Error, Result};
2
3#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5pub struct Locale {
6 tag: String,
7}
8
9impl Locale {
10 pub fn parse(text: &str) -> Result<Self> {
11 let mut parts = text.split(['-', '_']);
12 let language = parts
13 .next()
14 .filter(|language| is_letters(language, 2..=3))
15 .ok_or_else(|| invalid_locale("must start with a language such as en"))?;
16 let mut tag = language.to_ascii_lowercase();
17 let mut rest = parts.peekable();
18 if let Some(script) = rest.next_if(|script| is_letters(script, 4..=4)) {
19 tag.push('-');
20 tag.push_str(&script[..1].to_ascii_uppercase());
21 tag.push_str(&script[1..].to_ascii_lowercase());
22 }
23 if let Some(region) = rest.next() {
24 if !is_letters(region, 2..=2) && !is_digits(region, 3) {
25 return Err(invalid_locale("must have a region such as US or 419"));
26 }
27 tag.push('-');
28 tag.push_str(®ion.to_ascii_uppercase());
29 }
30 if rest.next().is_some() {
31 return Err(invalid_locale(
32 "must be a language, script and region at most",
33 ));
34 }
35 Ok(Self { tag })
36 }
37
38 #[must_use]
39 pub fn as_str(&self) -> &str {
40 &self.tag
41 }
42
43 #[must_use]
44 pub fn language(&self) -> &str {
45 self.tag.split('-').next().unwrap_or_default()
46 }
47}
48
49fn is_letters(part: &str, lengths: std::ops::RangeInclusive<usize>) -> bool {
50 lengths.contains(&part.len()) && part.bytes().all(|byte| byte.is_ascii_alphabetic())
51}
52
53fn is_digits(part: &str, length: usize) -> bool {
54 part.len() == length && part.bytes().all(|byte| byte.is_ascii_digit())
55}
56
57fn invalid_locale(message: &str) -> Error {
58 Error::invalid("locale", message)
59}
60
61sofie_core::text_value!(Locale, tag);
62
63crate::present::present_text!(
64 Locale,
65 crate::Input::typed(&[("type", "text"), ("maxlength", "35")]),
66 Exact
67);
68
69#[cfg(test)]
70mod tests {
71 use sofie_core::SqlValue;
72
73 use super::*;
74 use crate::testing::{assert_refused, round_trip};
75
76 #[test]
77 fn writes_each_part_in_its_usual_case() {
78 for (text, tag) in [
79 ("tr", "tr"),
80 ("en-us", "en-US"),
81 ("EN_GB", "en-GB"),
82 ("zh-hant-tw", "zh-Hant-TW"),
83 ("sr-Latn", "sr-Latn"),
84 ("es-419", "es-419"),
85 ("fil-PH", "fil-PH"),
86 ] {
87 assert_eq!(Locale::parse(text).unwrap().as_str(), tag, "{text}");
88 }
89 assert_eq!(Locale::parse("zh-Hant-TW").unwrap().language(), "zh");
90 }
91
92 #[test]
93 fn refuses_what_is_not_a_tag() {
94 assert_refused(
95 Locale::parse,
96 &[
97 "",
98 "e",
99 "engl",
100 "en-",
101 "en-USA",
102 "en-U1",
103 "en-US-x",
104 "en-Latn-US-1",
105 "1en",
106 "en US",
107 ],
108 );
109 }
110
111 #[test]
112 fn round_trips_through_a_text_column_and_json() {
113 let locale = Locale::parse("en-US").unwrap();
114 assert_eq!(round_trip(&locale), vec![SqlValue::Text("en-US".into())]);
115 let read: Locale = serde_json::from_value(serde_json::json!("en_us")).unwrap();
116 assert_eq!(read, locale);
117 assert_eq!(
118 serde_json::to_value(&read).unwrap(),
119 serde_json::json!("en-US")
120 );
121 }
122}