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, LoaderError> {
50 let mut resources: HashMap<LanguageIdentifier, Vec<Arc<FluentResource>>> = HashMap::new();
51 let entries = read_dir(self.location)
52 .map_err(|source| LoaderError::Fs { path: self.location.to_owned(), source })?;
53
54 for entry in entries {
55 let entry = entry.map_err(|source| LoaderError::Fs { path: self.location.to_owned(), source })?;
56 let file_type = entry.file_type().map_err(|source| LoaderError::Fs { path: entry.path(), source })?;
57 if file_type.is_dir() {
58 if let Ok(lang) = entry.file_name().into_string() {
59 let lang = lang.parse::<LanguageIdentifier>()?;
60 let lang_resources = crate::fs::read_from_dir(entry.path())?
61 .into_iter()
62 .map(Arc::new)
63 .collect::<Vec<_>>();
64 resources.entry(lang).or_default().extend(lang_resources);
65 }
66 } else if file_type.is_file()
67 && entry.path().extension().is_some_and(|e| e == "ftl")
68 {
69 if let Some(stem) = entry.path().file_stem().and_then(|s| s.to_str()) {
70 if let Ok(lang) = stem.parse::<LanguageIdentifier>() {
71 let res = Arc::new(crate::fs::read_from_file(entry.path())?);
72 resources.entry(lang).or_default().push(res);
73 }
74 }
75 }
76 }
77
78 let mut bundles = HashMap::new();
79 for (lang, v) in resources.iter() {
80 let mut bundle = FluentBundle::new_concurrent(vec![lang.clone()]);
81
82 for shared_resource in self.shared.unwrap_or(&[]) {
83 bundle
84 .add_resource(Arc::new(crate::fs::read_from_file(shared_resource)?))
85 .map_err(|errors| LoaderError::FluentBundle { errors })?;
86 }
87
88 for res in v {
89 bundle
90 .add_resource(res.clone())
91 .map_err(|errors| LoaderError::FluentBundle { errors })?;
92 }
93
94 if let Some(customize) = self.customize.as_mut() {
95 (customize)(&mut bundle);
96 }
97
98 bundles.insert(lang.clone(), bundle);
99 }
100
101 let fallbacks = super::build_fallbacks(&resources.keys().cloned().collect::<Vec<_>>());
102
103 Ok(ArcLoader {
104 bundles,
105 fallbacks,
106 fallback: self.fallback,
107 })
108 }
109}
110
111pub struct ArcLoader {
124 bundles: HashMap<LanguageIdentifier, FluentBundle<Arc<FluentResource>>>,
125 fallback: LanguageIdentifier,
126 fallbacks: HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
127}
128
129impl super::Loader for ArcLoader {
130 fn lookup_complete(
132 &self,
133 lang: &LanguageIdentifier,
134 text_id: &str,
135 args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
136 ) -> String {
137 for lang in negotiate_languages(&[lang], &self.bundles.keys().collect::<Vec<_>>(), None) {
138 if let Ok(val) = self.lookup_single_language(lang, text_id, args) {
139 return val;
140 }
141 }
142 if *lang != self.fallback {
143 if let Ok(val) = self.lookup_single_language(&self.fallback, text_id, args) {
144 return val;
145 }
146 }
147 format!("Unknown localization key: {text_id:?}")
148 }
149
150 fn try_lookup_complete(
152 &self,
153 lang: &LanguageIdentifier,
154 text_id: &str,
155 args: Option<&HashMap<Cow<'static, str>, FluentValue>>,
156 ) -> Option<String> {
157 for lang in negotiate_languages(&[lang], &self.bundles.keys().collect::<Vec<_>>(), None) {
158 if let Ok(val) = self.lookup_single_language(lang, text_id, args) {
159 return Some(val);
160 }
161 }
162 if *lang != self.fallback {
163 if let Ok(val) = self.lookup_single_language(&self.fallback, text_id, args) {
164 return Some(val);
165 }
166 }
167 None
168 }
169
170 fn locales(&self) -> Box<dyn Iterator<Item = &LanguageIdentifier> + '_> {
171 Box::new(self.fallbacks.keys())
172 }
173}
174
175impl ArcLoader {
176 pub fn builder<'a, P: AsRef<Path> + ?Sized>(
178 location: &'a P,
179 fallback: LanguageIdentifier,
180 ) -> ArcLoaderBuilder<'a, 'static> {
181 ArcLoaderBuilder {
182 location: location.as_ref(),
183 fallback,
184 shared: None,
185 customize: None,
186 }
187 }
188
189 pub fn lookup_single_language<T: AsRef<str>>(
191 &self,
192 lang: &LanguageIdentifier,
193 text_id: &str,
194 args: Option<&HashMap<T, FluentValue>>,
195 ) -> Result<String, LookupError> {
196 super::shared::lookup_single_language(&self.bundles, lang, text_id, args)
197 }
198
199 pub fn lookup_no_default_fallback<S: AsRef<str>>(
202 &self,
203 lang: &LanguageIdentifier,
204 text_id: &str,
205 args: Option<&HashMap<S, FluentValue>>,
206 ) -> Option<String> {
207 super::shared::lookup_no_default_fallback(
208 &self.bundles,
209 &self.fallbacks,
210 lang,
211 text_id,
212 args,
213 )
214 }
215
216 pub fn fallback(&self) -> &LanguageIdentifier {
218 &self.fallback
219 }
220}