Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 21017fd

Browse files
authoredJul 17, 2020
Change "settings['vue-i18n'].localeDir" to can specify object options. (#75)
1 parent cab4c01 commit 21017fd

File tree

31 files changed

+484
-104
lines changed

31 files changed

+484
-104
lines changed
 

‎docs/started.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,28 @@ module.export = {
3535
settings: {
3636
'vue-i18n': {
3737
localeDir: './path/to/locales/*.json' // extention is glob formatting!
38+
// or
39+
// localeDir: {
40+
// pattern: './path/to/locales/*.json', // extention is glob formatting!
41+
// localeKey: 'file' // or 'key'
42+
// }
3843
}
3944
}
4045
}
4146
```
4247

4348
See [the rule list](../rules/)
4449

50+
### `settings['vue-i18n']`
51+
52+
- `localeDir` ... You can specify a string or an object.
53+
- String option ... A glob for specifying files that store localization messages of project.
54+
- Object option
55+
- `pattern` (`string`) ... A glob for specifying files that store localization messages of project.
56+
- `localeKey` (`'file' | 'key'`) ... Specifies how to determine the locale for localization messages.
57+
- `'file'` ... Determine the locale name from the filename. The resource file should only contain messages for that locale. Use this option if you use `vue-cli-plugin-i18n`. This option is also used when String option is specified.
58+
- `'key'` ... Determine the locale name from the root key name of the file contents. The value of that key should only contain messages for that locale. Used when the resource file is in the format given to the `messages` option of the `VueI18n` constructor option.
59+
4560
### Running ESLint from command line
4661

4762
If you want to run `eslint` from command line, make sure you include the `.vue` and `.json` extension using [the `--ext` option](https://eslint.org/docs/user-guide/configuring#specifying-file-extensions-to-lint) or a glob pattern because ESLint targets only `.js` files by default.

‎lib/rules/no-html-messages.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const { extname } = require('path')
77
const parse5 = require('parse5')
88
const {
99
UNEXPECTED_ERROR_LOCATION,
10-
findExistLocaleMessage,
1110
getLocaleMessages,
1211
extractJsonInfo,
1312
generateJsonAst
@@ -55,7 +54,7 @@ function create (context) {
5554
}
5655

5756
const localeMessages = getLocaleMessages(settings['vue-i18n'].localeDir)
58-
const targetLocaleMessage = findExistLocaleMessage(filename, localeMessages)
57+
const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename)
5958
if (!targetLocaleMessage) {
6059
debug(`ignore ${filename} in no-html-messages`)
6160
return {}

‎lib/rules/no-missing-keys.js

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
const {
77
UNEXPECTED_ERROR_LOCATION,
88
defineTemplateBodyVisitor,
9-
getLocaleMessages,
10-
findMissingsFromLocaleMessages
9+
getLocaleMessages
1110
} = require('../utils/index')
1211

1312
function create (context) {
@@ -25,61 +24,61 @@ function create (context) {
2524

2625
return defineTemplateBodyVisitor(context, {
2726
"VAttribute[directive=true][key.name='t']" (node) {
28-
checkDirective(context, localeDir, localeMessages, node)
27+
checkDirective(context, localeMessages, node)
2928
},
3029

3130
"VAttribute[directive=true][key.name.name='t']" (node) {
32-
checkDirective(context, localeDir, localeMessages, node)
31+
checkDirective(context, localeMessages, node)
3332
},
3433

3534
"VElement[name=i18n] > VStartTag > VAttribute[key.name='path']" (node) {
36-
checkComponent(context, localeDir, localeMessages, node)
35+
checkComponent(context, localeMessages, node)
3736
},
3837

3938
"VElement[name=i18n] > VStartTag > VAttribute[key.name.name='path']" (node) {
40-
checkComponent(context, localeDir, localeMessages, node)
39+
checkComponent(context, localeMessages, node)
4140
},
4241

4342
CallExpression (node) {
44-
checkCallExpression(context, localeDir, localeMessages, node)
43+
checkCallExpression(context, localeMessages, node)
4544
}
4645
}, {
4746
CallExpression (node) {
48-
checkCallExpression(context, localeDir, localeMessages, node)
47+
checkCallExpression(context, localeMessages, node)
4948
}
5049
})
5150
}
5251

53-
function checkDirective (context, localeDir, localeMessages, node) {
52+
function checkDirective (context, localeMessages, node) {
5453
if ((node.value && node.value.type === 'VExpressionContainer') &&
5554
(node.value.expression && node.value.expression.type === 'Literal')) {
5655
const key = node.value.expression.value
5756
if (!key) {
5857
// TODO: should be error
5958
return
6059
}
61-
const missings = findMissingsFromLocaleMessages(localeMessages, key, localeDir)
60+
const missings = localeMessages.findMissingPaths(key)
6261
if (missings.length) {
63-
missings.forEach(missing => context.report({ node, ...missing }))
62+
missings.forEach((data) => context.report({ node, messageId: 'missing', data }))
6463
}
6564
}
6665
}
6766

68-
function checkComponent (context, localeDir, localeMessages, node) {
67+
function checkComponent (context, localeMessages, node) {
6968
if (node.value && node.value.type === 'VLiteral') {
7069
const key = node.value.value
7170
if (!key) {
7271
// TODO: should be error
7372
return
7473
}
75-
const missings = findMissingsFromLocaleMessages(localeMessages, key, localeDir)
74+
const missings = localeMessages.findMissingPaths(key)
7675
if (missings.length) {
77-
missings.forEach(missing => context.report({ node, ...missing }))
76+
missings.forEach((data) => context.report({ node, messageId: 'missing', data }))
7877
}
7978
}
8079
}
8180

82-
function checkCallExpression (context, localeDir, localeMessages, node) {
81+
function checkCallExpression (context, localeMessages, node) {
8382
const funcName = (node.callee.type === 'MemberExpression' && node.callee.property.name) || node.callee.name
8483

8584
if (!/^(\$t|t|\$tc|tc)$/.test(funcName) || !node.arguments || !node.arguments.length) {
@@ -95,9 +94,9 @@ function checkCallExpression (context, localeDir, localeMessages, node) {
9594
return
9695
}
9796

98-
const missings = findMissingsFromLocaleMessages(localeMessages, key, localeDir)
97+
const missings = localeMessages.findMissingPaths(key)
9998
if (missings.length) {
100-
missings.forEach(missing => context.report({ node, ...missing }))
99+
missings.forEach((data) => context.report({ node, messageId: 'missing', data }))
101100
}
102101
}
103102

@@ -110,7 +109,10 @@ module.exports = {
110109
recommended: true
111110
},
112111
fixable: null,
113-
schema: []
112+
schema: [],
113+
messages: {
114+
missing: "'{{path}}' does not exist in '{{locale}}'"
115+
}
114116
},
115117
create
116118
}

‎lib/rules/no-unused-keys.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,38 @@ const flatten = require('flat')
99
const collectKeys = require('../utils/collect-keys')
1010
const {
1111
UNEXPECTED_ERROR_LOCATION,
12-
findExistLocaleMessage,
1312
getLocaleMessages,
1413
extractJsonInfo,
1514
generateJsonAst
1615
} = require('../utils/index')
1716
const debug = require('debug')('eslint-plugin-vue-i18n:no-unused-keys')
1817

18+
/**
19+
* @typedef {import('../utils/locale-messages').LocaleMessage} LocaleMessage
20+
*/
21+
1922
let usedLocaleMessageKeys = null // used locale message keys
2023

21-
function getUnusedKeys (context, json, usedkeys) {
24+
/**
25+
* @param {RuleContext} context
26+
* @param {LocaleMessage} targetLocaleMessage
27+
* @param {string} json
28+
* @param {object} usedkeys
29+
*/
30+
function getUnusedKeys (context, targetLocaleMessage, json, usedkeys) {
2231
let unusedKeys = []
2332

2433
try {
34+
let compareKeys = { ...usedkeys }
35+
if (targetLocaleMessage.localeKey === 'key') {
36+
compareKeys = targetLocaleMessage.locales.reduce((keys, locale) => {
37+
keys[locale] = usedkeys
38+
return keys
39+
}, {})
40+
}
2541
const jsonValue = JSON.parse(json)
2642
const diffValue = jsonDiffPatch.diff(
27-
flatten(usedkeys, { safe: true }),
43+
flatten(compareKeys, { safe: true }),
2844
flatten(jsonValue, { safe: true })
2945
)
3046
const diffLocaleMessage = flatten(diffValue, { safe: true })
@@ -89,7 +105,7 @@ function create (context) {
89105
}
90106

91107
const localeMessages = getLocaleMessages(settings['vue-i18n'].localeDir)
92-
const targetLocaleMessage = findExistLocaleMessage(filename, localeMessages)
108+
const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename)
93109
if (!targetLocaleMessage) {
94110
debug(`ignore ${filename} in no-unused-keys`)
95111
return {}
@@ -111,7 +127,7 @@ function create (context) {
111127
const ast = generateJsonAst(context, jsonString, jsonFilename)
112128
if (!ast) { return }
113129

114-
const unusedKeys = getUnusedKeys(context, jsonString, usedLocaleMessageKeys)
130+
const unusedKeys = getUnusedKeys(context, targetLocaleMessage, jsonString, usedLocaleMessageKeys)
115131
if (!unusedKeys) { return }
116132

117133
traverseJsonAstWithUnusedKeys(unusedKeys, ast, (fullpath, node) => {

‎lib/utils/index.js

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,31 @@
77
const glob = require('glob')
88
const { resolve } = require('path')
99
const jsonAstParse = require('json-to-ast')
10+
const { LocaleMessage, LocaleMessages } = require('./locale-messages')
1011

1112
const UNEXPECTED_ERROR_LOCATION = { line: 1, column: 0 }
1213

14+
/**
15+
* How to determine the locale for localization messages.
16+
* - `'file'` ... Determine the locale name from the filename. The resource file should only contain messages for that locale.
17+
* Use this option if you use `vue-cli-plugin-i18n`. This method is also used when String option is specified.
18+
* - `'key'` ... Determine the locale name from the root key name of the file contents. The value of that key should only contain messages for that locale.
19+
* Used when the resource file is in the format given to the `messages` option of the `VueI18n` constructor option.
20+
* @typedef {'file' | 'key'} LocaleKeyType
21+
*/
22+
/**
23+
* Type of `settings['vue-i18n'].localeDir`
24+
* @typedef {SettingsVueI18nLocaleDirGlob | SettingsVueI18nLocaleDirObject} SettingsVueI18nLocaleDir
25+
* @typedef {string} SettingsVueI18nLocaleDirGlob A glob for specifying files that store localization messages of project.
26+
* @typedef {object} SettingsVueI18nLocaleDirObject Specifies a glob and messages format type.
27+
* @property {string} pattern A glob for specifying files that store localization messages of project.
28+
* @property {LocaleKeyType} localeKey Specifies how to determine the locale for localization messages.
29+
*/
30+
/**
31+
* @typedef {import('./locale-messages').LocaleMessage} LocaleMessage
32+
* @typedef {import('./locale-messages').LocaleMessages} LocaleMessages
33+
*/
34+
1335
/**
1436
* Register the given visitor to parser services.
1537
* Borrow from GitHub `vuejs/eslint-plugin-vue` repo
@@ -26,23 +48,29 @@ function defineTemplateBodyVisitor (context, templateBodyVisitor, scriptVisitor)
2648
return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
2749
}
2850

29-
function findExistLocaleMessage (fullpath, localeMessages) {
30-
return localeMessages.find(message => message.fullpath === fullpath)
31-
}
32-
33-
function loadLocaleMessages (pattern) {
34-
const files = glob.sync(pattern)
35-
return files.map(file => {
36-
const path = resolve(process.cwd(), file)
37-
const filename = file.replace(/^.*(\\|\/|:)/, '')
38-
const messages = require(path)
39-
return { fullpath: path, path: file, filename, messages }
40-
})
51+
/**
52+
* @param {SettingsVueI18nLocaleDir} localeDir
53+
* @returns {LocaleMessages}
54+
*/
55+
function loadLocaleMessages (localeDir) {
56+
if (typeof localeDir === 'string') {
57+
return loadLocaleMessages({ pattern: localeDir, localeKey: 'file' })
58+
} else {
59+
const files = glob.sync(localeDir.pattern)
60+
return new LocaleMessages(files.map(file => {
61+
const fullpath = resolve(process.cwd(), file)
62+
return new LocaleMessage({ fullpath, path: file, localeKey: localeDir.localeKey || 'file' })
63+
}))
64+
}
4165
}
4266

4367
let localeMessages = null // locale messages
4468
let localeDir = null // locale dir
4569

70+
/**
71+
* @param {SettingsVueI18nLocaleDir} localeDirectory
72+
* @returns {LocaleMessages}
73+
*/
4674
function getLocaleMessages (localeDirectory) {
4775
if (localeDir !== localeDirectory) {
4876
localeDir = localeDirectory
@@ -53,27 +81,6 @@ function getLocaleMessages (localeDirectory) {
5381
return localeMessages
5482
}
5583

56-
function findMissingsFromLocaleMessages (localeMessages, key) {
57-
const missings = []
58-
const paths = key.split('.')
59-
localeMessages.forEach(localeMessage => {
60-
const length = paths.length
61-
let last = localeMessage.messages
62-
let i = 0
63-
while (i < length) {
64-
const value = last && last[paths[i]]
65-
if (value === undefined) {
66-
missings.push({
67-
message: `'${key}' does not exist`
68-
})
69-
}
70-
last = value
71-
i++
72-
}
73-
})
74-
return missings
75-
}
76-
7784
function extractJsonInfo (context, node) {
7885
try {
7986
const [str, filename] = node.comments
@@ -110,8 +117,6 @@ module.exports = {
110117
UNEXPECTED_ERROR_LOCATION,
111118
defineTemplateBodyVisitor,
112119
getLocaleMessages,
113-
findMissingsFromLocaleMessages,
114-
findExistLocaleMessage,
115120
extractJsonInfo,
116121
generateJsonAst
117122
}

‎lib/utils/locale-messages.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* @fileoverview Classes that acquires and manages localization messages
3+
* @author Yosuke Ota
4+
*/
5+
'use strict'
6+
7+
/**
8+
* @typedef {import('./index').LocaleKeyType} LocaleKeyType
9+
* @typedef {LocaleMessage} LocaleMessage
10+
* @typedef {LocaleMessages} LocaleMessages
11+
*/
12+
/**
13+
* The localization message class
14+
*/
15+
class LocaleMessage {
16+
/**
17+
* @param {object} arg
18+
* @param {string} arg.fullpath Absolute path.
19+
* @param {string} arg.path Relative path.
20+
* @param {LocaleKeyType} arg.localeKey Specifies how to determine the locale for localization messages.
21+
*/
22+
constructor ({ fullpath, path, localeKey }) {
23+
this.fullpath = fullpath
24+
this.path = path
25+
/** @type {LocaleKeyType[]} Specifies how to determine the locale for localization messages. */
26+
this.localeKey = localeKey
27+
/** @type {string} The localization messages file name. */
28+
this.file = path.replace(/^.*(\\|\/|:)/, '')
29+
}
30+
31+
/**
32+
* @returns {object} The localization messages object.
33+
*/
34+
get messages () {
35+
return this._messages || (this._messages = require(this.fullpath))
36+
}
37+
/**
38+
* @returns {string[]} Array of locales.
39+
*/
40+
get locales () {
41+
if (this._locales) {
42+
return this._locales
43+
}
44+
if (this.localeKey === 'file') {
45+
// see https://github.com/kazupon/vue-cli-plugin-i18n/blob/e9519235a454db52fdafcd0517ce6607821ef0b4/generator/templates/js/src/i18n.js#L10
46+
const matched = this.file.match(/([A-Za-z0-9-_]+)\./i)
47+
return (this._locales = [matched && matched[1] || this.file])
48+
} else if (this.localeKey === 'key') {
49+
return (this._locales = Object.keys(this.messages))
50+
}
51+
return (this._locales = [])
52+
}
53+
54+
findMissingPath (locale, key) {
55+
const paths = key.split('.')
56+
const length = paths.length
57+
let last = this._getMessagesFromLocale(locale)
58+
let i = 0
59+
while (i < length) {
60+
const value = last && last[paths[i]]
61+
if (value == null) {
62+
return paths.slice(0, i + 1).join('.')
63+
}
64+
last = value
65+
i++
66+
}
67+
return null
68+
}
69+
70+
/**
71+
* Check if the message with the given key exists.
72+
* @param {string} locale The locale name
73+
* @param {string} key The given key to check
74+
* @returns {boolean}
75+
*/
76+
hasMessage (locale, key) {
77+
return this.getMessage(locale, key) != null
78+
}
79+
80+
/**
81+
* Gets the message for the given key.
82+
* @param {string} locale The locale name
83+
* @param {string} key The given key
84+
* @returns {any} The message for the given key. `null` if the message is missing.
85+
*/
86+
getMessage (locale, key) {
87+
const paths = key.split('.')
88+
const length = paths.length
89+
let last = this._getMessagesFromLocale(locale)
90+
let i = 0
91+
while (i < length) {
92+
const value = last && last[paths[i]]
93+
if (value == null) {
94+
return null
95+
}
96+
last = value
97+
i++
98+
}
99+
return last
100+
}
101+
102+
_getMessagesFromLocale (locale) {
103+
if (this.localeKey === 'file') {
104+
return this.messages
105+
}
106+
if (this.localeKey === 'key') {
107+
return this.messages[locale]
108+
}
109+
}
110+
}
111+
112+
/**
113+
* The localization messages class
114+
*/
115+
class LocaleMessages {
116+
/**
117+
* @param {LocaleMessage[]} localeMessages
118+
*/
119+
constructor (localeMessages) {
120+
this.localeMessages = localeMessages
121+
}
122+
123+
/**
124+
* Finds and gets the localization message for the given fullpath.
125+
* @param {string} fullpath
126+
* @returns {LocaleMessage}
127+
*/
128+
findExistLocaleMessage (fullpath) {
129+
return this.localeMessages.find(message => message.fullpath === fullpath)
130+
}
131+
132+
/**
133+
* Finds the paths that does not exist in the localization message resources.
134+
* @param {string} key
135+
*/
136+
findMissingPaths (key) {
137+
const missings = []
138+
this.localeMessages.forEach(localeMessage => {
139+
localeMessage.locales.forEach(locale => {
140+
const missingPath = localeMessage.findMissingPath(locale, key)
141+
if (missingPath) {
142+
missings.push({ path: missingPath, locale })
143+
}
144+
})
145+
})
146+
return missings.sort(({ locale: localeA }, { locale: localeB },) => localeA > localeB ? 1 : localeA < localeB ? -1 : 0)
147+
}
148+
}
149+
150+
module.exports = {
151+
LocaleMessage,
152+
LocaleMessages
153+
}

‎package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"release:prepare": "shipjs prepare",
8585
"release:trigger": "shipjs trigger",
8686
"test": "mocha ./tests/**/*.js",
87+
"test:debug": "mocha --inspect \"./tests/**/*.js\"",
8788
"test:coverage": "nyc mocha ./tests/**/*.js",
8889
"test:integrations": "mocha ./tests-integrations/*.js --timeout 60000"
8990
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"eslint.validate": [
3+
"javascript",
4+
"vue",
5+
"json"
6+
]
7+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"en": {
3+
"hello": "hello world",
4+
"messages": {
5+
"hello": "hi DIO!",
6+
"link": "@:message.hello",
7+
"nested": {
8+
"hello": "hi jojo!"
9+
}
10+
},
11+
"hello_dio": "hello underscore DIO!",
12+
"hello {name}": "hello {name}!",
13+
"hello-dio": "hello hyphen DIO!"
14+
},
15+
"ja": {
16+
"hello": "ハローワールド",
17+
"messages": {
18+
"hello": "こんにちは、DIO!",
19+
"link": "@:message.hello",
20+
"nested": {
21+
"hello": "こんにちは、ジョジョ!"
22+
}
23+
},
24+
"hello_dio": "こんにちは、アンダースコア DIO!",
25+
"hello {name}": "こんにちは、{name}!",
26+
"hello-dio": "こんにちは、ハイフン DIO!"
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"en": {
3+
"hello": "hello world",
4+
"messages": {
5+
"hello": "hi DIO!",
6+
"link": "@:message.hello",
7+
"nested": {
8+
"hello": "hi jojo!"
9+
}
10+
},
11+
"hello_dio": "hello underscore DIO!",
12+
"hello {name}": "hello {name}!",
13+
"hello-dio": "hello hyphen DIO!"
14+
},
15+
"ja": {
16+
"hello": "ハローワールド",
17+
"messages": {
18+
"hello": "こんにちは、DIO!",
19+
"link": "@:message.hello",
20+
"nested": {
21+
"hello": "こんにちは、ジョジョ!"
22+
}
23+
},
24+
"hello_dio": "こんにちは、アンダースコア DIO!",
25+
"hello {name}": "こんにちは、{name}!",
26+
"hello-dio": "こんにちは、ハイフン DIO!"
27+
}
28+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<template>
2+
<div id="app">
3+
<p v-t="'hello_dio'">{{ $t('messages.hello') }}</p>
4+
</div>
5+
</template>
6+
7+
<script>
8+
export default {
9+
name: 'App',
10+
created () {
11+
this.$i18n.t('hello {name}', { name: 'DIO' })
12+
}
13+
}
14+
</script>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const $t = () => {}
2+
$t('hello')
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"en": {
3+
"hello": "hello world",
4+
"messages": {
5+
"hello": "hi DIO!"
6+
},
7+
"hello_dio": "hello underscore DIO!",
8+
"hello {name}": "hello {name}!"
9+
},
10+
"ja": {
11+
"hello": "ハローワールド",
12+
"messages": {
13+
"hello": "こんにちは、DIO!"
14+
},
15+
"hello_dio": "こんにちは、アンダースコア DIO!",
16+
"hello {name}": "こんにちは、{name}!"
17+
}
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<template>
2+
<div id="app">
3+
<p v-t="'hello_dio'">{{ $t('messages.hello') }}</p>
4+
</div>
5+
</template>
6+
7+
<script>
8+
export default {
9+
name: 'App',
10+
created () {
11+
this.$i18n.t('hello {name}', { name: 'DIO' })
12+
}
13+
}
14+
</script>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const $t = () => {}
2+
$t('hello')
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<template>
2+
<div id="app">
3+
<p v-t="'hello_dio'">{{ $t('messages.hello') }}</p>
4+
</div>
5+
</template>
6+
7+
<script>
8+
export default {
9+
name: 'App',
10+
created () {
11+
this.$i18n.t('hello {name}', { name: 'DIO' })
12+
}
13+
}
14+
</script>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const $t = () => {}
2+
$t('hello')

‎tests/lib/rules/no-missing-keys.js

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,21 @@
66
const RuleTester = require('eslint').RuleTester
77
const rule = require('../../../lib/rules/no-missing-keys')
88

9-
const baseDir = './tests/fixtures/no-missing-keys/locales'
9+
const localeDirs = [
10+
'./tests/fixtures/no-missing-keys/vue-cli-format/locales/*.json',
11+
{ pattern: './tests/fixtures/no-missing-keys/constructor-option-format/locales/*.json', localeKey: 'key' }
12+
]
1013

11-
const settings = {
12-
'vue-i18n': {
13-
localeDir: `${baseDir}/*.json`
14+
function buildTestsForLocales (testcases) {
15+
const result = []
16+
for (const testcase of testcases) {
17+
for (const localeDir of localeDirs) {
18+
result.push({ ...testcase, settings: {
19+
'vue-i18n': { localeDir }
20+
}})
21+
}
1422
}
23+
return result
1524
}
1625

1726
const tester = new RuleTester({
@@ -20,103 +29,94 @@ const tester = new RuleTester({
2029
})
2130

2231
tester.run('no-missing-keys', rule, {
23-
valid: [{
32+
valid: buildTestsForLocales([{
2433
// basic key
25-
settings,
2634
code: `$t('hello')`
2735
}, {
2836
// nested key
29-
settings,
3037
code: `t('messages.nested.hello')`
3138
}, {
3239
// linked key
33-
settings,
3440
code: `$tc('messages.hello.link')`
3541
}, {
3642
// hypened key
37-
settings,
3843
code: `tc('hello-dio')`
3944
}, {
4045
// key like the message
41-
settings,
4246
code: `$t('hello {name}')`
4347
}, {
4448
// instance member
45-
settings,
4649
code: `i18n.t('hello {name}')`
4750
}, {
4851
// identifier
49-
settings,
5052
code: `$t(key)`
5153
}, {
5254
// using mustaches in template block
53-
settings,
5455
code: `<template>
5556
<p>{{ $t('hello') }}</p>
5657
</template>`
5758
}, {
5859
// using custom directive in template block
59-
settings,
6060
code: `<template>
6161
<p v-t="'hello'"></p>
6262
</template>`
63-
}],
63+
}]),
6464

65-
invalid: [{
65+
invalid: [...buildTestsForLocales([{
6666
// basic
67-
settings,
6867
code: `$t('missing')`,
6968
errors: [
70-
`'missing' does not exist`,
71-
`'missing' does not exist`
69+
`'missing' does not exist in 'en'`,
70+
`'missing' does not exist in 'ja'`
7271
]
7372
}, {
7473
// using mustaches in template block
75-
settings,
7674
code: `<template>
7775
<p>{{ $t('missing') }}</p>
7876
</template>`,
7977
errors: [
80-
`'missing' does not exist`,
81-
`'missing' does not exist`
78+
`'missing' does not exist in 'en'`,
79+
`'missing' does not exist in 'ja'`
8280
]
8381
}, {
8482
// using custom directive in template block
85-
settings,
8683
code: `<template>
8784
<p v-t="'missing'"></p>
8885
</template>`,
8986
errors: [
90-
`'missing' does not exist`,
91-
`'missing' does not exist`
87+
`'missing' does not exist in 'en'`,
88+
`'missing' does not exist in 'ja'`
9289
]
9390
}, {
9491
// using <i18n> functional component in template block
95-
settings,
9692
code: `<template>
9793
<div id="app">
9894
<i18n path="missing"/>
9995
</div>
10096
</template>`,
10197
errors: [
102-
`'missing' does not exist`,
103-
`'missing' does not exist`
98+
`'missing' does not exist in 'en'`,
99+
`'missing' does not exist in 'ja'`
104100
]
105101
}, {
106-
// settings.vue-i18n.localeDir' error
107-
code: `$t('missing')`,
102+
// nested basic
103+
code: `$t('missing.path')`,
108104
errors: [
109-
`You need to set 'localeDir' at 'settings. See the 'eslint-plugin-vue-i18n documentation`
105+
`'missing' does not exist in 'en'`,
106+
`'missing' does not exist in 'ja'`
110107
]
111108
}, {
112-
// nested basic
113-
settings,
114-
code: `$t('missing.path')`,
109+
// nested missing
110+
code: `$t('messages.missing')`,
111+
errors: [
112+
`'messages.missing' does not exist in 'en'`,
113+
`'messages.missing' does not exist in 'ja'`
114+
]
115+
}]), {
116+
// settings.vue-i18n.localeDir' error
117+
code: `$t('missing')`,
115118
errors: [
116-
`'missing.path' does not exist`,
117-
`'missing.path' does not exist`,
118-
`'missing.path' does not exist`,
119-
`'missing.path' does not exist`
119+
`You need to set 'localeDir' at 'settings. See the 'eslint-plugin-vue-i18n documentation`
120120
]
121121
}]
122122
})

‎tests/lib/rules/no-unused-keys.js

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('no-unused-keys', () => {
4545
})
4646

4747
const messages = linter.executeOnFiles(['.'])
48-
assert.equal(messages.errorCount, 4)
48+
assert.equal(messages.errorCount, 6)
4949
messages.results.map(result => {
5050
return result.messages
5151
.filter(message => message.ruleId === '@intlify/vue-i18n/no-unused-keys')
@@ -62,7 +62,33 @@ describe('no-unused-keys', () => {
6262
baseConfig: {
6363
settings: {
6464
'vue-i18n': {
65-
localeDir: `./valid/locales/*.json`
65+
localeDir: `./valid/vue-cli-format/locales/*.json`
66+
}
67+
}
68+
},
69+
parser: require.resolve('vue-eslint-parser'),
70+
parserOptions: {
71+
ecmaVersion: 2015
72+
},
73+
plugins: ['@intlify/vue-i18n'],
74+
rules: {
75+
'@intlify/vue-i18n/no-unused-keys': ['error', {
76+
src: resolve(__dirname, '../../fixtures/no-unused-keys/valid/vue-cli-format')
77+
}]
78+
},
79+
extensions: ['.js', '.vue', '.json']
80+
})
81+
82+
const messages = linter.executeOnFiles(['.'])
83+
assert.equal(messages.errorCount, 0)
84+
})
85+
86+
it('should be not detected unsued keys for constructor-option-format', () => {
87+
const linter = new CLIEngine({
88+
baseConfig: {
89+
settings: {
90+
'vue-i18n': {
91+
localeDir: { pattern: `./valid/constructor-option-format/locales/*.json`, localeKey: 'key' }
6692
}
6793
}
6894
},
@@ -73,7 +99,7 @@ describe('no-unused-keys', () => {
7399
plugins: ['@intlify/vue-i18n'],
74100
rules: {
75101
'@intlify/vue-i18n/no-unused-keys': ['error', {
76-
src: resolve(__dirname, '../../fixtures/no-unused-keys/valid')
102+
src: resolve(__dirname, '../../fixtures/no-unused-keys/valid/constructor-option-format')
77103
}]
78104
},
79105
extensions: ['.js', '.vue', '.json']
@@ -90,7 +116,42 @@ describe('no-unused-keys', () => {
90116
baseConfig: {
91117
settings: {
92118
'vue-i18n': {
93-
localeDir: `./invalid/locales/*.json`
119+
localeDir: `./invalid/vue-cli-format/locales/*.json`
120+
}
121+
}
122+
},
123+
parser: require.resolve('vue-eslint-parser'),
124+
parserOptions: {
125+
ecmaVersion: 2015
126+
},
127+
plugins: ['@intlify/vue-i18n'],
128+
rules: {
129+
'@intlify/vue-i18n/no-unused-keys': 'error'
130+
},
131+
extensions: ['.js', '.vue', '.json']
132+
})
133+
134+
const messages = linter.executeOnFiles(['.'])
135+
assert.equal(messages.errorCount, 6)
136+
137+
function checkRuleId (path) {
138+
const fullPath = resolve(__dirname, path)
139+
const [result] = messages.results
140+
.filter(result => result.filePath === fullPath)
141+
result.messages.forEach(message => {
142+
assert.equal(message.ruleId, '@intlify/vue-i18n/no-unused-keys')
143+
})
144+
}
145+
checkRuleId('../../fixtures/no-unused-keys/invalid/vue-cli-format/locales/en.json')
146+
checkRuleId('../../fixtures/no-unused-keys/invalid/vue-cli-format/locales/ja.json')
147+
})
148+
149+
it('should be detected unsued keys for constructor-option-format', () => {
150+
const linter = new CLIEngine({
151+
baseConfig: {
152+
settings: {
153+
'vue-i18n': {
154+
localeDir: { pattern: `./invalid/constructor-option-format/locales/*.json`, localeKey: 'key' }
94155
}
95156
}
96157
},
@@ -116,8 +177,7 @@ describe('no-unused-keys', () => {
116177
assert.equal(message.ruleId, '@intlify/vue-i18n/no-unused-keys')
117178
})
118179
}
119-
checkRuleId('../../fixtures/no-unused-keys/invalid/locales/en.json')
120-
checkRuleId('../../fixtures/no-unused-keys/invalid/locales/ja.json')
180+
checkRuleId('../../fixtures/no-unused-keys/invalid/constructor-option-format/locales/index.json')
121181
})
122182
})
123183
})

0 commit comments

Comments
 (0)
Please sign in to comment.