1pub use fluent::FluentArgs as Arguments;
8
9use icu::locid::Locale;
10use std::{
11 cell::{Cell}, collections::{HashMap, HashSet}, sync::{Arc, RwLock},
12};
13use rialight_util::{hashmap, hashset};
14
15pub macro arguments {
28 ($($key:expr => $value:expr,)+) => {
29 {
30 #[allow(unused_mut)]
31 let mut r_map = ::fluent::FluentArgs::new();
32 $(
33 let _ = r_map.set($key.to_string(), Box::new($value));
34 )*
35 r_map
36 }
37 },
38 ($($key:expr => $value:expr),*) => {
39 {
40 #[allow(unused_mut)]
41 let mut r_map = ::fluent::FluentArgs::new();
42 $(
43 let _ = r_map.set($key.to_string(), Box::new($value));
44 )*
45 r_map
46 }
47 }
48}
49
50pub struct Ftl {
52 m_current_locale: RwLock<Option<Locale>>,
53 m_locale_to_path_components: Arc<HashMap<Locale, String>>,
60 m_supported_locales: Arc<HashSet<Locale>>,
61 m_default_locale: Locale,
62 m_fallbacks: Arc<HashMap<Locale, Vec<Locale>>>,
63 m_locale_initializers: Arc<RwLock<Vec<fn(Locale, Arc<fluent::FluentBundle<fluent::FluentResource>>)>>>,
64 m_assets: Arc<RwLock<HashMap<Locale, Arc<fluent::FluentBundle<fluent::FluentResource>>>>>,
65 m_assets_source: String,
66 m_assets_files: Vec<String>,
67 m_assets_clean_unused: bool,
68 m_assets_load_method: FtlLoadMethod,
69}
70
71fn parse_locale_or_panic(s: &str) -> Locale {
72 Locale::try_from_bytes(s.as_bytes()).expect((format!("{} is a malformed locale.", s)).as_ref())
73}
74
75fn locale_to_unic_langid_impl_langid(locale: &Locale) -> unic_langid_impl::LanguageIdentifier {
76 unic_langid_impl::LanguageIdentifier::from_bytes(locale.id.to_string().as_bytes()).unwrap()
77}
78
79fn add_ftl_bundle_resource(file_name: String, source: String, bundle: &mut fluent::FluentBundle<fluent::FluentResource>) -> bool {
80 match fluent::FluentResource::try_new(source) {
81 Ok(res) => {
82 if let Err(error_list) = bundle.add_resource(res) {
83 for e in error_list {
84 println!("Error at {}.ftl: {}", file_name, e.to_string());
85 }
86 return false;
87 }
88 },
89 Err((_, error_list)) => {
90 for e in error_list {
91 println!("Syntax error at {}.ftl: {}", file_name, e);
92 }
93 return false;
94 },
95 }
96 true
97}
98
99impl Ftl {
100 pub fn new(options: &mut FtlOptions) -> Self {
102 let mut locale_to_path_components = HashMap::<Locale, String>::new();
103 let mut supported_locales = HashSet::<Locale>::new();
104 for unparsed_locale in options.m_supported_locales.get_mut().unwrap().iter() {
105 let parsed_locale = parse_locale_or_panic(unparsed_locale);
106 locale_to_path_components.insert(parsed_locale.clone(), unparsed_locale.clone());
107 supported_locales.insert(parsed_locale);
108 }
109 let mut fallbacks = HashMap::<Locale, Vec<Locale>>::new();
110 for (k, v) in options.m_fallbacks.get_mut().unwrap().iter() {
111 fallbacks.insert(parse_locale_or_panic(k), v.iter().map(|s| parse_locale_or_panic(s)).collect());
112 }
113 let default_locale = options.m_default_locale.get_mut().unwrap().clone();
114 Self {
115 m_current_locale: RwLock::new(None),
116 m_locale_to_path_components: Arc::new(locale_to_path_components),
117 m_supported_locales: Arc::new(supported_locales),
118 m_default_locale: parse_locale_or_panic(&default_locale),
119 m_fallbacks: Arc::new(fallbacks),
120 m_locale_initializers: Arc::new(RwLock::new(vec![])),
121 m_assets: Arc::new(RwLock::new(HashMap::new())),
122 m_assets_source: options.m_assets.get_mut().unwrap().m_source.get_mut().unwrap().clone(),
123 m_assets_files: options.m_assets.get_mut().unwrap().m_files.get_mut().unwrap().iter().map(|s| s.clone()).collect(),
124 m_assets_clean_unused: options.m_assets.get_mut().unwrap().m_clean_unused.get(),
125 m_assets_load_method: options.m_assets.get_mut().unwrap().m_load_method.get(),
126 }
127 }
128
129 pub fn supported_locales(&self) -> HashSet<Locale> {
132 self.m_supported_locales.as_ref().clone()
133 }
134
135 pub fn supports_locale(&self, arg: &Locale) -> bool {
139 self.m_supported_locales.contains(arg)
140 }
141
142 pub fn current_locale(&self) -> Option<Locale> {
144 self.m_current_locale.read().unwrap().clone()
145 }
146
147 pub fn locale_and_fallbacks(&self) -> HashSet<Locale> {
149 if let Some(c) = self.current_locale() {
150 let mut r: HashSet<Locale> = hashset![c.clone()];
151 self.enumerate_fallbacks(c.clone(), &mut r);
152 return r;
153 }
154 hashset![]
155 }
156
157 pub fn fallbacks(&self) -> HashSet<Locale> {
159 if let Some(c) = self.current_locale() {
160 let mut r: HashSet<Locale> = hashset![];
161 self.enumerate_fallbacks(c.clone(), &mut r);
162 return r;
163 }
164 hashset![]
165 }
166
167 pub fn initialize_locale(&self, callback: fn(Locale, Arc<fluent::FluentBundle<fluent::FluentResource>>)) {
170 self.m_locale_initializers.write().unwrap().push(callback);
171 }
172
173 pub async fn load(&self, mut new_locale: Option<Locale>) -> bool {
180 if new_locale.is_none() {
181 new_locale = Some(self.m_default_locale.clone());
182 }
183 let new_locale = new_locale.unwrap();
184 if !self.supports_locale(&new_locale) {
185 panic!("Unsupported locale: {}", new_locale);
186 }
187 let mut to_load: HashSet<Locale> = hashset![new_locale.clone()];
188 self.enumerate_fallbacks(new_locale.clone(), &mut to_load);
189
190 let mut new_assets: HashMap<Locale, Arc<fluent::FluentBundle<fluent::FluentResource>>> = hashmap![];
191 for locale in to_load {
192 let res = self.load_single_locale(&locale).await;
193 if res.is_none() {
194 return false;
195 }
196 new_assets.insert(locale.clone(), res.unwrap());
197 }
198 if self.m_assets_clean_unused {
199 self.m_assets.write().unwrap().clear();
200 }
201
202 for (locale, bundle) in new_assets {
203 self.m_assets.write().unwrap().insert(locale, bundle.clone());
204 }
205 *self.m_current_locale.write().unwrap() = Some(new_locale.clone());
206 for c in self.m_locale_initializers.read().unwrap().iter() {
207 c(new_locale.clone(), self.m_assets.read().unwrap()[&new_locale.clone()].clone());
208 }
209
210 true
211 }
212
213 async fn load_single_locale(&self, locale: &Locale) -> Option<Arc<fluent::FluentBundle<fluent::FluentResource>>> {
214 let mut r = fluent::FluentBundle::new(vec![locale_to_unic_langid_impl_langid(locale)]);
215 match self.m_assets_load_method {
216 FtlLoadMethod::FileSystem => {
217 for file_name in self.m_assets_files.iter() {
218 let locale_path_comp = self.m_locale_to_path_components.get(locale);
219 if locale_path_comp.is_none() {
220 panic!("Fallback is not supported a locale: {}", locale.to_string());
221 }
222 let res_path = format!("{}/{}/{}.ftl", self.m_assets_source, locale_path_comp.unwrap(), file_name);
223 let source = rialight_filesystem::File::new(res_path.clone()).read_bytes();
224 if source.is_err() {
225 println!("Failed to load resource at {}.", res_path);
226 return None;
227 }
228 let source = String::from_utf8(source.unwrap()).unwrap();
229 if !add_ftl_bundle_resource(file_name.clone(), source, &mut r) {
230 return None;
231 }
232 }
233 },
234 FtlLoadMethod::Http => {
235 for file_name in self.m_assets_files.iter() {
236 let locale_path_comp = self.m_locale_to_path_components.get(locale);
237 if locale_path_comp.is_none() {
238 panic!("Fallback is not supported a locale: {}", locale.to_string());
239 }
240 let res_path = format!("{}/{}/{}.ftl", self.m_assets_source, locale_path_comp.unwrap(), file_name);
241 let source = reqwest::get(reqwest::Url::parse(res_path.clone().as_ref()).unwrap()).await;
242 if source.is_err() {
243 println!("Failed to load resource at {}.", res_path);
244 return None;
245 }
246 let source = source.unwrap().text().await;
247 if source.is_err() {
248 println!("Failed to load resource at {}.", res_path);
249 return None;
250 }
251 let source = source.unwrap();
252 if !add_ftl_bundle_resource(file_name.clone(), source, &mut r) {
253 return None;
254 }
255 }
256 },
257 }
258 Some(Arc::new(r))
259 }
260
261 fn enumerate_fallbacks(&self, locale: Locale, output: &mut HashSet<Locale>) {
262 for list in self.m_fallbacks.get(&locale).iter() {
263 for item in list.iter() {
264 output.insert(item.clone());
265 self.enumerate_fallbacks(item.clone(), output);
266 }
267 }
268 }
269
270 pub fn get_message(&self, id: &str, args: Option<&Arguments>, errors: &mut Vec<fluent::FluentError>) -> Option<String> {
271 self.get_message_by_locale(id, self.m_current_locale.read().unwrap().clone()?, args, errors)
272 }
273
274 fn get_message_by_locale(&self, id: &str, locale: Locale, args: Option<&Arguments>, errors: &mut Vec<fluent::FluentError>) -> Option<String> {
275 if let Some(assets) = self.m_assets.read().unwrap().get(&locale) {
276 if let Some(message) = assets.get_message(id) {
277 return Some(self.format_pattern(message.value()?, args, errors));
278 }
279 }
280
281 let fallbacks = self.m_fallbacks.get(&locale);
282 if fallbacks.is_some() {
283 for fl in fallbacks.unwrap().iter() {
284 let r = self.get_message_by_locale(id, fl.clone(), args, errors);
285 if r.is_some() {
286 return r;
287 }
288 }
289 }
290 None
291 }
292
293 pub fn has_message(&self, id: &str) -> bool {
294 let locale = self.m_current_locale.read().unwrap().clone();
295 if locale.is_none() {
296 return false;
297 }
298 self.has_message_by_locale(id, locale.unwrap())
299 }
300
301 fn has_message_by_locale(&self, id: &str, locale: Locale) -> bool {
302 let assets = self.m_assets.read().unwrap();
303 let assets = assets.get(&locale);
304 if assets.is_some() {
305 if assets.unwrap().has_message(id) {
306 return true;
307 }
308 }
309
310 let fallbacks = self.m_fallbacks.get(&locale);
311 if fallbacks.is_some() {
312 for fl in fallbacks.unwrap().iter() {
313 let r = self.has_message_by_locale(id, fl.clone());
314 if r {
315 return true;
316 }
317 }
318 }
319 false
320 }
321
322 pub fn format_pattern(&self, pattern: &fluent_syntax::ast::Pattern<&str>, args: Option<&Arguments>, errors: &mut Vec<fluent::FluentError>) -> String {
323 let locale = self.m_current_locale.read().unwrap().clone();
324 if locale.is_none() {
325 return "".to_owned();
326 }
327 let asset = &self.m_assets.read().unwrap()[&locale.unwrap()];
328 asset.format_pattern(pattern, args, errors).into_owned().to_owned()
329 }
330}
331
332impl Clone for Ftl {
333 fn clone(&self) -> Self {
334 Self {
335 m_current_locale: RwLock::new(self.m_current_locale.read().unwrap().clone()),
336 m_locale_to_path_components: self.m_locale_to_path_components.clone(),
337 m_supported_locales: self.m_supported_locales.clone(),
338 m_default_locale: self.m_default_locale.clone(),
339 m_fallbacks: self.m_fallbacks.clone(),
340 m_locale_initializers: self.m_locale_initializers.clone(),
341 m_assets: self.m_assets.clone(),
342 m_assets_source: self.m_assets_source.clone(),
343 m_assets_files: self.m_assets_files.clone(),
344 m_assets_clean_unused: self.m_assets_clean_unused,
345 m_assets_load_method: self.m_assets_load_method,
346 }
347 }
348}
349
350pub struct FtlOptions {
352 m_default_locale: RwLock<String>,
353 m_supported_locales: RwLock<Vec<String>>,
354 m_fallbacks: RwLock<HashMap<String, Vec<String>>>,
355 m_assets: RwLock<FtlOptionsForAssets>,
356}
357
358impl FtlOptions {
359 pub fn new() -> Self {
360 FtlOptions {
361 m_default_locale: RwLock::new("en".to_string()),
362 m_supported_locales: RwLock::new(vec!["en".to_string()]),
363 m_fallbacks: RwLock::new(hashmap! {}),
364 m_assets: RwLock::new(FtlOptionsForAssets::new()),
365 }
366 }
367
368 pub fn default_locale(&mut self, value: impl AsRef<str>) -> &mut Self {
369 *self.m_default_locale.write().unwrap() = value.as_ref().to_owned();
370 self
371 }
372
373 pub fn supported_locales(&mut self, list: Vec<impl AsRef<str>>) -> &mut Self {
374 *self.m_supported_locales.write().unwrap() = list.iter().map(|name| name.as_ref().to_owned()).collect();
375 self
376 }
377
378 pub fn fallbacks(&mut self, map: HashMap<impl AsRef<str>, Vec<impl AsRef<str>>>) -> &mut Self {
379 *self.m_fallbacks.write().unwrap() = map.iter().map(|(k, v)| (
380 k.as_ref().to_owned(),
381 v.iter().map(|s| s.as_ref().to_owned()).collect()
382 )).collect();
383 self
384 }
385
386 pub fn assets(&mut self, options: &FtlOptionsForAssets) -> &mut Self {
387 *self.m_assets.write().unwrap() = options.clone();
388 self
389 }
390}
391
392pub struct FtlOptionsForAssets {
393 m_source: RwLock<String>,
394 m_files: RwLock<Vec<String>>,
395 m_clean_unused: Cell<bool>,
396 m_load_method: Cell<FtlLoadMethod>,
397}
398
399impl Clone for FtlOptionsForAssets {
400 fn clone(&self) -> Self {
401 Self {
402 m_source: RwLock::new(self.m_source.read().unwrap().clone()),
403 m_files: RwLock::new(self.m_files.read().unwrap().clone()),
404 m_clean_unused: self.m_clean_unused.clone(),
405 m_load_method: self.m_load_method.clone(),
406 }
407 }
408}
409
410impl FtlOptionsForAssets {
411 pub fn new() -> Self {
412 FtlOptionsForAssets {
413 m_source: RwLock::new("res/lang".to_string()),
414 m_files: RwLock::new(vec![]),
415 m_clean_unused: Cell::new(true),
416 m_load_method: Cell::new(FtlLoadMethod::Http),
417 }
418 }
419
420 pub fn source(&mut self, src: impl AsRef<str>) -> &mut Self {
421 *self.m_source.write().unwrap() = src.as_ref().to_owned();
422 self
423 }
424
425 pub fn files(&mut self, list: Vec<impl AsRef<str>>) -> &mut Self {
426 *self.m_files.write().unwrap() = list.iter().map(|name| name.as_ref().to_owned()).collect();
427 self
428 }
429
430 pub fn clean_unused(&mut self, value: bool) -> &mut Self {
431 self.m_clean_unused.set(value);
432 self
433 }
434
435 pub fn load_method(&mut self, value: FtlLoadMethod) -> &mut Self {
436 self.m_load_method.set(value);
437 self
438 }
439}
440
441#[derive(Copy, Clone, PartialEq)]
442pub enum FtlLoadMethod {
443 FileSystem,
444 Http,
445}