mirror of
https://github.com/stoatchat/javascript-lingui-solid.git
synced 2026-07-01 21:44:23 -04:00
329 lines
8.8 KiB
TypeScript
329 lines
8.8 KiB
TypeScript
import * as babelTypes from "@babel/types"
|
|
import * as t from "@babel/types"
|
|
import {
|
|
CallExpression,
|
|
Expression,
|
|
Identifier,
|
|
ObjectExpression,
|
|
ObjectProperty,
|
|
} from "@babel/types"
|
|
import { NodePath } from "@babel/traverse"
|
|
|
|
import { Tokens } from "./icu"
|
|
import { JsMacroName } from "./constants"
|
|
import { createMessageDescriptorFromTokens } from "./messageDescriptorUtils"
|
|
import {
|
|
isLinguiIdentifier,
|
|
isDefineMessage,
|
|
tokenizeTemplateLiteral,
|
|
tokenizeNode,
|
|
processDescriptor,
|
|
createMacroJsContext,
|
|
MacroJsContext,
|
|
} from "./macroJsAst"
|
|
|
|
export type MacroJsOpts = {
|
|
i18nImportName: string
|
|
useLinguiImportName: string
|
|
|
|
stripNonEssentialProps: boolean
|
|
stripMessageProp: boolean
|
|
isLinguiIdentifier: (node: Identifier, macro: JsMacroName) => boolean
|
|
}
|
|
|
|
export class MacroJs {
|
|
// Identifier of i18n object
|
|
i18nImportName: string
|
|
useLinguiImportName: string
|
|
|
|
needsUseLinguiImport = false
|
|
needsI18nImport = false
|
|
|
|
_ctx: MacroJsContext
|
|
|
|
constructor(opts: MacroJsOpts) {
|
|
this.i18nImportName = opts.i18nImportName
|
|
this.useLinguiImportName = opts.useLinguiImportName
|
|
|
|
this._ctx = createMacroJsContext(
|
|
opts.isLinguiIdentifier,
|
|
opts.stripNonEssentialProps,
|
|
opts.stripMessageProp
|
|
)
|
|
}
|
|
|
|
private replacePathWithMessage = (
|
|
path: NodePath,
|
|
tokens: Tokens,
|
|
linguiInstance?: babelTypes.Expression
|
|
) => {
|
|
return this.createI18nCall(
|
|
createMessageDescriptorFromTokens(
|
|
tokens,
|
|
path.node.loc,
|
|
this._ctx.stripNonEssentialProps,
|
|
this._ctx.stripMessageProp
|
|
),
|
|
linguiInstance
|
|
)
|
|
}
|
|
|
|
replacePath = (path: NodePath): false | babelTypes.Expression => {
|
|
const ctx = this._ctx
|
|
|
|
// defineMessage({ message: "Message", context: "My" }) -> {id: <hash + context>, message: "Message"}
|
|
if (
|
|
//
|
|
path.isCallExpression() &&
|
|
isDefineMessage(path.get("callee").node, ctx)
|
|
) {
|
|
return processDescriptor(
|
|
path.get("arguments")[0].node as ObjectExpression,
|
|
ctx
|
|
)
|
|
}
|
|
|
|
// defineMessage`Message` -> {id: <hash>, message: "Message"}
|
|
if (
|
|
path.isTaggedTemplateExpression() &&
|
|
isDefineMessage(path.get("tag").node, ctx)
|
|
) {
|
|
const tokens = tokenizeTemplateLiteral(path.get("quasi").node, ctx)
|
|
return createMessageDescriptorFromTokens(
|
|
tokens,
|
|
path.node.loc,
|
|
ctx.stripNonEssentialProps,
|
|
ctx.stripMessageProp
|
|
)
|
|
}
|
|
|
|
if (path.isTaggedTemplateExpression()) {
|
|
const tag = path.get("tag")
|
|
|
|
// t(i18nInstance)`Message` -> i18nInstance._(messageDescriptor)
|
|
if (
|
|
tag.isCallExpression() &&
|
|
tag.get("arguments")[0]?.isExpression() &&
|
|
isLinguiIdentifier(tag.get("callee").node, JsMacroName.t, ctx)
|
|
) {
|
|
// Use the first argument as i18n instance instead of the default i18n instance
|
|
const i18nInstance = tag.get("arguments")[0].node as Expression
|
|
const tokens = tokenizeNode(path.node, false, ctx)
|
|
|
|
return this.replacePathWithMessage(path, tokens, i18nInstance)
|
|
}
|
|
}
|
|
|
|
// t(i18nInstance)(messageDescriptor) -> i18nInstance._(messageDescriptor)
|
|
if (path.isCallExpression()) {
|
|
const callee = path.get("callee")
|
|
|
|
if (
|
|
callee.isCallExpression() &&
|
|
callee.get("arguments")[0]?.isExpression() &&
|
|
isLinguiIdentifier(callee.get("callee").node, JsMacroName.t, ctx)
|
|
) {
|
|
const i18nInstance = callee.node.arguments[0] as Expression
|
|
return this.replaceTAsFunction(
|
|
path.node as CallExpression,
|
|
ctx,
|
|
i18nInstance
|
|
)
|
|
}
|
|
}
|
|
|
|
// t({...})
|
|
if (
|
|
path.isCallExpression() &&
|
|
isLinguiIdentifier(path.get("callee").node, JsMacroName.t, ctx)
|
|
) {
|
|
this.needsI18nImport = true
|
|
return this.replaceTAsFunction(path.node, ctx)
|
|
}
|
|
|
|
// { t } = useLingui()
|
|
if (
|
|
path.isCallExpression() &&
|
|
isLinguiIdentifier(path.get("callee").node, JsMacroName.useLingui, ctx)
|
|
) {
|
|
this.needsUseLinguiImport = true
|
|
return this.processUseLingui(path, ctx)
|
|
}
|
|
|
|
const tokens = tokenizeNode(path.node, true, ctx)
|
|
|
|
if (tokens) {
|
|
this.needsI18nImport = true
|
|
return this.replacePathWithMessage(path, tokens)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* macro `t` is called with MessageDescriptor, after that
|
|
* we create a new node to append it to i18n._
|
|
*/
|
|
private replaceTAsFunction = (
|
|
node: CallExpression,
|
|
ctx: MacroJsContext,
|
|
linguiInstance?: babelTypes.Expression
|
|
): babelTypes.CallExpression => {
|
|
let arg: Expression = node.arguments[0] as Expression
|
|
|
|
if (t.isObjectExpression(arg)) {
|
|
arg = processDescriptor(arg, ctx)
|
|
}
|
|
|
|
return this.createI18nCall(arg, linguiInstance)
|
|
}
|
|
|
|
/**
|
|
* Receives reference to `useLingui()` call
|
|
*
|
|
* Finds every usage of { t } destructured from the call
|
|
* and process each reference as usual `t` macro.
|
|
*
|
|
* const { t } = useLingui()
|
|
* t`Message`
|
|
*
|
|
* ↓ ↓ ↓ ↓ ↓ ↓
|
|
*
|
|
* const { _: _t } = useLingui()
|
|
* _t({id: <hash>, message: "Message"})
|
|
*/
|
|
processUseLingui(path: NodePath<CallExpression>, ctx: MacroJsContext) {
|
|
/*
|
|
* path is CallExpression eq:
|
|
* useLingui()
|
|
*
|
|
* path.parentPath should be a VariableDeclarator eq:
|
|
* const { t } = useLingui()
|
|
*/
|
|
if (!path.parentPath.isVariableDeclarator()) {
|
|
throw new Error(
|
|
`\`useLingui\` macro must be used in variable declaration.
|
|
|
|
Example:
|
|
|
|
const { t } = useLingui()
|
|
`
|
|
)
|
|
}
|
|
|
|
// looking for `t` property in left side assigment
|
|
// in the declarator `const { t } = useLingui()`
|
|
const varDec = path.parentPath.node
|
|
|
|
if (!t.isObjectPattern(varDec.id)) {
|
|
// Enforce destructuring `t` from `useLingui` macro to prevent misuse
|
|
throw new Error(
|
|
`You have to destructure \`t\` when using the \`useLingui\` macro, i.e:
|
|
const { t } = useLingui()
|
|
or
|
|
const { t: _ } = useLingui()
|
|
`
|
|
)
|
|
}
|
|
|
|
const _property = t.isObjectPattern(varDec.id)
|
|
? varDec.id.properties.find(
|
|
(
|
|
property
|
|
): property is ObjectProperty & {
|
|
value: Identifier
|
|
key: Identifier
|
|
} =>
|
|
t.isObjectProperty(property) &&
|
|
t.isIdentifier(property.key) &&
|
|
t.isIdentifier(property.value) &&
|
|
property.key.name == "t"
|
|
)
|
|
: null
|
|
|
|
const newNode = t.callExpression(t.identifier(this.useLinguiImportName), [])
|
|
|
|
if (!_property) {
|
|
return newNode
|
|
}
|
|
|
|
const uniqTIdentifier = path.scope.generateUidIdentifier("t")
|
|
|
|
path.scope
|
|
.getBinding(_property.value.name)
|
|
?.referencePaths.forEach((refPath) => {
|
|
// reference usually points to Identifier,
|
|
// parent would be an Expression with this identifier which we are interesting in
|
|
const currentPath = refPath.parentPath
|
|
|
|
const _ctx = createMacroJsContext(
|
|
ctx.isLinguiIdentifier,
|
|
ctx.stripNonEssentialProps,
|
|
ctx.stripMessageProp
|
|
);
|
|
|
|
// { t } = useLingui()
|
|
// t`Hello!`
|
|
if (currentPath.isTaggedTemplateExpression()) {
|
|
const tokens = tokenizeTemplateLiteral(currentPath.node, _ctx)
|
|
|
|
const descriptor = createMessageDescriptorFromTokens(
|
|
tokens,
|
|
currentPath.node.loc,
|
|
_ctx.stripNonEssentialProps,
|
|
_ctx.stripMessageProp
|
|
)
|
|
|
|
const callExpr = t.callExpression(
|
|
t.identifier(uniqTIdentifier.name),
|
|
[descriptor]
|
|
)
|
|
|
|
return currentPath.replaceWith(callExpr)
|
|
}
|
|
|
|
// { t } = useLingui()
|
|
// t(messageDescriptor)
|
|
if (
|
|
currentPath.isCallExpression() &&
|
|
currentPath.get("arguments")[0]?.isObjectExpression()
|
|
) {
|
|
let descriptor = processDescriptor(
|
|
(currentPath.get("arguments")[0] as NodePath<ObjectExpression>)
|
|
.node,
|
|
_ctx
|
|
)
|
|
const callExpr = t.callExpression(
|
|
t.identifier(uniqTIdentifier.name),
|
|
[descriptor]
|
|
)
|
|
|
|
return currentPath.replaceWith(callExpr)
|
|
}
|
|
|
|
// for rest of cases just rename identifier for run-time counterpart
|
|
refPath.replaceWith(t.identifier(uniqTIdentifier.name))
|
|
})
|
|
|
|
// assign uniq identifier for runtime `_`
|
|
// { t } = useLingui() -> { _ : _t } = useLingui()
|
|
_property.key.name = "_"
|
|
_property.value.name = uniqTIdentifier.name
|
|
|
|
return t.callExpression(t.identifier(this.useLinguiImportName), [])
|
|
}
|
|
|
|
private createI18nCall(
|
|
messageDescriptor: Expression | undefined,
|
|
linguiInstance?: Expression
|
|
) {
|
|
return t.callExpression(
|
|
t.memberExpression(
|
|
linguiInstance ?? t.identifier(this.i18nImportName),
|
|
t.identifier("_")
|
|
),
|
|
messageDescriptor ? [messageDescriptor] : []
|
|
)
|
|
}
|
|
}
|