diff --git a/README.md b/README.md index 61bdc55..e3984ff 100644 --- a/README.md +++ b/README.md @@ -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" } } ``` @@ -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/) diff --git a/__tests__/lib/rules/no-single-quotes.js b/__tests__/lib/rules/no-single-quotes.js new file mode 100644 index 0000000..039cba1 --- /dev/null +++ b/__tests__/lib/rules/no-single-quotes.js @@ -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', + }, + ], + }, + ], +}); diff --git a/index.js b/index.js index df94ea4..f1f9a70 100644 --- a/index.js +++ b/index.js @@ -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, }, }; diff --git a/lib/rules/no-single-quotes.js b/lib/rules/no-single-quotes.js new file mode 100644 index 0000000..d73a4ca --- /dev/null +++ b/lib/rules/no-single-quotes.js @@ -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; + }); + }, + }; + }, +}; diff --git a/lib/utils.js b/lib/utils.js index 9a99b5e..cfbe32a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -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; @@ -50,6 +54,7 @@ ${args.length} arguments.`; module.exports = { checkRequiredArgument: checkRequiredArgument, isStringLiteral: isStringLiteral, + hasSingleQuoteInString: hasSingleQuoteInString, isI18nAPICall: isI18nAPICall, getReportNode: getReportNode, hasMultipleVaribles: hasMultipleVaribles,