Skip to content

Commit

Permalink
feat: support extracting from all forms of i18n._ / i18n.t calls
Browse files Browse the repository at this point in the history
  • Loading branch information
timofei-iatsenko committed Apr 6, 2023
1 parent 6d645de commit 22c6ae3
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 223 deletions.
173 changes: 85 additions & 88 deletions packages/babel-plugin-extract-messages/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,25 @@ function extractFromObjectExpression(
return props
}

const I18N_OBJECT = "i18n"

function hasComment(node: Node, comment: string): boolean {
return (
node.leadingComments &&
node.leadingComments.some((comm) => comm.value.trim() === comment)
)
}

function hasIgnoreComment(node: Node): boolean {
return hasComment(node, "lingui-extract-ignore")
}

function hasI18nComment(node: Node): boolean {
return hasComment(node, "i18n")
}

export default function ({ types: t }: { types: BabelTypes }): PluginObj {
let localTransComponentName: string
let localCoreI18nName: string

function isTransComponent(node: Node) {
return (
Expand All @@ -145,20 +161,33 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {

const isI18nMethod = (node: Node) =>
t.isMemberExpression(node) &&
t.isIdentifier(node.object, { name: "i18n" }) &&
t.isIdentifier(node.property, { name: "_" })

const isI18nTMethod = (node: Node) =>
t.isMemberExpression(node) &&
t.isIdentifier(node.object, { name: localCoreI18nName }) &&
t.isIdentifier(node.property, { name: "t" })
(t.isIdentifier(node.object, { name: I18N_OBJECT }) ||
(t.isMemberExpression(node.object) &&
t.isIdentifier(node.object.property, { name: I18N_OBJECT }) &&
(t.isIdentifier(node.property, { name: "_" }) ||
t.isIdentifier(node.property, { name: "t" }))))

const extractFromMessageDescriptor = (
path: NodePath<ObjectExpression>,
ctx: PluginPass
) => {
const props = extractFromObjectExpression(t, path.node, ctx.file.hub, [
"id",
"message",
"comment",
"context",
])

if (!props.id) {
console.warn(
path.buildCodeFrameError("Missing message ID, skipping.").message
)
return
}

function hasI18nComment(node: Node): boolean {
return (
node.leadingComments &&
node.leadingComments.some((comm) => comm.value.match(/^\s*i18n/))
)
collectMessage(path, props, ctx)
}

return {
visitor: {
// Get the local name of Trans component. Usually it's just `Trans`, but
Expand All @@ -182,17 +211,6 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {
// If there's no alias, consider it was imported as Trans.
localTransComponentName = importDeclarations["Trans"] || "Trans"
}

const coreImportDeclarations: Record<string, string> = {}
if (moduleName === "@lingui/core") {
node.specifiers.forEach((specifier) => {
specifier = specifier as ImportSpecifier
coreImportDeclarations[(specifier.imported as Identifier).name] =
specifier.local.name
})

localCoreI18nName = coreImportDeclarations["i18n"] || "i18n"
}
},

// Extract translation from <Trans /> component.
Expand Down Expand Up @@ -237,66 +255,59 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {
},

CallExpression(path, ctx) {
const hasComment = [path.node, path.parent].some((node) =>
hasI18nComment(node)
)

const firstArgument = path.node.arguments[0]

let props: Record<string, unknown> = {}

if (
isI18nTMethod(path.node.callee) &&
t.isObjectExpression(firstArgument)
) {
props = {
...extractFromObjectExpression(t, firstArgument, ctx.file.hub, [
"id",
"message",
"comment",
"context",
]),
}

collectMessage(path, props, ctx)
if ([path.node, path.parent].some((node) => hasIgnoreComment(node))) {
return
}

// support `i18n._` calls written by users in form i18n._(id, variables, descriptor)
// without explicit annotation with comment
// calls generated by macro has a form i18n._(/*i18n*/ {descriptor}) and
// processed by ObjectExpression visitor
const isNonMacroI18n =
isI18nMethod(path.node.callee) && !firstArgument?.leadingComments
if (!hasComment && !isNonMacroI18n) return

props = {
id: getTextFromExpression(
t,
firstArgument as Expression,
ctx.file.hub,
false
),
const firstArgument = path.get("arguments")[0]

// i18n._(...)
if (!isI18nMethod(path.node.callee)) {
return
}

if (!props.id) {
// call with explicit annotation
// i18n._(/*i18n*/ {descriptor})
// skipping this as it is processed
// by ObjectExpression visitor
if (hasI18nComment(firstArgument.node)) {
return
}

const msgDescArg = path.node.arguments[2]
if (firstArgument.isObjectExpression()) {
// i8n._({message, id, context})
extractFromMessageDescriptor(firstArgument, ctx)
return
} else {
// i18n._(id, variables, descriptor)
let props = {
id: getTextFromExpression(
t,
firstArgument.node as Expression,
ctx.file.hub,
false
),
}

if (t.isObjectExpression(msgDescArg)) {
props = {
...props,
...extractFromObjectExpression(t, msgDescArg, ctx.file.hub, [
"message",
"comment",
"context",
]),
if (!props.id) {
return
}
}

collectMessage(path, props, ctx)
const msgDescArg = path.node.arguments[2]

if (t.isObjectExpression(msgDescArg)) {
props = {
...props,
...extractFromObjectExpression(t, msgDescArg, ctx.file.hub, [
"message",
"comment",
"context",
]),
}
}

collectMessage(path, props, ctx)
}
},

StringLiteral(path, ctx) {
Expand All @@ -322,21 +333,7 @@ export default function ({ types: t }: { types: BabelTypes }): PluginObj {
ObjectExpression(path, ctx) {
if (!hasI18nComment(path.node)) return

const props = extractFromObjectExpression(t, path.node, ctx.file.hub, [
"id",
"message",
"comment",
"context",
])

if (!props.id) {
console.warn(
path.buildCodeFrameError("Missing message ID, skipping.").message
)
return
}

collectMessage(path, props, ctx)
extractFromMessageDescriptor(path, ctx)
},
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should ex
message: undefined,
origin: [
js-call-expression.js,
2,
1,
],
},
{
Expand All @@ -19,7 +19,7 @@ exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should ex
message: undefined,
origin: [
js-call-expression.js,
4,
3,
],
},
{
Expand All @@ -29,7 +29,7 @@ exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should ex
message: Message with id,
origin: [
js-call-expression.js,
6,
5,
],
},
{
Expand All @@ -39,7 +39,7 @@ exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should ex
message: undefined,
origin: [
js-call-expression.js,
8,
7,
],
},
{
Expand All @@ -49,22 +49,37 @@ exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should ex
message: undefined,
origin: [
js-call-expression.js,
10,
9,
],
},
]
`;

exports[`@lingui/babel-plugin-extract-messages CallExpression i18n._() should extract messages from i18n.t aliased expression 1`] = `
[
{
comment: Your comment,
comment: My comment,
context: undefined,
id: your.id,
message: Your Id Message,
id: my.id,
message: My Id Message,
origin: [
node-call-expression-aliased.js,
3,
js-call-expression.js,
12,
],
},
{
comment: undefined,
context: undefined,
id: Aliased Message,
message: undefined,
origin: [
js-call-expression.js,
19,
],
},
{
comment: My comment,
context: undefined,
id: my.id,
message: My Id Message,
origin: [
js-call-expression.js,
22,
],
},
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
const msg = i18n._("Message")

const msg = i18n._('Message')
const withDescription = i18n._("Description", {}, { comment: "description" })

const withDescription = i18n._('Description', {}, { comment: "description"});
const withId = i18n._("ID", {}, { message: "Message with id" })

const withId = i18n._('ID', {}, { message: 'Message with id' });
const withValues = i18n._("Values {param}", { param: param })

const withValues = i18n._('Values {param}', { param: param });
const withContext = i18n._("Some id", {}, { context: "Context1" })

const withContext = i18n._('Some id', {},{ context: 'Context1'});
// from message descriptor
i18n._({
id: "my.id",
message: "My Id Message",
comment: "My comment",
})

const withTMessageDescriptor = i18n.t({ id: 'my.id', message: 'My Id Message', comment: 'My comment'});
// support alias
i18n.t("Aliased Message")

// from message descriptor
i18n.t({
id: "my.id",
message: "My Id Message",
comment: "My comment",
})

This file was deleted.

This file was deleted.

21 changes: 18 additions & 3 deletions packages/babel-plugin-extract-messages/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,25 @@ import { Trans } from "@lingui/react";
})
})

it("should extract messages from i18n.t aliased expression", () => {
it("should extract from member access expressions", () => {
const code = `
// member access
ctx.i18n._("Message")
// member access any depth
ctx.req.i18n._("Message")
// should not extract if disabled via annotation
/* lingui-extract-ignore */
ctx.i18n._("Message")
/* lingui-extract-ignore */
ctx.req.i18n._("Message")
`
expectNoConsole(() => {
const messages = transform("node-call-expression-aliased.js")
expect(messages).toMatchSnapshot()
const messages = transformCode(code)
expect(messages.length).toBe(2)
})
})

Expand Down
Loading

0 comments on commit 22c6ae3

Please sign in to comment.