1use fluent::concurrent::FluentBundle;
7use fluent::{FluentArgs, FluentError, FluentResource};
8use fluent_syntax::parser::ParserError;
9use std::collections::hash_map::Entry;
10use std::collections::HashMap;
11use std::error;
12use std::fmt;
13use std::fs::File;
14use std::io;
15use std::io::Read;
16use std::path::Path;
17use std::string::FromUtf8Error;
18use std::sync::{Arc, RwLock};
19use unic_langid::LanguageIdentifier;
20
21#[derive(Debug)]
22pub enum Error {
23 FileEncodingError(FromUtf8Error),
25 FluentError(Vec<FluentError>),
27 FluentParserError(Vec<ParserError>),
29 IOError(io::Error),
31 NoMatchingMessage(String),
33}
34
35impl error::Error for Error {
36 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
37 match self {
38 Error::FileEncodingError(error) => Some(error),
39 Error::NoMatchingMessage(_) => None,
40 Error::FluentParserError(_) => None,
41 Error::FluentError(_) => None,
42 Error::IOError(error) => Some(error),
43 }
44 }
45}
46
47impl fmt::Display for Error {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match self {
50 Error::FileEncodingError(error) => {
51 write!(f, "Translation file has an encoding problem: {}", error)
52 }
53 Error::FluentError(errs) => write!(f, "Fluent Error: {:?}", errs),
54 Error::FluentParserError(errs) => write!(f, "Fluent Parser Error: {:?}", errs),
55 Error::IOError(error) => write!(f, "IO Error: {}", error),
56 Error::NoMatchingMessage(id) => write!(f, "No matching message for {}", id),
57 }
58 }
59}
60
61impl From<(FluentResource, Vec<ParserError>)> for Error {
62 fn from(inp: (FluentResource, Vec<ParserError>)) -> Self {
63 let (_, error) = inp;
64 Error::FluentParserError(error)
65 }
66}
67
68impl From<Vec<ParserError>> for Error {
69 fn from(error: Vec<ParserError>) -> Self {
70 Error::FluentParserError(error)
71 }
72}
73
74impl From<Vec<FluentError>> for Error {
75 fn from(error: Vec<FluentError>) -> Self {
76 Error::FluentError(error)
77 }
78}
79
80impl From<io::Error> for Error {
81 fn from(error: io::Error) -> Self {
82 Error::IOError(error)
83 }
84}
85
86impl From<FromUtf8Error> for Error {
87 fn from(error: FromUtf8Error) -> Self {
88 Error::FileEncodingError(error)
89 }
90}
91
92#[derive(Clone, Default)]
93pub struct FluentErgo {
94 languages: Vec<LanguageIdentifier>,
95 bundles: Arc<RwLock<HashMap<LanguageIdentifier, FluentBundle<FluentResource>>>>,
96}
97
98impl fmt::Debug for FluentErgo {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 write!(f, "FluentErgo")
101 }
107}
108
109impl FluentErgo {
111 pub fn new(languages: &[LanguageIdentifier]) -> FluentErgo {
130 FluentErgo {
131 languages: Vec::from(languages),
132 bundles: Arc::new(RwLock::new(HashMap::new())),
133 }
134 }
135
136 pub fn add_from_text(&mut self, lang: LanguageIdentifier, text: String) -> Result<(), Error> {
150 let res = FluentResource::try_new(text)?;
151 let mut bundles = self.bundles.write().unwrap();
152 let entry = bundles.entry(lang.clone());
153 match entry {
154 Entry::Occupied(mut e) => {
155 let bundle = e.get_mut();
156 bundle.add_resource(res).map_err(|err| Error::from(err))
157 }
158 Entry::Vacant(e) => {
159 let mut bundle = FluentBundle::new(&[lang]);
160 bundle.add_resource(res).map_err(|err| Error::from(err))?;
161 e.insert(bundle);
162 Ok(())
163 }
164 }?;
165 Ok(())
166 }
167
168 pub fn add_from_file(&mut self, lang: LanguageIdentifier, path: &Path) -> Result<(), Error> {
182 let mut v = Vec::new();
183 let mut f = File::open(path)?;
184 f.read_to_end(&mut v)?;
185 String::from_utf8(v)
186 .map_err(Error::FileEncodingError)
187 .and_then(|s| self.add_from_text(lang, s))
188 }
189
190 pub fn tr(&self, msgid: &str, args: Option<&FluentArgs>) -> Result<String, Error> {
229 let bundles = self.bundles.read().unwrap();
230 let result: Option<String> = self
231 .languages
232 .iter()
233 .map(|lang| {
234 let bundle = bundles.get(lang)?;
235 self.tr_(bundle, msgid, args)
236 })
237 .filter(|v| v.is_some())
238 .map(|v| v.unwrap())
239 .next();
240
241 match result {
242 Some(r) => Ok(r),
243 _ => Err(Error::NoMatchingMessage(String::from(msgid))),
244 }
245 }
246
247 fn tr_(
248 &self,
249 bundle: &FluentBundle<FluentResource>,
250 msgid: &str,
251 args: Option<&FluentArgs>,
252 ) -> Option<String> {
253 let mut errors = vec![];
254 let pattern = bundle.get_message(msgid).and_then(|msg| msg.value);
255 let res = match pattern {
256 None => None,
257 Some(p) => {
258 let res = bundle.format_pattern(&p, args, &mut errors);
259 if errors.len() > 0 {
260 println!("Errors in formatting: {:?}", errors)
261 }
262
263 Some(String::from(res))
264 }
265 };
266 match res {
267 Some(mut tr_string) => {
268 tr_string.retain(|v| v != '\u{2068}' && v != '\u{2069}');
269 Some(tr_string)
270 }
271 None => None,
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::FluentErgo;
279 use fluent::{FluentArgs, FluentValue};
280 use unic_langid::LanguageIdentifier;
281
282 const EN_TRANSLATIONS: &'static str = "
283preferences = Preferences
284history = History
285time_display = {$time} during the day
286nested_display = nesting a time display: {time_display}
287";
288
289 const EO_TRANSLATIONS: &'static str = "
290history = Historio
291";
292
293 #[test]
294 fn translations() {
295 let en_id = "en-US".parse::<LanguageIdentifier>().unwrap();
296 let mut fluent = FluentErgo::new(&vec![en_id.clone()]);
297 fluent
298 .add_from_text(en_id, String::from(EN_TRANSLATIONS))
299 .expect("text should load");
300 assert_eq!(
301 fluent.tr("preferences", None).unwrap(),
302 String::from("Preferences")
303 );
304 }
305
306 #[test]
307 fn translation_fallback() {
308 let eo_id = "eo".parse::<LanguageIdentifier>().unwrap();
309 let en_id = "en".parse::<LanguageIdentifier>().unwrap();
310 let mut fluent = FluentErgo::new(&vec![eo_id.clone(), en_id.clone()]);
311 fluent
312 .add_from_text(en_id, String::from(EN_TRANSLATIONS))
313 .expect("text should load");
314 fluent
315 .add_from_text(eo_id, String::from(EO_TRANSLATIONS))
316 .expect("text should load");
317 assert_eq!(
318 fluent.tr("preferences", None).unwrap(),
319 String::from("Preferences")
320 );
321 assert_eq!(
322 fluent.tr("history", None).unwrap(),
323 String::from("Historio")
324 );
325 }
326
327 #[test]
328 fn placeholder_insertion_should_strip_placeholder_markers() {
329 let en_id = "en".parse::<LanguageIdentifier>().unwrap();
330 let mut fluent = FluentErgo::new(&vec![en_id.clone()]);
331 fluent
332 .add_from_text(en_id, String::from(EN_TRANSLATIONS))
333 .expect("text should load");
334 let mut args = FluentArgs::new();
335 args.insert("time", FluentValue::from(String::from("13:00")));
336 assert_eq!(
337 fluent.tr("time_display", Some(&args)).unwrap(),
338 String::from("13:00 during the day")
339 );
340 }
341
342 #[test]
343 fn placeholder_insertion_should_strip_nested_placeholder_markers() {
344 let en_id = "en".parse::<LanguageIdentifier>().unwrap();
345 let mut fluent = FluentErgo::new(&vec![en_id.clone()]);
346 fluent
347 .add_from_text(en_id, String::from(EN_TRANSLATIONS))
348 .expect("text should load");
349 let mut args = FluentArgs::new();
350 args.insert("time", FluentValue::from(String::from("13:00")));
351 assert_eq!(
352 fluent.tr("nested_display", Some(&args)).unwrap(),
353 String::from("nesting a time display: 13:00 during the day")
354 );
355 }
356
357 #[test]
358 fn test_send() {
359 fn assert_send<T: Send>() {}
360 assert_send::<FluentErgo>();
361 }
362
363 #[test]
364 fn test_sync() {
365 fn assert_sync<T: Sync>() {}
366 assert_sync::<FluentErgo>();
367 }
368}