Skip to content

Commit

Permalink
feat(macro): add useLingui macro
Browse files Browse the repository at this point in the history
  • Loading branch information
thekip committed Feb 22, 2024
1 parent 3d7c9ae commit d8d4565
Show file tree
Hide file tree
Showing 6 changed files with 445 additions and 9 deletions.
13 changes: 13 additions & 0 deletions packages/macro/__typetests__/index.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Plural,
Select,
SelectOrdinal,
useLingui,
} from "../index"
// eslint-disable-next-line import/no-extraneous-dependencies
import React from "react"
Expand Down Expand Up @@ -338,3 +339,15 @@ m = (
other={<Trans>...</Trans>}
/>
)

////////////////////////
//// React useLingui()
////////////////////////
function MyComponent() {
const { t } = useLingui()

expectType<string>(t`Hello world`)
expectType<string>(t({ message: "my message" }))
// @ts-expect-error: you could not pass a custom instance here
expectType<string>(t(i18n)({ message: "my message" }))
}
32 changes: 32 additions & 0 deletions packages/macro/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,35 @@ export const SelectOrdinal: VFC<PluralChoiceProps>
* ```
*/
export const Select: VFC<SelectChoiceProps>

export function _t(descriptor: MacroMessageDescriptor): string
export function _t(
literals: TemplateStringsArray,
...placeholders: any[]
): string

/**
*
* Return `t` macro function which is bound to i18n passed from React.Context
*
* Returned `t` macro function has all the same signatures as global `t`
*
* @example
* ```
* const { t } = useLingui();
* const message = t`Text`;
* ```
*
* @example
* ```
* const { t } = useLingui();
* const message = t({
* id: "msg.hello",
* comment: "Greetings at the homepage",
* message: `Hello ${name}`,
* });
* ```
*/
export const useLingui: () => {
t: typeof _t
}
27 changes: 19 additions & 8 deletions packages/macro/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isImportSpecifier,
isIdentifier,
JSXIdentifier,
Statement,
} from "@babel/types"

export type LinguiMacroOpts = {
Expand All @@ -23,6 +24,7 @@ const jsMacroTags = new Set([
"msg",
"arg",
"t",
"useLingui",
"plural",
"select",
"selectOrdinal",
Expand All @@ -45,6 +47,8 @@ function getConfig(_config?: LinguiConfigNormalized) {
function macro({ references, state, babel, config }: MacroParams) {
const opts: LinguiMacroOpts = config as LinguiMacroOpts

const body = state.file.path.node.body

const {
i18nImportModule,
i18nImportName,
Expand All @@ -55,6 +59,7 @@ function macro({ references, state, babel, config }: MacroParams) {
const jsxNodes = new Set<NodePath>()
const jsNodes = new Set<NodePath>()
let needsI18nImport = false
let needsUseLinguiImport = false

let nameMap = new Map<string, string>()
Object.keys(references).forEach((tagName) => {
Expand Down Expand Up @@ -92,7 +97,9 @@ function macro({ references, state, babel, config }: MacroParams) {
nameMap,
})
try {
if (macro.replacePath(path)) needsI18nImport = true
macro.replacePath(path)
needsI18nImport = needsI18nImport || macro.needsI18nImport
needsUseLinguiImport = needsUseLinguiImport || macro.needsUseLinguiImport
} catch (e) {
reportUnsupportedSyntax(path, e as Error)
}
Expand All @@ -110,12 +117,16 @@ function macro({ references, state, babel, config }: MacroParams) {
}
})

if (needsUseLinguiImport) {
addImport(babel, body, "@lingui/react", "useLingui")
}

if (needsI18nImport) {
addImport(babel, state, i18nImportModule, i18nImportName)
addImport(babel, body, i18nImportModule, i18nImportName)
}

if (jsxNodes.size) {
addImport(babel, state, TransImportModule, TransImportName)
addImport(babel, body, TransImportModule, TransImportName)
}
}

Expand All @@ -130,13 +141,13 @@ function reportUnsupportedSyntax(path: NodePath, e: Error) {

function addImport(
babel: MacroParams["babel"],
state: MacroParams["state"],
body: Statement[],
module: string,
importName: string
) {
const { types: t } = babel

const linguiImport = state.file.path.node.body.find(
const linguiImport = body.find(
(importNode) =>
t.isImportDeclaration(importNode) &&
importNode.source.value === module &&
Expand All @@ -148,16 +159,16 @@ function addImport(
// Handle adding the import or altering the existing import
if (linguiImport) {
if (
linguiImport.specifiers.findIndex(
!linguiImport.specifiers.find(
(specifier) =>
isImportSpecifier(specifier) &&
isIdentifier(specifier.imported, { name: importName })
) === -1
)
) {
linguiImport.specifiers.push(t.importSpecifier(tIdentifier, tIdentifier))
}
} else {
state.file.path.node.body.unshift(
body.unshift(
t.importDeclaration(
[t.importSpecifier(tIdentifier, tIdentifier)],
t.stringLiteral(module)
Expand Down
99 changes: 98 additions & 1 deletion packages/macro/src/macroJs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export default class MacroJs {
nameMap: Map<string, string>
nameMapReversed: Map<string, string>

needsUseLinguiImport = false
needsI18nImport = false

// Positional expressions counter (e.g. for placeholders `Hello {0}, today is {1}`)
_expressionIndex = makeCounter()

Expand Down Expand Up @@ -148,13 +151,105 @@ export default class MacroJs {
this.isLinguiIdentifier(path.node.callee, "t")
) {
this.replaceTAsFunction(path as NodePath<CallExpression>)
this.needsI18nImport = true

return true
}

// { t } = useLingui()
if (
this.types.isCallExpression(path.node) &&
this.isLinguiIdentifier(path.node.callee, "useLingui") &&
this.types.isVariableDeclarator(path.parentPath.node)
) {
this.needsUseLinguiImport = true

const varDec = path.parentPath.node
const _property = this.types.isObjectPattern(varDec.id)
? varDec.id.properties.find(
(property): property is ObjectProperty & { value: Identifier } =>
this.types.isObjectProperty(property) &&
this.types.isIdentifier(property.key) &&
this.types.isIdentifier(property.value) &&
property.key.name == "t"
)
: null

// Enforce destructuring `t` from `useLingui` macro to prevent misuse
if (!_property) {
throw new Error(
`Must destruct _ when using useLingui macro, i.e:
const { t } = useLingui()
or
const { t: _ } = useLingui()`
)
}

const uniqTIdentifier = path.scope.generateUidIdentifier("t")

const newUseLinguiExpression = this.types.variableDeclarator(
this.types.objectPattern([
this.types.objectProperty(
this.types.identifier("_"),
uniqTIdentifier
),
]),
this.types.callExpression(this.types.identifier("useLingui"), [])
)

path.parentPath.replaceWith(newUseLinguiExpression)

path.scope
.getBinding(_property.value.name)
?.referencePaths.forEach((refPath) => {
const currentPath = refPath.parentPath

// { t } = useLingui()
// t`Hello!`
if (currentPath.isTaggedTemplateExpression()) {
const tokens = this.tokenizeTemplateLiteral(currentPath.node)

const descriptor = this.createMessageDescriptorFromTokens(
tokens,
currentPath.node.loc
)

const callExpr = this.types.callExpression(
this.types.identifier(uniqTIdentifier.name),
[descriptor]
)

return currentPath.replaceWith(callExpr)
}

// { t } = useLingui()
// t(messageDescriptor)
if (
currentPath.isCallExpression() &&
this.types.isExpression(currentPath.node.arguments[0])
) {
let descriptor = this.processDescriptor(
currentPath.node.arguments[0]
)
const callExpr = this.types.callExpression(
this.types.identifier(uniqTIdentifier.name),
[descriptor]
)

return currentPath.replaceWith(callExpr)
}

// for rest of cases just rename identifier for run-time counterpart
refPath.replaceWith(this.types.identifier(uniqTIdentifier.name))
})
return false
}

const tokens = this.tokenizeNode(path.node)

this.replacePathWithMessage(path, tokens)

this.needsI18nImport = true
return true
}

Expand Down Expand Up @@ -467,8 +562,10 @@ export default class MacroJs {
return (
this.types.isTaggedTemplateExpression(node) &&
(this.isLinguiIdentifier(node.tag, "t") ||
this.isLinguiIdentifier(node.tag, "_") ||
(this.types.isCallExpression(node.tag) &&
this.isLinguiIdentifier(node.tag.callee, "t")))
(this.isLinguiIdentifier(node.tag.callee, "t") ||
this.isLinguiIdentifier(node.tag.callee, "_"))))
)
}

Expand Down
1 change: 1 addition & 0 deletions packages/macro/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const testCases: Record<string, TestCase[]> = {
"jsx-plural": require("./jsx-plural").default,
"jsx-selectOrdinal": require("./jsx-selectOrdinal").default,
"js-defineMessage": require("./js-defineMessage").default,
"js-useLingui": require("./js-useLingui").default,
}

function stripIdPlugin(): PluginObj {
Expand Down
Loading

0 comments on commit d8d4565

Please sign in to comment.