Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ module.exports = {
ecmaVersion: 2015
},
rules: {
'object-shorthand': 'error'
}
}
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