use crate::{reactor::Reactor, translator::errors::*, PerseusNodeType};
use fluent_bundle::{bundle::FluentBundle, FluentArgs, FluentResource};
use intl_memoizer::concurrent::IntlLangMemoizer;
use std::sync::Arc;
use sycamore::prelude::{use_context, Scope};
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
pub const FLUENT_TRANSLATOR_FILE_EXT: &str = "ftl";
#[derive(Clone)]
pub struct FluentTranslator {
bundle: Arc<FluentBundle<FluentResource, IntlLangMemoizer>>,
locale: String,
}
impl std::fmt::Debug for FluentTranslator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FluentTranslator")
.field("locale", &self.locale)
.finish()
}
}
impl FluentTranslator {
pub fn new(locale: String, ftl_string: String) -> Result<Self, TranslatorError> {
let resource = FluentResource::try_new(ftl_string)
.map_err(|(_, errs)| TranslatorError::TranslationsStrSerFailed {
locale: locale.clone(),
source: errs
.iter()
.map(|e| e.to_string())
.collect::<String>()
.into(),
})?;
let lang_id: LanguageIdentifier =
locale.parse().map_err(|err: LanguageIdentifierError| {
TranslatorError::InvalidLocale {
locale: locale.clone(),
source: Box::new(err),
}
})?;
let mut bundle = FluentBundle::new_concurrent(vec![lang_id]);
bundle.add_resource(resource).map_err(|errs| {
TranslatorError::TranslationsStrSerFailed {
locale: locale.clone(),
source: errs
.iter()
.map(|e| e.to_string())
.collect::<String>()
.into(),
}
})?;
let bundle = Arc::new(bundle);
Ok(Self { bundle, locale })
}
pub fn url(&self, url: &str) -> String {
let url = url.strip_prefix('/').unwrap_or(url);
format!("{}/{}", self.locale, url)
}
pub fn get_locale(&self) -> String {
self.locale.clone()
}
pub fn translate(&self, id: &str, args: Option<FluentArgs>) -> String {
let translation_res = self.translate_checked(id, args);
match translation_res {
Ok(translation) => translation,
Err(_) => panic!("translation id '{}' not found for locale '{}' (if you're not hardcoding the id, use `.translate_checked()` instead)", id, self.locale)
}
}
pub fn translate_checked(
&self,
id: &str,
args: Option<FluentArgs>,
) -> Result<String, TranslatorError> {
let id_str = id.to_string();
let id_vec: Vec<&str> = id_str.split('.').collect();
let id_str = id_vec[0].to_string();
let variant = id_vec.get(1);
let msg = self.bundle.get_message(&id_str);
let msg = match msg {
Some(msg) => msg,
None => {
return Err(TranslatorError::TranslationIdNotFound {
id: id_str,
locale: self.locale.clone(),
})
}
};
let mut errors = Vec::new();
let value = msg.value();
let mut translation = None; if let Some(value) = value {
translation = Some(
self.bundle
.format_pattern(value, args.as_ref(), &mut errors),
);
} else {
if let Some(variant) = variant {
for attr in msg.attributes() {
if &attr.id() == variant {
translation = Some(self.bundle.format_pattern(
attr.value(),
args.as_ref(),
&mut errors,
));
break;
}
}
} else {
return Err(TranslatorError::TranslationFailed {
id: id_str,
locale: self.locale.clone(),
source: "no variant provided for compound message".into(),
});
}
}
if !errors.is_empty() {
return Err(TranslatorError::TranslationFailed {
id: id_str,
locale: self.locale.clone(),
source: errors
.iter()
.map(|e| e.to_string())
.collect::<String>()
.into(),
});
}
match translation {
Some(translation) => Ok(translation.to_string()),
None => Err(TranslatorError::NoTranslationDerived {
id: id_str,
locale: self.locale.clone(),
}),
}
}
pub fn get_bundle(&self) -> &FluentBundle<FluentResource, IntlLangMemoizer> {
&self.bundle
}
}
#[doc(hidden)]
#[allow(missing_debug_implementations)]
pub type TranslationArgs<'args> = FluentArgs<'args>;
#[doc(hidden)]
pub fn t_macro_backend(id: &str, cx: Scope) -> String {
let translator = use_context::<Reactor<PerseusNodeType>>(cx).get_translator();
translator.translate(id, None)
}
#[doc(hidden)]
pub fn t_macro_backend_with_args(id: &str, args: FluentArgs, cx: Scope) -> String {
let translator = use_context::<Reactor<PerseusNodeType>>(cx).get_translator();
translator.translate(id, Some(args))
}
#[doc(hidden)]
pub fn link_macro_backend(url: &str, cx: Scope) -> String {
let translator = use_context::<Reactor<PerseusNodeType>>(cx).get_translator();
translator.url(url)
}