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}