Skip to content

Commit d35001d

Browse files
committed
⭐ new(rule): add no-missing-key rule
1 parent 4737d33 commit d35001d

File tree

8 files changed

+263
-26
lines changed

8 files changed

+263
-26
lines changed

Diff for: lib/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @fileoverview ESLint plugin for vue-i18n
3-
* @author kazuya kawaguchi
3+
* @author kazuya kawaguchi (a.k.a. kazupon)
44
*/
55
'use strict'
66

@@ -14,7 +14,7 @@
1414

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

2020
// import processors

Diff for: lib/rules/no-missing-key.js

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @author kazuya kawaguchi (a.k.a. kazupon)
3+
*/
4+
'use strict'
5+
6+
const {
7+
defineTemplateBodyVisitor,
8+
loadLocaleMessages,
9+
findMissingsFromLocaleMessages
10+
} = require('../utils/index')
11+
12+
let localeMessages = null // cache
13+
14+
function create (context) {
15+
const { settings } = context
16+
const localeDir = settings['vue-i18n'].localeDir
17+
localeMessages = localeMessages || loadLocaleMessages(localeDir)
18+
19+
return defineTemplateBodyVisitor(context, {
20+
"VAttribute[directive=true][key.name='t']" (node) {
21+
checkDirective(context, localeDir, localeMessages, node)
22+
},
23+
24+
"VAttribute[directive=true][key.name.name='t']" (node) {
25+
checkDirective(context, localeDir, localeMessages, node)
26+
},
27+
28+
CallExpression (node) {
29+
checkCallExpression(context, localeDir, localeMessages, node)
30+
}
31+
}, {
32+
CallExpression (node) {
33+
checkCallExpression(context, localeDir, localeMessages, node)
34+
}
35+
})
36+
}
37+
38+
function checkDirective (context, localeDir, localeMessages, node) {
39+
if ((node.value && node.value.type === 'VExpressionContainer') &&
40+
(node.value.expression && node.value.expression.type === 'Literal')) {
41+
const key = node.value.expression.value
42+
if (!key) {
43+
// TODO: should be error
44+
return
45+
}
46+
const missings = findMissingsFromLocaleMessages(localeMessages, key, localeDir)
47+
if (missings.length) {
48+
missings.forEach(missing => context.report({ node, ...missing }))
49+
}
50+
}
51+
}
52+
53+
function checkCallExpression (context, localeDir, localeMessages, node) {
54+
const funcName = (node.callee.type === 'MemberExpression' && node.callee.property.name) || node.callee.name
55+
56+
if (!/^(\$t|t|\$tc|tc)$/.test(funcName) || !node.arguments || !node.arguments.length) {
57+
return
58+
}
59+
60+
const [keyNode] = node.arguments
61+
if (keyNode.type !== 'Literal') { return }
62+
63+
const key = keyNode.value
64+
if (!key) {
65+
// TODO: should be error
66+
return
67+
}
68+
69+
const missings = findMissingsFromLocaleMessages(localeMessages, key, localeDir)
70+
if (missings.length) {
71+
missings.forEach(missing => context.report({ node, ...missing }))
72+
}
73+
}
74+
75+
module.exports = {
76+
meta: {
77+
type: 'problem',
78+
docs: {
79+
description: 'disallow missing locale message key at localization methods',
80+
category: 'Possible Errors',
81+
recommended: true
82+
},
83+
fixable: null,
84+
schema: []
85+
},
86+
create
87+
}

Diff for: lib/rules/no-missing.js

-12
This file was deleted.

Diff for: lib/utils/index.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @author kazuya kawaguchi (a.k.a. kazupon)
3+
*/
4+
'use strict'
5+
6+
const glob = require('glob')
7+
const { resolve } = require('path')
8+
9+
function defineTemplateBodyVisitor (context, templateBodyVisitor, scriptVisitor) {
10+
if (context.parserServices.defineTemplateBodyVisitor === null) {
11+
context.report({
12+
loc: { line: 1, column: 0 },
13+
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'
14+
})
15+
return {}
16+
}
17+
return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
18+
}
19+
20+
function loadLocaleMessages (pattern) {
21+
const files = glob.sync(pattern)
22+
return files.map(file => {
23+
const path = resolve(process.cwd(), file)
24+
const filename = file.replace(/^.*(\\|\/|:)/, '')
25+
const messages = require(path)
26+
return { path: file, filename, messages }
27+
})
28+
}
29+
30+
function findMissingsFromLocaleMessages (localeMessages, key) {
31+
const missings = []
32+
const paths = key.split('.')
33+
localeMessages.forEach(localeMessage => {
34+
const length = paths.length
35+
let last = localeMessage.messages
36+
let i = 0
37+
while (i < length) {
38+
const value = last[paths[i]]
39+
if (value === undefined) {
40+
missings.push({
41+
message: `'${key}' does not exist in '${localeMessage.path}'`
42+
})
43+
}
44+
last = value
45+
i++
46+
}
47+
})
48+
return missings
49+
}
50+
51+
module.exports = {
52+
defineTemplateBodyVisitor,
53+
loadLocaleMessages,
54+
findMissingsFromLocaleMessages
55+
}

Diff for: tests/fixtures/locales/en.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"hello": "hello world",
3+
"messages": {
4+
"hello": "hi DIO!",
5+
"link": "@:message.hello",
6+
"nested": {
7+
"hello": "hi jojo!"
8+
}
9+
},
10+
"hello_dio": "hello underscore DIO!",
11+
"hello {name}": "hello {name}!",
12+
"hello-dio": "hello hyphen DIO!",
13+
"foo.bar.buz": "hi flat key!"
14+
}

Diff for: tests/fixtures/locales/ja.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"hello": "ハローワールド",
3+
"messages": {
4+
"hello": "こんにちは、DIO!",
5+
"link": "@:message.hello",
6+
"nested": {
7+
"hello": "こんにちは、ジョジョ!"
8+
}
9+
},
10+
"hello_dio": "こんにちは、アンダースコア DIO!",
11+
"hello {name}": "こんにちは、{name}!",
12+
"hello-dio": "こんにちは、ハイフン DIO!",
13+
"foo.bar.buz": "こんにちは、フラットなキー!"
14+
}

Diff for: tests/lib/rules/no-missing-key.js

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* @author kazuya kawaguchi (a.k.a. kazupon)
3+
*/
4+
'use strict'
5+
6+
const RuleTester = require('eslint').RuleTester
7+
const rule = require('../../../lib/rules/no-missing-key')
8+
9+
const baseDir = './tests/fixtures/locales'
10+
const resolve = file => `${baseDir}/${file}`
11+
12+
const settings = {
13+
'vue-i18n': {
14+
localeDir: `${baseDir}/*.json`
15+
}
16+
}
17+
18+
const tester = new RuleTester({
19+
parser: 'vue-eslint-parser',
20+
parserOptions: { ecmaVersion: 2015 }
21+
})
22+
23+
tester.run('no-missing-key', rule, {
24+
valid: [{
25+
// basic key
26+
settings,
27+
code: `$t('hello')`
28+
}, {
29+
// nested key
30+
settings,
31+
code: `t('messages.nested.hello')`
32+
}, {
33+
// linked key
34+
settings,
35+
code: `$tc('messages.hello.link')`
36+
}, {
37+
// hypened key
38+
settings,
39+
code: `tc('hello-dio')`
40+
}, {
41+
// key like the message
42+
settings,
43+
code: `$t('hello {name}')`
44+
}, {
45+
// Identifier
46+
settings,
47+
code: `$t(key)`
48+
}, {
49+
// using mustaches in template block
50+
settings,
51+
code: `<template>
52+
<p>{{ $t('hello') }}</p>
53+
</template>`
54+
}, {
55+
// using custom directive in template block
56+
settings,
57+
code: `<template>
58+
<p v-t="'hello'"></p>
59+
</template>`
60+
}],
61+
62+
invalid: [{
63+
// basic
64+
settings,
65+
code: `$t('missing')`,
66+
errors: [
67+
`'missing' does not exist in '${resolve('en.json')}'`,
68+
`'missing' does not exist in '${resolve('ja.json')}'`
69+
]
70+
}, {
71+
// using mustaches in template block
72+
settings,
73+
code: `<template>
74+
<p>{{ $t('missing') }}</p>
75+
</template>`,
76+
errors: [
77+
`'missing' does not exist in '${resolve('en.json')}'`,
78+
`'missing' does not exist in '${resolve('ja.json')}'`
79+
]
80+
}, {
81+
// using custom directive in template block
82+
settings,
83+
code: `<template>
84+
<p v-t="'missing'"></p>
85+
</template>`,
86+
errors: [
87+
`'missing' does not exist in '${resolve('en.json')}'`,
88+
`'missing' does not exist in '${resolve('ja.json')}'`
89+
]
90+
}]
91+
})

Diff for: tests/lib/rules/no-missing.js

-12
This file was deleted.

0 commit comments

Comments
 (0)