use fluent_bundle::{FluentArgs, FluentBundle, FluentResource};
use fluent_syntax::ast::{Expression, InlineExpression, Pattern, PatternElement};
use unic_langid::LanguageIdentifier;
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, String> {
Self::build(lang, bytes, true)
}
pub fn new_without_isolation(lang: impl AsRef<str>, bytes: &[u8]) -> Result<Self, String> {
Self::build(lang, bytes, false)
}
fn build(lang: impl AsRef<str>, bytes: &[u8], use_isolating: bool) -> Result<Self, String> {
let ftl = String::from_utf8(bytes.to_vec())
.map_err(|e| format!("Could not read ftl string due to: {e}"))?;
let lang_id: LanguageIdentifier = lang.as_ref().parse().map_err(|e| format!("{e:?}"))?;
let mut bundle = FluentBundle::new(vec![lang_id]);
bundle.set_use_isolating(use_isolating);
bundle.add_builtins().map_err(|e| format!("{e:?}"))?;
let resource = FluentResource::try_new(ftl).map_err(|e| format!("{e:?}"))?;
bundle
.add_resource(resource)
.map_err(|e| format!("{e:?}"))?;
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, String> {
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, String> {
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>, String> {
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, String> {
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(format!("Invalid format for message '{id}': {errors:?}"))
}
}
fn try_get_pattern(
&self,
msg_id: &str,
attr_id: Option<&str>,
) -> Result<&Pattern<&str>, String> {
let message = self
.bundle
.get_message(msg_id)
.ok_or_else(|| format!("Could not find {msg_id}"))?;
if let Some(attr_id) = attr_id {
message
.get_attribute(attr_id)
.map(|attr| attr.value())
.ok_or_else(|| {
format!("Could not find attribute '{attr_id}' for message '{msg_id}'")
})
} else {
message
.value()
.ok_or_else(|| format!("Could not find value for '{msg_id}'"))
}
}
fn format<'a>(
&'a self,
msg: &str,
attr: Option<&str>,
pattern: &'a Pattern<&str>,
args: Option<&FluentArgs>,
) -> Result<String, String> {
let mut errors = vec![];
let value = self.bundle.format_pattern(pattern, args, &mut errors);
if !errors.is_empty() {
let attr_str = attr
.map(|a| format!("attribute '{a}' in "))
.unwrap_or_default();
let arg_str = args
.map(|a| format!(" with args {}", arg_list(a)))
.unwrap_or_default();
Err(format!(
"Invalid format for {attr_str}message '{msg}'{arg_str}: {errors:?}"
))
} 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,
}
}
fn arg_list(args: &FluentArgs) -> String {
args.iter()
.map(|(k, v)| format!("{}={:?}", k, v))
.collect::<Vec<_>>()
.join(", ")
}