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
50 changes: 41 additions & 9 deletions crates/lingui_macro/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::ast_utils::{
get_jsx_attr, get_jsx_attr_value_as_string, is_jsx_elements_equal, omit_jsx_attrs,
};
use crate::options::LinguiOptions;
use crate::options::{JsxRuntime, LinguiOptions};
use crate::tokens::{CaseOrOffset, MsgArg, MsgToken};
use std::collections::HashSet;
use swc_core::{common::DUMMY_SP, ecma::ast::*};
use swc_core::{
common::DUMMY_SP,
ecma::{ast::*, utils::private_ident},
};

fn is_numeric(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
Expand Down Expand Up @@ -240,6 +243,7 @@ impl<'a> MessageBuilder<'a> {
});
}
} else {
// todo: cloning the whole opening element just to compare duplicates later is wasteful
self.elements_tracking.push((n.clone(), el.clone()));
}

Expand All @@ -259,19 +263,47 @@ impl<'a> MessageBuilder<'a> {
}

if !self.components.iter().any(|c| c.placeholder == name) {
// todo: it looks very dirty and bad to cloning this jsx values
self.components.push(ValueWithPlaceholder {
placeholder: name.clone(),
value: Box::new(Expr::JSXElement(Box::new(JSXElement {
opening: el,
closing: None,
children: vec![],
span: DUMMY_SP,
}))),
value: self.component_value(el),
});
}
}

/// Builds the value stored in `components` for a JSX element.
/// For Solid it is wrapped as `(_props) => <el ...attrs {..._props}/>`,
/// since the runtime renders it via `createComponent(element, { children })`.
fn component_value(&self, el: JSXOpeningElement) -> Box<Expr> {
let element = |opening| {
Box::new(Expr::JSXElement(Box::new(JSXElement {
opening,
closing: None,
children: vec![],
span: DUMMY_SP,
})))
};

match self.options.jsx_runtime {
JsxRuntime::React => element(el),
JsxRuntime::Solid => {
let props = private_ident!("_props");
let mut opening = el;
opening
.attrs
.push(JSXAttrOrSpread::SpreadElement(SpreadElement {
dot3_token: DUMMY_SP,
expr: Box::new(Expr::Ident(props.clone())),
}));

Box::new(Expr::Arrow(ArrowExpr {
params: vec![Pat::Ident(props.into())],
body: Box::new(ArrowFunctionBody::Expr(element(opening))),
..Default::default()
}))
}
}
}

fn push_tag_closing(&mut self) {
if let Some(name) = self.components_stack.pop() {
self.push_msg(&format!("</{name}>"));
Expand Down
4 changes: 2 additions & 2 deletions crates/lingui_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,8 +588,8 @@ where
}

pub use self::options::{
DescriptorFields, LinguiJsOptions, LinguiOptions, MacroPackagesConfig, RuntimeModulesConfigMap,
RuntimeModulesConfigMapNormalized,
DescriptorFields, JsxRuntime, LinguiJsOptions, LinguiOptions, MacroPackagesConfig,
RuntimeModulesConfigMap, RuntimeModulesConfigMapNormalized,
};

#[plugin_transform]
Expand Down
51 changes: 51 additions & 0 deletions crates/lingui_macro/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ impl DescriptorFields {
}
}

#[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Copy, Default)]
#[serde(rename_all = "lowercase")]
pub enum JsxRuntime {
#[default]
React,
Solid,
}

#[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct LinguiJsOptions {
Expand All @@ -50,6 +58,8 @@ pub struct LinguiJsOptions {
pub jsx_placeholder_attribute: Option<String>,
#[serde(default)]
pub jsx_placeholder_defaults: Option<HashMap<String, String>>,
#[serde(default)]
pub jsx_runtime: Option<JsxRuntime>,
}

#[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Default)]
Expand Down Expand Up @@ -123,6 +133,7 @@ impl LinguiJsOptions {
id_prefix_leader: self.id_prefix_leader.clone(),
jsx_placeholder_attribute: self.jsx_placeholder_attribute.clone(),
jsx_placeholder_defaults: self.jsx_placeholder_defaults.clone(),
jsx_runtime: self.jsx_runtime.unwrap_or_default(),
macro_packages: MacroPackagesConfig {
core: self
.core_package
Expand Down Expand Up @@ -184,6 +195,8 @@ pub struct LinguiOptions {
#[serde(skip_serializing_if = "is_default")]
pub jsx_placeholder_defaults: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "is_default")]
pub jsx_runtime: JsxRuntime,
#[serde(skip_serializing_if = "is_default")]
pub macro_packages: MacroPackagesConfig,
#[serde(skip_serializing_if = "is_default")]
pub runtime_modules: RuntimeModulesConfigMapNormalized,
Expand All @@ -202,6 +215,7 @@ impl Default for LinguiOptions {
id_prefix_leader: None,
jsx_placeholder_attribute: None,
jsx_placeholder_defaults: None,
jsx_runtime: JsxRuntime::default(),
macro_packages: Default::default(),
runtime_modules: Default::default(),
}
Expand Down Expand Up @@ -249,6 +263,7 @@ mod lib_tests {
id_prefix_leader: None,
jsx_placeholder_attribute: None,
jsx_placeholder_defaults: None,
jsx_runtime: None,
use_jsdoc_i18n_comment: None,
}
)
Expand Down Expand Up @@ -281,6 +296,7 @@ mod lib_tests {
id_prefix_leader: None,
jsx_placeholder_attribute: None,
jsx_placeholder_defaults: None,
jsx_runtime: None,
use_jsdoc_i18n_comment: None,
core_package: None,
jsx_package: None,
Expand Down Expand Up @@ -314,6 +330,7 @@ mod lib_tests {
id_prefix_leader: None,
jsx_placeholder_attribute: None,
jsx_placeholder_defaults: None,
jsx_runtime: None,
use_jsdoc_i18n_comment: None,
}
)
Expand Down Expand Up @@ -514,6 +531,40 @@ mod lib_tests {
assert!(!options.use_lingui_v5_id_generation);
}

#[test]
fn test_jsx_runtime_config() {
let config =
serde_json::from_str::<LinguiJsOptions>(r#"{ "jsxRuntime": "solid" }"#).unwrap();
assert_eq!(config.jsx_runtime, Some(JsxRuntime::Solid));
assert_eq!(
config.into_options("development").jsx_runtime,
JsxRuntime::Solid
);

let config =
serde_json::from_str::<LinguiJsOptions>(r#"{ "jsxRuntime": "react" }"#).unwrap();
assert_eq!(
config.into_options("development").jsx_runtime,
JsxRuntime::React
);
}

#[test]
fn test_jsx_runtime_default() {
let config = serde_json::from_str::<LinguiJsOptions>(r#"{}"#).unwrap();
assert_eq!(config.jsx_runtime, None);
assert_eq!(
config.into_options("development").jsx_runtime,
JsxRuntime::React
);
}

#[test]
fn test_jsx_runtime_invalid() {
let result = serde_json::from_str::<LinguiJsOptions>(r#"{ "jsxRuntime": "vue" }"#);
assert!(result.is_err());
}

#[test]
fn test_empty_config() {
let config = serde_json::from_str::<LinguiJsOptions>(r#"{}"#).unwrap();
Expand Down
133 changes: 133 additions & 0 deletions crates/lingui_macro/tests/jsx_solid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use lingui_macro::{JsxRuntime, LinguiOptions};

#[macro_use]
mod common;

fn solid() -> LinguiOptions {
LinguiOptions {
jsx_runtime: JsxRuntime::Solid,
..Default::default()
}
}

to!(
solid_basic_component_with_attrs,
solid(),
r#"
import { Trans } from "@lingui/react/macro";
<Trans>Hello <a href="/docs">docs</a>.</Trans>;
"#
);

to!(
solid_self_closing_element,
solid(),
r#"
import { Trans } from "@lingui/react/macro";
<Trans>Hello<br />world</Trans>;
"#
);

to!(
solid_nested_elements_and_values,
solid(),
r#"
import { Trans } from "@lingui/react/macro";
<Trans>
<p>Hello <a href="/u"><em>{name}</em></a></p>
</Trans>;
"#
);

to!(
solid_member_expression_tag,
solid(),
r#"
import { Trans } from "@lingui/react/macro";
<Trans>Click <Foo.Bar kind="x">here</Foo.Bar></Trans>;
"#
);

to!(
solid_existing_spread_attrs,
solid(),
r#"
import { Trans } from "@lingui/react/macro";
<Trans>Go <a {...rest} href="/x">there</a></Trans>;
"#
);

to!(
solid_named_placeholder_reused,
LinguiOptions {
jsx_runtime: JsxRuntime::Solid,
jsx_placeholder_attribute: Some("_t".into()),
..Default::default()
},
r#"
import { Trans } from "@lingui/react/macro";
<Trans>
<a _t="link" href="/a">first</a> and <a _t="link" href="/a">second</a>
</Trans>;
"#
);

to!(
solid_plural_inside_trans,
solid(),
r##"
import { Trans, Plural } from "@lingui/react/macro";
<Trans>
Hello <b>world</b>, <Plural value={count} one="# item" other="# items" />
</Trans>;
"##
);

to!(
solid_element_inside_icu_case,
solid(),
r#"
import { Trans, Plural } from "@lingui/react/macro";
<Trans>
<Plural value={count} one={<a href="/one">one</a>} other="many" />
</Trans>;
"#
);

to!(
solid_does_not_affect_t_and_use_lingui,
solid(),
r#"
import { t } from "@lingui/core/macro";
import { useLingui } from "@lingui/react/macro";
const a = t`Hello ${name}`;
function C() {
const { t } = useLingui();
return t`Hi ${name}`;
}
"#
);

to!(
solid_props_name_collision,
solid(),
r#"
import { Trans } from "@lingui/react/macro";
function C(_props) {
return <Trans>Hello <a href={_props.href}>docs</a>.</Trans>;
}
"#
);

to_panic!(
solid_distinct_elements_same_placeholder,
LinguiOptions {
jsx_runtime: JsxRuntime::Solid,
jsx_placeholder_attribute: Some("_t".into()),
..Default::default()
},
r#"
import { Trans } from "@lingui/react/macro";
<Trans><a _t="link" href="/a">one</a> and <a _t="link" href="/b">two</a></Trans>;
"#
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
source: crates/lingui_macro/tests/jsx_solid.rs
info:
jsx_runtime: solid
---
import { Trans } from "@lingui/react/macro";
<Trans>Hello <a href="/docs">docs</a>.</Trans>;

↓ ↓ ↓ ↓ ↓ ↓

import { Trans as Trans_ } from "@lingui/react";
<Trans_ {.../*i18n*/ {
id: "lqG909",
components: {
0: (_props)=><a href="/docs" {..._props}/>
},
message: "Hello <0>docs</0>."
}}/>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: crates/lingui_macro/tests/jsx_solid.rs
info:
jsx_placeholder_attribute: _t
jsx_runtime: solid
---
import { Trans } from "@lingui/react/macro";
<Trans><a _t="link" href="/a">one</a> and <a _t="link" href="/b">two</a></Trans>;

↓ ↓ ↓ ↓ ↓ ↓

error: Multiple distinct JSX elements with the same placeholder name (`link`). Differentiate them by adding/modifying the `_t` attribute (e.g. `<element _t="newName" />`).
--> input.tsx:2:43
|
2 | <Trans><a _t="link" href="/a">one</a> and <a _t="link" href="/b">two</a></Trans>;
| ^^^^^^^^^^^^^^^^^^^^^^^
Loading
Loading