aboutsummaryrefslogtreecommitdiff
path: root/src/lib.rs
blob: 074fc9d2db0d1bacc8adbfccbd69e61669cfd8f1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! # Fluent, fluently
//! A simple library providing IO for [Project Fluent](https://projectfluent.org/).
//!
//! Sample usage:
//!
//! ```rust
//! let loc = fluent_fluently::Localiser::try_load("./locale".to_string(), "en-US".to_string()).unwrap();
//! let msg = loc.get_message("hello-world", loc.available_languages.get("it"));
//! println!("{}", msg);
//! ```
//!
//! The [FluentMessage] you obtained this way will automatically fall back on `en-US` if no locale
//! of the requested type was found. Though, if you want, you `bundles` is a [HashMap], so you can
//! certainly check whether a language is available manually if you so wish.

use std::{collections::HashMap, sync::Arc};
use fluent::{bundle::FluentBundle, FluentResource, FluentArgs};
use intl_memoizer::concurrent::IntlLangMemoizer;
use unic_langid::LanguageIdentifier;
use crate::error::Result;

pub mod error;

/// Shorthand type handling the [FluentBundle]'s generic types.
type TypedFluentBundle = FluentBundle<Arc<FluentResource>, IntlLangMemoizer>;

/// The main struct of the program.
/// You can obtain a new instance by calling [`Self::try_load()`].
pub struct Localiser {
   /// A [HashMap] tying each bundle to its language identifier.
   pub bundles: HashMap<String, TypedFluentBundle>,
   /// A [HashMap] tying each *available* language identifier [String] to an actual [LanguageIdentifier].
   pub available_languages: HashMap<String, LanguageIdentifier>,
   /// The identifier of the default language.
   pub default_language: String
}

impl Localiser {
   /// Tries to create a new [Localiser] instance given a path and the name of the default language.
   /// The path's direct children will only be considered if their names are valid language codes as
   /// defined by [LanguageIdentifier], and if they are either files with the `.ftl` extension or
   /// directories. In the first case they will be read directly and converted in [FluentResource]s,
   /// in the second case the same will be done to their chilren instead.
   /// [FluentResource]s within a same folder will be considered part of a same [FluentBundle],
   /// forming a single localisation for all intents and purposes.
   pub fn try_load(path:  impl Into<String>, default_language: impl Into<String>) -> Result<Self> {
      let mut bundles = HashMap::new();
      let mut available_languages = HashMap::new();
      let paths = std::fs::read_dir(path.into())?
         .filter_map(|res| res.ok())
         .map(|dir_entry| dir_entry.path())
         .filter_map(|path| {
            if path.extension().map_or(false, |ext| ext == "ftl") || path.is_dir() {
               Some(path)
            } else {
               None
            }
         }).collect::<Vec<_>>();

      // validate default
      let default_language = default_language.into().parse::<LanguageIdentifier>()?.to_string();

      for path in paths {
         // validate filename as language code
         let language_code = path.file_stem()
            .and_then(|f| f.to_str())
            .map(|f| f.parse::<LanguageIdentifier>())
            .and_then(|id| match id {
               Ok(id) => Some(id),
               Err(_) => None
            });

         if language_code.is_none() {
            continue;
         }

         let language_code = language_code.unwrap();

         let mut bundle: TypedFluentBundle = fluent::bundle::FluentBundle::new_concurrent(vec![language_code.clone()]);
         if path.is_dir() { //is a directory
            for res in Self::path_to_resources(&path)? {
               bundle.add_resource(res)?;
            }
         } else { //is a single file
            bundle.add_resource(Self::file_to_resource(&path)?)?;
         }

         bundles.insert(language_code.to_string(), bundle);
         available_languages.insert(language_code.to_string(), language_code);
      }

      Ok(Self {
         bundles,
         available_languages,
         default_language
      })
   }

   /// Reads all files in a certain folder and all of its subfolders that have the `.ftl`
   /// extension, parses them into [FluentResource]s and returns them in a [Vec]. 
   fn path_to_resources(path: &std::path::PathBuf) -> Result<Vec<Arc<FluentResource>>> {
      let mut res = Vec::new();
      for entry in walkdir::WalkDir::new(path).follow_links(true).into_iter().filter_map(|e| e.ok()) {
         let entry_path = entry.path().to_path_buf();
         let entry_extension = entry_path.extension();
         if entry_extension.is_none() || entry_extension.unwrap() != "ftl" {
            continue;
         }

         res.push(Self::file_to_resource(&entry_path)?);
      }
      Ok(res)
   }

   /// Reads the file at the given path, and tries to parse it into a [FluentResource].
   fn file_to_resource(path: &std::path::PathBuf) -> Result<Arc<FluentResource>> {
      Ok(Arc::new(FluentResource::try_new(std::fs::read_to_string(path)?)?))
   }

   /// Extracts a message from the requested bundle, or from the default one if absent. 
   pub fn try_get_message(&self, key: impl Into<String>, language: impl Into<String>, args: Option<&FluentArgs>) -> Result<String> {
      let key = key.into();
      let language = language.into();

      let bundle = self.bundles.get(&language)
         .or_else(|| self.bundles.get(&self.default_language))
         .ok_or(error::Error::GenericError("Failed to get default bundle! This is not supposed to happen!".to_string()))?;

      let pattern = bundle.get_message(&key)
         .and_then(|msg| msg.value())
         .ok_or(error::Error::MissingMessageError(format!("No such message {} for language {}!", key, language)))?;

      let mut err = Vec::new();
      let res = bundle.format_pattern(pattern, args, &mut err);
      if err.is_empty() {
         Ok(res.to_string())
      } else {
         Err(error::Error::FluentError(err))
      }
   }

   /// Similar to [Localiser::try_get_message], but returns the given key on failure.
   pub fn get_message(&self, key: impl Into<String>, language: impl Into<String>, args: Option<&FluentArgs>) -> String {
      let key = key.into();
      self.try_get_message(&key, language, args)
         .unwrap_or(key)
   }
}