Skip to content

Commit

Permalink
feat: add ph() and explicit labels for placeholders
Browse files Browse the repository at this point in the history
  • Loading branch information
timofei-iatsenko committed Nov 24, 2024
1 parent 638aa29 commit 68b0809
Show file tree
Hide file tree
Showing 12 changed files with 332 additions and 31 deletions.
1 change: 1 addition & 0 deletions packages/babel-plugin-lingui-macro/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export enum JsMacroName {
defineMessage = "defineMessage",
arg = "arg",
useLingui = "useLingui",
ph = "ph",
}

export enum JsxMacroName {
Expand Down
2 changes: 2 additions & 0 deletions packages/babel-plugin-lingui-macro/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ export default function ({
stripMessageProp: shouldStripMessageProp(
state.opts as LinguiPluginOpts
),
isLinguiIdentifier: (node: Identifier, macro) =>
isLinguiIdentifier(path, node, macro),
}
)

Expand Down
61 changes: 52 additions & 9 deletions packages/babel-plugin-lingui-macro/src/macroJsAst.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import * as t from "@babel/types"
import {
ObjectExpression,
CallExpression,
Expression,
TemplateLiteral,
Identifier,
Node,
CallExpression,
StringLiteral,
ObjectExpression,
ObjectProperty,
StringLiteral,
TemplateLiteral,
} from "@babel/types"
import { MsgDescriptorPropKey, JsMacroName } from "./constants"
import { Token, TextToken, ArgToken } from "./icu"
import { JsMacroName, MsgDescriptorPropKey } from "./constants"
import { ArgToken, TextToken, Token } from "./icu"
import { createMessageDescriptorFromTokens } from "./messageDescriptorUtils"
import { makeCounter } from "./utils"

Expand Down Expand Up @@ -224,10 +224,52 @@ export function tokenizeChoiceComponent(
return token
}

function tokenizeLabeledExpression(
node: ObjectExpression,
ctx: MacroJsContext
): ArgToken {
if (node.properties.length > 1) {
throw new Error(
"Incorrect usage, expected exactly one property as `{variableName: variableValue}`"
)
}

// assume this is labeled expression, {label: value}
const property = node.properties[0]

if (t.isProperty(property) && t.isIdentifier(property.key)) {
return {
type: "arg",
name: expressionToArgument(property.key, ctx),
value: property.value as Expression,
}
} else {
throw new Error(
"Incorrect usage of a labeled expression. Expected to have one object property with property key as identifier"
)
}
}

export function tokenizeExpression(
node: Node | Expression,
ctx: MacroJsContext
): ArgToken {
if (t.isObjectExpression(node)) {
return tokenizeLabeledExpression(node, ctx)
} else if (
t.isCallExpression(node) &&
isLinguiIdentifier(node.callee, JsMacroName.ph, ctx) &&
node.arguments.length > 0
) {
if (!t.isObjectExpression(node.arguments[0])) {
throw new Error(
"Incorrect usage of `ph` macro. First argument should be an ObjectExpression"
)
}

return tokenizeLabeledExpression(node.arguments[0], ctx)
}

return {
type: "arg",
name: expressionToArgument(node as Expression, ctx),
Expand Down Expand Up @@ -255,11 +297,12 @@ export function expressionToArgument(
): string {
if (t.isIdentifier(exp)) {
return exp.name
} else if (t.isStringLiteral(exp)) {
}

if (t.isStringLiteral(exp)) {
return exp.value
} else {
return String(ctx.getExpressionIndex())
}
return String(ctx.getExpressionIndex())
}

export function isArgDecorator(node: Node, ctx: MacroJsContext): boolean {
Expand Down
50 changes: 32 additions & 18 deletions packages/babel-plugin-lingui-macro/src/macroJsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
StringLiteral,
TemplateLiteral,
SourceLocation,
Identifier,
} from "@babel/types"
import { NodePath } from "@babel/traverse"

Expand All @@ -22,9 +23,15 @@ import {
MACRO_REACT_PACKAGE,
MACRO_LEGACY_PACKAGE,
MsgDescriptorPropKey,
JsMacroName,
} from "./constants"
import cleanJSXElementLiteralChild from "./utils/cleanJSXElementLiteralChild"
import { createMessageDescriptorFromTokens } from "./messageDescriptorUtils"
import {
createMacroJsContext,
MacroJsContext,
tokenizeExpression,
} from "./macroJsAst"

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

export type MacroJsxContext = MacroJsContext & {
elementIndex: () => number
transImportName: string
}

export type MacroJsxOpts = {
stripNonEssentialProps: boolean
stripMessageProp: boolean
transImportName: string
isLinguiIdentifier: (node: Identifier, macro: JsMacroName) => boolean
}

export class MacroJSX {
types: typeof babelTypes
expressionIndex = makeCounter()
elementIndex = makeCounter()
stripNonEssentialProps: boolean
stripMessageProp: boolean
transImportName: string
ctx: MacroJsxContext

constructor({ types }: { types: typeof babelTypes }, opts: MacroJsxOpts) {
this.types = types
this.stripNonEssentialProps = opts.stripNonEssentialProps
this.stripMessageProp = opts.stripMessageProp
this.transImportName = opts.transImportName

this.ctx = {
...createMacroJsContext(
opts.isLinguiIdentifier,
opts.stripNonEssentialProps,
opts.stripMessageProp
),
transImportName: opts.transImportName,
elementIndex: makeCounter(),
}
}

replacePath = (path: NodePath): false | Node => {
Expand All @@ -86,8 +102,8 @@ export class MacroJSX {
const messageDescriptor = createMessageDescriptorFromTokens(
tokens,
path.node.loc,
this.stripNonEssentialProps,
this.stripMessageProp,
this.ctx.stripNonEssentialProps,
this.ctx.stripMessageProp,
{
id,
context,
Expand All @@ -99,7 +115,7 @@ export class MacroJSX {

const newNode = this.types.jsxElement(
this.types.jsxOpeningElement(
this.types.jsxIdentifier(this.transImportName),
this.types.jsxIdentifier(this.ctx.transImportName),
attributes,
true
),
Expand Down Expand Up @@ -345,7 +361,7 @@ export class MacroJSX {
tokenizeElement = (path: NodePath<JSXElement>): ElementToken => {
// !!! Important: Calculate element index before traversing children.
// That way outside elements are numbered before inner elements. (...and it looks pretty).
const name = this.elementIndex()
const name = this.ctx.elementIndex()

return {
type: "element",
Expand All @@ -363,11 +379,7 @@ export class MacroJSX {
}

tokenizeExpression = (path: NodePath<Expression | Node>): ArgToken => {
return {
type: "arg",
name: this.expressionToArgument(path),
value: path.node as Expression,
}
return tokenizeExpression(path.node, this.ctx)
}

tokenizeConditionalExpression = (
Expand Down Expand Up @@ -397,7 +409,9 @@ export class MacroJSX {
}

expressionToArgument(path: NodePath<Expression | Node>): string {
return path.isIdentifier() ? path.node.name : String(this.expressionIndex())
return path.isIdentifier()
? path.node.name
: String(this.ctx.getExpressionIndex())
}

isLinguiComponent = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,86 @@ _i18n._(
`;
exports[`Variables with explicit label 1`] = `
import { t } from "@lingui/core/macro";
t\`Variable \${{ name: random() }}\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "xRRkAE",
message: "Variable {name}",
values: {
name: random(),
},
}
);
`;
exports[`Variables with explicit label, shortcut syntax 1`] = `
import { t } from "@lingui/core/macro";
t\`Variable \${{ name }}\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "xRRkAE",
message: "Variable {name}",
values: {
name: name,
},
}
);
`;
exports[`Variables with explicit ph helper 1`] = `
import { t, ph } from "@lingui/core/macro";
t\`Variable \${ph({ name: random() })}\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "xRRkAE",
message: "Variable {name}",
values: {
name: random(),
},
}
);
`;
exports[`Variables with explicit with ph helper 1`] = `
import { t, ph } from "@lingui/core/macro";
t\`Variable \${ph({ name: random() })}\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "xRRkAE",
message: "Variable {name}",
values: {
name: random(),
},
}
);
`;
exports[`should correctly process nested macro when referenced from different imports 1`] = `
import { t } from "@lingui/core/macro";
import { plural } from "@lingui/core/macro";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,57 @@ import { Trans as _Trans } from "@lingui/react";
`;

exports[`Labeled expressions are supported 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>
Hi {{ name: getUserName() }}, my name is {{ myName: getMyName() }}
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "eqk/cH",
message: "Hi {name}, my name is {myName}",
values: {
name: getUserName(),
myName: getMyName(),
},
}
}
/>;
`;

exports[`Labeled expressions with ph helper 1`] = `
import { Trans } from "@lingui/react/macro";
import { ph } from "@lingui/core/macro";
<Trans>
Hi {ph({ name: getUserName() })}, my name is {ph({ myName: getMyName() })}
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "eqk/cH",
message: "Hi {name}, my name is {myName}",
values: {
name: getUserName(),
myName: getMyName(),
},
}
}
/>;
`;

exports[`Preserve custom ID (literal expression) 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans id={"msg.hello"}>Hello World</Trans>;
Expand Down
Loading

0 comments on commit 68b0809

Please sign in to comment.