git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/l10n.rs (4K)

 1 //! The toolkit's own words, translatable (`docs/rfc-accessibility-locale.md`, phase 5).
 2 //!
 3 //! Every string cce-ui shows of its own — a menu row, a placeholder, a dock's action — is
 4 //! a message in `locale/en-US/cce-ui.ftl`, looked up by id through [`tr`] / [`tr_args`] in
 5 //! the user's language (`cce_core::l10n`: a translation is `<tag>/cce-ui.ftl` in a
 6 //! translation directory). An app's own words are its own domain: a
 7 //! `static CATALOG: Catalog = Catalog::new("<app>", include_str!(…))` of its own.
 8 //! `every_message_the_toolkit_names_is_in_its_english` holds the ids to the file.
 9 
10 pub use cce_core::l10n::*;
11 
12 static CATALOG: Catalog = Catalog::new("cce-ui", include_str!("../locale/en-US/cce-ui.ftl"));
13 
14 /// The toolkit's catalogue (to add a translation to, as a page or a test does).
15 pub fn catalog() -> &'static Catalog {
16     &CATALOG
17 }
18 
19 /// The toolkit's message `id` in the user's language.
20 pub fn tr(id: &str) -> String {
21     CATALOG.get(id)
22 }
23 
24 /// The toolkit's message `id`, its `{ $name }` placeables filled from `args`.
25 pub fn tr_args(id: &str, args: &[(&str, &str)]) -> String {
26     CATALOG.format(id, args)
27 }
28 
29 #[cfg(test)]
30 mod tests {
31     /// Every id the toolkit's source looks up is in its English, and every message there
32     /// is looked up somewhere: a missing one shows as its id, a stray one is a string no
33     /// translator should be asked for.
34     #[test]
35     fn every_message_the_toolkit_names_is_in_its_english() {
36         let english = include_str!("../locale/en-US/cce-ui.ftl");
37         let defined: std::collections::BTreeSet<&str> = english
38             .lines()
39             .filter(|l| !l.starts_with('#') && !l.starts_with(' ') && l.contains(" = "))
40             .map(|l| l.split(" = ").next().unwrap().trim())
41             .collect();
42         let mut used = std::collections::BTreeSet::new();
43         let src = concat!(env!("CARGO_MANIFEST_DIR"), "/src");
44         let mut stack = vec![std::path::PathBuf::from(src)];
45         while let Some(dir) = stack.pop() {
46             for entry in std::fs::read_dir(&dir).unwrap().flatten() {
47                 let path = entry.path();
48                 if path.is_dir() {
49                     stack.push(path);
50                 } else if path.extension().is_some_and(|e| e == "rs") && !path.ends_with("l10n.rs") {
51                     let text = std::fs::read_to_string(&path).unwrap();
52                     for call in ["tr(\"", "tr_args(\""] {
53                         for (i, _) in text.match_indices(call) {
54                             // A call of ours, not the end of another name (`attr("`, `str("`).
55                             if text[..i].chars().next_back().is_some_and(|c| c.is_alphanumeric() || c == '_') {
56                                 continue;
57                             }
58                             let rest = &text[i + call.len()..];
59                             if let Some(end) = rest.find('"') {
60                                 used.insert(rest[..end].to_string());
61                             }
62                         }
63                     }
64                 }
65             }
66         }
67         let missing: Vec<&String> = used.iter().filter(|u| !defined.contains(u.as_str())).collect();
68         assert!(missing.is_empty(), "looked up but not in locale/en-US/cce-ui.ftl: {missing:?}");
69         let unused: Vec<&&str> = defined.iter().filter(|d| !used.contains(**d)).collect();
70         assert!(unused.is_empty(), "in locale/en-US/cce-ui.ftl but looked up nowhere: {unused:?}");
71     }
72 
73     /// A translation changes what a message says and falls back to English for what it
74     /// lacks.
75     #[test]
76     fn a_translation_is_used_where_it_has_the_message() {
77         let c = super::Catalog::new("cce-ui", include_str!("../locale/en-US/cce-ui.ftl"));
78         c.add_translation("de", "menu-copy = Kopieren\nmenu-config-key = Schlüssel: { $key }\n");
79         assert_eq!(c.get("menu-copy"), "Kopieren");
80         assert_eq!(c.format("menu-config-key", &[("key", "x.y")]), "Schlüssel: x.y");
81         assert_eq!(c.get("menu-paste"), "Paste");
82     }
83 }