webcomment/src/locales.rs

122 lines
3.4 KiB
Rust

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<LanguageIdentifier, FluentBundle<FluentResource, IntlLangMemoizer>>,
pub default_lang: LanguageIdentifier,
langs: Vec<LanguageIdentifier>,
}
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::<HashMap<LanguageIdentifier, FluentBundle<FluentResource, IntlLangMemoizer>>>(
),
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<Cow<str>> {
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<State>(req: &tide::Request<State>) -> Vec<LanguageIdentifier> {
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<String> {
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,
}
}