1use lumen_core::components::AuthoredStrings;
28
29use crate::layout_ir::Attributes;
30
31pub struct TranslatedStrings {
36 pub text: Option<String>,
38 pub placeholder: Option<String>,
40 pub alt: Option<String>,
42 pub tooltip: Option<String>,
44}
45
46pub fn attribute_key(key: &str, name: &str) -> String {
52 format!("{key}.{name}")
53}
54
55impl From<&Attributes> for AuthoredStrings {
61 fn from(attrs: &Attributes) -> Self {
62 Self {
63 key: attrs.translatable.clone(),
64 text: attrs.text.clone(),
65 placeholder: attrs.placeholder.clone(),
66 alt: attrs.alt.clone(),
67 tooltip: attrs.tooltip.as_ref().map(|spec| spec.text.clone()),
68 tooltip_key: attrs
69 .tooltip
70 .as_ref()
71 .and_then(|spec| spec.translatable.clone()),
72 }
73 }
74}
75
76pub fn translate(
87 authored: &AuthoredStrings,
88 lookup: &dyn Fn(&str) -> Option<String>,
89) -> TranslatedStrings {
90 let mut out = TranslatedStrings {
91 text: authored.text.clone(),
92 placeholder: authored.placeholder.clone(),
93 alt: authored.alt.clone(),
94 tooltip: authored.tooltip.clone(),
95 };
96 if let Some(key) = &authored.key {
97 if authored.placeholder.is_some() {
98 out.placeholder = lookup(&attribute_key(key, "placeholder")).or(out.placeholder);
99 }
100 if authored.alt.is_some() {
101 out.alt = lookup(&attribute_key(key, "alt")).or(out.alt);
102 }
103 out.text = lookup(key).or(out.text).or_else(|| {
111 (authored.placeholder.is_none() && authored.alt.is_none()).then(|| key.clone())
112 });
113 }
114 if let Some(key) = &authored.tooltip_key {
119 out.tooltip = lookup(key).or(out.tooltip);
120 }
121 out
122}
123
124#[cfg(test)]
125mod tests {
126 use std::collections::BTreeMap;
127
128 use super::*;
129 use crate::layout_ir::TooltipSpec;
130
131 fn catalogue(entries: &[(&str, &str)]) -> BTreeMap<String, String> {
132 entries
133 .iter()
134 .map(|(k, v)| (k.to_string(), v.to_string()))
135 .collect()
136 }
137
138 fn resolve(attrs: &Attributes, entries: &[(&str, &str)]) -> TranslatedStrings {
139 let map = catalogue(entries);
140 translate(&AuthoredStrings::from(attrs), &|key| map.get(key).cloned())
141 }
142
143 #[test]
144 fn an_untranslatable_element_keeps_every_authored_string() {
145 let attrs = Attributes {
146 text: Some("Search".into()),
147 placeholder: Some("Search catalogue".into()),
148 alt: Some("A logo".into()),
149 ..Attributes::default()
150 };
151 let out = resolve(&attrs, &[("search", "Suche")]);
152 assert_eq!(out.text.as_deref(), Some("Search"));
153 assert_eq!(out.placeholder.as_deref(), Some("Search catalogue"));
154 assert_eq!(out.alt.as_deref(), Some("A logo"));
155 }
156
157 #[test]
158 fn one_key_reaches_the_text_the_placeholder_and_the_alt() {
159 let attrs = Attributes {
160 text: Some("Search".into()),
161 placeholder: Some("Search catalogue".into()),
162 alt: Some("A logo".into()),
163 translatable: Some("search".into()),
164 ..Attributes::default()
165 };
166 let out = resolve(
167 &attrs,
168 &[
169 ("search", "Suche"),
170 ("search.placeholder", "Katalog durchsuchen"),
171 ("search.alt", "Ein Logo"),
172 ],
173 );
174 assert_eq!(out.text.as_deref(), Some("Suche"));
175 assert_eq!(out.placeholder.as_deref(), Some("Katalog durchsuchen"));
176 assert_eq!(out.alt.as_deref(), Some("Ein Logo"));
177 }
178
179 #[test]
180 fn a_string_the_markup_does_not_write_is_not_translated_into_existence() {
181 let attrs = Attributes {
182 text: Some("Search".into()),
183 translatable: Some("search".into()),
184 ..Attributes::default()
185 };
186 let out = resolve(&attrs, &[("search.placeholder", "Katalog durchsuchen")]);
187 assert_eq!(out.placeholder, None);
188 assert_eq!(out.alt, None);
189 }
190
191 #[test]
192 fn a_missing_message_leaves_the_authored_string() {
193 let attrs = Attributes {
194 text: Some("Search".into()),
195 placeholder: Some("Search catalogue".into()),
196 translatable: Some("search".into()),
197 ..Attributes::default()
198 };
199 let out = resolve(&attrs, &[]);
200 assert_eq!(out.text.as_deref(), Some("Search"));
201 assert_eq!(out.placeholder.as_deref(), Some("Search catalogue"));
202 }
203
204 #[test]
205 fn a_key_with_no_text_anywhere_shows_itself() {
206 let attrs = Attributes {
207 translatable: Some("app-title".into()),
208 ..Attributes::default()
209 };
210 assert_eq!(resolve(&attrs, &[]).text.as_deref(), Some("app-title"));
211 }
212
213 #[test]
214 fn a_key_naming_another_string_does_not_stand_in_for_the_text() {
215 let attrs = Attributes {
216 placeholder: Some("Search catalogue".into()),
217 translatable: Some("search".into()),
218 ..Attributes::default()
219 };
220 let out = resolve(&attrs, &[("search.placeholder", "Katalog durchsuchen")]);
221 assert_eq!(out.text, None);
222 assert_eq!(out.placeholder.as_deref(), Some("Katalog durchsuchen"));
223 }
224
225 #[test]
226 fn a_tooltip_carries_its_own_key() {
227 let attrs = Attributes {
228 text: Some("Save".into()),
229 translatable: Some("save".into()),
230 tooltip: Some(TooltipSpec {
231 text: "Save the file".into(),
232 translatable: Some("save-tip".into()),
233 delay_ms: None,
234 offset: None,
235 }),
236 ..Attributes::default()
237 };
238 let out = resolve(
239 &attrs,
240 &[("save", "Speichern"), ("save-tip", "Datei speichern")],
241 );
242 assert_eq!(out.text.as_deref(), Some("Speichern"));
243 assert_eq!(out.tooltip.as_deref(), Some("Datei speichern"));
244 }
245
246 #[test]
247 fn an_unmarked_tooltip_keeps_its_authored_body() {
248 let attrs = Attributes {
249 tooltip: Some(TooltipSpec {
250 text: "Save the file".into(),
251 translatable: None,
252 delay_ms: None,
253 offset: None,
254 }),
255 ..Attributes::default()
256 };
257 let out = resolve(&attrs, &[("save-tip", "Datei speichern")]);
258 assert_eq!(out.tooltip.as_deref(), Some("Save the file"));
259 }
260
261 #[test]
262 fn the_component_carries_every_string_the_markup_wrote() {
263 let attrs = Attributes {
264 text: Some("Save".into()),
265 placeholder: Some("Search catalogue".into()),
266 alt: Some("A logo".into()),
267 translatable: Some("save".into()),
268 tooltip: Some(TooltipSpec {
269 text: "Save the file".into(),
270 translatable: Some("save-tip".into()),
271 delay_ms: None,
272 offset: None,
273 }),
274 ..Attributes::default()
275 };
276 let authored = AuthoredStrings::from(&attrs);
277 assert_eq!(authored.key.as_deref(), Some("save"));
278 assert_eq!(authored.text.as_deref(), Some("Save"));
279 assert_eq!(authored.placeholder.as_deref(), Some("Search catalogue"));
280 assert_eq!(authored.alt.as_deref(), Some("A logo"));
281 assert_eq!(authored.tooltip.as_deref(), Some("Save the file"));
282 assert_eq!(authored.tooltip_key.as_deref(), Some("save-tip"));
283 }
284
285 #[test]
289 fn one_authored_element_resolves_in_every_locale() {
290 let attrs = Attributes {
291 text: Some("Good morning".into()),
292 placeholder: Some("Search".into()),
293 translatable: Some("greet".into()),
294 ..Attributes::default()
295 };
296 let authored = AuthoredStrings::from(&attrs);
297 let german = catalogue(&[
298 ("greet", "Guten Morgen"),
299 ("greet.placeholder", "Katalog durchsuchen"),
300 ]);
301 let out = translate(&authored, &|key| german.get(key).cloned());
302 assert_eq!(out.text.as_deref(), Some("Guten Morgen"));
303 assert_eq!(out.placeholder.as_deref(), Some("Katalog durchsuchen"));
304 let out = translate(&authored, &|_| None);
307 assert_eq!(out.text.as_deref(), Some("Good morning"));
308 assert_eq!(out.placeholder.as_deref(), Some("Search"));
309 }
310}