Skip to content

Commit f616c64

Browse files
praxxisadidahiya
authored andcommitted
Add options to allow HTML entities and/or punctuation to be excluded from translation check (#139)
1 parent 6ab778b commit f616c64

File tree

8 files changed

+212
-10
lines changed

8 files changed

+212
-10
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ The built-in configuration preset you get with `"extends": "tslint-react"` is se
8989
- Rule options: _none_
9090
- `jsx-use-translation-function` (since v2.4.0)
9191
- Enforces use of a translation function. Plain string literals are disallowed in JSX when enabled.
92-
- Rule options: _none_
92+
- Rule options: `["allow-punctuation", "allow-htmlentities"]`
9393
- Off by default
9494
- `jsx-self-close` (since v0.4.0)
9595
- Enforces that JSX elements with no children are self-closing.

src/rules/jsxUseTranslationFunctionRule.ts

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,100 @@
1616
*/
1717

1818
import * as Lint from "tslint";
19-
import { isJsxAttribute, isJsxElement, isJsxExpression, isJsxText, isStringLiteral } from "tsutils";
19+
import { isJsxAttribute, isJsxElement, isJsxExpression, isJsxText, isTextualLiteral } from "tsutils";
2020
import * as ts from "typescript";
2121

22+
interface IOptions {
23+
allowPunctuation: boolean;
24+
allowHtmlEntities: boolean;
25+
}
26+
2227
export class Rule extends Lint.Rules.AbstractRule {
28+
/* tslint:disable:object-literal-sort-keys */
29+
public static metadata: Lint.IRuleMetadata = {
30+
ruleName: "jsx-use-translation-function",
31+
description: Lint.Utils.dedent`
32+
Enforces use of a translation function. Most plain string literals are disallowed in JSX when enabled.`,
33+
options: {
34+
type: "array",
35+
items: {
36+
type: "string",
37+
enum: ["allow-punctuation", "allow-htmlentities"],
38+
},
39+
},
40+
optionsDescription: Lint.Utils.dedent`
41+
Whether to allow punctuation and or HTML entities`,
42+
type: "functionality",
43+
typescriptOnly: false,
44+
};
45+
/* tslint:enable:object-literal-sort-keys */
46+
2347
public static TRANSLATABLE_ATTRIBUTES = new Set(["placeholder", "title", "alt"]);
2448
public static FAILURE_STRING = "String literals are disallowed as JSX. Use a translation function";
2549
public static FAILURE_STRING_FACTORY = (text: string) =>
2650
`String literal is not allowed for value of ${text}. Use a translation function`
2751

2852
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
29-
return this.applyWithFunction(sourceFile, walk);
53+
return this.applyWithFunction(sourceFile, walk, {
54+
allowHtmlEntities: this.ruleArguments.indexOf("allow-htmlentities") !== -1,
55+
allowPunctuation: this.ruleArguments.indexOf("allow-punctuation") !== -1,
56+
});
3057
}
3158
}
3259

33-
function walk(ctx: Lint.WalkContext<void>) {
60+
function walk(ctx: Lint.WalkContext<IOptions>) {
3461
return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
3562
if (isJsxElement(node)) {
63+
3664
for (const child of node.children) {
37-
if (isJsxText(child) && child.getText().trim() !== "") {
65+
if (isJsxText(child) && isInvalidText(child.getText(), ctx.options)) {
3866
ctx.addFailureAtNode(child, Rule.FAILURE_STRING);
3967
}
68+
4069
if (isJsxExpression(child)
4170
&& child.expression !== undefined
42-
&& (isStringLiteral(child.expression)
43-
|| child.expression.kind === ts.SyntaxKind.FirstTemplateToken)) {
44-
ctx.addFailureAtNode(child, Rule.FAILURE_STRING);
71+
&& isTextualLiteral(child.expression)) {
72+
if (isInvalidText(child.expression.text, ctx.options)) {
73+
ctx.addFailureAtNode(child, Rule.FAILURE_STRING);
74+
}
4575
}
4676
}
77+
4778
} else if (isJsxAttribute(node)) {
4879
if (Rule.TRANSLATABLE_ATTRIBUTES.has(node.name.text) && node.initializer !== undefined) {
49-
if (isStringLiteral(node.initializer)
50-
|| (isJsxExpression(node.initializer) && isStringLiteral(node.initializer.expression!))) {
80+
if (isTextualLiteral(node.initializer) && isInvalidText(node.initializer.text, ctx.options)) {
5181
ctx.addFailureAtNode(node.initializer, Rule.FAILURE_STRING_FACTORY(node.name.text));
5282
}
83+
84+
if (isJsxExpression(node.initializer) && isTextualLiteral(node.initializer.expression!)) {
85+
if (isInvalidText((node.initializer.expression as ts.LiteralExpression).text, ctx.options)) {
86+
ctx.addFailureAtNode(node.initializer, Rule.FAILURE_STRING_FACTORY(node.name.text));
87+
}
88+
}
5389
}
5490
}
5591
return ts.forEachChild(node, cb);
5692
});
5793
}
94+
95+
function isInvalidText(text: string, options: Readonly<IOptions>) {
96+
const t = text.trim();
97+
98+
if (t === "") {
99+
return false;
100+
}
101+
102+
let invalid = true;
103+
104+
if (options.allowPunctuation) {
105+
invalid = /\w/.test(t);
106+
}
107+
108+
if (options.allowHtmlEntities && t.indexOf("&") !== -1) {
109+
invalid = t.split("&")
110+
.filter((entity) => entity !== "")
111+
.some((entity) => /^&(?:#[0-9]+|[a-zA-Z]+);$/.test(`&${entity}`) !== true);
112+
}
113+
114+
return invalid;
115+
}

test/rules/jsx-use-translation-function/test.tsx.lint renamed to test/rules/jsx-use-translation-function/allow-htmlentities/test.tsx.lint

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,20 @@
3737
~~~~~ [0]
3838
</ul>
3939

40+
<div> - </div>
41+
~~ [0]
42+
43+
<div>{' - '}</div>
44+
~~~~~~~ [0]
45+
46+
<input placeholder="-" />
47+
~~~ [1]
48+
49+
<div>&nbsp;</div>
50+
51+
<div>{'&nbsp;'}</div>
52+
53+
<input placeholder="&nbsp;" />
54+
4055
[0]: String literals are disallowed as JSX. Use a translation function
4156
[1]: String literal is not allowed for value of placeholder. Use a translation function
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"rules": {
3+
"jsx-use-translation-function": {
4+
"options": ["allow-htmlentities"]
5+
}
6+
}
7+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<div>Hello world!</div>
2+
~~~~~~~~~~~~ [0]
3+
4+
<div>{'Hello world!'}</div>
5+
~~~~~~~~~~~~~~~~ [0]
6+
7+
<div>{translate('hello-world')}</div>
8+
9+
<input placeholder={translate('name')} />
10+
<input placeholder="Name" />
11+
~~~~~~ [1]
12+
13+
<input placeholder={translate('name')} />
14+
<input placeholder={"Name"} />
15+
~~~~~~~~ [1]
16+
17+
<div>
18+
<div>{translate('hi')}</div>
19+
</div>
20+
21+
<div>
22+
<span>{translate('this')}</span>is bad<span>
23+
~~~~~~ [0]
24+
</div>
25+
26+
<div>{`foo`}</div>
27+
~~~~~~~ [0]
28+
29+
<div>{`foo ${1}`}</div>
30+
31+
<ul>
32+
<li>{translate('one')}</li>
33+
Two
34+
~~~
35+
<li>Three</li>
36+
~~~~ [0]
37+
~~~~~ [0]
38+
</ul>
39+
40+
<div> - </div>
41+
42+
<div>{' - '}</div>
43+
44+
<input placeholder="-" />
45+
46+
<div>&nbsp;</div>
47+
~~~~~~ [0]
48+
49+
<div>{'&nbsp;'}</div>
50+
~~~~~~~~~~ [0]
51+
52+
<input placeholder="&nbsp;" />
53+
~~~~~~~~ [1]
54+
55+
[0]: String literals are disallowed as JSX. Use a translation function
56+
[1]: String literal is not allowed for value of placeholder. Use a translation function
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"rules": {
3+
"jsx-use-translation-function": {
4+
"options": ["allow-punctuation"]
5+
}
6+
}
7+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<div>Hello world!</div>
2+
~~~~~~~~~~~~ [0]
3+
4+
<div>{'Hello world!'}</div>
5+
~~~~~~~~~~~~~~~~ [0]
6+
7+
<div>{translate('hello-world')}</div>
8+
9+
<input placeholder={translate('name')} />
10+
<input placeholder="Name" />
11+
~~~~~~ [1]
12+
13+
<input placeholder={translate('name')} />
14+
<input placeholder={"Name"} />
15+
~~~~~~~~ [1]
16+
17+
<div>
18+
<div>{translate('hi')}</div>
19+
</div>
20+
21+
<div>
22+
<span>{translate('this')}</span>is bad<span>
23+
~~~~~~ [0]
24+
</div>
25+
26+
<div>{`foo`}</div>
27+
~~~~~~~ [0]
28+
29+
<div>{`foo ${1}`}</div>
30+
31+
<ul>
32+
<li>{translate('one')}</li>
33+
Two
34+
~~~
35+
<li>Three</li>
36+
~~~~ [0]
37+
~~~~~ [0]
38+
</ul>
39+
40+
<div> - </div>
41+
~~ [0]
42+
43+
<div>{' - '}</div>
44+
~~~~~~~ [0]
45+
46+
<input placeholder="-" />
47+
~~~ [1]
48+
49+
<div>&nbsp;</div>
50+
~~~~~~ [0]
51+
52+
<div>{'&nbsp;'}</div>
53+
~~~~~~~~~~ [0]
54+
55+
<input placeholder="&nbsp;" />
56+
~~~~~~~~ [1]
57+
58+
[0]: String literals are disallowed as JSX. Use a translation function
59+
[1]: String literal is not allowed for value of placeholder. Use a translation function

0 commit comments

Comments
 (0)