diff --git a/packages/babel-plugin-lingui-macro/src/macro.ts b/packages/babel-plugin-lingui-macro/src/macro.ts index 679a88bfa..08fa9ded6 100644 --- a/packages/babel-plugin-lingui-macro/src/macro.ts +++ b/packages/babel-plugin-lingui-macro/src/macro.ts @@ -29,6 +29,8 @@ function macro({ state, babel, config }: MacroParams) { ;[ JsMacroName.defineMessage, JsMacroName.msg, + JsMacroName.arg, + JsMacroName.ph, JsMacroName.t, JsMacroName.useLingui, JsMacroName.plural, diff --git a/packages/babel-plugin-lingui-macro/src/macroJsAst.ts b/packages/babel-plugin-lingui-macro/src/macroJsAst.ts index f1bad5ba7..373ceec5d 100644 --- a/packages/babel-plugin-lingui-macro/src/macroJsAst.ts +++ b/packages/babel-plugin-lingui-macro/src/macroJsAst.ts @@ -257,20 +257,11 @@ export function tokenizeExpression( 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) + } else if (t.isCallExpression(node) && isPhDecorator(node, ctx)) { + return tokenizePhDecorator(node, ctx) } return { @@ -280,18 +271,24 @@ export function tokenizeExpression( } } +function tokenizePhDecorator(node: CallExpression, ctx: MacroJsContext) { + if (node.arguments.length !== 1 || !t.isObjectExpression(node.arguments[0])) { + throw new Error( + "Incorrect usage of `ph` macro. Expected exactly one argument as `{variableName: variableValue}`" + ) + } + + return tokenizeLabeledExpression(node.arguments[0], ctx) +} + export function tokenizeArg( node: CallExpression, ctx: MacroJsContext ): ArgToken { - const arg = node.arguments[0] as Expression + const token = tokenizeExpression(node.arguments[0] as Expression, ctx) + token.raw = true - return { - type: "arg", - name: expressionToArgument(arg, ctx), - raw: true, - value: arg, - } + return token } export function expressionToArgument( @@ -304,7 +301,14 @@ export function expressionToArgument( return String(ctx.getExpressionIndex()) } -export function isArgDecorator(node: Node, ctx: MacroJsContext): boolean { +export function isPhDecorator(node: Node, ctx: MacroJsContext) { + return ( + t.isCallExpression(node) && + isLinguiIdentifier(node.callee, JsMacroName.ph, ctx) + ) +} + +export function isArgDecorator(node: Node, ctx: MacroJsContext) { return ( t.isCallExpression(node) && isLinguiIdentifier(node.callee, JsMacroName.arg, ctx) diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-defineMessage.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-defineMessage.test.ts.snap index 414de0c8c..39883be93 100644 --- a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-defineMessage.test.ts.snap +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-defineMessage.test.ts.snap @@ -106,7 +106,7 @@ const message = `; exports[`should expand macros in message property 1`] = ` -import { defineMessage, plural, arg } from "@lingui/core/macro"; +import { defineMessage, plural } from "@lingui/core/macro"; const message = defineMessage({ comment: "Description", message: plural(value, { one: "book", other: "books" }), diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-plural.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-plural.test.ts.snap index d3bfff5d6..5cecfbb69 100644 --- a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-plural.test.ts.snap +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-plural.test.ts.snap @@ -147,6 +147,31 @@ _i18n._( `; +exports[`Macro with utility arg macro usage in options 1`] = ` +import { plural, arg } from "@lingui/core/macro"; +plural(count, { + one: \`# book on {\${arg(today)}, date}\`, + other: \`# books on {\${arg(today)}, date}\`, +}); + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as _i18n } from "@lingui/core"; +_i18n._( + /*i18n*/ + { + id: "lEIbMo", + message: + "{count, plural, one {# book on {today, date}} other {# books on {today, date}}}", + values: { + count: count, + today: today, + }, + } +); + +`; + exports[`plural macro could be renamed 1`] = ` import { plural as plural2 } from "@lingui/core/macro"; const a = plural2(count, { diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-t.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-t.test.ts.snap index 0aada704a..e47ac3f59 100644 --- a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-t.test.ts.snap +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-t.test.ts.snap @@ -514,6 +514,57 @@ _i18n._( `; +exports[`Variables with arg macro is not wrapped in curly brackets 1`] = ` +import { t, arg } from "@lingui/core/macro"; +t\`Number {\${arg(num)}, number, myNumberStyle}\`; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as _i18n } from "@lingui/core"; +_i18n._( + /*i18n*/ + { + id: "6HvXd1", + message: "Number {num, number, myNumberStyle}", + values: { + num: num, + }, + } +); + +`; + +exports[`Variables with arg macro supports named placeholders syntax 1`] = ` +import { t, arg, ph } from "@lingui/core/macro"; +t\`Number {\${arg({ num: getNum() })}, number, myNumberStyle}\`; +t\`Number {\${arg(ph({ num: getNum() }))}, number, myNumberStyle}\`; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as _i18n } from "@lingui/core"; +_i18n._( + /*i18n*/ + { + id: "6HvXd1", + message: "Number {num, number, myNumberStyle}", + values: { + num: getNum(), + }, + } +); +_i18n._( + /*i18n*/ + { + id: "6HvXd1", + message: "Number {num, number, myNumberStyle}", + values: { + num: getNum(), + }, + } +); + +`; + exports[`Variables with escaped double quotes are correctly formatted 1`] = ` import { t } from "@lingui/core/macro"; t\`Variable "name"\`; diff --git a/packages/babel-plugin-lingui-macro/test/js-defineMessage.test.ts b/packages/babel-plugin-lingui-macro/test/js-defineMessage.test.ts index f5bad8ac2..33b1e2d46 100644 --- a/packages/babel-plugin-lingui-macro/test/js-defineMessage.test.ts +++ b/packages/babel-plugin-lingui-macro/test/js-defineMessage.test.ts @@ -21,7 +21,7 @@ macroTester({ { name: "should expand macros in message property", code: ` - import { defineMessage, plural, arg } from '@lingui/core/macro'; + import { defineMessage, plural } from '@lingui/core/macro'; const message = defineMessage({ comment: "Description", message: plural(value, { one: "book", other: "books" }) diff --git a/packages/babel-plugin-lingui-macro/test/js-plural.test.ts b/packages/babel-plugin-lingui-macro/test/js-plural.test.ts index d30769ad2..6bf4fc494 100644 --- a/packages/babel-plugin-lingui-macro/test/js-plural.test.ts +++ b/packages/babel-plugin-lingui-macro/test/js-plural.test.ts @@ -69,6 +69,17 @@ macroTester({ `, }, + { + name: "Macro with utility arg macro usage in options", + code: ` + import { plural, arg } from '@lingui/core/macro'; + plural(count, { + "one": \`# book on {\${arg(today)}, date}\`, + other: \`# books on {\${arg(today)}, date}\` + }); + `, + }, + { useTypescriptPreset: true, name: "Macro with labeled expression with `as` expression", diff --git a/packages/babel-plugin-lingui-macro/test/js-t.test.ts b/packages/babel-plugin-lingui-macro/test/js-t.test.ts index b6e63ae61..a5f86b2f5 100644 --- a/packages/babel-plugin-lingui-macro/test/js-t.test.ts +++ b/packages/babel-plugin-lingui-macro/test/js-t.test.ts @@ -109,6 +109,21 @@ macroTester({ t\`Variable \${{name} as any}\`; `, }, + { + name: "Variables with arg macro is not wrapped in curly brackets", + code: ` + import { t, arg } from '@lingui/core/macro'; + t\`Number {\${arg(num)}, number, myNumberStyle}\`; + `, + }, + { + name: "Variables with arg macro supports named placeholders syntax", + code: ` + import { t, arg, ph } from '@lingui/core/macro'; + t\`Number {\${arg({num: getNum()})}, number, myNumberStyle}\`; + t\`Number {\${arg(ph({num: getNum()}))}, number, myNumberStyle}\`; + `, + }, { name: "Newlines are preserved", code: ` diff --git a/packages/core/macro/__typetests__/index.tst.ts b/packages/core/macro/__typetests__/index.tst.ts index 72ab87446..1bec04a12 100644 --- a/packages/core/macro/__typetests__/index.tst.ts +++ b/packages/core/macro/__typetests__/index.tst.ts @@ -9,6 +9,7 @@ import { selectOrdinal, select, ph, + arg, } from "@lingui/core/macro" const name = "Jack" @@ -295,3 +296,16 @@ expect( // should accept only strings expect(select).type.not.toBeCallableWith("male", { male: 5, other: 5 }) + +/////////////////// +//// Arg +/////////////////// + +// simple +expect(t`Hello ${arg(name)}`).type.toBe() + +// with labeled value +expect(t`Hello ${arg({ name: user.name })}`).type.toBe() + +// with ph labeled value +expect(t`Hello ${arg(ph({ name: user.name }))}`).type.toBe() diff --git a/packages/core/macro/index.d.ts b/packages/core/macro/index.d.ts index 8a26aa185..0e056f030 100644 --- a/packages/core/macro/index.d.ts +++ b/packages/core/macro/index.d.ts @@ -233,3 +233,15 @@ export const msg: typeof defineMessage * Helps to define a name for a variable in the message */ export function ph(def: LabeledExpression): string + +/** + * Helps to inject a variable into the other macro usage without automatically wrapping it in curly brackets, + * so it can be used with custom ICU expressions. + * + * @example + * ``` + * import { t, arg } from "@lingui/core/macro"; + * t`Number {${arg(num)}, number, myNumberStyle}`; + * ``` + */ +export function arg(def: MessagePlaceholder): string diff --git a/website/docs/ref/macro.mdx b/website/docs/ref/macro.mdx index 25f46fe37..47e323201 100644 --- a/website/docs/ref/macro.mdx +++ b/website/docs/ref/macro.mdx @@ -499,6 +499,35 @@ const message = /*i18n*/ { ::: +### `arg` + +The `arg` macro is a utility macro that helps to inject a message variables into the other macro usage without automatically wrapping them in curly brackets. + +```ts +arg(value: string | number) +``` + +It can be used to apply custom formatting to a variable inside the other macro call. It is an escape hatch for writing message syntax using [ICU MessageFormat](/guides/message-format) on your own. + +```js +import { t, arg } from "@lingui/core/macro"; +const message = t`Number {${arg(num)}, number, myNumberStyle}`; + +// ↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n } from "@lingui/core"; + +const message = i18n._( + /*i18n*/ { + id: "6HvXd1", + message: "Number {num, number, myNumberStyle}", + values: { num }, + } +); +``` + +::: + ## React Macros React (JSX) Macros are used in JSX elements and are transformed into the [`Trans`](/ref/react#trans) component imported from the [`@lingui/react`](/ref/react) package.