Moves everything into modules, and restructures the API to be much cleaner.
QFPQZR4K4UZ7R2GQZJG4NYBGVQJVL2ANIKGGTOHAMIRIBQHPSQGAC F5LG7WENUUDRSCTDMA4M6BAC5RWTGQO45C4ZEBZDX6FHCTTHBVGQC O77KA6C4UJGZXVGPEA7WCRQH6XYQJPWETSPDXI3VOKOSRQND7JEQC NO3PDO7PY7J3WPADNCS5VD6HKFY63E23I3SDR4DHXNVQJTG27RAAC 5TEX4MNUC4LDDRMNEOVCFNUUEZAGUXMKO3OIEQFXWRQKXSHY2NRQC XGNME3WRU3MJDTFHUFJYARLVXWBZIH5ODBOIIFTXHNCBTZQH2R7QC D652S2N3MHR7NJWSJIT7DUH5TPEFF6YII7EGV4C7IYWARXLMGAWQC RLX6XPNZKD6GIRLWKYXFH2RNIU4ZNXLMHXLOMID3E6H53QXXXNZQC UOMQT7LTURIIWHZT2ZHLCJG6XESYTN26EJC7IHRFR4PYJ355PNYAC V5S5K33ALIEG5ZABUSAPO4ULHEBFDB2PLTW27A4BFS342SJG7URQC 2XQ6ZB4WZNNR4KNC3VWNTV7IRMGGAEP33JPQUVB3CVWAKHECZVRQC BQ6N55O7RPG47G35YI37Z37456VKWT5KLGQKDQVAN2WI4K34TRBQC 3WEPY3OXJJ72TNVZLFCN2ZDWSADLT52T6DUONFGEAB46UWAQD3PQC ROSR4HD5ENPQU3HH5IVYSOA5YM72W77CHVQARSD3T67BUNYG7KZQC VNSHGQYNPGKGGPYNVP4Z2RWD7JCSDJVYAADD6UXWBYL6ZRXKLE4AC HJMYJDC77NLU44QZWIW7CELXJKD4EK4YZ6CCILYBG6FWGZ2KMBVAC 5FIVUZYFLOZ2CCH4GCOQQZFL3GDEB23VJ7J6YUXQDZQEAQDB76DQC QSK7JRBA55ZRY322WXGNRROJL7NTFBR6MJPOOA5B2XD2JAVM4MWQC // 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.clone(), resource);paths.insert(locale, entry.to_candidate_path().to_string());
// Insert this locale (and make sure it's unique!)let previous_value = locales.insert(locale, entry.into_path());assert!(previous_value.is_none());
pub fn localize(path: &syn::LitStr, derive_input: &DeriveInput) -> Result<TokenStream, MacroError> {let group = attribute_groups(path)?;
pub fn localize(attribute: &syn::LitStr,derive_input: &syn::DeriveInput,) -> Result<TokenStream, MacroError> {let locales = locales_from_attribute(&attribute)?;// let context = todo!();// TODO: user-controlled canonical localelet group = fluent::Group::new(locale!("en-US"), locales)?;
use crate::derive::DeriveContext;use std::collections::HashMap;use std::path::PathBuf;use fluent_syntax::ast::{Entry, Message, Pattern, Resource};use miette::Diagnostic;use thiserror::Error;mod ast;mod group;pub use group::Group;#[derive(Diagnostic, Debug, Error)]#[diagnostic(transparent)]#[error(transparent)]pub enum Error {InvalidReference(#[from] ast::InvalidReference),}#[derive(Clone, Copy, Debug)]pub struct MessageContext<'context> {syntax_tree: &'context Resource<String>,pattern: &'context Pattern<String>,path: &'context str,derive_context: &'context DeriveContext,}#[derive(Clone, Debug)]struct SourceFile {path: PathBuf,syntax_tree: fluent_syntax::ast::Resource<String>,messages: HashMap<String, Message<String>>,}impl SourceFile {fn new(path: PathBuf) -> Result<Self, Error> {let file_contents = std::fs::read_to_string(&path).unwrap();// TODO: return an error instead of paniclet syntax_tree = fluent_syntax::parser::parse(file_contents).unwrap();// Keep track of all the messages in our Fluent filelet mut messages = HashMap::with_capacity(syntax_tree.body.len());for entry in &syntax_tree.body {match entry {Entry::Message(message_entry) => {let message_id = message_entry.id.name.to_owned();// Insert this message id (and make sure it's unique!)let previous_value = messages.insert(message_id, message_entry.clone());assert!(previous_value.is_none());}Entry::Term(_) => todo!(),Entry::Comment(_) => todo!(),Entry::GroupComment(_) => todo!(),Entry::ResourceComment(_) => todo!(),Entry::Junk { .. } => todo!(),}}Ok(Self {path,syntax_tree,messages,})}/// Unique message IDs, to compare with other localesfn message_ids(&self) -> impl Iterator<Item = &str> {self.messages.keys().map(String::as_str)}fn contains_expression(&self, id: &str) -> bool {self.messages.contains_key(id)}fn remove_expression(&mut self,id: &str,derive_context: &DeriveContext,) -> Result<syn::Expr, Error> {let message = self.messages.remove(id).unwrap();let message_context = MessageContext {syntax_tree: &self.syntax_tree,// Any message where this value is `None` shouldn't be accessed directly, see:// https://docs.rs/fluent-syntax/latest/fluent_syntax/ast/struct.Attribute.html#example// TODO: return an error instead of panicpattern: message.value.as_ref().unwrap(),path: &self.path.to_string_lossy(),derive_context,};ast::message_body(message_context)}}
use crate::parse_fluent::{self, FluentError, References};use std::collections::HashMap;
use super::{Error, SourceFile};use crate::derive::DeriveContext;use std::collections::{HashMap, HashSet};use std::path::PathBuf;
struct LocaleGroup {locale: Locale,messages: Box<[Option<Message<String>>]>,}impl LocaleGroup {fn new(locale: Locale,resource: Resource<String>,canonical_messages: &[Message<String>],) -> Self {let mut messages = vec![None; canonical_messages.len()].into_boxed_slice();for entry in resource.body {if let Entry::Message(message) = entry {let index = canonical_messages.iter().position(|canonical_message| canonical_message.id.name == message.id.name).expect("Message ID must be in canonical group");assert!(messages[index].is_none());messages[index] = Some(message);}}Self { locale, messages }}}#[derive(Clone, Debug)]
mut resources: HashMap<Locale, Resource<String>>,paths: HashMap<Locale, String>,) -> Self {let canonical_resource = resources.remove(&canonical_locale).unwrap();
locale_paths: HashMap<Locale, PathBuf>,) -> Result<Self, Error> {let mut locales = HashMap::with_capacity(locale_paths.len());
for entry in canonical_resource.body {if let Entry::Message(message) = entry {canonical_messages.push(message);}
// Insert resource (and make sure it's unique!)let previous_value = locales.insert(locale, resource);assert!(previous_value.is_none());
let extra_locales = resources.into_iter().map(|(locale, resource)| LocaleGroup::new(locale, resource, &canonical_messages)).collect::<Vec<_>>();
// Collect all keys used in the Fluent source codelet keys: HashSet<&str> =HashSet::from_iter(locales.values().flat_map(|resource| resource.message_ids()));// Collect all keys from the canonical resource// TODO: return an error instead of paniclet canonical_resource = locales.get(&canonical_locale).unwrap();// TODO: return an error instead of panic// Make sure canonical resource contains all message keysassert_eq!(keys, HashSet::from_iter(canonical_resource.message_ids()));
references: &References,) -> Result<syn::Expr, FluentError> {let message = self.canonical_messages.iter().find(|message| message.id.name == id).expect("Message id must be valid");
derive_context: &DeriveContext,) -> Result<syn::Expr, Error> {let canonical_resource = self.locales.get_mut(&self.canonical_locale).unwrap();let message = canonical_resource.remove_expression(id, derive_context)?;
references: &References,) -> Result<Vec<(&Locale, syn::Expr)>, FluentError> {let mut messages = Vec::with_capacity(self.extra_locales.len());let message_column = self.canonical_messages.iter().position(|message| message.id.name == id).expect("Message id must be valid");
derive_context: &DeriveContext,) -> Result<HashMap<&Locale, syn::Expr>, Error> {// Create a message for every locale *except* the canonoical localelet mut messages = HashMap::with_capacity(self.locales.len() - 1);
if let Some(message) = &locale_group.messages[message_column] {let path = self.paths.get(&locale_group.locale).unwrap();let message_expr = parse_fluent::message(&message, references, path)?;messages.push((&locale_group.locale, message_expr))
if compiled_messages.contains_expression(id) {let message = compiled_messages.remove_expression(id, derive_context)?;// Insert this message (and make sure it's unique!)let previous_value = messages.insert(locale, message);assert!(previous_value.is_none());
use fluent_syntax::ast::{Entry, Expression, InlineExpression, Message, Pattern, PatternElement, Resource, Variant,VariantKey,};
use fluent_syntax::ast::{Entry, Expression, InlineExpression, PatternElement, VariantKey};
#[derive(Diagnostic, Debug, Error)]#[diagnostic(transparent)]#[error(transparent)]pub enum FluentError {InvalidReference(#[from] InvalidReference),}#[derive(Clone, Copy, Debug)]pub enum ReferenceKind {EnumField,StructField,}#[derive(Clone, Debug)]pub struct References {pub kind: ReferenceKind,pub candidates: HashSet<String>,}pub(crate) fn pattern(message: &Message<String>,pattern: &Pattern<String>,references: &References,path: &str,) -> Result<syn::Expr, FluentError> {
pub fn message_body(message_context: MessageContext) -> Result<syn::Expr, Error> {
let target = inline_expression(message, selector, references, path)?;let arms: Vec<syn::Arm> = variants.iter().map(|item| variant(message, item, references, path)).collect::<Result<_, FluentError>>()?;
let target = inline_expression(selector, message_context)?;let mut arms: Vec<syn::Arm> = Vec::with_capacity(variants.len());for variant in variants {let base_pattern: syn::Pat = match &variant.key {VariantKey::Identifier { name } => {let ident = format_ident!("{}", name.to_pascal_case());parse_quote!(::icu_plurals::PluralCategory::#ident)}VariantKey::NumberLiteral { .. } => todo!(),};// Create a new `MessageContext` for each variantlet variant_context = MessageContext {pattern: &variant.value,..message_context};let body = message_body(variant_context)?;// Default patterns match anything else// TODO: this can potentially generate unreachable patterns,// should be replaced with a more sophisticated implementationlet pattern = if variant.default {parse_quote!(#base_pattern | _)} else {base_pattern};
}fn variant(message: &Message<String>,variant: &Variant<String>,references: &References,path: &str,) -> Result<syn::Arm, FluentError> {let base_pattern = variant_key(&variant.key);let body = pattern(message, &variant.value, references, path)?;// Default patterns match anything else// TODO: this can potentially generate unreachable patterns,// should be replaced with a more sophisticated implementationlet pattern = if variant.default {parse_quote!(#base_pattern | _)} else {base_pattern};Ok(parse_quote!(#pattern => #body))}fn variant_key(variant_key: &VariantKey<String>) -> syn::Pat {match variant_key {VariantKey::Identifier { name } => {let ident = format_ident!("{}", name.to_pascal_case());parse_quote!(::icu_plurals::PluralCategory::#ident)}VariantKey::NumberLiteral { .. } => todo!(),}}pub(crate) fn message(message: &Message<String>,references: &References,path: &str,) -> Result<syn::Expr, FluentError> {if let Some(value) = message.value.as_ref() {pattern(message, value, references, path)} else {Ok(parse_quote!(()))}
syn::Fields::Named(named_fields) => References {kind: ReferenceKind::EnumField,candidates: unique_named_fields(named_fields),
syn::Fields::Named(named_fields) => DeriveContext {reference_kind: ReferenceKind::EnumField,valid_references: unique_named_fields(named_fields),