Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options to allow HTML entities and/or punctuation to be excluded from translation check #139

Merged
merged 3 commits into from
Feb 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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