Skip to main content

interly_macros/
lib.rs

1use heck::{ToShoutySnakeCase, ToSnakeCase};
2use proc_macro::TokenStream;
3use proc_macro2::{Span, TokenStream as TokenStream2};
4use quote::{quote, quote_spanned};
5use syn::{Data, DeriveInput, Ident, Visibility, parse_macro_input};
6use unic_langid::{LanguageIdentifier, langid};
7
8use prepare::make_init;
9
10mod locales;
11mod prepare;
12mod read;
13
14use locales::{LangInfo, extract_messages};
15
16use crate::prepare::make_messages_methods;
17
18const DEFAULT_PATH: &str = "locales";
19const DEFAULT_FALLBACK_LOCALE: LanguageIdentifier = langid!("en");
20
21/// Usage
22///
23/// ```rust,ignore
24/// use interly::localize;
25///
26/// #[localize]
27/// pub(crate) struct Localize;
28///
29/// # fn main() {
30/// assert_eq!(tr!(hello_world, "en", "your name"), "Hello, your name!".to_string());
31/// assert_eq!(tr_literal!("hello-world", "en"), "Hello, world!".to_string());
32/// # }
33/// ```
34///
35/// Arguments in `tr_literal!` currently not supported
36#[proc_macro_attribute]
37pub fn localize(_args: TokenStream, input: TokenStream) -> TokenStream {
38    let input = parse_macro_input!(input as DeriveInput);
39    match input.data {
40        Data::Struct(_) => (),
41        Data::Enum(d) => {
42            return quote_spanned! { d.enum_token.span => compile_error!("use struct"); }.into();
43        }
44        Data::Union(d) => {
45            return quote_spanned! { d.union_token.span => compile_error!("use struct"); }.into();
46        }
47    }
48
49    let dir = DEFAULT_PATH;
50    let files = match read::read_files(dir) {
51        Ok(c) => c,
52        Err(e) => {
53            let msg = format!("failed to read .ftl files from \"{dir}\": {e}");
54            return quote! { compile_error!(#msg); }.into();
55        }
56    };
57
58    let messages = match extract_messages(files) {
59        Ok(m) => m,
60        Err(e) => {
61            let msg = format!("invalid .ftl files:\n{e}");
62            return quote! { compile_error!(#msg); }.into();
63        }
64    };
65
66    let languages_names: Vec<_> = messages
67        .iter()
68        .map(|(lang, _)| lang.to_string())
69        .map(|l| l.to_snake_case())
70        .collect();
71
72    let ident = input.ident;
73    let vis = input.vis;
74    let res = localize_base(
75        vis.clone(),
76        ident,
77        messages,
78        languages_names,
79        DEFAULT_FALLBACK_LOCALE,
80    );
81
82    res.into()
83}
84
85fn localize_base(
86    vis: Visibility,
87    ident: Ident,
88    messages: Vec<(LanguageIdentifier, LangInfo)>,
89    languages_names: Vec<String>,
90    fallback_locale: LanguageIdentifier,
91) -> TokenStream2 {
92    let init_fun = make_init(&messages);
93    let message_methods = make_messages_methods(vis.clone(), &messages);
94
95    let mut languages_enum_variants = vec![];
96    let mut languages_enum_from = vec![];
97    for (lang_enum, lang_str) in languages_names
98        .iter()
99        .map(|l| (l.to_shouty_snake_case(), l))
100    {
101        let lang_enum = syn::Ident::new(&lang_enum, Span::call_site());
102        languages_enum_variants.push(quote! { #lang_enum });
103        languages_enum_from.push(quote! { #lang_str => Self::#lang_enum });
104    }
105
106    let fallback_lang_enum = syn::Ident::new(
107        fallback_locale.to_string().to_shouty_snake_case().as_str(),
108        Span::call_site(),
109    );
110    languages_enum_from.push(quote! {
111        _ => Self::#fallback_lang_enum,
112    });
113
114    quote! {
115        #[derive(Default)]
116        #vis struct #ident {
117            bundles: __interly::Bundles,
118        }
119
120        #vis mod __interly {
121            use ::std::collections::HashMap;
122            use ::std::sync::Arc;
123            use ::interly::{
124                FluentArgs,
125                FluentBundle,
126                FluentResource,
127                IntlLangMemoizer,
128                LanguageIdentifier,
129                Lazy,
130            };
131
132            use super::#ident;
133
134            pub(super) type Bundles = HashMap<
135                LANG,
136                FluentBundle<Arc<FluentResource>, IntlLangMemoizer>,
137            >;
138
139            impl #ident {
140                const FALLBACK_LANG: LANG = LANG::#fallback_lang_enum;
141
142                #vis fn init() -> Self {
143                    #init_fun
144                }
145
146                #vis fn languages() -> ::std::vec::Vec<&'static str> {
147                    ::std::vec![#(#languages_names),*]
148                }
149
150                #vis fn __format_msg(
151                    &self,
152                    msg_id: &str,
153                    lang: LANG,
154                    args: Option<&FluentArgs<'_>>,
155                ) -> String {
156                    let mut bundle = self.bundles.get(&lang).expect("no bundle");
157                    if !bundle.has_message(msg_id) {
158                        bundle = self
159                            .bundles
160                            .get(&Self::FALLBACK_LANG)
161                            .expect("no fallback bundle");
162                    }
163                    let msg = bundle
164                        .get_message(msg_id)
165                        .expect("no message")
166                        .value()
167                        .expect("no value in message");
168                    let mut errs = ::std::vec![];
169                    bundle.format_pattern(msg, args, &mut errs).to_string()
170                }
171
172                #message_methods
173            }
174
175            #vis static LOCALIZE: Lazy<#ident> = Lazy::new(|| { #ident::init() });
176
177            #[derive(PartialEq, Eq, Hash)]
178            #vis enum LANG {
179                #(#languages_enum_variants),*
180            }
181
182            impl From<&str> for LANG {
183                fn from(lang: &str) -> Self {
184                    match lang.to_lowercase().as_str() {
185                        #(#languages_enum_from),*
186                    }
187                }
188            }
189            impl From<&::std::string::String> for LANG {
190                fn from(lang: &::std::string::String) -> Self {
191                    lang.as_str().into()
192                }
193            }
194        }
195
196        #[allow(unused)]
197        #[macro_export] // probably should be disabled if #vis != pub
198        macro_rules! tr {
199            ($e:ident, $lang:expr) => {
200                tr!($e, $lang,)
201            };
202            ($e:ident, $lang:expr, $($v:expr),*) => {
203                $crate::__interly::LOCALIZE.$e($lang, $($v),*)
204            };
205        }
206
207        #[allow(unused)]
208        #[macro_export] // probably should be disabled if #vis != pub
209        macro_rules! tr_literal {
210            ($e:expr, $lang:expr) => {
211                $crate::__interly::LOCALIZE.__format_msg($e, $lang.into(), None)
212            };
213            /*($e:expr, $lang:expr, $($v:expr),*) => {
214                $crate::__interly::LOCALIZE.__format_msg($e, $lang, $($v),*)
215            };*/
216        }
217
218        // #vis use tr; // probably should be enabled if #vis != pub
219    }
220}