From f29f4ad7e22850d2d47d50633c8582d84fd79cb0 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:44:43 +0200 Subject: [PATCH 01/14] refactor: jsx named placeholders, reuse methods, fix readability --- src/ast_utils.rs | 17 ++ src/builder.rs | 77 +++----- tests/jsx.rs | 308 ------------------------------- tests/jsx_named_placeholders.rs | 312 ++++++++++++++++++++++++++++++++ 4 files changed, 352 insertions(+), 362 deletions(-) create mode 100644 tests/jsx_named_placeholders.rs diff --git a/src/ast_utils.rs b/src/ast_utils.rs index 08331f8..5efe3e7 100644 --- a/src/ast_utils.rs +++ b/src/ast_utils.rs @@ -101,6 +101,23 @@ pub fn pick_jsx_attrs( attrs } +pub fn omit_jsx_attrs( + mut attrs: Vec, + names: HashSet<&str>, +) -> Vec { + attrs.retain(|attr| { + if let JSXAttrOrSpread::JSXAttr(attr) = attr { + if let JSXAttrName::Ident(ident) = &attr.name { + let name: &str = &ident.sym; + return !names.contains(name); + } + } + false + }); + + attrs +} + pub fn match_callee_name bool>(call: &CallExpr, predicate: F) -> Option<&Ident> { if let Callee::Expr(expr) = &call.callee { if let Expr::Ident(ident) = expr.as_ref() { diff --git a/src/builder.rs b/src/builder.rs index ceefa2d..55328c9 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,9 +1,12 @@ -use crate::ast_utils::expand_ts_as_expr; +use crate::ast_utils::{ + expand_ts_as_expr, get_jsx_attr, get_jsx_attr_value_as_string, omit_jsx_attrs, +}; use crate::options::LinguiOptions; use crate::tokens::{CaseOrOffset, IcuChoice, MsgToken}; use std::collections::HashSet; +use swc_core::common::EqIgnoreSpan; use swc_core::{ - common::{EqIgnoreSpan, SyntaxContext, DUMMY_SP}, + common::{SyntaxContext, DUMMY_SP}, ecma::ast::*, }; @@ -161,42 +164,31 @@ impl<'a> MessageBuilder<'a> { let mut base_name: Option = None; if let Some(attr_name) = &self.options.jsx_placeholder_attribute { - if let Some(idx) = el.attrs.iter().position(|a| { - if let JSXAttrOrSpread::JSXAttr(attr) = a { - if let JSXAttrName::Ident(ident) = &attr.name { - return &ident.sym == attr_name; - } - } - false - }) { - let attr = el.attrs.remove(idx); - if let JSXAttrOrSpread::JSXAttr(attr) = attr { - let mut is_valid = false; - if let Some(JSXAttrValue::Str(s)) = attr.value { - let val = s.value.to_string_lossy().into_owned(); - if !val.is_empty() { - base_name = Some(val); - is_valid = true; - } - } + let attr = get_jsx_attr(&el, attr_name); - if !is_valid { - swc_core::plugin::errors::HANDLER.with(|h| { - h.struct_span_err( - el.span, - &format!("The `{attr_name}` attribute must be a non-empty string literal."), - ).emit(); - }); - } - } + let attr_value = + attr.and_then(|attr| get_jsx_attr_value_as_string(attr.value.as_ref()?)); + + if attr.is_some() && attr_value.is_none() { + swc_core::plugin::errors::HANDLER.with(|h| { + h.struct_span_err( + el.span, + &format!("The `{attr_name}` attribute must be a non-empty string literal."), + ) + .emit(); + }); } + + base_name = attr_value; + + el.attrs = omit_jsx_attrs(el.attrs, HashSet::from([attr_name.as_str()])) } if base_name.is_none() { if let Some(defaults) = &self.options.jsx_placeholder_defaults { if let JSXElementName::Ident(ident) = &el.name { if let Some(def) = defaults.get(&ident.sym.to_string()) { - base_name = Some(def.clone()); + base_name = Some(def.into()); } } } @@ -220,30 +212,7 @@ impl<'a> MessageBuilder<'a> { } if let Some((_, orig_el)) = self.elements_tracking.iter().find(|(k, _)| k == &n) { - let has_spreads = orig_el - .attrs - .iter() - .any(|a| matches!(a, JSXAttrOrSpread::SpreadElement(_))); - let attrs_equal = if orig_el.attrs.len() == el.attrs.len() { - if has_spreads { - orig_el - .attrs - .iter() - .zip(el.attrs.iter()) - .all(|(a, b)| a.eq_ignore_span(b)) - } else { - orig_el - .attrs - .iter() - .all(|a| el.attrs.iter().any(|b| a.eq_ignore_span(b))) - } - } else { - false - }; - - let tags_equal = el.name.eq_ignore_span(&orig_el.name); - - if !tags_equal || !attrs_equal { + if el.eq_ignore_span(orig_el) { swc_core::plugin::errors::HANDLER.with(|h| { let attr_name = self.options.jsx_placeholder_attribute.as_deref().unwrap_or("_t"); let eg = format!("(e.g. ``)"); diff --git a/tests/jsx.rs b/tests/jsx.rs index 80db846..cb416df 100644 --- a/tests/jsx.rs +++ b/tests/jsx.rs @@ -365,311 +365,3 @@ to!( // ; // `, // }, - -to!( - jsx_named_placeholders_basic, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from "@lingui/react/macro"; - - Hello world! -; - "# -); - -to!( - jsx_named_placeholders_stripped_ast, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from "@lingui/react/macro"; - - About -; - "# -); - -to!( - jsx_named_placeholders_defaults, - LinguiOptions { - jsx_placeholder_defaults: Some(std::collections::HashMap::from([ - ("a".into(), "link".into()), - ("em".into(), "em".into()), - ])), - ..Default::default() - }, - r#" -import { Trans } from "@lingui/react/macro"; - - Here's a link and emphasis. -; - "# -); - -to!( - jsx_named_placeholders_mixed_explicit_and_defaults, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - jsx_placeholder_defaults: Some(std::collections::HashMap::from([( - "a".into(), - "link".into() - ),])), - ..Default::default() - }, - r#" -import { Trans } from "@lingui/react/macro"; -Hello link 1, normal, link 2.; - "# -); - -to_panic!( - jsx_named_placeholders_deduplication_different_props, - LinguiOptions { - jsx_placeholder_defaults: Some(std::collections::HashMap::from( - [("a".into(), "a".into()),] - )), - ..Default::default() - }, - r#" -import { Trans } from "@lingui/react/macro"; -Hello link 1, normal, link 2.; - "# -); - -to!( - jsx_named_placeholders_deduplication_identical, - LinguiOptions { - jsx_placeholder_defaults: Some(std::collections::HashMap::from([( - "em".into(), - "em".into() - ),])), - ..Default::default() - }, - r#" -import { Trans } from "@lingui/react/macro"; -Hello emphasis, normal, more emphasis.; - "# -); - -to_panic!( - jsx_named_placeholders_deduplication_with_stripped_props, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from "@lingui/react/macro"; -Hello link 1, normal, link 1 copy and link 2.; - "# -); - -to!( - jsx_named_placeholders_attribute_ignored_when_not_configured, - LinguiOptions { - ..Default::default() - }, - r#" -import { Trans } from "@lingui/react/macro"; - - Hello world! -; - "# -); - -to!( - jsx_named_placeholders_prop_order, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from "@lingui/react/macro"; -Hello link 1, normal, link 1 copy.; - "# -); - -to_panic!( - jsx_named_placeholders_prop_order2, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from "@lingui/react/macro"; -Hello link 1, normal, link 1 copy.; - "# -); - -to_panic!( - jsx_named_placeholders_throws_on_non_string_attribute_value, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -const name = "link"; -click - "# -); - -to_panic!( - jsx_named_placeholders_throws_on_empty_attribute_value, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -click - "# -); - -to_panic!( - jsx_named_placeholders_throws_on_numeric_name, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -click - "# -); - -to!( - jsx_named_placeholders_allows_hyphenated, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -click - "# -); - -to!( - jsx_named_placeholders_allows_dotted, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -click - "# -); - -to_panic!( - jsx_named_placeholders_throws_starting_with_hyphen, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -click - "# -); - -to_panic!( - jsx_named_placeholders_throws_ending_with_dot, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -click - "# -); - -to_panic!( - jsx_named_placeholders_same_name_different_element_throws, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -A and B - "# -); - -to!( - jsx_named_placeholders_identical_spreads_reused, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -A B - "# -); - -to_panic!( - jsx_named_placeholders_different_spreads_throw, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -A B - "# -); - -to_panic!( - jsx_named_placeholders_same_spread_different_order_throws, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -A B - "# -); - -to_panic!( - jsx_named_placeholders_throws_on_empty_string, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -click - "# -); - -to_panic!( - jsx_named_placeholders_throws_on_jsx_expr, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -click - "# -); - -to_panic!( - jsx_named_placeholders_throws_on_boolean_expr, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -click - "# -); diff --git a/tests/jsx_named_placeholders.rs b/tests/jsx_named_placeholders.rs new file mode 100644 index 0000000..1c0feff --- /dev/null +++ b/tests/jsx_named_placeholders.rs @@ -0,0 +1,312 @@ +use lingui_macro_plugin::LinguiOptions; + +#[macro_use] +mod common; + +to!( + jsx_named_placeholders_basic, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from "@lingui/react/macro"; + + Hello world! +; + "# +); + +to!( + jsx_named_placeholders_stripped_ast, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from "@lingui/react/macro"; + + About +; + "# +); + +to!( + jsx_named_placeholders_defaults, + LinguiOptions { + jsx_placeholder_defaults: Some(std::collections::HashMap::from([ + ("a".into(), "link".into()), + ("em".into(), "em".into()), + ])), + ..Default::default() + }, + r#" +import { Trans } from "@lingui/react/macro"; + + Here's a link and emphasis. +; + "# +); + +to!( + jsx_named_placeholders_mixed_explicit_and_defaults, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + jsx_placeholder_defaults: Some(std::collections::HashMap::from([( + "a".into(), + "link".into() + ),])), + ..Default::default() + }, + r#" +import { Trans } from "@lingui/react/macro"; +Hello link 1, normal, link 2.; + "# +); + +to_panic!( + jsx_named_placeholders_deduplication_different_props, + LinguiOptions { + jsx_placeholder_defaults: Some(std::collections::HashMap::from( + [("a".into(), "a".into()),] + )), + ..Default::default() + }, + r#" +import { Trans } from "@lingui/react/macro"; +Hello link 1, normal, link 2.; + "# +); + +to!( + jsx_named_placeholders_deduplication_identical, + LinguiOptions { + jsx_placeholder_defaults: Some(std::collections::HashMap::from([( + "em".into(), + "em".into() + ),])), + ..Default::default() + }, + r#" +import { Trans } from "@lingui/react/macro"; +Hello emphasis, normal, more emphasis.; + "# +); + +to_panic!( + jsx_named_placeholders_deduplication_with_stripped_props, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from "@lingui/react/macro"; +Hello link 1, normal, link 1 copy and link 2.; + "# +); + +to!( + jsx_named_placeholders_attribute_ignored_when_not_configured, + LinguiOptions { + ..Default::default() + }, + r#" +import { Trans } from "@lingui/react/macro"; + + Hello world! +; + "# +); + +to!( + jsx_named_placeholders_prop_order, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from "@lingui/react/macro"; +Hello link 1, normal, link 1 copy.; + "# +); + +to_panic!( + jsx_named_placeholders_prop_order2, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from "@lingui/react/macro"; +Hello link 1, normal, link 1 copy.; + "# +); + +to_panic!( + jsx_named_placeholders_throws_on_non_string_attribute_value, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +const name = "link"; +click + "# +); + +to_panic!( + jsx_named_placeholders_throws_on_empty_attribute_value, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +click + "# +); + +to_panic!( + jsx_named_placeholders_throws_on_numeric_name, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +click + "# +); + +to!( + jsx_named_placeholders_allows_hyphenated, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +click + "# +); + +to!( + jsx_named_placeholders_allows_dotted, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +click + "# +); + +to_panic!( + jsx_named_placeholders_throws_starting_with_hyphen, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +click + "# +); + +to_panic!( + jsx_named_placeholders_throws_ending_with_dot, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +click + "# +); + +to_panic!( + jsx_named_placeholders_same_name_different_element_throws, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +A and B + "# +); + +to!( + jsx_named_placeholders_identical_spreads_reused, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +A B + "# +); + +to_panic!( + jsx_named_placeholders_different_spreads_throw, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +A B + "# +); + +to_panic!( + jsx_named_placeholders_same_spread_different_order_throws, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +A B + "# +); + +to_panic!( + jsx_named_placeholders_throws_on_empty_string, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +click + "# +); + +to_panic!( + jsx_named_placeholders_throws_on_jsx_expr, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +click + "# +); + +to_panic!( + jsx_named_placeholders_throws_on_boolean_expr, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +click + "# +); From 785a8c6b0b2f234b3dded9a0187f75c2c36e2286 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:58:32 +0200 Subject: [PATCH 02/14] readme update --- README.md | 52 +++++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index c68facc..3380615 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ https://swc.rs/docs/configuration/swcrc // // Optional. Controls which descriptor fields are preserved in output. // "descriptorFields": "auto" (default) | "all" | "id-only" | "message" - + // // Compatibility option allows to use v6.* SWC Plugin release channel with @lingui/cli@5.* // Controls the BASE64 alphabet used for generating message IDs. // - false (default): Uses URL-safe BASE64 alphabet (Lingui v6 behavior) @@ -61,9 +61,7 @@ https://swc.rs/docs/configuration/swcrc // // IMPORTANT: This option is temporal and will be removed in the next major release. // "useLinguiV5IdGeneration": true - // Lingui strips non-essential fields in production builds for performance. - // You can override the default behavior with: - // "stripNonEssentialFields": false/true + // // To configure custom JSX placeholder attribute and its defaults: // "jsxPlaceholderAttribute": "_t", // "jsxPlaceholderDefaults": { @@ -121,29 +119,29 @@ Below is a table referencing the `swc_core` version used during the plugin build To learn more about SWC Plugins compatibility check this issue https://github.com/lingui/swc-plugin/issues/179 -| Plugin Version | used `swc_core` | -|---------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `0.1.0`, `4.0.0-next.0` | `0.52.8` | -| `0.2.*`, `4.0.0-next.1` ~ `4.0.0-next.3` | `0.56.1` | -| `4.0.0` | `0.75.33` | -| `4.0.1` | `0.76.0` | -| `4.0.2` | `0.76.41` | -| `4.0.3` | `0.78.28` | -| `4.0.4` | `0.79.x` | -| `4.0.5`, `4.0.6` | [`0.87.x`](https://plugins.swc.rs/versions/range/10) | -| `4.0.7`, `4.0.8`, `5.0.0-next.0` ~ `5.0.0-next.1` | [`0.90.35`](https://plugins.swc.rs/versions/range/12) | -| `4.0.9` | [`0.96.9`](https://plugins.swc.rs/versions/range/15) | -| `4.0.10` | [`0.101.4`](https://plugins.swc.rs/versions/range/94) | -| `4.1.0`, `5.0.0` ~ `5.2.0` | [`0.106.3`](https://plugins.swc.rs/versions/range/95) | -| `5.3.0` | [`5.0.4`](https://plugins.swc.rs/versions/range/116) | -| `5.4.0` | [`14.1.0`](https://plugins.swc.rs/versions/range/138) | -| `5.5.0` ~ `5.5.2` | [`15.0.1`](https://plugins.swc.rs/versions/range/271) | -| `5.6.0` ~ `5.6.1` | [`27.0.6`](https://plugins.swc.rs/versions/range/364) | -| `5.7.0` | [`39.0.3`](https://plugins.swc.rs/versions/range/426) | -| `5.8.0` | [`45.0.2`](https://plugins.swc.rs/versions/range/497) | -| `5.9.0` | [`46.0.3`](https://plugins.swc.rs/versions/range/713) | -| `5.10.0` | [`50.2.3`](https://plugins.swc.rs/versions/range/768) | -| `5.10.1`, `5.11.0` | [`50.2.3`](https://plugins.swc.rs/versions/range/768) with [`--cfg=swc_ast_unknown`](https://swc.rs/docs/plugin/ecmascript/compatibility#make-your-plugin-compatible) | +| Plugin Version | used `swc_core` | +|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `0.1.0`, `4.0.0-next.0` | `0.52.8` | +| `0.2.*`, `4.0.0-next.1` ~ `4.0.0-next.3` | `0.56.1` | +| `4.0.0` | `0.75.33` | +| `4.0.1` | `0.76.0` | +| `4.0.2` | `0.76.41` | +| `4.0.3` | `0.78.28` | +| `4.0.4` | `0.79.x` | +| `4.0.5`, `4.0.6` | [`0.87.x`](https://plugins.swc.rs/versions/range/10) | +| `4.0.7`, `4.0.8`, `5.0.0-next.0` ~ `5.0.0-next.1` | [`0.90.35`](https://plugins.swc.rs/versions/range/12) | +| `4.0.9` | [`0.96.9`](https://plugins.swc.rs/versions/range/15) | +| `4.0.10` | [`0.101.4`](https://plugins.swc.rs/versions/range/94) | +| `4.1.0`, `5.0.0` ~ `5.2.0` | [`0.106.3`](https://plugins.swc.rs/versions/range/95) | +| `5.3.0` | [`5.0.4`](https://plugins.swc.rs/versions/range/116) | +| `5.4.0` | [`14.1.0`](https://plugins.swc.rs/versions/range/138) | +| `5.5.0` ~ `5.5.2` | [`15.0.1`](https://plugins.swc.rs/versions/range/271) | +| `5.6.0` ~ `5.6.1` | [`27.0.6`](https://plugins.swc.rs/versions/range/364) | +| `5.7.0` | [`39.0.3`](https://plugins.swc.rs/versions/range/426) | +| `5.8.0` | [`45.0.2`](https://plugins.swc.rs/versions/range/497) | +| `5.9.0` | [`46.0.3`](https://plugins.swc.rs/versions/range/713) | +| `5.10.0` | [`50.2.3`](https://plugins.swc.rs/versions/range/768) | +| `5.10.1` ~ `*`
Starting from this version Wasm plugins are compatible between `@swc/core` versions to some extent. Read more [here](https://swc.rs/docs/plugin/ecmascript/compatibility#make-your-plugin-compatible). | [`50.2.3`](https://plugins.swc.rs/versions/range/768) with [`--cfg=swc_ast_unknown`](https://swc.rs/docs/plugin/ecmascript/compatibility#make-your-plugin-compatible) | > **Note** From 30838daa27aaf7a5a19503e360a0a705c9989b94 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:45:47 +0200 Subject: [PATCH 03/14] fixes --- src/ast_utils.rs | 31 +++++++++++- src/builder.rs | 6 +-- ...ceholders_deduplication_different_props.js | 9 ---- ...lders_deduplication_with_stripped_props.js | 9 ---- .../jsx_named_placeholders_prop_order2.js | 9 ---- .../allows_dotted.js} | 0 .../allows_hyphenated.js} | 0 .../attribute_ignored_when_not_configured.js} | 0 .../basic.js} | 0 .../deduplication_identical.js} | 0 .../defaults.js} | 0 .../identical_spreads_reused.js} | 0 .../mixed_explicit_and_defaults.js} | 0 .../prop_order.js} | 0 .../stripped_ast.js} | 0 .../supports_string_in_jsx_expression.js} | 7 ++- tests/jsx_named_placeholders.rs | 50 +++++++++---------- 17 files changed, 60 insertions(+), 61 deletions(-) delete mode 100644 tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_deduplication_different_props.js delete mode 100644 tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_deduplication_with_stripped_props.js delete mode 100644 tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_prop_order2.js rename tests/__swc_snapshots__/tests/{jsx.rs/jsx_named_placeholders_allows_dotted.js => jsx_named_placeholders.rs/allows_dotted.js} (100%) rename tests/__swc_snapshots__/tests/{jsx.rs/jsx_named_placeholders_allows_hyphenated.js => jsx_named_placeholders.rs/allows_hyphenated.js} (100%) rename tests/__swc_snapshots__/tests/{jsx.rs/jsx_named_placeholders_attribute_ignored_when_not_configured.js => jsx_named_placeholders.rs/attribute_ignored_when_not_configured.js} (100%) rename tests/__swc_snapshots__/tests/{jsx.rs/jsx_named_placeholders_basic.js => jsx_named_placeholders.rs/basic.js} (100%) rename tests/__swc_snapshots__/tests/{jsx.rs/jsx_named_placeholders_deduplication_identical.js => jsx_named_placeholders.rs/deduplication_identical.js} (100%) rename tests/__swc_snapshots__/tests/{jsx.rs/jsx_named_placeholders_defaults.js => jsx_named_placeholders.rs/defaults.js} (100%) rename tests/__swc_snapshots__/tests/{jsx.rs/jsx_named_placeholders_identical_spreads_reused.js => jsx_named_placeholders.rs/identical_spreads_reused.js} (100%) rename tests/__swc_snapshots__/tests/{jsx.rs/jsx_named_placeholders_mixed_explicit_and_defaults.js => jsx_named_placeholders.rs/mixed_explicit_and_defaults.js} (100%) rename tests/__swc_snapshots__/tests/{jsx.rs/jsx_named_placeholders_prop_order.js => jsx_named_placeholders.rs/prop_order.js} (100%) rename tests/__swc_snapshots__/tests/{jsx.rs/jsx_named_placeholders_stripped_ast.js => jsx_named_placeholders.rs/stripped_ast.js} (100%) rename tests/__swc_snapshots__/tests/{jsx.rs/jsx_named_placeholders_throws_on_non_string_attribute_value.js => jsx_named_placeholders.rs/supports_string_in_jsx_expression.js} (52%) diff --git a/src/ast_utils.rs b/src/ast_utils.rs index 5efe3e7..86b9394 100644 --- a/src/ast_utils.rs +++ b/src/ast_utils.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use swc_core::atoms::atom; use swc_core::common::comments::{Comment, CommentKind, Comments}; -use swc_core::common::{Span, DUMMY_SP}; +use swc_core::common::{EqIgnoreSpan, Span, DUMMY_SP}; use swc_core::ecma::ast::*; use swc_core::ecma::atoms::Atom; use swc_core::ecma::utils::quote_ident; @@ -112,12 +112,39 @@ pub fn omit_jsx_attrs( return !names.contains(name); } } - false + true }); attrs } +pub fn is_jsx_elements_equal(a: &JSXOpeningElement, b: &JSXOpeningElement) -> bool { + let attrs_equal = if a.attrs.len() == b.attrs.len() { + let has_spreads = a + .attrs + .iter() + .any(|a| matches!(a, JSXAttrOrSpread::SpreadElement(_))); + + if has_spreads { + a.attrs + .iter() + .zip(b.attrs.iter()) + .all(|(a, b)| a.eq_ignore_span(b)) + } else { + a.attrs + .iter() + .all(|a| b.attrs.iter().any(|b| a.eq_ignore_span(b))) + } + } else { + false + }; + + let tags_equal = a.name.eq_ignore_span(&b.name); + + print!("{tags_equal} {attrs_equal}"); + tags_equal && attrs_equal +} + pub fn match_callee_name bool>(call: &CallExpr, predicate: F) -> Option<&Ident> { if let Callee::Expr(expr) = &call.callee { if let Expr::Ident(ident) = expr.as_ref() { diff --git a/src/builder.rs b/src/builder.rs index 55328c9..b6cd296 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,10 +1,10 @@ use crate::ast_utils::{ - expand_ts_as_expr, get_jsx_attr, get_jsx_attr_value_as_string, omit_jsx_attrs, + expand_ts_as_expr, 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 std::collections::HashSet; -use swc_core::common::EqIgnoreSpan; use swc_core::{ common::{SyntaxContext, DUMMY_SP}, ecma::ast::*, @@ -212,7 +212,7 @@ impl<'a> MessageBuilder<'a> { } if let Some((_, orig_el)) = self.elements_tracking.iter().find(|(k, _)| k == &n) { - if el.eq_ignore_span(orig_el) { + if !is_jsx_elements_equal(&el, orig_el) { swc_core::plugin::errors::HANDLER.with(|h| { let attr_name = self.options.jsx_placeholder_attribute.as_deref().unwrap_or("_t"); let eg = format!("(e.g. ``)"); diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_deduplication_different_props.js b/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_deduplication_different_props.js deleted file mode 100644 index c426058..0000000 --- a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_deduplication_different_props.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Trans as Trans_ } from "@lingui/react"; -, - a2: - }, - message: "Hello link 1, normal, link 2." -}}/>; diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_deduplication_with_stripped_props.js b/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_deduplication_with_stripped_props.js deleted file mode 100644 index f1b496b..0000000 --- a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_deduplication_with_stripped_props.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Trans as Trans_ } from "@lingui/react"; -, - link2: - }, - message: "Hello link 1, normal, link 1 copy and link 2." -}}/>; diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_prop_order2.js b/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_prop_order2.js deleted file mode 100644 index 9caf556..0000000 --- a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_prop_order2.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Trans as Trans_ } from "@lingui/react"; -, - link2: - }, - message: "Hello link 1, normal, link 1 copy." -}}/>; diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_allows_dotted.js b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/allows_dotted.js similarity index 100% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_allows_dotted.js rename to tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/allows_dotted.js diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_allows_hyphenated.js b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/allows_hyphenated.js similarity index 100% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_allows_hyphenated.js rename to tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/allows_hyphenated.js diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_attribute_ignored_when_not_configured.js b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/attribute_ignored_when_not_configured.js similarity index 100% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_attribute_ignored_when_not_configured.js rename to tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/attribute_ignored_when_not_configured.js diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_basic.js b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/basic.js similarity index 100% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_basic.js rename to tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/basic.js diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_deduplication_identical.js b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/deduplication_identical.js similarity index 100% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_deduplication_identical.js rename to tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/deduplication_identical.js diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_defaults.js b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/defaults.js similarity index 100% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_defaults.js rename to tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/defaults.js diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_identical_spreads_reused.js b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/identical_spreads_reused.js similarity index 100% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_identical_spreads_reused.js rename to tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/identical_spreads_reused.js diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_mixed_explicit_and_defaults.js b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/mixed_explicit_and_defaults.js similarity index 100% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_mixed_explicit_and_defaults.js rename to tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/mixed_explicit_and_defaults.js diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_prop_order.js b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/prop_order.js similarity index 100% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_prop_order.js rename to tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/prop_order.js diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_stripped_ast.js b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/stripped_ast.js similarity index 100% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_stripped_ast.js rename to tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/stripped_ast.js diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_throws_on_non_string_attribute_value.js b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/supports_string_in_jsx_expression.js similarity index 52% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_throws_on_non_string_attribute_value.js rename to tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/supports_string_in_jsx_expression.js index 6610fb6..44cbead 100644 --- a/tests/__swc_snapshots__/tests/jsx.rs/jsx_named_placeholders_throws_on_non_string_attribute_value.js +++ b/tests/__swc_snapshots__/tests/jsx_named_placeholders.rs/supports_string_in_jsx_expression.js @@ -1,9 +1,8 @@ import { Trans as Trans_ } from "@lingui/react"; -const name = "link"; + foo: }, - message: "<0>click" + message: "click" }}/>; diff --git a/tests/jsx_named_placeholders.rs b/tests/jsx_named_placeholders.rs index 1c0feff..e4a7885 100644 --- a/tests/jsx_named_placeholders.rs +++ b/tests/jsx_named_placeholders.rs @@ -4,7 +4,7 @@ use lingui_macro_plugin::LinguiOptions; mod common; to!( - jsx_named_placeholders_basic, + basic, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -18,7 +18,7 @@ import { Trans } from "@lingui/react/macro"; ); to!( - jsx_named_placeholders_stripped_ast, + stripped_ast, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -32,7 +32,7 @@ import { Trans } from "@lingui/react/macro"; ); to!( - jsx_named_placeholders_defaults, + defaults, LinguiOptions { jsx_placeholder_defaults: Some(std::collections::HashMap::from([ ("a".into(), "link".into()), @@ -49,7 +49,7 @@ import { Trans } from "@lingui/react/macro"; ); to!( - jsx_named_placeholders_mixed_explicit_and_defaults, + mixed_explicit_and_defaults, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), jsx_placeholder_defaults: Some(std::collections::HashMap::from([( @@ -65,7 +65,7 @@ import { Trans } from "@lingui/react/macro"; ); to_panic!( - jsx_named_placeholders_deduplication_different_props, + deduplication_different_props, LinguiOptions { jsx_placeholder_defaults: Some(std::collections::HashMap::from( [("a".into(), "a".into()),] @@ -79,7 +79,7 @@ import { Trans } from "@lingui/react/macro"; ); to!( - jsx_named_placeholders_deduplication_identical, + deduplication_identical, LinguiOptions { jsx_placeholder_defaults: Some(std::collections::HashMap::from([( "em".into(), @@ -94,7 +94,7 @@ import { Trans } from "@lingui/react/macro"; ); to_panic!( - jsx_named_placeholders_deduplication_with_stripped_props, + deduplication_with_stripped_props, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -106,7 +106,7 @@ import { Trans } from "@lingui/react/macro"; ); to!( - jsx_named_placeholders_attribute_ignored_when_not_configured, + attribute_ignored_when_not_configured, LinguiOptions { ..Default::default() }, @@ -119,7 +119,7 @@ import { Trans } from "@lingui/react/macro"; ); to!( - jsx_named_placeholders_prop_order, + prop_order, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -131,7 +131,7 @@ import { Trans } from "@lingui/react/macro"; ); to_panic!( - jsx_named_placeholders_prop_order2, + prop_order2, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -143,7 +143,7 @@ import { Trans } from "@lingui/react/macro"; ); to_panic!( - jsx_named_placeholders_throws_on_non_string_attribute_value, + throws_on_non_string_attribute_value, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -156,7 +156,7 @@ const name = "link"; ); to_panic!( - jsx_named_placeholders_throws_on_empty_attribute_value, + throws_on_empty_attribute_value, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -168,7 +168,7 @@ import { Trans } from '@lingui/react/macro'; ); to_panic!( - jsx_named_placeholders_throws_on_numeric_name, + throws_on_numeric_name, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -180,7 +180,7 @@ import { Trans } from '@lingui/react/macro'; ); to!( - jsx_named_placeholders_allows_hyphenated, + allows_hyphenated, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -192,7 +192,7 @@ import { Trans } from '@lingui/react/macro'; ); to!( - jsx_named_placeholders_allows_dotted, + allows_dotted, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -204,7 +204,7 @@ import { Trans } from '@lingui/react/macro'; ); to_panic!( - jsx_named_placeholders_throws_starting_with_hyphen, + throws_starting_with_hyphen, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -216,7 +216,7 @@ import { Trans } from '@lingui/react/macro'; ); to_panic!( - jsx_named_placeholders_throws_ending_with_dot, + throws_ending_with_dot, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -228,7 +228,7 @@ import { Trans } from '@lingui/react/macro'; ); to_panic!( - jsx_named_placeholders_same_name_different_element_throws, + same_name_different_element_throws, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -240,7 +240,7 @@ import { Trans } from '@lingui/react/macro'; ); to!( - jsx_named_placeholders_identical_spreads_reused, + identical_spreads_reused, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -252,7 +252,7 @@ import { Trans } from '@lingui/react/macro'; ); to_panic!( - jsx_named_placeholders_different_spreads_throw, + different_spreads_throw, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -264,7 +264,7 @@ import { Trans } from '@lingui/react/macro'; ); to_panic!( - jsx_named_placeholders_same_spread_different_order_throws, + same_spread_different_order_throws, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -276,7 +276,7 @@ import { Trans } from '@lingui/react/macro'; ); to_panic!( - jsx_named_placeholders_throws_on_empty_string, + throws_on_empty_string, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -287,8 +287,8 @@ import { Trans } from '@lingui/react/macro'; "# ); -to_panic!( - jsx_named_placeholders_throws_on_jsx_expr, +to!( + supports_string_in_jsx_expression, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() @@ -300,7 +300,7 @@ import { Trans } from '@lingui/react/macro'; ); to_panic!( - jsx_named_placeholders_throws_on_boolean_expr, + throws_on_boolean_expr, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() From 45a31cee549183dce0ed3e2313212e70cff1d18e Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:59:00 +0200 Subject: [PATCH 04/14] simplify flow --- src/ast_utils.rs | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/ast_utils.rs b/src/ast_utils.rs index 86b9394..1a733b1 100644 --- a/src/ast_utils.rs +++ b/src/ast_utils.rs @@ -119,30 +119,29 @@ pub fn omit_jsx_attrs( } pub fn is_jsx_elements_equal(a: &JSXOpeningElement, b: &JSXOpeningElement) -> bool { - let attrs_equal = if a.attrs.len() == b.attrs.len() { - let has_spreads = a - .attrs - .iter() - .any(|a| matches!(a, JSXAttrOrSpread::SpreadElement(_))); - - if has_spreads { - a.attrs - .iter() - .zip(b.attrs.iter()) - .all(|(a, b)| a.eq_ignore_span(b)) - } else { - a.attrs - .iter() - .all(|a| b.attrs.iter().any(|b| a.eq_ignore_span(b))) - } - } else { - false - }; + if !a.name.eq_ignore_span(&b.name) { + return false; + } + + if a.attrs.len() != b.attrs.len() { + return false; + } - let tags_equal = a.name.eq_ignore_span(&b.name); + let has_spreads = a + .attrs + .iter() + .any(|a| matches!(a, JSXAttrOrSpread::SpreadElement(_))); - print!("{tags_equal} {attrs_equal}"); - tags_equal && attrs_equal + if has_spreads { + a.attrs + .iter() + .zip(b.attrs.iter()) + .all(|(a, b)| a.eq_ignore_span(b)) + } else { + a.attrs + .iter() + .all(|a| b.attrs.iter().any(|b| a.eq_ignore_span(b))) + } } pub fn match_callee_name bool>(call: &CallExpr, predicate: F) -> Option<&Ident> { From 0eba12f471af8835e729e23a5a82364327161693 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:02:06 +0200 Subject: [PATCH 05/14] add est case --- tests/jsx_named_placeholders.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/jsx_named_placeholders.rs b/tests/jsx_named_placeholders.rs index e4a7885..d1c4898 100644 --- a/tests/jsx_named_placeholders.rs +++ b/tests/jsx_named_placeholders.rs @@ -239,6 +239,18 @@ import { Trans } from '@lingui/react/macro'; "# ); +to_panic!( + same_element_diffrent_attributes_count_throw, + LinguiOptions { + jsx_placeholder_attribute: Some("_t".into()), + ..Default::default() + }, + r#" +import { Trans } from '@lingui/react/macro'; +A and B + "# +); + to!( identical_spreads_reused, LinguiOptions { From fc42db95f70668a0d14108d5da39de02ac641679 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:25:40 +0200 Subject: [PATCH 06/14] add claude md --- CLAUDE.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f68aae2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +SWC plugin for [LinguiJS](https://lingui.dev) — a Rust-based compile-time macro that transforms `@lingui/macro` and `@lingui/react/macro` calls into optimized i18n runtime code. Compiles to WebAssembly (wasm32-wasip1) and runs inside SWC/Next.js build pipelines. + +## Build & Test Commands + +```bash +# Build WASM (primary target) +cargo build-wasi --release + +# Run all tests +cargo test + +# Run a single test by name +cargo test js_choices_may_contain_expressions + +# Run tests matching a prefix +cargo test jsx_ + +# Update snapshots after intentional changes +UPDATE=1 cargo test + +# Format / lint +cargo fmt --check +cargo clippy --all-targets --all-features -- -D warnings + +# E2E tests (requires WASM build + Node v22 + yarn) +cargo build-wasi --release && yarn test:e2e +``` + +## Architecture + +The plugin follows SWC's AST visitor pattern using the `Fold` trait for recursive descent transformation. + +**Core transformation pipeline:** +1. `lib.rs` — Entry point (`#[plugin_transform]`). Parses config, creates `LinguiMacroFolder` which implements `Fold`. +2. `macro_utils.rs` — `MacroCtx` tracks imports from `@lingui/macro` and `@lingui/react/macro`, maps symbol names to local identifiers. +3. `js_macro_folder.rs` — Transforms JS macro calls (`t()`, `defineMessage()`, `msg()`) into `MsgToken` streams. +4. `jsx_visitor.rs` — `TransJSXVisitor` transforms JSX elements (``, ``, `Should be untouched + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; import { Select } from "./my-select-cmp"; ; diff --git a/tests/__swc_snapshots__/tests/imports.rs/jsx_should_process_only_elements_imported_from_macro2.js b/tests/snapshots/imports__jsx_should_process_only_elements_imported_from_macro2.snap similarity index 53% rename from tests/__swc_snapshots__/tests/imports.rs/jsx_should_process_only_elements_imported_from_macro2.js rename to tests/snapshots/imports__jsx_should_process_only_elements_imported_from_macro2.snap index 3fd845e..972c5b8 100644 --- a/tests/__swc_snapshots__/tests/imports.rs/jsx_should_process_only_elements_imported_from_macro2.js +++ b/tests/snapshots/imports__jsx_should_process_only_elements_imported_from_macro2.snap @@ -1,3 +1,19 @@ +--- +source: tests/imports.rs +--- +import { Trans } from "@lingui/react"; +import { Plural } from "@lingui/react/macro"; + +; + +;Should be untouched + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans } from "@lingui/react"; import { Trans as Trans_ } from "@lingui/react"; ; diff --git a/tests/__swc_snapshots__/tests/imports.rs/jsx_should_support_renamed_imports.js b/tests/snapshots/imports__jsx_should_support_renamed_imports.snap similarity index 53% rename from tests/__swc_snapshots__/tests/imports.rs/jsx_should_support_renamed_imports.js rename to tests/snapshots/imports__jsx_should_support_renamed_imports.snap index 6571262..0d200b1 100644 --- a/tests/__swc_snapshots__/tests/imports.rs/jsx_should_support_renamed_imports.js +++ b/tests/snapshots/imports__jsx_should_support_renamed_imports.snap @@ -1,3 +1,18 @@ +--- +source: tests/imports.rs +--- +import { Trans as I18nTrans, Plural as I18nPlural } from "@lingui/react/macro"; + +; + +;Hello! + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; ; ; +Untouched + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; import { i18n as $_i18n } from "@lingui/core"; import { i18n } from "@lingui/core"; diff --git a/tests/snapshots/js_define_message__define_message_should_support_template_literal.snap b/tests/snapshots/js_define_message__define_message_should_support_template_literal.snap new file mode 100644 index 0000000..0d030a6 --- /dev/null +++ b/tests/snapshots/js_define_message__define_message_should_support_template_literal.snap @@ -0,0 +1,17 @@ +--- +source: tests/js_define_message.rs +--- +import { defineMessage, msg } from '@lingui/macro'; +const message1 = defineMessage`Message`; +const message2 = msg`Message` + +↓ ↓ ↓ ↓ ↓ ↓ + +const message1 = /*i18n*/ { + id: "xDAtGP", + message: "Message" +}; +const message2 = /*i18n*/ { + id: "xDAtGP", + message: "Message" +}; diff --git a/tests/snapshots/js_define_message__id_only_should_keep_only_id.snap b/tests/snapshots/js_define_message__id_only_should_keep_only_id.snap new file mode 100644 index 0000000..62663b6 --- /dev/null +++ b/tests/snapshots/js_define_message__id_only_should_keep_only_id.snap @@ -0,0 +1,25 @@ +--- +source: tests/js_define_message.rs +info: + descriptor_fields: id-only +--- +import { defineMessage } from '@lingui/macro' +const message1 = defineMessage`Message`; +const message2 = defineMessage({ + message: `Hello ${name}`, + id: 'msgId', + comment: 'description for translators', + context: 'My Context', +}) + +↓ ↓ ↓ ↓ ↓ ↓ + +const message1 = /*i18n*/ { + id: "xDAtGP" +}; +const message2 = /*i18n*/ { + id: "msgId", + values: { + name: name + } +}; diff --git a/tests/snapshots/js_define_message__message_should_keep_message_and_context.snap b/tests/snapshots/js_define_message__message_should_keep_message_and_context.snap new file mode 100644 index 0000000..3b19992 --- /dev/null +++ b/tests/snapshots/js_define_message__message_should_keep_message_and_context.snap @@ -0,0 +1,28 @@ +--- +source: tests/js_define_message.rs +info: + descriptor_fields: message +--- +import { defineMessage } from '@lingui/macro' +const message1 = defineMessage`Message`; +const message2 = defineMessage({ + message: `Hello ${name}`, + id: 'msgId', + comment: 'description for translators', + context: 'My Context', +}) + +↓ ↓ ↓ ↓ ↓ ↓ + +const message1 = /*i18n*/ { + id: "xDAtGP", + message: "Message" +}; +const message2 = /*i18n*/ { + id: "msgId", + message: "Hello {name}", + values: { + name: name + }, + context: "My Context" +}; diff --git a/tests/snapshots/js_define_message__should_expand_macros.snap b/tests/snapshots/js_define_message__should_expand_macros.snap new file mode 100644 index 0000000..ca04e78 --- /dev/null +++ b/tests/snapshots/js_define_message__should_expand_macros.snap @@ -0,0 +1,19 @@ +--- +source: tests/js_define_message.rs +--- +import { defineMessage, plural, arg } from '@lingui/macro'; +const message = defineMessage({ + comment: "Description", + message: plural(count, { one: "book", other: "books" }) +}) + +↓ ↓ ↓ ↓ ↓ ↓ + +const message = /*i18n*/ { + id: "AJdPPy", + message: "{count, plural, one {book} other {books}}", + values: { + count: count + }, + comment: "Description" +}; diff --git a/tests/snapshots/js_define_message__should_expand_values.snap b/tests/snapshots/js_define_message__should_expand_values.snap new file mode 100644 index 0000000..d1e899b --- /dev/null +++ b/tests/snapshots/js_define_message__should_expand_values.snap @@ -0,0 +1,17 @@ +--- +source: tests/js_define_message.rs +--- +import { defineMessage, plural, arg } from '@lingui/macro'; +const message = defineMessage({ + message: `Hello ${name}` +}) + +↓ ↓ ↓ ↓ ↓ ↓ + +const message = /*i18n*/ { + id: "OVaF9k", + message: "Hello {name}", + values: { + name: name + } +}; diff --git a/tests/snapshots/js_define_message__should_preserve_custom_id.snap b/tests/snapshots/js_define_message__should_preserve_custom_id.snap new file mode 100644 index 0000000..d2011f9 --- /dev/null +++ b/tests/snapshots/js_define_message__should_preserve_custom_id.snap @@ -0,0 +1,17 @@ +--- +source: tests/js_define_message.rs +--- +import { defineMessage, plural, arg } from '@lingui/macro'; +const message = defineMessage({ + comment: "Description", + id: "custom.id", + message: "Message", +}) + +↓ ↓ ↓ ↓ ↓ ↓ + +const message = /*i18n*/ { + id: "custom.id", + message: "Message", + comment: "Description" +}; diff --git a/tests/snapshots/js_define_message__should_transform_define_message.snap b/tests/snapshots/js_define_message__should_transform_define_message.snap new file mode 100644 index 0000000..341b8f8 --- /dev/null +++ b/tests/snapshots/js_define_message__should_transform_define_message.snap @@ -0,0 +1,25 @@ +--- +source: tests/js_define_message.rs +--- +import { defineMessage, msg } from '@lingui/macro'; +const message1 = defineMessage({ + comment: "Description", + message: "Message" +}) +const message2 = msg({ + comment: "Description", + message: "Message" +}) + +↓ ↓ ↓ ↓ ↓ ↓ + +const message1 = /*i18n*/ { + id: "xDAtGP", + message: "Message", + comment: "Description" +}; +const message2 = /*i18n*/ { + id: "xDAtGP", + message: "Message", + comment: "Description" +}; diff --git a/tests/__swc_snapshots__/tests/js_icu.rs/js_choices_may_contain_expressions.js b/tests/snapshots/js_icu__js_choices_may_contain_expressions.snap similarity index 62% rename from tests/__swc_snapshots__/tests/js_icu.rs/js_choices_may_contain_expressions.js rename to tests/snapshots/js_icu__js_choices_may_contain_expressions.snap index d531460..a4aaa5c 100644 --- a/tests/__swc_snapshots__/tests/js_icu.rs/js_choices_may_contain_expressions.js +++ b/tests/snapshots/js_icu__js_choices_may_contain_expressions.snap @@ -1,3 +1,20 @@ +--- +source: tests/js_icu.rs +--- +import { plural, select, selectOrdinal } from "@lingui/core/macro"; +const messagePlural = plural(count, { + one: foo.bar, + other: variable +}) +const messageSelect = select(gender, { + male: 'he', + female: variable, + third: fn(), + other: foo.bar +}) + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; const messagePlural = $_i18n._(/*i18n*/ { id: "l6reUi", diff --git a/tests/__swc_snapshots__/tests/js_icu.rs/js_dedup_values_in_icu.js b/tests/snapshots/js_icu__js_dedup_values_in_icu.snap similarity index 54% rename from tests/__swc_snapshots__/tests/js_icu.rs/js_dedup_values_in_icu.js rename to tests/snapshots/js_icu__js_dedup_values_in_icu.snap index b008b58..0df731e 100644 --- a/tests/__swc_snapshots__/tests/js_icu.rs/js_dedup_values_in_icu.js +++ b/tests/snapshots/js_icu__js_dedup_values_in_icu.snap @@ -1,3 +1,15 @@ +--- +source: tests/js_icu.rs +--- +import { plural } from "@lingui/core/macro"; + +const message = plural(count, { + one: `${name} has ${count} friend`, + other: `${name} has {count} friends` + }) + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; const message = $_i18n._(/*i18n*/ { id: "tK7kAV", diff --git a/tests/__swc_snapshots__/tests/js_icu.rs/js_icu_diffrent_object_literal_syntax.js b/tests/snapshots/js_icu__js_icu_diffrent_object_literal_syntax.snap similarity index 52% rename from tests/__swc_snapshots__/tests/js_icu.rs/js_icu_diffrent_object_literal_syntax.js rename to tests/snapshots/js_icu__js_icu_diffrent_object_literal_syntax.snap index dac3cff..16db03e 100644 --- a/tests/__swc_snapshots__/tests/js_icu.rs/js_icu_diffrent_object_literal_syntax.js +++ b/tests/snapshots/js_icu__js_icu_diffrent_object_literal_syntax.snap @@ -1,3 +1,16 @@ +--- +source: tests/js_icu.rs +--- +import { plural } from "@lingui/core/macro"; + +const messagePlural = plural(count, { + one: '# Book', + "other": '# Books', + few: ('# Books'), +}) + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; const messagePlural = $_i18n._(/*i18n*/ { id: "2y_Fr5", diff --git a/tests/__swc_snapshots__/tests/js_icu.rs/js_icu_macro.js b/tests/snapshots/js_icu__js_icu_macro.snap similarity index 58% rename from tests/__swc_snapshots__/tests/js_icu.rs/js_icu_macro.js rename to tests/snapshots/js_icu__js_icu_macro.snap index 61a942a..97bc5dd 100644 --- a/tests/__swc_snapshots__/tests/js_icu.rs/js_icu_macro.js +++ b/tests/snapshots/js_icu__js_icu_macro.snap @@ -1,3 +1,25 @@ +--- +source: tests/js_icu.rs +--- +import { plural, select, selectOrdinal } from "@lingui/core/macro"; +const messagePlural = plural(count, { + one: '# Book', + other: '# Books' +}) +const messageSelect = select(gender, { + male: 'he', + female: 'she', + other: 'they' +}) +const messageSelectOrdinal = selectOrdinal(count, { + one: '#st', + two: '#nd', + few: '#rd', + other: '#th', +}) + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; const messagePlural = $_i18n._(/*i18n*/ { id: "V_M0Vc", diff --git a/tests/__swc_snapshots__/tests/js_icu.rs/js_icu_nested_in_choices.js b/tests/snapshots/js_icu__js_icu_nested_in_choices.snap similarity index 51% rename from tests/__swc_snapshots__/tests/js_icu.rs/js_icu_nested_in_choices.js rename to tests/snapshots/js_icu__js_icu_nested_in_choices.snap index f0ec56c..5b9f7bb 100644 --- a/tests/__swc_snapshots__/tests/js_icu.rs/js_icu_nested_in_choices.js +++ b/tests/snapshots/js_icu__js_icu_nested_in_choices.snap @@ -1,3 +1,20 @@ +--- +source: tests/js_icu.rs +--- +import { plural } from "@lingui/core/macro" +const message = plural(numBooks, { + one: plural(numArticles, { + one: `1 book and 1 article`, + other: `1 book and ${numArticles} articles`, + }), + other: plural(numArticles, { + one: `${numBooks} books and 1 article`, + other: `${numBooks} books and ${numArticles} articles`, + }), +}) + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; const message = $_i18n._(/*i18n*/ { id: "AA3wsz", diff --git a/tests/__swc_snapshots__/tests/js_icu.rs/js_icu_nested_in_t.js b/tests/snapshots/js_icu__js_icu_nested_in_t.snap similarity index 52% rename from tests/__swc_snapshots__/tests/js_icu.rs/js_icu_nested_in_t.js rename to tests/snapshots/js_icu__js_icu_nested_in_t.snap index 9c39d03..f4df7fc 100644 --- a/tests/__swc_snapshots__/tests/js_icu.rs/js_icu_nested_in_t.js +++ b/tests/snapshots/js_icu__js_icu_nested_in_t.snap @@ -1,3 +1,16 @@ +--- +source: tests/js_icu.rs +--- +import { t, selectOrdinal } from '@lingui/macro' + +t`This is my ${selectOrdinal(count, { + one: "st", + two: "nd", + other: "rd" +})} cat` + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; $_i18n._(/*i18n*/ { id: "LF3Ndn", diff --git a/tests/__swc_snapshots__/tests/js_icu.rs/js_plural_with_offset_and_exact_matches.js b/tests/snapshots/js_icu__js_plural_with_offset_and_exact_matches.snap similarity index 52% rename from tests/__swc_snapshots__/tests/js_icu.rs/js_plural_with_offset_and_exact_matches.js rename to tests/snapshots/js_icu__js_plural_with_offset_and_exact_matches.snap index 31b012d..2981d14 100644 --- a/tests/__swc_snapshots__/tests/js_icu.rs/js_plural_with_offset_and_exact_matches.js +++ b/tests/snapshots/js_icu__js_plural_with_offset_and_exact_matches.snap @@ -1,3 +1,16 @@ +--- +source: tests/js_icu.rs +--- +import { plural } from '@lingui/macro' +plural(users.length, { + offset: 1, + 0: "No books", + 1: "1 book", + other: "\# books" +}); + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; $_i18n._(/*i18n*/ { id: "CF5t-7", diff --git a/tests/__swc_snapshots__/tests/js_icu.rs/js_plural_with_placeholders.js b/tests/snapshots/js_icu__js_plural_with_placeholders.snap similarity index 54% rename from tests/__swc_snapshots__/tests/js_icu.rs/js_plural_with_placeholders.js rename to tests/snapshots/js_icu__js_plural_with_placeholders.snap index 4cc7e19..227679f 100644 --- a/tests/__swc_snapshots__/tests/js_icu.rs/js_plural_with_placeholders.js +++ b/tests/snapshots/js_icu__js_plural_with_placeholders.snap @@ -1,3 +1,15 @@ +--- +source: tests/js_icu.rs +--- +import { plural } from "@lingui/core/macro"; + +const message = plural(count, { + one: `${name} has # friend`, + other: `${name} has # friends` + }) + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; const message = $_i18n._(/*i18n*/ { id: "CvuUwE", diff --git a/tests/snapshots/js_icu__js_should_not_touch_non_lungui_fns.snap b/tests/snapshots/js_icu__js_should_not_touch_non_lungui_fns.snap new file mode 100644 index 0000000..3cda85f --- /dev/null +++ b/tests/snapshots/js_icu__js_should_not_touch_non_lungui_fns.snap @@ -0,0 +1,15 @@ +--- +source: tests/js_icu.rs +--- +import { plural } from "@lingui/core/macro"; +const messagePlural = customName(count, { + one: '# Book', + other: '# Books' +}) + +↓ ↓ ↓ ↓ ↓ ↓ + +const messagePlural = customName(count, { + one: '# Book', + other: '# Books' +}); diff --git a/tests/__swc_snapshots__/tests/js_icu.rs/js_should_not_treat_offset_in_select.js b/tests/snapshots/js_icu__js_should_not_treat_offset_in_select.snap similarity index 54% rename from tests/__swc_snapshots__/tests/js_icu.rs/js_should_not_treat_offset_in_select.js rename to tests/snapshots/js_icu__js_should_not_treat_offset_in_select.snap index f810a4a..0dc3485 100644 --- a/tests/__swc_snapshots__/tests/js_icu.rs/js_should_not_treat_offset_in_select.js +++ b/tests/snapshots/js_icu__js_should_not_treat_offset_in_select.snap @@ -1,3 +1,15 @@ +--- +source: tests/js_icu.rs +--- +import { select } from '@lingui/macro' +select(value, { + offset: "..", + any: "..", + other: "..", +}); + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; $_i18n._(/*i18n*/ { id: "QHtFym", diff --git a/tests/__swc_snapshots__/tests/js_t.rs/js_choice_labels_in_tpl_literal.js b/tests/snapshots/js_t__js_choice_labels_in_tpl_literal.snap similarity index 59% rename from tests/__swc_snapshots__/tests/js_t.rs/js_choice_labels_in_tpl_literal.js rename to tests/snapshots/js_t__js_choice_labels_in_tpl_literal.snap index fd653dc..c68505e 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/js_choice_labels_in_tpl_literal.js +++ b/tests/snapshots/js_t__js_choice_labels_in_tpl_literal.snap @@ -1,3 +1,14 @@ +--- +source: tests/js_t.rs +--- +import { t, ph, plural, select, selectOrdinal } from "@lingui/core/macro"; + +t`We have ${plural({count: getDevelopersCount()}, {one: "# developer", other: "# developers"})}` +t`${select(gender, {male: "he", female: "she", other: "they"})}` +t`${selectOrdinal(count, {one: "#st", two: "#nd", few: "#rd", other: "#th"})}` + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; $_i18n._(/*i18n*/ { id: "-7z66M", diff --git a/tests/snapshots/js_t__js_continuation_character.snap b/tests/snapshots/js_t__js_continuation_character.snap new file mode 100644 index 0000000..b28a106 --- /dev/null +++ b/tests/snapshots/js_t__js_continuation_character.snap @@ -0,0 +1,14 @@ +--- +source: tests/js_t.rs +--- +import { t } from '@lingui/core/macro'; + t`Multiline\ + string`; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as $_i18n } from "@lingui/core"; +$_i18n._(/*i18n*/ { + id: "eGqEZt", + message: "Multiline string" +}); diff --git a/tests/__swc_snapshots__/tests/js_t.rs/js_custom_i18n_passed.js b/tests/snapshots/js_t__js_custom_i18n_passed.snap similarity index 65% rename from tests/__swc_snapshots__/tests/js_t.rs/js_custom_i18n_passed.js rename to tests/snapshots/js_t__js_custom_i18n_passed.snap index 55a9fd2..123d916 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/js_custom_i18n_passed.js +++ b/tests/snapshots/js_t__js_custom_i18n_passed.snap @@ -1,3 +1,17 @@ +--- +source: tests/js_t.rs +--- +import { t } from "@lingui/core/macro"; +import { custom_i18n } from "./i18n"; + +t(custom_i18n)`Refresh inbox` +t(custom_i18n)`Refresh ${foo} inbox ${bar}` +t(custom_i18n)`Refresh ${foo.bar} inbox ${bar}` +t(custom_i18n)`Refresh ${expr()}` +t(custom.i18n)`Refresh ${expr()}` + +↓ ↓ ↓ ↓ ↓ ↓ + import { custom_i18n } from "./i18n"; custom_i18n._(/*i18n*/ { id: "EsCV2T", diff --git a/tests/__swc_snapshots__/tests/js_t.rs/js_dedup_values_in_tpl_literal.js b/tests/snapshots/js_t__js_dedup_values_in_tpl_literal.snap similarity index 56% rename from tests/__swc_snapshots__/tests/js_t.rs/js_dedup_values_in_tpl_literal.js rename to tests/snapshots/js_t__js_dedup_values_in_tpl_literal.snap index e0f17ec..54041fd 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/js_dedup_values_in_tpl_literal.js +++ b/tests/snapshots/js_t__js_dedup_values_in_tpl_literal.snap @@ -1,3 +1,11 @@ +--- +source: tests/js_t.rs +--- +import { t } from "@lingui/core/macro"; +t`Refresh ${foo} inbox ${foo}` + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; $_i18n._(/*i18n*/ { id: "YZhODz", diff --git a/tests/__swc_snapshots__/tests/js_t.rs/js_ph_labels_in_tpl_literal.js b/tests/snapshots/js_t__js_explicit_labels_in_tpl_literal.snap similarity index 72% rename from tests/__swc_snapshots__/tests/js_t.rs/js_ph_labels_in_tpl_literal.js rename to tests/snapshots/js_t__js_explicit_labels_in_tpl_literal.snap index 6ed91e0..b03a23d 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/js_ph_labels_in_tpl_literal.js +++ b/tests/snapshots/js_t__js_explicit_labels_in_tpl_literal.snap @@ -1,3 +1,17 @@ +--- +source: tests/js_t.rs +--- +import { t } from "@lingui/core/macro"; + +t`Refresh ${{foo}} inbox` +t`Refresh ${{foo: foo.bar}} inbox` +t`Refresh ${{foo: expr()}} inbox` +t`Refresh ${{foo: bar, baz: qux}} inbox` +t`Refresh ${{}} inbox` +t`Refresh ${{...spread}} inbox` + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; $_i18n._(/*i18n*/ { id: "rtxU8c", diff --git a/tests/snapshots/js_t__js_id_only_should_keep_only_id.snap b/tests/snapshots/js_t__js_id_only_should_keep_only_id.snap new file mode 100644 index 0000000..5ec552f --- /dev/null +++ b/tests/snapshots/js_t__js_id_only_should_keep_only_id.snap @@ -0,0 +1,26 @@ +--- +source: tests/js_t.rs +info: + descriptor_fields: id-only +--- +import { t } from '@lingui/core/macro' +const msg1 = t`Message` +const msg2 = t({ + message: `Hello ${name}`, + id: 'msgId', + comment: 'description for translators', + context: 'My Context', +}) + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as $_i18n } from "@lingui/core"; +const msg1 = $_i18n._(/*i18n*/ { + id: "xDAtGP" +}); +const msg2 = $_i18n._(/*i18n*/ { + id: "msgId", + values: { + name: name + } +}); diff --git a/tests/snapshots/js_t__js_message_should_keep_message_and_context.snap b/tests/snapshots/js_t__js_message_should_keep_message_and_context.snap new file mode 100644 index 0000000..494b338 --- /dev/null +++ b/tests/snapshots/js_t__js_message_should_keep_message_and_context.snap @@ -0,0 +1,29 @@ +--- +source: tests/js_t.rs +info: + descriptor_fields: message +--- +import { t } from '@lingui/core/macro' +const msg1 = t`Message` +const msg2 = t({ + message: `Hello ${name}`, + id: 'msgId', + comment: 'description for translators', + context: 'My Context', +}) + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as $_i18n } from "@lingui/core"; +const msg1 = $_i18n._(/*i18n*/ { + id: "xDAtGP", + message: "Message" +}); +const msg2 = $_i18n._(/*i18n*/ { + id: "msgId", + message: "Hello {name}", + values: { + name: name + }, + context: "My Context" +}); diff --git a/tests/snapshots/js_t__js_newlines_are_preserved.snap b/tests/snapshots/js_t__js_newlines_are_preserved.snap new file mode 100644 index 0000000..d57df55 --- /dev/null +++ b/tests/snapshots/js_t__js_newlines_are_preserved.snap @@ -0,0 +1,14 @@ +--- +source: tests/js_t.rs +--- +import { t } from '@lingui/core/macro'; + t`Multiline + string`; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as $_i18n } from "@lingui/core"; +$_i18n._(/*i18n*/ { + id: "esSxMt", + message: "Multiline\n string" +}); diff --git a/tests/__swc_snapshots__/tests/js_t.rs/js_explicit_labels_in_tpl_literal.js b/tests/snapshots/js_t__js_ph_labels_in_tpl_literal.snap similarity index 70% rename from tests/__swc_snapshots__/tests/js_t.rs/js_explicit_labels_in_tpl_literal.js rename to tests/snapshots/js_t__js_ph_labels_in_tpl_literal.snap index 6ed91e0..389a99f 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/js_explicit_labels_in_tpl_literal.js +++ b/tests/snapshots/js_t__js_ph_labels_in_tpl_literal.snap @@ -1,3 +1,17 @@ +--- +source: tests/js_t.rs +--- +import { t, ph } from "@lingui/core/macro"; + +t`Refresh ${ph({foo})} inbox` +t`Refresh ${ph({foo: foo.bar})} inbox` +t`Refresh ${ph({foo: expr()})} inbox` +t`Refresh ${ph({foo: bar, baz: qux})} inbox` +t`Refresh ${ph({})} inbox` +t`Refresh ${ph({...spread})} inbox` + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; $_i18n._(/*i18n*/ { id: "rtxU8c", diff --git a/tests/snapshots/js_t__js_should_not_touch_code_if_no_macro_import.snap b/tests/snapshots/js_t__js_should_not_touch_code_if_no_macro_import.snap new file mode 100644 index 0000000..17efd3f --- /dev/null +++ b/tests/snapshots/js_t__js_should_not_touch_code_if_no_macro_import.snap @@ -0,0 +1,8 @@ +--- +source: tests/js_t.rs +--- +t`Refresh inbox`; + +↓ ↓ ↓ ↓ ↓ ↓ + +t`Refresh inbox`; diff --git a/tests/snapshots/js_t__js_should_not_touch_not_related_tagget_tpls.snap b/tests/snapshots/js_t__js_should_not_touch_not_related_tagget_tpls.snap new file mode 100644 index 0000000..7f10f98 --- /dev/null +++ b/tests/snapshots/js_t__js_should_not_touch_not_related_tagget_tpls.snap @@ -0,0 +1,12 @@ +--- +source: tests/js_t.rs +--- +import { t } from "@lingui/core/macro"; + +b`Refresh inbox`; +b(i18n)`Refresh inbox`; + +↓ ↓ ↓ ↓ ↓ ↓ + +b`Refresh inbox`; +b(i18n)`Refresh inbox`; diff --git a/tests/snapshots/js_t__js_should_produce_all_fields_when_no_message_set.snap b/tests/snapshots/js_t__js_should_produce_all_fields_when_no_message_set.snap new file mode 100644 index 0000000..3184536 --- /dev/null +++ b/tests/snapshots/js_t__js_should_produce_all_fields_when_no_message_set.snap @@ -0,0 +1,18 @@ +--- +source: tests/js_t.rs +--- +import { t } from '@lingui/core/macro' +const msg2 = t({ + id: 'msgId', + comment: 'description for translators', + context: 'My Context', +}) + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as $_i18n } from "@lingui/core"; +const msg2 = $_i18n._(/*i18n*/ { + id: "msgId", + context: "My Context", + comment: "description for translators" +}); diff --git a/tests/__swc_snapshots__/tests/js_t.rs/js_should_produce_all_fields_without_strip_flag.js b/tests/snapshots/js_t__js_should_produce_all_fields_without_strip_flag.snap similarity index 50% rename from tests/__swc_snapshots__/tests/js_t.rs/js_should_produce_all_fields_without_strip_flag.js rename to tests/snapshots/js_t__js_should_produce_all_fields_without_strip_flag.snap index 9a56ba0..8b79156 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/js_should_produce_all_fields_without_strip_flag.js +++ b/tests/snapshots/js_t__js_should_produce_all_fields_without_strip_flag.snap @@ -1,3 +1,16 @@ +--- +source: tests/js_t.rs +--- +import { t } from '@lingui/core/macro' +const msg2 = t({ + message: `Hello ${name}`, + id: 'msgId', + comment: 'description for translators', + context: 'My Context', +}) + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; const msg2 = $_i18n._(/*i18n*/ { id: "msgId", diff --git a/tests/snapshots/js_t__js_should_use_v5_generate_id_with_parameter.snap b/tests/snapshots/js_t__js_should_use_v5_generate_id_with_parameter.snap new file mode 100644 index 0000000..29c5296 --- /dev/null +++ b/tests/snapshots/js_t__js_should_use_v5_generate_id_with_parameter.snap @@ -0,0 +1,19 @@ +--- +source: tests/js_t.rs +info: + use_lingui_v5_id_generation: true +--- +import { t } from '@lingui/core/macro' +t({ + message: "Hello World", + context: "my context" +}); + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as $_i18n } from "@lingui/core"; +$_i18n._(/*i18n*/ { + id: "SO/WB8", + message: "Hello World", + context: "my context" +}); diff --git a/tests/__swc_snapshots__/tests/js_t.rs/js_should_work_with_legacy_import.js b/tests/snapshots/js_t__js_should_work_with_legacy_import.snap similarity index 51% rename from tests/__swc_snapshots__/tests/js_t.rs/js_should_work_with_legacy_import.js rename to tests/snapshots/js_t__js_should_work_with_legacy_import.snap index 7942410..5d4f23f 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/js_should_work_with_legacy_import.js +++ b/tests/snapshots/js_t__js_should_work_with_legacy_import.snap @@ -1,3 +1,12 @@ +--- +source: tests/js_t.rs +--- + import { t } from "@lingui/macro"; + +t`Refresh inbox`; + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; $_i18n._(/*i18n*/ { id: "EsCV2T", diff --git a/tests/__swc_snapshots__/tests/js_t.rs/js_substitution_in_tpl_literal.js b/tests/snapshots/js_t__js_substitution_in_tpl_literal.snap similarity index 71% rename from tests/__swc_snapshots__/tests/js_t.rs/js_substitution_in_tpl_literal.js rename to tests/snapshots/js_t__js_substitution_in_tpl_literal.snap index c8fcecb..e4b30f9 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/js_substitution_in_tpl_literal.js +++ b/tests/snapshots/js_t__js_substitution_in_tpl_literal.snap @@ -1,3 +1,15 @@ +--- +source: tests/js_t.rs +--- +import { t } from "@lingui/core/macro"; + +t`Refresh inbox` +t`Refresh ${foo} inbox ${bar}` +t`Refresh ${foo.bar} inbox ${bar}` +t`Refresh ${expr()}` + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; $_i18n._(/*i18n*/ { id: "EsCV2T", diff --git a/tests/__swc_snapshots__/tests/js_t.rs/js_support_message_descriptor_in_t_fn.js b/tests/snapshots/js_t__js_support_message_descriptor_in_t_fn.snap similarity index 52% rename from tests/__swc_snapshots__/tests/js_t.rs/js_support_message_descriptor_in_t_fn.js rename to tests/snapshots/js_t__js_support_message_descriptor_in_t_fn.snap index ed5007e..777b712 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/js_support_message_descriptor_in_t_fn.js +++ b/tests/snapshots/js_t__js_support_message_descriptor_in_t_fn.snap @@ -1,3 +1,11 @@ +--- +source: tests/js_t.rs +--- +import { t } from '@lingui/core/macro' +const msg = t({ message: `Hello ${name}`, id: 'msgId', comment: 'description for translators' }) + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; const msg = $_i18n._(/*i18n*/ { id: "msgId", diff --git a/tests/snapshots/js_t__js_support_template_strings_in_t_macro_message_with_custom_i18n_instance.snap b/tests/snapshots/js_t__js_support_template_strings_in_t_macro_message_with_custom_i18n_instance.snap new file mode 100644 index 0000000..427d6d9 --- /dev/null +++ b/tests/snapshots/js_t__js_support_template_strings_in_t_macro_message_with_custom_i18n_instance.snap @@ -0,0 +1,17 @@ +--- +source: tests/js_t.rs +--- +import { t } from '@lingui/core/macro' +import { i18n_custom } from './lingui' +const msg = t(i18n_custom)({ message: `Hello ${name}` }) + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n_custom } from './lingui'; +const msg = i18n_custom._(/*i18n*/ { + id: "OVaF9k", + message: "Hello {name}", + values: { + name: name + } +}); diff --git a/tests/snapshots/js_t__js_t_fn_wrapped_in_call_expr.snap b/tests/snapshots/js_t__js_t_fn_wrapped_in_call_expr.snap new file mode 100644 index 0000000..7309002 --- /dev/null +++ b/tests/snapshots/js_t__js_t_fn_wrapped_in_call_expr.snap @@ -0,0 +1,13 @@ +--- +source: tests/js_t.rs +--- +import { t } from '@lingui/core/macro' +const msg = message.error(t({message: "dasd"})) + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as $_i18n } from "@lingui/core"; +const msg = message.error($_i18n._(/*i18n*/ { + id: "9ZMZjU", + message: "dasd" +})); diff --git a/tests/__swc_snapshots__/tests/js_t.rs/should_generate_diffrent_id_when_context_provided.js b/tests/snapshots/js_t__should_generate_diffrent_id_when_context_provided.snap similarity index 58% rename from tests/__swc_snapshots__/tests/js_t.rs/should_generate_diffrent_id_when_context_provided.js rename to tests/snapshots/js_t__should_generate_diffrent_id_when_context_provided.snap index 2359798..28ab824 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/should_generate_diffrent_id_when_context_provided.js +++ b/tests/snapshots/js_t__should_generate_diffrent_id_when_context_provided.snap @@ -1,3 +1,13 @@ +--- +source: tests/js_t.rs +--- +import { t } from '@lingui/core/macro' +t({ message: 'Ola' }) +t({ message: 'Ola', context: "My Context"}) +t({ message: 'Ola', context: `My Context`}) + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; $_i18n._(/*i18n*/ { id: "l1LkPs", diff --git a/tests/__swc_snapshots__/tests/js_t.rs/support_id_and_comment_in_t_macro_as_call_expression.js b/tests/snapshots/js_t__support_id_and_comment_in_t_macro_as_call_expression.snap similarity index 50% rename from tests/__swc_snapshots__/tests/js_t.rs/support_id_and_comment_in_t_macro_as_call_expression.js rename to tests/snapshots/js_t__support_id_and_comment_in_t_macro_as_call_expression.snap index 6021bbd..b4ee257 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/support_id_and_comment_in_t_macro_as_call_expression.js +++ b/tests/snapshots/js_t__support_id_and_comment_in_t_macro_as_call_expression.snap @@ -1,3 +1,11 @@ +--- +source: tests/js_t.rs +--- +import { t, plural } from '@lingui/core/macro' +const msg = t({ id: 'msgId', comment: 'description for translators', message: plural(val, { one: '...', other: '...' }) }) + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; const msg = $_i18n._(/*i18n*/ { id: "msgId", diff --git a/tests/snapshots/js_t__support_id_in_template_literal.snap b/tests/snapshots/js_t__support_id_in_template_literal.snap new file mode 100644 index 0000000..ad476a4 --- /dev/null +++ b/tests/snapshots/js_t__support_id_in_template_literal.snap @@ -0,0 +1,12 @@ +--- +source: tests/js_t.rs +--- +import { t } from '@lingui/core/macro' +const msg = t({ id: `msgId` }) + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as $_i18n } from "@lingui/core"; +const msg = $_i18n._(/*i18n*/ { + id: "msgId" +}); diff --git a/tests/__swc_snapshots__/tests/js_t.rs/unicode_characters_interpreted.js b/tests/snapshots/js_t__unicode_characters_interpreted.snap similarity index 58% rename from tests/__swc_snapshots__/tests/js_t.rs/unicode_characters_interpreted.js rename to tests/snapshots/js_t__unicode_characters_interpreted.snap index 7b8327e..aa5c884 100644 --- a/tests/__swc_snapshots__/tests/js_t.rs/unicode_characters_interpreted.js +++ b/tests/snapshots/js_t__unicode_characters_interpreted.snap @@ -1,3 +1,12 @@ +--- +source: tests/js_t.rs +--- +import { t } from '@lingui/core/macro'; +t`Message \u0020`; +t`Bienvenue\xA0!` + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; $_i18n._(/*i18n*/ { id: "dZXeyN", diff --git a/tests/__swc_snapshots__/tests/jsx.rs/elements_inside_expression_container.js b/tests/snapshots/jsx__elements_inside_expression_container.snap similarity index 54% rename from tests/__swc_snapshots__/tests/jsx.rs/elements_inside_expression_container.js rename to tests/snapshots/jsx__elements_inside_expression_container.snap index b5c4ab0..ef204b9 100644 --- a/tests/__swc_snapshots__/tests/jsx.rs/elements_inside_expression_container.js +++ b/tests/snapshots/jsx__elements_inside_expression_container.snap @@ -1,3 +1,11 @@ +--- +source: tests/jsx.rs +--- +import { Trans } from "@lingui/react/macro"; +{Component inside expression container}; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; {
}
; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; + This should work   +
; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; Hello {name} + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + } +}} render="render" i18n="i18n"/>; diff --git a/tests/snapshots/jsx__ignore_jsx_empty_expression.snap b/tests/snapshots/jsx__ignore_jsx_empty_expression.snap new file mode 100644 index 0000000..9757532 --- /dev/null +++ b/tests/snapshots/jsx__ignore_jsx_empty_expression.snap @@ -0,0 +1,13 @@ +--- +source: tests/jsx.rs +--- +import { Trans } from "@lingui/react/macro"; +Hello {/* and I cannot stress this enough */} World; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; +; diff --git a/tests/__swc_snapshots__/tests/jsx.rs/jsx_comments_should_not_affect_expression_index.js b/tests/snapshots/jsx__jsx_comments_should_not_affect_expression_index.snap similarity index 57% rename from tests/__swc_snapshots__/tests/jsx.rs/jsx_comments_should_not_affect_expression_index.js rename to tests/snapshots/jsx__jsx_comments_should_not_affect_expression_index.snap index 0d512f8..7d021eb 100644 --- a/tests/__swc_snapshots__/tests/jsx.rs/jsx_comments_should_not_affect_expression_index.js +++ b/tests/snapshots/jsx__jsx_comments_should_not_affect_expression_index.snap @@ -1,3 +1,25 @@ +--- +source: tests/jsx.rs +--- +import { Trans } from '@lingui/react/macro'; +// Without comment - expression gets index 0 + + Click here + + {getText()} + +; +// With comment before expression - expression should STILL get index 0 + + Click here + + {/* @ts-expect-error */} + {getText()} + +; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; // Without comment - expression gets index 0 + Hello World!
+
+ + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; Refresh {{foo}} inbox; +Refresh {{foo: foo.bar}} inbox; +Refresh {{foo: expr()}} inbox; +Refresh {{foo: bar, baz: qux}} inbox; +Refresh {{}} inbox; +Refresh {{...spread}} inbox; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; Refresh {{foo} as unknown as string} inbox; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; + Property {props.name}, + function {random()}, + array {array[index]}, + constant {42}, + object {new Date()}, + everything {props.messages[index].value()} + ; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; Refresh {{foo}} inbox; +Refresh {ph({foo})} inbox; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; Refresh {ph({foo})} inbox; +Refresh {ph({foo: foo.bar})} inbox; +Refresh {ph({foo: expr()})} inbox; +Refresh {ph({foo: bar, baz: qux})} inbox; +Refresh {ph({})} inbox; +Refresh {ph({...spread})} inbox; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react";
{p.translation}
} render={(v) => v}>Refresh inbox; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; const exp2 = Refresh inbox; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; const exp2 = Refresh inbox; +const exp2 = Refresh inbox; +const exp3 =
Refresh inbox
; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; const exp1 = Refresh inbox; const exp2 = {`Hello ${foo} and ${bar}`} + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; + Hello {foo} and {foo}{" "} + {bar} + + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; Refresh inbox; +const exp2 = Refresh inbox; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; const exp1 = Refresh inbox; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; +const exp2 = ; diff --git a/tests/snapshots/jsx__keep_multiple_forced_newlines.snap b/tests/snapshots/jsx__keep_multiple_forced_newlines.snap new file mode 100644 index 0000000..c77c63e --- /dev/null +++ b/tests/snapshots/jsx__keep_multiple_forced_newlines.snap @@ -0,0 +1,17 @@ +--- +source: tests/jsx.rs +--- +import { Trans } from "@lingui/react/macro"; + + Keep multiple{"\n"} + forced{"\n"} + newlines! + + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; +; diff --git a/tests/__swc_snapshots__/tests/jsx.rs/message_keeps_message_and_context.js b/tests/snapshots/jsx__message_keeps_message_and_context.snap similarity index 50% rename from tests/__swc_snapshots__/tests/jsx.rs/message_keeps_message_and_context.js rename to tests/snapshots/jsx__message_keeps_message_and_context.snap index b29a2fb..5a2c15b 100644 --- a/tests/__swc_snapshots__/tests/jsx.rs/message_keeps_message_and_context.js +++ b/tests/snapshots/jsx__message_keeps_message_and_context.snap @@ -1,3 +1,18 @@ +--- +source: tests/jsx.rs +info: + descriptor_fields: message +--- +import { Trans } from "@lingui/react/macro"; +Hello {name} + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; + hello +   + world +; + + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; +hello +; + +hello +; + hello ; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; Speak "friend"!; +Speak "friend"!; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; + Strip whitespace around arguments: ' + {name} + ' + + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; + Strip whitespace around tags, but keep{" "} + forced spaces + ! + + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; + {"Wonderful framework "} + Next.js + {" say hi. And "} + Next.js + {" say hi."} + + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; & + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; +; diff --git a/tests/__swc_snapshots__/tests/jsx.rs/use_js_macro_in_jsx_attrs.js b/tests/snapshots/jsx__use_js_macro_in_jsx_attrs.snap similarity index 65% rename from tests/__swc_snapshots__/tests/jsx.rs/use_js_macro_in_jsx_attrs.js rename to tests/snapshots/jsx__use_js_macro_in_jsx_attrs.snap index 1cf5e4d..a9a4ecc 100644 --- a/tests/__swc_snapshots__/tests/jsx.rs/use_js_macro_in_jsx_attrs.js +++ b/tests/snapshots/jsx__use_js_macro_in_jsx_attrs.snap @@ -1,3 +1,12 @@ +--- +source: tests/jsx.rs +--- +import { t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; +Read more + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; import { i18n as $_i18n } from "@lingui/core"; About + +↓ ↓ ↓ ↓ ↓ ↓ + import { i18n as $_i18n } from "@lingui/core"; A lot of them} + /> + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + } +}} render="render" i18n="i18n"/>; diff --git a/tests/__swc_snapshots__/tests/jsx_icu.rs/jsx_icu.js b/tests/snapshots/jsx_icu__jsx_icu.snap similarity index 60% rename from tests/__swc_snapshots__/tests/jsx_icu.rs/jsx_icu.js rename to tests/snapshots/jsx_icu__jsx_icu.snap index 4945b55..0ad6d0c 100644 --- a/tests/__swc_snapshots__/tests/jsx_icu.rs/jsx_icu.js +++ b/tests/snapshots/jsx_icu__jsx_icu.snap @@ -1,3 +1,22 @@ +--- +source: tests/jsx_icu.rs +--- +import { Plural } from "@lingui/react/macro"; + +const ex1 = + +const ex2 =
+ +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; const ex1 = + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; + You have{" "} + + + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; +; diff --git a/tests/__swc_snapshots__/tests/jsx_icu.rs/jsx_icu_with_template_literal.js b/tests/snapshots/jsx_icu__jsx_icu_with_template_literal.snap similarity index 53% rename from tests/__swc_snapshots__/tests/jsx_icu.rs/jsx_icu_with_template_literal.js rename to tests/snapshots/jsx_icu__jsx_icu_with_template_literal.snap index 36c65df..9072dce 100644 --- a/tests/__swc_snapshots__/tests/jsx_icu.rs/jsx_icu_with_template_literal.js +++ b/tests/snapshots/jsx_icu__jsx_icu_with_template_literal.snap @@ -1,3 +1,16 @@ +--- +source: tests/jsx_icu.rs +--- +import { Plural } from "@lingui/react/macro"; + + ; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; + + second level one + + } + other={ + + second level other + + } + /> + + # slot added + + } + other={ + + # slots added + + } +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; +, + 1: + }, + message: "{count, plural, one {{count2, plural, one {second level one} other {second level other}}<0># slot added} other {<1># slots added}}" +}}/>; diff --git a/tests/__swc_snapshots__/tests/jsx_icu.rs/jsx_plural_preserve_reserved_attrs.js b/tests/snapshots/jsx_icu__jsx_plural_preserve_reserved_attrs.snap similarity index 54% rename from tests/__swc_snapshots__/tests/jsx_icu.rs/jsx_plural_preserve_reserved_attrs.js rename to tests/snapshots/jsx_icu__jsx_plural_preserve_reserved_attrs.snap index 7546026..d5fd81f 100644 --- a/tests/__swc_snapshots__/tests/jsx_icu.rs/jsx_plural_preserve_reserved_attrs.js +++ b/tests/snapshots/jsx_icu__jsx_plural_preserve_reserved_attrs.snap @@ -1,3 +1,19 @@ +--- +source: tests/jsx_icu.rs +--- + import { Plural } from "@lingui/react/macro"; + + v} + value={count} + one="..." + other="..." +/> + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; A lot of them} + />; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; ; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; Other} +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; Other} +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; v} + + value={count} + _male="He" + _female={`She`} + other={Other} +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; + # slot added + + } + other={ + + # slots added + + } + />; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; +, + 1: + }, + message: "{count, plural, one {<0># slot added} other {<1># slots added}}" +}}/>; diff --git a/tests/snapshots/jsx_icu__message_keeps_message_and_context.snap b/tests/snapshots/jsx_icu__message_keeps_message_and_context.snap new file mode 100644 index 0000000..5da97ec --- /dev/null +++ b/tests/snapshots/jsx_icu__message_keeps_message_and_context.snap @@ -0,0 +1,33 @@ +--- +source: tests/jsx_icu.rs +info: + descriptor_fields: message +--- +import { Plural } from '@lingui/macro'; + +A lot of them} + /> + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + }, + message: "{count, plural, offset:1 =0 {Zero items} other {<0>A lot of them}}", + context: "My Context" +}} render="render" i18n="i18n"/>; diff --git a/tests/__swc_snapshots__/tests/jsx_icu.rs/multiple_new_lines_with_nbsp_endind.js b/tests/snapshots/jsx_icu__multiple_new_lines_with_nbsp_endind.snap similarity index 51% rename from tests/__swc_snapshots__/tests/jsx_icu.rs/multiple_new_lines_with_nbsp_endind.js rename to tests/snapshots/jsx_icu__multiple_new_lines_with_nbsp_endind.snap index 161785c..bb39af5 100644 --- a/tests/__swc_snapshots__/tests/jsx_icu.rs/multiple_new_lines_with_nbsp_endind.js +++ b/tests/snapshots/jsx_icu__multiple_new_lines_with_nbsp_endind.snap @@ -1,3 +1,14 @@ +--- +source: tests/jsx_icu.rs +--- +import { Trans } from "@lingui/react/macro"; + + Line ending in non-breaking space.  + text in element +; + +↓ ↓ ↓ ↓ ↓ ↓ + import { Trans as Trans_ } from "@lingui/react"; click + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + }, + message: "click" +}}/>; diff --git a/tests/snapshots/jsx_named_placeholders__allows_hyphenated.snap b/tests/snapshots/jsx_named_placeholders__allows_hyphenated.snap new file mode 100644 index 0000000..6d2b573 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__allows_hyphenated.snap @@ -0,0 +1,18 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +click + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + }, + message: "click" +}}/>; diff --git a/tests/snapshots/jsx_named_placeholders__attribute_ignored_when_not_configured.snap b/tests/snapshots/jsx_named_placeholders__attribute_ignored_when_not_configured.snap new file mode 100644 index 0000000..168bdbd --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__attribute_ignored_when_not_configured.snap @@ -0,0 +1,19 @@ +--- +source: tests/jsx_named_placeholders.rs +info: {} +--- +import { Trans } from "@lingui/react/macro"; + + Hello world! +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + }, + message: "Hello <0>world!" +}}/>; diff --git a/tests/snapshots/jsx_named_placeholders__basic.snap b/tests/snapshots/jsx_named_placeholders__basic.snap new file mode 100644 index 0000000..a97df28 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__basic.snap @@ -0,0 +1,20 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from "@lingui/react/macro"; + + Hello world! +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + }, + message: "Hello world!" +}}/>; diff --git a/tests/snapshots/jsx_named_placeholders__deduplication_different_props.snap b/tests/snapshots/jsx_named_placeholders__deduplication_different_props.snap new file mode 100644 index 0000000..ae5cfb9 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__deduplication_different_props.snap @@ -0,0 +1,16 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_defaults: + a: a +--- +import { Trans } from "@lingui/react/macro"; +Hello link 1, normal, link 2.; + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Multiple distinct JSX elements with the same placeholder name (`a`). Differentiate them by setting `macro.jsxPlaceholderAttribute` in the lingui config and then adding the attribute to your JSX elements (e.g. ``). + --> input.tsx:2:47 + | +2 | Hello link 1, normal, link 2.; + | ^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__deduplication_identical.snap b/tests/snapshots/jsx_named_placeholders__deduplication_identical.snap new file mode 100644 index 0000000..068c830 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__deduplication_identical.snap @@ -0,0 +1,19 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_defaults: + em: em +--- +import { Trans } from "@lingui/react/macro"; +Hello emphasis, normal, more emphasis.; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + }, + message: "Hello emphasis, normal, more emphasis." +}}/>; diff --git a/tests/snapshots/jsx_named_placeholders__deduplication_with_stripped_props.snap b/tests/snapshots/jsx_named_placeholders__deduplication_with_stripped_props.snap new file mode 100644 index 0000000..470e1c6 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__deduplication_with_stripped_props.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from "@lingui/react/macro"; +Hello link 1, normal, link 1 copy and link 2.; + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Multiple distinct JSX elements with the same placeholder name (`link`). Differentiate them by adding/modifying the `_t` attribute (e.g. ``). + --> input.tsx:2:100 + | +2 | Hello link 1, normal, link 1 copy and link 2.; + | ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__defaults.snap b/tests/snapshots/jsx_named_placeholders__defaults.snap new file mode 100644 index 0000000..7c68cea --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__defaults.snap @@ -0,0 +1,23 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_defaults: + a: link + em: em +--- +import { Trans } from "@lingui/react/macro"; + + Here's a link and emphasis. +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; +, + em: + }, + message: "Here's a link and emphasis." +}}/>; diff --git a/tests/snapshots/jsx_named_placeholders__different_spreads_throw.snap b/tests/snapshots/jsx_named_placeholders__different_spreads_throw.snap new file mode 100644 index 0000000..f62855b --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__different_spreads_throw.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +A B + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Multiple distinct JSX elements with the same placeholder name (`same`). Differentiate them by adding/modifying the `_t` attribute (e.g. ``). + --> input.tsx:2:40 + | +2 | A B + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__identical_spreads_reused.snap b/tests/snapshots/jsx_named_placeholders__identical_spreads_reused.snap new file mode 100644 index 0000000..ab37219 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__identical_spreads_reused.snap @@ -0,0 +1,18 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +A B + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + }, + message: "A B" +}}/>; diff --git a/tests/snapshots/jsx_named_placeholders__mixed_explicit_and_defaults.snap b/tests/snapshots/jsx_named_placeholders__mixed_explicit_and_defaults.snap new file mode 100644 index 0000000..49d008d --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__mixed_explicit_and_defaults.snap @@ -0,0 +1,21 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t + jsx_placeholder_defaults: + a: link +--- +import { Trans } from "@lingui/react/macro"; +Hello link 1, normal, link 2.; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; +, + link2: + }, + message: "Hello link 1, normal, link 2." +}}/>; diff --git a/tests/snapshots/jsx_named_placeholders__prop_order.snap b/tests/snapshots/jsx_named_placeholders__prop_order.snap new file mode 100644 index 0000000..6e57f42 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__prop_order.snap @@ -0,0 +1,18 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from "@lingui/react/macro"; +Hello link 1, normal, link 1 copy.; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + }, + message: "Hello link 1, normal, link 1 copy." +}}/>; diff --git a/tests/snapshots/jsx_named_placeholders__prop_order2.snap b/tests/snapshots/jsx_named_placeholders__prop_order2.snap new file mode 100644 index 0000000..d48df23 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__prop_order2.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from "@lingui/react/macro"; +Hello link 1, normal, link 1 copy.; + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Multiple distinct JSX elements with the same placeholder name (`link`). Differentiate them by adding/modifying the `_t` attribute (e.g. ``). + --> input.tsx:2:69 + | +2 | Hello link 1, normal, link 1 copy.; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__same_element_diffrent_attributes_count_throw.snap b/tests/snapshots/jsx_named_placeholders__same_element_diffrent_attributes_count_throw.snap new file mode 100644 index 0000000..dbeaac0 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__same_element_diffrent_attributes_count_throw.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +A and B + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Multiple distinct JSX elements with the same placeholder name (`same`). Differentiate them by adding/modifying the `_t` attribute (e.g. ``). + --> input.tsx:2:47 + | +2 | A and B + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__same_name_different_element_throws.snap b/tests/snapshots/jsx_named_placeholders__same_name_different_element_throws.snap new file mode 100644 index 0000000..f4fe6f8 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__same_name_different_element_throws.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +A and B + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Multiple distinct JSX elements with the same placeholder name (`same`). Differentiate them by adding/modifying the `_t` attribute (e.g. ``). + --> input.tsx:2:33 + | +2 | A and B + | ^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__same_spread_different_order_throws.snap b/tests/snapshots/jsx_named_placeholders__same_spread_different_order_throws.snap new file mode 100644 index 0000000..54523a2 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__same_spread_different_order_throws.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +A B + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Multiple distinct JSX elements with the same placeholder name (`same`). Differentiate them by adding/modifying the `_t` attribute (e.g. ``). + --> input.tsx:2:48 + | +2 | A B + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__stripped_ast.snap b/tests/snapshots/jsx_named_placeholders__stripped_ast.snap new file mode 100644 index 0000000..ff9a966 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__stripped_ast.snap @@ -0,0 +1,20 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from "@lingui/react/macro"; + + About +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + }, + message: "About" +}}/>; diff --git a/tests/snapshots/jsx_named_placeholders__supports_string_in_jsx_expression.snap b/tests/snapshots/jsx_named_placeholders__supports_string_in_jsx_expression.snap new file mode 100644 index 0000000..b91d8c0 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__supports_string_in_jsx_expression.snap @@ -0,0 +1,18 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +click + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as Trans_ } from "@lingui/react"; + + }, + message: "click" +}}/>; diff --git a/tests/snapshots/jsx_named_placeholders__throws_ending_with_dot.snap b/tests/snapshots/jsx_named_placeholders__throws_ending_with_dot.snap new file mode 100644 index 0000000..5bcf605 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__throws_ending_with_dot.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +click + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Placeholder name `foo.` is not valid. Names must start and end with a letter/digit/underscore, but may contain `.-` in between. + --> input.tsx:2:8 + | +2 | click + | ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__throws_on_boolean_expr.snap b/tests/snapshots/jsx_named_placeholders__throws_on_boolean_expr.snap new file mode 100644 index 0000000..d5c6080 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__throws_on_boolean_expr.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +click + +↓ ↓ ↓ ↓ ↓ ↓ + +error: The `_t` attribute must be a non-empty string literal. + --> input.tsx:2:8 + | +2 | click + | ^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__throws_on_empty_attribute_value.snap b/tests/snapshots/jsx_named_placeholders__throws_on_empty_attribute_value.snap new file mode 100644 index 0000000..cc1eb94 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__throws_on_empty_attribute_value.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +click + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Placeholder name `` is not valid. Names must start and end with a letter/digit/underscore, but may contain `.-` in between. + --> input.tsx:2:8 + | +2 | click + | ^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__throws_on_empty_string.snap b/tests/snapshots/jsx_named_placeholders__throws_on_empty_string.snap new file mode 100644 index 0000000..cc1eb94 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__throws_on_empty_string.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +click + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Placeholder name `` is not valid. Names must start and end with a letter/digit/underscore, but may contain `.-` in between. + --> input.tsx:2:8 + | +2 | click + | ^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__throws_on_non_string_attribute_value.snap b/tests/snapshots/jsx_named_placeholders__throws_on_non_string_attribute_value.snap new file mode 100644 index 0000000..339cbd3 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__throws_on_non_string_attribute_value.snap @@ -0,0 +1,16 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +const name = "link"; +click + +↓ ↓ ↓ ↓ ↓ ↓ + +error: The `_t` attribute must be a non-empty string literal. + --> input.tsx:3:8 + | +3 | click + | ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__throws_on_numeric_name.snap b/tests/snapshots/jsx_named_placeholders__throws_on_numeric_name.snap new file mode 100644 index 0000000..c636836 --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__throws_on_numeric_name.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +click + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Placeholder name `0` is not allowed because it conflicts with auto-generated numeric placeholders. Use a non-numeric name instead. + --> input.tsx:2:8 + | +2 | click + | ^^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/jsx_named_placeholders__throws_starting_with_hyphen.snap b/tests/snapshots/jsx_named_placeholders__throws_starting_with_hyphen.snap new file mode 100644 index 0000000..2219efb --- /dev/null +++ b/tests/snapshots/jsx_named_placeholders__throws_starting_with_hyphen.snap @@ -0,0 +1,15 @@ +--- +source: tests/jsx_named_placeholders.rs +info: + jsx_placeholder_attribute: _t +--- +import { Trans } from '@lingui/react/macro'; +click + +↓ ↓ ↓ ↓ ↓ ↓ + +error: Placeholder name `-foo` is not valid. Names must start and end with a letter/digit/underscore, but may contain `.-` in between. + --> input.tsx:2:8 + | +2 | click + | ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/snapshots/runtime_config__should_use_provided_runtime_modules.snap b/tests/snapshots/runtime_config__should_use_provided_runtime_modules.snap new file mode 100644 index 0000000..4cdd1da --- /dev/null +++ b/tests/snapshots/runtime_config__should_use_provided_runtime_modules.snap @@ -0,0 +1,32 @@ +--- +source: tests/runtime_config.rs +info: + runtime_modules: + i18n: + - "./custom-core" + - customI18n + trans: + - "./custom-react" + - CustomTrans + use_lingui: + - "./custom-react" + - useLingui2 +--- +import { t } from "@lingui/core/macro"; +import { Trans } from "@lingui/react/macro"; + +t`Refresh inbox`; +const exp2 = Refresh inbox; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { CustomTrans as Trans_ } from "./custom-react"; +import { customI18n as $_i18n } from "./custom-core"; +$_i18n._(/*i18n*/ { + id: "EsCV2T", + message: "Refresh inbox" +}); +const exp2 = ; diff --git a/tests/__swc_snapshots__/tests/use_lingui.rs/js_use_lingui_hook.js b/tests/snapshots/use_lingui__js_use_lingui_hook.snap similarity index 53% rename from tests/__swc_snapshots__/tests/use_lingui.rs/js_use_lingui_hook.js rename to tests/snapshots/use_lingui__js_use_lingui_hook.snap index 2b9f8a9..5f50f73 100644 --- a/tests/__swc_snapshots__/tests/use_lingui.rs/js_use_lingui_hook.js +++ b/tests/snapshots/use_lingui__js_use_lingui_hook.snap @@ -1,3 +1,19 @@ +--- +source: tests/use_lingui.rs +--- +import { useLingui } from "@lingui/react/macro"; + +const bla1 = () => { + console.log() +} + + function bla() { + const { t, i18n } = useLingui(); + t`Refresh inbox`; + } + +↓ ↓ ↓ ↓ ↓ ↓ + import { useLingui as $_useLingui } from "@lingui/react"; const bla1 = ()=>{ console.log(); diff --git a/tests/__swc_snapshots__/tests/use_lingui.rs/should_process_macro_with_matching_name_in_correct_scopes.js b/tests/snapshots/use_lingui__should_process_macro_with_matching_name_in_correct_scopes.snap similarity index 57% rename from tests/__swc_snapshots__/tests/use_lingui.rs/should_process_macro_with_matching_name_in_correct_scopes.js rename to tests/snapshots/use_lingui__should_process_macro_with_matching_name_in_correct_scopes.snap index 236366c..3a16cdb 100644 --- a/tests/__swc_snapshots__/tests/use_lingui.rs/should_process_macro_with_matching_name_in_correct_scopes.js +++ b/tests/snapshots/use_lingui__should_process_macro_with_matching_name_in_correct_scopes.snap @@ -1,3 +1,25 @@ +--- +source: tests/use_lingui.rs +--- +import { useLingui } from '@lingui/react/macro'; + +function MyComponent() { + const { t } = useLingui(); + const a = t`Text`; + + { + // here is child scope with own "t" binding, shouldn't be processed + const t = () => {}; + t`Text`; + } + { + // here is child scope which should be processed, since 't' relates to outer scope + t`Text`; + } +} + +↓ ↓ ↓ ↓ ↓ ↓ + import { useLingui as $_useLingui } from "@lingui/react"; function MyComponent() { const { i18n: $__i18n, _: $__ } = $_useLingui(); diff --git a/tests/snapshots/use_lingui__support_nested_macro.snap b/tests/snapshots/use_lingui__support_nested_macro.snap new file mode 100644 index 0000000..9744b6d --- /dev/null +++ b/tests/snapshots/use_lingui__support_nested_macro.snap @@ -0,0 +1,29 @@ +--- +source: tests/use_lingui.rs +--- +import { useLingui } from '@lingui/react/macro'; +import { plural } from '@lingui/core/macro'; + +function MyComponent() { + const { t } = useLingui(); + const a = t`Text ${plural(users.length, { + offset: 1, + 0: "No books", + 1: "1 book", + other: "\# books" + })}`; +} + +↓ ↓ ↓ ↓ ↓ ↓ + +import { useLingui as $_useLingui } from "@lingui/react"; +function MyComponent() { + const { i18n: $__i18n, _: $__ } = $_useLingui(); + const a = $__i18n._(/*i18n*/ { + id: "hJRCh6", + message: "Text {0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}", + values: { + 0: users.length + } + }); +} diff --git a/tests/snapshots/use_lingui__support_nested_macro_when_in_arrow_function_issue_2095.snap b/tests/snapshots/use_lingui__support_nested_macro_when_in_arrow_function_issue_2095.snap new file mode 100644 index 0000000..c2c6ba0 --- /dev/null +++ b/tests/snapshots/use_lingui__support_nested_macro_when_in_arrow_function_issue_2095.snap @@ -0,0 +1,29 @@ +--- +source: tests/use_lingui.rs +--- +import { plural } from '@lingui/core/macro' +import { useLingui } from '@lingui/react/macro' + +const MyComponent = () => { + const { t } = useLingui(); + const a = t`Text ${plural(users.length, { + offset: 1, + 0: "No books", + 1: "1 book", + other: "\# books" + })}`; +} + +↓ ↓ ↓ ↓ ↓ ↓ + +import { useLingui as $_useLingui } from "@lingui/react"; +const MyComponent = ()=>{ + const { i18n: $__i18n, _: $__ } = $_useLingui(); + const a = $__i18n._(/*i18n*/ { + id: "hJRCh6", + message: "Text {0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}", + values: { + 0: users.length + } + }); +}; diff --git a/tests/__swc_snapshots__/tests/use_lingui.rs/support_passing_t_variable_as_dependency.js b/tests/snapshots/use_lingui__support_passing_t_variable_as_dependency.snap similarity index 56% rename from tests/__swc_snapshots__/tests/use_lingui.rs/support_passing_t_variable_as_dependency.js rename to tests/snapshots/use_lingui__support_passing_t_variable_as_dependency.snap index f8b2407..3e7095e 100644 --- a/tests/__swc_snapshots__/tests/use_lingui.rs/support_passing_t_variable_as_dependency.js +++ b/tests/snapshots/use_lingui__support_passing_t_variable_as_dependency.snap @@ -1,3 +1,15 @@ +--- +source: tests/use_lingui.rs +--- +import { useLingui } from '@lingui/react/macro'; + +function MyComponent() { + const { t } = useLingui(); + const a = useMemo(() => t`Text`, [t]); +} + +↓ ↓ ↓ ↓ ↓ ↓ + import { useLingui as $_useLingui } from "@lingui/react"; function MyComponent() { const { i18n: $__i18n, _: $__ } = $_useLingui(); diff --git a/tests/__swc_snapshots__/tests/use_lingui.rs/support_renamed_destructuring.js b/tests/snapshots/use_lingui__support_renamed_destructuring.snap similarity index 54% rename from tests/__swc_snapshots__/tests/use_lingui.rs/support_renamed_destructuring.js rename to tests/snapshots/use_lingui__support_renamed_destructuring.snap index ea8b622..3258259 100644 --- a/tests/__swc_snapshots__/tests/use_lingui.rs/support_renamed_destructuring.js +++ b/tests/snapshots/use_lingui__support_renamed_destructuring.snap @@ -1,3 +1,15 @@ +--- +source: tests/use_lingui.rs +--- +import { useLingui } from '@lingui/react/macro'; + +function MyComponent() { + const { t: _ } = useLingui(); + const a = _`Text`; +} + +↓ ↓ ↓ ↓ ↓ ↓ + import { useLingui as $_useLingui } from "@lingui/react"; function MyComponent() { const { i18n: $__i18n, _: $__ } = $_useLingui(); diff --git a/tests/snapshots/use_lingui__work_when_t_is_not_used.snap b/tests/snapshots/use_lingui__work_when_t_is_not_used.snap new file mode 100644 index 0000000..625b1b8 --- /dev/null +++ b/tests/snapshots/use_lingui__work_when_t_is_not_used.snap @@ -0,0 +1,17 @@ +--- +source: tests/use_lingui.rs +--- +import { useLingui } from '@lingui/react/macro'; + +function MyComponent() { + const { i18n } = useLingui(); + console.log(i18n); +} + +↓ ↓ ↓ ↓ ↓ ↓ + +import { useLingui as $_useLingui } from "@lingui/react"; +function MyComponent() { + const { i18n, _: $__ } = $_useLingui(); + console.log(i18n); +} diff --git a/tests/__swc_snapshots__/tests/use_lingui.rs/work_with_components_defined_as_arrow_function.js b/tests/snapshots/use_lingui__work_with_components_defined_as_arrow_function.snap similarity index 54% rename from tests/__swc_snapshots__/tests/use_lingui.rs/work_with_components_defined_as_arrow_function.js rename to tests/snapshots/use_lingui__work_with_components_defined_as_arrow_function.snap index 5100fc5..6379c8d 100644 --- a/tests/__swc_snapshots__/tests/use_lingui.rs/work_with_components_defined_as_arrow_function.js +++ b/tests/snapshots/use_lingui__work_with_components_defined_as_arrow_function.snap @@ -1,3 +1,15 @@ +--- +source: tests/use_lingui.rs +--- +import { useLingui } from '@lingui/react/macro'; + +const MyComponent = () => { + const { t } = useLingui(); + const a = t`Text`; +} + +↓ ↓ ↓ ↓ ↓ ↓ + import { useLingui as $_useLingui } from "@lingui/react"; const MyComponent = ()=>{ const { i18n: $__i18n, _: $__ } = $_useLingui(); diff --git a/tests/__swc_snapshots__/tests/use_lingui.rs/work_with_existing_use_lingui_statement.js b/tests/snapshots/use_lingui__work_with_existing_use_lingui_statement.snap similarity index 51% rename from tests/__swc_snapshots__/tests/use_lingui.rs/work_with_existing_use_lingui_statement.js rename to tests/snapshots/use_lingui__work_with_existing_use_lingui_statement.snap index 6cbd52a..dae7280 100644 --- a/tests/__swc_snapshots__/tests/use_lingui.rs/work_with_existing_use_lingui_statement.js +++ b/tests/snapshots/use_lingui__work_with_existing_use_lingui_statement.snap @@ -1,3 +1,19 @@ +--- +source: tests/use_lingui.rs +--- +import { useLingui as useLinguiMacro } from '@lingui/react/macro'; +import { useLingui } from '@lingui/react'; + +function MyComponent() { + const { _ } = useLingui(); + + console.log(_); + const { t } = useLinguiMacro(); + const a = t`Text`; +} + +↓ ↓ ↓ ↓ ↓ ↓ + import { useLingui as $_useLingui } from "@lingui/react"; import { useLingui } from '@lingui/react'; function MyComponent() { diff --git a/tests/__swc_snapshots__/tests/use_lingui.rs/work_with_multiple_react_components.js b/tests/snapshots/use_lingui__work_with_multiple_react_components.snap similarity index 59% rename from tests/__swc_snapshots__/tests/use_lingui.rs/work_with_multiple_react_components.js rename to tests/snapshots/use_lingui__work_with_multiple_react_components.snap index cacb131..6bbe8ec 100644 --- a/tests/__swc_snapshots__/tests/use_lingui.rs/work_with_multiple_react_components.js +++ b/tests/snapshots/use_lingui__work_with_multiple_react_components.snap @@ -1,3 +1,20 @@ +--- +source: tests/use_lingui.rs +--- +import { useLingui } from '@lingui/react/macro'; + +function MyComponent() { + const { t } = useLingui(); + const a = t`Text`; +} + +function MyComponent2() { + const { t } = useLingui(); + const b = t`Text`; +} + +↓ ↓ ↓ ↓ ↓ ↓ + import { useLingui as $_useLingui } from "@lingui/react"; function MyComponent() { const { i18n: $__i18n, _: $__ } = $_useLingui(); From 367c85d171655e42ba6b1e6379e6513991a11c7d Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:05:42 +0200 Subject: [PATCH 10/14] add more ai docs --- .agents/SWC_TRANSFORM_TESTING.md | 374 +++++++++++++++++++++++++++++++ CLAUDE.md | 4 + 2 files changed, 378 insertions(+) create mode 100644 .agents/SWC_TRANSFORM_TESTING.md diff --git a/.agents/SWC_TRANSFORM_TESTING.md b/.agents/SWC_TRANSFORM_TESTING.md new file mode 100644 index 0000000..cb7a09e --- /dev/null +++ b/.agents/SWC_TRANSFORM_TESTING.md @@ -0,0 +1,374 @@ +# Running SWC transforms outside the plugin host + +This document explains how to parse, transform, and emit JavaScript/TypeScript code using SWC's Rust API **without** the SWC plugin host (WASM runtime). This is what powers our test suite, but the techniques apply any time you want to run an SWC `Fold`/`VisitMut` pass in a standalone Rust binary or test harness. + +## Required `swc_core` features + +```toml +swc_core = { version = "50.2.3", features = [ + "ecma_ast", # AST types, Program, Pass trait + "ecma_parser", # Parser, Lexer, Syntax + "ecma_visit", # Fold, VisitMut, FoldWith, fold_pass() + "ecma_codegen", # Emitter, to_code_default() + "ecma_utils", # pulls in swc_ecma_transforms_base (resolver, hygiene, fixer) + "common", # SourceMap, GLOBALS, HANDLER, Mark, Comments +] } +``` + +The `ecma_utils` feature transitively enables `swc_ecma_transforms_base`, which provides the standard passes: `resolver`, `hygiene`, and `fixer`. + +The `ecma_codegen` feature provides `to_code_default()` and the lower-level `Emitter` + `JsWriter` for turning an AST back into a string. + +> **You do NOT need `testing_transform`.** That feature pulls in the entire `swc_ecma_transforms_testing` crate (and its transitive deps: `testing`, `swc_error_reporters`, `ansi_term`, `base64`, `tempfile`, Node.js exec support, etc.). Everything it does can be inlined in ~60 lines. + +## Thread-local globals + +SWC uses two scoped thread-locals that must be set before any transform work: + +### `GLOBALS` + +```rust +use swc_core::common::{Globals, GLOBALS}; + +GLOBALS.set(&Globals::new(), || { + // All SWC work goes here +}); +``` + +`Mark::new()` (used by the resolver and by hygiene) allocates from a global arena stored in `GLOBALS`. If `GLOBALS` is not set, `Mark::new()` will panic. + +### `HANDLER` + +```rust +use swc_core::common::errors::{Handler, HandlerFlags, HANDLER}; + +let handler = Handler::with_emitter_and_flags( + Box::new(my_emitter), + HandlerFlags { + can_emit_warnings: true, + ..Default::default() + }, +); + +HANDLER.set(&handler, || { + // Transforms that emit diagnostics go here +}); +``` + +Plugin code (and some built-in SWC transforms) report errors via `HANDLER.with(|h| h.struct_span_err(span, msg).emit())`. If `HANDLER` is not set, those calls panic with a "HANDLER not set" message. + +**`HANDLER` must be nested inside `GLOBALS`** — both must be active simultaneously. + +#### Handler behavior flags + +| Flag | Default | Effect | +|---|---|---| +| `can_emit_warnings` | `true` | Whether warning-level diagnostics are emitted | +| `treat_err_as_bug` | `false` | If `true`, any error immediately panics via `bug!()` | +| `continue_after_error` | `true` (Cell) | If `true`, errors are collected and execution continues. If `false`, the first error triggers `FatalError.raise()` which calls `panic::resume_unwind` | + +For tests, the default flags work well: errors are collected silently, and you check for them after the transform completes. + +#### Custom error emitters + +The `Handler` accepts a `Box` where `Emitter` is `swc_core::common::errors::Emitter` (not the codegen `Emitter`). This is how you capture error messages programmatically: + +```rust +use std::sync::{Arc, Mutex}; + +struct StringEmitter { + buffer: Arc>, +} + +impl swc_core::common::errors::Emitter for StringEmitter { + fn emit(&mut self, db: &mut swc_core::common::errors::DiagnosticBuilder<'_>) { + // DiagnosticBuilder derefs to Diagnostic + // Diagnostic.message is Vec where Message(pub String, pub Style) + let msg: String = db.message.iter().map(|m| m.0.as_str()).collect::>().join(""); + let mut buf = self.buffer.lock().unwrap(); + if !buf.is_empty() { + buf.push('\n'); + } + buf.push_str(&msg); + } +} +``` + +The `Arc>` gives you a handle to read errors after the transform. This is preferable to `Handler::with_tty_emitter()` when you need to inspect or assert on error output. + +> **Caveat:** The `Emitter` trait requires `Send`. `Rc<...>` won't work. Use `Arc>` or `Arc>`. + +## Parsing + +```rust +use swc_core::common::{FileName, SourceMap, sync::Lrc}; +use swc_core::common::comments::SingleThreadedComments; +use swc_core::ecma::parser::{lexer::Lexer, Parser, StringInput, Syntax, TsSyntax}; + +let cm: Lrc = Default::default(); +let comments = SingleThreadedComments::default(); + +let syntax = Syntax::Typescript(TsSyntax { + tsx: true, + ..Default::default() +}); + +let fm = cm.new_source_file( + FileName::Real("input.tsx".into()).into(), + input.to_string(), +); + +let lexer = Lexer::new( + syntax, + Default::default(), // EsVersion — latest + StringInput::from(&*fm), + Some(&comments), // attaches comments to the AST +); + +let mut parser = Parser::new_from(lexer); +let program: Program = parser.parse_program().expect("parse error"); +``` + +**Key points:** + +- `SourceMap` (`Lrc`) is required. It maps byte positions in the AST back to source locations. Even if you don't need source maps in output, the parser and codegen need it. +- `SingleThreadedComments` stores comments (leading/trailing) associated with AST nodes. Pass it to the parser, your transforms, and the code emitter to preserve `/* ... */` and `// ...` comments through the pipeline. +- `parse_program()` auto-detects Module vs Script. Use `parse_module()` or `parse_script()` if you need a specific mode. +- Parse errors are returned as `Result`, not emitted through `HANDLER`. The parser does **not** require `HANDLER` to be set (though it won't hurt). + +### Routing parse errors through HANDLER + +The parser returns errors as `Result`, but you can convert them to diagnostics emitted through `HANDLER`. This is useful when you want parse errors to be captured by a custom `Emitter` alongside transform errors, rather than causing a hard panic: + +```rust +let mut parser = Parser::new_from(lexer); +let program = match parser.parse_program() { + Ok(program) => program, + Err(e) => { + // Convert the parse error to a diagnostic and emit it through HANDLER + e.into_diagnostic(&handler).emit(); + for e in parser.take_errors() { + e.into_diagnostic(&handler).emit(); + } + return None; // or however you signal failure + } +}; +// Also emit any non-fatal parse errors (warnings, recoverable errors) +for e in parser.take_errors() { + e.into_diagnostic(&handler).emit(); +} +``` + +This pattern (from SWC's `Tester::with_parser`) is what we use in `tests/common/mod.rs` so that `to_panic!` tests can capture parse errors in snapshots just like transform errors. + +## Applying transforms + +SWC has two transform APIs: the older `Fold` trait (immutable, returns new nodes) and the newer `VisitMut`/`Pass` trait (mutable in-place). Both are actively used. + +### The `Pass` trait + +```rust +// Defined in swc_core::ecma::ast +pub trait Pass { + fn process(&mut self, program: &mut Program); +} +``` + +`Program` has an `.apply()` convenience method: + +```rust +let program = program.apply(my_pass); // consumes and returns Program +``` + +### Wrapping `Fold` into `Pass` + +If your transform implements `Fold` (like `LinguiMacroFolder`), wrap it: + +```rust +use swc_core::ecma::visit::fold_pass; + +let program = program.apply(fold_pass(MyFolder::new())); +``` + +`fold_pass()` returns an opaque type implementing `Pass`. + +### Standard passes + +All three standard passes return `impl Pass + VisitMut`: + +```rust +use swc_core::ecma::transforms::base::{resolver, hygiene, fixer}; +use swc_core::common::Mark; + +// 1. Resolver — assigns unique Marks to identifiers for scope tracking +// Requires GLOBALS to be set (Mark::new() allocates from it) +let program = program.apply(resolver( + Mark::new(), // unresolved_mark — for free identifiers + Mark::new(), // top_level_mark — for top-level declarations + true, // typescript mode +)); + +// 2. Your transform +let program = program.apply(fold_pass(MyTransform::new())); + +// 3. Hygiene — renames identifiers to avoid conflicts introduced by transforms +let program = program.apply(hygiene::hygiene()); + +// 4. Fixer — corrects operator precedence, adds necessary parentheses +let program = program.apply(fixer::fixer(Some(&comments))); +``` + +### Pass composition + +Tuples of `Pass` implement `Pass` (up to 12 elements): + +```rust +let program = program.apply(( + resolver(Mark::new(), Mark::new(), true), + fold_pass(MyTransform::new()), +)); +``` + +### Ordering caveats + +- **Resolver must come first** if your transform relies on identifier scoping (e.g., distinguishing local `t` from imported `t`). Without it, all identifiers with the same name look identical. +- **Hygiene must come after your transform.** It cleans up naming conflicts your transform may have introduced (e.g., injecting an `i18n` import that clashes with a user's `i18n` variable). +- **Fixer must come last.** It fixes the AST for correct code emission (adds parens for precedence, etc.). Running transforms after fixer can undo its corrections. +- **Fixer takes `Option<&dyn Comments>`** so it can adjust comment positions when it restructures the AST. Always pass your comments reference. + +## Code emission + +### Simple: `to_code_default` + +```rust +use swc_core::ecma::codegen::to_code_default; + +let code: String = to_code_default(cm.clone(), Some(&comments), &program); +``` + +This is a one-liner that creates an `Emitter` + `JsWriter` internally, emits the program, and returns a `String`. Good enough for tests and most use cases. + +### Manual: `Emitter` + `JsWriter` + +For source map output or custom writer config: + +```rust +use swc_core::ecma::codegen::{Emitter, text_writer::JsWriter}; + +let mut buf = Vec::new(); +let mut src_map = Vec::new(); // (BytePos, LineCol) pairs + +{ + let mut emitter = Emitter { + cfg: Default::default(), + cm: cm.clone(), + comments: Some(&comments), + wr: JsWriter::new( + cm.clone(), + "\n", // line separator + &mut buf, + Some(&mut src_map), // None to skip source map + ), + }; + emitter.emit_program(&program).unwrap(); +} + +let output = String::from_utf8(buf).unwrap(); +``` + +> **Naming collision:** `swc_core::ecma::codegen::Emitter` (code emission struct) and `swc_core::common::errors::Emitter` (diagnostic trait) are completely different types that happen to share a name. If you use both, alias one: `use swc_core::common::errors::Emitter as DiagEmitter;` + +## Complete example + +Putting it all together — a function that parses TSX, applies a `Fold` transform, and returns the output code (or collected error messages): + +```rust +use std::sync::{Arc, Mutex}; +use swc_core::common::comments::SingleThreadedComments; +use swc_core::common::errors::{Handler, HandlerFlags, HANDLER}; +use swc_core::common::{FileName, Globals, Mark, SourceMap, GLOBALS, sync::Lrc}; +use swc_core::ecma::codegen::to_code_default; +use swc_core::ecma::parser::{lexer::Lexer, Parser, StringInput, Syntax, TsSyntax}; +use swc_core::ecma::transforms::base::{fixer, hygiene, resolver}; +use swc_core::ecma::visit::fold_pass; + +fn run_transform(input: &str) -> Result { + let error_buffer = Arc::new(Mutex::new(String::new())); + let cm: Lrc = Default::default(); + let comments = SingleThreadedComments::default(); + + let handler = Handler::with_emitter_and_flags( + Box::new(StringEmitter { buffer: error_buffer.clone() }), + HandlerFlags { can_emit_warnings: true, ..Default::default() }, + ); + + let syntax = Syntax::Typescript(TsSyntax { tsx: true, ..Default::default() }); + + let output = GLOBALS.set(&Globals::new(), || { + HANDLER.set(&handler, || { + let fm = cm.new_source_file( + FileName::Real("input.tsx".into()).into(), + input.to_string(), + ); + let lexer = Lexer::new(syntax, Default::default(), StringInput::from(&*fm), Some(&comments)); + let mut parser = Parser::new_from(lexer); + let program = parser.parse_program().expect("parse failed"); + + let program = program + .apply(resolver(Mark::new(), Mark::new(), true)) + .apply(fold_pass(MyTransform::new())) + .apply(hygiene::hygiene()) + .apply(fixer::fixer(Some(&comments))); + + to_code_default(cm.clone(), Some(&comments), &program) + }) + }); + + let errors = error_buffer.lock().unwrap().clone(); + if errors.is_empty() { Ok(output) } else { Err(errors) } +} +``` + +## How `swc_ecma_transforms_testing` does it (and why we don't use it) + +The `testing_transform` feature of `swc_core` pulls in `swc_ecma_transforms_testing`, which provides `test!()` and `test_inlined_transform()`. Under the hood: + +1. `test!` is a macro that generates a `#[test]` fn calling `test_inlined_transform`. +2. `test_inlined_transform` uses `#[track_caller]` to derive the snapshot file path from the calling file location: `tests/__swc_snapshots__/{test_file}.rs/{test_name}.js`. +3. It delegates to `test_fixture_inner`, which: + - Creates a `Tester` (wraps `SourceMap`, `Handler`, `Rc`) + - Sets `GLOBALS`, `HANDLER`, and `HELPERS` thread-locals + - Parses input, applies the transform pass, then `hygiene()` + `fixer()` + - Emits code via `Emitter` + `JsWriter` (manually, not `to_code_default`) + - Compares output to a snapshot file using `NormalizedOutput::compare_to_file` + - Supports `UPDATE=1` env var to overwrite snapshots + +This is roughly 200 lines of code, but it pulls in a large dependency tree (the `testing` crate with graphical error reporters, the `swc_ecma_testing` crate with Node.js execution support, etc.). The actual transform logic can be inlined in ~60 lines (see `tests/common/mod.rs` in this repo). + +### What `HELPERS` is (and why we skip it) + +`swc_ecma_transforms_testing` also sets `HELPERS` (from `swc_ecma_transforms_base::helpers`). This thread-local tracks which runtime helpers a transform needs to inject (e.g., `_class_call_check`, `_inherits` for class transpilation). Our plugin doesn't use runtime helpers, so we don't set it. If your transform calls `helper!()` macros, you'll need: + +```rust +use swc_core::ecma::transforms::base::helpers::HELPERS; + +HELPERS.set(&Default::default(), || { + // transform code here +}); +``` + +## SWC error handling model + +SWC transforms report errors as **side effects** through the `HANDLER` thread-local, not by returning `Result`. This is a deliberate design choice — the `Fold` and `VisitMut` traits don't have `Result` return types. + +The flow: +1. Transform calls `HANDLER.with(|h| h.struct_span_err(span, "message").emit())` +2. The handler's emitter receives the diagnostic +3. Depending on `HandlerFlags`: + - `continue_after_error: true` (default) — error is recorded, execution continues + - `continue_after_error: false` — `abort_if_errors()` is called, which triggers `FatalError.raise()` → `panic::resume_unwind(Box::new(FatalErrorMarker))` + - `treat_err_as_bug: true` — immediate panic via `bug!()` + +**Caveat:** `FatalError.raise()` uses `resume_unwind`, not `panic!()`. The panic payload is a `FatalErrorMarker` struct, not a string. If you use `catch_unwind`, you won't get a human-readable message from the panic itself — you need to capture it from the emitter. + +This is why our test helper uses a custom `StringEmitter`: it collects error text in a buffer, then checks the buffer after the transform completes. The `to_panic!` test macro uses `Result::expect_err()` on the buffered errors rather than `#[should_panic]`. diff --git a/CLAUDE.md b/CLAUDE.md index 8388383..39a8364 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,6 +61,10 @@ Snapshots live in `tests/snapshots/` and contain input + `↓ ↓ ↓ ↓ ↓ To update snapshots use `INSTA_UPDATE=always cargo test` command or `cargo insta test --review` to review them interactively +## Reference + +- [SWC Transform Testing](.agents/SWC_TRANSFORM_TESTING.md) — how to parse, transform, and emit code using SWC's Rust API outside the plugin host (thread-locals, error handling, pass ordering, codegen). + ## Toolchain - Rust 1.85 pinned in `rust-toolchain.toml` From 13f4a459b458f676971ba17af619eed4fe411162 Mon Sep 17 00:00:00 2001 From: Timofei Iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:11:00 +0200 Subject: [PATCH 11/14] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/builder.rs | 2 +- tests/jsx_named_placeholders.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index b6cd296..630d262 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -181,7 +181,7 @@ impl<'a> MessageBuilder<'a> { base_name = attr_value; - el.attrs = omit_jsx_attrs(el.attrs, HashSet::from([attr_name.as_str()])) + el.attrs = omit_jsx_attrs(el.attrs, HashSet::from([attr_name.as_str()])); } if base_name.is_none() { diff --git a/tests/jsx_named_placeholders.rs b/tests/jsx_named_placeholders.rs index 972b138..6d220f2 100644 --- a/tests/jsx_named_placeholders.rs +++ b/tests/jsx_named_placeholders.rs @@ -240,7 +240,7 @@ import { Trans } from '@lingui/react/macro'; ); to_panic!( - same_element_diffrent_attributes_count_throw, + same_element_different_attributes_count_throws, LinguiOptions { jsx_placeholder_attribute: Some("_t".into()), ..Default::default() From 9a67f0d80cab7a96d0f9f0d8ec4b6d3e66d4d0aa Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:11:26 +0200 Subject: [PATCH 12/14] delete extra case --- tests/jsx_named_placeholders.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/jsx_named_placeholders.rs b/tests/jsx_named_placeholders.rs index 6d220f2..3b26a27 100644 --- a/tests/jsx_named_placeholders.rs +++ b/tests/jsx_named_placeholders.rs @@ -287,18 +287,6 @@ import { Trans } from '@lingui/react/macro'; "# ); -to_panic!( - throws_on_empty_string, - LinguiOptions { - jsx_placeholder_attribute: Some("_t".into()), - ..Default::default() - }, - r#" -import { Trans } from '@lingui/react/macro'; -click - "# -); - to!( supports_string_in_jsx_expression, LinguiOptions { From ae9eb88f56246ed8cdba165b5c3ef490e1f70a9d Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:16:18 +0200 Subject: [PATCH 13/14] fixes --- .gitignore | 1 + ...lement_different_attributes_count_throws.snap} | 0 ...amed_placeholders__throws_on_empty_string.snap | 15 --------------- 3 files changed, 1 insertion(+), 15 deletions(-) rename tests/snapshots/{jsx_named_placeholders__same_element_diffrent_attributes_count_throw.snap => jsx_named_placeholders__same_element_different_attributes_count_throws.snap} (100%) delete mode 100644 tests/snapshots/jsx_named_placeholders__throws_on_empty_string.snap diff --git a/.gitignore b/.gitignore index 96ea0a7..d5a5977 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ lcov.info node_modules .yarn .swc +.new diff --git a/tests/snapshots/jsx_named_placeholders__same_element_diffrent_attributes_count_throw.snap b/tests/snapshots/jsx_named_placeholders__same_element_different_attributes_count_throws.snap similarity index 100% rename from tests/snapshots/jsx_named_placeholders__same_element_diffrent_attributes_count_throw.snap rename to tests/snapshots/jsx_named_placeholders__same_element_different_attributes_count_throws.snap diff --git a/tests/snapshots/jsx_named_placeholders__throws_on_empty_string.snap b/tests/snapshots/jsx_named_placeholders__throws_on_empty_string.snap deleted file mode 100644 index cc1eb94..0000000 --- a/tests/snapshots/jsx_named_placeholders__throws_on_empty_string.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: tests/jsx_named_placeholders.rs -info: - jsx_placeholder_attribute: _t ---- -import { Trans } from '@lingui/react/macro'; -click - -↓ ↓ ↓ ↓ ↓ ↓ - -error: Placeholder name `` is not valid. Names must start and end with a letter/digit/underscore, but may contain `.-` in between. - --> input.tsx:2:8 - | -2 | click - | ^^^^^^^^^^^^^^^^^^ From 3a836c6f6d22afbcbb94d06d469546201e862cf6 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:18:23 +0200 Subject: [PATCH 14/14] fixes --- src/options.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/options.rs b/src/options.rs index 4bdd370..197ccd1 100644 --- a/src/options.rs +++ b/src/options.rs @@ -5,10 +5,11 @@ fn is_default(t: &T) -> bool { t == &T::default() } -#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Default)] #[serde(rename_all = "kebab-case")] pub enum DescriptorFields { Auto, + #[default] All, IdOnly, Message, @@ -28,12 +29,6 @@ impl DescriptorFields { } } -impl Default for DescriptorFields { - fn default() -> Self { - DescriptorFields::All - } -} - #[derive(Deserialize, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub struct LinguiJsOptions {

+ My name is {" "} + {name} +