Skip to content

Commit 4d28320

Browse files
authored
Add support for <i18n> blocks of SFC. (#80)
* Add support for <i18n> blocks of SFC. * update docs * Use shorthand
1 parent c9356ad commit 4d28320

19 files changed

+732
-201
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ module.exports = {
1515
ecmaVersion: 2015
1616
},
1717
rules: {
18+
'object-shorthand': 'error'
1819
}
1920
}

docs/rules/no-missing-keys.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ You can be detected with this rule the following:
1515
- `$tc`
1616
- `tc`
1717
- `v-t`
18-
- `<i18n>`
18+
- `<i18n>`
1919

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

@@ -92,3 +92,22 @@ const i18n = new VueI18n({
9292
/* ✓ GOOD */
9393
i18n.t('hello')
9494
```
95+
96+
For SFC.
97+
98+
```vue
99+
<i18n>
100+
{
101+
"en": {
102+
"hi": "Hi! DIO!"
103+
}
104+
}
105+
</i18n>
106+
107+
<template>
108+
<div class="app">
109+
<!-- ✓ GOOD -->
110+
<p>{{ $t('hi') }}</p>
111+
</div>
112+
</template>
113+
```

docs/rules/no-unused-keys.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ const i18n = new VueI18n({
4242
i18n.t('hello')
4343
```
4444

45+
For SFC.
46+
47+
```vue
48+
<i18n>
49+
{
50+
"en": {
51+
"hello": "Hello! DIO!",
52+
"hi": "Hi! DIO!" // not used in SFC
53+
}
54+
}
55+
</i18n>
56+
57+
<template>
58+
<div class="app">
59+
<p>{{ $t('hello') }}</p>
60+
</div>
61+
</template>
62+
```
63+
4564
:+1: Examples of **correct** code for this rule:
4665

4766
locale messages:

lib/rules/no-html-messages.js

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
const { extname } = require('path')
77
const parse5 = require('parse5')
88
const {
9-
UNEXPECTED_ERROR_LOCATION,
109
getLocaleMessages,
1110
extractJsonInfo,
1211
generateJsonAst
@@ -39,48 +38,63 @@ function findHTMLNode (node) {
3938

4039
function create (context) {
4140
const filename = context.getFilename()
42-
if (extname(filename) !== '.json') {
43-
debug(`ignore ${filename} in no-html-messages`)
44-
return {}
45-
}
4641

47-
const { settings } = context
48-
if (!settings['vue-i18n'] || !settings['vue-i18n'].localeDir) {
49-
context.report({
50-
loc: UNEXPECTED_ERROR_LOCATION,
51-
message: `You need to 'localeDir' at 'settings. See the 'eslint-plugin-vue-i18n documentation`
52-
})
53-
return {}
54-
}
42+
function verifyJson (jsonString, jsonFilename, offsetLoc = { line: 1, column: 1 }) {
43+
const ast = generateJsonAst(context, jsonString, jsonFilename)
44+
if (!ast) { return }
5545

56-
const localeMessages = getLocaleMessages(settings['vue-i18n'].localeDir)
57-
const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename)
58-
if (!targetLocaleMessage) {
59-
debug(`ignore ${filename} in no-html-messages`)
60-
return {}
46+
traverseNode(ast, messageNode => {
47+
const htmlNode = parse5.parseFragment(messageNode.value, { sourceCodeLocationInfo: true })
48+
const foundNode = findHTMLNode(htmlNode)
49+
if (!foundNode) { return }
50+
const loc = {
51+
line: messageNode.loc.start.line,
52+
column: messageNode.loc.start.column + foundNode.sourceCodeLocation.startOffset
53+
}
54+
if (loc.line === 1) {
55+
loc.line += offsetLoc.line - 1
56+
loc.column += offsetLoc.column - 1
57+
} else {
58+
loc.line += offsetLoc.line - 1
59+
}
60+
context.report({
61+
message: `used HTML localization message`,
62+
loc
63+
})
64+
})
6165
}
6266

63-
return {
64-
Program (node) {
65-
const [jsonString, jsonFilename] = extractJsonInfo(context, node)
66-
if (!jsonString || !jsonFilename) { return }
67-
68-
const ast = generateJsonAst(context, jsonString, jsonFilename)
69-
if (!ast) { return }
67+
if (extname(filename) === '.vue') {
68+
return {
69+
Program (node) {
70+
const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment()
71+
/** @type {VElement[]} */
72+
const i18nBlocks = documentFragment && documentFragment.children.filter(node => node.type === 'VElement' && node.name === 'i18n') || []
7073

71-
traverseNode(ast, messageNode => {
72-
const htmlNode = parse5.parseFragment(messageNode.value, { sourceCodeLocationInfo: true })
73-
const foundNode = findHTMLNode(htmlNode)
74-
if (!foundNode) { return }
75-
context.report({
76-
message: `used HTML localization message`,
77-
loc: {
78-
line: messageNode.loc.start.line,
79-
column: messageNode.loc.start.column + foundNode.sourceCodeLocation.startOffset
74+
for (const block of i18nBlocks) {
75+
if (block.startTag.attributes.some(attr => !attr.directive && attr.key.name === 'src') || !block.endTag) {
76+
continue
8077
}
81-
})
82-
})
78+
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
79+
const tokens = tokenStore.getTokensBetween(block.startTag, block.endTag)
80+
const jsonString = tokens.map(t => t.value).join('')
81+
if (jsonString.trim()) {
82+
verifyJson(jsonString, filename, block.startTag.loc.start)
83+
}
84+
}
85+
}
86+
}
87+
} else if (extname(filename) === '.json' && getLocaleMessages(context).findExistLocaleMessage(filename)) {
88+
return {
89+
Program (node) {
90+
const [jsonString, jsonFilename] = extractJsonInfo(context, node)
91+
if (!jsonString || !jsonFilename) { return }
92+
verifyJson(jsonString, jsonFilename)
93+
}
8394
}
95+
} else {
96+
debug(`ignore ${filename} in no-html-messages`)
97+
return {}
8498
}
8599
}
86100

lib/rules/no-missing-keys.js

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,41 @@
44
'use strict'
55

66
const {
7-
UNEXPECTED_ERROR_LOCATION,
87
defineTemplateBodyVisitor,
98
getLocaleMessages
109
} = require('../utils/index')
1110

1211
function create (context) {
13-
const { settings } = context
14-
if (!settings['vue-i18n'] || !settings['vue-i18n'].localeDir) {
15-
context.report({
16-
loc: UNEXPECTED_ERROR_LOCATION,
17-
message: `You need to set 'localeDir' at 'settings. See the 'eslint-plugin-vue-i18n documentation`
18-
})
19-
return {}
20-
}
21-
22-
const localeDir = settings['vue-i18n'].localeDir
23-
const localeMessages = getLocaleMessages(localeDir)
24-
2512
return defineTemplateBodyVisitor(context, {
2613
"VAttribute[directive=true][key.name='t']" (node) {
27-
checkDirective(context, localeMessages, node)
14+
checkDirective(context, node)
2815
},
2916

3017
"VAttribute[directive=true][key.name.name='t']" (node) {
31-
checkDirective(context, localeMessages, node)
18+
checkDirective(context, node)
3219
},
3320

3421
"VElement[name=i18n] > VStartTag > VAttribute[key.name='path']" (node) {
35-
checkComponent(context, localeMessages, node)
22+
checkComponent(context, node)
3623
},
3724

3825
"VElement[name=i18n] > VStartTag > VAttribute[key.name.name='path']" (node) {
39-
checkComponent(context, localeMessages, node)
26+
checkComponent(context, node)
4027
},
4128

4229
CallExpression (node) {
43-
checkCallExpression(context, localeMessages, node)
30+
checkCallExpression(context, node)
4431
}
4532
}, {
4633
CallExpression (node) {
47-
checkCallExpression(context, localeMessages, node)
34+
checkCallExpression(context, node)
4835
}
4936
})
5037
}
5138

52-
function checkDirective (context, localeMessages, node) {
39+
function checkDirective (context, node) {
40+
const localeMessages = getLocaleMessages(context)
41+
if (localeMessages.isEmpty()) { return }
5342
if ((node.value && node.value.type === 'VExpressionContainer') &&
5443
(node.value.expression && node.value.expression.type === 'Literal')) {
5544
const key = node.value.expression.value
@@ -64,7 +53,9 @@ function checkDirective (context, localeMessages, node) {
6453
}
6554
}
6655

67-
function checkComponent (context, localeMessages, node) {
56+
function checkComponent (context, node) {
57+
const localeMessages = getLocaleMessages(context)
58+
if (localeMessages.isEmpty()) { return }
6859
if (node.value && node.value.type === 'VLiteral') {
6960
const key = node.value.value
7061
if (!key) {
@@ -78,13 +69,16 @@ function checkComponent (context, localeMessages, node) {
7869
}
7970
}
8071

81-
function checkCallExpression (context, localeMessages, node) {
72+
function checkCallExpression (context, node) {
8273
const funcName = (node.callee.type === 'MemberExpression' && node.callee.property.name) || node.callee.name
8374

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

79+
const localeMessages = getLocaleMessages(context)
80+
if (localeMessages.isEmpty()) { return }
81+
8882
const [keyNode] = node.arguments
8983
if (keyNode.type !== 'Literal') { return }
9084

lib/rules/no-unused-keys.js

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
const { extname } = require('path')
77
const jsonDiffPatch = require('jsondiffpatch').create({})
88
const flatten = require('flat')
9-
const collectKeys = require('../utils/collect-keys')
9+
const { collectKeysFromFiles, collectKeysFromAST } = require('../utils/collect-keys')
1010
const collectLinkedKeys = require('../utils/collect-linked-keys')
1111
const {
1212
UNEXPECTED_ERROR_LOCATION,
@@ -21,7 +21,7 @@ const debug = require('debug')('eslint-plugin-vue-i18n:no-unused-keys')
2121
*/
2222

2323
/** @type {string[] | null} */
24-
let usedLocaleMessageKeys = null // used locale message keys
24+
let cacheUsedLocaleMessageKeys = null // used locale message keys
2525

2626
/**
2727
* @param {RuleContext} context
@@ -99,54 +99,78 @@ function traverseNode (fullpath, paths, ast, fn) {
9999

100100
function create (context) {
101101
const filename = context.getFilename()
102-
if (extname(filename) !== '.json') {
103-
debug(`ignore ${filename} in no-unused-keys`)
104-
return {}
105-
}
106102

107-
const { settings } = context
108-
if (!settings['vue-i18n'] || !settings['vue-i18n'].localeDir) {
109-
context.report({
110-
loc: UNEXPECTED_ERROR_LOCATION,
111-
message: `You need to 'localeDir' at 'settings. See the 'eslint-plugin-vue-i18n documentation`
112-
})
113-
return {}
114-
}
115-
116-
const localeMessages = getLocaleMessages(settings['vue-i18n'].localeDir)
117-
const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename)
118-
if (!targetLocaleMessage) {
119-
debug(`ignore ${filename} in no-unused-keys`)
120-
return {}
121-
}
103+
function verifyJson (jsonString, jsonFilename, targetLocaleMessage, usedLocaleMessageKeys, offsetLoc = { line: 1, column: 1 }) {
104+
const ast = generateJsonAst(context, jsonString, jsonFilename)
105+
if (!ast) { return }
122106

123-
const options = (context.options && context.options[0]) || {}
124-
const src = options.src || process.cwd()
125-
const extensions = options.extensions || ['.js', '.vue']
107+
const unusedKeys = getUnusedKeys(context, targetLocaleMessage, jsonString, usedLocaleMessageKeys)
108+
if (!unusedKeys) { return }
126109

127-
if (!usedLocaleMessageKeys) {
128-
usedLocaleMessageKeys = collectKeys([src], extensions)
110+
traverseJsonAstWithUnusedKeys(unusedKeys, ast, (fullpath, node) => {
111+
let { line, column } = node.loc.start
112+
if (line === 1) {
113+
line += offsetLoc.line - 1
114+
column += offsetLoc.column - 1
115+
} else {
116+
line += offsetLoc.line - 1
117+
}
118+
context.report({
119+
message: `unused '${fullpath}' key'`,
120+
loc: { line, column }
121+
})
122+
})
129123
}
130124

131-
return {
132-
Program (node) {
133-
const [jsonString, jsonFilename] = extractJsonInfo(context, node)
134-
if (!jsonString || !jsonFilename) { return }
125+
if (extname(filename) === '.vue') {
126+
return {
127+
Program (node) {
128+
const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment()
129+
/** @type {VElement[]} */
130+
const i18nBlocks = documentFragment && documentFragment.children.filter(node => node.type === 'VElement' && node.name === 'i18n') || []
131+
if (!i18nBlocks.length) {
132+
return
133+
}
134+
const localeMessages = getLocaleMessages(context)
135+
const usedLocaleMessageKeys = collectKeysFromAST(node, context.getSourceCode().visitorKeys)
135136

136-
const ast = generateJsonAst(context, jsonString, jsonFilename)
137-
if (!ast) { return }
137+
for (const block of i18nBlocks) {
138+
if (block.startTag.attributes.some(attr => !attr.directive && attr.key.name === 'src') || !block.endTag) {
139+
continue
140+
}
141+
const targetLocaleMessage = localeMessages.findBlockLocaleMessage(block)
142+
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
143+
const tokens = tokenStore.getTokensBetween(block.startTag, block.endTag)
144+
const jsonString = tokens.map(t => t.value).join('')
145+
if (jsonString.trim()) {
146+
verifyJson(jsonString, filename, targetLocaleMessage, usedLocaleMessageKeys, block.startTag.loc.start)
147+
}
148+
}
149+
}
150+
}
151+
} else if (extname(filename) === '.json') {
152+
const localeMessages = getLocaleMessages(context)
153+
const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename)
154+
if (!targetLocaleMessage) {
155+
debug(`ignore ${filename} in no-unused-keys`)
156+
return {}
157+
}
158+
const options = (context.options && context.options[0]) || {}
159+
const src = options.src || process.cwd()
160+
const extensions = options.extensions || ['.js', '.vue']
138161

139-
const unusedKeys = getUnusedKeys(context, targetLocaleMessage, jsonString, usedLocaleMessageKeys)
140-
if (!unusedKeys) { return }
162+
const usedLocaleMessageKeys = cacheUsedLocaleMessageKeys || (cacheUsedLocaleMessageKeys = collectKeysFromFiles([src], extensions))
141163

142-
traverseJsonAstWithUnusedKeys(unusedKeys, ast, (fullpath, node) => {
143-
const { line, column } = node.loc.start
144-
context.report({
145-
message: `unused '${fullpath}' key'`,
146-
loc: { line, column }
147-
})
148-
})
164+
return {
165+
Program (node) {
166+
const [jsonString, jsonFilename] = extractJsonInfo(context, node)
167+
if (!jsonString || !jsonFilename) { return }
168+
verifyJson(jsonString, jsonFilename, targetLocaleMessage, usedLocaleMessageKeys)
169+
}
149170
}
171+
} else {
172+
debug(`ignore ${filename} in no-unused-keys`)
173+
return {}
150174
}
151175
}
152176

0 commit comments

Comments
 (0)