2019-11-19 17:18:42 +01:00
|
|
|
import {
|
|
|
|
Node,
|
|
|
|
ObjectExpression,
|
|
|
|
ImportDeclaration,
|
|
|
|
ImportSpecifier,
|
|
|
|
CallExpression,
|
|
|
|
Identifier,
|
2019-11-29 02:57:05 +01:00
|
|
|
Literal,
|
2019-11-19 17:18:42 +01:00
|
|
|
} from 'estree'
|
2019-11-26 17:05:17 +01:00
|
|
|
import delve from 'dlv'
|
2019-11-19 17:18:42 +01:00
|
|
|
import { walk } from 'estree-walker'
|
|
|
|
import { Ast } from 'svelte/types/compiler/interfaces'
|
|
|
|
import { parse } from 'svelte/compiler'
|
|
|
|
|
2019-11-29 02:57:05 +01:00
|
|
|
import { deepSet } from './includes/deepSet'
|
|
|
|
import { getObjFromExpression } from './includes/getObjFromExpression'
|
|
|
|
import { Message } from './types'
|
|
|
|
|
2019-11-19 17:18:42 +01:00
|
|
|
const LIB_NAME = 'svelte-i18n'
|
|
|
|
const DEFINE_MESSAGES_METHOD_NAME = 'defineMessages'
|
2019-11-29 02:57:05 +01:00
|
|
|
const FORMAT_METHOD_NAMES = new Set(['format', '_', 't'])
|
|
|
|
const IGNORED_UTILITIES = new Set(['number', 'date', 'time'])
|
2019-11-19 17:18:42 +01:00
|
|
|
|
|
|
|
function isFormatCall(node: Node, imports: Set<string>) {
|
|
|
|
if (node.type !== 'CallExpression') return false
|
|
|
|
|
|
|
|
let identifier: Identifier
|
2019-11-29 02:57:05 +01:00
|
|
|
if (
|
|
|
|
node.callee.type === 'MemberExpression' &&
|
|
|
|
node.callee.property.type === 'Identifier' &&
|
|
|
|
!IGNORED_UTILITIES.has(node.callee.property.name)
|
|
|
|
) {
|
2019-11-19 17:18:42 +01:00
|
|
|
identifier = node.callee.object as Identifier
|
|
|
|
} else if (node.callee.type === 'Identifier') {
|
|
|
|
identifier = node.callee
|
|
|
|
}
|
|
|
|
if (!identifier || identifier.type !== 'Identifier') {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const methodName = identifier.name.slice(1)
|
|
|
|
return imports.has(methodName)
|
|
|
|
}
|
|
|
|
|
|
|
|
function isMessagesDefinitionCall(node: Node, methodName: string) {
|
|
|
|
if (node.type !== 'CallExpression') return false
|
|
|
|
|
|
|
|
return (
|
|
|
|
node.callee &&
|
|
|
|
node.callee.type === 'Identifier' &&
|
|
|
|
node.callee.name === methodName
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function getLibImportDeclarations(ast: Ast) {
|
|
|
|
return (ast.instance
|
|
|
|
? ast.instance.content.body.filter(
|
|
|
|
node =>
|
2019-11-20 20:31:55 +01:00
|
|
|
node.type === 'ImportDeclaration' && node.source.value === LIB_NAME
|
2019-11-19 17:18:42 +01:00
|
|
|
)
|
|
|
|
: []) as ImportDeclaration[]
|
|
|
|
}
|
|
|
|
|
|
|
|
function getDefineMessagesSpecifier(decl: ImportDeclaration) {
|
|
|
|
return decl.specifiers.find(
|
|
|
|
spec =>
|
2019-11-20 20:31:55 +01:00
|
|
|
'imported' in spec && spec.imported.name === DEFINE_MESSAGES_METHOD_NAME
|
2019-11-19 17:18:42 +01:00
|
|
|
) as ImportSpecifier
|
|
|
|
}
|
|
|
|
|
|
|
|
function getFormatSpecifiers(decl: ImportDeclaration) {
|
|
|
|
return decl.specifiers.filter(
|
2019-11-20 20:31:55 +01:00
|
|
|
spec => 'imported' in spec && FORMAT_METHOD_NAMES.has(spec.imported.name)
|
2019-11-19 17:18:42 +01:00
|
|
|
) as ImportSpecifier[]
|
|
|
|
}
|
|
|
|
|
|
|
|
export function collectFormatCalls(ast: Ast) {
|
|
|
|
const importDecls = getLibImportDeclarations(ast)
|
|
|
|
|
|
|
|
if (importDecls.length === 0) return []
|
|
|
|
|
|
|
|
const imports = new Set(
|
|
|
|
importDecls.flatMap(decl =>
|
2019-11-20 20:31:55 +01:00
|
|
|
getFormatSpecifiers(decl).map(n => n.local.name)
|
|
|
|
)
|
2019-11-19 17:18:42 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
if (imports.size === 0) return []
|
|
|
|
|
|
|
|
const calls: CallExpression[] = []
|
2019-11-29 02:57:05 +01:00
|
|
|
function enter(node: Node) {
|
2019-11-19 17:18:42 +01:00
|
|
|
if (isFormatCall(node, imports)) {
|
|
|
|
calls.push(node as CallExpression)
|
|
|
|
this.skip()
|
|
|
|
}
|
|
|
|
}
|
2019-11-29 02:57:05 +01:00
|
|
|
walk(ast.instance as any, { enter })
|
|
|
|
walk(ast.html as any, { enter })
|
2019-11-19 17:18:42 +01:00
|
|
|
|
|
|
|
return calls
|
|
|
|
}
|
|
|
|
|
|
|
|
export function collectMessageDefinitions(ast: Ast) {
|
|
|
|
const definitions: ObjectExpression[] = []
|
|
|
|
const defineImportDecl = getLibImportDeclarations(ast).find(
|
2019-11-20 20:31:55 +01:00
|
|
|
getDefineMessagesSpecifier
|
2019-11-19 17:18:42 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
if (defineImportDecl == null) return []
|
|
|
|
|
|
|
|
const defineMethodName = getDefineMessagesSpecifier(defineImportDecl).local
|
|
|
|
.name
|
|
|
|
|
|
|
|
walk(ast.instance as any, {
|
|
|
|
enter(node: Node) {
|
|
|
|
if (isMessagesDefinitionCall(node, defineMethodName) === false) return
|
|
|
|
const [arg] = (node as CallExpression).arguments
|
|
|
|
if (arg.type === 'ObjectExpression') {
|
|
|
|
definitions.push(arg)
|
|
|
|
this.skip()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
return definitions.flatMap(definitionDict =>
|
2019-11-29 02:57:05 +01:00
|
|
|
definitionDict.properties.map(
|
|
|
|
propNode => propNode.value as ObjectExpression
|
|
|
|
)
|
2019-11-19 17:18:42 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export function collectMessages(markup: string): Message[] {
|
|
|
|
const ast = parse(markup)
|
|
|
|
const calls = collectFormatCalls(ast)
|
|
|
|
const definitions = collectMessageDefinitions(ast)
|
|
|
|
return [
|
|
|
|
...definitions.map(definition => getObjFromExpression(definition)),
|
|
|
|
...calls.map(call => {
|
|
|
|
const [pathNode, options] = call.arguments
|
|
|
|
if (pathNode.type === 'ObjectExpression') {
|
|
|
|
return getObjFromExpression(pathNode)
|
|
|
|
}
|
|
|
|
|
2019-11-29 02:57:05 +01:00
|
|
|
const node = pathNode as Literal
|
|
|
|
const id = node.value as string
|
2019-11-19 17:18:42 +01:00
|
|
|
|
|
|
|
if (options && options.type === 'ObjectExpression') {
|
|
|
|
const messageObj = getObjFromExpression(options)
|
2019-11-29 02:57:05 +01:00
|
|
|
messageObj.meta.id = id
|
2019-11-19 17:18:42 +01:00
|
|
|
return messageObj
|
|
|
|
}
|
|
|
|
|
2019-11-29 03:16:15 +01:00
|
|
|
return { node, meta: { id } }
|
2019-11-19 17:18:42 +01:00
|
|
|
}),
|
|
|
|
].filter(Boolean)
|
|
|
|
}
|
|
|
|
|
|
|
|
export function extractMessages(
|
|
|
|
markup: string,
|
2019-11-20 20:31:55 +01:00
|
|
|
{ accumulator = {}, shallow = false, overwrite = false } = {} as any
|
2019-11-19 17:18:42 +01:00
|
|
|
) {
|
|
|
|
collectMessages(markup).forEach(message => {
|
|
|
|
let defaultValue = message.meta.default
|
|
|
|
if (typeof defaultValue === 'undefined') {
|
|
|
|
defaultValue = ''
|
|
|
|
}
|
|
|
|
|
|
|
|
if (shallow) {
|
|
|
|
if (overwrite === false && message.meta.id in accumulator) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
accumulator[message.meta.id] = defaultValue
|
|
|
|
} else {
|
|
|
|
if (
|
|
|
|
overwrite === false &&
|
2019-11-26 17:05:17 +01:00
|
|
|
typeof delve(accumulator, message.meta.id) !== 'undefined'
|
2019-11-19 17:18:42 +01:00
|
|
|
) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
deepSet(accumulator, message.meta.id, defaultValue)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return accumulator
|
|
|
|
}
|