1use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(try_from = "LocalizedWire", into = "LocalizedWire")]
20pub enum Localized {
21 Plain(String),
23 Translated(BTreeMap<String, String>),
25}
26
27impl Localized {
28 pub fn languages(&self) -> impl Iterator<Item = &str> {
31 let map = match self {
32 Localized::Plain(_) => None,
33 Localized::Translated(map) => Some(map.keys().map(String::as_str)),
34 };
35 map.into_iter().flatten()
36 }
37}
38
39impl From<&str> for Localized {
40 fn from(text: &str) -> Self {
41 Localized::Plain(text.to_owned())
42 }
43}
44
45#[derive(Serialize, Deserialize)]
48#[serde(untagged)]
49pub(crate) enum LocalizedWire {
50 Plain(String),
51 Translated(BTreeMap<String, String>),
52}
53
54impl TryFrom<LocalizedWire> for Localized {
55 type Error = String;
56
57 fn try_from(raw: LocalizedWire) -> Result<Self, Self::Error> {
58 match raw {
59 LocalizedWire::Plain(text) => Ok(Localized::Plain(text)),
60 LocalizedWire::Translated(map) => {
61 if map.is_empty() {
62 return Err("a translated text names no language".to_owned());
63 }
64 if let Some(bad) = map.keys().find(|tag| !is_language_tag(tag)) {
65 return Err(format!("`{bad}` is not a language tag"));
66 }
67 Ok(Localized::Translated(map))
68 }
69 }
70 }
71}
72
73impl From<Localized> for LocalizedWire {
74 fn from(text: Localized) -> Self {
75 match text {
76 Localized::Plain(text) => LocalizedWire::Plain(text),
77 Localized::Translated(map) => LocalizedWire::Translated(map),
78 }
79 }
80}
81
82pub fn is_language_tag(tag: &str) -> bool {
87 let mut parts = tag.split('-');
88 let language = parts.next().unwrap_or_default();
89 (2..=3).contains(&language.len())
90 && language.bytes().all(|b| b.is_ascii_alphabetic())
91 && parts.all(|part| {
92 (1..=8).contains(&part.len()) && part.bytes().all(|b| b.is_ascii_alphanumeric())
93 })
94}