Skip to main content

dioxus_translate_macro/
lib.rs

1extern crate proc_macro;
2
3use std::cell::RefCell;
4use std::rc::Rc;
5
6use proc_macro::TokenStream;
7use quote::{ToTokens, quote};
8use syn::parse::{Parse, ParseStream};
9use syn::{DeriveInput, Fields, Ident, Lit, LitStr, Meta, Token, braced, parse_macro_input};
10
11#[proc_macro]
12pub fn translate(input: TokenStream) -> TokenStream {
13    let input = parse_macro_input!(input as TranslateInput);
14
15    let struct_name = input.struct_name;
16    let mut fields = Vec::new();
17    let mut ko_impl = Vec::new();
18    let mut en_impl = Vec::new();
19
20    for field in input.fields {
21        let field_name = field.field_name;
22
23        fields.push(quote! {
24            pub #field_name: &'static str,
25        });
26
27        let mut ko_value = None;
28        let mut en_value = None;
29
30        for translation in field.translations {
31            if translation.lang == "ko" {
32                ko_value = Some(translation.value);
33            } else if translation.lang == "en" {
34                en_value = Some(translation.value);
35            }
36        }
37
38        ko_impl.push(quote! {
39            #field_name: #ko_value,
40        });
41
42        en_impl.push(quote! {
43            #field_name: #en_value,
44        });
45    }
46
47    let en = quote! {
48            fn en() -> Self {
49                Self {
50                    #(#en_impl)*
51                }
52            }
53    };
54
55    #[allow(unused_variables)]
56    let ko = quote! {};
57
58    #[cfg(feature = "ko")]
59    let ko = quote! {
60        fn ko() -> Self {
61            Self {
62                #(#ko_impl)*
63            }
64        }
65
66    };
67
68    #[allow(unused_variables)]
69    let new_ko = quote! {};
70
71    #[cfg(feature = "ko")]
72    let new_ko = quote! {
73        dioxus_translate::Language::Ko => Self::ko(),
74    };
75
76    let expanded = quote! {
77        #[derive(Debug, Clone, PartialEq)]
78        pub struct #struct_name {
79            #(#fields)*
80        }
81
82        impl #struct_name {
83            pub fn new(lang: &dioxus_translate::Language) -> Self {
84                match lang {
85                    dioxus_translate::Language::En => Self::en(),
86                    #new_ko
87                }
88            }
89        }
90
91        impl dioxus_translate::Translator for #struct_name {
92            #en
93
94            #ko
95        }
96    };
97
98    TokenStream::from(expanded)
99}
100
101/// Macro input structure
102struct TranslateInput {
103    struct_name: Ident,
104    fields: Vec<FieldTranslations>,
105}
106
107struct FieldTranslations {
108    field_name: Ident,
109    translations: Vec<LanguageTranslation>,
110}
111
112struct LanguageTranslation {
113    lang: Ident,
114    value: String,
115}
116
117impl Parse for TranslateInput {
118    fn parse(input: ParseStream) -> syn::Result<Self> {
119        // Parse the struct name
120        let struct_name: Ident = input.parse()?;
121        input.parse::<Token![;]>()?;
122
123        let mut fields = Vec::new();
124
125        // Parse fields
126        while !input.is_empty() {
127            let field_name: Ident = input.parse()?;
128            input.parse::<Token![:]>()?;
129            let content;
130            braced!(content in input);
131
132            let mut translations = Vec::new();
133
134            while !content.is_empty() {
135                let lang: Ident = content.parse()?;
136                content.parse::<Token![:]>()?;
137                let value: Lit = content.parse()?;
138                content.parse::<Token![,]>().ok(); // Allow trailing commas
139                if let Lit::Str(lit_str) = value {
140                    translations.push(LanguageTranslation {
141                        lang,
142                        value: lit_str.value(),
143                    });
144                }
145            }
146
147            fields.push(FieldTranslations {
148                field_name,
149                translations,
150            });
151
152            input.parse::<Token![,]>().ok(); // Allow trailing commas
153        }
154
155        Ok(TranslateInput {
156            struct_name,
157            fields,
158        })
159    }
160}
161
162/// Implements a custom derive macro for `Translate`, which automatically generates
163/// a `translate(&self, lang: &Language) -> &'static str` method for enums.
164///
165/// This macro extracts `#[translate(ko = "...")]` attributes from the enum variants
166/// and maps them to Korean translations. If no translation is provided, the variant
167/// name is used as the default English translation.
168///
169/// For tuple variants wrapping an inner type that also implements `Translate`,
170/// use `#[translate(from)]` to delegate translation to the inner type:
171///
172/// ```ignore
173/// #[error("{0}")]
174/// #[translate(from)]
175/// SpaceReward(#[from] SpaceRewardError),
176/// ```
177#[proc_macro_derive(Translate, attributes(translate))]
178pub fn translate_derive(input: TokenStream) -> TokenStream {
179    let ast = parse_macro_input!(input as DeriveInput);
180    let enum_name = ast.ident;
181
182    // Ensure that the derive macro is applied to an enum
183    let variants = match ast.data {
184        syn::Data::Enum(ref data_enum) => &data_enum.variants,
185        _ => {
186            return syn::Error::new_spanned(enum_name, "Translate can only be derived for enums")
187                .to_compile_error()
188                .into();
189        }
190    };
191
192    let mut en_arms = Vec::new();
193    #[cfg(feature = "ko")]
194    let mut ko_arms = Vec::new();
195    // let mut display_arms = Vec::new();
196    // let mut from_str_arms = Vec::new();
197
198    let mut idents: Vec<Ident> = Vec::new();
199    let mut unit_variants = Vec::new();
200
201    for variant in variants {
202        let mut field_names = vec![];
203        let mut tuple_len = 0;
204
205        match variant.fields {
206            Fields::Unit => {
207                idents.push(variant.ident.clone());
208                unit_variants.push(variant.ident.clone());
209            }
210            Fields::Named(ref f) => {
211                idents.push(variant.ident.clone());
212                for field in f.named.iter() {
213                    field_names.push(field.ident.clone().unwrap());
214                }
215            }
216            Fields::Unnamed(ref f) => {
217                idents.push(variant.ident.clone());
218                tuple_len = f.unnamed.len();
219            }
220        }
221        let variant_ident = &variant.ident;
222        let default_str = variant_ident.to_string();
223        let en_translation = Rc::new(RefCell::new(default_str.clone()));
224        #[cfg(feature = "ko")]
225        let ko_translation = Rc::new(RefCell::new(default_str.clone()));
226
227        // Check if this variant has `#[translate(from)]` attribute,
228        // which delegates translation to the inner type's Translate impl
229        let mut is_from = false;
230
231        // Process attributes to extract translations
232        for attr in &variant.attrs {
233            if let Meta::List(ref meta_list) = attr.meta {
234                if meta_list.path.is_ident("translate") {
235                    let en = Rc::clone(&en_translation);
236                    #[cfg(feature = "ko")]
237                    let ko = Rc::clone(&ko_translation);
238
239                    let is_from_ref = Rc::new(RefCell::new(false));
240                    let is_from_clone = Rc::clone(&is_from_ref);
241
242                    let _ = meta_list.parse_nested_meta(move |nv| {
243                        if nv.path.is_ident("en") {
244                            let s: LitStr = nv.value()?.parse()?;
245                            *en.borrow_mut() = s.value();
246                        }
247
248                        #[cfg(feature = "ko")]
249                        if nv.path.is_ident("ko") {
250                            let s: LitStr = nv.value()?.parse()?;
251                            *ko.borrow_mut() = s.value();
252                        }
253
254                        if nv.path.is_ident("from") {
255                            *is_from_clone.borrow_mut() = true;
256                        }
257
258                        Ok(())
259                    });
260
261                    if *is_from_ref.borrow() {
262                        is_from = true;
263                    }
264                }
265            }
266        }
267
268        let en_str = syn::LitStr::new(&en_translation.borrow(), proc_macro2::Span::call_site());
269        #[cfg(feature = "ko")]
270        let ko_str = syn::LitStr::new(&ko_translation.borrow(), proc_macro2::Span::call_site());
271        let _lower_name = syn::LitStr::new(
272            &variant_ident.to_string().to_lowercase(),
273            proc_macro2::Span::call_site(),
274        );
275
276        // For `#[translate(from)]` variants with a single tuple field,
277        // delegate to the inner type's translate() method
278        if is_from && tuple_len == 1 {
279            let arm_name = quote! {
280                #enum_name::#variant_ident(inner)
281            };
282            en_arms.push(quote! {
283                #arm_name => inner.translate(lang),
284            });
285
286            #[cfg(feature = "ko")]
287            {
288                ko_arms.push(quote! {
289                    #arm_name => inner.translate(lang),
290                });
291            }
292            continue;
293        }
294
295        let arm_name = if field_names.len() > 0 {
296            quote! {
297                #enum_name::#variant_ident { .. }
298            }
299        } else if tuple_len > 0 {
300            quote! {
301                #enum_name::#variant_ident(..)
302            }
303        } else {
304            quote! {
305                #enum_name::#variant_ident
306            }
307        };
308
309        en_arms.push(quote! {
310            #arm_name => #en_str,
311        });
312
313        if let Some((_, expr)) = &variant.discriminant {
314            let _value = LitStr::new(
315                expr.to_token_stream().to_string().as_str(),
316                proc_macro2::Span::call_site(),
317            );
318        }
319
320        #[cfg(feature = "ko")]
321        {
322            ko_arms.push(quote! {
323                #arm_name => #ko_str,
324            });
325        }
326    }
327
328    #[cfg(feature = "ko")]
329    let ko_arm = quote! {
330        dioxus_translate::Language::Ko => match self {
331            #(#ko_arms)*
332        },
333    };
334    #[cfg(not(feature = "ko"))]
335    let ko_arm = quote! {};
336
337    // Generate the implementation block for `translate`
338    let r#gen = quote! {
339        impl dioxus_translate::Translate for #enum_name {
340            fn translate(&self, lang: &dioxus_translate::Language) -> &'static str {
341                match lang {
342                    dioxus_translate::Language::En => match self {
343                        #(#en_arms)*
344                    },
345                    #ko_arm
346                }
347            }
348        }
349
350        impl #enum_name {
351            pub const VARIANTS: &'static [Self] = &[ #(#enum_name::#unit_variants,)* ];
352            pub fn variants(lang: &dioxus_translate::Language) -> Vec<String> {
353                Self::VARIANTS.iter().map(|v| v.translate(&lang).to_string()).collect::<Vec<_>>()
354            }
355        }
356
357        // impl std::fmt::Display for #enum_name {
358        //     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        //         match self {
360        //             #(#display_arms)*
361        //         }
362        //     }
363        // }
364
365
366        // impl std::str::FromStr for #enum_name {
367        //     type Err = String;
368
369        //     fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
370        //         match s {
371        //             #(#from_str_arms)*
372        //             _ => Err(format!("invalid field")),
373        //         }
374        //     }
375        // }
376    };
377
378    r#gen.into()
379}