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 any keyed signal (value or signal) to be used,
62//! binding the fluent variable to application data and updating when that data changes.
63//! ```ignore
64//! # use vizia_core::prelude::*;
65//! # let mut cx = &mut Context::default();
66//! #
67//! # pub struct AppData {
68//! #   user: String,
69//! # }
70//! Label::new(cx, Localized::new("welcome").arg("user", AppData::user));
71//! ```
72use crate::context::LocalizationContext;
73use crate::prelude::*;
74use fluent_bundle::FluentArgs;
75use fluent_bundle::FluentValue;
76use hashbrown::HashMap;
77use std::marker::PhantomData;
78use std::rc::Rc;
79use std::sync::Arc;
80
81pub(crate) trait FluentStore {
82    fn get_val(&self, cx: &LocalizationContext) -> FluentValue<'static>;
83    fn make_clone(&self) -> Box<dyn FluentStore>;
84    fn bind(&self, cx: &mut Context, closure: Box<dyn Fn(&mut Context)>);
85}
86
87#[derive(Clone)]
88pub(crate) struct ResState<R, T> {
89    res: R,
90    _marker: PhantomData<T>,
91}
92
93#[derive(Clone)]
94pub(crate) struct ValState<T> {
95    val: T,
96}
97
98impl<R, T> FluentStore for ResState<R, T>
99where
100    R: 'static + Clone + Res<T>,
101    T: 'static + Clone + Into<FluentValue<'static>>,
102{
103    fn get_val(&self, cx: &LocalizationContext) -> FluentValue<'static> {
104        self.res.get_value(cx).into()
105    }
106
107    fn make_clone(&self) -> Box<dyn FluentStore> {
108        Box::new(self.clone())
109    }
110
111    fn bind(&self, cx: &mut Context, closure: Box<dyn Fn(&mut Context)>) {
112        self.res.clone().set_or_bind(cx, move |cx, _| closure(cx));
113    }
114}
115
116impl<T> FluentStore for ValState<T>
117where
118    T: 'static + Clone + Into<FluentValue<'static>>,
119{
120    fn get_val(&self, _cx: &LocalizationContext) -> FluentValue<'static> {
121        self.val.clone().into()
122    }
123
124    fn make_clone(&self) -> Box<dyn FluentStore> {
125        Box::new(self.clone())
126    }
127
128    fn bind(&self, cx: &mut Context, closure: Box<dyn Fn(&mut Context)>) {
129        closure(cx);
130    }
131}
132
133/// A type which formats a localized message with any number of named arguments.
134pub struct Localized {
135    key: String,
136    args: HashMap<String, Box<dyn FluentStore>>,
137    map: Rc<dyn Fn(&str) -> String + 'static>,
138}
139
140impl PartialEq for Localized {
141    fn eq(&self, other: &Self) -> bool {
142        self.key == other.key
143    }
144}
145
146impl Clone for Localized {
147    fn clone(&self) -> Self {
148        Self {
149            key: self.key.clone(),
150            args: self.args.iter().map(|(k, v)| (k.clone(), v.make_clone())).collect(),
151            map: self.map.clone(),
152        }
153    }
154}
155
156impl Localized {
157    fn get_args(&self, cx: &LocalizationContext) -> FluentArgs {
158        let mut res = FluentArgs::new();
159        for (name, arg) in &self.args {
160            res.set(name.to_owned(), arg.get_val(cx));
161        }
162        res
163    }
164
165    /// Creates a new Localized type with a given key.
166    ///
167    /// The given key is used to retrieve a translation from a fluent bundle resource.
168    ///
169    /// # Example
170    /// ```no_run
171    /// # use vizia_core::prelude::*;
172    ///
173    /// # use vizia_winit::application::Application;
174    /// Application::new(|cx|{
175    ///     Label::new(cx, Localized::new("key"));
176    /// })
177    /// .run();
178    pub fn new(key: &str) -> Self {
179        Self { key: key.to_owned(), args: HashMap::new(), map: Rc::new(|s| s.to_string()) }
180    }
181
182    /// Sets a mapping function to apply to the translated text.
183    pub fn map(mut self, mapping: impl Fn(&str) -> String + 'static) -> Self {
184        self.map = Rc::new(mapping);
185
186        self
187    }
188
189    /// Add a variable argument binding to the Localized type.
190    ///
191    /// Takes a key name and a signal for the argument value (value or signal).
192    ///
193    /// # Example
194    /// ```no_run
195    /// # use vizia_core::prelude::*;
196    ///
197    /// # use vizia_winit::application::Application;
198    /// #
199    /// # struct AppData {
200    /// #   value: i32,
201    /// # }
202    /// # impl Model for AppData {}
203    /// Application::new(|cx|{
204    ///     
205    ///     AppData {
206    ///         value: 5,
207    ///     }.build(cx);
208    ///
209    ///     Label::new(cx, Localized::new("key").arg("value", AppData::value));
210    /// })
211    /// .run();
212    pub fn arg<R, T>(mut self, key: &str, res: R) -> Self
213    where
214        R: 'static + Clone + Res<T>,
215        T: 'static + Clone + Into<FluentValue<'static>>,
216    {
217        self.args.insert(key.to_owned(), Box::new(ResState { res, _marker: PhantomData }));
218        self
219    }
220
221    /// Add a constant argument to the Localized type.
222    ///
223    /// Takes a key name and a value for the argument.
224    ///
225    /// # Example
226    /// ```no_run
227    /// # use vizia_core::prelude::*;
228    /// # use vizia_winit::application::Application;
229    /// Application::new(|cx|{
230    ///
231    ///     Label::new(cx, Localized::new("key").arg_const("value", 32));
232    /// })
233    /// .run();
234    pub fn arg_const<T: Into<FluentValue<'static>> + Clone + 'static>(
235        mut self,
236        key: &str,
237        val: T,
238    ) -> Self {
239        self.args.insert(key.to_owned(), Box::new(ValState { val }));
240        self
241    }
242}
243
244impl Res<String> for Localized {
245    fn get_value(&self, cx: &impl DataContext) -> String {
246        let cx = cx.localization_context().expect("Failed to get context");
247        let locale = &cx.environment().locale.get();
248        let bundle = cx.resource_manager.current_translation(locale);
249        let message = if let Some(msg) = bundle.get_message(&self.key) {
250            msg
251        } else {
252            return (self.map)(&self.key);
253        };
254
255        let value = if let Some(value) = message.value() {
256            value
257        } else {
258            return (self.map)(&self.key);
259        };
260
261        let mut err = vec![];
262        let args = self.get_args(&cx);
263        let res = bundle.format_pattern(value, Some(&args), &mut err);
264
265        if err.is_empty() { (self.map)(&res) } else { format!("{} {{ERROR: {:?}}}", res, err) }
266    }
267
268    fn set_or_bind<F>(self, cx: &mut Context, closure: F)
269    where
270        F: 'static + Fn(&mut Context, Localized),
271    {
272        let current = cx.current();
273        let self2 = self.clone();
274        let closure = Arc::new(closure);
275        let locale_signal = cx.environment().locale;
276        locale_signal.set_or_bind(cx, move |cx, _| {
277            cx.with_current(current, |cx| {
278                let stores = self2.args.values().map(|x| x.make_clone()).collect::<Vec<_>>();
279                let self3 = self2.clone();
280                let closure = closure.clone();
281                bind_recursive(cx, &stores, move |cx| {
282                    closure(cx, self3.clone());
283                });
284            });
285        });
286    }
287}
288
289fn bind_recursive<F>(cx: &mut Context, stores: &[Box<dyn FluentStore>], closure: F)
290where
291    F: 'static + Clone + Fn(&mut Context),
292{
293    if let Some((store, rest)) = stores.split_last() {
294        let rest = rest.iter().map(|x| x.make_clone()).collect::<Vec<_>>();
295        store.bind(
296            cx,
297            Box::new(move |cx| {
298                bind_recursive(cx, &rest, closure.clone());
299            }),
300        );
301    } else {
302        closure(cx);
303    }
304}
305
306impl<T: ToString> ToStringLocalized for T {
307    fn to_string_local(&self, _cx: &impl DataContext) -> String {
308        self.to_string()
309    }
310}
311
312/// A trait for converting from [Localized] to a `String` via a translation using fluent.
313pub trait ToStringLocalized {
314    /// Method for converting the current type to a `String` via a translation using fluent.
315    fn to_string_local(&self, cx: &impl DataContext) -> String;
316}
317
318impl ToStringLocalized for Localized {
319    fn to_string_local(&self, cx: &impl DataContext) -> String {
320        let cx = cx.localization_context().expect("Failed to get context");
321
322        let locale = &cx.environment().locale.get();
323        let bundle = cx.resource_manager.current_translation(locale);
324        let message = if let Some(msg) = bundle.get_message(&self.key) {
325            msg
326        } else {
327            // Warn here of missing key
328            return (self.map)(&self.key);
329        };
330
331        let value = if let Some(value) = message.value() {
332            value
333        } else {
334            // Warn here of missing value
335            return (self.map)(&self.key);
336        };
337
338        let mut err = vec![];
339        let args = self.get_args(&cx);
340        let res = bundle.format_pattern(value, Some(&args), &mut err);
341
342        if err.is_empty() { (self.map)(&res) } else { format!("{} {{ERROR: {:?}}}", res, err) }
343    }
344}