Add rudimentary support for built-in `LEN()` function
Dependencies
- [2]
XDJBTEXUAdd support for integer selectors - [3]
D652S2N3Rename `parse` module to `parse_fluent` - [4]
CESJ4CTOMove macro-specific code into `macro_impl` module - [5]
F5LG7WENEmit compilation errors from Fluent source code - [6]
3NMKD6I5Refactor `Localize` trait to use `std::io::Write` - [7]
QFPQZR4KRefactor `fluent_embed` - [8]
5TEX4MNUSplit `fluent_embed` into `group` and `parse` modules - [*]
VNSHGQYNSupport using glob paths in `localize` macro - [*]
XEEXWJLGAdd simple end-to-end test for selectors - [*]
2SITVDYWHandle common errors in Fluent code
Change contents
- file addition: functions.ftl[10.990]
length = Item count: { LEN($items) }numbers = { LEN($items) ->[0] No items[1] One item*[other] { LEN($items) } items}plurals = { LEN($items) ->[one] One item*[other] { LEN($items) } items} - file addition: functions.rs[11.101]
//! End-to-end test for function support in the `l10n_embed_derive` macromod common;use common::compare_message;use icu_locale::{Locale, locale};use l10n_embed_derive::localize;use rstest::rstest;const DEFAULT_LOCALE: Locale = locale!("en-US");#[localize("tests/locale/**/functions.ftl")]pub enum MessageEnum {Length { items: Vec<String> },Numbers { items: Vec<String> },Plurals { items: Vec<String> },}#[localize("tests/locale/**/functions.ftl")]pub struct Length {items: Vec<String>,}#[localize("tests/locale/**/functions.ftl")]pub struct Numbers {items: Vec<String>,}#[localize("tests/locale/**/functions.ftl")]pub struct Plurals {items: Vec<String>,}#[rstest]#[case::zero(0)]#[case::one(1)]#[case::two(2)]fn len(#[case] item_count: usize) {let items: Vec<String> = (0..item_count).map(|index| format!("Item {index}")).collect();compare_message(MessageEnum::Length {items: items.clone(),},format!("Item count: {item_count}"),DEFAULT_LOCALE,);compare_message(Length { items },format!("Item count: {item_count}"),DEFAULT_LOCALE,);}#[rstest]#[case::zero(0, "No items")]#[case::one(1, "One item")]#[case::two(2, "2 items")]fn numbers(#[case] item_count: usize, #[case] expected: &str) {let items: Vec<String> = (0..item_count).map(|index| format!("Item {index}")).collect();compare_message(MessageEnum::Numbers {items: items.clone(),},expected,DEFAULT_LOCALE,);compare_message(Numbers { items }, expected, DEFAULT_LOCALE);}#[rstest]#[case::zero(0, "0 items")]#[case::one(1, "One item")]#[case::two(2, "2 items")]fn plurals(#[case] item_count: usize, #[case] expected: &str) {let items: Vec<String> = (0..item_count).map(|index| format!("Item {index}")).collect();compare_message(MessageEnum::Plurals {items: items.clone(),},expected,DEFAULT_LOCALE,);compare_message(Plurals { items }, expected, DEFAULT_LOCALE);} - edit in l10n_embed_derive/src/fluent/mod.rs at line 24[12.346][3.1320]
#[error("unable to parse Fluent function")]pub enum FunctionError {InvalidName {name: String,#[source_code]source_code: NamedSource<String>,// TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373// #[label("This key isn't in the `{canonical_locale}` locale")]// span: SourceSpan,},IncorrectPositionalArgumentCount {expected_len: usize,actual_len: usize,#[source_code]source_code: NamedSource<String>,// TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373// #[label("This key isn't in the `{canonical_locale}` locale")]// span: SourceSpan,},UnexpectedNamedArguments {#[source_code]source_code: NamedSource<String>,// TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373// #[label("This key isn't in the `{canonical_locale}` locale")]// span: SourceSpan,},NonVariableReference {#[source_code]source_code: NamedSource<String>,// TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373// #[label("This key isn't in the `{canonical_locale}` locale")]// span: SourceSpan,},}#[derive(Diagnostic, Debug, Error)] - edit in l10n_embed_derive/src/fluent/mod.rs at line 106[2.2860][12.1364]
#[error(transparent)]#[diagnostic(transparent)]Function(FunctionError), - replacement in l10n_embed_derive/src/fluent/ast.rs at line 3
use super::{Error, MessageContext};use super::{Error, FunctionError, MessageContext}; - replacement in l10n_embed_derive/src/fluent/ast.rs at line 46
// Only dereference if enum variantlet category_reference =match message_context.derive_context.reference_kind {let category_reference = match selector {// Can't dereference value returned by functionInlineExpression::FunctionReference { .. } => {quote!(#raw_match_target)}// Only dereference if enum variant_ => match message_context.derive_context.reference_kind { - replacement in l10n_embed_derive/src/fluent/ast.rs at line 55
};},}; - edit in l10n_embed_derive/src/fluent/ast.rs at line 288
InlineExpression::FunctionReference { id, arguments } => {match id.name.to_snake_case().as_str() {"len" => {if !arguments.named.is_empty() {return Err(Error::Function(FunctionError::UnexpectedNamedArguments {source_code: message_context.source.named_source.clone(),}));}let argument = match arguments.positional.as_slice() {[single_argument] => single_argument,_ => {return Err(Error::Function(FunctionError::IncorrectPositionalArgumentCount {expected_len: 1,actual_len: arguments.positional.len(),source_code: message_context.source.named_source.clone(),},));}};let variable_reference = match argument {InlineExpression::VariableReference { .. } => {inline_expression(argument, message_context)?}_ => {return Err(Error::Function(FunctionError::NonVariableReference {source_code: message_context.source.named_source.clone(),}));}};let vec_reference = match message_context.derive_context.reference_kind {ReferenceKind::EnumField => quote!(#variable_reference),ReferenceKind::StructField => quote!(&#variable_reference),};parse_quote!(Vec::len(#vec_reference))}_ => {return Err(Error::Function(FunctionError::InvalidName {name: id.name.clone(),source_code: message_context.source.named_source.clone(),}));}}}