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#[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 { 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
270pub 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}