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 locale
let 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 panic
let syntax_tree = fluent_syntax::parser::parse(file_contents).unwrap();
// Keep track of all the messages in our Fluent file
let 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 locales
fn 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 panic
pattern: 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 code
let 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 panic
let canonical_resource = locales.get(&canonical_locale).unwrap();
// TODO: return an error instead of panic
// Make sure canonical resource contains all message keys
assert_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 locale
let 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 variant
let 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 implementation
let 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 implementation
let 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),