Skip to main content

ravenrs_app_a11y/
words.rs

1//! What screen readers hear from the framework itself, in each language.
2//!
3//! Every text has a key under `a11y.`, as `a11y.saved`. The words come in English and in
4//! Brazilian Portuguese; [`AccessWords::from_lookup`] takes them from anywhere else that answers
5//! by key, which is where the words of the whole application framework plug in.
6
7/// A text of [`AccessWords`] by its key, for whoever keeps the words of an application.
8pub const KEYS: &[&str] =
9    &["a11y.saved", "a11y.not_saved", "a11y.deleted_one", "a11y.deleted_many", "a11y.showing"];
10
11/// The words of the announcements. `{0}` is what each one is about.
12#[derive(Clone, Debug, PartialEq, Eq)]
13#[non_exhaustive]
14pub struct AccessWords {
15    /// A record was kept: `{0}` is what names it.
16    pub saved: String,
17    /// A record could not be kept: `{0}` is why.
18    pub not_saved: String,
19    /// One record was deleted.
20    pub deleted_one: String,
21    /// More than one was: `{0}` is how many.
22    pub deleted_many: String,
23    /// A screen opened: `{0}` is its title.
24    pub showing: String,
25}
26
27impl Default for AccessWords {
28    fn default() -> Self {
29        Self::english()
30    }
31}
32
33impl AccessWords {
34    /// In English.
35    pub fn english() -> Self {
36        Self {
37            saved: "Saved: {0}".into(),
38            not_saved: "Not saved. {0}".into(),
39            deleted_one: "1 record deleted".into(),
40            deleted_many: "{0} records deleted".into(),
41            showing: "Showing {0}".into(),
42        }
43    }
44
45    /// In Brazilian Portuguese.
46    pub fn pt_br() -> Self {
47        Self {
48            saved: "Salvo: {0}".into(),
49            not_saved: "Não foi salvo. {0}".into(),
50            deleted_one: "1 registro apagado".into(),
51            deleted_many: "{0} registros apagados".into(),
52            showing: "Mostrando {0}".into(),
53        }
54    }
55
56    /// In the language of this code, as `en` or `pt-BR`; English for any other.
57    pub fn for_language(code: &str) -> Self {
58        let code = code.trim().replace('_', "-").to_ascii_lowercase();
59        if code == "pt" || code.starts_with("pt-") { Self::pt_br() } else { Self::english() }
60    }
61
62    /// Takes each text from `lookup` by its key in [`KEYS`], keeping these words for a key it
63    /// does not know. This is how the words of an application, kept anywhere, reach here.
64    pub fn from_lookup(self, lookup: impl Fn(&str) -> Option<String>) -> Self {
65        let take = |key: &str, kept: String| lookup(key).unwrap_or(kept);
66        Self {
67            saved: take("a11y.saved", self.saved),
68            not_saved: take("a11y.not_saved", self.not_saved),
69            deleted_one: take("a11y.deleted_one", self.deleted_one),
70            deleted_many: take("a11y.deleted_many", self.deleted_many),
71            showing: take("a11y.showing", self.showing),
72        }
73    }
74
75    /// The text of this key, for the ones in [`KEYS`].
76    pub fn get(&self, key: &str) -> Option<&str> {
77        Some(match key {
78            "a11y.saved" => &self.saved,
79            "a11y.not_saved" => &self.not_saved,
80            "a11y.deleted_one" => &self.deleted_one,
81            "a11y.deleted_many" => &self.deleted_many,
82            "a11y.showing" => &self.showing,
83            _ => return None,
84        })
85    }
86
87    /// That a record named `name` was kept.
88    pub fn saved(&self, name: &str) -> String {
89        self.saved.replace("{0}", name)
90    }
91
92    /// That a record was not kept, and why.
93    pub fn not_saved(&self, why: &str) -> String {
94        self.not_saved.replace("{0}", why).trim().to_owned()
95    }
96
97    /// That `count` records were deleted.
98    pub fn deleted(&self, count: usize) -> String {
99        if count == 1 {
100            self.deleted_one.clone()
101        } else {
102            self.deleted_many.replace("{0}", &count.to_string())
103        }
104    }
105
106    /// That the screen with this title opened.
107    pub fn showing(&self, title: &str) -> String {
108        self.showing.replace("{0}", title)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn every_key_has_words_in_each_language() {
118        for words in [AccessWords::english(), AccessWords::pt_br()] {
119            for key in KEYS {
120                assert!(words.get(key).is_some_and(|text| !text.is_empty()), "{key}");
121            }
122        }
123        assert_eq!(AccessWords::for_language("pt_BR"), AccessWords::pt_br());
124        assert_eq!(AccessWords::for_language("de"), AccessWords::english());
125    }
126
127    #[test]
128    fn words_from_elsewhere_replace_by_key() {
129        let words =
130            AccessWords::pt_br().from_lookup(|key| (key == "a11y.saved").then(|| "Gravado: {0}".to_owned()));
131        assert_eq!(words.saved("Conta"), "Gravado: Conta");
132        assert_eq!(words.deleted(3), "3 registros apagados");
133        assert_eq!(words.deleted(1), "1 registro apagado");
134    }
135}