Skip to content

Commit

Permalink
docs(fluent-bundle): Add typesafe message arguments example
Browse files Browse the repository at this point in the history
  • Loading branch information
JasperDeSutter committed May 6, 2024
1 parent 13d5c56 commit 7c2d199
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 0 deletions.
142 changes: 142 additions & 0 deletions fluent-bundle/examples/typesafe_messages.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// This is an example of an application which adds a custom argument resolver
// to add type safety.
// See the external_arguments example if you are not yet familiar with fluent arguments.
//
// The goal is that we prevent bugs caused by mixing up arguments that belong
// to different messages.
// We can achieve this by defining structs for each message that encode the
// argument types with the corresponding message ID, and then hooking into
// fluent's resolver using a custom fluent_bundle::ArgumentResolver implementation.

use std::borrow::Cow;

use fluent_bundle::{ArgumentResolver, FluentBundle, FluentError, FluentResource, FluentValue};
use unic_langid::langid;

fn main() {
let ftl_string = String::from(
"
hello-world = Hello { $name }
ref = The previous message says { hello-world }
unread-emails =
{ $emailCount ->
[one] You have { $emailCount } unread email
*[other] You have { $emailCount } unread emails
}
",
);
let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string.");
let langid_en = langid!("en");
let mut bundle = FluentBundle::new(vec![langid_en]);
bundle
.add_resource(res)
.expect("Failed to add FTL resources to the bundle.");

let hello_world = messages::HelloWorld { name: "John" };
let mut errors = vec![];
let value = bundle.format_message(&hello_world, &mut errors);
println!("{}", value);

let ref_msg = messages::Ref { hello_world };
let mut errors = vec![];
let value = bundle.format_message(&ref_msg, &mut errors);
println!("{}", value);

let unread_emails = messages::UnreadEmails {
email_count: Some(1),
};
let mut errors = vec![];
let value = bundle.format_message(&unread_emails, &mut errors);
println!("{}", value);
}

// these definitions could be generated by a macro or a code generation tool
mod messages {
use super::*;

pub struct HelloWorld<'a> {
pub name: &'a str,
}

impl<'a> Message<'a> for HelloWorld<'a> {
fn id(&self) -> &'static str {
"hello-world"
}

fn get_arg(&self, name: &str) -> Option<FluentValue<'a>> {
Some(match name {
"name" => self.name.into(),
_ => return None,
})
}
}

pub struct Ref<'a> {
pub hello_world: HelloWorld<'a>,
}

impl<'a> Message<'a> for Ref<'a> {
fn id(&self) -> &'static str {
"ref"
}

fn get_arg(&self, name: &str) -> Option<FluentValue<'a>> {
self.hello_world.get_arg(name)
}
}

pub struct UnreadEmails {
pub email_count: Option<u32>,
}

impl<'a> Message<'a> for UnreadEmails {
fn id(&self) -> &'static str {
"unread-emails"
}

fn get_arg(&self, name: &str) -> Option<FluentValue<'a>> {
Some(match name {
"emailCount" => self.email_count.into(),
_ => return None,
})
}
}
}

trait Message<'a> {
fn id(&self) -> &'static str;
fn get_arg(&self, name: &str) -> Option<FluentValue<'a>>;
}

// by using &dyn, we prevent monomorphization for each Message struct
// this keeps binary code size in check
impl<'a, 'b> ArgumentResolver<'a> for &'a dyn Message<'b> {
fn resolve(self, name: &str) -> Option<Cow<FluentValue<'a>>> {
let arg = self.get_arg(name)?;
Some(Cow::Owned(arg))
}
}

// allows for method syntax, i.e. bundle.format_message(...)
trait CustomizedBundle {
fn format_message<'b>(
&'b self,
message: &dyn Message,
errors: &mut Vec<FluentError>,
) -> Cow<'b, str>;
}

impl CustomizedBundle for FluentBundle<FluentResource> {
fn format_message<'b>(
&'b self,
message: &dyn Message,
errors: &mut Vec<FluentError>,
) -> Cow<'b, str> {
let msg = self
.get_message(message.id())
.expect("Message doesn't exist.");

let pattern = msg.value().expect("Message has no value.");
self.format_pattern_with_argument_resolver(pattern, message, errors)
}
}
5 changes: 5 additions & 0 deletions fluent-bundle/src/resolver/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ impl<'bundle, 'ast, 'args, 'errors, R, M, Args: ArgumentResolver<'args>>
}
}

/// Determines how to retrieve argument values when resolving fluent messages.
/// This trait can be used to implement an alternative to [`FluentArgs`].
///
/// One example usage is for argument type safety that [`FluentArgs`] can't provide due to its
/// flexible nature. See `fluent-bundle/examples/typesafe_messages.rs` for an example of this.
pub trait ArgumentResolver<'a>: Copy {
fn resolve(self, name: &str) -> Option<Cow<FluentValue<'a>>>;
}
Expand Down

0 comments on commit 7c2d199

Please sign in to comment.