From da589d5eb5bdf0bda7c2325697df93664962ba49 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Fri, 26 Jun 2026 09:56:51 +0200 Subject: [PATCH 1/5] refactor labeled expressions --- crates/lingui_macro/src/ast_utils.rs | 11 -- crates/lingui_macro/src/builder.rs | 126 ++++----------- crates/lingui_macro/src/js_macro_folder.rs | 26 +-- crates/lingui_macro/src/jsx_visitor.rs | 115 ++++++++------ crates/lingui_macro/src/lib.rs | 9 +- crates/lingui_macro/src/macro_utils.rs | 150 ++++++++++++++---- crates/lingui_macro/src/tokens.rs | 29 ++-- crates/lingui_macro/tests/jsx_icu.rs | 41 +++++ ...u__js_choices_may_contain_expressions.snap | 4 +- .../js_t__js_custom_i18n_passed.snap | 4 +- .../js_t__js_substitution_in_tpl_literal.snap | 4 +- ...icu__with_labeled_expression_as_value.snap | 23 +++ ...h_labeled_expression_as_value_with_ph.snap | 24 +++ ..._icu__with_non_identifier_value_first.snap | 17 ++ ...icu__with_value_index_in_source_order.snap | 17 ++ 15 files changed, 379 insertions(+), 221 deletions(-) create mode 100644 crates/lingui_macro/tests/snapshots/jsx_icu__with_labeled_expression_as_value.snap create mode 100644 crates/lingui_macro/tests/snapshots/jsx_icu__with_labeled_expression_as_value_with_ph.snap create mode 100644 crates/lingui_macro/tests/snapshots/jsx_icu__with_non_identifier_value_first.snap create mode 100644 crates/lingui_macro/tests/snapshots/jsx_icu__with_value_index_in_source_order.snap diff --git a/crates/lingui_macro/src/ast_utils.rs b/crates/lingui_macro/src/ast_utils.rs index 4ae1248..82cdfda 100644 --- a/crates/lingui_macro/src/ast_utils.rs +++ b/crates/lingui_macro/src/ast_utils.rs @@ -185,17 +185,6 @@ pub fn get_prop_key(prop: &KeyValueProp) -> Option { } } -// recursively expands TypeScript's as expressions until it reaches a real value -pub fn expand_ts_as_expr(mut expr: Box) -> Box { - while let Expr::TsAs(TsAsExpr { - expr: inner_expr, .. - }) = *expr - { - expr = inner_expr; - } - - expr -} pub fn create_key_value_prop(key: &str, value: Box) -> PropOrSpread { PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { diff --git a/crates/lingui_macro/src/builder.rs b/crates/lingui_macro/src/builder.rs index 3d19754..2e1855c 100644 --- a/crates/lingui_macro/src/builder.rs +++ b/crates/lingui_macro/src/builder.rs @@ -1,14 +1,10 @@ use crate::ast_utils::{ - expand_ts_as_expr, get_jsx_attr, get_jsx_attr_value_as_string, is_jsx_elements_equal, - omit_jsx_attrs, + get_jsx_attr, get_jsx_attr_value_as_string, is_jsx_elements_equal, omit_jsx_attrs, }; use crate::options::LinguiOptions; -use crate::tokens::{CaseOrOffset, IcuChoice, MsgToken}; +use crate::tokens::{CaseOrOffset, MsgArg, MsgToken}; use std::collections::HashSet; -use swc_core::{ - common::{SyntaxContext, DUMMY_SP}, - ecma::ast::*, -}; +use swc_core::{common::DUMMY_SP, ecma::ast::*}; fn is_numeric(s: &str) -> bool { !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) @@ -87,7 +83,6 @@ pub struct MessageBuilder<'a> { components: Vec, values: Vec, - values_indexed: Vec, options: &'a LinguiOptions, elements_tracking: Vec<(String, JSXOpeningElement)>, @@ -101,7 +96,6 @@ impl<'a> MessageBuilder<'a> { components_stack: Vec::new(), components: Vec::new(), values: Vec::new(), - values_indexed: Vec::new(), options, elements_tracking: Vec::new(), element_index: 0, @@ -111,7 +105,7 @@ impl<'a> MessageBuilder<'a> { builder.into_args() } - pub fn into_args(mut self) -> MessageBuilderResult { + pub fn into_args(self) -> MessageBuilderResult { let message_str = self.message; let message = Box::new(Expr::Lit(Lit::Str(Str { @@ -120,8 +114,6 @@ impl<'a> MessageBuilder<'a> { raw: None, }))); - self.values.append(&mut self.values_indexed); - let values = if self.values.is_empty() { None } else { @@ -162,9 +154,8 @@ impl<'a> MessageBuilder<'a> { self.push_msg(&str); } - MsgToken::Expression(val) => { - let placeholder = self.push_exp(val); - self.push_msg(&format!("{{{placeholder}}}")); + MsgToken::Arg(arg) => { + self.push_arg(arg); } MsgToken::TagOpening(val) => { @@ -173,9 +164,6 @@ impl<'a> MessageBuilder<'a> { MsgToken::TagClosing => { self.push_tag_closing(); } - MsgToken::IcuChoice(icu) => { - self.push_icu(icu); - } } } } @@ -292,91 +280,37 @@ impl<'a> MessageBuilder<'a> { } } - fn push_exp(&mut self, mut exp: Box) -> String { - exp = expand_ts_as_expr(exp); + fn push_arg(&mut self, arg: MsgArg) { + let placeholder = arg.name.clone(); - match exp.as_ref() { - Expr::Ident(ident) => { - self.values.push(ValueWithPlaceholder { - placeholder: ident.sym.to_string().clone(), - value: exp.clone(), - }); + self.values.push(ValueWithPlaceholder { + placeholder: placeholder.clone(), + value: arg.value, + }); - ident.sym.to_string() - } - Expr::Object(object) => { - if let Some(PropOrSpread::Prop(prop)) = object.props.first() { - // {foo} - if let Some(short) = prop.as_shorthand() { - self.values_indexed.push(ValueWithPlaceholder { - placeholder: short.sym.to_string(), - value: Box::new(Expr::Ident(Ident { - span: DUMMY_SP, - sym: short.sym.clone(), - ctxt: SyntaxContext::empty(), - optional: false, - })), - }); - - return short.sym.to_string(); - } - // {foo: bar} - if let Prop::KeyValue(kv) = prop.as_ref() { - if let PropName::Ident(ident) = &kv.key { - self.values_indexed.push(ValueWithPlaceholder { - placeholder: ident.sym.to_string(), - value: kv.value.clone(), - }); - - return ident.sym.to_string(); + if let Some(format) = arg.format { + self.push_msg(&format!("{{{placeholder}, {format},")); + + if let Some(cases) = arg.cases { + for choice in cases { + match choice { + // produce offset:{number} + CaseOrOffset::Offset(val) => { + self.push_msg(&format!(" offset:{val}")); + } + CaseOrOffset::Case(choice) => { + let key = choice.key; + self.push_msg(&format!(" {key} {{")); + self.process_tokens(choice.tokens); + self.push_msg("}"); } } } - - // fallback for {...spread} or {} - let index = self.values_indexed.len().to_string(); - - self.values_indexed.push(ValueWithPlaceholder { - placeholder: index.clone(), - value: exp.clone(), - }); - - index } - _ => { - let index = self.values_indexed.len().to_string(); - self.values_indexed.push(ValueWithPlaceholder { - placeholder: index.clone(), - value: exp.clone(), - }); - - index - } - } - } - - 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},")); - - for choice in icu.cases { - match choice { - // produce offset:{number} - CaseOrOffset::Offset(val) => { - self.push_msg(&format!(" offset:{val}")); - } - CaseOrOffset::Case(choice) => { - let key = choice.key; - - self.push_msg(&format!(" {key} {{")); - self.process_tokens(choice.tokens); - self.push_msg("}"); - } - } + self.push_msg("}"); + } else { + self.push_msg(&format!("{{{placeholder}}}")); } - - self.push_msg("}"); } } diff --git a/crates/lingui_macro/src/js_macro_folder.rs b/crates/lingui_macro/src/js_macro_folder.rs index b760a26..bbe2437 100644 --- a/crates/lingui_macro/src/js_macro_folder.rs +++ b/crates/lingui_macro/src/js_macro_folder.rs @@ -126,9 +126,9 @@ where } // take {message: "", id: "", ...} object literal, process message and return updated props - fn update_msg_descriptor_props(&self, expr: Box, span: Span) -> Box { + fn update_msg_descriptor_props(&mut self, expr: Box, span: Span) -> Box { if let Expr::Object(obj) = *expr { - let defaults = self.ctx.get_comment_directive(span.lo); + let defaults = self.ctx.get_comment_directive(span.lo).cloned(); let id_prop = get_object_prop(&obj.props, "id"); let explicit_context_prop = get_object_prop(&obj.props, "context"); @@ -143,7 +143,7 @@ where if let Some(id_prop) = id_prop { if let Some(value) = get_expr_as_string(&id_prop.value) { let value = - build_prefixed_id(&self.ctx.options, &value, defaults).unwrap_or(value); + build_prefixed_id(&self.ctx.options, &value, defaults.as_ref()).unwrap_or(value); new_props.push(create_key_value_prop("id", value.into())); } else { new_props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue( @@ -160,7 +160,7 @@ where if id_prop.is_none() { let resolved_context = context_val .as_deref() - .or_else(|| defaults.and_then(|defaults| defaults.context.as_deref())) + .or_else(|| defaults.as_ref().and_then(|defaults| defaults.context.as_deref())) .unwrap_or_default(); new_props.push(create_key_value_prop( @@ -189,7 +189,7 @@ where context_prop.clone(), )))); } else if let Some(context) = - defaults.and_then(|defaults| defaults.context.as_deref()) + defaults.as_ref().and_then(|defaults| defaults.context.as_deref()) { new_props.push(create_key_value_prop("context", context.into())); } @@ -201,7 +201,7 @@ where comment_prop.clone(), )))); } else if let Some(value) = - defaults.and_then(|defaults| defaults.comment.as_deref()) + defaults.as_ref().and_then(|defaults| defaults.comment.as_deref()) { new_props.push(create_key_value_prop("comment", value.into())); } @@ -231,11 +231,15 @@ where let (is_t, callee) = self.ctx.is_lingui_t_call_expr(&tagged_tpl.tag); if is_t { + self.ctx.reset_expression_index(); + let tokens = self.ctx.tokenize_tpl(&tagged_tpl.tpl); + let tpl_span = tagged_tpl.tpl.span(); + let expr_span = expr.span(); return Expr::Call(self.create_i18n_fn_call_from_tokens( callee, - self.ctx.tokenize_tpl(&tagged_tpl.tpl), - tagged_tpl.tpl.span(), - expr.span(), + tokens, + tpl_span, + expr_span, )); } } @@ -245,6 +249,7 @@ where let span = tagged_tpl.span(); if let Expr::Ident(ident) = tagged_tpl.tag.as_ref() { if self.ctx.is_define_message_ident(ident) { + self.ctx.reset_expression_index(); let tokens = self.ctx.tokenize_tpl(&tagged_tpl.tpl); let defaults = self.ctx.get_comment_directive(span.lo).cloned(); return self.create_message_descriptor_from_tokens( @@ -261,6 +266,7 @@ where if match_callee_name(call, |n| self.ctx.is_define_message_ident(n)).is_some() && call.args.len() == 1 { + self.ctx.reset_expression_index(); let descriptor = self.update_msg_descriptor_props( call.args.clone().into_iter().next().unwrap().expr, call.span(), @@ -280,6 +286,7 @@ where let span = expr.span(); if is_t && expr.args.len() == 1 { + self.ctx.reset_expression_index(); let msg_dscrpt_expr = expr.args.into_iter().next().unwrap().expr; let msg_dscrpt_expr_span = msg_dscrpt_expr.span(); @@ -291,6 +298,7 @@ where } // plural / selectOrdinal / select + self.ctx.reset_expression_index(); if let Some(tokens) = self.ctx.try_tokenize_call_expr_as_choice_cmp(&expr) { let msg_dscrptr_span = expr.args.first().map(|arg| arg.span()).unwrap_or(DUMMY_SP); diff --git a/crates/lingui_macro/src/jsx_visitor.rs b/crates/lingui_macro/src/jsx_visitor.rs index f1727c3..3e9720b 100644 --- a/crates/lingui_macro/src/jsx_visitor.rs +++ b/crates/lingui_macro/src/jsx_visitor.rs @@ -1,24 +1,46 @@ -use crate::ast_utils::{get_jsx_attr, get_jsx_attr_value_as_string}; +use crate::ast_utils::get_jsx_attr_value_as_string; use crate::macro_utils::MacroCtx; -use crate::tokens::{CaseOrOffset, ChoiceCase, IcuChoice, MsgToken, TagOpening}; -use swc_core::common::DUMMY_SP; +use crate::tokens::{CaseOrOffset, ChoiceCase, MsgArg, MsgToken, TagOpening}; use swc_core::ecma::ast::*; use swc_core::ecma::atoms::Atom; -use swc_core::ecma::visit::{Visit, VisitWith}; use swc_core::plugin::errors::HANDLER; pub struct TransJSXVisitor<'a> { pub tokens: Vec, - ctx: &'a MacroCtx, + ctx: &'a mut MacroCtx, } impl<'a> TransJSXVisitor<'a> { - pub fn new(ctx: &'a MacroCtx) -> TransJSXVisitor<'a> { + pub fn new(ctx: &'a mut MacroCtx) -> TransJSXVisitor<'a> { TransJSXVisitor { tokens: Vec::new(), ctx, } } + + pub fn visit_jsx_children(&mut self, children: &Vec) { + for child in children { + match child { + JSXElementChild::JSXText(el) => self.visit_jsx_text(el), + JSXElementChild::JSXExprContainer(cont) => self.visit_jsx_expr_container(cont), + JSXElementChild::JSXElement(el) => { + self.visit_jsx_element(el); + } + JSXElementChild::JSXFragment(frag) => { + self.visit_jsx_children(&frag.children); + } + _ => {} + } + } + } + + pub fn visit_jsx_element(&mut self, el: &JSXElement) { + self.visit_jsx_opening_element(&el.opening); + if !el.opening.self_closing { + self.visit_jsx_children(&el.children); + self.visit_jsx_closing_element(); + } + } } // taken from babel repo -> packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts @@ -112,18 +134,35 @@ fn is_allowed_plural_option(key: &str) -> Option { impl TransJSXVisitor<'_> { // fn visit_icu_macro(&mut self, el: &JSXOpeningElement, icu_format: &str) { - let mut arg = MsgArg { - name: String::new(), - value: Box::new(Expr::Lit(Lit::Null(Null { - span: swc_core::common::DUMMY_SP, - }))), - format: Some(icu_format.into()), - cases: Some(Vec::new()), - }; + let mut cases: Vec = Vec::new(); + let mut value_arg: Option = None; for attr in &el.attrs { if let JSXAttrOrSpread::JSXAttr(attr) = attr { @@ -155,14 +149,13 @@ impl TransJSXVisitor<'_> { }) = attr_value { let token_arg = self.ctx.tokenize_expr_to_arg(exp.clone()); - arg.name = token_arg.name; - arg.value = token_arg.value; + value_arg = Some(token_arg) } } else if &ident.sym == "offset" && icu_format != "select" { if let Some(value) = get_jsx_attr_value_as_string(attr_value) { - if let Some(ref mut cases) = arg.cases { - cases.push(CaseOrOffset::Offset(value.to_string())); - } + cases.push(CaseOrOffset::Offset(value.to_string())) + } else { + // todo: panic offset might be only a number, other forms are not supported } } else if let Some(key) = is_allowed_plural_option(&ident.sym) { let mut tokens: Vec = Vec::new(); @@ -208,9 +201,7 @@ impl TransJSXVisitor<'_> { } } - if let Some(ref mut cases) = arg.cases { - cases.push(CaseOrOffset::Case(ChoiceCase { tokens, key })); - } + cases.push(CaseOrOffset::Case(ChoiceCase { tokens, key })); } } } @@ -223,7 +214,20 @@ impl TransJSXVisitor<'_> { } } - self.tokens.push(MsgToken::Arg(arg)); + if let Some(arg) = value_arg { + self.tokens.push(MsgToken::Arg(MsgArg { + name: arg.name, + value: arg.value, + format: Some(icu_format.into()), + cases: Some(cases), + })); + } else { + HANDLER.with(|h| { + h.struct_span_warn(el.span, "Incorrect Macro Usage") + .note("The macro element should has a `value` property") + .emit() + }); + } } } @@ -274,6 +278,7 @@ impl TransJSXVisitor<'_> { .push(MsgToken::String(str.value.to_string_lossy().into_owned())); } + // support calls to js macro inside JSX, but not to t`` Expr::Call(call) => { if let Some(tokens) = self.ctx.try_tokenize_call_expr_as_choice_cmp(call) { self.tokens.extend(tokens); diff --git a/crates/lingui_macro/tests/js_t.rs b/crates/lingui_macro/tests/js_t.rs index eff32fd..4fc3623 100644 --- a/crates/lingui_macro/tests/js_t.rs +++ b/crates/lingui_macro/tests/js_t.rs @@ -49,7 +49,6 @@ to!( "# ); - to!( js_custom_i18n_passed, r#" diff --git a/crates/lingui_macro/tests/jsx.rs b/crates/lingui_macro/tests/jsx.rs index aef16e1..2282ed7 100644 --- a/crates/lingui_macro/tests/jsx.rs +++ b/crates/lingui_macro/tests/jsx.rs @@ -108,7 +108,6 @@ to!( "# ); - to!( jsx_template_literal_in_children, r#" diff --git a/crates/lingui_macro/tests/jsx_icu.rs b/crates/lingui_macro/tests/jsx_icu.rs index 640c671..b4a8083 100644 --- a/crates/lingui_macro/tests/jsx_icu.rs +++ b/crates/lingui_macro/tests/jsx_icu.rs @@ -266,7 +266,6 @@ import { Trans } from "@lingui/react/macro"; "# ); - to!( with_value_index_in_source_order, r#" From 6be9afbd1f25015318c455c83eaec3a2a207c27c Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:54:04 +0200 Subject: [PATCH 4/5] refactor --- crates/lingui_macro/src/js_macro_folder.rs | 21 +- crates/lingui_macro/src/jsx_visitor.rs | 39 ++- crates/lingui_macro/src/lib.rs | 8 +- crates/lingui_macro/src/macro_utils.rs | 383 +++++++++++---------- 4 files changed, 241 insertions(+), 210 deletions(-) diff --git a/crates/lingui_macro/src/js_macro_folder.rs b/crates/lingui_macro/src/js_macro_folder.rs index 2d394ad..e65124b 100644 --- a/crates/lingui_macro/src/js_macro_folder.rs +++ b/crates/lingui_macro/src/js_macro_folder.rs @@ -19,7 +19,7 @@ pub struct JsMacroFolder<'a, C> where C: Comments, { - pub ctx: &'a mut MacroCtx, + pub ctx: &'a mut TransformCtx, pub comments: &'a Option, } @@ -27,7 +27,7 @@ impl<'a, C> JsMacroFolder<'a, C> where C: Comments, { - pub fn new(ctx: &'a mut MacroCtx, comments: &'a Option) -> JsMacroFolder<'a, C> { + pub fn new(ctx: &'a mut TransformCtx, comments: &'a Option) -> JsMacroFolder<'a, C> { JsMacroFolder { ctx, comments } } @@ -153,7 +153,8 @@ where } if let Some(prop) = message_prop { - let tokens = self.ctx.try_tokenize_expr(&prop.value).unwrap_or_default(); + let mut macro_ctx = MacroCtx::new(self.ctx); + let tokens = try_tokenize_expr(&mut macro_ctx, &prop.value).unwrap_or_default(); let parsed = MessageBuilder::parse(tokens, &self.ctx.options); @@ -237,8 +238,8 @@ where let (is_t, callee) = self.ctx.is_lingui_t_call_expr(&tagged_tpl.tag); if is_t { - self.ctx.reset_expression_index(); - let tokens = self.ctx.tokenize_tpl(&tagged_tpl.tpl); + let mut macro_ctx = MacroCtx::new(self.ctx); + let tokens = tokenize_tpl(&mut macro_ctx, &tagged_tpl.tpl); let tpl_span = tagged_tpl.tpl.span(); let expr_span = expr.span(); return Expr::Call( @@ -252,8 +253,8 @@ where let span = tagged_tpl.span(); if let Expr::Ident(ident) = tagged_tpl.tag.as_ref() { if self.ctx.is_define_message_ident(ident) { - self.ctx.reset_expression_index(); - let tokens = self.ctx.tokenize_tpl(&tagged_tpl.tpl); + let mut macro_ctx = MacroCtx::new(self.ctx); + let tokens = tokenize_tpl(&mut macro_ctx, &tagged_tpl.tpl); let defaults = self.ctx.get_comment_directive(span.lo).cloned(); return self.create_message_descriptor_from_tokens( tokens, @@ -269,7 +270,6 @@ where if match_callee_name(call, |n| self.ctx.is_define_message_ident(n)).is_some() && call.args.len() == 1 { - self.ctx.reset_expression_index(); let descriptor = self.update_msg_descriptor_props( call.args.clone().into_iter().next().unwrap().expr, call.span(), @@ -289,7 +289,6 @@ where let span = expr.span(); if is_t && expr.args.len() == 1 { - self.ctx.reset_expression_index(); let msg_dscrpt_expr = expr.args.into_iter().next().unwrap().expr; let msg_dscrpt_expr_span = msg_dscrpt_expr.span(); @@ -301,8 +300,8 @@ where } // plural / selectOrdinal / select - self.ctx.reset_expression_index(); - if let Some(tokens) = self.ctx.try_tokenize_call_expr_as_choice_cmp(&expr) { + let mut macro_ctx = MacroCtx::new(self.ctx); + if let Some(tokens) = try_tokenize_call_expr_as_choice_cmp(&mut macro_ctx, &expr) { let msg_dscrptr_span = expr.args.first().map(|arg| arg.span()).unwrap_or(DUMMY_SP); return self.create_i18n_fn_call_from_tokens( diff --git a/crates/lingui_macro/src/jsx_visitor.rs b/crates/lingui_macro/src/jsx_visitor.rs index d135993..58bc553 100644 --- a/crates/lingui_macro/src/jsx_visitor.rs +++ b/crates/lingui_macro/src/jsx_visitor.rs @@ -1,5 +1,7 @@ use crate::ast_utils::get_jsx_attr_value_as_string; -use crate::macro_utils::MacroCtx; +use crate::macro_utils::{ + tokenize_expr_to_arg, tokenize_tpl, try_tokenize_call_expr_as_choice_cmp, MacroCtx, +}; use crate::tokens::{CaseOrOffset, ChoiceCase, MsgArg, MsgToken, TagOpening}; use swc_core::ecma::ast::*; use swc_core::ecma::atoms::Atom; @@ -7,11 +9,11 @@ use swc_core::plugin::errors::HANDLER; pub struct TransJSXVisitor<'a> { pub tokens: Vec, - ctx: &'a mut MacroCtx, + pub ctx: MacroCtx<'a>, } impl<'a> TransJSXVisitor<'a> { - pub fn new(ctx: &'a mut MacroCtx) -> TransJSXVisitor<'a> { + pub fn new(ctx: MacroCtx<'a>) -> TransJSXVisitor<'a> { TransJSXVisitor { tokens: Vec::new(), ctx, @@ -148,7 +150,7 @@ impl TransJSXVisitor<'_> { .. }) = attr_value { - let token_arg = self.ctx.tokenize_expr_to_arg(exp.clone()); + let token_arg = tokenize_expr_to_arg(&mut self.ctx, exp.clone()); value_arg = Some(token_arg) } } else if &ident.sym == "offset" && icu_format != "select" { @@ -178,19 +180,20 @@ impl TransJSXVisitor<'_> { )), // some={`# books ${name}`} Expr::Tpl(tpl) => { - tokens.extend(self.ctx.tokenize_tpl(tpl)); + tokens.extend(tokenize_tpl(&mut self.ctx, tpl)); } - // some={``} - Expr::JSXElement(exp) => { - let mut visitor = TransJSXVisitor::new(self.ctx); - visitor.visit_jsx_element(exp); + // some={} + Expr::JSXElement(el) => { + let mut visitor = + TransJSXVisitor::new(self.ctx.reborrow()); + visitor.visit_jsx_element(el); - tokens.extend(visitor.tokens) + tokens.extend(visitor.tokens); } _ => { let token_arg = - self.ctx.tokenize_expr_to_arg(exp.clone()); + tokenize_expr_to_arg(&mut self.ctx, exp.clone()); tokens.push(MsgToken::Arg(token_arg)); } } @@ -234,13 +237,14 @@ impl TransJSXVisitor<'_> { impl TransJSXVisitor<'_> { pub fn visit_jsx_opening_element(&mut self, el: &JSXOpeningElement) { if let JSXElementName::Ident(ident) = &el.name { - if self.ctx.is_lingui_ident("Trans", ident) { + if self.ctx.transform.is_lingui_ident("Trans", ident) { return; } - if self.ctx.is_lingui_jsx_choice_cmp(ident) { + if self.ctx.transform.is_lingui_jsx_choice_cmp(ident) { let icu_method = self .ctx + .transform .get_ident_export_name(ident) .unwrap() .to_lowercase(); @@ -280,10 +284,11 @@ impl TransJSXVisitor<'_> { // support calls to js macro inside JSX, but not to t`` Expr::Call(call) => { - if let Some(tokens) = self.ctx.try_tokenize_call_expr_as_choice_cmp(call) { + if let Some(tokens) = try_tokenize_call_expr_as_choice_cmp(&mut self.ctx, call) + { self.tokens.extend(tokens); } else { - let arg = self.ctx.tokenize_expr_to_arg(exp.clone()); + let arg = tokenize_expr_to_arg(&mut self.ctx, exp.clone()); self.tokens.push(MsgToken::Arg(arg)); } } @@ -293,10 +298,10 @@ impl TransJSXVisitor<'_> { } Expr::Tpl(tpl) => { - self.tokens.extend(self.ctx.tokenize_tpl(tpl)); + self.tokens.extend(tokenize_tpl(&mut self.ctx, tpl)); } _ => { - let arg = self.ctx.tokenize_expr_to_arg(exp.clone()); + let arg = tokenize_expr_to_arg(&mut self.ctx, exp.clone()); self.tokens.push(MsgToken::Arg(arg)); } } diff --git a/crates/lingui_macro/src/lib.rs b/crates/lingui_macro/src/lib.rs index 826c27e..b8e88f2 100644 --- a/crates/lingui_macro/src/lib.rs +++ b/crates/lingui_macro/src/lib.rs @@ -57,7 +57,7 @@ where C: Comments + Clone, { has_lingui_macro_imports: bool, - ctx: MacroCtx, + ctx: TransformCtx, comments: Option, source_map: Lrc, } @@ -73,7 +73,7 @@ where ) -> LinguiMacroFolder { LinguiMacroFolder { has_lingui_macro_imports: false, - ctx: MacroCtx::new(options), + ctx: TransformCtx::new(options), comments, source_map, } @@ -103,8 +103,8 @@ where // Message // fn transform_jsx_macro(&mut self, el: JSXElement, is_trans_el: bool) -> JSXElement { - self.ctx.reset_expression_index(); - let mut trans_visitor = TransJSXVisitor::new(&mut self.ctx); + let macro_ctx = MacroCtx::new(&mut self.ctx); + let mut trans_visitor = TransJSXVisitor::new(macro_ctx); let message_dscrptr_span: Span; diff --git a/crates/lingui_macro/src/macro_utils.rs b/crates/lingui_macro/src/macro_utils.rs index 53eb579..483123f 100644 --- a/crates/lingui_macro/src/macro_utils.rs +++ b/crates/lingui_macro/src/macro_utils.rs @@ -8,7 +8,7 @@ use swc_core::ecma::utils::quote_ident; use swc_core::ecma::{ast::*, atoms::Atom}; use swc_core::plugin::errors::HANDLER; -pub fn expression_to_name(expr: &Expr, get_index: &mut impl FnMut() -> usize) -> String { +fn expression_to_name(expr: &Expr, get_index: &mut impl FnMut() -> usize) -> String { let expr = unwrap_ts_as_expr(expr); match expr { @@ -32,7 +32,7 @@ pub fn expression_to_name(expr: &Expr, get_index: &mut impl FnMut() -> usize) -> } } -pub fn expression_to_value(expr: Box) -> Box { +fn expression_to_value(expr: Box) -> Box { let unwrapped = unwrap_ts_as_expr(&expr); match unwrapped { @@ -78,7 +78,7 @@ fn unwrap_ts_as_expr(expr: &Expr) -> &Expr { current } -pub fn tokenize_expression(expr: Box, get_index: &mut impl FnMut() -> usize) -> MsgArg { +fn tokenize_expression(expr: Box, get_index: &mut impl FnMut() -> usize) -> MsgArg { let name = expression_to_name(&expr, get_index); let value = expression_to_value(expr); MsgArg { @@ -89,6 +89,181 @@ pub fn tokenize_expression(expr: Box, get_index: &mut impl FnMut() -> usiz } } +/// Take KeyValueProp and return Key as string if parsable +/// If key is numeric, return an exact match syntax `={number}` +fn get_js_choice_case_key(prop: &KeyValueProp) -> Option { + match &prop.key { + // {one: ""} + PropName::Ident(IdentName { sym, .. }) => Some(sym.clone()), + // {"one": ""} + PropName::Str(Str { value, .. }) => Some(value.to_string_lossy().into_owned().into()), + // {0: ""} -> `={number}` + PropName::Num(Number { value, .. }) => Some(format!("={value}").into()), + _ => None, + } +} + +fn unwrap_ph_call(ctx: &MacroCtx, expr: Box) -> Box { + if let Expr::Call(call) = expr.as_ref() { + if call.callee.as_expr().is_some_and(|c| { + c.as_ident() + .is_some_and(|i| ctx.transform.is_lingui_placeholder_expr(i)) + }) { + if let Some(first) = call.args.first() { + if !first.expr.is_object() { + HANDLER.with(|h| { + h.struct_span_err( + first.expr.span(), + "Incorrect usage of `ph` macro. First argument should be an object expression like `ph({name: value})`.", + ) + .emit(); + }); + return expr; + } + return first.expr.clone(); + } + } + } + expr +} + +pub fn tokenize_expr_to_arg(ctx: &mut MacroCtx, expr: Box) -> MsgArg { + let expr = unwrap_ph_call(ctx, expr); + let idx = &mut ctx.expression_index; + let mut get_index = || { + let i = *idx; + *idx += 1; + i + }; + tokenize_expression(expr, &mut get_index) +} + +/// Receive TemplateLiteral with variables and return MsgTokens +pub fn tokenize_tpl(ctx: &mut MacroCtx, tpl: &Tpl) -> Vec { + let mut tokens: Vec = Vec::with_capacity(tpl.quasis.len()); + + for (i, tpl_element) in tpl.quasis.iter().enumerate() { + let value = tpl_element + .cooked + .as_ref() + .map(|c| c.to_string_lossy().into_owned()) + .unwrap_or_else(|| tpl_element.raw.to_string()); + tokens.push(MsgToken::String(value)); + + if let Some(exp) = tpl.exprs.get(i) { + if let Expr::Call(call) = exp.as_ref() { + if let Some(call_tokens) = try_tokenize_call_expr_as_choice_cmp(ctx, call) { + tokens.extend(call_tokens); + continue; + } + } + + let arg = tokenize_expr_to_arg(ctx, exp.clone()); + tokens.push(MsgToken::Arg(arg)); + } + } + + tokens +} + +/// Try to tokenize call expression as ICU Choice macro +/// Return None if this call is not related to macros or is not parsable +pub fn try_tokenize_call_expr_as_choice_cmp( + ctx: &mut MacroCtx, + expr: &CallExpr, +) -> Option> { + if let Some(ident) = match_callee_name(expr, |name| ctx.transform.is_lingui_fn_choice_cmp(name)) + { + if expr.args.len() != 2 { + // malformed plural call, exit + return None; + } + + // ICU value + let arg = expr.args.first().unwrap(); + let icu_value = arg.expr.clone(); + + // ICU Choice Cases + let arg = expr.args.get(1).unwrap(); + if let Expr::Object(object) = &arg.expr.as_ref() { + let format = ctx + .transform + .get_ident_export_name(ident) + .unwrap() + .to_lowercase(); + let mut token_arg = tokenize_expr_to_arg(ctx, icu_value); + let cases = get_choice_cases_from_obj(ctx, &object.props, &format); + token_arg.format = Some(format.into()); + token_arg.cases = Some(cases); + + return Some(vec![MsgToken::Arg(token_arg)]); + } else { + // todo passed not an ObjectLiteral, + // we should panic here or just skip this call + } + } + + None +} + +pub fn try_tokenize_expr(ctx: &mut MacroCtx, expr: &Expr) -> Option> { + match expr { + // String Literal: "has # friend" + Expr::Lit(Lit::Str(str)) => Some(vec![MsgToken::String( + str.value.to_string_lossy().into_owned(), + )]), + // Template Literal: `${name} has # friend` + Expr::Tpl(tpl) => Some(tokenize_tpl(ctx, tpl)), + + // ParenthesisExpression: ("has # friend") + Expr::Paren(ParenExpr { expr, .. }) => try_tokenize_expr(ctx, expr), + + // Call Expression: {one: plural(numArticles, {...})} + Expr::Call(expr) => try_tokenize_call_expr_as_choice_cmp(ctx, expr), + _ => None, + } +} + +/// receive ObjectLiteral {few: "..", many: "..", other: ".."} and create tokens +/// If messages passed as TemplateLiterals with variables, it extracts variables +pub fn get_choice_cases_from_obj( + ctx: &mut MacroCtx, + props: &Vec, + icu_format: &str, +) -> Vec { + // todo: there might be more props then real choices. Id for example + let mut choices: Vec = Vec::with_capacity(props.len()); + + for prop_or_spread in props { + if let PropOrSpread::Prop(prop) = prop_or_spread { + if let Prop::KeyValue(prop) = prop.as_ref() { + if let Some(key) = get_js_choice_case_key(prop) { + if &key == "offset" && icu_format != "select" { + if let Expr::Lit(Lit::Num(Number { value, .. })) = prop.value.as_ref() { + choices.push(CaseOrOffset::Offset(value.to_string())) + } else { + // todo: panic offset might be only a number, other forms is not supported + } + } else { + let tokens = try_tokenize_expr(ctx, &prop.value).unwrap_or_else(|| { + let arg = tokenize_expr_to_arg(ctx, prop.value.clone()); + vec![MsgToken::Arg(arg)] + }); + + choices.push(CaseOrOffset::Case(ChoiceCase { tokens, key })); + } + } + } else { + // todo: panic here we could not parse anything else then KeyValue pair + } + } else { + // todo: panic here, we could not parse spread + } + } + + choices +} + const LINGUI_T: &str = "t"; pub fn build_prefixed_id( @@ -107,8 +282,10 @@ pub fn build_prefixed_id( Some(format!("{id_prefix}{id}")) } +/// Global context for the entire plugin transform run on a file. +/// Tracks macro imports, options, directives, and runtime identifiers. #[derive(Default, Clone)] -pub struct MacroCtx { +pub struct TransformCtx { // export name -> local name symbol_to_id_map: HashMap>, // local name -> export name @@ -121,8 +298,6 @@ pub struct MacroCtx { pub options: LinguiOptions, pub directives: LinguiCommentDirectives, pub runtime_idents: RuntimeIdents, - - expression_index: usize, } #[derive(Clone)] @@ -142,18 +317,14 @@ impl Default for RuntimeIdents { } } -impl MacroCtx { - pub fn new(options: LinguiOptions) -> MacroCtx { - MacroCtx { +impl TransformCtx { + pub fn new(options: LinguiOptions) -> TransformCtx { + TransformCtx { options, ..Default::default() } } - pub fn reset_expression_index(&mut self) { - self.expression_index = 0; - } - pub fn set_directives(&mut self, directives: LinguiCommentDirectives) { self.directives = directives; } @@ -163,8 +334,7 @@ impl MacroCtx { } /// is given ident exported from @lingui/macro? and one of choice functions? - fn is_lingui_fn_choice_cmp(&self, ident: &Ident) -> bool { - // self.symbol_to_id_map. + pub fn is_lingui_fn_choice_cmp(&self, ident: &Ident) -> bool { self.is_lingui_ident("plural", ident) || self.is_lingui_ident("select", ident) || self.is_lingui_ident("selectOrdinal", ident) @@ -211,6 +381,7 @@ impl MacroCtx { self.id_to_symbol_map.insert(id.clone(), symbol.clone()); } + pub fn register_macro_import(&mut self, imp: &ImportDecl) { for spec in &imp.specifiers { if let ImportSpecifier::Named(spec) = spec { @@ -242,174 +413,30 @@ impl MacroCtx { _ => (false, None), } } +} - /// Receive TemplateLiteral with variables and return MsgTokens - pub fn tokenize_tpl(&mut self, tpl: &Tpl) -> Vec { - let mut tokens: Vec = Vec::with_capacity(tpl.quasis.len()); - - for (i, tpl_element) in tpl.quasis.iter().enumerate() { - let value = tpl_element - .cooked - .as_ref() - .map(|c| c.to_string_lossy().into_owned()) - .unwrap_or_else(|| tpl_element.raw.to_string()); - tokens.push(MsgToken::String(value)); - - if let Some(exp) = tpl.exprs.get(i) { - if let Expr::Call(call) = exp.as_ref() { - if let Some(call_tokens) = self.try_tokenize_call_expr_as_choice_cmp(call) { - tokens.extend(call_tokens); - continue; - } - } - - let arg = self.tokenize_expr_to_arg(exp.clone()); - tokens.push(MsgToken::Arg(arg)); - } - } - - tokens - } - - /// Try to tokenize call expression as ICU Choice macro - /// Return None if this call is not related to macros or is not parsable - pub fn try_tokenize_call_expr_as_choice_cmp( - &mut self, - expr: &CallExpr, - ) -> Option> { - if let Some(ident) = match_callee_name(expr, |name| self.is_lingui_fn_choice_cmp(name)) { - if expr.args.len() != 2 { - // malformed plural call, exit - return None; - } - - // ICU value - let arg = expr.args.first().unwrap(); - let icu_value = arg.expr.clone(); - - // ICU Choice Cases - let arg = expr.args.get(1).unwrap(); - if let Expr::Object(object) = &arg.expr.as_ref() { - let format = self.get_ident_export_name(ident).unwrap().to_lowercase(); - let mut token_arg = self.tokenize_expr_to_arg(icu_value); - let cases = self.get_choice_cases_from_obj(&object.props, &format); - token_arg.format = Some(format.into()); - token_arg.cases = Some(cases); - - return Some(vec![MsgToken::Arg(token_arg)]); - } else { - // todo passed not an ObjectLiteral, - // we should panic here or just skip this call - } - } - - None - } - - pub fn try_tokenize_expr(&mut self, expr: &Expr) -> Option> { - match expr { - // String Literal: "has # friend" - Expr::Lit(Lit::Str(str)) => Some(vec![MsgToken::String( - str.value.to_string_lossy().into_owned(), - )]), - // Template Literal: `${name} has # friend` - Expr::Tpl(tpl) => Some(self.tokenize_tpl(tpl)), - - // ParenthesisExpression: ("has # friend") - Expr::Paren(ParenExpr { expr, .. }) => self.try_tokenize_expr(expr), - - // Call Expression: {one: plural(numArticles, {...})} - Expr::Call(expr) => self.try_tokenize_call_expr_as_choice_cmp(expr), - _ => None, - } - } - - /// Take KeyValueProp and return Key as string if parsable - /// If key is numeric, return an exact match syntax `={number}` - pub fn get_js_choice_case_key(&self, prop: &KeyValueProp) -> Option { - match &prop.key { - // {one: ""} - PropName::Ident(IdentName { sym, .. }) => Some(sym.clone()), - // {"one": ""} - PropName::Str(Str { value, .. }) => Some(value.to_string_lossy().into_owned().into()), - // {0: ""} -> `={number}` - PropName::Num(Number { value, .. }) => Some(format!("={value}").into()), - _ => None, - } - } - - /// receive ObjectLiteral {few: "..", many: "..", other: ".."} and create tokens - /// If messages passed as TemplateLiterals with variables, it extracts variables - pub fn get_choice_cases_from_obj( - &mut self, - props: &Vec, - icu_format: &str, - ) -> Vec { - // todo: there might be more props then real choices. Id for example - let mut choices: Vec = Vec::with_capacity(props.len()); - - for prop_or_spread in props { - if let PropOrSpread::Prop(prop) = prop_or_spread { - if let Prop::KeyValue(prop) = prop.as_ref() { - if let Some(key) = self.get_js_choice_case_key(prop) { - if &key == "offset" && icu_format != "select" { - if let Expr::Lit(Lit::Num(Number { value, .. })) = prop.value.as_ref() { - choices.push(CaseOrOffset::Offset(value.to_string())) - } else { - // todo: panic offset might be only a number, other forms is not supported - } - } else { - let tokens = self.try_tokenize_expr(&prop.value).unwrap_or_else(|| { - let arg = self.tokenize_expr_to_arg(prop.value.clone()); - vec![MsgToken::Arg(arg)] - }); +/// Local context for a single macro invocation. +/// Owns a fresh expression index counter and borrows the global TransformCtx. +pub struct MacroCtx<'a> { + pub transform: &'a mut TransformCtx, + expression_index: usize, +} - choices.push(CaseOrOffset::Case(ChoiceCase { tokens, key })); - } - } - } else { - // todo: panic here we could not parse anything else then KeyValue pair - } - } else { - // todo: panic here, we could not parse spread - } +impl<'a> MacroCtx<'a> { + pub fn new(transform: &'a mut TransformCtx) -> MacroCtx<'a> { + MacroCtx { + transform, + expression_index: 0, } - - choices - } - - pub fn tokenize_expr_to_arg(&mut self, expr: Box) -> MsgArg { - let expr = self.unwrap_ph_call(expr); - let idx = &mut self.expression_index; - let mut get_index = || { - let i = *idx; - *idx += 1; - i - }; - tokenize_expression(expr, &mut get_index) } - fn unwrap_ph_call(&self, expr: Box) -> Box { - if let Expr::Call(call) = expr.as_ref() { - if call.callee.as_expr().is_some_and(|c| { - c.as_ident() - .is_some_and(|i| self.is_lingui_placeholder_expr(i)) - }) { - if let Some(first) = call.args.first() { - if !first.expr.is_object() { - HANDLER.with(|h| { - h.struct_span_err( - first.expr.span(), - "Incorrect usage of `ph` macro. First argument should be an object expression like `ph({name: value})`.", - ) - .emit(); - }); - return expr; - } - return first.expr.clone(); - } - } + /// Re-borrow this context with a shorter lifetime, sharing the same + /// counter and TransformCtx. Use when passing to a child visitor + /// that continues the same macro invocation. + pub fn reborrow(&mut self) -> MacroCtx<'_> { + MacroCtx { + transform: self.transform, + expression_index: self.expression_index, } - expr } } From 7ff152f659c237fc786eb0c92409e4547842e0dd Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:18:52 +0200 Subject: [PATCH 5/5] snapshots --- crates/lingui_macro/tests/labeled_expressions.rs | 12 ++++++++++++ ...expressions__js_choice_labels_in_tpl_literal.snap | 2 +- ...pressions__js_explicit_labels_in_tpl_literal.snap | 2 +- ...ssions__js_labeled_expression_multiple_props.snap | 2 +- ...ons__js_ph_labeled_expression_multiple_props.snap | 2 +- ...led_expressions__js_ph_labels_in_tpl_literal.snap | 2 +- ...beled_expressions__js_ph_with_non_object_arg.snap | 2 +- .../labeled_expressions__jsx_explicit_labels.snap | 2 +- ...sions__jsx_explicit_labels_with_as_statement.snap | 2 +- ...ns__jsx_icu_with_labeled_expression_as_value.snap | 2 +- ...icu_with_labeled_expression_as_value_with_ph.snap | 2 +- ...sions__jsx_labeled_expression_multiple_props.snap | 2 +- .../labeled_expressions__jsx_nested_labels.snap | 2 +- ...xpressions__jsx_ph_label_with_nested_plural.snap} | 2 +- ...ns__jsx_ph_labeled_expression_multiple_props.snap | 2 +- .../labeled_expressions__jsx_ph_labels.snap | 2 +- ...eled_expressions__jsx_ph_with_non_object_arg.snap | 2 +- 17 files changed, 28 insertions(+), 16 deletions(-) rename crates/lingui_macro/tests/snapshots/{jsx__jsx_ph_label_with_nested_plural.snap => labeled_expressions__jsx_ph_label_with_nested_plural.snap} (90%) diff --git a/crates/lingui_macro/tests/labeled_expressions.rs b/crates/lingui_macro/tests/labeled_expressions.rs index 18c91fc..811756a 100644 --- a/crates/lingui_macro/tests/labeled_expressions.rs +++ b/crates/lingui_macro/tests/labeled_expressions.rs @@ -115,6 +115,18 @@ import { Plural } from '@lingui/react/macro'; "# ); +to!( + jsx_ph_label_with_nested_plural, + r##" + import { Trans, Plural, ph } from "@lingui/react/macro"; + + const zones = [1, 2] + const x = + {ph({ available: 1 })} of available + + "## +); + // --- Error cases --- to_panic!( diff --git a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_choice_labels_in_tpl_literal.snap b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_choice_labels_in_tpl_literal.snap index 3cc3b85..bd6a837 100644 --- a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_choice_labels_in_tpl_literal.snap +++ b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_choice_labels_in_tpl_literal.snap @@ -1,5 +1,5 @@ --- -source: crates/lingui_macro/tests/js_t.rs +source: crates/lingui_macro/tests/labeled_expressions.rs --- import { t, ph, plural, select, selectOrdinal } from "@lingui/core/macro"; diff --git a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_explicit_labels_in_tpl_literal.snap b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_explicit_labels_in_tpl_literal.snap index f6296d6..2c71cb7 100644 --- a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_explicit_labels_in_tpl_literal.snap +++ b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_explicit_labels_in_tpl_literal.snap @@ -1,5 +1,5 @@ --- -source: crates/lingui_macro/tests/js_t.rs +source: crates/lingui_macro/tests/labeled_expressions.rs --- import { t } from "@lingui/core/macro"; diff --git a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_labeled_expression_multiple_props.snap b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_labeled_expression_multiple_props.snap index 9dd920e..39c3f4c 100644 --- a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_labeled_expression_multiple_props.snap +++ b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_labeled_expression_multiple_props.snap @@ -1,5 +1,5 @@ --- -source: crates/lingui_macro/tests/js_t.rs +source: crates/lingui_macro/tests/labeled_expressions.rs info: {} --- import { t } from "@lingui/core/macro"; diff --git a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_labeled_expression_multiple_props.snap b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_labeled_expression_multiple_props.snap index 71ad0e5..daff45f 100644 --- a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_labeled_expression_multiple_props.snap +++ b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_labeled_expression_multiple_props.snap @@ -1,5 +1,5 @@ --- -source: crates/lingui_macro/tests/js_t.rs +source: crates/lingui_macro/tests/labeled_expressions.rs info: {} --- import { t, ph } from "@lingui/core/macro"; diff --git a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_labels_in_tpl_literal.snap b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_labels_in_tpl_literal.snap index 1b3b131..322471a 100644 --- a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_labels_in_tpl_literal.snap +++ b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_labels_in_tpl_literal.snap @@ -1,5 +1,5 @@ --- -source: crates/lingui_macro/tests/js_t.rs +source: crates/lingui_macro/tests/labeled_expressions.rs --- import { t, ph } from "@lingui/core/macro"; diff --git a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_with_non_object_arg.snap b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_with_non_object_arg.snap index f1f6661..6208bfe 100644 --- a/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_with_non_object_arg.snap +++ b/crates/lingui_macro/tests/snapshots/labeled_expressions__js_ph_with_non_object_arg.snap @@ -1,5 +1,5 @@ --- -source: crates/lingui_macro/tests/js_t.rs +source: crates/lingui_macro/tests/labeled_expressions.rs info: {} --- import { t, ph } from "@lingui/core/macro"; diff --git a/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_explicit_labels.snap b/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_explicit_labels.snap index d67e0a0..c2e39ee 100644 --- a/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_explicit_labels.snap +++ b/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_explicit_labels.snap @@ -1,5 +1,5 @@ --- -source: crates/lingui_macro/tests/jsx.rs +source: crates/lingui_macro/tests/labeled_expressions.rs --- import { Trans } from "@lingui/react/macro"; diff --git a/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_explicit_labels_with_as_statement.snap b/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_explicit_labels_with_as_statement.snap index 4864f3c..5cee97d 100644 --- a/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_explicit_labels_with_as_statement.snap +++ b/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_explicit_labels_with_as_statement.snap @@ -1,5 +1,5 @@ --- -source: crates/lingui_macro/tests/jsx.rs +source: crates/lingui_macro/tests/labeled_expressions.rs --- import { Trans } from "@lingui/react/macro"; Refresh {{foo} as unknown as string} inbox; diff --git a/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_icu_with_labeled_expression_as_value.snap b/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_icu_with_labeled_expression_as_value.snap index c3e93ba..f553c82 100644 --- a/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_icu_with_labeled_expression_as_value.snap +++ b/crates/lingui_macro/tests/snapshots/labeled_expressions__jsx_icu_with_labeled_expression_as_value.snap @@ -1,5 +1,5 @@ --- -source: crates/lingui_macro/tests/jsx_icu.rs +source: crates/lingui_macro/tests/labeled_expressions.rs --- import { Plural } from '@lingui/react/macro';