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 }