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>, 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 LocalVar(fallback).boxed()
75 } else {
76 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 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 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 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 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 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}