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 locale
let 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 literal
let [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 path
let 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 capture
let 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 panic
let 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 path
let 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 capture
let 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 panic
let 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 locale
let 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` implementation
quote! {
#original_item