1use crate::{I18nAssets, I18nEmbedError, LanguageLoader};
9
10use arc_swap::ArcSwap;
11pub use fluent_langneg::NegotiationStrategy;
12pub use i18n_embed_impl::fluent_language_loader;
13
14use fluent::{
15 bundle::FluentBundle, FluentArgs, FluentAttribute, FluentMessage, FluentResource, FluentValue,
16};
17use fluent_syntax::ast::{self, Pattern};
18use intl_memoizer::concurrent::IntlLangMemoizer;
19use parking_lot::RwLock;
20use std::{borrow::Cow, collections::HashMap, fmt::Debug, iter::FromIterator, sync::Arc};
21use unic_langid::LanguageIdentifier;
22
23struct LanguageBundle {
24 language: LanguageIdentifier,
25 bundle: FluentBundle<Arc<FluentResource>, IntlLangMemoizer>,
26 resource: Arc<FluentResource>,
27}
28
29impl LanguageBundle {
30 fn new(language: LanguageIdentifier, resource: FluentResource) -> Self {
31 let mut bundle = FluentBundle::new_concurrent(vec![language.clone()]);
32 let resource = Arc::new(resource);
33 if let Err(errors) = bundle.add_resource(resource.clone()) {
34 errors.iter().for_each(|error | {
35 log::error!(target: "i18n_embed::fluent", "Error while adding resource to bundle: {0:?}.", error);
36 })
37 }
38 Self {
39 language,
40 bundle,
41 resource,
42 }
43 }
44}
45
46impl Debug for LanguageBundle {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 write!(f, "LanguageBundle(language: {})", self.language)
49 }
50}
51
52#[derive(Debug)]
53struct LanguageConfig {
54 language_bundles: Vec<Vec<LanguageBundle>>,
58 language_map: HashMap<LanguageIdentifier, usize>,
61}
62
63#[derive(Debug)]
64struct CurrentLanguages {
65 languages: Vec<LanguageIdentifier>,
67 indices: Vec<usize>,
70}
71
72#[derive(Debug)]
73struct FluentLanguageLoaderInner {
74 language_config: Arc<RwLock<LanguageConfig>>,
75 current_languages: CurrentLanguages,
76}
77
78#[derive(Debug)]
84pub struct FluentLanguageLoader {
85 inner: ArcSwap<FluentLanguageLoaderInner>,
86 domain: String,
87 fallback_language: unic_langid::LanguageIdentifier,
88}
89
90impl FluentLanguageLoader {
91 pub fn new<S: Into<String>>(
96 domain: S,
97 fallback_language: unic_langid::LanguageIdentifier,
98 ) -> Self {
99 let config = LanguageConfig {
100 language_bundles: Vec::new(),
101 language_map: HashMap::new(),
102 };
103
104 Self {
105 inner: ArcSwap::new(Arc::new(FluentLanguageLoaderInner {
106 language_config: Arc::new(RwLock::new(config)),
107 current_languages: CurrentLanguages {
108 languages: vec![fallback_language.clone()],
109 indices: vec![],
110 },
111 })),
112 domain: domain.into(),
113 fallback_language,
114 }
115 }
116
117 fn current_language_impl(
118 &self,
119 inner: &FluentLanguageLoaderInner,
120 ) -> unic_langid::LanguageIdentifier {
121 inner
122 .current_languages
123 .languages
124 .first()
125 .map_or_else(|| self.fallback_language.clone(), Clone::clone)
126 }
127
128 pub fn current_languages(&self) -> Vec<unic_langid::LanguageIdentifier> {
130 self.inner.load().current_languages.languages.clone()
131 }
132
133 pub fn get(&self, message_id: &str) -> String {
135 self.get_args_fluent(message_id, None)
136 }
137
138 pub fn get_args_concrete<'args>(
140 &self,
141 message_id: &str,
142 args: HashMap<&'args str, FluentValue<'args>>,
143 ) -> String {
144 self.get_args_fluent(message_id, hash_map_to_fluent_args(args).as_ref())
145 }
146
147 pub fn get_args_fluent<'args>(
150 &self,
151 message_id: &str,
152 args: Option<&'args FluentArgs<'args>>,
153 ) -> String {
154 let inner = self.inner.load();
155 let language_config = inner.language_config.read();
156 inner
157 .current_languages
158 .indices
159 .iter()
160 .map(|&idx| &language_config.language_bundles[idx])
161 .flat_map(|language_bundles| language_bundles.iter())
162 .find_map(|language_bundle| language_bundle
163 .bundle
164 .get_message(message_id)
165 .and_then(|m: FluentMessage<'_>| m.value())
166 .map(|pattern: &Pattern<&str>| {
167 let mut errors = Vec::new();
168 let value = language_bundle.bundle.format_pattern(pattern, args, &mut errors);
169 if !errors.is_empty() {
170 log::error!(
171 target:"i18n_embed::fluent",
172 "Failed to format a message for language \"{}\" and id \"{}\".\nErrors\n{:?}.",
173 inner.current_languages.languages.first().unwrap_or(&self.fallback_language), message_id, errors
174 )
175 }
176 value.into()
177 })
178 )
179 .unwrap_or_else(|| {
180 log::error!(
181 target:"i18n_embed::fluent",
182 "Unable to find localization for language \"{}\" and id \"{}\".",
183 inner.current_languages.languages.first().unwrap_or(&self.fallback_language),
184 message_id
185 );
186 format!("No localization for id: \"{}\"", message_id)
187 })
188 }
189
190 pub fn get_args<'a, S, V>(&self, id: &str, args: HashMap<S, V>) -> String
193 where
194 S: Into<Cow<'a, str>> + Clone,
195 V: Into<FluentValue<'a>> + Clone,
196 {
197 self.get_args_fluent(id, hash_map_to_fluent_args(args).as_ref())
198 }
199
200 pub fn get_attr(&self, message_id: &str, attribute_id: &str) -> String {
202 self.get_attr_args_fluent(message_id, attribute_id, None)
203 }
204
205 pub fn get_attr_args_concrete<'args>(
207 &self,
208 message_id: &str,
209 attribute_id: &str,
210 args: HashMap<&'args str, FluentValue<'args>>,
211 ) -> String {
212 self.get_attr_args_fluent(
213 message_id,
214 attribute_id,
215 hash_map_to_fluent_args(args).as_ref(),
216 )
217 }
218
219 pub fn get_attr_args_fluent<'args>(
222 &self,
223 message_id: &str,
224 attribute_id: &str,
225 args: Option<&'args FluentArgs<'args>>,
226 ) -> String {
227 let inner = self.inner.load();
228 let language_config = inner.language_config.read();
229 let current_language = self.current_language_impl(&inner);
230
231 language_config.language_bundles.iter()
232 .flat_map(|language_bundles| language_bundles.iter())
233 .find_map(|language_bundle| {
234 language_bundle
235 .bundle
236 .get_message(message_id)
237 .and_then(|m: FluentMessage<'_>| {
238 m.get_attribute(attribute_id)
239 .map(|a: FluentAttribute<'_>| {
240 a.value()
241 })
242 })
243 .map(|pattern: &Pattern<&str>| {
244 let mut errors = Vec::new();
245 let value = language_bundle.bundle.format_pattern(pattern, args, &mut errors);
246 if !errors.is_empty() {
247 log::error!(
248 target:"i18n_embed::fluent",
249 "Failed to format a message for language \"{}\" and id \"{}\".\nErrors\n{:?}.",
250 current_language, message_id, errors
251 )
252 }
253 value.into()
254 })
255 })
256 .unwrap_or_else(|| {
257 log::error!(
258 target:"i18n_embed::fluent",
259 "Unable to find localization for language \"{}\", message id \"{}\" and attribute id \"{}\".",
260 current_language,
261 message_id,
262 attribute_id
263 );
264 format!("No localization for message id: \"{message_id}\" and attribute id: \"{attribute_id}\"")
265 })
266 }
267
268 pub fn get_attr_args<'a, S, V>(
271 &self,
272 message_id: &str,
273 attribute_id: &str,
274 args: HashMap<S, V>,
275 ) -> String
276 where
277 S: Into<Cow<'a, str>> + Clone,
278 V: Into<FluentValue<'a>> + Clone,
279 {
280 self.get_attr_args_fluent(
281 message_id,
282 attribute_id,
283 hash_map_to_fluent_args(args).as_ref(),
284 )
285 }
286
287 pub fn has(&self, message_id: &str) -> bool {
290 self.inner
291 .load()
292 .language_config
293 .read()
294 .language_bundles
295 .iter()
296 .flat_map(|language_bundles| language_bundles.iter())
297 .any(|language_bundle| language_bundle.bundle.has_message(message_id))
298 }
299
300 pub fn has_attr(&self, message_id: &str, attribute_id: &str) -> bool {
309 self.inner
310 .load()
311 .language_config
312 .read()
313 .language_bundles
314 .iter()
315 .flat_map(|bundles| bundles.iter())
316 .find_map(|bundle| {
317 bundle
318 .bundle
319 .get_message(message_id)
320 .map(|message| message.get_attribute(attribute_id).is_some())
321 })
322 .unwrap_or(false)
323 }
324
325 pub fn with_fluent_message<OUT, C>(&self, message_id: &str, closure: C) -> Option<OUT>
331 where
332 C: Fn(fluent::FluentMessage<'_>) -> OUT,
333 {
334 self.inner
335 .load()
336 .language_config
337 .read()
338 .language_bundles
339 .iter()
340 .flat_map(|language_bundles| language_bundles.iter())
341 .find_map(|language_bundle| language_bundle.bundle.get_message(message_id))
342 .map(closure)
343 }
344
345 pub fn with_fluent_message_and_bundle<OUT, C>(
356 &self,
357 message_id: &str,
358 closure: C,
359 ) -> Option<OUT>
360 where
361 C: Fn(FluentMessage<'_>, &FluentBundle<Arc<FluentResource>, IntlLangMemoizer>) -> OUT,
362 {
363 self.inner
364 .load()
365 .language_config
366 .read()
367 .language_bundles
368 .iter()
369 .flat_map(|language_bundles| language_bundles.iter())
370 .find_map(|language_bundle| {
371 Some((
372 language_bundle.bundle.get_message(message_id)?,
373 &language_bundle.bundle,
374 ))
375 })
376 .map(|(msg, bundle)| closure(msg, bundle))
377 }
378
379 pub fn with_message_iter<OUT, C>(&self, language: &LanguageIdentifier, closure: C) -> OUT
384 where
385 C: Fn(&mut dyn Iterator<Item = &ast::Message<&str>>) -> OUT,
386 {
387 let inner = self.inner.load();
388 let config_lock = inner.language_config.read();
389
390 let mut iter = config_lock
391 .language_bundles
392 .iter()
393 .flat_map(|language_bundles| language_bundles.iter())
394 .filter(|language_bundle| &language_bundle.language == language)
395 .flat_map(|language_bundle| {
396 language_bundle
397 .resource
398 .entries()
399 .filter_map(|entry| match entry {
400 ast::Entry::Message(message) => Some(message),
401 _ => None,
402 })
403 });
404
405 (closure)(&mut iter)
406 }
407
408 pub fn set_use_isolating(&self, value: bool) {
419 self.with_bundles_mut(|bundle| bundle.set_use_isolating(value));
420 }
421
422 pub fn with_bundles_mut<F>(&self, f: F)
427 where
428 F: Fn(&mut FluentBundle<Arc<FluentResource>, IntlLangMemoizer>),
429 {
430 for bundle in self
431 .inner
432 .load()
433 .language_config
434 .write()
435 .language_bundles
436 .iter_mut()
437 .flat_map(|bundles| bundles.iter_mut())
438 {
439 f(&mut bundle.bundle);
440 }
441 }
442
443 pub fn select_languages<LI: AsRef<LanguageIdentifier>>(
449 &self,
450 languages: &[LI],
451 ) -> FluentLanguageLoader {
452 let inner = self.inner.load();
453 let config_lock = inner.language_config.read();
454 let fallback_language: Option<&unic_langid::LanguageIdentifier> = if languages
455 .iter()
456 .any(|language| language.as_ref() == &self.fallback_language)
457 {
458 None
459 } else {
460 Some(&self.fallback_language)
461 };
462
463 let indices = languages
464 .iter()
465 .map(|lang| lang.as_ref())
466 .chain(fallback_language)
467 .filter_map(|lang| config_lock.language_map.get(lang.as_ref()))
468 .cloned()
469 .collect();
470 FluentLanguageLoader {
471 inner: ArcSwap::new(Arc::new(FluentLanguageLoaderInner {
472 current_languages: CurrentLanguages {
473 languages: languages.iter().map(|lang| lang.as_ref().clone()).collect(),
474 indices,
475 },
476 language_config: self.inner.load().language_config.clone(),
477 })),
478 domain: self.domain.clone(),
479 fallback_language: self.fallback_language.clone(),
480 }
481 }
482
483 pub fn select_languages_negotiate<LI: AsRef<LanguageIdentifier>>(
486 &self,
487 languages: &[LI],
488 strategy: NegotiationStrategy,
489 ) -> FluentLanguageLoader {
490 let available_languages = &self.inner.load().current_languages.languages;
491 let negotiated_languages = fluent_langneg::negotiate_languages(
492 languages,
493 available_languages,
494 Some(self.fallback_language()),
495 strategy,
496 );
497
498 self.select_languages(&negotiated_languages)
499 }
500}
501
502impl LanguageLoader for FluentLanguageLoader {
503 fn fallback_language(&self) -> &unic_langid::LanguageIdentifier {
506 &self.fallback_language
507 }
508 fn domain(&self) -> &str {
510 &self.domain
511 }
512
513 fn language_file_name(&self) -> String {
515 format!("{}.ftl", self.domain())
516 }
517
518 fn current_language(&self) -> unic_langid::LanguageIdentifier {
520 self.current_language_impl(&self.inner.load())
521 }
522
523 #[allow(single_use_lifetimes)]
530 fn load_languages<'a>(
531 &self,
532 i18n_assets: &dyn I18nAssets,
533 language_ids: &[unic_langid::LanguageIdentifier],
534 ) -> Result<(), I18nEmbedError> {
535 let mut language_ids = language_ids.iter().peekable();
536 if language_ids.peek().is_none() {
537 return Err(I18nEmbedError::RequestedLanguagesEmpty);
538 }
539
540 let language_ids: Vec<unic_langid::LanguageIdentifier> =
542 language_ids.map(|id| (*id).clone()).collect();
543 let mut load_language_ids: Vec<unic_langid::LanguageIdentifier> = language_ids.clone();
544
545 if !load_language_ids.contains(&self.fallback_language) {
546 load_language_ids.push(self.fallback_language.clone());
547 }
548 let language_bundles: Vec<Vec<_>> = load_language_ids.iter().map(|language| {
549 let (path, files) = self.language_files(language, i18n_assets);
550
551 if files.is_empty() {
552 log::debug!(target:"i18n_embed::fluent", "Unable to find language file: \"{0}\" for language: \"{1}\"", path, language);
553 if language == &self.fallback_language {
554 return Err(I18nEmbedError::LanguageNotAvailable(path, language.clone()));
555 }
556 }
557 files.into_iter().map(|file| {
558 log::debug!(target:"i18n_embed::fluent", "Loaded language file: \"{0}\" for language: \"{1}\"", path, language);
559
560 let file_string = String::from_utf8(file.to_vec())
561 .map_err(|err| I18nEmbedError::ErrorParsingFileUtf8(path.clone(), err))?
562 .replace("\u{000D}\n", "\n");
565
566 let resource = match FluentResource::try_new(file_string) {
567 Ok(resource) => resource,
568 Err((resource, errors)) => {
569 errors.iter().for_each(|err| {
570 log::error!(target: "i18n_embed::fluent", "Error while parsing fluent language file \"{0}\": \"{1:?}\".", path, err);
571 });
572 resource
573 }
574 };
575
576 Ok(LanguageBundle::new(language.clone(), resource))
577 }).collect::<Result<Vec<_>, I18nEmbedError>>()
578 }).collect::<Result<_, I18nEmbedError>>()?;
579
580 self.inner.swap(Arc::new(FluentLanguageLoaderInner {
581 current_languages: CurrentLanguages {
582 languages: language_ids,
583 indices: (0..load_language_ids.len()).collect(),
584 },
585 language_config: Arc::new(RwLock::new(LanguageConfig {
586 language_map: language_bundles
587 .iter()
588 .enumerate()
589 .map(|(i, language_bundles)| {
590 (
591 language_bundles.first().expect("Expect there to be at least bundle in a set of bundles per language").language.clone(),
592 i
593 )
594 })
595 .collect(),
596 language_bundles,
597 })),
598 }));
599
600 Ok(())
601 }
602
603 fn reload(&self, i18n_assets: &dyn I18nAssets) -> Result<(), I18nEmbedError> {
604 self.load_languages(
605 i18n_assets,
606 &self.inner.load().current_languages.languages.clone(),
607 )
608 }
609}
610
611fn hash_map_to_fluent_args<'args, K, V>(map: HashMap<K, V>) -> Option<FluentArgs<'args>>
612where
613 K: Into<Cow<'args, str>>,
614 V: Into<FluentValue<'args>>,
615{
616 if map.is_empty() {
617 None
618 } else {
619 Some(FluentArgs::from_iter(map))
620 }
621}