From 0459c5f29abec250cd41d0b8ae3d868bd094114d Mon Sep 17 00:00:00 2001 From: Timofei Iatsenko Date: Wed, 15 Feb 2023 16:00:32 +0100 Subject: [PATCH 01/14] feature(macro): hash based id for js macro --- .../test/__snapshots__/index.ts.snap | 24 +- packages/macro/package.json | 3 +- packages/macro/src/generateMessageId.test.ts | 24 ++ packages/macro/src/generateMessageId.ts | 11 + packages/macro/src/macroJs.ts | 297 ++++++++++-------- packages/macro/src/utils.ts | 13 - .../js-t-continuation-character.expected.js | 10 +- .../fixtures/js-t-var/js-t-var.expected.js | 34 +- packages/macro/test/index.ts | 4 +- packages/macro/test/js-arg.ts | 8 +- packages/macro/test/js-defineMessage.ts | 20 +- packages/macro/test/js-plural.ts | 27 +- packages/macro/test/js-select.ts | 32 +- packages/macro/test/js-selectOrdinal.ts | 15 +- packages/macro/test/js-t.ts | 230 +++++++++----- packages/macro/test/jsx-trans.ts | 48 ++- 16 files changed, 513 insertions(+), 287 deletions(-) create mode 100644 packages/macro/src/generateMessageId.test.ts create mode 100644 packages/macro/src/generateMessageId.ts 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 75d690769..31e9e26a1 100644 --- a/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap +++ b/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap @@ -140,8 +140,8 @@ Array [ Object { comment: undefined, context: undefined, - id: Message, - message: undefined, + id: xDAtGP, + message: Message, origin: Array [ js-with-macros.js, 3, @@ -150,8 +150,8 @@ Array [ Object { comment: undefined, context: undefined, - id: Message, - message: undefined, + id: xDAtGP, + message: Message, origin: Array [ js-with-macros.js, 5, @@ -160,8 +160,8 @@ Array [ Object { comment: description, context: undefined, - id: Description, - message: undefined, + id: Nu4oKW, + message: Description, origin: Array [ js-with-macros.js, 7, @@ -180,8 +180,8 @@ Array [ Object { comment: undefined, context: undefined, - id: Values {param}, - message: undefined, + id: QCVtWw, + message: Values {param}, origin: Array [ js-with-macros.js, 17, @@ -285,8 +285,8 @@ Array [ Object { comment: undefined, context: undefined, - id: Title, - message: undefined, + id: MHrjPM, + message: Title, origin: Array [ jsx-with-macros.js, 7, @@ -295,8 +295,8 @@ Array [ Object { comment: undefined, context: undefined, - id: {count, plural, one {# book} other {# books}}, - message: undefined, + id: esnaQO, + message: {count, plural, one {# book} other {# books}}, origin: Array [ jsx-with-macros.js, 9, diff --git a/packages/macro/package.json b/packages/macro/package.json index a94e70878..2e066f6b7 100644 --- a/packages/macro/package.json +++ b/packages/macro/package.json @@ -34,8 +34,7 @@ "dependencies": { "@babel/runtime": "^7.20.13", "@babel/types": "^7.20.7", - "@lingui/conf": "3.17.1", - "ramda": "^0.27.1" + "@lingui/conf": "3.17.1" }, "peerDependencies": { "@lingui/core": "^3.13.0", diff --git a/packages/macro/src/generateMessageId.test.ts b/packages/macro/src/generateMessageId.test.ts new file mode 100644 index 000000000..232837cc7 --- /dev/null +++ b/packages/macro/src/generateMessageId.test.ts @@ -0,0 +1,24 @@ +import { generateMessageId } from "./generateMessageId" + +describe("generateMessageId", () => { + it("Should generate an id for a message", () => { + expect(generateMessageId("my message")).toMatchInlineSnapshot(`vQhkQx`) + }) + + it("Should generate different id when context is provided", () => { + const withContext = generateMessageId("my message", "custom context") + expect(withContext).toMatchInlineSnapshot(`gGUeZH`) + + expect(withContext != generateMessageId("my message")).toBeTruthy() + }) + + it("Message + context should not clash with message with suffix or prefix", () => { + const context = "custom context" + const withContext = generateMessageId("my message", context) + const withSuffix = generateMessageId("my message" + context) + const withPrefix = generateMessageId(context + "my message") + + expect(withContext != withSuffix).toBeTruthy() + expect(withContext != withPrefix).toBeTruthy() + }) +}) diff --git a/packages/macro/src/generateMessageId.ts b/packages/macro/src/generateMessageId.ts new file mode 100644 index 000000000..fc9e92926 --- /dev/null +++ b/packages/macro/src/generateMessageId.ts @@ -0,0 +1,11 @@ +import crypto from "crypto" + +const UNIT_SEPARATOR = "\u001F" + +export function generateMessageId(msg: string, context = "") { + return crypto + .createHash("sha256") + .update(msg + UNIT_SEPARATOR + context) + .digest("base64") + .slice(0, 6) +} diff --git a/packages/macro/src/macroJs.ts b/packages/macro/src/macroJs.ts index 54f964ae1..362be15a0 100644 --- a/packages/macro/src/macroJs.ts +++ b/packages/macro/src/macroJs.ts @@ -1,14 +1,15 @@ -import * as R from "ramda" import * as babelTypes from "@babel/types" import { + CallExpression, Expression, + Identifier, + isObjectProperty, Node, - CallExpression, ObjectExpression, - isObjectProperty, ObjectProperty, - Identifier, + SourceLocation, StringLiteral, + TemplateLiteral, } from "@babel/types" import { NodePath } from "@babel/traverse" @@ -16,10 +17,11 @@ import ICUMessageFormat, { ArgToken, ParsedResult, TextToken, - Tokens, + Token, } from "./icu" -import { zip, makeCounter } from "./utils" -import { COMMENT, ID, MESSAGE, EXTRACT_MARK } from "./constants" +import { makeCounter } from "./utils" +import { COMMENT, CONTEXT, EXTRACT_MARK, ID, MESSAGE } from "./constants" +import { generateMessageId } from "./generateMessageId" const keepSpaceRe = /(?:\\(?:\r\n|\r|\n))+\s+/g const keepNewLineRe = /(?:\r\n|\r|\n)+\s+/g @@ -58,30 +60,21 @@ export default class MacroJs { }: { message: ParsedResult["message"]; values: ParsedResult["values"] }, linguiInstance?: babelTypes.Identifier ) => { - const args = [] - - args.push(isString(message) ? this.types.stringLiteral(message) : message) - - if (Object.keys(values).length) { - const valuesObject = Object.keys(values).map((key) => - this.types.objectProperty(this.types.identifier(key), values[key]) - ) - - args.push(this.types.objectExpression(valuesObject)) - } - - const newNode = this.types.callExpression( - this.types.memberExpression( - linguiInstance ?? this.types.identifier(this.i18nImportName), - this.types.identifier("_") + 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 ), - args + linguiInstance ) - // preserve line number - newNode.loc = path.node.loc - - path.addComment("leading", EXTRACT_MARK) path.replaceWith(newNode) } @@ -95,7 +88,7 @@ export default class MacroJs { return true } - // t(i18nInstance)`Message` -> i18nInstance._('Message') + // t(i18nInstance)`Message` -> i18nInstance._(messageDescriptor) if ( this.types.isCallExpression(path.node) && this.types.isTaggedTemplateExpression(path.parentPath.node) && @@ -190,16 +183,8 @@ export default class MacroJs { 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), - this.types.identifier("_") - ), - [descriptor] - ) - path.replaceWith(newNode) + const descriptor = this.processDescriptor(path.node.arguments[0]) + path.replaceWith(this.createI18nCall(descriptor, linguiInstance)) } /** @@ -222,78 +207,80 @@ export default class MacroJs { processDescriptor = (descriptor_: Node) => { 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) - ) - if (messageIndex === -1) { - return descriptor + const messageProperty = this.getObjectPropertyByKey(descriptor, MESSAGE) + const idProperty = this.getObjectPropertyByKey(descriptor, ID) + const contextProperty = this.getObjectPropertyByKey(descriptor, CONTEXT) + + const properties: ObjectProperty[] = [idProperty] + + if (!this.stripNonEssentialProps) { + properties.push(contextProperty) } // if there's `message` property, replace macros with formatted message - const node = descriptor.properties[messageIndex] as ObjectProperty + if (messageProperty) { + // Inside message descriptor the `t` macro in `message` prop is optional. + // Template strings are always processed as if they were wrapped by `t`. + const tokens = this.types.isTemplateLiteral(messageProperty.value) + ? this.tokenizeTemplateLiteral(messageProperty.value) + : this.tokenizeNode(messageProperty.value, true) + + let messageNode = messageProperty.value as StringLiteral + + if (tokens) { + const messageFormat = new ICUMessageFormat() + const { message: messageRaw, values } = messageFormat.fromTokens(tokens) + const message = normalizeWhitespace(messageRaw) + messageNode = this.types.stringLiteral(message) + + properties.push(this.createValuesProperty(values)) + } - // Inside message descriptor the `t` macro in `message` prop is optional. - // Template strings are always processed as if they were wrapped by `t`. - const tokens = this.types.isTemplateLiteral(node.value) - ? this.tokenizeTemplateLiteral(node.value) - : this.tokenizeNode(node.value, true) + if (!this.stripNonEssentialProps) { + properties.push( + this.createObjectProperty(MESSAGE, messageNode as Expression) + ) + } - let messageNode = node.value - if (tokens != null) { - const messageFormat = new ICUMessageFormat() - const { message: messageRaw, values } = messageFormat.fromTokens(tokens) - const message = normalizeWhitespace(messageRaw) - messageNode = this.types.stringLiteral(message) + if (!idProperty && this.types.isStringLiteral(messageNode)) { + // todo: context could be different types, get implementation from extractor + const context = + contextProperty && this.types.isStringLiteral(contextProperty.value) + ? contextProperty.value.value + : undefined - this.addValues(descriptor.properties, values) + properties.push(this.createIdProperty(messageNode.value, context)) + } } - // Don't override custom ID - const hasId = - descriptor.properties.findIndex( - (property) => - isObjectProperty(property) && this.isIdentifier(property.key, ID) - ) !== -1 - - descriptor.properties[messageIndex] = this.types.objectProperty( - this.types.identifier(hasId ? MESSAGE : ID), - messageNode - ) - - if (this.stripNonEssentialProps) { - descriptor.properties = descriptor.properties.filter( - (property) => - isObjectProperty(property) && - !this.isIdentifier(property.key, MESSAGE) && - isObjectProperty(property) && - !this.isIdentifier(property.key, COMMENT) - ) + if (!this.stripNonEssentialProps) { + properties.push(this.getObjectPropertyByKey(descriptor, COMMENT)) } - return descriptor + return this.createMessageDescriptor(properties, descriptor.loc) } - addValues = ( - obj: ObjectExpression["properties"], - values: ParsedResult["values"] - ) => { + createIdProperty(message: string, context?: string) { + return this.createObjectProperty( + ID, + this.types.stringLiteral(generateMessageId(message, context)) + ) + } + + createValuesProperty(values: ParsedResult["values"]) { const valuesObject = Object.keys(values).map((key) => this.types.objectProperty(this.types.identifier(key), values[key]) ) if (!valuesObject.length) return - obj.push( - this.types.objectProperty( - this.types.identifier("values"), - this.types.objectExpression(valuesObject) - ) + return this.types.objectProperty( + this.types.identifier("values"), + this.types.objectExpression(valuesObject) ) } - tokenizeNode = (node: Node, ignoreExpression = false) => { + tokenizeNode(node: Node, ignoreExpression = false): Token[] { if (this.isI18nMethod(node)) { // t return this.tokenizeTemplateLiteral(node as Expression) @@ -304,7 +291,7 @@ export default class MacroJs { // // date, number // return transformFormatMethod(node, file, props, root) } else if (!ignoreExpression) { - return this.tokenizeExpression(node) + return [this.tokenizeExpression(node)] } } @@ -313,43 +300,45 @@ export default class MacroJs { * text chunks and node.expressions contains expressions. * Both arrays must be zipped together to get the final list of tokens. */ - 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), - } - }), - expressions: R.map((exp: babelTypes.Expression) => - this.types.isCallExpression(exp) - ? this.tokenizeNode(exp) - : this.tokenizeExpression(exp) - ), - }), - (exp) => zip(exp.quasis, exp.expressions), - R.flatten, - R.filter(Boolean) - ) + tokenizeTemplateLiteral(node: babelTypes.Expression): Token[] { + const tpl = this.types.isTaggedTemplateExpression(node) + ? node.quasi + : (node as TemplateLiteral) + + const expressions = tpl.expressions as Expression[] + + return tpl.quasis.flatMap((text, i) => { + // 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 + + let argTokens: Token[] = [] + const currExp = expressions[i] + + if (currExp) { + argTokens = this.types.isCallExpression(currExp) + ? this.tokenizeNode(currExp) + : [this.tokenizeExpression(currExp)] + } - return tokenize( - this.types.isTaggedTemplateExpression(node) ? node.quasi : node - ) + return [ + ...(value + ? [ + { + type: "text", + value: this.clearBackslashes(value), + } as TextToken, + ] + : []), + ...argTokens, + ] + }) } - tokenizeChoiceComponent = (node: CallExpression): ArgToken => { + tokenizeChoiceComponent(node: CallExpression): ArgToken { const format = (node.callee as Identifier).name.toLowerCase() const token: ArgToken = { @@ -392,7 +381,7 @@ export default class MacroJs { return token } - tokenizeExpression = (node: Node | Expression): ArgToken => { + tokenizeExpression(node: Node | Expression): ArgToken { if (this.isArg(node) && this.types.isCallExpression(node)) { return { type: "arg", @@ -407,7 +396,7 @@ export default class MacroJs { } } - expressionToArgument = (exp: Expression): string => { + expressionToArgument(exp: Expression): string { if (this.types.isIdentifier(exp)) { return exp.name } else if (this.types.isStringLiteral(exp)) { @@ -425,27 +414,69 @@ export default class MacroJs { return value.replace(/\\`/g, "`") } + createI18nCall( + messageDescriptor: ObjectExpression, + linguiInstance?: Identifier + ) { + return this.types.callExpression( + this.types.memberExpression( + linguiInstance ?? this.types.identifier(this.i18nImportName), + this.types.identifier("_") + ), + [messageDescriptor] + ) + } + + createMessageDescriptor( + properties: ObjectProperty[], + oldLoc?: SourceLocation + ): ObjectExpression { + const newDescriptor = this.types.objectExpression( + properties.filter(Boolean) + ) + this.types.addComment(newDescriptor, "leading", EXTRACT_MARK) + if (oldLoc) { + newDescriptor.loc = oldLoc + } + + return newDescriptor + } + + createObjectProperty(key: string, value: Expression) { + return this.types.objectProperty(this.types.identifier(key), value) + } + + getObjectPropertyByKey( + objectExp: ObjectExpression, + key: string + ): ObjectProperty { + return objectExp.properties.find( + (property) => + isObjectProperty(property) && this.isIdentifier(property.key, key) + ) as ObjectProperty + } + /** * Custom matchers */ - isIdentifier = (node: Node | Expression, name: string) => { + isIdentifier(node: Node | Expression, name: string) { return this.types.isIdentifier(node, { name }) } - isDefineMessage = (node: Node): boolean => { + isDefineMessage(node: Node): boolean { return ( this.types.isCallExpression(node) && this.isIdentifier(node.callee, "defineMessage") ) } - isArg = (node: Node) => { + isArg(node: Node) { return ( this.types.isCallExpression(node) && this.isIdentifier(node.callee, "arg") ) } - isI18nMethod = (node: Node) => { + isI18nMethod(node: Node) { return ( this.types.isTaggedTemplateExpression(node) && (this.isIdentifier(node.tag, "t") || @@ -454,7 +485,7 @@ export default class MacroJs { ) } - isChoiceMethod = (node: Node) => { + isChoiceMethod(node: Node) { return ( this.types.isCallExpression(node) && (this.isIdentifier(node.callee, "plural") || @@ -463,5 +494,3 @@ export default class MacroJs { ) } } - -const isString = (s: unknown): s is string => typeof s === "string" diff --git a/packages/macro/src/utils.ts b/packages/macro/src/utils.ts index b9cd7899a..17261d099 100644 --- a/packages/macro/src/utils.ts +++ b/packages/macro/src/utils.ts @@ -1,16 +1,3 @@ -import * as R from "ramda" - -/** - * Custom zip method which takes length of the larger array - * (usually zip functions use the `smaller` length, discarding values in larger array) - */ -export function zip(a: A[], b: B[]): [A, B][] { - return R.range(0, Math.max(a.length, b.length)).map((index) => [ - a[index], - b[index], - ]) -} - export const makeCounter = (index = 0) => () => diff --git a/packages/macro/test/fixtures/js-t-continuation-character.expected.js b/packages/macro/test/fixtures/js-t-continuation-character.expected.js index 416d6fde4..bb22822cd 100644 --- a/packages/macro/test/fixtures/js-t-continuation-character.expected.js +++ b/packages/macro/test/fixtures/js-t-continuation-character.expected.js @@ -1,2 +1,8 @@ -import { i18n } from "@lingui/core"; -/*i18n*/i18n._("Multiline with continuation"); +import { i18n } from "@lingui/core" +i18n._( + /*i18n*/ + { + id: "LBYoFK", + message: "Multiline with continuation", + } +) diff --git a/packages/macro/test/fixtures/js-t-var/js-t-var.expected.js b/packages/macro/test/fixtures/js-t-var/js-t-var.expected.js index caa293f5d..17a544413 100644 --- a/packages/macro/test/fixtures/js-t-var/js-t-var.expected.js +++ b/packages/macro/test/fixtures/js-t-var/js-t-var.expected.js @@ -1,16 +1,30 @@ -"use strict"; +"use strict" -var _core = require("@lingui/core"); +var _core = require("@lingui/core") function scoped(foo) { if (foo) { - var bar = 50; - /*i18n*/_core.i18n._("This is bar {bar}", { - bar: bar - }); + var bar = 50 + _core.i18n._( + /*i18n*/ + { + id: "EvVtyn", + message: "This is bar {bar}", + values: { + bar: bar, + }, + } + ) } else { - var _bar = 10; - /*i18n*/_core.i18n._("This is a different bar {bar}", { - bar: _bar - }); + var _bar = 10 + _core.i18n._( + /*i18n*/ + { + id: "e6QGtZ", + message: "This is a different bar {bar}", + values: { + bar: _bar, + }, + } + ) } } diff --git a/packages/macro/test/index.ts b/packages/macro/test/index.ts index a3d37bdbc..3c661f4ff 100644 --- a/packages/macro/test/index.ts +++ b/packages/macro/test/index.ts @@ -61,7 +61,7 @@ describe("macro", function () { try { return transformSync(code, getDefaultBabelOptions()).code.trim() } catch (e) { - e.message = e.message.replace(/([^:]*:){2}/, "") + ;(e as Error).message = (e as Error).message.replace(/([^:]*:){2}/, "") throw e } } @@ -125,7 +125,7 @@ describe("macro", function () { const actual = transformFileSync(inputPath, _babelOptions) .code.replace(/\r/g, "") .trim() - expect(actual).toEqual(expected) + expect(clean(actual)).toEqual(clean(expected)) } else { const actual = transformSync(input, babelOptions).code.trim() diff --git a/packages/macro/test/js-arg.ts b/packages/macro/test/js-arg.ts index 3aeaeaf9a..7cd66be0f 100644 --- a/packages/macro/test/js-arg.ts +++ b/packages/macro/test/js-arg.ts @@ -9,7 +9,13 @@ const cases: TestCase[] = [ `, expected: ` import { i18n } from "@lingui/core"; - const a = /*i18n*/ i18n._("Hello {name}") + const a = i18n._( + /*i18n*/ + { + id: "OVaF9k", + message: "Hello {name}", + } + ); `, }, ] diff --git a/packages/macro/test/js-defineMessage.ts b/packages/macro/test/js-defineMessage.ts index 8d8d23cf6..a913a8283 100644 --- a/packages/macro/test/js-defineMessage.ts +++ b/packages/macro/test/js-defineMessage.ts @@ -15,8 +15,9 @@ const cases: TestCase[] = [ const message = /*i18n*/ { + message: "{value, plural, one {book} other {books}}", + id: "SlmyxX", comment: "Description", - id: "{value, plural, one {book} other {books}}" }; `, }, @@ -33,7 +34,8 @@ const cases: TestCase[] = [ const message = /*i18n*/ { - id: "Message" + message: "Message", + id: "xDAtGP", }; `, }, @@ -50,10 +52,11 @@ const cases: TestCase[] = [ const message = /*i18n*/ { - id: "Message {name}", values: { name: name - } + }, + message: "Message {name}", + id: "A2aVLF", }; `, }, @@ -92,11 +95,10 @@ const cases: TestCase[] = [ const msg = /*i18n*/ { - id: 'Hello {name}', - context: 'My Context', values: { name: name, }, + id: "oT92lS", }; `, }, @@ -118,7 +120,6 @@ const cases: TestCase[] = [ /*i18n*/ { id: 'msgId', - context: 'My Context', values: { name: name, }, @@ -138,10 +139,11 @@ const cases: TestCase[] = [ const message = /*i18n*/ { - id: "Hello {name}", values: { name: name - } + }, + message: "Hello {name}", + id: "OVaF9k", }; `, }, diff --git a/packages/macro/test/js-plural.ts b/packages/macro/test/js-plural.ts index 2c3a3c7dd..034b736f3 100644 --- a/packages/macro/test/js-plural.ts +++ b/packages/macro/test/js-plural.ts @@ -12,9 +12,17 @@ const cases: TestCase[] = [ `, expected: ` import { i18n } from "@lingui/core"; - const a = /*i18n*/ i18n._("{count, plural, one {# book} other {# books}}", { - count: count - }); + const a = i18n._( + /*i18n*/ + { + id: "esnaQO", + message: "{count, plural, one {# book} other {# books}}", + values: { + count: count, + }, + } + ); + `, }, { @@ -30,9 +38,16 @@ const cases: TestCase[] = [ `, expected: ` import { i18n } from "@lingui/core"; - /*i18n*/ i18n._("{0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}", { - 0: users.length - }); + i18n._( + /*i18n*/ + { + id: "CF5t+7", + message: "{0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}", + values: { + 0: users.length, + }, + } + ); `, }, ] diff --git a/packages/macro/test/js-select.ts b/packages/macro/test/js-select.ts index 5b15f1c25..59ed8c246 100644 --- a/packages/macro/test/js-select.ts +++ b/packages/macro/test/js-select.ts @@ -16,13 +16,24 @@ const cases: TestCase[] = [ `, expected: ` import { i18n } from "@lingui/core"; - /*i18n*/ i18n._("{gender, select, male {{numOfGuests, plural, one {He invites one guest} other {He invites # guests}}} female {She is {gender}} other {They is {gender}}}", { - gender: gender, - numOfGuests: numOfGuests - }); + i18n._( + /*i18n*/ + { + id: "G8xqGf", + message: "{gender, select, male {{numOfGuests, plural, one {He invites one guest} other {He invites # guests}}} female {She is {gender}} other {They is {gender}}}", + values: { + gender: gender, + numOfGuests: numOfGuests, + }, + } + ); `, }, { + // todo: the original test case is weird. + // The `comment field` was never escaped here + // and never allowed as property + name: "Macro with escaped reserved props", input: ` import { select } from '@lingui/macro' @@ -33,9 +44,16 @@ const cases: TestCase[] = [ `, expected: ` import { i18n } from "@lingui/core"; - /*i18n*/ i18n._("{value, select, id {test escaped id} comment {test escaped comment}}", { - value: value - }); + i18n._( + /*i18n*/ + { + id: "WNOR8i", + message: "{value, select, id {test escaped id} comment {test escaped comment}}", + values: { + value: value, + }, + } + ); `, }, ] diff --git a/packages/macro/test/js-selectOrdinal.ts b/packages/macro/test/js-selectOrdinal.ts index 6664f26eb..12e5469fa 100644 --- a/packages/macro/test/js-selectOrdinal.ts +++ b/packages/macro/test/js-selectOrdinal.ts @@ -11,10 +11,17 @@ const cases: TestCase[] = [ })} cat\` `, expected: ` - import { i18n } from "@lingui/core"; - /*i18n*/ i18n._("This is my {count, selectordinal, one {#st} two {#nd} other {#rd}} cat", { - count: count - }); + import { i18n } from "@lingui/core"; + i18n._( + /*i18n*/ + { + id: "dJXd3T", + message: "This is my {count, selectordinal, one {#st} two {#nd} other {#rd}} cat", + values: { + count: count, + }, + } + ); `, }, ] diff --git a/packages/macro/test/js-t.ts b/packages/macro/test/js-t.ts index f31b4189c..5488e22de 100644 --- a/packages/macro/test/js-t.ts +++ b/packages/macro/test/js-t.ts @@ -9,19 +9,31 @@ const cases: TestCase[] = [ `, expected: ` import { i18n } from "@lingui/core"; - const a = /*i18n*/ i18n._("Expression assignment") + const a = i18n._( + /*i18n*/ + { + id: "mjnlP0", + message: "Expression assignment", + } + ); `, }, { name: "Macro is used in expression assignment, with custom lingui instance", input: ` import { t } from '@lingui/macro'; - import { i18n } from './lingui'; - const a = t(i18n)\`Expression assignment\`; + import { customI18n } from './lingui'; + const a = t(customI18n)\`Expression assignment\`; `, expected: ` - import { i18n } from './lingui'; - const a = /*i18n*/ i18n._("Expression assignment") + import { customI18n } from './lingui'; + const a = customI18n._( + /*i18n*/ + { + id: "mjnlP0", + message: "Expression assignment", + } + ); `, }, { @@ -32,33 +44,53 @@ const cases: TestCase[] = [ `, expected: ` import { i18n } from "@lingui/core"; - /*i18n*/ i18n._("Variable {name}", { - name: name - }) + i18n._( + /*i18n*/ + { + id: "xRRkAE", + message: "Variable {name}", + values: { + name: name, + }, + } + ); `, }, { - name: "Variables with scaped template literals are correctly formatted", + name: "Variables with escaped template literals are correctly formatted", input: ` import { t } from '@lingui/macro'; t\`Variable \\\`\${name}\\\`\`; `, expected: ` import { i18n } from "@lingui/core"; - /*i18n*/ i18n._("Variable \`{name}\`", { - name: name - }) + i18n._( + /*i18n*/ + { + id: "ICBco+", + message: "Variable \`{name}\`", + values: { + name: name, + }, + } + ); `, }, { - name: "Variables with scaped double quotes are correctly formatted", + name: "Variables with escaped double quotes are correctly formatted", input: ` import { t } from '@lingui/macro'; t\`Variable \"name\" \`; `, expected: ` - import { i18n } from "@lingui/core"; - /*i18n*/ i18n._("Variable \\"name\\"") + import { i18n } from "@lingui/core"; + i18n._( + /*i18n*/ + { + id: "CcPIZW", + message: 'Variable "name"', + } + ); `, }, { @@ -68,10 +100,17 @@ const cases: TestCase[] = [ t\`\${duplicate} variable \${duplicate}\`; `, expected: ` - import { i18n } from "@lingui/core"; - /*i18n*/ i18n._("{duplicate} variable {duplicate}", { - duplicate: duplicate - }) + import { i18n } from "@lingui/core"; + i18n._( + /*i18n*/ + { + id: "+nhkwg", + message: "{duplicate} variable {duplicate}", + values: { + duplicate: duplicate, + }, + } + ); `, }, { @@ -89,14 +128,19 @@ const cases: TestCase[] = [ `, expected: ` import { i18n } from "@lingui/core"; - /*i18n*/ i18n._( - "Property {0}, function {1}, array {2}, constant {3}, object {4} anything {5}", { - 0: props.name, - 1: random(), - 2: array[index], - 3: 42, - 4: new Date(), - 5: props.messages[index].value() + i18n._( + /*i18n*/ + { + id: "X1jIKa", + message: "Property {0}, function {1}, array {2}, constant {3}, object {4} anything {5}", + values: { + 0: props.name, + 1: random(), + 2: array[index], + 3: 42, + 4: new Date(), + 5: props.messages[index].value(), + }, } ); `, @@ -110,7 +154,13 @@ const cases: TestCase[] = [ `, expected: ` import { i18n } from "@lingui/core"; - /*i18n*/ i18n._("Multiline\\nstring") + i18n._( + /*i18n*/ + { + id: "EfogM+", + message: "Multiline\\nstring", + } + ); `, }, { @@ -119,15 +169,18 @@ const cases: TestCase[] = [ import { t } from '@lingui/macro' const msg = t({ message: \`Hello \${name}\` }) `, - expected: `import { i18n } from "@lingui/core"; - const msg = - i18n._(/*i18n*/ - { - id: "Hello {name}", - values: { - name: name, - }, - }); + expected: ` + import { i18n } from "@lingui/core"; + const msg = i18n._( + /*i18n*/ + { + values: { + name: name, + }, + message: "Hello {name}", + id: "OVaF9k", + } + ); `, }, { @@ -137,15 +190,44 @@ const cases: TestCase[] = [ import { i18n } from './lingui' const msg = t(i18n)({ message: \`Hello \${name}\` }) `, - expected: `import { i18n } from "./lingui"; - const msg = - i18n._(/*i18n*/ - { - id: "Hello {name}", - values: { - name: name, - }, - }); + expected: ` + import { i18n } from "./lingui"; + const msg = i18n._( + /*i18n*/ + { + values: { + name: name, + }, + message: "Hello {name}", + id: "OVaF9k", + } + ); + `, + }, + { + name: "Should generate different id when context provided", + input: ` + import { t } from '@lingui/macro' + t({ message: "Hello" }) + t({ message: "Hello", context: "my custom" }) + `, + expected: ` + import { i18n } from "@lingui/core"; + i18n._( + /*i18n*/ + { + message: "Hello", + id: "uzTaYi", + } + ); + i18n._( + /*i18n*/ + { + context: "my custom", + message: "Hello", + id: "BYqAaU", + } + ); `, }, { @@ -154,17 +236,19 @@ const cases: TestCase[] = [ import { t } from '@lingui/macro' const msg = t({ id: 'msgId', comment: 'description for translators', message: plural(val, { one: '...', other: '...' }) }) `, - expected: `import { i18n } from "@lingui/core"; - const msg = - i18n._(/*i18n*/ - { - id: "msgId", - comment: "description for translators", - message: "{val, plural, one {...} other {...}}", - values: { - val: val, - }, - }); + expected: ` + import { i18n } from "@lingui/core"; + const msg = i18n._( + /*i18n*/ + { + id: "msgId", + values: { + val: val, + }, + message: "{val, plural, one {...} other {...}}", + comment: "description for translators", + } + ); `, }, { @@ -173,16 +257,18 @@ const cases: TestCase[] = [ import { t } from '@lingui/macro' const msg = t({ id: 'msgId', message: \`Some \${value}\` }) `, - expected: `import { i18n } from "@lingui/core"; - const msg = - i18n._(/*i18n*/ - { - id: "msgId", - message: "Some {value}", - values: { - value: value, - }, - }); + expected: ` + import { i18n } from "@lingui/core"; + const msg = i18n._( + /*i18n*/ + { + id: "msgId", + values: { + value: value, + }, + message: "Some {value}", + } + ); `, }, { @@ -191,7 +277,8 @@ const cases: TestCase[] = [ import { t } from '@lingui/macro' const msg = t({ id: \`msgId\` }) `, - expected: `import { i18n } from "@lingui/core"; + expected: ` + import { i18n } from "@lingui/core"; const msg = i18n._(/*i18n*/ { @@ -217,7 +304,6 @@ const cases: TestCase[] = [ i18n._(/*i18n*/ { id: "msgId", - context: "some context", values: { val: val, }, @@ -243,7 +329,6 @@ const cases: TestCase[] = [ i18n._(/*i18n*/ { id: 'msgId', - context: 'My Context', values: { name: name, }, @@ -268,7 +353,6 @@ const cases: TestCase[] = [ i18n._(/*i18n*/ { id: 'msgId', - context: 'My Context', values: { name: name, }, @@ -295,13 +379,13 @@ const cases: TestCase[] = [ const msg = i18n._(/*i18n*/ { - message: "Hello {name}", id: 'msgId', - comment: "description for translators", context: 'My Context', values: { name: name, }, + message: "Hello {name}", + comment: "description for translators", }); `, }, diff --git a/packages/macro/test/jsx-trans.ts b/packages/macro/test/jsx-trans.ts index 358c39c71..c002b52a9 100644 --- a/packages/macro/test/jsx-trans.ts +++ b/packages/macro/test/jsx-trans.ts @@ -415,13 +415,27 @@ const cases: TestCase[] = [ expected: ` import { Trans } from "@lingui/react"; import { i18n } from "@lingui/core"; - more"} components={{ - 0: - }} />; + more"} + components={{ + 0: ( + + ), + }} + />; + `, }, { @@ -435,11 +449,21 @@ const cases: TestCase[] = [ `, expected: ` import { i18n } from "@lingui/core"; - About; + + About + ; `, }, { From e0117b103636d7d0c87979c7d65eb5add2bb881c Mon Sep 17 00:00:00 2001 From: Timofei Iatsenko Date: Wed, 15 Feb 2023 18:03:21 +0100 Subject: [PATCH 02/14] feature(macro): hash based id for jsx macro --- .../test/__snapshots__/index.ts.snap | 24 +-- packages/macro/src/macroJsx.ts | 103 +++++----- .../jsx-plural-select-nested.expected.js | 20 +- packages/macro/test/index.ts | 45 ++++- packages/macro/test/jsx-plural.ts | 58 +++--- packages/macro/test/jsx-select.ts | 19 +- packages/macro/test/jsx-selectOrdinal.ts | 27 ++- packages/macro/test/jsx-trans.ts | 176 ++++++++++++------ 8 files changed, 301 insertions(+), 171 deletions(-) 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 31e9e26a1..d83db9581 100644 --- a/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap +++ b/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap @@ -115,8 +115,8 @@ Array [ Object { comment: undefined, context: undefined, - id: {count, plural, one {# book} other {# books}}, - message: undefined, + id: CbxEcW, + message: {count, plural, one {# book} other {# books}}, origin: Array [ jsx-without-trans.js, 3, @@ -125,8 +125,8 @@ Array [ Object { comment: undefined, context: Some context, - id: {count, plural, one {# book} other {# books}}, - message: undefined, + id: 8qNz+K, + message: {count, plural, one {# book} other {# books}}, origin: Array [ jsx-without-trans.js, 4, @@ -245,8 +245,8 @@ Array [ Object { comment: undefined, context: undefined, - id: Hi, my name is {name}, - message: undefined, + id: stjtW+, + message: Hi, my name is {name}, origin: Array [ jsx-with-macros.js, 3, @@ -255,8 +255,8 @@ Array [ Object { comment: undefined, context: Context1, - id: Some message, - message: undefined, + id: YikuIL, + message: Some message, origin: Array [ jsx-with-macros.js, 4, @@ -265,8 +265,8 @@ Array [ Object { comment: undefined, context: Context1, - id: Some other message, - message: undefined, + id: LBCs5C, + message: Some other message, origin: Array [ jsx-with-macros.js, 5, @@ -275,8 +275,8 @@ Array [ Object { comment: undefined, context: Context2, - id: Some message, - message: undefined, + id: ru2rzr, + message: Some message, origin: Array [ jsx-with-macros.js, 6, diff --git a/packages/macro/src/macroJsx.ts b/packages/macro/src/macroJsx.ts index 4eb280670..2698b43c6 100644 --- a/packages/macro/src/macroJsx.ts +++ b/packages/macro/src/macroJsx.ts @@ -10,6 +10,7 @@ import { Literal, Node, StringLiteral, + TemplateLiteral, } from "@babel/types" import { NodePath } from "@babel/traverse" @@ -21,6 +22,7 @@ import ICUMessageFormat, { } from "./icu" import { makeCounter } from "./utils" import { COMMENT, CONTEXT, ID, MESSAGE } from "./constants" +import { generateMessageId } from "./generateMessageId" const pluralRuleRe = /(_[\d\w]+|zero|one|two|few|many|other)/ const jsx2icuExactChoice = (value: string) => @@ -76,7 +78,7 @@ export default class MacroJSX { this.stripNonEssentialProps = opts.stripNonEssentialProps } - safeJsxAttribute = (name: string, value: string) => { + createStringJsxAttribute = (name: string, value: string) => { // This handles quoted JSX attributes and html entities. return this.types.jsxAttribute( this.types.jsxIdentifier(name), @@ -101,21 +103,23 @@ export default class MacroJSX { if (!id && !message) { return - } else if (id && id !== message) { - // If `id` prop already exists and generated ID is different, - // add it as a `default` prop + } + + if (id) { attributes.push( this.types.jsxAttribute( this.types.jsxIdentifier(ID), this.types.stringLiteral(id) ) ) - - if (!this.stripNonEssentialProps && message) { - attributes.push(this.safeJsxAttribute(MESSAGE, message)) - } } else { - attributes.push(this.safeJsxAttribute(ID, message)) + attributes.push( + this.createStringJsxAttribute(ID, generateMessageId(message, context)) + ) + } + + if (!this.stripNonEssentialProps && message) { + attributes.push(this.createStringJsxAttribute(MESSAGE, message)) } if (!this.stripNonEssentialProps && comment) { @@ -127,7 +131,7 @@ export default class MacroJSX { ) } - if (context) { + if (!this.stripNonEssentialProps && context) { attributes.push( this.types.jsxAttribute( this.types.jsxIdentifier(CONTEXT), @@ -196,15 +200,14 @@ export default class MacroJSX { stripMacroAttributes = (path: NodePath) => { const { attributes } = path.node.openingElement - const id = attributes.filter(this.attrName([ID]))[0] - const message = attributes.filter(this.attrName([MESSAGE]))[0] - const comment = attributes.filter(this.attrName([COMMENT]))[0] - const context = attributes.filter(this.attrName([CONTEXT]))[0] + const id = attributes.find(this.attrName([ID])) + const message = attributes.find(this.attrName([MESSAGE])) + const comment = attributes.find(this.attrName([COMMENT])) + const context = attributes.find(this.attrName([CONTEXT])) let reserved = [ID, MESSAGE, COMMENT, CONTEXT] - if (this.isI18nComponent(path)) { - // no reserved prop names - } else if (this.isChoiceComponent(path)) { + + if (this.isChoiceComponent(path)) { reserved = [ ...reserved, "_\\w+", @@ -230,7 +233,7 @@ export default class MacroJSX { } tokenizeNode = (path: NodePath): Token[] => { - if (this.isI18nComponent(path)) { + if (this.isTransComponent(path)) { // t return this.tokenizeTrans(path) } else if (this.isChoiceComponent(path)) { @@ -259,32 +262,7 @@ export default class MacroJSX { return [this.tokenizeText(exp.node.value.replace(/\n/g, "\\n"))] } if (exp.isTemplateLiteral()) { - const expressions = exp.get("expressions") as NodePath[] - - return exp.get("quasis").flatMap(({ node: text }, i) => { - // 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 - - let argTokens: Token[] = [] - const currExp = expressions[i] - - if (currExp) { - argTokens = currExp.isCallExpression() - ? this.tokenizeNode(currExp) - : [this.tokenizeExpression(currExp)] - } - - return [ - ...(value ? [this.tokenizeText(this.clearBackslashes(value))] : []), - ...argTokens, - ] - }) + return this.tokenizeTemplateLiteral(exp) } if (exp.isConditionalExpression()) { return [this.tokenizeConditionalExpression(exp)] @@ -306,6 +284,33 @@ export default class MacroJSX { } } + tokenizeTemplateLiteral(exp: NodePath): Token[] { + const expressions = exp.get("expressions") as NodePath[] + + return exp.get("quasis").flatMap(({ node: text }, i) => { + // 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 + + let argTokens: Token[] = [] + const currExp = expressions[i] + + if (currExp) { + argTokens = currExp.isCallExpression() + ? this.tokenizeNode(currExp) + : [this.tokenizeExpression(currExp)] + } + + return [ + ...(value ? [this.tokenizeText(this.clearBackslashes(value))] : []), + ...argTokens, + ] + }) + } + tokenizeChoiceComponent = (path: NodePath): Token => { const element = path.get("openingElement") const format = this.getJsxTagName(path.node).toLowerCase() @@ -422,7 +427,7 @@ export default class MacroJSX { ): ArgToken => { exp.traverse({ JSXElement: (el) => { - if (this.isI18nComponent(el) || this.isChoiceComponent(el)) { + if (this.isTransComponent(el) || this.isChoiceComponent(el)) { this.replacePath(el) el.skip() } @@ -455,7 +460,7 @@ export default class MacroJSX { return value.replace(/\\`/g, "`") } - isI18nComponent = ( + isTransComponent = ( path: NodePath, name = "Trans" ): path is NodePath => { @@ -469,9 +474,9 @@ export default class MacroJSX { isChoiceComponent = (path: NodePath): path is NodePath => { return ( - this.isI18nComponent(path, "Plural") || - this.isI18nComponent(path, "Select") || - this.isI18nComponent(path, "SelectOrdinal") + this.isTransComponent(path, "Plural") || + this.isTransComponent(path, "Select") || + this.isTransComponent(path, "SelectOrdinal") ) } diff --git a/packages/macro/test/fixtures/jsx-plural-select-nested.expected.js b/packages/macro/test/fixtures/jsx-plural-select-nested.expected.js index fd19a6c2e..417121afb 100644 --- a/packages/macro/test/fixtures/jsx-plural-select-nested.expected.js +++ b/packages/macro/test/fixtures/jsx-plural-select-nested.expected.js @@ -1,7 +1,13 @@ -import { Trans } from "@lingui/react"; -; +import { Trans } from "@lingui/react" +; diff --git a/packages/macro/test/index.ts b/packages/macro/test/index.ts index 3c661f4ff..eab967d63 100644 --- a/packages/macro/test/index.ts +++ b/packages/macro/test/index.ts @@ -1,8 +1,20 @@ import fs from "fs" import path from "path" -import { transformFileSync, TransformOptions, transformSync } from "@babel/core" +import { + PluginObj, + transformFileSync, + TransformOptions, + transformSync, +} from "@babel/core" import prettier from "prettier" import { LinguiMacroOpts } from "../src/index" +import { + JSXAttribute, + jsxExpressionContainer, + JSXIdentifier, + stringLiteral, +} from "@babel/types" +import { NodePath } from "@babel/traverse" export type TestCase = { name?: string @@ -12,6 +24,8 @@ export type TestCase = { production?: boolean useTypescriptPreset?: boolean macroOpts?: LinguiMacroOpts + /** Remove hash id from snapshot for more stable testing */ + stripId?: boolean only?: boolean skip?: boolean } @@ -29,11 +43,34 @@ const testCases: Record = { "js-defineMessage": require("./js-defineMessage").default, } +function stripIdPlugin(): PluginObj { + return { + visitor: { + JSXOpeningElement: (path) => { + const idAttr = path + .get("attributes") + .find( + (attr) => + attr.isJSXAttribute() && + (attr.node.name as JSXIdentifier).name === "id" + ) as NodePath + + if (idAttr) { + idAttr + .get("value") + .replaceWith(jsxExpressionContainer(stringLiteral(""))) + } + }, + }, + } +} + describe("macro", function () { process.env.LINGUI_CONFIG = path.join(__dirname, "lingui.config.js") const getDefaultBabelOptions = ( - macroOpts: LinguiMacroOpts = {} + macroOpts: LinguiMacroOpts = {}, + stripId = false ): TransformOptions => { return { filename: "", @@ -52,6 +89,7 @@ describe("macro", function () { resolvePath: (source: string) => require.resolve(source), }, ], + ...(stripId ? [stripIdPlugin] : []), ], } } @@ -85,6 +123,7 @@ describe("macro", function () { only, skip, macroOpts, + stripId, }, index ) => { @@ -92,7 +131,7 @@ describe("macro", function () { if (only) run = it.only if (skip) run = it.skip run(name != null ? name : `${suiteName} #${index + 1}`, () => { - const babelOptions = getDefaultBabelOptions(macroOpts) + const babelOptions = getDefaultBabelOptions(macroOpts, stripId) expect(input || filename).toBeDefined() const originalEnv = process.env.NODE_ENV diff --git a/packages/macro/test/jsx-plural.ts b/packages/macro/test/jsx-plural.ts index 8238cf572..54a7ccfe8 100644 --- a/packages/macro/test/jsx-plural.ts +++ b/packages/macro/test/jsx-plural.ts @@ -14,14 +14,18 @@ const cases: TestCase[] = [ `, expected: ` import { Trans } from "@lingui/react"; - A lot of them}}" - } - values={{ - count: count - }} components={{ - 0: - }} />; + A lot of them}}" + } + values={{ + count: count + }} + components={{ + 0: + }} + />; `, }, { @@ -60,6 +64,7 @@ const cases: TestCase[] = [ `, }, { + stripId: true, input: ` import { Trans, Plural } from '@lingui/macro'; # slot added} other {<1># slots added}}" - } - values={{ - count: count - }} components={{ - 0: , - 1: - }} />; + "} + message={ + "{count, plural, one {<0># slot added} other {<1># slots added}}" + } + values={{ + count: count + }} + components={{ + 0: , + 1: + }} + />; `, }, { + stripId: true, name: "Should return cases without leading or trailing spaces for nested Trans inside Plural", input: ` import { Trans, Plural } from '@lingui/macro'; @@ -109,12 +119,14 @@ const cases: TestCase[] = [ `, expected: ` import { Trans } from "@lingui/react"; - "} + message={ + "{count, plural, one {One hello} other {Other hello}}" + } + values={{ + count: count + }} />; `, }, diff --git a/packages/macro/test/jsx-select.ts b/packages/macro/test/jsx-select.ts index 51fed07fa..bfdc29f31 100644 --- a/packages/macro/test/jsx-select.ts +++ b/packages/macro/test/jsx-select.ts @@ -2,6 +2,7 @@ import { TestCase } from "./index" const cases: TestCase[] = [ { + stripId: true, input: ` import { Select } from '@lingui/macro'; ` inside `` macro if you want to provide `id`, `context` or `comment` + +```jsx + + ` inside `` macro if you want to provide `id`, `context` or `comment` +Use `