my_fluent_rs_helper/
lib.rs

1use fluent::{FluentArgs, FluentValue, FluentBundle, FluentResource};
2use std::{
3    io,
4    env,
5    fs,
6    sync::{Mutex, Arc, OnceLock},
7    io::Read,
8    path::PathBuf,
9};
10
11use unic_langid::LanguageIdentifier;
12
13
14
15/// fluent functions
16#[derive(Debug, )]
17pub enum LanguageChoiceError {
18    IoError(io::Error),
19    NoLanguageFilesAt(String),
20    LanguageNegotiatedFailed(String, Vec<String>, ),
21}
22
23impl From<io::Error> for LanguageChoiceError {
24    fn from(e: io::Error) -> Self {
25        Self::IoError(e)
26    }
27}
28
29fn language_matches_score(l1: &LanguageIdentifier, l2: &LanguageIdentifier) -> u8 {
30    let mut base = 0u8;
31    base |= if l1.matches(l2, false, false) { 0b1000 } else { 0 };
32    base |= if l1.matches(l2, false, true) { 0b0100 } else { 0 };
33    base |= if l1.matches(l2, true, false) { 0b0010 } else { 0 };
34    base |= if l1.matches(l2, true, true) { 0b0001 } else { 0 };
35    base
36}
37
38#[derive(Debug)]
39struct LanguageDeductionHelperS {
40    pub lid: LanguageIdentifier,
41    pub lang_name: String,
42    pub dir_path: PathBuf,
43    pub score: u8,
44}
45
46fn resolve_desired_lang(lang_name: Option<String>, lang_dir: &PathBuf) 
47    -> Result<Vec<LanguageDeductionHelperS>, LanguageChoiceError> {
48        if !lang_dir.exists() || !lang_dir.is_dir() {
49            return Err(LanguageChoiceError::NoLanguageFilesAt(
50                    lang_dir.canonicalize()
51                    .unwrap()
52                    .to_string_lossy()
53                    .into_owned()
54                    ));
55        }
56
57        let (desired_lang_identifier, desired_dirname) = match &lang_name {
58            Some(lang) => {
59                (lang.parse::<LanguageIdentifier>()
60                    .expect(&format!("MFH: Parse {lang} as language identifier failed.")),
61                    lang.clone())
62            },
63            None => {
64                let n = sys_locale::get_locale()
65                    .expect(&format!("MFH Get system locale failed."));
66                let li = n.clone()
67                    .parse::<LanguageIdentifier>()
68                    .expect("MFH: System's default locale parses failed.");
69                (li, n)
70            }
71        };
72
73        let available_langs = {
74            let mut available_langs = Vec::new();
75            let read_dir = fs::read_dir(lang_dir)
76                .expect(&format!("MFH: Read dir {:?} failed.", lang_dir));
77            for dir in read_dir {
78                let dir_ent = dir.expect(&format!("MFH: Read a dir entry in {:?} failed.", lang_dir));
79                let dir_path = dir_ent.path();
80
81                let dirname = {
82                    let os_name = dir_ent.file_name();
83                    os_name.to_str()
84                        .expect(&format!("MFH: OsString {:?} converts to String failed.", &os_name)).to_owned()
85                };
86                match &dirname.parse::<LanguageIdentifier>() {
87                    Ok(id) => {
88                        let tmp = LanguageDeductionHelperS {
89                            lid: id.clone(),
90                            lang_name: dirname,
91                            dir_path,
92                            score: language_matches_score(&id, &desired_lang_identifier)
93                        };
94                        available_langs.push(tmp);
95                    },
96                    Err(_e) => {
97                    }
98                }
99            }
100            available_langs.sort_by_cached_key(|a| { a.lang_name.clone() });
101            available_langs.sort_by(|a, b| { b.score.cmp(&a.score) });
102            available_langs
103        };
104        if !available_langs.is_empty() {
105            Ok(available_langs)
106        } else {
107            Err(LanguageChoiceError::LanguageNegotiatedFailed(desired_dirname, available_langs.into_iter()
108                    .map(|a| {
109                        a.lang_name
110                    })
111                    .collect()
112                    ))
113        }
114   }
115
116pub struct LanguageSystem {
117    pub bundle: fluent::FluentBundle<FluentResource>,
118    pub current_lang: LanguageIdentifier,
119    pub current_lang_dir_path: PathBuf,
120}
121
122unsafe impl Sync for LanguageSystem {}
123unsafe impl Send for LanguageSystem {}
124
125static LANG: OnceLock<Mutex<Arc<LanguageSystem>>> = OnceLock::new();
126
127impl LanguageSystem {
128    pub fn new(desired_lang: Option<String>, lang_dir: Option<String>) -> Self {
129
130        let default_lang_dir_str = "i18n/fluent".to_owned();
131        let lang_dir = lang_dir.or(Some(default_lang_dir_str)).unwrap();
132        let lang_dir = {
133            let mut tmp = env::current_dir()
134                .expect("MFH: Get current dir failed.");
135            tmp.extend(lang_dir.split("/"));
136            tmp
137        };
138
139
140        let ordered_langs = resolve_desired_lang(desired_lang.clone(), &lang_dir)
141            .expect(&format!("MFH: Fetch languages {:?} failed.", desired_lang));
142        let v = ordered_langs
143            .iter()
144            .map(|a| { a.lid.clone() })
145            .collect();
146        let mut bundle = FluentBundle::new(v);
147            
148        let desired_lang_helper_s = &ordered_langs.first().unwrap();
149
150        { // add ftl files under desired directory to bundle.
151            let read_dir = fs::read_dir(&desired_lang_helper_s.dir_path)
152                .expect(&format!("MFH: Read language dir {:?} failed", &desired_lang_helper_s.dir_path));
153
154            for entry in read_dir {
155                if let Ok(dir_entry) = entry {
156                    let path = dir_entry.path();
157                    if path.is_file() && path.extension().is_some()
158                        && path.extension().unwrap() == "ftl" {
159                            {
160                                let mut f = fs::File::open(path)
161                                    .expect("MFH: Failed to open one of ftl files.");
162                                let mut s = String::new();
163                                f.read_to_string(&mut s).expect("read ftl file to string failed.");
164                                let r = FluentResource::try_new(s)
165                                    .expect("MFH: Could not parse an FTL string.");
166                                bundle.add_resource(r)
167                                    .expect("MFH: Failed to add FTL resources to the bundle.");
168                                }
169                    }
170                }
171            }
172        }
173
174        Self {
175            bundle,
176            current_lang: desired_lang_helper_s.lid.clone(),
177            current_lang_dir_path: desired_lang_helper_s.dir_path.clone(),
178        }
179    }
180}
181
182pub fn build_language_0<'a>(msg_key: &str) -> String {
183    match LANG.get()
184        .expect("MFH: Uninitialized language bundle.").lock() {
185        Ok(bs) => {
186            
187            let msg = bs.bundle
188                .get_message(msg_key)
189                .expect(&format!("MFH: Failed to find message {msg_key}"));
190            let mut errors = vec![];
191            let pattern = msg.value()
192                .expect("MFH: Message has no value.");
193            let v = bs.bundle.format_pattern(pattern, None, &mut errors);
194            v.to_string()
195        },
196        Err(e) => {
197            panic!("MFH: Language bundle mutext poisoned. {e}");
198        }
199    }
200}
201
202pub fn build_language_1<'a, T>(msg_key: &str, arg_name: &str, v: T) -> String
203    where T: Into<FluentValue<'a>> {
204    build_language(msg_key, 
205        vec![(arg_name, v.into())])
206}
207
208pub fn build_language_2<'a, T, R>(msg_key: &str, arg1_name: &str, v1: T, arg2_name: &str, v2: R) -> String
209    where T: Into<FluentValue<'a>>,
210          R: Into<FluentValue<'a>>,
211{
212    build_language(msg_key, 
213        vec![(arg1_name, v1.into()),
214        (arg2_name, v2.into()),
215        ])
216}
217
218pub fn build_language_3<'a, T, R>(msg_key: &str, arg1_name: &str, v1: T, 
219    arg2_name: &str, v2: R, 
220    arg3_name: &str, v3: R) -> String
221    where T: Into<FluentValue<'a>>,
222          R: Into<FluentValue<'a>>,
223{
224    build_language(msg_key, 
225        vec![(arg1_name, v1.into()),
226        (arg2_name, v2.into()),
227        (arg3_name, v3.into()),
228        ])
229}
230
231
232pub fn build_language_fns<'a, F>(msg_key: &str, args_pairs_builders: Vec<(&str, F)>) -> String 
233where F: FnOnce() -> FluentValue<'a>{
234    let args_pairs: Vec<_> = args_pairs_builders.into_iter()
235        .map(
236            |a| {
237            (a.0,
238             a.1())
239            }
240            )
241        .collect();
242    build_language(msg_key, args_pairs)
243}
244
245pub fn build_language<'a>(msg_key: &str, args_pairs: Vec<(&str, FluentValue)>) -> String {
246    if let Ok(bs) = LANG.get().expect("MFH: Uninitialized language bundle.").lock() {
247        let msg = bs
248            .bundle
249            .get_message(msg_key)
250            .expect("MFH: Failed to find message {msg_key}");
251
252        let pattern = msg.value()
253            .expect("MFH: Message has no value");
254
255        let mut args  = FluentArgs::new();
256        for kv in args_pairs {
257            args.set(kv.0, 
258                kv.1);
259        }
260
261        let mut errors = vec![];
262        let value = bs.bundle.format_pattern(pattern, Some(&args), &mut errors);
263        value.to_string()
264    } else {
265        panic!("MFH: Language bundle mutex poisoned")
266    }
267}
268
269
270///
271/// run before use any build_language_* functions
272/// if desired_lang is None, use the sys_locale::get_locale function.
273/// if lang_dir is None, use the dir [`i18n/fluent`]
274///
275pub fn init_lang(desired_lang: Option<String>, lang_dir: Option<String>) {
276    if LANG
277        .set(Mutex::new(
278                Arc::new(LanguageSystem::new(desired_lang, lang_dir))))
279            .is_err()  {
280                panic!("MFH: set initialized Language system failed.");
281    }
282}
283
284
285
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn it_works() {
293    }
294}