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