Skip to content

Commit 2026c83

Browse files
feat: add ph() and explicit labels for placeholders (#2092)
1 parent ad0151c commit 2026c83

22 files changed

+668
-63
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export enum JsMacroName {
2121
defineMessage = "defineMessage",
2222
arg = "arg",
2323
useLingui = "useLingui",
24+
ph = "ph",
2425
}
2526

2627
export enum JsxMacroName {

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

+2
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ export default function ({
196196
stripMessageProp: shouldStripMessageProp(
197197
state.opts as LinguiPluginOpts
198198
),
199+
isLinguiIdentifier: (node: Identifier, macro) =>
200+
isLinguiIdentifier(path, node, macro),
199201
}
200202
)
201203

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

+52-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import * as t from "@babel/types"
22
import {
3-
ObjectExpression,
3+
CallExpression,
44
Expression,
5-
TemplateLiteral,
65
Identifier,
76
Node,
8-
CallExpression,
9-
StringLiteral,
7+
ObjectExpression,
108
ObjectProperty,
9+
StringLiteral,
10+
TemplateLiteral,
1111
} from "@babel/types"
12-
import { MsgDescriptorPropKey, JsMacroName } from "./constants"
13-
import { Token, TextToken, ArgToken } from "./icu"
12+
import { JsMacroName, MsgDescriptorPropKey } from "./constants"
13+
import { ArgToken, TextToken, Token } from "./icu"
1414
import { createMessageDescriptorFromTokens } from "./messageDescriptorUtils"
1515
import { makeCounter } from "./utils"
1616

@@ -224,10 +224,55 @@ export function tokenizeChoiceComponent(
224224
return token
225225
}
226226

227+
function tokenizeLabeledExpression(
228+
node: ObjectExpression,
229+
ctx: MacroJsContext
230+
): ArgToken {
231+
if (node.properties.length > 1) {
232+
throw new Error(
233+
"Incorrect usage, expected exactly one property as `{variableName: variableValue}`"
234+
)
235+
}
236+
237+
// assume this is labeled expression, {label: value}
238+
const property = node.properties[0]
239+
240+
if (t.isProperty(property) && t.isIdentifier(property.key)) {
241+
return {
242+
type: "arg",
243+
name: expressionToArgument(property.key, ctx),
244+
value: property.value as Expression,
245+
}
246+
} else {
247+
throw new Error(
248+
"Incorrect usage of a labeled expression. Expected to have one object property with property key as identifier"
249+
)
250+
}
251+
}
252+
227253
export function tokenizeExpression(
228254
node: Node | Expression,
229255
ctx: MacroJsContext
230256
): ArgToken {
257+
if (t.isTSAsExpression(node)) {
258+
return tokenizeExpression(node.expression, ctx)
259+
}
260+
if (t.isObjectExpression(node)) {
261+
return tokenizeLabeledExpression(node, ctx)
262+
} else if (
263+
t.isCallExpression(node) &&
264+
isLinguiIdentifier(node.callee, JsMacroName.ph, ctx) &&
265+
node.arguments.length > 0
266+
) {
267+
if (!t.isObjectExpression(node.arguments[0])) {
268+
throw new Error(
269+
"Incorrect usage of `ph` macro. First argument should be an ObjectExpression"
270+
)
271+
}
272+
273+
return tokenizeLabeledExpression(node.arguments[0], ctx)
274+
}
275+
231276
return {
232277
type: "arg",
233278
name: expressionToArgument(node as Expression, ctx),
@@ -255,11 +300,8 @@ export function expressionToArgument(
255300
): string {
256301
if (t.isIdentifier(exp)) {
257302
return exp.name
258-
} else if (t.isStringLiteral(exp)) {
259-
return exp.value
260-
} else {
261-
return String(ctx.getExpressionIndex())
262303
}
304+
return String(ctx.getExpressionIndex())
263305
}
264306

265307
export function isArgDecorator(node: Node, ctx: MacroJsContext): boolean {

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

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ function createMacro() {
3737
stripNonEssentialProps: false,
3838
stripMessageProp: false,
3939
transImportName: "Trans",
40+
isLinguiIdentifier: () => true,
4041
}
4142
)
4243
}

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

+37-31
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
StringLiteral,
1313
TemplateLiteral,
1414
SourceLocation,
15+
Identifier,
1516
} from "@babel/types"
1617
import type { NodePath } from "@babel/traverse"
1718

@@ -22,9 +23,15 @@ import {
2223
MACRO_REACT_PACKAGE,
2324
MACRO_LEGACY_PACKAGE,
2425
MsgDescriptorPropKey,
26+
JsMacroName,
2527
} from "./constants"
2628
import cleanJSXElementLiteralChild from "./utils/cleanJSXElementLiteralChild"
2729
import { createMessageDescriptorFromTokens } from "./messageDescriptorUtils"
30+
import {
31+
createMacroJsContext,
32+
MacroJsContext,
33+
tokenizeExpression,
34+
} from "./macroJsAst"
2835

2936
const pluralRuleRe = /(_[\d\w]+|zero|one|two|few|many|other)/
3037
const jsx2icuExactChoice = (value: string) =>
@@ -43,25 +50,34 @@ function maybeNodeValue(node: Node): { text: string; loc: SourceLocation } {
4350
return null
4451
}
4552

53+
export type MacroJsxContext = MacroJsContext & {
54+
elementIndex: () => number
55+
transImportName: string
56+
}
57+
4658
export type MacroJsxOpts = {
4759
stripNonEssentialProps: boolean
4860
stripMessageProp: boolean
4961
transImportName: string
62+
isLinguiIdentifier: (node: Identifier, macro: JsMacroName) => boolean
5063
}
5164

5265
export class MacroJSX {
5366
types: typeof babelTypes
54-
expressionIndex = makeCounter()
55-
elementIndex = makeCounter()
56-
stripNonEssentialProps: boolean
57-
stripMessageProp: boolean
58-
transImportName: string
67+
ctx: MacroJsxContext
5968

6069
constructor({ types }: { types: typeof babelTypes }, opts: MacroJsxOpts) {
6170
this.types = types
62-
this.stripNonEssentialProps = opts.stripNonEssentialProps
63-
this.stripMessageProp = opts.stripMessageProp
64-
this.transImportName = opts.transImportName
71+
72+
this.ctx = {
73+
...createMacroJsContext(
74+
opts.isLinguiIdentifier,
75+
opts.stripNonEssentialProps,
76+
opts.stripMessageProp
77+
),
78+
transImportName: opts.transImportName,
79+
elementIndex: makeCounter(),
80+
}
6581
}
6682

6783
replacePath = (path: NodePath): false | Node => {
@@ -86,8 +102,8 @@ export class MacroJSX {
86102
const messageDescriptor = createMessageDescriptorFromTokens(
87103
tokens,
88104
path.node.loc,
89-
this.stripNonEssentialProps,
90-
this.stripMessageProp,
105+
this.ctx.stripNonEssentialProps,
106+
this.ctx.stripMessageProp,
91107
{
92108
id,
93109
context,
@@ -99,7 +115,7 @@ export class MacroJSX {
99115

100116
const newNode = this.types.jsxElement(
101117
this.types.jsxOpeningElement(
102-
this.types.jsxIdentifier(this.transImportName),
118+
this.types.jsxIdentifier(this.ctx.transImportName),
103119
attributes,
104120
true
105121
),
@@ -278,7 +294,7 @@ export class MacroJSX {
278294
)(attr.node)
279295
})
280296

281-
const token: Token = {
297+
let token: Token = {
282298
type: "arg",
283299
format,
284300
name: null,
@@ -305,10 +321,12 @@ export class MacroJSX {
305321
| NodePath<JSXExpressionContainer>
306322

307323
if (name === "value") {
308-
const exp = value.isLiteral() ? value : value.get("expression")
309-
310-
token.name = this.expressionToArgument(exp)
311-
token.value = exp.node as Expression
324+
token = {
325+
...token,
326+
...this.tokenizeExpression(
327+
value.isLiteral() ? value : value.get("expression")
328+
),
329+
}
312330
} else if (format !== "select" && name === "offset") {
313331
// offset is static parameter, so it must be either string or number
314332
token.options.offset =
@@ -345,7 +363,7 @@ export class MacroJSX {
345363
tokenizeElement = (path: NodePath<JSXElement>): ElementToken => {
346364
// !!! Important: Calculate element index before traversing children.
347365
// That way outside elements are numbered before inner elements. (...and it looks pretty).
348-
const name = this.elementIndex()
366+
const name = this.ctx.elementIndex()
349367

350368
return {
351369
type: "element",
@@ -363,11 +381,7 @@ export class MacroJSX {
363381
}
364382

365383
tokenizeExpression = (path: NodePath<Expression | Node>): ArgToken => {
366-
return {
367-
type: "arg",
368-
name: this.expressionToArgument(path),
369-
value: path.node as Expression,
370-
}
384+
return tokenizeExpression(path.node, this.ctx)
371385
}
372386

373387
tokenizeConditionalExpression = (
@@ -382,11 +396,7 @@ export class MacroJSX {
382396
},
383397
})
384398

385-
return {
386-
type: "arg",
387-
name: this.expressionToArgument(exp),
388-
value: exp.node,
389-
}
399+
return this.tokenizeExpression(exp)
390400
}
391401

392402
tokenizeText = (value: string): TextToken => {
@@ -396,10 +406,6 @@ export class MacroJSX {
396406
}
397407
}
398408

399-
expressionToArgument(path: NodePath<Expression | Node>): string {
400-
return path.isIdentifier() ? path.node.name : String(this.expressionIndex())
401-
}
402-
403409
isLinguiComponent = (
404410
path: NodePath,
405411
name: JsxMacroName

packages/babel-plugin-lingui-macro/test/__snapshots__/js-plural.test.ts.snap

+72
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,78 @@ _i18n._(
5050
5151
`;
5252
53+
exports[`Macro with labeled expression as value 1`] = `
54+
import { plural } from "@lingui/core/macro";
55+
const a = plural(
56+
{ count: getCount() },
57+
{
58+
one: \`# book\`,
59+
other: "# books",
60+
}
61+
);
62+
63+
↓ ↓ ↓ ↓ ↓ ↓
64+
65+
import { i18n as _i18n } from "@lingui/core";
66+
const a = _i18n._(
67+
/*i18n*/
68+
{
69+
id: "esnaQO",
70+
message: "{count, plural, one {# book} other {# books}}",
71+
values: {
72+
count: getCount(),
73+
},
74+
}
75+
);
76+
77+
`;
78+
79+
exports[`Macro with labeled expression as value 2`] = `
80+
import { plural, ph } from "@lingui/core/macro";
81+
const a = plural(ph({ count: getCount() }), {
82+
one: \`# book\`,
83+
other: "# books",
84+
});
85+
86+
↓ ↓ ↓ ↓ ↓ ↓
87+
88+
import { i18n as _i18n } from "@lingui/core";
89+
const a = _i18n._(
90+
/*i18n*/
91+
{
92+
id: "esnaQO",
93+
message: "{count, plural, one {# book} other {# books}}",
94+
values: {
95+
count: getCount(),
96+
},
97+
}
98+
);
99+
100+
`;
101+
102+
exports[`Macro with labeled expression with \`as\` expression 1`] = `
103+
import { plural } from "@lingui/core/macro";
104+
const a = plural({ count: getCount() } as any, {
105+
one: \`# book\`,
106+
other: "# books",
107+
});
108+
109+
↓ ↓ ↓ ↓ ↓ ↓
110+
111+
import { i18n as _i18n } from "@lingui/core";
112+
const a = _i18n._(
113+
/*i18n*/
114+
{
115+
id: "esnaQO",
116+
message: "{count, plural, one {# book} other {# books}}",
117+
values: {
118+
count: getCount(),
119+
},
120+
}
121+
);
122+
123+
`;
124+
53125
exports[`Macro with offset and exact matches 1`] = `
54126
import { plural } from "@lingui/core/macro";
55127
plural(users.length, {

0 commit comments

Comments
 (0)