Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ph() and explicit labels for placeholders #2092

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -196,6 +196,8 @@ export default function ({
stripMessageProp: shouldStripMessageProp(
state.opts as LinguiPluginOpts
),
isLinguiIdentifier: (node: Identifier, macro) =>
isLinguiIdentifier(path, node, macro),
}
)

Expand Down
62 changes: 52 additions & 10 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,55 @@ 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.isTSAsExpression(node)) {
return tokenizeExpression(node.expression, ctx)
}
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 +300,8 @@ export function expressionToArgument(
): string {
if (t.isIdentifier(exp)) {
return exp.name
} else 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
1 change: 1 addition & 0 deletions packages/babel-plugin-lingui-macro/src/macroJsx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function createMacro() {
stripNonEssentialProps: false,
stripMessageProp: false,
transImportName: "Trans",
isLinguiIdentifier: () => true,
}
)
}
Expand Down
68 changes: 37 additions & 31 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 type { 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 @@ -278,7 +294,7 @@ export class MacroJSX {
)(attr.node)
})

const token: Token = {
let token: Token = {
type: "arg",
format,
name: null,
Expand All @@ -305,10 +321,12 @@ export class MacroJSX {
| NodePath<JSXExpressionContainer>

if (name === "value") {
const exp = value.isLiteral() ? value : value.get("expression")

token.name = this.expressionToArgument(exp)
token.value = exp.node as Expression
token = {
...token,
...this.tokenizeExpression(
value.isLiteral() ? value : value.get("expression")
),
}
} else if (format !== "select" && name === "offset") {
// offset is static parameter, so it must be either string or number
token.options.offset =
Expand Down Expand Up @@ -345,7 +363,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 +381,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 All @@ -382,11 +396,7 @@ export class MacroJSX {
},
})

return {
type: "arg",
name: this.expressionToArgument(exp),
value: exp.node,
}
return this.tokenizeExpression(exp)
}

tokenizeText = (value: string): TextToken => {
Expand All @@ -396,10 +406,6 @@ export class MacroJSX {
}
}

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

isLinguiComponent = (
path: NodePath,
name: JsxMacroName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,78 @@ _i18n._(

`;

exports[`Macro with labeled expression as value 1`] = `
import { plural } from "@lingui/core/macro";
const a = plural(
{ count: getCount() },
{
one: \`# book\`,
other: "# books",
}
);

↓ ↓ ↓ ↓ ↓ ↓

import { i18n as _i18n } from "@lingui/core";
const a = _i18n._(
/*i18n*/
{
id: "esnaQO",
message: "{count, plural, one {# book} other {# books}}",
values: {
count: getCount(),
},
}
);

`;

exports[`Macro with labeled expression as value 2`] = `
import { plural, ph } from "@lingui/core/macro";
const a = plural(ph({ count: getCount() }), {
one: \`# book\`,
other: "# books",
});

↓ ↓ ↓ ↓ ↓ ↓

import { i18n as _i18n } from "@lingui/core";
const a = _i18n._(
/*i18n*/
{
id: "esnaQO",
message: "{count, plural, one {# book} other {# books}}",
values: {
count: getCount(),
},
}
);

`;

exports[`Macro with labeled expression with \`as\` expression 1`] = `
import { plural } from "@lingui/core/macro";
const a = plural({ count: getCount() } as any, {
one: \`# book\`,
other: "# books",
});

↓ ↓ ↓ ↓ ↓ ↓

import { i18n as _i18n } from "@lingui/core";
const a = _i18n._(
/*i18n*/
{
id: "esnaQO",
message: "{count, plural, one {# book} other {# books}}",
values: {
count: getCount(),
},
}
);

`;

exports[`Macro with offset and exact matches 1`] = `
import { plural } from "@lingui/core/macro";
plural(users.length, {
Expand Down
Loading
Loading