Browse Source

Feature: Mobile - Add ESLint rule for translatable strings.

Martin Gruner 3 years ago
parent
commit
01f0b37e43

+ 81 - 0
.eslint/lib/rules/zammad-detect-translatable-string.js

@@ -0,0 +1,81 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+/**
+ * @fileoverview Detect unmarked translatable strings
+ * @author Martin Gruner
+ */
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+/**
+ * @type {import('eslint').Rule.RuleModule}
+ */
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'Detect unmarked translatable strings',
+      category: 'Layout & Formatting',
+      recommended: true,
+      url: null,
+    },
+    fixable: 'code',
+    schema: [],
+  },
+
+  create(context) {
+    const IGNORE_STRING_PATTERNS = [
+      /^[^A-Z]/, // Only look at strings starting with upper case letters
+      /\$\{/, // Ignore strings with interpolation
+    ]
+    const IGNORE_METHODS = ['__']
+    const IGNORE_OBJECTS = ['log', 'console', 'i18n']
+
+    return {
+      Literal(node) {
+        if (typeof node.value === 'string') {
+          string = node.value
+
+          // Ignore strings with less than two words.
+          if (string.split(' ').length < 2) return
+
+          for (const pattern of IGNORE_STRING_PATTERNS) {
+            if (string.match(pattern)) return
+          }
+
+          // Ignore strings used for comparison
+          const tokenBefore = context.getTokenBefore(node)
+          if (
+            tokenBefore &&
+            tokenBefore.type === 'Punctuator' &&
+            ['==', '==='].includes(tokenBefore.value)
+          ) {
+            return
+          }
+
+          const { parent } = node
+
+          if (parent.type === 'CallExpression') {
+            if (IGNORE_METHODS.includes(parent.callee.name)) return
+            if (parent.callee.type === 'MemberExpression') {
+              if (IGNORE_OBJECTS.includes(parent.callee.object.name)) return
+            }
+          }
+
+          // console.log(node, parent)
+
+          context.report({
+            node,
+            message:
+              'This string looks like it should be marked as translatable via __(...)',
+            fix(fixer) {
+              return fixer.replaceText(node, `__(${node.raw})`)
+            },
+          })
+        }
+      },
+    }
+  },
+}

+ 2 - 2
.eslint/package.json

@@ -17,8 +17,8 @@
     "requireindex": "^1.2.0"
   },
   "devDependencies": {
-    "eslint": "^8.0.1",
-    "eslint-plugin-eslint-plugin": "^4.0.1",
+    "eslint": "^8.6.0",
+    "eslint-plugin-eslint-plugin": "^4.1.0",
     "eslint-plugin-node": "^11.1.0",
     "mocha": "^9.1.3"
   },

+ 66 - 0
.eslint/tests/lib/rules/zammad-detect-translatable-string.js

@@ -0,0 +1,66 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+/**
+ * @fileoverview Detect unmarked translatable strings
+ * @author Martin Gruner
+ */
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const { RuleTester } = require('eslint')
+const rule = require('../../../lib/rules/zammad-detect-translatable-string')
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester()
+ruleTester.run('zammad-detect-translatable-string', rule, {
+  valid: [
+    {
+      filename: 'test.ts',
+      code: `'OnlyOneWord'`,
+    },
+    {
+      filename: 'test.ts',
+      code: `'starts with lower case'`,
+    },
+    {
+      filename: 'test.ts',
+      code: `if (variable === 'Some test string') true`,
+    },
+    {
+      filename: 'test.ts',
+      code: `__('Already marked message.')`,
+    },
+    {
+      filename: 'test.ts',
+      code: `i18n.t('Already translated string.')`,
+    },
+    {
+      filename: 'test.ts',
+      code: `console.log('Some debug message.')`,
+    },
+    {
+      filename: 'test.ts',
+      // eslint-disable-next-line no-template-curly-in-string
+      code: '"String with ${interpolation}..."', // Not fully correct, but a ``-template string does not seem to work.
+    },
+  ],
+
+  invalid: [
+    {
+      filename: 'test.js',
+      code: `'String that should be translatable'`,
+      errors: [
+        {
+          message:
+            'This string looks like it should be marked as translatable via __(...)',
+        },
+      ],
+      output: `__('String that should be translatable')`,
+    },
+  ],
+})

+ 42 - 42
.eslint/yarn.lock

@@ -2,14 +2,14 @@
 # yarn lockfile v1
 
 
-"@eslint/eslintrc@^1.0.4":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.4.tgz#dfe0ff7ba270848d10c5add0715e04964c034b31"
-  integrity sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==
+"@eslint/eslintrc@^1.0.5":
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318"
+  integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==
   dependencies:
     ajv "^6.12.4"
     debug "^4.3.2"
-    espree "^9.0.0"
+    espree "^9.2.0"
     globals "^13.9.0"
     ignore "^4.0.6"
     import-fresh "^3.2.1"
@@ -17,16 +17,16 @@
     minimatch "^3.0.4"
     strip-json-comments "^3.1.1"
 
-"@humanwhocodes/config-array@^0.6.0":
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.6.0.tgz#b5621fdb3b32309d2d16575456cbc277fa8f021a"
-  integrity sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==
+"@humanwhocodes/config-array@^0.9.2":
+  version "0.9.2"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914"
+  integrity sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==
   dependencies:
-    "@humanwhocodes/object-schema" "^1.2.0"
+    "@humanwhocodes/object-schema" "^1.2.1"
     debug "^4.1.1"
     minimatch "^3.0.4"
 
-"@humanwhocodes/object-schema@^1.2.0":
+"@humanwhocodes/object-schema@^1.2.1":
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
   integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
@@ -41,10 +41,10 @@ acorn-jsx@^5.3.1:
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
 
-acorn@^8.5.0:
-  version "8.5.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2"
-  integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==
+acorn@^8.7.0:
+  version "8.7.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
+  integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
 
 ajv@^6.10.0, ajv@^6.12.4:
   version "6.12.6"
@@ -243,10 +243,10 @@ eslint-plugin-es@^3.0.0:
     eslint-utils "^2.0.0"
     regexpp "^3.0.0"
 
-eslint-plugin-eslint-plugin@^4.0.1:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-4.0.2.tgz#80f63080c0a5b444a949ef1ed5f79a4dc2ef4ef4"
-  integrity sha512-EUNNLrWvwQ7QU6GGYWGMw7IHGSIGDn5GCbnQlzFHH5ypgL1gR9Fk0dUnOpzyPaI+OLc6rAs7Askv+IPFUKJdOQ==
+eslint-plugin-eslint-plugin@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-4.1.0.tgz#40ae944d79e845dc9d4a85328eea3c5bf4ae0f7d"
+  integrity sha512-QJVw+WYXJuG2469gx5G929bz7crfxySDlK1i569FkuT6dpeHDeP7MmDrKaswCx17snG25LRFD6wmVX+AO5x7Qg==
   dependencies:
     eslint-utils "^3.0.0"
     estraverse "^5.2.0"
@@ -263,10 +263,10 @@ eslint-plugin-node@^11.1.0:
     resolve "^1.10.1"
     semver "^6.1.0"
 
-eslint-scope@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-6.0.0.tgz#9cf45b13c5ac8f3d4c50f46a5121f61b3e318978"
-  integrity sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==
+eslint-scope@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153"
+  integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==
   dependencies:
     esrecurse "^4.3.0"
     estraverse "^5.2.0"
@@ -295,18 +295,18 @@ eslint-visitor-keys@^2.0.0:
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
   integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
 
-eslint-visitor-keys@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz#e32e99c6cdc2eb063f204eda5db67bfe58bb4186"
-  integrity sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==
+eslint-visitor-keys@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2"
+  integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==
 
-eslint@^8.0.1:
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.2.0.tgz#44d3fb506d0f866a506d97a0fc0e90ee6d06a815"
-  integrity sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw==
+eslint@^8.6.0:
+  version "8.6.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.6.0.tgz#4318c6a31c5584838c1a2e940c478190f58d558e"
+  integrity sha512-UvxdOJ7mXFlw7iuHZA4jmzPaUqIw54mZrv+XPYKNbKdLR0et4rf60lIZUU9kiNtnzzMzGWxMV+tQ7uG7JG8DPw==
   dependencies:
-    "@eslint/eslintrc" "^1.0.4"
-    "@humanwhocodes/config-array" "^0.6.0"
+    "@eslint/eslintrc" "^1.0.5"
+    "@humanwhocodes/config-array" "^0.9.2"
     ajv "^6.10.0"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
@@ -314,10 +314,10 @@ eslint@^8.0.1:
     doctrine "^3.0.0"
     enquirer "^2.3.5"
     escape-string-regexp "^4.0.0"
-    eslint-scope "^6.0.0"
+    eslint-scope "^7.1.0"
     eslint-utils "^3.0.0"
-    eslint-visitor-keys "^3.0.0"
-    espree "^9.0.0"
+    eslint-visitor-keys "^3.1.0"
+    espree "^9.3.0"
     esquery "^1.4.0"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
@@ -344,14 +344,14 @@ eslint@^8.0.1:
     text-table "^0.2.0"
     v8-compile-cache "^2.0.3"
 
-espree@^9.0.0:
-  version "9.0.0"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.0.0.tgz#e90a2965698228502e771c7a58489b1a9d107090"
-  integrity sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==
+espree@^9.2.0, espree@^9.3.0:
+  version "9.3.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.0.tgz#c1240d79183b72aaee6ccfa5a90bc9111df085a8"
+  integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==
   dependencies:
-    acorn "^8.5.0"
+    acorn "^8.7.0"
     acorn-jsx "^5.3.1"
-    eslint-visitor-keys "^3.0.0"
+    eslint-visitor-keys "^3.1.0"
 
 esquery@^1.4.0:
   version "1.4.0"

+ 7 - 0
.eslintrc.js

@@ -31,6 +31,7 @@ module.exports = {
   ],
   rules: {
     'zammad/zammad-copyright': 'error',
+    'zammad/zammad-detect-translatable-string': 'error',
     'vue/script-setup-uses-vars': 'error',
     'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
     'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
@@ -119,6 +120,12 @@ module.exports = {
         '@typescript-eslint/no-var-requires': 'off',
       },
     },
+    {
+      files: ['app/frontend/tests/**', 'app/frontend/stories/**', '.eslint/**'],
+      rules: {
+        'zammad/zammad-detect-translatable-string': 'off',
+      },
+    },
   ],
   settings: {
     'import/resolver': {

+ 2 - 1
app/frontend/common/server/apollo/handler/BaseHandler.ts

@@ -30,8 +30,9 @@ export default abstract class BaseHandler<
 
   protected baseHandlerOptions: BaseHandlerOptions = {
     errorShowNotification: true,
-    errorNotitifactionMessage:
+    errorNotitifactionMessage: __(
       'An error occured during the operation. Please contact your administrator.',
+    ),
     errorNotitifactionType: NotificationTypes.ERROR,
   }