use std::{cell::RefCell, collections::BTreeMap, rc::Rc, sync::OnceLock};
use fluent_bundle::{FluentArgs, FluentBundle, FluentError, FluentResource, FluentValue};
use crate::LocalizationError;
pub struct Message {
namespace: &'static str,
id: &'static str,
source: &'static SourceCatalog,
}
pub struct SourceCatalog {
source_locale: &'static str,
resource: &'static str,
parsed: OnceLock<Result<FluentResource, String>>,
}
impl Message {
pub const fn new(
namespace: &'static str,
id: &'static str,
source: &'static SourceCatalog,
) -> Self {
Self {
namespace,
id,
source,
}
}
pub fn namespace(&self) -> &'static str {
self.namespace
}
pub fn id(&self) -> &'static str {
self.id
}
pub fn format_source(&self, arguments: &[Argument<'_>]) -> Result<String, LocalizationError> {
self.source(&fluent_args(arguments))
}
pub fn fallback_text(&self, arguments: &[Argument<'_>]) -> String {
self.source_output(&fluent_args(arguments))
.map_or_else(|_| "…".to_owned(), |(text, _)| text)
}
pub(crate) fn source(&self, args: &FluentArgs<'_>) -> Result<String, LocalizationError> {
let (text, errors) = self.source_output(args)?;
if errors.is_empty() {
Ok(text)
} else {
Err(self.error(format!("{errors:?}")))
}
}
fn source_output(
&self,
args: &FluentArgs<'_>,
) -> Result<(String, Vec<FluentError>), LocalizationError> {
let bundle = self.source.bundle().map_err(|detail| self.error(detail))?;
let pattern = bundle
.get_message(self.id)
.and_then(|message| message.value())
.ok_or_else(|| self.error("source message has no value".to_owned()))?;
let mut errors = Vec::new();
let text = bundle.format_pattern(pattern, Some(args), &mut errors);
Ok((text.into_owned(), errors))
}
pub(crate) fn error(&self, detail: String) -> LocalizationError {
LocalizationError::Format {
namespace: self.namespace.to_owned(),
id: self.id.to_owned(),
detail,
}
}
}
pub(crate) fn fluent_args<'a>(arguments: &'a [Argument<'_>]) -> FluentArgs<'a> {
arguments
.iter()
.map(|argument| {
let value = match &argument.value {
FluentValue::String(text) => FluentValue::from(text.as_ref()),
value => value.clone(),
};
(argument.name, value)
})
.collect()
}
impl SourceCatalog {
pub const fn new(source_locale: &'static str, resource: &'static str) -> Self {
Self {
source_locale,
resource,
parsed: OnceLock::new(),
}
}
fn bundle(&'static self) -> Result<Rc<FluentBundle<&'static FluentResource>>, String> {
type SourceBundle = Result<Rc<FluentBundle<&'static FluentResource>>, String>;
thread_local! {
static SOURCES: RefCell<BTreeMap<usize, SourceBundle>> = const { RefCell::new(BTreeMap::new()) };
}
SOURCES.with(|sources| {
sources
.borrow_mut()
.entry(std::ptr::from_ref(self).addr())
.or_insert_with(|| self.prepare().map(Rc::new))
.clone()
})
}
fn prepare(&'static self) -> Result<FluentBundle<&'static FluentResource>, String> {
let language = self
.source_locale
.parse()
.map_err(|error| format!("{error}"))?;
let resource = self
.parsed
.get_or_init(|| {
FluentResource::try_new(self.resource.to_owned())
.map_err(|(_, errors)| format!("{errors:?}"))
})
.as_ref()
.map_err(Clone::clone)?;
let mut bundle = crate::new_bundle(language);
bundle
.add_resource(resource)
.map_err(|errors| format!("{errors:?}"))?;
Ok(bundle)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Argument<'a> {
pub(crate) name: &'static str,
pub(crate) value: FluentValue<'a>,
}
impl<'a> Argument<'a> {
pub fn new(name: &'static str, value: impl Into<FluentValue<'a>>) -> Self {
Self {
name,
value: value.into(),
}
}
pub fn take_owned(&mut self) -> Argument<'static> {
let value = match std::mem::replace(&mut self.value, FluentValue::None) {
FluentValue::String(text) => FluentValue::String(text.into_owned().into()),
FluentValue::Number(number) => FluentValue::Number(number),
FluentValue::Custom(custom) => FluentValue::Custom(custom),
FluentValue::None => FluentValue::None,
FluentValue::Error => FluentValue::Error,
};
Argument {
name: self.name,
value,
}
}
pub fn update_from(&mut self, source: &mut Argument<'_>) {
self.name = source.name;
if let FluentValue::String(target) = &mut self.value
&& let FluentValue::String(std::borrow::Cow::Borrowed(value)) = &source.value
{
let target = target.to_mut();
target.clear();
target.push_str(value);
return;
}
self.value = source.take_owned().value;
}
}