Skip to content

Commit dedfaae

Browse files
committed
fix: one more ICU escape corner case
1 parent 8611fb3 commit dedfaae

4 files changed

Lines changed: 37 additions & 32 deletions

File tree

cmp/compiler/src/plugin/transform/__snapshots__/transform.test.ts.snap

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,17 @@ export function ListComponent({
193193
}"
194194
`;
195195

196+
exports[`transformComponent > corner cases > should escape literal angle brackets in text 1`] = `
197+
"import { useTranslation } from "@lingo.dev/_compiler/react";
198+
export default function Help() {
199+
const t = useTranslation(["9839f15c9f2b"]);
200+
return <div>{t("9839f15c9f2b", "To wrap text, write '<'>content'<'/> <p0>Or use '<'Fragment>content'<'/Fragment></p0> or wrap it into the '<'>{content}'<'/>", {
201+
content,
202+
p0: chunks => <p>{chunks}</p>
203+
})}</div>;
204+
}"
205+
`;
206+
196207
exports[`transformComponent > corner cases > should handle function expressions correctly 1`] = `
197208
"import { useTranslation } from "@lingo.dev/_compiler/react";
198209
export const Button = function () {

cmp/compiler/src/plugin/transform/process-file.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,8 @@ function serializeJSXChildren(
316316
}
317317
} else if (expr.type === "StringLiteral") {
318318
// String literal (like {" "}) - include the literal text directly
319-
text += expr.value;
319+
text += escapeTextForICU(expr.value);
320+
// TODO (AleksandrSl 07/12/2025): Should we just ignore the empty expression?
320321
} else if (expr.type !== "JSXEmptyExpression") {
321322
const name = `expression${expressions.size}`;
322323
text += `{${name}}`;

cmp/compiler/src/plugin/transform/transform.test.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2373,13 +2373,14 @@ export const Button = function() { return <button>Click me</button>; }
23732373
expect(result.code).toMatchSnapshot();
23742374
});
23752375

2376-
it.only("should escape literal angle brackets in text", () => {
2376+
it("should escape literal angle brackets in text", () => {
23772377
const code = `
23782378
export default function Help() {
23792379
return (
23802380
<div>
2381-
<p>To wrap text, write &lt;&gt;content&lt;/&gt;</p>
2381+
To wrap text, write &lt;&gt;content&lt;/&gt;
23822382
<p>Or use &lt;Fragment&gt;content&lt;/Fragment&gt;</p>
2383+
or wrap it into the {"<>"}{content}{"</>"}
23832384
</div>
23842385
);
23852386
}
@@ -2391,16 +2392,7 @@ export default function Help() {
23912392
config,
23922393
});
23932394

2394-
expect(result.newEntries).toHaveLength(2);
2395-
const firstEntry = asContent(result.newEntries![0]);
2396-
expect(firstEntry.sourceText).toBe(
2397-
"To wrap text, write '<'>content'<'/>",
2398-
);
2399-
2400-
const secondEntry = asContent(result.newEntries![1]);
2401-
expect(secondEntry.sourceText).toBe(
2402-
"Or use '<'Fragment>content'<'/Fragment>",
2403-
);
2395+
expect(result.code).toMatchSnapshot();
24042396
});
24052397
});
24062398
});

cmp/compiler/src/react/shared/render-rich-text.tsx

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
import {
2-
Children,
3-
cloneElement,
4-
Fragment,
5-
isValidElement,
6-
type ReactNode,
7-
} from "react";
1+
import { Children, Fragment, isValidElement, type ReactNode } from "react";
82
import IntlMessageFormat, { type FormatXMLElementFn } from "intl-messageformat";
3+
import { logger } from "../../utils/logger";
94

105
/**
116
* Component renderer function for rich text translation
@@ -81,18 +76,24 @@ export function renderRichText(
8176
text: string,
8277
params: RichTextParams,
8378
): ReactNode {
84-
const formatter = new IntlMessageFormat(text, "en");
85-
const keyedParams = assignUniqueKeysToParams(params);
86-
const result = formatter.format<ReactNode>(keyedParams);
79+
try {
80+
const formatter = new IntlMessageFormat(text, "en");
81+
const keyedParams = assignUniqueKeysToParams(params);
82+
const result = formatter.format<ReactNode>(keyedParams);
8783

88-
if (Array.isArray(result)) {
89-
// Making all elements keyed here somehow fixes all the things. Maybe I also need to key everything in toKeyedReactNodeArray and I just don't get the error because I don't have a corner case for it? Need to investigate
90-
return result.map((item, index) => {
91-
// Each item gets wrapped in Fragment with unique key
92-
// This handles strings, React elements (like <>text</>), everything
93-
return <Fragment key={index}>{item}</Fragment>;
94-
});
95-
}
84+
if (Array.isArray(result)) {
85+
// Making all elements keyed here somehow fixes all the things. Maybe I also need to key everything in toKeyedReactNodeArray and I just don't get the error because I don't have a corner case for it? Need to investigate
86+
return result.map((item, index) => {
87+
// Each item gets wrapped in Fragment with unique key
88+
// This handles strings, React elements (like <>text</>), everything
89+
return <Fragment key={index}>{item}</Fragment>;
90+
});
91+
}
9692

97-
return result;
93+
return result;
94+
} catch (error) {
95+
// It's better to render at least something than break the whole app.
96+
logger.warn(`Error rendering rich text (${text}): ${error}`);
97+
return text;
98+
}
9899
}

0 commit comments

Comments
 (0)