sofie-mail 0.4.0

Mail: messages rendered from templates, sent by a job over SMTP, to the log or to a test's outbox.
Documentation
use sofie_core::Show;

use crate::Mailer;

#[cfg(feature = "i18n")]
pub(crate) use spoken::{Language, Spoken, rendered};

#[cfg(feature = "i18n")]
pub use spoken::translations;

#[cfg(not(feature = "i18n"))]
pub(crate) use plain::{Language, rendered};

#[cfg(feature = "i18n")]
mod spoken {
    use std::sync::Arc;

    use sofie_core::Show;
    use sofie_db::Db;
    use sofie_engine::rendering::Rendering;
    use sofie_engine::{Template, TemplateError, Translations};

    pub(crate) type Language<'a> = sofie_engine::Language<'a>;

    pub(crate) struct Spoken(pub(crate) Arc<Translations>);

    pub fn translations(db: &Db, translations: Arc<Translations>) {
        sofie_db::extend(db, Arc::new(Spoken(translations)));
    }

    pub(crate) fn rendered(
        template: &Template,
        data: &(impl Show + ?Sized),
        language: Language<'_>,
    ) -> Result<String, TemplateError> {
        Rendering::new(template).language(language).render(data)
    }
}

#[cfg(not(feature = "i18n"))]
mod plain {
    use std::marker::PhantomData;

    use sofie_core::Show;
    use sofie_engine::{Template, TemplateError};

    #[derive(Clone, Copy)]
    pub(crate) struct Language<'a>(PhantomData<&'a ()>);

    impl Language<'_> {
        pub(crate) fn plain() -> Self {
            Self(PhantomData)
        }
    }

    pub(crate) fn rendered(
        template: &Template,
        data: &(impl Show + ?Sized),
        _language: Language<'_>,
    ) -> Result<String, TemplateError> {
        template.render(data)
    }
}

impl Mailer {
    /// `None`, or a language the app does not declare, writes its source language.
    #[cfg(feature = "i18n")]
    #[must_use]
    pub fn in_language(mut self, locale: Option<sofie_values::Locale>) -> Self {
        self.locale = locale;
        self
    }

    /// `fallback`, in the source language, where no language of the app has the key.
    #[must_use]
    pub fn text(&self, key: &str, arguments: &[(&str, String)], fallback: &str) -> String {
        #[cfg(feature = "i18n")]
        return self.language().field_error(key, arguments, fallback);
        #[cfg(not(feature = "i18n"))]
        {
            let _ = (key, arguments);
            fallback.to_owned()
        }
    }

    #[cfg(feature = "i18n")]
    pub(crate) fn language(&self) -> Language<'_> {
        match &self.translations {
            Some(translations) => translations.negotiate(self.locale.as_ref(), None),
            None => Language::plain(),
        }
    }

    #[cfg(not(feature = "i18n"))]
    #[expect(
        clippy::unused_self,
        reason = "under the `i18n` feature the mailer holds the translations"
    )]
    pub(crate) fn language(&self) -> Language<'_> {
        Language::plain()
    }

    pub(crate) fn render(
        &self,
        template: &crate::Template,
        data: &(impl Show + ?Sized),
    ) -> sofie_core::Result<crate::Body> {
        template.render(data, self.language())
    }
}