122 lines
3.4 KiB
Rust
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,
|
|
}
|
|
}
|