1use 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
14enum 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
88struct 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 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 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() }, }
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;