Skip to content

Commit 3274a55

Browse files
authored
i18n-embed-fl: check args on MessageReference (#146)
* i18n-embed: retrieve fluent bundle with message Add `FluentLanguageLoader::with_fluent_message_and_bundle()`. This method functions like `with_fluent_message()`, but it also returns the `FluentBundle` which owns the message. This addition permits the closure to resolve `MessageReference` and to retrieve other messages on the same bundle. * fix(i18n-embed-fl): check args on MessageReference The `fl!()` macro checks that all assigned arguments exist on the message in question. But fluent messages can also refer to other messages, and those messages can also have arguments. ```ftl hello = Hello { to-you } to-you = to you, {$name}! ``` Previous versions of the macro could not check arguments on `MessageReference`s since it had no way to resolve them. Bring the owning `FluentBundle` into scope so that message references can be resolved. Some unnecessary generics on the recursive functions are made concrete. This patch requires an update to i18-embed for new API.
1 parent bf603a7 commit 3274a55

File tree

4 files changed

+178
-26
lines changed

4 files changed

+178
-26
lines changed

i18n-embed-fl/i18n/en-US/i18n_embed_fl.ftl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,13 @@ hello-arg = Hello {$name}!
33
.attr = Hello {$name}'s attribute!
44
hello-arg-2 = Hello {$name1} and {$name2}!
55
hello-attr = Uninspiring.
6-
.text = Hello, attribute!
6+
.text = Hello, attribute!
7+
hello-recursive = Hello { hello-recursive-descent }
8+
.attr = Why hello { hello-recursive-descent }
9+
.again = Why hello { hello-recursive-descent.attr }
10+
hello-recursive-descent = to you, {$name}!
11+
.attr = again, {$name}!
12+
hello-select = { $attr ->
13+
*[no] { hello-recursive }
14+
[yes] { hello-recursive.attr }
15+
}

i18n-embed-fl/src/lib.rs

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use fluent::{FluentAttribute, FluentMessage};
1+
use fluent::concurrent::FluentBundle;
2+
use fluent::{FluentAttribute, FluentMessage, FluentResource};
23
use fluent_syntax::ast::{CallArguments, Expression, InlineExpression, Pattern, PatternElement};
34
use i18n_embed::{fluent::FluentLanguageLoader, FileSystemAssets, LanguageLoader};
45
use proc_macro::TokenStream;
@@ -557,8 +558,8 @@ pub fn fl(input: TokenStream) -> TokenStream {
557558
if let Some(message_id_str) = &message_id_string {
558559
checked_loader_has_message = domain_data
559560
.loader
560-
.with_fluent_message(message_id_str, |message: FluentMessage<'_>| {
561-
check_message_args(message, &specified_args);
561+
.with_fluent_message_and_bundle(message_id_str, |message, bundle| {
562+
check_message_args(message, bundle, &specified_args);
562563
})
563564
.is_some();
564565
}
@@ -577,11 +578,11 @@ pub fn fl(input: TokenStream) -> TokenStream {
577578
} else {
578579
if let Some(message_id_str) = &message_id_string {
579580
if let Some(attr_id_str) = &attr_str {
580-
let attr_res = domain_data.loader.with_fluent_message(
581+
let attr_res = domain_data.loader.with_fluent_message_and_bundle(
581582
message_id_str,
582-
|message: FluentMessage<'_>| match message.get_attribute(attr_id_str) {
583+
|message, bundle| match message.get_attribute(attr_id_str) {
583584
Some(attr) => {
584-
check_attribute_args(attr, &specified_args);
585+
check_attribute_args(attr, bundle, &specified_args);
585586
true
586587
}
587588
None => false,
@@ -721,13 +722,16 @@ fn fuzzy_attribute_suggestions(
721722
.collect()
722723
}
723724

724-
fn check_message_args(
725+
fn check_message_args<R>(
725726
message: FluentMessage<'_>,
727+
bundle: &FluentBundle<R>,
726728
specified_args: &HashMap<syn::LitStr, Box<syn::Expr>>,
727-
) {
729+
) where
730+
R: std::borrow::Borrow<FluentResource>,
731+
{
728732
if let Some(pattern) = message.value() {
729733
let mut args = Vec::new();
730-
args_from_pattern(pattern, &mut args);
734+
args_from_pattern(pattern, bundle, &mut args);
731735

732736
let args_set: HashSet<&str> = args.into_iter().collect();
733737

@@ -788,13 +792,16 @@ fn check_message_args(
788792
}
789793
}
790794

791-
fn check_attribute_args(
795+
fn check_attribute_args<R>(
792796
attr: FluentAttribute<'_>,
797+
bundle: &FluentBundle<R>,
793798
specified_args: &HashMap<syn::LitStr, Box<syn::Expr>>,
794-
) {
799+
) where
800+
R: std::borrow::Borrow<FluentResource>,
801+
{
795802
let pattern = attr.value();
796803
let mut args = Vec::new();
797-
args_from_pattern(pattern, &mut args);
804+
args_from_pattern(pattern, bundle, &mut args);
798805

799806
let args_set: HashSet<&str> = args.into_iter().collect();
800807

@@ -854,56 +861,101 @@ fn check_attribute_args(
854861
}
855862
}
856863

857-
fn args_from_pattern<S: Copy>(pattern: &Pattern<S>, args: &mut Vec<S>) {
864+
fn args_from_pattern<'m, R>(
865+
pattern: &Pattern<&'m str>,
866+
bundle: &'m FluentBundle<R>,
867+
args: &mut Vec<&'m str>,
868+
) where
869+
R: std::borrow::Borrow<FluentResource>,
870+
{
858871
pattern.elements.iter().for_each(|element| {
859872
if let PatternElement::Placeable { expression } = element {
860-
args_from_expression(expression, args)
873+
args_from_expression(expression, bundle, args)
861874
}
862875
});
863876
}
864877

865-
fn args_from_expression<S: Copy>(expr: &Expression<S>, args: &mut Vec<S>) {
878+
fn args_from_expression<'m, R>(
879+
expr: &Expression<&'m str>,
880+
bundle: &'m FluentBundle<R>,
881+
args: &mut Vec<&'m str>,
882+
) where
883+
R: std::borrow::Borrow<FluentResource>,
884+
{
866885
match expr {
867886
Expression::Inline(inline_expr) => {
868-
args_from_inline_expression(inline_expr, args);
887+
args_from_inline_expression(inline_expr, bundle, args);
869888
}
870889
Expression::Select { selector, variants } => {
871-
args_from_inline_expression(selector, args);
890+
args_from_inline_expression(selector, bundle, args);
872891

873892
variants.iter().for_each(|variant| {
874-
args_from_pattern(&variant.value, args);
893+
args_from_pattern(&variant.value, bundle, args);
875894
})
876895
}
877896
}
878897
}
879898

880-
fn args_from_inline_expression<S: Copy>(inline_expr: &InlineExpression<S>, args: &mut Vec<S>) {
899+
fn args_from_inline_expression<'m, R>(
900+
inline_expr: &InlineExpression<&'m str>,
901+
bundle: &'m FluentBundle<R>,
902+
args: &mut Vec<&'m str>,
903+
) where
904+
R: std::borrow::Borrow<FluentResource>,
905+
{
881906
match inline_expr {
882907
InlineExpression::FunctionReference {
883908
id: _,
884909
arguments: call_args,
885910
} => {
886-
args_from_call_arguments(call_args, args);
911+
args_from_call_arguments(call_args, bundle, args);
887912
}
888913
InlineExpression::TermReference {
889914
id: _,
890915
attribute: _,
891916
arguments: Some(call_args),
892917
} => {
893-
args_from_call_arguments(call_args, args);
918+
args_from_call_arguments(call_args, bundle, args);
894919
}
895920
InlineExpression::VariableReference { id } => args.push(id.name),
896-
InlineExpression::Placeable { expression } => args_from_expression(expression, args),
921+
InlineExpression::Placeable { expression } => {
922+
args_from_expression(expression, bundle, args)
923+
}
924+
InlineExpression::MessageReference {
925+
id,
926+
attribute: None,
927+
} => {
928+
bundle
929+
.get_message(&id.name)
930+
.and_then(|m| m.value())
931+
.map(|p| args_from_pattern(p, bundle, args));
932+
}
933+
InlineExpression::MessageReference {
934+
id,
935+
attribute: Some(attribute),
936+
} => {
937+
bundle
938+
.get_message(&id.name)
939+
.and_then(|m| m.get_attribute(&attribute.name))
940+
.map(|m| m.value())
941+
.map(|p| args_from_pattern(p, bundle, args));
942+
}
897943
_ => {}
898944
}
899945
}
900946

901-
fn args_from_call_arguments<S: Copy>(call_args: &CallArguments<S>, args: &mut Vec<S>) {
947+
fn args_from_call_arguments<'m, R>(
948+
call_args: &CallArguments<&'m str>,
949+
bundle: &'m FluentBundle<R>,
950+
args: &mut Vec<&'m str>,
951+
) where
952+
R: std::borrow::Borrow<FluentResource>,
953+
{
902954
call_args.positional.iter().for_each(|expr| {
903-
args_from_inline_expression(expr, args);
955+
args_from_inline_expression(expr, bundle, args);
904956
});
905957

906958
call_args.named.iter().for_each(|named_arg| {
907-
args_from_inline_expression(&named_arg.value, args);
959+
args_from_inline_expression(&named_arg.value, bundle, args);
908960
})
909961
}

i18n-embed-fl/tests/fl_macro.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,60 @@ fn with_attr_and_args() {
9090
fl!(loader, "hello-arg", "attr", name = "Bob")
9191
);
9292
}
93+
94+
#[test]
95+
fn with_args_in_messagereference() {
96+
let loader: FluentLanguageLoader = fluent_language_loader!();
97+
loader
98+
.load_languages(&Localizations, &[loader.fallback_language().clone()])
99+
.unwrap();
100+
101+
pretty_assertions::assert_eq!(
102+
"Hello to you, \u{2068}Bob\u{2069}!",
103+
fl!(loader, "hello-recursive", name = "Bob")
104+
);
105+
}
106+
107+
#[test]
108+
fn with_args_in_messagereference_attr() {
109+
let loader: FluentLanguageLoader = fluent_language_loader!();
110+
loader
111+
.load_languages(&Localizations, &[loader.fallback_language().clone()])
112+
.unwrap();
113+
114+
pretty_assertions::assert_eq!(
115+
"Why hello to you, \u{2068}Bob\u{2069}!",
116+
fl!(loader, "hello-recursive", "attr", name = "Bob")
117+
);
118+
}
119+
120+
#[test]
121+
fn with_args_in_messagereference_attr_to_attr() {
122+
let loader: FluentLanguageLoader = fluent_language_loader!();
123+
loader
124+
.load_languages(&Localizations, &[loader.fallback_language().clone()])
125+
.unwrap();
126+
127+
pretty_assertions::assert_eq!(
128+
"Why hello again, \u{2068}Bob\u{2069}!",
129+
fl!(loader, "hello-recursive", "again", name = "Bob")
130+
);
131+
}
132+
133+
#[test]
134+
fn with_args_in_select_messagereference() {
135+
let loader: FluentLanguageLoader = fluent_language_loader!();
136+
loader
137+
.load_languages(&Localizations, &[loader.fallback_language().clone()])
138+
.unwrap();
139+
140+
pretty_assertions::assert_eq!(
141+
"Hello to you, \u{2068}Bob\u{2069}!",
142+
fl!(loader, "hello-select", attr = "", name = "Bob")
143+
);
144+
145+
pretty_assertions::assert_eq!(
146+
"Why hello to you, \u{2068}Bob\u{2069}!",
147+
fl!(loader, "hello-select", attr = "yes", name = "Bob")
148+
);
149+
}

i18n-embed/src/fluent.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,40 @@ impl FluentLanguageLoader {
342342
.map(closure)
343343
}
344344

345+
/// Searches for a message named `message_id` in all languages that
346+
/// are currently loaded, including the fallback language. If the
347+
/// message is found, invokes the `closure` with the:
348+
///
349+
/// 0. [message](FluentMessage)
350+
/// 1. the language-specific [bundle](FluentBundle)
351+
/// that owns it.
352+
///
353+
/// Returns `Some` of whatever the closure returns, or `None` if no
354+
/// messages were found matching the `message_id`.
355+
pub fn with_fluent_message_and_bundle<OUT, C>(
356+
&self,
357+
message_id: &str,
358+
closure: C,
359+
) -> Option<OUT>
360+
where
361+
C: Fn(FluentMessage<'_>, &FluentBundle<Arc<FluentResource>, IntlLangMemoizer>) -> OUT,
362+
{
363+
self.inner
364+
.load()
365+
.language_config
366+
.read()
367+
.language_bundles
368+
.iter()
369+
.flat_map(|language_bundles| language_bundles.iter())
370+
.find_map(|language_bundle| {
371+
Some((
372+
language_bundle.bundle.get_message(message_id)?,
373+
&language_bundle.bundle,
374+
))
375+
})
376+
.map(|(msg, bundle)| closure(msg, bundle))
377+
}
378+
345379
/// Runs the provided `closure` with an iterator over the messages
346380
/// available for the specified `language`. There may be duplicate
347381
/// messages when they are duplicated in resources applicable to

0 commit comments

Comments
 (0)