When a struct or enum variant has a single unnamed field, their implementation of Localize now forwards to the field's implementation.
R2BAN2V6VS4OBNG6MK5BOUYQCJMGLD37IIE5RX4HXZLQ3C5AQRHAC ARB66QTX6V7ZDXQUZQRDQGCUS2LOIB6SKMCL4SK7TVWK3FPBSI3QC PGBXJWIHSVTRD7CGDSCPC4YHI65EBKMQFEX62RZWL4EZB63622XAC XEEXWJLGVIPIGURSDU4ETZMGAIFTFDPECM4QWFOSRHU7GMGVOUVQC CESJ4CTO26X4GBZBPXRXLOJT3JQJOGFN5EJSNAAZELNQRZF7QSYAC VQBJBFEXRTJDBH27SVWRBCBFC7OOFOZ3DSMX7PE5BIZQLGHPVDYAC YZ6PVVQCLWYRFM33CH6BDB7U6BSP5PM5LH3FMKRXV3BH5KCRFM4AC 3NMKD6I57ONAGHEN4PZIAV2KPYESVR4JL3DTWSHXKCMVJBEQ4GIQC JWZT34UC7OTMMUZKGYFF6NDGIFNOA6TYXAZ6K66ELM3ZW7ZM7I5AC QJC4IQITOQP65AFLA5CMH2EXHB6B3SOLW2XBV72U5ZQU2KOR2EIAC RUCC2HKZZTUHN3G6IWS4NK3VYGXAI6PORJH2YZKPRAYSDWH63ESQC LU6IFZFGPIKF3CBWZWITWVBSCYWF7Q4UXJDXVRWZ4XV7PKE5RSTQC NEBSVXIASWSJO2CVU3VWRONIWJJDLL3VDS6WNKQBWQBRUUI7RSYAC OWXLFLRMQDTXWN5QQQLJNAATWFWXIN2S4UQA2LC2A6AWX4UWM6LQC 7JPOCQEISAIOD7LV4JYBE6NNUWUKKNE73MEPQYTIZ7PP44ZAD2RAC XGNME3WRU3MJDTFHUFJYARLVXWBZIH5ODBOIIFTXHNCBTZQH2R7QC QFPQZR4K4UZ7R2GQZJG4NYBGVQJVL2ANIKGGTOHAMIRIBQHPSQGAC F5LG7WENUUDRSCTDMA4M6BAC5RWTGQO45C4ZEBZDX6FHCTTHBVGQC QSK7JRBA55ZRY322WXGNRROJL7NTFBR6MJPOOA5B2XD2JAVM4MWQC 7U2DXFMPZO4P53AMWYCVXG3EPB7UIAPEY4PDDINX4TTABHD5NGMQC NO3PDO7PY7J3WPADNCS5VD6HKFY63E23I3SDR4DHXNVQJTG27RAAC //! End-to-end test for unnamed field support in the `l10n_embed_derive` macromod common;use icu_locale::{Locale, locale};use l10n_embed::Localize;use l10n_embed_derive::localize;const DEFAULT_LOCALE: Locale = locale!("en-US");const EXPECTED_LOCALES: [Locale; 4] = [locale!("zh-CN"),locale!("en-US"),locale!("ja-JP"),locale!("fr"),];pub struct LocalizableType;impl Localize for LocalizableType {fn available_locales(&self) -> Vec<Locale> {EXPECTED_LOCALES.to_vec()}fn localize_for(&self, locale: &Locale) -> String {format!("Localized with locale: {locale}")}}#[localize("tests/locale/**/basic.ftl")]pub struct MessageStruct(LocalizableType);#[localize("tests/locale/**/basic.ftl")]pub enum MessageEnum {Greeting { name: String },Forward(LocalizableType),}mod forward {use super::common::compare_message;use super::{DEFAULT_LOCALE, EXPECTED_LOCALES, LocalizableType, MessageEnum, MessageStruct};use l10n_embed::Localize;use pretty_assertions::assert_eq;#[test]fn message() {let expected = "Localized with locale: en-US";compare_message(MessageStruct(LocalizableType), expected, DEFAULT_LOCALE);compare_message(MessageEnum::Forward(LocalizableType),expected,DEFAULT_LOCALE,);}#[test]fn available_locales() {let expected_locales = EXPECTED_LOCALES.to_vec();assert_eq!(expected_locales,MessageStruct(LocalizableType).available_locales());assert_eq!(expected_locales,MessageEnum::Forward(LocalizableType).available_locales());}}/// Ensure other enum variants are not affectedmod preserves_enum_variants {use super::common::compare_message;use super::{DEFAULT_LOCALE, LocalizableType, MessageEnum};use l10n_embed::Localize;use pretty_assertions::{assert_eq, assert_ne};#[test]fn message() {compare_message(MessageEnum::Greeting {name: String::from("Ferris"),},"Hello, Ferris!",DEFAULT_LOCALE,);}#[test]fn available_locales() {let greeting = MessageEnum::Greeting {name: String::from("Ferris"),};assert_ne!(greeting.available_locales(),MessageEnum::Forward(LocalizableType).available_locales());assert_eq!(vec![DEFAULT_LOCALE], greeting.available_locales());}}
Union,#[error("Unnamed fields are not supported")]UnnamedFields,}#[derive(Debug, Error)]#[error("Unsupported Rust code")]pub struct UnsupportedError {span: syn::Ident,reason: UnsupportedReason,
Union { span: syn::Ident },#[error("Only 1 unnamed field is supported")]UnnamedFields {span: syn::Ident,field_count: usize,},
let named_fields: Vec<&syn::Type> = match &derive_input.data {syn::Data::Struct(struct_data) => match &struct_data.fields {syn::Fields::Named(named_fields) => {named_fields.named.iter().map(|field| &field.ty).collect()}syn::Fields::Unit => Vec::new(),syn::Fields::Unnamed(_unnamed_fields) => {return Err(MacroError::Unsupported(UnsupportedError {span: derive_input.ident.clone(),reason: UnsupportedReason::UnnamedFields,}));}},
let field_types: Vec<&syn::Type> = match &derive_input.data {syn::Data::Struct(struct_data) => {types_for_fields(&struct_data.fields, derive_input.ident.clone())?}
.map(|variant| match &variant.fields {syn::Fields::Named(named_fields) => {Ok(named_fields.named.iter().map(|field| &field.ty).collect())}syn::Fields::Unit => Ok(Vec::new()),syn::Fields::Unnamed(_unnamed_fields) => {Err(MacroError::Unsupported(UnsupportedError {span: variant.ident.clone(),reason: UnsupportedReason::UnnamedFields,}))}}).collect::<Result<Vec<Vec<&syn::Type>>, _>>()?
.map(|variant| types_for_fields(&variant.fields, variant.ident.clone())).collect::<Result<Vec<Vec<&syn::Type>>, UnsupportedError>>()?
let additional_bounds = named_fields.into_iter().map(|field| -> syn::WherePredicate {// Attribute this bound to the original source codelet span = field.span();parse_quote_spanned!(span=> #field: ::l10n_embed::Localize)});
let additional_bounds = field_types.into_iter().map(|field| -> syn::WherePredicate {// Attribute this bound to the original source codelet span = field.span();parse_quote_spanned!(span=> #field: ::l10n_embed::Localize)});
fn types_for_fields(fields: &syn::Fields,span: syn::Ident,) -> Result<Vec<&syn::Type>, UnsupportedError> {match fields {syn::Fields::Named(named_fields) => {Ok(named_fields.named.iter().map(|field| &field.ty).collect())}syn::Fields::Unit => Ok(Vec::new()),syn::Fields::Unnamed(unnamed_fields) => {let unnamed_field_types: Vec<&syn::Type> = unnamed_fields.unnamed.iter().map(|field| &field.ty).collect();match unnamed_field_types.len() {1 => Ok(unnamed_field_types),_ => Err(UnsupportedError::UnnamedFields {span,field_count: unnamed_field_types.len(),}),}}}}
UnsupportedReason::UnnamedFields => {emit_error! { error.span, "only named fields are supported";help = "Each field needs a name so it can be referenced by Fluent code";note = "There must be at least one named field (unit structs are unsupported!)";
UnsupportedError::UnnamedFields { span, field_count } => {emit_error! { span, "only 1 unnamed field is supported, got {} fields", field_count;help = "When there are multiple fields, each field needs a name so it can be referenced by Fluent code";note = "Using a single unnamed field forwards to that field's implementation of `Localize`";
fn expr_for_unnamed_fields(unnamed_fields: &syn::FieldsUnnamed,ident: &syn::Ident,context: &Context,) -> Result<TokenStream, UnsupportedError> {let field_count = unnamed_fields.unnamed.iter().count();if field_count != 1 {return Err(UnsupportedError::UnnamedFields {span: ident.clone(),field_count,});}let field_reference = match context.reference_kind {ReferenceKind::EnumField => quote!(unnamed_field),ReferenceKind::StructField => quote!(self.0),};Ok(quote!(#field_reference.localize_for(locale)))}
pub fn locales_for_ident(group: &fluent::Group, ident: &syn::Ident) -> TokenStream {let id = ident.to_string().to_kebab_case();
pub fn locales_for_ident(group: &fluent::Group,fields: &syn::Fields,reference_kind: ReferenceKind,ident: &syn::Ident,) -> TokenStream {match fields {// Provide available locales based on what Fluent source files are availablesyn::Fields::Named(_) | syn::Fields::Unit => {let id = ident.to_string().to_kebab_case();
let locale_literals = group.locales_for_message(&id).map(|locale| locale.id.to_string()).map(|locale_string| syn::LitStr::new(&locale_string, proc_macro2::Span::call_site()));
let locale_literals = group.locales_for_message(&id).map(|locale| locale.id.to_string()).map(|locale_string| {syn::LitStr::new(&locale_string, proc_macro2::Span::call_site())});quote!(vec![self.canonical_locale(), #(::l10n_embed::macro_prelude::icu_locale::locale!(#locale_literals)),*])}// Forward to the unnamed field's implementationsyn::Fields::Unnamed(_unnamed_fields) => {let unnamed_field_ident = match reference_kind {ReferenceKind::EnumField => quote!(unnamed_field),ReferenceKind::StructField => quote!(self.0),};
// There is only one message for this struct, so just list every supported localequote!(vec![self.canonical_locale(), #(::l10n_embed::macro_prelude::icu_locale::locale!(#locale_literals)),*])
quote!(#unnamed_field_ident.available_locales())}}
let references = match fields {syn::Fields::Named(named_fields) => Context {// Reference using `self.{reference_name}`reference_kind: ReferenceKind::StructField,// Create a list of unique field names that can be referencedvalid_references: unique_named_fields(named_fields),},syn::Fields::Unit => Context {
let context = match fields {syn::Fields::Named(named_fields) => {Context {// Reference using `self.{reference_name}`reference_kind: ReferenceKind::StructField,// Create a list of unique field names that can be referencedvalid_references: unique_named_fields(named_fields),}}syn::Fields::Unit | syn::Fields::Unnamed(_) => Context {
syn::Fields::Unnamed(_unnamed_fields) => {return Err(MacroError::Unsupported(UnsupportedError {span: ident.clone(),reason: UnsupportedReason::UnnamedFields,}));}
Ok(expr_for_message(&mut group, ident, &references)?)
Ok(match fields {syn::Fields::Named(_) | syn::Fields::Unit => expr_for_message(&mut group, ident, &context)?,syn::Fields::Unnamed(unnamed_fields) => {expr_for_unnamed_fields(unnamed_fields, ident, &context)?}})
// Simplify match code by always ignoring enum fields (even if they don't exist)// We are matching the variant name, not any data, so each arm will have something like:// Self::VariantName { .. }// Even if `Self::VariantName` doesn't contain any datalet locales_for_variant = locales_for_ident(group, variant_ident);match_arms.push(quote!(Self::#variant_ident { .. } => #locales_for_variant));
let destructuring_pattern = match &enum_variant.fields {// Simplify match code by always ignoring enum fields (even if they don't exist)// We are matching the variant name, not any data, so each arm will have something like:// Self::VariantName { .. }// Even if `Self::VariantName` doesn't contain any datasyn::Fields::Named(_) | syn::Fields::Unit => quote!(Self::#variant_ident { .. }),// Bind the single unnamed field so we can use its implementation of `Localize::available_locales()`syn::Fields::Unnamed(_unnamed_fields) => quote!(Self::#variant_ident(unnamed_field)),};let locales_for_variant = locales_for_ident(group,&enum_variant.fields,ReferenceKind::EnumField,variant_ident,);match_arms.push(quote!(#destructuring_pattern => #locales_for_variant));
syn::Fields::Unnamed(_unnamed_fields) => {return Err(MacroError::Unsupported(UnsupportedError {span: enum_variant.ident.clone(),reason: UnsupportedReason::UnnamedFields,}));
syn::Fields::Unnamed(unnamed_fields) => {let field_count = unnamed_fields.unnamed.iter().count();if field_count != 1 {return Err(MacroError::Unsupported(UnsupportedError::UnnamedFields {span: enum_variant.ident.clone(),field_count,}));}quote!(#variant_pascal_case(unnamed_field))
syn::Fields::Unnamed(_unnamed_fields) => {return Err(MacroError::Unsupported(UnsupportedError {span: enum_variant.ident.clone(),reason: UnsupportedReason::UnnamedFields,}));
};let arm_body = match &enum_variant.fields {syn::Fields::Named(_) | syn::Fields::Unit => {expr_for_message(&mut group, &enum_variant.ident, &context)?}syn::Fields::Unnamed(unnamed_fields) => {expr_for_unnamed_fields(unnamed_fields, &enum_variant.ident, &context)?