Skip to content

Commit 7c2d199

Browse files
docs(fluent-bundle): Add typesafe message arguments example
1 parent 13d5c56 commit 7c2d199

File tree

2 files changed

+147
-0
lines changed

2 files changed

+147
-0
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// This is an example of an application which adds a custom argument resolver
2+
// to add type safety.
3+
// See the external_arguments example if you are not yet familiar with fluent arguments.
4+
//
5+
// The goal is that we prevent bugs caused by mixing up arguments that belong
6+
// to different messages.
7+
// We can achieve this by defining structs for each message that encode the
8+
// argument types with the corresponding message ID, and then hooking into
9+
// fluent's resolver using a custom fluent_bundle::ArgumentResolver implementation.
10+
11+
use std::borrow::Cow;
12+
13+
use fluent_bundle::{ArgumentResolver, FluentBundle, FluentError, FluentResource, FluentValue};
14+
use unic_langid::langid;
15+
16+
fn main() {
17+
let ftl_string = String::from(
18+
"
19+
hello-world = Hello { $name }
20+
ref = The previous message says { hello-world }
21+
unread-emails =
22+
{ $emailCount ->
23+
[one] You have { $emailCount } unread email
24+
*[other] You have { $emailCount } unread emails
25+
}
26+
",
27+
);
28+
let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string.");
29+
let langid_en = langid!("en");
30+
let mut bundle = FluentBundle::new(vec![langid_en]);
31+
bundle
32+
.add_resource(res)
33+
.expect("Failed to add FTL resources to the bundle.");
34+
35+
let hello_world = messages::HelloWorld { name: "John" };
36+
let mut errors = vec![];
37+
let value = bundle.format_message(&hello_world, &mut errors);
38+
println!("{}", value);
39+
40+
let ref_msg = messages::Ref { hello_world };
41+
let mut errors = vec![];
42+
let value = bundle.format_message(&ref_msg, &mut errors);
43+
println!("{}", value);
44+
45+
let unread_emails = messages::UnreadEmails {
46+
email_count: Some(1),
47+
};
48+
let mut errors = vec![];
49+
let value = bundle.format_message(&unread_emails, &mut errors);
50+
println!("{}", value);
51+
}
52+
53+
// these definitions could be generated by a macro or a code generation tool
54+
mod messages {
55+
use super::*;
56+
57+
pub struct HelloWorld<'a> {
58+
pub name: &'a str,
59+
}
60+
61+
impl<'a> Message<'a> for HelloWorld<'a> {
62+
fn id(&self) -> &'static str {
63+
"hello-world"
64+
}
65+
66+
fn get_arg(&self, name: &str) -> Option<FluentValue<'a>> {
67+
Some(match name {
68+
"name" => self.name.into(),
69+
_ => return None,
70+
})
71+
}
72+
}
73+
74+
pub struct Ref<'a> {
75+
pub hello_world: HelloWorld<'a>,
76+
}
77+
78+
impl<'a> Message<'a> for Ref<'a> {
79+
fn id(&self) -> &'static str {
80+
"ref"
81+
}
82+
83+
fn get_arg(&self, name: &str) -> Option<FluentValue<'a>> {
84+
self.hello_world.get_arg(name)
85+
}
86+
}
87+
88+
pub struct UnreadEmails {
89+
pub email_count: Option<u32>,
90+
}
91+
92+
impl<'a> Message<'a> for UnreadEmails {
93+
fn id(&self) -> &'static str {
94+
"unread-emails"
95+
}
96+
97+
fn get_arg(&self, name: &str) -> Option<FluentValue<'a>> {
98+
Some(match name {
99+
"emailCount" => self.email_count.into(),
100+
_ => return None,
101+
})
102+
}
103+
}
104+
}
105+
106+
trait Message<'a> {
107+
fn id(&self) -> &'static str;
108+
fn get_arg(&self, name: &str) -> Option<FluentValue<'a>>;
109+
}
110+
111+
// by using &dyn, we prevent monomorphization for each Message struct
112+
// this keeps binary code size in check
113+
impl<'a, 'b> ArgumentResolver<'a> for &'a dyn Message<'b> {
114+
fn resolve(self, name: &str) -> Option<Cow<FluentValue<'a>>> {
115+
let arg = self.get_arg(name)?;
116+
Some(Cow::Owned(arg))
117+
}
118+
}
119+
120+
// allows for method syntax, i.e. bundle.format_message(...)
121+
trait CustomizedBundle {
122+
fn format_message<'b>(
123+
&'b self,
124+
message: &dyn Message,
125+
errors: &mut Vec<FluentError>,
126+
) -> Cow<'b, str>;
127+
}
128+
129+
impl CustomizedBundle for FluentBundle<FluentResource> {
130+
fn format_message<'b>(
131+
&'b self,
132+
message: &dyn Message,
133+
errors: &mut Vec<FluentError>,
134+
) -> Cow<'b, str> {
135+
let msg = self
136+
.get_message(message.id())
137+
.expect("Message doesn't exist.");
138+
139+
let pattern = msg.value().expect("Message has no value.");
140+
self.format_pattern_with_argument_resolver(pattern, message, errors)
141+
}
142+
}

fluent-bundle/src/resolver/scope.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ impl<'bundle, 'ast, 'args, 'errors, R, M, Args: ArgumentResolver<'args>>
146146
}
147147
}
148148

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

0 commit comments

Comments
 (0)