Skip to content

Add support for <i18n> blocks of SFC. #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 21, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs/rules/no-missing-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ You can be detected with this rule the following:
- `$tc`
- `tc`
- `v-t`
- `<i18n>`
- `<i18n>`

:-1: Examples of **incorrect** code for this rule:

Expand Down Expand Up @@ -92,3 +92,22 @@ const i18n = new VueI18n({
/* ✓ GOOD */
i18n.t('hello')
```

For SFC.

```vue
<i18n>
{
"en": {
"hi": "Hi! DIO!"
}
}
</i18n>

<template>
<div class="app">
<!-- ✓ GOOD -->
<p>{{ $t('hi') }}</p>
</div>
</template>
```
19 changes: 19 additions & 0 deletions docs/rules/no-unused-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ const i18n = new VueI18n({
i18n.t('hello')
```

For SFC.

```vue
<i18n>
{
"en": {
"hello": "Hello! DIO!",
"hi": "Hi! DIO!" // not used in SFC
}
}
</i18n>

<template>
<div class="app">
<p>{{ $t('hello') }}</p>
</div>
</template>
```

:+1: Examples of **correct** code for this rule:

locale messages:
Expand Down
86 changes: 50 additions & 36 deletions lib/rules/no-html-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
const { extname } = require('path')
const parse5 = require('parse5')
const {
UNEXPECTED_ERROR_LOCATION,
getLocaleMessages,
extractJsonInfo,
generateJsonAst
Expand Down Expand Up @@ -39,48 +38,63 @@ function findHTMLNode (node) {

function create (context) {
const filename = context.getFilename()
if (extname(filename) !== '.json') {
debug(`ignore ${filename} in no-html-messages`)
return {}
}

const { settings } = context
if (!settings['vue-i18n'] || !settings['vue-i18n'].localeDir) {
context.report({
loc: UNEXPECTED_ERROR_LOCATION,
message: `You need to 'localeDir' at 'settings. See the 'eslint-plugin-vue-i18n documentation`
})
return {}
}
function verifyJson (jsonString, jsonFilename, offsetLoc = { line: 1, column: 1 }) {
const ast = generateJsonAst(context, jsonString, jsonFilename)
if (!ast) { return }

const localeMessages = getLocaleMessages(settings['vue-i18n'].localeDir)
const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename)
if (!targetLocaleMessage) {
debug(`ignore ${filename} in no-html-messages`)
return {}
traverseNode(ast, messageNode => {
const htmlNode = parse5.parseFragment(messageNode.value, { sourceCodeLocationInfo: true })
const foundNode = findHTMLNode(htmlNode)
if (!foundNode) { return }
const loc = {
line: messageNode.loc.start.line,
column: messageNode.loc.start.column + foundNode.sourceCodeLocation.startOffset
}
if (loc.line === 1) {
loc.line += offsetLoc.line - 1
loc.column += offsetLoc.column - 1
} else {
loc.line += offsetLoc.line - 1
}
context.report({
message: `used HTML localization message`,
loc
})
})
}

return {
Program (node) {
const [jsonString, jsonFilename] = extractJsonInfo(context, node)
if (!jsonString || !jsonFilename) { return }

const ast = generateJsonAst(context, jsonString, jsonFilename)
if (!ast) { return }
if (extname(filename) === '.vue') {
return {
Program (node) {
const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment()
/** @type {VElement[]} */
const i18nBlocks = documentFragment && documentFragment.children.filter(node => node.type === 'VElement' && node.name === 'i18n') || []

traverseNode(ast, messageNode => {
const htmlNode = parse5.parseFragment(messageNode.value, { sourceCodeLocationInfo: true })
const foundNode = findHTMLNode(htmlNode)
if (!foundNode) { return }
context.report({
message: `used HTML localization message`,
loc: {
line: messageNode.loc.start.line,
column: messageNode.loc.start.column + foundNode.sourceCodeLocation.startOffset
for (const block of i18nBlocks) {
if (block.startTag.attributes.some(attr => !attr.directive && attr.key.name === 'src') || !block.endTag) {
continue
}
})
})
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
const tokens = tokenStore.getTokensBetween(block.startTag, block.endTag)
const jsonString = tokens.map(t => t.value).join('')
if (jsonString.trim()) {
verifyJson(jsonString, filename, block.startTag.loc.start)
}
}
}
}
} else if (extname(filename) === '.json' && getLocaleMessages(context).findExistLocaleMessage(filename)) {
return {
Program (node) {
const [jsonString, jsonFilename] = extractJsonInfo(context, node)
if (!jsonString || !jsonFilename) { return }
verifyJson(jsonString, jsonFilename)
}
}
} else {
debug(`ignore ${filename} in no-html-messages`)
return {}
}
}

Expand Down
38 changes: 16 additions & 22 deletions lib/rules/no-missing-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,41 @@
'use strict'

const {
UNEXPECTED_ERROR_LOCATION,
defineTemplateBodyVisitor,
getLocaleMessages
} = require('../utils/index')

function create (context) {
const { settings } = context
if (!settings['vue-i18n'] || !settings['vue-i18n'].localeDir) {
context.report({
loc: UNEXPECTED_ERROR_LOCATION,
message: `You need to set 'localeDir' at 'settings. See the 'eslint-plugin-vue-i18n documentation`
})
return {}
}

const localeDir = settings['vue-i18n'].localeDir
const localeMessages = getLocaleMessages(localeDir)

return defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name='t']" (node) {
checkDirective(context, localeMessages, node)
checkDirective(context, node)
},

"VAttribute[directive=true][key.name.name='t']" (node) {
checkDirective(context, localeMessages, node)
checkDirective(context, node)
},

"VElement[name=i18n] > VStartTag > VAttribute[key.name='path']" (node) {
checkComponent(context, localeMessages, node)
checkComponent(context, node)
},

"VElement[name=i18n] > VStartTag > VAttribute[key.name.name='path']" (node) {
checkComponent(context, localeMessages, node)
checkComponent(context, node)
},

CallExpression (node) {
checkCallExpression(context, localeMessages, node)
checkCallExpression(context, node)
}
}, {
CallExpression (node) {
checkCallExpression(context, localeMessages, node)
checkCallExpression(context, node)
}
})
}

function checkDirective (context, localeMessages, node) {
function checkDirective (context, node) {
const localeMessages = getLocaleMessages(context)
if (localeMessages.isEmpty()) { return }
if ((node.value && node.value.type === 'VExpressionContainer') &&
(node.value.expression && node.value.expression.type === 'Literal')) {
const key = node.value.expression.value
Expand All @@ -64,7 +53,9 @@ function checkDirective (context, localeMessages, node) {
}
}

function checkComponent (context, localeMessages, node) {
function checkComponent (context, node) {
const localeMessages = getLocaleMessages(context)
if (localeMessages.isEmpty()) { return }
if (node.value && node.value.type === 'VLiteral') {
const key = node.value.value
if (!key) {
Expand All @@ -78,13 +69,16 @@ function checkComponent (context, localeMessages, node) {
}
}

function checkCallExpression (context, localeMessages, node) {
function checkCallExpression (context, node) {
const funcName = (node.callee.type === 'MemberExpression' && node.callee.property.name) || node.callee.name

if (!/^(\$t|t|\$tc|tc)$/.test(funcName) || !node.arguments || !node.arguments.length) {
return
}

const localeMessages = getLocaleMessages(context)
if (localeMessages.isEmpty()) { return }

const [keyNode] = node.arguments
if (keyNode.type !== 'Literal') { return }

Expand Down
106 changes: 65 additions & 41 deletions lib/rules/no-unused-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
const { extname } = require('path')
const jsonDiffPatch = require('jsondiffpatch').create({})
const flatten = require('flat')
const collectKeys = require('../utils/collect-keys')
const { collectKeysFromFiles, collectKeysFromAST } = require('../utils/collect-keys')
const collectLinkedKeys = require('../utils/collect-linked-keys')
const {
UNEXPECTED_ERROR_LOCATION,
Expand All @@ -21,7 +21,7 @@ const debug = require('debug')('eslint-plugin-vue-i18n:no-unused-keys')
*/

/** @type {string[] | null} */
let usedLocaleMessageKeys = null // used locale message keys
let cacheUsedLocaleMessageKeys = null // used locale message keys

/**
* @param {RuleContext} context
Expand Down Expand Up @@ -99,54 +99,78 @@ function traverseNode (fullpath, paths, ast, fn) {

function create (context) {
const filename = context.getFilename()
if (extname(filename) !== '.json') {
debug(`ignore ${filename} in no-unused-keys`)
return {}
}

const { settings } = context
if (!settings['vue-i18n'] || !settings['vue-i18n'].localeDir) {
context.report({
loc: UNEXPECTED_ERROR_LOCATION,
message: `You need to 'localeDir' at 'settings. See the 'eslint-plugin-vue-i18n documentation`
})
return {}
}

const localeMessages = getLocaleMessages(settings['vue-i18n'].localeDir)
const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename)
if (!targetLocaleMessage) {
debug(`ignore ${filename} in no-unused-keys`)
return {}
}
function verifyJson (jsonString, jsonFilename, targetLocaleMessage, usedLocaleMessageKeys, offsetLoc = { line: 1, column: 1 }) {
const ast = generateJsonAst(context, jsonString, jsonFilename)
if (!ast) { return }

const options = (context.options && context.options[0]) || {}
const src = options.src || process.cwd()
const extensions = options.extensions || ['.js', '.vue']
const unusedKeys = getUnusedKeys(context, targetLocaleMessage, jsonString, usedLocaleMessageKeys)
if (!unusedKeys) { return }

if (!usedLocaleMessageKeys) {
usedLocaleMessageKeys = collectKeys([src], extensions)
traverseJsonAstWithUnusedKeys(unusedKeys, ast, (fullpath, node) => {
let { line, column } = node.loc.start
if (line === 1) {
line += offsetLoc.line - 1
column += offsetLoc.column - 1
} else {
line += offsetLoc.line - 1
}
context.report({
message: `unused '${fullpath}' key'`,
loc: { line, column }
})
})
}

return {
Program (node) {
const [jsonString, jsonFilename] = extractJsonInfo(context, node)
if (!jsonString || !jsonFilename) { return }
if (extname(filename) === '.vue') {
return {
Program (node) {
const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment()
/** @type {VElement[]} */
const i18nBlocks = documentFragment && documentFragment.children.filter(node => node.type === 'VElement' && node.name === 'i18n') || []
if (!i18nBlocks.length) {
return
}
const localeMessages = getLocaleMessages(context)
const usedLocaleMessageKeys = collectKeysFromAST(node, context.getSourceCode().visitorKeys)

const ast = generateJsonAst(context, jsonString, jsonFilename)
if (!ast) { return }
for (const block of i18nBlocks) {
if (block.startTag.attributes.some(attr => !attr.directive && attr.key.name === 'src') || !block.endTag) {
continue
}
const targetLocaleMessage = localeMessages.findBlockLocaleMessage(block)
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
const tokens = tokenStore.getTokensBetween(block.startTag, block.endTag)
const jsonString = tokens.map(t => t.value).join('')
if (jsonString.trim()) {
verifyJson(jsonString, filename, targetLocaleMessage, usedLocaleMessageKeys, block.startTag.loc.start)
}
}
}
}
} else if (extname(filename) === '.json') {
const localeMessages = getLocaleMessages(context)
const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename)
if (!targetLocaleMessage) {
debug(`ignore ${filename} in no-unused-keys`)
return {}
}
const options = (context.options && context.options[0]) || {}
const src = options.src || process.cwd()
const extensions = options.extensions || ['.js', '.vue']

const unusedKeys = getUnusedKeys(context, targetLocaleMessage, jsonString, usedLocaleMessageKeys)
if (!unusedKeys) { return }
const usedLocaleMessageKeys = cacheUsedLocaleMessageKeys || (cacheUsedLocaleMessageKeys = collectKeysFromFiles([src], extensions))

traverseJsonAstWithUnusedKeys(unusedKeys, ast, (fullpath, node) => {
const { line, column } = node.loc.start
context.report({
message: `unused '${fullpath}' key'`,
loc: { line, column }
})
})
return {
Program (node) {
const [jsonString, jsonFilename] = extractJsonInfo(context, node)
if (!jsonString || !jsonFilename) { return }
verifyJson(jsonString, jsonFilename, targetLocaleMessage, usedLocaleMessageKeys)
}
}
} else {
debug(`ignore ${filename} in no-unused-keys`)
return {}
}
}

Expand Down
Loading