egui_charts/
localization.rs1use egui::{Context, Id};
19use std::sync::Arc;
20
21type Lookup = dyn Fn(&str) -> Option<String> + Send + Sync;
22
23#[derive(Clone)]
24struct Translator(Arc<Lookup>);
25
26fn translator_id() -> Id {
27 Id::NULL.with("egui_charts::localization")
28}
29
30pub fn set_translator(
37 ctx: &Context,
38 lookup: impl Fn(&str) -> Option<String> + Send + Sync + 'static,
39) {
40 ctx.data_mut(|data| data.insert_temp(translator_id(), Translator(Arc::new(lookup))));
41 ctx.request_repaint();
42}
43
44pub fn clear_translator(ctx: &Context) {
46 ctx.data_mut(|data| data.remove::<Translator>(translator_id()));
47 ctx.request_repaint();
48}
49
50pub fn text(ctx: &Context, english: impl AsRef<str>) -> String {
56 let english = english.as_ref();
57 lookup(ctx, english).unwrap_or_else(|| english.to_owned())
58}
59
60pub fn format_text(ctx: &Context, english: &str, values: &[(&str, impl AsRef<str>)]) -> String {
66 substitute(lookup(ctx, english).as_deref().unwrap_or(english), values)
67}
68
69pub(crate) fn lookup(ctx: &Context, english: &str) -> Option<String> {
71 let translator = ctx.data(|data| data.get_temp::<Translator>(translator_id()));
72 translator.and_then(|lookup| (lookup.0)(english))
73}
74
75pub(crate) fn substitute(template: &str, values: &[(&str, impl AsRef<str>)]) -> String {
78 let mut output = String::with_capacity(
80 template.len()
81 + values
82 .iter()
83 .map(|(_, value)| value.as_ref().len())
84 .sum::<usize>(),
85 );
86 let mut rest = template;
87 while let Some(open) = rest.find('{') {
88 output.push_str(&rest[..open]);
89 rest = &rest[open..];
90 let Some(close) = rest.find('}') else {
91 break;
92 };
93 let key = &rest[1..close];
94 if let Some((_, value)) = values.iter().find(|(name, _)| *name == key) {
95 output.push_str(value.as_ref());
96 } else {
97 output.push_str(&rest[..=close]);
98 }
99 rest = &rest[close + 1..];
100 }
101 output.push_str(rest);
102 output
103}
104
105#[cfg(test)]
108pub(crate) fn mark_keys(ctx: &Context) {
109 set_translator(ctx, |key| {
110 if !key.chars().any(|c| c.is_ascii_alphabetic()) {
111 return None;
112 }
113 let mut marked = String::from("~");
114 let mut rest = key;
115 while let Some(open) = rest.find('{') {
116 if rest[..open].chars().any(|c| c.is_ascii_digit()) {
117 return None;
118 }
119 rest = &rest[open..];
120 let Some(close) = rest.find('}') else {
121 break;
122 };
123 marked.push_str(&rest[..=close]);
124 marked.push('~');
125 rest = &rest[close + 1..];
126 }
127 if rest.chars().any(|c| c.is_ascii_digit()) {
128 None
129 } else {
130 Some(marked)
131 }
132 });
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn context_lookup_falls_back_switches_and_substitutes_values_once() {
141 let ctx = Context::default();
142 let other = Context::default();
143 assert_eq!(text(&ctx, "Cancel"), "Cancel");
144 let read_context = ctx.clone();
145 set_translator(&ctx, move |english| {
146 let _ = read_context.global_style();
148 match english {
149 "Cancel" => Some("Abbrechen".into()),
150 "Copy price {price_str}" => Some("{price_str} kopieren ({price_str})".into()),
151 "Name {name}, price {price}" => Some("{price}: {name}; {missing}; {".into()),
152 "Hidden" => Some(String::new()),
153 _ => None,
154 }
155 });
156 assert_eq!(text(&ctx, "Cancel"), "Abbrechen");
157 assert_eq!(text(&other, "Cancel"), "Cancel");
158 assert_eq!(text(&ctx, "Unknown"), "Unknown");
159 assert_eq!(text(&ctx, "Hidden"), "");
160 for (english, values, expected) in [
161 (
162 "Copy price {price_str}",
163 vec![("price_str", "12.50".to_owned())],
164 "12.50 kopieren (12.50)",
165 ),
166 (
167 "Name {name}, price {price}",
168 vec![
169 ("name", "{price}".to_owned()),
170 ("price", "12.50".to_owned()),
171 ],
172 "12.50: {price}; {missing}; {",
173 ),
174 (
175 "Missing {name}",
176 vec![("name", "TEST".to_owned())],
177 "Missing TEST",
178 ),
179 ] {
180 assert_eq!(format_text(&ctx, english, &values), expected);
181 }
182 set_translator(&ctx, |english| {
183 (english == "Cancel").then(|| "Annuler".into())
184 });
185 assert_eq!(text(&ctx, "Cancel"), "Annuler");
186 clear_translator(&ctx);
187 assert_eq!(text(&ctx, "Cancel"), "Cancel");
188 }
189}