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 Attr(syn::Lit),
33 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 HashMap(syn::Expr),
62 KeyValuePairs {
69 specified_args: Vec<(syn::LitStr, Box<syn::Expr>)>,
70 },
71 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 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 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
150struct 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#[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 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 let mut checked_loader_has_message = false;
527 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}