use crate::config::Config; use fluent_bundle::{bundle::FluentBundle, FluentArgs, FluentResource, FluentValue}; use fluent_langneg::{ accepted_languages, negotiate::filter_matches, negotiate_languages, NegotiationStrategy, }; use intl_memoizer::concurrent::IntlLangMemoizer; use log::error; use std::{borrow::Cow, collections::HashMap, ops::Deref, str::FromStr}; use unic_langid::{langid, LanguageIdentifier}; static LOCALE_FILES: &[(LanguageIdentifier, &str)] = &[ (langid!("en"), include_str!("../locales/en.ftl")), (langid!("fr"), include_str!("../locales/fr.ftl")), ]; pub struct Locales { bundles: HashMap>, pub default_lang: LanguageIdentifier, langs: Vec, } impl Locales { pub fn new(config: &Config) -> Self { let mut langs = Vec::new(); Self { bundles: LOCALE_FILES .iter() .map(|(lang, raw)| { let mut bundle = FluentBundle::new_concurrent(vec![lang.clone()]); // We don't want dangerous zero-width bidi chars everywhere! (issue #26) bundle.set_use_isolating(false); bundle .add_resource( FluentResource::try_new(raw.to_string()).unwrap_or_else(|e| { panic!("Failed parsing `{lang}` locale: {e:?}") }), ) .unwrap(); langs.push(lang.clone()); (lang.clone(), bundle) }) .collect::>>( ), default_lang: filter_matches( &[LanguageIdentifier::from_str(&config.default_lang) .expect("Invalid default language")], &langs, NegotiationStrategy::Filtering, ) .get(0) .expect("Unavailable default language") .deref() .clone(), langs, } } // TODO fix fluent-langneg's weird API pub fn tr<'a>( &'a self, langs: &[LanguageIdentifier], key: &str, args: Option<&'a FluentArgs>, ) -> Option> { for prefered_lang in negotiate_languages( langs, &self.langs, Some(&self.default_lang), NegotiationStrategy::Filtering, ) { if let Some(bundle) = self.bundles.get(prefered_lang.as_ref()) { if let Some(message) = bundle.get_message(key) { let mut errors = Vec::new(); let ret = bundle.format_pattern(message.value().unwrap(), args, &mut errors); for error in errors { error!("Formatting message `{key}` in lang `{prefered_lang}`: {error}"); } return Some(ret); } } } None } } pub fn get_client_langs(req: &tide::Request) -> Vec { if let Some(header) = req.header("Accept-Language") { accepted_languages::parse(header.as_str()) } else { println!("NO HEADER"); Vec::new() } } /// Get the first language that is likely to be usable with chrono pub fn get_time_lang(langs: &[LanguageIdentifier]) -> Option { for lang in langs { if let Some(region) = &lang.region { return Some(format!("{}_{}", lang.language.as_str(), region.as_str())); } } None } pub fn tera_to_fluent(val: &tera::Value) -> FluentValue { match val { tera::Value::Null => FluentValue::None, tera::Value::Number(v) => { if v.is_i64() { FluentValue::Number(v.as_i64().unwrap().into()) } else if v.is_u64() { FluentValue::Number(v.as_u64().unwrap().into()) } else { FluentValue::Number(v.as_f64().unwrap().into()) } } tera::Value::String(v) => FluentValue::String(v.into()), _ => FluentValue::Error, } }