The previous implementation supported just enums, but now both structs and enums are handled correctly. This was mostly removing enum-specific assumptions in the code, and pulling out common logic where applicable. The group::Group struct was also changed to have a more consistent interface; canonical_message() and additional_messages() instead of just message()
NO3PDO7PY7J3WPADNCS5VD6HKFY63E23I3SDR4DHXNVQJTG27RAAC WBI5HFOBBUMDSGKY2RX3YA6N7YDCJEP23JNEJ7PG5VZXHLYIRJRQC P6FW2GGOW24UZZAWQ6IDDI66JBWTIY26TATMCIOETZ4GRRGGUI3AC XGNME3WRU3MJDTFHUFJYARLVXWBZIH5ODBOIIFTXHNCBTZQH2R7QC 2XQ6ZB4WZNNR4KNC3VWNTV7IRMGGAEP33JPQUVB3CVWAKHECZVRQC QSK7JRBA55ZRY322WXGNRROJL7NTFBR6MJPOOA5B2XD2JAVM4MWQC 5FIVUZYFLOZ2CCH4GCOQQZFL3GDEB23VJ7J6YUXQDZQEAQDB76DQC 3WEPY3OXJJ72TNVZLFCN2ZDWSADLT52T6DUONFGEAB46UWAQD3PQC HJMYJDC77NLU44QZWIW7CELXJKD4EK4YZ6CCILYBG6FWGZ2KMBVAC 4MRF5E76QSW3EPICI6TNEGJ2KSBWODWMIDQPLYALDWBYWKAV5LJAC VNSHGQYNPGKGGPYNVP4Z2RWD7JCSDJVYAADD6UXWBYL6ZRXKLE4AC ROSR4HD5ENPQU3HH5IVYSOA5YM72W77CHVQARSD3T67BUNYG7KZQC XEEXWJLGVIPIGURSDU4ETZMGAIFTFDPECM4QWFOSRHU7GMGVOUVQC OCR4YRQ2LXK3PXSWPEWCBED4DFVMXZIF4RS35XQZSJ2D2KEIB2VQC 5TEX4MNUC4LDDRMNEOVCFNUUEZAGUXMKO3OIEQFXWRQKXSHY2NRQC D652S2N3MHR7NJWSJIT7DUH5TPEFF6YII7EGV4C7IYWARXLMGAWQC O77KA6C4UJGZXVGPEA7WCRQH6XYQJPWETSPDXI3VOKOSRQND7JEQC UOMQT7LTURIIWHZT2ZHLCJG6XESYTN26EJC7IHRFR4PYJ355PNYAC BQ6N55O7RPG47G35YI37Z37456VKWT5KLGQKDQVAN2WI4K34TRBQC SHNZZSZGIBTTD4IV5SMW5BIN5DORUWQVTVTNB5RMRD5CTFNOMJ6AC UKFEFT6LSI4K7X6UHQFZYD52DILKXMZMYSO2UYS2FCHNPXIF4BEQC pub fn derive_enum(group: Group,ident: syn::Ident,variants: Punctuated<syn::Variant, syn::token::Comma>,) -> TokenStream {let mut idents = Vec::with_capacity(variants.len());let mut messages = Vec::with_capacity(variants.len());
fn expr_for_message(group: &Group, id: &str, reference_kind: ReferenceKind) -> TokenStream {let canonical_locale = group.canonical_locale().id.to_string();let canonical_message = group.canonical_message(id, reference_kind);
for variant in variants {let kebab_case_ident = variant.ident.to_string().to_kebab_case();
let (additional_locales, additional_messages): (Vec<_>, Vec<_>) =group.additional_messages(id, reference_kind).unzip();let additional_locales = additional_locales.iter().map(|locale| locale.id.to_string()).collect::<Vec<_>>();
let variant_ident = variant.ident;idents.push(match variant.fields {syn::Fields::Named(fields) => {// Get the name of each field for pattern-matchinglet field_idents = fields.named.iter().map(|field| field.ident.as_ref().unwrap()).map(|ident| format_ident!("{}", ident.to_string().to_snake_case()));quote!(#variant_ident { #(#field_idents),* })}syn::Fields::Unnamed(_) => todo!(),syn::Fields::Unit => quote!(#variant_ident),});messages.push(group.message(&kebab_case_ident));}let extra_locales = group.extra_locales().map(|locale| locale.id.to_string());let canonical_locale = group.canonical_locale().id.to_string();
impl #ident {fn localize(&self) -> String {// Find the appropriate locale to uselet extra_locales = [#(::icu_locid::langid!(#extra_locales)),*];let canonical_locale = ::icu_locid::langid!(#canonical_locale);let locale = ::locale_select::match_locales(&extra_locales, &canonical_locale);
// Find the appropriate locale to uselet additional_locales = [#(::icu_locid::langid!(#additional_locales)),*];let canonical_locale = ::icu_locid::langid!(#canonical_locale);let locale = ::locale_select::match_locales(&additional_locales, &canonical_locale);
// TODO: this shouldn't be generated on every call// TODO: handle different rule types according to fluent code (not just cardinal)let plural_rule_type = ::icu_plurals::PluralRuleType::Cardinal;let plural_rules = ::icu_plurals::PluralRules::try_new(&locale.clone().into(), plural_rule_type).unwrap();
// TODO: handle different rule types according to fluent code (not just cardinal)let plural_rule_type = ::icu_plurals::PluralRuleType::Cardinal;let plural_rules = ::icu_plurals::PluralRules::try_new(&locale.clone().into(), plural_rule_type).unwrap();
// Match the information to the messagematch self {#(Self::#idents => #messages),*}}}
#(if locale.normalizing_eq(#additional_locales) { return #additional_messages })else*#canonical_message
pub fn attribute_groups(path_literal: syn::LitStr) -> Group {// Read the fluent file at the given pathlet manifest_root = std::env::var("CARGO_MANIFEST_DIR").unwrap();let attribute_glob = path_literal.value();
// TODO: check that the fields in fluent source reference fields that existpub fn derive_struct(group: Group, ident: &syn::Ident) -> TokenStream {let ident_kebab_case = ident.to_string().to_kebab_case();expr_for_message(&group, &ident_kebab_case, ReferenceKind::StructField)}
let mut resources = HashMap::new();
pub fn derive_enum(group: Group,enum_variants: &Punctuated<syn::Variant, syn::token::Comma>,) -> TokenStream {// let mut match_arms: HashMap<String, syn::ExprBlock> = HashMap::with_capacity(enum_variants.len());let mut match_arms: Vec<TokenStream> = Vec::with_capacity(enum_variants.len());
let glob = wax::Glob::new(&attribute_glob).unwrap();for potential_entry in glob.walk(&manifest_root) {// TODO: this assumes that the locale is the first capturelet entry = potential_entry.unwrap();let captured_locale = entry.matched().get(1).unwrap();
for enum_variant in enum_variants {let variant_kebab_case = enum_variant.ident.to_string().to_kebab_case();let variant_pascal_case = &enum_variant.ident;
// Captured directories may suffix with a `/`let stripped_locale = captured_locale.strip_suffix('/').unwrap_or(captured_locale);let locale = Locale::try_from_bytes(stripped_locale.as_bytes()).unwrap();
// Destructure fields of the enum variant// E.g. for the variant:// Emails { unread_emails: u64 }// Create the expression:// Self::Emails { unread_emails }let destructuring_pattern = match &enum_variant.fields {syn::Fields::Named(named_fields) => {let named_field_idents = named_fields.named.iter().map(|field| &field.ident);quote!(#variant_pascal_case { #(#named_field_idents),* })}syn::Fields::Unnamed(_) => todo!(),syn::Fields::Unit => todo!(),};
// Parse the file into a `Group`let fluent_contents = std::fs::read_to_string(entry.path()).unwrap();let resource = fluent_syntax::parser::parse(fluent_contents).unwrap();resources.insert(locale, resource);
let arm_body = expr_for_message(&group, &variant_kebab_case, ReferenceKind::EnumField);match_arms.push(quote!(Self::#destructuring_pattern => { #arm_body }));
pub fn localize(path: syn::LitStr,ident: syn::Ident,variants: Punctuated<syn::Variant, syn::token::Comma>,) -> TokenStream {let groups = attribute_groups(path);derive_enum(groups, ident, variants)
quote! {match self {#(#match_arms),*}}
let target = inline_expression(selector);let arms: Vec<syn::Arm> = variants.iter().map(variant).collect();
let target = inline_expression(selector, reference_kind);let arms: Vec<syn::Arm> = variants.iter().map(|item| variant(item, reference_kind)).collect();
pub use parse_macro::localize;
pub fn attribute_groups(path_literal: &syn::LitStr) -> Group {// Read the fluent file at the given pathlet manifest_root = std::env::var("CARGO_MANIFEST_DIR").unwrap();let attribute_glob = path_literal.value();let mut resources = HashMap::new();let glob = wax::Glob::new(&attribute_glob).unwrap();for potential_entry in glob.walk(&manifest_root) {// TODO: this assumes that the locale is the first capturelet entry = potential_entry.unwrap();let captured_locale = entry.matched().get(1).unwrap();// Captured directories may have a suffix of `/`let stripped_locale = captured_locale.strip_suffix('/').unwrap_or(captured_locale);let locale = Locale::try_from_bytes(stripped_locale.as_bytes()).unwrap();// Parse the file into a `Group`let fluent_contents = std::fs::read_to_string(entry.path()).unwrap();let resource = fluent_syntax::parser::parse(fluent_contents).unwrap();resources.insert(locale, resource);}Group::new(locale!("en-US"), resources)}pub fn localize(path: &syn::LitStr, derive_input: &DeriveInput) -> TokenStream {let group = attribute_groups(path);let body = match &derive_input.data {syn::Data::Struct(_struct_data) => parse_macro::derive_struct(group, &derive_input.ident),syn::Data::Enum(enum_data) => parse_macro::derive_enum(group, &enum_data.variants),syn::Data::Union(_) => todo!(),};let ident = &derive_input.ident;quote! {impl #ident {// TODO: most of this shouldn't be generated on every callfn localize(&self) -> String {#body}}}}
/// Returns an iterator over the localized messages paired with the relevant localefn messages_in_column(
fn message_column(&self, id: &str) -> usize {self.canonical_messages.iter().position(|message| message.id.name == id).expect("Message id must be valid")}pub fn canonical_locale(&self) -> &Locale {&self.canonical_locale}pub fn canonical_message(&self, id: &str, reference_kind: ReferenceKind) -> syn::Expr {let message_column = self.message_column(id);let message = &self.canonical_messages[message_column];parse_fluent::message(message, reference_kind)}pub fn additional_messages(
pub fn message(&self, id: &str) -> syn::ExprBlock {let message_column = self.canonical_messages.iter().position(|message| message.id.name == id).expect("Message id must be valid");let additional_locale_names = self.messages_in_column(message_column).map(|(locale, _message)| locale.to_string()).map(|locale_string| syn::LitStr::new(&locale_string, proc_macro2::Span::call_site()));let additional_locale_messages = self.messages_in_column(message_column).map(|(_locale, message)| crate::parse_fluent::message(message));let canonical_message = crate::parse_fluent::message(&self.canonical_messages[message_column]);parse_quote! {{#(if locale.normalizing_eq(#additional_locale_names) { return #additional_locale_messages })else*else {#canonical_message}}}}pub fn canonical_locale(&self) -> &Locale {&self.canonical_locale}pub fn extra_locales(&self) -> impl Iterator<Item = &Locale> {self.extra_locales.iter().map(| LocaleGroup { locale, .. }| locale)}