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