use super::error::{fluent_load_err, LocalizationLoadError, LocalizationTranslateError};
use async_std::fs;
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use fluent::{bundle, FluentArgs, FluentMessage, FluentResource};
use intl_memoizer::concurrent::IntlLangMemoizer;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::{self, Debug};
use unic_langid::LanguageIdentifier;
pub type FluentBundle = bundle::FluentBundle<FluentResource, IntlLangMemoizer>;
pub struct Localizations {
bundles: HashMap<LanguageIdentifier, FluentBundle>,
}
impl Localizations {
pub async fn open<P: Into<PathBuf>>(
directory: P,
) -> Result<Self, LocalizationLoadError> {
tide::log::debug!("Reading Fluent localization directory...");
let directory = {
let mut path = directory.into();
path.push("fluent");
path
};
let mut bundles = HashMap::new();
let mut entries = fs::read_dir(&directory).await?;
while let Some(result) = entries.next().await {
let entry = result?;
let path = entry.path();
Self::load_component(&mut bundles, &path).await?;
}
Ok(Localizations { bundles })
}
async fn load_component(
bundles: &mut HashMap<LanguageIdentifier, FluentBundle>,
directory: &Path,
) -> Result<(), LocalizationLoadError> {
tide::log::debug!("Reading component at {}", directory.display());
let mut entries = fs::read_dir(directory).await?;
while let Some(result) = entries.next().await {
let entry = result?;
let path = entry.path();
let locale_name = path
.file_stem()
.expect("No base name in locale path")
.to_str()
.expect("Path is not valid UTF-8");
tide::log::debug!("Loading locale {locale_name}");
let locale: LanguageIdentifier = locale_name.parse()?;
let source = fs::read_to_string(&path).await?;
let resource = FluentResource::try_new(source).map_err(fluent_load_err)?;
let locale2 = locale.clone();
let bundle = bundles
.entry(locale)
.or_insert_with(|| FluentBundle::new_concurrent(vec![locale2]));
bundle.add_resource(resource).map_err(fluent_load_err)?;
}
Ok(())
}
pub fn parse_selector(key: &str) -> (&str, Option<&str>) {
match key.find('.') {
None => (key, None),
Some(idx) => {
let (path, rest) = key.split_at(idx);
let attribute = &rest[1..]; (path, Some(attribute))
}
}
}
pub fn has_message(&self, locale: &LanguageIdentifier, key: &str) -> bool {
let (path, attribute) = Self::parse_selector(key);
self.bundles
.get(locale)
.map(|bundle| match attribute {
None => bundle.has_message(key),
Some(attribute) => bundle
.get_message(path)
.map(|message| message.get_attribute(attribute).is_some())
.unwrap_or(false),
})
.unwrap_or(false)
}
fn get_message(
&self,
locale: &LanguageIdentifier,
key: &str,
) -> Result<(&FluentBundle, FluentMessage), LocalizationTranslateError> {
match self.bundles.get(locale) {
None => Err(LocalizationTranslateError::NoLocale),
Some(bundle) => match bundle.get_message(key) {
Some(message) => Ok((bundle, message)),
None => Err(LocalizationTranslateError::NoMessage),
},
}
}
pub fn translate<'a>(
&'a self,
locale: &LanguageIdentifier,
key: &str,
args: &'a FluentArgs<'a>,
) -> Result<Cow<'a, str>, LocalizationTranslateError> {
let (path, attribute) = Self::parse_selector(key);
let (bundle, message) = self.get_message(locale, path)?;
tide::log::info!(
"Translating for locale {}, message path {}, attribute {}",
locale,
path,
attribute.unwrap_or("<none>"),
);
let pattern = match attribute {
Some(attribute) => match message.get_attribute(attribute) {
Some(attrib) => attrib.value(),
None => return Err(LocalizationTranslateError::NoMessageAttribute),
},
None => match message.value() {
Some(pattern) => pattern,
None => return Err(LocalizationTranslateError::NoMessageValue),
},
};
let mut errors = vec![];
let output = bundle.format_pattern(pattern, Some(args), &mut errors);
if !errors.is_empty() {
tide::log::warn!(
"Errors formatting message for locale {locale}, message key {key}",
);
for (key, value) in args.iter() {
tide::log::warn!("Passed formatting argument: {key} -> {value:?}");
}
for error in errors {
tide::log::warn!("Message formatting error: {error}");
}
}
Ok(output)
}
}
impl Debug for Localizations {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Localizations")
.field(
"bundles",
&format!(
"HashMap {{ LanguageIdentifier => FluentBundle }} ({} items)",
self.bundles.len(),
),
)
.finish()
}
}