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 1
plurals =
// 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 body
let 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 stataments
if 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 variant
let 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 available
let 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 variant
let 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 type
if 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 matched
true => 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,
})
}