zng_ext_l10n/
service.rs

1use std::{
2    borrow::Cow,
3    collections::{HashMap, hash_map},
4    fmt, ops,
5    path::PathBuf,
6    str::FromStr,
7    sync::Arc,
8};
9
10use parking_lot::Mutex;
11use zng_app_context::app_local;
12use zng_txt::Txt;
13use zng_var::{
14    ArcEq, ArcVar, BoxedVar, BoxedWeakVar, LocalVar, MergeVarBuilder, ReadOnlyArcVar, Var, WeakVar, merge_var, types::ArcCowVar, var,
15};
16use zng_view_api::config::LocaleConfig;
17
18use crate::{
19    FluentParserErrors, L10nArgument, L10nSource, Lang, LangFilePath, LangMap, LangResource, LangResourceStatus, Langs, SwapL10nSource,
20};
21
22pub(super) struct L10nService {
23    source: Mutex<SwapL10nSource>, // Mutex for `Sync` only.
24    sys_lang: ArcVar<Langs>,
25    app_lang: ArcCowVar<Langs, ArcVar<Langs>>,
26
27    perm_res: Vec<BoxedVar<Option<ArcEq<fluent::FluentResource>>>>,
28    bundles: HashMap<(Langs, LangFilePath), BoxedWeakVar<ArcFluentBundle>>,
29}
30impl L10nService {
31    pub fn new() -> Self {
32        let sys_lang = var(Langs::default());
33        Self {
34            source: Mutex::new(SwapL10nSource::new()),
35            app_lang: sys_lang.cow(),
36            sys_lang,
37            perm_res: vec![],
38            bundles: HashMap::new(),
39        }
40    }
41
42    pub fn load(&mut self, source: impl L10nSource) {
43        self.source.get_mut().load(source);
44    }
45
46    pub fn available_langs(&mut self) -> BoxedVar<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>> {
47        self.source.get_mut().available_langs()
48    }
49
50    pub fn available_langs_status(&mut self) -> BoxedVar<LangResourceStatus> {
51        self.source.get_mut().available_langs_status()
52    }
53
54    pub fn sys_lang(&self) -> ReadOnlyArcVar<Langs> {
55        self.sys_lang.read_only()
56    }
57
58    pub fn app_lang(&self) -> ArcCowVar<Langs, ArcVar<Langs>> {
59        self.app_lang.clone()
60    }
61
62    pub fn localized_message(
63        &mut self,
64        langs: Langs,
65        file: LangFilePath,
66        id: Txt,
67        attribute: Txt,
68        fallback: Txt,
69        mut args: Vec<(Txt, BoxedVar<L10nArgument>)>,
70    ) -> BoxedVar<Txt> {
71        if langs.is_empty() {
72            return if args.is_empty() {
73                // no lang, no args
74                LocalVar(fallback).boxed()
75            } else {
76                // no lang, but args can change
77                fluent_args_var(args)
78                    .map(move |args| {
79                        let args = args.lock();
80                        format_fallback(&file.file, id.as_str(), attribute.as_str(), &fallback, Some(&*args))
81                    })
82                    .boxed()
83            };
84        }
85
86        let bundle = self.resource_bundle(langs, file.clone());
87
88        if args.is_empty() {
89            // no args, but message can change
90            bundle
91                .map(move |b| {
92                    if let Some(msg) = b.get_message(&id) {
93                        let value = if attribute.is_empty() {
94                            msg.value()
95                        } else {
96                            msg.get_attribute(&attribute).map(|attr| attr.value())
97                        };
98                        if let Some(pattern) = value {
99                            let mut errors = vec![];
100                            let r = b.format_pattern(pattern, None, &mut errors);
101                            if !errors.is_empty() {
102                                let e = FluentErrors(errors);
103                                if attribute.is_empty() {
104                                    tracing::error!("error formatting {id}\n{e}");
105                                } else {
106                                    tracing::error!("error formatting {id}.{attribute}\n{e}");
107                                }
108                            }
109                            return Txt::from_str(r.as_ref());
110                        }
111                    }
112                    fallback.clone()
113                })
114                .boxed()
115        } else if args.len() == 1 {
116            // one arg and message can change
117            let (name, arg) = args.remove(0);
118
119            merge_var!(bundle, arg, move |b, arg| {
120                let mut args = fluent::FluentArgs::with_capacity(1);
121                args.set(Cow::Borrowed(name.as_str()), arg.fluent_value());
122
123                if let Some(msg) = b.get_message(&id) {
124                    let value = if attribute.is_empty() {
125                        msg.value()
126                    } else {
127                        msg.get_attribute(&attribute).map(|attr| attr.value())
128                    };
129                    if let Some(pattern) = value {
130                        let mut errors = vec![];
131
132                        let r = b.format_pattern(pattern, Some(&args), &mut errors);
133                        if !errors.is_empty() {
134                            let e = FluentErrors(errors);
135                            let key = DisplayKey {
136                                file: &file.file,
137                                id: id.as_str(),
138                                attribute: attribute.as_str(),
139                            };
140                            tracing::error!("error formatting {key}\n{e}");
141                        }
142                        return Txt::from_str(r.as_ref());
143                    }
144                }
145
146                format_fallback(&file.file, id.as_str(), attribute.as_str(), &fallback, Some(&args))
147            })
148            .boxed()
149        } else {
150            // many args and message can change
151            merge_var!(bundle, fluent_args_var(args), move |b, args| {
152                if let Some(msg) = b.get_message(&id) {
153                    let value = if attribute.is_empty() {
154                        msg.value()
155                    } else {
156                        msg.get_attribute(&attribute).map(|attr| attr.value())
157                    };
158                    if let Some(pattern) = value {
159                        let mut errors = vec![];
160
161                        let args = args.lock();
162                        let r = b.format_pattern(pattern, Some(&*args), &mut errors);
163                        if !errors.is_empty() {
164                            let e = FluentErrors(errors);
165                            let key = DisplayKey {
166                                file: &file.file,
167                                id: id.as_str(),
168                                attribute: attribute.as_str(),
169                            };
170                            tracing::error!("error formatting {key}\n{e}");
171                        }
172                        return Txt::from_str(r.as_ref());
173                    }
174                }
175
176                let args = args.lock();
177                format_fallback(&file.file, id.as_str(), attribute.as_str(), &fallback, Some(&*args))
178            })
179            .boxed()
180        }
181    }
182
183    fn resource_bundle(&mut self, langs: Langs, file: LangFilePath) -> BoxedVar<ArcFluentBundle> {
184        match self.bundles.entry((langs, file)) {
185            hash_map::Entry::Occupied(mut e) => {
186                if let Some(r) = e.get().upgrade() {
187                    return r;
188                }
189                let (langs, file) = e.key();
190                let r = Self::new_resource_bundle(self.source.get_mut(), langs, file);
191                e.insert(r.downgrade());
192                r
193            }
194            hash_map::Entry::Vacant(e) => {
195                let (langs, file) = e.key();
196                let r = Self::new_resource_bundle(self.source.get_mut(), langs, file);
197                e.insert(r.downgrade());
198                r
199            }
200        }
201    }
202    fn new_resource_bundle(source: &mut SwapL10nSource, langs: &Langs, file: &LangFilePath) -> BoxedVar<ArcFluentBundle> {
203        if langs.len() == 1 {
204            let lang = langs[0].clone();
205            let res = source.lang_resource(lang.clone(), file.clone());
206            res.map(move |r| {
207                let mut bundle = ConcurrentFluentBundle::new_concurrent(vec![lang.0.clone()]);
208                if let Some(r) = r {
209                    bundle.add_resource_overriding(r.0.clone());
210                }
211                ArcFluentBundle(Arc::new(bundle))
212            })
213            .boxed()
214        } else {
215            debug_assert!(langs.len() > 1);
216
217            let langs = langs.0.clone();
218
219            let mut res = MergeVarBuilder::new();
220            for l in langs.iter().rev() {
221                res.push(source.lang_resource(l.clone(), file.clone()));
222            }
223            res.build(move |res| {
224                let mut bundle = ConcurrentFluentBundle::new_concurrent(langs.iter().map(|l| l.0.clone()).collect());
225                for r in res.iter().flatten() {
226                    bundle.add_resource_overriding(r.0.clone());
227                }
228                ArcFluentBundle(Arc::new(bundle))
229            })
230            .boxed()
231        }
232    }
233
234    pub fn lang_resource(&mut self, lang: Lang, file: LangFilePath) -> LangResource {
235        LangResource {
236            res: self.source.get_mut().lang_resource(lang.clone(), file.clone()),
237            status: self.source.get_mut().lang_resource_status(lang, file),
238        }
239    }
240
241    pub fn set_sys_langs(&self, cfg: &LocaleConfig) {
242        let langs = cfg
243            .langs
244            .iter()
245            .filter_map(|l| match Lang::from_str(l) {
246                Ok(l) => Some(l),
247                Err(e) => {
248                    tracing::error!("invalid lang {l:?}, {e}");
249                    None
250                }
251            })
252            .collect();
253        self.sys_lang.set(Langs(langs));
254    }
255
256    pub fn push_perm_resource(&mut self, r: LangResource) {
257        let ptr = r.res.var_ptr();
258        if !self.perm_res.iter().any(|r| r.var_ptr() == ptr) {
259            self.perm_res.push(r.res);
260        }
261    }
262}
263app_local! {
264    pub(super) static L10N_SV: L10nService = L10nService::new();
265}
266
267type ConcurrentFluentBundle = fluent::bundle::FluentBundle<Arc<fluent::FluentResource>, intl_memoizer::concurrent::IntlLangMemoizer>;
268
269#[derive(Clone)]
270struct ArcFluentBundle(Arc<ConcurrentFluentBundle>);
271impl fmt::Debug for ArcFluentBundle {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        write!(f, "ArcFluentBundle")
274    }
275}
276impl PartialEq for ArcFluentBundle {
277    fn eq(&self, other: &Self) -> bool {
278        Arc::ptr_eq(&self.0, &other.0)
279    }
280}
281impl ops::Deref for ArcFluentBundle {
282    type Target = ConcurrentFluentBundle;
283
284    fn deref(&self) -> &Self::Target {
285        &self.0
286    }
287}
288
289struct FluentErrors(Vec<fluent::FluentError>);
290
291impl fmt::Display for FluentErrors {
292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        let mut sep = "";
294        for e in &self.0 {
295            write!(f, "{sep}{e}")?;
296            sep = "\n";
297        }
298        Ok(())
299    }
300}
301
302fn format_fallback(file: &str, id: &str, attribute: &str, fallback: &Txt, args: Option<&fluent::FluentArgs>) -> Txt {
303    let mut fallback_pattern = None;
304
305    let mut entry = "k = ".to_owned();
306    let mut prefix = "";
307    for line in fallback.lines() {
308        entry.push_str(prefix);
309        entry.push_str(line);
310        prefix = "\n   ";
311    }
312    match fluent_syntax::parser::parse_runtime(entry.as_str()) {
313        Ok(mut f) => {
314            if let Some(fluent_syntax::ast::Entry::Message(m)) = f.body.pop() {
315                if let Some(p) = m.value {
316                    fallback_pattern = Some(p)
317                }
318            }
319        }
320        Err(e) => {
321            let key = DisplayKey { file, id, attribute };
322            tracing::error!("invalid fallback for `{key}`\n{}", FluentParserErrors(e.1));
323        }
324    }
325    let fallback = match fallback_pattern {
326        Some(f) => f,
327        None => fluent_syntax::ast::Pattern {
328            elements: vec![fluent_syntax::ast::PatternElement::TextElement { value: fallback.as_str() }],
329        },
330    };
331
332    let mut errors = vec![];
333    let blank = fluent::FluentBundle::<fluent::FluentResource>::new(vec![]);
334    let txt = blank.format_pattern(&fallback, args, &mut errors);
335
336    if !errors.is_empty() {
337        let key = DisplayKey { file, id, attribute };
338        tracing::error!("error formatting fallback `{key}`\n{}", FluentErrors(errors));
339    }
340
341    Txt::from_str(txt.as_ref())
342}
343
344fn fluent_args_var(args: Vec<(Txt, BoxedVar<L10nArgument>)>) -> impl Var<ArcEq<Mutex<fluent::FluentArgs<'static>>>> {
345    let mut fluent_args = MergeVarBuilder::new();
346    let mut names = Vec::with_capacity(args.len());
347    for (name, arg) in args {
348        names.push(name);
349        fluent_args.push(arg);
350    }
351    fluent_args.build(move |values| {
352        // review after https://github.com/projectfluent/fluent-rs/issues/319
353        let mut args = fluent::FluentArgs::with_capacity(values.len());
354        for (name, value) in names.iter().zip(values.iter()) {
355            args.set(Cow::Owned(name.to_string()), value.to_fluent_value());
356        }
357
358        // Mutex because ValueType is not Sync
359        ArcEq::new(Mutex::new(args))
360    })
361}
362
363struct DisplayKey<'a> {
364    file: &'a str,
365    id: &'a str,
366    attribute: &'a str,
367}
368impl fmt::Display for DisplayKey<'_> {
369    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370        if !self.file.is_empty() {
371            write!(f, "{}/", self.file)?
372        }
373        write!(f, "{}", self.id)?;
374        if !self.attribute.is_empty() {
375            write!(f, ".{}", self.attribute)?;
376        }
377        Ok(())
378    }
379}