Skip to content

Commit 731fc03

Browse files
authored
Add sfc-locale-attr rule (#286)
* Add sfc-locale-attr rule * fix doc * format
1 parent 2870269 commit 731fc03

File tree

8 files changed

+275
-3
lines changed

8 files changed

+275
-3
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@
3636
| Rule ID | Description | |
3737
|:--------|:------------|:---|
3838
| [@intlify/vue-i18n/<wbr>prefer-linked-key-with-paren](./prefer-linked-key-with-paren.html) | enforce linked key to be enclosed in parentheses | :black_nib: |
39+
| [@intlify/vue-i18n/<wbr>sfc-locale-attr](./sfc-locale-attr.html) | require or disallow the locale attribute on `<i18n>` block | |

docs/rules/sfc-locale-attr.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
---
2+
title: '@intlify/vue-i18n/sfc-locale-attr'
3+
description: require or disallow the locale attribute on `<i18n>` block
4+
---
5+
6+
# @intlify/vue-i18n/sfc-locale-attr
7+
8+
> require or disallow the locale attribute on `<i18n>` block
9+
10+
## :book: Rule Details
11+
12+
This rule aims to enforce the `<i18n>` block to use or not the `locale` attribute.
13+
14+
<eslint-code-block>
15+
16+
<!-- eslint-skip -->
17+
18+
```vue
19+
<script>
20+
/* eslint @intlify/vue-i18n/sfc-locale-attr: "error" */
21+
</script>
22+
23+
<!-- ✓ GOOD -->
24+
<i18n locale="en">
25+
{
26+
"message": "hello!"
27+
}
28+
</i18n>
29+
30+
<!-- ✗ BAD -->
31+
<i18n>
32+
{
33+
"en": {
34+
"message": "hello!"
35+
}
36+
}
37+
</i18n>
38+
```
39+
40+
</eslint-code-block>
41+
42+
## :gear: Options
43+
44+
```json
45+
{
46+
"@intlify/vue-i18n/sfc-locale-attr": [
47+
"error",
48+
"always" //"always" or "never"
49+
]
50+
}
51+
```
52+
53+
- `"always"` ... The `<i18n>` blocks requires the locale attribute.
54+
- `"never"` ... Do not use the locale attribute on `<i18n>` blocks.
55+
56+
### Examples of code for this rule with `"never"` option:
57+
58+
<eslint-code-block>
59+
60+
<!-- eslint-skip -->
61+
62+
```vue
63+
<script>
64+
/* eslint @intlify/vue-i18n/sfc-locale-attr: ["error", "never"] */
65+
</script>
66+
67+
<!-- ✓ GOOD -->
68+
<i18n>
69+
{
70+
"en": {
71+
"message": "hello!"
72+
}
73+
}
74+
</i18n>
75+
76+
<!-- ✗ BAD -->
77+
<i18n locale="en">
78+
{
79+
"message": "hello!"
80+
}
81+
</i18n>
82+
```
83+
84+
</eslint-code-block>
85+
86+
## :mag: Implementation
87+
88+
- [Rule source](https://github.com/intlify/eslint-plugin-vue-i18n/blob/master/lib/rules/sfc-locale-attr.ts)
89+
- [Test source](https://github.com/intlify/eslint-plugin-vue-i18n/tree/master/tests/lib/rules/sfc-locale-attr.ts)

lib/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ 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'
1616
import preferSfcLangAttr from './rules/prefer-sfc-lang-attr'
17+
import sfcLocaleAttr from './rules/sfc-locale-attr'
1718
import validMessageSyntax from './rules/valid-message-syntax'
1819

1920
export = {
@@ -32,5 +33,6 @@ export = {
3233
'no-v-html': noVHtml,
3334
'prefer-linked-key-with-paren': preferLinkedKeyWithParen,
3435
'prefer-sfc-lang-attr': preferSfcLangAttr,
36+
'sfc-locale-attr': sfcLocaleAttr,
3537
'valid-message-syntax': validMessageSyntax
3638
}

lib/rules/sfc-locale-attr.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { RuleContext, RuleListener } from '../types'
2+
import { isI18nBlock, getAttribute } from '../utils/index'
3+
import { createRule } from '../utils/rule'
4+
5+
export = createRule({
6+
meta: {
7+
type: 'suggestion',
8+
docs: {
9+
description: 'require or disallow the locale attribute on `<i18n>` block',
10+
category: 'Stylistic Issues',
11+
url: 'https://eslint-plugin-vue-i18n.intlify.dev/rules/sfc-locale-attr.html',
12+
recommended: false
13+
},
14+
fixable: null,
15+
schema: [
16+
{
17+
enum: ['always', 'never']
18+
}
19+
],
20+
messages: {
21+
required: '`locale` attribute is required.',
22+
disallowed: '`locale` attribute is disallowed.'
23+
}
24+
},
25+
create(context: RuleContext): RuleListener {
26+
const df = context.parserServices.getDocumentFragment?.()
27+
if (!df) {
28+
return {}
29+
}
30+
const always = context.options[0] !== 'never'
31+
return {
32+
Program() {
33+
for (const i18n of df.children.filter(isI18nBlock)) {
34+
const srcAttrs = getAttribute(i18n, 'src')
35+
if (srcAttrs != null) {
36+
continue
37+
}
38+
const localeAttrs = getAttribute(i18n, 'locale')
39+
40+
if (
41+
localeAttrs != null &&
42+
localeAttrs.value != null &&
43+
localeAttrs.value.value
44+
) {
45+
if (always) {
46+
continue
47+
}
48+
// disallowed
49+
context.report({
50+
loc: localeAttrs.loc,
51+
messageId: 'disallowed'
52+
})
53+
} else {
54+
if (!always) {
55+
continue
56+
}
57+
// missing
58+
context.report({
59+
loc: i18n.startTag.loc,
60+
messageId: 'required'
61+
})
62+
}
63+
}
64+
}
65+
}
66+
}
67+
})

lib/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import * as localeMessages from './utils/locale-messages'
1515
import * as parsers from './utils/parsers'
1616
import * as pathUtils from './utils/path-utils'
1717
import * as resourceLoader from './utils/resource-loader'
18+
import * as rule from './utils/rule'
1819

1920
export = {
2021
'cache-function': cacheFunction,
@@ -32,5 +33,6 @@ export = {
3233
'locale-messages': localeMessages,
3334
parsers,
3435
'path-utils': pathUtils,
35-
'resource-loader': resourceLoader
36+
'resource-loader': resourceLoader,
37+
rule
3638
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,11 @@
107107
"coverage": "nyc report --reporter lcov && opener coverage/lcov-report/index.html",
108108
"docs": "npm run build && vuepress dev docs",
109109
"docs:build": "npm run build && vuepress build docs",
110-
"generate": "ts-node scripts/update.ts && prettier . --write",
110+
"generate": "ts-node scripts/update.ts && npm run format",
111111
"lint": "eslint . --ext js,ts,vue,md --ignore-pattern \"/tests/fixtures\"",
112112
"lint-fix": "eslint . --ext js,ts,vue,md --ignore-pattern \"/tests/fixtures\" --fix",
113113
"lint:docs": "prettier docs --check",
114+
"format": "prettier . --write",
114115
"release:prepare": "shipjs prepare",
115116
"release:trigger": "shipjs trigger",
116117
"test": "mocha --require ts-node/register \"./tests/**/*.ts\"",

scripts/new-rule.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ description: description
9393
9494
> description
9595
96-
## :book: Rule Details
9796
## :book: Rule Details
9897
9998
This rule reports ???.

tests/lib/rules/sfc-locale-attr.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { RuleTester } from 'eslint'
2+
import rule = require('../../../lib/rules/sfc-locale-attr')
3+
const vueParser = require.resolve('vue-eslint-parser')
4+
5+
const tester = new RuleTester({
6+
parser: vueParser,
7+
parserOptions: {
8+
ecmaVersion: 2020,
9+
sourceType: 'module'
10+
}
11+
})
12+
13+
tester.run('sfc-locale-attr', rule as never, {
14+
valid: [
15+
{
16+
filename: 'test.vue',
17+
code: `
18+
<i18n locale="en">
19+
{
20+
"message": "hello!"
21+
}
22+
</i18n>
23+
`
24+
},
25+
{
26+
filename: 'test.vue',
27+
code: `
28+
<i18n locale="en">
29+
{
30+
"message": "hello!"
31+
}
32+
</i18n>
33+
`,
34+
options: ['always']
35+
},
36+
{
37+
filename: 'test.vue',
38+
code: `
39+
<i18n>
40+
{
41+
"en": {
42+
"message": "hello!"
43+
}
44+
}
45+
</i18n>
46+
`,
47+
options: ['never']
48+
}
49+
],
50+
invalid: [
51+
{
52+
filename: 'test.vue',
53+
code: `
54+
<i18n>
55+
{
56+
"en": {
57+
"message": "hello!"
58+
}
59+
}
60+
</i18n>
61+
`,
62+
errors: [
63+
{
64+
message: '`locale` attribute is required.',
65+
line: 2,
66+
column: 7
67+
}
68+
]
69+
},
70+
{
71+
filename: 'test.vue',
72+
code: `
73+
<i18n>
74+
{
75+
"en": {
76+
"message": "hello!"
77+
}
78+
}
79+
</i18n>
80+
`,
81+
options: ['always'],
82+
errors: [
83+
{
84+
message: '`locale` attribute is required.',
85+
line: 2,
86+
column: 7
87+
}
88+
]
89+
},
90+
{
91+
filename: 'test.vue',
92+
code: `
93+
<i18n locale="en">
94+
{
95+
"en": {
96+
"message": "hello!"
97+
}
98+
}
99+
</i18n>
100+
`,
101+
options: ['never'],
102+
errors: [
103+
{
104+
message: '`locale` attribute is disallowed.',
105+
line: 2,
106+
column: 13
107+
}
108+
]
109+
}
110+
]
111+
})

0 commit comments

Comments
 (0)