From 14f67c0c2d71d67a4a08f5775721b3943550bd8e Mon Sep 17 00:00:00 2001 From: Peter Kasarda Date: Mon, 20 Jul 2026 12:33:44 +0200 Subject: [PATCH 1/2] fix: unwrap non-null assertion (`x!`) and `satisfies` in placeholders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `unwrap_ts_only_expr` (formerly `unwrap_ts_as_expr`) only unwrapped `TsAs`, so a placeholder like `t`${x!}`` was treated as a complex expression and assigned an indexed placeholder (`{0}`) instead of a named one (`{x}`) — diverging from `@lingui/babel-plugin-lingui-macro` and breaking catalog lookups (mismatched message ids) when extracting with the JS macro but running with the SWC plugin. Unwrap `TsNonNull` and `TsSatisfies` alongside `TsAs` so all TS-only wrappers let the inner expression name the placeholder. Companion to lingui/js-lingui#2622. Co-Authored-By: Claude Opus 4.8 --- crates/lingui_macro/src/macro_utils.rs | 22 +++++++++++-------- crates/lingui_macro/tests/js_t.rs | 10 +++++++++ ...null_assertion_gets_named_placeholder.snap | 16 ++++++++++++++ 3 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 crates/lingui_macro/tests/snapshots/js_t__js_non_null_assertion_gets_named_placeholder.snap diff --git a/crates/lingui_macro/src/macro_utils.rs b/crates/lingui_macro/src/macro_utils.rs index c92eebe..735bd96 100644 --- a/crates/lingui_macro/src/macro_utils.rs +++ b/crates/lingui_macro/src/macro_utils.rs @@ -9,7 +9,7 @@ use swc_core::ecma::{ast::*, atoms::Atom}; use swc_core::plugin::errors::HANDLER; fn expression_to_name(expr: &Expr, ctx: &mut MacroCtx) -> String { - let expr = unwrap_ts_as_expr(expr); + let expr = unwrap_ts_only_expr(expr); match expr { Expr::Ident(ident) => ident.sym.to_string(), @@ -33,7 +33,7 @@ fn expression_to_name(expr: &Expr, ctx: &mut MacroCtx) -> String { } fn expression_to_value(expr: Box) -> Box { - let unwrapped = unwrap_ts_as_expr(&expr); + let unwrapped = unwrap_ts_only_expr(&expr); match unwrapped { Expr::Object(object) => { @@ -66,14 +66,18 @@ fn expression_to_value(expr: Box) -> Box { } } -// recursively expands TypeScript's as expressions until it reaches a real value -fn unwrap_ts_as_expr(expr: &Expr) -> &Expr { +// recursively unwraps TypeScript-only expression wrappers (`x as T`, `x!`, +// `x satisfies T`) until it reaches a real value, so the inner expression drives +// placeholder naming (e.g. `${x!}` → `{x}`, not `{0}`). +fn unwrap_ts_only_expr(expr: &Expr) -> &Expr { let mut current = expr; - while let Expr::TsAs(TsAsExpr { - expr: inner_expr, .. - }) = current - { - current = inner_expr; + loop { + current = match current { + Expr::TsAs(TsAsExpr { expr, .. }) + | Expr::TsNonNull(TsNonNullExpr { expr, .. }) + | Expr::TsSatisfies(TsSatisfiesExpr { expr, .. }) => expr, + _ => break, + }; } current } diff --git a/crates/lingui_macro/tests/js_t.rs b/crates/lingui_macro/tests/js_t.rs index 4fc3623..db0ce27 100644 --- a/crates/lingui_macro/tests/js_t.rs +++ b/crates/lingui_macro/tests/js_t.rs @@ -228,3 +228,13 @@ to!( t`Hello World` "# ); + +// TS-only expression wrappers unwrap to a named placeholder (parity with `as`), +// matching @lingui/babel-plugin-lingui-macro (see lingui/js-lingui#2622). +to!( + js_non_null_assertion_gets_named_placeholder, + r#" + import { t } from '@lingui/core/macro'; + t`Variable ${name!}`; + "# +); diff --git a/crates/lingui_macro/tests/snapshots/js_t__js_non_null_assertion_gets_named_placeholder.snap b/crates/lingui_macro/tests/snapshots/js_t__js_non_null_assertion_gets_named_placeholder.snap new file mode 100644 index 0000000..7ae2a5b --- /dev/null +++ b/crates/lingui_macro/tests/snapshots/js_t__js_non_null_assertion_gets_named_placeholder.snap @@ -0,0 +1,16 @@ +--- +source: crates/lingui_macro/tests/js_t.rs +--- +import { t } from '@lingui/core/macro'; +t`Variable ${name!}`; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as $_i18n } from "@lingui/core"; +$_i18n._(/*i18n*/ { + id: "xRRkAE", + message: "Variable {name}", + values: { + name: name! + } +}); From ce81d7fb7b2663b1094719603389089f06bd094d Mon Sep 17 00:00:00 2001 From: Peter Kasarda Date: Mon, 20 Jul 2026 15:17:54 +0200 Subject: [PATCH 2/2] test: add satisfies placeholder regression test Covers `${x satisfies T}` producing a named placeholder (`{x}`), matching the non-null case. Addresses review feedback that `TsSatisfies` handling was otherwise untested. Co-Authored-By: Claude Opus 4.8 --- crates/lingui_macro/tests/js_t.rs | 8 ++++++++ ...s_t__js_satisfies_gets_named_placeholder.snap | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 crates/lingui_macro/tests/snapshots/js_t__js_satisfies_gets_named_placeholder.snap diff --git a/crates/lingui_macro/tests/js_t.rs b/crates/lingui_macro/tests/js_t.rs index db0ce27..68a2dc5 100644 --- a/crates/lingui_macro/tests/js_t.rs +++ b/crates/lingui_macro/tests/js_t.rs @@ -238,3 +238,11 @@ to!( t`Variable ${name!}`; "# ); + +to!( + js_satisfies_gets_named_placeholder, + r#" + import { t } from '@lingui/core/macro'; + t`Variable ${name satisfies string}`; + "# +); diff --git a/crates/lingui_macro/tests/snapshots/js_t__js_satisfies_gets_named_placeholder.snap b/crates/lingui_macro/tests/snapshots/js_t__js_satisfies_gets_named_placeholder.snap new file mode 100644 index 0000000..a33aeec --- /dev/null +++ b/crates/lingui_macro/tests/snapshots/js_t__js_satisfies_gets_named_placeholder.snap @@ -0,0 +1,16 @@ +--- +source: crates/lingui_macro/tests/js_t.rs +--- +import { t } from '@lingui/core/macro'; +t`Variable ${name satisfies string}`; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as $_i18n } from "@lingui/core"; +$_i18n._(/*i18n*/ { + id: "xRRkAE", + message: "Variable {name}", + values: { + name: name satisfies string + } +});