use fluent_bundle::concurrent::FluentBundle;
use fluent_bundle::{FluentArgs, FluentResource};
use fluent_syntax::ast::{Expression, InlineExpression, Pattern, PatternElement};
use unic_langid::LanguageIdentifier;
use crate::error::L10nError;
use crate::structured::Segment;
pub struct L10nBundle {
lang: String,
bundle: FluentBundle<FluentResource>,
}
impl L10nBundle {
pub fn new(lang: impl AsRef<str>, bytes: &[u8]) -> Result<Self, L10nError> {
Self::build(lang, bytes, true)
}
pub fn new_without_isolation(lang: impl AsRef<str>, bytes: &[u8]) -> Result<Self, L10nError> {
Self::build(lang, bytes, false)
}
fn build(lang: impl AsRef<str>, bytes: &[u8], use_isolating: bool) -> Result<Self, L10nError> {
let ftl = String::from_utf8(bytes.to_vec()).map_err(L10nError::InvalidUtf8)?;
let lang_id: LanguageIdentifier =
lang.as_ref()
.parse()
.map_err(|e| L10nError::InvalidLanguage {
lang: lang.as_ref().to_string(),
reason: format!("{e:?}"),
})?;
let mut bundle = FluentBundle::new_concurrent(vec![lang_id]);
bundle.set_use_isolating(use_isolating);
bundle
.add_builtins()
.map_err(|e| L10nError::Builtins(format!("{e:?}")))?;
let resource = FluentResource::try_new(ftl).map_err(|(_, errors)| {
L10nError::ResourceParse(errors.iter().map(|e| format!("{e:?}")).collect())
})?;
bundle.add_resource(resource).map_err(|errors| {
L10nError::AddResource(errors.iter().map(|e| format!("{e:?}")).collect())
})?;
Ok(Self {
bundle,
lang: lang.as_ref().to_string(),
})
}
pub fn lang(&self) -> &str {
&self.lang
}
pub fn msg(&self, id: &str, args: Option<FluentArgs>) -> Result<String, L10nError> {
let pattern = self.try_get_pattern(id, None)?;
self.format(id, None, pattern, args.as_ref())
}
pub fn attr(
&self,
msg: &str,
attr: &str,
args: Option<FluentArgs>,
) -> Result<String, L10nError> {
let pattern = self.try_get_pattern(msg, Some(attr))?;
self.format(msg, Some(attr), pattern, args.as_ref())
}
pub fn msg_segments(
&self,
id: &str,
element_vars: &[&str],
element_terms: &[&str],
args: Option<FluentArgs>,
) -> Result<Vec<Segment>, L10nError> {
let pattern = self.try_get_pattern(id, None)?;
let args = args.as_ref();
let mut segments = Vec::new();
let mut current: Vec<PatternElement<&str>> = Vec::new();
for element in &pattern.elements {
match element_marker(element, element_vars, element_terms) {
Some(Marker::Variable) => {
segments.push(Segment::Text(self.resolve_segment(id, ¤t, args)?));
current.clear();
segments.push(Segment::Gap);
}
Some(Marker::Term) => {
segments.push(Segment::Text(self.resolve_segment(id, ¤t, args)?));
current.clear();
let text = self.resolve_segment(id, std::slice::from_ref(element), args)?;
segments.push(Segment::Term(text));
}
None => current.push(element.clone()),
}
}
segments.push(Segment::Text(self.resolve_segment(id, ¤t, args)?));
Ok(segments)
}
fn resolve_segment(
&self,
id: &str,
elements: &[PatternElement<&str>],
args: Option<&FluentArgs>,
) -> Result<String, L10nError> {
let mut padded = Vec::with_capacity(elements.len() + 1);
padded.push(PatternElement::TextElement { value: "" });
padded.extend(elements.iter().cloned());
let sub = Pattern { elements: padded };
let mut errors = vec![];
let value = self.bundle.format_pattern(&sub, args, &mut errors);
if errors.is_empty() {
Ok(value.to_string())
} else {
Err(L10nError::Format {
id: id.to_string(),
attribute: None,
errors: errors.iter().map(|e| format!("{e:?}")).collect(),
})
}
}
fn try_get_pattern(
&self,
msg_id: &str,
attr_id: Option<&str>,
) -> Result<&Pattern<&str>, L10nError> {
let message =
self.bundle
.get_message(msg_id)
.ok_or_else(|| L10nError::MessageNotFound {
id: msg_id.to_string(),
})?;
if let Some(attr_id) = attr_id {
message
.get_attribute(attr_id)
.map(|attr| attr.value())
.ok_or_else(|| L10nError::AttributeNotFound {
id: msg_id.to_string(),
attribute: attr_id.to_string(),
})
} else {
message.value().ok_or_else(|| L10nError::MessageHasNoValue {
id: msg_id.to_string(),
})
}
}
fn format<'a>(
&'a self,
msg: &str,
attr: Option<&str>,
pattern: &'a Pattern<&str>,
args: Option<&FluentArgs>,
) -> Result<String, L10nError> {
let mut errors = vec![];
let value = self.bundle.format_pattern(pattern, args, &mut errors);
if !errors.is_empty() {
Err(L10nError::Format {
id: msg.to_string(),
attribute: attr.map(|a| a.to_string()),
errors: errors.iter().map(|e| format!("{e:?}")).collect(),
})
} else {
Ok(value.to_string())
}
}
}
enum Marker {
Variable,
Term,
}
fn element_marker(
element: &PatternElement<&str>,
element_vars: &[&str],
element_terms: &[&str],
) -> Option<Marker> {
let PatternElement::Placeable { expression } = element else {
return None;
};
match expression {
Expression::Inline(InlineExpression::VariableReference { id })
if element_vars.contains(&id.name) =>
{
Some(Marker::Variable)
}
Expression::Inline(InlineExpression::TermReference { id, .. })
if element_terms.contains(&id.name) =>
{
Some(Marker::Term)
}
_ => None,
}
}