fluent_templates/loader/
arc_loader.rs1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::fs::read_dir;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use crate::languages::negotiate_languages;
8use crate::FluentBundle;
9use fluent_bundle::{FluentResource, FluentValue};
10
11use crate::error::{LoaderError, LookupError};
12
13pub use unic_langid::LanguageIdentifier;
14
15type Customize = Option<Box<dyn FnMut(&mut FluentBundle<Arc<FluentResource>>)>>;
16
17pub struct ArcLoaderBuilder<'a, 'b> {
19 location: &'a Path,
20 fallback: LanguageIdentifier,
21 shared: Option<&'b [PathBuf]>,
22 customize: Customize,
23}
24
25impl<'a, 'b> ArcLoaderBuilder<'a, 'b> {
26 pub fn shared_resources<'b2>(
28 self,
29 shared: Option<&'b2 [PathBuf]>,
30 ) -> ArcLoaderBuilder<'a, 'b2> {
31 ArcLoaderBuilder {
32 location: self.location,
33 fallback: self.fallback,
34 shared,
35 customize: self.customize,
36 }
37 }
38
39 pub fn customize(
41 mut self,
42 customize: impl FnMut(&mut FluentBundle<Arc<FluentResource>>) + 'static,
43 ) -> Self {
44 self.customize = Some(Box::new(customize));
45 self
46 }
47
48 pub fn build(mut self) -> Result<ArcLoader, Box<dyn std::error::Error>> {
50 let mut resources: HashMap<LanguageIdentifier, Vec<Arc<FluentResource>>> = HashMap::new();
51
52 for entry in read_dir(self.location)? {
53 let entry = entry?;
54 let file_type = entry.file_type()?;
55 if file_type.is_dir() {
56 if let Ok(lang) = entry.file_name().into_string() {
57 let lang = lang.parse::<LanguageIdentifier>()?;
58 let lang_resources = crate::fs::read_from_dir(entry.path())?
59 .into_iter()
60 .map(Arc::new)
61 .collect::<Vec<_>>();
62 resources.entry(lang).or_default().extend(lang_resources);
63 }
64 } else if file_type.is_file()
65 && entry.path().extension().is_some_and(|e| e == "ftl")
66 {
67 if let Some(stem) = entry.path().file_stem().and_then(|s| s.to_str()) {
68 if let Ok(lang) = stem.parse::<LanguageIdentifier>() {
69 let res = Arc::new(crate::fs::read_from_file(entry.path())?);
70 resources.entry(lang).or_default().push(res);
71 }
72 }
73 }
74 }
75
76 let mut bundles = HashMap::new();
77 for (lang, v) in resources.iter() {
78 let mut bundle = FluentBundle::new_concurrent(vec![lang.clone()]);
79
80 for shared_resource in self.shared.unwrap_or(&[]) {
81 bundle
82 .add_resource(Arc::new(crate::fs::read_from_file(shared_resource)?))
83 .map_err(|errors| LoaderError::FluentBundle { errors })?;
84 }
85
86 for res in v {
87 bundle
88 .add_resource(res.clone())
89 .map_err(|errors| LoaderError::FluentBundle { errors })?;
90 }
91
92 if let Some(customize) = self.customize.as_mut() {
93 (customize)(&mut bundle);
94 }
95
96 bundles.insert(lang.clone(), bundle);
97 }
98
99 let fallbacks = super::build_fallbacks(&resources.keys().cloned().collect::<Vec<_>>());
100
101 Ok(ArcLoader {
102 bundles,
103 fallbacks,
104 fallback: self.fallback,
105 })
106 }
107}
108
109pub struct ArcLoader {
122 bundles: HashMap<LanguageIdentifier, FluentBundle<Arc<FluentResource>>>,
123 fallback: LanguageIdentifier,
124 fallbacks: HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
125}
126
127impl super::Loader for ArcLoader {
128 fn lookup_complete(
130 &self,
131 lang: &LanguageIdentifier,
132 text_id: &str,
133 args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
134 ) -> String {
135 for lang in negotiate_languages(&[lang], &self.bundles.keys().collect::<Vec<_>>(), None) {
136 if let Ok(val) = self.lookup_single_language(lang, text_id, args) {
137 return val;
138 }
139 }
140 if *lang != self.fallback {
141 if let Ok(val) = self.lookup_single_language(&self.fallback, text_id, args) {
142 return val;
143 }
144 }
145 format!("Unknown localization key: {text_id:?}")
146 }
147
148 fn try_lookup_complete(
150 &self,
151 lang: &LanguageIdentifier,
152 text_id: &str,
153 args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
154 ) -> Option<String> {
155 for lang in negotiate_languages(&[lang], &self.bundles.keys().collect::<Vec<_>>(), None) {
156 if let Ok(val) = self.lookup_single_language(lang, text_id, args) {
157 return Some(val);
158 }
159 }
160 if *lang != self.fallback {
161 if let Ok(val) = self.lookup_single_language(&self.fallback, text_id, args) {
162 return Some(val);
163 }
164 }
165 None
166 }
167
168 fn locales(&self) -> Box<dyn Iterator<Item = &LanguageIdentifier> + '_> {
169 Box::new(self.fallbacks.keys())
170 }
171}
172
173impl ArcLoader {
174 pub fn builder<'a, P: AsRef<Path> + ?Sized>(
176 location: &'a P,
177 fallback: LanguageIdentifier,
178 ) -> ArcLoaderBuilder<'a, 'static> {
179 ArcLoaderBuilder {
180 location: location.as_ref(),
181 fallback,
182 shared: None,
183 customize: None,
184 }
185 }
186
187 pub fn lookup_single_language<T: AsRef<str>>(
189 &self,
190 lang: &LanguageIdentifier,
191 text_id: &str,
192 args: Option<&HashMap<T, FluentValue>>,
193 ) -> Result<String, LookupError> {
194 super::shared::lookup_single_language(&self.bundles, lang, text_id, args)
195 }
196
197 pub fn lookup_no_default_fallback<S: AsRef<str>>(
200 &self,
201 lang: &LanguageIdentifier,
202 text_id: &str,
203 args: Option<&HashMap<S, FluentValue>>,
204 ) -> Option<String> {
205 super::shared::lookup_no_default_fallback(
206 &self.bundles,
207 &self.fallbacks,
208 lang,
209 text_id,
210 args,
211 )
212 }
213
214 pub fn fallback(&self) -> &LanguageIdentifier {
216 &self.fallback
217 }
218}