Skip to content

Commit

Permalink
fix: Named components not working in Trans in @lingui/react (#1402)
Browse files Browse the repository at this point in the history
  • Loading branch information
Martin005 authored Feb 6, 2023
1 parent a8c110d commit bf7f655
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 30 deletions.
27 changes: 27 additions & 0 deletions packages/react/src/Trans.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,33 @@ describe("Trans component", function () {
expect(translation).toEqual("Hello <strong>John</strong>")
})

it("should render named component in components", function () {
const translation = html(
<Trans
id="Read <named>the docs</named>"
components={{ named: <a href="/docs" /> }}
/>
)
expect(translation).toEqual(`Read <a href="/docs">the docs</a>`)
})

it("should render nested named components in components", function () {
const translation = html(
<Trans
id="Read <link>the <strong>docs</strong></link>"
components={{ link: <a href="/docs" />, strong: <strong /> }}
/>
)
expect(translation).toEqual(`Read <a href="/docs">the <strong>docs</strong></a>`)
})

it("should render non-named component in components", function () {
const translation = html(
<Trans id="Read <0>the docs</0>" components={{ 0: <a href="/docs" /> }} />
)
expect(translation).toEqual(`Read <a href="/docs">the docs</a>`)
})

it("should render translation inside custom component", function () {
const Component = (props) => <p className="lead">{props.children}</p>
const html1 = html(<Trans component={Component} id="Original" />)
Expand Down
70 changes: 54 additions & 16 deletions packages/react/src/format.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ describe("formatElements", function () {
).toEqual('<a href="/about">About</a>')
})

it("should preserve named element props", function () {
expect(
html(
formatElements("<named>About</named>", { named: <a href="/about" /> })
)
).toEqual('<a href="/about">About</a>')
})

it("should preserve nested named element props", function () {
expect(
html(
formatElements("<named>About <b>us</b></named>", {
named: <a href="/about" />,
b: <strong />,
})
)
).toEqual('<a href="/about">About <strong>us</strong></a>')
})

it("should format nested elements", function () {
expect(
html(
Expand All @@ -52,35 +71,54 @@ describe("formatElements", function () {
)
})

it("should ignore non existing element", function() {
it("should ignore non existing element", function () {
expect(html(formatElements("<0>First</0>"))).toEqual("First")
expect(html(formatElements("<0>First</0>Second"))).toEqual("FirstSecond")
expect(html(formatElements("First<0>Second</0>Third")))
.toEqual("FirstSecondThird")
expect(html(formatElements("First<0>Second</0>Third"))).toEqual(
"FirstSecondThird"
)
expect(html(formatElements("Fir<0/>st"))).toEqual("First")
expect(html(formatElements("<tag>text</tag>"))).toEqual("text")
expect(html(formatElements("text <br/>"))).toEqual("text ")
})

it("should ignore incorrect tags and print them as a text", function() {
it("should ignore incorrect tags and print them as a text", function () {
expect(html(formatElements("text</0>"))).toEqual("text&lt;/0&gt;")
expect(html(formatElements("text<0 />"))).toEqual("text&lt;0 /&gt;")
expect(html(formatElements("<tag>text</tag>")))
.toEqual("&lt;tag&gt;text&lt;/tag&gt;")
expect(html(formatElements("text <br/>"))).toEqual("text &lt;br/&gt;")
})

it("should ignore unpaired element used as paired", function() {
expect(html(formatElements("<0>text</0>", {0: <br />}))).toEqual("text")
it("should ignore unpaired element used as paired", function () {
expect(html(formatElements("<0>text</0>", { 0: <br /> }))).toEqual("text")
})

it("should ignore unpaired named element used as paired", function () {
expect(
html(formatElements("<named>text</named>", { named: <br /> }))
).toEqual("text")
})

it("should ignore paired element used as unpaired", function () {
expect(html(formatElements("text<0/>", { 0: <span /> }))).toEqual(
"text<span></span>"
)
})

it("should ignore paired element used as unpaired", function() {
expect(html(formatElements("text<0/>", {0: <span />})))
.toEqual("text<span></span>")
it("should ignore paired named element used as unpaired", function () {
expect(html(formatElements("text<named/>", { named: <span /> }))).toEqual(
"text<span></span>"
)
})

it("should create two children with different keys", function() {
const cleanPrefix = (str: string): number => Number.parseInt(str.replace("$lingui$_", ""), 10)
const childElements = formatElements("<div><0/><0/></div>", { 0: <span>hi</span> }) as Array<any>
const childKeys = childElements.map(el => el?.key).filter(Boolean);
it("should create two children with different keys", function () {
const cleanPrefix = (str: string): number =>
Number.parseInt(str.replace("$lingui$_", ""), 10)
const elements = formatElements("<div><0/><0/></div>", {
0: <span>hi</span>,
}) as Array<React.ReactElement>

expect(elements).toHaveLength(1)
const childElements = elements[0].props.children
const childKeys = childElements.map((el) => el?.key).filter(Boolean)
expect(cleanPrefix(childKeys[0])).toBeLessThan(cleanPrefix(childKeys[1]))
})
})
12 changes: 6 additions & 6 deletions packages/react/src/format.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react"

// match <0>paired</0> and <1/> unpaired tags
const tagRe = /<(\d+)>(.*?)<\/\1>|<(\d+)\/>/
// match <tag>paired</tag> and <tag/> unpaired tags
const tagRe = /<([a-zA-Z0-9]+)>(.*?)<\/\1>|<([a-zA-Z0-9]+)\/>/
const nlRe = /(?:\r\n|\r|\n)/g

// For HTML, certain tags should omit their close tag. We keep a whitelist for
Expand All @@ -22,21 +22,21 @@ const voidElementTags = {
source: true,
track: true,
wbr: true,
menuitem: true
menuitem: true,
}

/**
* `formatElements` - parse string and return tree of react elements
*
* `value` is string to be formatted with <0>Paired<0/> or <0/> (unpaired)
* `value` is string to be formatted with <tag>Paired<tag/> or <tag/> (unpaired)
* placeholders. `elements` is a array of react elements which indexes
* correspond to element indexes in formatted string
*/
function formatElements(
value: string,
elements: { [key: string]: React.ReactElement<any> } = {}
): string | Array<any> {
const uniqueId = makeCounter(0, '$lingui$')
const uniqueId = makeCounter(0, "$lingui$")
const parts = value.replace(nlRe, "").split(tagRe)

// no inline elements, return
Expand Down Expand Up @@ -101,7 +101,7 @@ function getElements(parts) {

const [paired, children, unpaired, after] = parts.slice(0, 4)

return [[parseInt(paired || unpaired), children || "", after]].concat(
return [[paired || unpaired, children || "", after]].concat(
getElements(parts.slice(4, parts.length))
)
}
Expand Down
8 changes: 3 additions & 5 deletions website/docs/misc/react-intl.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,12 @@ In [react-intl](https://github.com/yahoo/react-intl), this would be translated a
``` jsx
<Trans
id='msg.docs'
message='Read the <0>documentation</0>.'
components={[
<a href="/docs" />
]}
message='Read the <link>documentation</link>.'
components={{ link: <a href="/docs" />}}
/>
```

and the translator gets the message in one piece: `Read the <0>documentation</0>`.
and the translator gets the message in one piece: `Read the <link>documentation</link>`.

However, let's go yet another level deeper.

Expand Down
6 changes: 3 additions & 3 deletions website/docs/ref/react.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ It's also possible to use `Trans` component directly without macros. In that cas

// number of tag corresponds to index in `components` prop
<Trans
id="Read <0>Description</0> below."
components={[<Link to="/docs" />]}
/>;
id="Read <link>Description</link> below."
components={{ link: <a href="/docs" /> }}
/>
```

#### Plurals
Expand Down

1 comment on commit bf7f655

@vercel
Copy link

@vercel vercel bot commented on bf7f655 Feb 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.