diff --git a/crates/mf1-macros/src/load_locales.rs b/crates/mf1-macros/src/load_locales.rs index 0bcce75..8dd26c9 100644 --- a/crates/mf1-macros/src/load_locales.rs +++ b/crates/mf1-macros/src/load_locales.rs @@ -1,12 +1,12 @@ use convert_case::Case::Snake; use convert_case::Casing; -use mf1_parser::{parse, LexerSpan, Token as AstToken}; +use mf1_parser::{parse, ArgType, LexerSpan, Token as AstToken, TokenSlice}; use proc_macro2::Ident; use proc_macro2::{Span, TokenStream}; use quote::quote; use serde::{Deserialize, Serialize}; -use std::io; use std::{borrow::Cow, collections::HashMap, fs::File, path::PathBuf}; +use std::{io, iter}; use thiserror::Error; use toml::Value; #[derive(Debug, Error)] @@ -125,22 +125,19 @@ pub fn load_locales() -> Result { let get_strings_match_arms = locale_idents .iter() - .map(|locale| quote!(Locale::#locale => &#locale)) - .collect::>(); + .map(|locale| quote!(Locale::#locale => &#locale)); let as_str_match_arms = locale_idents .iter() .zip(locales.iter()) .map(|(key, l)| (key, l.name)) - .map(|(variant, locale)| quote!(Locale::#variant => #locale)) - .collect::>(); + .map(|(variant, locale)| quote!(Locale::#variant => #locale)); let from_str_match_arms = locale_idents .iter() .zip(locales.iter()) .map(|(key, l)| (key, l.name)) - .map(|(variant, locale)| quote!(#locale => Ok(Locale::#variant))) - .collect::>(); + .map(|(variant, locale)| quote!(#locale => Ok(Locale::#variant))); let locales_enum = quote! { #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] @@ -212,40 +209,219 @@ pub fn load_locales() -> Result { }) .collect::>(); - let string_field_defs: Vec = string_keys + let mut dyn_keys = HashMap::new(); + for (locale, asts) in locale_ast.iter() { + for (k, ast) in asts.iter().filter(|(k, _)| !string_keys.contains(k)) { + if base_locale_strings.keys.contains_key(k) { + if let Ok(ast) = ast { + dyn_keys + .entry(k) + .and_modify(|args| ast.get_args_into(args)) + .or_insert_with(|| ast.get_args()); + } + } else { + // Default locale is missing this key! + eprintln!( + "Default locale is missing key {:?} from locale {}!", + k, locale + ) + } + } + } + let dyn_keys = dyn_keys + .into_iter() + .map(|(k, a)| { + ( + k, + a.into_iter() + .map(|(a, v)| { + if v.len() > 1 { + eprintln!( + "Argument {a:?} from key {k} is used in multiple ways! Picking first type", + ) + } + let arg_type = *v.first().expect("arguments should have a type"); + (a, arg_type) + }) + .collect::>(), + ) + }) + .collect::>(); + + let builder_defs: Vec = dyn_keys .iter() - .map(|key| Ident::new(key, Span::call_site())) - .map(|key| quote!(pub #key: &'static str)) + .map(|(key, args)| { + let ident = Ident::new(key, Span::call_site()); + let type_params = args + .iter() + .map(|(arg, _)| Ident::new(&format!("__{}", arg), Span::call_site())); + let concrete_types: Vec<_> = args + .iter() + .map(|(_, arg_type)| match arg_type { + ArgType::OrdinalArg => quote! {i32}, + ArgType::PlainArg | ArgType::SelectArg => quote! {&str}, + ArgType::FunctionArg => todo!() + }).collect(); + + let field_names: Vec<_> = args.iter().map(|(arg, _)| { + Ident::new(&format!("arg_{}", arg), Span::call_site()) + }).collect(); + let fields = args.iter().map(|(arg, _)| { + let key = Ident::new(&format!("arg_{}", arg), Span::call_site()); + let type_param = Ident::new(&format!("__{}", arg), Span::call_site()); + quote!(#key: #type_param) + }); + let default_type_params = args.iter().map(|_| quote!(EmptyValue)); + let default_fields = field_names.iter().map(|key| { + quote!(#key: EmptyValue) + }); + let formatter_args = field_names.iter().map(|key| { + quote!(self.#key) + }); + let formatter_type = quote!(&'a for<'x, 'y> fn(&'x mut dyn mf1::Formatable<'y>, #(#concrete_types,)*) -> Result<(), Box>); + fn gen_setter<'a>(ident: &syn::Ident, field: &syn::Ident, other_fields: impl Iterator + Clone) -> proc_macro2::TokenStream { + let restructure_others = other_fields.clone(); + quote! { + + impl<'a> #ident<'a, EmptyValue> { + pub fn #field(self, #field: &str) -> interpolated<'a, &str> { + let #ident { formatter, #(#other_fields,)* .. } = self; + #ident { formatter, #field, #(#restructure_others,)* } + } + } + } + } + fn split_at(slice: &[T], i: usize) -> (&[T], &T, &[T]) { + let (left, rest) = slice.split_at(i); + let (mid, right) = rest.split_first().unwrap(); + (left, mid, right) + } + let setters = (0..field_names.len()) + .map(|i| split_at(&field_names, i)) + .map(|(left_fields, field, right_fields)| { + gen_setter(&ident, field, left_fields.iter().chain(right_fields.iter())) + }); + quote! { + #[allow(non_camel_case_types, non_snake_case)] + // #[derive(Clone, Copy)] + #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] + // #[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] + pub struct #ident<'a, #(#type_params,)*> { + formatter: #formatter_type, + #(#fields,)* + } + + impl<'a> #ident<'a, #(#default_type_params,)*> { + pub const fn new(formatter: #formatter_type) -> Self { + Self { + formatter, + #(#default_fields,)* + } + } + } + #(#setters)* + impl<'a> mf1::BuildStr for #ident<'a, #(#concrete_types,)*> { + #[inline] + fn build_string(self) -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Owned(format!("{}", self)) + } + } + impl<'a> std::fmt::Display for #ident<'a, #(#concrete_types,)*> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match (self.formatter)(f, #(#formatter_args)*) { + Ok(_) => Ok(()), + Err(e) => Err(*e.downcast().unwrap()), + } + } + } + + } + }) .collect::>(); + let dyn_field_defs: Vec = dyn_keys + .iter() + .map(|(key, args)| { + let key: Ident = Ident::new(key, Span::call_site()); + let type_params = args.iter().map(|_| quote!(builders::EmptyValue)); + quote!(pub #key: builders::#key<'static, #(#type_params,)*>) + }) + .collect::>(); + + let string_field_defs = string_keys + .iter() + .map(|key| Ident::new(key, Span::call_site())) + .map(|key| quote!(pub #key: &'static str)); let keys_type = quote! { + #[doc(hidden)] + pub mod builders { + #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] + pub struct EmptyValue; + #(#builder_defs)* + } + #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] #[allow(non_camel_case_types, non_snake_case)] pub struct #i18n_keys_ident { #(#string_field_defs,)* + #(#dyn_field_defs,)* } }; let locale_values = locales.iter().map(|locale| { - let fields = string_keys.iter().map(|key| { + let string_fields = string_keys.iter().map(|key| { let key_ident = Ident::new(key, Span::call_site()); match locale.keys.get(*key) { Some(value) => quote!(#key_ident: #value), _ => { quote!(#key_ident: #base_locale_ident.#key_ident) - // global_locale - // .keys - // .get(key) - // .map(|value| quote!(#key: #value)) } } }); + let formatter_fields = dyn_keys.iter().map(|(key, arg_types)| { + let key_ident = Ident::new(key, Span::call_site()); + match locale_ast.get(locale.name).unwrap().get(*key) { + Some(Ok(ast)) => { + let args = arg_types + .iter() + .map(|(name, arg_type)| { + let name = Ident::new(name, Span::call_site()); + match arg_type { + ArgType::OrdinalArg => quote! {#name: i32}, + ArgType::PlainArg | ArgType::SelectArg => quote! {#name: &str}, + ArgType::FunctionArg => todo!() + } + }); + fn gen_items(token: &AstToken) -> impl Iterator { + match token { + AstToken::Content { value } => iter::once(quote! {fmt.write_str(#value)?;}), + AstToken::PlainArg { arg } => { + let arg = Ident::new(arg, Span::call_site()); + iter::once(quote! {fmt.write_str(#arg)?;}) + }, + AstToken::Octothorpe { } => iter::once(quote! {fmt.write_str("#")?;}), + _ => todo!(), + } + } + let items = ast.iter().flat_map(gen_items); + quote!(#key_ident: builders::#key_ident::new(&(|fmt: &mut dyn mf1::Formatable, #(#args,)*| -> Result<(), _> { + #(#items)* + Ok(()) + } as _))) + }, + _ => { + quote!(#key_ident: #base_locale_ident.#key_ident) + } + } + }); + let ident = locale.ident(); quote! { #[allow(non_upper_case_globals)] const #ident: #i18n_keys_ident = #i18n_keys_ident { - #(#fields,)* + #(#string_fields,)* + #(#formatter_fields,)* }; } }); @@ -255,13 +431,10 @@ pub fn load_locales() -> Result { #locale_values )* }; - // #(#builder_fields,)* - // #(#subkeys_fields,)* + Ok(quote! { #locales_enum #keys_type #locale_static }) - // } - // Err(Error::Misc) } diff --git a/crates/mf1-macros/src/t_macro.rs b/crates/mf1-macros/src/t_macro.rs index 25b1e3b..ce6e1bb 100644 --- a/crates/mf1-macros/src/t_macro.rs +++ b/crates/mf1-macros/src/t_macro.rs @@ -1,4 +1,4 @@ -use proc_macro2::TokenStream; +use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{token, Expr, Ident}; @@ -27,6 +27,7 @@ impl From for proc_macro::TokenStream { pub struct ParsedInput { pub context: Expr, pub key: Ident, + pub interpolations: Option>, } impl syn::parse::Parse for ParsedInput { @@ -34,23 +35,93 @@ impl syn::parse::Parse for ParsedInput { let context = input.parse()?; input.parse::()?; let key = input.parse()?; - Ok(ParsedInput { context, key }) + let interpolations = match input.parse::() { + Ok(_) => { + let interpolations = input + .parse_terminated(InterpolatedValue::parse, token::Comma)? + .into_iter() + .collect(); + Some(interpolations) + } + Err(_) if input.is_empty() => None, + Err(err) => return Err(err), + }; + Ok(ParsedInput { + context, + key, + interpolations, + }) } } +pub enum InterpolatedValue { + Var(Ident), + AssignedVar { key: Ident, value: Expr }, +} +impl syn::parse::Parse for InterpolatedValue { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let key = input.parse()?; + let value = if input.peek(syn::Token![=]) { + input.parse::()?; + let value = input.parse()?; + InterpolatedValue::AssignedVar { key, value } + } else { + InterpolatedValue::Var(key) + }; + Ok(value) + } +} pub fn t_macro(tokens: TokenStream, output_type: OutputType) -> Result { - let ParsedInput { context, key } = syn::parse2(tokens)?; + let ParsedInput { + context, + key, + interpolations, + } = syn::parse2(tokens)?; let get_key = quote!(#context.get_strings().#key); let build_fn = match output_type { OutputType::String => quote!(build_string), }; - let inner = quote! { - { - #[allow(unused)] - use mf1::BuildStr; - let _key = #get_key; - _key.#build_fn() + let inner = if let Some(interpolations) = interpolations { + let (keys, values): (Vec<_>, Vec<_>) = interpolations + .iter() + .map(|iv| match iv { + InterpolatedValue::Var(ident) => (ident.clone(), quote!(#ident)), + InterpolatedValue::AssignedVar { key, value } => (key.clone(), quote!(#value)), + }) + .unzip(); + let params = quote! { + let (#(#keys,)*) = (#(#values,)*); + }; + + let builders = interpolations.iter().map(|inter| { + let key = match inter { + InterpolatedValue::Var(key) | InterpolatedValue::AssignedVar { key, .. } => key, + }; + let builder = Ident::new(&format!("arg_{}", key), Span::call_site()); + quote!(#builder(&#key)) + }); + quote! { + { + #params + #[allow(unused)] + use mf1::BuildStr; + let _key = #get_key; + #( + let _key = _key.#builders; + )* + #[deny(deprecated)] + _key.#build_fn() + } + } + } else { + quote! { + { + #[allow(unused)] + use mf1::BuildStr; + let _key = #get_key; + _key.#build_fn() + } } }; Ok(inner) diff --git a/crates/mf1/src/lib.rs b/crates/mf1/src/lib.rs index 971c6cb..b5f9bc0 100644 --- a/crates/mf1/src/lib.rs +++ b/crates/mf1/src/lib.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, error::Error}; #[cfg(feature = "macros")] pub use mf1_macros::{load_locales, t_l_string}; @@ -31,3 +31,22 @@ impl BuildStr for &'static str { Cow::Borrowed(self) } } + +#[doc(hidden)] +pub trait Formatable<'a> { + // type Error; + fn write_str(&mut self, data: &str) -> Result<(), Box>; + fn write_fmt(&mut self, args: std::fmt::Arguments) -> Result<(), Box>; +} + +impl<'a> Formatable<'a> for core::fmt::Formatter<'a> { + // type Error = std::fmt::Error; + + fn write_str(&mut self, data: &str) -> Result<(), Box> { + Ok(self.write_str(data)?) + } + + fn write_fmt(&mut self, args: std::fmt::Arguments) -> Result<(), Box> { + Ok(self.write_fmt(args)?) + } +} diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 85a4de8..7ee5c0c 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -10,7 +10,9 @@ fn main() { .get(1) .map(|l| Locale::from_str(l).unwrap()) .unwrap_or_default(); + let la = l.as_str(); dbg!(l.get_strings()); + println!("{}", t!(l, interpolated, var = la)); println!("{}", t!(l, message)); println!("{}", t!(l, message_2)); }