Skip to content

Commit

Permalink
⭐ new(rule): add no-missing-key rule
Browse files Browse the repository at this point in the history
  • Loading branch information
kazupon committed Mar 8, 2019
1 parent 4737d33 commit d35001d
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 26 deletions.
4 changes: 2 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @fileoverview ESLint plugin for vue-i18n
* @author kazuya kawaguchi
* @author kazuya kawaguchi (a.k.a. kazupon)
*/
'use strict'

Expand All @@ -14,7 +14,7 @@

// import all rules in lib/rules
module.exports.rules = {
// add your processors here
'no-missing-key': require('./rules/no-missing-key')
}

// import processors
Expand Down
87 changes: 87 additions & 0 deletions lib/rules/no-missing-key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* @author kazuya kawaguchi (a.k.a. kazupon)
*/
'use strict'

const {
defineTemplateBodyVisitor,
loadLocaleMessages,
findMissingsFromLocaleMessages
} = require('../utils/index')

let localeMessages = null // cache

function create (context) {
const { settings } = context
const localeDir = settings['vue-i18n'].localeDir
localeMessages = localeMessages || loadLocaleMessages(localeDir)

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

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

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

function checkDirective (context, localeDir, localeMessages, node) {
if ((node.value && node.value.type === 'VExpressionContainer') &&
(node.value.expression && node.value.expression.type === 'Literal')) {
const key = node.value.expression.value
if (!key) {
// TODO: should be error
return
}
const missings = findMissingsFromLocaleMessages(localeMessages, key, localeDir)
if (missings.length) {
missings.forEach(missing => context.report({ node, ...missing }))
}
}
}

function checkCallExpression (context, localeDir, localeMessages, 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 [keyNode] = node.arguments
if (keyNode.type !== 'Literal') { return }

const key = keyNode.value
if (!key) {
// TODO: should be error
return
}

const missings = findMissingsFromLocaleMessages(localeMessages, key, localeDir)
if (missings.length) {
missings.forEach(missing => context.report({ node, ...missing }))
}
}

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow missing locale message key at localization methods',
category: 'Possible Errors',
recommended: true
},
fixable: null,
schema: []
},
create
}
12 changes: 0 additions & 12 deletions lib/rules/no-missing.js

This file was deleted.

55 changes: 55 additions & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @author kazuya kawaguchi (a.k.a. kazupon)
*/
'use strict'

const glob = require('glob')
const { resolve } = require('path')

function defineTemplateBodyVisitor (context, templateBodyVisitor, scriptVisitor) {
if (context.parserServices.defineTemplateBodyVisitor === null) {
context.report({
loc: { line: 1, column: 0 },
message: 'Use the latest vue-eslint-parser. See also https://github.com/vuejs/eslint-plugin-vue#what-is-the-use-the-latest-vue-eslint-parser-error'
})
return {}
}
return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
}

function loadLocaleMessages (pattern) {
const files = glob.sync(pattern)
return files.map(file => {
const path = resolve(process.cwd(), file)
const filename = file.replace(/^.*(\\|\/|:)/, '')
const messages = require(path)
return { path: file, filename, messages }
})
}

function findMissingsFromLocaleMessages (localeMessages, key) {
const missings = []
const paths = key.split('.')
localeMessages.forEach(localeMessage => {
const length = paths.length
let last = localeMessage.messages
let i = 0
while (i < length) {
const value = last[paths[i]]
if (value === undefined) {
missings.push({
message: `'${key}' does not exist in '${localeMessage.path}'`
})
}
last = value
i++
}
})
return missings
}

module.exports = {
defineTemplateBodyVisitor,
loadLocaleMessages,
findMissingsFromLocaleMessages
}
14 changes: 14 additions & 0 deletions tests/fixtures/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hello": "hello world",
"messages": {
"hello": "hi DIO!",
"link": "@:message.hello",
"nested": {
"hello": "hi jojo!"
}
},
"hello_dio": "hello underscore DIO!",
"hello {name}": "hello {name}!",
"hello-dio": "hello hyphen DIO!",
"foo.bar.buz": "hi flat key!"
}
14 changes: 14 additions & 0 deletions tests/fixtures/locales/ja.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hello": "ハローワールド",
"messages": {
"hello": "こんにちは、DIO!",
"link": "@:message.hello",
"nested": {
"hello": "こんにちは、ジョジョ!"
}
},
"hello_dio": "こんにちは、アンダースコア DIO!",
"hello {name}": "こんにちは、{name}!",
"hello-dio": "こんにちは、ハイフン DIO!",
"foo.bar.buz": "こんにちは、フラットなキー!"
}
91 changes: 91 additions & 0 deletions tests/lib/rules/no-missing-key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* @author kazuya kawaguchi (a.k.a. kazupon)
*/
'use strict'

const RuleTester = require('eslint').RuleTester
const rule = require('../../../lib/rules/no-missing-key')

const baseDir = './tests/fixtures/locales'
const resolve = file => `${baseDir}/${file}`

const settings = {
'vue-i18n': {
localeDir: `${baseDir}/*.json`
}
}

const tester = new RuleTester({
parser: 'vue-eslint-parser',
parserOptions: { ecmaVersion: 2015 }
})

tester.run('no-missing-key', rule, {
valid: [{
// basic key
settings,
code: `$t('hello')`
}, {
// nested key
settings,
code: `t('messages.nested.hello')`
}, {
// linked key
settings,
code: `$tc('messages.hello.link')`
}, {
// hypened key
settings,
code: `tc('hello-dio')`
}, {
// key like the message
settings,
code: `$t('hello {name}')`
}, {
// Identifier
settings,
code: `$t(key)`
}, {
// using mustaches in template block
settings,
code: `<template>
<p>{{ $t('hello') }}</p>
</template>`
}, {
// using custom directive in template block
settings,
code: `<template>
<p v-t="'hello'"></p>
</template>`
}],

invalid: [{
// basic
settings,
code: `$t('missing')`,
errors: [
`'missing' does not exist in '${resolve('en.json')}'`,
`'missing' does not exist in '${resolve('ja.json')}'`
]
}, {
// using mustaches in template block
settings,
code: `<template>
<p>{{ $t('missing') }}</p>
</template>`,
errors: [
`'missing' does not exist in '${resolve('en.json')}'`,
`'missing' does not exist in '${resolve('ja.json')}'`
]
}, {
// using custom directive in template block
settings,
code: `<template>
<p v-t="'missing'"></p>
</template>`,
errors: [
`'missing' does not exist in '${resolve('en.json')}'`,
`'missing' does not exist in '${resolve('ja.json')}'`
]
}]
})
12 changes: 0 additions & 12 deletions tests/lib/rules/no-missing.js

This file was deleted.

0 comments on commit d35001d

Please sign in to comment.