Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,9 @@ https://swc.rs/docs/configuration/swcrc
// "i18n": ["@lingui/core", "i18n"],
// "trans": ["@lingui/react", "Trans"]
// }
// Lingui strips non-essential fields in production builds for performance.
// Docs https://lingui.dev/guides/optimizing-bundle-size
// You can override the default behavior with:
// "stripNonEssentialFields": false/true
//
// 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.
Expand All @@ -70,6 +69,17 @@ https://swc.rs/docs/configuration/swcrc
}
```

### `descriptorFields`

Controls which fields are preserved in the transformed message descriptors. Accepts one of:

- **`"auto"`** (default) — In production (`NODE_ENV=production`), behaves like `"id-only"`. Otherwise, behaves like `"all"`.
- **`"all"`** — Keeps `id`, `message`, `context`, and `comment`. Use this for extraction (replaces the old `extract: true` from the Babel plugin).
- **`"id-only"`** — Keeps only the `id`. Most optimized for production bundles.
- **`"message"`** — Keeps `id`, `message`, and `context` (but not `comment`). Useful when you need message content at runtime.

Check [this article](https://lingui.dev/guides/optimizing-bundle-size) for more info about this configuration.

Or Next JS Usage:

`next.config.js`
Expand Down
23 changes: 11 additions & 12 deletions src/js_macro_folder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ where
.into(),
)];

if !self.ctx.options.strip_non_essential_fields {
if self.ctx.options.descriptor_fields.should_keep_message() {
props.push(create_key_value_prop("message", parsed.message));
}

Expand Down Expand Up @@ -111,10 +111,8 @@ where

let mut new_props: Vec<PropOrSpread> = vec![];

if let Some(prop) = id_prop {
if let Some(value) = get_expr_as_string(&prop.value) {
new_props.push(create_key_value_prop("id", value.into()));
}
if let Some(value) = id_prop.and_then(|prop| get_expr_as_string(&prop.value)) {
new_props.push(create_key_value_prop("id", value.into()));
}

if let Some(prop) = message_prop {
Expand All @@ -134,7 +132,7 @@ where
))
}

if !self.ctx.options.strip_non_essential_fields {
if self.ctx.options.descriptor_fields.should_keep_message() {
new_props.push(create_key_value_prop("message", parsed.message));
}

Expand All @@ -143,16 +141,17 @@ where
}
}

if !self.ctx.options.strip_non_essential_fields {
if self.ctx.options.descriptor_fields.should_keep_context() {
if let Some(context) = context_val {
new_props.push(create_key_value_prop("context", context.into()));
}
}

let comment = get_object_prop(&obj.props, "comment")
.and_then(|prop| get_expr_as_string(&prop.value));

if let Some(comment) = comment {
new_props.push(create_key_value_prop("comment", comment.into()));
if self.ctx.options.descriptor_fields.should_keep_comment() {
if let Some(value) = get_object_prop(&obj.props, "comment")
.and_then(|prop| get_expr_as_string(&prop.value))
{
new_props.push(create_key_value_prop("comment", value.into()));
}
}

Expand Down
22 changes: 12 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,20 +152,22 @@ where
message_descriptor_props.push(create_key_value_prop("components", exp));
}

if !self.ctx.options.strip_non_essential_fields {
let comment_attr = get_jsx_attr(&el.opening, "comment")
.and_then(|attr| attr.value.as_ref())
.and_then(get_jsx_attr_value_as_string);
if self.ctx.options.descriptor_fields.should_keep_comment() {
let comment_attr_val = get_jsx_attr(&el.opening, "comment")
.and_then(|attr| get_jsx_attr_value_as_string(attr.value.as_ref()?));

if let Some(comment) = comment_attr {
message_descriptor_props.push(create_key_value_prop("comment", comment.into()));
if let Some(comment_attr_val) = comment_attr_val {
message_descriptor_props
.push(create_key_value_prop("comment", comment_attr_val.into()));
}
}

if self.ctx.options.descriptor_fields.should_keep_message() {
message_descriptor_props.push(create_key_value_prop("message", parsed.message));
}

if let Some(context_attr) = context_attr {
let context_attr_val = get_jsx_attr_value_as_string(context_attr).unwrap();

if self.ctx.options.descriptor_fields.should_keep_context() {
if let Some(context_attr_val) = context_attr.and_then(get_jsx_attr_value_as_string) {
message_descriptor_props.push(create_key_value_prop(
"context",
Box::new(Expr::Lit(Lit::Str(Str {
Expand Down Expand Up @@ -515,7 +517,7 @@ where
}
}

pub use self::options::{LinguiOptions, RuntimeModulesConfigMapNormalized};
pub use self::options::{DescriptorFields, LinguiOptions, RuntimeModulesConfigMapNormalized};

#[plugin_transform]
pub fn process_transform(program: Program, metadata: TransformPluginProgramMetadata) -> Program {
Expand Down
112 changes: 96 additions & 16 deletions src/options.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
use serde::Deserialize;

#[derive(Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum DescriptorFields {
Auto,
All,
IdOnly,
Message,
}

impl DescriptorFields {
pub fn should_keep_message(&self) -> bool {
matches!(self, DescriptorFields::All | DescriptorFields::Message)
}

pub fn should_keep_context(&self) -> bool {
matches!(self, DescriptorFields::All | DescriptorFields::Message)
}

pub fn should_keep_comment(&self) -> bool {
matches!(self, DescriptorFields::All)
}
}

#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct LinguiJsOptions {
runtime_modules: Option<RuntimeModulesConfigMap>,
#[serde(default)]
strip_non_essential_fields: Option<bool>,
Comment thread
andrii-bodnar marked this conversation as resolved.
descriptor_fields: Option<DescriptorFields>,
#[serde(default)]
use_lingui_v5_id_generation: Option<bool>,
}
Expand All @@ -30,10 +53,19 @@ pub struct RuntimeModulesConfigMapNormalized {

impl LinguiJsOptions {
pub fn into_options(self, env_name: &str) -> LinguiOptions {
let descriptor_fields = match self.descriptor_fields.unwrap_or(DescriptorFields::Auto) {
DescriptorFields::Auto => {
if matches!(env_name, "production") {
DescriptorFields::IdOnly
} else {
DescriptorFields::All
}
}
other => other,
};

LinguiOptions {
strip_non_essential_fields: self
.strip_non_essential_fields
.unwrap_or(matches!(env_name, "production")),
descriptor_fields,
use_lingui_v5_id_generation: self.use_lingui_v5_id_generation.unwrap_or(false),
runtime_modules: RuntimeModulesConfigMapNormalized {
i18n: (
Expand Down Expand Up @@ -79,15 +111,15 @@ impl LinguiJsOptions {

#[derive(Debug, Clone)]
pub struct LinguiOptions {
pub strip_non_essential_fields: bool,
pub descriptor_fields: DescriptorFields,
pub runtime_modules: RuntimeModulesConfigMapNormalized,
pub use_lingui_v5_id_generation: bool,
}

impl Default for LinguiOptions {
fn default() -> LinguiOptions {
LinguiOptions {
strip_non_essential_fields: false,
descriptor_fields: DescriptorFields::All,
Comment thread
andrii-bodnar marked this conversation as resolved.
use_lingui_v5_id_generation: false,
runtime_modules: RuntimeModulesConfigMapNormalized {
i18n: ("@lingui/core".into(), "i18n".into()),
Expand Down Expand Up @@ -132,7 +164,7 @@ mod lib_tests {
Some("myUseLingui".into())
)),
}),
strip_non_essential_fields: None,
descriptor_fields: None,
use_lingui_v5_id_generation: None,
}
)
Expand All @@ -157,58 +189,106 @@ mod lib_tests {
trans: None,
use_lingui: None,
}),
strip_non_essential_fields: None,
descriptor_fields: None,
use_lingui_v5_id_generation: None,
}
)
}

#[test]
fn test_strip_non_essential_fields_config() {
fn test_descriptor_fields_config() {
let config = serde_json::from_str::<LinguiJsOptions>(
r#"{
"descriptorFields": "id-only",
"runtimeModules": {}
}"#,
)
.unwrap();

let options = config.into_options("development");
assert!(matches!(
options.descriptor_fields,
DescriptorFields::IdOnly
));

let config = serde_json::from_str::<LinguiJsOptions>(
r#"{
"descriptorFields": "all",
"runtimeModules": {}
}"#,
)
.unwrap();

let options = config.into_options("production");
assert!(matches!(options.descriptor_fields, DescriptorFields::All));

let config = serde_json::from_str::<LinguiJsOptions>(
r#"{
"descriptorFields": "message",
"runtimeModules": {}
}"#,
)
.unwrap();

let options = config.into_options("production");
assert!(matches!(
options.descriptor_fields,
DescriptorFields::Message
));
}

#[test]
fn test_descriptor_fields_auto_default() {
let config = serde_json::from_str::<LinguiJsOptions>(
r#"{
"stripNonEssentialFields": true,
"runtimeModules": {}
}"#,
)
.unwrap();

let options = config.into_options("development");
assert!(options.strip_non_essential_fields);
assert!(matches!(options.descriptor_fields, DescriptorFields::All));

let config = serde_json::from_str::<LinguiJsOptions>(
r#"{
"stripNonEssentialFields": false,
"runtimeModules": {}
}"#,
)
.unwrap();

let options = config.into_options("production");
assert!(!options.strip_non_essential_fields);
assert!(matches!(
options.descriptor_fields,
DescriptorFields::IdOnly
));
}

#[test]
fn test_strip_non_essential_fields_default() {
fn test_descriptor_fields_explicit_auto() {
let config = serde_json::from_str::<LinguiJsOptions>(
r#"{
"descriptorFields": "auto",
"runtimeModules": {}
}"#,
)
.unwrap();

let options = config.into_options("development");
assert!(!options.strip_non_essential_fields);
assert!(matches!(options.descriptor_fields, DescriptorFields::All));

let config = serde_json::from_str::<LinguiJsOptions>(
r#"{
"descriptorFields": "auto",
"runtimeModules": {}
}"#,
)
.unwrap();

let options = config.into_options("production");
assert!(options.strip_non_essential_fields);
assert!(matches!(
options.descriptor_fields,
DescriptorFields::IdOnly
));
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const message1 = /*i18n*/ {
id: "xDAtGP",
message: "Message"
};
const message2 = /*i18n*/ {
id: "msgId",
message: "Hello {name}",
values: {
name: name
},
context: "My Context"
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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"
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Trans as Trans_ } from "@lingui/react";
<Trans_ {.../*i18n*/ {
id: "msg.hello",
values: {
name: name
},
components: {
0: <strong/>
},
message: "Hello <0>{name}</0>",
context: "My Context"
}} render="render" i18n="i18n"/>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Trans as Trans_ } from "@lingui/react";
<Trans_ {.../*i18n*/ {
id: "custom.id",
values: {
count: count
},
components: {
0: <a href="/more"/>
},
message: "{count, plural, offset:1 =0 {Zero items} other {<0>A lot of them</0>}}",
context: "My Context"
}} render="render" i18n="i18n"/>;
Loading
Loading