Handles VariantKey::NumberLiteral when converting Fluent select statements to Rust match statements. Currently does not support decimal values, but there isn't any reason they couldn't be added.
XDJBTEXUZNIAC2TKC4Z3OZORWAXR4ZWYUHNM6OEGXTL6WZDXOVZQC FF67HCOFIP6LBJCPUC7PBL74KDFZEFP6NELQPILRIFLYHV3JQWLAC EKXWNEPK4FTYKT2RJL2L7HTM64VQGDD3DYD6NZIDGMMV6ITHUVZAC PGBXJWIHSVTRD7CGDSCPC4YHI65EBKMQFEX62RZWL4EZB63622XAC MVTRHSJLQ32Q62257NC3ADECW42A32ZTBDPGUZMBUQXSAM64CWHQC F64TRIFZZYRZJTM3OWBI7IEKY3S37GGH2KKKS65HRKIOZZOKPFJQC Y6YSEDJMU4RLAQG4LJUV5MN6Q2KK2FWOBBIIQY2ZUVM2PREVSNFAC XEEXWJLGVIPIGURSDU4ETZMGAIFTFDPECM4QWFOSRHU7GMGVOUVQC 7FYXVNAB6JAP3CJKE4MY57UWYSUPEXFVER6K264BSKYHVU6V4SGQC 6ABVDTXZOHVUDZDKDQS256F74LFIMM5DO3OZWHKRXZBUTPII4WAQC 6XEMHUGSNX5YSWZYM7PZUTTUMFODMGO74QLHGEXQ5LAC7LPS7JNQC AAERM7PBDVDFDEEXZ7UJ5WWQF7SPEJQQYXRBZ63ETB5YAVIECHMAC S2444K42FJFLTQMMU6PAVA4YRQGDNCMIFBQ5VO2LCD4GJ7LUCRYQC QFPQZR4K4UZ7R2GQZJG4NYBGVQJVL2ANIKGGTOHAMIRIBQHPSQGAC 2SITVDYW6KANM24QXRHVSBL6S77UHKJLOSOHSUZQBJFL5NAAGQYAC 7M4UI3TWQIAA333GQ577HDWDWZPSZKWCYG556L6SBRLB6SZDQYPAC 5TEX4MNUC4LDDRMNEOVCFNUUEZAGUXMKO3OIEQFXWRQKXSHY2NRQC C6W7N6N57UCNHEV55HEZ3G7WN2ZOBGMFBB5M5ZPDB2HNNHHTOPBQC F5LG7WENUUDRSCTDMA4M6BAC5RWTGQO45C4ZEBZDX6FHCTTHBVGQC 3NMKD6I57ONAGHEN4PZIAV2KPYESVR4JL3DTWSHXKCMVJBEQ4GIQC 7X4MEZJUMLYYIBV7ANLADELOZ7I7AJ5CKFAR35CJ2SBZDDVJFZOQC EAPOUW73YRB5FPBHD6Z2DR33Y7CZGEPM2C4UVOHAOH3OVINVE4FAC KZLFC7OWYNK3G5YNHRANUK3VUVCM6W6J34N7UABYA24XMZWAVVHQC HHJDRLLNN36UNIA7STAXEEVBCEMPJNB7SJQOS3TJLLYN4AEZ4MHQC RUCC2HKZZTUHN3G6IWS4NK3VYGXAI6PORJH2YZKPRAYSDWH63ESQC #[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]
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());
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 =
// 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")]
#[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,},
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)?;
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))
// 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})?;
// 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>>()?;
match plural_rules.category_for(#category_reference) {#(#additional_arms,)*#default_arm,
match #match_target {#(#match_patterns => #match_expressions),*#default_match_pattern => #default_match_expression,
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,})}