vizia_core/localization/mod.rs
1//! Provides types for adapting an application to a particular language or regional peculiarities.
2//!
3//! # Language Translation
4//!
5//! Vizia provides the ability to dynamically translate text using [fluent](https://projectfluent.org/). Fluent provides a syntax for describing how text should be translated into different languages.
6//! The [fluent syntax guide](https://projectfluent.org/fluent/guide/) contains more information on the fluent syntax.
7//!
8//! ## Adding Fluent Files
9//! Before text can be translated, one or more fluent files must be added to an application with the corresponding locale:
10//! ```ignore
11//! # use vizia_core::prelude::*;
12//! # let mut cx = &mut Context::default();
13//! // Adds a fluent file to the application resource manager.
14//! // This file is then used for translations to the corresponding locale.
15//! cx.add_translation(
16//! "en-US".parse().unwrap(),
17//! include_str!("resources/en-US/translation.ftl").to_owned(),
18//! );
19//!
20//! ```
21//!
22//! ## Setting the Locale
23//! The application will use the system locale by default, however an environment event can be used to set a custom locale.
24//! If no fluent file can be found for the specified locale, then a fallback fluent file is used from the list of available files.
25//! ```ignore
26//! # use vizia_core::prelude::*;
27//! # let mut cx = &mut Context::default();
28//! // Sets the current locale to en-US, regardless of the system locale
29//! cx.emit(EnvironmentEvent::SetLocale("en-US".parse().unwrap()));
30//! ```
31//!
32//! ## Basic Translation
33//! Use the [`Localized`] type to specify a translation key to be used with fluent files. The key is then used to look up the corresponding translation.
34//! ```ignore
35//! # use vizia_core::prelude::*;
36//! # let mut cx = &mut Context::default();
37//! Label::new(cx, Localized::new("hello-world"));
38//! ```
39//! The markup in the loaded fluent (.ftl) files defines the translations for a particular key. The translation used depends on the application locale, which can be queried from [`Environment`].
40//! ```ftl
41//! // en-US/hello.ftl
42//! hello-world = Hello, world!
43//! ```
44//! ```ftl
45//! // fr/hello.ftl
46//! hello-world = Bonjour, monde!
47//! ```
48//!
49//! ## Variables
50//! Data from the application can be inserted into translated text using a [placeable](https://projectfluent.org/fluent/guide/variables.html).
51//! The variable is enclosed in curly braces and prefixed with a `$` symbol.
52//! ```ftl
53//! welcome = Welcome, { $user }!
54//! ```
55//! The [`Localized`] type provides two methods for referencing a variable. The `arg_const(...)` method allows a keyed value to be inserted into the translation.
56//! ```ignore
57//! # use vizia_core::prelude::*;
58//! # let mut cx = &mut Context::default();
59//! Label::new(cx, Localized::new("welcome").arg_const("user", "Jane"));
60//! ```
61//! While the `arg(...)` method allows a keyed lens to be used, binding the fluent variable to a piece of application data, and updating when that data changes.
62//! ```ignore
63//! # use vizia_core::prelude::*;
64//! # let mut cx = &mut Context::default();
65//! # #[derive(Lens)]
66//! # pub struct AppData {
67//! # user: String,
68//! # }
69//! Label::new(cx, Localized::new("welcome").arg("user", AppData::user));
70//! ```
71use crate::context::LocalizationContext;
72use crate::prelude::*;
73use fluent_bundle::FluentArgs;
74use fluent_bundle::FluentValue;
75use hashbrown::HashMap;
76use std::rc::Rc;
77use std::sync::Arc;
78
79pub(crate) trait FluentStore {
80 fn get_val(&self, cx: &LocalizationContext) -> FluentValue<'static>;
81 fn make_clone(&self) -> Box<dyn FluentStore>;
82 fn bind(&self, cx: &mut Context, closure: Box<dyn Fn(&mut Context)>);
83}
84
85#[derive(Copy, Clone, Debug)]
86pub(crate) struct LensState<L> {
87 lens: L,
88}
89
90#[derive(Copy, Clone, Debug)]
91pub(crate) struct ValState<T> {
92 val: T,
93}
94
95impl<L> FluentStore for LensState<L>
96where
97 L: Lens<Target: Into<FluentValue<'static>> + Data>,
98{
99 fn get_val(&self, cx: &LocalizationContext) -> FluentValue<'static> {
100 self.lens
101 .view(
102 cx.data()
103 .expect("Failed to get data from context. Has it been built into the tree?"),
104 )
105 .unwrap()
106 .into_owned()
107 .into()
108 }
109
110 fn make_clone(&self) -> Box<dyn FluentStore> {
111 Box::new(*self)
112 }
113
114 fn bind(&self, cx: &mut Context, closure: Box<dyn Fn(&mut Context)>) {
115 Binding::new(cx, self.lens, move |cx, _| closure(cx));
116 }
117}
118
119impl<T> FluentStore for ValState<T>
120where
121 T: 'static + Clone + Into<FluentValue<'static>>,
122{
123 fn get_val(&self, _cx: &LocalizationContext) -> FluentValue<'static> {
124 self.val.clone().into()
125 }
126
127 fn make_clone(&self) -> Box<dyn FluentStore> {
128 Box::new(self.clone())
129 }
130
131 fn bind(&self, cx: &mut Context, closure: Box<dyn Fn(&mut Context)>) {
132 closure(cx);
133 }
134}
135
136/// A type which formats a localized message with any number of named arguments.
137pub struct Localized {
138 key: String,
139 args: HashMap<String, Box<dyn FluentStore>>,
140 map: Rc<dyn Fn(&str) -> String + 'static>,
141}
142
143impl PartialEq for Localized {
144 fn eq(&self, other: &Self) -> bool {
145 self.key == other.key
146 }
147}
148
149impl Clone for Localized {
150 fn clone(&self) -> Self {
151 Self {
152 key: self.key.clone(),
153 args: self.args.iter().map(|(k, v)| (k.clone(), v.make_clone())).collect(),
154 map: self.map.clone(),
155 }
156 }
157}
158
159impl Localized {
160 fn get_args(&self, cx: &LocalizationContext) -> FluentArgs {
161 let mut res = FluentArgs::new();
162 for (name, arg) in &self.args {
163 res.set(name.to_owned(), arg.get_val(cx));
164 }
165 res
166 }
167
168 /// Creates a new Localized type with a given key.
169 ///
170 /// The given key is used to retrieve a translation from a fluent bundle resource.
171 ///
172 /// # Example
173 /// ```no_run
174 /// # use vizia_core::prelude::*;
175 /// # use vizia_derive::*;
176 /// # use vizia_winit::application::Application;
177 /// Application::new(|cx|{
178 /// Label::new(cx, Localized::new("key"));
179 /// })
180 /// .run();
181 pub fn new(key: &str) -> Self {
182 Self { key: key.to_owned(), args: HashMap::new(), map: Rc::new(|s| s.to_string()) }
183 }
184
185 /// Sets a mapping function to apply to the translated text.
186 pub fn map(mut self, mapping: impl Fn(&str) -> String + 'static) -> Self {
187 self.map = Rc::new(mapping);
188
189 self
190 }
191
192 /// Add a variable argument binding to the Localized type.
193 ///
194 /// Takes a key name and a lens to the value for the argument.
195 ///
196 /// # Example
197 /// ```no_run
198 /// # use vizia_core::prelude::*;
199 /// # use vizia_derive::*;
200 /// # use vizia_winit::application::Application;
201 /// # #[derive(Lens)]
202 /// # struct AppData {
203 /// # value: i32,
204 /// # }
205 /// # impl Model for AppData {}
206 /// Application::new(|cx|{
207 ///
208 /// AppData {
209 /// value: 5,
210 /// }.build(cx);
211 ///
212 /// Label::new(cx, Localized::new("key").arg("value", AppData::value));
213 /// })
214 /// .run();
215 pub fn arg<L>(mut self, key: &str, lens: L) -> Self
216 where
217 L: Lens<Target: Into<FluentValue<'static>> + Data>,
218 {
219 self.args.insert(key.to_owned(), Box::new(LensState { lens }));
220 self
221 }
222
223 /// Add a constant argument to the Localized type.
224 ///
225 /// Takes a key name and a value for the argument.
226 ///
227 /// # Example
228 /// ```no_run
229 /// # use vizia_core::prelude::*;
230 /// # use vizia_winit::application::Application;
231 /// Application::new(|cx|{
232 ///
233 /// Label::new(cx, Localized::new("key").arg_const("value", 32));
234 /// })
235 /// .run();
236 pub fn arg_const<T: Into<FluentValue<'static>> + Data>(mut self, key: &str, val: T) -> Self {
237 self.args.insert(key.to_owned(), Box::new(ValState { val }));
238 self
239 }
240}
241
242impl ResGet<String> for Localized {
243 fn get_ref<'a>(&'a self, cx: &'a impl DataContext) -> Option<LensValue<'a, String>> {
244 Some(LensValue::Owned(self.get(cx)))
245 }
246
247 fn get(&self, cx: &impl DataContext) -> String {
248 let cx = cx.localization_context().expect("Failed to get context");
249 let locale = &cx.environment().locale;
250 let bundle = cx.resource_manager.current_translation(locale);
251 let message = if let Some(msg) = bundle.get_message(&self.key) {
252 msg
253 } else {
254 return (self.map)(&self.key);
255 };
256
257 let value = if let Some(value) = message.value() {
258 value
259 } else {
260 return (self.map)(&self.key);
261 };
262
263 let mut err = vec![];
264 let args = self.get_args(&cx);
265 let res = bundle.format_pattern(value, Some(&args), &mut err);
266
267 if err.is_empty() {
268 (self.map)(&res)
269 } else {
270 format!("{} {{ERROR: {:?}}}", res, err)
271 }
272 }
273}
274
275impl Res<String> for Localized {
276 fn set_or_bind<F>(self, cx: &mut Context, entity: Entity, closure: F)
277 where
278 F: 'static + Fn(&mut Context, Localized),
279 {
280 let self2 = self.clone();
281 let closure = Arc::new(closure);
282 Binding::new(cx, Environment::locale, move |cx, _| {
283 cx.with_current(entity, |cx| {
284 let lenses = self2.args.values().map(|x| x.make_clone()).collect::<Vec<_>>();
285 let self3 = self2.clone();
286 let closure = closure.clone();
287 bind_recursive(cx, &lenses, move |cx| {
288 closure(cx, self3.clone());
289 });
290 });
291 });
292 }
293}
294
295fn bind_recursive<F>(cx: &mut Context, lenses: &[Box<dyn FluentStore>], closure: F)
296where
297 F: 'static + Clone + Fn(&mut Context),
298{
299 if let Some((lens, rest)) = lenses.split_last() {
300 let rest = rest.iter().map(|x| x.make_clone()).collect::<Vec<_>>();
301 lens.bind(
302 cx,
303 Box::new(move |cx| {
304 bind_recursive(cx, &rest, closure.clone());
305 }),
306 );
307 } else {
308 closure(cx);
309 }
310}
311
312impl<T: ToString> ToStringLocalized for T {
313 fn to_string_local(&self, _cx: &impl DataContext) -> String {
314 self.to_string()
315 }
316}
317
318/// A trait for converting from [Localized] to a `String` via a translation using fluent.
319pub trait ToStringLocalized {
320 /// Method for converting the current type to a `String` via a translation using fluent.
321 fn to_string_local(&self, cx: &impl DataContext) -> String;
322}
323
324impl ToStringLocalized for Localized {
325 fn to_string_local(&self, cx: &impl DataContext) -> String {
326 let cx = cx.localization_context().expect("Failed to get context");
327
328 let locale = &cx.environment().locale;
329 let bundle = cx.resource_manager.current_translation(locale);
330 let message = if let Some(msg) = bundle.get_message(&self.key) {
331 msg
332 } else {
333 // Warn here of missing key
334 return (self.map)(&self.key);
335 };
336
337 let value = if let Some(value) = message.value() {
338 value
339 } else {
340 // Warn here of missing value
341 return (self.map)(&self.key);
342 };
343
344 let mut err = vec![];
345 let args = self.get_args(&cx);
346 let res = bundle.format_pattern(value, Some(&args), &mut err);
347
348 if err.is_empty() {
349 (self.map)(&res)
350 } else {
351 format!("{} {{ERROR: {:?}}}", res, err)
352 }
353 }
354}