Add support for integer selectors
Dependencies
- [2]
FF67HCOFImprove Fluent syntax error spans - [3]
EKXWNEPKRename `Localize::message_for_locale` to `Localize::localize_for` - [4]
PGBXJWIHMove `l10n_embed` re-exports into `macro_prelude` module - [5]
MVTRHSJLMove from `fluent-syntax` PR to main branch - [6]
F64TRIFZTest both structs and enums in `l10n_embed_derive` - [7]
HHJDRLLNCreate `fluent_embed_runtime` crate - [8]
KZLFC7OWRename `fluent_embed_runtime` to `fluent_embed` - [9]
6XEMHUGSUse full `Locale` instead of `LanguageIdentifier` subset - [10]
MABGENI7Refactor `fluent_embed_derive` tests - [11]
6ABVDTXZImprove `fluent_embed_derive` test suite - [12]
3NMKD6I5Refactor `Localize` trait to use `std::io::Write` - [13]
C6W7N6N5Implement `Localize` for `FixedDecimal` and primitive number types - [14]
7X4MEZJUUse Fluent AST when reporting error spans - [15]
2SITVDYWHandle common errors in Fluent code - [16]
7FYXVNABIgnore comments in Fluent source code - [17]
7M4UI3TWUpdate dependencies to latest versions - [18]
Y6YSEDJMFix bug preventing structs from using selectors - [19]
F5LG7WENEmit compilation errors from Fluent source code - [20]
VNSHGQYNSupport using glob paths in `localize` macro - [21]
CESJ4CTOMove macro-specific code into `macro_impl` module - [22]
EAPOUW73Fix compilation error with `fluent-syntax` span PR - [23]
QFPQZR4KRefactor `fluent_embed` - [24]
5TEX4MNUSplit `fluent_embed` into `group` and `parse` modules - [25]
S2444K42Refactor selectors test to not rely on funciton calls - [26]
XEEXWJLGAdd simple end-to-end test for selectors - [27]
RUCC2HKZRename from `fluent_embed` to `l10n_embed` - [28]
AAERM7PBAdd selector tests for the `fr` locale
Change contents
- replacement in l10n_embed_derive/tests/selectors.rs at line 12
Emails { unread_emails: u64 },Numbers { unread_emails: u64 },Plurals { unread_emails: u64 },}#[localize("tests/locale/**/selectors.ftl")]pub struct Numbers {unread_emails: u64, - replacement in l10n_embed_derive/tests/selectors.rs at line 22
pub struct Emails {pub struct Plurals { - edit in l10n_embed_derive/tests/selectors.rs at line 28
#[case::zero_en(locale!("en-US"), 0, "You have no unread emails.")]#[case::one_en(locale!("en-US"), 1, "You have an unread email.")]#[case::two_en(locale!("en-US"), 2, "You have 2 unread emails.")]#[case::max_en(locale!("en-US"),u64::MAX,"You have 18,446,744,073,709,551,615 unread emails.")]#[case::zero_fr(locale!("fr"), 0, "Vous n'avez aucun e-mail non lu.")]#[case::one_fr(locale!("fr"), 1, "Vous avez un e-mail non lu.")]#[case::two_fr(locale!("fr"), 2, "Vous avez 2 e-mails non lus.")]#[case::max_fr(locale!("fr"), u64::MAX, "Vous avez 18 446 744 073 709 551 615 e-mails non lus.")]fn numbers(#[case] locale: Locale, #[case] unread_emails: u64, #[case] expected_message: String) {compare_message(Numbers { unread_emails }, &expected_message, locale.clone());compare_message(MessageEnum::Numbers { unread_emails },expected_message,locale,);}/// End-to-end test of locale-specific selectors implementation by checking final output#[rstest] - replacement in l10n_embed_derive/tests/selectors.rs at line 63[7.562]→[3.603:620](∅→∅),[3.620]→[7.563:591](∅→∅),[7.1090]→[7.563:591](∅→∅),[7.591]→[7.702:772](∅→∅),[7.702]→[7.702:772](∅→∅),[7.772]→[7.1186:1190](∅→∅),[7.1186]→[7.1186:1190](∅→∅),[7.1190]→[6.0:82](∅→∅)
fn localize_for(#[case] locale: Locale,#[case] unread_emails: u64,#[case] expected_message: String,) {compare_message(Emails { unread_emails }, &expected_message, locale.clone());fn plurals(#[case] locale: Locale, #[case] unread_emails: u64, #[case] expected_message: String) {compare_message(Plurals { unread_emails }, &expected_message, locale.clone()); - replacement in l10n_embed_derive/tests/selectors.rs at line 66
MessageEnum::Emails { unread_emails },MessageEnum::Plurals { unread_emails }, - replacement in l10n_embed_derive/tests/locale/fr/selectors.ftl at line 1
emails =# Selectors example from Fluent guide: https://projectfluent.org/fluent/guide/selectors.html# Poorly translated into French :)numbers ={ $unreadEmails ->[0] Vous n'avez aucun e-mail non lu.[1] Vous avez un e-mail non lu.*[other] Vous avez { $unreadEmails } e-mails non lus.}# French singular includes both 0 and 1plurals = - replacement in l10n_embed_derive/tests/locale/en-US/selectors.ftl at line 3
emails =numbers ={ $unreadEmails ->[0] You have no unread emails.[1] You have an unread email.*[other] You have { $unreadEmails } unread emails.}plurals = - edit in l10n_embed_derive/src/fluent/mod.rs at line 3
use std::num::ParseIntError; - edit in l10n_embed_derive/src/fluent/mod.rs at line 47
// span: SourceSpan,},#[error("invalid number literal")]InvalidNumberLiteral {invalid_literal: String,parse_error: ParseIntError,#[source_code]source_code: NamedSource<String>,// TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373// #[label("This can't be parsed as an unsigned 128-bit integer")] - edit in l10n_embed_derive/src/fluent/mod.rs at line 59
#[error("invalid identifier type")]InvalidIdentifierType {invalid_identifier: String,expected_type: ast::VariantType,found_type: ast::VariantType,#[source_code]source_code: NamedSource<String>,// TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373// #[label("This can't be parsed as an unsigned 128-bit integer")]// span: SourceSpan,}, - replacement in l10n_embed_derive/src/fluent/ast.rs at line 7
use fluent_syntax::ast::{Expression, InlineExpression, PatternElement, VariantKey};use fluent_syntax::ast::{Expression, InlineExpression, Pattern, PatternElement, Variant, VariantKey,}; - replacement in l10n_embed_derive/src/fluent/ast.rs at line 11
use quote::{format_ident, quote};use proc_macro2::{Literal, TokenStream};use quote::{ToTokens, format_ident, quote}; - edit in l10n_embed_derive/src/fluent/ast.rs at line 14
#[derive(PartialEq, Eq, Clone, Copy, Debug)]pub enum VariantType {Integer,Plural,}struct Select<'variants> {variant_type: VariantType,default_pattern: TokenStream,default_expression: &'variants Pattern<String>,match_arms: Vec<(TokenStream, &'variants Pattern<String>)>,} - replacement in l10n_embed_derive/src/fluent/ast.rs at line 40
let match_target = inline_expression(selector, message_context)?;let default_arm = OnceCell::new();let mut additional_arms = Vec::with_capacity(variants.len());let select = select(&message_context, variants)?; - replacement in l10n_embed_derive/src/fluent/ast.rs at line 42[7.6889]→[7.6889:6939](∅→∅),[7.6939]→[7.1529:1610](∅→∅),[7.1610]→[5.640:709](∅→∅),[7.78]→[7.1611:1755](∅→∅),[5.709]→[7.1611:1755](∅→∅),[7.7086]→[7.1611:1755](∅→∅),[7.1755]→[4.1064:1191](∅→∅),[7.371]→[7.7266:7408](∅→∅),[4.1191]→[7.7266:7408](∅→∅),[7.1728]→[7.7266:7408](∅→∅),[7.1869]→[7.7266:7408](∅→∅),[7.2276]→[7.7266:7408](∅→∅),[7.7266]→[7.7266:7408](∅→∅),[7.7408]→[7.1870:1954](∅→∅),[7.1954]→[7.7486:7691](∅→∅),[7.7486]→[7.7486:7691](∅→∅),[7.7691]→[7.1955:2145](∅→∅),[7.7763]→[7.2146:2422](∅→∅),[7.2422]→[7.8138:8175](∅→∅),[7.8138]→[7.8138:8175](∅→∅),[7.8175]→[7.2423:2520](∅→∅)
for variant in variants {let variant_pattern: syn::Pat = match &variant.key {VariantKey::Identifier { name } => {let ident_pascal_case =format_ident!("{}", name.to_pascal_case());parse_quote!(::l10n_embed::macro_prelude::icu_plurals::PluralCategory::#ident_pascal_case)}VariantKey::NumberLiteral { .. } => todo!(),};// Create a new `MessageContext` for each variant bodylet variant_context = MessageContext {pattern: &variant.value,..message_context};let variant_body = message_body(variant_context)?;// The default pattern must go last so we don't generate invalid match statamentsif variant.default {default_arm.set(quote!(#variant_pattern | _ => #variant_body)).expect("Multiple default patterns for match statement.");} else {additional_arms.push(quote!(#variant_pattern => #variant_body));let raw_match_target = inline_expression(selector, message_context)?;let match_target = match select.variant_type {VariantType::Integer { .. } => quote!(#raw_match_target),VariantType::Plural { .. } => {// Only dereference if enum variantlet category_reference =match message_context.derive_context.reference_kind {ReferenceKind::EnumField => quote!(*#raw_match_target),ReferenceKind::StructField => quote!(#raw_match_target),};quote!(plural_rules.category_for(#category_reference)) - replacement in l10n_embed_derive/src/fluent/ast.rs at line 54
}}; - replacement in l10n_embed_derive/src/fluent/ast.rs at line 56
// The parser should guarantee a default arm is availablelet default_arm = default_arm.get().unwrap();let default_match_pattern = select.default_pattern;let default_match_expression = message_body(MessageContext {pattern: select.default_expression,..message_context})?; - replacement in l10n_embed_derive/src/fluent/ast.rs at line 62
// Only dereference if enum variantlet category_reference = match message_context.derive_context.reference_kind {ReferenceKind::EnumField => quote!(*#match_target),ReferenceKind::StructField => quote!(#match_target),};let match_patterns = select.match_arms.iter().map(|(pattern, _expression)| pattern).collect::<Vec<&TokenStream>>();let match_expressions = select.match_arms.iter().map(|(_match_pattern, expression)| {message_body(MessageContext {pattern: expression,..message_context})}).collect::<Result<Vec<syn::Expr>, Error>>()?; - replacement in l10n_embed_derive/src/fluent/ast.rs at line 79[7.3664]→[7.713:796](∅→∅),[7.796]→[7.2782:2882](∅→∅),[7.1616]→[7.2782:2882](∅→∅),[7.2782]→[7.2782:2882](∅→∅)
match plural_rules.category_for(#category_reference) {#(#additional_arms,)*#default_arm,match #match_target {#(#match_patterns => #match_expressions),*#default_match_pattern => #default_match_expression, - edit in l10n_embed_derive/src/fluent/ast.rs at line 101
fn select<'variants>(message_context: &MessageContext,variants: &'variants Vec<Variant<String>>,) -> Result<Select<'variants>, Error> {let expected_variant_type = OnceCell::new();for variant in variants {let variant_type = match &variant.key {VariantKey::Identifier { name } => match name.as_str() {// "other" is ambiguous as it could be used for plurals or as the default integer case"other" => None,_ => Some(VariantType::Plural),},VariantKey::NumberLiteral { .. } => Some(VariantType::Integer),};// Make sure all variants are of the same typeif let Some(variant_kind) = variant_type {let expected_type = expected_variant_type.get_or_init(|| variant_kind);if *expected_type != variant_kind {todo!()}}}match expected_variant_type.get().unwrap() {VariantType::Integer => select_integers(message_context, variants),VariantType::Plural => select_plurals(message_context, variants),}}fn select_integers<'variants>(message_context: &MessageContext,variants: &'variants Vec<Variant<String>>,) -> Result<Select<'variants>, Error> {let mut default_expression = OnceCell::new();let mut match_arms = Vec::new();for variant in variants {match &variant.key {VariantKey::Identifier { name } => match name.to_lowercase().as_str() {"other" => default_expression.set(&variant.value).expect("Multiple default variants"),_ => {return Err(Error::InvalidIdentifierType {invalid_identifier: name.clone(),expected_type: VariantType::Integer,found_type: VariantType::Plural,source_code: message_context.source.named_source.clone(),});}},VariantKey::NumberLiteral { value } => {let parsed_integer: u128 = match value.parse() {Ok(integer) => integer,Err(parse_error) => {return Err(Error::InvalidNumberLiteral {invalid_literal: value.clone(),parse_error,source_code: message_context.source.named_source.clone(),});}};let integer_literal = Literal::u128_unsuffixed(parsed_integer);match_arms.push((integer_literal.to_token_stream(), &variant.value));}}}let default_expression = default_expression.take().expect("No default variant");Ok(Select {variant_type: VariantType::Integer,default_pattern: quote!(_),default_expression,match_arms,})}fn select_plurals<'variants>(message_context: &MessageContext,variants: &'variants Vec<Variant<String>>,) -> Result<Select<'variants>, Error> {let mut default_arm = OnceCell::new();let mut match_arms = Vec::new();for variant in variants {match &variant.key {VariantKey::Identifier { name } => {let name_pascal_case = name.to_pascal_case();let ident = syn::Ident::new(&name_pascal_case, proc_macro2::Span::call_site());let pattern =quote!(::l10n_embed::macro_prelude::icu_plurals::PluralCategory::#ident);match variant.default {// The default arm should include any enum variants not matchedtrue => default_arm.set((quote!(#pattern | _), &variant.value)).expect("Multiple default variants"),false => match_arms.push((pattern, &variant.value)),}}VariantKey::NumberLiteral { value } => {return Err(Error::InvalidIdentifierType {invalid_identifier: value.clone(),expected_type: VariantType::Plural,found_type: VariantType::Integer,source_code: message_context.source.named_source.clone(),});}}}let (default_pattern, default_expression) = default_arm.take().expect("No default variant");Ok(Select {variant_type: VariantType::Plural,default_pattern,default_expression,match_arms,})}