batbox_i18n_macro/
lib.rs

1use proc_macro2::{Span, TokenStream};
2use quote::quote;
3
4#[proc_macro]
5pub fn gen(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
6    process(syn::parse_macro_input!(input)).into()
7}
8
9struct Input {
10    path: syn::LitStr,
11    mod_ident: syn::Ident,
12}
13
14impl syn::parse::Parse for Input {
15    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
16        input.parse::<syn::Token!(mod)>()?;
17        let mod_ident = input.parse()?;
18        input.parse::<syn::Token!(:)>()?;
19        let path = input.parse()?;
20        Ok(Self { path, mod_ident })
21    }
22}
23
24type Locale = std::collections::HashMap<String, String>;
25
26fn parse_toml(path: impl AsRef<std::path::Path>) -> std::collections::HashMap<String, Locale> {
27    let file = std::fs::File::open(path).unwrap();
28    let mut reader = std::io::BufReader::new(file);
29    let mut toml = String::new();
30    std::io::Read::read_to_string(&mut reader, &mut toml).unwrap();
31    toml::from_str(&toml).expect("Failed to parse toml")
32}
33
34fn process(input: Input) -> TokenStream {
35    let mod_ident = input.mod_ident;
36    let locales = parse_toml({
37        let manifest_dir: std::path::PathBuf =
38            std::env::var_os("CARGO_MANIFEST_DIR").unwrap().into();
39        manifest_dir.join(input.path.value())
40    });
41    if locales.is_empty() {
42        panic!("No locale files found");
43    }
44    let (first_locale, field_names): (String, std::collections::HashSet<String>) = {
45        let (name, locale) = locales.iter().next().unwrap();
46        (name.clone(), locale.keys().cloned().collect())
47    };
48    for (name, locale) in &locales {
49        if field_names != locale.keys().cloned().collect() {
50            if let Some(key) = locale.keys().find(|key| !field_names.contains(*key)) {
51                panic!("{name:?} has {key:?} but {first_locale:?} does not");
52            } else if let Some(key) = field_names.iter().find(|key| !locale.contains_key(*key)) {
53                panic!("{first_locale:?} has {key:?} but {name:?} does not");
54            } else {
55                unreachable!()
56            }
57        }
58    }
59    let fields = field_names.iter().map(|name| {
60        let name = syn::Ident::new(name, Span::call_site());
61        quote! { #name: &'static str }
62    });
63    let locale_matches = locales.keys().map(|locale| {
64        let lower = locale.to_lowercase();
65        let name = syn::Ident::new(&locale.to_uppercase(), Span::call_site());
66        quote! {
67            #lower => &#name
68        }
69    });
70    let locales = locales.iter().map(|(name, locale)| {
71        let name = syn::Ident::new(&name.to_uppercase(), Span::call_site());
72        let fields = locale.iter().map(|(key, value)| {
73            let field_name = syn::Ident::new(key, Span::call_site());
74            quote! { #field_name: #value }
75        });
76        quote! {
77            pub static #name: Locale = Locale {
78                #(#fields,)*
79            };
80        }
81    });
82    let locale_methods = field_names.iter().map(|name| {
83        let name = syn::Ident::new(name, Span::call_site());
84        quote! {
85            pub fn #name(&self) -> &'static str {
86                self.#name
87            }
88        }
89    });
90
91    quote! {
92        mod #mod_ident {
93            pub struct Locale {
94                #(#fields,)*
95            }
96
97            impl Locale {
98                #(#locale_methods)*
99            }
100
101            pub fn get(locale: &str) -> Option<&'static Locale> {
102                Some(match locale {
103                    #(#locale_matches,)*
104                    _ => return None,
105                })
106            }
107
108            pub fn get_or_en(locale: &str) -> &'static Locale {
109                get(locale).unwrap_or(&EN)
110            }
111
112            #(#locales)*
113        }
114    }
115}