i18n_embed_fl/
lib.rs

1use fluent::concurrent::FluentBundle;
2use fluent::{FluentAttribute, FluentMessage, FluentResource};
3use fluent_syntax::ast::{CallArguments, Expression, InlineExpression, Pattern, PatternElement};
4use i18n_embed::{fluent::FluentLanguageLoader, FileSystemAssets, LanguageLoader};
5use proc_macro::TokenStream;
6use proc_macro_error2::{abort, emit_error, proc_macro_error};
7use quote::quote;
8use std::{
9    collections::{HashMap, HashSet},
10    path::Path,
11    sync::OnceLock,
12};
13
14#[cfg(feature = "dashmap")]
15use dashmap::mapref::one::Ref;
16#[cfg(not(feature = "dashmap"))]
17use std::sync::{Arc, RwLock};
18
19use syn::{parse::Parse, parse_macro_input, spanned::Spanned};
20use unic_langid::LanguageIdentifier;
21
22#[cfg(doctest)]
23#[macro_use]
24extern crate doc_comment;
25
26#[cfg(doctest)]
27doctest!("../README.md");
28
29#[derive(Debug)]
30enum FlAttr {
31    /// An attribute ID got provided.
32    Attr(syn::Lit),
33    /// No attribute ID got provided.
34    None,
35}
36
37impl Parse for FlAttr {
38    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
39        if !input.is_empty() {
40            let fork = input.fork();
41            fork.parse::<syn::Token![,]>()?;
42            if fork.parse::<syn::Lit>().is_ok()
43                && (fork.parse::<syn::Token![,]>().is_ok() || fork.is_empty())
44            {
45                input.parse::<syn::Token![,]>()?;
46                let literal = input.parse::<syn::Lit>()?;
47                Ok(Self::Attr(literal))
48            } else {
49                Ok(Self::None)
50            }
51        } else {
52            Ok(Self::None)
53        }
54    }
55}
56
57#[derive(Debug)]
58enum FlArgs {
59    /// `fl!(LOADER, "message", "optional-attribute", args)` where `args` is a
60    /// `HashMap<&'a str, FluentValue<'a>>`.
61    HashMap(syn::Expr),
62    /// ```ignore
63    /// fl!(LOADER, "message", "optional-attribute",
64    ///     arg1 = "value",
65    ///     arg2 = value2,
66    ///     arg3 = calc_value());
67    /// ```
68    KeyValuePairs {
69        specified_args: HashMap<syn::LitStr, Box<syn::Expr>>,
70    },
71    /// `fl!(LOADER, "message", "optional-attribute")` no arguments after the message id and optional attribute id.
72    None,
73}
74
75impl Parse for FlArgs {
76    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
77        if !input.is_empty() {
78            input.parse::<syn::Token![,]>()?;
79
80            let lookahead = input.fork();
81            if lookahead.parse::<syn::ExprAssign>().is_err() {
82                let hash_map = input.parse()?;
83                return Ok(FlArgs::HashMap(hash_map));
84            }
85
86            let mut args_map: HashMap<syn::LitStr, Box<syn::Expr>> = HashMap::new();
87
88            while let Ok(expr) = input.parse::<syn::ExprAssign>() {
89                let argument_name_ident_opt = match &*expr.left {
90                    syn::Expr::Path(path) => path.path.get_ident(),
91                    _ => None,
92                };
93
94                let argument_name_ident = match argument_name_ident_opt {
95                    Some(ident) => ident,
96                    None => {
97                        return Err(syn::Error::new(
98                            expr.left.span(),
99                            "fl!() unable to parse argument identifier",
100                        ))
101                    }
102                }
103                .clone();
104
105                let argument_name_string = argument_name_ident.to_string();
106                let argument_name_lit_str =
107                    syn::LitStr::new(&argument_name_string, argument_name_ident.span());
108
109                let argument_value = expr.right;
110
111                if let Some(_duplicate) = args_map.insert(argument_name_lit_str, argument_value) {
112                    // There's no Clone implementation by default.
113                    let argument_name_lit_str =
114                        syn::LitStr::new(&argument_name_string, argument_name_ident.span());
115                    return Err(syn::Error::new(
116                        argument_name_lit_str.span(),
117                        format!(
118                            "fl!() macro contains a duplicate argument `{}`",
119                            argument_name_lit_str.value()
120                        ),
121                    ));
122                }
123
124                // parse the next comma if there is one
125                let _result = input.parse::<syn::Token![,]>();
126            }
127
128            if args_map.is_empty() {
129                let span = match input.fork().parse::<syn::Expr>() {
130                    Ok(expr) => expr.span(),
131                    Err(_) => input.span(),
132                };
133                Err(syn::Error::new(span, "fl!() unable to parse args input"))
134            } else {
135                Ok(FlArgs::KeyValuePairs {
136                    specified_args: args_map,
137                })
138            }
139        } else {
140            Ok(FlArgs::None)
141        }
142    }
143}
144
145/// Input for the [fl()] macro.
146struct FlMacroInput {
147    fluent_loader: syn::Expr,
148    message_id: syn::Lit,
149    attr: FlAttr,
150    args: FlArgs,
151}
152
153impl Parse for FlMacroInput {
154    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
155        let fluent_loader = input.parse()?;
156        input.parse::<syn::Token![,]>()?;
157        let message_id = input.parse()?;
158        let attr = input.parse()?;
159        let args = input.parse()?;
160
161        Ok(Self {
162            fluent_loader,
163            message_id,
164            attr,
165            args,
166        })
167    }
168}
169
170struct DomainSpecificData {
171    loader: FluentLanguageLoader,
172    _assets: FileSystemAssets,
173}
174
175#[derive(Default)]
176struct DomainsMap {
177    #[cfg(not(feature = "dashmap"))]
178    map: RwLock<HashMap<String, Arc<DomainSpecificData>>>,
179
180    #[cfg(feature = "dashmap")]
181    map: dashmap::DashMap<String, DomainSpecificData>,
182}
183
184#[cfg(feature = "dashmap")]
185impl DomainsMap {
186    fn get(&self, domain: &String) -> Option<Ref<String, DomainSpecificData>> {
187        self.map.get(domain)
188    }
189
190    fn entry_or_insert(
191        &self,
192        domain: &String,
193        data: DomainSpecificData,
194    ) -> Ref<String, DomainSpecificData> {
195        self.map.entry(domain.clone()).or_insert(data).downgrade()
196    }
197}
198
199#[cfg(not(feature = "dashmap"))]
200impl DomainsMap {
201    fn get(&self, domain: &String) -> Option<Arc<DomainSpecificData>> {
202        match self.map.read().unwrap().get(domain) {
203            None => None,
204            Some(data) => Some(data.clone()),
205        }
206    }
207
208    fn entry_or_insert(
209        &self,
210        domain: &String,
211        data: DomainSpecificData,
212    ) -> Arc<DomainSpecificData> {
213        self.map
214            .write()
215            .unwrap()
216            .entry(domain.clone())
217            .or_insert(Arc::new(data))
218            .clone()
219    }
220}
221
222fn domains() -> &'static DomainsMap {
223    static DOMAINS: OnceLock<DomainsMap> = OnceLock::new();
224
225    DOMAINS.get_or_init(|| DomainsMap::default())
226}
227
228/// A macro to obtain localized messages and optionally their attributes, and check the `message_id`, `attribute_id`
229/// and arguments at compile time.
230///
231/// Compile time checks are performed using the `fallback_language`
232/// specified in the current crate's `i18n.toml` confiration file.
233///
234/// This macro supports three different calling syntaxes which are
235/// explained in the following sections.
236///
237/// ## No Arguments
238///
239/// ```ignore
240/// fl!(loader: FluentLanguageLoader, "message_id")
241/// ```
242///
243/// This is the simplest form of the `fl!()` macro, just obtaining a
244/// message with no arguments. The `message_id` should be specified as
245/// a literal string, and is checked at compile time.
246///
247/// ### Example
248///
249/// ```
250/// use i18n_embed::{
251///     fluent::{fluent_language_loader, FluentLanguageLoader},
252///     LanguageLoader,
253/// };
254/// use i18n_embed_fl::fl;
255/// use rust_embed::RustEmbed;
256///
257/// #[derive(RustEmbed)]
258/// #[folder = "i18n/"]
259/// struct Localizations;
260///
261/// let loader: FluentLanguageLoader = fluent_language_loader!();
262/// loader
263///     .load_languages(&Localizations, &[loader.fallback_language().clone()])
264///     .unwrap();
265///
266/// // Invoke the fl!() macro to obtain the translated message, and
267/// // check the message id compile time.
268/// assert_eq!("Hello World!", fl!(loader, "hello-world"));
269/// ```
270///
271/// ## Individual Arguments
272///
273/// ```ignore
274/// fl!(
275///     loader: FluentLanguageLoader,
276///     "message_id",
277///     arg1 = value,
278///     arg2 = "value",
279///     arg3 = function(),
280///     ...
281/// )
282/// ```
283///
284/// This form of the `fl!()` macro allows individual arguments to be
285/// specified in the form `key = value` after the `message_id`. `key`
286/// needs to be a valid literal argument name, and `value` can be any
287/// expression that resolves to a type that implements
288/// `Into<FluentValue>`. The `key`s will be checked at compile time to
289/// ensure that they match the arguments specified in original fluent
290/// message.
291///
292/// ### Example
293///
294/// ```
295/// # use i18n_embed::{
296/// #     fluent::{fluent_language_loader, FluentLanguageLoader},
297/// #     LanguageLoader,
298/// # };
299/// # use i18n_embed_fl::fl;
300/// # use rust_embed::RustEmbed;
301/// # #[derive(RustEmbed)]
302/// # #[folder = "i18n/"]
303/// # struct Localizations;
304/// # let loader: FluentLanguageLoader = fluent_language_loader!();
305/// # loader
306/// #     .load_languages(&Localizations, &[loader.fallback_language().clone()])
307/// #     .unwrap();
308/// let calc_james = || "James".to_string();
309/// pretty_assertions::assert_eq!(
310///     "Hello \u{2068}Bob\u{2069} and \u{2068}James\u{2069}!",
311///     // Invoke the fl!() macro to obtain the translated message, and
312///     // check the message id, and arguments at compile time.
313///     fl!(loader, "hello-arg-2", name1 = "Bob", name2 = calc_james())
314/// );
315/// ```
316///
317/// ## Arguments Hashmap
318///
319/// ```ignore
320/// fl!(
321///     loader: FluentLanguageLoader,
322///     "message_id",
323///     args: HashMap<
324///         S where S: Into<Cow<'a, str>> + Clone,
325///         T where T: Into<FluentValue>> + Clone>
326/// )
327/// ```
328///
329/// With this form of the `fl!()` macro, arguments can be specified at
330/// runtime using a [HashMap](std::collections::HashMap), using the
331/// same signature as in
332/// [FluentLanguageLoader::get_args()](i18n_embed::fluent::FluentLanguageLoader::get_args()).
333/// When using this method of specifying argments, they are not
334/// checked at compile time.
335///
336/// ### Example
337///
338/// ```
339/// # use i18n_embed::{
340/// #     fluent::{fluent_language_loader, FluentLanguageLoader},
341/// #     LanguageLoader,
342/// # };
343/// # use i18n_embed_fl::fl;
344/// # use rust_embed::RustEmbed;
345/// # #[derive(RustEmbed)]
346/// # #[folder = "i18n/"]
347/// # struct Localizations;
348/// # let loader: FluentLanguageLoader = fluent_language_loader!();
349/// # loader
350/// #     .load_languages(&Localizations, &[loader.fallback_language().clone()])
351/// #     .unwrap();
352/// use std::collections::HashMap;
353///
354/// let mut args: HashMap<&str, &str> = HashMap::new();
355/// args.insert("name", "Bob");
356///
357/// assert_eq!("Hello \u{2068}Bob\u{2069}!", fl!(loader, "hello-arg", args));
358/// ```
359///
360/// ## Attributes
361///
362/// In all of the above patterns you can optionally include an `attribute_id`
363/// after the `message_id`, in which case `fl!` will attempt retrieving the specified
364/// attribute belonging to the specified message, optionally formatted with the provided arguments.
365///
366/// ### Example
367///
368/// ```
369/// # use i18n_embed::{
370/// #     fluent::{fluent_language_loader, FluentLanguageLoader},
371/// #     LanguageLoader,
372/// # };
373/// # use i18n_embed_fl::fl;
374/// # use rust_embed::RustEmbed;
375/// # #[derive(RustEmbed)]
376/// # #[folder = "i18n/"]
377/// # struct Localizations;
378/// # let loader: FluentLanguageLoader = fluent_language_loader!();
379/// # loader
380/// #     .load_languages(&Localizations, &[loader.fallback_language().clone()])
381/// #     .unwrap();
382/// use std::collections::HashMap;
383///
384/// let mut args: HashMap<&str, &str> = HashMap::new();
385/// args.insert("name", "Bob");
386///
387/// assert_eq!("Hello \u{2068}Bob\u{2069}'s attribute!", fl!(loader, "hello-arg", "attr", args));
388/// ```
389#[proc_macro]
390#[proc_macro_error]
391pub fn fl(input: TokenStream) -> TokenStream {
392    let input: FlMacroInput = parse_macro_input!(input as FlMacroInput);
393
394    let fluent_loader = input.fluent_loader;
395    let message_id = input.message_id;
396
397    let domain = {
398        let manifest = find_crate::Manifest::new().expect("Error reading Cargo.toml");
399        manifest.crate_package().map(|pkg| pkg.name).unwrap_or(
400            std::env::var("CARGO_PKG_NAME").expect("Error fetching `CARGO_PKG_NAME` env"),
401        )
402    };
403
404    let domain_data = if let Some(domain_data) = domains().get(&domain) {
405        domain_data
406    } else {
407        let crate_paths = i18n_config::locate_crate_paths()
408            .unwrap_or_else(|error| panic!("fl!() is unable to locate crate paths: {}", error));
409
410        let config_file_path = &crate_paths.i18n_config_file;
411
412        let config = i18n_config::I18nConfig::from_file(config_file_path).unwrap_or_else(|err| {
413            abort! {
414                proc_macro2::Span::call_site(),
415                format!(
416                    "fl!() had a problem reading i18n config file {config_file_path:?}: {err}"
417                );
418                help = "Try creating the `i18n.toml` configuration file.";
419            }
420        });
421
422        let fluent_config = config.fluent.unwrap_or_else(|| {
423            abort! {
424                proc_macro2::Span::call_site(),
425                format!(
426                    "fl!() had a problem parsing i18n config file {config_file_path:?}: \
427                    there is no `[fluent]` subsection."
428                );
429                help = "Add the `[fluent]` subsection to `i18n.toml`, \
430                        along with its required `assets_dir`.";
431            }
432        });
433
434        // Use the domain override in the configuration.
435        let domain = fluent_config.domain.unwrap_or(domain);
436
437        let assets_dir = Path::new(&crate_paths.crate_dir).join(fluent_config.assets_dir);
438        let assets = FileSystemAssets::try_new(assets_dir).unwrap();
439
440        let fallback_language: LanguageIdentifier = config.fallback_language;
441
442        let loader = FluentLanguageLoader::new(&domain, fallback_language.clone());
443
444        loader
445            .load_languages(&assets, &[fallback_language.clone()])
446            .unwrap_or_else(|err| match err {
447                i18n_embed::I18nEmbedError::LanguageNotAvailable(file, language_id) => {
448                    if fallback_language != language_id {
449                        panic!(
450                            "fl!() encountered an unexpected problem, \
451                            the language being loaded (\"{0}\") is not the \
452                            `fallback_language` (\"{1}\")",
453                            language_id, fallback_language
454                        )
455                    }
456                    abort! {
457                        proc_macro2::Span::call_site(),
458                        format!(
459                            "fl!() was unable to load the localization \
460                            file for the `fallback_language` \
461                            (\"{fallback_language}\"): {file}"
462                        );
463                        help = "Try creating the required fluent localization file.";
464                    }
465                }
466                _ => panic!(
467                    "fl!() had an unexpected problem while \
468                        loading language \"{0}\": {1}",
469                    fallback_language, err
470                ),
471            });
472
473        let data = DomainSpecificData {
474            loader,
475            _assets: assets,
476        };
477
478        domains().entry_or_insert(&domain, data)
479    };
480
481    let message_id_string = match &message_id {
482        syn::Lit::Str(message_id_str) => {
483            let message_id_str = message_id_str.value();
484            Some(message_id_str)
485        }
486        unexpected_lit => {
487            emit_error! {
488                unexpected_lit,
489                "fl!() `message_id` should be a literal rust string"
490            };
491            None
492        }
493    };
494
495    let attr = input.attr;
496    let attr_str;
497    let attr_lit = match &attr {
498        FlAttr::Attr(literal) => match literal {
499            syn::Lit::Str(string_lit) => {
500                attr_str = Some(string_lit.value());
501                Some(literal)
502            }
503            unexpected_lit => {
504                attr_str = None;
505                emit_error! {
506                    unexpected_lit,
507                    "fl!() `message_id` should be a literal rust string"
508                };
509                None
510            }
511        },
512        FlAttr::None => {
513            attr_str = None;
514            None
515        }
516    };
517
518    // If we have already confirmed that the loader has the message.
519    // `false` if we haven't checked, or we have checked but no
520    // message was found.
521    let mut checked_loader_has_message = false;
522    // Same procedure for attributes
523    let mut checked_message_has_attribute = false;
524
525    let gen = match input.args {
526        FlArgs::HashMap(args_hash_map) => {
527            if attr_lit.is_none() {
528                quote! {
529                    (#fluent_loader).get_args(#message_id, #args_hash_map)
530                }
531            } else {
532                quote! {
533                    (#fluent_loader).get_attr_args(#message_id, #attr_lit, #args_hash_map)
534                }
535            }
536        }
537        FlArgs::None => {
538            if attr_lit.is_none() {
539                quote! {
540                    (#fluent_loader).get(#message_id)
541                }
542            } else {
543                quote! {
544                    (#fluent_loader).get_attr(#message_id, #attr_lit)
545                }
546            }
547        }
548        FlArgs::KeyValuePairs { specified_args } => {
549            let mut arg_assignments = proc_macro2::TokenStream::default();
550            for (key, value) in &specified_args {
551                arg_assignments = quote! {
552                    #arg_assignments
553                    args.insert(#key, #value.into());
554                }
555            }
556
557            if attr_lit.is_none() {
558                if let Some(message_id_str) = &message_id_string {
559                    checked_loader_has_message = domain_data
560                        .loader
561                        .with_fluent_message_and_bundle(message_id_str, |message, bundle| {
562                            check_message_args(message, bundle, &specified_args);
563                        })
564                        .is_some();
565                }
566
567                let gen = quote! {
568                    (#fluent_loader).get_args_concrete(
569                        #message_id,
570                        {
571                            let mut args = std::collections::HashMap::new();
572                            #arg_assignments
573                            args
574                        })
575                };
576
577                gen
578            } else {
579                if let Some(message_id_str) = &message_id_string {
580                    if let Some(attr_id_str) = &attr_str {
581                        let attr_res = domain_data.loader.with_fluent_message_and_bundle(
582                            message_id_str,
583                            |message, bundle| match message.get_attribute(attr_id_str) {
584                                Some(attr) => {
585                                    check_attribute_args(attr, bundle, &specified_args);
586                                    true
587                                }
588                                None => false,
589                            },
590                        );
591                        checked_loader_has_message = attr_res.is_some();
592                        checked_message_has_attribute = attr_res.unwrap_or(false);
593                    }
594                }
595
596                let gen = quote! {
597                    (#fluent_loader).get_attr_args_concrete(
598                        #message_id,
599                        #attr_lit,
600                        {
601                            let mut args = std::collections::HashMap::new();
602                            #arg_assignments
603                            args
604                        })
605                };
606
607                gen
608            }
609        }
610    };
611
612    if let Some(message_id_str) = &message_id_string {
613        if !checked_loader_has_message && !domain_data.loader.has(message_id_str) {
614            let suggestions =
615                fuzzy_message_suggestions(&domain_data.loader, message_id_str, 5).join("\n");
616
617            let hint = format!(
618                "Perhaps you are looking for one of the following messages?\n\n\
619                {suggestions}"
620            );
621
622            emit_error! {
623                message_id,
624                format!(
625                    "fl!() `message_id` validation failed. `message_id` \
626                    of \"{0}\" does not exist in the `fallback_language` (\"{1}\")",
627                    message_id_str,
628                    domain_data.loader.current_language(),
629                );
630                help = "Enter the correct `message_id` or create \
631                        the message in the localization file if the \
632                        intended message does not yet exist.";
633
634                hint = hint;
635            };
636        } else if let Some(attr_id_str) = &attr_str {
637            if !checked_message_has_attribute
638                && !&domain_data.loader.has_attr(message_id_str, attr_id_str)
639            {
640                let suggestions = &domain_data
641                    .loader
642                    .with_fluent_message(message_id_str, |message| {
643                        fuzzy_attribute_suggestions(&message, attr_id_str, 5).join("\n")
644                    })
645                    .unwrap();
646
647                let hint = format!(
648                    "Perhaps you are looking for one of the following attributes?\n\n\
649                    {suggestions}"
650                );
651
652                emit_error! {
653                    attr_lit,
654                    format!(
655                        "fl!() `attribute_id` validation failed. `attribute_id` \
656                        of \"{0}\" does not exist in the `fallback_language` (\"{1}\")",
657                        attr_id_str,
658                        domain_data.loader.current_language(),
659                    );
660                    help = "Enter the correct `attribute_id` or create \
661                            the attribute associated with the message in the localization file if the \
662                            intended attribute does not yet exist.";
663
664                    hint = hint;
665                };
666            }
667        }
668    }
669
670    gen.into()
671}
672
673fn fuzzy_message_suggestions(
674    loader: &FluentLanguageLoader,
675    message_id_str: &str,
676    n_suggestions: usize,
677) -> Vec<String> {
678    let mut scored_messages: Vec<(String, usize)> =
679        loader.with_message_iter(loader.fallback_language(), |message_iter| {
680            message_iter
681                .map(|message| {
682                    (
683                        message.id.name.to_string(),
684                        strsim::levenshtein(message_id_str, message.id.name),
685                    )
686                })
687                .collect()
688        });
689
690    scored_messages.sort_by_key(|(_message, score)| *score);
691
692    scored_messages.truncate(n_suggestions);
693
694    scored_messages
695        .into_iter()
696        .map(|(message, _score)| message)
697        .collect()
698}
699
700fn fuzzy_attribute_suggestions(
701    message: &FluentMessage<'_>,
702    attribute_id_str: &str,
703    n_suggestions: usize,
704) -> Vec<String> {
705    let mut scored_attributes: Vec<(String, usize)> = message
706        .attributes()
707        .map(|attribute| {
708            (
709                attribute.id().to_string(),
710                strsim::levenshtein(attribute_id_str, attribute.id()),
711            )
712        })
713        .collect();
714
715    scored_attributes.sort_by_key(|(_attr, score)| *score);
716
717    scored_attributes.truncate(n_suggestions);
718
719    scored_attributes
720        .into_iter()
721        .map(|(attribute, _score)| attribute)
722        .collect()
723}
724
725fn check_message_args<R>(
726    message: FluentMessage<'_>,
727    bundle: &FluentBundle<R>,
728    specified_args: &HashMap<syn::LitStr, Box<syn::Expr>>,
729) where
730    R: std::borrow::Borrow<FluentResource>,
731{
732    if let Some(pattern) = message.value() {
733        let mut args = Vec::new();
734        args_from_pattern(pattern, bundle, &mut args);
735
736        let args_set: HashSet<&str> = args.into_iter().collect();
737
738        let key_args: Vec<String> = specified_args
739            .keys()
740            .map(|key| {
741                let arg = key.value();
742
743                if !args_set.contains(arg.as_str()) {
744                    let available_args: String = args_set
745                        .iter()
746                        .map(|arg| format!("`{arg}`"))
747                        .collect::<Vec<String>>()
748                        .join(", ");
749
750                    emit_error! {
751                        key,
752                        format!(
753                            "fl!() argument `{0}` does not exist in the \
754                            fluent message. Available arguments: {1}.",
755                            &arg, available_args
756                        );
757                        help = "Enter the correct arguments, or fix the message \
758                                in the fluent localization file so that the arguments \
759                                match this macro invocation.";
760                    };
761                }
762
763                arg
764            })
765            .collect();
766
767        let key_args_set: HashSet<&str> = key_args.iter().map(|v| v.as_str()).collect();
768
769        let unspecified_args: Vec<String> = args_set
770            .iter()
771            .filter_map(|arg| {
772                if !key_args_set.contains(arg) {
773                    Some(format!("`{arg}`"))
774                } else {
775                    None
776                }
777            })
778            .collect();
779
780        if !unspecified_args.is_empty() {
781            emit_error! {
782                proc_macro2::Span::call_site(),
783                format!(
784                    "fl!() the following arguments have not been specified: {}",
785                    unspecified_args.join(", ")
786                );
787                help = "Enter the correct arguments, or fix the message \
788                        in the fluent localization file so that the arguments \
789                        match this macro invocation.";
790            };
791        }
792    }
793}
794
795fn check_attribute_args<R>(
796    attr: FluentAttribute<'_>,
797    bundle: &FluentBundle<R>,
798    specified_args: &HashMap<syn::LitStr, Box<syn::Expr>>,
799) where
800    R: std::borrow::Borrow<FluentResource>,
801{
802    let pattern = attr.value();
803    let mut args = Vec::new();
804    args_from_pattern(pattern, bundle, &mut args);
805
806    let args_set: HashSet<&str> = args.into_iter().collect();
807
808    let key_args: Vec<String> = specified_args
809        .keys()
810        .map(|key| {
811            let arg = key.value();
812
813            if !args_set.contains(arg.as_str()) {
814                let available_args: String = args_set
815                    .iter()
816                    .map(|arg| format!("`{arg}`"))
817                    .collect::<Vec<String>>()
818                    .join(", ");
819
820                emit_error! {
821                    key,
822                    format!(
823                        "fl!() argument `{0}` does not exist in the \
824                        fluent attribute. Available arguments: {1}.",
825                        &arg, available_args
826                    );
827                    help = "Enter the correct arguments, or fix the attribute \
828                            in the fluent localization file so that the arguments \
829                            match this macro invocation.";
830                };
831            }
832
833            arg
834        })
835        .collect();
836
837    let key_args_set: HashSet<&str> = key_args.iter().map(|v| v.as_str()).collect();
838
839    let unspecified_args: Vec<String> = args_set
840        .iter()
841        .filter_map(|arg| {
842            if !key_args_set.contains(arg) {
843                Some(format!("`{arg}`"))
844            } else {
845                None
846            }
847        })
848        .collect();
849
850    if !unspecified_args.is_empty() {
851        emit_error! {
852            proc_macro2::Span::call_site(),
853            format!(
854                "fl!() the following arguments have not been specified: {}",
855                unspecified_args.join(", ")
856            );
857            help = "Enter the correct arguments, or fix the attribute \
858                    in the fluent localization file so that the arguments \
859                    match this macro invocation.";
860        };
861    }
862}
863
864fn 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{
871    pattern.elements.iter().for_each(|element| {
872        if let PatternElement::Placeable { expression } = element {
873            args_from_expression(expression, bundle, args)
874        }
875    });
876}
877
878fn 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{
885    match expr {
886        Expression::Inline(inline_expr) => {
887            args_from_inline_expression(inline_expr, bundle, args);
888        }
889        Expression::Select { selector, variants } => {
890            args_from_inline_expression(selector, bundle, args);
891
892            variants.iter().for_each(|variant| {
893                args_from_pattern(&variant.value, bundle, args);
894            })
895        }
896    }
897}
898
899fn 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{
906    match inline_expr {
907        InlineExpression::FunctionReference {
908            id: _,
909            arguments: call_args,
910        } => {
911            args_from_call_arguments(call_args, bundle, args);
912        }
913        InlineExpression::TermReference {
914            id: _,
915            attribute: _,
916            arguments: Some(call_args),
917        } => {
918            args_from_call_arguments(call_args, bundle, args);
919        }
920        InlineExpression::VariableReference { id } => args.push(id.name),
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        }
943        _ => {}
944    }
945}
946
947fn 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{
954    call_args.positional.iter().for_each(|expr| {
955        args_from_inline_expression(expr, bundle, args);
956    });
957
958    call_args.named.iter().for_each(|named_arg| {
959        args_from_inline_expression(&named_arg.value, bundle, args);
960    })
961}