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}