1use std::collections::HashMap;
2use std::fs::read_dir;
3use std::fs::File;
4use std::io;
5use std::io::prelude::*;
6use std::path::Path;
7
8use fluent_bundle::concurrent::FluentBundle;
9use fluent_bundle::{FluentArgs, FluentResource};
10use fluent_langneg::negotiate_languages;
11
12pub use unic_langid::{langid, langids, LanguageIdentifier};
13
14pub trait Loader {
18 fn lookup(&self, lang: &LanguageIdentifier, text_id: &str, args: Option<&FluentArgs>)
19 -> String;
20}
21
22#[macro_export]
61macro_rules! simple_loader {
62 ($constructor:ident, $location:expr, $fallback:expr) => {
63 $crate::lazy_static::lazy_static! {
64 static ref RESOURCES: std::collections::HashMap<$crate::loader::LanguageIdentifier, Vec<$crate::fluent_bundle::FluentResource>> = $crate::loader::build_resources($location);
65 static ref BUNDLES: std::collections::HashMap<$crate::loader::LanguageIdentifier, $crate::fluent_bundle::concurrent::FluentBundle<&'static $crate::fluent_bundle::FluentResource>> = $crate::loader::build_bundles(&&RESOURCES, None, |_bundle| {});
66 static ref LOCALES: Vec<$crate::loader::LanguageIdentifier> = RESOURCES.keys().cloned().collect();
67 static ref FALLBACKS: std::collections::HashMap<$crate::loader::LanguageIdentifier, Vec<$crate::loader::LanguageIdentifier>> = $crate::loader::build_fallbacks(&*LOCALES);
68 }
69
70 pub fn $constructor() -> $crate::loader::SimpleLoader {
71 $crate::loader::SimpleLoader::new(&*BUNDLES, &*FALLBACKS, $fallback.parse().expect("fallback language not valid"))
72 }
73 };
74 ($constructor:ident, $location:expr, $fallback:expr, core: $core:expr, customizer: $custom:expr) => {
75 $crate::lazy_static::lazy_static! {
76 static ref CORE_RESOURCE: $crate::fluent_bundle::FluentResource = $crate::loader::load_core_resource($core);
77 static ref RESOURCES: std::collections::HashMap<$crate::loader::LanguageIdentifier, Vec<$crate::fluent_bundle::FluentResource>> = $crate::loader::build_resources($location);
78 static ref BUNDLES: std::collections::HashMap<$crate::loader::LanguageIdentifier, $crate::fluent_bundle::concurrent::FluentBundle<&'static $crate::fluent_bundle::FluentResource>> = $crate::loader::build_bundles(&*RESOURCES, Some(&CORE_RESOURCE), $custom);
79 static ref LOCALES: Vec<$crate::loader::LanguageIdentifier> = RESOURCES.keys().cloned().collect();
80 static ref FALLBACKS: std::collections::HashMap<$crate::loader::LanguageIdentifier, Vec<$crate::loader::LanguageIdentifier>> = $crate::loader::build_fallbacks(&*LOCALES);
81 }
82
83 pub fn $constructor() -> $crate::loader::SimpleLoader {
84 $crate::loader::SimpleLoader::new(&*BUNDLES, &*FALLBACKS, $fallback.parse().expect("fallback language not valid"))
85 }
86 };
87}
88
89pub fn build_fallbacks(
90 locales: &[LanguageIdentifier],
91) -> HashMap<LanguageIdentifier, Vec<LanguageIdentifier>> {
92 let mut map = HashMap::new();
93
94 for locale in locales.iter() {
95 map.insert(
96 locale.to_owned(),
97 negotiate_languages(
98 &[locale],
99 locales,
100 None,
101 fluent_langneg::NegotiationStrategy::Filtering,
102 )
103 .into_iter()
104 .cloned()
105 .collect::<Vec<_>>(),
106 );
107 }
108
109 map
110}
111
112pub struct SimpleLoader {
115 bundles: &'static HashMap<LanguageIdentifier, FluentBundle<&'static FluentResource>>,
116 fallbacks: &'static HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
117 fallback: LanguageIdentifier,
118}
119
120impl SimpleLoader {
121 pub fn new(
125 bundles: &'static HashMap<LanguageIdentifier, FluentBundle<&'static FluentResource>>,
126 fallbacks: &'static HashMap<LanguageIdentifier, Vec<LanguageIdentifier>>,
127 fallback: LanguageIdentifier,
128 ) -> Self {
129 Self {
130 bundles,
131 fallbacks,
132 fallback,
133 }
134 }
135
136 pub fn lookup_single_language(
138 &self,
139 lang: &LanguageIdentifier,
140 text_id: &str,
141 args: Option<&FluentArgs>,
142 ) -> Option<String> {
143 if let Some(bundle) = self.bundles.get(lang) {
144 if let Some(message) = bundle.get_message(text_id).and_then(|m| m.value()) {
145 let mut errors = Vec::new();
146
147 let value = bundle.format_pattern(message, args, &mut errors);
148
149 if errors.is_empty() {
150 Some(value.into())
151 } else {
152 panic!(
153 "Failed to format a message for locale {} and id {}.\nErrors\n{:?}",
154 lang, text_id, errors
155 )
156 }
157 } else {
158 None
159 }
160 } else {
161 panic!("Unknown language {}", lang)
162 }
163 }
164
165 pub fn lookup_no_default_fallback(
167 &self,
168 lang: &LanguageIdentifier,
169 text_id: &str,
170 args: Option<&FluentArgs>,
171 ) -> Option<String> {
172 for l in self.fallbacks.get(lang).expect("language not found") {
173 if let Some(val) = self.lookup_single_language(l, text_id, args) {
174 return Some(val);
175 }
176 }
177
178 None
179 }
180}
181
182impl Loader for SimpleLoader {
183 fn lookup(
185 &self,
186 lang: &LanguageIdentifier,
187 text_id: &str,
188 args: Option<&FluentArgs>,
189 ) -> String {
190 for l in self.fallbacks.get(lang).expect("language not found") {
191 if let Some(val) = self.lookup_single_language(l, text_id, args) {
192 return val;
193 }
194 }
195 if *lang != self.fallback {
196 if let Some(val) = self.lookup_single_language(&self.fallback, text_id, args) {
197 return val;
198 }
199 }
200 format!("Unknown localization {}", text_id)
201 }
202}
203
204fn read_from_file<P: AsRef<Path>>(filename: P) -> io::Result<FluentResource> {
205 let mut file = File::open(filename)?;
206 let mut string = String::new();
207
208 file.read_to_string(&mut string)?;
209
210 Ok(FluentResource::try_new(string).expect("File did not parse!"))
211}
212
213fn read_from_dir<P: AsRef<Path>>(dirname: P) -> io::Result<Vec<FluentResource>> {
214 let mut result = Vec::new();
215 for dir_entry in read_dir(dirname)? {
216 let entry = dir_entry?;
217
218 if entry.path().extension().and_then(|e| e.to_str()) != Some("ftl") {
220 continue;
221 }
222
223 let resource = read_from_file(entry.path())?;
224 result.push(resource);
225 }
226 Ok(result)
227}
228
229pub fn create_bundle(
230 lang: LanguageIdentifier,
231 resources: &'static [FluentResource],
232 core_resource: Option<&'static FluentResource>,
233 customizer: &impl Fn(&mut FluentBundle<&'static FluentResource>),
234) -> FluentBundle<&'static FluentResource> {
235 let mut bundle: FluentBundle<&'static FluentResource> =
236 FluentBundle::new_concurrent([lang].to_vec());
237
238 bundle.set_use_isolating(false);
240 if let Some(core) = core_resource {
241 bundle
242 .add_resource(core)
243 .expect("Failed to add core resource to bundle");
244 }
245 for res in resources {
246 bundle
247 .add_resource(res)
248 .expect("Failed to add FTL resources to the bundle.");
249 }
250
251 customizer(&mut bundle);
252 bundle
253}
254
255pub fn build_resources(dir: &str) -> HashMap<LanguageIdentifier, Vec<FluentResource>> {
256 let mut all_resources = HashMap::new();
257 let entries = read_dir(dir).unwrap();
258 for entry in entries {
259 let entry = entry.unwrap();
260 if entry.file_type().unwrap().is_dir() {
261 if let Ok(lang) = entry.file_name().into_string() {
262 let resources = read_from_dir(entry.path()).unwrap();
263 all_resources.insert(lang.parse().unwrap(), resources);
264 }
265 }
266 }
267 all_resources
268}
269
270pub fn build_bundles(
271 resources: &'static HashMap<LanguageIdentifier, Vec<FluentResource>>,
272 core_resource: Option<&'static FluentResource>,
273 customizer: impl Fn(&mut FluentBundle<&'static FluentResource>),
274) -> HashMap<LanguageIdentifier, FluentBundle<&'static FluentResource>> {
275 let mut bundles = HashMap::new();
276 for (k, v) in resources.iter() {
277 bundles.insert(
278 k.clone(),
279 create_bundle(k.clone(), v, core_resource, &customizer),
280 );
281 }
282 bundles
283}
284
285pub fn load_core_resource(path: &str) -> FluentResource {
286 read_from_file(path).expect("cannot find core resource")
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use fluent_bundle::concurrent::FluentBundle;
293 use std::error::Error;
294
295 #[test]
296 fn test_load_from_dir() -> Result<(), Box<dyn Error>> {
297 let dir = tempfile::tempdir()?;
298 std::fs::write(dir.path().join("core.ftl"), "foo = bar\n".as_bytes())?;
299 std::fs::write(dir.path().join("other.ftl"), "bar = baz\n".as_bytes())?;
300 std::fs::write(dir.path().join("invalid.txt"), "baz = foo\n".as_bytes())?;
301 std::fs::write(dir.path().join(".binary_file.swp"), [0, 1, 2, 3, 4, 5])?;
302
303 let result = read_from_dir(dir.path())?;
304 assert_eq!(2, result.len()); let mut bundle = FluentBundle::new_concurrent((&[unic_langid::langid!("en-US")]).to_vec());
307 for resource in &result {
308 bundle.add_resource(resource).unwrap();
309 }
310
311 let mut errors = Vec::new();
312
313 assert_eq!(
315 "bar",
316 bundle.format_pattern(
317 bundle.get_message("foo").and_then(|m| m.value()).unwrap(),
318 None,
319 &mut errors
320 )
321 );
322
323 assert_eq!(
324 "baz",
325 bundle.format_pattern(
326 bundle.get_message("bar").and_then(|m| m.value()).unwrap(),
327 None,
328 &mut errors
329 )
330 );
331 assert_eq!(None, bundle.get_message("baz")); Ok(())
334 }
335}