Skip to content

Commit 0fdcc91

Browse files
feat!: Consolidate Metadata Transformation Options into descriptorFields
1 parent 0e5c2d9 commit 0fdcc91

19 files changed

Lines changed: 284 additions & 54 deletions

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,9 @@ https://swc.rs/docs/configuration/swcrc
5050
// "i18n": ["@lingui/core", "i18n"],
5151
// "trans": ["@lingui/react", "Trans"]
5252
// }
53-
// Lingui strips non-essential fields in production builds for performance.
54-
// Docs https://lingui.dev/guides/optimizing-bundle-size
55-
// You can override the default behavior with:
56-
// "stripNonEssentialFields": false/true
53+
//
54+
// Optional. Controls which descriptor fields are preserved in output.
55+
// "descriptorFields": "auto" (default) | "all" | "id-only" | "message"
5756

5857
// Compatibility option allows to use v6.* SWC Plugin release channel with @lingui/cli@5.*
5958
// Controls the BASE64 alphabet used for generating message IDs.
@@ -70,6 +69,17 @@ https://swc.rs/docs/configuration/swcrc
7069
}
7170
```
7271

72+
### `descriptorFields`
73+
74+
Controls which fields are preserved in the transformed message descriptors. Accepts one of:
75+
76+
- **`"auto"`** (default) — In production (`NODE_ENV=production`), behaves like `"id-only"`. Otherwise, behaves like `"all"`.
77+
- **`"all"`** — Keeps `id`, `message`, `context`, and `comment`. Use this for extraction (replaces the old `extract: true` from the Babel plugin).
78+
- **`"id-only"`** — Keeps only the `id`. Most optimized for production bundles.
79+
- **`"message"`** — Keeps `id`, `message`, and `context` (but not `comment`). Useful when you need message content at runtime.
80+
81+
Check [this article](https://lingui.dev/guides/optimizing-bundle-size) for more info about this configuration.
82+
7383
Or Next JS Usage:
7484

7585
`next.config.js`

src/js_macro_folder.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ where
4343
.into(),
4444
)];
4545

46-
if !self.ctx.options.strip_non_essential_fields {
46+
if self.ctx.options.descriptor_fields.should_keep_message() {
4747
props.push(create_key_value_prop("message", parsed.message));
4848
}
4949

@@ -104,8 +104,10 @@ where
104104
if let Expr::Object(obj) = *expr {
105105
let id_prop = get_object_prop(&obj.props, "id");
106106

107-
let context_val = get_object_prop(&obj.props, "context")
108-
.and_then(|prop| get_expr_as_string(&prop.value));
107+
let context_prop = get_object_prop(&obj.props, "context");
108+
let context_val = context_prop.and_then(|prop| get_expr_as_string(&prop.value));
109+
110+
let comment_prop = get_object_prop(&obj.props, "comment");
109111

110112
let message_prop = get_object_prop(&obj.props, "message");
111113

@@ -134,7 +136,7 @@ where
134136
))
135137
}
136138

137-
if !self.ctx.options.strip_non_essential_fields {
139+
if self.ctx.options.descriptor_fields.should_keep_message() {
138140
new_props.push(create_key_value_prop("message", parsed.message));
139141
}
140142

@@ -143,16 +145,17 @@ where
143145
}
144146
}
145147

146-
if !self.ctx.options.strip_non_essential_fields {
147-
if let Some(context) = context_val {
148-
new_props.push(create_key_value_prop("context", context.into()));
148+
if self.ctx.options.descriptor_fields.should_keep_context() {
149+
if let Some(context_str) = &context_val {
150+
new_props.push(create_key_value_prop("context", context_str.clone().into()));
149151
}
152+
}
150153

151-
let comment = get_object_prop(&obj.props, "comment")
152-
.and_then(|prop| get_expr_as_string(&prop.value));
153-
154-
if let Some(comment) = comment {
155-
new_props.push(create_key_value_prop("comment", comment.into()));
154+
if self.ctx.options.descriptor_fields.should_keep_comment() {
155+
if let Some(prop) = comment_prop {
156+
if let Some(value) = get_expr_as_string(&prop.value) {
157+
new_props.push(create_key_value_prop("comment", value.into()));
158+
}
156159
}
157160
}
158161

src/lib.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,24 @@ where
152152
message_descriptor_props.push(create_key_value_prop("components", exp));
153153
}
154154

155-
if !self.ctx.options.strip_non_essential_fields {
156-
let comment_attr = get_jsx_attr(&el.opening, "comment")
157-
.and_then(|attr| attr.value.as_ref())
158-
.and_then(get_jsx_attr_value_as_string);
155+
if self.ctx.options.descriptor_fields.should_keep_comment() {
156+
let comment_attr =
157+
get_jsx_attr(&el.opening, "comment").and_then(|attr| attr.value.as_ref());
159158

160-
if let Some(comment) = comment_attr {
161-
message_descriptor_props.push(create_key_value_prop("comment", comment.into()));
159+
if let Some(comment_attr) = comment_attr {
160+
let comment_attr_val = get_jsx_attr_value_as_string(comment_attr).unwrap();
161+
162+
message_descriptor_props.push(create_key_value_prop(
163+
"comment", comment.into(),
164+
));
162165
}
166+
}
163167

168+
if self.ctx.options.descriptor_fields.should_keep_message() {
164169
message_descriptor_props.push(create_key_value_prop("message", parsed.message));
170+
}
165171

172+
if self.ctx.options.descriptor_fields.should_keep_context() {
166173
if let Some(context_attr) = context_attr {
167174
let context_attr_val = get_jsx_attr_value_as_string(context_attr).unwrap();
168175

@@ -177,6 +184,8 @@ where
177184
}
178185
}
179186

187+
188+
180189
let message_descriptor = Expr::Object(ObjectLit {
181190
span: message_dscrptr_span,
182191
props: message_descriptor_props,
@@ -515,7 +524,7 @@ where
515524
}
516525
}
517526

518-
pub use self::options::{LinguiOptions, RuntimeModulesConfigMapNormalized};
527+
pub use self::options::{DescriptorFields, LinguiOptions, RuntimeModulesConfigMapNormalized};
519528

520529
#[plugin_transform]
521530
pub fn process_transform(program: Program, metadata: TransformPluginProgramMetadata) -> Program {

src/options.rs

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,34 @@
11
use serde::Deserialize;
22

3+
#[derive(Deserialize, Debug, PartialEq, Clone)]
4+
#[serde(rename_all = "kebab-case")]
5+
pub enum DescriptorFields {
6+
Auto,
7+
All,
8+
IdOnly,
9+
Message,
10+
}
11+
12+
impl DescriptorFields {
13+
pub fn should_keep_message(&self) -> bool {
14+
matches!(self, DescriptorFields::All | DescriptorFields::Message)
15+
}
16+
17+
pub fn should_keep_context(&self) -> bool {
18+
matches!(self, DescriptorFields::All | DescriptorFields::Message)
19+
}
20+
21+
pub fn should_keep_comment(&self) -> bool {
22+
matches!(self, DescriptorFields::All)
23+
}
24+
}
25+
326
#[derive(Deserialize, Debug, PartialEq)]
427
#[serde(rename_all = "camelCase")]
528
pub struct LinguiJsOptions {
629
runtime_modules: Option<RuntimeModulesConfigMap>,
730
#[serde(default)]
8-
strip_non_essential_fields: Option<bool>,
31+
descriptor_fields: Option<DescriptorFields>,
932
#[serde(default)]
1033
use_lingui_v5_id_generation: Option<bool>,
1134
}
@@ -30,10 +53,19 @@ pub struct RuntimeModulesConfigMapNormalized {
3053

3154
impl LinguiJsOptions {
3255
pub fn into_options(self, env_name: &str) -> LinguiOptions {
56+
let descriptor_fields = match self.descriptor_fields.unwrap_or(DescriptorFields::Auto) {
57+
DescriptorFields::Auto => {
58+
if matches!(env_name, "production") {
59+
DescriptorFields::IdOnly
60+
} else {
61+
DescriptorFields::All
62+
}
63+
}
64+
other => other,
65+
};
66+
3367
LinguiOptions {
34-
strip_non_essential_fields: self
35-
.strip_non_essential_fields
36-
.unwrap_or(matches!(env_name, "production")),
68+
descriptor_fields,
3769
use_lingui_v5_id_generation: self.use_lingui_v5_id_generation.unwrap_or(false),
3870
runtime_modules: RuntimeModulesConfigMapNormalized {
3971
i18n: (
@@ -79,15 +111,15 @@ impl LinguiJsOptions {
79111

80112
#[derive(Debug, Clone)]
81113
pub struct LinguiOptions {
82-
pub strip_non_essential_fields: bool,
114+
pub descriptor_fields: DescriptorFields,
83115
pub runtime_modules: RuntimeModulesConfigMapNormalized,
84116
pub use_lingui_v5_id_generation: bool,
85117
}
86118

87119
impl Default for LinguiOptions {
88120
fn default() -> LinguiOptions {
89121
LinguiOptions {
90-
strip_non_essential_fields: false,
122+
descriptor_fields: DescriptorFields::All,
91123
use_lingui_v5_id_generation: false,
92124
runtime_modules: RuntimeModulesConfigMapNormalized {
93125
i18n: ("@lingui/core".into(), "i18n".into()),
@@ -132,7 +164,7 @@ mod lib_tests {
132164
Some("myUseLingui".into())
133165
)),
134166
}),
135-
strip_non_essential_fields: None,
167+
descriptor_fields: None,
136168
use_lingui_v5_id_generation: None,
137169
}
138170
)
@@ -157,58 +189,106 @@ mod lib_tests {
157189
trans: None,
158190
use_lingui: None,
159191
}),
160-
strip_non_essential_fields: None,
192+
descriptor_fields: None,
161193
use_lingui_v5_id_generation: None,
162194
}
163195
)
164196
}
165197

166198
#[test]
167-
fn test_strip_non_essential_fields_config() {
199+
fn test_descriptor_fields_config() {
200+
let config = serde_json::from_str::<LinguiJsOptions>(
201+
r#"{
202+
"descriptorFields": "id-only",
203+
"runtimeModules": {}
204+
}"#,
205+
)
206+
.unwrap();
207+
208+
let options = config.into_options("development");
209+
assert!(matches!(
210+
options.descriptor_fields,
211+
DescriptorFields::IdOnly
212+
));
213+
214+
let config = serde_json::from_str::<LinguiJsOptions>(
215+
r#"{
216+
"descriptorFields": "all",
217+
"runtimeModules": {}
218+
}"#,
219+
)
220+
.unwrap();
221+
222+
let options = config.into_options("production");
223+
assert!(matches!(options.descriptor_fields, DescriptorFields::All));
224+
225+
let config = serde_json::from_str::<LinguiJsOptions>(
226+
r#"{
227+
"descriptorFields": "message",
228+
"runtimeModules": {}
229+
}"#,
230+
)
231+
.unwrap();
232+
233+
let options = config.into_options("production");
234+
assert!(matches!(
235+
options.descriptor_fields,
236+
DescriptorFields::Message
237+
));
238+
}
239+
240+
#[test]
241+
fn test_descriptor_fields_auto_default() {
168242
let config = serde_json::from_str::<LinguiJsOptions>(
169243
r#"{
170-
"stripNonEssentialFields": true,
171244
"runtimeModules": {}
172245
}"#,
173246
)
174247
.unwrap();
175248

176249
let options = config.into_options("development");
177-
assert!(options.strip_non_essential_fields);
250+
assert!(matches!(options.descriptor_fields, DescriptorFields::All));
178251

179252
let config = serde_json::from_str::<LinguiJsOptions>(
180253
r#"{
181-
"stripNonEssentialFields": false,
182254
"runtimeModules": {}
183255
}"#,
184256
)
185257
.unwrap();
186258

187259
let options = config.into_options("production");
188-
assert!(!options.strip_non_essential_fields);
260+
assert!(matches!(
261+
options.descriptor_fields,
262+
DescriptorFields::IdOnly
263+
));
189264
}
190265

191266
#[test]
192-
fn test_strip_non_essential_fields_default() {
267+
fn test_descriptor_fields_explicit_auto() {
193268
let config = serde_json::from_str::<LinguiJsOptions>(
194269
r#"{
270+
"descriptorFields": "auto",
195271
"runtimeModules": {}
196272
}"#,
197273
)
198274
.unwrap();
199275

200276
let options = config.into_options("development");
201-
assert!(!options.strip_non_essential_fields);
277+
assert!(matches!(options.descriptor_fields, DescriptorFields::All));
202278

203279
let config = serde_json::from_str::<LinguiJsOptions>(
204280
r#"{
281+
"descriptorFields": "auto",
205282
"runtimeModules": {}
206283
}"#,
207284
)
208285
.unwrap();
209286

210287
let options = config.into_options("production");
211-
assert!(options.strip_non_essential_fields);
288+
assert!(matches!(
289+
options.descriptor_fields,
290+
DescriptorFields::IdOnly
291+
));
212292
}
213293

214294
#[test]

tests/__swc_snapshots__/tests/js_define_message.rs/should_kept_only_essential_props.js renamed to tests/__swc_snapshots__/tests/js_define_message.rs/id_only_should_keep_only_id.js

File renamed without changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const message1 = /*i18n*/ {
2+
id: "xDAtGP",
3+
message: "Message"
4+
};
5+
const message2 = /*i18n*/ {
6+
id: "msgId",
7+
message: "Hello {name}",
8+
values: {
9+
name: name
10+
},
11+
context: "My Context"
12+
};

tests/__swc_snapshots__/tests/js_t.rs/js_should_kept_only_essential_props.js renamed to tests/__swc_snapshots__/tests/js_t.rs/js_id_only_should_keep_only_id.js

File renamed without changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { i18n as $_i18n } from "@lingui/core";
2+
const msg1 = $_i18n._(/*i18n*/ {
3+
id: "xDAtGP",
4+
message: "Message"
5+
});
6+
const msg2 = $_i18n._(/*i18n*/ {
7+
id: "msgId",
8+
message: "Hello {name}",
9+
values: {
10+
name: name
11+
},
12+
context: "My Context"
13+
});

tests/__swc_snapshots__/tests/jsx.rs/production_only_essential_props_are_kept.js renamed to tests/__swc_snapshots__/tests/jsx.rs/id_only_essential_props_are_kept.js

File renamed without changes.

tests/__swc_snapshots__/tests/jsx.rs/jsx_preserve_reserved_attrs.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ const exp2 = <Trans_ {.../*i18n*/ {
33
id: "6J8UtY",
44
comment: "Translators Comment",
55
message: "Refresh inbox",
6-
context: "Message Context"
6+
context: "Message Context",
7+
comment: "Translators Comment"
78
}} i18n="i18n" component={(p)=><div>{p.translation}</div>} render={(v)=>v}/>;

0 commit comments

Comments
 (0)