Should be a cleaner separation between macro code and fluent code, while keeping all the proc-macro logic in the same crate.
CESJ4CTO26X4GBZBPXRXLOJT3JQJOGFN5EJSNAAZELNQRZF7QSYAC 7U2DXFMPZO4P53AMWYCVXG3EPB7UIAPEY4PDDINX4TTABHD5NGMQC AAERM7PBDVDFDEEXZ7UJ5WWQF7SPEJQQYXRBZ63ETB5YAVIECHMAC 6ABVDTXZOHVUDZDKDQS256F74LFIMM5DO3OZWHKRXZBUTPII4WAQC XEEXWJLGVIPIGURSDU4ETZMGAIFTFDPECM4QWFOSRHU7GMGVOUVQC O77KA6C4UJGZXVGPEA7WCRQH6XYQJPWETSPDXI3VOKOSRQND7JEQC OWXLFLRMQDTXWN5QQQLJNAATWFWXIN2S4UQA2LC2A6AWX4UWM6LQC QFPQZR4K4UZ7R2GQZJG4NYBGVQJVL2ANIKGGTOHAMIRIBQHPSQGAC XGNME3WRU3MJDTFHUFJYARLVXWBZIH5ODBOIIFTXHNCBTZQH2R7QC F5LG7WENUUDRSCTDMA4M6BAC5RWTGQO45C4ZEBZDX6FHCTTHBVGQC NO3PDO7PY7J3WPADNCS5VD6HKFY63E23I3SDR4DHXNVQJTG27RAAC RLX6XPNZKD6GIRLWKYXFH2RNIU4ZNXLMHXLOMID3E6H53QXXXNZQC BANMRGROVYKYRJ4N2P4HSOJ2JVV6VSEB3W34BFXPOEFND5O36CGAC 2SITVDYW6KANM24QXRHVSBL6S77UHKJLOSOHSUZQBJFL5NAAGQYAC UOMQT7LTURIIWHZT2ZHLCJG6XESYTN26EJC7IHRFR4PYJ355PNYAC D652S2N3MHR7NJWSJIT7DUH5TPEFF6YII7EGV4C7IYWARXLMGAWQC 5TEX4MNUC4LDDRMNEOVCFNUUEZAGUXMKO3OIEQFXWRQKXSHY2NRQC V5S5K33ALIEG5ZABUSAPO4ULHEBFDB2PLTW27A4BFS342SJG7URQC HHJDRLLNN36UNIA7STAXEEVBCEMPJNB7SJQOS3TJLLYN4AEZ4MHQC KZLFC7OWYNK3G5YNHRANUK3VUVCM6W6J34N7UABYA24XMZWAVVHQC use crate::fluent;use icu_locid::locale;use miette::Diagnostic;use quote::quote;use thiserror::Error;mod attribute;pub mod derive;pub mod error;#[derive(Diagnostic, Debug, Error)]#[diagnostic(transparent)]#[error(transparent)]pub enum MacroError {Attribute(#[from] attribute::Error),#[error("invalid Fluent source code")]Fluent(#[from] fluent::Error),}pub fn localize(attribute: &syn::LitStr,derive_input: &syn::DeriveInput,) -> Result<proc_macro2::TokenStream, MacroError> {let locales = attribute::locales(attribute)?;// TODO: user-controlled canonical localelet group = fluent::Group::new(locale!("en-US"), locales)?;let canonical_locale = group.canonical_locale().id.clone().to_string();let locales = match &derive_input.data {syn::Data::Struct(_struct_data) => derive::locales_for_ident(&group, &derive_input.ident),syn::Data::Enum(enum_data) => derive::locales_for_enum(&group, &enum_data.variants),syn::Data::Union(_) => todo!(),};let message_body = match &derive_input.data {syn::Data::Struct(struct_data) => {derive::message_for_struct(group, &derive_input.ident, &struct_data.fields)}syn::Data::Enum(enum_data) => derive::messages_for_enum(group, &enum_data.variants),syn::Data::Union(_) => todo!(),}?;let ident = &derive_input.ident;Ok(quote! {impl ::fluent_embed::Localize for #ident {const CANONICAL_LOCALE: ::fluent_embed::icu_locid::LanguageIdentifier =::fluent_embed::icu_locid::langid!(#canonical_locale);fn message_for_locale(&self, locale: &::fluent_embed::icu_locid::LanguageIdentifier) -> String {#message_body}fn localize(&self) -> String {let available_locales = #locales;let selected_locale = ::fluent_embed::locale_select::match_locales(&available_locales, &Self::CANONICAL_LOCALE);self.message_for_locale(&selected_locale)}}})}
AttributeError::Build(build_error) => {for location in build_error.locations() {// Create a token stream from the attribute's string literallet [proc_macro2::TokenTree::Literal(ref string_literal)] = derive_attribute.to_token_stream().into_iter().collect::<Vec<_>>()[..]else {abort!(derive_attribute, "unexpected macro attribute");};let (span_start, span_length) = location.span();let error_source = string_literal// Offset by 1 to skip the starting `"` double-quote character.subspan(span_start + 1..=span_start + span_length)// Fall back to the whole attribute if `subspan()` returns `None`// This will always happend on stable as subspan is nightly-only:// https://docs.rs/proc-macro2/latest/proc_macro2/struct.Literal.html#method.subspan.unwrap_or(derive_attribute.span());
attribute::Error::Build(build_error) => {for located_error in build_error.locations() {let error_source = attribute_stream.span();
use std::collections::HashMap;use std::path::PathBuf;use icu_locid::Locale;use miette::Diagnostic;use thiserror::Error;use wax::walk::Entry;#[derive(Diagnostic, Debug, Error)]#[error(transparent)]pub enum Error {Build(#[from] wax::BuildError),Walk(#[from] wax::walk::WalkError),#[error("Directory could not be matched by glob in macro attribute")]NoMatches {path: PathBuf,complete_match: String,},}pub fn locales(attribute: &syn::LitStr) -> Result<HashMap<Locale, PathBuf>, Error> {// Read the fluent file at the given pathlet manifest_root = std::env::var("CARGO_MANIFEST_DIR").unwrap();let attribute_glob = attribute.value();let mut locales = HashMap::new();let glob = wax::Glob::new(&attribute_glob)?;for potential_entry in glob.walk(&manifest_root) {// TODO: this assumes that the locale is the first capturelet entry = potential_entry?;let captured_locale = if let Some(captured) = entry.matched().get(1) {captured} else {return Err(Error::NoMatches {path: entry.path().to_path_buf(),complete_match: entry.matched().complete().to_string(),});};// Captured directories may have a suffix of `/`let stripped_locale = captured_locale.strip_suffix('/').unwrap_or(captured_locale);// TODO: return an error instead of paniclet locale = Locale::try_from_bytes(stripped_locale.as_bytes()).unwrap();// Insert this locale (and make sure it's unique!)let previous_value = locales.insert(locale, entry.into_path());assert!(previous_value.is_none());}Ok(locales)}
#[derive(Diagnostic, Debug, Error)]#[error(transparent)]enum AttributeError {Build(#[from] wax::BuildError),Walk(#[from] wax::walk::WalkError),#[error("Directory could not be matched by glob in macro attribute")]NoMatches {path: PathBuf,complete_match: String,},}#[derive(Diagnostic, Debug, Error)]#[diagnostic(transparent)]#[error(transparent)]enum MacroError {Attribute(#[from] AttributeError),#[error("invalid Fluent source code")]Fluent(#[from] fluent::Error),}fn locales_from_attribute(attribute: &syn::LitStr,) -> Result<HashMap<Locale, PathBuf>, AttributeError> {// Read the fluent file at the given pathlet manifest_root = std::env::var("CARGO_MANIFEST_DIR").unwrap();let attribute_glob = attribute.value();let mut locales = HashMap::new();let glob = wax::Glob::new(&attribute_glob)?;for potential_entry in glob.walk(&manifest_root) {// TODO: this assumes that the locale is the first capturelet entry = potential_entry?;let captured_locale = if let Some(captured) = entry.matched().get(1) {captured} else {return Err(AttributeError::NoMatches {path: entry.path().to_path_buf(),complete_match: entry.matched().complete().to_string(),});};// Captured directories may have a suffix of `/`let stripped_locale = captured_locale.strip_suffix('/').unwrap_or(captured_locale);// TODO: return an error instead of paniclet locale = Locale::try_from_bytes(stripped_locale.as_bytes()).unwrap();// Insert this locale (and make sure it's unique!)let previous_value = locales.insert(locale, entry.into_path());assert!(previous_value.is_none());}Ok(locales)}fn localize_impl(attribute: &syn::LitStr,derive_input: &syn::DeriveInput,) -> Result<proc_macro2::TokenStream, MacroError> {let locales = locales_from_attribute(attribute)?;// TODO: user-controlled canonical localelet group = fluent::Group::new(locale!("en-US"), locales)?;let canonical_locale = group.canonical_locale().id.clone().to_string();let locales = match &derive_input.data {syn::Data::Struct(_struct_data) => derive::locales_for_ident(&group, &derive_input.ident),syn::Data::Enum(enum_data) => derive::locales_for_enum(&group, &enum_data.variants),syn::Data::Union(_) => todo!(),};
mod macro_impl;
let message_body = match &derive_input.data {syn::Data::Struct(struct_data) => {derive::message_for_struct(group, &derive_input.ident, &struct_data.fields)}syn::Data::Enum(enum_data) => derive::messages_for_enum(group, &enum_data.variants),syn::Data::Union(_) => todo!(),}?;let ident = &derive_input.ident;Ok(quote! {impl ::fluent_embed::Localize for #ident {const CANONICAL_LOCALE: ::fluent_embed::icu_locid::LanguageIdentifier =::fluent_embed::icu_locid::langid!(#canonical_locale);fn message_for_locale(&self, locale: &::fluent_embed::icu_locid::LanguageIdentifier) -> String {#message_body}fn localize(&self) -> String {let available_locales = #locales;let selected_locale = ::fluent_embed::locale_select::match_locales(&available_locales, &Self::CANONICAL_LOCALE);self.message_for_locale(&selected_locale)}}})}
let implementation = localize_impl(&derive_attribute, &derive_input).unwrap_or_else(|error| error::handle(error, &derive_attribute, &derive_input.ident));
match macro_impl::localize(&derive_attribute, &derive_input) {Ok(implementation) => {// No errors found, emit the generated `fluent_embed::Localize` implementationquote! {#original_item