i18n_again_macro/
lib.rs

1//! Parse the content of `format_t!` arguments
2//!
3//! Required, to filter out `a.b.c` style paths
4//! and avoid erroring for no good reason.
5
6use fs_err as fs;
7use proc_macro2::Ident;
8use proc_macro2::Span;
9use quote::{quote, ToTokens};
10use std::collections::HashMap;
11use syn::Token;
12use syn::{parse::Parse, punctuated::Punctuated, Expr};
13
14/// A single argument as passed to `format!`
15///
16/// Skips the initial literal string!
17enum FormatArg {
18    AliasEqIdent {
19        alias: Ident,
20        #[allow(dead_code)]
21        eq: Token![=],
22        ident: Ident,
23    },
24    AliasEqExpr {
25        alias: Ident,
26        #[allow(dead_code)]
27        eq: Token![=],
28        expr: Expr,
29    },
30    Ident {
31        ident: Ident,
32    },
33}
34
35use std::fmt;
36
37impl fmt::Debug for FormatArg {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::Ident { ident } => {
41                write!(f, "{ident}")?;
42            }
43            Self::AliasEqIdent { alias, ident, .. } => {
44                write!(f, "{alias} = {ident}")?;
45            }
46            Self::AliasEqExpr { alias, .. } => {
47                write!(f, "{alias} = <expr>")?;
48            }
49        }
50
51        Ok(())
52    }
53}
54
55impl syn::parse::Parse for FormatArg {
56    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
57        let ident = input.parse()?;
58
59        let lookahead = input.lookahead1();
60        let me = if lookahead.peek(Token![=]) {
61            let eq = input.parse::<Token![=]>()?;
62            let alias = ident;
63
64            let expr = input.parse::<Expr>().map_err(|_e| {
65                syn::Error::new(
66                    input.span(),
67                    "Expected `Expr` after = since it's not an ident",
68                )
69            })?;
70            Self::AliasEqExpr { alias, eq, expr }
71        } else {
72            Self::Ident { ident }
73        };
74        Ok(me)
75    }
76}
77
78impl ToTokens for FormatArg {
79    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
80        match self {
81            Self::Ident { ident } => tokens.extend(ident.to_token_stream()),
82            Self::AliasEqExpr { alias, eq, expr } => tokens.extend(quote! { #alias #eq #expr }),
83            Self::AliasEqIdent { alias, eq, ident } => tokens.extend(quote! { #alias #eq #ident }),
84        }
85    }
86}
87
88/// All format arguments.
89///
90/// Including the str literal.
91struct FormatArgs {
92    fmt_str: syn::LitStr,
93    #[allow(dead_code)]
94    maybe_comma: Option<Token![,]>,
95    maybe_args: Punctuated<FormatArg, Token![,]>,
96}
97
98impl Parse for FormatArgs {
99    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
100        let lit = input.parse::<syn::Lit>()?;
101        let fmt_str = match lit {
102            syn::Lit::Str(alias) => alias,
103            other => {
104                return Err(syn::Error::new(
105                    other.span(),
106                    "Expected a literal str for format arg but found...",
107                ))
108            }
109        };
110        let lookahead = input.lookahead1();
111
112        if lookahead.peek(Token![,]) {
113            let comma = input.parse::<Token![,]>()?;
114
115            let maybe_comma = Some(comma);
116            let maybe_args = Punctuated::<FormatArg, Token![,]>::parse_terminated(&input)?;
117
118            Ok(Self {
119                fmt_str,
120                maybe_comma,
121                maybe_args,
122            })
123        } else {
124            Ok(Self {
125                fmt_str,
126                maybe_comma: None,
127                maybe_args: Punctuated::new(),
128            })
129        }
130    }
131}
132
133impl ToTokens for FormatArgs {
134    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
135        let FormatArgs {
136            ref fmt_str,
137            maybe_comma,
138            ref maybe_args,
139        } = self;
140        tokens.extend(fmt_str.to_token_stream());
141        if let Some(comma) = maybe_comma {
142            comma.to_tokens(tokens);
143            if !maybe_args.is_empty() {
144                tokens.extend(maybe_args.to_token_stream());
145            }
146        }
147    }
148}
149
150impl fmt::Debug for FormatArgs {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(f, r##""{}""##, self.fmt_str.value())?;
153        if let Some(_comma) = self.maybe_comma {
154            f.write_str(",")?;
155            for pair in self.maybe_args.pairs() {
156                write!(f, "{:?},", pair.value())?;
157            }
158        }
159        Ok(())
160    }
161}
162
163fn format_inner(input: proc_macro2::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
164    let support = support_crate_path();
165    let FormatArgs {
166        fmt_str,
167        maybe_comma: _,
168        maybe_args,
169    } = syn::parse2(input)?;
170
171    // must be (a.b.c -> (language_2_letter_code -> translation_text)* )*
172
173    let path = if let Ok(locale_dir) = std::env::var("I18N_SERIALIZED_TRANSLATIONS") {
174        std::path::PathBuf::from(locale_dir)
175    } else {
176        std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("locales")
177    };
178    dbg!(path.display());
179    eprintln!("Reading {}", path.display());
180    let bytes = fs::read(&path).unwrap();
181    let tp2trans_per_locale = i18n_again_support::deserialize(&bytes[..]).unwrap();
182
183    eprintln!("Read {:?}", &tp2trans_per_locale);
184    // Will cause quite a bit of load during compilation for applications with many
185    // invocations, but whatever...
186    let tp = fmt_str.value();
187    let tp = tp.as_str();
188    let translations: &HashMap<String, String> = tp2trans_per_locale.get(tp).ok_or_else(|| {
189        syn::Error::new(
190            Span::call_site(),
191            format!("No translation for \"{tp}\" in {tp2trans_per_locale:?}"),
192        )
193    })?;
194
195    let language = translations.keys();
196    let translation = translations.values().map(|v| v.trim().to_owned());
197    let ts = quote!(
198        match #support::locale() {
199            #( #language => { ::std::format!( #translation, #maybe_args ) }, )*
200            _ => { "<missing translation>".to_owned() }, // TODO FIXME, use a default language
201        }
202    );
203    println!("{s}", s = ts.to_string());
204    Ok(ts)
205}
206
207#[proc_macro]
208pub fn format_t(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
209    format_inner(proc_macro2::TokenStream::from(input))
210        .unwrap_or_else(|e| e.to_compile_error())
211        .into()
212}
213
214fn support_crate_path() -> syn::Path {
215    use proc_macro_crate as pmc;
216    let found_crate = pmc::crate_name("i18n-again")
217        .expect("i18n-again must be present in `Cargo.toml`, but it's not");
218
219    let ident = match found_crate {
220        pmc::FoundCrate::Itself => Ident::new("crate", Span::call_site()),
221        pmc::FoundCrate::Name(name) => Ident::new(&name, Span::call_site()),
222    };
223    syn::Path::from(ident)
224}
225
226#[cfg(test)]
227mod tests;