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