Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion i18n-embed-fl/i18n/en-US/i18n_embed_fl.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,13 @@ hello-arg = Hello {$name}!
.attr = Hello {$name}'s attribute!
hello-arg-2 = Hello {$name1} and {$name2}!
hello-attr = Uninspiring.
.text = Hello, attribute!
.text = Hello, attribute!
hello-recursive = Hello { hello-recursive-descent }
.attr = Why hello { hello-recursive-descent }
.again = Why hello { hello-recursive-descent.attr }
hello-recursive-descent = to you, {$name}!
.attr = again, {$name}!
hello-select = { $attr ->
*[no] { hello-recursive }
[yes] { hello-recursive.attr }
}
102 changes: 77 additions & 25 deletions i18n-embed-fl/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use fluent::{FluentAttribute, FluentMessage};
use fluent::concurrent::FluentBundle;
use fluent::{FluentAttribute, FluentMessage, FluentResource};
use fluent_syntax::ast::{CallArguments, Expression, InlineExpression, Pattern, PatternElement};
use i18n_embed::{fluent::FluentLanguageLoader, FileSystemAssets, LanguageLoader};
use proc_macro::TokenStream;
Expand Down Expand Up @@ -504,8 +505,8 @@ pub fn fl(input: TokenStream) -> TokenStream {
if let Some(message_id_str) = &message_id_string {
checked_loader_has_message = domain_data
.loader
.with_fluent_message(message_id_str, |message: FluentMessage<'_>| {
check_message_args(message, &specified_args);
.with_fluent_message_and_bundle(message_id_str, |message, bundle| {
check_message_args(message, bundle, &specified_args);
})
.is_some();
}
Expand All @@ -524,11 +525,11 @@ pub fn fl(input: TokenStream) -> TokenStream {
} else {
if let Some(message_id_str) = &message_id_string {
if let Some(attr_id_str) = &attr_str {
let attr_res = domain_data.loader.with_fluent_message(
let attr_res = domain_data.loader.with_fluent_message_and_bundle(
message_id_str,
|message: FluentMessage<'_>| match message.get_attribute(attr_id_str) {
|message, bundle| match message.get_attribute(attr_id_str) {
Some(attr) => {
check_attribute_args(attr, &specified_args);
check_attribute_args(attr, bundle, &specified_args);
true
}
None => false,
Expand Down Expand Up @@ -668,13 +669,16 @@ fn fuzzy_attribute_suggestions(
.collect()
}

fn check_message_args(
fn check_message_args<R>(
message: FluentMessage<'_>,
bundle: &FluentBundle<R>,
specified_args: &HashMap<syn::LitStr, Box<syn::Expr>>,
) {
) where
R: std::borrow::Borrow<FluentResource>,
{
if let Some(pattern) = message.value() {
let mut args = Vec::new();
args_from_pattern(pattern, &mut args);
args_from_pattern(pattern, bundle, &mut args);

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

Expand Down Expand Up @@ -735,13 +739,16 @@ fn check_message_args(
}
}

fn check_attribute_args(
fn check_attribute_args<R>(
attr: FluentAttribute<'_>,
bundle: &FluentBundle<R>,
specified_args: &HashMap<syn::LitStr, Box<syn::Expr>>,
) {
) where
R: std::borrow::Borrow<FluentResource>,
{
let pattern = attr.value();
let mut args = Vec::new();
args_from_pattern(pattern, &mut args);
args_from_pattern(pattern, bundle, &mut args);

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

Expand Down Expand Up @@ -801,56 +808,101 @@ fn check_attribute_args(
}
}

fn args_from_pattern<S: Copy>(pattern: &Pattern<S>, args: &mut Vec<S>) {
fn args_from_pattern<'m, R>(
pattern: &Pattern<&'m str>,
bundle: &'m FluentBundle<R>,
args: &mut Vec<&'m str>,
) where
R: std::borrow::Borrow<FluentResource>,
{
pattern.elements.iter().for_each(|element| {
if let PatternElement::Placeable { expression } = element {
args_from_expression(expression, args)
args_from_expression(expression, bundle, args)
}
});
}

fn args_from_expression<S: Copy>(expr: &Expression<S>, args: &mut Vec<S>) {
fn args_from_expression<'m, R>(
expr: &Expression<&'m str>,
bundle: &'m FluentBundle<R>,
args: &mut Vec<&'m str>,
) where
R: std::borrow::Borrow<FluentResource>,
{
match expr {
Expression::Inline(inline_expr) => {
args_from_inline_expression(inline_expr, args);
args_from_inline_expression(inline_expr, bundle, args);
}
Expression::Select { selector, variants } => {
args_from_inline_expression(selector, args);
args_from_inline_expression(selector, bundle, args);

variants.iter().for_each(|variant| {
args_from_pattern(&variant.value, args);
args_from_pattern(&variant.value, bundle, args);
})
}
}
}

fn args_from_inline_expression<S: Copy>(inline_expr: &InlineExpression<S>, args: &mut Vec<S>) {
fn args_from_inline_expression<'m, R>(
inline_expr: &InlineExpression<&'m str>,
bundle: &'m FluentBundle<R>,
args: &mut Vec<&'m str>,
) where
R: std::borrow::Borrow<FluentResource>,
{
match inline_expr {
InlineExpression::FunctionReference {
id: _,
arguments: call_args,
} => {
args_from_call_arguments(call_args, args);
args_from_call_arguments(call_args, bundle, args);
}
InlineExpression::TermReference {
id: _,
attribute: _,
arguments: Some(call_args),
} => {
args_from_call_arguments(call_args, args);
args_from_call_arguments(call_args, bundle, args);
}
InlineExpression::VariableReference { id } => args.push(id.name),
InlineExpression::Placeable { expression } => args_from_expression(expression, args),
InlineExpression::Placeable { expression } => {
args_from_expression(expression, bundle, args)
}
InlineExpression::MessageReference {
id,
attribute: None,
} => {
bundle
.get_message(&id.name)
.and_then(|m| m.value())
.map(|p| args_from_pattern(p, bundle, args));
}
InlineExpression::MessageReference {
id,
attribute: Some(attribute),
} => {
bundle
.get_message(&id.name)
.and_then(|m| m.get_attribute(&attribute.name))
.map(|m| m.value())
.map(|p| args_from_pattern(p, bundle, args));
}
_ => {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we go ahead and match this enum exhaustively? The source enum is not marked #[non_exhaustive], which means that any additions on fluent's part can be considered breaking. Making our match exhaustive will detect any future additions to the AST with breakage here.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @cbs228 that's a great observation! Let's do this in a future PR perhaps? I've created an issue for it #148

}
}

fn args_from_call_arguments<S: Copy>(call_args: &CallArguments<S>, args: &mut Vec<S>) {
fn args_from_call_arguments<'m, R>(
call_args: &CallArguments<&'m str>,
bundle: &'m FluentBundle<R>,
args: &mut Vec<&'m str>,
) where
R: std::borrow::Borrow<FluentResource>,
{
call_args.positional.iter().for_each(|expr| {
args_from_inline_expression(expr, args);
args_from_inline_expression(expr, bundle, args);
});

call_args.named.iter().for_each(|named_arg| {
args_from_inline_expression(&named_arg.value, args);
args_from_inline_expression(&named_arg.value, bundle, args);
})
}
57 changes: 57 additions & 0 deletions i18n-embed-fl/tests/fl_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,60 @@ fn with_attr_and_args() {
fl!(loader, "hello-arg", "attr", name = "Bob")
);
}

#[test]
fn with_args_in_messagereference() {
let loader: FluentLanguageLoader = fluent_language_loader!();
loader
.load_languages(&Localizations, &[loader.fallback_language().clone()])
.unwrap();

pretty_assertions::assert_eq!(
"Hello to you, \u{2068}Bob\u{2069}!",
fl!(loader, "hello-recursive", name = "Bob")
);
}

#[test]
fn with_args_in_messagereference_attr() {
let loader: FluentLanguageLoader = fluent_language_loader!();
loader
.load_languages(&Localizations, &[loader.fallback_language().clone()])
.unwrap();

pretty_assertions::assert_eq!(
"Why hello to you, \u{2068}Bob\u{2069}!",
fl!(loader, "hello-recursive", "attr", name = "Bob")
);
}

#[test]
fn with_args_in_messagereference_attr_to_attr() {
let loader: FluentLanguageLoader = fluent_language_loader!();
loader
.load_languages(&Localizations, &[loader.fallback_language().clone()])
.unwrap();

pretty_assertions::assert_eq!(
"Why hello again, \u{2068}Bob\u{2069}!",
fl!(loader, "hello-recursive", "again", name = "Bob")
);
}

#[test]
fn with_args_in_select_messagereference() {
let loader: FluentLanguageLoader = fluent_language_loader!();
loader
.load_languages(&Localizations, &[loader.fallback_language().clone()])
.unwrap();

pretty_assertions::assert_eq!(
"Hello to you, \u{2068}Bob\u{2069}!",
fl!(loader, "hello-select", attr = "", name = "Bob")
);

pretty_assertions::assert_eq!(
"Why hello to you, \u{2068}Bob\u{2069}!",
fl!(loader, "hello-select", attr = "yes", name = "Bob")
);
}
34 changes: 34 additions & 0 deletions i18n-embed/src/fluent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,40 @@ impl FluentLanguageLoader {
.map(closure)
}

/// Searches for a message named `message_id` in all languages that
/// are currently loaded, including the fallback language. If the
/// message is found, invokes the `closure` with the:
///
/// 0. [message](FluentMessage)
/// 1. the language-specific [bundle](FluentBundle)
/// that owns it.
///
/// Returns `Some` of whatever the closure returns, or `None` if no
/// messages were found matching the `message_id`.
pub fn with_fluent_message_and_bundle<OUT, C>(
&self,
message_id: &str,
closure: C,
) -> Option<OUT>
where
C: Fn(FluentMessage<'_>, &FluentBundle<Arc<FluentResource>, IntlLangMemoizer>) -> OUT,
{
self.inner
.load()
.language_config
.read()
.language_bundles
.iter()
.flat_map(|language_bundles| language_bundles.iter())
.find_map(|language_bundle| {
Some((
language_bundle.bundle.get_message(message_id)?,
&language_bundle.bundle,
))
})
.map(|(msg, bundle)| closure(msg, bundle))
}

/// Runs the provided `closure` with an iterator over the messages
/// available for the specified `language`. There may be duplicate
/// messages when they are duplicated in resources applicable to
Expand Down
Loading