From 2a76ac2474a8b6460bc1a3472cd0689573c1c72d Mon Sep 17 00:00:00 2001 From: Timofei Iatsenko Date: Mon, 23 Dec 2024 17:34:23 +0100 Subject: [PATCH] support labeled expressions in macro methods arguments --- .../src/macroJsAst.ts | 4 -- .../babel-plugin-lingui-macro/src/macroJsx.ts | 24 +++---- .../test/__snapshots__/js-plural.test.ts.snap | 72 +++++++++++++++++++ .../__snapshots__/jsx-plural.test.ts.snap | 59 +++++++++++++++ .../test/js-defineMessage.test.ts | 1 + .../test/js-plural.test.ts | 34 +++++++++ .../test/js-select.test.ts | 1 + .../test/js-selectOrdinal.test.ts | 1 + .../test/jsx-plural.test.ts | 24 +++++++ .../test/jsx-select.test.ts | 2 + .../test/jsx-selectOrdinal.test.ts | 1 + .../core/macro/__typetests__/index.test-d.tsx | 58 +++++++++++++++ packages/core/macro/index.d.ts | 45 +++++++----- 13 files changed, 288 insertions(+), 38 deletions(-) diff --git a/packages/babel-plugin-lingui-macro/src/macroJsAst.ts b/packages/babel-plugin-lingui-macro/src/macroJsAst.ts index 13a7ed3bb..f1bad5ba7 100644 --- a/packages/babel-plugin-lingui-macro/src/macroJsAst.ts +++ b/packages/babel-plugin-lingui-macro/src/macroJsAst.ts @@ -301,10 +301,6 @@ export function expressionToArgument( if (t.isIdentifier(exp)) { return exp.name } - - if (t.isStringLiteral(exp)) { - return exp.value - } return String(ctx.getExpressionIndex()) } diff --git a/packages/babel-plugin-lingui-macro/src/macroJsx.ts b/packages/babel-plugin-lingui-macro/src/macroJsx.ts index f74515a07..903382453 100644 --- a/packages/babel-plugin-lingui-macro/src/macroJsx.ts +++ b/packages/babel-plugin-lingui-macro/src/macroJsx.ts @@ -294,7 +294,7 @@ export class MacroJSX { )(attr.node) }) - const token: Token = { + let token: Token = { type: "arg", format, name: null, @@ -321,10 +321,12 @@ export class MacroJSX { | NodePath 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 = @@ -394,11 +396,7 @@ export class MacroJSX { }, }) - return { - type: "arg", - name: this.expressionToArgument(exp), - value: exp.node, - } + return this.tokenizeExpression(exp) } tokenizeText = (value: string): TextToken => { @@ -408,12 +406,6 @@ export class MacroJSX { } } - expressionToArgument(path: NodePath): string { - return path.isIdentifier() - ? path.node.name - : String(this.ctx.getExpressionIndex()) - } - isLinguiComponent = ( path: NodePath, name: JsxMacroName 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 925fb504a..d3bfff5d6 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 @@ -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, { diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-plural.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-plural.test.ts.snap index 920ba58e5..7f4a6431a 100644 --- a/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-plural.test.ts.snap +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-plural.test.ts.snap @@ -261,3 +261,62 @@ import { Trans as _Trans } from "@lingui/react"; />; `; + +exports[`With labeled expression as value 1`] = ` +import { Plural } from "@lingui/react/macro"; +A lot of them} +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "blU5AK", + message: "{count, plural, one {oneText} other {<0>A lot of them}}", + values: { + count: getCount(), + }, + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`With labeled expression as value with ph 1`] = ` +import { Plural } from "@lingui/react/macro"; +import { ph } from "@lingui/core/macro"; +A lot of them} +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "blU5AK", + message: "{count, plural, one {oneText} other {<0>A lot of them}}", + values: { + count: getCount(), + }, + components: { + 0: , + }, + } + } +/>; + +`; 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 e596a9267..f5bad8ac2 100644 --- a/packages/babel-plugin-lingui-macro/test/js-defineMessage.test.ts +++ b/packages/babel-plugin-lingui-macro/test/js-defineMessage.test.ts @@ -1,4 +1,5 @@ import { macroTester } from "./macroTester" +describe.skip("", () => {}) macroTester({ cases: [ 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 9e89a9065..d30769ad2 100644 --- a/packages/babel-plugin-lingui-macro/test/js-plural.test.ts +++ b/packages/babel-plugin-lingui-macro/test/js-plural.test.ts @@ -1,4 +1,5 @@ import { macroTester } from "./macroTester" +describe.skip("", () => {}) macroTester({ cases: [ @@ -46,5 +47,38 @@ macroTester({ }); `, }, + { + name: "Macro with labeled expression as value", + code: ` + import { plural } from '@lingui/core/macro' + const a = plural({ count: getCount() }, { + "one": \`# book\`, + other: "# books" + }); + `, + }, + + { + name: "Macro with labeled expression as value", + code: ` + import { plural, ph } from '@lingui/core/macro' + const a = plural(ph({ count: getCount() }), { + "one": \`# book\`, + other: "# books" + }); + `, + }, + + { + useTypescriptPreset: true, + name: "Macro with labeled expression with `as` expression", + code: ` + import { plural } from '@lingui/core/macro' + const a = plural({ count: getCount() } as any, { + "one": \`# book\`, + other: "# books" + }); + `, + }, ], }) diff --git a/packages/babel-plugin-lingui-macro/test/js-select.test.ts b/packages/babel-plugin-lingui-macro/test/js-select.test.ts index 300466a72..0e2c92d31 100644 --- a/packages/babel-plugin-lingui-macro/test/js-select.test.ts +++ b/packages/babel-plugin-lingui-macro/test/js-select.test.ts @@ -1,4 +1,5 @@ import { macroTester } from "./macroTester" +describe.skip("", () => {}) macroTester({ cases: [ diff --git a/packages/babel-plugin-lingui-macro/test/js-selectOrdinal.test.ts b/packages/babel-plugin-lingui-macro/test/js-selectOrdinal.test.ts index 0ca4d07b4..c3675b27f 100644 --- a/packages/babel-plugin-lingui-macro/test/js-selectOrdinal.test.ts +++ b/packages/babel-plugin-lingui-macro/test/js-selectOrdinal.test.ts @@ -1,4 +1,5 @@ import { macroTester } from "./macroTester" +describe.skip("", () => {}) macroTester({ cases: [ diff --git a/packages/babel-plugin-lingui-macro/test/jsx-plural.test.ts b/packages/babel-plugin-lingui-macro/test/jsx-plural.test.ts index e01faeb0d..de36b0be6 100644 --- a/packages/babel-plugin-lingui-macro/test/jsx-plural.test.ts +++ b/packages/babel-plugin-lingui-macro/test/jsx-plural.test.ts @@ -121,6 +121,30 @@ macroTester({ />; `, }, + { + name: "With labeled expression as value", + code: ` + import { Plural } from '@lingui/react/macro'; + A lot of them} + />; + `, + }, + { + name: "With labeled expression as value with ph", + code: ` + import { Plural } from '@lingui/react/macro'; + import { ph } from '@lingui/core/macro'; + A lot of them} + />; + `, + }, + { filename: `jsx-plural-select-nested.js`, }, diff --git a/packages/babel-plugin-lingui-macro/test/jsx-select.test.ts b/packages/babel-plugin-lingui-macro/test/jsx-select.test.ts index fc5ab05b6..29a20d4cf 100644 --- a/packages/babel-plugin-lingui-macro/test/jsx-select.test.ts +++ b/packages/babel-plugin-lingui-macro/test/jsx-select.test.ts @@ -1,4 +1,5 @@ import { macroTester } from "./macroTester" +describe.skip("", () => {}) macroTester({ cases: [ @@ -27,6 +28,7 @@ macroTester({ `, }, { + only: true, name: "Select should support JSX elements in cases", code: ` import { Select, Trans } from '@lingui/react/macro'; diff --git a/packages/babel-plugin-lingui-macro/test/jsx-selectOrdinal.test.ts b/packages/babel-plugin-lingui-macro/test/jsx-selectOrdinal.test.ts index 1c2432d69..dc423744f 100644 --- a/packages/babel-plugin-lingui-macro/test/jsx-selectOrdinal.test.ts +++ b/packages/babel-plugin-lingui-macro/test/jsx-selectOrdinal.test.ts @@ -1,4 +1,5 @@ import { macroTester } from "./macroTester" +describe.skip("", () => {}) macroTester({ cases: [ diff --git a/packages/core/macro/__typetests__/index.test-d.tsx b/packages/core/macro/__typetests__/index.test-d.tsx index f9407500f..d60346352 100644 --- a/packages/core/macro/__typetests__/index.test-d.tsx +++ b/packages/core/macro/__typetests__/index.test-d.tsx @@ -160,6 +160,25 @@ expectType( }) ) +// with labeled value +expectType( + plural( + { count: 5 }, + { + one: "...", + other: "...", + } + ) +) + +// with labeled value with ph helper +expectType( + plural(ph({ count: 5 }), { + one: "...", + other: "...", + }) +) + expectType( plural(5, { // @ts-expect-error: should accept only strings @@ -224,6 +243,25 @@ expectType( }) ) +// with labeled value +expectType( + selectOrdinal( + { count: 5 }, + { + one: "...", + other: "...", + } + ) +) + +// with labeled value with ph helper +expectType( + selectOrdinal(ph({ count: 5 }), { + one: "...", + other: "...", + }) +) + /////////////////// //// Select /////////////////// @@ -251,6 +289,26 @@ expectType( }) ) +// with labeled value +expectType( + select( + // @ts-expect-error value could be strings only + { count: 5 }, + { + male: "...", + other: "...", + } + ) +) + +// with labeled value with ph helper +expectType( + select(ph({ value: "one" }), { + male: "...", + other: "...", + }) +) + expectType( select("male", { // @ts-expect-error: should accept only strings diff --git a/packages/core/macro/index.d.ts b/packages/core/macro/index.d.ts index fe644c5f8..8559e0279 100644 --- a/packages/core/macro/index.d.ts +++ b/packages/core/macro/index.d.ts @@ -38,7 +38,7 @@ type MacroMessageDescriptor = ( * const message = t({ * id: "msg.hello", * comment: "Greetings at the homepage", - * message: `Hello ${name}`, + * message: `Hello ${{name}}`, * }); * ``` * @@ -55,8 +55,11 @@ type MacroMessageDescriptor = ( */ export function t(descriptor: MacroMessageDescriptor): string -export type LabeledExpression = Record -export type MessagePlaceholder = string | number | LabeledExpression +export type LabeledExpression = Record +export type MessagePlaceholder = + | string + | number + | LabeledExpression /** * Translates a template string using the global I18n instance @@ -64,7 +67,7 @@ export type MessagePlaceholder = string | number | LabeledExpression * @example * ``` * import { t } from "@lingui/core/macro"; - * const message = t`Hello ${name}`; + * const message = t`Hello ${{name}}`; * ``` */ export function t( @@ -81,9 +84,9 @@ export function t( * import { I18n } from "@lingui/core"; * const i18n = new I18n({ * locale: "nl", - * messages: { "Hello {name}": "Hallo {name}" }, + * messages: { "Hello {{name}}": "Hallo {{name}}" }, * }); - * const message = t(i18n)`Hello ${name}`; + * const message = t(i18n)`Hello ${{name}}`; * ``` * * @example @@ -92,13 +95,13 @@ export function t( * import { I18n } from "@lingui/core"; * const i18n = new I18n({ * locale: "nl", - * messages: { "Hello {name}": "Hallo {name}" }, + * messages: { "Hello {{name}}": "Hallo {{name}}" }, * }); - * const message = t(i18n)({ message: `Hello ${name}` }); + * const message = t(i18n)({ message: `Hello ${{name}}` }); * ``` * * @deprecated in v5, would be removed in v6. - * Please use `` i18n._(msg`Hello ${name}`) `` instead + * Please use `` i18n._(msg`Hello ${{name}}`) `` instead * */ export function t(i18n: I18n): { @@ -112,7 +115,7 @@ export function t(i18n: I18n): { * @example * ``` * import { plural } from "@lingui/core/macro"; - * const message = plural(count, { + * const message = plural({count}, { * one: "# Book", * other: "# Books", * }); @@ -121,7 +124,10 @@ export function t(i18n: I18n): { * @param value Determines the plural form * @param options Object with available plural forms */ -export function plural(value: number | string, options: ChoiceOptions): string +export function plural( + value: number | string | LabeledExpression, + options: ChoiceOptions +): string /** * Pluralize a message using ordinal forms @@ -132,7 +138,7 @@ export function plural(value: number | string, options: ChoiceOptions): string * @example * ``` * import { selectOrdinal } from "@lingui/core/macro"; - * const message = selectOrdinal(count, { + * const message = selectOrdinal({count}, { * one: "#st", * two: "#nd", * few: "#rd", @@ -144,7 +150,7 @@ export function plural(value: number | string, options: ChoiceOptions): string * @param options Object with available plural forms */ export function selectOrdinal( - value: number | string, + value: number | string | LabeledExpression, options: ChoiceOptions ): string @@ -164,7 +170,7 @@ type SelectOptions = { * @example * ``` * import { select } from "@lingui/core/macro"; - * const message = select(gender, { + * const message = select({gender}, { * male: "he", * female: "she", * other: "they", @@ -174,7 +180,10 @@ type SelectOptions = { * @param value The key of choices to use * @param choices */ -export function select(value: string, choices: SelectOptions): string +export function select( + value: string | LabeledExpression, + choices: SelectOptions +): string /** * Define a message for later use @@ -187,7 +196,7 @@ export function select(value: string, choices: SelectOptions): string * import { defineMessage } from "@lingui/core/macro"; * const message = defineMessage({ * comment: "Greetings on the welcome page", - * message: `Welcome, ${name}!`, + * message: `Welcome, ${{name}}!`, * }); * ``` * @@ -203,10 +212,10 @@ export function defineMessage( * @example * ``` * import { defineMessage, msg } from "@lingui/core/macro"; - * const message = defineMessage`Hello ${name}`; + * const message = defineMessage`Hello ${{name}}`; * * // or using shorter version - * const message = msg`Hello ${name}`; + * const message = msg`Hello ${{name}}`; * ``` */ export function defineMessage(