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-matching
let 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 use
let 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 use
let 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 message
match 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 path
let 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 exist
pub 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 capture
let 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 path
let 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 capture
let 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 call
fn localize(&self) -> String {
#body
}
}
}
}
/// Returns an iterator over the localized messages paired with the relevant locale
fn 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)
}