From abde7f2bcc41fcd96757926752ae34a3468259e2 Mon Sep 17 00:00:00 2001 From: Victor Hallberg Date: Thu, 18 Jun 2026 17:06:48 +0200 Subject: [PATCH] fix: align numeric placeholder generation in `` with babel implementation --- crates/lingui_macro/src/builder.rs | 41 +++++++++++++++---- crates/lingui_macro/src/jsx_visitor.rs | 20 +++++++-- crates/lingui_macro/src/macro_utils.rs | 1 + crates/lingui_macro/src/tokens.rs | 6 +++ crates/lingui_macro/tests/jsx_icu.rs | 23 +++++++++++ ..._jsx_icu_value_first_keeps_index_zero.snap | 18 ++++++++ ..._icu_value_index_follows_source_order.snap | 18 ++++++++ 7 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 crates/lingui_macro/tests/snapshots/jsx_icu__jsx_icu_value_first_keeps_index_zero.snap create mode 100644 crates/lingui_macro/tests/snapshots/jsx_icu__jsx_icu_value_index_follows_source_order.snap diff --git a/crates/lingui_macro/src/builder.rs b/crates/lingui_macro/src/builder.rs index 3d19754..f05b788 100644 --- a/crates/lingui_macro/src/builder.rs +++ b/crates/lingui_macro/src/builder.rs @@ -357,26 +357,51 @@ impl<'a> MessageBuilder<'a> { } fn push_icu(&mut self, icu: IcuChoice) { - let value_placeholder = self.push_exp(icu.value); - let method = icu.format; - self.push_msg(&format!("{{{value_placeholder}, {method},")); + let IcuChoice { + value, + format: method, + cases, + value_pos, + } = icu; + + // The value placeholder is always emitted first (`{value, plural, ...}`), + // but its numeric index must be allocated in source order relative to the + // cases. Render the cases into `tail` so they can follow the value in the + // output, while index allocation happens as each case is processed. + let mut value = Some(value); + let mut value_placeholder: Option = None; + let mut tail = String::new(); + + for (i, choice) in cases.into_iter().enumerate() { + if i == value_pos { + value_placeholder = Some(self.push_exp(value.take().unwrap())); + } - for choice in icu.cases { match choice { // produce offset:{number} CaseOrOffset::Offset(val) => { - self.push_msg(&format!(" offset:{val}")); + tail.push_str(&format!(" offset:{val}")); } CaseOrOffset::Case(choice) => { let key = choice.key; - self.push_msg(&format!(" {key} {{")); + // Render the case body into `self.message`, then split it off + // so it can be placed after the value. Index allocation + // (numeric_index / values_indexed) persists across the split. + let body_start = self.message.len(); self.process_tokens(choice.tokens); - self.push_msg("}"); + let body = self.message.split_off(body_start); + tail.push_str(&format!(" {key} {{{body}}}")); } } } - self.push_msg("}"); + // `value_pos` may equal `cases.len()` (value is the last/only attribute). + let value_placeholder = match value_placeholder { + Some(placeholder) => placeholder, + None => self.push_exp(value.take().unwrap()), + }; + + self.push_msg(&format!("{{{value_placeholder}, {method},{tail}}}")); } } diff --git a/crates/lingui_macro/src/jsx_visitor.rs b/crates/lingui_macro/src/jsx_visitor.rs index f1727c3..e1c9064 100644 --- a/crates/lingui_macro/src/jsx_visitor.rs +++ b/crates/lingui_macro/src/jsx_visitor.rs @@ -112,14 +112,25 @@ fn is_allowed_plural_option(key: &str) -> Option { impl TransJSXVisitor<'_> { //