diff --git a/docs/rules/no-raw-text.md b/docs/rules/no-raw-text.md
index 34772e6..41106e3 100644
--- a/docs/rules/no-raw-text.md
+++ b/docs/rules/no-raw-text.md
@@ -55,6 +55,17 @@ This rule encourage i18n in about the application needs to be localized.
"@intlify/svelte/no-raw-text": [
"error",
{
+ "attributes": {
+ "/.+/": [
+ "title",
+ "aria-label",
+ "aria-placeholder",
+ "aria-roledescription",
+ "aria-valuetext"
+ ],
+ "input": ["placeholder"],
+ "img": ["alt"]
+ },
"ignoreNodes": ["md-icon", "v-icon"],
"ignorePattern": "^[-#:()&]+$",
"ignoreText": ["EUR", "HKD", "USD"]
@@ -63,10 +74,44 @@ This rule encourage i18n in about the application needs to be localized.
}
```
+- `attributes`: An object whose keys are tag name or patterns and value is an array of attributes to check for that tag name. Default empty.
- `ignoreNodes`: specify nodes to ignore such as icon components
- `ignorePattern`: specify a regexp pattern that matches strings to ignore
- `ignoreText`: specify an array of strings to ignore
+### `attributes`
+
+
+
+
+
+```svelte
+
+
+
+
+```
+
+
+
+
+
+
+
+```svelte
+
+
+
+
+
+```
+
+
+
## :rocket: Version
This rule was introduced in `@intlify/eslint-plugin-svelte` v0.0.1
diff --git a/lib/rules/no-raw-text.ts b/lib/rules/no-raw-text.ts
index 06f78ce..f2b6720 100644
--- a/lib/rules/no-raw-text.ts
+++ b/lib/rules/no-raw-text.ts
@@ -6,23 +6,63 @@ import type ESTree from 'estree'
import type { RuleContext, RuleListener } from '../types'
import { defineRule } from '../utils'
-type AnyValue = ESTree.Literal['value']
+type LiteralValue = ESTree.Literal['value']
+type StaticTemplateLiteral = ESTree.TemplateLiteral & {
+ quasis: [ESTree.TemplateElement]
+ expressions: [/* empty */]
+}
+type TargetAttrs = { name: RegExp; attrs: Set }
type Config = {
+ attributes: TargetAttrs[]
ignorePattern: RegExp
ignoreNodes: string[]
ignoreText: string[]
}
+const RE_REGEXP_STR = /^\/(.+)\/(.*)$/u
+function toRegExp(str: string): RegExp {
+ const parts = RE_REGEXP_STR.exec(str)
+ if (parts) {
+ return new RegExp(parts[1], parts[2])
+ }
+ return new RegExp(`^${escape(str)}$`)
+}
const hasOnlyWhitespace = (value: string) => /^[\r\n\s\t\f\v]+$/.test(value)
-function isValidValue(value: AnyValue, config: Config) {
- return (
- typeof value !== 'string' ||
- hasOnlyWhitespace(value) ||
- config.ignorePattern.test(value.trim()) ||
- config.ignoreText.includes(value.trim())
+/**
+ * Get the attribute to be verified from the element name.
+ */
+function getTargetAttrs(tagName: string, config: Config): Set {
+ const result = []
+ for (const { name, attrs } of config.attributes) {
+ name.lastIndex = 0
+ if (name.test(tagName)) {
+ result.push(...attrs)
+ }
+ }
+
+ return new Set(result)
+}
+
+function isStaticTemplateLiteral(
+ node: ESTree.Expression | ESTree.Pattern
+): node is StaticTemplateLiteral {
+ return Boolean(
+ node && node.type === 'TemplateLiteral' && node.expressions.length === 0
)
}
+function testValue(value: LiteralValue, config: Config): boolean {
+ if (typeof value === 'string') {
+ return (
+ hasOnlyWhitespace(value) ||
+ config.ignorePattern.test(value.trim()) ||
+ config.ignoreText.includes(value.trim())
+ )
+ } else {
+ return false
+ }
+}
+
function checkSvelteMustacheTagText(
context: RuleContext,
node: SvAST.SvelteMustacheTag,
@@ -34,41 +74,90 @@ function checkSvelteMustacheTagText(
if (node.parent.type === 'SvelteElement') {
// parent is element (e.g. { ... }
)
- if (node.expression.type === 'Literal') {
- const literalNode = node.expression
- if (isValidValue(literalNode.value, config)) {
- return
+ checkExpressionText(context, node.expression, config)
+ }
+}
+
+function checkExpressionText(
+ context: RuleContext,
+ expression: ESTree.Expression,
+ config: Config
+) {
+ if (expression.type === 'Literal') {
+ checkLiteral(context, expression, config)
+ } else if (isStaticTemplateLiteral(expression)) {
+ checkLiteral(context, expression, config)
+ } else if (expression.type === 'ConditionalExpression') {
+ const targets = [expression.consequent, expression.alternate]
+ targets.forEach(target => {
+ if (target.type === 'Literal') {
+ checkLiteral(context, target, config)
+ } else if (isStaticTemplateLiteral(target)) {
+ checkLiteral(context, target, config)
}
+ })
+ }
+}
- context.report({
- node: literalNode,
- message: `raw text '${literalNode.value}' is used`
- })
- } else if (node.expression.type === 'ConditionalExpression') {
- for (const target of [
- node.expression.consequent,
- node.expression.alternate
- ]) {
- if (target.type !== 'Literal') {
- continue
- }
- if (isValidValue(target.value, config)) {
- continue
- }
+function checkSvelteLiteralOrText(
+ context: RuleContext,
+ literal: SvAST.SvelteLiteral | SvAST.SvelteText,
+ config: Config
+) {
+ if (testValue(literal.value, config)) {
+ return
+ }
- context.report({
- node: target,
- message: `raw text '${target.value}' is used`
- })
- }
- }
+ const loc = literal.loc!
+ context.report({
+ loc,
+ message: `raw text '${literal.value}' is used`
+ })
+}
+
+function checkLiteral(
+ context: RuleContext,
+ literal: ESTree.Literal | StaticTemplateLiteral,
+ config: Config
+) {
+ const value =
+ literal.type !== 'TemplateLiteral'
+ ? literal.value
+ : literal.quasis[0].value.cooked
+
+ if (testValue(value, config)) {
+ return
+ }
+
+ const loc = literal.loc!
+ context.report({
+ loc,
+ message: `raw text '${value}' is used`
+ })
+}
+/**
+ * Parse attributes option
+ */
+function parseTargetAttrs(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ options: any
+) {
+ const regexps: TargetAttrs[] = []
+ for (const tagName of Object.keys(options)) {
+ const attrs: Set = new Set(options[tagName])
+ regexps.push({
+ name: toRegExp(tagName),
+ attrs
+ })
}
+ return regexps
}
function create(context: RuleContext): RuleListener {
const sourceCode = context.getSourceCode()
const config: Config = {
- ignorePattern: /^[^\S\s]$/,
+ attributes: [],
+ ignorePattern: /^$/,
ignoreNodes: [],
ignoreText: []
}
@@ -76,14 +165,15 @@ function create(context: RuleContext): RuleListener {
if (context.options[0]?.ignorePattern) {
config.ignorePattern = new RegExp(context.options[0].ignorePattern, 'u')
}
-
if (context.options[0]?.ignoreNodes) {
config.ignoreNodes = context.options[0].ignoreNodes
}
-
if (context.options[0]?.ignoreText) {
config.ignoreText = context.options[0].ignoreText
}
+ if (context.options[0]?.attributes) {
+ config.attributes = parseTargetAttrs(context.options[0].attributes)
+ }
function isIgnore(node: SvAST.SvelteMustacheTag | SvAST.SvelteText) {
const element = getElement(node)
@@ -98,7 +188,8 @@ function create(context: RuleContext): RuleListener {
| SvAST.SvelteText['parent']
| SvAST.SvelteMustacheTag['parent']
| SvAST.SvelteElement
- | SvAST.SvelteAwaitBlock = node.parent
+ | SvAST.SvelteAwaitBlock
+ | SvAST.SvelteElseBlockElseIf = node.parent
while (
target.type === 'SvelteIfBlock' ||
target.type === 'SvelteElseBlock' ||
@@ -118,6 +209,19 @@ function create(context: RuleContext): RuleListener {
}
return {
+ SvelteAttribute(node: SvAST.SvelteAttribute) {
+ if (node.value.length !== 1 || node.value[0].type !== 'SvelteLiteral') {
+ return
+ }
+ const nameNode = node.parent.parent.name
+ const tagName = sourceCode.text.slice(...nameNode.range!)
+ const attrName = node.key.name
+ if (!getTargetAttrs(tagName, config).has(attrName)) {
+ return
+ }
+
+ checkSvelteLiteralOrText(context, node.value[0], config)
+ },
SvelteMustacheTag(node: SvAST.SvelteMustacheTag) {
if (isIgnore(node)) {
return
@@ -129,15 +233,7 @@ function create(context: RuleContext): RuleListener {
if (isIgnore(node)) {
return
}
-
- if (isValidValue(node.value, config)) {
- return
- }
-
- context.report({
- node,
- message: `raw text '${node.value}' is used`
- })
+ checkSvelteLiteralOrText(context, node, config)
}
}
}
@@ -154,6 +250,17 @@ export = defineRule('no-raw-text', {
{
type: 'object',
properties: {
+ attributes: {
+ type: 'object',
+ patternProperties: {
+ '^(?:\\S+|/.*/[a-z]*)$': {
+ type: 'array',
+ items: { type: 'string' },
+ uniqueItems: true
+ }
+ },
+ additionalProperties: false
+ },
ignoreNodes: {
type: 'array'
},
diff --git a/package.json b/package.json
index 6d72cbf..3a226fc 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
},
"dependencies": {
"debug": "^4.3.1",
- "svelte-eslint-parser": "^0.4.1"
+ "svelte-eslint-parser": "^0.8.0"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0-0",
@@ -94,6 +94,7 @@
"generate": "ts-node --transpile-only scripts/update.ts && prettier . --write",
"lint": "eslint . --ext js,ts,vue,md --ignore-pattern \"/tests/fixtures\"",
"lint:docs": "prettier docs --check",
+ "format:docs": "prettier docs --write",
"release:prepare": "shipjs prepare",
"release:trigger": "shipjs trigger",
"test": "mocha --require ts-node/register \"./tests/**/*.ts\"",
diff --git a/tests/lib/rules/no-raw-text.ts b/tests/lib/rules/no-raw-text.ts
index 53cff28..980275f 100644
--- a/tests/lib/rules/no-raw-text.ts
+++ b/tests/lib/rules/no-raw-text.ts
@@ -181,6 +181,49 @@ tester.run('no-raw-text', rule as never, {
line: 3
}
]
+ },
+ {
+ code: `
+
+ `,
+ options: [
+ {
+ attributes: {
+ '/.*/': ['label']
+ }
+ }
+ ],
+ errors: [
+ {
+ message: "raw text 'hello' is used",
+ line: 2,
+ column: 23
+ },
+ {
+ message: "raw text 'hello' is used",
+ line: 3,
+ column: 28
+ }
+ ]
+ },
+ {
+ code: `
+
+ `,
+ options: [
+ {
+ attributes: {
+ MyInput: ['label']
+ }
+ }
+ ],
+ errors: [
+ {
+ message: "raw text 'hello' is used",
+ line: 2,
+ column: 23
+ }
+ ]
}
]
})
diff --git a/yarn.lock b/yarn.lock
index babd24b..6cca1d0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1762,6 +1762,11 @@ acorn@^7.1.1, acorn@^7.4.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
+acorn@^8.6.0:
+ version "8.6.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895"
+ integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==
+
add-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa"
@@ -4266,6 +4271,14 @@ eslint-scope@^5.1.1:
esrecurse "^4.3.0"
estraverse "^4.1.1"
+eslint-scope@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153"
+ integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^5.2.0"
+
eslint-utils@^2.0.0, eslint-utils@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
@@ -4288,6 +4301,11 @@ eslint-visitor-keys@^3.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz#e32e99c6cdc2eb063f204eda5db67bfe58bb4186"
integrity sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==
+eslint-visitor-keys@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2"
+ integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==
+
eslint4b@^7.16.0:
version "7.16.0"
resolved "https://registry.yarnpkg.com/eslint4b/-/eslint4b-7.16.0.tgz#6bea3e3440814828deef6e5e9e42448603edf3b2"
@@ -4375,6 +4393,15 @@ espree@^7.3.0, espree@^7.3.1:
acorn-jsx "^5.3.1"
eslint-visitor-keys "^1.3.0"
+espree@^9.0.0:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-9.1.0.tgz#ba9d3c9b34eeae205724124e31de4543d59fbf74"
+ integrity sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ==
+ dependencies:
+ acorn "^8.6.0"
+ acorn-jsx "^5.3.1"
+ eslint-visitor-keys "^3.1.0"
+
esprima@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
@@ -10016,14 +10043,14 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
-svelte-eslint-parser@^0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/svelte-eslint-parser/-/svelte-eslint-parser-0.4.1.tgz#ac645aea60a3dfa977b2693aaa654210f193ae9a"
- integrity sha512-OoCyDI+0O7BjOCHxLiQMsS6e02T/ahh5u7o+CiQBM4RoChadYn/AfgR9SaIMHL2P+aVToxPnX94sC5e482w/Lg==
+svelte-eslint-parser@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/svelte-eslint-parser/-/svelte-eslint-parser-0.8.0.tgz#33946ad6ff01a0932640095d17fd318c8fbb7dda"
+ integrity sha512-ZWmwCoqpVZiQ3RG6Id1BSA1ZCpFNdEn1T4owXCNJUd9+oXSh00QUxIKBA4AvkTbqrOi6sIoOphD50ugpPpt6tw==
dependencies:
- eslint-scope "^5.1.1"
+ eslint-scope "^7.0.0"
eslint-visitor-keys "^3.0.0"
- espree "^7.3.1"
+ espree "^9.0.0"
svelte@^3.37.0:
version "3.37.0"