Skip to content

Add a no-single-quotes rule to enforce a fancy closing single quote #5

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ npm install eslint-plugin-gettext --save-dev
"plugins": ["gettext"],
"rules": {
"gettext/no-variable-string": "error",
"gettext/required-positional-markers-for-multiple-variables": "error"
"gettext/required-positional-markers-for-multiple-variables": "error",
"gettext/no-single-quotes": "error"
}
}
```
Expand Down Expand Up @@ -66,6 +67,18 @@ gettext('There is %d more event in the game.')
ngettext('cat %1$s $2$s', '%1$d cats %2$d dogs', count)
```

### `gettext/no-single-quotes`

Require that literal strings do not contain a regular single quote (`'`), which implies a preference for the closing single quote (`’`) instead. This only applies to literal strings and would build on the above rules to enforce literal strings.

``` js
// Disallows:
gettext('The cat\\'s paw is dirty.')

// Allows:
gettext('The cat’s paw is dirty.')
```

## License

MIT © [App Annie](https://www.appannie.com/en/about/careers/engineering/)
Expand Down
190 changes: 190 additions & 0 deletions __tests__/lib/rules/no-single-quotes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
'use strict';

const ruleNoSingleQuote = require('../../../lib/rules/no-single-quotes');
const RuleTester = require('eslint').RuleTester;

const ruleTester = new RuleTester();
const invalidMessage =
'Unexpected single quote, gettext function only allows usage of closing single quotes.';

ruleTester.run('no-single-quotes', ruleNoSingleQuote, {
valid: [
"gettext('Don’t do it')",
"i18n.gettext('Don’t do it')",
"ngettext('cat’s paw', '%d cat’s paws', 5)",
"ngettext('cat’s paw', '%d cat’s paws', count)",
"i18n.ngettext('cat’s paw', '%d cat’s paws', 5)",
"pgettext('homepage', 'Don’t do it')",
"i18n.pgettext('homePage', 'Don’t do it')",
"npgettext('homepage', 'cat’s paw', '%d cat’s paws', 5)",
"i18n.npgettext('homepage', 'cat’s paw', '%d cat’s paws', 5)",
'gettext(undefined)',
'gettext(123)',
'gettext(hello)',
'i18n.gettext(hello)',
'pgettext(123, 456)',
"pgettext('homepage', hello)",
"pgettext('cat\\'s paw', 'whatever')",
"npgettext('cat\\'s paw', 'whatever', '%d whatevers', 5)",
],
invalid: [
{
code: 'gettext()',
errors: [
{
message: 'Here required 1 arguments, actually get 0 arguments.',
type: 'Identifier',
},
],
},
{
code: 'gettext(null)',
errors: [
{
message: 'Here should require valid arguments, not null',
type: 'Literal',
},
],
},
{
code: 'gettext("don\'t do it")',
errors: [
{
message: invalidMessage,
type: 'Literal',
},
],
},
{
code: 'ngettext()',
errors: [
{
message: 'Here required 3 arguments, actually get 0 arguments.',
type: 'Identifier',
},
{
message: 'Here required 3 arguments, actually get 0 arguments.',
type: 'Identifier',
},
{
message: 'Here required 3 arguments, actually get 0 arguments.',
type: 'Identifier',
},
],
},
{
code: "ngettext('cat\\'s paw', 5)",
errors: [
{
message: 'Here required 3 arguments, actually get 2 arguments.',
type: 'Identifier',
},
{
message: invalidMessage,
type: 'Literal',
},
],
},
{
code: "ngettext(null, 'cat\\'s paw', 5)",
errors: [
{
message: 'Here should require valid arguments, not null',
type: 'Literal',
},
{
message: invalidMessage,
type: 'Literal',
},
],
},
{
code: "ngettext(cat, 'cat\\'s paw', 5)",
errors: [
{
message: invalidMessage,
type: 'Literal',
},
],
},
{
code: "ngettext('cat\\'s paw', cats, 5)",
errors: [
{
message: invalidMessage,
type: 'Literal',
},
],
},
{
code: "pgettext(homepage, 'hello cat\\'s paw')",
errors: [
{
message: invalidMessage,
type: 'Literal',
},
],
},
{
code: "npgettext(null, 'cat\\'s paw', 'cat\\'s paws', 5)",
errors: [
{
message: 'Here should require valid arguments, not null',
type: 'Literal',
},
{
message: invalidMessage,
type: 'Literal',
},
{
message: invalidMessage,
type: 'Literal',
},
],
},
{
code: "npgettext(undefined, 'cat\\'s paw', 'cat\\'s paws', 5)",
errors: [
{
message: invalidMessage,
type: 'Literal',
},
{
message: invalidMessage,
type: 'Literal',
},
],
},
{
code: "npgettext('my', 'cat\\'s paw', cats, 5)",
errors: [
{
message: invalidMessage,
type: 'Literal',
},
],
},
{
code: "npgettext(homepage, 'cat\\'s paw', 'cat\\'s paws', 5)",
errors: [
{
message: invalidMessage,
type: 'Literal',
},
{
message: invalidMessage,
type: 'Literal',
},
],
},
{
code: "npgettext('homepage', cat, '%d cat\\'s paws', 5)",
errors: [
{
message: invalidMessage,
type: 'Literal',
},
],
},
],
});
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ module.exports = {
rules: {
'no-variable-string': require('./lib/rules/no-variable-string'),
'required-positional-markers-for-multiple-variables': require('./lib/rules/required-positional-markers-for-multiple-variables'),
'no-single-quotes': require('./lib/rules/no-single-quotes'),
},
rulesConfig: {
'no-variable-string': 0,
'no-single-quotes': 0,
},
};
96 changes: 96 additions & 0 deletions lib/rules/no-single-quotes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use strict';

const utils = require('../utils');

const errorMsg =
'Unexpected single quote, gettext function only allows usage of closing single quotes.';

const i18nMethodMap = {
gettext(context, node) {
utils.checkRequiredArgument(context, node, 1);

const keyTxt = node.arguments[0];
if (utils.hasSingleQuoteInString(keyTxt)) {
const reportNode = utils.getReportNode(keyTxt, node);
context.report(reportNode, errorMsg);
return true;
}
return false;
},
ngettext(context, node) {
utils.checkRequiredArgument(context, node, 3);

let catchError = false;
const keyTxt = node.arguments[0];
const pluralTxt = node.arguments[1];

if (utils.hasSingleQuoteInString(keyTxt)) {
context.report(utils.getReportNode(keyTxt, node), errorMsg);
catchError = true;
}

if (utils.hasSingleQuoteInString(pluralTxt)) {
context.report(utils.getReportNode(pluralTxt, node), errorMsg);
catchError = true;
}

return catchError;
},
pgettext(context, node) {
utils.checkRequiredArgument(context, node, 2);

let catchError = false;
const keyTxt = node.arguments[1];

if (utils.hasSingleQuoteInString(keyTxt)) {
context.report(utils.getReportNode(keyTxt, node), errorMsg);
catchError = true;
}

return catchError;
},
npgettext(context, node) {
utils.checkRequiredArgument(context, node, 4);

let catchError = false;
const keyTxt = node.arguments[1];
const pluralTxt = node.arguments[2];

if (utils.hasSingleQuoteInString(keyTxt)) {
context.report(utils.getReportNode(keyTxt, node), errorMsg);
catchError = true;
}

if (utils.hasSingleQuoteInString(pluralTxt)) {
context.report(utils.getReportNode(pluralTxt, node), errorMsg);
catchError = true;
}

return catchError;
},
};

module.exports = {
meta: {
docs: {
description:
'Translated string literals should not contain a regular single quote. A closing single quote should be used instead.',
recommended: false,
},
fixable: null,
schema: [],
},

create: function(context) {
return {
CallExpression(node) {
Object.keys(i18nMethodMap).find(apiName => {
if (utils.isI18nAPICall(node, apiName)) {
return i18nMethodMap[apiName](context, node);
}
return false;
});
},
};
},
};
5 changes: 5 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ function isStringLiteral(node) {
return node && node.type === 'Literal' && typeof node.value === 'string';
}

function hasSingleQuoteInString(node) {
return isStringLiteral(node) && node.value.includes("'");
}

const hasMultipleVaribles = str => {
if (str.match(variableMarkerReg) && str.match(variablePositionMarkerReg)) {
return true;
Expand Down Expand Up @@ -50,6 +54,7 @@ ${args.length} arguments.`;
module.exports = {
checkRequiredArgument: checkRequiredArgument,
isStringLiteral: isStringLiteral,
hasSingleQuoteInString: hasSingleQuoteInString,
isI18nAPICall: isI18nAPICall,
getReportNode: getReportNode,
hasMultipleVaribles: hasMultipleVaribles,
Expand Down