Skip to content

Commit

Permalink
Add options to allow HTML entities and/or punctuation to be excluded …
Browse files Browse the repository at this point in the history
…from translation check (#139)
  • Loading branch information
praxxis authored and adidahiya committed Feb 12, 2018
1 parent 6ab778b commit f616c64
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 10 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ The built-in configuration preset you get with `"extends": "tslint-react"` is se
- Rule options: _none_
- `jsx-use-translation-function` (since v2.4.0)
- Enforces use of a translation function. Plain string literals are disallowed in JSX when enabled.
- Rule options: _none_
- Rule options: `["allow-punctuation", "allow-htmlentities"]`
- Off by default
- `jsx-self-close` (since v0.4.0)
- Enforces that JSX elements with no children are self-closing.
Expand Down
76 changes: 67 additions & 9 deletions src/rules/jsxUseTranslationFunctionRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,42 +16,100 @@
*/

import * as Lint from "tslint";
import { isJsxAttribute, isJsxElement, isJsxExpression, isJsxText, isStringLiteral } from "tsutils";
import { isJsxAttribute, isJsxElement, isJsxExpression, isJsxText, isTextualLiteral } from "tsutils";
import * as ts from "typescript";

interface IOptions {
allowPunctuation: boolean;
allowHtmlEntities: boolean;
}

export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "jsx-use-translation-function",
description: Lint.Utils.dedent`
Enforces use of a translation function. Most plain string literals are disallowed in JSX when enabled.`,
options: {
type: "array",
items: {
type: "string",
enum: ["allow-punctuation", "allow-htmlentities"],
},
},
optionsDescription: Lint.Utils.dedent`
Whether to allow punctuation and or HTML entities`,
type: "functionality",
typescriptOnly: false,
};
/* tslint:enable:object-literal-sort-keys */

public static TRANSLATABLE_ATTRIBUTES = new Set(["placeholder", "title", "alt"]);
public static FAILURE_STRING = "String literals are disallowed as JSX. Use a translation function";
public static FAILURE_STRING_FACTORY = (text: string) =>
`String literal is not allowed for value of ${text}. Use a translation function`

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
return this.applyWithFunction(sourceFile, walk, {
allowHtmlEntities: this.ruleArguments.indexOf("allow-htmlentities") !== -1,
allowPunctuation: this.ruleArguments.indexOf("allow-punctuation") !== -1,
});
}
}

function walk(ctx: Lint.WalkContext<void>) {
function walk(ctx: Lint.WalkContext<IOptions>) {
return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
if (isJsxElement(node)) {

for (const child of node.children) {
if (isJsxText(child) && child.getText().trim() !== "") {
if (isJsxText(child) && isInvalidText(child.getText(), ctx.options)) {
ctx.addFailureAtNode(child, Rule.FAILURE_STRING);
}

if (isJsxExpression(child)
&& child.expression !== undefined
&& (isStringLiteral(child.expression)
|| child.expression.kind === ts.SyntaxKind.FirstTemplateToken)) {
ctx.addFailureAtNode(child, Rule.FAILURE_STRING);
&& isTextualLiteral(child.expression)) {
if (isInvalidText(child.expression.text, ctx.options)) {
ctx.addFailureAtNode(child, Rule.FAILURE_STRING);
}
}
}

} else if (isJsxAttribute(node)) {
if (Rule.TRANSLATABLE_ATTRIBUTES.has(node.name.text) && node.initializer !== undefined) {
if (isStringLiteral(node.initializer)
|| (isJsxExpression(node.initializer) && isStringLiteral(node.initializer.expression!))) {
if (isTextualLiteral(node.initializer) && isInvalidText(node.initializer.text, ctx.options)) {
ctx.addFailureAtNode(node.initializer, Rule.FAILURE_STRING_FACTORY(node.name.text));
}

if (isJsxExpression(node.initializer) && isTextualLiteral(node.initializer.expression!)) {
if (isInvalidText((node.initializer.expression as ts.LiteralExpression).text, ctx.options)) {
ctx.addFailureAtNode(node.initializer, Rule.FAILURE_STRING_FACTORY(node.name.text));
}
}
}
}
return ts.forEachChild(node, cb);
});
}

function isInvalidText(text: string, options: Readonly<IOptions>) {
const t = text.trim();

if (t === "") {
return false;
}

let invalid = true;

if (options.allowPunctuation) {
invalid = /\w/.test(t);
}

if (options.allowHtmlEntities && t.indexOf("&") !== -1) {
invalid = t.split("&")
.filter((entity) => entity !== "")
.some((entity) => /^&(?:#[0-9]+|[a-zA-Z]+);$/.test(`&${entity}`) !== true);
}

return invalid;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,20 @@
~~~~~ [0]
</ul>

<div> - </div>
~~ [0]

<div>{' - '}</div>
~~~~~~~ [0]

<input placeholder="-" />
~~~ [1]

<div>&nbsp;</div>

<div>{'&nbsp;'}</div>

<input placeholder="&nbsp;" />

[0]: String literals are disallowed as JSX. Use a translation function
[1]: String literal is not allowed for value of placeholder. Use a translation function
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"rules": {
"jsx-use-translation-function": {
"options": ["allow-htmlentities"]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<div>Hello world!</div>
~~~~~~~~~~~~ [0]

<div>{'Hello world!'}</div>
~~~~~~~~~~~~~~~~ [0]

<div>{translate('hello-world')}</div>

<input placeholder={translate('name')} />
<input placeholder="Name" />
~~~~~~ [1]

<input placeholder={translate('name')} />
<input placeholder={"Name"} />
~~~~~~~~ [1]

<div>
<div>{translate('hi')}</div>
</div>

<div>
<span>{translate('this')}</span>is bad<span>
~~~~~~ [0]
</div>

<div>{`foo`}</div>
~~~~~~~ [0]

<div>{`foo ${1}`}</div>

<ul>
<li>{translate('one')}</li>
Two
~~~
<li>Three</li>
~~~~ [0]
~~~~~ [0]
</ul>

<div> - </div>

<div>{' - '}</div>

<input placeholder="-" />

<div>&nbsp;</div>
~~~~~~ [0]

<div>{'&nbsp;'}</div>
~~~~~~~~~~ [0]

<input placeholder="&nbsp;" />
~~~~~~~~ [1]

[0]: String literals are disallowed as JSX. Use a translation function
[1]: String literal is not allowed for value of placeholder. Use a translation function
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"rules": {
"jsx-use-translation-function": {
"options": ["allow-punctuation"]
}
}
}
59 changes: 59 additions & 0 deletions test/rules/jsx-use-translation-function/default/test.tsx.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<div>Hello world!</div>
~~~~~~~~~~~~ [0]

<div>{'Hello world!'}</div>
~~~~~~~~~~~~~~~~ [0]

<div>{translate('hello-world')}</div>

<input placeholder={translate('name')} />
<input placeholder="Name" />
~~~~~~ [1]

<input placeholder={translate('name')} />
<input placeholder={"Name"} />
~~~~~~~~ [1]

<div>
<div>{translate('hi')}</div>
</div>

<div>
<span>{translate('this')}</span>is bad<span>
~~~~~~ [0]
</div>

<div>{`foo`}</div>
~~~~~~~ [0]

<div>{`foo ${1}`}</div>

<ul>
<li>{translate('one')}</li>
Two
~~~
<li>Three</li>
~~~~ [0]
~~~~~ [0]
</ul>

<div> - </div>
~~ [0]

<div>{' - '}</div>
~~~~~~~ [0]

<input placeholder="-" />
~~~ [1]

<div>&nbsp;</div>
~~~~~~ [0]

<div>{'&nbsp;'}</div>
~~~~~~~~~~ [0]

<input placeholder="&nbsp;" />
~~~~~~~~ [1]

[0]: String literals are disallowed as JSX. Use a translation function
[1]: String literal is not allowed for value of placeholder. Use a translation function

0 comments on commit f616c64

Please sign in to comment.