diff --git a/packages/macro/src/macroJs.ts b/packages/macro/src/macroJs.ts index 18cd9b12c..98619d63a 100644 --- a/packages/macro/src/macroJs.ts +++ b/packages/macro/src/macroJs.ts @@ -1,9 +1,23 @@ import * as R from "ramda" import * as babelTypes from "@babel/types" -import {Expression, Node, CallExpression, ObjectExpression, isObjectProperty, ObjectProperty, Identifier, StringLiteral} from "@babel/types" +import { + Expression, + Node, + CallExpression, + ObjectExpression, + isObjectProperty, + ObjectProperty, + Identifier, + StringLiteral, +} from "@babel/types" import { NodePath } from "@babel/traverse" -import ICUMessageFormat, {ArgToken, ParsedResult, TextToken, Tokens} from "./icu" +import ICUMessageFormat, { + ArgToken, + ParsedResult, + TextToken, + Tokens, +} from "./icu" import { zip, makeCounter } from "./utils" import { COMMENT, ID, MESSAGE, EXTRACT_MARK } from "./constants" @@ -24,22 +38,25 @@ export default class MacroJs { // Positional expressions counter (e.g. for placeholders `Hello {0}, today is {1}`) _expressionIndex = makeCounter() - constructor({ types }: {types: typeof babelTypes}, { i18nImportName }: { i18nImportName: string }) { + constructor( + { types }: { types: typeof babelTypes }, + { i18nImportName }: { i18nImportName: string } + ) { this.types = types this.i18nImportName = i18nImportName } replacePathWithMessage = ( path: NodePath, - {message, values}: {message: ParsedResult['message'], values: ParsedResult['values']}, + { + message, + values, + }: { message: ParsedResult["message"]; values: ParsedResult["values"] }, linguiInstance?: babelTypes.Identifier ) => { const args = [] - args.push(isString(message) - ? this.types.stringLiteral(message) - : message) - + args.push(isString(message) ? this.types.stringLiteral(message) : message) if (Object.keys(values).length) { const valuesObject = Object.keys(values).map((key) => @@ -86,10 +103,7 @@ export default class MacroJs { const tokens = this.tokenizeNode(path.parentPath.node) const messageFormat = new ICUMessageFormat() - const { - message: messageRaw, - values, - } = messageFormat.fromTokens(tokens) + const { message: messageRaw, values } = messageFormat.fromTokens(tokens) const message = normalizeWhitespace(messageRaw) this.replacePathWithMessage( @@ -108,7 +122,10 @@ export default class MacroJs { this.isIdentifier(path.node.callee, "t") ) { const i18nInstance = path.node.arguments[0] - this.replaceTAsFunction(path.parentPath as NodePath, i18nInstance) + this.replaceTAsFunction( + path.parentPath as NodePath, + i18nInstance + ) return false } @@ -123,10 +140,7 @@ export default class MacroJs { const tokens = this.tokenizeNode(path.node) const messageFormat = new ICUMessageFormat() - const { - message: messageRaw, - values, - } = messageFormat.fromTokens(tokens) + const { message: messageRaw, values } = messageFormat.fromTokens(tokens) const message = normalizeWhitespace(messageRaw) this.replacePathWithMessage(path, { message, values }) @@ -159,7 +173,8 @@ export default class MacroJs { // reset the expression counter this._expressionIndex = makeCounter() - const descriptor = this.processDescriptor(path.node.arguments[0]) + let descriptor = this.processDescriptor(path.node.arguments[0]) + path.replaceWith(descriptor) } @@ -167,8 +182,12 @@ export default class MacroJs { * macro `t` is called with MessageDescriptor, after that * we create a new node to append it to i18n._ */ - replaceTAsFunction = (path: NodePath, linguiInstance?: babelTypes.Identifier) => { - const descriptor = this.processDescriptor(path.node.arguments[0]) + replaceTAsFunction = ( + path: NodePath, + linguiInstance?: babelTypes.Identifier + ) => { + let descriptor = this.processDescriptor(path.node.arguments[0]) + const newNode = this.types.callExpression( this.types.memberExpression( linguiInstance ?? this.types.identifier(this.i18nImportName), @@ -197,18 +216,19 @@ export default class MacroJs { * */ processDescriptor = (descriptor_: Node) => { - const descriptor = descriptor_ as ObjectExpression; + const descriptor = descriptor_ as ObjectExpression this.types.addComment(descriptor, "leading", EXTRACT_MARK) const messageIndex = descriptor.properties.findIndex( - (property) => isObjectProperty(property) && this.isIdentifier(property.key, MESSAGE) + (property) => + isObjectProperty(property) && this.isIdentifier(property.key, MESSAGE) ) if (messageIndex === -1) { return descriptor } // if there's `message` property, replace macros with formatted message - const node = (descriptor.properties[messageIndex]) as ObjectProperty; + const node = descriptor.properties[messageIndex] as ObjectProperty // Inside message descriptor the `t` macro in `message` prop is optional. // Template strings are always processed as if they were wrapped by `t`. @@ -229,7 +249,8 @@ export default class MacroJs { // Don't override custom ID const hasId = descriptor.properties.findIndex( - (property) => isObjectProperty(property) && this.isIdentifier(property.key, ID) + (property) => + isObjectProperty(property) && this.isIdentifier(property.key, ID) ) !== -1 descriptor.properties[messageIndex] = this.types.objectProperty( @@ -237,10 +258,23 @@ export default class MacroJs { messageNode ) + if (process.env.NODE_ENV === "production") { + descriptor.properties = descriptor.properties.filter( + (property) => + isObjectProperty(property) && + !this.isIdentifier(property.key, MESSAGE) && + isObjectProperty(property) && + !this.isIdentifier(property.key, COMMENT) + ) + } + return descriptor } - addValues = (obj: ObjectExpression['properties'], values: ParsedResult["values"]) => { + addValues = ( + obj: ObjectExpression["properties"], + values: ParsedResult["values"] + ) => { const valuesObject = Object.keys(values).map((key) => this.types.objectProperty(this.types.identifier(key), values[key]) ) @@ -278,23 +312,25 @@ export default class MacroJs { tokenizeTemplateLiteral = (node: babelTypes.Expression): Tokens => { const tokenize = R.pipe( R.evolve({ - quasis: R.map((text: babelTypes.TemplateElement): TextToken => { - // Don't output tokens without text. - // if it's an unicode we keep the cooked value because it's the parsed value by babel (without unicode chars) - // This regex will detect if a string contains unicode chars, when they're we should interpolate them - // why? because platforms like react native doesn't parse them, just doing a JSON.parse makes them UTF-8 friendly - const value = /\\u[a-fA-F0-9]{4}|\\x[a-fA-F0-9]{2}/g.test( - text.value.raw - ) - ? text.value.cooked - : text.value.raw - if (value === "") return null - - return { - type: "text", - value: this.clearBackslashes(value), + quasis: R.map( + (text: babelTypes.TemplateElement): TextToken => { + // Don't output tokens without text. + // if it's an unicode we keep the cooked value because it's the parsed value by babel (without unicode chars) + // This regex will detect if a string contains unicode chars, when they're we should interpolate them + // why? because platforms like react native doesn't parse them, just doing a JSON.parse makes them UTF-8 friendly + const value = /\\u[a-fA-F0-9]{4}|\\x[a-fA-F0-9]{2}/g.test( + text.value.raw + ) + ? text.value.cooked + : text.value.raw + if (value === "") return null + + return { + type: "text", + value: this.clearBackslashes(value), + } } - }), + ), expressions: R.map((exp: babelTypes.Expression) => this.types.isCallExpression(exp) ? this.tokenizeNode(exp) @@ -338,7 +374,7 @@ export default class MacroJs { if (format !== "select" && name === "offset") { token.options.offset = (attrValue as StringLiteral).value } else { - let value: ArgToken['options'][string] + let value: ArgToken["options"][string] if (this.types.isTemplateLiteral(attrValue)) { value = this.tokenizeTemplateLiteral(attrValue) @@ -359,7 +395,7 @@ export default class MacroJs { return { type: "arg", name: (node.arguments[0] as StringLiteral).value, - value: undefined + value: undefined, } } return { @@ -410,8 +446,9 @@ export default class MacroJs { isI18nMethod = (node: Node) => { return ( this.types.isTaggedTemplateExpression(node) && - (this.isIdentifier(node.tag, "t") - || (this.types.isCallExpression(node.tag) && this.isIdentifier(node.tag.callee, "t"))) + (this.isIdentifier(node.tag, "t") || + (this.types.isCallExpression(node.tag) && + this.isIdentifier(node.tag.callee, "t"))) ) } diff --git a/packages/macro/test/js-defineMessage.ts b/packages/macro/test/js-defineMessage.ts index ecbf19108..8d8d23cf6 100644 --- a/packages/macro/test/js-defineMessage.ts +++ b/packages/macro/test/js-defineMessage.ts @@ -1,4 +1,4 @@ -import {TestCase} from "./index" +import { TestCase } from "./index" const cases: TestCase[] = [ { @@ -76,6 +76,55 @@ const cases: TestCase[] = [ }; `, }, + { + name: "Production - only essential props are kept, without id", + production: true, + input: ` + import { defineMessage } from '@lingui/macro'; + const msg = defineMessage({ + message: \`Hello $\{name\}\`, + comment: 'description for translators', + context: 'My Context', + }) + `, + expected: ` + import { i18n } from "@lingui/core"; + const msg = + /*i18n*/ + { + id: 'Hello {name}', + context: 'My Context', + values: { + name: name, + }, + }; + `, + }, + { + name: "Production - only essential props are kept", + production: true, + input: ` + import { defineMessage } from '@lingui/macro'; + const msg = defineMessage({ + message: \`Hello $\{name\}\`, + id: 'msgId', + comment: 'description for translators', + context: 'My Context', + }) + `, + expected: ` + import { i18n } from "@lingui/core"; + const msg = + /*i18n*/ + { + id: 'msgId', + context: 'My Context', + values: { + name: name, + }, + }; + `, + }, { name: "should preserve values", input: ` @@ -98,4 +147,4 @@ const cases: TestCase[] = [ }, ] -export default cases; +export default cases diff --git a/packages/macro/test/js-t.ts b/packages/macro/test/js-t.ts index 9e0745af5..acc146aa3 100644 --- a/packages/macro/test/js-t.ts +++ b/packages/macro/test/js-t.ts @@ -1,4 +1,4 @@ -import {TestCase} from "./index" +import { TestCase } from "./index" const cases: TestCase[] = [ { @@ -211,6 +211,84 @@ const cases: TestCase[] = [ }); `, }, + { + name: + "Production - only essential props are kept, with plural, with custom i18n instance", + production: true, + input: ` + import { t } from '@lingui/macro'; + const msg = t({ + id: 'msgId', + comment: 'description for translators', + context: 'some context', + message: plural(val, { one: '...', other: '...' }) + }) + `, + expected: ` + import { i18n } from "@lingui/core"; + const msg = + i18n._(/*i18n*/ + { + id: "msgId", + context: "some context", + values: { + val: val, + }, + }); + `, + }, + { + name: + "Production - only essential props are kept, with custom i18n instance", + production: true, + input: ` + import { t } from '@lingui/macro'; + import { i18n } from './lingui'; + const msg = t(i18n)({ + message: \`Hello $\{name\}\`, + id: 'msgId', + comment: 'description for translators', + context: 'My Context', + }) + `, + expected: ` + import { i18n } from "./lingui"; + const msg = + i18n._(/*i18n*/ + { + id: 'msgId', + context: 'My Context', + values: { + name: name, + }, + }); + `, + }, + { + name: "Production - only essential props are kept", + production: true, + input: ` + import { t } from '@lingui/macro'; + const msg = t({ + message: \`Hello $\{name\}\`, + id: 'msgId', + comment: 'description for translators', + context: 'My Context', + }) + `, + expected: ` + import { i18n } from "@lingui/core"; + const msg = + i18n._(/*i18n*/ + { + id: 'msgId', + context: 'My Context', + values: { + name: name, + }, + }); + `, + }, { name: "Newlines after continuation character are removed", filename: "js-t-continuation-character.js", @@ -219,4 +297,5 @@ const cases: TestCase[] = [ filename: "js-t-var/js-t-var.js", }, ] -export default cases; + +export default cases diff --git a/website/docs/ref/macro.md b/website/docs/ref/macro.md index 69c83fd19..36008e3c6 100644 --- a/website/docs/ref/macro.md +++ b/website/docs/ref/macro.md @@ -568,20 +568,24 @@ const message = /*i18n*/{ ``` :::caution Note -In production build, the whole macro is replaced with an `id`: +In production build, the macro is stripped of `message` and `comment` properties: ``` jsx import { defineMessage } from "@lingui/macro" const message = defineMessage({ - id: "Navigation / About", + id: "msg.navigation.about", comment: "Link in navigation pointing to About page", - message: "About us" + message: "About us", + context: "Context about the link" }) // process.env.NODE_ENV === "production" // ↓ ↓ ↓ ↓ ↓ ↓ -const message = "Navigation / About" +const message = /*i18n*/{ + context: "Context about the link", + id: "msg.navigation.about" +} ``` `message` and `comment` are used in message catalogs only.