Skip to content

Commit e8acb7a

Browse files
committed
feat: add optional configurable JSX placeholder naming
1 parent 1597e3a commit e8acb7a

8 files changed

Lines changed: 567 additions & 8 deletions

File tree

packages/babel-plugin-lingui-macro/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ export default function ({
232232
path.traverse(
233233
{
234234
JSXElement(path, state) {
235+
const linguiConfig = state.get("linguiConfig") as LinguiConfigNormalized
236+
235237
const macro = new MacroJSX(
236238
{ types: t },
237239
{
@@ -244,6 +246,10 @@ export default function ({
244246
),
245247
isLinguiIdentifier: (node: Identifier, macro) =>
246248
isLinguiIdentifier(path, node, macro),
249+
jsxPlaceholderAttribute:
250+
linguiConfig.macro?.jsxPlaceholderAttribute,
251+
jsxPlaceholderDefaults:
252+
linguiConfig.macro?.jsxPlaceholderDefaults,
247253
},
248254
)
249255

packages/babel-plugin-lingui-macro/src/macroJsx.ts

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,18 @@ function maybeNodeValue(node: Node): { text: string; loc: SourceLocation } {
4949
export type MacroJsxContext = MacroJsContext & {
5050
elementIndex: () => number
5151
transImportName: string
52+
elementsTracking: Map<string, JSXElement>
53+
jsxPlaceholderAttribute?: string
54+
jsxPlaceholderDefaults?: Record<string, string>
5255
}
5356

5457
export type MacroJsxOpts = {
5558
stripNonEssentialProps: boolean
5659
stripMessageProp: boolean
5760
transImportName: string
5861
isLinguiIdentifier: (node: Identifier, macro: JsMacroName) => boolean
62+
jsxPlaceholderAttribute?: string
63+
jsxPlaceholderDefaults?: Record<string, string>
5964
}
6065

6166
const choiceComponentAttributesWhitelist = [
@@ -86,6 +91,9 @@ export class MacroJSX {
8691
),
8792
transImportName: opts.transImportName,
8893
elementIndex: makeCounter(),
94+
elementsTracking: new Map(),
95+
jsxPlaceholderAttribute: opts.jsxPlaceholderAttribute,
96+
jsxPlaceholderDefaults: opts.jsxPlaceholderDefaults,
8997
}
9098
}
9199

@@ -351,18 +359,82 @@ export class MacroJSX {
351359
}
352360

353361
tokenizeElement = (path: NodePath<JSXElement>): ElementToken => {
354-
// !!! Important: Calculate element index before traversing children.
355-
// That way outside elements are numbered before inner elements. (...and it looks pretty).
356-
const name = this.ctx.elementIndex()
362+
const {
363+
jsxPlaceholderAttribute,
364+
jsxPlaceholderDefaults,
365+
elementsTracking,
366+
} = this.ctx
367+
368+
let node = path.node
369+
let name: string | undefined = undefined
370+
371+
if (jsxPlaceholderAttribute) {
372+
const { attributes } = node.openingElement
373+
const attrIndex = attributes.findIndex(
374+
(attr) =>
375+
attr.type === "JSXAttribute" &&
376+
attr.name.name === jsxPlaceholderAttribute,
377+
)
378+
379+
if (attrIndex !== -1) {
380+
const attr = attributes[attrIndex] as JSXAttribute
381+
if (attr.value && attr.value.type === "StringLiteral") {
382+
name = attr.value.value
383+
}
384+
385+
const newAttributes = [...attributes]
386+
newAttributes.splice(attrIndex, 1)
387+
388+
node = {
389+
...node,
390+
openingElement: {
391+
...node.openingElement,
392+
attributes: newAttributes,
393+
},
394+
}
395+
}
396+
}
397+
398+
if (!name && jsxPlaceholderDefaults) {
399+
const tagName = node.openingElement.name
400+
if (tagName.type === "JSXIdentifier") {
401+
name = jsxPlaceholderDefaults[tagName.name]
402+
}
403+
}
404+
405+
if (!name) {
406+
name = String(this.ctx.elementIndex())
407+
elementsTracking.set(name, node)
408+
} else {
409+
const existingElement = elementsTracking.get(name)
410+
411+
if (existingElement) {
412+
const existingAttrs = existingElement.openingElement.attributes
413+
const openingAttrs = node.openingElement.attributes
414+
if (
415+
existingAttrs.length !== openingAttrs.length ||
416+
!existingAttrs.every((a) =>
417+
openingAttrs.some((b) => this.types.isNodesEquivalent(a, b)),
418+
)
419+
) {
420+
throw path.buildCodeFrameError(
421+
`Multiple distinct JSX elements with the same placeholder name (\`${name}\`). ` +
422+
`Differentiate them by adding/modifying the JSX attribute (e.g. \`<element ${jsxPlaceholderAttribute}="newName" />\`).`,
423+
)
424+
}
425+
} else {
426+
elementsTracking.set(name, node)
427+
}
428+
}
357429

358430
return {
359431
type: "element",
360432
name,
361433
value: {
362-
...path.node,
434+
...node,
363435
children: [],
364436
openingElement: {
365-
...path.node.openingElement,
437+
...node.openingElement,
366438
selfClosing: true,
367439
},
368440
},
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`Deduplication: Explicit names with different attributes throw an error 1`] = `
4+
[SyntaxError: <cwd>/<filename>jsx: Unsupported macro usage. Please check the examples at https://lingui.dev/ref/macro#examples-of-js-macros.
5+
If you think this is a bug, fill in an issue at https://github.com/lingui/js-lingui/issues
6+
7+
Error: Multiple distinct JSX elements with the same placeholder name (\`link\`). Differentiate them by adding/modifying the JSX attribute (e.g. \`<element _t="newName" />\`).
8+
1 |
9+
2 | import { Trans } from '@lingui/react/macro';
10+
> 3 | <Trans>Hello <a _t="link" href="/a">link 1</a>, normal, <a _t="link" href="/b">link 2</a>.</Trans>
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
4 |
13+
1 |
14+
2 | import { Trans } from '@lingui/react/macro';
15+
> 3 | <Trans>Hello <a _t="link" href="/a">link 1</a>, normal, <a _t="link" href="/b">link 2</a>.</Trans>
16+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17+
4 | ]
18+
`;
19+
20+
exports[`Deduplication: Identical elements are reused 1`] = `
21+
import { Trans } from "@lingui/react/macro";
22+
<Trans>
23+
Hello <em>emphasis</em>, normal, <em>more emphasis</em>.
24+
</Trans>;
25+
26+
↓ ↓ ↓ ↓ ↓ ↓
27+
28+
import { Trans as _Trans } from "@lingui/react";
29+
<_Trans
30+
{
31+
/*i18n*/
32+
...{
33+
id: "idxihm",
34+
message: "Hello <em>emphasis</em>, normal, <em>more emphasis</em>.",
35+
components: {
36+
em: <em />,
37+
},
38+
}
39+
}
40+
/>;
41+
42+
`;
43+
44+
exports[`Deduplication: Identical elements with different prop order are reused 1`] = `
45+
import { Trans } from "@lingui/react/macro";
46+
<Trans>
47+
Hello{" "}
48+
<a _t="link" href="/a" class="foo">
49+
link 1
50+
</a>
51+
, normal,{" "}
52+
<a _t="link" class="foo" href="/a">
53+
link 1 copy
54+
</a>
55+
.
56+
</Trans>;
57+
58+
↓ ↓ ↓ ↓ ↓ ↓
59+
60+
import { Trans as _Trans } from "@lingui/react";
61+
<_Trans
62+
{
63+
/*i18n*/
64+
...{
65+
id: "9en3MH",
66+
message: "Hello <link>link 1</link>, normal, <link>link 1 copy</link>.",
67+
components: {
68+
link: <a class="foo" href="/a" />,
69+
},
70+
}
71+
}
72+
/>;
73+
74+
`;
75+
76+
exports[`Deduplication: Implicit names with distinct attributes throw an error 1`] = `
77+
[SyntaxError: <cwd>/<filename>jsx: Unsupported macro usage. Please check the examples at https://lingui.dev/ref/macro#examples-of-js-macros.
78+
If you think this is a bug, fill in an issue at https://github.com/lingui/js-lingui/issues
79+
80+
Error: Multiple distinct JSX elements with the same placeholder name (\`a\`). Differentiate them by adding/modifying the JSX attribute (e.g. \`<element undefined="newName" />\`).
81+
1 |
82+
2 | import { Trans } from '@lingui/react/macro';
83+
> 3 | <Trans>Hello <a href="/a">link 1</a>, normal, <a href="/b">link 2</a>.</Trans>
84+
| ^^^^^^^^^^^^^^^^^^^^^^^
85+
4 |
86+
1 |
87+
2 | import { Trans } from '@lingui/react/macro';
88+
> 3 | <Trans>Hello <a href="/a">link 1</a>, normal, <a href="/b">link 2</a>.</Trans>
89+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
90+
4 | ]
91+
`;
92+
93+
exports[`Deduplication: Same explicit placeholder with identical attributes does not throw 1`] = `
94+
import { Trans } from "@lingui/react/macro";
95+
<Trans>
96+
Hello{" "}
97+
<a _t="link" href="/a">
98+
link 1
99+
</a>
100+
, normal,{" "}
101+
<a _t="link" href="/a">
102+
link 1 copy
103+
</a>
104+
.
105+
</Trans>;
106+
107+
↓ ↓ ↓ ↓ ↓ ↓
108+
109+
import { Trans as _Trans } from "@lingui/react";
110+
<_Trans
111+
{
112+
/*i18n*/
113+
...{
114+
id: "9en3MH",
115+
message: "Hello <link>link 1</link>, normal, <link>link 1 copy</link>.",
116+
components: {
117+
link: <a href="/a" />,
118+
},
119+
}
120+
}
121+
/>;
122+
123+
`;
124+
125+
exports[`Mixing explicit _t together with jsxPlaceholderDefaults 1`] = `
126+
import { Trans } from "@lingui/react/macro";
127+
<Trans>
128+
Hello <a href="/a">link 1</a>, normal,{" "}
129+
<a _t="link2" href="/b">
130+
link 2
131+
</a>
132+
.
133+
</Trans>;
134+
135+
↓ ↓ ↓ ↓ ↓ ↓
136+
137+
import { Trans as _Trans } from "@lingui/react";
138+
<_Trans
139+
{
140+
/*i18n*/
141+
...{
142+
id: "yU9TUm",
143+
message: "Hello <link>link 1</link>, normal, <link2>link 2</link2>.",
144+
components: {
145+
link: <a href="/a" />,
146+
link2: <a href="/b" />,
147+
},
148+
}
149+
}
150+
/>;
151+
152+
`;
153+
154+
exports[`Placeholder attribute is stripped from AST 1`] = `
155+
import { Trans } from "@lingui/react/macro";
156+
<Trans>
157+
<a _t="link" href="/about">
158+
About
159+
</a>
160+
</Trans>;
161+
162+
↓ ↓ ↓ ↓ ↓ ↓
163+
164+
import { Trans as _Trans } from "@lingui/react";
165+
<_Trans
166+
{
167+
/*i18n*/
168+
...{
169+
id: "Ym2S6K",
170+
message: "<link>About</link>",
171+
components: {
172+
link: <a href="/about" />,
173+
},
174+
}
175+
}
176+
/>;
177+
178+
`;
179+
180+
exports[`Respects jsxPlaceholderDefaults 1`] = `
181+
import { Trans } from "@lingui/react/macro";
182+
<Trans>
183+
Here's a <a>link</a> and <em>emphasis</em>.
184+
</Trans>;
185+
186+
↓ ↓ ↓ ↓ ↓ ↓
187+
188+
import { Trans as _Trans } from "@lingui/react";
189+
<_Trans
190+
{
191+
/*i18n*/
192+
...{
193+
id: "Jg_WOt",
194+
message: "Here's a <link>link</link> and <em>emphasis</em>.",
195+
components: {
196+
link: <a />,
197+
em: <em />,
198+
},
199+
}
200+
}
201+
/>;
202+
203+
`;

0 commit comments

Comments
 (0)