Skip to content

Commit 863848b

Browse files
authored
Add prefer-sfc-lang-attr rule (#267)
* Add `prefer-sfc-lang-attr` rule * fix
1 parent adbe0fe commit 863848b

File tree

6 files changed

+258
-0
lines changed

6 files changed

+258
-0
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
| [@intlify/vue-i18n/<wbr>no-dynamic-keys](./no-dynamic-keys.html) | disallow localization dynamic keys at localization methods | |
2929
| [@intlify/vue-i18n/<wbr>no-missing-keys-in-other-locales](./no-missing-keys-in-other-locales.html) | disallow missing locale message keys in other locales | |
3030
| [@intlify/vue-i18n/<wbr>no-unused-keys](./no-unused-keys.html) | disallow unused localization keys | :black_nib: |
31+
| [@intlify/vue-i18n/<wbr>prefer-sfc-lang-attr](./prefer-sfc-lang-attr.html) | require lang attribute on `<i18n>` block | :black_nib: |
3132

3233
## Stylistic Issues
3334

docs/rules/prefer-sfc-lang-attr.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
title: '@intlify/vue-i18n/prefer-sfc-lang-attr'
3+
description: require lang attribute on `<i18n>` block
4+
---
5+
6+
# @intlify/vue-i18n/prefer-sfc-lang-attr
7+
8+
> require lang attribute on `<i18n>` block
9+
10+
- :black_nib:️ The `--fix` option on the [command line](http://eslint.org/docs/user-guide/command-line-interface#fix) can automatically fix some of the problems reported by this rule.
11+
12+
## :book: Rule Details
13+
14+
This rule enforce `lang` attribute to be specified to `<i18n>` custom block.
15+
16+
:-1: Examples of **incorrect** code for this rule:
17+
18+
locale messages:
19+
20+
<eslint-code-block fix>
21+
22+
<!-- eslint-skip -->
23+
24+
```vue
25+
<i18n>
26+
{
27+
"en": {
28+
"message": "hello!"
29+
}
30+
}
31+
</i18n>
32+
<script>
33+
/* eslint @intlify/vue-i18n/prefer-sfc-lang-attr: 'error' */
34+
</script>
35+
```
36+
37+
</eslint-code-block>
38+
39+
:+1: Examples of **correct** code for this rule:
40+
41+
locale messages:
42+
43+
<eslint-code-block fix>
44+
45+
<!-- eslint-skip -->
46+
47+
```vue
48+
<i18n lang="json">
49+
{
50+
"en": {
51+
"message": "hello!"
52+
}
53+
}
54+
</i18n>
55+
<script>
56+
/* eslint @intlify/vue-i18n/prefer-sfc-lang-attr: 'error' */
57+
</script>
58+
```
59+
60+
</eslint-code-block>
61+
62+
## :mag: Implementation
63+
64+
- [Rule source](https://github.com/intlify/eslint-plugin-vue-i18n/blob/master/lib/rules/prefer-sfc-lang-attr.ts)
65+
- [Test source](https://github.com/intlify/eslint-plugin-vue-i18n/tree/master/tests/lib/rules/prefer-sfc-lang-attr.ts)

lib/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import noRawText from './rules/no-raw-text'
1313
import noUnusedKeys from './rules/no-unused-keys'
1414
import noVHtml from './rules/no-v-html'
1515
import preferLinkedKeyWithParen from './rules/prefer-linked-key-with-paren'
16+
import preferSfcLangAttr from './rules/prefer-sfc-lang-attr'
1617
import validMessageSyntax from './rules/valid-message-syntax'
1718

1819
export = {
@@ -30,5 +31,6 @@ export = {
3031
'no-unused-keys': noUnusedKeys,
3132
'no-v-html': noVHtml,
3233
'prefer-linked-key-with-paren': preferLinkedKeyWithParen,
34+
'prefer-sfc-lang-attr': preferSfcLangAttr,
3335
'valid-message-syntax': validMessageSyntax
3436
}

lib/rules/prefer-sfc-lang-attr.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { getAttribute, isI18nBlock } from '../utils/index'
2+
import type { RuleContext, RuleListener } from '../types'
3+
4+
function create(context: RuleContext): RuleListener {
5+
const df = context.parserServices.getDocumentFragment?.()
6+
if (!df) {
7+
return {}
8+
}
9+
10+
return {
11+
Program() {
12+
for (const i18n of df.children.filter(isI18nBlock)) {
13+
const srcAttrs = getAttribute(i18n, 'src')
14+
if (srcAttrs != null) {
15+
continue
16+
}
17+
const langAttrs = getAttribute(i18n, 'lang')
18+
if (
19+
langAttrs == null ||
20+
langAttrs.value == null ||
21+
!langAttrs.value.value
22+
) {
23+
context.report({
24+
loc: (langAttrs?.value ?? langAttrs ?? i18n.startTag).loc,
25+
messageId: 'required',
26+
fix(fixer) {
27+
if (langAttrs) {
28+
return fixer.replaceTextRange(langAttrs.range, 'lang="json"')
29+
}
30+
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
31+
const closeToken = tokenStore.getLastToken(i18n.startTag)
32+
const beforeToken = tokenStore.getTokenBefore(closeToken)
33+
return fixer.insertTextBeforeRange(
34+
closeToken.range,
35+
(beforeToken.range[1] < closeToken.range[0] ? '' : ' ') +
36+
'lang="json" '
37+
)
38+
}
39+
})
40+
}
41+
}
42+
}
43+
}
44+
}
45+
46+
export = {
47+
meta: {
48+
type: 'suggestion',
49+
docs: {
50+
description: 'require lang attribute on `<i18n>` block',
51+
category: 'Best Practices',
52+
recommended: false
53+
},
54+
fixable: 'code',
55+
schema: [],
56+
messages: {
57+
required: '`lang` attribute is required.'
58+
}
59+
},
60+
create
61+
}

lib/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as casing from './utils/casing'
55
import * as collectKeys from './utils/collect-keys'
66
import * as collectLinkedKeys from './utils/collect-linked-keys'
77
import * as defaultTimeouts from './utils/default-timeouts'
8+
import * as getCwd from './utils/get-cwd'
89
import * as globSync from './utils/glob-sync'
910
import * as globUtils from './utils/glob-utils'
1011
import * as ignoredPaths from './utils/ignored-paths'
@@ -22,6 +23,7 @@ export = {
2223
'collect-keys': collectKeys,
2324
'collect-linked-keys': collectLinkedKeys,
2425
'default-timeouts': defaultTimeouts,
26+
'get-cwd': getCwd,
2527
'glob-sync': globSync,
2628
'glob-utils': globUtils,
2729
'ignored-paths': ignoredPaths,
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { RuleTester } from 'eslint'
2+
import rule = require('../../../lib/rules/prefer-sfc-lang-attr')
3+
4+
new RuleTester({
5+
parser: require.resolve('vue-eslint-parser'),
6+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
7+
}).run('prefer-sfc-lang-attr', rule as never, {
8+
valid: [
9+
{
10+
filename: 'test.vue',
11+
code: `
12+
<i18n lang="json">{}</i18n>
13+
<template></template>
14+
<script></script>`
15+
},
16+
{
17+
filename: 'test.vue',
18+
code: `
19+
<i18n lang="json5">{}</i18n>
20+
<template></template>
21+
<script></script>`
22+
},
23+
{
24+
filename: 'test.vue',
25+
code: `
26+
<i18n lang="yaml">{}</i18n>
27+
<template></template>
28+
<script></script>`
29+
}
30+
],
31+
invalid: [
32+
{
33+
filename: 'test.vue',
34+
code: `
35+
<i18n>{}</i18n>
36+
<template></template>
37+
<script></script>`,
38+
output: `
39+
<i18n lang="json" >{}</i18n>
40+
<template></template>
41+
<script></script>`,
42+
errors: [
43+
{
44+
message: '`lang` attribute is required.',
45+
line: 2,
46+
column: 7,
47+
endLine: 2,
48+
endColumn: 13
49+
}
50+
]
51+
},
52+
{
53+
filename: 'test.vue',
54+
code: `
55+
<i18n>{}</i18n>
56+
<template></template>`,
57+
output: `
58+
<i18n lang="json" >{}</i18n>
59+
<template></template>`,
60+
errors: [
61+
{
62+
message: '`lang` attribute is required.',
63+
line: 2,
64+
column: 7,
65+
endLine: 2,
66+
endColumn: 13
67+
}
68+
]
69+
},
70+
{
71+
filename: 'test.vue',
72+
code: `
73+
<i18n locale="en" >{}</i18n>
74+
<template></template>
75+
<script></script>`,
76+
output: `
77+
<i18n locale="en" lang="json" >{}</i18n>
78+
<template></template>
79+
<script></script>`,
80+
errors: [
81+
{
82+
message: '`lang` attribute is required.',
83+
line: 2,
84+
column: 7,
85+
endLine: 2,
86+
endColumn: 26
87+
}
88+
]
89+
},
90+
{
91+
filename: 'test.vue',
92+
code: `
93+
<i18n lang>{}</i18n>
94+
<template></template>`,
95+
output: `
96+
<i18n lang="json">{}</i18n>
97+
<template></template>`,
98+
errors: [
99+
{
100+
message: '`lang` attribute is required.',
101+
line: 2,
102+
column: 13,
103+
endLine: 2,
104+
endColumn: 17
105+
}
106+
]
107+
},
108+
{
109+
filename: 'test.vue',
110+
code: `
111+
<i18n lang="">{}</i18n>
112+
<template></template>`,
113+
output: `
114+
<i18n lang="json">{}</i18n>
115+
<template></template>`,
116+
errors: [
117+
{
118+
message: '`lang` attribute is required.',
119+
line: 2,
120+
column: 18,
121+
endLine: 2,
122+
endColumn: 20
123+
}
124+
]
125+
}
126+
]
127+
})

0 commit comments

Comments
 (0)