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}