From bf7f655ccac3fc22fb7d36662ab0ec96595574e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Chr=C3=A1stek?= Date: Mon, 6 Feb 2023 09:21:26 +0100 Subject: [PATCH] fix: Named components not working in Trans in @lingui/react (#1402) --- packages/react/src/Trans.test.tsx | 27 ++++++++++++ packages/react/src/format.test.tsx | 70 +++++++++++++++++++++++------- packages/react/src/format.ts | 12 ++--- website/docs/misc/react-intl.md | 8 ++-- website/docs/ref/react.md | 6 +-- 5 files changed, 93 insertions(+), 30 deletions(-) diff --git a/packages/react/src/Trans.test.tsx b/packages/react/src/Trans.test.tsx index 0b89b813b..26015daa8 100644 --- a/packages/react/src/Trans.test.tsx +++ b/packages/react/src/Trans.test.tsx @@ -108,6 +108,33 @@ describe("Trans component", function () { expect(translation).toEqual("Hello John") }) + it("should render named component in components", function () { + const translation = html( + }} + /> + ) + expect(translation).toEqual(`Read the docs`) + }) + + it("should render nested named components in components", function () { + const translation = html( + , strong: }} + /> + ) + expect(translation).toEqual(`Read the docs`) + }) + + it("should render non-named component in components", function () { + const translation = html( + }} /> + ) + expect(translation).toEqual(`Read the docs`) + }) + it("should render translation inside custom component", function () { const Component = (props) =>

{props.children}

const html1 = html() diff --git a/packages/react/src/format.test.tsx b/packages/react/src/format.test.tsx index ca7f9e32b..016ccf506 100644 --- a/packages/react/src/format.test.tsx +++ b/packages/react/src/format.test.tsx @@ -30,6 +30,25 @@ describe("formatElements", function () { ).toEqual('About') }) + it("should preserve named element props", function () { + expect( + html( + formatElements("About", { named: }) + ) + ).toEqual('About') + }) + + it("should preserve nested named element props", function () { + expect( + html( + formatElements("About us", { + named: , + b: , + }) + ) + ).toEqual('About us') + }) + it("should format nested elements", function () { expect( html( @@ -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"))).toEqual("First") expect(html(formatElements("<0>FirstSecond"))).toEqual("FirstSecond") - expect(html(formatElements("First<0>SecondThird"))) - .toEqual("FirstSecondThird") + expect(html(formatElements("First<0>SecondThird"))).toEqual( + "FirstSecondThird" + ) expect(html(formatElements("Fir<0/>st"))).toEqual("First") + expect(html(formatElements("text"))).toEqual("text") + expect(html(formatElements("text
"))).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"))).toEqual("text</0>") expect(html(formatElements("text<0 />"))).toEqual("text<0 />") - expect(html(formatElements("text"))) - .toEqual("<tag>text</tag>") - expect(html(formatElements("text
"))).toEqual("text <br/>") }) - it("should ignore unpaired element used as paired", function() { - expect(html(formatElements("<0>text", {0:
}))).toEqual("text") + it("should ignore unpaired element used as paired", function () { + expect(html(formatElements("<0>text", { 0:
}))).toEqual("text") + }) + + it("should ignore unpaired named element used as paired", function () { + expect( + html(formatElements("text", { named:
})) + ).toEqual("text") + }) + + it("should ignore paired element used as unpaired", function () { + expect(html(formatElements("text<0/>", { 0: }))).toEqual( + "text" + ) }) - it("should ignore paired element used as unpaired", function() { - expect(html(formatElements("text<0/>", {0: }))) - .toEqual("text") + it("should ignore paired named element used as unpaired", function () { + expect(html(formatElements("text", { named: }))).toEqual( + "text" + ) }) - it("should create two children with different keys", function() { - const cleanPrefix = (str: string): number => Number.parseInt(str.replace("$lingui$_", ""), 10) - const childElements = formatElements("
<0/><0/>
", { 0: hi }) as Array - 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("
<0/><0/>
", { + 0: hi, + }) as Array + + 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])) }) }) diff --git a/packages/react/src/format.ts b/packages/react/src/format.ts index 2c5f814a5..b46210ae3 100644 --- a/packages/react/src/format.ts +++ b/packages/react/src/format.ts @@ -1,7 +1,7 @@ import React from "react" -// match <0>paired and <1/> unpaired tags -const tagRe = /<(\d+)>(.*?)<\/\1>|<(\d+)\/>/ +// match paired and 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 @@ -22,13 +22,13 @@ 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 Paired or (unpaired) * placeholders. `elements` is a array of react elements which indexes * correspond to element indexes in formatted string */ @@ -36,7 +36,7 @@ function formatElements( value: string, elements: { [key: string]: React.ReactElement } = {} ): string | Array { - const uniqueId = makeCounter(0, '$lingui$') + const uniqueId = makeCounter(0, "$lingui$") const parts = value.replace(nlRe, "").split(tagRe) // no inline elements, return @@ -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)) ) } diff --git a/website/docs/misc/react-intl.md b/website/docs/misc/react-intl.md index a6a87607f..5549640c0 100644 --- a/website/docs/misc/react-intl.md +++ b/website/docs/misc/react-intl.md @@ -55,14 +55,12 @@ In [react-intl](https://github.com/yahoo/react-intl), this would be translated a ``` jsx - ]} + message='Read the documentation.' + components={{ link: }} /> ``` -and the translator gets the message in one piece: `Read the <0>documentation`. +and the translator gets the message in one piece: `Read the documentation`. However, let's go yet another level deeper. diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index eb95966b6..9ba3dc5a4 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -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 ]} -/>; + id="Read Description below." + components={{ link: }} +/> ``` #### Plurals