Repository: edvardchen/eslint-plugin-i18next (js)

Issue:
Notice and skip emojis and HTML entities
First of all, this plugin is life-changing for my team of lazy devs :joy: 

But then, we noticed we every now and then need to use the (verbose) `eslint-disable` comment in places that really don't need to be translated, namely strings composed of emojis or entities/symbols (such as the infamous `&times;` for popup closure).

It would be great if the plugin was able to identify that and skip notifying.

Pull request description:
Ignore emoji and html entities
closes #105  #93

Patch:
diff --git a/docs/rules/no-literal-string.md b/docs/rules/no-literal-string.md
index 7e07234..aeffddc 100644
--- a/docs/rules/no-literal-string.md
+++ b/docs/rules/no-literal-string.md
@@ -125,7 +125,7 @@ const message = 'foob';
 - `message` defines the custom error message
 - `should-validate-template` decides if we should validate the string templates
 
-You can see [the default options here](../../lib/options/defaults.json)
+You can see [the default options here](../../lib/options/defaults.js)
 
 ## When Not To Use It
 
diff --git a/lib/helper/generateFullMatchRegExp.js b/lib/helper/generateFullMatchRegExp.js
index 854994a..cf704fb 100644
--- a/lib/helper/generateFullMatchRegExp.js
+++ b/lib/helper/generateFullMatchRegExp.js
@@ -3,8 +3,7 @@ module.exports = function generateFullMatchRegExp(source) {
     return source;
   }
   if (typeof source !== 'string') {
-    console.error('generateFullMatchRegExp: expect string but get', source);
-    return new RegExp();
+    throw new Error('generateFullMatchRegExp: expect string but get', source);
   }
   // allow dot ahead
   return new RegExp(`(^|\\.)${source}${source.endsWith('$') ? '' : '$'}`);
diff --git a/lib/options/defaults.js b/lib/options/defaults.js
new file mode 100644
index 0000000..1deba22
--- /dev/null
+++ b/lib/options/defaults.js
@@ -0,0 +1,55 @@
+module.exports = {
+  mode: 'jsx-text-only',
+  'jsx-components': {
+    include: [],
+    exclude: ['Trans'],
+  },
+  'jsx-attributes': {
+    include: [],
+    exclude: [
+      'className',
+      'styleName',
+      'style',
+      'type',
+      'key',
+      'id',
+      'width',
+      'height',
+    ],
+  },
+  words: {
+    exclude: [
+      '[0-9!-/:-@[-`{-~]+',
+      '[A-Z_-]+',
+      require('./htmlEntities'),
+      /^\p{Emoji}+$/u,
+    ],
+  },
+  callees: {
+    exclude: [
+      'i18n(ext)?',
+      't',
+      'require',
+      'addEventListener',
+      'removeEventListener',
+      'postMessage',
+      'getElementById',
+      'dispatch',
+      'commit',
+      'includes',
+      'indexOf',
+      'endsWith',
+      'startsWith',
+    ],
+  },
+  'object-properties': {
+    include: [],
+    exclude: ['[A-Z_-]+'],
+  },
+  'class-properties': {
+    include: [],
+    exclude: ['displayName'],
+  },
+  message: 'disallow literal string',
+  'should-validate-template': false,
+};
diff --git a/lib/options/defaults.json b/lib/options/defaults.json
deleted file mode 100644
index e54592b..0000000
--- a/lib/options/defaults.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
-  "mode": "jsx-text-only",
-  "jsx-components": {
-    "include": [],
-    "exclude": ["Trans"]
-  },
-  "jsx-attributes": {
-    "include": [],
-    "exclude": [
-      "className",
-      "styleName",
-      "style",
-      "type",
-      "key",
-      "id",
-      "width",
-      "height"
-    ]
-  },
-  "words": {
-    "exclude": ["[0-9!-/:-@[-`{-~]+", "[A-Z_-]+"]
-  },
-  "callees": {
-    "exclude": [
-      "i18n(ext)?",
-      "t",
-      "require",
-      "addEventListener",
-      "removeEventListener",
-      "postMessage",
-      "getElementById",
-      "dispatch",
-      "commit",
-      "includes",
-      "indexOf",
-      "endsWith",
-      "startsWith"
-    ]
-  },
-  "object-properties": {
-    "include": [],
-    "exclude": ["[A-Z_-]+"]
-  },
-  "class-properties": {
-    "include": [],
-    "exclude": ["displayName"]
-  },
-  "message": "disallow literal string",
-  "should-validate-template": false
-}
diff --git a/lib/options/htmlEntities.js b/lib/options/htmlEntities.js
new file mode 100644
index 0000000..68a7ebe
--- /dev/null
+++ b/lib/options/htmlEntities.js
@@ -0,0 +1,257 @@
+// copy from https://github.com/babel/babel/blob/8d17ae6/packages/babel-parser/src/plugins/jsx/xhtml.ts
+const entities = {
+  quot: '\u0022',
+  amp: '&',
+  apos: '\u0027',
+  lt: '<',
+  gt: '>',
+  nbsp: '\u00A0',
+  iexcl: '\u00A1',
+  cent: '\u00A2',
+  pound: '\u00A3',
+  curren: '\u00A4',
+  yen: '\u00A5',
+  brvbar: '\u00A6',
+  sect: '\u00A7',
+  uml: '\u00A8',
+  copy: '\u00A9',
+  ordf: '\u00AA',
+  laquo: '\u00AB',
+  not: '\u00AC',
+  shy: '\u00AD',
+  reg: '\u00AE',
+  macr: '\u00AF',
+  deg: '\u00B0',
+  plusmn: '\u00B1',
+  sup2: '\u00B2',
+  sup3: '\u00B3',
+  acute: '\u00B4',
+  micro: '\u00B5',
+  para: '\u00B6',
+  middot: '\u00B7',
+  cedil: '\u00B8',
+  sup1: '\u00B9',
+  ordm: '\u00BA',
+  raquo: '\u00BB',
+  frac14: '\u00BC',
+  frac12: '\u00BD',
+  frac34: '\u00BE',
+  iquest: '\u00BF',
+  Agrave: '\u00C0',
+  Aacute: '\u00C1',
+  Acirc: '\u00C2',
+  Atilde: '\u00C3',
+  Auml: '\u00C4',
+  Aring: '\u00C5',
+  AElig: '\u00C6',
+  Ccedil: '\u00C7',
+  Egrave: '\u00C8',
+  Eacute: '\u00C9',
+  Ecirc: '\u00CA',
+  Euml: '\u00CB',
+  Igrave: '\u00CC',
+  Iacute: '\u00CD',
+  Icirc: '\u00CE',
+  Iuml: '\u00CF',
+  ETH: '\u00D0',
+  Ntilde: '\u00D1',
+  Ograve: '\u00D2',
+  Oacute: '\u00D3',
+  Ocirc: '\u00D4',
+  Otilde: '\u00D5',
+  Ouml: '\u00D6',
+  times: '\u00D7',
+  Oslash: '\u00D8',
+  Ugrave: '\u00D9',
+  Uacute: '\u00DA',
+  Ucirc: '\u00DB',
+  Uuml: '\u00DC',
+  Yacute: '\u00DD',
+  THORN: '\u00DE',
+  szlig: '\u00DF',
+  agrave: '\u00E0',
+  aacute: '\u00E1',
+  acirc: '\u00E2',
+  atilde: '\u00E3',
+  auml: '\u00E4',
+  aring: '\u00E5',
+  aelig: '\u00E6',
+  ccedil: '\u00E7',
+  egrave: '\u00E8',
+  eacute: '\u00E9',
+  ecirc: '\u00EA',
+  euml: '\u00EB',
+  igrave: '\u00EC',
+  iacute: '\u00ED',
+  icirc: '\u00EE',
+  iuml: '\u00EF',
+  eth: '\u00F0',
+  ntilde: '\u00F1',
+  ograve: '\u00F2',
+  oacute: '\u00F3',
+  ocirc: '\u00F4',
+  otilde: '\u00F5',
+  ouml: '\u00F6',
+  divide: '\u00F7',
+  oslash: '\u00F8',
+  ugrave: '\u00F9',
+  uacute: '\u00FA',
+  ucirc: '\u00FB',
+  uuml: '\u00FC',
+  yacute: '\u00FD',
+  thorn: '\u00FE',
+  yuml: '\u00FF',
+  OElig: '\u0152',
+  oelig: '\u0153',
+  Scaron: '\u0160',
+  scaron: '\u0161',
+  Yuml: '\u0178',
+  fnof: '\u0192',
+  circ: '\u02C6',
+  tilde: '\u02DC',
+  Alpha: '\u0391',
+  Beta: '\u0392',
+  Gamma: '\u0393',
+  Delta: '\u0394',
+  Epsilon: '\u0395',
+  Zeta: '\u0396',
+  Eta: '\u0397',
+  Theta: '\u0398',
+  Iota: '\u0399',
+  Kappa: '\u039A',
+  Lambda: '\u039B',
+  Mu: '\u039C',
+  Nu: '\u039D',
+  Xi: '\u039E',
+  Omicron: '\u039F',
+  Pi: '\u03A0',
+  Rho: '\u03A1',
+  Sigma: '\u03A3',
+  Tau: '\u03A4',
+  Upsilon: '\u03A5',
+  Phi: '\u03A6',
+  Chi: '\u03A7',
+  Psi: '\u03A8',
+  Omega: '\u03A9',
+  alpha: '\u03B1',
+  beta: '\u03B2',
+  gamma: '\u03B3',
+  delta: '\u03B4',
+  epsilon: '\u03B5',
+  zeta: '\u03B6',
+  eta: '\u03B7',
+  theta: '\u03B8',
+  iota: '\u03B9',
+  kappa: '\u03BA',
+  lambda: '\u03BB',
+  mu: '\u03BC',
+  nu: '\u03BD',
+  xi: '\u03BE',
+  omicron: '\u03BF',
+  pi: '\u03C0',
+  rho: '\u03C1',
+  sigmaf: '\u03C2',
+  sigma: '\u03C3',
+  tau: '\u03C4',
+  upsilon: '\u03C5',
+  phi: '\u03C6',
+  chi: '\u03C7',
+  psi: '\u03C8',
+  omega: '\u03C9',
+  thetasym: '\u03D1',
+  upsih: '\u03D2',
+  piv: '\u03D6',
+  ensp: '\u2002',
+  emsp: '\u2003',
+  thinsp: '\u2009',
+  zwnj: '\u200C',
+  zwj: '\u200D',
+  lrm: '\u200E',
+  rlm: '\u200F',
+  ndash: '\u2013',
+  mdash: '\u2014',
+  lsquo: '\u2018',
+  rsquo: '\u2019',
+  sbquo: '\u201A',
+  ldquo: '\u201C',
+  rdquo: '\u201D',
+  bdquo: '\u201E',
+  dagger: '\u2020',
+  Dagger: '\u2021',
+  bull: '\u2022',
+  hellip: '\u2026',
+  permil: '\u2030',
+  prime: '\u2032',
+  Prime: '\u2033',
+  lsaquo: '\u2039',
+  rsaquo: '\u203A',
+  oline: '\u203E',
+  frasl: '\u2044',
+  euro: '\u20AC',
+  image: '\u2111',
+  weierp: '\u2118',
+  real: '\u211C',
+  trade: '\u2122',
+  alefsym: '\u2135',
+  larr: '\u2190',
+  uarr: '\u2191',
+  rarr: '\u2192',
+  darr: '\u2193',
+  harr: '\u2194',
+  crarr: '\u21B5',
+  lArr: '\u21D0',
+  uArr: '\u21D1',
+  rArr: '\u21D2',
+  dArr: '\u21D3',
+  hArr: '\u21D4',
+  forall: '\u2200',
+  part: '\u2202',
+  exist: '\u2203',
+  empty: '\u2205',
+  nabla: '\u2207',
+  isin: '\u2208',
+  notin: '\u2209',
+  ni: '\u220B',
+  prod: '\u220F',
+  sum: '\u2211',
+  minus: '\u2212',
+  lowast: '\u2217',
+  radic: '\u221A',
+  prop: '\u221D',
+  infin: '\u221E',
+  ang: '\u2220',
+  and: '\u2227',
+  or: '\u2228',
+  cap: '\u2229',
+  cup: '\u222A',
+  int: '\u222B',
+  there4: '\u2234',
+  sim: '\u223C',
+  cong: '\u2245',
+  asymp: '\u2248',
+  ne: '\u2260',
+  equiv: '\u2261',
+  le: '\u2264',
+  ge: '\u2265',
+  sub: '\u2282',
+  sup: '\u2283',
+  nsub: '\u2284',
+  sube: '\u2286',
+  supe: '\u2287',
+  oplus: '\u2295',
+  otimes: '\u2297',
+  perp: '\u22A5',
+  sdot: '\u22C5',
+  lceil: '\u2308',
+  rceil: '\u2309',
+  lfloor: '\u230A',
+  rfloor: '\u230B',
+  lang: '\u2329',
+  rang: '\u232A',
+  loz: '\u25CA',
+  spades: '\u2660',
+  clubs: '\u2663',
+  hearts: '\u2665',
+  diams: '\u2666',
+};
+module.exports = new RegExp(`^(${Object.values(entities).join('|')})+$`);
diff --git a/lib/rules/no-literal-string.js b/lib/rules/no-literal-string.js
index b1c385d..be3b6bc 100644
--- a/lib/rules/no-literal-string.js
+++ b/lib/rules/no-literal-string.js
@@ -50,7 +50,7 @@ module.exports = {
     const options = _.defaults(
       {},
       context.options[0],
-      require('../options/defaults.json')
+      require('../options/defaults')
     );
 
     const {


Test patch:
diff --git a/tests/lib/fixtures/invalid.jsx b/tests/lib/fixtures/invalid.jsx
index 4aed627..5dfdbe0 100644
--- a/tests/lib/fixtures/invalid.jsx
+++ b/tests/lib/fixtures/invalid.jsx
@@ -2,6 +2,8 @@ i18nextXt('taa');
 
 a + 'b';
 
+var emojis = "🌼🌺🌸hello world";
+
 switch (a) {
   case '':
     var a = 'b';
diff --git a/tests/lib/fixtures/valid.jsx b/tests/lib/fixtures/valid.jsx
index 30cb613..88d4c25 100644
--- a/tests/lib/fixtures/valid.jsx
+++ b/tests/lib/fixtures/valid.jsx
@@ -5,6 +5,8 @@ var aa = `hello world`;
 
 function bar(a = 'jianhua') { }
 
+var emojis = "🌼🌺🌸";
+
 name === 'Android' || name === 'iOS';
 
 switch (a) {
@@ -75,5 +77,6 @@ class Form extends Component {
   <Trans>
     <span>bar</span>
   </Trans>
+  <span>&times;&amp;</span>
   <div>{import("abc")}</div>
 </>;
diff --git a/tests/lib/rules/no-literal-string/all.js b/tests/lib/rules/no-literal-string/all.js
index 655cff5..b64abc7 100644
--- a/tests/lib/rules/no-literal-string/all.js
+++ b/tests/lib/rules/no-literal-string/all.js
@@ -4,7 +4,7 @@ const runTest = require('../../helpers/runTest');
 const cases = {
   valid: [testFile('valid.jsx')],
   invalid: [
-    { ...testFile('invalid.jsx'), errors: 13 },
+    { ...testFile('invalid.jsx'), errors: 14 },
     {
       code: `export const validationSchema = Yup.object({
         email: Yup
diff --git a/tests/lib/rules/no-literal-string/callees.js b/tests/lib/rules/no-literal-string/callees.js
index d48e29f..4588110 100644
--- a/tests/lib/rules/no-literal-string/callees.js
+++ b/tests/lib/rules/no-literal-string/callees.js
@@ -12,7 +12,7 @@ const cases = {
       options: [{ mode: 'all', callees: { exclude: ['foo.+'] } }],
     },
   ],
-  invalid: [{ ...testFile('invalid.jsx'), errors: 13 }],
+  invalid: [{ ...testFile('invalid.jsx'), errors: 14 }],
 };
 
 runTest('no-literal-string: callees', cases);
diff --git a/tests/lib/rules/no-literal-string/default.js b/tests/lib/rules/no-literal-string/default.js
index 5e59cf5..54f9c47 100644
--- a/tests/lib/rules/no-literal-string/default.js
+++ b/tests/lib/rules/no-literal-string/default.js
@@ -3,7 +3,7 @@ const runTest = require('../../helpers/runTest');
 
 const cases = {
   valid: [{ code: 'const a = "absfoo";<DIV abc="bcd" />' }],
-  invalid: [{ ...testFile('invalid.jsx'), errors: 13 }],
+  invalid: [{ ...testFile('invalid.jsx'), errors: 14 }],
 };
 
 runTest('no-literal-string: default', cases);