diff --git a/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap b/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap index d1c8dc8cc..51760bd42 100644 --- a/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap +++ b/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap @@ -237,6 +237,26 @@ exports[`@lingui/babel-plugin-extract-messages should extract all messages from 38, ], }, + { + comment: undefined, + context: Context2, + id: Some ID, + message: undefined, + origin: [ + js-with-macros.js, + 43, + ], + }, + { + comment: undefined, + context: undefined, + id: sD7MQ4, + message: TplLiteral, + origin: [ + js-with-macros.js, + 48, + ], + }, ] `; diff --git a/packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js b/packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js index f25dca51e..57083825a 100644 --- a/packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js +++ b/packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js @@ -1,41 +1,48 @@ -import { t, defineMessage } from "@lingui/macro" +import { t, defineMessage, msg } from "@lingui/macro" t`Message` -const msg = t`Message` +const msg1 = t`Message` const withDescription = defineMessage({ - message: 'Description', - comment: "description" + message: "Description", + comment: "description", }) const withId = defineMessage({ - id: 'ID', - message: 'Message with id' + id: "ID", + message: "Message with id", }) const withValues = t`Values ${param}` const withTId = t({ id: "ID Some", - message: "Message with id some" + message: "Message with id some", }) const withTIdBacktick = t({ - id: `Backtick` + id: `Backtick`, }) const tWithContextA = t({ id: "Some ID", - context: "Context1" + context: "Context1", }) const tWithContextB = t({ id: "Some other ID", - context: "Context1" + context: "Context1", }) const defineMessageWithContext = defineMessage({ id: "Some ID", - context: "Context2" + context: "Context2", }) + +const defineMessageAlias = msg({ + id: "Some ID", + context: "Context2", +}) + +const defineMessageAlias2 = msg`TplLiteral` diff --git a/packages/macro/__typetests__/index.test-d.tsx b/packages/macro/__typetests__/index.test-d.tsx index ee9f57d72..2dbcd3887 100644 --- a/packages/macro/__typetests__/index.test-d.tsx +++ b/packages/macro/__typetests__/index.test-d.tsx @@ -4,6 +4,7 @@ import type { MessageDescriptor, I18n } from "@lingui/core" import { t, defineMessage, + msg, plural, selectOrdinal, select, @@ -76,6 +77,16 @@ expectType( message: "Hello world", }) ) +expectType( + msg({ + id: "custom.id", + comment: "Hello", + context: "context", + message: "Hello world", + }) +) +expectType(defineMessage`Message`) +expectType(msg`Message`) // @ts-expect-error id or message should be presented expectType(defineMessage({})) diff --git a/packages/macro/__typetests__/tsconfig.json b/packages/macro/__typetests__/tsconfig.json index 4c701ebe2..d1deb8a19 100644 --- a/packages/macro/__typetests__/tsconfig.json +++ b/packages/macro/__typetests__/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "jsx": "react", - "esModuleInterop": true + "esModuleInterop": true, + "strict": true }, - "paths": {} + "paths": {}, + "include": [ + "./**.tsx" + ] } diff --git a/packages/macro/index.d.ts b/packages/macro/index.d.ts index 5eee8991d..b09a005f8 100644 --- a/packages/macro/index.d.ts +++ b/packages/macro/index.d.ts @@ -193,6 +193,29 @@ export function defineMessage( descriptor: MacroMessageDescriptor ): MessageDescriptor +/** + * Define a message for later use + * + * @example + * ``` + * import { defineMessage, msg } from "@lingui/macro"; + * const message = defineMessage`Hello ${name}`; + * + * // or using shorter version + * const message = msg`Hello ${name}`; + * ``` + */ +export function defineMessage( + literals: TemplateStringsArray, + ...placeholders: any[] +): string + +/** + * Define a message for later use + * Alias for {@see defineMessage} + */ +export const msg: typeof defineMessage + type CommonProps = { id?: string comment?: string diff --git a/packages/macro/src/index.ts b/packages/macro/src/index.ts index 9b144b2de..5a22004e3 100644 --- a/packages/macro/src/index.ts +++ b/packages/macro/src/index.ts @@ -20,6 +20,7 @@ export type LinguiMacroOpts = { const jsMacroTags = new Set([ "defineMessage", + "msg", "arg", "t", "plural", diff --git a/packages/macro/src/macroJs.ts b/packages/macro/src/macroJs.ts index 2f6063006..0198eb0ca 100644 --- a/packages/macro/src/macroJs.ts +++ b/packages/macro/src/macroJs.ts @@ -18,6 +18,7 @@ import ICUMessageFormat, { ParsedResult, TextToken, Token, + Tokens, } from "./icu" import { makeCounter } from "./utils" import { COMMENT, CONTEXT, EXTRACT_MARK, ID, MESSAGE } from "./constants" @@ -30,6 +31,13 @@ function normalizeWhitespace(text: string): string { return text.replace(keepSpaceRe, " ").replace(keepNewLineRe, "\n").trim() } +function buildICUFromTokens(tokens: Tokens) { + const messageFormat = new ICUMessageFormat() + const { message, values } = messageFormat.fromTokens(tokens) + + return { message: normalizeWhitespace(message), values } +} + export type MacroJsOpts = { i18nImportName: string stripNonEssentialProps: boolean @@ -62,24 +70,11 @@ export default class MacroJs { replacePathWithMessage = ( path: NodePath, - { - message, - values, - }: { message: ParsedResult["message"]; values: ParsedResult["values"] }, + tokens: Tokens, linguiInstance?: babelTypes.Expression ) => { - const properties: ObjectProperty[] = [ - this.createIdProperty(message), - this.createObjectProperty(MESSAGE, this.types.stringLiteral(message)), - this.createValuesProperty(values), - ] - const newNode = this.createI18nCall( - this.createMessageDescriptor( - properties, - // preserve line numbers for extractor - path.node.loc - ), + this.createMessageDescriptorFromTokens(tokens, path.node.loc), linguiInstance ) @@ -91,9 +86,29 @@ export default class MacroJs { // reset the expression counter this._expressionIndex = makeCounter() - if (this.isDefineMessage(path.node)) { - this.replaceDefineMessage(path as NodePath) - return true + // defineMessage({ message: "Message", context: "My" }) -> {id: , message: "Message"} + if ( + this.types.isCallExpression(path.node) && + this.isDefineMessage(path.node.callee) + ) { + let descriptor = this.processDescriptor(path.node.arguments[0]) + path.replaceWith(descriptor) + return false + } + + // defineMessage`Message` -> {id: , message: "Message"} + if ( + this.types.isTaggedTemplateExpression(path.node) && + this.isDefineMessage(path.node.tag) + ) { + const tokens = this.tokenizeTemplateLiteral(path.node.quasi) + const descriptor = this.createMessageDescriptorFromTokens( + tokens, + path.node.loc + ) + + path.replaceWith(descriptor) + return false } // t(i18nInstance)`Message` -> i18nInstance._(messageDescriptor) @@ -107,15 +122,7 @@ export default class MacroJs { const i18nInstance = path.node.arguments[0] const tokens = this.tokenizeNode(path.parentPath.node) - const messageFormat = new ICUMessageFormat() - const { message: messageRaw, values } = messageFormat.fromTokens(tokens) - const message = normalizeWhitespace(messageRaw) - - this.replacePathWithMessage( - path.parentPath, - { message, values }, - i18nInstance - ) + this.replacePathWithMessage(path.parentPath, tokens, i18nInstance) return false } @@ -135,6 +142,7 @@ export default class MacroJs { return false } + // t({...}) if ( this.types.isCallExpression(path.node) && this.isLinguiIdentifier(path.node.callee, "t") @@ -145,45 +153,11 @@ export default class MacroJs { const tokens = this.tokenizeNode(path.node) - const messageFormat = new ICUMessageFormat() - const { message: messageRaw, values } = messageFormat.fromTokens(tokens) - const message = normalizeWhitespace(messageRaw) - - this.replacePathWithMessage(path, { message, values }) + this.replacePathWithMessage(path, tokens) return true } - /** - * macro `defineMessage` is called with MessageDescriptor. The only - * thing that happens is that any macros used in `message` property - * are replaced with formatted message. - * - * import { defineMessage, plural } from '@lingui/macro'; - * const message = defineMessage({ - * id: "msg.id", - * comment: "Description", - * message: plural(value, { one: "book", other: "books" }) - * }) - * - * ↓ ↓ ↓ ↓ ↓ ↓ - * - * const message = { - * id: "msg.id", - * comment: "Description", - * message: "{value, plural, one {book} other {books}}" - * } - * - */ - replaceDefineMessage = (path: NodePath) => { - // reset the expression counter - this._expressionIndex = makeCounter() - - let descriptor = this.processDescriptor(path.node.arguments[0]) - - path.replaceWith(descriptor) - } - /** * macro `t` is called with MessageDescriptor, after that * we create a new node to append it to i18n._ @@ -209,7 +183,8 @@ export default class MacroJs { * * { * comment: "Description", - * id: "{value, plural, one {book} other {books}}" + * id: + * message: "{value, plural, one {book} other {books}}" * } * */ @@ -237,9 +212,7 @@ export default class MacroJs { let messageNode = messageProperty.value as StringLiteral if (tokens) { - const messageFormat = new ICUMessageFormat() - const { message: messageRaw, values } = messageFormat.fromTokens(tokens) - const message = normalizeWhitespace(messageRaw) + const { message, values } = buildICUFromTokens(tokens) messageNode = this.types.stringLiteral(message) properties.push(this.createValuesProperty(values)) @@ -435,6 +408,26 @@ export default class MacroJs { ) } + createMessageDescriptorFromTokens(tokens: Tokens, oldLoc?: SourceLocation) { + const { message, values } = buildICUFromTokens(tokens) + + const properties: ObjectProperty[] = [ + this.createIdProperty(message), + + !this.stripNonEssentialProps + ? this.createObjectProperty(MESSAGE, this.types.stringLiteral(message)) + : null, + + this.createValuesProperty(values), + ] + + return this.createMessageDescriptor( + properties, + // preserve line numbers for extractor + oldLoc + ) + } + createMessageDescriptor( properties: ObjectProperty[], oldLoc?: SourceLocation @@ -473,10 +466,10 @@ export default class MacroJs { }) } - isDefineMessage(node: Node): boolean { + isDefineMessage(node: Node | Expression): boolean { return ( - this.types.isCallExpression(node) && - this.isLinguiIdentifier(node.callee, "defineMessage") + this.isLinguiIdentifier(node, "defineMessage") || + this.isLinguiIdentifier(node, "msg") ) } diff --git a/packages/macro/test/js-defineMessage.ts b/packages/macro/test/js-defineMessage.ts index 934d1db61..d5fabb7c1 100644 --- a/packages/macro/test/js-defineMessage.ts +++ b/packages/macro/test/js-defineMessage.ts @@ -1,6 +1,43 @@ import { TestCase } from "./index" const cases: TestCase[] = [ + { + name: "defineMessage should support template literal", + input: ` + import { defineMessage } from '@lingui/macro'; + const message = defineMessage\`Message\` + `, + expected: ` + const message = + /*i18n*/ + { + id: "xDAtGP", + message: "Message", + }; + `, + }, + { + name: "defineMessage can be called by alias `msg`", + input: ` + import { msg } from '@lingui/macro'; + const message1 = msg\`Message\` + const message2 = msg({message: "Message"}) + `, + expected: ` + const message1 = + /*i18n*/ + { + id: "xDAtGP", + message: "Message", + }; + const message2 = + /*i18n*/ + { + message: "Message", + id: "xDAtGP", + }; + `, + }, { name: "should expand macros in message property", input: ` @@ -11,7 +48,6 @@ const cases: TestCase[] = [ }) `, expected: ` - import { i18n } from "@lingui/core"; const message = /*i18n*/ { @@ -31,7 +67,6 @@ const cases: TestCase[] = [ }) `, expected: ` - import { i18n } from "@lingui/core"; const message = /*i18n*/ { @@ -53,7 +88,6 @@ const cases: TestCase[] = [ }) `, expected: ` - import { i18n } from "@lingui/core"; const message = /*i18n*/ { @@ -71,7 +105,6 @@ const cases: TestCase[] = [ }) `, expected: ` - import { i18n } from "@lingui/core"; const message = /*i18n*/ { @@ -93,7 +126,6 @@ const cases: TestCase[] = [ }) `, expected: ` - import { i18n } from "@lingui/core"; const message = /*i18n*/ { @@ -114,7 +146,6 @@ const cases: TestCase[] = [ }) `, expected: ` - import { i18n } from "@lingui/core"; const msg = /*i18n*/ { @@ -138,7 +169,6 @@ const cases: TestCase[] = [ }) `, expected: ` - import { i18n } from "@lingui/core"; const msg = /*i18n*/ { @@ -158,7 +188,6 @@ const cases: TestCase[] = [ }) `, expected: ` - import { i18n } from "@lingui/core"; const message = /*i18n*/ { diff --git a/packages/macro/test/js-t.ts b/packages/macro/test/js-t.ts index fcb1b2c13..e48684c81 100644 --- a/packages/macro/test/js-t.ts +++ b/packages/macro/test/js-t.ts @@ -367,6 +367,21 @@ const cases: TestCase[] = [ }); `, }, + { + name: "Production - only essential props are kept", + production: true, + input: ` + import { t } from '@lingui/macro'; + const msg = t\`Message\` + `, + expected: ` + import { i18n } from "@lingui/core"; + const msg = i18n._(/*i18n*/ + { + id: "xDAtGP", + }); + `, + }, { name: "Production - only essential props are kept, with plural, with custom i18n instance", production: true, diff --git a/website/docs/ref/macro.md b/website/docs/ref/macro.md index 07cd010c6..397ed4bec 100644 --- a/website/docs/ref/macro.md +++ b/website/docs/ref/macro.md @@ -56,7 +56,7 @@ The advantages of using macros are: **JSX macros** are transformed to [`Trans`](/docs/ref/react.md#trans) component from [`@lingui/react`](/docs/ref/react.md): -``` jsx +```jsx import { Trans } from "@lingui/macro" Attachment {name} saved @@ -195,6 +195,16 @@ customI18n._(/*i18n*/{ }) ``` ```js +const msg = defineMessage`Refresh inbox` + +// ↓ ↓ ↓ ↓ ↓ ↓ + +const msg = /*i18n*/{ + id: "EsCV2T", + message: "Refresh inbox" +} +``` +```js const msg = defineMessage({ id: "msg.refresh", message: "Refresh inbox" @@ -310,7 +320,6 @@ const message = i18n._(/*i18n*/ { id: "mY42CM", message: "Hello World", - values: { name } }) ``` @@ -598,21 +607,47 @@ const message = t({ ``` ::: -### `defineMessage` +### `defineMessage` alias: `msg` {#definemessage} + +`defineMessage` macro allows to define message for later use. It has the same signature as `t` macro, but unlike it, it doesn't wrap generated *MessageDescription* into [`i18n._`](/docs/ref/core.md#i18n._) call. + +```ts +import { defineMessage } from "@lingui/macro" +const message = defineMessage`Hello World` + +// ↓ ↓ ↓ ↓ ↓ ↓ + +const message = + i18n._(/*i18n*/ { + id: "mY42CM", + message: "Hello World", + }) +``` + +You also can use shorter alias of `defineMessage` macro: -`defineMessage` macro is a wrapper around macros above which allows you to add comments for translators or override the message ID. +```ts +import { msg } from "@lingui/macro" +const message = msg`Hello World` -Unlike the other JS macros, it doesn't wrap generated *MessageDescription* into [`i18n._`](/docs/ref/core.md#i18n._) call. +// ↓ ↓ ↓ ↓ ↓ ↓ + +const message = + i18n._(/*i18n*/ { + id: "mY42CM", + message: "Hello World", + }) +``` + +`defineMessage` macro also supports `MacroMessageDescriptor` object as input. That can be used to provide additional information for message such as comment or context. ```ts -type MessageDescriptor = { +type MacroMessageDescriptor = { id?: string, message?: string, comment?: string, context?: string, } - -defineMessage(message: MessageDescriptor) ``` Either `id` or `message` property is required. `id` is a custom message ID. If it isn't set, the `message` (and `context` if provided) are used for generating an ID. @@ -635,7 +670,7 @@ const message = /*i18n*/{ `message` is the default message. Any JS macro can be used here. Template string literals don't need to be tagged with [`t`](#t). ```js -import { defineMessage, t } from "@lingui/macro" +import { defineMessage } from "@lingui/macro" const name = "Joe" diff --git a/website/docs/tutorials/react-patterns.md b/website/docs/tutorials/react-patterns.md index 46a70ff49..9e678c1fa 100644 --- a/website/docs/tutorials/react-patterns.md +++ b/website/docs/tutorials/react-patterns.md @@ -164,13 +164,13 @@ export function alert() { Messages don't have to be declared at the same code location where they're displayed. Tag a string with the [`defineMessage`](/docs/ref/macro.md#definemessage) macro, and you've created a "message descriptor", which can then be passed around as a variable, and can be displayed as a translated string by passing its `id` to [`Trans`](/docs/ref/macro.md#trans) as its `id` prop: ```jsx -import { defineMessage, Trans } from "@lingui/macro" +import { msg, Trans } from "@lingui/macro" const favoriteColors = [ - defineMessage({message: "Red"}), - defineMessage({message: "Orange"}), - defineMessage({message: "Yellow"}), - defineMessage({message: "Green"}), + msg`Red`, + msg`Orange`, + msg`Yellow`, + msg`Green`, ] export default function ColorList() { @@ -188,13 +188,13 @@ Or to render the message descriptor as a string-only translation, just pass it t ```jsx import { i18n } from "@lingui/core" -import { defineMessage } from "@lingui/macro" +import { msg } from "@lingui/macro" const favoriteColors = [ - defineMessage({message: "Red"}), - defineMessage({message: "Orange"}), - defineMessage({message: "Yellow"}), - defineMessage({message: "Green"}), + msg`Red`, + msg`Orange`, + msg`Yellow`, + msg`Green`, ] export function getTranslatedColorNames() { @@ -251,10 +251,10 @@ import { defineMessage } from "@lingui/macro"; import { i18n } from "@lingui/core"; const statusMessages = { - ['STATUS_OPEN']: defineMessage({message: "Open"}), - ['STATUS_CLOSED']: defineMessage({message: "Closed"}), - ['STATUS_CANCELLED']: defineMessage({message: "Cancelled"}), - ['STATUS_COMPLETED']: defineMessage({message: "Completed"}), + ['STATUS_OPEN']: msg`Open`, + ['STATUS_CLOSED']: msg`Closed`, + ['STATUS_CANCELLED']: msg`Cancelled`, + ['STATUS_COMPLETED']: msg`Completed`, } export default function StatusDisplay({ statusCode }) {