mediawiki/extensions/ContentTranslation (master)

sourcepatches
From 0c5579e53bc2ead1e2573666bbe318b7a5a6172f Mon Sep 17 00:00:00 2001
From: libraryupgrader <tools.libraryupgrader@tools.wmflabs.org>
Date: Sat, 6 Mar 2021 11:15:10 +0000
Subject: [PATCH] build: Updating eslint-config-wikimedia to 0.18.2

Change-Id: I9ee4c7adebdb036dcfb1883945c2876e0e0c047a
---
 app/package-lock.json | 123 +++++++----
 package-lock.json     | 502 ++++++++++++++----------------------------
 package.json          |   2 +-
 3 files changed, 238 insertions(+), 389 deletions(-)

diff --git a/app/package-lock.json b/app/package-lock.json
index 7f9df3b..532eeb5 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -5309,17 +5309,6 @@
             "unique-filename": "^1.1.1"
           }
         },
-        "chalk": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
-          "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
         "chownr": {
           "version": "1.1.4",
           "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
@@ -5618,42 +5607,6 @@
             "schema-utils": "^2.5.0"
           }
         },
-        "vue-loader-v16": {
-          "version": "npm:vue-loader@16.1.2",
-          "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
-          "integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "chalk": "^4.1.0",
-            "hash-sum": "^2.0.0",
-            "loader-utils": "^2.0.0"
-          },
-          "dependencies": {
-            "json5": {
-              "version": "2.2.0",
-              "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
-              "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
-              "dev": true,
-              "optional": true,
-              "requires": {
-                "minimist": "^1.2.5"
-              }
-            },
-            "loader-utils": {
-              "version": "2.0.0",
-              "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
-              "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
-              "dev": true,
-              "optional": true,
-              "requires": {
-                "big.js": "^5.2.2",
-                "emojis-list": "^3.0.0",
-                "json5": "^2.1.2"
-              }
-            }
-          }
-        },
         "wrap-ansi": {
           "version": "6.2.0",
           "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@@ -26392,6 +26345,82 @@
         }
       }
     },
+    "vue-loader-v16": {
+      "version": "npm:vue-loader@16.1.2",
+      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
+      "integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "chalk": "^4.1.0",
+        "hash-sum": "^2.0.0",
+        "loader-utils": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+          "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true,
+          "optional": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true,
+          "optional": true
+        },
+        "hash-sum": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz",
+          "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==",
+          "dev": true,
+          "optional": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
     "vue-router": {
       "version": "3.5.1",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.1.tgz",
diff --git a/package-lock.json b/package-lock.json
index 958a685..a2657e9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -552,9 +552,9 @@
 			}
 		},
 		"@eslint/eslintrc": {
-			"version": "0.3.0",
-			"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz",
-			"integrity": "sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==",
+			"version": "0.4.0",
+			"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz",
+			"integrity": "sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog==",
 			"dev": true,
 			"requires": {
 				"ajv": "^6.12.4",
@@ -564,23 +564,10 @@
 				"ignore": "^4.0.6",
 				"import-fresh": "^3.2.1",
 				"js-yaml": "^3.13.1",
-				"lodash": "^4.17.20",
 				"minimatch": "^3.0.4",
 				"strip-json-comments": "^3.1.1"
 			},
 			"dependencies": {
-				"ajv": {
-					"version": "6.12.6",
-					"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-					"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-					"dev": true,
-					"requires": {
-						"fast-deep-equal": "^3.1.1",
-						"fast-json-stable-stringify": "^2.0.0",
-						"json-schema-traverse": "^0.4.1",
-						"uri-js": "^4.2.2"
-					}
-				},
 				"debug": {
 					"version": "4.3.1",
 					"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
@@ -590,12 +577,6 @@
 						"ms": "2.1.2"
 					}
 				},
-				"lodash": {
-					"version": "4.17.20",
-					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-					"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
-					"dev": true
-				},
 				"ms": {
 					"version": "2.1.2",
 					"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -726,31 +707,29 @@
 			"dev": true
 		},
 		"acorn-jsx": {
-			"version": "5.2.0",
-			"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
-			"integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
+			"version": "5.3.1",
+			"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
+			"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
 			"dev": true
 		},
 		"ajv": {
-			"version": "6.12.2",
-			"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
-			"integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
+			"version": "6.12.6",
+			"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+			"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
 			"dev": true,
 			"requires": {
 				"fast-deep-equal": "^3.1.1",
 				"fast-json-stable-stringify": "^2.0.0",
 				"json-schema-traverse": "^0.4.1",
 				"uri-js": "^4.2.2"
-			},
-			"dependencies": {
-				"fast-deep-equal": {
-					"version": "3.1.1",
-					"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
-					"integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
-					"dev": true
-				}
 			}
 		},
+		"ansi-colors": {
+			"version": "4.1.1",
+			"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+			"integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+			"dev": true
+		},
 		"ansi-regex": {
 			"version": "5.0.0",
 			"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
@@ -1191,9 +1170,9 @@
 			"dev": true
 		},
 		"core-js": {
-			"version": "3.8.3",
-			"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz",
-			"integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==",
+			"version": "3.9.1",
+			"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.9.1.tgz",
+			"integrity": "sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==",
 			"dev": true
 		},
 		"core-util-is": {
@@ -1614,14 +1593,6 @@
 			"dev": true,
 			"requires": {
 				"ansi-colors": "^4.1.1"
-			},
-			"dependencies": {
-				"ansi-colors": {
-					"version": "4.1.1",
-					"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
-					"integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
-					"dev": true
-				}
 			}
 		},
 		"entities": {
@@ -1652,13 +1623,13 @@
 			"dev": true
 		},
 		"eslint": {
-			"version": "7.18.0",
-			"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz",
-			"integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==",
+			"version": "7.21.0",
+			"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.21.0.tgz",
+			"integrity": "sha512-W2aJbXpMNofUp0ztQaF40fveSsJBjlSCSWpy//gzfTvwC+USs/nceBrKmlJOiM8r1bLwP2EuYkCqArn/6QTIgg==",
 			"dev": true,
 			"requires": {
-				"@babel/code-frame": "^7.0.0",
-				"@eslint/eslintrc": "^0.3.0",
+				"@babel/code-frame": "7.12.11",
+				"@eslint/eslintrc": "^0.4.0",
 				"ajv": "^6.10.0",
 				"chalk": "^4.0.0",
 				"cross-spawn": "^7.0.2",
@@ -1669,9 +1640,9 @@
 				"eslint-utils": "^2.1.0",
 				"eslint-visitor-keys": "^2.0.0",
 				"espree": "^7.3.1",
-				"esquery": "^1.2.0",
+				"esquery": "^1.4.0",
 				"esutils": "^2.0.2",
-				"file-entry-cache": "^6.0.0",
+				"file-entry-cache": "^6.0.1",
 				"functional-red-black-tree": "^1.0.1",
 				"glob-parent": "^5.0.0",
 				"globals": "^12.1.0",
@@ -1696,213 +1667,88 @@
 				"v8-compile-cache": "^2.0.3"
 			},
 			"dependencies": {
-				"debug": {
-					"version": "4.3.1",
-					"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
-					"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
-					"dev": true,
-					"requires": {
-						"ms": "2.1.2"
-					}
-				},
-				"lodash": {
-					"version": "4.17.20",
-					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-					"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
-					"dev": true
-				},
-				"lru-cache": {
-					"version": "6.0.0",
-					"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-					"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+				"@babel/code-frame": {
+					"version": "7.12.11",
+					"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
+					"integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
 					"dev": true,
 					"requires": {
-						"yallist": "^4.0.0"
+						"@babel/highlight": "^7.10.4"
 					}
 				},
-				"ms": {
-					"version": "2.1.2",
-					"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-					"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+				"@babel/helper-validator-identifier": {
+					"version": "7.12.11",
+					"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
+					"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
 					"dev": true
 				},
-				"semver": {
-					"version": "7.3.4",
-					"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
-					"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
-					"dev": true,
-					"requires": {
-						"lru-cache": "^6.0.0"
-					}
-				},
-				"yallist": {
-					"version": "4.0.0",
-					"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-					"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-					"dev": true
-				}
-			}
-		},
-		"eslint-config-wikimedia": {
-			"version": "0.18.1",
-			"resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.18.1.tgz",
-			"integrity": "sha512-93nHVH4CGxYwelbTjJQLr0xqn4XIe9WwWeGL4wMnELZW/Aceg52aT7AgIdV0659ReLzrCgxLPDvbeqB47LjBTQ==",
-			"dev": true,
-			"requires": {
-				"eslint": "^7.17.0",
-				"eslint-plugin-compat": "^3.9.0",
-				"eslint-plugin-es": "^4.1.0",
-				"eslint-plugin-jsdoc": "^30.7.13",
-				"eslint-plugin-json-es": "^1.5.1",
-				"eslint-plugin-mediawiki": "^0.2.6",
-				"eslint-plugin-mocha": "^8.0.0",
-				"eslint-plugin-no-jquery": "^2.5.0",
-				"eslint-plugin-node": "^11.1.0",
-				"eslint-plugin-qunit": "^5.2.0",
-				"eslint-plugin-vue": "^7.4.1",
-				"eslint-plugin-wdio": "^6.0.12"
-			},
-			"dependencies": {
-				"@eslint/eslintrc": {
-					"version": "0.3.0",
-					"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz",
-					"integrity": "sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==",
+				"@babel/highlight": {
+					"version": "7.13.8",
+					"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.8.tgz",
+					"integrity": "sha512-4vrIhfJyfNf+lCtXC2ck1rKSzDwciqF7IWFhXXrSOUC2O5DrVp+w4c6ed4AllTxhTkUP5x2tYj41VaxdVMMRDw==",
 					"dev": true,
 					"requires": {
-						"ajv": "^6.12.4",
-						"debug": "^4.1.1",
-						"espree": "^7.3.0",
-						"globals": "^12.1.0",
-						"ignore": "^4.0.6",
-						"import-fresh": "^3.2.1",
-						"js-yaml": "^3.13.1",
-						"lodash": "^4.17.20",
-						"minimatch": "^3.0.4",
-						"strip-json-comments": "^3.1.1"
+						"@babel/helper-validator-identifier": "^7.12.11",
+						"chalk": "^2.0.0",
+						"js-tokens": "^4.0.0"
 					},
 					"dependencies": {
-						"ajv": {
-							"version": "6.12.6",
-							"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-							"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+						"chalk": {
+							"version": "2.4.2",
+							"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+							"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
 							"dev": true,
 							"requires": {
-								"fast-deep-equal": "^3.1.1",
-								"fast-json-stable-stringify": "^2.0.0",
-								"json-schema-traverse": "^0.4.1",
-								"uri-js": "^4.2.2"
+								"ansi-styles": "^3.2.1",
+								"escape-string-regexp": "^1.0.5",
+								"supports-color": "^5.3.0"
 							}
 						}
 					}
 				},
-				"acorn-jsx": {
-					"version": "5.3.1",
-					"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
-					"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
-					"dev": true
-				},
-				"debug": {
-					"version": "4.3.1",
-					"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
-					"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
-					"dev": true,
-					"requires": {
-						"ms": "2.1.2"
-					}
-				},
-				"eslint": {
-					"version": "7.18.0",
-					"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz",
-					"integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==",
+				"ansi-styles": {
+					"version": "3.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+					"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
 					"dev": true,
 					"requires": {
-						"@babel/code-frame": "^7.0.0",
-						"@eslint/eslintrc": "^0.3.0",
-						"ajv": "^6.10.0",
-						"chalk": "^4.0.0",
-						"cross-spawn": "^7.0.2",
-						"debug": "^4.0.1",
-						"doctrine": "^3.0.0",
-						"enquirer": "^2.3.5",
-						"eslint-scope": "^5.1.1",
-						"eslint-utils": "^2.1.0",
-						"eslint-visitor-keys": "^2.0.0",
-						"espree": "^7.3.1",
-						"esquery": "^1.2.0",
-						"esutils": "^2.0.2",
-						"file-entry-cache": "^6.0.0",
-						"functional-red-black-tree": "^1.0.1",
-						"glob-parent": "^5.0.0",
-						"globals": "^12.1.0",
-						"ignore": "^4.0.6",
-						"import-fresh": "^3.0.0",
-						"imurmurhash": "^0.1.4",
-						"is-glob": "^4.0.0",
-						"js-yaml": "^3.13.1",
-						"json-stable-stringify-without-jsonify": "^1.0.1",
-						"levn": "^0.4.1",
-						"lodash": "^4.17.20",
-						"minimatch": "^3.0.4",
-						"natural-compare": "^1.4.0",
-						"optionator": "^0.9.1",
-						"progress": "^2.0.0",
-						"regexpp": "^3.1.0",
-						"semver": "^7.2.1",
-						"strip-ansi": "^6.0.0",
-						"strip-json-comments": "^3.1.0",
-						"table": "^6.0.4",
-						"text-table": "^0.2.0",
-						"v8-compile-cache": "^2.0.3"
+						"color-convert": "^1.9.0"
 					}
 				},
-				"espree": {
-					"version": "7.3.1",
-					"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
-					"integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
+				"color-convert": {
+					"version": "1.9.3",
+					"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+					"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
 					"dev": true,
 					"requires": {
-						"acorn": "^7.4.0",
-						"acorn-jsx": "^5.3.1",
-						"eslint-visitor-keys": "^1.3.0"
-					},
-					"dependencies": {
-						"eslint-visitor-keys": {
-							"version": "1.3.0",
-							"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
-							"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
-							"dev": true
-						}
+						"color-name": "1.1.3"
 					}
 				},
-				"file-entry-cache": {
-					"version": "6.0.0",
-					"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz",
-					"integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==",
-					"dev": true,
-					"requires": {
-						"flat-cache": "^3.0.4"
-					}
+				"color-name": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+					"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+					"dev": true
 				},
-				"flat-cache": {
-					"version": "3.0.4",
-					"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
-					"integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+				"debug": {
+					"version": "4.3.1",
+					"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+					"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
 					"dev": true,
 					"requires": {
-						"flatted": "^3.1.0",
-						"rimraf": "^3.0.2"
+						"ms": "2.1.2"
 					}
 				},
-				"flatted": {
-					"version": "3.1.1",
-					"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz",
-					"integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
+				"has-flag": {
+					"version": "3.0.0",
+					"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+					"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
 					"dev": true
 				},
 				"lodash": {
-					"version": "4.17.20",
-					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-					"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+					"version": "4.17.21",
+					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+					"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
 					"dev": true
 				},
 				"lru-cache": {
@@ -1929,36 +1775,13 @@
 						"lru-cache": "^6.0.0"
 					}
 				},
-				"table": {
-					"version": "6.0.7",
-					"resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz",
-					"integrity": "sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g==",
+				"supports-color": {
+					"version": "5.5.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+					"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
 					"dev": true,
 					"requires": {
-						"ajv": "^7.0.2",
-						"lodash": "^4.17.20",
-						"slice-ansi": "^4.0.0",
-						"string-width": "^4.2.0"
-					},
-					"dependencies": {
-						"ajv": {
-							"version": "7.0.3",
-							"resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz",
-							"integrity": "sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==",
-							"dev": true,
-							"requires": {
-								"fast-deep-equal": "^3.1.1",
-								"json-schema-traverse": "^1.0.0",
-								"require-from-string": "^2.0.2",
-								"uri-js": "^4.2.2"
-							}
-						},
-						"json-schema-traverse": {
-							"version": "1.0.0",
-							"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
-							"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-							"dev": true
-						}
+						"has-flag": "^3.0.0"
 					}
 				},
 				"yallist": {
@@ -1969,6 +1792,26 @@
 				}
 			}
 		},
+		"eslint-config-wikimedia": {
+			"version": "0.18.2",
+			"resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.18.2.tgz",
+			"integrity": "sha512-OllUgce2qODU/6481jg/a1kT/dygBDY1xhxXuAiQdYxJARV6LXyuiJw+wl1QBQz+huV9NXRxoJGC3L6x/NzC4g==",
+			"dev": true,
+			"requires": {
+				"eslint": "^7.17.0",
+				"eslint-plugin-compat": "^3.9.0",
+				"eslint-plugin-es": "^4.1.0",
+				"eslint-plugin-jsdoc": "^30.7.13",
+				"eslint-plugin-json-es": "^1.5.1",
+				"eslint-plugin-mediawiki": "^0.2.7",
+				"eslint-plugin-mocha": "^8.0.0",
+				"eslint-plugin-no-jquery": "^2.5.0",
+				"eslint-plugin-node": "^11.1.0",
+				"eslint-plugin-qunit": "^5.2.0",
+				"eslint-plugin-vue": "^7.7.0",
+				"eslint-plugin-wdio": "^6.0.12"
+			}
+		},
 		"eslint-plugin-compat": {
 			"version": "3.9.0",
 			"resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-3.9.0.tgz",
@@ -1986,9 +1829,9 @@
 			},
 			"dependencies": {
 				"caniuse-lite": {
-					"version": "1.0.30001180",
-					"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001180.tgz",
-					"integrity": "sha512-n8JVqXuZMVSPKiPiypjFtDTXc4jWIdjxull0f92WLo7e1MSi3uJ3NvveakSh/aCl1QKFAvIz3vIj0v+0K+FrXw==",
+					"version": "1.0.30001196",
+					"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001196.tgz",
+					"integrity": "sha512-CPvObjD3ovWrNBaXlAIGWmg2gQQuJ5YhuciUOjPRox6hIQttu8O+b51dx6VIpIY9ESd2d0Vac1RKpICdG4rGUg==",
 					"dev": true
 				},
 				"semver": {
@@ -2034,9 +1877,9 @@
 					}
 				},
 				"lodash": {
-					"version": "4.17.20",
-					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-					"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+					"version": "4.17.21",
+					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+					"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
 					"dev": true
 				},
 				"lru-cache": {
@@ -2072,67 +1915,29 @@
 			}
 		},
 		"eslint-plugin-json-es": {
-			"version": "1.5.1",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-json-es/-/eslint-plugin-json-es-1.5.1.tgz",
-			"integrity": "sha512-YMzAWMcmKOYWiH0MsN3JOr0AdtZ2Rvmk3YmscsX1rHYJZRsL4KRo+yj9ktRk7S7mgy+G5TORWJ5D3/vH/u7R5A==",
+			"version": "1.5.3",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-json-es/-/eslint-plugin-json-es-1.5.3.tgz",
+			"integrity": "sha512-9wWjwhoN+ipMel70ktkWy0H7jj9sm5OAbAy3N3F3AT0swpIofVsIjDXyjGZJwSzy9tZzDtI/aKIj2WsqMHw2QA==",
 			"dev": true,
 			"requires": {
-				"eslint-visitor-keys": "^1.3.0",
+				"eslint-visitor-keys": "^2.0.0",
 				"espree": "^7.3.1"
-			},
-			"dependencies": {
-				"acorn-jsx": {
-					"version": "5.3.1",
-					"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
-					"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
-					"dev": true
-				},
-				"eslint-visitor-keys": {
-					"version": "1.3.0",
-					"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
-					"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
-					"dev": true
-				},
-				"espree": {
-					"version": "7.3.1",
-					"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
-					"integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
-					"dev": true,
-					"requires": {
-						"acorn": "^7.4.0",
-						"acorn-jsx": "^5.3.1",
-						"eslint-visitor-keys": "^1.3.0"
-					}
-				}
 			}
 		},
 		"eslint-plugin-mediawiki": {
-			"version": "0.2.6",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-mediawiki/-/eslint-plugin-mediawiki-0.2.6.tgz",
-			"integrity": "sha512-e7gx15H39ceam9AnSr6DDyfhMM9L43PVagHzclH3CF33DvWKi/OA+j2dqzJTuJcl5P/EmVIQHG5qoTaepkADsw==",
+			"version": "0.2.7",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-mediawiki/-/eslint-plugin-mediawiki-0.2.7.tgz",
+			"integrity": "sha512-2ZvPvLEwCIqrJxV1349bdX5Q03c30WccuUMCfB1Gh2IVxbBSrY0gbzOk/gPZeYigVhODt9xoFWUCIz8jwTWfrA==",
 			"dev": true,
 			"requires": {
-				"eslint-plugin-vue": "^6.2.2",
+				"eslint-plugin-vue": "^7.7.0",
 				"upath": "^1.2.0"
-			},
-			"dependencies": {
-				"eslint-plugin-vue": {
-					"version": "6.2.2",
-					"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-6.2.2.tgz",
-					"integrity": "sha512-Nhc+oVAHm0uz/PkJAWscwIT4ijTrK5fqNqz9QB1D35SbbuMG1uB6Yr5AJpvPSWg+WOw7nYNswerYh0kOk64gqQ==",
-					"dev": true,
-					"requires": {
-						"natural-compare": "^1.4.0",
-						"semver": "^5.6.0",
-						"vue-eslint-parser": "^7.0.0"
-					}
-				}
 			}
 		},
 		"eslint-plugin-mocha": {
-			"version": "8.0.0",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.0.0.tgz",
-			"integrity": "sha512-n67etbWDz6NQM+HnTwZHyBwz/bLlYPOxUbw7bPuCyFujv7ZpaT/Vn6KTAbT02gf7nRljtYIjWcTxK/n8a57rQQ==",
+			"version": "8.1.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.1.0.tgz",
+			"integrity": "sha512-1EgHvXKRl7W3mq3sntZAi5T24agRMyiTPL4bSXe+B4GksYOjAPEWYx+J3eJg4It1l2NMNZJtk0gQyQ6mfiPhQg==",
 			"dev": true,
 			"requires": {
 				"eslint-utils": "^2.1.0",
@@ -2194,15 +1999,15 @@
 			}
 		},
 		"eslint-plugin-vue": {
-			"version": "7.5.0",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.5.0.tgz",
-			"integrity": "sha512-QnMMTcyV8PLxBz7QQNAwISSEs6LYk2LJvGlxalXvpCtfKnqo7qcY0aZTIxPe8QOnHd7WCwiMZLOJzg6A03T0Gw==",
+			"version": "7.7.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.7.0.tgz",
+			"integrity": "sha512-mYz4bpLGv5jx6YG/GvKkqbGSfV7uma2u1P3mLA41Q5vQl8W1MeuTneB8tfsLq6xxxesFubcrOC0BZBJ5R+eaCQ==",
 			"dev": true,
 			"requires": {
 				"eslint-utils": "^2.1.0",
 				"natural-compare": "^1.4.0",
 				"semver": "^7.3.2",
-				"vue-eslint-parser": "^7.4.1"
+				"vue-eslint-parser": "^7.6.0"
 			},
 			"dependencies": {
 				"lru-cache": {
@@ -2321,18 +2126,18 @@
 			"dev": true
 		},
 		"esquery": {
-			"version": "1.3.1",
-			"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz",
-			"integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==",
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+			"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
 			"dev": true,
 			"requires": {
 				"estraverse": "^5.1.0"
 			},
 			"dependencies": {
 				"estraverse": {
-					"version": "5.1.0",
-					"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz",
-					"integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==",
+					"version": "5.2.0",
+					"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+					"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
 					"dev": true
 				}
 			}
@@ -2554,9 +2359,9 @@
 			}
 		},
 		"file-entry-cache": {
-			"version": "6.0.0",
-			"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz",
-			"integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==",
+			"version": "6.0.1",
+			"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+			"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
 			"dev": true,
 			"requires": {
 				"flat-cache": "^3.0.4"
@@ -3413,9 +3218,9 @@
 			"dev": true
 		},
 		"js-yaml": {
-			"version": "3.13.1",
-			"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
-			"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+			"version": "3.14.1",
+			"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+			"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
 			"dev": true,
 			"requires": {
 				"argparse": "^1.0.7",
@@ -5496,9 +5301,9 @@
 			},
 			"dependencies": {
 				"ajv": {
-					"version": "7.0.3",
-					"resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz",
-					"integrity": "sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==",
+					"version": "7.1.1",
+					"resolved": "https://registry.npmjs.org/ajv/-/ajv-7.1.1.tgz",
+					"integrity": "sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ==",
 					"dev": true,
 					"requires": {
 						"fast-deep-equal": "^3.1.1",
@@ -5514,9 +5319,9 @@
 					"dev": true
 				},
 				"lodash": {
-					"version": "4.17.20",
-					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-					"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+					"version": "4.17.21",
+					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+					"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
 					"dev": true
 				}
 			}
@@ -5783,9 +5588,9 @@
 			"dev": true
 		},
 		"v8-compile-cache": {
-			"version": "2.1.0",
-			"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
-			"integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
+			"version": "2.3.0",
+			"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+			"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
 			"dev": true
 		},
 		"v8flags": {
@@ -5837,16 +5642,16 @@
 			}
 		},
 		"vue-eslint-parser": {
-			"version": "7.4.1",
-			"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.4.1.tgz",
-			"integrity": "sha512-AFvhdxpFvliYq1xt/biNBslTHE/zbEvSnr1qfHA/KxRIpErmEDrQZlQnvEexednRHmLfDNOMuDYwZL5xkLzIXQ==",
+			"version": "7.6.0",
+			"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.6.0.tgz",
+			"integrity": "sha512-QXxqH8ZevBrtiZMZK0LpwaMfevQi9UL7lY6Kcp+ogWHC88AuwUPwwCIzkOUc1LR4XsYAt/F9yHXAB/QoD17QXA==",
 			"dev": true,
 			"requires": {
 				"debug": "^4.1.1",
 				"eslint-scope": "^5.0.0",
 				"eslint-visitor-keys": "^1.1.0",
 				"espree": "^6.2.1",
-				"esquery": "^1.0.1",
+				"esquery": "^1.4.0",
 				"lodash": "^4.17.15"
 			},
 			"dependencies": {
@@ -5876,6 +5681,21 @@
 						"eslint-visitor-keys": "^1.1.0"
 					}
 				},
+				"esquery": {
+					"version": "1.4.0",
+					"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+					"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+					"dev": true,
+					"requires": {
+						"estraverse": "^5.1.0"
+					}
+				},
+				"estraverse": {
+					"version": "5.2.0",
+					"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+					"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
+					"dev": true
+				},
 				"ms": {
 					"version": "2.1.2",
 					"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
diff --git a/package.json b/package.json
index aa33fdd..c476641 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
 	},
 	"pre-commit": "precommit",
 	"devDependencies": {
-		"eslint-config-wikimedia": "0.18.1",
+		"eslint-config-wikimedia": "0.18.2",
 		"grunt": "1.3.0",
 		"grunt-banana-checker": "0.9.0",
 		"grunt-eslint": "23.0.0",
-- 
2.20.1

$ date
Sat Mar  6 11:05:29 UTC 2021

$ git clone file:///srv/git/mediawiki-extensions-ContentTranslation.git repo --depth=1 -b master
Cloning into 'repo'...

$ git config user.name libraryupgrader

$ git config user.email tools.libraryupgrader@tools.wmflabs.org

$ git submodule update --init

$ grr init
Installed commit-msg hook.

$ git show-ref refs/heads/master
3406b3dcc7bcfa772f4a85665917aa187362afb0 refs/heads/master

$ composer install
Loading composer repositories with package information
Warning from https://repo.packagist.org: You are using an outdated version of Composer. Composer 2 is now available and you should upgrade. See https://getcomposer.org/2
Updating dependencies (including require-dev)
Package operations: 33 installs, 0 updates, 0 removals
  - Installing composer/installers (v1.10.0): Loading from cache
  - Installing firebase/php-jwt (v5.2.0): Loading from cache
  - Installing squizlabs/php_codesniffer (3.5.8): Loading from cache
  - Installing composer/spdx-licenses (1.5.5): Loading from cache
  - Installing composer/semver (3.2.4): Loading from cache
  - Installing mediawiki/mediawiki-codesniffer (v35.0.0): Loading from cache
  - Installing symfony/polyfill-php80 (v1.22.1): Loading from cache
  - Installing symfony/polyfill-mbstring (v1.22.1): Loading from cache
  - Installing symfony/polyfill-intl-normalizer (v1.22.1): Loading from cache
  - Installing symfony/polyfill-intl-grapheme (v1.22.1): Loading from cache
  - Installing symfony/polyfill-ctype (v1.22.1): Loading from cache
  - Installing symfony/string (v5.2.4): Loading from cache
  - Installing psr/container (1.1.1): Loading from cache
  - Installing symfony/service-contracts (v2.2.0): Loading from cache
  - Installing symfony/polyfill-php73 (v1.22.1): Loading from cache
  - Installing symfony/console (v5.2.4): Loading from cache
  - Installing psr/log (1.1.3): Loading from cache
  - Installing sabre/event (5.1.2): Loading from cache
  - Installing netresearch/jsonmapper (v2.1.0): Loading from cache
  - Installing microsoft/tolerant-php-parser (v0.0.23): Loading from cache
  - Installing phpdocumentor/reflection-common (2.2.0): Loading from cache
  - Installing webmozart/assert (1.9.1): Loading from cache
  - Installing phpdocumentor/type-resolver (1.4.0): Loading from cache
  - Installing phpdocumentor/reflection-docblock (5.2.2): Loading from cache
  - Installing felixfbecker/advanced-json-rpc (v3.2.0): Loading from cache
  - Installing composer/xdebug-handler (1.4.5): Loading from cache
  - Installing phan/phan (3.2.6): Loading from cache
  - Installing mediawiki/phan-taint-check-plugin (3.2.1): Loading from cache
  - Installing mediawiki/mediawiki-phan-config (0.10.6): Loading from cache
  - Installing mediawiki/minus-x (1.1.1): Loading from cache
  - Installing php-parallel-lint/php-console-color (v0.3): Loading from cache
  - Installing php-parallel-lint/php-console-highlighter (v0.5): Loading from cache
  - Installing php-parallel-lint/php-parallel-lint (v1.2.0): Loading from cache
symfony/service-contracts suggests installing symfony/service-implementation
symfony/console suggests installing symfony/event-dispatcher
symfony/console suggests installing symfony/lock
symfony/console suggests installing symfony/process
phan/phan suggests installing ext-ast (Needed for parsing ASTs (unless --use-fallback-parser is used). 1.0.1+ is needed, 1.0.8+ is recommended.)
Writing lock file
Generating autoload files
13 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

Upgrading n:eslint-config-wikimedia from 0.18.1 -> 0.18.2
$ npm install

> pre-commit@1.2.2 install /src/repo/node_modules/pre-commit
> node install.js


> core-js@3.9.1 postinstall /src/repo/node_modules/core-js
> node -e "try{require('./postinstall')}catch(e){}"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon: 
> https://opencollective.com/core-js 
> https://www.patreon.com/zloirock 

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> spawn-sync@1.0.15 postinstall /src/repo/node_modules/spawn-sync
> node postinstall

added 655 packages from 347 contributors and audited 655 packages in 58.004s

67 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities


$ npm update eslint -depth 10
+ eslint@7.21.0
added 9 packages from 5 contributors, removed 3 packages, updated 11 packages, moved 1 package and audited 661 packages in 84.62s

65 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities


$ npm install grunt-eslint@23.0.0 --save-exact
+ grunt-eslint@23.0.0
updated 1 package and audited 661 packages in 5.487s

67 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities


$ ./node_modules/.bin/eslint modules/tools/ext.cx.tools.manager.js modules/ui/mw.cx.ui.TranslationView.js i18n/api/hu.json i18n/mr.json i18n/xsy.json i18n/mk.json i18n/sgs.json i18n/api/ne.json i18n/zh-hant.json i18n/ann.json i18n/btm.json modules/ve-cx/ui/ve.ui.CXTextSelectionContextItem.js i18n/api/eu.json i18n/pt-br.json i18n/frr.json modules/publish/ext.cx.wikibase.link.js modules/ve-cx/ui/ve.ui.CXTranslationToolbar.js i18n/ar.json i18n/api/fi.json i18n/gaa.json i18n/inh.json i18n/api/ar.json modules/source/conf/en-cy.json i18n/tay.json i18n/ryu.json i18n/arq.json i18n/si.json i18n/ti.json i18n/vec.json tests/qunit/mw.cx.MachineTranslationService.test.js i18n/hil.json modules/entrypoints/ext.cx.entrypoints.contributionsmenu.js i18n/jv.json i18n/cy.json i18n/api/bcl.json modules/publish/ext.cx.publish.js modules/tools/ext.cx.tools.mtabuse.js i18n/api/nds-nl.json i18n/ug-arab.json i18n/api/frc.json i18n/api/pt-br.json i18n/bqi.json i18n/api/cv.json i18n/zgh.json modules/ui/mw.cx.ui.ToolsColumn.js i18n/api/or.json i18n/gom-deva.json i18n/jbo.json i18n/eu.json i18n/or.json i18n/ig.json i18n/bpy.json i18n/en.json i18n/api/cs.json modules/ve-cx/dm/ve.dm.CXImageCaptionNode.js modules/source/mw.cx.SelectedSourcePageDialog.js modules/ve-cx/ce/ve.ce.CXPlaceholderNode.js modules/base/ext.cx.model.js i18n/ami.json i18n/id.json modules/tools/ext.cx.tools.link.js i18n/hi.json i18n/gl.json i18n/vo.json i18n/uk.json modules/ve-cx/ui/ve.ui.CXPublishTool.js i18n/mn.json i18n/api/ast.json modules/widgets/feedback/ext.cx.feedback.js i18n/av.json i18n/tcy.json i18n/sh.json modules/mw.cx.init.js i18n/lv.json modules/mw.cx.init.Translation.js modules/cache/mw.cx.ApiResponseCache.js modules/cache/mw.cx.TitlePairCache.js i18n/api/ur.json i18n/bg.json modules/widgets/callout/ext.cx.callout.js i18n/tt-cyrl.json i18n/api/pnb.json i18n/api/et.json i18n/war.json i18n/hr.json modules/tools/mw.cx.tools.TemplateTool.js i18n/sd.json modules/dashboard/mw.cx.TranslationList.js modules/ve-cx/ui/tools/ve.ui.CXResetSectionTool.js modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js i18n/api/nn.json i18n/bho.json i18n/api/lki.json i18n/br.json i18n/my.json modules/ui/widgets/mw.cx.ui.TitleOptionWidget.js modules/eventlogging/ext.cx.eventlogging.dashboard.js modules/ve-cx/ui/ve.ui.CXReferenceDialog.js i18n/api/ml.json i18n/api/nah.json i18n/api/mg.json modules/tools/ext.cx.tools.template.card.js modules/cache/mw.cx.NamespaceCache.js i18n/ia.json i18n/api/ja.json i18n/af.json modules/tools/mw.cx.tools.TranslationToolFactory.js modules/ve-cx/ce/ve.ce.CXImageCaptionNode.js modules/cache/mw.cx.CategoryCache.js i18n/hyw.json modules/source/conf/common.json modules/ui/mw.cx.ui.LoginDialog.js modules/ve-cx/ui/ve.ui.CXLinkAnnotationInspector.js i18n/tet.json modules/widgets/progressbar/ext.cx.progressbar.js i18n/hu-formal.json modules/tools/mw.cx.tools.TranslationTool.js i18n/lag.json i18n/hy.json i18n/api/sr-el.json i18n/api/hr.json package.json i18n/api/qu.json i18n/th.json i18n/gom-latn.json i18n/api/lt.json i18n/pnb.json i18n/api/as.json i18n/mo.json modules/ui/widgets/mw.cx.ui.PersonalMenuWidget.js i18n/api/id.json modules/ui/widgets/mw.cx.ui.CategoryMultiselectWidget.js i18n/api/ban.json modules/ui/mw.cx.ui.TargetColumn.js i18n/api/vi.json i18n/qqq.json modules/base/mw.cx.SiteMapper.js i18n/kjp.json i18n/api/de.json i18n/shn.json i18n/api/ku-latn.json i18n/fr.json modules/entrypoints/ext.cx.entrypoints.newarticle.veloader.js i18n/ckb.json tests/qunit/publish/ext.cx.publish.prepare.test.js modules/ui/widgets/mw.cx.ui.PublishSettingsWidget.js i18n/be.json i18n/kw.json modules/source/mw.cx.SelectedSourcePage.js modules/translation/ext.cx.translation.progress.js i18n/api/bg.json i18n/api/tr.json modules/translation/ext.cx.translation.conflict.js modules/source/ext.cx.source.js modules/ui/legacy/mw.cx.ui.TranslationView.js i18n/se.json i18n/ml.json i18n/api/bqi.json i18n/api/lb.json i18n/bs.json i18n/kcg.json i18n/ro.json modules/widgets/spinner/ext.cx.spinner.js i18n/sah.json modules/eventlogging/legacy/ext.cx.eventlogging.translation.js i18n/nn.json i18n/ady-cyrl.json modules/ve-cx/dm/ve.dm.CXBlockImageNode.js i18n/smn.json i18n/pam.json i18n/api/ro.json i18n/tr.json modules/tools/ext.cx.tools.validator.js tests/qunit/translation/ext.cx.translation.test.js tests/qunit/tools/ext.cx.tools.mtabuse.test.js modules/ui/widgets/mw.cx.ui.FeatureDiscoveryWidget.js i18n/sr-ec.json i18n/api/ka.json modules/tools/ext.cx.tools.poem.js i18n/ne.json tests/qunit/mw.cx.util.test.js i18n/api/cy.json i18n/api/en.json i18n/udm.json i18n/nds-nl.json modules/mw.cx.MwApiRequestManager.js i18n/lmo.json modules/translation/ext.cx.translation.aligner.js i18n/api/be-tarask.json i18n/cv.json modules/tools/mw.cx.tools.SearchTool.js modules/dm/mw.cx.dm.PageTitleModel.js modules/ui/widgets/mw.cx.ui.MessageWidget.js modules/ve-cx/ce/ve.ce.CXBlockImageNode.js tests/qunit/dm/mw.cx.dm.Translation.test.js i18n/api/af.json i18n/api/oc.json modules/source/conf/en-pl.json modules/tools/ext.cx.tools.dictionary.js i18n/api/sv.json i18n/api/ca.json modules/ve-cx/ce/ve.ce.CXTransclusionNode.js i18n/api/nl.json i18n/ku-latn.json modules/ve-cx/dm/mixins/ve.dm.CXLintableNode.js modules/ve-cx/ce/mixins/ve.ce.CXPendingNode.js i18n/uz.json i18n/diq.json modules/util/ext.cx.util.js i18n/li.json i18n/api/gcr.json modules/ve-cx/ui/ve.ui.CXPublishSettingsTool.js i18n/ang.json modules/ui/mw.cx.ui.Header.js i18n/ko.json i18n/om.json i18n/it.json i18n/api/pam.json i18n/cu.json i18n/sq.json i18n/ace.json modules/tools/ext.cx.tools.categories.js i18n/so.json modules/tools/ext.cx.tools.mt.js i18n/bbc-latn.json i18n/lus.json modules/ve-cx/ui/actions/ve.ui.CXTranslationAction.js modules/ui/mw.cx.ui.ArticleColumn.js modules/ui/widgets/mw.cx.ui.CategoryInputWidget.js i18n/api/ckb.json i18n/fit.json i18n/szy.json i18n/ban.json modules/translation/ext.cx.translation.js modules/tools/ext.cx.tools.linter.js modules/ve-cx/ui/ve.ui.CXTransclusionContextItem.js modules/util/mw.cx.util.js tests/qunit/mw.cx.TranslationTracker.test.js i18n/api/yi.json modules/ui/legacy/mw.cx.ui.ToolsColumn.js i18n/wa.json i18n/ps.json modules/ve-cx/ui/ve.ui.CXLinkContextItem.js modules/tools/ext.cx.tools.js modules/entrypoints/ext.cx.contributions.js i18n/api/olo.json tests/qunit/base/mw.cx.SiteMapper.test.js i18n/api/bn.json modules/ve-cx/ce/ve.ce.CXLinkAnnotation.js modules/ve-cx/dm/ve.dm.CXTranslationUnitModel.js modules/tools/ext.cx.tools.gallery.js modules/mw.cx.TranslationController.js i18n/lrc.json modules/ui/legacy/mw.cx.ui.TranslationColumn.js modules/ve-cx/ce/ve.ce.CXSentenceSegmentAnnotation.js i18n/xmf.json modules/dashboard/mw.cx.DashboardList.js i18n/sa.json modules/ve-cx/dm/ve.dm.CXSectionNode.js i18n/api/gu.json i18n/wuu.json i18n/yue.json i18n/pt.json i18n/io.json i18n/api/ms.json i18n/lo.json i18n/ba.json modules/eventlogging/ext.cx.eventlogging.campaigns.js modules/mw.cx.TranslationTracker.js modules/ui/mw.cx.ui.Infobar.js i18n/api/ko.json i18n/sms.json i18n/gd.json i18n/mai.json i18n/api/pl.json modules/entrypoints/ext.cx.betafeature.init.js i18n/bn.json i18n/api/bs.json i18n/mg.json i18n/oc.json i18n/es.json modules/dashboard/ext.cx.recommendtool.client.js i18n/api/ksh.json modules/ve-cx/ui/ve.ui.CXReferenceContextItem.js modules/tools/mw.cx.tools.IssueTrackingTool.js modules/tools/ext.cx.tools.mt.card.js i18n/nl.json i18n/dty.json i18n/mt.json i18n/lfn.json i18n/min.json i18n/bcc.json i18n/api/zh-hant.json i18n/sco.json i18n/gsw.json i18n/mni.json modules/source/conf/es-ca.json i18n/api/da.json i18n/lg.json modules/ve-cx/ui/ve.ui.CXSurface.js i18n/bcl.json i18n/ky.json i18n/el.json modules/ui/mw.cx.ui.js i18n/dsb.json modules/ve-cx/ce/ve.ce.CXSectionNode.js modules/dm/mw.cx.dm.WikiPage.js i18n/mrh.json i18n/en-gb.json i18n/atj.json i18n/api/io.json i18n/api/he.json i18n/lki.json modules/ui/legacy/mw.cx.ui.TranslationView.init.js i18n/olo.json modules/tools/ext.cx.tools.template.editor.js i18n/api/ce.json i18n/api/sr-ec.json i18n/api/ta.json i18n/ca.json i18n/ka.json i18n/api/lzh.json modules/ve-cx/init/ve.init.mw.CXTarget.js i18n/frc.json modules/tools/ext.cx.tools.reference.js i18n/ce.json i18n/api/sah.json modules/ui/legacy/mw.cx.ui.Columns.js i18n/ms.json i18n/vep.json i18n/api/tl.json modules/entrypoints/ext.cx.interlanguagelink.init.js i18n/api/diq.json i18n/api/sq.json i18n/api/ba.json modules/tools/mw.cx.tools.InstructionsTool.js i18n/awa.json modules/widgets/pageselector/ext.cx.pageselector.js tests/qunit/publish/ext.cx.publish.test.js i18n/api/fr.json i18n/api/be.json i18n/api/es.json Gruntfile.js modules/tools/ext.cx.tools.formatter.js i18n/ur.json modules/entrypoints/ext.cx.interlanguagelink.js modules/ui/widgets/mw.cx.ui.TranslationIssueWidget.js i18n/nb.json i18n/api/my.json i18n/skr-arab.json modules/ve-cx/ce/ve.ce.CXReferenceNode.js i18n/api/sd.json i18n/nah.json modules/ve-cx/dm/ve.dm.CXReferenceNode.js i18n/nap.json i18n/qwh.json i18n/kn.json i18n/api/sk.json i18n/cs.json i18n/api/it.json i18n/vi.json i18n/tl.json modules/entrypoints/ext.cx.entrypoints.newbytranslation.js i18n/yi.json i18n/de-formal.json i18n/tg-cyrl.json composer.json i18n/api/fy.json i18n/nqo.json modules/mw.cx.MachineTranslationManager.js i18n/api/el.json modules/dashboard/mw.cx.SuggestionList.js i18n/anp.json i18n/lb.json i18n/gcr.json i18n/api/pt.json i18n/qu.json i18n/fi.json modules/ui/mw.cx.ui.LanguageFilter.js i18n/bci.json i18n/api/qqq.json tests/qunit/ui/mw.cx.ui.Infobar.test.js i18n/ee.json i18n/es-formal.json i18n/api/hi.json i18n/roa-tara.json i18n/tly.json i18n/api/uk.json modules/editor/ext.cx.editor.js i18n/sw.json i18n/api/zh-hans.json i18n/rue.json i18n/api/sl.json i18n/he.json i18n/mwl.json i18n/lt.json i18n/sl.json i18n/sk.json modules/tools/ext.cx.tools.template.js modules/source/mw.cx.SourcePageSelector.js i18n/api/is.json modules/tools/ext.cx.tools.images.js modules/dm/mw.cx.dm.Translation.js i18n/api/si.json i18n/sr-el.json i18n/got.json i18n/api/wuu.json modules/translation/ext.cx.translation.loader.js i18n/as.json i18n/api/gl.json i18n/sat.json i18n/de.json i18n/scn.json i18n/ksw.json i18n/sv.json i18n/api/kn.json i18n/fa.json i18n/api/nap.json i18n/ary.json i18n/api/nb.json modules/ve-cx/dm/ve.dm.CXSentenceSegmentAnnotation.js package-lock.json i18n/api/hy.json i18n/te.json i18n/zh-hans.json i18n/mui.json i18n/api/kab.json i18n/myv.json i18n/api/jv.json modules/dashboard/ext.cx.dashboard.js i18n/api/ru.json i18n/trv.json i18n/is.json modules/dm/mw.cx.dm.TranslationIssue.js modules/mw.cx.MachineTranslationService.js i18n/ht.json i18n/gu.json i18n/api/roa-tara.json i18n/lij.json i18n/krc.json i18n/bto.json i18n/ga.json i18n/ksh.json modules/ui/mw.cx.ui.Categories.js i18n/kk-cyrl.json i18n/az.json i18n/azb.json i18n/be-tarask.json i18n/ciw.json i18n/api/pa.json tests/qunit/tools/ext.cx.tools.categories.test.js tests/qunit/translation/ext.cx.translation.loader.test.js modules/widgets/templates/mw.cx.widgets.TemplateParamOptionWidget.js modules/ui/widgets/mw.cx.ui.TranslationToolWidget.js i18n/api/mr.json modules/ve-cx/ui/ve.ui.CXPublishSettingsDialog.js modules/ve-cx/ui/ve.ui.CXInternalLinkAnnotationWidget.js i18n/api/yue.json i18n/api/uz.json modules/ui/legacy/mw.cx.ui.SourceColumn.js i18n/api/kk-cyrl.json i18n/api/fit.json i18n/api/fa.json i18n/tyv.json i18n/da.json modules/ve-cx/dm/ve.dm.CXPlaceholderNode.js i18n/pl.json modules/ui/mw.cx.ui.SourceColumn.js i18n/api/lv.json modules/ui/legacy/mw.cx.ui.Header.js modules/stats/ext.cx.stats.js i18n/xal.json i18n/hu.json modules/entrypoints/ext.cx.entrypoints.newarticle.js modules/widgets/translator/ext.cx.translator.js i18n/lzh.json i18n/api/awa.json modules/util/ext.cx.util.selection.js i18n/api/hu-formal.json i18n/su.json i18n/ja.json extension.json modules/dm/mw.cx.dm.js i18n/hsb.json i18n/ta.json modules/ui/widgets/mw.cx.ui.CategoryTagItemWidget.js modules/dm/mw.cx.dm.SectionState.js modules/ui/widgets/mw.cx.ui.PageTitleWidget.js i18n/os.json modules/translation/ext.cx.translation.storage.js modules/ve-cx/ui/ve.ui.CXDesktopContext.js modules/ve-cx/ce/mixins/ve.ce.CXLintableNode.js i18n/kab.json modules/ui/mw.cx.ui.TranslationHeader.js i18n/ast.json i18n/api/ps.json i18n/et.json i18n/kiu.json i18n/api/eo.json i18n/km.json modules/widgets/overlay/ext.cx.overlay.js i18n/eo.json modules/ve-cx/dm/ve.dm.CXTransclusionNode.js i18n/api/th.json i18n/pa.json i18n/api/mk.json i18n/api/ia.json i18n/api/krc.json i18n/fy.json modules/ui/mw.cx.ui.CaptchaDialog.js i18n/api/anp.json i18n/ru.json i18n/api/tcy.json i18n/zu.json modules/ve-cx/ui/ve.ui.CXTranslationUnitContextItem.js tests/qunit/mw.cx.TargetArticle.test.js modules/mw.cx.TargetArticle.js modules/eventlogging/ext.cx.eventlogging.translation.js modules/tools/ext.cx.tools.instructions.js modules/publish/ext.cx.publish.dialog.js i18n/lld.json modules/ve-cx/dm/ve.dm.CXLinkAnnotation.js --fix

/src/repo/Gruntfile.js
  35:11  warning  Object.assign() is not supported in Safari 5.1, iOS Safari 6.0-6.1, IE 11  compat/compat

/src/repo/modules/dashboard/mw.cx.DashboardList.js
  127:7  warning  'language' is already declared in the upper scope on line 122 column 13  no-shadow

/src/repo/modules/dashboard/mw.cx.SuggestionList.js
   12:0   warning  @extends should not have a bracketed type in "jsdoc" mode            jsdoc/valid-types
  141:10  warning  'list' is already declared in the upper scope on line 120 column 57  no-shadow

/src/repo/modules/dashboard/mw.cx.TranslationList.js
  14:0  warning  @extends should not have a bracketed type in "jsdoc" mode  jsdoc/valid-types
  15:0  warning  Invalid JSDoc tag name "mixins"                            jsdoc/check-tag-names

/src/repo/modules/dm/mw.cx.dm.PageTitleModel.js
   9:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  10:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/dm/mw.cx.dm.Translation.js
  7:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/entrypoints/ext.cx.entrypoints.newbytranslation.js
  177:35  warning  navigator.languages() is not supported in Safari 5.1  compat/compat

/src/repo/modules/mw.cx.TargetArticle.js
  458:7  warning  'title' is already declared in the upper scope on line 449 column 66  no-shadow

/src/repo/modules/mw.cx.TranslationTracker.js
  594:1  warning  Found more than one @return declaration  jsdoc/require-returns
  594:1  warning  Found more than one @return declaration  jsdoc/require-returns-check

/src/repo/modules/mw.cx.init.Translation.js
  201:7   warning  'sourceDom' is already declared in the upper scope on line 173 column 6    no-shadow
  201:18  warning  'targetDom' is already declared in the upper scope on line 173 column 17   no-shadow
  203:4   warning  'sourceHtml' is already declared in the upper scope on line 172 column 68  no-shadow

/src/repo/modules/source/mw.cx.SelectedSourcePageDialog.js
  15:0  warning  @extends should not have a bracketed type in "jsdoc" mode  jsdoc/valid-types

/src/repo/modules/stats/ext.cx.stats.js
  617:55  warning  'data' is already declared in the upper scope on line 612 column 7   no-shadow
  629:55  warning  'data' is already declared in the upper scope on line 612 column 7   no-shadow
  641:49  warning  'data' is already declared in the upper scope on line 612 column 7   no-shadow
  662:58  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  674:58  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  686:52  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  698:55  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  718:55  warning  'data' is already declared in the upper scope on line 714 column 7   no-shadow
  727:55  warning  'data' is already declared in the upper scope on line 714 column 7   no-shadow
  736:49  warning  'data' is already declared in the upper scope on line 714 column 7   no-shadow
  757:58  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow
  766:58  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow
  775:52  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow
  784:55  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow

/src/repo/modules/tools/ext.cx.tools.template.js
  107:56  warning  'title' is already declared in the upper scope on line 47 column 7  no-shadow
  631:2   warning  Found more than one @return declaration                             jsdoc/require-returns
  631:2   warning  Found more than one @return declaration                             jsdoc/require-returns-check

/src/repo/modules/tools/ext.cx.tools.validator.js
  8:2  warning  Found more than one @return declaration  jsdoc/require-returns
  8:2  warning  Found more than one @return declaration  jsdoc/require-returns-check

/src/repo/modules/translation/ext.cx.translation.progress.js
  46:2  warning  Found more than one @return declaration  jsdoc/require-returns
  46:2  warning  Found more than one @return declaration  jsdoc/require-returns-check
  93:2  warning  Found more than one @return declaration  jsdoc/require-returns
  93:2  warning  Found more than one @return declaration  jsdoc/require-returns-check

/src/repo/modules/ui/widgets/mw.cx.ui.MessageWidget.js
   9:0  warning  The type 'mediawiki' is undefined  jsdoc/no-undefined-types
  10:0  warning  The type 'mediawiki' is undefined  jsdoc/no-undefined-types

/src/repo/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
  219:31  warning  'pages' is already declared in the upper scope on line 184 column 71  no-shadow

/src/repo/modules/ui/widgets/mw.cx.ui.PageTitleWidget.js
  9:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ui/widgets/mw.cx.ui.PersonalMenuWidget.js
  15:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/ce/ve.ce.CXPlaceholderNode.js
  11:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  12:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/ce/ve.ce.CXSectionNode.js
  7:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  8:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/dm/ve.dm.CXPlaceholderNode.js
  6:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  7:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/dm/ve.dm.CXReferenceNode.js
  12:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/dm/ve.dm.CXSectionNode.js
  12:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  13:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/init/ve.init.mw.CXTarget.js
  724:7   warning  'model' is already declared in the upper scope on line 710 column 3  no-shadow
  802:34  warning  'tx' is already declared in the upper scope on line 784 column 42    no-shadow
  802:38  warning  'doc' is already declared in the upper scope on line 786 column 3    no-shadow

/src/repo/modules/ve-cx/ui/actions/ve.ui.CXTranslationAction.js
  35:0  warning  Missing JSDoc @property "" type  jsdoc/require-property-type

/src/repo/modules/ve-cx/ui/ve.ui.CXTextSelectionContextItem.js
  6:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/ui/ve.ui.CXTranslationToolbar.js
  60:65  warning  'MTManager' is already declared in the upper scope on line 59 column 62  no-shadow

✖ 60 problems (0 errors, 60 warnings)


$ ./node_modules/.bin/eslint modules/tools/ext.cx.tools.manager.js modules/ui/mw.cx.ui.TranslationView.js i18n/api/hu.json i18n/mr.json i18n/xsy.json i18n/mk.json i18n/sgs.json i18n/api/ne.json i18n/zh-hant.json i18n/ann.json i18n/btm.json modules/ve-cx/ui/ve.ui.CXTextSelectionContextItem.js i18n/api/eu.json i18n/pt-br.json i18n/frr.json modules/publish/ext.cx.wikibase.link.js modules/ve-cx/ui/ve.ui.CXTranslationToolbar.js i18n/ar.json i18n/api/fi.json i18n/gaa.json i18n/inh.json i18n/api/ar.json modules/source/conf/en-cy.json i18n/tay.json i18n/ryu.json i18n/arq.json i18n/si.json i18n/ti.json i18n/vec.json tests/qunit/mw.cx.MachineTranslationService.test.js i18n/hil.json modules/entrypoints/ext.cx.entrypoints.contributionsmenu.js i18n/jv.json i18n/cy.json i18n/api/bcl.json modules/publish/ext.cx.publish.js modules/tools/ext.cx.tools.mtabuse.js i18n/api/nds-nl.json i18n/ug-arab.json i18n/api/frc.json i18n/api/pt-br.json i18n/bqi.json i18n/api/cv.json i18n/zgh.json modules/ui/mw.cx.ui.ToolsColumn.js i18n/api/or.json i18n/gom-deva.json i18n/jbo.json i18n/eu.json i18n/or.json i18n/ig.json i18n/bpy.json i18n/en.json i18n/api/cs.json modules/ve-cx/dm/ve.dm.CXImageCaptionNode.js modules/source/mw.cx.SelectedSourcePageDialog.js modules/ve-cx/ce/ve.ce.CXPlaceholderNode.js modules/base/ext.cx.model.js i18n/ami.json i18n/id.json modules/tools/ext.cx.tools.link.js i18n/hi.json i18n/gl.json i18n/vo.json i18n/uk.json modules/ve-cx/ui/ve.ui.CXPublishTool.js i18n/mn.json i18n/api/ast.json modules/widgets/feedback/ext.cx.feedback.js i18n/av.json i18n/tcy.json i18n/sh.json modules/mw.cx.init.js i18n/lv.json modules/mw.cx.init.Translation.js modules/cache/mw.cx.ApiResponseCache.js modules/cache/mw.cx.TitlePairCache.js i18n/api/ur.json i18n/bg.json modules/widgets/callout/ext.cx.callout.js i18n/tt-cyrl.json i18n/api/pnb.json i18n/api/et.json i18n/war.json i18n/hr.json modules/tools/mw.cx.tools.TemplateTool.js i18n/sd.json modules/dashboard/mw.cx.TranslationList.js modules/ve-cx/ui/tools/ve.ui.CXResetSectionTool.js modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js i18n/api/nn.json i18n/bho.json i18n/api/lki.json i18n/br.json i18n/my.json modules/ui/widgets/mw.cx.ui.TitleOptionWidget.js modules/eventlogging/ext.cx.eventlogging.dashboard.js modules/ve-cx/ui/ve.ui.CXReferenceDialog.js i18n/api/ml.json i18n/api/nah.json i18n/api/mg.json modules/tools/ext.cx.tools.template.card.js modules/cache/mw.cx.NamespaceCache.js i18n/ia.json i18n/api/ja.json i18n/af.json modules/tools/mw.cx.tools.TranslationToolFactory.js modules/ve-cx/ce/ve.ce.CXImageCaptionNode.js modules/cache/mw.cx.CategoryCache.js i18n/hyw.json modules/source/conf/common.json modules/ui/mw.cx.ui.LoginDialog.js modules/ve-cx/ui/ve.ui.CXLinkAnnotationInspector.js i18n/tet.json modules/widgets/progressbar/ext.cx.progressbar.js i18n/hu-formal.json modules/tools/mw.cx.tools.TranslationTool.js i18n/lag.json i18n/hy.json i18n/api/sr-el.json i18n/api/hr.json package.json i18n/api/qu.json i18n/th.json i18n/gom-latn.json i18n/api/lt.json i18n/pnb.json i18n/api/as.json i18n/mo.json modules/ui/widgets/mw.cx.ui.PersonalMenuWidget.js i18n/api/id.json modules/ui/widgets/mw.cx.ui.CategoryMultiselectWidget.js i18n/api/ban.json modules/ui/mw.cx.ui.TargetColumn.js i18n/api/vi.json i18n/qqq.json modules/base/mw.cx.SiteMapper.js i18n/kjp.json i18n/api/de.json i18n/shn.json i18n/api/ku-latn.json i18n/fr.json modules/entrypoints/ext.cx.entrypoints.newarticle.veloader.js i18n/ckb.json tests/qunit/publish/ext.cx.publish.prepare.test.js modules/ui/widgets/mw.cx.ui.PublishSettingsWidget.js i18n/be.json i18n/kw.json modules/source/mw.cx.SelectedSourcePage.js modules/translation/ext.cx.translation.progress.js i18n/api/bg.json i18n/api/tr.json modules/translation/ext.cx.translation.conflict.js modules/source/ext.cx.source.js modules/ui/legacy/mw.cx.ui.TranslationView.js i18n/se.json i18n/ml.json i18n/api/bqi.json i18n/api/lb.json i18n/bs.json i18n/kcg.json i18n/ro.json modules/widgets/spinner/ext.cx.spinner.js i18n/sah.json modules/eventlogging/legacy/ext.cx.eventlogging.translation.js i18n/nn.json i18n/ady-cyrl.json modules/ve-cx/dm/ve.dm.CXBlockImageNode.js i18n/smn.json i18n/pam.json i18n/api/ro.json i18n/tr.json modules/tools/ext.cx.tools.validator.js tests/qunit/translation/ext.cx.translation.test.js tests/qunit/tools/ext.cx.tools.mtabuse.test.js modules/ui/widgets/mw.cx.ui.FeatureDiscoveryWidget.js i18n/sr-ec.json i18n/api/ka.json modules/tools/ext.cx.tools.poem.js i18n/ne.json tests/qunit/mw.cx.util.test.js i18n/api/cy.json i18n/api/en.json i18n/udm.json i18n/nds-nl.json modules/mw.cx.MwApiRequestManager.js i18n/lmo.json modules/translation/ext.cx.translation.aligner.js i18n/api/be-tarask.json i18n/cv.json modules/tools/mw.cx.tools.SearchTool.js modules/dm/mw.cx.dm.PageTitleModel.js modules/ui/widgets/mw.cx.ui.MessageWidget.js modules/ve-cx/ce/ve.ce.CXBlockImageNode.js tests/qunit/dm/mw.cx.dm.Translation.test.js i18n/api/af.json i18n/api/oc.json modules/source/conf/en-pl.json modules/tools/ext.cx.tools.dictionary.js i18n/api/sv.json i18n/api/ca.json modules/ve-cx/ce/ve.ce.CXTransclusionNode.js i18n/api/nl.json i18n/ku-latn.json modules/ve-cx/dm/mixins/ve.dm.CXLintableNode.js modules/ve-cx/ce/mixins/ve.ce.CXPendingNode.js i18n/uz.json i18n/diq.json modules/util/ext.cx.util.js i18n/li.json i18n/api/gcr.json modules/ve-cx/ui/ve.ui.CXPublishSettingsTool.js i18n/ang.json modules/ui/mw.cx.ui.Header.js i18n/ko.json i18n/om.json i18n/it.json i18n/api/pam.json i18n/cu.json i18n/sq.json i18n/ace.json modules/tools/ext.cx.tools.categories.js i18n/so.json modules/tools/ext.cx.tools.mt.js i18n/bbc-latn.json i18n/lus.json modules/ve-cx/ui/actions/ve.ui.CXTranslationAction.js modules/ui/mw.cx.ui.ArticleColumn.js modules/ui/widgets/mw.cx.ui.CategoryInputWidget.js i18n/api/ckb.json i18n/fit.json i18n/szy.json i18n/ban.json modules/translation/ext.cx.translation.js modules/tools/ext.cx.tools.linter.js modules/ve-cx/ui/ve.ui.CXTransclusionContextItem.js modules/util/mw.cx.util.js tests/qunit/mw.cx.TranslationTracker.test.js i18n/api/yi.json modules/ui/legacy/mw.cx.ui.ToolsColumn.js i18n/wa.json i18n/ps.json modules/ve-cx/ui/ve.ui.CXLinkContextItem.js modules/tools/ext.cx.tools.js modules/entrypoints/ext.cx.contributions.js i18n/api/olo.json tests/qunit/base/mw.cx.SiteMapper.test.js i18n/api/bn.json modules/ve-cx/ce/ve.ce.CXLinkAnnotation.js modules/ve-cx/dm/ve.dm.CXTranslationUnitModel.js modules/tools/ext.cx.tools.gallery.js modules/mw.cx.TranslationController.js i18n/lrc.json modules/ui/legacy/mw.cx.ui.TranslationColumn.js modules/ve-cx/ce/ve.ce.CXSentenceSegmentAnnotation.js i18n/xmf.json modules/dashboard/mw.cx.DashboardList.js i18n/sa.json modules/ve-cx/dm/ve.dm.CXSectionNode.js i18n/api/gu.json i18n/wuu.json i18n/yue.json i18n/pt.json i18n/io.json i18n/api/ms.json i18n/lo.json i18n/ba.json modules/eventlogging/ext.cx.eventlogging.campaigns.js modules/mw.cx.TranslationTracker.js modules/ui/mw.cx.ui.Infobar.js i18n/api/ko.json i18n/sms.json i18n/gd.json i18n/mai.json i18n/api/pl.json modules/entrypoints/ext.cx.betafeature.init.js i18n/bn.json i18n/api/bs.json i18n/mg.json i18n/oc.json i18n/es.json modules/dashboard/ext.cx.recommendtool.client.js i18n/api/ksh.json modules/ve-cx/ui/ve.ui.CXReferenceContextItem.js modules/tools/mw.cx.tools.IssueTrackingTool.js modules/tools/ext.cx.tools.mt.card.js i18n/nl.json i18n/dty.json i18n/mt.json i18n/lfn.json i18n/min.json i18n/bcc.json i18n/api/zh-hant.json i18n/sco.json i18n/gsw.json i18n/mni.json modules/source/conf/es-ca.json i18n/api/da.json i18n/lg.json modules/ve-cx/ui/ve.ui.CXSurface.js i18n/bcl.json i18n/ky.json i18n/el.json modules/ui/mw.cx.ui.js i18n/dsb.json modules/ve-cx/ce/ve.ce.CXSectionNode.js modules/dm/mw.cx.dm.WikiPage.js i18n/mrh.json i18n/en-gb.json i18n/atj.json i18n/api/io.json i18n/api/he.json i18n/lki.json modules/ui/legacy/mw.cx.ui.TranslationView.init.js i18n/olo.json modules/tools/ext.cx.tools.template.editor.js i18n/api/ce.json i18n/api/sr-ec.json i18n/api/ta.json i18n/ca.json i18n/ka.json i18n/api/lzh.json modules/ve-cx/init/ve.init.mw.CXTarget.js i18n/frc.json modules/tools/ext.cx.tools.reference.js i18n/ce.json i18n/api/sah.json modules/ui/legacy/mw.cx.ui.Columns.js i18n/ms.json i18n/vep.json i18n/api/tl.json modules/entrypoints/ext.cx.interlanguagelink.init.js i18n/api/diq.json i18n/api/sq.json i18n/api/ba.json modules/tools/mw.cx.tools.InstructionsTool.js i18n/awa.json modules/widgets/pageselector/ext.cx.pageselector.js tests/qunit/publish/ext.cx.publish.test.js i18n/api/fr.json i18n/api/be.json i18n/api/es.json Gruntfile.js modules/tools/ext.cx.tools.formatter.js i18n/ur.json modules/entrypoints/ext.cx.interlanguagelink.js modules/ui/widgets/mw.cx.ui.TranslationIssueWidget.js i18n/nb.json i18n/api/my.json i18n/skr-arab.json modules/ve-cx/ce/ve.ce.CXReferenceNode.js i18n/api/sd.json i18n/nah.json modules/ve-cx/dm/ve.dm.CXReferenceNode.js i18n/nap.json i18n/qwh.json i18n/kn.json i18n/api/sk.json i18n/cs.json i18n/api/it.json i18n/vi.json i18n/tl.json modules/entrypoints/ext.cx.entrypoints.newbytranslation.js i18n/yi.json i18n/de-formal.json i18n/tg-cyrl.json composer.json i18n/api/fy.json i18n/nqo.json modules/mw.cx.MachineTranslationManager.js i18n/api/el.json modules/dashboard/mw.cx.SuggestionList.js i18n/anp.json i18n/lb.json i18n/gcr.json i18n/api/pt.json i18n/qu.json i18n/fi.json modules/ui/mw.cx.ui.LanguageFilter.js i18n/bci.json i18n/api/qqq.json tests/qunit/ui/mw.cx.ui.Infobar.test.js i18n/ee.json i18n/es-formal.json i18n/api/hi.json i18n/roa-tara.json i18n/tly.json i18n/api/uk.json modules/editor/ext.cx.editor.js i18n/sw.json i18n/api/zh-hans.json i18n/rue.json i18n/api/sl.json i18n/he.json i18n/mwl.json i18n/lt.json i18n/sl.json i18n/sk.json modules/tools/ext.cx.tools.template.js modules/source/mw.cx.SourcePageSelector.js i18n/api/is.json modules/tools/ext.cx.tools.images.js modules/dm/mw.cx.dm.Translation.js i18n/api/si.json i18n/sr-el.json i18n/got.json i18n/api/wuu.json modules/translation/ext.cx.translation.loader.js i18n/as.json i18n/api/gl.json i18n/sat.json i18n/de.json i18n/scn.json i18n/ksw.json i18n/sv.json i18n/api/kn.json i18n/fa.json i18n/api/nap.json i18n/ary.json i18n/api/nb.json modules/ve-cx/dm/ve.dm.CXSentenceSegmentAnnotation.js package-lock.json i18n/api/hy.json i18n/te.json i18n/zh-hans.json i18n/mui.json i18n/api/kab.json i18n/myv.json i18n/api/jv.json modules/dashboard/ext.cx.dashboard.js i18n/api/ru.json i18n/trv.json i18n/is.json modules/dm/mw.cx.dm.TranslationIssue.js modules/mw.cx.MachineTranslationService.js i18n/ht.json i18n/gu.json i18n/api/roa-tara.json i18n/lij.json i18n/krc.json i18n/bto.json i18n/ga.json i18n/ksh.json modules/ui/mw.cx.ui.Categories.js i18n/kk-cyrl.json i18n/az.json i18n/azb.json i18n/be-tarask.json i18n/ciw.json i18n/api/pa.json tests/qunit/tools/ext.cx.tools.categories.test.js tests/qunit/translation/ext.cx.translation.loader.test.js modules/widgets/templates/mw.cx.widgets.TemplateParamOptionWidget.js modules/ui/widgets/mw.cx.ui.TranslationToolWidget.js i18n/api/mr.json modules/ve-cx/ui/ve.ui.CXPublishSettingsDialog.js modules/ve-cx/ui/ve.ui.CXInternalLinkAnnotationWidget.js i18n/api/yue.json i18n/api/uz.json modules/ui/legacy/mw.cx.ui.SourceColumn.js i18n/api/kk-cyrl.json i18n/api/fit.json i18n/api/fa.json i18n/tyv.json i18n/da.json modules/ve-cx/dm/ve.dm.CXPlaceholderNode.js i18n/pl.json modules/ui/mw.cx.ui.SourceColumn.js i18n/api/lv.json modules/ui/legacy/mw.cx.ui.Header.js modules/stats/ext.cx.stats.js i18n/xal.json i18n/hu.json modules/entrypoints/ext.cx.entrypoints.newarticle.js modules/widgets/translator/ext.cx.translator.js i18n/lzh.json i18n/api/awa.json modules/util/ext.cx.util.selection.js i18n/api/hu-formal.json i18n/su.json i18n/ja.json extension.json modules/dm/mw.cx.dm.js i18n/hsb.json i18n/ta.json modules/ui/widgets/mw.cx.ui.CategoryTagItemWidget.js modules/dm/mw.cx.dm.SectionState.js modules/ui/widgets/mw.cx.ui.PageTitleWidget.js i18n/os.json modules/translation/ext.cx.translation.storage.js modules/ve-cx/ui/ve.ui.CXDesktopContext.js modules/ve-cx/ce/mixins/ve.ce.CXLintableNode.js i18n/kab.json modules/ui/mw.cx.ui.TranslationHeader.js i18n/ast.json i18n/api/ps.json i18n/et.json i18n/kiu.json i18n/api/eo.json i18n/km.json modules/widgets/overlay/ext.cx.overlay.js i18n/eo.json modules/ve-cx/dm/ve.dm.CXTransclusionNode.js i18n/api/th.json i18n/pa.json i18n/api/mk.json i18n/api/ia.json i18n/api/krc.json i18n/fy.json modules/ui/mw.cx.ui.CaptchaDialog.js i18n/api/anp.json i18n/ru.json i18n/api/tcy.json i18n/zu.json modules/ve-cx/ui/ve.ui.CXTranslationUnitContextItem.js tests/qunit/mw.cx.TargetArticle.test.js modules/mw.cx.TargetArticle.js modules/eventlogging/ext.cx.eventlogging.translation.js modules/tools/ext.cx.tools.instructions.js modules/publish/ext.cx.publish.dialog.js i18n/lld.json modules/ve-cx/dm/ve.dm.CXLinkAnnotation.js -f json
[{"filePath":"/src/repo/Gruntfile.js","messages":[{"ruleId":"compat/compat","severity":1,"message":"Object.assign() is not supported in Safari 5.1, iOS Safari 6.0-6.1, IE 11","line":35,"column":11,"nodeType":"MemberExpression","endLine":35,"endColumn":24}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/* eslint-env node, es6 */\nmodule.exports = function ( grunt ) {\n\t'use strict';\n\n\tvar conf = grunt.file.readJSON( 'extension.json' );\n\tgrunt.loadNpmTasks( 'grunt-eslint' );\n\tgrunt.loadNpmTasks( 'grunt-banana-checker' );\n\tgrunt.loadNpmTasks( 'grunt-stylelint' );\n\n\tgrunt.initConfig( {\n\t\teslint: {\n\t\t\toptions: {\n\t\t\t\tcache: true,\n\t\t\t\tfix: grunt.option( 'fix' )\n\t\t\t},\n\t\t\tall: [\n\t\t\t\t'**/*.{js,json}',\n\t\t\t\t'!{lib,vendor,node_modules,app}/**'\n\t\t\t]\n\t\t},\n\t\tstylelint: {\n\t\t\toptions: {\n\t\t\t\tsyntax: 'less'\n\t\t\t},\n\t\t\tsrc: [\n\t\t\t\t'**/*.css',\n\t\t\t\t'**/*.less',\n\t\t\t\t'!lib/**',\n\t\t\t\t'!node_modules/**',\n\t\t\t\t'!vendor/**',\n\t\t\t\t'!app/**'\n\t\t\t]\n\t\t},\n\t\t// eslint-disable-next-line es/no-object-assign\n\t\tbanana: Object.assign( {\n\t\t\toptions: { requireLowerCase: false }\n\t\t}, conf.MessagesDirs )\n\t} );\n\n\tgrunt.registerTask( 'lint', [ 'eslint', 'stylelint', 'banana' ] );\n\tgrunt.registerTask( 'test', [ 'lint' ] );\n\tgrunt.registerTask( 'default', 'test' );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/composer.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/extension.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ace.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ady-cyrl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/af.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ami.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ang.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ann.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/anp.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/af.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/anp.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ar.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/as.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ast.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/awa.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ba.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ban.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/bcl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/be-tarask.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/be.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/bg.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/bn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/bqi.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/bs.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ca.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ce.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ckb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/cs.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/cv.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/cy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/da.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/de.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/diq.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/el.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/en.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/eo.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/es.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/et.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/eu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/fa.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/fi.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/fit.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/fr.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/frc.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/fy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/gcr.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/gl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/gu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/he.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/hi.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/hr.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/hu-formal.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/hu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/hy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ia.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/id.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/io.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/is.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/it.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ja.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/jv.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ka.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/kab.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/kk-cyrl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/kn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ko.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/krc.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ksh.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ku-latn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/lb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/lki.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/lt.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/lv.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/lzh.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/mg.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/mk.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ml.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/mr.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ms.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/my.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/nah.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/nap.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/nb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/nds-nl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ne.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/nl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/nn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/oc.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/olo.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/or.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/pa.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/pam.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/pl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/pnb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ps.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/pt-br.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/pt.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/qqq.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/qu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ro.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/roa-tara.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ru.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/sah.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/sd.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/si.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/sk.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/sl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/sq.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/sr-ec.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/sr-el.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/sv.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ta.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/tcy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/th.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/tl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/tr.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/uk.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/ur.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/uz.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/vi.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/wuu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/yi.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/yue.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/zh-hans.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/api/zh-hant.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ar.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/arq.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ary.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/as.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ast.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/atj.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/av.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/awa.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/az.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/azb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ba.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ban.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/bbc-latn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/bcc.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/bci.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/bcl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/be-tarask.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/be.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/bg.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/bho.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/bn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/bpy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/bqi.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/br.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/bs.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/btm.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/bto.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ca.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ce.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ciw.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ckb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/cs.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/cu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/cv.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/cy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/da.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/de-formal.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/de.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/diq.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/dsb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/dty.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ee.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/el.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/en-gb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/en.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/eo.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/es-formal.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/es.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/et.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/eu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/fa.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/fi.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/fit.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/fr.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/frc.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/frr.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/fy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ga.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/gaa.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/gcr.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/gd.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/gl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/gom-deva.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/gom-latn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/got.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/gsw.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/gu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/he.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/hi.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/hil.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/hr.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/hsb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ht.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/hu-formal.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/hu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/hy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/hyw.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ia.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/id.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ig.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/inh.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/io.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/is.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/it.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ja.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/jbo.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/jv.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ka.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/kab.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/kcg.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/kiu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/kjp.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/kk-cyrl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/km.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/kn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ko.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/krc.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ksh.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ksw.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ku-latn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/kw.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ky.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lag.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lfn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lg.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/li.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lij.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lki.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lld.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lmo.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lo.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lrc.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lt.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lus.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lv.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/lzh.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/mai.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/mg.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/min.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/mk.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ml.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/mn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/mni.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/mo.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/mr.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/mrh.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ms.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/mt.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/mui.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/mwl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/my.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/myv.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/nah.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/nap.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/nb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/nds-nl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ne.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/nl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/nn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/nqo.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/oc.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/olo.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/om.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/or.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/os.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/pa.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/pam.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/pl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/pnb.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ps.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/pt-br.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/pt.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/qqq.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/qu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/qwh.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ro.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/roa-tara.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ru.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/rue.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ryu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sa.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sah.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sat.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/scn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sco.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sd.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/se.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sgs.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sh.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/shn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/si.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sk.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/skr-arab.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/smn.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sms.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/so.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sq.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sr-ec.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sr-el.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/su.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sv.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/sw.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/szy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ta.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/tay.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/tcy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/te.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/tet.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/tg-cyrl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/th.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ti.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/tl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/tly.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/tr.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/trv.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/tt-cyrl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/tyv.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/udm.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ug-arab.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/uk.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/ur.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/uz.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/vec.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/vep.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/vi.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/vo.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/wa.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/war.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/wuu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/xal.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/xmf.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/xsy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/yi.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/yue.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/zgh.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/zh-hans.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/zh-hant.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/i18n/zu.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/base/ext.cx.model.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/base/mw.cx.SiteMapper.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/cache/mw.cx.ApiResponseCache.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/cache/mw.cx.CategoryCache.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/cache/mw.cx.NamespaceCache.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/cache/mw.cx.TitlePairCache.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dashboard/ext.cx.dashboard.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dashboard/ext.cx.recommendtool.client.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dashboard/mw.cx.DashboardList.js","messages":[{"ruleId":"no-shadow","severity":1,"message":"'language' is already declared in the upper scope on line 122 column 13.","line":127,"column":7,"nodeType":"Identifier","messageId":"noShadow","endLine":127,"endColumn":15}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n'use strict';\n\n/**\n * @class\n * @constructor\n * @abstract\n *\n * @param {jQuery} $container The container for this suggestion list\n * @param {mw.cx.SiteMapper} siteMapper\n */\nmw.cx.DashboardList = function ( $container, siteMapper ) {\n\tthis.$container = $container;\n\tthis.siteMapper = siteMapper;\n\n\tthis.$headerContainer = null;\n\tthis.$listContainer = null;\n\tthis.$loadingIndicatorSpinner = null;\n\tthis.languageFilter = null;\n\tthis.pendingRequests = 0;\n\tthis.active = false;\n\n\tthis.init();\n\tthis.listen();\n};\n\n/* Initialization */\n\nOO.initClass( mw.cx.DashboardList );\n\n/* Static properties */\n\nmw.cx.DashboardList.static.lostSessionTitle = OO.ui.deferMsg( 'cx-lost-session' );\nmw.cx.DashboardList.static.lostSessionMessage = OO.ui.deferMsg( 'cx-lost-session-dashboard' );\n\n/* Static methods */\n\n/**\n * Display the modal dialog that lets the user know session has expired.\n * Login button is provided and no other action can be taken before user logs in again.\n */\nmw.cx.DashboardList.static.showLoginDialog = function () {\n\tOO.ui.getWindowManager().openWindow( 'message', {\n\t\ttitle: this.lostSessionTitle,\n\t\tmessage: this.lostSessionMessage,\n\t\tactions: [\n\t\t\t{ action: 'login', label: mw.msg( 'login' ), flags: [ 'primary', 'progressive' ] }\n\t\t]\n\t} ).closed.then( function () {\n\t\tlocation.href = mw.cx.getLoginHref();\n\t} );\n};\n\n/* Methods */\n\nmw.cx.DashboardList.prototype.show = function () {\n\tthis.active = true;\n\tthis.$listContainer.show();\n};\n\nmw.cx.DashboardList.prototype.hide = function () {\n\tthis.active = false;\n\tthis.$listContainer.hide();\n};\n\nmw.cx.DashboardList.prototype.init = function ( languageFilterConfig ) {\n\tthis.languageFilter = new mw.cx.ui.LanguageFilter( $.extend( {\n\t\tonSourceLanguageChange: this.applyFilters.bind( this ),\n\t\tonTargetLanguageChange: this.applyFilters.bind( this )\n\t}, languageFilterConfig ) );\n\n\tthis.$loadingIndicatorSpinner = $( '<div>' )\n\t\t.addClass( 'cx-dashboardlist__loading-indicator' )\n\t\t.append( mw.cx.widgets.spinner() );\n\n\tthis.$listContainer = $( '<div>' );\n\tthis.$container.append( this.$listContainer );\n};\n\nmw.cx.DashboardList.prototype.listen = function () {\n\t$( window ).on( 'scroll', OO.ui.throttle( this.scrollHandler.bind( this ), 250 ) );\n};\n\nmw.cx.DashboardList.prototype.scrollHandler = function () {\n\tif ( !this.active ) {\n\t\treturn;\n\t}\n\n\tthis.onScroll();\n};\n\n/**\n * Get the details for pages with given titles.\n *\n * @param {string} language The language of the title.\n * @param {string[]} titles Title\n * @return {jQuery.Promise}\n */\nmw.cx.DashboardList.prototype.getPageDetails = function ( language, titles ) {\n\treturn this.siteMapper.getApi( language ).get( {\n\t\taction: 'query',\n\t\ttitles: titles,\n\t\tprop: this.getPageProps(),\n\t\tpiprop: 'thumbnail',\n\t\tpilimit: 50, // maximum\n\t\tpithumbsize: 100,\n\t\tredirects: true\n\t} );\n\n\t// TODO: Handle continue\n};\n\n/**\n * Show a title image and description based on source title.\n *\n * @param {Object} list\n */\nmw.cx.DashboardList.prototype.showTitleDetails = function ( list ) {\n\tvar apply, language, processPageDetails,\n\t\tqueries = {},\n\t\tmap = {};\n\n\tlist.forEach( function ( item ) {\n\t\tvar language = this.siteMapper.getWikiDomainCode( item.sourceLanguage ),\n\t\t\ttitle = item.sourceTitle || item.title;\n\n\t\tqueries[ language ] = queries[ language ] || [];\n\t\tqueries[ language ].push( title );\n\n\t\t// So that we can easily find the element in the callback\n\t\t// Same source title might be translated to multiple languages.\n\t\tmap[ title ] = map[ title ] || [];\n\t\tmap[ title ].push( item );\n\t}, this );\n\n\tapply = function ( page ) {\n\t\tif ( !map[ page.title ] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tmap[ page.title ].forEach( function ( item ) {\n\t\t\tif ( page.thumbnail ) {\n\t\t\t\titem.$image.removeClass( 'oo-ui-icon-article' )\n\t\t\t\t\t.css( 'background-image', 'url(' + page.thumbnail.source + ')' );\n\t\t\t}\n\n\t\t\tif ( page.description ) {\n\t\t\t\titem.$desc.text( page.description ).show();\n\t\t\t}\n\t\t} );\n\t};\n\n\tprocessPageDetails = function ( response ) {\n\t\tvar pageId, page,\n\t\t\tredirects = response.query.redirects || [],\n\t\t\tredirectsTo = {},\n\t\t\tpages = response.query.pages;\n\n\t\tredirects.forEach( function ( redirect ) {\n\t\t\tredirectsTo[ redirect.to ] = redirect.from;\n\t\t} );\n\n\t\tfor ( pageId in pages ) {\n\t\t\tpage = pages[ pageId ];\n\t\t\tpage.title = redirectsTo[ page.title ] || page.title;\n\t\t\tapply( page );\n\t\t}\n\t};\n\n\tfor ( language in queries ) {\n\t\tthis.getPageDetails( language, queries[ language ] ).done( processPageDetails );\n\t}\n};\n\n/* Abstract methods */\n\n/**\n * Page properties which need to be fetched in this.getPageDetails\n *\n * @method\n * @abstract\n * @return {string[]}\n */\nmw.cx.DashboardList.prototype.getPageProps = null;\n\n/**\n * @method\n * @abstract\n */\nmw.cx.DashboardList.prototype.applyFilters = null;\n\n/**\n * @method\n * @abstract\n */\nmw.cx.DashboardList.prototype.onScroll = null;\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dashboard/mw.cx.SuggestionList.js","messages":[{"ruleId":"jsdoc/valid-types","severity":1,"message":"@extends should not have a bracketed type in \"jsdoc\" mode.","line":12,"column":null,"nodeType":"Block","endLine":12,"endColumn":null},{"ruleId":"no-shadow","severity":1,"message":"'list' is already declared in the upper scope on line 120 column 57.","line":141,"column":10,"nodeType":"Identifier","messageId":"noShadow","endLine":141,"endColumn":14}],"errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation extension - Translation suggestions listing in dashboard.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n'use strict';\n\n/**\n * @class\n * @constructor\n * @extends {mw.cx.DashboardList}\n *\n * @param {jQuery} $container The container for this suggestion list\n * @param {mw.cx.SiteMapper} siteMapper\n */\nmw.cx.CXSuggestionList = function CXSuggestionList() {\n\tthis.suggestions = [];\n\tthis.lists = {};\n\n\tthis.$personalCollection = null;\n\tthis.$publicCollection = null;\n\tthis.$publicCollectionContainer = null;\n\tthis.refreshTrigger = null;\n\tthis.seed = null;\n\tthis.selectedSourcePage = null;\n\tthis.suggestionDialog = null;\n\n\t// Parent constructor\n\tmw.cx.CXSuggestionList.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.CXSuggestionList, mw.cx.DashboardList );\n\n/* Static properties */\n\n// Name of the empty list, used to show when there is no suggestions\nmw.cx.CXSuggestionList.static.emptyListName = 'cx-suggestionlist-empty';\nmw.cx.CXSuggestionList.static.listTypes = {\n\tTYPE_DEFAULT: 0,\n\tTYPE_FEATURED: 1,\n\tTYPE_DISCARDED: 2,\n\tTYPE_FAVORITE: 3,\n\tTYPE_CATEGORY: 4,\n\tTYPE_PERSONALIZED: 5\n};\nmw.cx.CXSuggestionList.static.listOrder = {};\nmw.cx.CXSuggestionList.static.listOrder[ mw.cx.CXSuggestionList.static.listTypes.TYPE_FAVORITE ] = 0;\nmw.cx.CXSuggestionList.static.listOrder[ mw.cx.CXSuggestionList.static.listTypes.TYPE_DISCARDED ] = 1;\nmw.cx.CXSuggestionList.static.listOrder[ mw.cx.CXSuggestionList.static.listTypes.TYPE_CATEGORY ] = 2;\nmw.cx.CXSuggestionList.static.listOrder[ mw.cx.CXSuggestionList.static.listTypes.TYPE_PERSONALIZED ] = 3;\nmw.cx.CXSuggestionList.static.listOrder[ mw.cx.CXSuggestionList.static.listTypes.TYPE_FEATURED ] = 4;\nmw.cx.CXSuggestionList.static.listOrder[ mw.cx.CXSuggestionList.static.listTypes.TYPE_DEFAULT ] = 5;\n\n/* Static methods */\n\nmw.cx.CXSuggestionList.static.friendlyListTypeName = function ( type ) {\n\tswitch ( type ) {\n\t\tcase this.listTypes.TYPE_DEFAULT:\n\t\t\treturn 'default';\n\t\tcase this.listTypes.TYPE_FEATURED:\n\t\t\treturn 'featured';\n\t\tcase this.listTypes.TYPE_DISCARDED:\n\t\t\treturn 'discarded';\n\t\tcase this.listTypes.TYPE_FAVORITE:\n\t\t\treturn 'favorite';\n\t\tcase this.listTypes.TYPE_CATEGORY:\n\t\t\treturn 'category';\n\t\tcase this.listTypes.TYPE_PERSONALIZED:\n\t\t\treturn 'personalized';\n\t\tdefault:\n\t\t\treturn 'unknown';\n\t}\n};\n\nmw.cx.CXSuggestionList.static.listCompare = function ( listA, listB ) {\n\tif ( this.listOrder[ listA.type ] > this.listOrder[ listB.type ] ) {\n\t\treturn 1;\n\t}\n\n\treturn -1;\n};\n\n/* Methods */\n\nmw.cx.CXSuggestionList.prototype.init = function () {\n\t// Parent method\n\tmw.cx.CXSuggestionList.parent.prototype.init.call( this, {\n\t\tupdateLocalStorage: true\n\t} );\n\n\tthis.seed = Math.floor( Math.random() * 10000 );\n\n\tthis.$personalCollection = $( '<div>' ).addClass( 'cx-suggestionlist__personal' );\n\tthis.$headerContainer = $( '<div>' )\n\t\t.addClass( 'cx-suggestionlist__header' )\n\t\t.append( $( '<span>' )\n\t\t\t.text( mw.msg( 'cx-suggestionlist-title' ) )\n\t\t\t.addClass( 'cx-suggestionlist__public-title' ),\n\t\tthis.languageFilter.$element );\n\n\tthis.$publicCollectionContainer = $( '<div>' )\n\t\t.addClass( 'cx-suggestionlist__public' )\n\t\t.append( this.$headerContainer, this.$loadingIndicatorSpinner );\n\tthis.$publicCollection = $( '<div>' )\n\t\t.addClass( 'cx-suggestionlist__public-items' );\n\tthis.$publicCollectionContainer.append( this.$publicCollection );\n\n\tthis.$listContainer\n\t\t.addClass( 'cx-suggestionlist-container' )\n\t\t.append( this.$personalCollection, this.$publicCollectionContainer );\n};\n\n/**\n * @param {Object} [list]\n * @return {jQuery.Promise}\n */\nmw.cx.CXSuggestionList.prototype.loadItems = function ( list ) {\n\tvar lists, promise,\n\t\tisEmpty = true,\n\t\tself = this;\n\n\tif ( !list ) {\n\t\t// Initial load, load available lists and couple of suggestions for them\n\t\tpromise = this.getSuggestionLists();\n\t} else {\n\t\t// Afterwards, suggestions are loaded per list\n\t\tif ( list.promise ) {\n\t\t\treturn;\n\t\t}\n\n\t\tpromise = this.loadSuggestionsForList( list );\n\t}\n\n\tthis.$loadingIndicatorSpinner.show();\n\tthis.pendingRequests++;\n\n\treturn promise.then( function ( suggestions ) {\n\t\tvar i, list, listId, listIds;\n\n\t\tlists = suggestions.lists;\n\n\t\t// Hide empty list, if any\n\t\tif (\n\t\t\tself.lists[ self.constructor.static.emptyListName ] &&\n\t\t\tself.lists[ self.constructor.static.emptyListName ].$list\n\t\t) {\n\t\t\tself.lists[ self.constructor.static.emptyListName ].$list.hide();\n\t\t}\n\n\t\tlistIds = self.sortLists( lists );\n\t\tfor ( i = 0; i < listIds.length; i++ ) {\n\t\t\tlistId = listIds[ i ];\n\t\t\tlist = lists[ listId ];\n\t\t\tif ( self.lists[ listId ] ) {\n\t\t\t\t// Add new set of suggestions to existing list\n\t\t\t\tself.lists[ listId ].suggestions =\n\t\t\t\t\tself.lists[ listId ].suggestions.concat( list.suggestions );\n\t\t\t} else {\n\t\t\t\t// Add as new list\n\t\t\t\tlist.id = listId;\n\t\t\t\tlist.seed = self.seed;\n\t\t\t\tself.lists[ listId ] = list;\n\t\t\t}\n\t\t\tif ( self.lists[ listId ].suggestions.length ) {\n\t\t\t\tisEmpty = false;\n\t\t\t}\n\t\t\t// Show the suggestions items.\n\t\t\tself.insertSuggestionList( listId, list.suggestions );\n\t\t}\n\n\t\treturn isEmpty;\n\t} ).always( function () {\n\t\tself.pendingRequests--;\n\n\t\tif ( self.pendingRequests === 0 ) {\n\t\t\tself.$loadingIndicatorSpinner.hide();\n\t\t}\n\t} );\n};\n\n/**\n * @return {jQuery.Promise}\n */\nmw.cx.CXSuggestionList.prototype.loadAllSuggestions = function () {\n\tvar self = this;\n\n\treturn $.when(\n\t\tthis.loadItems(),\n\t\tthis.loadItems( {\n\t\t\tid: 'trex'\n\t\t} )\n\t).then( function ( empty1, empty2 ) {\n\t\tif ( empty1 && empty2 ) {\n\t\t\t// If both are empty Show empty list information.\n\t\t\tself.showEmptySuggestionList();\n\t\t}\n\t}, function ( error ) {\n\t\tif ( error === 'assertuserfailed' ) {\n\t\t\tself.constructor.static.showLoginDialog();\n\t\t}\n\n\t\t// On fail, show empty list\n\t\tself.showEmptySuggestionList();\n\t} );\n};\n\n/**\n * Get all the translation suggestion lists of given user.\n *\n * @return {jQuery.Promise}\n */\nmw.cx.CXSuggestionList.prototype.getSuggestionLists = function () {\n\tvar params,\n\t\tapi = new mw.Api();\n\n\tparams = {\n\t\tassert: 'user',\n\t\taction: 'query',\n\t\tlist: 'contenttranslationsuggestions',\n\t\tfrom: this.languageFilter.getSourceLanguage(),\n\t\tto: this.languageFilter.getTargetLanguage(),\n\t\tlimit: 4,\n\t\tseed: this.seed\n\t};\n\n\treturn api.get( params ).then( function ( response ) {\n\t\treturn response.query.contenttranslationsuggestions;\n\t} );\n};\n\n/**\n * Get all the translation suggestion lists of given user.\n *\n * @param {Object} list\n * @return {jQuery.Promise}\n */\nmw.cx.CXSuggestionList.prototype.loadSuggestionsForList = function ( list ) {\n\tvar params,\n\t\tapi = new mw.Api();\n\n\tif ( list.id === 'trex' ) {\n\t\tthis.recommendtool = this.recommendtool || new mw.cx.Recommendtool(\n\t\t\tthis.languageFilter.getSourceLanguage(),\n\t\t\tthis.languageFilter.getTargetLanguage()\n\t\t);\n\t\treturn this.recommendtool.getSuggestionList();\n\t}\n\n\tif ( list.hasMore === false ) {\n\t\t// This method is supposed to be called only if we there are items to fetch\n\t\treturn $.Deferred().reject();\n\t}\n\n\tif ( !list.queryContinue ) {\n\t\t// Along with list information, we had fetch 4 suggestions as well.\n\t\tlist.queryContinue = {\n\t\t\toffset: 4\n\t\t};\n\t}\n\tparams = $.extend( {\n\t\tassert: 'user',\n\t\taction: 'query',\n\t\tlist: 'contenttranslationsuggestions',\n\t\tlistid: list.id,\n\t\tfrom: this.languageFilter.getSourceLanguage(),\n\t\tto: this.languageFilter.getTargetLanguage(),\n\t\tlimit: 10,\n\t\tseed: list.seed\n\t}, list.queryContinue );\n\n\tlist.promise = api.get( params ).then( function ( response ) {\n\t\tlist.promise = undefined;\n\t\tlist.queryContinue = response.continue;\n\t\tlist.hasMore = !!response.continue;\n\t\treturn response.query.contenttranslationsuggestions;\n\t} );\n\n\treturn list.promise;\n};\n\nmw.cx.CXSuggestionList.prototype.show = function () {\n\t// Parent method\n\tmw.cx.CXSuggestionList.parent.prototype.show.apply( this, arguments );\n\n\tif ( !Object.keys( this.lists ).length ) {\n\t\tthis.loadAllSuggestions();\n\t}\n};\n\nmw.cx.CXSuggestionList.prototype.applyFilters = function () {\n\tvar i, suggestion, listName, list;\n\n\t// Hide all lists\n\tfor ( listName in this.lists ) {\n\t\tlist = this.lists[ listName ];\n\n\t\t// List of favorite articles (a.k.a. \"For later\" list) should always be shown\n\t\tif ( list.type === this.constructor.static.listTypes.TYPE_FAVORITE ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif ( list.$list ) {\n\t\t\tlist.$list.hide();\n\t\t}\n\n\t\tif ( list.suggestions ) {\n\t\t\tfor ( i = 0; i < list.suggestions.length; i++ ) {\n\t\t\t\tsuggestion = list.suggestions[ i ];\n\t\t\t\tsuggestion.$element.remove();\n\t\t\t}\n\t\t}\n\n\t\tdelete this.lists[ listName ];\n\t}\n\n\tthis.recommendtool = null;\n\tthis.$publicCollection.empty().show();\n\n\t// Load suggested articles for new set of source and target languages.\n\t// This will bypass loading featured articles as suggestions for a new language pair,\n\t// because those are shipped alongside favorite articles, which we\n\t// show no matter which language pair is selected, meaning we don't\n\t// want to re-download favorite list on every language pair change. See T194476\n\t// TODO: Refactor suggestion list view and API and resolve this problem.\n\tthis.loadItems( { id: 'trex' } );\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.CXSuggestionList.prototype.getPageProps = function () {\n\treturn [ 'pageimages', 'description' ];\n};\n\n/**\n * List all suggestions.\n *\n * @param {string} listId\n * @param {Object[]} suggestions\n */\nmw.cx.CXSuggestionList.prototype.insertSuggestionList = function ( listId, suggestions ) {\n\tvar i, list, $suggestion, $listHeading,\n\t\tself = this,\n\t\t$suggestions = [];\n\n\tif ( !suggestions || !suggestions.length ) {\n\t\treturn;\n\t}\n\n\tlist = this.lists[ listId ];\n\t// Create the list container if not present already.\n\tif ( !list.$list ) {\n\t\tlist.$list = $( '<div>' )\n\t\t\t.attr( 'data-listid', listId )\n\t\t\t// The following classes are used here:\n\t\t\t// * cx-suggestionlist-type-0\n\t\t\t// * cx-suggestionlist-type-1\n\t\t\t// * cx-suggestionlist-type-2\n\t\t\t// * cx-suggestionlist-type-3\n\t\t\t// * cx-suggestionlist-type-4\n\t\t\t// * cx-suggestionlist-type-5\n\t\t\t.addClass( 'cx-suggestionlist cx-suggestionlist-type-' + list.type );\n\n\t\tif ( list.type === this.constructor.static.listTypes.TYPE_FAVORITE ) {\n\t\t\t// No need to show heading for misc fallback suggestions shown at the end.\n\t\t\t$listHeading = $( '<div>' )\n\t\t\t\t.addClass( 'cx-suggestionlist__header' )\n\t\t\t\t.append( $( '<span>' )\n\t\t\t\t\t.text( mw.msg( 'cx-suggestionlist-favorite' ) )\n\t\t\t\t);\n\t\t\tlist.$list.append( $listHeading );\n\t\t}\n\t\tif ( list.type === this.constructor.static.listTypes.TYPE_FAVORITE ) {\n\t\t\tthis.$personalCollection.append( list.$list );\n\t\t} else {\n\t\t\tthis.$publicCollectionContainer.show();\n\t\t\tthis.$publicCollection.append( list.$list );\n\t\t\tthis.$publicCollection.find( '.cx-suggestionlist' ).sort( function ( a, b ) {\n\t\t\t\treturn self.constructor.static.listCompare(\n\t\t\t\t\tself.lists[ $( a ).data( 'listid' ) ],\n\t\t\t\t\tself.lists[ $( b ).data( 'listid' ) ]\n\t\t\t\t);\n\t\t\t} ).appendTo( this.$publicCollection );\n\t\t}\n\t} else {\n\t\t// The list might be hidden if it became empty due to item removals.\n\t\tlist.$list.show();\n\t}\n\n\tfor ( i = 0; i < suggestions.length; i++ ) {\n\t\tsuggestions[ i ].rank = i;\n\t\tsuggestions[ i ].type = list.type;\n\t\tsuggestions[ i ].typeExtra = list.algorithm || '';\n\t\t$suggestion = this.buildSuggestionItem( suggestions[ i ] );\n\t\t$suggestions.push( $suggestion );\n\t\tmw.hook( 'mw.cx.suggestion.action' ).fire( 'shown', suggestions[ i ].rank,\n\t\t\tthis.constructor.static.friendlyListTypeName( suggestions[ i ].type ), suggestions[ i ].typeExtra,\n\t\t\tsuggestions[ i ].sourceLanguage, suggestions[ i ].targetLanguage, suggestions[ i ].title );\n\t}\n\tthis.showTitleDetails( suggestions );\n\n\t// Insert after last suggestion, but before any buttons etc.\n\tif ( list.$list.find( '.cx-slitem' ).length ) {\n\t\tlist.$list.find( '.cx-slitem' ).last().after( $suggestions );\n\t} else {\n\t\tlist.$list.append( $suggestions );\n\t}\n\n\tif ( list.type === this.constructor.static.listTypes.TYPE_CATEGORY ) {\n\t\tthis.makeExpandableList( listId );\n\t} else if (\n\t\tlist.type === this.constructor.static.listTypes.TYPE_FEATURED ||\n\t\tlist.type === this.constructor.static.listTypes.TYPE_PERSONALIZED\n\t) {\n\t\tthis.addRefreshTrigger();\n\t}\n};\n\n/**\n * Build the DOM for suggestion item\n *\n * @param {Object} suggestion\n * @return {jQuery}\n */\nmw.cx.CXSuggestionList.prototype.buildSuggestionItem = function ( suggestion ) {\n\tvar $image, $desc, $featured, $actions, discardAction, favoriteAction,\n\t\tsourceDir, targetDir, $targetLanguage,\n\t\t$translationLink, $suggestion, $metaDataContainer,\n\t\t$sourceLanguage, $languageContainer,\n\t\t$titleLanguageBlock;\n\n\t$suggestion = $( '<div>' )\n\t\t.addClass( 'cx-slitem' )\n\t\t.attr( 'id', suggestion.id );\n\t$image = $( '<div>' )\n\t\t.addClass( 'cx-slitem__image oo-ui-icon-article' );\n\n\tsourceDir = $.uls.data.getDir( suggestion.sourceLanguage );\n\ttargetDir = $.uls.data.getDir( suggestion.targetLanguage );\n\n\t$featured = $( [] );\n\tif ( this.lists[ suggestion.listId ].type === this.constructor.static.listTypes.TYPE_FEATURED ) {\n\t\t$featured = $( '<span>' )\n\t\t\t.addClass( 'cx-sltag cx-sltag--featured' )\n\t\t\t.text( this.lists[ suggestion.listId ].displayName );\n\t}\n\n\t$translationLink = $( '<div>' )\n\t\t.addClass( 'cx-slitem__translation-link' )\n\t\t.attr( 'data-suggestion', JSON.stringify( suggestion ) )\n\t\t// It must be a separate element to ensure\n\t\t// separation from the target title\n\t\t.append(\n\t\t\t$( '<span>' )\n\t\t\t\t.text( suggestion.title )\n\t\t\t\t.addClass( 'source-title' )\n\t\t\t\t.prop( {\n\t\t\t\t\tlang: suggestion.sourceLanguage,\n\t\t\t\t\tdir: sourceDir\n\t\t\t\t} ),\n\t\t\t$featured\n\t\t);\n\n\t$sourceLanguage = $( '<a>' )\n\t\t.prop( {\n\t\t\tlang: suggestion.sourcelanguage,\n\t\t\tdir: sourceDir,\n\t\t\thref: this.siteMapper.getPageUrl( suggestion.sourceLanguage, suggestion.title ),\n\t\t\ttarget: '_blank',\n\t\t\ttitle: mw.msg( 'cx-suggestionlist-view-source-page' )\n\t\t} )\n\t\t.on( 'click', function ( e ) {\n\t\t\t// Do not propagate to the parent suggestion item. Prevent opening selected source page dialog\n\t\t\te.stopPropagation();\n\t\t} )\n\t\t.addClass( 'cx-slitem__languages__language cx-slitem__languages__language--source' )\n\t\t.text( $.uls.data.getAutonym( suggestion.sourceLanguage ) );\n\n\t$targetLanguage = $( '<div>' )\n\t\t.prop( {\n\t\t\tlang: suggestion.targetLanguage,\n\t\t\tdir: targetDir\n\t\t} )\n\t\t.addClass( 'cx-slitem__languages__language cx-slitem__languages__language--target' )\n\t\t.text( $.uls.data.getAutonym( suggestion.targetLanguage ) );\n\n\t$languageContainer = $( '<div>' )\n\t\t.addClass( 'cx-slitem__languages' )\n\t\t.append( $sourceLanguage, $targetLanguage );\n\n\t$desc = $( '<div>' )\n\t\t.prop( {\n\t\t\tlang: suggestion.sourceLanguage,\n\t\t\tdir: sourceDir\n\t\t} )\n\t\t// We need to set ellipsis for pseudo element through data attribute\n\t\t// as there is no way to add localized message to LESS or manipulate\n\t\t// pseudo elements directly with JS\n\t\t.attr( 'data-ellipsis', mw.msg( 'ellipsis' ) )\n\t\t.addClass( 'cx-slitem__desc' )\n\t\t.hide();\n\n\tdiscardAction = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\ticon: 'close',\n\t\tclasses: [ 'cx-slitem__action--discard' ]\n\t} );\n\tdiscardAction.once( 'click', this.discardSuggestion.bind( this, suggestion ) );\n\n\tif ( this.lists[ suggestion.listId ].type === this.constructor.static.listTypes.TYPE_FAVORITE ) {\n\t\tdiscardAction.$element.hide();\n\n\t\tfavoriteAction = new OO.ui.ButtonWidget( {\n\t\t\tframed: false,\n\t\t\tflags: [ 'progressive' ],\n\t\t\tclasses: [ 'cx-slitem__action--nonfavorite' ],\n\t\t\ticon: 'bookmark'\n\t\t} );\n\n\t\tfavoriteAction.once( 'click', this.unmarkFavorite.bind( this, suggestion ) );\n\t\tfavoriteAction.$element.on( 'mouseenter', this.setOutlineIcon.bind( favoriteAction ) );\n\t\tfavoriteAction.$element.on( 'mouseleave', this.setFilledIcon.bind( favoriteAction ) );\n\t} else {\n\t\tfavoriteAction = new OO.ui.ButtonWidget( {\n\t\t\tframed: false,\n\t\t\tclasses: [ 'cx-slitem__action--favorite' ],\n\t\t\ticon: 'bookmarkOutline'\n\t\t} );\n\n\t\tfavoriteAction.once( 'click', this.markFavorite.bind( this, suggestion ) );\n\t\tfavoriteAction.$element.on( 'mouseenter', this.setFilledIcon.bind( favoriteAction ) );\n\t\tfavoriteAction.$element.on( 'mouseleave', this.setOutlineIcon.bind( favoriteAction ) );\n\t}\n\n\t$metaDataContainer = $( '<div>' )\n\t\t.addClass( 'cx-slitem__meta' )\n\t\t.append( $languageContainer );\n\n\t$titleLanguageBlock = $( '<div>' )\n\t\t.addClass( 'cx-slitem__details' )\n\t\t.append( $translationLink, $desc, $metaDataContainer );\n\t$actions = $( '<div>' )\n\t\t.addClass( 'cx-slitem__actions' )\n\t\t.append( favoriteAction.$element, discardAction.$element );\n\t$suggestion.append(\n\t\t$image,\n\t\t$titleLanguageBlock,\n\t\t$actions\n\t);\n\n\t// Store reference to the DOM node\n\tsuggestion.$element = $suggestion;\n\tsuggestion.$desc = $desc;\n\tsuggestion.$image = $image;\n\tsuggestion.$discardAction = discardAction.$element;\n\n\treturn $suggestion;\n};\n\n/**\n * Change \"favorite\" button icon to bookmark outline\n */\nmw.cx.CXSuggestionList.prototype.setOutlineIcon = function () {\n\tthis.clearFlags();\n\tthis.setIcon( 'bookmarkOutline' );\n};\n\n/**\n * Change \"favorite\" button icon to filled bookmark\n */\nmw.cx.CXSuggestionList.prototype.setFilledIcon = function () {\n\tthis.setFlags( 'progressive' );\n\tthis.setIcon( 'bookmark' );\n};\n\n/**\n * Discard a suggestion.\n *\n * @param  {Object} suggestion\n * @return {boolean}\n */\nmw.cx.CXSuggestionList.prototype.discardSuggestion = function ( suggestion ) {\n\tvar params,\n\t\tapi = new mw.Api();\n\n\tparams = {\n\t\tassert: 'user',\n\t\taction: 'cxsuggestionlist',\n\t\tlistname: 'cx-suggestionlist-discarded',\n\t\tlistaction: 'add',\n\t\ttitles: suggestion.title,\n\t\tfrom: suggestion.sourceLanguage,\n\t\tto: suggestion.targetLanguage\n\t};\n\tapi.postWithToken( 'csrf', params ).done( function ( response ) {\n\t\tif ( response.cxsuggestionlist.result === 'success' ) {\n\t\t\tmw.hook( 'mw.cx.suggestion.action' ).fire( 'discard', suggestion.rank,\n\t\t\t\tmw.cx.CXSuggestionList.static.friendlyListTypeName( suggestion.type ), suggestion.typeExtra,\n\t\t\t\tsuggestion.sourceLanguage, suggestion.targetLanguage, suggestion.title\n\t\t\t);\n\t\t\t// FIXME: Use CSS transition\n\t\t\t// eslint-disable-next-line no-jquery/no-slide\n\t\t\tsuggestion.$element.slideUp( 'slow', function () {\n\t\t\t\t$( this ).remove();\n\t\t\t} );\n\t\t}\n\t} ).fail( this.suggestionListFailHandler );\n\t// Avoid event propagation.\n\treturn false;\n};\n\n/**\n * Mark a suggestion as favorite.\n *\n * @param  {Object} suggestion\n * @return {boolean}\n */\nmw.cx.CXSuggestionList.prototype.markFavorite = function ( suggestion ) {\n\tvar params, api = new mw.Api(),\n\t\tself = this;\n\n\tparams = {\n\t\tassert: 'user',\n\t\taction: 'cxsuggestionlist',\n\t\tlistname: 'cx-suggestionlist-favorite',\n\t\tlistaction: 'add',\n\t\ttitles: suggestion.title,\n\t\tfrom: suggestion.sourceLanguage,\n\t\tto: suggestion.targetLanguage\n\t};\n\tapi.postWithToken( 'csrf', params ).done( function ( response ) {\n\t\tvar favoriteListId;\n\t\tif ( response.cxsuggestionlist.result === 'success' ) {\n\t\t\tmw.hook( 'mw.cx.suggestion.action' ).fire( 'favorite', suggestion.rank,\n\t\t\t\tself.constructor.static.friendlyListTypeName( suggestion.type ),\n\t\t\t\tsuggestion.typeExtra, suggestion.sourceLanguage,\n\t\t\t\tsuggestion.targetLanguage, suggestion.title\n\t\t\t);\n\t\t\tsuggestion.$element.addClass( 'cx-slideup-hide' );\n\t\t\tsuggestion.$element.one( 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd',\n\t\t\t\tfunction () {\n\t\t\t\t\t$( this ).remove();\n\t\t\t\t}\n\t\t\t);\n\n\t\t\tfavoriteListId = self.getListId( 'cx-suggestionlist-favorite' );\n\t\t\tsuggestion.listId = favoriteListId;\n\t\t\tif ( favoriteListId === null ) {\n\t\t\t\t// We need to construct a dummy list for now to help the UI rendering.\n\t\t\t\tfavoriteListId = 'cx-suggestionlist-favorite';\n\t\t\t\tself.lists[ favoriteListId ] = {\n\t\t\t\t\tdisplayName: mw.msg( 'cx-suggestionlist-favorite' ),\n\t\t\t\t\tname: favoriteListId,\n\t\t\t\t\tsuggestions: [],\n\t\t\t\t\ttype: self.constructor.static.listTypes.TYPE_FAVORITE\n\t\t\t\t};\n\t\t\t}\n\t\t\tsuggestion.listId = favoriteListId;\n\t\t\tself.lists[ favoriteListId ].suggestions.push( suggestion );\n\t\t\tself.insertSuggestionList( favoriteListId, [ suggestion ], true );\n\t\t\t// Remove favorited article from the list of suggestions\n\t\t\tself.lists.trex.suggestions = self.lists.trex.suggestions.filter( function ( item ) {\n\t\t\t\treturn item.title !== suggestion.title;\n\t\t\t} );\n\t\t}\n\t} ).fail( this.suggestionListFailHandler );\n\t// Avoid event propagation.\n\treturn false;\n};\n\n/**\n * Unmark a suggestion as favorite.\n *\n * @param  {Object} suggestion\n * @return {boolean}\n */\nmw.cx.CXSuggestionList.prototype.unmarkFavorite = function ( suggestion ) {\n\tvar params, self = this,\n\t\tapi = new mw.Api();\n\n\tparams = {\n\t\tassert: 'user',\n\t\taction: 'cxsuggestionlist',\n\t\tlistname: 'cx-suggestionlist-favorite',\n\t\tlistaction: 'remove',\n\t\ttitles: suggestion.title,\n\t\tfrom: suggestion.sourceLanguage,\n\t\tto: suggestion.targetLanguage\n\t};\n\tapi.postWithToken( 'csrf', params ).done( function ( response ) {\n\t\tif ( response.cxsuggestionlist.result === 'success' ) {\n\t\t\tsuggestion.$element.addClass( 'cx-slidedown-hide' );\n\t\t\tsuggestion.$element.one( 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd',\n\t\t\t\tfunction () {\n\t\t\t\t\tvar favoriteListId;\n\t\t\t\t\t$( this ).remove();\n\t\t\t\t\tfavoriteListId = self.getListId( 'cx-suggestionlist-favorite' );\n\t\t\t\t\tif ( !self.lists[ favoriteListId ].$list.find( '.cx-slitem' ).length ) {\n\t\t\t\t\t\tself.lists[ favoriteListId ].$list.hide();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t);\n\t\t\t// Do we need to add to general suggestions?\n\t\t}\n\t} ).fail( this.suggestionListFailHandler );\n\t// Avoid event propagation.\n\treturn false;\n};\n\n/**\n * Failure handler for API calls which are using action: 'cxsuggestionlist'\n *\n * @param {string} error\n */\nmw.cx.CXSuggestionList.prototype.suggestionListFailHandler = function ( error ) {\n\tif ( error === 'assertuserfailed' ) {\n\t\tmw.cx.CXTranslationList.static.showLoginDialog();\n\t}\n\t// TODO: How do we handle other types of failure?\n};\n\nmw.cx.CXSuggestionList.prototype.showEmptySuggestionList = function () {\n\tvar $img, $title, $desc,\n\t\tlistId = this.constructor.static.emptyListName;\n\n\tif ( !this.lists[ listId ] ) {\n\t\tthis.lists[ listId ] = {\n\t\t\tname: listId\n\t\t};\n\n\t\t$img = $( '<div>' )\n\t\t\t.addClass( 'cx-suggestionlist-empty__img' );\n\n\t\t$title = $( '<div>' )\n\t\t\t.addClass( 'cx-suggestionlist-empty__title' )\n\t\t\t.text( mw.msg( 'cx-suggestionlist-empty-title' ) );\n\n\t\t$desc = $( '<div>' )\n\t\t\t.addClass( 'cx-suggestionlist-empty__desc' )\n\t\t\t.text( mw.msg( 'cx-suggestionlist-empty-desc' ) );\n\n\t\tthis.lists[ listId ].$list = $( '<div>' )\n\t\t\t.addClass( 'cx-suggestionlist-empty' )\n\t\t\t.append( $img, $title, $desc );\n\n\t\t$desc.after( $( '<div>' )\n\t\t\t.addClass( 'cx-suggestionlist-empty__recommend' )\n\t\t\t.append( $( '<a>' )\n\t\t\t\t.text( mw.msg( 'cx-suggestionlist-empty-desc-recommend-link-text' ) )\n\t\t\t\t.prop( 'href', 'https://recommend.wmflabs.org' )\n\t\t\t)\n\t\t);\n\n\t\tthis.$publicCollection.empty().show();\n\t\tif ( this.refreshTrigger ) {\n\t\t\tthis.refreshTrigger.toggle( false );\n\t\t}\n\t\tthis.$publicCollectionContainer.append( this.lists[ listId ].$list );\n\t}\n\n\tthis.lists[ listId ].$list.show();\n};\n\n/**\n * Get the list identifier by its name.\n *\n * @param  {string} listName List name.\n * @return {string|null} list identifier.\n */\nmw.cx.CXSuggestionList.prototype.getListId = function ( listName ) {\n\tvar listId, list;\n\n\tfor ( listId in this.lists ) {\n\t\tlist = this.lists[ listId ];\n\n\t\tif ( listName === list.name ) {\n\t\t\treturn listId;\n\t\t}\n\t}\n\n\treturn null;\n};\n\n/**\n * Event handlers\n */\nmw.cx.CXSuggestionList.prototype.listen = function () {\n\tvar self = this;\n\n\t// Parent method\n\tmw.cx.CXSuggestionList.parent.prototype.listen.apply( this, arguments );\n\n\tthis.$listContainer.on( 'click', '.cx-suggestionlist .cx-slitem', function () {\n\t\tvar $this = $( this ),\n\t\t\tsuggestion = $this.find( '.cx-slitem__translation-link' ).data( 'suggestion' ),\n\t\t\timageUrl = $this\n\t\t\t\t.find( '.cx-slitem__image:not(.oo-ui-icon-article)' )\n\t\t\t\t.css( 'background-image' );\n\t\tself.showSuggestionDialog( suggestion, imageUrl );\n\n\t\tmw.hook( 'mw.cx.suggestion.action' ).fire( 'accept', suggestion.rank,\n\t\t\tself.constructor.static.friendlyListTypeName( suggestion.type ),\n\t\t\tsuggestion.typeExtra, suggestion.sourceLanguage,\n\t\t\tsuggestion.targetLanguage, suggestion.title\n\t\t);\n\t} );\n};\n\n/**\n * Show dialog for selected suggestion\n *\n * @param {Object} suggestion Selected suggestion, for which dialog is shown\n * @param {string|null} imageUrl URL of suggestion page image\n */\nmw.cx.CXSuggestionList.prototype.showSuggestionDialog = function ( suggestion, imageUrl ) {\n\tif ( imageUrl ) {\n\t\timageUrl = imageUrl.slice( 5, -2 );\n\t}\n\n\tthis.selectedSourcePage = new mw.cx.SelectedSourcePage( this.siteMapper, {\n\t\tonDiscard: this.discardSuggestionDialog.bind( this )\n\t} );\n\n\tthis.selectedSourcePage.setData(\n\t\tsuggestion.title,\n\t\tthis.siteMapper.getPageUrl( suggestion.sourceLanguage, suggestion.title ),\n\t\t{\n\t\t\timageUrl: imageUrl,\n\t\t\timageIcon: 'article',\n\t\t\tsourceLanguage: suggestion.sourceLanguage,\n\t\t\ttargetLanguage: suggestion.targetLanguage,\n\t\t\tparams: { prop: [ 'langlinks', 'pageviews', 'langlinkscount' ] }\n\t\t}\n\t);\n\n\tif ( !this.suggestionDialog ) {\n\t\tthis.suggestionDialog = new mw.cx.SelectedSourcePageDialog();\n\t\tOO.ui.getWindowManager().addWindows( [ this.suggestionDialog ] );\n\t}\n\tOO.ui.getWindowManager().openWindow( this.suggestionDialog, { selectedSourcePage: this.selectedSourcePage } );\n};\n\n/**\n * Closes suggestion dialog\n */\nmw.cx.CXSuggestionList.prototype.discardSuggestionDialog = function () {\n\tOO.ui.getWindowManager().closeWindow( this.suggestionDialog );\n};\n\n/**\n * Scroll handler for the suggestions\n */\nmw.cx.CXSuggestionList.prototype.onScroll = function () {\n\tvar expandedListId, $expandedList, triggerPos,\n\t\tscrollTop, windowHeight, visibleArea, $loadTrigger;\n\n\tscrollTop = window.pageYOffset;\n\twindowHeight = document.documentElement.clientHeight;\n\tvisibleArea = windowHeight + scrollTop;\n\n\t// Load next batch of items when loadTrigger is in viewpot\n\t$expandedList = this.$container.find( '.cx-suggestionlist--expanded' );\n\t$loadTrigger = $expandedList.find( '.cx-suggestionlist__collapse' );\n\n\tif ( !$loadTrigger.length ) {\n\t\treturn;\n\t}\n\n\ttriggerPos = $loadTrigger.offset().top + $loadTrigger.outerHeight();\n\texpandedListId = $expandedList.data( 'listid' );\n\tif (\n\t\texpandedListId &&\n\t\tthis.lists[ expandedListId ].hasMore !== false &&\n\t\tvisibleArea >= triggerPos && scrollTop <= triggerPos\n\t) {\n\t\tthis.loadItems( this.lists[ expandedListId ] ).fail( function ( error ) {\n\t\t\tif ( error === 'assertuserfailed' ) {\n\t\t\t\t$( window ).off( 'scroll' );\n\t\t\t\tmw.cx.CXSuggestionList.static.showLoginDialog();\n\t\t\t}\n\t\t} );\n\t}\n};\n\n/**\n * Sort the lists in their logical order to display.\n *\n * @param  {Object[]} lists\n * @return {number[]} Ordered list ids.\n */\nmw.cx.CXSuggestionList.prototype.sortLists = function ( lists ) {\n\tfunction compareKeys( a, b ) {\n\t\treturn mw.cx.CXSuggestionList.static.listCompare( lists[ a ], lists[ b ] );\n\t}\n\n\treturn Object.keys( lists ).sort( compareKeys );\n};\n\n/**\n * Make the list expandable and collapsible.\n *\n * @param {string} listId\n */\nmw.cx.CXSuggestionList.prototype.makeExpandableList = function ( listId ) {\n\tvar list = this.lists[ listId ];\n\n\tif ( list.$list.is( '.cx-suggestionlist--collapsed' ) ||\n\t\tlist.$list.is( '.cx-suggestionlist--expanded' )\n\t) {\n\t\treturn;\n\t}\n\n\tlist.$list.find( 'h2' ).on( 'click', this.expandOrCollapse.bind( this, list.id ) );\n\tif ( list.suggestions.length > 2 ) {\n\t\tlist.$list.append( $( '<div>' )\n\t\t\t.addClass( 'cx-suggestionlist__expand' )\n\t\t\t.text( mw.msg( 'cx-suggestionlist-expand' ) )\n\t\t\t.on( 'click', this.expandOrCollapse.bind( this, list.id ) )\n\t\t);\n\t}\n\t// By default, the list is collapsed.\n\tlist.$list.addClass( 'cx-suggestionlist--collapsed' );\n};\n\n/**\n * Expand of collapse the list with the given listId.\n *\n * @param {string} listId List identifier.\n */\nmw.cx.CXSuggestionList.prototype.expandOrCollapse = function ( listId ) {\n\tvar $trigger,\n\t\tlist = this.lists[ listId ];\n\n\t$trigger = list.$list.find( '.cx-suggestionlist__expand, .cx-suggestionlist__collapse' );\n\n\tif ( list.$list.is( '.cx-suggestionlist--collapsed' ) ) {\n\t\t// Collapse all expended lists.\n\t\tthis.$listContainer.find( '.cx-suggestionlist__collapse' ).trigger( 'click' );\n\t\t$trigger.text( mw.msg( 'cx-suggestionlist-collapse' ) );\n\t} else {\n\t\t$trigger.text( mw.msg( 'cx-suggestionlist-expand' ) );\n\t}\n\n\t// eslint-disable-next-line no-jquery/no-class-state\n\t$trigger.toggleClass( 'cx-suggestionlist__collapse cx-suggestionlist__expand' );\n\t// eslint-disable-next-line no-jquery/no-class-state\n\tlist.$list.toggleClass( 'cx-suggestionlist--collapsed cx-suggestionlist--expanded' );\n};\n\n/**\n * Make the list refreshable\n */\nmw.cx.CXSuggestionList.prototype.addRefreshTrigger = function () {\n\tif ( this.refreshTrigger ) {\n\t\tthis.refreshTrigger.toggle( true );\n\t\treturn;\n\t}\n\n\tthis.refreshTrigger = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\tclasses: [ 'cx-suggestionlist__refresh' ],\n\t\tlabel: mw.msg( 'cx-suggestionlist-refresh' ),\n\t\ticon: 'reload',\n\t\tflags: 'progressive'\n\t} ).connect( this, { click: 'refreshPublicLists' } );\n\n\tthis.$publicCollectionContainer.append( this.refreshTrigger.$element );\n};\n\nmw.cx.CXSuggestionList.prototype.refreshPublicLists = function () {\n\tvar listId, list,\n\t\tcategoryListCount = 2;\n\n\t// Scroll the page up to the beginning of $publicCollection\n\t$( 'html, body' ).animate( {\n\t\t// 200 px subtracted to deal with the sticky header.\n\t\t// It need not be 100% accurate. The idea is to scroll up\n\t\t// so that the beginning of public collection is visible.\n\t\tscrollTop: this.$publicCollectionContainer.offset().top - 200\n\t}, 'slow' );\n\n\tfor ( listId in this.lists ) {\n\t\tlist = this.lists[ listId ];\n\n\t\tif ( !list.$list ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (\n\t\t\tlist.type === this.constructor.static.listTypes.TYPE_FEATURED ||\n\t\t\tlist.type === this.constructor.static.listTypes.TYPE_PERSONALIZED\n\t\t) {\n\t\t\tthis.refreshList( list.id );\n\t\t} else if (\n\t\t\tlist.type === this.constructor.static.listTypes.TYPE_CATEGORY &&\n\t\t\tcategoryListCount\n\t\t) {\n\t\t\t// The first two lists shown will be removed.\n\t\t\tlist.$list.remove();\n\t\t\tdelete this.lists[ listId ];\n\t\t\tcategoryListCount--;\n\t\t}\n\t}\n};\n\n/**\n * @param {string} listId\n */\nmw.cx.CXSuggestionList.prototype.refreshList = function ( listId ) {\n\tvar i, itemsToRemove = [],\n\t\tlist = this.lists[ listId ];\n\n\tif ( !list ) {\n\t\treturn;\n\t}\n\tif ( list.suggestions ) {\n\t\titemsToRemove = list.suggestions;\n\t}\n\tlist.suggestions = [];\n\t// Do not run out of suggestions\n\tlist.seed = Math.floor( Math.random() * 10000 );\n\tlist.queryContinue = undefined;\n\tlist.hasMore = true;\n\t// Remove the old items.\n\tthis.loadItems( list ).then( function () {\n\t\tfor ( i = 0; i < itemsToRemove.length; i++ ) {\n\t\t\titemsToRemove[ i ].$element.remove();\n\t\t}\n\t}, function ( error ) {\n\t\tif ( error === 'assertuserfailed' ) {\n\t\t\tmw.cx.CXSuggestionList.static.showLoginDialog();\n\t\t}\n\t} );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dashboard/mw.cx.TranslationList.js","messages":[{"ruleId":"jsdoc/valid-types","severity":1,"message":"@extends should not have a bracketed type in \"jsdoc\" mode.","line":14,"column":null,"nodeType":"Block","endLine":14,"endColumn":null},{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":15,"column":null,"nodeType":"Block","endLine":15,"endColumn":null}],"errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation extension - Translation listing in dashboard.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n'use strict';\n\n/**\n * CXTranslationList\n *\n * @class\n * @constructor\n * @extends {mw.cx.DashboardList}\n * @mixins OO.EventEmitter\n *\n * @param {jQuery} $container\n * @param {mw.cx.SiteMapper} siteMapper\n * @param {string} type\n */\nmw.cx.CXTranslationList = function CXTranslationList( $container, siteMapper, type ) {\n\tthis.type = type;\n\n\tthis.translations = [];\n\t// sourceLanguages and targetLanguages are arrays of languages,\n\t// for which there are translation list items\n\tthis.sourceLanguages = [];\n\tthis.targetLanguages = [];\n\n\tthis.promise = null;\n\tthis.queryContinue = null;\n\tthis.hasMore = true;\n\n\t// Parent constructor\n\tmw.cx.CXTranslationList.super.call( this, $container, siteMapper );\n\n\t// Mixin constructors\n\tOO.EventEmitter.call( this );\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.CXTranslationList, mw.cx.DashboardList );\nOO.mixinClass( mw.cx.CXTranslationList, OO.EventEmitter );\n\n/* Methods */\n\n/**\n * Get all the translations of given user.\n *\n * @return {jQuery.Promise}\n */\nmw.cx.CXTranslationList.prototype.getTranslations = function () {\n\tvar self = this,\n\t\tparams,\n\t\tapi = new mw.Api();\n\n\tif ( this.promise ) {\n\t\t// Avoid duplicate API requests.\n\t\treturn this.promise;\n\t}\n\n\tif ( this.hasMore === false ) {\n\t\treturn $.Deferred().resolve( [] );\n\t}\n\n\tparams = $.extend( {\n\t\tassert: 'user',\n\t\tlist: 'contenttranslation',\n\t\ttype: this.type,\n\t\tlimit: 15\n\t}, this.queryContinue );\n\n\tthis.promise = api.get( params ).then( function ( response ) {\n\t\tself.promise = null;\n\t\tself.queryContinue = response.continue;\n\t\tself.hasMore = !!response.continue;\n\n\t\tif ( response.query.contenttranslation.languages ) {\n\t\t\tself.languages = response.query.contenttranslation.languages;\n\t\t}\n\n\t\t// Remove unnecessary object wrapping to get plain list of objects\n\t\treturn response.query.contenttranslation.translations.map( function ( e ) {\n\t\t\treturn e.translation;\n\t\t} );\n\t} );\n\n\treturn this.promise;\n};\n\nmw.cx.CXTranslationList.prototype.init = function () {\n\t// Parent method\n\tmw.cx.CXTranslationList.parent.prototype.init.call( this, {\n\t\tcanBeSame: true,\n\t\tcanBeUndefined: true\n\t} );\n\n\tthis.$headerContainer = $( '<div>' )\n\t\t.addClass( 'cx-translationlist__header' )\n\t\t.append(\n\t\t\t// The following messages are used here\n\t\t\t// * cx-translation-label-draft\n\t\t\t// * cx-translation-label-published\n\t\t\t$( '<span>' ).text( mw.msg( 'cx-translation-label-' + this.type ) ),\n\t\t\tthis.languageFilter.$element.hide()\n\t\t);\n\tthis.$listContainer\n\t\t.addClass( 'cx-translationlist' )\n\t\t.append( this.$headerContainer, this.$loadingIndicatorSpinner );\n\n\tthis.$container.append( this.$emptyTranslationsList );\n};\n\nmw.cx.CXTranslationList.prototype.loadItems = function () {\n\tvar promise, self = this;\n\n\tif ( this.promise ) {\n\t\treturn this.promise;\n\t}\n\n\tfunction insertUnique( array, value ) {\n\t\tif ( array.indexOf( value ) < 0 ) {\n\t\t\tarray.push( value );\n\t\t}\n\t}\n\n\tthis.$loadingIndicatorSpinner.show();\n\tthis.pendingRequests++;\n\n\tpromise = this.getTranslations();\n\tpromise.done( function ( translations ) {\n\t\tself.translations = self.translations.concat( translations );\n\n\t\tif ( !self.translations.length ) {\n\t\t\tself.$emptyTranslationsList = self.buildEmptyTranslationList();\n\t\t\tself.$listContainer.append( self.$emptyTranslationsList );\n\t\t\tself.emit( 'noDrafts' );\n\t\t\treturn;\n\t\t}\n\n\t\ttranslations.forEach( function ( translation ) {\n\t\t\tinsertUnique( self.sourceLanguages, translation.sourceLanguage );\n\t\t\tinsertUnique( self.targetLanguages, translation.targetLanguage );\n\t\t} );\n\n\t\tself.fillULS();\n\n\t\tself.renderTranslations( translations );\n\t} ).fail( function ( error ) {\n\t\tself.promise = null;\n\n\t\tif ( error === 'assertuserfailed' ) {\n\t\t\t$( window ).off( 'scroll' );\n\t\t\tself.constructor.static.showLoginDialog();\n\t\t}\n\t} ).always( function () {\n\t\tself.pendingRequests--;\n\n\t\tif ( self.pendingRequests === 0 ) {\n\t\t\tself.$loadingIndicatorSpinner.hide();\n\t\t}\n\t} );\n\n\treturn promise;\n};\n\n/**\n * Fill source and target language filter with languages for which there are translationlist items\n */\nmw.cx.CXTranslationList.prototype.fillULS = function () {\n\tvar languageDecorator;\n\t// Check if there is only one language combination, e.g. English to Spanish\n\t// sourceLanguages - [ 'en' ]\n\t// targetLanguages - [ 'es' ]\n\tif ( this.sourceLanguages.length === 1 && this.targetLanguages.length === 1 ) {\n\t\treturn;\n\t}\n\n\t// At this point, we know there is more than one language combination\n\n\tthis.sourceLanguages.unshift( 'x-all' );\n\tthis.targetLanguages.unshift( 'x-all' );\n\n\tlanguageDecorator = function ( $language, languageCode ) {\n\t\tif ( languageCode === 'x-all' ) {\n\t\t\t$language.parent().addClass( 'cx-translationlist-uls-all-languages' );\n\t\t}\n\t};\n\n\tthis.languageFilter.fillSourceLanguages( this.sourceLanguages, true, {\n\t\tulsPurpose: 'cx-translationlist-source',\n\t\tlanguageDecorator: languageDecorator\n\t} );\n\tthis.languageFilter.fillTargetLanguages( this.targetLanguages, true, {\n\t\tulsPurpose: 'cx-translationlist-target',\n\t\tlanguageDecorator: languageDecorator\n\t} );\n\n\tthis.languageFilter.$element.show();\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.CXTranslationList.prototype.getPageProps = function () {\n\treturn [ 'pageimages' ];\n};\n\nmw.cx.CXTranslationList.prototype.show = function () {\n\t// Parent method\n\tmw.cx.CXTranslationList.parent.prototype.show.apply( this, arguments );\n\n\tif ( !this.translations.length ) {\n\t\tthis.loadItems();\n\t}\n};\n\n/**\n * Go to translation view\n *\n * @param {Object} translation\n */\nmw.cx.CXTranslationList.prototype.continueTranslation = function ( translation ) {\n\tif ( translation.status === 'deleted' ) {\n\t\treturn;\n\t}\n\n\t// Set CX token as cookie.\n\tmw.cx.siteMapper.setCXToken(\n\t\ttranslation.sourceLanguage,\n\t\ttranslation.targetLanguage,\n\t\ttranslation.sourceTitle\n\t);\n\tlocation.href = new mw.Uri( mw.cx.siteMapper.getCXUrl(\n\t\ttranslation.sourceTitle,\n\t\ttranslation.targetTitle,\n\t\ttranslation.sourceLanguage,\n\t\ttranslation.targetLanguage,\n\t\t{ campaign: new mw.Uri().query.campaign }\n\t) ).toString();\n};\n\n/**\n * List all translations.\n *\n * @param {Object[]} translations\n */\nmw.cx.CXTranslationList.prototype.renderTranslations = function ( translations ) {\n\tvar i, translation, progress, $translation,\n\t\t$lastUpdated, $image, $progressbar,\n\t\tsourceDir, targetDir, $targetTitle,\n\t\t$translationLink,\n\t\t$sourceLanguage, $targetLanguage, $languageContainer,\n\t\tdeleteTranslation, $actions,\n\t\tcontinueTranslation,\n\t\t$titleLanguageBlock,\n\t\t$translations = [];\n\n\tfor ( i = 0; i < translations.length; i++ ) {\n\t\ttranslation = translations[ i ];\n\n\t\ttry {\n\t\t\tprogress = JSON.parse( translation.progress );\n\t\t} catch ( e ) {\n\t\t\tprogress = {};\n\t\t}\n\n\t\t$translation = $( '<div>' )\n\t\t\t.addClass( 'cx-tlitem' )\n\t\t\t.data( 'translation', translation );\n\t\t$lastUpdated = $( '<div>' )\n\t\t\t.addClass( 'last-updated' )\n\t\t\t.text( moment.utc( translation.lastUpdateTimestamp, 'YYYYMMDDHHmmss' ).local().fromNow() );\n\t\t$image = $( '<div>' )\n\t\t\t.addClass( 'cx-tlitem__image oo-ui-icon-article' );\n\t\t$progressbar = $( '<div>' )\n\t\t\t.addClass( 'progressbar' )\n\t\t\t.cxProgressBar( {\n\t\t\t\tweights: progress,\n\t\t\t\tversion: translation.cxVersion\n\t\t\t} );\n\n\t\tsourceDir = $.uls.data.getDir( translation.sourceLanguage );\n\t\ttargetDir = $.uls.data.getDir( translation.targetLanguage );\n\n\t\t$translationLink = $( '<a>' )\n\t\t\t.addClass( 'translation-link' )\n\t\t\t// It must be a separate element to ensure\n\t\t\t// separation from the target title\n\t\t\t.append( $( '<span>' )\n\t\t\t\t.text( translation.sourceTitle )\n\t\t\t\t.addClass( 'source-title' )\n\t\t\t\t.prop( {\n\t\t\t\t\tlang: translation.sourceLanguage,\n\t\t\t\t\tdir: sourceDir\n\t\t\t\t} )\n\t\t\t);\n\n\t\t// If the translated title is different from the source title,\n\t\t// show it near the source title\n\t\tif ( translation.sourceTitle !== translation.targetTitle ) {\n\t\t\t$targetTitle = $( '<span>' )\n\t\t\t\t.prop( {\n\t\t\t\t\tlang: translation.targetLanguage,\n\t\t\t\t\tdir: targetDir\n\t\t\t\t} )\n\t\t\t\t.addClass( 'target-title' )\n\t\t\t\t.text( translation.targetTitle );\n\t\t\t$translationLink.append(\n\t\t\t\t$( '<span>' ).html( '&#160;' ), // nbsp to ensure separation between words\n\t\t\t\t$targetTitle\n\t\t\t);\n\t\t}\n\n\t\t$sourceLanguage = $( '<div>' )\n\t\t\t.prop( {\n\t\t\t\tlang: translation.sourceLanguage,\n\t\t\t\tdir: sourceDir\n\t\t\t} )\n\t\t\t.addClass( 'cx-tlitem__languages__language cx-tlitem__languages__language--source' )\n\t\t\t.text( $.uls.data.getAutonym( translation.sourceLanguage ) );\n\n\t\t$targetLanguage = $( '<div>' )\n\t\t\t.prop( {\n\t\t\t\tlang: translation.targetLanguage,\n\t\t\t\tdir: targetDir\n\t\t\t} )\n\t\t\t.addClass( 'cx-tlitem__languages__language cx-tlitem__languages__language--target' )\n\t\t\t.text( $.uls.data.getAutonym( translation.targetLanguage ) );\n\n\t\t$languageContainer = $( '<div>' )\n\t\t\t.addClass( 'cx-tlitem__languages' )\n\t\t\t.append( $sourceLanguage, $targetLanguage );\n\n\t\t$actions = $( '<div>' )\n\t\t\t.addClass( 'cx-tlitem__actions' );\n\t\t// If the translation is draft, allow deleting it\n\t\tif ( translation.status === 'draft' ) {\n\t\t\tdeleteTranslation = new OO.ui.ButtonWidget( {\n\t\t\t\tframed: false,\n\t\t\t\tclasses: [ 'cx-discard-translation' ],\n\t\t\t\ticon: 'trash',\n\t\t\t\ttitle: mw.msg( 'cx-discard-translation' )\n\t\t\t} );\n\t\t\t$actions.append( deleteTranslation.$element );\n\t\t} else if ( translation.status === 'published' ) {\n\t\t\tcontinueTranslation = new OO.ui.ButtonWidget( {\n\t\t\t\tframed: false,\n\t\t\t\tclasses: [ 'cx-continue-translation' ],\n\t\t\t\ticon: 'edit',\n\t\t\t\ttitle: mw.msg( 'cx-continue-translation' )\n\t\t\t} );\n\t\t\t$actions.append( continueTranslation.$element );\n\t\t}\n\n\t\t$titleLanguageBlock = $( '<div>' )\n\t\t\t.addClass( 'cx-tlitem__details' )\n\t\t\t.append( $translationLink, $progressbar, $lastUpdated, $languageContainer );\n\n\t\t$translation.append(\n\t\t\t$image,\n\t\t\t$titleLanguageBlock,\n\t\t\t$actions\n\t\t);\n\n\t\t$translations.push( $translation );\n\n\t\t// Store reference to the DOM nodes\n\t\ttranslation.$element = $translation;\n\t\ttranslation.$image = $image;\n\t}\n\n\tthis.$listContainer.append( $translations );\n\tthis.showTitleDetails( translations );\n};\n\nmw.cx.CXTranslationList.prototype.buildEmptyTranslationList = function () {\n\tvar $img, $title, $desc;\n\n\tif ( this.$emptyTranslationsList ) {\n\t\treturn this.$emptyTranslationsList;\n\t}\n\t$img = $( '<div>' )\n\t\t.addClass( 'cx-translationlist-empty__img' );\n\t$title = $( '<div>' )\n\t\t.addClass( 'cx-translationlist-empty__title' )\n\t\t.text( mw.msg( 'cx-translationlist-empty-title' ) );\n\t$desc = $( '<div>' )\n\t\t.addClass( 'cx-translationlist-empty__desc' )\n\t\t.text( mw.msg( 'cx-translationlist-empty-desc' ) );\n\treturn $( '<div>' )\n\t\t.addClass( 'cx-translationlist-empty' )\n\t\t.append(\n\t\t\t$img, $title, $desc\n\t\t);\n};\n\nmw.cx.CXTranslationList.prototype.listen = function () {\n\tvar self = this;\n\n\t// Parent method\n\tmw.cx.CXTranslationList.parent.prototype.listen.apply( this, arguments );\n\n\tthis.$listContainer.on( 'click', '.cx-discard-translation', function ( e ) {\n\t\tvar translation;\n\n\t\te.stopPropagation();\n\t\t$( this ).find( 'a' ).trigger( 'blur' );\n\t\ttranslation = $( this ).closest( '.cx-tlitem' ).data( 'translation' );\n\n\t\tOO.ui.getWindowManager().openWindow( 'message', $.extend( {\n\t\t\tmessage: mw.msg( 'cx-draft-discard-confirmation-message' ),\n\t\t\tactions: [\n\t\t\t\t{ action: 'discard', label: mw.msg( 'cx-draft-discard-button-label' ), flags: [ 'primary', 'destructive' ] },\n\t\t\t\t{ action: 'cancel', label: mw.msg( 'cx-draft-cancel-button-label' ), flags: 'safe' }\n\t\t\t]\n\t\t} ) ).closed.then( function ( data ) {\n\t\t\tif ( data && data.action === 'discard' ) {\n\t\t\t\tself.discardTranslation( translation ).done( function ( response ) {\n\t\t\t\t\tif ( response.cxdelete.result !== 'success' ) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\ttranslation.status = 'deleted';\n\t\t\t\t\tself.markTranslationAsDeleted( translation );\n\t\t\t\t\tmw.hook( 'mw.cx.translation.deleted' ).fire(\n\t\t\t\t\t\ttranslation.sourceLanguage,\n\t\t\t\t\t\ttranslation.targetLanguage,\n\t\t\t\t\t\ttranslation.sourceTitle,\n\t\t\t\t\t\ttranslation.targetTitle\n\t\t\t\t\t);\n\t\t\t\t} ).fail( function ( error ) {\n\t\t\t\t\tif ( error === 'assertuserfailed' ) {\n\t\t\t\t\t\tself.constructor.static.showLoginDialog();\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\t\t} );\n\t} );\n\n\tthis.$listContainer.on( 'click', '.cx-continue-translation', function ( e ) {\n\t\tvar translation;\n\n\t\te.stopPropagation();\n\t\t$( this ).find( 'a' ).trigger( 'blur' );\n\t\ttranslation = $( this ).closest( '.cx-tlitem' ).data( 'translation' );\n\t\tself.continueTranslation( translation );\n\t\treturn false;\n\t} );\n\n\tthis.$listContainer.on( 'click', '.cx-tlitem', function () {\n\t\tvar translation = $( this ).data( 'translation' );\n\t\tif ( translation.status === 'published' ) {\n\t\t\tlocation.href = translation.targetURL;\n\t\t} else {\n\t\t\tself.continueTranslation( translation );\n\t\t}\n\t} );\n};\n\nmw.cx.CXTranslationList.prototype.onScroll = function () {\n\tvar scrollTop = window.pageYOffset,\n\t\twindowHeight = document.documentElement.clientHeight;\n\n\t// Load next batch of items on scroll.\n\tif ( scrollTop > 0 && scrollTop + windowHeight + 100 > $( document ).height() ) {\n\t\tthis.loadItems();\n\t}\n};\n\n/**\n * Mark the translation item in the translation list as deleted.\n *\n * @param {Object} translation\n */\nmw.cx.CXTranslationList.prototype.markTranslationAsDeleted = function ( translation ) {\n\ttranslation.$element\n\t\t.addClass( 'cx-translation-deleted' )\n\t\t.find( '.status' )\n\t\t.removeClass( 'status-draft status-published' )\n\t\t.addClass( 'status-deleted' )\n\t\t.text( mw.msg( 'cx-translation-status-deleted' ) )\n\t\t.end()\n\t\t.find( '.cx-tlitem__actions' )\n\t\t.remove()\n\t\t.end()\n\t\t.find( '.translation-link' )\n\t\t.addClass( 'disabled' );\n};\n\n/**\n * Discard a translation.\n *\n * @param {Object} translation\n * @return {jQuery.Promise}\n */\nmw.cx.CXTranslationList.prototype.discardTranslation = function ( translation ) {\n\tvar apiParams = {\n\t\tassert: 'user',\n\t\taction: 'cxdelete',\n\t\tfrom: translation.sourceLanguage,\n\t\tto: translation.targetLanguage,\n\t\tsourcetitle: translation.sourceTitle\n\t};\n\n\treturn new mw.Api().postWithToken( 'csrf', apiParams );\n};\n\nmw.cx.CXTranslationList.prototype.applyFilters = function () {\n\tvar i, translation, visible,\n\t\tsourceLanguage = this.languageFilter.getSourceLanguage(),\n\t\ttargetLanguage = this.languageFilter.getTargetLanguage();\n\n\tfor ( i = 0; i < this.translations.length; i++ ) {\n\t\ttranslation = this.translations[ i ];\n\n\t\tvisible = ( !sourceLanguage || translation.sourceLanguage === sourceLanguage ) &&\n\t\t\t( !targetLanguage || translation.targetLanguage === targetLanguage );\n\n\t\tif ( visible ) {\n\t\t\ttranslation.$element.show();\n\t\t} else {\n\t\t\ttranslation.$element.hide();\n\t\t}\n\t}\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dm/mw.cx.dm.PageTitleModel.js","messages":[{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":9,"column":null,"nodeType":"Block","endLine":9,"endColumn":null},{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":10,"column":null,"nodeType":"Block","endLine":10,"endColumn":null}],"errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @constructor\n * @mixins OO.EventEmitter\n * @mixins ve.dm.CXLintableNode\n */\nmw.cx.dm.PageTitleModel = function MwCxDmPageTitleModel() {\n\t// Mixin constructors\n\tOO.EventEmitter.call( this );\n\tve.dm.CXLintableNode.call( this );\n};\n\n/* Inheritance */\n\n// ve.dm.CXLintableNode expects to be mixed into a node, where OO.EventEmitter is available.\nOO.mixinClass( mw.cx.dm.PageTitleModel, OO.EventEmitter );\nOO.mixinClass( mw.cx.dm.PageTitleModel, ve.dm.CXLintableNode );\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nmw.cx.dm.PageTitleModel.prototype.getId = function () {\n\treturn 'title';\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dm/mw.cx.dm.SectionState.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dm/mw.cx.dm.Translation.js","messages":[{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":7,"column":null,"nodeType":"Block","endLine":7,"endColumn":null}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * CX Translation model\n *\n * @abstract\n * @mixins OO.EventEmitter\n *\n * @constructor\n * @param {mw.cx.dm.WikiPage} sourceWikiPage Details of source wiki page\n * @param {mw.cx.dm.WikiPage} targetWikiPage Details of target wiki page\n * @param {HTMLDocument} sourceDom\n * @param {HTMLDocument} targetDom\n */\nmw.cx.dm.Translation = function MwCxDmTranslation( sourceWikiPage, targetWikiPage, sourceDom, targetDom ) {\n\t// Mixin constructor\n\tOO.EventEmitter.call( this );\n\n\tthis.sourceWikiPage = sourceWikiPage;\n\tthis.targetWikiPage = targetWikiPage;\n\tthis.id = null;\n\tthis.adaptedCategories = null;\n\tthis.sourceCategories = null;\n\tthis.targetCategories = null;\n\tthis.targetTitle = this.targetWikiPage.getTitle();\n\tthis.targetURL = null;\n\tthis.sourceRevisionId = this.sourceWikiPage.getRevision();\n\tthis.targetRevisionId = this.targetWikiPage.getRevision();\n\tthis.status = 'draft';\n\tthis.sectionsChanged = false;\n\tthis.changedSignificantly = false;\n\tthis.progress = {\n\t\tany: 0,\n\t\thuman: 0,\n\t\tmt: 0\n\t};\n\tthis.savedTranslationUnits = null;\n\t// @var {mw.cx.dm.TranslationIssue[]}\n\tthis.translationIssues = [];\n\n\tthis.sourceDoc = ve.dm.converter.getModelFromDom(\n\t\tsourceDom, { lang: this.getSourceLanguage(), dir: this.sourceWikiPage.getDirection() }\n\t);\n\n\tthis.targetDoc = ve.dm.converter.getModelFromDom(\n\t\ttargetDom, { lang: this.getTargetLanguage(), dir: this.targetWikiPage.getDirection() }\n\t);\n\n\tthis.once( 'sectionChange', this.onSectionChange.bind( this ) );\n};\n\n/* Inheritance */\n\nOO.mixinClass( mw.cx.dm.Translation, OO.EventEmitter );\n\n/* Events */\n\n/**\n * @event translationIssues\n *\n * The translation has some issues (errors and/or warnings).\n * @param {string} id Special value of 'global' used as an ID for unattached issues.\n * @param {boolean} [hasErrors] True if any of the unattached issues is an error.\n */\n\n/**\n * @event issuesResolved\n *\n * Some of translation issues have been resolved.\n * @param {string} id Special value of 'global' used as an ID for unattached issues.\n */\n\n/* Static methods */\n\n/**\n * Parse and restructure the source HTML for source and target languages.\n *\n * @param {string} sourceHtml The source HTML\n * @param {boolean} forTarget Whether the DOM to be prepared for target language.\n * @param {Object} [savedTranslationUnits] Saved translation units if any\n * @param {string} sourceLanguage Source language code\n * @return {HTMLDocument} Restructured source DOM\n */\nmw.cx.dm.Translation.static.getSourceDom = function (\n\tsourceHtml, forTarget, savedTranslationUnits, sourceLanguage\n) {\n\tvar childNodes, restoredContent,\n\t\tdomDoc = ve.init.target.parseDocument( sourceHtml, 'visual' ),\n\t\tarticleNode = domDoc.createElement( 'article' ),\n\t\tbaseNodes;\n\n\tif ( forTarget ) {\n\t\t// Remove any and all <base> tags pointing to the source wiki\n\t\tbaseNodes = domDoc.getElementsByTagName( 'base' );\n\t\twhile ( baseNodes.length ) {\n\t\t\tbaseNodes[ 0 ].parentNode.removeChild( baseNodes[ 0 ] );\n\t\t}\n\t\t// Rerun fixBase, which will add a <base> tag pointing to the current wiki\n\t\tve.init.mw.CXTarget.static.fixBase( domDoc );\n\t}\n\n\t// Convert Nodelist to proper array\n\tchildNodes = [].slice.call( domDoc.body.childNodes );\n\tchildNodes.forEach( function ( node ) {\n\t\tvar sectionId, mwSectionNumber, sectionClass, sectionNode, savedSectionNode, savedSection,\n\t\t\tvalidSection = false;\n\n\t\tif ( node.nodeType !== Node.ELEMENT_NODE ) {\n\t\t\treturn;\n\t\t}\n\n\t\tsectionId = node.getAttribute( 'id' );\n\t\tmwSectionNumber = node.dataset.mwSectionNumber;\n\n\t\tvalidSection = node.tagName === 'SECTION' && sectionId &&\n\t\t\tnode.getAttribute( 'rel' ) === 'cx:Section';\n\t\tif ( !validSection ) {\n\t\t\tmw.log.error( '[CX] Node is not under a section: ' + node.tagName +\n\t\t\t\t' after section ' + sectionId + '. Ignoring.' );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( forTarget ) {\n\t\t\tsavedSection = this.getSavedSection( savedTranslationUnits, node, sourceLanguage );\n\n\t\t\tsectionId = sectionId.replace( 'cxSourceSection', 'cxTargetSection' );\n\t\t\tsectionClass = 'mw-target-section-' + mwSectionNumber;\n\t\t\tif ( savedSection ) {\n\t\t\t\t// Saved translated section. Extract content and create a DOM element\n\t\t\t\tsavedSectionNode = domDoc.createElement( 'div' );\n\t\t\t\trestoredContent = this.getLatestTranslation( savedSection );\n\t\t\t\tif ( !restoredContent ) {\n\t\t\t\t\tmw.log.error( '[CX] Blank saved section for ' + sectionId + ' while restoring' );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsavedSectionNode.innerHTML = restoredContent;\n\t\t\t\tsectionNode = savedSectionNode.firstChild;\n\t\t\t\t// Make sure the restored section has matching section id for the source section.\n\t\t\t\tsectionNode.setAttribute( 'id', sectionId );\n\t\t\t} else {\n\t\t\t\t// Prepare a placeholder section\n\t\t\t\tsectionNode = domDoc.createElement( 'section' );\n\t\t\t\tsectionNode.setAttribute( 'id', sectionId );\n\t\t\t\tsectionNode.setAttribute( 'rel', 'cx:Placeholder' );\n\t\t\t}\n\t\t} else {\n\t\t\tsectionClass = 'mw-source-section-' + mwSectionNumber;\n\t\t\tsectionNode = node.cloneNode( true );\n\t\t}\n\n\t\tif ( this.getMode() === 'section' ) {\n\t\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\t\tsectionNode.classList.add( sectionClass );\n\t\t}\n\n\t\t// Remove the original node now.\n\t\tnode.parentNode.removeChild( node );\n\t\tarticleNode.appendChild( sectionNode );\n\t}, this );\n\n\tdomDoc.body.appendChild( articleNode );\n\n\treturn domDoc;\n};\n\n/**\n * Find the latest translation type using the timestamps and return the content\n *\n * @param {Object} translationUnit\n * @return {string|null}\n */\nmw.cx.dm.Translation.static.getLatestTranslation = function ( translationUnit ) {\n\tvar user = translationUnit.user,\n\t\tmt = translationUnit.mt;\n\n\tif ( user && mt ) {\n\t\t// Both user translation and unmodified MT present. Find which one is latest.\n\t\tif ( user.timestamp >= mt.timestamp ) {\n\t\t\treturn user.content;\n\t\t} else {\n\t\t\treturn mt.content;\n\t\t}\n\t} else if ( user ) {\n\t\treturn user.content;\n\t} else if ( mt ) {\n\t\treturn mt.content;\n\t}\n\n\treturn null;\n};\n\nmw.cx.dm.Translation.static.getMode = function () {\n\treturn mw.cx.sectionForTranslation() ? 'section' : 'article';\n};\n\n/**\n * From saved translation units, find a match for the source section, if any.\n * Sometimes, both will have same section numbers, but in case source article\n * changed, we will need to some approximate matching to find a corresponding\n * source section. At the end, we should not have any saved translations that\n * we were not able to restore.\n *\n * @param {Object|undefined} savedTranslationUnits Saved translation units if any\n * @param {Node} sourceSectionNode\n * @param {string} sourceLanguage Source language code\n * @return {Object|undefined} saved translationUnit\n */\nmw.cx.dm.Translation.static.getSavedSection = function (\n\tsavedTranslationUnits, sourceSectionNode, sourceLanguage\n) {\n\tvar savedSection, translationUnitId, savedTranslationUnit,\n\t\tparsoidId, $savedTranslationUnitSource, savedSectionParsoidId;\n\n\tif ( !savedTranslationUnits ) {\n\t\treturn;\n\t}\n\n\t// CX1 translations use parsoid generated Id attribute values in\n\t// section content instead of numerical section numbers\n\tparsoidId = sourceSectionNode.firstChild && sourceSectionNode.firstChild.id;\n\tsavedSection = savedTranslationUnits[ parsoidId ];\n\tif ( sourceSectionNode.tagName !== 'SECTION' && savedSection && !savedSection.restored ) {\n\t\tsavedTranslationUnits[ parsoidId ].restored = true;\n\t\treturn savedSection;\n\t}\n\n\t// Even if source section number changed, try locating matching id in content\n\t// For CX2, translationUnitId is section number\n\tfor ( translationUnitId in savedTranslationUnits ) {\n\t\tsavedTranslationUnit = savedTranslationUnits[ translationUnitId ];\n\t\tif ( savedTranslationUnit.restored ) {\n\t\t\t// Already restored section.\n\t\t\tcontinue;\n\t\t}\n\t\tif ( !savedTranslationUnit.source ) {\n\t\t\tmw.log.error( '[CX] Section saved without source? ' + translationUnitId );\n\t\t\tcontinue;\n\t\t}\n\t\t$savedTranslationUnitSource = $( savedTranslationUnit.source.content );\n\t\tif ( !$savedTranslationUnitSource.is( 'section' ) ) {\n\t\t\t// CX1 saved translation\n\t\t\tsavedSectionParsoidId = $savedTranslationUnitSource.attr( 'id' );\n\n\t\t\tif ( parsoidId === savedSectionParsoidId ) {\n\t\t\t\tsavedTranslationUnit.restored = true;\n\t\t\t\treturn savedTranslationUnit;\n\t\t\t}\n\t\t}\n\n\t\t// The parsoid ids did not match. We can try the section numbers now.\n\t\t// But section numbers are sequential numbers given by CX.\n\t\t// Unconditionally using that will cause wrongly restored sections.\n\t\t// For example, a translation A1:a1,B2:b2:C3:c3, if the source changed\n\t\t// to A1,C2,B3 will get restored as A1:a1,C2:b2:B3:c3.\n\t\t// (A,a,B..are section ids, 1,2,3.. are section numbers in above notation.)\n\t\t// So, we should be extra cautious before using section numbers for restoring.\n\t\tif ( this.isMatchingForRestore(\n\t\t\tsavedTranslationUnit.source.content, sourceSectionNode, sourceLanguage )\n\t\t) {\n\t\t\tsavedTranslationUnit.restored = true;\n\t\t\treturn savedTranslationUnit;\n\t\t}\n\t}\n};\n\n/**\n * A saved translation unit is a candidate for restoring against a source section, iff\n * the saved source and current source share a common tokens ratio greater than a threshold.\n * Check if that is the case.\n *\n * @param {string} savedSourceContent\n * @param {Element|string} currSourceNode\n * @param {string} language Source language code, required for tokenization\n * @return {boolean}\n */\nmw.cx.dm.Translation.static.isMatchingForRestore = function (\n\tsavedSourceContent, currSourceNode, language\n) {\n\tvar commonTokenRatio,\n\t\t$savedTranslationUnitSource = $( savedSourceContent ),\n\t\t$sourceSectionNode = $( currSourceNode );\n\n\tif ( $savedTranslationUnitSource.is( 'section' ) ) {\n\t\tif ( $savedTranslationUnitSource.children().eq( 0 ).prop( 'tagName' ) !==\n\t\t\t$sourceSectionNode.children().eq( 0 ).prop( 'tagName' )\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\t} else if ( $savedTranslationUnitSource.prop( 'tagName' ) !==\n\t\t$sourceSectionNode.prop( 'tagName' )\n\t) {\n\t\treturn false;\n\t}\n\n\t// If old and new source content has some edits, causing some words change,\n\t// find out the common token ratio. The definition of token depends on the language\n\t// but mostly it means words.\n\tcommonTokenRatio = mw.cx.TranslationTracker.static.calculateUnmodifiedContent(\n\t\t$savedTranslationUnitSource.text(),\n\t\t$sourceSectionNode.text(),\n\t\tlanguage\n\t);\n\n\tif ( commonTokenRatio > 0.5 ) {\n\t\treturn true;\n\t}\n\n\t// It is possible that the new or old source section has lot of new content added compared to other.\n\t// For example, a section had 1 sentence and now it has 4 sentences. In such case,\n\t// find if the new section has with old source section content.\n\treturn this.hasIncludedContent( $savedTranslationUnitSource.text(), $sourceSectionNode.text() );\n};\n\n/**\n * Check if one of the strings has the other string included in it.\n * Do this only if one string is more than double the size of other. If it is less size\n * than that, the commonRatio approach above should have detected the match.\n * The comparison is case insensitive and ignores punctuations.\n *\n * @param {string} string1\n * @param {string} string2\n * @return {boolean}\n */\nmw.cx.dm.Translation.static.hasIncludedContent = function ( string1, string2 ) {\n\tvar bigString = string1.trim(),\n\t\tsmallString = string2.trim();\n\n\tif ( bigString.length < smallString.length ) {\n\t\t// Swap the sets\n\t\tbigString = string2.trim();\n\t\tsmallString = string1.trim();\n\t}\n\n\t// If smaller string is empty, we should not count as content is included. See T222905\n\tif ( !smallString ) {\n\t\treturn false;\n\t}\n\n\tif ( bigString.length >= smallString.length * 2 ) {\n\t\treturn bigString.toLowerCase().replace( /[^\\w\\s]/g, '' )\n\t\t\t.indexOf( smallString.toLowerCase().replace( /[^\\w\\s]/g, '' ) ) >= 0;\n\t}\n\n\treturn false;\n};\n\n/**\n * Get HTML content of a translation unit to restore.\n *\n * @param {Object} translationUnit\n * @return {Element} Document element corresponding to the saved HTML of the section.\n */\nmw.cx.dm.Translation.static.getSavedTranslation = function ( translationUnit ) {\n\tvar translation;\n\n\t// If the translator has manual translation from scratch or on top of MT use that.\n\tif ( translationUnit.user && translationUnit.user.content ) {\n\t\ttranslation = translationUnit.user.content;\n\t} else if ( translationUnit.mt ) { // Machine translation, unmodified.\n\t\ttranslation = translationUnit.mt.content;\n\t} else if ( translationUnit.source ) { // Unmodified source copy.\n\t\ttranslation = translationUnit.source.content;\n\t}\n\n\treturn $.parseHTML( translation )[ 0 ];\n};\n\n/* Methods */\n\nmw.cx.dm.Translation.prototype.getTargetPage = function () {\n\treturn this.targetPage;\n};\n\n/**\n * @return {Array} Source categories\n */\nmw.cx.dm.Translation.prototype.getSourceCategories = function () {\n\treturn this.sourceCategories;\n};\n\n/**\n * @param {Array} categories Target categories\n */\nmw.cx.dm.Translation.prototype.setTargetCategories = function ( categories ) {\n\tthis.targetCategories = categories;\n\tthis.emit( 'targetCategoriesChange' );\n};\n\n/**\n * @return {Array} Target categories\n */\nmw.cx.dm.Translation.prototype.getTargetCategories = function () {\n\treturn this.targetCategories;\n};\n\nmw.cx.dm.Translation.prototype.isChangedSignificantly = function () {\n\treturn this.changedSignificantly;\n};\n\nmw.cx.dm.Translation.prototype.setChangedSignificantly = function ( isChangedSignificantly ) {\n\tthis.changedSignificantly = isChangedSignificantly;\n};\n\n/**\n * @param {Object} adaptedCategories\n */\nmw.cx.dm.Translation.prototype.initCategories = function ( adaptedCategories ) {\n\tthis.adaptedCategories = adaptedCategories;\n\n\tthis.sourceCategories = Object.keys( adaptedCategories );\n\tthis.targetCategories = this.targetCategories || this.extractTargetCategories();\n};\n\n/**\n * @return {Array} Target categories\n */\nmw.cx.dm.Translation.prototype.extractTargetCategories = function () {\n\tvar source, target, categories = [];\n\n\tfor ( source in this.adaptedCategories ) {\n\t\ttarget = this.adaptedCategories[ source ];\n\t\tif ( target ) {\n\t\t\tcategories.push( target );\n\t\t}\n\t}\n\n\treturn categories;\n};\n\n/**\n * For a given source category, find corresponding target category.\n *\n * @param {string} sourceCategory Source category name\n * @return {string|null} Corresponding target category name, or null\n */\nmw.cx.dm.Translation.prototype.getCorrespondingTargetCategory = function ( sourceCategory ) {\n\treturn this.adaptedCategories[ sourceCategory ] || null;\n};\n\n/**\n * For a given target (adapted) category, find corresponding source category.\n *\n * @param {string} targetCategory Target category name\n * @return {string|null} Corresponding source category name, or null\n */\nmw.cx.dm.Translation.prototype.getCorrespondingSourceCategory = function ( targetCategory ) {\n\tvar i, length, category;\n\n\tfor ( i = 0, length = this.sourceCategories.length; i < length; i++ ) {\n\t\tcategory = this.sourceCategories[ i ];\n\n\t\tif ( this.adaptedCategories[ category ] === targetCategory ) {\n\t\t\treturn category;\n\t\t}\n\t}\n\n\treturn null;\n};\n\n/**\n * Get adapted categories, which aren't removed from target categories array.\n *\n * @return {Array} Removed categories\n */\nmw.cx.dm.Translation.prototype.getRemovedCategories = function () {\n\tvar allTargetCategories = this.extractTargetCategories();\n\n\treturn OO.simpleArrayDifference( allTargetCategories, this.getTargetCategories() );\n};\n\n/**\n * Get Translation id\n *\n * @return {string} Translation Id\n */\nmw.cx.dm.Translation.prototype.getId = function () {\n\treturn this.id;\n};\n\n/**\n * Set Translation id\n *\n * @param {string} id Translation Id\n */\nmw.cx.dm.Translation.prototype.setId = function ( id ) {\n\tthis.id = id;\n};\n\nmw.cx.dm.Translation.prototype.setTargetURL = function ( targetURL ) {\n\tthis.targetURL = targetURL;\n};\n\n/**\n * Get revision id\n *\n * @return {string} revision Id\n */\nmw.cx.dm.Translation.prototype.getSourceRevisionId = function () {\n\treturn this.sourceRevisionId;\n};\n\n/**\n * Set revision id\n *\n * @param {string} revisionId revision Id\n */\nmw.cx.dm.Translation.prototype.setSourceRevisionId = function ( revisionId ) {\n\tthis.sourceRevisionId = revisionId;\n};\n\n/**\n * Set target revision id\n *\n * @param {string} revisionId revision Id\n */\nmw.cx.dm.Translation.prototype.setTargetRevisionId = function ( revisionId ) {\n\tthis.targetRevisionId = revisionId;\n};\n\n/**\n * Get source title for translation\n *\n * @return {string} Source title\n */\nmw.cx.dm.Translation.prototype.getSourceTitle = function () {\n\treturn this.sourceWikiPage.getTitle();\n};\n\n/**\n * Set Translation title\n *\n * @param {string} title Translation Id\n */\nmw.cx.dm.Translation.prototype.setTargetTitle = function ( title ) {\n\tthis.targetTitle = title;\n};\n\n/**\n * Get target title for translation\n *\n * @return {string} Target title\n */\nmw.cx.dm.Translation.prototype.getTargetTitle = function () {\n\treturn this.targetTitle;\n};\n\n/**\n * Get source language for translation\n *\n * @return {string} Source language\n */\nmw.cx.dm.Translation.prototype.getSourceLanguage = function () {\n\treturn this.sourceWikiPage.getLanguage();\n};\n\n/**\n * Get target language for translation\n *\n * @return {string} Target language\n */\nmw.cx.dm.Translation.prototype.getTargetLanguage = function () {\n\treturn this.targetWikiPage.getLanguage();\n};\n\nmw.cx.dm.Translation.prototype.hasBeenPublished = function () {\n\treturn this.status === 'published' || this.targetURL !== null;\n};\n\nmw.cx.dm.Translation.prototype.setStatus = function ( status ) {\n\tthis.status = status;\n};\n\nmw.cx.dm.Translation.prototype.getProgress = function () {\n\treturn this.progress;\n};\n\nmw.cx.dm.Translation.prototype.setProgress = function ( progress ) {\n\tthis.progress = progress;\n};\n\n/**\n * Extract translation metadata from the draft translation fetched\n * and set to this model.\n *\n * @param {Object} draft Saved translation.\n */\nmw.cx.dm.Translation.prototype.setSavedTranslation = function ( draft ) {\n\tthis.setTargetURL( draft.targetURL );\n\tthis.setStatus( draft.status );\n\tthis.setTargetRevisionId( draft.targetRevisionId );\n\tthis.setProgress( JSON.parse( draft.progress ) );\n\tthis.setId( draft.id );\n\tthis.setTargetTitle( draft.targetTitle );\n\tthis.savedTranslationUnits = draft.translationUnits;\n\t// Only target categories are retrieved when translation draft is restored\n\t// Source categories aren't retrieved, only saved in cx_corpora for pairing\n\t// with target categories.\n\tthis.targetCategories = JSON.parse( draft.targetCategories );\n\n\t// Handle badly stored categories caused by T248302\n\tif ( Array.isArray( this.targetCategories ) ) {\n\t\tthis.targetCategories = this.targetCategories.map( function ( item ) {\n\t\t\treturn ( item.indexOf( ':' ) > 0 ) ? item : 'Category:' + item;\n\t\t} );\n\t}\n};\n\n/**\n * Check if there are translated sections, which can be published.\n *\n * @return {boolean}\n */\nmw.cx.dm.Translation.prototype.hasTranslatedSections = function () {\n\treturn this.sectionsChanged ||\n\t\t(\n\t\t\t// this.savedTranslationUnits is not null and not an empty array\n\t\t\tthis.savedTranslationUnits !== null &&\n\t\t\t!( Array.isArray( this.savedTranslationUnits ) && this.savedTranslationUnits.length === 0 )\n\t\t);\n};\n\nmw.cx.dm.Translation.prototype.onSectionChange = function () {\n\tthis.sectionsChanged = true;\n};\n\n/**\n * Add issues global for the whole translation, not attached to any DOM node.\n *\n * @param {mw.cx.dm.TranslationIssue[]} issues\n * @fires translationIssues\n */\nmw.cx.dm.Translation.prototype.addUnattachedIssues = function ( issues ) {\n\tthis.translationIssues = this.translationIssues.concat( issues );\n\n\t// Allow to suppress all resolvable issues\n\tissues.forEach( function ( issue ) {\n\t\t// When issue is suppressed, emit event about the resolved state\n\t\tissue.setSuppressCallback( this.resolveNotify.bind( this ) );\n\t}, this );\n\n\tthis.emit( 'translationIssues', 'global', this.hasErrors() );\n};\n\n/**\n * Called when one unattached issue is resolved. Events about the issue state are emitted.\n *\n * @fires translationIssues\n * @fires issuesResolved\n */\nmw.cx.dm.Translation.prototype.resolveNotify = function () {\n\tif ( this.getTranslationIssues().length === 0 ) {\n\t\tthis.emit( 'issuesResolved', 'global' );\n\t} else {\n\t\tthis.emit( 'translationIssues', 'global', this.hasErrors() );\n\t}\n};\n\n/**\n * @param {string} name\n */\nmw.cx.dm.Translation.prototype.resolveIssueByName = function ( name ) {\n\tvar index = this.findIssueIndex( name );\n\n\tif ( index > -1 ) {\n\t\tthis.translationIssues.splice( index, 1 );\n\t\tthis.resolveNotify();\n\t}\n};\n\n/**\n * Find the index of issue with some name, inside issue array.\n * Names act as unique ID and there should not be duplicates.\n *\n * @param {string} name Name of the issue\n * @return {number} Index of issue or -1 if not found.\n */\nmw.cx.dm.Translation.prototype.findIssueIndex = function ( name ) {\n\tvar i, length;\n\n\tfor ( i = 0, length = this.translationIssues.length; i < length; i++ ) {\n\t\tif ( this.translationIssues[ i ].getName() === name ) {\n\t\t\treturn i;\n\t\t}\n\t}\n\n\treturn -1;\n};\n\n/**\n * True if there is at least one unattached issue that is an error. Number of warnings is irrelevant.\n *\n * @return {boolean}\n */\nmw.cx.dm.Translation.prototype.hasErrors = function () {\n\treturn this.getTranslationIssues().some( function ( issue ) {\n\t\treturn issue.type === 'error';\n\t} );\n};\n\n/**\n * @return {mw.cx.dm.TranslationIssue[]}\n */\nmw.cx.dm.Translation.prototype.getTranslationIssues = function () {\n\treturn this.translationIssues.filter( function ( issue ) {\n\t\treturn !issue.isSuppressed();\n\t} );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dm/mw.cx.dm.TranslationIssue.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dm/mw.cx.dm.WikiPage.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dm/mw.cx.dm.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/editor/ext.cx.editor.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.betafeature.init.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.contributions.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.contributionsmenu.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.newarticle.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.newarticle.veloader.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.newbytranslation.js","messages":[{"ruleId":"compat/compat","severity":1,"message":"navigator.languages() is not supported in Safari 5.1","line":177,"column":35,"nodeType":"MemberExpression","endLine":177,"endColumn":54}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * Content Translation invitation for editors while trying to create a new article.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\tvar CAMPAIGN = 'newarticle';\n\n\t/**\n\t * @class\n\t * @param {Object} config\n\t */\n\tfunction CXNewByTranslationInvitation( config ) {\n\t\tthis.siteMapper = config.siteMapper;\n\t\tthis.targetTitle = config.targetTitle;\n\t\tthis.targetLanguage = this.siteMapper.getCurrentWikiLanguageCode();\n\t\tthis.suggestion = config.suggestion;\n\t\tthis.invitation = this.render();\n\t}\n\n\tCXNewByTranslationInvitation.prototype.render = function () {\n\t\tvar content = this.getContent();\n\t\treturn new OO.ui.PopupWidget( {\n\t\t\t$content: content.$element,\n\t\t\tclasses: [ 'cx-entrypoint-newbytranslation' ],\n\t\t\tpadded: true,\n\t\t\tanchor: false,\n\t\t\thead: true,\n\t\t\twidth: 600,\n\t\t\theight: 'auto',\n\t\t\tautoClose: false,\n\t\t\thideWhenOutOfView: false\n\t\t} );\n\t};\n\n\tCXNewByTranslationInvitation.prototype.getCXLink = function ( options ) {\n\t\treturn mw.util.getUrl( 'Special:ContentTranslation', options );\n\t};\n\n\tCXNewByTranslationInvitation.prototype.getContent = function () {\n\t\tvar $sourceSuggestionButton, $suggestionImage, $suggestionDetails, container,\n\t\t\tsearchButton, settingsButton,\n\t\t\tstartCXButton, actions;\n\n\t\tcontainer = new OO.ui.StackLayout( {\n\t\t\tcontinuous: true,\n\t\t\texpanded: false\n\t\t} );\n\n\t\tsettingsButton = new OO.ui.ButtonWidget( {\n\t\t\tclasses: [ 'cx-campaign-newbytranslation-settings' ],\n\t\t\ticon: 'settings',\n\t\t\tframed: false,\n\t\t\thref: mw.util.getUrl( 'Special:Preferences#mw-prefsection-rendering-languages' ),\n\t\t\ttarget: '_blank'\n\t\t} );\n\n\t\tif ( this.suggestion ) {\n\t\t\t$suggestionImage = $( '<div>' ).addClass( 'suggestion-image oo-ui-icon-article' );\n\t\t\t$suggestionDetails = $( '<div>' ).addClass( 'suggestion-details' );\n\n\t\t\t$suggestionDetails.append(\n\t\t\t\t$( '<div>' ).addClass( 'suggestion-title' ).text( this.suggestion.title ),\n\t\t\t\t$( '<div>' ).addClass( 'suggestion-desc' ).text( this.suggestion.description ),\n\t\t\t\t$( '<div>' ).addClass( 'suggestion-langs' ).text(\n\t\t\t\t\tmw.msg( 'cx-campaign-newbytranslation-languages',\n\t\t\t\t\t\t$.uls.data.getAutonym( this.suggestion.language ),\n\t\t\t\t\t\t$.uls.data.getAutonym( this.targetLanguage )\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\n\t\t\t$sourceSuggestionButton = $( '<a>' )\n\t\t\t\t.addClass( 'cx-campaign-newbytranslation-source' )\n\t\t\t\t.attr( 'href', this.getCXLink( {\n\t\t\t\t\tcampaign: CAMPAIGN,\n\t\t\t\t\ttargettitle: this.targetTitle,\n\t\t\t\t\tpage: this.suggestion.title,\n\t\t\t\t\tfrom: this.suggestion.language,\n\t\t\t\t\tto: this.targetLanguage\n\t\t\t\t} ) )\n\t\t\t\t.append( $suggestionImage, $suggestionDetails );\n\t\t\tif ( this.suggestion.thumbnail ) {\n\t\t\t\t$suggestionImage\n\t\t\t\t\t.addClass( 'suggestion-image--with-thumbnail' )\n\t\t\t\t\t.removeClass( 'oo-ui-icon-article' )\n\t\t\t\t\t.css( 'background-image', 'url(\"' + this.suggestion.thumbnail.source + '\")' );\n\t\t\t}\n\n\t\t\tsearchButton = new OO.ui.ButtonWidget( {\n\t\t\t\tclasses: [ 'cx-campaign-newbytranslation-search-source' ],\n\t\t\t\ticon: 'search',\n\t\t\t\tflags: [ 'progressive' ],\n\t\t\t\tlabel: mw.msg( 'cx-campaign-newbytranslation-search' ),\n\t\t\t\tframed: false,\n\t\t\t\thref: this.getCXLink( {\n\t\t\t\t\tcampaign: CAMPAIGN,\n\t\t\t\t\ttargettitle: this.targetTitle,\n\t\t\t\t\tto: this.targetLanguage\n\t\t\t\t} )\n\t\t\t} );\n\t\t\tactions = [\n\t\t\t\t$sourceSuggestionButton,\n\t\t\t\tsearchButton,\n\t\t\t\tsettingsButton\n\t\t\t];\n\t\t} else {\n\t\t\t// Generic dialog\n\t\t\tstartCXButton = new OO.ui.ButtonWidget( {\n\t\t\t\tlabel: mw.msg( 'cx-campaign-newbytranslation-start' ),\n\t\t\t\tflags: [ 'primary', 'progressive' ],\n\t\t\t\thref: this.getCXLink( {\n\t\t\t\t\tcampaign: CAMPAIGN,\n\t\t\t\t\ttargettitle: this.targetTitle,\n\t\t\t\t\tto: this.targetLanguage\n\t\t\t\t} )\n\t\t\t} );\n\t\t\tactions = [\n\t\t\t\tstartCXButton,\n\t\t\t\tsettingsButton\n\t\t\t];\n\t\t}\n\n\t\tcontainer.addItems( [\n\t\t\tnew OO.ui.HorizontalLayout( {\n\t\t\t\titems: [\n\t\t\t\t\tnew OO.ui.IconWidget( {\n\t\t\t\t\t\ticon: 'language',\n\t\t\t\t\t\tflags: [ 'progressive' ]\n\t\t\t\t\t} ),\n\t\t\t\t\tnew OO.ui.LabelWidget( {\n\t\t\t\t\t\tlabel: mw.msg( 'cx-campaign-newbytranslation-title' ),\n\t\t\t\t\t\tclasses: [ 'cx-campaign-newbytranslation-title' ]\n\t\t\t\t\t} )\n\t\t\t\t]\n\t\t\t} ),\n\t\t\tnew OO.ui.LabelWidget( {\n\t\t\t\tclasses: [ 'cx-campaign-newbytranslation-label' ],\n\t\t\t\tlabel: mw.msg( 'cx-campaign-newbytranslation-notice' ),\n\t\t\t\talign: 'left'\n\t\t\t} ),\n\t\t\tnew OO.ui.HorizontalLayout( {\n\t\t\t\tclasses: [ 'cx-campaign-newbytranslation-actions' ],\n\t\t\t\tcontent: actions\n\t\t\t} )\n\t\t] );\n\n\t\treturn container;\n\t};\n\n\tCXNewByTranslationInvitation.prototype.listen = function () {\n\t\tthis.invitation.on( 'toggle', this.onToggle.bind( this ) );\n\t};\n\n\tCXNewByTranslationInvitation.prototype.onToggle = function ( visible ) {\n\t\tif ( visible ) {\n\t\t\tmw.hook( 'mw.cx.cta.shown' ).fire( CAMPAIGN );\n\t\t} else {\n\t\t\t// Campaign or call to action was rejected by the user.\n\t\t\tmw.hook( 'mw.cx.cta.reject' ).fire( CAMPAIGN );\n\t\t}\n\t};\n\n\tCXNewByTranslationInvitation.prototype.show = function () {\n\t\t$( document.body ).append( this.invitation.$element );\n\t\tsetTimeout( function () {\n\t\t\t// Wait till everything painted on screen so that we get correct dimensions\n\t\t\tthis.invitation.toggle( true );\n\t\t}.bind( this ), 200 );\n\t\tthis.listen();\n\t};\n\n\tfunction getCandidateSourceLanguages( targetLanguage ) {\n\t\tvar candidates = [ navigator.language ];\n\t\tcandidates = candidates.concat( navigator.languages );\n\t\tif ( mw.uls ) {\n\t\t\tcandidates = candidates.concat( mw.uls.getPreviousLanguages() );\n\t\t}\n\t\tcandidates = candidates\n\t\t\t.map( function ( lang ) {\n\t\t\t\tif ( lang ) {\n\t\t\t\t\t// Remove country codes\n\t\t\t\t\treturn lang.split( '-' )[ 0 ];\n\t\t\t\t}\n\t\t\t\treturn null;\n\t\t\t} )\n\t\t\t.filter( function ( lang, index, self ) {\n\t\t\t\treturn lang && lang !== targetLanguage && // Remove target language\n\t\t\t\t\tself.indexOf( lang ) === index; // Remove duplicates\n\t\t\t} );\n\t\treturn candidates.splice( 0, 5 );\n\t}\n\n\tfunction getSourceSuggestions( siteMapper, targetTitle ) {\n\t\tvar targetLanguage, sourceSuggestionApi, candidateSourceLanguages;\n\t\ttargetLanguage = siteMapper.getCurrentWikiLanguageCode();\n\t\tcandidateSourceLanguages = getCandidateSourceLanguages( targetLanguage );\n\t\tsourceSuggestionApi = siteMapper.getCXServerUrl(\n\t\t\t'/suggest/source/$title/$to?sourcelanguages=$from',\n\t\t\t{\n\t\t\t\t$title: targetTitle,\n\t\t\t\t$to: targetLanguage,\n\t\t\t\t$from: candidateSourceLanguages.join( ',' )\n\t\t\t} );\n\n\t\treturn $.get( sourceSuggestionApi ).then( function ( response ) {\n\t\t\treturn response.suggestions || [];\n\t\t} );\n\t}\n\n\t$( function () {\n\t\tvar siteMapper = new mw.cx.SiteMapper(),\n\t\t\ttargetTitle = mw.config.get( 'wgTitle' );\n\n\t\tgetSourceSuggestions( siteMapper, targetTitle ).then( function ( suggestions ) {\n\t\t\tvar api, invitation, shownOnce;\n\n\t\t\tshownOnce = mw.config.get( 'wgContentTranslationNewByTranslationShown' ) === 'true';\n\n\t\t\tif ( !suggestions.length &&\n\t\t\t\t(\n\t\t\t\t\tmw.config.get( 'wgContentTranslationExistingTranslator' ) ||\n\t\t\t\t\tshownOnce\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\t// No suggestion. User is existing translator.\n\t\t\t\t// or the invitation was shown once. Nothing to do.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tinvitation = new CXNewByTranslationInvitation( {\n\t\t\t\tsiteMapper: siteMapper,\n\t\t\t\ttargetTitle: targetTitle,\n\t\t\t\tsuggestion: suggestions.length ? suggestions[ 0 ] : null\n\t\t\t} );\n\t\t\tinvitation.show();\n\n\t\t\tif ( !shownOnce ) {\n\t\t\t\tapi = new mw.Api();\n\t\t\t\t// Mark that the user saw invitation once\n\t\t\t\tapi.postWithToken( 'csrf', {\n\t\t\t\t\taction: 'globalpreferences',\n\t\t\t\t\toptionname: 'cx_campaign_newarticle_shown',\n\t\t\t\t\toptionvalue: 'true'\n\t\t\t\t} ).then( function ( res ) {\n\t\t\t\t\t// Should we care?\n\t\t\t\t\tif ( res.error ) {\n\t\t\t\t\t\tmw.log.error( res.error );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\t\t} );\n\t} );\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.interlanguagelink.init.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.interlanguagelink.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/eventlogging/ext.cx.eventlogging.campaigns.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/eventlogging/ext.cx.eventlogging.dashboard.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/eventlogging/ext.cx.eventlogging.translation.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/eventlogging/legacy/ext.cx.eventlogging.translation.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.MachineTranslationManager.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.MachineTranslationService.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.MwApiRequestManager.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.TargetArticle.js","messages":[{"ruleId":"no-shadow","severity":1,"message":"'title' is already declared in the upper scope on line 449 column 66.","line":458,"column":7,"nodeType":"Identifier","messageId":"noShadow","endLine":458,"endColumn":12}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Target Article for CX - Validation, Publishing, Success and Error handling.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @param {mw.cx.dm.Translation} translation\n * @param {ve.init.mw.CXTarget} veTarget\n * @param {Object} config Translation configuration\n */\nmw.cx.TargetArticle = function MWCXTargetArticle( translation, veTarget, config ) {\n\tthis.translation = translation;\n\tthis.veTarget = veTarget;\n\tthis.config = config;\n\tthis.siteMapper = config.siteMapper;\n\tthis.sourceTitle = config.sourceTitle;\n\tthis.sourceLanguage = config.sourceLanguage;\n\tthis.targetLanguage = config.targetLanguage;\n\n\t// Mixin constructors\n\tOO.EventEmitter.call( this );\n\n\t// Properties\n\tthis.captcha = null;\n\tthis.captchaDialog = null;\n};\n\n/* Inheritance */\n\nOO.mixinClass( mw.cx.TargetArticle, OO.EventEmitter );\n\n/* Events */\n\n/**\n * @event publishCancel\n *\n * User canceled the publishing process.\n */\n\n/**\n * @event publishSuccess\n *\n * Translation is successfully published.\n */\n\n/**\n * @event captchaCancel\n *\n * User exited the captcha dialog.\n */\n\n/**\n * @event publishError\n *\n * Error occured during publishing.\n * @param {OO.ui.Error} error\n */\n\n/* Static Methods */\n\n/**\n * Clean up the input document by removing CX specific markup and attributes.\n *\n * @param {HTMLDocument} doc\n * @return {HTMLDocument} Cleaned up document.\n **/\nmw.cx.TargetArticle.static.getCleanedupContent = function ( doc ) {\n\tArray.prototype.forEach.call( doc.body.querySelectorAll( 'article, section, [data-segmentid]' ), function ( segment ) {\n\t\tvar parent = segment.parentNode;\n\t\t// move all children out of the element\n\t\twhile ( segment.firstChild ) {\n\t\t\tparent.insertBefore( segment.firstChild, segment );\n\t\t}\n\t\tsegment.remove();\n\t} );\n\n\t// Remove all unadapted links except the ones that are explicitly marked as missing.\n\t// Refer ve.ui.CXLinkContextItem#createRedLink\n\tArray.prototype.forEach.call( doc.querySelectorAll( '.cx-link' ), function ( link ) {\n\t\tvar dataCX = JSON.parse( link.getAttribute( 'data-cx' ) || '{}' );\n\t\tif ( dataCX.adapted === false && OO.getProp( dataCX, 'targetTitle', 'missing' ) !== true ) {\n\t\t\t// Replace the link with its inner content.\n\t\t\tlink.replaceWith( link.innerHTML );\n\t\t} else {\n\t\t\t[ 'data-linkid', 'class', 'title', 'id' ].forEach( function ( attr ) {\n\t\t\t\tlink.removeAttribute( attr );\n\t\t\t} );\n\t\t}\n\t} );\n\n\t// Remove empty references. Such references are initially marked as unadapted and CX data\n\t// is reset upon editing, so we check if reference is still marked as unadapted.\n\tArray.prototype.forEach.call( doc.querySelectorAll( '.mw-ref' ), function ( element ) {\n\t\tvar dataCX = JSON.parse( element.getAttribute( 'data-cx' ) || '{}' );\n\n\t\tif ( dataCX.adapted === false ) {\n\t\t\telement.parentNode.removeChild( element );\n\t\t}\n\t} );\n\n\t// Remove all data-cx attributes. It is irrelevant for publish, reduces the HTML size.\n\tArray.prototype.forEach.call( doc.querySelectorAll( '[data-cx]' ), function ( element ) {\n\t\telement.removeAttribute( 'data-cx' );\n\t} );\n\n\t// Remove all id attributes from table cells, div tags that are assigned by cxserver.\n\tArray.prototype.forEach.call(\n\t\tdoc.querySelectorAll( 'tr[id], td[id], th[id], table[id], tbody[id], thead[id], div[id]' ), function ( element ) {\n\t\t\telement.removeAttribute( 'id' );\n\t\t}\n\t);\n\n\treturn doc;\n};\n\n/* Methods */\n\n/**\n * Publish the translated content to target wiki.\n *\n * @param {boolean} hasIssues True if translation being published has some issues.\n * @param {boolean} shouldAddHighMTCategory Whether article being published\n * should be added to high MT tracking category.\n */\nmw.cx.TargetArticle.prototype.publish = function ( hasIssues, shouldAddHighMTCategory ) {\n\tvar apiParams = {\n\t\tassert: 'user',\n\t\taction: 'cxpublish',\n\t\tfrom: this.sourceLanguage,\n\t\tto: this.targetLanguage,\n\t\tsourcetitle: this.sourceTitle,\n\t\ttitle: this.getTargetTitle(),\n\t\thtml: this.getContent( true ),\n\t\tcategories: this.getTargetCategories( shouldAddHighMTCategory ),\n\t\tpublishtags: this.getTags(),\n\t\twpCaptchaId: this.captcha && this.captcha.id,\n\t\twpCaptchaWord: this.captcha && this.captcha.input.getValue(),\n\t\tcxversion: 2\n\t};\n\n\t// Check for title conflicts\n\tthis.checkForPublishAnyway( this.getTargetTitle(), hasIssues ).then( function () {\n\t\treturn new mw.Api().postWithToken( 'csrf', apiParams, {\n\t\t\t// A bigger timeout since publishing after converting html to wikitext\n\t\t\t// parsoid is not a fast operation.\n\t\t\ttimeout: 100 * 1000 // in milliseconds\n\t\t} ).then( this.publishSuccess.bind( this ), this.publishFail.bind( this ) );\n\t}.bind( this ), function () {\n\t\tthis.emit( 'publishCancel' );\n\t}.bind( this ) );\n};\n\n/**\n * Publish success handler\n *\n * @param {Object} response Response object from the publishing api\n * @param {Object} jqXHR\n * @return {null|jQuery.Promise}\n */\nmw.cx.TargetArticle.prototype.publishSuccess = function ( response, jqXHR ) {\n\tvar publishResult = response.cxpublish;\n\tif ( publishResult.result === 'success' ) {\n\t\treturn this.publishComplete();\n\t}\n\n\tif ( publishResult.edit.captcha ) {\n\t\t// If there is a captcha challenge, get the solution and retry.\n\t\treturn this.loadCaptchaDialog().then(\n\t\t\tthis.showErrorCaptcha.bind( this, publishResult.edit.captcha )\n\t\t);\n\t}\n\n\t// Any other failure\n\treturn this.publishFail( '', publishResult, publishResult, jqXHR );\n};\n\n/**\n * @fires publishSuccess\n */\nmw.cx.TargetArticle.prototype.publishComplete = function () {\n\tthis.captcha = null;\n\tthis.emit( 'publishSuccess' );\n};\n\n/**\n * Publish failure handler\n *\n * The 'messageOrFailObjOrData' parameter could be a string explaining the error,\n * or an object with textStatus, exception and jqXHR keys (but jqXHR can be missing),\n * or equal to data. If data is present, jqXHR is also present. See T176704.\n *\n * @param {string} errorCode\n * @param {string|Object} messageOrFailObjOrData Error message (string), or object with textStatus,\n *   exception and (optionally) jqXHR, or equal to data\n * @param {Object} [data] Data returned by api.php\n * @param {Object} [jqXHR] jQuery XHR object\n */\nmw.cx.TargetArticle.prototype.publishFail = function ( errorCode, messageOrFailObjOrData, data, jqXHR ) {\n\tvar editError, editResult;\n\n\tif ( !data ) {\n\t\tif ( errorCode === 'ok-but-empty' ) {\n\t\t\tthis.showPublishError( mw.msg( 'cx-publish-error-empty' ) );\n\t\t\treturn;\n\t\t}\n\n\t\tthis.showErrorException( messageOrFailObjOrData );\n\t\treturn;\n\t}\n\n\t// Event logging\n\tmw.hook( 'mw.cx.translation.publish.error' ).fire(\n\t\tthis.sourceLanguage,\n\t\tthis.targetLanguage,\n\t\tthis.sourceTitle,\n\t\tthis.getTargetTitle(),\n\t\tdata\n\t);\n\n\teditError = data.error;\n\tif ( editError ) {\n\t\t// Handle spam blacklist error (either from core or from Extension:SpamBlacklist)\n\t\t// Example of API result - https://phabricator.wikimedia.org/P8991\n\t\tif ( editError.code === 'spamblacklist' ) {\n\t\t\tthis.showPublishError(\n\t\t\t\tmw.msg( 'cx-publish-error-spam-blacklist', editError.info ),\n\t\t\t\teditError.info\n\t\t\t);\n\t\t\treturn;\n\t\t} else if ( editError.code.indexOf( 'abusefilter' ) === 0 ) {\n\t\t\t// Handle Abuse Filter errors.\n\t\t\tthis.showPublishError(\n\t\t\t\tmw.msg( 'cx-publish-error-abuse-filter', editError.abusefilter.description ),\n\t\t\t\teditError.info\n\t\t\t);\n\t\t\treturn;\n\t\t} else if ( editError.code === 'invalidtitle' ) {\n\t\t\tthis.showPublishError(\n\t\t\t\tmw.message( 'title-invalid-characters', this.getTargetTitle() ).text(),\n\t\t\t\tJSON.stringify( editError )\n\t\t\t);\n\t\t\treturn;\n\t\t} else if ( editError.code === 'badtoken' || editError.code === 'assertuserfailed' ) {\n\t\t\tthis.showUnrecoverablePublishError(\n\t\t\t\tmw.msg( 'cx-lost-session-publish' ),\n\t\t\t\tJSON.stringify( editError )\n\t\t\t);\n\t\t\treturn;\n\t\t} else if ( editError.code === 'titleblacklist-forbidden' ) {\n\t\t\tthis.showPublishError( mw.msg( 'cx-publish-error-title-blacklist' ), JSON.stringify( editError ) );\n\t\t\treturn;\n\t\t} else if ( editError.code === 'readonly' ) {\n\t\t\tthis.showUnrecoverablePublishError( mw.msg( 'cx-publish-error-readonly' ), editError.readonlyreason );\n\t\t\treturn;\n\t\t}\n\t}\n\n\teditResult = data.edit;\n\t// Handle captcha\n\t// Captcha \"errors\" usually aren't errors. We simply don't know about them ahead of time,\n\t// so we save once, then (if required) we get an error with a captcha back and try again after\n\t// the user solved the captcha.\n\tif ( editResult && editResult.captcha && (\n\t\teditResult.captcha.type === 'image' ||\n\t\teditResult.captcha.type === 'simple' ||\n\t\teditResult.captcha.type === 'math' ||\n\t\teditResult.captcha.type === 'question'\n\t) ) {\n\t\tthis.loadCaptchaDialog().then( this.showErrorCaptcha.bind( this, editResult ) );\n\t\treturn;\n\t}\n\n\t// Handle (other) unknown and/or unrecoverable errors\n\tthis.showErrorUnknown( editResult, data, jqXHR );\n};\n\n/**\n * Load captcha dialog dependency dynamically, since captcha dialog is rarely shown.\n *\n * @return {jQuery}\n */\nmw.cx.TargetArticle.prototype.loadCaptchaDialog = function () {\n\treturn mw.loader.using( 'mw.cx.ui.CaptchaDialog' ).then( this.setupCaptchaDialog.bind( this ) );\n};\n\nmw.cx.TargetArticle.prototype.setupCaptchaDialog = function () {\n\tif ( this.captchaDialog ) {\n\t\t// Dialog is already set up\n\t\treturn;\n\t}\n\n\tthis.captchaDialog = new mw.cx.ui.CaptchaDialog();\n\tthis.captchaDialog.connect( this, {\n\t\tpublish: 'publish',\n\t\tcancel: 'onCaptchaCancel'\n\t} );\n\tOO.ui.getWindowManager().addWindows( [ this.captchaDialog ] );\n};\n\n/**\n * Handle captcha challenge error\n *\n * @param {Object} apiResult publishing API result\n * @fires publishErrorCaptcha\n */\nmw.cx.TargetArticle.prototype.showErrorCaptcha = function ( apiResult ) {\n\tif ( this.captcha ) {\n\t\tthis.captchaDialog.showErrors( mw.msg( 'cx-captcha-dialog-error' ) );\n\t}\n\n\tthis.captcha = {\n\t\tinput: this.captchaDialog.input,\n\t\tid: apiResult.id\n\t};\n\n\tif ( apiResult.type === 'image' ) {\n\t\t// FancyCaptcha\n\t\t// Based on FancyCaptcha::getFormInformation() (https://git.io/v6mml) and\n\t\t// ext.confirmEdit.fancyCaptcha.js in the ConfirmEdit extension.\n\t\tmw.loader.load( 'ext.confirmEdit.fancyCaptcha' );\n\t\tthis.captchaDialog.setFancyCaptcha( apiResult.url );\n\t} else if ( apiResult.type === 'simple' || apiResult.type === 'math' ) {\n\t\t// SimpleCaptcha and MathCaptcha\n\t\tthis.captchaDialog.setCaptcha( 'captcha-create', apiResult.question, apiResult.mime );\n\t} else if ( apiResult.type === 'question' ) {\n\t\t// QuestyCaptcha\n\t\tthis.captchaDialog.setCaptcha( 'questycaptcha-create', apiResult.question, apiResult.mime );\n\t} else {\n\t\tmw.log.error( '[CX] Unsupported captcha type: ' + apiResult.type );\n\t\t// At this point, we encountered unknown or unsupported captcha type, or ConfirmEdit is\n\t\t// malfunctioning in some fashion. User is stuck at this point and cannot publish,\n\t\t// but we can at least unblock the UI and show the error message.\n\t\tthis.onCaptchaCancel();\n\t\tthis.showUnrecoverablePublishError( mw.msg( 'cx-captcha-unsupported-type' ) );\n\t\treturn;\n\t}\n\n\tOO.ui.getWindowManager().openWindow( 'cxCaptcha' );\n};\n\n/**\n * @fires captchaCancel\n */\nmw.cx.TargetArticle.prototype.onCaptchaCancel = function () {\n\tthis.captcha = null;\n\tthis.emit( 'captchaCancel' );\n};\n\n/**\n * Show an error based on an exception+textStatus object\n *\n * @param {Object} failObj Object from the rejection params of mw.Api, with exception and textStatus\n */\nmw.cx.TargetArticle.prototype.showErrorException = function ( failObj ) {\n\tvar errorMsg = failObj.exception || failObj.textStatus;\n\n\tif ( errorMsg instanceof Error ) {\n\t\terrorMsg = errorMsg.toString();\n\t}\n\n\tif ( errorMsg ) {\n\t\tthis.showUnrecoverablePublishError( errorMsg, errorMsg );\n\t} else {\n\t\tthis.showErrorUnknown( null, null, failObj.jqXHR );\n\t}\n};\n\n/**\n * Handle unknown publish error\n *\n * @method\n * @param {Object} editResult\n * @param {Object|null} data API response data\n * @param {Object} jqXHR\n */\nmw.cx.TargetArticle.prototype.showErrorUnknown = function ( editResult, data, jqXHR ) {\n\tvar errorDetails,\n\t\terrorMsg = ( editResult && editResult.info ) || ( data && data.error && data.error.info ),\n\t\terrorCode = ( editResult && editResult.code ) || ( data && data.error && data.error.code ),\n\t\tunknown = 'Unknown error';\n\n\tif ( jqXHR && jqXHR.status !== 200 ) {\n\t\tunknown += ', HTTP status ' + data.xhr.status;\n\t}\n\n\terrorDetails = errorMsg || errorCode || unknown;\n\tthis.showUnrecoverablePublishError(\n\t\tmw.msg( 'cx-publish-error-unknown', errorDetails ),\n\t\terrorDetails\n\t);\n};\n\n/**\n * Show publish process error message\n *\n * @method\n * @param {string|jQuery|Node[]} msg Message content (string of HTML, jQuery object or array of\n *  Node objects)\n * @param {string} [errorLog]\n * @param {boolean} [allowReapply=true] Whether or not to allow the user to reapply.\n *  Reset when swapping panels. Assumed to be true unless explicitly set to false.\n *\n * @fires publishError\n */\nmw.cx.TargetArticle.prototype.showPublishError = function ( msg, errorLog, allowReapply ) {\n\tthis.emit( 'publishError', new OO.ui.Error( msg, { recoverable: allowReapply } ) );\n\n\tif ( !errorLog ) {\n\t\treturn;\n\t}\n\n\tmw.log.error( '[CX] Publishing failed ' + errorLog );\n};\n\n/**\n * Show publish error which doesn't allow reapply.\n *\n * @param {string|jQuery|Node[]} msg Message content (string of HTML, jQuery object or array of\n *  Node objects)\n * @param {string} [errorLog]\n */\nmw.cx.TargetArticle.prototype.showUnrecoverablePublishError = function ( msg, errorLog ) {\n\tthis.showPublishError( msg, errorLog, false );\n};\n\n/**\n * Get content for publishing\n *\n * @param {boolean} deflate Whether the content should be deflated\n * @return {string} Content for publishing, may be deflated\n */\nmw.cx.TargetArticle.prototype.getContent = function ( deflate ) {\n\tvar cleanupHtml,\n\t\tdoc = this.veTarget.getSurface().getDom();\n\n\tcleanupHtml = mw.libs.ve.targetSaver.getHtml( this.constructor.static.getCleanedupContent( doc ) );\n\n\treturn deflate ? mw.deflate( cleanupHtml ) : cleanupHtml;\n};\n\n/**\n * Check to see if \"Publish anyway\" dialog needs to be displayed, in case of\n * page with the given title already existing or translation having issues.\n *\n * @param {string} title The title to check\n * @param {boolean} hasIssues Whether the translation has issues\n * @return {jQuery.Promise}\n */\nmw.cx.TargetArticle.prototype.checkForPublishAnyway = function ( title, hasIssues ) {\n\t// CAPTCHA check may occur as a response to the request to publish the translation.\n\t// If that happens, we can and should skip these checks to avoid showing\n\t// \"Publish anyway\" dialog again if the target page already exists.\n\tif ( this.captcha ) {\n\t\treturn $.Deferred().resolve().promise();\n\t}\n\n\treturn ve.init.platform.linkCache.get( title ).then( function ( result ) {\n\t\tvar title, message,\n\t\t\ttargetExists = !result.missing;\n\n\t\tif ( hasIssues && targetExists ) {\n\t\t\ttitle = mw.msg( 'cx-publishing-dialog-title' );\n\t\t\tmessage = mw.msg( 'cx-overwriting-with-issues' );\n\t\t} else if ( hasIssues && !targetExists ) {\n\t\t\ttitle = mw.msg( 'cx-publishing-with-issues-dialog-title' );\n\t\t\tmessage = mw.msg( 'cx-publishing-with-issues-dialog-message' );\n\t\t} else if ( !hasIssues && targetExists ) {\n\t\t\ttitle = mw.msg( 'cx-publishing-dialog-title' );\n\t\t\tmessage = mw.msg( 'cx-publishing-dialog-sub-title' );\n\t\t} else {\n\t\t\treturn;\n\t\t}\n\n\t\treturn this.showDialog( title, message );\n\t}.bind( this ) );\n};\n\n/**\n * Display the dialog which asks the user to \"publish anyway\", in spite of some problems.\n *\n * @param {string} title Title for the publishing dialog.\n * @param {string} message Main message of the publishing dialog.\n * @return {jQuery.Promise}\n */\nmw.cx.TargetArticle.prototype.showDialog = function ( title, message ) {\n\tvar windowManager = OO.ui.getWindowManager(),\n\t\tmessageDialog = windowManager.getWindow( 'message' );\n\n\treturn messageDialog.then( function ( win ) {\n\t\twin.message.$element.css( 'white-space', 'pre-line' );\n\n\t\treturn windowManager.openWindow( win, {\n\t\t\ttitle: title,\n\t\t\tmessage: message,\n\t\t\tactions: [\n\t\t\t\t{ action: 'publish', label: mw.msg( 'cx-publishing-dialog-publish-anyway-button' ), flags: 'primary' },\n\t\t\t\t{ action: 'cancel', label: mw.msg( 'cx-draft-cancel-button-label' ), flags: 'safe' }\n\t\t\t]\n\t\t} ).closed.then( function ( data ) {\n\t\t\tif ( !data || data.action === 'cancel' ) {\n\t\t\t\treturn $.Deferred().reject();\n\t\t\t}\n\t\t} );\n\t} );\n};\n\n/**\n * Get current target title from translation data model.\n * Not the translation title can be changed by translator at any point of translation.\n *\n * @return {string} target title\n */\nmw.cx.TargetArticle.prototype.getTargetTitle = function () {\n\treturn this.translation.getTargetTitle();\n};\n\nmw.cx.TargetArticle.prototype.getTargetURL = function () {\n\treturn this.siteMapper.getPageUrl( this.targetLanguage, this.getTargetTitle() );\n};\n\n/**\n * Get the categories for the article to be published\n *\n * @param {boolean} shouldAddHighMTCategory True if high MT tracking category should be added.\n * @return {string[]}\n */\nmw.cx.TargetArticle.prototype.getTargetCategories = function ( shouldAddHighMTCategory ) {\n\tvar targetCategories, index,\n\t\tmaintenanceCategoryMsg = 'cx-unreviewed-translation-category';\n\n\ttargetCategories = this.translation.getTargetCategories();\n\tindex = targetCategories.indexOf( maintenanceCategoryMsg );\n\n\tif ( shouldAddHighMTCategory ) {\n\t\t// Avoid duplicates.\n\t\tif ( index < 0 ) {\n\t\t\t// Note that we are adding the msg as an indicator that\n\t\t\t// this article need the tracking category. The prefixing\n\t\t\t// of appropriate namespace and category title localization\n\t\t\t// is done at publish api backend.\n\t\t\ttargetCategories.push( maintenanceCategoryMsg );\n\t\t}\n\t} else if ( index >= 0 ) {\n\t\t// Make sure to remove if maintenanceCategoryMsg is already in targetCategories\n\t\ttargetCategories.splice( index, 1 );\n\t}\n\n\treturn targetCategories;\n};\n\n/**\n * Get the tags for the article to be published.\n * API accepts multiple values separated by '|'\n *\n * @return {string}\n */\nmw.cx.TargetArticle.prototype.getTags = function () {\n\tvar query = new mw.Uri().query,\n\t\tcampainConfig = mw.config.get( 'wgContentTranslationCampaigns' );\n\treturn OO.getProp( campainConfig, query.campaign, 'edittag' ) || '';\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.TranslationController.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.TranslationTracker.js","messages":[{"ruleId":"jsdoc/require-returns","severity":1,"message":"Found more than one @return declaration.","line":594,"column":1,"nodeType":"Block","endLine":604,"endColumn":4},{"ruleId":"jsdoc/require-returns-check","severity":1,"message":"Found more than one @return declaration.","line":594,"column":1,"nodeType":"Block","endLine":604,"endColumn":4}],"errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Translation progress tracker and MT abuse detection.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @param {ve.init.mw.CXTarget} veTarget\n * @param {Object} config\n * @cfg {string} sourceLanguage\n * @cfg {string} targetLanguage\n */\nmw.cx.TranslationTracker = function MwCXTranslationTracker( veTarget, config ) {\n\tthis.sourceLanguage = config.sourceLanguage;\n\tthis.targetLanguage = config.targetLanguage;\n\tthis.veTarget = veTarget;\n\n\t// Array that stores IDs of sections with issues, along with special value for title issues.\n\tthis.nodesWithIssues = [];\n\t// Sections in the translation. Associative array with section numbers as keys\n\t// and values as mw.cx.dm.SectionState\n\tthis.sections = {};\n\tthis.validationDelayQueue = [];\n\tthis.changeQueue = [];\n\tthis.saveQueue = [];\n\tthis.validationScheduler = OO.ui.debounce( this.processValidationQueue.bind( this ), 15 * 1000 );\n};\n\n/* Initialization */\n\nOO.initClass( mw.cx.TranslationTracker );\n\n/* Static members */\n\n// Values determining how much unmodified content we tolerate in various cases\nmw.cx.TranslationTracker.static.unmodifiedContentThreshold = {\n\tmt: 0.85,\n\tmtAfterSuppressWarning: 0.95,\n\tsource: 0.6,\n\tsourceAfterSuppressWarning: 0.75\n};\n\n/**\n * Calculate the section translation progress based on relative number of tokens.\n * If there are 10 tokens and all translated, return 1, if 5 more tokens\n * added, return 1.5 and so on.\n *\n * @param {string} string1\n * @param {string} string2\n * @param {string} language\n * @return {number}\n */\nmw.cx.TranslationTracker.static.calculateSectionTranslationProgress = function ( string1, string2, language ) {\n\tvar tokens1, tokens2;\n\tif ( string1 === string2 ) {\n\t\treturn 1;\n\t}\n\tif ( !string1 || !string2 ) {\n\t\treturn 0;\n\t}\n\n\ttokens1 = this.tokenise( string1, language );\n\ttokens2 = this.tokenise( string2, language );\n\n\treturn tokens2.length / tokens1.length;\n};\n\n/**\n * A very simple method to calculate the difference between two strings in the scale\n * of 0 to 1, based on relative number of tokens changed in string2 from string1.\n *\n * @param {string} string1\n * @param {string} string2\n * @param {string} language\n * @return {number} A value between 0 and 1\n */\nmw.cx.TranslationTracker.static.calculateUnmodifiedContent = function ( string1, string2, language ) {\n\tvar unmodifiedTokens, bigSet, smallSet, tokens1, tokens2;\n\n\tif ( !string1 || !string2 ) {\n\t\treturn 0;\n\t}\n\n\tif ( string1 === string2 ) {\n\t\t// Both strings are equal. So string2 is 100% unmodified version of string1\n\t\treturn 1;\n\t}\n\n\tbigSet = tokens1 = this.tokenise( string1, language );\n\tsmallSet = tokens2 = this.tokenise( string2, language );\n\n\tif ( tokens2.length > tokens1.length ) {\n\t\t// Swap the sets\n\t\tbigSet = tokens2;\n\t\tsmallSet = tokens1;\n\t}\n\n\t// Find the intersection(tokens that did not change) two token sets\n\tunmodifiedTokens = bigSet.filter( function ( token ) {\n\t\treturn smallSet.indexOf( token ) >= 0;\n\t} );\n\n\t// If string1 has 10 tokens and we see that 2 tokens are different or not present in string2,\n\t// we are saying that string2 is 80% (ie. 10-2/10) of unmodified version fo string1.\n\treturn unmodifiedTokens.length / bigSet.length;\n};\n\n/**\n * Tokenize a given string. Here tokens is basically words for non CJK languages.\n * For CJK languages, we just split at each codepoint level.\n *\n * @param {string} string\n * @param {string} language\n * @return {string[]}\n */\nmw.cx.TranslationTracker.static.tokenise = function ( string, language ) {\n\tif ( !string ) {\n\t\treturn [];\n\t}\n\tif ( $.uls.data.scriptgroups.CJK.indexOf( $.uls.data.getScript( language ) ) >= 0 ) {\n\t\treturn string.split( '' );\n\t}\n\n\t// Match all non whitespace characters for tokens.\n\treturn string.match( /\\S+/g ) || [];\n};\n\n/**\n * Check if a node is excluded from MT abuse validation or not.\n *\n * @param {ve.dm.BranchNode} nodeModel\n * @return {boolean}\n */\nmw.cx.TranslationTracker.static.isExcludedFromValidation = function ( nodeModel ) {\n\tvar children, childType,\n\t\texcludedTypes = [\n\t\t\t'cxBlockImage', 'mwBlockImage', // Both are required since new images can be inserted too.\n\t\t\t'cxTransclusionBlock', 'mwTransclusionBlock',\n\t\t\t'mwReferencesList',\n\t\t\t'mwMath',\n\t\t\t'definitionList',\n\t\t\t'mwAlienBlockExtension', 'mwAlienInlineExtension',\n\t\t\t'mwTable', 'list', 'mwHeading'\n\t\t];\n\n\t// check if node itself is excluded before check it\n\tif ( nodeModel && nodeModel.getType && excludedTypes.indexOf( nodeModel.getType() ) >= 0 ) {\n\t\treturn true;\n\t}\n\n\tif ( nodeModel && nodeModel.getChildren ) {\n\t\t// Make sure than nodeModel is a ve.dm.BranchNode by checking\n\t\t// if getChildren method exist\n\t\tchildren = nodeModel.getChildren();\n\t}\n\n\tif ( children && children.length === 1 ) {\n\t\t// Get the type of one and only one child of the nodeModel\n\t\tchildType = children[ 0 ].getType();\n\t\tif ( excludedTypes.indexOf( childType ) >= 0 ) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\t// Recurse through the single child path in the node tree.\n\t\t\treturn this.isExcludedFromValidation( children[ 0 ] );\n\t\t}\n\t}\n\n\treturn false;\n};\n\n/**\n * Returns all children nodes of a given section node that should be validated for MT abuse, by checking\n * for each child node if it is excluded from validation\n *\n * @param {ve.dm.BranchNode} nodeModel\n * @return {ve.dm.BranchNode[]} Flat array of nodes\n */\nmw.cx.TranslationTracker.static.getChildrenNodesForValidation = function ( nodeModel ) {\n\tvar children,\n\t\tcurrentNode,\n\t\tnodesToBeValidated = [],\n\t\ti;\n\tif ( nodeModel && nodeModel.getChildren ) {\n\t\tchildren = nodeModel.getChildren();\n\t}\n\n\tif ( !children ) {\n\t\treturn [];\n\t}\n\n\tfor ( i = 0; i < children.length; i++ ) {\n\t\tcurrentNode = children[ i ];\n\t\tif ( !this.isExcludedFromValidation( currentNode ) ) {\n\t\t\tif ( currentNode.getChildren && currentNode.getChildren().length > 1 ) {\n\t\t\t\tnodesToBeValidated = nodesToBeValidated.concat( this.getChildrenNodesForValidation( currentNode ) );\n\t\t\t} else {\n\t\t\t\tnodesToBeValidated.push( currentNode );\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nodesToBeValidated;\n};\n\n/**\n * Returns all tokens for an array of nodes that should be validated for MT abuse\n *\n * @param {ve.dm.BranchNode[]} validationTree\n * @param {string} language\n * @return {string[]}\n */\nmw.cx.TranslationTracker.static.getTokensFromValidationTree = function ( validationTree, language ) {\n\tvar sourceTokens = [],\n\t\tsourceText,\n\t\tvalidationNode,\n\t\ti;\n\n\tif ( !Array.isArray( validationTree ) ) {\n\t\tmw.log.warn( '[CX] No nodes for MT abuse validation' );\n\t\treturn [];\n\t}\n\n\tfor ( i = 0; i < validationTree.length; i++ ) {\n\t\tvalidationNode = validationTree[ i ];\n\t\tsourceText = $( ve.dm.converter.getDomFromNode( validationNode ) ).text();\n\t\tsourceTokens = sourceTokens.concat( this.tokenise( sourceText, language ) );\n\t}\n\n\treturn sourceTokens;\n};\n\n/**\n * Returns all tokens for a section node that should be validated for MT abuse\n *\n * @param {ve.dm.BranchNode} sectionModel\n * @param {string} language\n * @return {string[]}\n */\nmw.cx.TranslationTracker.static.getSectionNodeValidationTokens = function ( sectionModel, language ) {\n\tvar validationTree = this.getChildrenNodesForValidation( sectionModel );\n\treturn this.getTokensFromValidationTree( validationTree, language );\n};\n\n/* Methods */\n\n/**\n * Initialize translation.\n *\n * @param {mw.cx.dm.Translation} translationModel\n */\nmw.cx.TranslationTracker.prototype.init = function ( translationModel ) {\n\tvar i, sectionNumber, sectionModels, sectionModel, sectionState,\n\t\tsavedTranslationUnits, savedTranslationUnit, progress,\n\t\trestoredSections = 0;\n\n\tsectionModels = translationModel.sourceDoc.getNodesByType( 'cxSection' );\n\tsavedTranslationUnits = translationModel.savedTranslationUnits || [];\n\tfor ( i = 0; i < sectionModels.length; i++ ) {\n\t\tsectionModel = sectionModels[ i ];\n\n\t\tsectionNumber = sectionModel.getSectionNumber();\n\t\tsectionState = new mw.cx.dm.SectionState( sectionNumber );\n\t\tsectionState.setSource( ve.dm.converter.getDomFromNode( sectionModel ).body.innerHTML );\n\t\tsavedTranslationUnit = savedTranslationUnits[ sectionNumber ];\n\t\tif ( savedTranslationUnit ) {\n\t\t\tif ( savedTranslationUnit.user ) {\n\t\t\t\tsectionState.setCurrentMTProvider( savedTranslationUnit.user.engine );\n\t\t\t\tsectionState.setUserTranslation( savedTranslationUnit.user.content );\n\t\t\t}\n\t\t\tif ( savedTranslationUnit.mt ) {\n\t\t\t\t// Machine translation, unmodified.\n\t\t\t\tsectionState.setCurrentMTProvider( savedTranslationUnit.mt.engine );\n\t\t\t\tsectionState.setUnmodifiedMT( savedTranslationUnit.mt.content );\n\t\t\t\tsectionState.markUnmodifiedMTSaved();\n\t\t\t}\n\t\t\trestoredSections++;\n\t\t\tthis.changeQueue.push( sectionNumber );\n\t\t}\n\n\t\tthis.sections[ sectionNumber ] = sectionState;\n\t}\n\n\tthis.adjustSectionStateForSourceTranslations( this.getSectionsTranslatedFromSource( translationModel ) );\n\n\tmw.log( '[CX] Translation tracker initialized for ' +\n\t\tsectionModels.length + ' sections (' + restoredSections + ' restored)' );\n\n\tif ( restoredSections > 0 ) {\n\t\tprogress = this.getTranslationProgress();\n\t\tif ( !OO.compare( translationModel.progress, progress ) ) {\n\t\t\tmw.log.error( '[CX] Mismatch in restored translation has progress. Saved progress was: ' +\n\t\t\t\tJSON.stringify( translationModel.progress ) );\n\t\t}\n\t\tmw.log( '[CX] Restored translation has progress: ' + JSON.stringify( progress ) );\n\t\t// Do the change processing and validations on the restored sections without any delay.\n\t\tthis.processChangeQueue();\n\t\tthis.processValidationQueue();\n\t}\n\n\tthis.attachOnBlurListeners( translationModel.targetDoc.getNodesByType( 'cxSection' ) );\n};\n\n/**\n * Attach listeners for 'blur' events on restored sections as well as on newly added sections.\n *\n * @param {ve.dm.CXSectionNode[]} sections\n */\nmw.cx.TranslationTracker.prototype.attachOnBlurListeners = function ( sections ) {\n\t// Register event listeners for 'blur' event on restored sections\n\tsections.map( function ( sectionModel ) {\n\t\treturn sectionModel.getId();\n\t} ).forEach( this.registerOnBlurListenerForSection.bind( this ) );\n\n\t// Register event listeners for 'blur' event for every newly added section\n\tthis.veTarget.connect( this, { changeContentSource: 'registerOnBlurListenerForSection' } );\n};\n\n/**\n * When section is translated by adapting the source section, that is not saved in the\n * parallel corpora table. So, when we restore that section, we don't have anything to\n * compare user translation to, when section progress is calculated.\n * Therefore, use source content as unmodified MT.\n *\n * @param {number[]} sectionIds Array of IDs of sections translated from source.\n */\nmw.cx.TranslationTracker.prototype.adjustSectionStateForSourceTranslations = function ( sectionIds ) {\n\tif ( !Array.isArray( sectionIds ) ) {\n\t\tthrow new Error( 'Must provide IDs of sections translated from source as array' );\n\t}\n\n\tsectionIds.forEach( function ( sectionId ) {\n\t\tvar sectionState = this.sections[ sectionId ];\n\n\t\tsectionState.setCurrentMTProvider( 'source' );\n\t\tsectionState.setUnmodifiedMT( sectionState.getSource().html );\n\t}.bind( this ) );\n};\n\n/**\n * @param {mw.cx.dm.Translation} translationModel\n * @return {number[]} IDs of sections translated from source.\n */\nmw.cx.TranslationTracker.prototype.getSectionsTranslatedFromSource = function ( translationModel ) {\n\tvar targetSections = translationModel.targetDoc.getNodesByType( 'cxSection' );\n\n\treturn targetSections.filter( function ( sectionModel ) {\n\t\treturn sectionModel.getOriginalContentSource() === 'source';\n\t} ).map( function ( sectionModel ) {\n\t\treturn sectionModel.getId();\n\t} );\n};\n\n/**\n * @param {boolean} includeAll True if all sections should be returned. False if sections\n * excluded from MT abuse validation should be left out.\n * @return {ve.dm.CXSectionNode[]} Target section models which are validated for MT abuse\n */\nmw.cx.TranslationTracker.prototype.getTargetSectionModels = function ( includeAll ) {\n\treturn this.veTarget.translation.targetDoc.getNodesByType( 'article' )[ 0 ].getChildren()\n\t\t.filter( function ( node ) {\n\t\t\treturn node.getType() === 'cxSection' && ( includeAll || !this.constructor.static.isExcludedFromValidation( node ) );\n\t\t}, this );\n};\n\n/**\n * Process the change queue.\n * This will be called by getTranslationProgress when saving happens, also\n * by section changes in debounced manner.\n */\nmw.cx.TranslationTracker.prototype.processChangeQueue = function () {\n\tvar i = this.changeQueue.length;\n\twhile ( i-- ) {\n\t\tthis.processSectionChange( this.changeQueue[ i ] );\n\t\tthis.changeQueue.splice( i, 1 );\n\t}\n};\n\n/**\n * Section change handler\n *\n * @param {string} sectionNumber\n */\nmw.cx.TranslationTracker.prototype.processSectionChange = function ( sectionNumber ) {\n\tvar sectionModel, sectionState, newContent, existingContent,\n\t\tcurrentMTProvider, unmodifiedMTContent, newMTProvider, freshTranslation;\n\n\tsectionModel = this.veTarget.getTargetSectionNodeFromSectionNumber( sectionNumber );\n\tif ( !sectionModel ) {\n\t\t// sectionModel can be null in case this handler is executed while the node\n\t\t// is being modified. Since this method is debounced, chances are rare.\n\t\t// Still checking for null.\n\t\treturn;\n\t}\n\tsectionState = this.sections[ sectionNumber ];\n\n\tif ( !( sectionModel instanceof ve.dm.CXSectionNode ) ) {\n\t\t// sectionModel can be a PlaceholderNode by undo operation too.\n\t\tsectionState.setCurrentMTProvider( null );\n\t\tsectionState.setUserTranslation( '' );\n\t\t// Remove it from the delayed queues.\n\t\tthis.removeSectionFromSaveQueue( sectionNumber );\n\t\tthis.removeSectionFromValidationQueue( sectionNumber );\n\t\treturn;\n\t}\n\n\tcurrentMTProvider = sectionState.getCurrentMTProvider();\n\tnewMTProvider = sectionModel.getOriginalContentSource();\n\tif ( currentMTProvider !== newMTProvider ) {\n\t\t// Fresh translation or MT Engine change\n\t\tmw.log( '[CX] MT Engine change for section ' + sectionNumber + ' to MT ' + newMTProvider );\n\t\tsectionState.setCurrentMTProvider( newMTProvider );\n\t\t// Reset the saved content in section state.\n\t\tsectionState.setUserTranslation( null );\n\t\tfreshTranslation = true;\n\t}\n\n\tnewContent = ve.dm.converter.getDomFromNode( sectionModel ).body.innerHTML;\n\texistingContent = sectionState.getUserTranslation();\n\tunmodifiedMTContent = sectionState.getUnmodifiedMT();\n\tif ( !unmodifiedMTContent.html ) {\n\t\t// Fresh translation. Extract and save the unmodified MT content to section state.\n\t\tsectionState.setCurrentMTProvider( newMTProvider );\n\t\tsectionState.setUnmodifiedMT( newContent );\n\t\tmw.log( '[CX] Fresh translation for section ' + sectionNumber + ' with MT ' + newMTProvider );\n\t}\n\tif ( newContent !== existingContent.html ) {\n\t\t// A modification of user translated content. Save the modified content to section state\n\t\tsectionState.setUserTranslation( newContent );\n\t\tmw.log( '[CX] Content modified for section ' + sectionNumber + ' with MT ' + newMTProvider );\n\t}\n\n\t// NOTE: For unmodified MT, we use the same content for userTranslatedContent\n\n\t// Let the section model know whether it has been modified on top of initial value\n\tsectionModel.setHasUserModifications( sectionState.isModified() );\n\n\t// Calculate and update the progress\n\tthis.updateSectionProgress( sectionNumber );\n\n\tif ( freshTranslation ) {\n\t\t// For freshly translated section, delay the validation till next action on same section\n\t\t// or other sections. But do validations for any queued sections.\n\t\tthis.processValidationQueue();\n\t\tthis.pushToValidationQueue( sectionNumber );\n\t\treturn;\n\t}\n\n\tthis.pushToValidationQueue( sectionNumber );\n\tthis.validationScheduler();\n};\n\n/**\n * Calculate and update the section translation progress.\n *\n * @param {number} sectionNumber\n */\nmw.cx.TranslationTracker.prototype.updateSectionProgress = function ( sectionNumber ) {\n\tvar unmodifiedPercentage, progress,\n\t\tsectionState = this.sections[ sectionNumber ],\n\t\tunmodifiedContent = sectionState.getUnmodifiedMT(),\n\t\tuserTranslation = sectionState.getUserTranslation();\n\n\tunmodifiedPercentage = this.constructor.static.calculateUnmodifiedContent(\n\t\tunmodifiedContent.text,\n\t\tuserTranslation.text,\n\t\tthis.targetLanguage\n\t);\n\tsectionState.setUnmodifiedPercentage( unmodifiedPercentage );\n\n\t// Calculate the progress. It is a value between 0 and 1\n\tprogress = this.constructor.static.calculateSectionTranslationProgress(\n\t\tsectionState.getSource().text,\n\t\tuserTranslation.text,\n\t\tthis.targetLanguage\n\t);\n\tsectionState.setTranslationProgressPercentage( progress );\n};\n\n/**\n * Check if a section has unmodified MT beyond a threshold. If so, add a warning issue\n * to the section model.\n *\n * @param {number} sectionNumber\n * @return {boolean} Whether the section is crossing the unmodified MT threshold\n */\nmw.cx.TranslationTracker.prototype.validateForMTAbuse = function ( sectionNumber ) {\n\tvar sectionState = this.sections[ sectionNumber ],\n\t\tsectionModel = this.veTarget.getTargetSectionNodeFromSectionNumber( sectionNumber ),\n\t\tsourceTokens = this.constructor.static.getSectionNodeValidationTokens( sectionModel, this.sourceLanguage );\n\n\tif ( sourceTokens.length < 10 ) {\n\t\t// Exclude smaller sections from MT abuse validations\n\t\treturn false;\n\t}\n\n\treturn sectionState.getUnmodifiedPercentage() > this.getUnmodifiedContentThreshold( sectionState );\n};\n\nmw.cx.TranslationTracker.prototype.setMTAbuseWarning = function ( sectionModel ) {\n\tvar percentage, sectionState;\n\n\tif ( !sectionModel ) {\n\t\treturn;\n\t}\n\n\tsectionState = this.sections[ sectionModel.getSectionNumber() ];\n\tpercentage = mw.language.convertNumber(\n\t\tMath.round( sectionState.getUnmodifiedPercentage() * 100 ) );\n\tmw.log( '[CX] Unmodified MT percentage for section ' + sectionModel.getSectionNumber() +\n\t\t' ' + percentage + '% crossed the threshold ' + this.getUnmodifiedContentThreshold( sectionState ) * 100 );\n\n\tsectionModel.addTranslationIssues( [ {\n\t\tname: 'mt-abuse',\n\t\tmessage: mw.message( 'cx-mt-abuse-warning-text' ),\n\t\tmessageInfo: {\n\t\t\ttitle: mw.msg( 'cx-mt-abuse-warning-title', percentage ),\n\t\t\ttype: 'warning',\n\t\t\thelp: 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Content_translation/Translating/Translation_quality',\n\t\t\tresolvable: true\n\t\t}\n\t} ] );\n};\n\n/**\n * @param {mw.cx.dm.SectionState} sectionState\n * @param {boolean} forSuppressed True if getter is used for suppressed issues.\n * @return {number} Threshold which indicates if text is considered insufficiently modified\n * to be treated as a good translation:\n * - For paragraphs started with MT,\n * content is considered unmodified above the threshold of \"unmodifiedContentThreshold.mt\".\n * - For paragraphs started by copying the source text,\n * content is considered unmodified above the threshold of \"unmodifiedContentThreshold.source\".\n *\n * When MT abuse issue is marked as resolved by user, higher thresholds are used:\n * - \"unmodifiedContentThreshold.mtAfterSuppressWarning\" - for paragraphs started with MT\n * - \"unmodifiedContentThreshold.sourceAfterSuppressWarning\" - for paragraphs started\n * by copying the source text\n */\nmw.cx.TranslationTracker.prototype.getUnmodifiedContentThreshold = function ( sectionState, forSuppressed ) {\n\tvar unmodifiedContentThreshold = this.constructor.static.unmodifiedContentThreshold,\n\t\tisSource = sectionState.getCurrentMTProvider() === 'source';\n\n\tif ( !forSuppressed ) {\n\t\treturn isSource ? unmodifiedContentThreshold.source : unmodifiedContentThreshold.mt;\n\t}\n\n\treturn isSource ?\n\t\tunmodifiedContentThreshold.sourceAfterSuppressWarning :\n\t\tunmodifiedContentThreshold.mtAfterSuppressWarning;\n};\n\nmw.cx.TranslationTracker.prototype.clearMTAbuseWarning = function ( sectionModel ) {\n\tif ( sectionModel && sectionModel instanceof ve.dm.CXSectionNode ) {\n\t\tsectionModel.resolveTranslationIssues( [ 'mt-abuse' ] );\n\t}\n};\n\n/**\n * @return {ve.dm.CXSectionNode[]} Target sections with MT abuse.\n */\nmw.cx.TranslationTracker.prototype.sectionsWithMTAbuse = function () {\n\treturn this.getTargetSectionModels().filter( function ( sectionModel ) {\n\t\tvar sectionState, issue, threshold, unmodifiedPercentage,\n\t\t\tindex = sectionModel.findIssueIndex( 'mt-abuse' );\n\n\t\tif ( index < 0 ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tissue = sectionModel.translationIssues[ index ];\n\t\tif ( !issue.isSuppressed() ) {\n\t\t\treturn true;\n\t\t}\n\n\t\tsectionState = this.sections[ sectionModel.getSectionNumber() ];\n\t\tunmodifiedPercentage = sectionState.getUnmodifiedPercentage();\n\t\tthreshold = this.getUnmodifiedContentThreshold( sectionState, true );\n\n\t\tif ( unmodifiedPercentage > threshold ) {\n\t\t\tmw.log(\n\t\t\t\t'[CX] Section ' + sectionModel.getSectionNumber() + ' has MT percentage of ' +\n\t\t\t\tMath.round( unmodifiedPercentage ) + '%. Issue is suppressed, ' +\n\t\t\t\t'but percentage is greater than ' + threshold\n\t\t\t);\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}, this );\n};\n\n/**\n * Calculate the translation progress percentages.\n *\n * @return {Object} Map for translation progress metrics\n * @return {number} return.any Translation progress, expressed as number of translated sections\n * out of total number of translations.\n * @return {number} return.mt Number of unmodified tokens in translation. Sections excluded from\n * MT abuse checking are not counted.\n * @return {number} return.human Number of user-provided tokens, calculated as MT percentage\n * subtracted from 100%.\n */\nmw.cx.TranslationTracker.prototype.getTranslationProgress = function () {\n\tvar unmodifiedMTPercentage,\n\t\tsourceSectionCount = Object.keys( this.sections ).length,\n\t\ttargetSectionCount = this.getTargetSectionModels( true ).length;\n\n\t// Recalculate the progress. Make sure we are not using old data.\n\tthis.processChangeQueue();\n\tunmodifiedMTPercentage = this.getUnmodifiedMTPercentageInTranslation() / 100;\n\n\treturn {\n\t\tany: targetSectionCount / sourceSectionCount,\n\t\tmt: unmodifiedMTPercentage,\n\t\thuman: 1 - unmodifiedMTPercentage\n\t};\n};\n\n/**\n * Get percentage of unmodified tokens in translation.\n *\n * @return {number} Number of unmodified tokens relative to total user translation tokens.\n */\nmw.cx.TranslationTracker.prototype.getUnmodifiedMTPercentageInTranslation = function () {\n\tvar unmodifiedTokens = 0,\n\t\ttotalTokens = 0;\n\n\tthis.getTargetSectionModels().forEach( function ( sectionModel ) {\n\t\tvar sectionState = this.sections[ sectionModel.getId() ],\n\t\t\tunmodifiedMTTokens = this.constructor.static.tokenise(\n\t\t\t\tsectionState.getUnmodifiedMT().text,\n\t\t\t\tthis.targetLanguage\n\t\t\t),\n\t\t\tuserTranslationTokens = this.constructor.static.tokenise(\n\t\t\t\tsectionState.getUserTranslation().text,\n\t\t\t\tthis.targetLanguage\n\t\t\t);\n\n\t\ttotalTokens += userTranslationTokens.length;\n\t\tunmodifiedTokens += userTranslationTokens.filter( function ( token ) {\n\t\t\treturn unmodifiedMTTokens.indexOf( token ) >= 0;\n\t\t} ).length;\n\t}, this );\n\n\t// Avoid division by zero\n\tif ( totalTokens === 0 ) {\n\t\treturn 0;\n\t}\n\n\treturn ( unmodifiedTokens / totalTokens ) * 100;\n};\n\n/**\n * @param {number} sectionNumber\n */\nmw.cx.TranslationTracker.prototype.registerOnBlurListenerForSection = function ( sectionNumber ) {\n\tvar sectionNode = this.veTarget.getTargetSectionElementFromSectionNumber( sectionNumber );\n\n\tsectionNode.connect( this, { blur: 'processValidationQueue' } );\n};\n\n/**\n * Process any delayed validations on sections.\n */\nmw.cx.TranslationTracker.prototype.processValidationQueue = function () {\n\tvar i, sectionNumber, sectionModel;\n\n\ti = this.validationDelayQueue.length;\n\twhile ( i-- ) {\n\t\tsectionNumber = this.validationDelayQueue[ i ];\n\t\tsectionModel = this.veTarget.getTargetSectionNodeFromSectionNumber( sectionNumber );\n\t\tif ( !this.constructor.static.isExcludedFromValidation( sectionModel ) ) {\n\t\t\tif ( this.validateForMTAbuse( sectionNumber ) ) {\n\t\t\t\tthis.setMTAbuseWarning( sectionModel );\n\t\t\t} else {\n\t\t\t\tthis.clearMTAbuseWarning( sectionModel );\n\t\t\t}\n\t\t}\n\t\tthis.validationDelayQueue.splice( i, 1 );\n\t}\n};\n\n/**\n * Adds new nodes with issues to the tracking array. Nodes that have\n * their issues resolved, are removed from the array.\n *\n * @param {number|string} id Section number or special values of 'title' and 'global'\n * @param {boolean} state True if node has issues\n */\nmw.cx.TranslationTracker.prototype.setTranslationIssues = function ( id, state ) {\n\tvar index = this.nodesWithIssues.indexOf( id ),\n\t\tsortLettersAndNumbers = function ( a, b ) {\n\t\t\t// When 'title' and 'global' are compared, put 'global' in front\n\t\t\tif ( isNaN( a ) && isNaN( b ) ) {\n\t\t\t\treturn a > b ? 1 : -1;\n\t\t\t}\n\n\t\t\t// When `a` is string ('global' or 'title'), put it before numerical values\n\t\t\tif ( isNaN( a ) ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\t// When `a` is number, put it after string values\n\t\t\tif ( isNaN( b ) ) {\n\t\t\t\treturn 1;\n\t\t\t}\n\n\t\t\treturn a > b ? 1 : -1;\n\t\t};\n\n\tif ( index !== -1 ) {\n\t\tif ( !state ) {\n\t\t\tthis.nodesWithIssues.splice( index, 1 );\n\t\t}\n\n\t\treturn;\n\t} else if ( !state ) {\n\t\treturn;\n\t}\n\n\tthis.nodesWithIssues.push( id );\n\t// Sort, so that special string keys, like 'title' or 'global' come first\n\t// and then section numbers in ascending order. Duplicates are unexpected\n\tthis.nodesWithIssues.sort( sortLettersAndNumbers );\n};\n\n/**\n * Get IDs of all nodes with issues. Nodes include target title, translation sections.\n * Unattached issues don't have a node, but are kept in mw.cx.dm.Translation.\n *\n * @return {Mixed[]} Node IDs\n */\nmw.cx.TranslationTracker.prototype.getNodesWithIssues = function () {\n\treturn this.nodesWithIssues;\n};\n\n/**\n * Check if the section is in the change queue\n *\n * @param {string} sectionNumber\n * @return {boolean}\n */\nmw.cx.TranslationTracker.prototype.isSectionInChangeQueue = function ( sectionNumber ) {\n\treturn this.changeQueue.indexOf( sectionNumber ) >= 0;\n};\n\nmw.cx.TranslationTracker.prototype.pushToChangeQueue = function ( sectionNumber ) {\n\tif ( !this.isSectionInChangeQueue( sectionNumber ) ) {\n\t\tthis.changeQueue.push( sectionNumber );\n\t}\n};\n\n/**\n * Check if the section is in the save queue\n *\n * @param {string} sectionNumber\n * @return {boolean}\n */\nmw.cx.TranslationTracker.prototype.isSectionInSaveQueue = function ( sectionNumber ) {\n\treturn this.saveQueue.indexOf( sectionNumber ) >= 0;\n};\n\nmw.cx.TranslationTracker.prototype.pushToSaveQueue = function ( sectionNumber ) {\n\tif ( !this.isSectionInSaveQueue( sectionNumber ) ) {\n\t\tthis.saveQueue.push( sectionNumber );\n\t}\n};\n\nmw.cx.TranslationTracker.prototype.pushToValidationQueue = function ( sectionNumber ) {\n\tif ( this.validationDelayQueue.indexOf( sectionNumber ) < 0 ) {\n\t\tthis.validationDelayQueue.push( sectionNumber );\n\t}\n};\n\n/**\n * Remove section from the save queue for the given section number,\n *\n * @param {string} sectionNumber\n */\nmw.cx.TranslationTracker.prototype.removeSectionFromSaveQueue = function ( sectionNumber ) {\n\tvar index = this.saveQueue.indexOf( sectionNumber );\n\tif ( index >= 0 ) {\n\t\tthis.saveQueue.splice( index, 1 );\n\t} else {\n\t\tmw.log.warn( '[CX] Attempting to remove non-existing section ' + sectionNumber + ' from save queue.' );\n\t}\n};\n\n/**\n * Remove section from the validation delay queue for the given section number,\n *\n * @param {string} sectionNumber\n */\nmw.cx.TranslationTracker.prototype.removeSectionFromValidationQueue = function ( sectionNumber ) {\n\tvar index = this.validationDelayQueue.indexOf( sectionNumber );\n\tif ( index >= 0 ) {\n\t\tthis.validationDelayQueue.splice( index, 1 );\n\t}\n};\n\n/**\n * Get the current save queue\n *\n * @return {number[]}\n */\nmw.cx.TranslationTracker.prototype.getSaveQueue = function () {\n\treturn this.saveQueue;\n};\n\n/**\n * Get the section state for the given section number,\n *\n * @param {number} sectionNumber\n * @return {mw.cx.dm.SectionState}\n */\nmw.cx.TranslationTracker.prototype.getSectionState = function ( sectionNumber ) {\n\treturn this.sections[ sectionNumber ];\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.init.Translation.js","messages":[{"ruleId":"no-shadow","severity":1,"message":"'sourceDom' is already declared in the upper scope on line 173 column 6.","line":201,"column":7,"nodeType":"Identifier","messageId":"noShadow","endLine":201,"endColumn":16},{"ruleId":"no-shadow","severity":1,"message":"'targetDom' is already declared in the upper scope on line 173 column 17.","line":201,"column":18,"nodeType":"Identifier","messageId":"noShadow","endLine":201,"endColumn":27},{"ruleId":"no-shadow","severity":1,"message":"'sourceHtml' is already declared in the upper scope on line 172 column 68.","line":203,"column":4,"nodeType":"Identifier","messageId":"noShadow","endLine":203,"endColumn":14}],"errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\nmw.cx.init = {};\n\n/**\n * This class loads translation documents (source and target) and sets up the main views and models.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @param {mw.cx.dm.WikiPage} sourceWikiPage\n * @param {mw.cx.dm.WikiPage} targetWikiPage\n * @param {Object} config Standard services TODO not optional so should not be called config\n * @cfg {mw.cx.SiteMapper} siteMapper\n * @cfg {string} [campaign] String indicating which CTA was used to start this translation\n */\nmw.cx.init.Translation = function MwCXInitTranslation( sourceWikiPage, targetWikiPage, config ) {\n\tthis.sourceWikiPage = sourceWikiPage;\n\tthis.targetWikiPage = targetWikiPage;\n\n\t// BC with other code\n\tthis.config = config;\n\tthis.config.sourceTitle = sourceWikiPage.getTitle();\n\tthis.config.sourceLanguage = sourceWikiPage.getLanguage();\n\tthis.config.sourceRevision = sourceWikiPage.getRevision();\n\tthis.config.targetTitle = targetWikiPage.getTitle();\n\tthis.config.targetLanguage = targetWikiPage.getLanguage();\n\n\tthis.mainNamespaceId = mw.config.get( 'wgNamespaceIds' )[ '' ];\n\tthis.userNamespaceId = mw.config.get( 'wgNamespaceIds' ).user;\n\n\t// @var {ve.init.mw.CXTarget}\n\tthis.veTarget = null;\n\t// @var {mw.cx.dm.Translation}\n\tthis.translationModel = null;\n\t// @var {mw.cx.TranslationController}\n\tthis.translationController = null;\n\t// @var {mw.cx.ui.TranslationView}\n\tthis.translationView = null;\n};\n\n/**\n * Initialize translation.\n */\nmw.cx.init.Translation.prototype.init = function () {\n\tvar platformPromise, translationPromise, modulePromise, pluginModules;\n\n\tif ( mw.user.isAnon() ) {\n\t\tmw.hook( 'mw.cx.error.anonuser' ).fire();\n\t\treturn;\n\t}\n\tif ( this.config.campaign ) {\n\t\tmw.hook( 'mw.cx.cta.accept' ).fire(\n\t\t\tthis.config.campaign,\n\t\t\tthis.sourceWikiPage.getLanguage(),\n\t\t\tthis.targetWikiPage.getLanguage()\n\t\t);\n\t}\n\tthis.translationView = new mw.cx.ui.TranslationView( this.config );\n\tthis.veTarget = new ve.init.mw.CXTarget( this.translationView, this.config );\n\t// Paint the initial UI.\n\tthis.attachToDOM( this.veTarget );\n\n\tthis.veTarget.connect( this, { namespaceChange: 'onNamespaceChange' } );\n\n\t// TODO: Use mw.libs.ve.targetLoader.loadModules instead of manually getting the plugin\n\t// modules and manually initializing the platform\n\tplatformPromise = new ve.init.mw.Platform().initialize();\n\ttranslationPromise = this.fetchTranslationData();\n\tpluginModules = mw.config.get( 'wgVisualEditorConfig' ).pluginModules;\n\tmodulePromise = mw.loader.using( [ 'mw.cx.visualEditor' ].concat( pluginModules ) );\n\t$.when( translationPromise, modulePromise, platformPromise ).then( function ( translationData ) {\n\t\tvar categoryUI,\n\t\t\tsourcePageContent = translationData[ 0 ],\n\t\t\tdraft = translationData[ 1 ];\n\n\t\t// Set the link cache for source language\n\t\tve.init.platform.sourceLinkCache = new ve.init.mw.LinkCache(\n\t\t\tthis.config.siteMapper.getApi( this.sourceWikiPage.getLanguage() )\n\t\t);\n\n\t\t// Set the link cache for target language\n\t\tve.init.platform.linkCache = new ve.init.mw.LinkCache(\n\t\t\tthis.config.siteMapper.getApi( this.targetWikiPage.getLanguage() )\n\t\t);\n\n\t\tthis.sourceWikiPage.setRevision( sourcePageContent.revision );\n\n\t\tthis.initTranslationModel( sourcePageContent.segmentedContent, draft ).then( function ( translationModel ) {\n\t\t\tvar mwSectionNumber = mw.cx.sectionForTranslation();\n\n\t\t\tthis.translationModel = translationModel;\n\n\t\t\tif ( draft ) {\n\t\t\t\ttranslationModel.setSavedTranslation( draft );\n\t\t\t}\n\n\t\t\t// Initialize translation controller\n\t\t\tthis.translationController = new mw.cx.TranslationController(\n\t\t\t\ttranslationModel, this.veTarget, this.config.siteMapper, this.config\n\t\t\t);\n\n\t\t\tthis.veTarget.setTranslation( translationModel );\n\n\t\t\tthis.checkIfUserCanPublish();\n\t\t\tif ( translationModel.isChangedSignificantly() ) {\n\t\t\t\tthis.addChangedSignificantlyIssue( translationModel );\n\t\t\t}\n\n\t\t\t// If section translation is enabled for CX (by using \"section\" param in URL),\n\t\t\t// hide all other sections.\n\t\t\tif ( mwSectionNumber ) {\n\t\t\t\tmw.loader.addStyleTag(\n\t\t\t\t\t'.ve-ce-cxSectionNode:not( .mw-source-section-' + mwSectionNumber + ' ),' +\n\t\t\t\t\t'.ve-ce-cxPlaceholderNode:not( .mw-target-section-' + mwSectionNumber + ' )' +\n\t\t\t\t\t'{ display: none; }'\n\t\t\t\t);\n\t\t\t}\n\n\t\t\ttranslationModel.initCategories(\n\t\t\t\tthis.processCategories( sourcePageContent.categories )\n\t\t\t);\n\t\t\tcategoryUI = new mw.cx.ui.Categories( translationModel, this.config );\n\t\t\tthis.translationView.showCategories( categoryUI );\n\n\t\t\tif ( draft ) {\n\t\t\t\tmw.hook( 'mw.cx.draft.restored' ).fire();\n\t\t\t}\n\t\t\tmw.log( '[CX] Translation initialized successfully' );\n\t\t}.bind( this ) );\n\t}.bind( this ), this.initializationError.bind( this ) );\n\n\tthis.addFeedbackLink();\n};\n\n/**\n * Fetch all data necessary to start a translation.\n *\n * @return {jQuery.Promise}\n */\nmw.cx.init.Translation.prototype.fetchTranslationData = function () {\n\tvar sourcePageFetchDeferred, draftFetchDeferred;\n\n\tmw.log( '[CX] Fetching Source page...' );\n\tsourcePageFetchDeferred = this.fetchSourcePageContent(\n\t\tthis.sourceWikiPage, this.targetWikiPage.getLanguage(), this.config.siteMapper\n\t).fail( this.fetchSourcePageContentError.bind( this ) );\n\n\tmw.log( '[CX] Checking existing translation...' );\n\tdraftFetchDeferred = this.fetchDraftInformation(\n\t\tthis.sourceWikiPage, this.targetWikiPage\n\t).then(\n\t\tthis.fetchDraftInformationSuccess.bind( this ),\n\t\tthis.fetchDraftInformationError.bind( this )\n\t).then( function ( draftId ) {\n\t\tmw.log( '[CX] Fetching existing translation for id: ' + draftId );\n\t\treturn this.fetchDraft( draftId ).fail( this.fetchDraftError.bind( this ) );\n\t}.bind( this ) );\n\n\treturn $.when( sourcePageFetchDeferred, draftFetchDeferred );\n};\n\n/**\n * Create translation model object. If latest revision causes any user translations to be lost,\n * load the original revision used when translation was started.\n *\n * @param {string} sourceHtml\n * @param {Object} draft\n * @return {jQuery.Promise}\n */\nmw.cx.init.Translation.prototype.initTranslationModel = function ( sourceHtml, draft ) {\n\tvar sourceDom, targetDom, translationModel, translationUnitId,\n\t\ttranslationUnits = draft && draft.translationUnits,\n\t\tnumberOfUnrestoredSections = 0;\n\n\ttargetDom = mw.cx.dm.Translation.static.getSourceDom(\n\t\tsourceHtml, true, translationUnits, this.sourceWikiPage.getLanguage()\n\t);\n\n\tfor ( translationUnitId in translationUnits ) {\n\t\tif ( !translationUnits[ translationUnitId ].restored ) {\n\t\t\tnumberOfUnrestoredSections++;\n\t\t}\n\t}\n\n\t// If no translated section was lost, create source DOM and return early\n\t// This should cover initial start of translation, when there's no draft at all.\n\tif ( numberOfUnrestoredSections < 1 ) {\n\t\tsourceDom = mw.cx.dm.Translation.static.getSourceDom( sourceHtml );\n\n\t\ttranslationModel = new mw.cx.dm.Translation( this.sourceWikiPage, this.targetWikiPage, sourceDom, targetDom );\n\t\treturn $.Deferred().resolve( translationModel ).promise();\n\t}\n\n\t// Update revision of source page\n\tthis.sourceWikiPage.setRevision( draft.sourceRevisionId );\n\treturn this.fetchSourcePageContent(\n\t\tthis.sourceWikiPage, this.targetWikiPage.getLanguage(), this.config.siteMapper\n\t).then( function ( sourcePageContent ) {\n\t\tvar sourceDom, targetDom,\n\t\t\turi = new mw.Uri(),\n\t\t\tsourceHtml = sourcePageContent.segmentedContent;\n\n\t\t// Reset restoration status for all translation units\n\t\tfor ( translationUnitId in translationUnits ) {\n\t\t\ttranslationUnits[ translationUnitId ].restored = false;\n\t\t}\n\n\t\tsourceDom = mw.cx.dm.Translation.static.getSourceDom( sourceHtml );\n\t\ttargetDom = mw.cx.dm.Translation.static.getSourceDom(\n\t\t\tsourceHtml, true, translationUnits, this.sourceWikiPage.getLanguage()\n\t\t);\n\n\t\ttranslationModel = new mw.cx.dm.Translation( this.sourceWikiPage, this.targetWikiPage, sourceDom, targetDom );\n\t\ttranslationModel.setChangedSignificantly( true );\n\n\t\t// Append revision number to URL\n\t\turi = uri.extend( { revision: draft.sourceRevisionId } );\n\t\twindow.history.pushState( null, document.title, uri.toString() );\n\n\t\treturn translationModel;\n\t}.bind( this ), this.fetchSourcePageContentError.bind( this ) );\n};\n\n/**\n * Initialization error handler\n */\nmw.cx.init.Translation.prototype.initializationError = function () {\n\t// Any error in the above deferreds is critical\n\tthis.translationView.showMessage( 'error', mw.msg( 'cx-init-critical-error' ) );\n\t// Nothing happens beyond this. Some internal error happened.\n\tmw.log.error( '[CX] Translation initialization failed.' );\n};\n\n/**\n * Attach the translation view to DOM.\n *\n * @private\n * @param {ve.init.mw.CXTarget} veTarget\n */\nmw.cx.init.Translation.prototype.attachToDOM = function ( veTarget ) {\n\t$( 'body' ).append( veTarget.$element );\n};\n\n/**\n * Fetch the source page content from cxserver.\n *\n * @private\n * @param {mw.cx.dm.WikiPage} wikiPage\n * @param {string} targetLanguage\n * @param {mw.cx.SiteMapper} siteMapper\n * @return {jQuery.Promise}\n */\nmw.cx.init.Translation.prototype.fetchSourcePageContent = function ( wikiPage, targetLanguage, siteMapper ) {\n\tvar fetchParams, apiURL, fetchPageUrl;\n\n\tfetchParams = {\n\t\t$sourcelanguage: siteMapper.getWikiDomainCode( wikiPage.getLanguage() ),\n\t\t$targetlanguage: targetLanguage,\n\t\t// Manual normalisation to avoid redirects on spaces but not to break namespaces\n\t\t$title: wikiPage.getTitle().replace( / /g, '_' )\n\t};\n\n\tapiURL = '/page/$sourcelanguage/$targetlanguage/$title';\n\n\t// If revision is requested, load that revision of page.\n\tif ( wikiPage.getRevision() ) {\n\t\tfetchParams.$revision = wikiPage.getRevision();\n\t\tapiURL += '/$revision';\n\t}\n\n\tfetchPageUrl = siteMapper.getCXServerUrl( apiURL, fetchParams );\n\n\treturn $.get( fetchPageUrl ).then( function ( data ) {\n\t\t// The $.when that use the output of this will treat respose as array(data, textStatus, jqXHR)\n\t\t// We need only the first argument.\n\t\treturn data;\n\t} );\n};\n\nmw.cx.init.Translation.prototype.fetchSourcePageContentError = function ( xhr ) {\n\tif ( xhr.status === 404 ) {\n\t\tmw.hook( 'mw.cx.error' ).fire(\n\t\t\tmw.msg(\n\t\t\t\t'cx-error-page-not-found',\n\t\t\t\tthis.sourceWikiPage.getTitle(),\n\t\t\t\t$.uls.data.getAutonym( this.sourceWikiPage.getLanguage() )\n\t\t\t)\n\t\t);\n\t} else {\n\t\tmw.hook( 'mw.cx.error' ).fire( mw.msg( 'cx-error-server-connection' ) );\n\t}\n};\n\n/**\n * Find if there is a draft existing for the current title and language pair.\n *\n * @private\n * @param {mw.cx.dm.WikiPage} sourceWikiPage\n * @param {mw.cx.dm.WikiPage} targetWikiPage\n * @return {jQuery.Promise} Information about an existing draft (if any) as returned by the API.\n */\nmw.cx.init.Translation.prototype.fetchDraftInformation = function ( sourceWikiPage, targetWikiPage ) {\n\treturn new mw.Api().get( {\n\t\taction: 'query',\n\t\tlist: 'contenttranslation',\n\t\tsourcetitle: sourceWikiPage.getTitle(),\n\t\tfrom: sourceWikiPage.getLanguage(),\n\t\tto: targetWikiPage.getLanguage()\n\t} ).then( function ( response ) {\n\t\treturn response.query && response.query.contenttranslation.translation;\n\t} );\n};\n\n/**\n * Check whether an existing draft can be used.\n *\n * @private\n * @param {Object} draft\n * @return {jQuery.Promise} Draft id or null.\n */\nmw.cx.init.Translation.prototype.fetchDraftInformationSuccess = function ( draft ) {\n\tif ( !draft ) {\n\t\t// No draft exists\n\t\tmw.log( '[CX] No existing translation found' );\n\t\treturn $.Deferred().resolve( null ).promise();\n\t}\n\n\t// Do not allow two users to start a draft at the same time. The API only\n\t// returns a translation with different translatorName if this is the case.\n\tif ( draft.translatorName !== mw.user.getName() ) {\n\t\tmw.log( '[CX] Existing translation found. But owned by another translator' );\n\t\tthis.translationView.showConflictWarning( draft );\n\t\t// Stop further processing!\n\t\treturn $.Deferred().reject().promise();\n\t}\n\n\t// Don't restore deleted drafts\n\tif ( draft.status === 'deleted' ) {\n\t\tmw.log( '[CX] Existing translation found. But it is a deleted one.' );\n\t\treturn $.Deferred().resolve( null ).promise();\n\t}\n\n\treturn $.Deferred().resolve( draft.id ).promise();\n};\n\nmw.cx.init.Translation.prototype.fetchDraftInformationError = function () {\n\t// XXX\n\tmw.hook( 'mw.cx.error' ).fire( 'Unable to fetch draft information.' );\n\tmw.log( '[CX]', arguments );\n};\n\n/**\n * Fetch the translation from database (if any exists)\n *\n * @private\n * @param {string|null} draftId Id for saved draft\n * @return {jQuery.Promise}\n */\nmw.cx.init.Translation.prototype.fetchDraft = function ( draftId ) {\n\t// In case there is no draft, skip loading it\n\tif ( draftId === null ) {\n\t\treturn $.Deferred().resolve().promise();\n\t}\n\n\tthis.translationView.setStatusMessage( mw.msg( 'cx-draft-restoring' ) );\n\n\treturn new mw.Api().get( {\n\t\taction: 'query',\n\t\tlist: 'contenttranslation',\n\t\ttranslationid: draftId\n\t} ).then( function ( response ) {\n\t\treturn response.query.contenttranslation.translation;\n\t} );\n};\n\nmw.cx.init.Translation.prototype.fetchDraftError = function ( errorCode, details ) {\n\tif ( details.exception instanceof Error ) {\n\t\tdetails.exception = details.exception.toString();\n\t}\n\tdetails.errorCode = errorCode;\n\tthis.translationView.setStatusMessage( mw.msg( 'cx-draft-restore-failed' ) );\n};\n\n/**\n * Process fetched categories to create mapping of source category and target category or null, if\n * there is no adapted target category.\n *\n * @param {Array} fetchedCategories\n * @return {Object} Array of categories transformed into object of\n * sourceTitle:targetTitle property-value pairs\n */\nmw.cx.init.Translation.prototype.processCategories = function ( fetchedCategories ) {\n\tvar category,\n\t\tcategories = {},\n\t\tlength = fetchedCategories.length;\n\n\twhile ( length-- ) {\n\t\tcategory = fetchedCategories[ length ];\n\t\tcategories[ category.sourceTitle ] = category.targetTitle || null;\n\t}\n\n\treturn categories;\n};\n\nmw.cx.init.Translation.prototype.addFeedbackLink = function () {\n\tvar feedback = new OO.ui.ButtonWidget( {\n\t\tlabel: mw.msg( 'cx-feedback-link' ),\n\t\ticon: 'speechBubbles',\n\t\thref: '//www.mediawiki.org/wiki/Talk:Content_translation',\n\t\ttarget: '_blank',\n\t\tframed: false,\n\t\tclasses: [ 'cx-feedback-link' ],\n\t\tflags: [ 'progressive' ]\n\t} );\n\tthis.translationView.addItems( [ feedback ] );\n};\n\nmw.cx.init.Translation.prototype.addChangedSignificantlyIssue = function ( translationModel ) {\n\tvar diff, translationIssuesParams;\n\n\tthis.translationView.showViewIssuesMessage(\n\t\tmw.msg( 'cx-infobar-old-version' ), 'old-version', 'warning'\n\t);\n\n\tdiff = this.config.siteMapper.getPageUrl(\n\t\ttranslationModel.getSourceLanguage(),\n\t\ttranslationModel.getSourceTitle(),\n\t\t{\n\t\t\ttype: 'revision',\n\t\t\tdiff: 'cur',\n\t\t\toldid: translationModel.getSourceRevisionId()\n\t\t}\n\t);\n\n\ttranslationIssuesParams = {\n\t\ttitle: mw.msg( 'cx-tools-linter-old-revision' ),\n\t\tresolvable: true\n\t};\n\n\tif ( !translationModel.hasBeenPublished() ) {\n\t\ttranslationIssuesParams.additionalButtons = [\n\t\t\t{\n\t\t\t\ticon: 'reload',\n\t\t\t\tlabel: mw.msg( 'cx-tools-linter-old-revision-label' ),\n\t\t\t\taction: this.restartTranslation.bind( this )\n\t\t\t}\n\t\t];\n\t}\n\n\ttranslationModel.addUnattachedIssues( [\n\t\tnew mw.cx.dm.TranslationIssue(\n\t\t\t'old-version', // Issue name\n\t\t\tmw.message( 'cx-tools-linter-old-revision-message', diff ), // Message body\n\t\t\ttranslationIssuesParams\n\t\t)\n\t] );\n};\n\nmw.cx.init.Translation.prototype.restartTranslation = function () {\n\tOO.ui.getWindowManager().openWindow( 'message', {\n\t\ttitle: mw.msg( 'cx-tools-linter-restart-translation-title' ),\n\t\tmessage: mw.msg( 'cx-tools-linter-restart-translation-message' ),\n\t\tactions: [\n\t\t\t{ action: 'restart', label: mw.msg( 'cx-tools-linter-old-revision-label' ), flags: [ 'primary', 'destructive' ] },\n\t\t\t{ action: 'cancel', label: mw.msg( 'cx-tools-linter-restart-translation-cancel' ), flags: 'safe' }\n\t\t]\n\t} ).closed.then( function ( data ) {\n\t\tvar sourceLanguage, targetLanguage, sourceTitle, apiParams;\n\n\t\tif ( !data || data.action !== 'restart' ) {\n\t\t\treturn;\n\t\t}\n\n\t\tsourceLanguage = this.translationModel.getSourceLanguage();\n\t\ttargetLanguage = this.translationModel.getTargetLanguage();\n\t\tsourceTitle = this.translationModel.getSourceTitle();\n\t\tapiParams = {\n\t\t\tassert: 'user',\n\t\t\taction: 'cxdelete',\n\t\t\tfrom: sourceLanguage,\n\t\t\tto: targetLanguage,\n\t\t\tsourcetitle: sourceTitle\n\t\t};\n\n\t\treturn new mw.Api().postWithToken( 'csrf', apiParams ).done( function () {\n\t\t\tvar uri = new mw.Uri();\n\t\t\tdelete uri.query.revision;\n\n\t\t\tthis.config.siteMapper.setCXToken( sourceLanguage, targetLanguage, sourceTitle );\n\n\t\t\tlocation.href = uri.getRelativePath();\n\t\t}.bind( this ) );\n\t}.bind( this ) );\n};\n\nmw.cx.init.Translation.prototype.isUserAllowedToPublishToMainNamespace = function () {\n\tvar userGroups = mw.config.get( 'wgUserGroups' ) || [],\n\t\tpublishConfig = ( mw.config.get( 'wgContentTranslationPublishRequirements' ) || [] ).userGroups;\n\n\tif ( typeof publishConfig === 'string' ) {\n\t\tpublishConfig = [ publishConfig ];\n\t}\n\n\tif ( !Array.isArray( publishConfig ) ) {\n\t\tmw.log.error( 'Publish requirement config should be of type array or string' );\n\t\treturn true;\n\t}\n\n\treturn publishConfig.some( function ( userGroup ) {\n\t\treturn userGroups.indexOf( userGroup ) > -1;\n\t} );\n};\n\nmw.cx.init.Translation.prototype.checkIfUserCanPublish = function () {\n\tif ( this.veTarget.getPublishNamespace() !== this.mainNamespaceId ) {\n\t\treturn;\n\t}\n\n\tif ( !this.isUserAllowedToPublishToMainNamespace() ) {\n\t\tthis.displayCannotPublishError();\n\t}\n};\n\n/**\n * Display the error when user cannot publish into main namespace.\n */\nmw.cx.init.Translation.prototype.displayCannotPublishError = function () {\n\tthis.translationView.showViewIssuesMessage(\n\t\tmw.msg( 'cx-infobar-cannot-publish' ), 'cannot-publish', 'error'\n\t);\n\n\t// User isn't allowed to publish, display the information in the issue card.\n\tthis.translationModel.resolveIssueByName( 'cannot-publish' );\n\tthis.translationModel.addUnattachedIssues( [\n\t\tnew mw.cx.dm.TranslationIssue(\n\t\t\t'cannot-publish', // Issue name\n\t\t\tmw.message( 'cx-tools-linter-cannot-publish-message' ).parseDom(), // message body\n\t\t\t{\n\t\t\t\ttitle: mw.msg( 'cx-tools-linter-cannot-publish-title' ),\n\t\t\t\ttype: 'error',\n\t\t\t\thelp: 'https://en.wikipedia.org/wiki/Wikipedia:Content_translation_tool',\n\t\t\t\tresolvable: true,\n\t\t\t\tactionIcon: 'article',\n\t\t\t\tactionLabel: mw.msg( 'cx-tools-linter-cannot-publish-action-label' ),\n\t\t\t\taction: this.switchToUserNamespace.bind( this )\n\t\t\t}\n\t\t)\n\t] );\n};\n\nmw.cx.init.Translation.prototype.switchToUserNamespace = function () {\n\tvar popup = new OO.ui.PopupWidget( {\n\t\t$content: $( '<p>' ).text( mw.msg( 'cx-publish-destination-namespace-changed' ) ),\n\t\tpadded: true,\n\t\tautoClose: true\n\t} );\n\n\tthis.veTarget.getActions()\n\t\t.getToolGroupByName( 'publish' )\n\t\t.findItemFromData( 'publishSettings' )\n\t\t.$element.append( popup.$element );\n\tpopup.toggle( true );\n\n\tthis.translationModel.resolveIssueByName( 'cannot-publish' );\n\tthis.translationView.clearMessages();\n\tthis.veTarget.onPublishNamespaceChange( this.userNamespaceId );\n};\n\nmw.cx.init.Translation.prototype.onNamespaceChange = function ( namespaceId ) {\n\tthis.checkIfUserCanPublish();\n\n\tif ( this.mainNamespaceId !== namespaceId ) {\n\t\tthis.translationModel.resolveIssueByName( 'cannot-publish' );\n\t\tthis.translationView.removeMessage( 'cannot-publish' );\n\t}\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.init.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/publish/ext.cx.publish.dialog.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/publish/ext.cx.publish.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/publish/ext.cx.wikibase.link.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/source/conf/common.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/source/conf/en-cy.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/source/conf/en-pl.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/source/conf/es-ca.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/source/ext.cx.source.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/source/mw.cx.SelectedSourcePage.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/source/mw.cx.SelectedSourcePageDialog.js","messages":[{"ruleId":"jsdoc/valid-types","severity":1,"message":"@extends should not have a bracketed type in \"jsdoc\" mode.","line":15,"column":null,"nodeType":"Block","endLine":15,"endColumn":null}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * SelectedSourcePageDialog\n *\n * Simple dialog wrapper for SelectedSourcePage\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n'use strict';\n\n/**\n * SelectedSourcePageDialog\n *\n * @class\n * @extends {OO.ui.Dialog}\n */\nmw.cx.SelectedSourcePageDialog = function () {\n\t// Parent method\n\tmw.cx.SelectedSourcePageDialog.super.apply( this, arguments );\n\n\tthis.selectedSourcePage = null;\n};\n\nOO.inheritClass( mw.cx.SelectedSourcePageDialog, OO.ui.Dialog );\n\nmw.cx.SelectedSourcePageDialog.static.name = 'selectedSourcePage';\nmw.cx.SelectedSourcePageDialog.static.size = 'large';\n\n/**\n * @inheritdoc\n */\nmw.cx.SelectedSourcePageDialog.prototype.getSetupProcess = function ( data ) {\n\t// Parent method\n\treturn mw.cx.SelectedSourcePageDialog.super.prototype.getSetupProcess.call( this, data )\n\t\t.next( function () {\n\t\t\tthis.selectedSourcePage = data.selectedSourcePage;\n\t\t\tthis.$body.append( data.selectedSourcePage.$element );\n\t\t}, this );\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.SelectedSourcePageDialog.prototype.getReadyProcess = function ( data ) {\n\t// Parent method\n\treturn mw.cx.SelectedSourcePageDialog.super.prototype.getReadyProcess.call( this, data )\n\t\t.next( function () {\n\t\t\tthis.selectedSourcePage.focusStartTranslationButton();\n\t\t}, this );\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.SelectedSourcePageDialog.prototype.getTeardownProcess = function ( data ) {\n\t// Parent method\n\treturn mw.cx.SelectedSourcePageDialog.super.prototype.getTeardownProcess.call( this, data )\n\t\t.next( function () {\n\t\t\tthis.selectedSourcePage = null;\n\t\t\tthis.$body.empty();\n\t\t}, this );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/source/mw.cx.SourcePageSelector.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/stats/ext.cx.stats.js","messages":[{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 612 column 7.","line":617,"column":55,"nodeType":"Identifier","messageId":"noShadow","endLine":617,"endColumn":59},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 612 column 7.","line":629,"column":55,"nodeType":"Identifier","messageId":"noShadow","endLine":629,"endColumn":59},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 612 column 7.","line":641,"column":49,"nodeType":"Identifier","messageId":"noShadow","endLine":641,"endColumn":53},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 657 column 7.","line":662,"column":58,"nodeType":"Identifier","messageId":"noShadow","endLine":662,"endColumn":62},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 657 column 7.","line":674,"column":58,"nodeType":"Identifier","messageId":"noShadow","endLine":674,"endColumn":62},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 657 column 7.","line":686,"column":52,"nodeType":"Identifier","messageId":"noShadow","endLine":686,"endColumn":56},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 657 column 7.","line":698,"column":55,"nodeType":"Identifier","messageId":"noShadow","endLine":698,"endColumn":59},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 714 column 7.","line":718,"column":55,"nodeType":"Identifier","messageId":"noShadow","endLine":718,"endColumn":59},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 714 column 7.","line":727,"column":55,"nodeType":"Identifier","messageId":"noShadow","endLine":727,"endColumn":59},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 714 column 7.","line":736,"column":49,"nodeType":"Identifier","messageId":"noShadow","endLine":736,"endColumn":53},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 752 column 12.","line":757,"column":58,"nodeType":"Identifier","messageId":"noShadow","endLine":757,"endColumn":62},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 752 column 12.","line":766,"column":58,"nodeType":"Identifier","messageId":"noShadow","endLine":766,"endColumn":62},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 752 column 12.","line":775,"column":52,"nodeType":"Identifier","messageId":"noShadow","endLine":775,"endColumn":56},{"ruleId":"no-shadow","severity":1,"message":"'data' is already declared in the upper scope on line 752 column 12.","line":784,"column":55,"nodeType":"Identifier","messageId":"noShadow","endLine":784,"endColumn":59}],"errorCount":0,"warningCount":14,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation Stats\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\t/* global Chart:false */\n\n\tfunction CXStats( $container, options ) {\n\t\tthis.$container = $container;\n\t\tthis.sitemapper = options.siteMapper;\n\t\tthis.sourceTargetModel = {};\n\t\tthis.targetSourceModel = {};\n\t\tthis.totalTranslationTrend = null;\n\t\tthis.languageTranslationTrend = null;\n\t\tthis.$highlights = null;\n\t\tthis.$graph = null;\n\t\tthis.chartOptions = {};\n\t}\n\n\tCXStats.prototype.init = function () {\n\t\tvar self = this,\n\t\t\t$spinner;\n\n\t\t$spinner = mw.cx.widgets.spinner();\n\t\tthis.$highlights = $( '<div>' ).addClass( 'cx-stats-highlights' );\n\t\tthis.$container.append( $spinner, this.$highlights );\n\n\t\t$.when(\n\t\t\tthis.getCXTrends(),\n\t\t\tthis.getCXTrends( mw.config.get( 'wgContentLanguage' ) ),\n\t\t\tthis.getCXStats()\n\t\t).done( function ( totalTrend, languageTrend, stats ) {\n\t\t\t// Remove spinner\n\t\t\t$spinner.remove();\n\n\t\t\tself.totalTranslationTrend = totalTrend.translations || [];\n\t\t\tself.totalDraftTrend = totalTrend.drafts || [];\n\t\t\tself.languageTranslationTrend = languageTrend.translations || [];\n\t\t\tself.languageDraftTrend = languageTrend.drafts || [];\n\t\t\tself.languageDeletionTrend = languageTrend.deletions || [];\n\t\t\tself.transformJsonToModel( stats[ 0 ].query.contenttranslationstats );\n\t\t\t// Now render them all\n\t\t\tself.renderHighlights();\n\t\t\tself.render();\n\t\t} );\n\n\t\tthis.chartOptions = {\n\t\t\tscales: {\n\t\t\t\txAxes: [\n\t\t\t\t\t{\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcallback: function ( value ) {\n\t\t\t\t\t\t\t\treturn moment( value ).format( 'L' );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t],\n\t\t\t\tyAxes: [\n\t\t\t\t\t{\n\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tcallback: function ( value ) {\n\t\t\t\t\t\t\t\treturn mw.language.convertNumber( Number( value ) );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\ttooltips: {\n\t\t\t\tcallbacks: {\n\t\t\t\t\tlabel: function ( tooltipItem, data ) {\n\t\t\t\t\t\tvar convertedValue = mw.language.convertNumber( Number( tooltipItem.yLabel ) );\n\t\t\t\t\t\treturn data.datasets[ tooltipItem.datasetIndex ].label + ': ' + convertedValue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t};\n\n\t/**\n\t * Render the boxes at the top with the most interesting recent data.\n\t */\n\tCXStats.prototype.renderHighlights = function () {\n\t\tvar getTrend, info, infoLanguage, localLanguage,\n\t\t\t$total, $weeklyStats,\n\t\t\tweekLangTrendText, weekTrendText, weekTrendClass,\n\t\t\t$parenthesizedTrend, $trendInLanguage,\n\t\t\tfmt = mw.language.convertNumber; // Shortcut\n\n\t\tgetTrend = function ( data ) {\n\t\t\tvar thisWeek, total, trend,\n\t\t\t\toneWeekAgoDelta, twoWeeksAgoDelta;\n\n\t\t\tif ( data.length < 3 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthisWeek = data.length - 1;\n\n\t\t\ttotal = data[ thisWeek ].count;\n\n\t\t\toneWeekAgoDelta = data[ thisWeek - 1 ].delta;\n\t\t\ttwoWeeksAgoDelta = data[ thisWeek - 2 ].delta;\n\n\t\t\tif ( twoWeeksAgoDelta ) {\n\t\t\t\ttrend = Math.round( ( oneWeekAgoDelta - twoWeeksAgoDelta ) / twoWeeksAgoDelta * 100 );\n\t\t\t} else {\n\t\t\t\ttrend = oneWeekAgoDelta ? 100 : 0;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttotal: total,\n\t\t\t\ttrend: trend,\n\t\t\t\tlastWeek: oneWeekAgoDelta\n\t\t\t};\n\t\t};\n\n\t\tlocalLanguage = $.uls.data.getAutonym( mw.config.get( 'wgContentLanguage' ) );\n\t\tinfo = getTrend( this.totalTranslationTrend );\n\t\tinfoLanguage = getTrend( this.languageTranslationTrend );\n\n\t\tif ( !info || !infoLanguage ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$total = $( '<div>' )\n\t\t\t.addClass( 'cx-stats-box' )\n\t\t\t.append(\n\t\t\t\t$( '<div>' )\n\t\t\t\t\t.addClass( 'cx-stats-box__title' )\n\t\t\t\t\t.text( mw.msg( 'cx-stats-total-published' ) ),\n\t\t\t\t$( '<div>' )\n\t\t\t\t\t.addClass( 'cx-stats-box__total' )\n\t\t\t\t\t.text( fmt( info.total ) ),\n\t\t\t\t$( '<div>' )\n\t\t\t\t\t.addClass( 'cx-stats-box__localtotal' )\n\t\t\t\t\t.text( mw.msg(\n\t\t\t\t\t\t'cx-stats-local-published-number',\n\t\t\t\t\t\tfmt( infoLanguage.total ),\n\t\t\t\t\t\tfmt( localLanguage )\n\t\t\t\t\t) )\n\t\t\t);\n\n\t\tweekLangTrendText = mw.msg( 'percent', fmt( infoLanguage.trend ) );\n\t\tif ( infoLanguage.trend >= 0 ) {\n\t\t\t// Add the plus sign to make clear that it's an increase\n\t\t\tweekLangTrendText = '+' + weekLangTrendText;\n\t\t}\n\n\t\tweekTrendText = mw.msg( 'percent', fmt( info.trend ) );\n\t\tif ( info.trend >= 0 ) {\n\t\t\t// Add the plus sign to make clear that it's an increase\n\t\t\tweekTrendText = '+' + weekTrendText;\n\t\t\tweekTrendClass = 'increase';\n\t\t} else {\n\t\t\tweekTrendClass = 'decrease';\n\t\t}\n\n\t\t$parenthesizedTrend = $( '<span>' )\n\t\t\t// This is needed to show the plus or minus sign on the correct side\n\t\t\t.prop( 'dir', 'ltr' )\n\t\t\t.text( weekLangTrendText );\n\t\t$trendInLanguage = $( '<div>' )\n\t\t\t.addClass( 'cx-stats-box__localtotal' )\n\t\t\t.text( mw.msg(\n\t\t\t\t'cx-stats-local-published',\n\t\t\t\tfmt( infoLanguage.lastWeek ),\n\t\t\t\tlocalLanguage,\n\t\t\t\t'$3'\n\t\t\t) );\n\t\t$trendInLanguage.html( $trendInLanguage.html().replace(\n\t\t\t'$3',\n\t\t\t$parenthesizedTrend.get( 0 ).outerHTML\n\t\t) );\n\n\t\t$weeklyStats = $( '<div>' )\n\t\t\t.addClass( 'cx-stats-box' )\n\t\t\t.append(\n\t\t\t\t$( '<div>' )\n\t\t\t\t\t.addClass( 'cx-stats-box__title' )\n\t\t\t\t\t.text( mw.msg( 'cx-stats-weekly-published' ) ),\n\t\t\t\t$( '<div>' ).append(\n\t\t\t\t\t$( '<span>' )\n\t\t\t\t\t\t.addClass( 'cx-stats-box__total' )\n\t\t\t\t\t\t.text( fmt( info.lastWeek ) ),\n\t\t\t\t\t// nbsp is needed for separation between the numbers.\n\t\t\t\t\t// Without it the numbers appear in the wrong order in RTL environments.\n\t\t\t\t\t$( '<span>' )\n\t\t\t\t\t\t.html( '&#160;' ),\n\t\t\t\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\t\t\t\t$( '<span>' )\n\t\t\t\t\t\t.prop( 'dir', 'ltr' )\n\t\t\t\t\t\t.addClass( 'cx-stats-box__trend ' + weekTrendClass )\n\t\t\t\t\t\t.text( weekTrendText )\n\t\t\t\t),\n\t\t\t\t$trendInLanguage\n\t\t\t);\n\n\t\tthis.$highlights.append( $total, $weeklyStats );\n\t};\n\n\tCXStats.prototype.render = function () {\n\t\tvar self = this;\n\n\t\tthis.$cumulativeGraph = $( '<canvas>' ).attr( {\n\t\t\tid: 'cxcumulative',\n\t\t\twidth: this.$container.width() - 200, // Leave a 200px margin buffer to avoid overflow\n\t\t\theight: 400\n\t\t} );\n\n\t\tthis.$languageCumulativeGraph = $( '<canvas>' ).attr( {\n\t\t\tid: 'cxlangcumulative',\n\t\t\twidth: this.$container.width() - 200, // Leave a 200px margin buffer to avoid overflow\n\t\t\theight: 400\n\t\t} );\n\n\t\tthis.$translationTrendBarChart = $( '<canvas>' ).attr( {\n\t\t\tid: 'cxtrendchart',\n\t\t\twidth: this.$container.width() - 200, // Leave a 200px margin buffer to avoid overflow\n\t\t\theight: 400\n\t\t} );\n\n\t\tthis.$langTranslationTrendBarChart = $( '<canvas>' ).attr( {\n\t\t\tid: 'cxlangtrendchart',\n\t\t\twidth: this.$container.width() - 200, // Leave a 200px margin buffer to avoid overflow\n\t\t\theight: 400\n\t\t} );\n\n\t\tthis.$container.append( $( '<h2>' ).text( mw.msg( 'cx-stats-all-translations-title' ) ) );\n\t\tthis.createTabs(\n\t\t\t'cx-graph-total', [\n\t\t\t\t{\n\t\t\t\t\ttitle: mw.msg( 'cx-stats-cumulative-tab-title' ),\n\t\t\t\t\tid: 'global-translations',\n\t\t\t\t\tcontent: $( '<div>' )\n\t\t\t\t\t\t.addClass( 'cx-stats-graph cx-stats-cumulative-total' )\n\t\t\t\t\t\t.append( this.$cumulativeGraph ),\n\t\t\t\t\tonVisible: function () {\n\t\t\t\t\t\tself.drawCumulativeGraph( 'count' );\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: mw.msg( 'cx-stats-weekly-trend-tab-title' ),\n\t\t\t\t\tid: 'global-translations-weekly',\n\t\t\t\t\tcontent: $( '<div>' )\n\t\t\t\t\t\t.addClass( 'cx-stats-graph cx-stats-trend-total' )\n\t\t\t\t\t\t.append( this.$translationTrendBarChart ),\n\t\t\t\t\tonVisible: function () {\n\t\t\t\t\t\tself.drawTranslationTrend();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t]\n\t\t);\n\n\t\tthis.$container.append( $( '<h2>' ).text( mw.message(\n\t\t\t'cx-trend-translations-to',\n\t\t\t$.uls.data.getAutonym( mw.config.get( 'wgContentLanguage' ) )\n\t\t).escaped() ) );\n\t\tthis.createTabs(\n\t\t\t'cx-graph-language', [\n\t\t\t\t{\n\t\t\t\t\ttitle: mw.msg( 'cx-stats-cumulative-tab-title' ),\n\t\t\t\t\tid: 'language-translations',\n\t\t\t\t\tcontent: $( '<div>' )\n\t\t\t\t\t\t.addClass( 'cx-stats-graph cx-stats-cumulative-lang' )\n\t\t\t\t\t\t.append( this.$languageCumulativeGraph ),\n\t\t\t\t\tonVisible: function () {\n\t\t\t\t\t\tself.drawLanguageCumulativeGraph( 'count' );\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: mw.msg( 'cx-stats-weekly-trend-tab-title' ),\n\t\t\t\t\tid: 'language-translations-weekly',\n\t\t\t\t\tcontent: $( '<div>' )\n\t\t\t\t\t\t.addClass( 'cx-stats-graph cx-stats-trend-lang' )\n\t\t\t\t\t\t.append( this.$langTranslationTrendBarChart ),\n\t\t\t\t\tonVisible: function () {\n\t\t\t\t\t\tself.drawLangTranslationTrend();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t]\n\t\t);\n\n\t\tthis.$container.append( $( '<h2>' ).text( mw.msg( 'cx-stats-published-translations-title' ) ) );\n\t\tthis.createTabs(\n\t\t\t'cx-stats-published', [\n\t\t\t\t{\n\t\t\t\t\ttitle: mw.msg( 'cx-stats-published-target-source' ),\n\t\t\t\t\tid: 'published-from',\n\t\t\t\t\tcontent: this.drawTranslationsChart( 'to', 'published', 'count' )\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: mw.msg( 'cx-stats-published-source-target' ),\n\t\t\t\t\tid: 'published-to',\n\t\t\t\t\tcontent: this.drawTranslationsChart( 'from', 'published', 'count' )\n\t\t\t\t}\n\t\t\t],\n\t\t\ttrue\n\t\t);\n\n\t\tthis.$container.append( $( '<h2>' ).text( mw.msg( 'cx-stats-draft-translations-title' ) ) );\n\t\tthis.createTabs(\n\t\t\t'cx-stats-draft', [\n\t\t\t\t{\n\t\t\t\t\ttitle: mw.msg( 'cx-stats-draft-target-source' ),\n\t\t\t\t\tid: 'drafted-from',\n\t\t\t\t\tcontent: this.drawTranslationsChart( 'to', 'draft', 'count' )\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: mw.msg( 'cx-stats-draft-source-target' ),\n\t\t\t\t\tid: 'drafted-to',\n\t\t\t\t\tcontent: this.drawTranslationsChart( 'from', 'draft', 'count' )\n\t\t\t\t}\n\t\t\t],\n\t\t\ttrue\n\t\t);\n\n\t\tthis.$container.append( $( '<h2>' ).text( mw.msg( 'cx-stats-published-translators-title' ) ) );\n\t\tthis.createTabs(\n\t\t\t'cx-stats-translators', [\n\t\t\t\t{\n\t\t\t\t\ttitle: mw.msg( 'cx-stats-published-target-source' ),\n\t\t\t\t\tid: 'translators-from',\n\t\t\t\t\tcontent: this.drawTranslationsChart( 'to', 'published', 'translators' )\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: mw.msg( 'cx-stats-published-source-target' ),\n\t\t\t\t\tid: 'translators-to',\n\t\t\t\t\tcontent: this.drawTranslationsChart( 'from', 'published', 'translators' )\n\t\t\t\t}\n\t\t\t],\n\t\t\ttrue\n\t\t);\n\t};\n\n\t/**\n\t * Create a tabbed container for holding related stats.\n\t *\n\t * @param {string} tabGroupId Tab group id\n\t * @param {Object[]} items\n\t * @param {boolean} expandable\n\t */\n\tCXStats.prototype.createTabs = function ( tabGroupId, items, expandable ) {\n\t\tvar $tabContainer, i, $tabs, $expand, tabToShow = 0;\n\n\t\t$tabContainer = $( '<div>' ).addClass( 'cx-stats-tabs-container' );\n\t\t$tabs = $( '<ul>' ).addClass( 'cx-stats-tabs' );\n\t\t$tabContainer.append( $tabs );\n\t\tthis.$container.append( $tabContainer );\n\t\tfor ( i = 0; i < items.length; i++ ) {\n\t\t\titems[ i ].$tab = $( '<li>' )\n\t\t\t\t.addClass( 'cx-stats-tabs-tabtitle' )\n\t\t\t\t.attr( 'about', tabGroupId + 'tab-' + i )\n\t\t\t\t.attr( 'data-itemid', i )\n\t\t\t\t.attr( 'id', items[ i ].id )\n\t\t\t\t.text( items[ i ].title );\n\t\t\titems[ i ].$content = items[ i ].content\n\t\t\t\t.attr( 'id', tabGroupId + 'tab-' + i )\n\t\t\t\t.addClass( 'cx-stats-tabs-tab-content cx-stats-tabs-collapsed' );\n\n\t\t\t$tabs.append( items[ i ].$tab );\n\t\t\t$tabContainer.append( items[ i ].$content );\n\n\t\t\tif ( location.hash === '#' + items[ i ].id ) {\n\t\t\t\ttabToShow = i;\n\t\t\t\t$( 'html, body' ).animate( {\n\t\t\t\t\tscrollTop: items[ i ].$tab.offset().top\n\t\t\t\t}, 500 );\n\t\t\t}\n\t\t}\n\n\t\titems[ tabToShow ].$tab.addClass( 'cx-stats-tabs-current' );\n\t\titems[ tabToShow ].$content.addClass( 'cx-stats-tabs-current' );\n\t\tif ( items[ tabToShow ].onVisible ) {\n\t\t\titems[ tabToShow ].onVisible.apply( this );\n\t\t\titems[ tabToShow ].onVisible = null;\n\t\t}\n\n\t\t// Click handler for tabs\n\t\t$tabs.find( 'li' ).on( 'click', function () {\n\t\t\tvar onVisible,\n\t\t\t\t$this = $( this ),\n\t\t\t\ttabId = $( this ).attr( 'about' ),\n\t\t\t\titemId = $this.data( 'itemid' );\n\n\t\t\t$tabs.find( 'li' ).removeClass( 'cx-stats-tabs-current' );\n\t\t\t$tabContainer.find( '.cx-stats-tabs-tab-content' )\n\t\t\t\t.removeClass( 'cx-stats-tabs-current' );\n\t\t\t$( this ).addClass( 'cx-stats-tabs-current' );\n\t\t\t$( '#' + tabId ).addClass( 'cx-stats-tabs-current' );\n\n\t\t\tonVisible = items[ itemId ].onVisible;\n\t\t\tif ( onVisible ) {\n\t\t\t\tonVisible.apply( this );\n\t\t\t\titems[ itemId ].onVisible = null;\n\t\t\t}\n\t\t} );\n\t\tif ( expandable ) {\n\t\t\t$expand = $( '<a>' )\n\t\t\t\t.addClass( 'cx-stats-tabs-toggle-all' )\n\t\t\t\t.text( mw.msg( 'cx-stats-tabs-expand' ) )\n\t\t\t\t.on( 'click', function () {\n\t\t\t\t\t$tabContainer\n\t\t\t\t\t\t.find( '.cx-stats-tabs-tab-content' )\n\t\t\t\t\t\t.removeClass( 'cx-stats-tabs-collapsed' );\n\t\t\t\t\t$( this ).remove();\n\t\t\t\t} );\n\t\t\t$tabContainer.append( $expand );\n\t\t}\n\t};\n\n\t/**\n\t * Sorts in descending order\n\t *\n\t * @param {Object} a\n\t * @param {Object} b\n\t * @return {number}\n\t */\n\tfunction sortByCount( a, b ) {\n\t\treturn b.count - a.count;\n\t}\n\n\t/**\n\t * Sorts in descending order\n\t *\n\t * @param {Object} a\n\t * @param {Object} b\n\t * @return {number}\n\t */\n\tfunction sortByTranslators( a, b ) {\n\t\treturn b.translators - a.translators;\n\t}\n\n\tCXStats.prototype.drawTranslationsChart = function ( direction, status, property ) {\n\t\tvar $chart, $bar, translations, $translations, model, i, j, $rows = [],\n\t\t\t$callout,\n\t\t\t$row, width, max = 0,\n\t\t\t$tail, tailWidth = 0,\n\t\t\ttail, langCode,\n\t\t\t$langCode, $autonym, $total, $rowLabelContainer,\n\t\t\tfmt = mw.language.convertNumber;\n\n\t\t$chart = $( '<div>' ).addClass( 'cx-stats-chart' );\n\n\t\tmodel = direction === 'to' ?\n\t\t\tthis.targetSourceModel[ status ].sort(\n\t\t\t\tproperty === 'count' ? sortByCount : sortByTranslators\n\t\t\t) :\n\t\t\tthis.sourceTargetModel[ status ].sort(\n\t\t\t\tproperty === 'count' ? sortByCount : sortByTranslators\n\t\t\t);\n\n\t\tfor ( i = 0; i < model.length; i++ ) {\n\t\t\t$row = $( '<div>' ).addClass( 'cx-stats-chart__row' );\n\n\t\t\t$translations = $( '<span>' ).addClass( 'cx-stats-chart__bars' );\n\t\t\ttranslations = model[ i ].translations.sort(\n\t\t\t\tproperty === 'count' ? sortByCount : sortByTranslators\n\t\t\t);\n\n\t\t\ttail = false;\n\t\t\ttailWidth = 0;\n\t\t\tmax = max || model[ 0 ][ property ];\n\n\t\t\tif (\n\t\t\t\tmax / ( Math.ceil( model[ i ][ property ] / 100 ) * 100 ) >= 10 &&\n\t\t\t\tmax >= 1000\n\t\t\t) {\n\t\t\t\tmax = Math.ceil( model[ i ][ property ] / 100 ) * 100;\n\t\t\t\t$rows.push( $( '<div>' )\n\t\t\t\t\t.addClass( 'cx-stats-chart__row separator' )\n\t\t\t\t\t.text( mw.msg( 'cx-stats-grouping-title', fmt( max ) ) ) );\n\t\t\t}\n\n\t\t\t$callout = $( '<table>' ).addClass( 'cx-stats-chart__callout' );\n\t\t\tfor ( j = 0; j < translations.length; j++ ) {\n\t\t\t\twidth = ( translations[ j ][ property ] / max ) * 100;\n\t\t\t\tlangCode = translations[ j ][ ( direction === 'to' ? 'sourceLanguage' : 'targetLanguage' ) ];\n\n\t\t\t\tif ( width > 2 || j === 0 ) {\n\t\t\t\t\t// languages with more than 2% are represented in chart.\n\t\t\t\t\t$bar = $( '<span>' )\n\t\t\t\t\t\t.addClass( 'cx-stats-chart__bar' )\n\t\t\t\t\t\t.prop( {\n\t\t\t\t\t\t\tlang: 'en',\n\t\t\t\t\t\t\tdir: 'ltr'\n\t\t\t\t\t\t} )\n\t\t\t\t\t\t.css( 'width', width + '%' )\n\t\t\t\t\t\t.text( langCode );\n\n\t\t\t\t\t$translations.append( $bar );\n\t\t\t\t} else {\n\t\t\t\t\ttail = true;\n\t\t\t\t\ttailWidth += width;\n\t\t\t\t}\n\n\t\t\t\t$callout.append( $( '<tr>' ).append(\n\t\t\t\t\t$( '<td>' )\n\t\t\t\t\t\t.addClass( 'cx-stats-chart__callout-count' )\n\t\t\t\t\t\t.text( fmt( translations[ j ][ property ] ) ),\n\t\t\t\t\t$( '<td>' )\n\t\t\t\t\t\t.addClass( 'cx-stats-chart__callout-lang' )\n\t\t\t\t\t\t.prop( {\n\t\t\t\t\t\t\tlang: langCode,\n\t\t\t\t\t\t\tdir: $.uls.data.getDir( langCode )\n\t\t\t\t\t\t} )\n\t\t\t\t\t\t.text( $.uls.data.getAutonym( langCode ) )\n\t\t\t\t) );\n\t\t\t}\n\n\t\t\tif ( tail ) {\n\t\t\t\t$tail = $( '<span>' )\n\t\t\t\t\t.addClass( 'cx-stats-chart__bar tail' )\n\t\t\t\t\t.text( '…' )\n\t\t\t\t\t.css( 'width', tailWidth + '%' );\n\t\t\t\t$translations.append( $tail );\n\t\t\t}\n\n\t\t\t$translations.find( '.cx-stats-chart__bar' ).last().callout( {\n\t\t\t\ttrigger: 'hover',\n\t\t\t\tclasses: 'cx-stats-chart__callout-container',\n\t\t\t\tdirection: $.fn.callout.autoDirection( '0' ),\n\t\t\t\tcontent: $callout\n\t\t\t} );\n\n\t\t\t$langCode = $( '<span>' )\n\t\t\t\t.addClass( 'cx-stats-chart__langcode' )\n\t\t\t\t// Always Latin (like English).\n\t\t\t\t// Make sure it's aligned correctly on all screen sizes.\n\t\t\t\t.prop( {\n\t\t\t\t\tlang: 'en',\n\t\t\t\t\tdir: 'ltr'\n\t\t\t\t} )\n\t\t\t\t.text( model[ i ].language );\n\n\t\t\t$autonym = $( '<span>' )\n\t\t\t\t.addClass( 'cx-stats-chart__autonym' )\n\t\t\t\t.prop( {\n\t\t\t\t\tlang: model[ i ].language,\n\t\t\t\t\tdir: $.uls.data.getDir( model[ i ].language )\n\t\t\t\t} )\n\t\t\t\t.text( $.uls.data.getAutonym( model[ i ].language ) );\n\n\t\t\t$total = $( '<span>' )\n\t\t\t\t.addClass( 'cx-stats-chart__total' )\n\t\t\t\t.text( fmt( model[ i ][ property ] ) );\n\n\t\t\tif ( direction === 'to' ) {\n\t\t\t\t$total = $( '<a>' )\n\t\t\t\t\t.addClass( 'cx-stats-chart__total' )\n\t\t\t\t\t.prop( 'href', mw.cx.siteMapper.getPageUrl(\n\t\t\t\t\t\tmodel[ i ].language, 'Special:NewPages', {\n\t\t\t\t\t\t\ttagfilter: 'contenttranslation'\n\t\t\t\t\t\t}\n\t\t\t\t\t) )\n\t\t\t\t\t.text( fmt( model[ i ][ property ] ) );\n\t\t\t} else {\n\t\t\t\t$total = $( '<span>' )\n\t\t\t\t\t.addClass( 'cx-stats-chart__total' )\n\t\t\t\t\t.text( fmt( model[ i ][ property ] ) );\n\t\t\t}\n\n\t\t\t$rowLabelContainer = $( '<span>' )\n\t\t\t\t.addClass( 'cx-stats-chart__row-label-container' )\n\t\t\t\t.append( $langCode, $autonym, $total );\n\n\t\t\t$row.append( $rowLabelContainer, $translations );\n\n\t\t\t$rows.push( $row );\n\t\t}\n\n\t\t$chart.append( $rows );\n\n\t\treturn $chart;\n\t};\n\n\t/**\n\t * Get the Content Translation stats.\n\t *\n\t * @return {jQuery.Promise}\n\t */\n\tCXStats.prototype.getCXStats = function () {\n\t\tvar api = new mw.Api();\n\n\t\treturn api.get( {\n\t\t\taction: 'query',\n\t\t\tlist: 'contenttranslationstats'\n\t\t} );\n\t};\n\n\t/**\n\t * Get the Content Translation trend for the given target language.\n\t * Fetch the number of translations to the given language.\n\t *\n\t * @param {string} targetLanguage Target language code\n\t * @return {jQuery.Promise}\n\t */\n\tCXStats.prototype.getCXTrends = function ( targetLanguage ) {\n\t\treturn ( new mw.Api() ).get( {\n\t\t\taction: 'query',\n\t\t\tlist: 'contenttranslationlangtrend',\n\t\t\ttarget: targetLanguage\n\t\t} ).then( function ( response ) {\n\t\t\treturn response.query.contenttranslationlangtrend;\n\t\t} );\n\t};\n\n\tCXStats.prototype.drawCumulativeGraph = function ( type ) {\n\t\tvar data, ctx;\n\n\t\tctx = this.$cumulativeGraph[ 0 ].getContext( '2d' );\n\n\t\tdata = {\n\t\t\tlabels: this.totalTranslationTrend.map( function ( data ) {\n\t\t\t\treturn data.date;\n\t\t\t} ),\n\t\t\tdatasets: [\n\t\t\t\t{\n\t\t\t\t\tlabel: mw.msg( 'cx-stats-published-translations-label' ),\n\t\t\t\t\tfill: false,\n\t\t\t\t\tborderColor: '#36c',\n\t\t\t\t\tpointBorderColor: '#36c',\n\t\t\t\t\tpointBackgroundColor: '#36c',\n\t\t\t\t\tpointHoverBackgroundColor: '#FFFFFF',\n\t\t\t\t\tpointHoverBorderColor: '#36c',\n\t\t\t\t\tdata: this.totalTranslationTrend.map( function ( data ) {\n\t\t\t\t\t\treturn data[ type ];\n\t\t\t\t\t} )\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: mw.msg( 'cx-stats-draft-translations-label' ),\n\t\t\t\t\tfill: false,\n\t\t\t\t\tborderColor: '#72777d',\n\t\t\t\t\tpointBorderColor: '#72777d',\n\t\t\t\t\tpointBackgroundColor: '#72777d',\n\t\t\t\t\tpointHoverBackgroundColor: '#FFFFFF',\n\t\t\t\t\tpointHoverBorderColor: '#72777d',\n\t\t\t\t\tdata: this.totalDraftTrend.map( function ( data ) {\n\t\t\t\t\t\treturn data[ type ];\n\t\t\t\t\t} )\n\t\t\t\t}\n\t\t\t]\n\t\t};\n\n\t\t// eslint-disable-next-line no-new\n\t\tnew Chart( ctx, {\n\t\t\ttype: 'line',\n\t\t\tdata: data,\n\t\t\toptions: this.chartOptions\n\t\t} );\n\t};\n\n\tCXStats.prototype.drawLanguageCumulativeGraph = function ( type ) {\n\t\tvar data, ctx;\n\n\t\tctx = this.$languageCumulativeGraph[ 0 ].getContext( '2d' );\n\n\t\tdata = {\n\t\t\tlabels: this.languageTranslationTrend.map( function ( data ) {\n\t\t\t\treturn data.date;\n\t\t\t} ),\n\t\t\tdatasets: [\n\t\t\t\t{\n\t\t\t\t\tlabel: mw.msg( 'cx-stats-published-translations-label' ),\n\t\t\t\t\tfill: false,\n\t\t\t\t\tborderColor: '#36c',\n\t\t\t\t\tpointBorderColor: '#36c',\n\t\t\t\t\tpointBackgroundColor: '#36c',\n\t\t\t\t\tpointHoverBackgroundColor: '#FFFFFF',\n\t\t\t\t\tpointHoverBorderColor: '#36c',\n\t\t\t\t\tdata: this.languageTranslationTrend.map( function ( data ) {\n\t\t\t\t\t\treturn data[ type ];\n\t\t\t\t\t} )\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: mw.msg( 'cx-stats-draft-translations-label' ),\n\t\t\t\t\tfill: false,\n\t\t\t\t\tborderColor: '#72777d',\n\t\t\t\t\tpointBorderColor: '#72777d',\n\t\t\t\t\tpointBackgroundColor: '#72777d',\n\t\t\t\t\tpointHoverBackgroundColor: '#FFFFFF',\n\t\t\t\t\tpointHoverBorderColor: '#72777d',\n\t\t\t\t\tdata: this.languageDraftTrend.map( function ( data ) {\n\t\t\t\t\t\treturn data[ type ];\n\t\t\t\t\t} )\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: mw.msg( 'cx-trend-deletions' ),\n\t\t\t\t\tfill: false,\n\t\t\t\t\tborderColor: '#FF0000',\n\t\t\t\t\tpointBorderColor: '#FF0000',\n\t\t\t\t\tpointBackgroundColor: '#FF0000',\n\t\t\t\t\tpointHoverBackgroundColor: '#FFFFFF',\n\t\t\t\t\tpointHoverBorderColor: '#FF0000',\n\t\t\t\t\tdata: this.languageDeletionTrend.map( function ( data ) {\n\t\t\t\t\t\treturn data[ type ];\n\t\t\t\t\t} )\n\t\t\t\t}\n\t\t\t]\n\t\t};\n\n\t\t// eslint-disable-next-line no-new\n\t\tnew Chart( ctx, {\n\t\t\ttype: 'line',\n\t\t\tdata: data,\n\t\t\toptions: this.chartOptions\n\t\t} );\n\t};\n\n\tCXStats.prototype.drawTranslationTrend = function () {\n\t\tvar data, ctx, type = 'delta';\n\n\t\tctx = this.$translationTrendBarChart[ 0 ].getContext( '2d' );\n\t\tdata = {\n\t\t\tlabels: this.totalTranslationTrend.map( function ( data ) {\n\t\t\t\treturn data.date;\n\t\t\t} ),\n\t\t\tdatasets: [\n\t\t\t\t{\n\t\t\t\t\tlabel: mw.msg( 'cx-stats-published-translations-label' ),\n\t\t\t\t\tborderColor: '#36c',\n\t\t\t\t\tbackgroundColor: '#36c',\n\t\t\t\t\tborderWidth: 1,\n\t\t\t\t\tdata: this.totalTranslationTrend.map( function ( data ) {\n\t\t\t\t\t\treturn data[ type ];\n\t\t\t\t\t} )\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: mw.msg( 'cx-stats-new-draft-translations-label' ),\n\t\t\t\t\tborderColor: '#72777d',\n\t\t\t\t\tbackgroundColor: '#72777d',\n\t\t\t\t\tborderWidth: 1,\n\t\t\t\t\tdata: this.totalDraftTrend.map( function ( data ) {\n\t\t\t\t\t\treturn data[ type ];\n\t\t\t\t\t} )\n\t\t\t\t}\n\t\t\t]\n\t\t};\n\n\t\t// eslint-disable-next-line no-new\n\t\tnew Chart( ctx, {\n\t\t\ttype: 'bar',\n\t\t\tdata: data,\n\t\t\toptions: this.chartOptions\n\t\t} );\n\t};\n\n\tCXStats.prototype.drawLangTranslationTrend = function () {\n\t\tvar ctx, data,\n\t\t\ttype = 'delta';\n\n\t\tctx = this.$langTranslationTrendBarChart[ 0 ].getContext( '2d' );\n\t\tdata = {\n\t\t\tlabels: this.languageTranslationTrend.map( function ( data ) {\n\t\t\t\treturn data.date;\n\t\t\t} ),\n\t\t\tdatasets: [\n\t\t\t\t{\n\t\t\t\t\tlabel: mw.msg( 'cx-stats-published-translations-label' ),\n\t\t\t\t\tborderColor: '#36c',\n\t\t\t\t\tbackgroundColor: '#36c',\n\t\t\t\t\tborderWidth: 1,\n\t\t\t\t\tdata: this.languageTranslationTrend.map( function ( data ) {\n\t\t\t\t\t\treturn data[ type ];\n\t\t\t\t\t} )\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: mw.msg( 'cx-stats-new-draft-translations-label' ),\n\t\t\t\t\tborderColor: '#72777d',\n\t\t\t\t\tbackgroundColor: '#72777d',\n\t\t\t\t\tborderWidth: 1,\n\t\t\t\t\tdata: this.languageDraftTrend.map( function ( data ) {\n\t\t\t\t\t\treturn data[ type ];\n\t\t\t\t\t} )\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: mw.msg( 'cx-trend-deletions' ),\n\t\t\t\t\tborderColor: '#FF0000',\n\t\t\t\t\tbackgroundColor: '#FF0000',\n\t\t\t\t\tborderWidth: 1,\n\t\t\t\t\tdata: this.languageDeletionTrend.map( function ( data ) {\n\t\t\t\t\t\treturn data[ type ];\n\t\t\t\t\t} )\n\t\t\t\t}\n\t\t\t]\n\t\t};\n\n\t\t// eslint-disable-next-line no-new\n\t\tnew Chart( ctx, {\n\t\t\ttype: 'bar',\n\t\t\tdata: data,\n\t\t\toptions: this.chartOptions\n\t\t} );\n\t};\n\n\tCXStats.prototype.transformJsonToModel = function ( records ) {\n\t\tvar i, record, language, status, count, translators,\n\t\t\tsourceLanguage, targetLanguage,\n\t\t\ttempModel,\n\t\t\thasOwn = Object.prototype.hasOwnProperty;\n\n\t\tthis.sourceTargetModel.draft = {};\n\t\tthis.targetSourceModel.draft = {};\n\t\tthis.sourceTargetModel.published = {};\n\t\tthis.targetSourceModel.published = {};\n\n\t\tfor ( i = 0; i < records.pages.length; i++ ) {\n\t\t\trecord = records.pages[ i ];\n\t\t\tstatus = record.status;\n\t\t\tsourceLanguage = record.sourceLanguage;\n\t\t\ttargetLanguage = record.targetLanguage;\n\t\t\tthis.sourceTargetModel[ status ][ sourceLanguage ] = this.sourceTargetModel[ status ][ sourceLanguage ] || [];\n\t\t\tthis.targetSourceModel[ status ][ targetLanguage ] = this.targetSourceModel[ status ][ targetLanguage ] || [];\n\t\t\tthis.sourceTargetModel[ status ][ sourceLanguage ].push( record );\n\t\t\tthis.targetSourceModel[ status ][ targetLanguage ].push( record );\n\t\t}\n\n\t\tfor ( status in this.sourceTargetModel ) {\n\t\t\ttempModel = this.sourceTargetModel[ status ];\n\t\t\tthis.sourceTargetModel[ status ] = [];\n\t\t\tfor ( language in tempModel ) {\n\t\t\t\tif ( hasOwn.call( tempModel, language ) ) {\n\t\t\t\t\tfor ( count = 0, translators = 0, i = 0; i < tempModel[ language ].length; i++ ) {\n\t\t\t\t\t\tcount += +tempModel[ language ][ i ].count;\n\t\t\t\t\t\ttranslators += +tempModel[ language ][ i ].translators;\n\t\t\t\t\t}\n\t\t\t\t\tthis.sourceTargetModel[ status ].push( {\n\t\t\t\t\t\tlanguage: language,\n\t\t\t\t\t\ttranslations: tempModel[ language ],\n\t\t\t\t\t\tcount: count,\n\t\t\t\t\t\ttranslators: translators\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttempModel = this.targetSourceModel[ status ];\n\t\t\tthis.targetSourceModel[ status ] = [];\n\t\t\tfor ( language in tempModel ) {\n\t\t\t\tif ( hasOwn.call( tempModel, language ) ) {\n\t\t\t\t\tfor ( count = 0, translators = 0, i = 0; i < tempModel[ language ].length; i++ ) {\n\t\t\t\t\t\tcount += +tempModel[ language ][ i ].count;\n\t\t\t\t\t\ttranslators += +tempModel[ language ][ i ].translators;\n\t\t\t\t\t}\n\t\t\t\t\tthis.targetSourceModel[ status ].push( {\n\t\t\t\t\t\tlanguage: language,\n\t\t\t\t\t\ttranslations: tempModel[ language ],\n\t\t\t\t\t\tcount: count,\n\t\t\t\t\t\ttranslators: translators\n\t\t\t\t\t} );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\t$( function () {\n\t\tvar cxLink, cxstats, header, $header, $container;\n\n\t\t$header = $( '<div>' ).addClass( 'cx-widget__header' );\n\t\t$container = $( '<div>' ).addClass( 'cx-stats-container' );\n\n\t\t// Set the global siteMapper for code which we cannot inject it\n\t\tmw.cx.siteMapper = new mw.cx.SiteMapper();\n\t\t$( 'body' ).append(\n\t\t\t$( '<div>' ).addClass( 'cx-widget' ).append(\n\t\t\t\t$header, $container\n\t\t\t)\n\t\t);\n\t\theader = new mw.cx.ui.Header( {\n\t\t\tsiteMapper: this.siteMapper,\n\t\t\ttitleText: mw.msg( 'cx-stats-title' )\n\t\t} );\n\t\t$header.append( header.$element );\n\t\tcxstats = new CXStats( $container, {\n\t\t\tsiteMapper: new mw.cx.SiteMapper()\n\t\t} );\n\t\tcxstats.init();\n\n\t\tif ( !mw.user.isAnon() &&\n\t\t\tmw.config.get( 'wgContentTranslationCampaigns' ).cxstats &&\n\t\t\tmw.user.options.get( 'cx' ) !== '1'\n\t\t) {\n\t\t\tcxLink = mw.util.getUrl( 'Special:ContentTranslation', {\n\t\t\t\tcampaign: 'cxstats',\n\t\t\t\tto: mw.config.get( 'wgContentLanguage' )\n\t\t\t} );\n\n\t\t\tmw.hook( 'mw.cx.error' ).fire( mw.message( 'cx-stats-try-contenttranslation', cxLink ) );\n\t\t}\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.categories.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.dictionary.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.formatter.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.gallery.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.images.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.instructions.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.link.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.linter.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.manager.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.mt.card.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.mt.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.mtabuse.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.poem.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.reference.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.template.card.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.template.editor.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.template.js","messages":[{"ruleId":"no-shadow","severity":1,"message":"'title' is already declared in the upper scope on line 47 column 7.","line":107,"column":56,"nodeType":"Identifier","messageId":"noShadow","endLine":107,"endColumn":61},{"ruleId":"jsdoc/require-returns","severity":1,"message":"Found more than one @return declaration.","line":631,"column":2,"nodeType":"Block","endLine":638,"endColumn":5},{"ruleId":"jsdoc/require-returns-check","severity":1,"message":"Found more than one @return declaration.","line":631,"column":2,"nodeType":"Block","endLine":638,"endColumn":5}],"errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * Content Translation template adaptation tool\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\tvar cachedTemplateNamespaceRequests = {},\n\t\tcachedTemplateRequests = {},\n\t\tcachedTemplateDataAPIRequests = {};\n\n\t/**\n\t * A Template in the translation context.\n\t * Can be source or target template.\n\t *\n\t * It determines the best course of action and implements it. Some possible\n\t * actions are to make the template non-editable, remove it or expand it as\n\t * inline content (destruction).\n\t *\n\t * @class\n\t * @param {jQuery} $template The template element\n\t * @param {Object} [options] options\n\t * @cfg {string} [language] The corresponding language\n\t * @cfg {Object} [siteMapper] siteMapper\n\t */\n\tfunction Template( $template, options ) {\n\t\tthis.$template = $template;\n\t\tthis.options = options || {};\n\t\tthis.title = null;\n\t\tthis.language = options.language;\n\t\tthis.siteMapper = options.siteMapper;\n\t\tthis.params = [];\n\t\tthis.namespace = null;\n\t\tthis.initPromise = this.init();\n\t}\n\n\tTemplate.static = {};\n\n\t/**\n\t * Get ready with all required information about the template.\n\t *\n\t * @return {jQuery.Promise}\n\t */\n\tTemplate.prototype.init = function () {\n\t\tvar title, sourceDef, mwData, sourceTitle,\n\t\t\tself = this;\n\n\t\t// Templates are not editable as a contenteditable. We provide an editor.\n\t\tthis.$template.attr( 'contenteditable', false );\n\n\t\tif ( this.initPromise ) {\n\t\t\treturn this.initPromise;\n\t\t}\n\n\t\t// Get the data-mw from the template itself.\n\t\tmwData = this.$template.data( 'mw' );\n\t\t// And get it from the source template.\n\t\tsourceDef = Template.static.getTemplateDef( this.$template ).data( 'mw' );\n\n\t\tif ( !mwData ) {\n\t\t\tmwData = sourceDef;\n\t\t\tif ( !mwData ) {\n\t\t\t\tmw.log( '[CX] Could not find data-mw for Template#' + this.$template.attr( 'id' ) );\n\t\t\t\treturn $.Deferred().reject().promise();\n\t\t\t}\n\t\t}\n\n\t\tif ( !mwData.parts ) {\n\t\t\tmw.log( '[CX] data-mw for Template#' + this.$template.attr( 'id' ) + ' has no parts.' );\n\t\t\treturn $.Deferred().reject().promise();\n\t\t}\n\n\t\tif ( mwData.parts.length > 1 ) {\n\t\t\tmw.log( '[CX] Skipping multipart template for now for Template#' + this.$template.attr( 'id' ) );\n\t\t\treturn $.Deferred().reject().promise();\n\t\t}\n\n\t\tif ( this.isSourceTemplate() ) {\n\t\t\ttitle = mwData.parts[ 0 ].template.target.wt;\n\t\t\t// Sanitize and normalize the name. It can contain new lines, spaces etc.\n\t\t\ttitle = mw.Title.newFromUserInput( title, 0 );\n\t\t\tif ( title ) {\n\t\t\t\ttitle = title.getPrefixedText();\n\t\t\t} else {\n\t\t\t\tmw.log( '[CX] Failed to parse the template title as valid title for ' +\n\t\t\t\t\tmwData.parts[ 0 ].template.target.wt\n\t\t\t\t);\n\t\t\t\treturn $.Deferred().reject().promise();\n\t\t\t}\n\t\t\tthis.initPromise = $.Deferred().resolve( title ).promise();\n\t\t} else {\n\t\t\tsourceTitle = sourceDef.parts[ 0 ].template.target.wt;\n\t\t\tsourceTitle = mw.Title.newFromUserInput( sourceTitle, 0 );\n\t\t\tif ( sourceTitle ) {\n\t\t\t\ttitle = sourceTitle.getPrefixedText();\n\t\t\t} else {\n\t\t\t\tmw.log( '[CX] Failed to parse the template title as valid title for ' +\n\t\t\t\t\tsourceDef.parts[ 0 ].template.target.wt\n\t\t\t\t);\n\t\t\t\treturn $.Deferred().reject().promise();\n\t\t\t}\n\t\t\tthis.initPromise = this.getTargetTitle( sourceTitle );\n\t\t}\n\n\t\tthis.initPromise = this.initPromise.then( function ( title ) {\n\t\t\t// This is with namespace prefix.\n\t\t\tself.title = title;\n\t\t\tmw.log( '[CX] Initializing for template title ' + self.title );\n\t\t\treturn self.getNamespaceTranslation( self.language ).then( function ( namespace ) {\n\t\t\t\tself.namespace = namespace;\n\t\t\t\t// Remove namespace\n\t\t\t\tself.title = self.title.replace(\n\t\t\t\t\tnew RegExp( '^' + mw.util.escapeRegExp( self.namespace + ':' ) ),\n\t\t\t\t\t''\n\t\t\t\t);\n\t\t\t} );\n\t\t}, function () {\n\t\t\tmw.log( '[CX] Target template does not exist for ' + sourceTitle );\n\t\t\treturn $.Deferred().reject().promise();\n\t\t} );\n\n\t\tthis.initPromise = this.initPromise.then( function () {\n\t\t\treturn self.getTemplateData().then( function ( templateData ) {\n\t\t\t\tvar params = mwData.parts[ 0 ].template.params;\n\n\t\t\t\t// Create a deep copy of this since we are going to\n\t\t\t\t// add additional properties and dont want them in the cached copy.\n\t\t\t\tself.templateData = $.extend( true, {}, templateData );\n\t\t\t\tself.params = self.templateData.params;\n\n\t\t\t\tObject.keys( self.params ).forEach( function ( key ) {\n\t\t\t\t\tvar aliases, normalizedTemplatedateKey = key.trim().toLowerCase();\n\t\t\t\t\taliases = self.params[ key ].aliases || [];\n\t\t\t\t\t// It is important to trim here since the params in data-mw\n\t\t\t\t\t// has trailing whitespaces.\n\t\t\t\t\tObject.keys( params ).forEach( function ( paramkey ) {\n\t\t\t\t\t\tvar i, normalizedDataMWKey, normalizedAlias;\n\t\t\t\t\t\tnormalizedDataMWKey = paramkey.trim().toLowerCase();\n\t\t\t\t\t\tif ( normalizedDataMWKey === normalizedTemplatedateKey ) {\n\t\t\t\t\t\t\tself.params[ key ].wt = params[ paramkey ].wt;\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor ( i = 0; i < aliases.length; i++ ) {\n\t\t\t\t\t\t\tnormalizedAlias = aliases[ i ].trim().toLowerCase();\n\t\t\t\t\t\t\tif ( normalizedDataMWKey === normalizedAlias ) {\n\t\t\t\t\t\t\t\tself.params[ key ].wt = params[ paramkey ].wt;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\t\t\t\t} );\n\t\t\t} );\n\t\t\t// TODO: Yeah. to refactor.\n\t\t} );\n\n\t\tthis.listen();\n\n\t\treturn this.initPromise;\n\t};\n\n\t/**\n\t * Get the template fragment having the template Definition.\n\t *\n\t * @param {jQuery} $template\n\t * @return {jQuery}\n\t */\n\tTemplate.static.getTemplateDef = function ( $template ) {\n\t\tvar aboutAttr,\n\t\t\t$sourceTemplate = $( [] );\n\n\t\tif ( !$template ) {\n\t\t\treturn $sourceTemplate;\n\t\t}\n\n\t\taboutAttr = $template.attr( 'about' ) ||\n\t\t\tmw.cx.getSourceSection( $template.data( 'source' ) ).attr( 'about' );\n\n\t\t//  Template definition is usually at source template at source column\n\t\t$( '[about=\"' + aboutAttr + '\"]' ).each( function ( index, fragment ) {\n\t\t\tvar $fragment = $( fragment );\n\n\t\t\tif (\n\t\t\t\t// Not all fragments are mw:Transclusion\n\t\t\t\t// See https://phabricator.wikimedia.org/T97220\n\t\t\t\t$fragment.is( '[typeof*=\"mw:Transclusion\"]' ) &&\n\t\t\t\t$fragment.attr( 'data-mw' )\n\t\t\t) {\n\t\t\t\t$sourceTemplate = $fragment;\n\t\t\t\t// Exit.\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} );\n\n\t\treturn $sourceTemplate;\n\t};\n\n\t/**\n\t * Event handlers\n\t */\n\tTemplate.prototype.listen = function () {\n\t\tthis.$template.on( 'click', this.onClick.bind( this ) );\n\t};\n\n\t/**\n\t * Get the data-mw in JSON format\n\t *\n\t * @return {string} data-mw in JSON format\n\t */\n\tTemplate.prototype.toJSON = function () {\n\t\treturn JSON.stringify( this.getData() );\n\t};\n\n\t/**\n\t * Get all associated DOM fragments for the given template\n\t *\n\t * @return {jQuery} Template fragments\n\t */\n\tTemplate.prototype.getFragments = function () {\n\t\tvar aboutAttr;\n\n\t\taboutAttr = this.$template.attr( 'about' ) ||\n\t\t\tmw.cx.getSourceSection( this.$template.data( 'source' ) ).attr( 'about' );\n\n\t\treturn this.$template\n\t\t\t.parents( '.cx-column__content' )\n\t\t\t.find( '[about=\"' + aboutAttr + '\"]' );\n\t};\n\n\t/**\n\t * Show this template. If it has fragments, show them too.\n\t */\n\tTemplate.prototype.show = function () {\n\t\tif ( this.options.inline ) {\n\t\t\tthis.$parentSection = this.$parentSection ||\n\t\t\t\tthis.$template.parents( mw.cx.getSectionSelector() );\n\t\t\tthis.$parentSection.show();\n\t\t} else {\n\t\t\tthis.$template.show();\n\t\t\tthis.getFragments( this.$template ).show();\n\t\t}\n\t};\n\n\t/**\n\t * Hide this template. If it has fragments, hide them too.\n\t */\n\tTemplate.prototype.hide = function () {\n\t\tif ( this.options.inline ) {\n\t\t\tthis.$parentSection = this.$parentSection ||\n\t\t\t\tthis.$template.parents( mw.cx.getSectionSelector() );\n\t\t\tthis.$parentSection.hide();\n\t\t} else {\n\t\t\tthis.$template.hide();\n\t\t\tthis.getFragments( this.$template ).hide();\n\t\t}\n\t};\n\n\t/**\n\t * Templates will have fragments and sometimes some of them will be\n\t * invisible elements holding mw-data. We cannot align target sections\n\t * against these 0-height fragments. Restoring is also problematic.\n\t * So we find a 0-height fragments to use for alignment and section\n\t * restore.\n\t *\n\t * @return {jQuery|undefined}\n\t */\n\tTemplate.prototype.getFirstVisibleFragment = function () {\n\t\tvar $fragments, $fragment;\n\n\t\t$fragments = this.getFragments();\n\t\t$fragments.each( function ( index, fragment ) {\n\t\t\t$fragment = $( fragment );\n\n\t\t\t// Find a fragment with visible height\n\t\t\tif ( $fragment.height() > 0 ) {\n\t\t\t\t// break the loop\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} );\n\n\t\treturn $fragment;\n\t};\n\n\t/**\n\t * Get the container to which the editor to append\n\t *\n\t * @return {jQuery}\n\t */\n\tTemplate.prototype.getEditorContainer = function () {\n\t\tvar $container, $visibleFragment;\n\n\t\t$visibleFragment = this.getFirstVisibleFragment() || this.$template;\n\t\tif ( this.options.inline ) {\n\t\t\tthis.$parentSection = this.$parentSection || $visibleFragment.parent( mw.cx.getSectionSelector() );\n\t\t\t$container = this.$parentSection;\n\t\t} else {\n\t\t\t$container = $visibleFragment;\n\t\t}\n\n\t\treturn $container;\n\t};\n\n\t/**\n\t * Get the data-mw\n\t *\n\t * @return {Object}\n\t */\n\tTemplate.prototype.getData = function () {\n\t\tvar mwData;\n\t\t// Get the original data-mw from source template.\n\t\tmwData = Template.static.getTemplateDef( this.$template ).data( 'mw' );\n\t\t// Now update it with fresh values.\n\t\tmwData.parts[ 0 ].template.target.wt = this.title;\n\t\tmwData.parts[ 0 ].template.params = this.params;\n\t\treturn mwData;\n\t};\n\n\t/**\n\t * Template click handler.\n\t *\n\t * @param {Event} event\n\t * @return {boolean}\n\t */\n\tTemplate.prototype.onClick = function ( event ) {\n\t\tevent.stopPropagation();\n\n\t\tif ( this.options.onEdit ) {\n\t\t\t// Template is editable\n\t\t\tif ( this.$template.is( '.cx-highlight' ) ) {\n\t\t\t\tthis.options.onEdit.call( this );\n\t\t\t}\n\n\t\t\t// eslint-disable-next-line no-jquery/no-class-state\n\t\t\tthis.$template.toggleClass( 'cx-highlight' );\n\t\t}\n\n\t\tif ( this.isTargetTemplate() ) {\n\t\t\tmw.hook( 'mw.cx.translation.template.focus' ).fire( this.$template );\n\t\t}\n\n\t\t// Dont bubble this. Will cause section focus events and all.\n\t\treturn false;\n\t};\n\n\t/**\n\t * Check if the given element is a template or a fragment of template\n\t * connected using about attribute.\n\t *\n\t * @param {jQuery} $element The element to check.\n\t * @return {boolean} Whether the element is template or not.\n\t */\n\tTemplate.static.isTemplate = function ( $element ) {\n\t\treturn !!this.getTemplateDef( $element ).length;\n\t};\n\n\t/**\n\t * Whether the template is source template or not.\n\t * TODO: We should just pass this as option to the constructor of this class\n\t * instead of referring a global mw.cx\n\t *\n\t * @return {boolean}\n\t */\n\tTemplate.prototype.isSourceTemplate = function () {\n\t\treturn this.language === mw.cx.sourceLanguage;\n\t};\n\n\t/**\n\t * Whether the template is target template or not.\n\t * TODO: We should just pass this as option to the constructor of this class\n\t * instead of referring a global mw.cx.\n\t *\n\t * @return {boolean}\n\t */\n\tTemplate.prototype.isTargetTemplate = function () {\n\t\treturn this.language === mw.cx.targetLanguage;\n\t};\n\n\t/**\n\t * Get the template data from wiki using TemplateData extension.\n\t * If template data does not exist for the template, we extract the parameters\n\t * using the source code of the template as fallback.\n\t *\n\t * @return {jQuery.Promise}\n\t */\n\tTemplate.prototype.getTemplateData = function () {\n\t\tvar self = this,\n\t\t\tcacheKey;\n\n\t\tcacheKey = this.language + '|' + this.title;\n\t\tif ( cachedTemplateDataAPIRequests[ cacheKey ] ) {\n\t\t\treturn cachedTemplateDataAPIRequests[ cacheKey ];\n\t\t}\n\n\t\tcachedTemplateDataAPIRequests[ cacheKey ] =\n\t\t\tthis.getNamespaceTranslation( this.language ).then( function ( namespace ) {\n\t\t\t\tvar targetName;\n\n\t\t\t\tif ( self.title.indexOf( namespace + ':' ) !== 0 ) {\n\t\t\t\t\ttargetName = namespace + ':' + self.title;\n\t\t\t\t} else {\n\t\t\t\t\ttargetName = self.title;\n\t\t\t\t}\n\n\t\t\t\treturn self.siteMapper.getApi( self.language ).get( {\n\t\t\t\t\taction: 'templatedata',\n\t\t\t\t\ttitles: targetName,\n\t\t\t\t\tredirects: true\n\t\t\t\t} ).then( function ( response ) {\n\t\t\t\t\tvar pageId, templateData;\n\n\t\t\t\t\tpageId = Object.keys( response.pages )[ 0 ];\n\t\t\t\t\ttemplateData = response.pages[ pageId ];\n\n\t\t\t\t\tif ( !templateData ) {\n\t\t\t\t\t\treturn self.getTemplateParamsUsingSource( targetName );\n\t\t\t\t\t}\n\n\t\t\t\t\treturn templateData;\n\t\t\t\t} );\n\t\t\t} );\n\n\t\treturn cachedTemplateDataAPIRequests[ cacheKey ];\n\t};\n\n\t/**\n\t * Retrieve template parameters from the template code.\n\t * Adapted from https://he.wikipedia.org/wiki/MediaWiki:Gadget-TemplateParamWizard.js\n\t *\n\t * @param {string} templateCode Source of the template.\n\t * @return {Object} An associative array of parameters that appear in the template code\n\t */\n\tTemplate.prototype.extractParametersFromTemplateCode = function ( templateCode ) {\n\t\tvar matches,\n\t\t\tparamNames = {},\n\t\t\tparamExtractor = /{{3,}(.*?)[<|}]/mg;\n\n\t\twhile ( ( matches = paramExtractor.exec( templateCode ) ) !== null ) {\n\t\t\tif ( paramNames.indexOf( matches[ 1 ] ) === -1 ) {\n\t\t\t\tparamNames[ matches[ 1 ].trim() ] = {};\n\t\t\t}\n\t\t}\n\n\t\treturn paramNames;\n\t};\n\n\t/**\n\t * Fetch the template source code and extract the template parameters from it.\n\t *\n\t * @param {string} templateName Template name with namespace prefix.\n\t * @return {Object}\n\t */\n\tTemplate.prototype.getTemplateParamsUsingSource = function ( templateName ) {\n\t\tvar self = this;\n\n\t\treturn self.siteMapper.getApi( this.language ).get( {\n\t\t\tformatversion: 2,\n\t\t\taction: 'query',\n\t\t\ttitles: templateName,\n\t\t\tredirects: true,\n\t\t\tprop: 'revisions',\n\t\t\trvprop: 'content'\n\t\t} ).then( function ( resp ) {\n\t\t\tvar page = resp.query.pages[ 0 ], title, pageContent = '';\n\n\t\t\tif ( page.revisions && page.revisions[ 0 ] ) {\n\t\t\t\tpageContent = page.revisions[ 0 ].content;\n\t\t\t\ttitle = page.title;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttitle: title,\n\t\t\t\tparams: self.extractParametersFromTemplateCode( pageContent )\n\t\t\t};\n\t\t} );\n\t};\n\n\t/**\n\t * Get the namespace translation in a wiki.\n\t * Uses the canonical name for lookup.\n\t *\n\t * @return {jQuery.Promise}\n\t */\n\tTemplate.prototype.getNamespaceTranslation = function () {\n\t\tvar request;\n\n\t\tif ( cachedTemplateNamespaceRequests[ this.language ] ) {\n\t\t\treturn cachedTemplateNamespaceRequests[ this.language ];\n\t\t}\n\n\t\trequest = this.siteMapper.getApi( this.language ).get( {\n\t\t\taction: 'query',\n\t\t\tmeta: 'siteinfo',\n\t\t\tsiprop: 'namespaces'\n\t\t} ).then( function ( response ) {\n\t\t\tvar namespaceId, namespaceObj;\n\n\t\t\tfor ( namespaceId in response.query.namespaces ) {\n\t\t\t\tnamespaceObj = response.query.namespaces[ namespaceId ];\n\t\t\t\tif ( namespaceObj.canonical === 'Template' ) {\n\t\t\t\t\treturn namespaceObj[ '*' ];\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\n\t\tcachedTemplateNamespaceRequests[ this.language ] = request;\n\n\t\treturn request;\n\t};\n\n\t/**\n\t * Escape a template parameter. Helper function for #getWikitext.\n\t * See ve.dm.MWTransclusionNode.static.escapeParameter\n\t *\n\t * @static\n\t * @param {string} param Parameter value\n\t * @return {string} Escaped parameter value\n\t */\n\tTemplate.static.escapeParameter = function ( param ) {\n\t\tvar match, needsNowiki,\n\t\t\tinput = param,\n\t\t\toutput = '',\n\t\t\tinNowiki = false,\n\t\t\tbracketStack = 0,\n\t\t\tlinkStack = 0;\n\n\t\twhile ( input.length > 0 ) {\n\t\t\tmatch = input.match( /(?:\\[\\[)|(?:\\]\\])|(?:\\{\\{)|(?:\\}\\})|\\|+|<\\/?nowiki>|<nowiki\\s*\\/>/ );\n\n\t\t\tif ( !match ) {\n\t\t\t\toutput += input;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\toutput += input.slice( 0, match.index );\n\t\t\tinput = input.slice( match.index + match[ 0 ].length );\n\n\t\t\tif ( inNowiki ) {\n\t\t\t\tif ( match[ 0 ] === '</nowiki>' ) {\n\t\t\t\t\tinNowiki = false;\n\t\t\t\t\toutput += match[ 0 ];\n\t\t\t\t} else {\n\t\t\t\t\toutput += match[ 0 ];\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tneedsNowiki = true;\n\t\t\t\tif ( match[ 0 ] === '<nowiki>' ) {\n\t\t\t\t\tinNowiki = true;\n\t\t\t\t\tneedsNowiki = false;\n\t\t\t\t} else if ( match[ 0 ] === '</nowiki>' || match[ 0 ].match( /<nowiki\\s*\\/>/ ) ) {\n\t\t\t\t\tneedsNowiki = false;\n\t\t\t\t} else if ( match[ 0 ].match( /(?:\\[\\[)/ ) ) {\n\t\t\t\t\tlinkStack++;\n\t\t\t\t\tneedsNowiki = false;\n\t\t\t\t} else if ( match[ 0 ].match( /(?:\\]\\])/ ) ) {\n\t\t\t\t\tif ( linkStack > 0 ) {\n\t\t\t\t\t\tlinkStack--;\n\t\t\t\t\t\tneedsNowiki = false;\n\t\t\t\t\t}\n\t\t\t\t} else if ( match[ 0 ].match( /(?:\\{\\{)/ ) ) {\n\t\t\t\t\tbracketStack++;\n\t\t\t\t\tneedsNowiki = false;\n\t\t\t\t} else if ( match[ 0 ].match( /(?:\\}\\})/ ) ) {\n\t\t\t\t\tif ( bracketStack > 0 ) {\n\t\t\t\t\t\tbracketStack--;\n\t\t\t\t\t\tneedsNowiki = false;\n\t\t\t\t\t}\n\t\t\t\t} else if ( match[ 0 ].match( /\\|+/ ) ) {\n\t\t\t\t\tif ( bracketStack > 0 || linkStack > 0 ) {\n\t\t\t\t\t\tneedsNowiki = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ( needsNowiki ) {\n\t\t\t\t\toutput += '<nowiki>' + match[ 0 ] + '</nowiki>';\n\t\t\t\t} else {\n\t\t\t\t\toutput += match[ 0 ];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn output;\n\t};\n\n\t/**\n\t * Get the wikitext for this transclusion.\n\t * See ve.dm.MWTransclusionNode.static.getWikitext\n\t *\n\t * @static\n\t * @return {string} Wikitext like `{{foo|1=bar|baz=quux}}`\n\t */\n\tTemplate.prototype.getWikitext = function () {\n\t\tvar i, len, part, template, param,\n\t\t\tcontent,\n\t\t\twikitext = '';\n\n\t\t// Normalize to multi template format\n\t\tif ( this.params ) {\n\t\t\tcontent = {\n\t\t\t\tparts: [ {\n\t\t\t\t\ttemplate: {\n\t\t\t\t\t\ttarget: {\n\t\t\t\t\t\t\twt: this.title\n\t\t\t\t\t\t},\n\t\t\t\t\t\tparams: this.params\n\t\t\t\t\t}\n\t\t\t\t} ]\n\t\t\t};\n\t\t}\n\n\t\t// Build wikitext from content\n\t\tfor ( i = 0, len = content.parts.length; i < len; i++ ) {\n\t\t\tpart = content.parts[ i ];\n\t\t\tif ( part.template ) {\n\t\t\t\t// Template\n\t\t\t\ttemplate = part.template;\n\t\t\t\twikitext += '{{' + template.target.wt;\n\t\t\t\tfor ( param in template.params ) {\n\t\t\t\t\tif ( template.params[ param ] && template.params[ param ].wt ) {\n\t\t\t\t\t\twikitext += '|' + param + '=' +\n\t\t\t\t\t\t\tmw.cx.Template.static.escapeParameter( template.params[ param ].wt );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twikitext += '}}';\n\t\t\t} else {\n\t\t\t\t// Plain wikitext\n\t\t\t\twikitext += part;\n\t\t\t}\n\t\t}\n\t\treturn wikitext;\n\t};\n\n\t/**\n\t * Fetch the template from target wiki.\n\t *\n\t * @param {string} sourceTitle Source title without namespace prefix.\n\t * @return {jQuery.Promise}\n\t * @return {Function} return.done\n\t * @return {number} return.done.data The page id\n\t */\n\tTemplate.prototype.getTargetTitle = function ( sourceTitle ) {\n\t\tvar api, request, cacheKey;\n\n\t\tcacheKey = mw.cx.sourceLanguage + '|' + mw.cx.targetLanguage + '|' + sourceTitle;\n\t\tif ( cachedTemplateRequests[ cacheKey ] ) {\n\t\t\treturn cachedTemplateRequests[ cacheKey ];\n\t\t}\n\n\t\t// TODO: Avoid direct access to globals\n\t\tapi = this.siteMapper.getApi( mw.cx.sourceLanguage );\n\n\t\t// Note that we use canonical namespace 'Template' for title.\n\t\trequest = api.get( {\n\t\t\taction: 'query',\n\t\t\ttitles: 'Template:' + sourceTitle,\n\t\t\tprop: 'langlinks',\n\t\t\tlllang: this.siteMapper.getWikiDomainCode( mw.cx.targetLanguage ),\n\t\t\tredirects: true\n\t\t} ).then( function ( response ) {\n\t\t\tvar page,\n\t\t\t\tpageId = Object.keys( response.query.pages )[ 0 ];\n\t\t\tpage = response.query.pages[ pageId ];\n\t\t\tif ( page && page.langlinks ) {\n\t\t\t\treturn page.langlinks[ 0 ][ '*' ];\n\t\t\t} else {\n\t\t\t\treturn $.Deferred().reject().promise();\n\t\t\t}\n\t\t} );\n\n\t\tcachedTemplateRequests[ cacheKey ] = request;\n\n\t\treturn request;\n\t};\n\n\tTemplate.prototype.getUpdatedTemplate = function () {\n\t\tvar wikitext, self = this;\n\n\t\twikitext = this.getWikitext();\n\t\tmw.log( '[CX] Updated template: ' + wikitext );\n\n\t\treturn mw.cx.wikitextToHTML( this.siteMapper, this.language, wikitext, mw.cx.targetTitle )\n\t\t\t.then( function ( response ) {\n\t\t\t\t// TODO: Refactor this to a new method.\n\t\t\t\tvar $newTemplate, i, $new;\n\n\t\t\t\t$newTemplate = $( response );\n\n\t\t\t\t// Filter out auto-generated items, e.g. reference lists\n\t\t\t\t$newTemplate = $newTemplate.filter( function ( index, node ) {\n\t\t\t\t\tvar dataMw = node.nodeType === Node.ELEMENT_NODE &&\n\t\t\t\t\t\tnode.hasAttribute( 'data-mw' ) &&\n\t\t\t\t\t\tJSON.parse( node.getAttribute( 'data-mw' ) );\n\t\t\t\t\tif ( dataMw && dataMw.autoGenerated ) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Filter out line breaks.\n\t\t\t\t\tif ( node.nodeType === Node.TEXT_NODE && node.nodeValue === '\\n' ) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\n\t\t\t\t\t// We dont need category links. We cannot handle them.\n\t\t\t\t\tif ( $( node ).is( 'link[rel~=\"mw:PageProp/Category\"]' ) ) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn true;\n\t\t\t\t} );\n\n\t\t\t\tif ( $newTemplate.length >= 2 &&\n\t\t\t\t\t( $newTemplate.first().is( 'link' ) || $newTemplate.first().is( 'span' ) ) &&\n\t\t\t\t\t!$newTemplate.eq( 1 ).data( 'mw' )\n\t\t\t\t) {\n\t\t\t\t\t// Some times parsoid gives the data-mw and typeof attribute in a\n\t\t\t\t\t// different <link> tag. It is difficult to work with that 0 height,\n\t\t\t\t\t// invisible element at translation. So we copy them to the real section.\n\t\t\t\t\t$newTemplate.eq( 1 ).attr( {\n\t\t\t\t\t\ttypeof: $newTemplate.first().attr( 'typeof' ),\n\t\t\t\t\t\t'data-mw': $newTemplate.first().attr( 'data-mw' )\n\t\t\t\t\t} );\n\t\t\t\t\t$newTemplate = $newTemplate.slice( 1 );\n\t\t\t\t}\n\n\t\t\t\t// HACK: if $content consists of a single paragraph for the inline template, make it inline.\n\t\t\t\t// We have to do this because the parser wraps everything in <p>s, and inline templates\n\t\t\t\t// will render strangely when wrapped in <p>s.\n\t\t\t\tif ( $newTemplate.length === 1 && $newTemplate.is( 'p' ) && self.options.inline ) {\n\t\t\t\t\t// If the paragraph has no template definition and just a wrapper, unwrap it\n\t\t\t\t\tif ( !$newTemplate.data( 'mw' ) ) {\n\t\t\t\t\t\t$newTemplate = $( $newTemplate.html() );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// If the paragraph has template definition, For example: {{Convert|1meter}}\n\t\t\t\t\t\t// gives this HTML:  <p about=\"#mwt1\" typeof=\"mw:Transclusion\"\n\t\t\t\t\t\t// data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"Convert\",\"href\":\"./Template:Convert\"},\n\t\t\t\t\t\t// \"params\":{\"1\":{\"wt\":\"1\"},\"2\":{\"wt\":\"meter\"}},\"i\":0}}]}' id=\"mwAQ\">1 meter\n\t\t\t\t\t\t// (3<span typeof=\"mw:Entity\"> </span>ft 3<span typeof=\"mw:Entity\"> </span>in)</p>\n\t\t\t\t\t\t// Here, we cannot unwrap. We need to make the HTML inline by changing the tag <p> to <span>\n\t\t\t\t\t\t$new = $( '<span>' ).html( $newTemplate.html() );\n\t\t\t\t\t\tfor ( i in $newTemplate[ 0 ].attributes ) {\n\t\t\t\t\t\t\t$new.attr( $newTemplate[ 0 ].attributes[ i ].name, $newTemplate[ 0 ].attributes[ i ].value );\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// TODO: Copy classes too?\n\t\t\t\t\t\t$newTemplate = $new;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif ( !$newTemplate.data( 'mw' ) ) {\n\t\t\t\t\tmw.log.error( '[CX] Error in adapting template. missing data-mw' );\n\t\t\t\t}\n\t\t\t\treturn $newTemplate;\n\t\t\t} );\n\t};\n\n\tmw.cx.Template = Template;\n\n\t/**\n\t * TemplateTool encapsulates the handling of templates in translation.\n\t *\n\t * @class\n\t * @param {mw.cx.Template} sourceTemplate\n\t * @param {mw.cx.Template} targetTemplate\n\t * @param {Object} options\n\t */\n\tfunction TemplateTool( sourceTemplate, targetTemplate, options ) {\n\t\tthis.sourceTemplate = sourceTemplate;\n\t\tthis.targetTemplate = targetTemplate;\n\t\tthis.templateMapping = null;\n\t\tthis.options = $.extend( {}, mw.cx.TemplateTool.defaults, options );\n\t\tthis.siteMapper = this.options.siteMapper;\n\t\tthis.status = null;\n\t}\n\n\tTemplateTool.static = {};\n\n\t/**\n\t * Get the template mapping if any set by source.filter module\n\t *\n\t * @return {jQuery.Promise} The template mapping\n\t */\n\tTemplateTool.prototype.getTemplateMapping = function () {\n\t\tvar self = this;\n\n\t\treturn this.sourceTemplate.init().then( function () {\n\t\t\treturn self.targetTemplate.init().then( function () {\n\t\t\t\t// Both source and target params are adapted now.\n\t\t\t\treturn mw.cx.getCXConfiguration(\n\t\t\t\t\tself.sourceTemplate.language, self.targetTemplate.language\n\t\t\t\t).then( function ( response ) {\n\t\t\t\t\tvar configuration, templateParamMapping;\n\n\t\t\t\t\tconfiguration = response.configuration.templates || {};\n\t\t\t\t\ttemplateParamMapping = configuration[ self.sourceTemplate.title ] &&\n\t\t\t\t\t\tconfiguration[ self.sourceTemplate.title ].parameters;\n\t\t\t\t\treturn $.extend(\n\t\t\t\t\t\t{},\n\t\t\t\t\t\ttemplateParamMapping,\n\t\t\t\t\t\tself.targetTemplate.$template.data( 'template-mapping' )\n\t\t\t\t\t);\n\t\t\t\t} );\n\t\t\t} );\n\t\t} );\n\t};\n\n\t/**\n\t * Adapt a template using template mapping\n\t *\n\t * @return {jQuery.Promise}\n\t */\n\tTemplateTool.prototype.adapt = function () {\n\t\tvar self = this;\n\n\t\treturn this.getTemplateMapping().then( function ( cxMapping ) {\n\t\t\t// Since templateData is initialized to {} if a corresponding template exists, this\n\t\t\t// condition is true only if there is no equivalent template in the target language.\n\t\t\tif ( !self.targetTemplate.templateData ) {\n\t\t\t\treturn $.Deferred().reject().promise();\n\t\t\t}\n\n\t\t\tself.templateParamMapping = cxMapping;\n\t\t\tmw.log( '[CX] Adapting template ' + self.sourceTemplate.title + ' using a mapping.' );\n\n\t\t\tself.doFuzzyAdaptation();\n\t\t\t// Here we can try loading a saved mapping\n\t\t\tif ( !$.isEmptyObject( self.sourceTemplate.params ) && $.isEmptyObject( self.templateParamMapping ) ) {\n\t\t\t\tif ( self.targetTemplate.templateData.params &&\n\t\t\t\t\tObject.keys( self.targetTemplate.templateData.params ).length > 0\n\t\t\t\t) {\n\t\t\t\t\t// Checking whether there are any params defined for target template.\n\t\t\t\t\t// It is possible that source template definition has params and target\n\t\t\t\t\t// does not have any. Mapping failed only when target has some params.\n\t\t\t\t\tmw.log( '[CX] None of template params were able to map for ' + self.sourceTemplate.title );\n\t\t\t\t\t// Manually adaptable, but not automatically adaptable.\n\t\t\t\t\tself.status = 'adaptable';\n\t\t\t\t\treturn $.Deferred().resolve().promise();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// So we were able to map at least one parameter to the target template.\n\t\t\t// TODO: Should we check for all params mapping based on count match of\n\t\t\t// source and target template params?\n\t\t\tif ( Object.keys( self.templateParamMapping ).length > 0 ) {\n\t\t\t\tself.status = 'adaptable';\n\t\t\t}\n\n\t\t\tif ( Object.keys( self.templateParamMapping ).length === Object.keys( self.sourceTemplate.params ).length ) {\n\t\t\t\t// Fully adapted\n\t\t\t\tself.status = 'adapted';\n\t\t\t}\n\n\t\t\tObject.keys( self.sourceTemplate.templateData.params ).forEach( function ( key ) {\n\t\t\t\tvar savedParamValue, mappedTargetKey;\n\n\t\t\t\tmappedTargetKey = self.templateParamMapping[ key ];\n\t\t\t\tif ( mappedTargetKey ) {\n\t\t\t\t\t// In case of restored templates, there will be a wt value. Keep that.\n\t\t\t\t\tsavedParamValue = self.targetTemplate.templateData.params[ mappedTargetKey ] &&\n\t\t\t\t\t\tself.targetTemplate.templateData.params[ mappedTargetKey ].wt;\n\t\t\t\t\t// Copy the value from source template to target template\n\t\t\t\t\tself.targetTemplate.templateData.params[ mappedTargetKey ] =\n\t\t\t\t\t\tself.sourceTemplate.params[ key ];\n\t\t\t\t\t// restore the old value\n\t\t\t\t\tif ( savedParamValue ) {\n\t\t\t\t\t\tself.targetTemplate.templateData.params[ mappedTargetKey ].wt = savedParamValue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\n\t\t\tmw.log( '[CX] Template ' + self.sourceTemplate.title + ' adapted to ' + self.targetTemplate.title );\n\t\t\tself.targetTemplate.mapping = self.templateParamMapping;\n\t\t\treturn self.targetTemplate;\n\t\t} );\n\t};\n\n\t/**\n\t * Get the target parameters for the template after mapping\n\t */\n\tTemplateTool.prototype.doFuzzyAdaptation = function () {\n\t\tvar self = this;\n\n\t\t// Update the template parameters\n\t\tObject.keys( this.sourceTemplate.params ).forEach( function ( key ) {\n\t\t\tvar normalizedKey;\n\n\t\t\tif ( !isNaN( key ) ) {\n\t\t\t\t// Key is like \"1\" or \"2\" etc. Unnamed params.\n\t\t\t\tself.templateParamMapping[ key ] = key;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( !self.targetTemplate.templateData.params ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tnormalizedKey = key.trim().toLowerCase().replace( /[\\s+_-]+/g, '' );\n\t\t\t// Search in the aliases for a match - case insensitive.\n\t\t\tObject.keys( self.targetTemplate.templateData.params ).forEach( function ( paramName ) {\n\t\t\t\tvar i, normalizedCandidate, candidates,\n\t\t\t\t\tparam = self.targetTemplate.templateData.params[ paramName ];\n\n\t\t\t\tcandidates = param.aliases || [];\n\t\t\t\tcandidates = candidates.concat( paramName );\n\n\t\t\t\tfor ( i = 0; i < candidates.length; i++ ) {\n\t\t\t\t\tnormalizedCandidate = candidates[ i ].trim().toLowerCase().replace( /[\\s+_-]+/g, '' );\n\t\t\t\t\tif ( normalizedCandidate === normalizedKey ) {\n\t\t\t\t\t\tself.templateParamMapping[ key ] = paramName;\n\t\t\t\t\t\t// Break\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\t};\n\n\t/**\n\t * Prepare the editor instance.\n\t *\n\t * @return {boolean}\n\t */\n\tTemplateTool.prototype.prepareEditor = function () {\n\t\tvar self = this;\n\t\tif ( !mw.cx.TemplateEditor ) {\n\t\t\t// Qunit tests might note load this module.\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( !this.isEditable() ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif ( this.editor ) {\n\t\t\treturn true;\n\t\t}\n\n\t\tthis.editor = new mw.cx.TemplateEditor(\n\t\t\tthis.sourceTemplate,\n\t\t\tthis.targetTemplate, {\n\t\t\t\tsiteMapper: self.siteMapper,\n\t\t\t\tonUpdate: function ( $updatedTemplate, templateParamMapping ) {\n\t\t\t\t\tself.replaceTargetTemplate( $updatedTemplate, 'adapt' );\n\t\t\t\t\tself.targetTemplate.$template.attr( {\n\t\t\t\t\t\t'data-template-mapping': JSON.stringify( templateParamMapping )\n\t\t\t\t\t} );\n\t\t\t\t\tself.onUpdate();\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t\tthis.targetTemplate.options.onEdit = this.onEdit.bind( this );\n\t};\n\n\t/**\n\t * Editor request handler\n\t */\n\tTemplateTool.prototype.onEdit = function () {\n\t\tthis.editor.show();\n\t};\n\n\tTemplateTool.prototype.isEditable = function () {\n\t\t// Editor is given only when 'adapt' is the option\n\t\treturn this.status === 'adaptable' || this.status === 'adapted';\n\t};\n\n\t/**\n\t * Update and rerender the target template\n\t *\n\t * @return {jQuery.Promise}\n\t */\n\tTemplateTool.prototype.updateTargetTemplate = function () {\n\t\tvar self = this;\n\n\t\tthis.targetTemplate.$template.first().cxoverlay( {\n\t\t\tfullscreen: false,\n\t\t\tshowLoading: true\n\t\t} );\n\t\treturn this.targetTemplate.getUpdatedTemplate().then( function ( $newTemplate ) {\n\t\t\tself.targetTemplate.$template.first().cxoverlay( 'hide' );\n\t\t\tself.replaceTargetTemplate( $newTemplate, 'adapt' );\n\t\t} );\n\t};\n\n\t/**\n\t * Replace the target template with a new one.\n\t *\n\t * @param {jQuery} $newTemplate Replace with this template\n\t * @param {string} state The new state of the template.\n\t */\n\tTemplateTool.prototype.replaceTargetTemplate = function ( $newTemplate, state ) {\n\t\tvar sourceId, $new, $visibleFragment;\n\n\t\t$visibleFragment = this.sourceTemplate.getFirstVisibleFragment();\n\t\tif ( !$visibleFragment ) {\n\t\t\t// This should not happen. But prevent the js error if it happens. See T176237\n\t\t\tmw.error( '[CX] Error: No visible fragment for template for ' + this.sourceTemplate.title );\n\t\t\t// Stopping here means no template editor for this particular template.\n\t\t\treturn;\n\t\t}\n\t\tsourceId = $visibleFragment.prop( 'id' );\n\t\t$new = $newTemplate\n\t\t\t.clone()\n\t\t\t.attr( {\n\t\t\t\t'data-source': sourceId,\n\t\t\t\tid: 'cx' + sourceId,\n\t\t\t\tabout: this.sourceTemplate.$template.attr( 'about' ),\n\t\t\t\tcontenteditable: false,\n\t\t\t\ttabindex: 0 // Making it focusable\n\t\t\t} );\n\t\t// data-mw and typeof attributes need to set only to the first item in the set of\n\t\t// template fragments. We set about attribute to all items above.\n\t\t$new.first().attr( {\n\t\t\t'data-template-state': state,\n\t\t\ttypeof: $newTemplate.attr( 'typeof' ),\n\t\t\t'data-mw': $newTemplate.attr( 'data-mw' )\n\t\t} );\n\t\t// Trick to reduce the multiple template fragments with just one.\n\t\t// eslint-disable-next-line no-jquery/no-sizzle\n\t\tthis.targetTemplate.$template.not( ':first' ).remove();\n\t\tthis.targetTemplate.$template = this.targetTemplate.$template.replaceWith( $new );\n\t\tthis.targetTemplate.$template = $( document.getElementById( 'cx' + sourceId ) );\n\t\tthis.targetTemplate = new mw.cx.Template(\n\t\t\tthis.targetTemplate.$template,\n\t\t\tthis.targetTemplate.options\n\t\t);\n\t\t// Update the reference in editor object\n\t\tif ( this.editor ) {\n\t\t\tthis.editor.targetTemplate.$template = this.targetTemplate.$template;\n\t\t}\n\t};\n\n\t/**\n\t * Get inline templates in a section\n\t *\n\t * @param {jQuery} $section Target section\n\t * @return {Object[]} Array of source-target pairs of the found inline templates\n\t */\n\tTemplateTool.static.getInlineTemplates = function ( $section ) {\n\t\tvar $targetTemplates, inlineTemplates = [];\n\n\t\t$targetTemplates = $section.find( '[typeof~=\"mw:Transclusion\"]' );\n\t\t$targetTemplates.each( function () {\n\t\t\tvar inlineTemplate, $targetTemplate = $( this );\n\n\t\t\tinlineTemplate = {\n\t\t\t\tsource: mw.cx.Template.static.getTemplateDef( $targetTemplate ),\n\t\t\t\ttarget: $section.find( '[about=\"' + $targetTemplate.attr( 'about' ) + '\"]' )\n\t\t\t};\n\n\t\t\tif ( inlineTemplate.source.length ) {\n\t\t\t\tinlineTemplates.push( inlineTemplate );\n\t\t\t}\n\t\t} );\n\n\t\treturn inlineTemplates;\n\t};\n\n\t/**\n\t * Mark the template unadaptable. This is like a placeholder, ignored from\n\t * content to publish.\n\t */\n\tTemplateTool.prototype.markUndaptable = function () {\n\t\tvar $new;\n\n\t\tif ( this.targetTemplate.options.inline ) {\n\t\t\t// just leave the template unchanged. Add a class.\n\t\t\tthis.targetTemplate.$template.addClass( 'cx-unadaptable-template' );\n\t\t\t// Copy the attributes explicitly since we cannot assume they are present\n\t\t\t// after MT of parent section.\n\t\t\tthis.targetTemplate.$template.attr( {\n\t\t\t\t'data-template-state': 'keep-original',\n\t\t\t\t'data-mw': this.sourceTemplate.$template.attr( 'data-mw' ),\n\t\t\t\ttypeof: this.sourceTemplate.$template.attr( 'typeof' )\n\t\t\t} );\n\t\t} else {\n\t\t\t$new = $( '<div>' )\n\t\t\t\t.addClass( 'placeholder cx-unadaptable-template' )\n\t\t\t\t.attr( {\n\t\t\t\t\t'data-mw': this.sourceTemplate.$template.attr( 'data-mw' ),\n\t\t\t\t\ttypeof: this.sourceTemplate.$template.attr( 'typeof' )\n\t\t\t\t} );\n\t\t\tthis.replaceTargetTemplate( $new, 'unadaptable' );\n\t\t}\n\n\t\tthis.onUpdate();\n\t};\n\n\t/**\n\t * Mark the template skipped. This is like a placeholder, ignored from\n\t * content to publish.\n\t */\n\tTemplateTool.prototype.skip = function () {\n\t\tvar $new;\n\n\t\tif ( this.targetTemplate.options.inline ) {\n\t\t\t// just remove the inline template\n\t\t\tthis.targetTemplate.$template.remove();\n\t\t} else {\n\t\t\t$new = $( '<div>' )\n\t\t\t\t.addClass( 'placeholder' )\n\t\t\t\t.attr( {\n\t\t\t\t\t'data-mw': this.sourceTemplate.$template.attr( 'data-mw' ),\n\t\t\t\t\ttypeof: this.sourceTemplate.$template.attr( 'typeof' )\n\t\t\t\t} );\n\t\t\tthis.replaceTargetTemplate( $new, 'skip' );\n\t\t\tthis.onUpdate();\n\t\t}\n\t};\n\n\tTemplateTool.prototype.onUpdate = function () {\n\t\t// Fire appropriate change events.\n\t\tif ( this.targetTemplate.options.inline ) {\n\t\t\t// Fire change event for the parent section.\n\t\t\tmw.hook( 'mw.cx.translation.change' ).fire(\n\t\t\t\tthis.targetTemplate.$template.parents( '[data-source]' )\n\t\t\t);\n\t\t} else {\n\t\t\tmw.hook( 'mw.cx.translation.change' ).fire( this.targetTemplate.$template );\n\t\t}\n\t};\n\n\t/**\n\t * Process a template pair.\n\t *\n\t * @param {jQuery} $sourceTemplate Source template\n\t * @param {jQuery} $targetTemplate Target template\n\t * @param {Object} options Options for processing\n\t * @return {jQuery.Promise}\n\t */\n\tTemplateTool.static.processTemplate = function ( $sourceTemplate, $targetTemplate, options ) {\n\t\tvar sourceTemplate, targetTemplate, templateTool;\n\n\t\tif ( !$sourceTemplate.length ) {\n\t\t\treturn $.Deferred().resolve().promise();\n\t\t}\n\n\t\tsourceTemplate = new mw.cx.Template( $sourceTemplate, {\n\t\t\tlanguage: mw.cx.sourceLanguage,\n\t\t\tsiteMapper: mw.cx.siteMapper,\n\t\t\tinline: options.inline\n\t\t} );\n\t\ttargetTemplate = new mw.cx.Template( $targetTemplate, {\n\t\t\tlanguage: mw.cx.targetLanguage,\n\t\t\tsiteMapper: mw.cx.siteMapper,\n\t\t\tinline: options.inline,\n\t\t\teditable: !options.references\n\t\t} );\n\t\ttemplateTool = new mw.cx.TemplateTool( sourceTemplate, targetTemplate );\n\n\t\t// Save the instance.\n\t\t$sourceTemplate.data( 'cxtemplate', templateTool );\n\n\t\tif ( options.references ) {\n\t\t\t// References need special handling because the fragments required for them\n\t\t\t// are scattered in different sections, but our processing is confined\n\t\t\t// to a single section. When we request a template's html rendering, the parser\n\t\t\t// won't get this information and end up with returning just the container,\n\t\t\t// loosing all reference data. To avoid this, we process each internal reference\n\t\t\t// template and keep the container unchanged.\n\t\t\ttemplateTool.replaceTargetTemplate( sourceTemplate.$template, 'keep-original' );\n\t\t\t// The internal references are processed on demand when they get added to translation.\n\t\t\t// See mw.cx.adapted.reference hook handler\n\t\t\treturn;\n\t\t}\n\n\t\t// Not a processed template. Proceed with attempt to adapt.\n\t\t$targetTemplate.first().cxoverlay( {\n\t\t\tfullscreen: false,\n\t\t\tshowLoading: true\n\t\t} );\n\n\t\treturn templateTool.adapt().then( function () {\n\t\t\t// Successful adaptation.\n\t\t\ttemplateTool.prepareEditor();\n\t\t\treturn templateTool.updateTargetTemplate();\n\t\t}, function () {\n\t\t\t// Adaptation failed.\n\t\t\t$targetTemplate.first().cxoverlay( 'hide' );\n\t\t\ttemplateTool.markUndaptable();\n\n\t\t\t// Allow proceeding inline templates even after failure.\n\t\t\tif ( options.inline ) {\n\t\t\t\treturn $.Deferred().resolve().promise();\n\t\t\t} else {\n\t\t\t\treturn $.Deferred().reject().promise();\n\t\t\t}\n\t\t} ).always( function () {\n\t\t\ttemplateTool.onUpdate();\n\t\t} );\n\t};\n\n\t/**\n\t * Process all inline templates in a section.\n\t *\n\t * @param {jQuery} $section The translation source section.\n\t * @return {jQuery.Promise}\n\t */\n\tTemplateTool.static.processInlineTemplates = function ( $section ) {\n\t\tvar inlineTemplates, queue = $.Deferred().resolve();\n\n\t\tinlineTemplates = mw.cx.TemplateTool.static.getInlineTemplates( $section ) || [];\n\t\t// forEach used to get easy and simple closure inside.\n\t\tinlineTemplates.forEach( function ( inlineTemplate ) {\n\t\t\tqueue = queue.then( function () {\n\t\t\t\treturn mw.cx.TemplateTool.static.processTemplate( inlineTemplate.source, inlineTemplate.target, {\n\t\t\t\t\tinline: true\n\t\t\t\t} );\n\t\t\t} );\n\t\t} );\n\t\treturn queue.promise();\n\t};\n\n\t/**\n\t * Check if the template is a references or a container for it.\n\t *\n\t * @param {jQuery} $sourceTemplate The template\n\t * @return {boolean}\n\t */\n\tTemplateTool.static.isReferencesBlock = function ( $sourceTemplate ) {\n\t\treturn $sourceTemplate.is( '[typeof~=\"mw:Extension/references\"]' ) ||\n\t\t\t$sourceTemplate.find( '[typeof~=\"mw:Extension/references\"]' ).length > 0;\n\t};\n\n\t/**\n\t * Process a section template.\n\t *\n\t * @param {jQuery|string} section The translation source section or its id.\n\t */\n\tTemplateTool.static.processBlockTemplate = function ( section ) {\n\t\tvar $targetTemplate, $sourceTemplate, isReferencesBlock;\n\n\t\tif ( typeof section === 'string' ) {\n\t\t\t$targetTemplate = mw.cx.getTranslationSection( section );\n\t\t} else {\n\t\t\t$targetTemplate = section;\n\t\t}\n\n\t\t// Source section and source template wont be same since the template\n\t\t// with template definition may be another element with same about attribute.\n\t\t$sourceTemplate = mw.cx.Template.static.getTemplateDef( $targetTemplate );\n\t\tisReferencesBlock = mw.cx.TemplateTool.static.isReferencesBlock( $sourceTemplate );\n\t\tif ( mw.cx.Template.static.isTemplate( $sourceTemplate ) || isReferencesBlock ) {\n\t\t\tmw.cx.TemplateTool.static.processTemplate( $sourceTemplate, $targetTemplate, {\n\t\t\t\tinline: false,\n\t\t\t\treferences: isReferencesBlock\n\t\t\t} );\n\t\t} else {\n\t\t\tmw.cx.TemplateTool.static.processInlineTemplates( $targetTemplate );\n\t\t}\n\t};\n\n\tmw.cx.TemplateTool = TemplateTool;\n\n\tmw.cx.TemplateTool.defaults = {\n\t\tsiteMapper: mw.cx.siteMapper,\n\t\tsourceLanguage: mw.cx.sourceLanguage,\n\t\ttargetLanguage: mw.cx.targetLanguage\n\t};\n\n\t$( function () {\n\t\tmw.hook( 'mw.cx.translation.postMT' ).add( mw.cx.TemplateTool.static.processBlockTemplate );\n\t\tmw.hook( 'mw.cx.translation.add' ).add( mw.cx.TemplateTool.static.processBlockTemplate );\n\t\tmw.hook( 'mw.cx.adapted.reference' ).add( function ( referenceId ) {\n\t\t\tvar $targetTemplate;\n\t\t\t$targetTemplate = $( '.cx-column--translation #' + referenceId );\n\t\t\tmw.cx.TemplateTool.static.processInlineTemplates( $targetTemplate );\n\t\t} );\n\t} );\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/ext.cx.tools.validator.js","messages":[{"ruleId":"jsdoc/require-returns","severity":1,"message":"Found more than one @return declaration.","line":8,"column":2,"nodeType":"Block","endLine":17,"endColumn":5},{"ruleId":"jsdoc/require-returns-check","severity":1,"message":"Found more than one @return declaration.","line":8,"column":2,"nodeType":"Block","endLine":17,"endColumn":5}],"errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"( function () {\n\t'use strict';\n\n\tfunction ContentTranslationValidator( siteMapper ) {\n\t\tthis.siteMapper = siteMapper;\n\t}\n\n\t/**\n\t * Checks to see if a title exists in the specified language wiki. Returns\n\t * the normalised title and resolves redirects.\n\t *\n\t * @param {string} language The language of the wiki to check\n\t * @param {string} title The title to look for\n\t * @return {jQuery.promise}\n\t * @return {Function} return.done If title exists\n\t * @return {string|boolean} return.done.title\n\t */\n\tContentTranslationValidator.prototype.isTitleExistInLanguage = function ( language, title ) {\n\t\tvar api = this.siteMapper.getApi( language );\n\n\t\t// Short circuit empty titles\n\t\tif ( title === '' ) {\n\t\t\treturn $.Deferred().resolve( false ).promise();\n\t\t}\n\n\t\t// Reject titles with pipe in the name, as it has special meaning in the api\n\t\tif ( /\\|/.test( title ) ) {\n\t\t\treturn $.Deferred().resolve( false ).promise();\n\t\t}\n\n\t\treturn api.get( {\n\t\t\tformatversion: 2,\n\t\t\taction: 'query',\n\t\t\ttitles: title,\n\t\t\tredirects: true\n\t\t} ).then( function ( response ) {\n\t\t\tvar page = response.query.pages[ 0 ];\n\n\t\t\tif ( page.missing || page.invalid ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\treturn page.title;\n\t\t} );\n\t};\n\n\t/**\n\t * Checks for an equivalent page in the target wiki based on source title.\n\t *\n\t * @param {string} sourceLanguage the source language\n\t * @param {string} targetLanguage the target language\n\t * @param {string} sourceTitle the title to check\n\t * @return {jQuery.promise}\n\t */\n\tContentTranslationValidator.prototype.isTitleConnectedInLanguages = function (\n\t\tsourceLanguage,\n\t\ttargetLanguage,\n\t\tsourceTitle\n\t) {\n\t\tvar api = this.siteMapper.getApi( sourceLanguage );\n\n\t\treturn api.get( {\n\t\t\taction: 'query',\n\t\t\tprop: 'langlinks',\n\t\t\ttitles: sourceTitle,\n\t\t\tlllang: mw.cx.siteMapper.getWikiDomainCode( targetLanguage ),\n\t\t\tlllimit: 1,\n\t\t\tredirects: true\n\t\t} ).then( function ( response ) {\n\t\t\tvar equivalentTargetPage = false;\n\n\t\t\tif ( response.query && response.query.pages ) {\n\t\t\t\tObject.keys( response.query.pages ).forEach( function ( pageId ) {\n\t\t\t\t\tvar page = response.query.pages[ pageId ];\n\t\t\t\t\tif ( page.langlinks ) {\n\t\t\t\t\t\tequivalentTargetPage = page.langlinks[ 0 ][ '*' ];\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t}\n\n\t\t\treturn equivalentTargetPage;\n\t\t} );\n\t};\n\n\t/**\n\t * Check whether a page with the same title already exists\n\t * and show a warning if needed.\n\t */\n\tContentTranslationValidator.prototype.validateTargetTitle = function () {\n\t\tvar viewTargetUrl = this.siteMapper.getPageUrl( mw.cx.targetLanguage, mw.cx.targetTitle );\n\n\t\tthis.isTitleExistInLanguage( mw.cx.targetLanguage, mw.cx.targetTitle )\n\t\t\t.then( function ( pageExist ) {\n\t\t\t\t// If page doesn't exist, it's OK\n\t\t\t\tif ( !pageExist ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tmw.hook( 'mw.cx.warning' ).fire( mw.message(\n\t\t\t\t\t'cx-translation-target-page-exists',\n\t\t\t\t\tviewTargetUrl,\n\t\t\t\t\tmw.cx.targetTitle\n\t\t\t\t) );\n\t\t\t} );\n\t};\n\n\tmw.cx.ContentTranslationValidator = ContentTranslationValidator;\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/mw.cx.tools.InstructionsTool.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/mw.cx.tools.IssueTrackingTool.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/mw.cx.tools.SearchTool.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/mw.cx.tools.TemplateTool.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/mw.cx.tools.TranslationTool.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/mw.cx.tools.TranslationToolFactory.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/translation/ext.cx.translation.aligner.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/translation/ext.cx.translation.conflict.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/translation/ext.cx.translation.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/translation/ext.cx.translation.loader.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/translation/ext.cx.translation.progress.js","messages":[{"ruleId":"jsdoc/require-returns","severity":1,"message":"Found more than one @return declaration.","line":46,"column":2,"nodeType":"Block","endLine":55,"endColumn":5},{"ruleId":"jsdoc/require-returns-check","severity":1,"message":"Found more than one @return declaration.","line":46,"column":2,"nodeType":"Block","endLine":55,"endColumn":5},{"ruleId":"jsdoc/require-returns","severity":1,"message":"Found more than one @return declaration.","line":93,"column":2,"nodeType":"Block","endLine":101,"endColumn":5},{"ruleId":"jsdoc/require-returns-check","severity":1,"message":"Found more than one @return declaration.","line":93,"column":2,"nodeType":"Block","endLine":101,"endColumn":5}],"errorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation - Calculate the progress\n * A tool that allows editors to translate pages from one language\n * to another with the help of machine translation and other translation tools\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\tvar totalSourceWeight = 0,\n\t\ttranslationThreshold = 0.05;\n\n\t/**\n\t * Get the total source weight.\n\t * This is only calculated once per session and cached, because the source doesn't change.\n\t *\n\t * @return {number} Total source weight\n\t */\n\tfunction getTotalSourceWeight() {\n\t\tvar $sourceContainer, $sections;\n\n\t\t// Return the cached value\n\t\tif ( totalSourceWeight ) {\n\t\t\treturn totalSourceWeight;\n\t\t}\n\n\t\t$sourceContainer = $( '.cx-column--source .cx-column__content' );\n\t\t$sections = $sourceContainer.children( mw.cx.getSectionSelector() );\n\n\t\t$sections.each( function () {\n\t\t\ttotalSourceWeight += $( this ).text().length;\n\t\t} );\n\n\t\treturn totalSourceWeight;\n\t}\n\n\t/**\n\t * Calculate and show the translation progress.\n\t */\n\tfunction showProgress() {\n\t\tmw.hook( 'mw.cx.progress' ).fire( mw.cx.getProgress() );\n\t}\n\n\t/**\n\t * Calculate the percentage of machine translation for the given sections.\n\t *\n\t * @param {jQuery} $sections List of sections.\n\t * @return {Object} Map of weights\n\t * @return {number} return.any Weight of sections with content\n\t * @return {number} return.human Weight of sections with human modified content\n\t * @return {number} return.mt Weight of sections with unmodified mt content\n\t * @return {number} return.mtSectionsCount Count of sections with unmodified mt content\n\t */\n\tfunction getTranslationWeights( $sections ) {\n\t\tvar weights = {\n\t\t\tany: 0,\n\t\t\thuman: 0,\n\t\t\tmt: 0,\n\t\t\tmtSectionsCount: 0\n\t\t};\n\n\t\t$sections.each( function () {\n\t\t\tvar weight, state, $section = $( this );\n\n\t\t\tweight = +$section.attr( 'data-cx-weight' );\n\t\t\tweights.any += weight;\n\n\t\t\tstate = $section.data( 'cx-state' );\n\t\t\tif ( state === 'mt' || state === 'source' ) {\n\t\t\t\t// If the section has unmodified MT or source content copied,\n\t\t\t\t// count it as MT.\n\t\t\t\tweights.mt += weight;\n\t\t\t\tweights.mtSectionsCount += 1;\n\t\t\t} else {\n\t\t\t\tweights.human += weight;\n\t\t\t}\n\t\t} );\n\n\t\treturn weights;\n\t}\n\n\t/**\n\t * Return all translation sections that have any text.\n\t *\n\t * @return {jQuery}\n\t */\n\tfunction getSectionsWithContent() {\n\t\treturn $( '.cx-column--translation [data-cx-weight]' );\n\t}\n\n\t/**\n\t * Calculate the percentage of machine translation out of the whole article.\n\t *\n\t * @return {Object} Map of weights\n\t * @return {number} return.any Weight of sections with content\n\t * @return {number} return.human Weight of sections with human modified content\n\t * @return {number} return.mt Weight of sections with unmodified mt content\n\t * @return {number} return.mtSectionsCount Count of sections with unmodified mt content\n\t */\n\tmw.cx.getProgress = function () {\n\t\tvar sourceWeight, weights;\n\n\t\tsourceWeight = getTotalSourceWeight();\n\t\tweights = getTranslationWeights( getSectionsWithContent() );\n\t\tif ( sourceWeight > 0 ) {\n\t\t\tweights.any /= sourceWeight;\n\t\t\tweights.human /= sourceWeight;\n\t\t\tweights.mt /= sourceWeight;\n\t\t}\n\n\t\treturn weights;\n\t};\n\n\t/**\n\t * Update/Change handler for section.\n\t *\n\t * @param {jQuery} $section The source section\n\t */\n\tfunction onSectionUpdate( $section ) {\n\t\tvar $sourceSection, translationLength, sourceLength;\n\n\t\tif ( !$section ) {\n\t\t\treturn;\n\t\t}\n\n\t\t$sourceSection = mw.cx.getTranslationSection( $section.data( 'source' ) );\n\n\t\ttranslationLength = $section.text().length;\n\t\tsourceLength = $sourceSection.text().length;\n\n\t\t// Check if the translation is above the defined threshold to count\n\t\tif ( translationLength / sourceLength < translationThreshold ) {\n\t\t\t// Do not count the section as translated\n\t\t\t$section.removeAttr( 'data-cx-weight' );\n\t\t} else {\n\t\t\t$section.attr( 'data-cx-weight', sourceLength );\n\t\t}\n\n\t\t// Calculate the total translation progress\n\t\tshowProgress();\n\t}\n\n\t$( function () {\n\t\tmw.hook( 'mw.cx.translation.change' ).add( onSectionUpdate );\n\t\t// Show the progress bar with 0 progress.\n\t\tmw.hook( 'mw.cx.progress' ).fire( {\n\t\t\tany: 0\n\t\t} );\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/translation/ext.cx.translation.storage.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/legacy/mw.cx.ui.Columns.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/legacy/mw.cx.ui.Header.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/legacy/mw.cx.ui.SourceColumn.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/legacy/mw.cx.ui.ToolsColumn.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/legacy/mw.cx.ui.TranslationColumn.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/legacy/mw.cx.ui.TranslationView.init.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/legacy/mw.cx.ui.TranslationView.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.ArticleColumn.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.CaptchaDialog.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.Categories.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.Header.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.Infobar.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.LanguageFilter.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.LoginDialog.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.SourceColumn.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.TargetColumn.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.ToolsColumn.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.TranslationHeader.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.TranslationView.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.CategoryInputWidget.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.CategoryMultiselectWidget.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.CategoryTagItemWidget.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.FeatureDiscoveryWidget.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.MessageWidget.js","messages":[{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'mediawiki' is undefined.","line":9,"column":null,"nodeType":"Block","endLine":9,"endColumn":null},{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'mediawiki' is undefined.","line":10,"column":null,"nodeType":"Block","endLine":10,"endColumn":null}],"errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * CX Message widget (dismissible banners)\n *\n * @class\n * @extends OO.ui.MessageWidget\n *\n * @constructor\n * @param {Object} [config] Configuration options\n * @cfg {mediawiki.Message|string} [message] Main message.\n * @cfg {mediawiki.Message|string} [details] Additional details.\n * @cfg {OO.ui.ButtonWidget[]} [buttons] Array of additional buttons.\n */\nmw.cx.ui.MessageWidget = function CXMessageWidget( config ) {\n\t// Configuration initialization\n\tconfig = config || {};\n\n\tconfig.label = config.label || this.composeMessage( config.message, config.details );\n\n\t// Parent constructor\n\tmw.cx.ui.MessageWidget.super.call( this, config );\n\n\t// Events\n\tthis.closeButton = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\ticon: 'close',\n\t\tclasses: [ 'cx-message-widget-close' ],\n\t\ttabIndex: -1\n\t} ).connect( this, { click: 'onCloseClick' } );\n\n\t// Initialization\n\tthis.$element\n\t\t.addClass( 'cx-message-widget' )\n\t\t.append( this.closeButton.$element );\n\n\tthis.addButtons( config.buttons );\n};\n\n/* Setup */\n\nOO.inheritClass( mw.cx.ui.MessageWidget, OO.ui.MessageWidget );\n\n/* Methods */\n\n/**\n * Handle close icon clicks\n */\nmw.cx.ui.MessageWidget.prototype.onCloseClick = function () {\n\tthis.$element.remove();\n};\n\nmw.cx.ui.MessageWidget.prototype.composeMessage = function ( message, details ) {\n\tvar $message, $details;\n\t$message = $( '<span>' ).addClass( 'cx-message-widget-message' );\n\t$details = $( '<span>' ).addClass( 'cx-message-widget-details' );\n\tif ( message instanceof mw.Message ) {\n\t\t$message.html( message.parse() );\n\t} else {\n\t\t$message.text( message );\n\t}\n\tif ( details ) {\n\t\tif ( details instanceof mw.Message ) {\n\t\t\t$details.html( details.parse() );\n\t\t} else {\n\t\t\t$details.text( details );\n\t\t}\n\t}\n\n\treturn $message.add( $details );\n};\n\n/**\n * @param {OO.ui.ButtonWidget[]} buttons Array of additional buttons.\n */\nmw.cx.ui.MessageWidget.prototype.addButtons = function ( buttons ) {\n\tvar $buttons = $( '<div>' ).addClass( 'cx-message-widget-buttons' );\n\n\tif ( !buttons ) {\n\t\treturn;\n\t}\n\n\tbuttons.forEach( function ( button ) {\n\t\t$buttons.append( button.$element );\n\t} );\n\n\tthis.$element.append( $buttons );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js","messages":[{"ruleId":"no-shadow","severity":1,"message":"'pages' is already declared in the upper scope on line 184 column 71.","line":219,"column":31,"nodeType":"Identifier","messageId":"noShadow","endLine":219,"endColumn":36}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * Content Translation UserInterface PageSelectorWidget class.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n\n'use strict';\n\n/**\n * Creates an mw.cx.ui.PageSelectorWidget object.\n *\n * @class\n * @extends mw.widgets.TitleInputWidget\n *\n * @constructor\n * @param {Object} config Configuration options\n * @cfg {mw.cx.SiteMapper} siteMapper Site mapper\n * @cfg {string} [language] Source language\n */\nmw.cx.ui.PageSelectorWidget = function PageSelectorWidget( config ) {\n\tconfig = $.extend( {}, {\n\t\tnamespace: mw.config.get( 'wgNamespaceIds' )[ '' ], // Main namespace\n\t\tlimit: 5,\n\t\tshowDescriptions: true,\n\t\tshowImages: true,\n\t\tshowMissing: false,\n\t\taddQueryInput: false,\n\t\texcludeDynamicNamespaces: true,\n\t\ticon: 'search'\n\t}, config );\n\n\t// Parent constructor\n\tmw.cx.ui.PageSelectorWidget.super.call( this, config );\n\n\tthis.siteMapper = config.siteMapper;\n\tthis.language = config.language || 'en';\n\tthis.excludedNamespaces = [];\n\tif ( config.targetLanguage ) {\n\t\tthis.setTargetLanguage( config.targetLanguage );\n\t}\n\tthis.lookupChooseFirstItem = false;\n\tthis.listen();\n\n\t// Initialization\n\tthis.$element.addClass( 'mw-cx-ui-PageSelectorWidget' );\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.ui.PageSelectorWidget, mw.widgets.TitleInputWidget );\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.PageSelectorWidget.prototype.initializeLookupMenuSelection = function () {\n\tvar matchingItem;\n\n\tmw.cx.ui.PageSelectorWidget.super.prototype.initializeLookupMenuSelection.apply( this, arguments );\n\n\tif ( !this.lookupChooseFirstItem ) {\n\t\treturn this;\n\t}\n\n\tmatchingItem = this.lookupMenu.findItemFromData( this.getValue() );\n\tif ( matchingItem ) {\n\t\tthis.lookupMenu.chooseItem( matchingItem );\n\t\tthis.lookupChooseFirstItem = false; // Reset to the default value\n\t}\n\n\treturn this;\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.PageSelectorWidget.prototype.getApi = function () {\n\treturn this.siteMapper.getApi( this.language );\n};\n\nmw.cx.ui.PageSelectorWidget.prototype.setLanguage = function ( language ) {\n\tthis.language = language;\n\tthis.setDir( $.uls.data.getDir( language ) );\n\n\t// Reset the requestCache of OO.ui.mixin.LookupElement\n\tthis.requestCache = {};\n\tthis.closeLookupMenu();\n\n\t// Reset the \"no-results\" and \"has-suggestions\" classes\n\tthis.$overlay.removeClass( 'mw-cx-ui-PageSelectorWidget--no-results' );\n\tthis.$overlay.removeClass( 'mw-cx-ui-PageSelectorWidget--has-suggestions' );\n\n\tthis.populateSuggestions();\n};\n\nmw.cx.ui.PageSelectorWidget.prototype.setTargetLanguage = function ( language ) {\n\tthis.targetLanguage = language;\n};\n\nmw.cx.ui.PageSelectorWidget.prototype.onChangeHandler = function () {\n\tthis.$overlay.removeClass( 'mw-cx-ui-PageSelectorWidget--no-results' );\n\tthis.$overlay.removeClass( 'mw-cx-ui-PageSelectorWidget--has-suggestions' );\n\n\tthis.$overlay.toggleClass(\n\t\t'mw-cx-ui-PageSelectorWidget--input', !!this.getQueryValue()\n\t);\n};\n\n/**\n * Sets value for PageSelectorWidget and input element value,\n * without emitting 'change' event, therefore not triggering network call\n *\n * @param {string} value String input for PageSelectorWidget\n */\nmw.cx.ui.PageSelectorWidget.prototype.setValueNoEmit = function ( value ) {\n\tvalue = this.cleanUpValue( value );\n\n\tif ( this.$input.val() !== value ) {\n\t\tthis.$input.val( value );\n\t}\n\n\tif ( this.value !== value ) {\n\t\tthis.value = value;\n\t}\n};\n\nmw.cx.ui.PageSelectorWidget.prototype.listen = function () {\n\t// Unbind event handlers so search results don't disappear when focus is lost\n\tthis.$input.off( 'blur' );\n\tthis.lookupMenu.onDocumentMouseDownHandler = function () {};\n\t// Disable width and height calculation for search results container\n\tthis.lookupMenu.setIdealSize = function () {};\n\n\tthis.connect( this, { change: 'onChangeHandler' } );\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.PageSelectorWidget.prototype.getOptionWidgetData = function ( title, data ) {\n\t// Parent method\n\tvar optionWidgetData = mw.cx.ui.PageSelectorWidget.super.prototype.getOptionWidgetData.apply( this, arguments );\n\n\t// Correct the URL so that it can point to the source language wiki.\n\toptionWidgetData.url = this.siteMapper.getPageUrl( this.language, title );\n\t// If item is not missing, one language is added to get actual total number of languages\n\toptionWidgetData.numOfLanguages = !data.missing && ( OO.getProp( data.originalData, 'langlinkscount' ) || 0 ) + 1;\n\toptionWidgetData.missingInTargetLanguage = !OO.getProp( data.originalData, 'langlinks' );\n\toptionWidgetData.targetLanguage = this.targetLanguage;\n\toptionWidgetData.sourceLanguage = this.language;\n\n\treturn optionWidgetData;\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.PageSelectorWidget.prototype.getApiParams = function () {\n\t// Parent method\n\tvar params = mw.cx.ui.PageSelectorWidget.super.prototype.getApiParams.apply( this, arguments );\n\n\tparams.prop.push( 'langlinks', 'langlinkscount' );\n\tparams.lllang = this.targetLanguage;\n\treturn params;\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.PageSelectorWidget.prototype.createOptionWidget = function ( data ) {\n\treturn new mw.cx.ui.TitleOptionWidget( data );\n};\n\n/**\n * Get option widgets and labels from the server response.\n * This method creates option widgets from suggested pages (when there is no user input) or\n * from search results (when there is user input).\n *\n * @param {Object} pages Query result\n * @return {Array} Array of OO.ui.OptionWidget menu items and mw.cx.ui.MenuLabelWidget labels\n */\nmw.cx.ui.PageSelectorWidget.prototype.getOptionsFromData = function ( pages ) {\n\tvar index, suggestionPage, page, optionsData, hasResults,\n\t\tnearbyPages = pages.nearby,\n\t\trecentEditPages = pages.recentEdits,\n\t\tpageData = {},\n\t\titems = [],\n\t\tquery = this.getQueryValue(),\n\t\tself = this;\n\n\t// If there is user input, we execute parent method, process possible no results case and return early\n\tif ( query ) {\n\t\tif ( query.indexOf( ':' ) >= 0 ) {\n\t\t\t// If query is from a non-default namespace, accept results from those namespaces.\n\t\t\t// Remove namespace preference.\n\t\t\tthis.setNamespace( null );\n\t\t} else {\n\t\t\t// Reset to default namespace preference.\n\t\t\tthis.setNamespace( mw.config.get( 'wgNamespaceIds' )[ '' ] ); // Main namespace\n\t\t}\n\t\toptionsData = mw.cx.ui.PageSelectorWidget.super.prototype.getOptionsFromData.apply( this, arguments );\n\t\thasResults = optionsData.length > 0;\n\n\t\tif ( !hasResults ) {\n\t\t\tthis.emit( 'noResults' );\n\t\t}\n\t\tthis.$overlay.toggleClass( 'mw-cx-ui-PageSelectorWidget--no-results', !hasResults );\n\n\t\treturn optionsData;\n\t}\n\n\t// When there is no user input, we display two lists with suggestions: recently edited pages and nearby pages.\n\t// We need this specific override to keep the two lists separate, and prevent sorting by page index,\n\t// which happens in parent method. Even without the sorting in parent method, since data is passed\n\t// in objects, not arrays, the two separate lists could be mixed up, since ordering in JS objects\n\t// is not guaranteed.\n\tfunction processQueryResult( pages, label ) {\n\t\tif ( !pages ) {\n\t\t\treturn false;\n\t\t}\n\n\t\titems.push( new OO.ui.MenuSectionOptionWidget( {\n\t\t\tlabel: label\n\t\t} ) );\n\n\t\tfor ( index in pages ) {\n\t\t\tsuggestionPage = pages[ index ];\n\n\t\t\tpageData[ suggestionPage.title ] = {\n\t\t\t\tdisambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,\n\t\t\t\timageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),\n\t\t\t\tdescription: suggestionPage.description,\n\t\t\t\toriginalData: suggestionPage\n\t\t\t};\n\n\t\t\t// Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true\n\t\t\t// and we encounter a cross-namespace redirect.\n\t\t\tif ( self.namespace === null || self.namespace === suggestionPage.ns ) {\n\t\t\t\tpage = pageData[ suggestionPage.title ];\n\t\t\t\titems.push( self.createOptionWidget( self.getOptionWidgetData( suggestionPage.title, page ) ) );\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\thasResults = processQueryResult(\n\t\trecentEditPages,\n\t\tmw.msg( 'cx-page-selector-widget-recent-edits-label' )\n\t);\n\thasResults = processQueryResult(\n\t\tnearbyPages,\n\t\tmw.msg( 'cx-page-selector-widget-nearby-label' )\n\t) || hasResults;\n\n\tif ( !hasResults ) {\n\t\tthis.emit( 'noResults' );\n\t}\n\tthis.$overlay.toggleClass( 'mw-cx-ui-PageSelectorWidget--no-results', !hasResults );\n\n\tif ( this.cache ) {\n\t\tthis.cache.set( pageData );\n\t}\n\n\treturn items;\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.PageSelectorWidget.prototype.getLookupRequest = function () {\n\tif ( !this.isValidNamespace( this.getQueryValue() ) ) {\n\t\treturn $.Deferred().resolve( {} ).promise();\n\t}\n\n\treturn mw.cx.ui.PageSelectorWidget.super.prototype.getLookupRequest.apply( this, arguments );\n};\n\n/**\n * Populates suggestions to display when search input field is empty.\n */\nmw.cx.ui.PageSelectorWidget.prototype.populateSuggestions = function () {\n\tvar self = this;\n\n\tif ( !this.allowSuggestionsWhenEmpty ) {\n\t\treturn;\n\t}\n\n\tthis.pushPending();\n\t$.when(\n\t\tthis.getPageDetails(),\n\t\tthis.getNearbyPages()\n\t).done( function ( recentEdits, nearby ) {\n\t\tvar recentEditPages = OO.getProp( recentEdits, 'query', 'pages' ),\n\t\t\tnearbyPages = OO.getProp( nearby, 'query', 'pages' );\n\n\t\tself.requestCache[ '' ] = {\n\t\t\tnearby: nearbyPages,\n\t\t\trecentEdits: recentEditPages\n\t\t};\n\t} ).fail( function ( error ) {\n\t\tmw.log( 'Error getting page data. ' + error );\n\t} ).always( function () {\n\t\tself.populateLookupMenu();\n\t\tself.popPending();\n\t} );\n};\n\n/**\n * Get user geolocation coordinates using GeoIP or ULSGeo cookies.\n *\n * @return {string|null}\n */\nmw.cx.ui.PageSelectorWidget.prototype.getUserCoordinates = function () {\n\tvar geoIP = mw.cookie.get( 'GeoIP', '' ), // GeoIP format: 'FI:Helsinki:60.1756:24.9342:v4'\n\t\tgeoIPCoordsMatch = geoIP && geoIP.match( /\\d+\\.?\\d*:\\d+\\.?\\d*/g ),\n\t\tgeoIPCoords = geoIPCoordsMatch && geoIPCoordsMatch[ 0 ].replace( ':', '|' ),\n\t\tulsGeo = JSON.parse( mw.cookie.get( 'ULSGeo' ) ), // Outside Wikimedia, ULS stores geolocation info in 'ULSGeo' cookie\n\t\tulsGeoCoords = ulsGeo && ( ulsGeo.latitude + '|' + ulsGeo.longitude );\n\n\treturn geoIPCoords || ulsGeoCoords;\n};\n\n/**\n * Get the thumbnail image, description and langlinks count for pages geographically close to\n * user's physical location.\n *\n * @return {jQuery.Promise}\n */\nmw.cx.ui.PageSelectorWidget.prototype.getNearbyPages = function () {\n\tvar coords = this.getUserCoordinates();\n\n\tif ( !coords ) {\n\t\t// If we can't get user coordinates, use `$.when()` to create and return resolved promise.\n\t\t// We return resolved promise, because we don't want `$.when` in populateSuggestions() method\n\t\t// to fail if we don't have valid coordinates.\n\t\treturn $.when();\n\t}\n\n\treturn this.siteMapper.getApi( this.language ).get( {\n\t\taction: 'query',\n\t\tprop: [ 'pageimages', 'description', 'langlinks', 'langlinkscount' ],\n\t\tgenerator: 'geosearch',\n\t\tpiprop: 'thumbnail',\n\t\tpithumbsize: 80,\n\t\tlllang: this.targetLanguage,\n\t\tggscoord: coords,\n\t\tggsradius: 1000, // Search radius in meters\n\t\tggslimit: 3,\n\t\tggsnamespace: mw.config.get( 'wgNamespaceIds' )[ '' ] // Main namespace\n\t} ).then( function ( data ) { return data; } );\n};\n\n/**\n * Get the thumbnail image, description and langlinks count for pages with the given titles.\n *\n * @return {jQuery.Promise}\n */\nmw.cx.ui.PageSelectorWidget.prototype.getPageDetails = function () {\n\tvar self = this;\n\n\treturn this.getRecentlyEditedArticleTitles().then( function ( titles ) {\n\t\treturn self.siteMapper.getApi( self.language ).get( {\n\t\t\taction: 'query',\n\t\t\ttitles: titles,\n\t\t\tprop: [ 'pageimages', 'description', 'langlinks', 'langlinkscount' ],\n\t\t\tpiprop: 'thumbnail',\n\t\t\tpilimit: 10,\n\t\t\tpithumbsize: 80,\n\t\t\tlllang: self.targetLanguage\n\t\t} ).then( function ( data ) { return data; } );\n\t}, function ( error ) {\n\t\tmw.log( 'Error getting recent edit titles. ' + error );\n\t} );\n};\n\n/**\n * Gets recently edited articles by user (using usercontribs API)\n *\n * @return {jQuery.Promise}\n */\nmw.cx.ui.PageSelectorWidget.prototype.getRecentlyEditedArticleTitles = function () {\n\tvar params, userName = mw.config.get( 'wgUserName' ),\n\t\tapi = this.siteMapper.getApi( this.language );\n\n\tparams = {\n\t\taction: 'query',\n\t\tlist: [ 'usercontribs' ],\n\t\tucuser: userName,\n\t\tuclimit: 3,\n\t\tucnamespace: mw.config.get( 'wgNamespaceIds' )[ '' ], // Main namespace\n\t\tucprop: 'title'\n\t};\n\n\treturn api.get( params ).then( function ( data ) {\n\t\tvar articles = OO.getProp( data, 'query', 'usercontribs' );\n\n\t\tif ( !articles ) {\n\t\t\treturn $.Deferred().reject( 'No recent user contributions' ).promise();\n\t\t}\n\n\t\treturn articles.map( function ( article ) {\n\t\t\treturn article.title;\n\t\t} );\n\t}, function ( error ) {\n\t\tmw.log( 'Error getting recent edits for ' + userName + '. ' + error );\n\t} );\n};\n\nmw.cx.ui.PageSelectorWidget.prototype.setExcludedNamespaces = function ( excludedNamespaces ) {\n\tthis.excludedNamespaces = excludedNamespaces;\n};\n\n/**\n * Validate the current query against excluded namespaces,\n *\n * @param {string} query\n * @return {boolean} True if validation passes. False otherwise.\n */\nmw.cx.ui.PageSelectorWidget.prototype.isValidNamespace = function ( query ) {\n\treturn query.indexOf( ':' ) < 0 ||\n\t\tthis.excludedNamespaces.every( function ( namespace ) {\n\t\t\treturn query.split( ':' )[ 0 ].replace( '_', ' ' ).toLocaleLowerCase() !==\n\t\t\tnamespace.toLocaleLowerCase();\n\t\t} );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.PageTitleWidget.js","messages":[{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":9,"column":null,"nodeType":"Block","endLine":9,"endColumn":null}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Page title widget. This is the header for source and translation columns.\n * It is editable (contenteditable) for translation and readonly for source page.\n *\n * @class\n * @extends OO.ui.MultilineTextInputWidget\n * @mixins ve.ce.CXLintableNode\n * @param {mw.cx.dm.PageTitleModel} model\n * @param {Object} [config] Configuration object\n */\nmw.cx.ui.PageTitleWidget = function ( model, config ) {\n\t// Configuration initialization\n\tconfig = $.extend( config, {\n\t\tclasses: [ 'cx-pagetitle' ],\n\t\ttype: 'text',\n\t\tautosize: true\n\t} );\n\n\tthis.model = model;\n\n\t// Parent constructor\n\tmw.cx.ui.PageTitleWidget.super.call( this, config );\n\n\t// Mixin constructor\n\tve.ce.CXLintableNode.call( this );\n\n\tthis.validTitle = null;\n\n\t// Events\n\t$( this.getElementWindow() ).on(\n\t\t'resize',\n\t\tOO.ui.throttle( this.onWindowResize.bind( this ), 300 )\n\t);\n\n\tthis.getFocusableElement().off( 'focus' ).on( 'focus', this.emit.bind( this, 'focus' ) );\n\t$( document )\n\t\t.off( 'blur', '.cx-pagetitle' )\n\t\t.on( 'blur', '.cx-pagetitle', this.emit.bind( this, 'blur' ) );\n\tthis.connect( this, {\n\t\tchange: OO.ui.debounce( this.validateTitle.bind( this ), 300 )\n\t} );\n};\n\n/* Setup */\n\nOO.inheritClass( mw.cx.ui.PageTitleWidget, OO.ui.MultilineTextInputWidget );\nOO.mixinClass( mw.cx.ui.PageTitleWidget, ve.ce.CXLintableNode );\n\n/* Methods */\n\n/**\n * @return {mw.cx.dm.PageTitleModel}\n */\nmw.cx.ui.PageTitleWidget.prototype.getModel = function () {\n\treturn this.model;\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.PageTitleWidget.prototype.getFocusableElement = function () {\n\treturn this.$tabIndexed;\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.PageTitleWidget.prototype.blursEditingSurface = function () {\n\treturn true;\n};\n\nmw.cx.ui.PageTitleWidget.prototype.validateTitle = function ( value ) {\n\t// Empty array in param resolves all issues with the title\n\tthis.model.resolveTranslationIssues( [] );\n\n\tif ( !mw.Title.newFromText( value ) ) {\n\t\tthis.model.addTranslationIssues( [ value === '' ? this.getEmptyTitleError() : this.getInvalidCharacterError() ] );\n\t\treturn;\n\t}\n\n\tve.init.platform.linkCache.get( this.getValue() ).then( function ( result ) {\n\t\tif ( result.missing ) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.model.addTranslationIssues( [ this.getExistingTitleWarning() ] );\n\t}.bind( this ) );\n};\n\nmw.cx.ui.PageTitleWidget.prototype.getExistingTitleWarning = function () {\n\treturn {\n\t\tname: 'existing-title',\n\t\tmessage: mw.message(\n\t\t\t'cx-tools-linter-page-exists-message',\n\t\t\t$( '<a>' ).prop( 'href', mw.util.getUrl( this.getValue() ) ).text( this.getValue() )\n\t\t),\n\t\tmessageInfo: {\n\t\t\ttitle: mw.msg( 'cx-tools-linter-page-exists' ),\n\t\t\t// FIXME: Point to the more informative page about overwriting content\n\t\t\thelp: 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Editing_pages',\n\t\t\tresolvable: true\n\t\t}\n\t};\n};\n\nmw.cx.ui.PageTitleWidget.prototype.getEmptyTitleError = function () {\n\treturn {\n\t\tname: 'empty-title',\n\t\tmessage: mw.message( 'cx-tools-linter-empty-title-message' ),\n\t\tmessageInfo: {\n\t\t\ttitle: mw.msg( 'cx-tools-linter-empty-title' ),\n\t\t\t// FIXME: Link to localized help page\n\t\t\thelp: 'https://en.wikipedia.org/wiki/Wikipedia:Page_name',\n\t\t\ttype: 'error'\n\t\t}\n\t};\n};\n\nmw.cx.ui.PageTitleWidget.prototype.getInvalidCharacterError = function () {\n\tvar titleObj = mw.Title.newFromUserInput( this.getValue() ),\n\t\tmessageData = {\n\t\t\tname: 'invalid-title',\n\t\t\tmessage: mw.message( 'cx-tools-linter-invalid-character-message' ),\n\t\t\tmessageInfo: {\n\t\t\t\ttitle: mw.msg( 'cx-tools-linter-invalid-character' ),\n\t\t\t\t// FIXME: Link to localized help page\n\t\t\t\thelp: 'https://en.wikipedia.org/wiki/Wikipedia:Page_name',\n\t\t\t\ttype: 'error'\n\t\t\t}\n\t\t};\n\n\tif ( titleObj ) {\n\t\tthis.validTitle = titleObj.title.replace( /_/g, ' ' );\n\n\t\tmessageData.messageInfo = $.extend( messageData.messageInfo, {\n\t\t\tresolvable: true,\n\t\t\tactionIcon: 'trash',\n\t\t\tactionLabel: mw.msg( 'cx-tools-linter-invalid-character-action' ),\n\t\t\taction: this.fixTitle.bind( this )\n\t\t} );\n\t}\n\n\treturn messageData;\n};\n\nmw.cx.ui.PageTitleWidget.prototype.fixTitle = function () {\n\tthis.setValue( this.validTitle );\n};\n\n/**\n * Handle key press events. Disable enter key presses.\n *\n * @private\n * @param {jQuery.Event} e Key press event\n * @fires enter If enter key is pressed and input is not multiline\n * @return {boolean}\n */\nmw.cx.ui.PageTitleWidget.prototype.onKeyPress = function ( e ) {\n\tif ( e.which === OO.ui.Keys.ENTER ) {\n\t\tthis.emit( 'enter', e );\n\t\treturn false;\n\t}\n};\n\n/**\n * Window resize handler\n */\nmw.cx.ui.PageTitleWidget.prototype.onWindowResize = function () {\n\t// We need to trick the parent adjustSize() method not to exit early\n\t// because it checks if input string has changed by comparing with\n\t// cache value. If there was no limitation here, we would just\n\t// register adjustSize() method as window resize handler.\n\tthis.valCache = null;\n\tthis.adjustSize();\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.PersonalMenuWidget.js","messages":[{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":15,"column":null,"nodeType":"Block","endLine":15,"endColumn":null}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n* Content Translation UserInterface PersonalMenuWidget class.\n*\n* @copyright See AUTHORS.txt\n* @license GPL-2.0-or-later\n*/\n\n'use strict';\n\n/**\n * Creates an mw.cx.ui.PersonalMenuWidget object.\n *\n * @class\n * @extends OO.ui.DropdownWidget\n * @mixins OO.ui.mixin.FlaggedElement\n *\n * @constructor\n * @param {Object} [config] Configuration options\n */\nmw.cx.ui.PersonalMenuWidget = function PersonalMenuWidget( config ) {\n\t// Parent constructor\n\tmw.cx.ui.PersonalMenuWidget.super.call( this, config );\n\t// Mixin constructor\n\tOO.ui.mixin.FlaggedElement.call( this, config );\n\n\t// Initialization\n\tthis.$element.addClass( 'mw-cx-ui-PersonalMenuWidget' );\n\tif ( mw.user.isAnon() ) {\n\t\tthis.$element.addClass( 'mw-cx-ui-PersonalMenuWidget--anon' );\n\t}\n};\n\n/* Inheritance */\nOO.inheritClass( mw.cx.ui.PersonalMenuWidget, OO.ui.DropdownWidget );\nOO.mixinClass( mw.cx.ui.PersonalMenuWidget, OO.ui.mixin.FlaggedElement );\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.PersonalMenuWidget.prototype.onMenuSelect = function ( item ) {\n\t// Navigate browser to a new page\n\tlocation.href = item.getData();\n\t// Don't call parent to avoid changing handle\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.PersonalMenuWidget.prototype.onMenuToggle = function () {\n\tthis.menu.$element.css( 'min-width', this.$element.outerWidth() );\n\t// Parent method\n\tmw.cx.ui.PersonalMenuWidget.super.prototype.onMenuToggle.apply( this, arguments );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.PublishSettingsWidget.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.TitleOptionWidget.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.TranslationIssueWidget.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.TranslationToolWidget.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/util/ext.cx.util.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/util/ext.cx.util.selection.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/util/mw.cx.util.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/mixins/ve.ce.CXLintableNode.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/mixins/ve.ce.CXPendingNode.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXBlockImageNode.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXImageCaptionNode.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXLinkAnnotation.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXPlaceholderNode.js","messages":[{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":11,"column":null,"nodeType":"Block","endLine":11,"endColumn":null},{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":12,"column":null,"nodeType":"Block","endLine":12,"endColumn":null}],"errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Empty placeholder Section in the target document, corresponding to a source Section\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @extends ve.ce.LeafNode\n * @mixins ve.ce.FocusableNode\n * @mixins ve.ce.CXPendingNode\n * @constructor\n */\nve.ce.CXPlaceholderNode = function VeCeCXPlaceholderNode() {\n\t// Parent constructor\n\tve.ce.CXPlaceholderNode.super.apply( this, arguments );\n\t// Mixin constructors\n\tve.ce.FocusableNode.call( this );\n\tve.ce.CXPendingNode.call( this );\n\n\tthis.button = new ve.ui.NoFocusButtonWidget( {\n\t\tlabel: ve.msg( 'cx-translation-add-translation' ),\n\t\ticon: 'add',\n\t\tframed: false\n\t} );\n\tthis.button.connect( this, { click: 'onFocusableMouseDown' } );\n\n\tthis.model.connect( this, {\n\t\tbeforeTranslation: 'onBeforeTranslation',\n\t\tafterTranslation: 'onAfterTranslation'\n\t} );\n\n\tthis.$element\n\t\t.addClass( 've-ce-cxPlaceholderNode' )\n\t\t.attr( 'id', this.model.getAttribute( 'cxid' ) )\n\t\t.append( this.button.$element );\n\tthis.active = false;\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ce.CXPlaceholderNode, ve.ce.LeafNode );\nOO.mixinClass( ve.ce.CXPlaceholderNode, ve.ce.FocusableNode );\nOO.mixinClass( ve.ce.CXPlaceholderNode, ve.ce.CXPendingNode );\n\n/* Static Properties */\n\nve.ce.CXPlaceholderNode.static.tagName = 'section';\n\nve.ce.CXPlaceholderNode.static.name = 'cxPlaceholder';\n\n/* Methods */\n\nve.ce.CXPlaceholderNode.prototype.onFocusableMouseDown = function ( e ) {\n\tif ( this.focusableSurface.isReadOnly() ) {\n\t\treturn;\n\t}\n\tif ( this.active || ( e && e.which !== OO.ui.MouseButtons.LEFT ) ) {\n\t\treturn;\n\t}\n\tthis.executeCommand();\n};\n\nve.ce.CXPlaceholderNode.prototype.executeCommand = function () {\n\tthis.active = true;\n\tthis.getDocument().emit( 'activatePlaceholder', this );\n};\n\nve.ce.CXPlaceholderNode.prototype.createHighlights = function () {\n};\n\nve.ce.CXPlaceholderNode.prototype.onBeforeTranslation = function () {\n\tthis.button.toggle( false );\n\tthis.setPending( true );\n};\n\nve.ce.CXPlaceholderNode.prototype.onAfterTranslation = function () {\n\tthis.setPending( false );\n\tthis.button.toggle( true );\n\tthis.active = false;\n};\n\n/* Registration */\n\nve.ce.nodeFactory.register( ve.ce.CXPlaceholderNode );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXReferenceNode.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXSectionNode.js","messages":[{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":7,"column":null,"nodeType":"Block","endLine":7,"endColumn":null},{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":8,"column":null,"nodeType":"Block","endLine":8,"endColumn":null}],"errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Node representing an adapted section\n *\n * @class\n * @constructor\n * @extends ve.ce.SectionNode\n * @mixins ve.ce.CXPendingNode\n * @mixins ve.ce.CXLintableNode\n *\n * @param {ve.dm.CXSectionNode} model\n */\nve.ce.CXSectionNode = function VeCeCXSectionNode() {\n\t// Parent constructor\n\tve.ce.CXSectionNode.super.apply( this, arguments );\n\t// Mixin constructors\n\tve.ce.CXPendingNode.call( this );\n\tve.ce.CXLintableNode.call( this );\n\n\tthis.$element\n\t\t.attr( {\n\t\t\tid: this.model.getAttribute( 'cxid' ),\n\t\t\trel: 'cx:Section'\n\t\t} )\n\t\t.addClass( 've-ce-cxSectionNode' );\n\n\tthis.getFocusableElement().on( {\n\t\tfocus: this.emit.bind( this, 'focus' ),\n\t\tblur: this.emit.bind( this, 'blur' )\n\t} );\n\n\tthis.model.connect( this, {\n\t\tbeforeTranslation: 'onBeforeTranslation',\n\t\tafterTranslation: 'onAfterTranslation'\n\t} );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ce.CXSectionNode, ve.ce.SectionNode );\nOO.mixinClass( ve.ce.CXSectionNode, ve.ce.CXPendingNode );\nOO.mixinClass( ve.ce.CXSectionNode, ve.ce.CXLintableNode );\n\n/* Static Properties */\n\nve.ce.CXSectionNode.static.tagName = 'section';\n\nve.ce.CXSectionNode.static.name = 'cxSection';\n\n/* Methods */\n\nve.ce.CXSectionNode.prototype.onBeforeTranslation = function () {\n\tthis.setPending( true );\n};\n\nve.ce.CXSectionNode.prototype.onAfterTranslation = function () {\n\tthis.setPending( false );\n};\n\n/**\n * @inheritdoc\n */\nve.ce.CXSectionNode.prototype.getFocusableElement = function () {\n\tvar firstChild = OO.getProp( this, 'children', 0 );\n\n\t// Returning this.$element causes problems for block transclusion nodes. See T226247\n\tif ( firstChild instanceof ve.ce.CXTransclusionBlockNode ) {\n\t\treturn firstChild.$focusable;\n\t}\n\n\t// Parent (mixin) method\n\treturn ve.ce.CXLintableNode.prototype.getFocusableElement.call( this );\n};\n\n/* Registration */\n\nve.ce.nodeFactory.register( ve.ce.CXSectionNode );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXSentenceSegmentAnnotation.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXTransclusionNode.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/mixins/ve.dm.CXLintableNode.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXBlockImageNode.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXImageCaptionNode.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXLinkAnnotation.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXPlaceholderNode.js","messages":[{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":6,"column":null,"nodeType":"Block","endLine":6,"endColumn":null},{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":7,"column":null,"nodeType":"Block","endLine":7,"endColumn":null}],"errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Empty placeholder Section in the target document, corresponding to a source Section\n *\n * @class\n * @extends ve.dm.LeafNode\n * @mixins ve.dm.FocusableNode\n * @mixins ve.dm.CXTranslationUnitModel\n * @constructor\n */\nve.dm.CXPlaceholderNode = function VeDmCXPlaceholderNode() {\n\t// Parent constructor\n\tve.dm.CXPlaceholderNode.super.apply( this, arguments );\n\t// Mixin constructors\n\tve.dm.FocusableNode.call( this );\n\tve.dm.CXTranslationUnitModel.call( this );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.dm.CXPlaceholderNode, ve.dm.LeafNode );\nOO.mixinClass( ve.dm.CXPlaceholderNode, ve.dm.FocusableNode );\nOO.mixinClass( ve.dm.CXPlaceholderNode, ve.dm.CXTranslationUnitModel );\n\n/* Static Properties */\n\nve.dm.CXPlaceholderNode.static.name = 'cxPlaceholder';\n\nve.dm.CXPlaceholderNode.static.matchTagNames = [ 'section' ];\n\nve.dm.CXPlaceholderNode.static.matchRdfaTypes = [ 'cx:Placeholder' ];\n\n/* Static Methods */\n\nve.dm.CXPlaceholderNode.static.toDataElement = function ( domElements ) {\n\treturn { type: this.name, attributes: { cxid: domElements[ 0 ].id } };\n};\n\nve.dm.CXPlaceholderNode.static.toDomElements = function ( dataElement, doc ) {\n\tvar sectionNode = doc.createElement( 'section' );\n\tsectionNode.setAttribute( 'rel', 'cx:Placeholder' );\n\tsectionNode.setAttribute( 'id', dataElement.attributes.cxid );\n\treturn [ sectionNode ];\n};\n\n/* Methods */\n\nve.dm.CXPlaceholderNode.prototype.canHaveSlugBefore = function () {\n\treturn false;\n};\n\nve.dm.CXPlaceholderNode.prototype.canHaveSlugAfter = function () {\n\treturn false;\n};\n\n/* Registration */\n\nve.dm.modelRegistry.register( ve.dm.CXPlaceholderNode );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXReferenceNode.js","messages":[{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":12,"column":null,"nodeType":"Block","endLine":12,"endColumn":null}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Node representing a reference\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @constructor\n * @extends ve.dm.MWReferenceNode\n * @mixins ve.dm.CXLintableNode\n */\nve.dm.CXReferenceNode = function VeDmCXReferenceNode() {\n\t// Parent constructor\n\tve.dm.CXReferenceNode.super.apply( this, arguments );\n\n\t// Mixin constructor\n\tve.dm.CXLintableNode.call( this );\n\n\t// attach is fired when section is filled with MT, but not on restoring.\n\tthis.connect( this, {\n\t\tattach: 'onAttach',\n\t\tdetach: 'onDetach'\n\t} );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.dm.CXReferenceNode, ve.dm.MWReferenceNode );\nOO.mixinClass( ve.dm.CXReferenceNode, ve.dm.CXLintableNode );\n\n/* Static Methods */\n\n/**\n * @inheritdoc\n */\nve.dm.CXReferenceNode.static.toDataElement = function ( domElements ) {\n\tvar dataElement, cxData,\n\t\tcxDataJSON = domElements[ 0 ].getAttribute( 'data-cx' );\n\n\t// Parent method\n\tdataElement = ve.dm.CXReferenceNode.super.static.toDataElement.apply( this, arguments );\n\n\ttry {\n\t\tcxData = cxDataJSON ? JSON.parse( cxDataJSON ) : {};\n\t} catch ( e ) {\n\t\tcxData = {};\n\t}\n\n\tdataElement.attributes.cx = cxData;\n\n\treturn dataElement;\n};\n\n/**\n * @inheritdoc\n */\nve.dm.CXReferenceNode.static.toDomElements = function ( dataElement ) {\n\tvar elements = ve.dm.CXReferenceNode.super.static.toDomElements.apply( this, arguments ),\n\t\tcxData = OO.getProp( dataElement, 'attributes', 'cx' );\n\n\tif ( cxData ) {\n\t\telements[ 0 ].setAttribute( 'data-cx', JSON.stringify( cxData ) );\n\t}\n\n\treturn elements;\n};\n\n/* Methods */\n\nve.dm.CXReferenceNode.prototype.onAttach = function () {\n\tvar sectionNode, title, message, cxData;\n\n\tsectionNode = this.findParent( ve.dm.CXSectionNode );\n\t// When section content is replaced, this happens:\n\t// 1) attach is called with VeDmSectionNode and we cannot access VeDmCXSectionNode\n\t// 2) detach is called with VeDmCXSectionNode and we unregister our warning\n\tif ( !sectionNode ) {\n\t\treturn;\n\t}\n\n\t// This is just a sanity check, since source column does not have data-cx\n\tif ( !sectionNode.isTargetSection() ) {\n\t\treturn;\n\t}\n\n\tcxData = this.getAdaptationInfo();\n\tif ( cxData.partial === true ) {\n\t\ttitle = mw.msg( 'cx-tools-linter-incomplete-reference' );\n\t\tmessage = mw.message( 'cx-tools-linter-incomplete-reference-message' );\n\t} else if ( cxData.adapted === false ) {\n\t\ttitle = mw.msg( 'cx-tools-linter-reference' );\n\t\tmessage = mw.message( 'cx-tools-linter-reference-message' );\n\t} else {\n\t\tif ( cxData.adapted !== true ) {\n\t\t\tmw.log.warn(\n\t\t\t\t'[CX] Reference adaptation status is missing for the reference in section ' +\n\t\t\t\tsectionNode.getId()\n\t\t\t);\n\t\t}\n\n\t\treturn;\n\t}\n\n\tsectionNode.addTranslationIssues( [ {\n\t\tname: 'reference',\n\t\tmessage: message,\n\t\tmessageInfo: {\n\t\t\ttitle: title,\n\t\t\tresolvable: true,\n\t\t\thelp: 'https://www.mediawiki.org/wiki/Special:MyLanguage/Content_translation/Templates'\n\t\t}\n\t} ] );\n};\n\nve.dm.CXReferenceNode.prototype.onDetach = function ( parent ) {\n\tif ( parent instanceof ve.dm.CXSectionNode && parent.isTargetSection() ) {\n\t\tparent.resolveTranslationIssues( [ 'reference' ] );\n\t}\n};\n\n/**\n * Get the adaptation info supplied by cxserver\n *\n * @return {Object} The adaptation info\n */\nve.dm.CXReferenceNode.prototype.getAdaptationInfo = function () {\n\tvar nodeGroup, kinNodes, contentsUsed,\n\t\tcxData = {};\n\n\tcontentsUsed = this.getAttribute( 'contentsUsed' );\n\t// If contentsUsed is false, then this reference is a reused reference.\n\t// The adaptation status needs to be extracted from original reference.\n\tif ( contentsUsed ) {\n\t\tcxData = this.getAttribute( 'cx' ) || {};\n\t} else {\n\t\tnodeGroup = this.doc.getInternalList().getNodeGroup(\n\t\t\tthis.getAttribute( 'listGroup' )\n\t\t);\n\t\tkinNodes = nodeGroup && nodeGroup.keyedNodes[ this.getAttribute( 'listKey' ) ];\n\t\t// See if there is any kin nodes and if so, use the first one.\n\t\tif ( kinNodes && kinNodes.length > 0 ) {\n\t\t\tcxData = kinNodes[ 0 ].getAttribute( 'cx' ) || {};\n\t\t}\n\t}\n\treturn cxData;\n};\n\n/* Registration */\n\nve.dm.modelRegistry.register( ve.dm.CXReferenceNode );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXSectionNode.js","messages":[{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":12,"column":null,"nodeType":"Block","endLine":12,"endColumn":null},{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":13,"column":null,"nodeType":"Block","endLine":13,"endColumn":null}],"errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Node representing an adapted section\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @constructor\n * @extends ve.dm.SectionNode\n * @mixins ve.dm.CXTranslationUnitModel\n * @mixins ve.dm.CXLintableNode\n */\nve.dm.CXSectionNode = function VeDmCXSectionNode() {\n\t// Parent constructor\n\tve.dm.CXSectionNode.super.apply( this, arguments );\n\n\t// Mixin constructors\n\tve.dm.CXTranslationUnitModel.call( this );\n\tve.dm.CXLintableNode.call( this );\n\n\tthis.translation = this.getTranslation();\n\tthis.isModified = false;\n\n\tthis.connect( this, {\n\t\tupdate: 'onUpdate',\n\t\tafterRender: 'onAfterRender'\n\t} );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.dm.CXSectionNode, ve.dm.SectionNode );\nOO.mixinClass( ve.dm.CXSectionNode, ve.dm.CXTranslationUnitModel );\nOO.mixinClass( ve.dm.CXSectionNode, ve.dm.CXLintableNode );\n\n/* Static Properties */\n\nve.dm.CXSectionNode.static.name = 'cxSection';\n\nve.dm.CXSectionNode.static.defaultAttributes = {\n\tcxsource: 'source'\n};\n\nve.dm.CXSectionNode.static.matchTagNames = [ 'section' ];\n\nve.dm.CXSectionNode.static.matchRdfaTypes = [ 'cx:Section' ];\n\n/* Static Methods */\n\nve.dm.CXSectionNode.static.toDataElement = function ( domElements ) {\n\t// Parent method\n\tvar dataElement = ve.dm.CXSectionNode.super.static.toDataElement.apply( this, arguments );\n\n\tdataElement.attributes.cxid = domElements[ 0 ].id;\n\tdataElement.attributes.cxsource = domElements[ 0 ].dataset.mwCxSource;\n\treturn dataElement;\n};\n\nve.dm.CXSectionNode.static.toDomElements = function ( dataElement ) {\n\tvar elements = ve.dm.CXSectionNode.super.static.toDomElements.apply( this, arguments );\n\telements[ 0 ].setAttribute( 'rel', 'cx:Section' );\n\telements[ 0 ].setAttribute( 'id', dataElement.attributes.cxid );\n\telements[ 0 ].dataset.mwCxSource = dataElement.attributes.cxsource;\n\treturn elements;\n};\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.dm.CXSectionNode.prototype.getId = function () {\n\treturn this.getSectionNumber();\n};\n\nve.dm.CXSectionNode.prototype.onAfterRender = function () {\n\tif ( this.isTargetSection() ) {\n\t\tsetTimeout( function () {\n\t\t\tthis.translation.emit( 'afterRender' );\n\t\t}.bind( this ) );\n\t}\n};\n\nve.dm.CXSectionNode.prototype.onUpdate = function () {\n\tif ( this.isTargetSection() ) {\n\t\t// Update is triggered by a tree modification. Wait for the whole tree modification\n\t\t// to finish, e.g. if there are relevant internal list changes to wait for.\n\t\tsetTimeout( this.emitSectionChange.bind( this ) );\n\t}\n};\n\nve.dm.CXSectionNode.prototype.emitSectionChange = function () {\n\tthis.translation.emit( 'sectionChange', this.getSectionId() );\n};\n\n/**\n * Check whether this section has modifications on top of initial machine translation.\n * Note: The modifications on the sections are handled after a few milliseconds delay.\n *\n * @return {boolean}\n */\nve.dm.CXSectionNode.prototype.hasUserModifications = function () {\n\treturn this.isModified;\n};\n\n/**\n * Set a flag to indicate that this section has user modifications.\n *\n * @param {boolean} isModified\n */\nve.dm.CXSectionNode.prototype.setHasUserModifications = function ( isModified ) {\n\tthis.isModified = isModified;\n};\n\n/**\n * Whether the section is target section or not.\n *\n * @return {boolean}\n */\nve.dm.CXSectionNode.prototype.isTargetSection = function () {\n\treturn this.translation && this.translation.targetDoc === this.getDocument();\n};\n\n/**\n * Get the original content source.\n * Example: Apertium\n *\n * @return {string}\n */\nve.dm.CXSectionNode.prototype.getOriginalContentSource = function () {\n\treturn this.getAttribute( 'cxsource' );\n};\n\n/**\n * ...\n *\n * @param {string} source One of 'source', 'scratch' or name of MT engine.\n */\nve.dm.CXSectionNode.prototype.setOriginalContentSource = function ( source ) {\n\tthis.element.attributes.cxsource = source;\n};\n\n/* Registration */\n\nve.dm.modelRegistry.register( ve.dm.CXSectionNode );\n\n/**\n * HACK: We need to improve how suggestedParentNodes works\n */\n\nve.dm.MWHeadingNode.static.suggestedParentNodeTypes.push( 'cxSection' );\nve.dm.MWPreformattedNode.static.suggestedParentNodeTypes.push( 'cxSection' );\nve.dm.MWTableNode.static.suggestedParentNodeTypes.push( 'cxSection' );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXSentenceSegmentAnnotation.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXTransclusionNode.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXTranslationUnitModel.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/init/ve.init.mw.CXTarget.js","messages":[{"ruleId":"no-shadow","severity":1,"message":"'model' is already declared in the upper scope on line 710 column 3.","line":724,"column":7,"nodeType":"Identifier","messageId":"noShadow","endLine":724,"endColumn":12},{"ruleId":"no-shadow","severity":1,"message":"'tx' is already declared in the upper scope on line 784 column 42.","line":802,"column":34,"nodeType":"Identifier","messageId":"noShadow","endLine":802,"endColumn":36},{"ruleId":"no-shadow","severity":1,"message":"'doc' is already declared in the upper scope on line 786 column 3.","line":802,"column":38,"nodeType":"Identifier","messageId":"noShadow","endLine":802,"endColumn":41}],"errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * CX Target\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @param {mw.cx.ui.TranslationView} translationView\n * @param {Object} [config] Configuration object\n * TODO: Only pass optional parameters in config\n * @cfg {mw.cx.SiteMapper} siteMapper\n * @cfg {mw.cx.MachineTranslationManager} MTManager\n * @cfg {mw.cx.MachineTranslationService} MTService\n * TODO: toolbarConfig\n */\nve.init.mw.CXTarget = function VeInitMwCXTarget( translationView, config ) {\n\t// Configuration initialization\n\tthis.config = config = $.extend( {}, config, {\n\t\tcontinuous: true,\n\t\texpanded: false,\n\t\tscrollable: false,\n\t\tpadded: false\n\t} );\n\tconfig.toolbarConfig = $.extend(\n\t\t{ shadow: true, actions: true, floatable: false, $overlay: true },\n\t\tconfig.toolbarConfig\n\t);\n\t// Parent constructor\n\tve.init.mw.CXTarget.super.call( this, config );\n\n\tthis.MTManager = config.MTManager;\n\tthis.MTService = config.MTService;\n\tthis.siteMapper = config.siteMapper;\n\tthis.requestManager = config.requestManager;\n\n\tthis.errorsInTranslation = false;\n\n\t// @var {mw.cx.dm.Translation}\n\tthis.translation = null;\n\t// @var {mw.cx.ui.TranslationView}\n\tthis.translationView = translationView;\n\tthis.publishButton = null;\n\t// @var {string}\n\tthis.pageName = this.translationView.targetColumn.getTitle();\n\t// @var {ve.ui.CXSurface}\n\tthis.sourceSurface = null;\n\t// @var {ve.ui.CXSurface}\n\tthis.targetSurface = null;\n\t// @var {Object}\n\tthis.contentSourceCache = {};\n\t// @var {Object}\n\tthis.translationRequestCache = {};\n\t// Complex dialog is the dialog with VE surface.\n\t// In order to reset the overlay classes, which move overlay, we want only the first\n\t// complex dialog to reset these classes, since complex dialogs can be nested. See T193587\n\tthis.complexDialogOpened = false;\n\tthis.contextStack = [];\n\n\tthis.$element\n\t\t.addClass( 've-init-mw-cxTarget' )\n\t\t.append( this.translationView.$element );\n\n\tthis.debounceAlignSectionPairs = OO.ui.debounce(\n\t\tthis.alignSectionPairs.bind( this ),\n\t\t500\n\t);\n\n\tthis.translationView.connect( this, {\n\t\thasTranslationIssues: 'onTranslationIssues'\n\t} );\n\n\tthis.translationView.targetColumn.connect( this, {\n\t\ttitleChange: 'onTargetTitleChange'\n\t} );\n\n\tthis.connect( this, {\n\t\tcontentChange: 'onChange',\n\t\tsurfaceReady: 'onSurfaceReady'\n\t} );\n\tmw.hook( 'mw.cx.draft.restored' ).add( this.onTranslationRestore.bind( this ) );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.init.mw.CXTarget, ve.init.mw.Target );\n\n/* Events */\n\n/**\n * @event targetTitleChange\n *\n * Target title of the translation has changed.\n */\n\n/**\n * @event publish\n *\n * User clicked on \"Publish\" button to start the publication process.\n */\n\n/**\n * @event contentChange\n *\n * Content in the document has changed.\n */\n\n/**\n * @event changeContentSource\n *\n * Some target section's content source has changed.\n * @param {number} sectionNumber\n */\n\n/**\n * @event namespaceChange\n *\n * Namespace of the target title has changed.\n * @param {number} namespaceId ID of the new namespace\n */\n\n/* Static Properties */\n\nve.init.mw.CXTarget.static.name = 'cx';\n\nve.init.mw.CXTarget.static.actionGroups = [\n\t// Publish settings\n\t{\n\t\tname: 'publish',\n\t\tinclude: [ 'publishSettings', 'publish' ]\n\t}\n];\n\nve.init.mw.CXTarget.static.translationToolbarGroups = [\n\t{\n\t\tname: 'cx-mt',\n\t\ttype: 'menu',\n\t\tinclude: [ { group: 'mt' } ]\n\t}\n];\n\nve.init.mw.CXTarget.static.toolbarGroups = [\n\t// History\n\t{\n\t\tname: 'history',\n\t\tinclude: [ 'undo', 'redo' ]\n\t},\n\t// Style\n\t{\n\t\tname: 'style',\n\t\tclasses: [ 've-cx-toolbar-style' ],\n\t\ttype: 'list',\n\t\ticon: 'textStyle',\n\t\ttitle: OO.ui.deferMsg( 'visualeditor-toolbar-style-tooltip' ),\n\t\tinclude: [ { group: 'textStyle' }, 'language', 'clear' ],\n\t\tforceExpand: [ 'bold', 'italic', 'clear' ],\n\t\tpromote: [ 'bold', 'italic' ],\n\t\tdemote: [ 'strikethrough', 'code', 'underline', 'language', 'clear' ]\n\t},\n\t// Link\n\t{\n\t\tname: 'link',\n\t\tclasses: [ 've-cx-toolbar-link' ],\n\t\tinclude: [ 'link' ]\n\t},\n\t// Structure\n\t{\n\t\tname: 'structure',\n\t\tclasses: [ 've-cx-toolbar-structure' ],\n\t\ttype: 'list',\n\t\ticon: 'listBullet',\n\t\ttitle: OO.ui.deferMsg( 'visualeditor-toolbar-structure' ),\n\t\tinclude: [ { group: 'structure' } ],\n\t\tdemote: [ 'outdent', 'indent' ]\n\t},\n\t// Placeholder for reference tools (e.g. Cite)\n\t{\n\t\tname: 'reference'\n\t},\n\t// Insert\n\t{\n\t\tname: 'extra',\n\t\tclasses: [ 've-cx-toolbar-insert' ],\n\t\ticon: 'ellipsis',\n\t\tlabel: '',\n\t\tindicator: null,\n\t\ttype: 'list',\n\t\ttitle: OO.ui.deferMsg( 'visualeditor-toolbar-insert' ),\n\t\tinclude: '*',\n\t\texclude: [ { group: 'format' } ],\n\t\tforceExpand: [ 'media', 'transclusion', 'insertTable', 'specialCharacter' ],\n\t\tpromote: [ 'media', 'transclusion', 'insertTable', 'specialCharacter' ]\n\t}\n];\n\n/* Methods */\n\nve.init.mw.CXTarget.prototype.setupToolbar = function () {\n\t// Parent method\n\tve.init.mw.CXTarget.super.prototype.setupToolbar.apply( this, arguments );\n\n\tthis.publishButton = this.actionsToolbar.getToolGroupByName( 'publish' ).findItemFromData( 'publish' );\n\tmw.hook( 'mw.cx.progress' ).add( function ( weights ) {\n\t\tthis.publishButton.setDisabled( weights.any === 0 );\n\t}.bind( this ) );\n\n\tthis.translationView.translationHeader.$toolbar.append( this.toolbar.$actions );\n};\n\nve.init.mw.CXTarget.prototype.unbindHandlers = function () {\n\t// Parent method\n\tve.init.mw.CXTarget.super.prototype.unbindHandlers.call( this );\n\n\t$( this.getElementWindow() ).off( 'resize', this.debounceAlignSectionPairs );\n};\n\n/**\n * Present the source article and section placeholders\n *\n * @param {mw.cx.dm.Translation} translation\n */\nve.init.mw.CXTarget.prototype.setTranslation = function ( translation ) {\n\tvar sourceSurface, targetSurface;\n\n\tthis.translation = translation;\n\tthis.sourceSurface = sourceSurface = this.createSurface(\n\t\tthis.translation.sourceDoc,\n\t\tthis.getSurfaceConfig( {\n\t\t\tclasses: [ 've-ui-cxSurface', 've-ui-cxSourceSurface', 'mw-body-content' ]\n\t\t} )\n\t);\n\tthis.targetSurface = targetSurface = this.createSurface(\n\t\tthis.translation.targetDoc,\n\t\tthis.getSurfaceConfig( {\n\t\t\tclasses: [ 've-ui-cxSurface', 've-ui-cxTargetSurface', 'mw-body-content' ]\n\t\t} )\n\t);\n\tsourceSurface.setReadOnly( true );\n\tthis.translationView.sourceColumn.setTranslation( translation );\n\tthis.translationView.targetColumn.setTranslation( translation );\n\tthis.translationView.toolsColumn.setTranslation( translation );\n\tthis.clearSurfaces();\n\tthis.surfaces.push( targetSurface );\n\ttargetSurface.getDialogs().connect( this, {\n\t\topening: this.onDialogOpening.bind( this, targetSurface.getContext() ),\n\t\tclosing: 'onDialogClosing'\n\t} );\n\ttargetSurface.getView().connect( this, {\n\t\tfocus: [ 'onSurfaceViewFocus', targetSurface ]\n\t} );\n\tthis.setSurface( targetSurface );\n\ttargetSurface.getModel().getDocument().connect( this, {\n\t\ttransact: 'onDocumentTransact'\n\t} );\n\ttargetSurface.getView().getDocument().connect( this, {\n\t\tactivatePlaceholder: 'onDocumentActivatePlaceholder'\n\t} );\n\tthis.translationView.sourceColumn.attachSurface( sourceSurface );\n\tthis.translationView.targetColumn.attachSurface( targetSurface );\n\tsourceSurface.initialize();\n\ttargetSurface.initialize();\n\n\tthis.setupHighlighting( sourceSurface.getView().$element, targetSurface.getView().$element );\n\n\t$( this.getElementWindow() ).on( 'resize', this.debounceAlignSectionPairs );\n\t// Wait for document to render fully.\n\t// In mw.Target this happens after documentReady and a setTimeout,\n\t// but we don't use documentReady in this target.\n\tsetTimeout( this.surfaceReady.bind( this ) );\n\n\tthis.translation.connect( this, {\n\t\tsectionChange: this.debounceAlignSectionPairs,\n\t\tafterRender: this.debounceAlignSectionPairs\n\t} );\n};\n\nve.init.mw.CXTarget.prototype.setupHighlighting = function ( $sourceView, $targetView ) {\n\tvar $views = $( [ $sourceView[ 0 ], $targetView[ 0 ] ] );\n\n\t$views.on(\n\t\t{\n\t\t\tmouseenter: function () {\n\t\t\t\tvar segmentSelector;\n\n\t\t\t\tif ( this.classList.contains( 'cx-sentence-highlight' ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tsegmentSelector = '[data-segmentid=\"_\"]'.replace( '_', this.dataset.segmentid );\n\t\t\t\t$views.find( segmentSelector ).addClass( 'cx-sentence-highlight' );\n\t\t\t},\n\t\t\tmouseleave: function () {\n\t\t\t\t$views.find( '.cx-sentence-highlight' ).removeClass( 'cx-sentence-highlight' );\n\t\t\t}\n\t\t},\n\t\t'.cx-segment'\n\t);\n\n\t$targetView.on(\n\t\t{\n\t\t\tmouseenter: function () {\n\t\t\t\tvar sectionNumber;\n\n\t\t\t\tif ( this.classList.contains( 'cx-section-highlight' ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tsectionNumber = mw.cx.getSectionNumberFromSectionId( this.id );\n\t\t\t\tdocument.getElementById( 'cxSourceSection' + sectionNumber )\n\t\t\t\t\t.classList.add( 'cx-section-highlight' );\n\t\t\t},\n\t\t\tmouseleave: function () {\n\t\t\t\t$views.find( '.cx-section-highlight' ).removeClass( 'cx-section-highlight' );\n\t\t\t}\n\t\t},\n\t\t'[rel=\"cx:Placeholder\"]'\n\t);\n};\n\n/**\n * @inheritdoc\n */\nve.init.mw.CXTarget.prototype.createSurface = function ( dmDoc, config ) {\n\tvar surface, documentView;\n\n\tsurface = new ve.ui.CXSurface( dmDoc, this.translationView.toolsColumn, config );\n\n\t// eslint-disable-next-line mediawiki/class-doc\n\tsurface.$element.addClass( this.protectedClasses );\n\n\t// T164790\n\tdocumentView = surface.getView().getDocument();\n\t// The following classes are used here\n\t// * mw-content-ltr\n\t// * mw-content-rtl\n\tdocumentView.getDocumentNode().$element.addClass( 'mw-parser-output mw-content-' + documentView.getDir() );\n\n\t// If configuration object has 'inDialog' param, that means surface is created for usage\n\t// inside a modal dialog. Such complex dialogs need to have access to context tools inside\n\t// tools column, so we move the overlay. Also, other, non-complex tools, shouldn't be\n\t// showing. See T193587\n\tif ( config.inDialog ) {\n\t\tsurface.getDialogs().connect( this, {\n\t\t\topening: this.onDialogOpening.bind( this, surface.getContext() ),\n\t\t\tclosing: 'onDialogClosing'\n\t\t} );\n\n\t\tif ( !this.complexDialogOpened ) {\n\t\t\tthis.toggleContextTools( true );\n\t\t\tsurface.connect( this, { destroy: [ 'toggleContextTools', false ] } );\n\t\t}\n\t}\n\n\treturn surface;\n};\n\nve.init.mw.CXTarget.prototype.surfaceReady = function () {\n\t// Parent method\n\tve.init.mw.CXTarget.super.prototype.surfaceReady.apply( this, arguments );\n\n\tthis.debounceAlignSectionPairs();\n\n\t// Wait for 300ms because of debounced section alignment and then mark target surface as ready.\n\t// This CSS class is used in order to avoid showing initial placeholder\n\t// until it is sized to match corresponding source section.\n\tsetTimeout( function () {\n\t\tthis.targetSurface.$element.addClass( 've-ui-cxTargetSurface--ready' );\n\t}.bind( this ), 300 );\n};\n\n/**\n * Toggle the tools column CSS class which hides non-context tools.\n *\n * @param {boolean} state Toggle state of tools column class\n */\nve.init.mw.CXTarget.prototype.toggleContextTools = function ( state ) {\n\tthis.complexDialogOpened = state;\n\n\tthis.translationView.toolsColumn.toolContainer.$element.toggleClass( 'cx-column-tools-container--dialog', state );\n};\n\nve.init.mw.CXTarget.prototype.getTranslation = function () {\n\treturn this.translation;\n};\n\nve.init.mw.CXTarget.prototype.onDialogOpening = function ( context, dialog ) {\n\tvar headerHeight, scrollPosition;\n\n\tif ( !( dialog instanceof ve.ui.NodeDialog ) ) {\n\t\treturn;\n\t}\n\n\tthis.targetSurface.getGlobalOverlay().$element.addClass( 've-cx-ui-overlay-global' );\n\tthis.contextStack.push( context );\n\tcontext.connect( this, { afterContextChange: [ 'processContextItems', true ] } );\n\tthis.processContextItems( true );\n\n\t// We can use setSize( 'full' ) method here and it would work for some dialogs,\n\t// like reference dialog, but VE hardcodes the size for media dialog in\n\t// ve.ui.MWMediaDialog.prototype.switchPanels.\n\t// See T198390\n\tdialog.getSize = function () {\n\t\treturn 'full';\n\t};\n\n\t// Don't cover the top header with overlay when the user is at the top of the viewport\n\t// See T193587\n\theaderHeight = this.translationView.header.$element.outerHeight();\n\tscrollPosition = $( this.getElementWindow() ).scrollTop();\n\n\tif ( scrollPosition === 0 ) {\n\t\tdialog.$element.css( 'top', headerHeight );\n\t} else {\n\t\tdialog.$element.css( 'top', '' );\n\t}\n};\n\nve.init.mw.CXTarget.prototype.onDialogClosing = function () {\n\tthis.processContextItems( false );\n\tthis.contextStack.pop();\n\tif ( !this.contextStack.length ) {\n\t\tthis.targetSurface.getGlobalOverlay().$element.removeClass( 've-cx-ui-overlay-global' );\n\t}\n};\n\n/**\n * Process the stack of contexts, with their context items. Stack contains contexts\n * for nested modal dialogs, e.g. opening reference dialog, for a reference that\n * has a template, and then opening the template dialog.\n *\n * The logic when dialog is opening is to hide context item(s) for all but last\n * context inside a stack. Item(s) for last context are disabled.\n *\n * On the other side, when dialog is closing, context item(s) of last context are\n * enabled and visible, while context item(s) for second-to-last context are\n * disabled and visible. Item(s) for all other contexts are just toggled invisible.\n *\n * @param {boolean} disabled True if context items need to be disabled\n */\nve.init.mw.CXTarget.prototype.processContextItems = function ( disabled ) {\n\tvar process, lastItem = this.contextStack.length - 1;\n\n\t// Iterate all context(s) in a stack\n\tthis.contextStack.forEach( function ( context, index ) {\n\t\t// Whether items for second to last context in a stack should be disabled.\n\t\t// Used when dialog is closing.\n\t\tvar disableSecondToLast = !disabled && index === ( lastItem - 1 );\n\n\t\t// If item is last (during opening) or second-to-last (during closing)\n\t\tif ( index === lastItem || disableSecondToLast ) {\n\t\t\tprocess = function ( item ) {\n\t\t\t\titem.toggle( true );\n\t\t\t\titem.setDisabled( disabled || disableSecondToLast );\n\t\t\t\t// Set disabled state for action buttons\n\t\t\t\titem.actionButtons.getItems().forEach( function ( button ) {\n\t\t\t\t\tbutton.setDisabled( disabled || disableSecondToLast );\n\t\t\t\t} );\n\t\t\t};\n\t\t} else {\n\t\t\tprocess = function ( item ) {\n\t\t\t\titem.toggle( false );\n\t\t\t};\n\t\t}\n\n\t\tcontext.getItems().forEach( process );\n\t} );\n};\n\n/**\n * @fires targetTitleChange\n */\nve.init.mw.CXTarget.prototype.onTargetTitleChange = function () {\n\tthis.pageName = this.translationView.targetColumn.getTitle();\n\tthis.updateNamespace();\n\tthis.emit( 'targetTitleChange' );\n\tthis.debounceAlignSectionPairs();\n};\n\nve.init.mw.CXTarget.prototype.enablePublishButton = function () {\n\tif ( this.translation.hasTranslatedSections() ) {\n\t\tthis.publishButton.setDisabled( false );\n\t}\n};\n\n/**\n * Translation restore event handler\n *\n * @param {mw.cx.dm.Translation} translationModel\n */\nve.init.mw.CXTarget.prototype.onTranslationRestore = function () {\n\tif ( mw.Title.newFromText( this.pageName ) ) {\n\t\tthis.enablePublishButton();\n\t}\n\n\t// Update publish settings namespace choice\n\tthis.updateNamespace();\n};\n\n/**\n * Call this when translation editor is ready.\n */\nve.init.mw.CXTarget.prototype.onSurfaceReady = function () {\n\t// Update namespace tools\n\tthis.updateNamespace();\n\t// Get ready with the translation of first section.\n\tthis.prefetchTranslationForSection( 0 );\n\n\tif ( this.translation.hasTranslatedSections() ) {\n\t\tthis.targetSurface.$element.addClass( 've-ui-cxTargetSurface--non-empty' );\n\t}\n};\n\n/**\n * Call this whenever something changes in the translation that requires saving.\n */\nve.init.mw.CXTarget.prototype.onChange = function () {\n\tif ( mw.Title.newFromText( this.pageName ) && !this.errorsInTranslation ) {\n\t\tthis.publishButton.setDisabled( false );\n\t}\n\tthis.translationView.clearMessages();\n};\n\n/**\n * Target namespace change handler\n *\n * @param {number} namespaceId\n */\nve.init.mw.CXTarget.prototype.onPublishNamespaceChange = function ( namespaceId ) {\n\tvar newTitle = mw.cx.getTitleForNamespace( this.pageName, namespaceId );\n\t// Setting title in targetColumn will take care of necessary event firing for title change.\n\tthis.translationView.targetColumn.setTitle( newTitle );\n\tmw.log( '[CX] Target title changed to ' + newTitle );\n\tthis.emitNamespaceChange( namespaceId );\n};\n\n/**\n * @param {number} namespaceId\n *\n * @fires namespaceChange\n */\nve.init.mw.CXTarget.prototype.emitNamespaceChange = function ( namespaceId ) {\n\tthis.emit( 'namespaceChange', namespaceId );\n};\n\nve.init.mw.CXTarget.prototype.updateNamespace = function () {\n\tthis.getActions().updateToolState();\n};\n\nve.init.mw.CXTarget.prototype.getPublishNamespace = function () {\n\tvar titleObj = mw.Title.newFromText( this.pageName );\n\n\treturn titleObj ? titleObj.getNamespaceId() : mw.cx.getDefaultTargetNamespace();\n};\n\n/**\n * @fires publish\n */\nve.init.mw.CXTarget.prototype.onPublishButtonClick = function () {\n\t// Disable the trigger button\n\tthis.publishButton.setDisabled( true )\n\t\t.setTitle( mw.msg( 'cx-publish-button-publishing' ) );\n\tthis.targetSurface.setReadOnly( true );\n\tthis.translationView.contentContainer.$element.toggleClass( 'oo-ui-widget-disabled', true );\n\tthis.emit( 'publish' );\n\tthis.updateNamespace();\n};\n\nve.init.mw.CXTarget.prototype.attachToolbar = function () {\n\tthis.translationView.toolsColumn.editingToolbarContainer.$element.append(\n\t\tthis.getToolbar().$element\n\t\t\t.addClass( 'oo-ui-toolbar-narrow' ) // Quick fix to avoid overflowing toolbar.\n\t);\n\n\tve.ui.CXTranslationToolbar.static.registerTools( this.MTManager ).then( function () {\n\t\tvar mtToolbar = new ve.ui.CXTranslationToolbar();\n\t\tmtToolbar.setup( this.constructor.static.translationToolbarGroups, this.targetSurface );\n\t\tthis.translationView.toolsColumn.mtToolbarContainer.$element.append( mtToolbar.$element );\n\t\tmtToolbar.initialize();\n\t}.bind( this ) );\n};\n\n/**\n * @fires contentChange\n */\nve.init.mw.CXTarget.prototype.onDocumentTransact = function () {\n\tthis.emit( 'contentChange' );\n\tthis.debounceAlignSectionPairs();\n};\n\nve.init.mw.CXTarget.prototype.alignSectionPairs = function () {\n\tvar i, sourceDocumentNode, targetDocumentNode, sourceOffsetTop, targetOffsetTop,\n\t\tdocumentNodeChildren, alignSectionPair, articleNode;\n\n\tsourceDocumentNode = this.sourceSurface.getView().getDocument().getDocumentNode();\n\ttargetDocumentNode = this.targetSurface.getView().getDocument().getDocumentNode();\n\n\t// This method can be called before restoration is complete and all nodes are attached\n\t// to the DOM (e.g. via mw.cx.ui.TargetColumn#setTitle). If so, skip alignment.\n\tif (\n\t\t!document.contains( sourceDocumentNode.$element[ 0 ] ) ||\n\t\t!document.contains( targetDocumentNode.$element[ 0 ] )\n\t) {\n\t\treturn;\n\t}\n\n\tthis.translationView.alignTitles();\n\n\tsourceOffsetTop = sourceDocumentNode.$element.offset().top;\n\ttargetOffsetTop = targetDocumentNode.$element.offset().top;\n\tdocumentNodeChildren = sourceDocumentNode.getChildren();\n\n\tfor ( i = 0; i < documentNodeChildren.length; i++ ) {\n\t\tif ( documentNodeChildren[ i ].getType() === 'article' ) {\n\t\t\tarticleNode = documentNodeChildren[ i ];\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tif ( !articleNode ) {\n\t\tmw.log.error( '[CX] Fatal: articleNode not found in documentNode' );\n\t\treturn;\n\t}\n\n\talignSectionPair = this.translationView.constructor.static.alignSectionPair;\n\tarticleNode.getChildren().forEach( function ( node ) {\n\t\tvar sectionNumber,\n\t\t\telement = node.$element[ 0 ],\n\t\t\tid = element && element.id,\n\t\t\tmatch = id && id.match( /^cxSourceSection([0-9]+)$/ );\n\t\tif ( match ) {\n\t\t\tsectionNumber = +match[ 1 ];\n\t\t\talignSectionPair( sourceOffsetTop, targetOffsetTop, sectionNumber );\n\t\t} else {\n\t\t\tmw.log.warn( '[CX] Invalid source section ' + id + ' found. Alignment may go wrong' );\n\t\t}\n\t} );\n};\n\n/**\n * Get the jQuery element for the given source section id.\n *\n * @param {string} sectionId Section id. E.g. cxSourceSection15 or cxTargetSection15\n * @return {jQuery} Source section element\n */\nve.init.mw.CXTarget.prototype.getSourceSectionElement = function ( sectionId ) {\n\tvar sectionNumber, sourceId;\n\n\tsectionNumber = mw.cx.getSectionNumberFromSectionId( sectionId );\n\tsourceId = 'cxSourceSection' + sectionNumber;\n\treturn this.sourceSurface.$element.find( '#' + sourceId );\n};\n\n/**\n * Get the source node for the given section id. Accepts section id for source or target.\n *\n * @param {string} sectionId Section id. Example cxSourceSection15 or cxTargetSection15\n * @return {ve.dm.CXSectionNode}\n */\nve.init.mw.CXTarget.prototype.getSourceSectionNode = function ( sectionId ) {\n\treturn this.getSourceSectionElement( sectionId ).data( 'view' ).getModel();\n};\n\n/**\n * Get the translation node for the given section id. Accepts section id of source or target.\n *\n * @param  {string} sectionId Section id. Example cxSourceSection15 or cxTargetSection15\n * @return {ve.dm.CXSectionNode|null}\n */\nve.init.mw.CXTarget.prototype.getTargetSectionNode = function ( sectionId ) {\n\tvar sectionNumber, targetId, view;\n\n\tsectionNumber = mw.cx.getSectionNumberFromSectionId( sectionId );\n\ttargetId = 'cxTargetSection' + sectionNumber;\n\tview = this.targetSurface.$element.find( '#' + targetId ).data( 'view' );\n\treturn view ? view.getModel() : null;\n};\n\n/**\n * Get the content editable node for the given section number. Accepts section id for target.\n *\n * @param {string} sectionNumber Section number. Example 4, 5 etc.\n * @return {ve.ce.CXSectionNode|null}\n */\nve.init.mw.CXTarget.prototype.getTargetSectionElementFromSectionNumber = function ( sectionNumber ) {\n\tvar targetId = 'cxTargetSection' + sectionNumber,\n\t\tview = this.targetSurface.$element.find( '#' + targetId ).data( 'view' );\n\n\treturn !view ? null : view;\n};\n\n/**\n * Get the translation node for the given section number. Accepts section id of source or target.\n *\n * @param  {string} sectionNumber Section number. Example 4, 5 etc.\n * @return {ve.dm.CXSectionNode|null}\n */\nve.init.mw.CXTarget.prototype.getTargetSectionNodeFromSectionNumber = function ( sectionNumber ) {\n\tvar view = this.getTargetSectionElementFromSectionNumber( sectionNumber );\n\treturn view ? view.getModel() : null;\n};\n\n/**\n * Handle clicks for placeholder sections.\n *\n * @param {ve.ce.CXPlaceholderNode} placeholder\n */\nve.init.mw.CXTarget.prototype.onDocumentActivatePlaceholder = function ( placeholder ) {\n\tvar $sourceElement,\n\t\tmodel = placeholder.getModel(),\n\t\tcxid = model.getAttribute( 'cxid' );\n\n\tthis.targetSurface.$element.addClass( 've-ui-cxTargetSurface--non-empty' );\n\n\tmodel.emit( 'beforeTranslation' );\n\tthis.MTManager.getPreferredProvider().then( function ( provider ) {\n\t\treturn this.changeContentSource( model, null, provider );\n\t}.bind( this ) ).fail( function () {\n\t\tmw.notify( mw.msg( 'cx-auto-failed' ) );\n\t\treturn this.MTManager.getDefaultNonMTProvider().then( function ( provider ) {\n\t\t\treturn this.changeContentSource( model, null, provider );\n\t\t}.bind( this ) );\n\t}.bind( this ) ).always( function () {\n\t\tvar model;\n\t\t$sourceElement = this.getSourceSectionElement( cxid );\n\t\t$sourceElement.removeClass( 'cx-section-highlight' );\n\t\tmodel = this.getTargetSectionNode( cxid );\n\t\tif ( model ) {\n\t\t\tmodel.emit( 'afterTranslation' );\n\t\t\tthis.prefetchTranslationForSection( model.getSectionNumber() + 1 );\n\t\t} else {\n\t\t\tmw.log.error( '[CX] No model found after translation for ' + cxid );\n\t\t}\n\t}.bind( this ) );\n};\n\nve.init.mw.CXTarget.prototype.onPublishCancel = function () {\n\tthis.publishButton.setDisabled( false ).setTitle( mw.msg( 'cx-publish-button' ) );\n\tthis.targetSurface.setReadOnly( false );\n\tthis.updateNamespace();\n\tthis.translationView.contentContainer.$element.toggleClass( 'oo-ui-widget-disabled', false );\n};\n\nve.init.mw.CXTarget.prototype.onPublishSuccess = function () {\n\tthis.translationView.showMessage(\n\t\t'success',\n\t\tmw.message( 'cx-publish-page-success',\n\t\t\t$( '<a>' ).attr( {\n\t\t\t\thref: mw.util.getUrl( this.translation.getTargetTitle() ),\n\t\t\t\ttarget: '_blank'\n\t\t\t} ).text( this.translation.getTargetTitle() )\n\t\t)\n\t);\n\tthis.publishButton.setDisabled( true ).setTitle( mw.msg( 'cx-publish-button' ) );\n\tthis.targetSurface.setReadOnly( false );\n\tthis.updateNamespace();\n\tthis.translationView.contentContainer.$element.toggleClass( 'oo-ui-widget-disabled', false );\n};\n\nve.init.mw.CXTarget.prototype.onPublishFailure = function ( errorMessage ) {\n\tthis.translationView.showMessage( 'error', errorMessage );\n\tthis.onPublishCancel();\n};\n\n/**\n * @param {boolean} hasErrors True if any of the issues is error, false if all are warnings.\n */\nve.init.mw.CXTarget.prototype.onTranslationIssues = function ( hasErrors ) {\n\t// If there used to be errors, which are now gone, enable publish button\n\tif ( this.errorsInTranslation && !hasErrors ) {\n\t\tthis.enablePublishButton();\n\t}\n\tthis.errorsInTranslation = hasErrors;\n};\n\n/**\n * Set the section content to the given content.\n *\n * @param {ve.dm.CXSectionNode|ve.dm.CXPlaceholderNode} section Section model\n * @param {string} content\n * @param {string} source Original content source\n */\nve.init.mw.CXTarget.prototype.setSectionContent = function ( section, content, source ) {\n\tvar pasteDoc, newCursorRange, newRange, tx, docLen,\n\t\tsurfaceModel = this.getSurface().getModel(),\n\t\tdoc = surfaceModel.getDocument(),\n\t\tcxid = section.getSectionId(),\n\t\tfragment = surfaceModel.getLinearFragment( section.getOuterRange(), true /* noAutoSelect */ );\n\n\t/**\n\t * Fix internal list indexes for duplicated references in a newFromDocumentInsertion transaction.\n\t *\n\t * This finds references inserted by the transaction that are duplicates of references already\n\t * present in the document, and changes them to point to the existing internal list item.\n\t *\n\t * This is a super hacky way to prevent errors in VE due to name collisions for duplicated\n\t * references.\n\t *\n\t * @param {ve.dm.Transaction} tx Transaction generated by newFromDocumentInsertion()\n\t * @param {ve.dm.Document} doc Document the transaction is intended for\n\t */\n\tfunction deduplicateReferences( tx, doc ) {\n\t\tvar o, i, element, nodeGroup, kinNodes;\n\t\tfor ( o = 0; o < tx.operations.length; o++ ) {\n\t\t\tif ( tx.operations[ o ].type !== 'replace' ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor ( i = 0; i < tx.operations[ o ].insert.length; i++ ) {\n\t\t\t\telement = tx.operations[ o ].insert[ i ];\n\t\t\t\tif ( element.type !== 'mwReference' ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// Find any existing references this reference is a duplicate of\n\t\t\t\tnodeGroup = doc.getInternalList().getNodeGroup( element.attributes.listGroup );\n\t\t\t\tkinNodes = nodeGroup && nodeGroup.keyedNodes[ element.attributes.listKey ];\n\t\t\t\tif ( kinNodes && kinNodes.length > 0 ) {\n\t\t\t\t\t// This reference is a duplicate. Point it to the existing internal list item\n\t\t\t\t\telement.attributes.listIndex = kinNodes[ 0 ].getAttribute( 'listIndex' );\n\t\t\t\t\t// Only the first reference in the group should have contentsUsed=true\n\t\t\t\t\telement.attributes.contentsUsed = false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tpasteDoc = ve.dm.converter.getModelFromDom( ve.createDocumentFromHtml( content ) );\n\tdocLen = pasteDoc.getInternalList().getListNode().getOuterRange().start;\n\n\tfragment.insertContent( [\n\t\t{ type: 'cxSection', attributes: { style: 'section', cxid: cxid, cxsource: source } },\n\t\t// Put a temporary paragraph inside the section so the cursor has somewhere\n\t\t// sensible to go, preventing scrollCursorIntoView from triggering a jump\n\t\t{ type: 'paragraph' },\n\t\t{ type: '/paragraph' },\n\t\t{ type: '/cxSection' }\n\t] );\n\tfragment = fragment\n\t\t.collapseToStart().adjustLinearSelection( 1, 3 )\n\t\t.removeContent();\n\n\ttx = ve.dm.TransactionBuilder.static.newFromDocumentInsertion(\n\t\tdoc,\n\t\tfragment.getSelection().getCoveringRange().start,\n\t\tpasteDoc,\n\t\tnew ve.Range( 1, docLen - 1 )\n\t);\n\t// HACK: modify the internal list indexes of any reused references being inserted to avoid errors in VE\n\t// If we don't do this, a reused reference will bring along a second copy of its internal list item,\n\t// and VE will crash because there are two references with the same name but pointing to different\n\t// internal list items.\n\t// We have to perform these modifications after generating the transaction, because if we do it before,\n\t// our modified indexes will be corrupted by the remapping step in newFromDocumentInsertion().\n\tdeduplicateReferences( tx, doc );\n\tnewRange = tx.getModifiedRange( doc );\n\tsurfaceModel.change( tx, new ve.dm.LinearSelection( newRange ) );\n\n\t// Select first content offset within new content\n\tnewCursorRange = new ve.Range( surfaceModel.getDocument().data.getNearestContentOffset( newRange.start, 1 ) );\n\tif ( newRange.containsRange( newCursorRange ) ) {\n\t\tsurfaceModel.setLinearSelection( newCursorRange );\n\t}\n};\n\n/**\n * @inheritDoc\n */\nve.init.mw.CXTarget.prototype.getContentApi = function ( doc, options ) {\n\tdoc = doc || this.targetSurface.getModel().getDocument();\n\treturn this.siteMapper.getApi( doc.getLang(), options );\n};\n\n/**\n * @inheritDoc\n */\nve.init.mw.CXTarget.prototype.getPageName = function ( doc ) {\n\tdoc = doc || this.targetSurface.getModel().getDocument();\n\treturn doc.getLang() === this.translation.getSourceLanguage() ?\n\t\tthis.translation.getSourceTitle() : this.translation.getTargetTitle();\n};\n\n/**\n * Translate and adapt the source section for the given section id.\n *\n * @param {string} sectionId Section ID\n * @param {string} provider Machine translation privider\n * @param {boolean} noCache If true, do a fresh translation from server\n * @return {jQuery.Promise}\n */\nve.init.mw.CXTarget.prototype.translateSection = function ( sectionId, provider, noCache ) {\n\tvar sourceNode, mtRequest,\n\t\tmode = ve.dm.Converter.static.CLIPBOARD_MODE,\n\t\tsourceNodeModel = this.getSourceSectionNode( sectionId ),\n\t\tsectionNumber = sourceNodeModel.getSectionNumber();\n\n\tmtRequest = OO.getProp( this.translationRequestCache, sectionNumber, provider );\n\tif ( !noCache && mtRequest ) {\n\t\treturn mtRequest;\n\t}\n\n\t// Convert DOM to node, preserving full internal list\n\t// Use clipboard mode to ensure reference body is outputted\n\tif ( OO.getProp( sourceNodeModel, 'children', 0, 'type' ) === 'mwReferencesList' ) {\n\t\t// If the section is referencelist, we don't need to have the reference body resolved in it.\n\t\t// The reflist template wrapping of mw:Extension/refs will be lost in the\n\t\t// clipboard mode too. See T220491\n\t\tmode = ve.dm.Converter.static.PARSER_MODE;\n\t}\n\n\t// TODO: Extend converter and make a new TRANSLATION mode\n\tve.dm.converter.isForTranslation = true;\n\tsourceNode = ve.dm.converter.getDomFromNode( sourceNodeModel, mode ).body.children[ 0 ];\n\tve.dm.converter.isForTranslation = false;\n\n\tfunction restructure( section ) {\n\t\tsection = section.cloneNode( true );\n\t\tsection.removeAttribute( 'rel' );\n\t\tsection.id = 'cxTargetSection' + sectionNumber;\n\t\t// TODO: it's horrible that id attributes get duplicated\n\t\t// $( section ).find( '[id]' ).each( function ( i, node ) {\n\t\t//  node.setAttribute( 'id', 'cx' + node.getAttribute( 'id' ) );\n\t\t// } );\n\t\treturn section;\n\t}\n\n\tmtRequest = this.MTService.translate( restructure( sourceNode ).outerHTML, provider );\n\t// Set the request in the cache\n\tOO.setProp( this.translationRequestCache, sectionNumber, provider, mtRequest );\n\n\treturn mtRequest;\n};\n\n/**\n * Change content source for given target section.\n *\n * This handles caching of previous content when switching back and forth.\n * This might be redundant with undo/redo.\n *\n * @param {ve.dm.CXSectionNode|ve.dm.CXPlaceholderNode} section\n * @param {string|null} previousProvider\n * @param {string} newProvider\n * @param {Object} options\n * @cfg {boolean} noCache Do not use cached version\n * @return {jQuery.promise}\n * @fires changeContentSource\n */\nve.init.mw.CXTarget.prototype.changeContentSource = function (\n\tsection,\n\tpreviousProvider,\n\tnewProvider,\n\toptions\n) {\n\tvar cxid, html, cachedContent;\n\n\toptions = options || {};\n\tcxid = section.getSectionId();\n\tve.dm.converter.isForTranslation = true;\n\thtml = ve.dm.converter.getDomFromNode( section, ve.dm.Converter.static.CLIPBOARD_MODE ).body.children[ 0 ].outerHTML;\n\tve.dm.converter.isForTranslation = false;\n\n\tif ( previousProvider !== null ) {\n\t\tOO.setProp( this.contentSourceCache, cxid, previousProvider, html );\n\t}\n\n\tif ( !options.noCache ) {\n\t\tcachedContent = OO.getProp( this.contentSourceCache, cxid, newProvider );\n\n\t\tif ( cachedContent ) {\n\t\t\tthis.setSectionContent( section, cachedContent, newProvider );\n\t\t\treturn $.Deferred().resolve().promise();\n\t\t}\n\t}\n\n\treturn this.translateSection( cxid, newProvider, options.noCache ).then( function ( content ) {\n\t\tthis.setSectionContent( section, content, newProvider );\n\t\tthis.emit( 'changeContentSource', mw.cx.getSectionNumberFromSectionId( cxid ) );\n\t}.bind( this ) );\n};\n\n/**\n * Prefetch the translation for the given section. The API request is raised, and set it in the cache.\n * Nothing done with the content.\n *\n * @param {number} sectionNumber\n */\nve.init.mw.CXTarget.prototype.prefetchTranslationForSection = function ( sectionNumber ) {\n\tvar $section = this.sourceSurface.$element.find( '#cxSourceSection' + sectionNumber );\n\tif ( $section.length ) {\n\t\tthis.MTManager.getPreferredProvider().then( function ( provider ) {\n\t\t\tthis.translateSection( $section.prop( 'id' ), provider );\n\t\t}.bind( this ) );\n\t}\n};\n\n/* Registration */\n\nve.init.mw.targetFactory.register( ve.init.mw.CXTarget );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/actions/ve.ui.CXTranslationAction.js","messages":[{"ruleId":"jsdoc/require-property-type","severity":1,"message":"Missing JSDoc @property \"\" type.","line":35,"column":null,"nodeType":"Block","endLine":35,"endColumn":null}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/*!\n * Content Translation UserInterface TranslationAction class.\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n\n/**\n * Translation action.\n *\n * @class\n * @extends ve.ui.Action\n * @constructor\n * @param {ve.ui.Surface} surface Surface to act on\n */\nve.ui.CXTranslationAction = function VeUiCXTranslationAction() {\n\t// Parent constructor\n\tve.ui.CXTranslationAction.super.apply( this, arguments );\n\tthis.beforeTranslationData = {};\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXTranslationAction, ve.ui.Action );\n\n/* Static Properties */\n\nve.ui.CXTranslationAction.static.name = 'translation';\n\n/**\n * List of allowed methods for the action.\n *\n * @static\n * @property\n */\nve.ui.CXTranslationAction.static.methods = [ 'translate' ];\n\n/* Methods */\n\n/**\n * Find the currently active section and request to change the source.\n *\n * @param {string} source Selected MT provider or `source` or `scratch`\n * @return {boolean} False if action is cancelled.\n */\nve.ui.CXTranslationAction.prototype.translate = function ( source ) {\n\tvar section, promise, originalSource,\n\t\ttarget = ve.init.target,\n\t\tselection = this.surface.getModel().getSelection();\n\n\tif ( !( selection instanceof ve.dm.LinearSelection ) ) {\n\t\treturn false;\n\t}\n\n\tsection = mw.cx.getParentSectionForSelection( this.surface, selection );\n\n\tif ( !section ) {\n\t\tmw.log.error( '[CX] Could not find a CX Section as parent for the context.' );\n\t\treturn false;\n\t}\n\n\toriginalSource = section.getOriginalContentSource();\n\n\tthis.beforeTranslate( section );\n\n\tif ( source === 'reset-translation' ) {\n\t\tpromise = target.changeContentSource( section, null, originalSource, { noCache: true } );\n\t} else {\n\t\tpromise = target.changeContentSource( section, originalSource, source );\n\t}\n\n\tpromise\n\t\t.always( function () {\n\t\t\t// Recalculate the section, since the instance got distroyed in content change\n\t\t\tsection = target.getTargetSectionNode( section.getSectionId() );\n\t\t\tif ( section ) {\n\t\t\t\tthis.afterTranslate( section );\n\t\t\t}\n\t\t}.bind( this ) ).fail( function () {\n\t\t\tmw.notify( mw.msg( 'cx-mt-failed ' ) );\n\t\t\tthis.surface.getModel().emit( 'contextChange' );\n\t\t}.bind( this ) );\n};\n\n/**\n * Pre-translate handler\n *\n * @param {ve.dm.CXSectionNode} section\n */\nve.ui.CXTranslationAction.prototype.beforeTranslate = function ( section ) {\n\t// Save scroll position before changing section content\n\tthis.beforeTranslationData.scrollTop = this.surface.view.$window.scrollTop();\n\tsection.emit( 'beforeTranslation' );\n};\n\n/**\n * Post-translate handler\n *\n * @param {ve.dm.CXSectionNode} section\n */\nve.ui.CXTranslationAction.prototype.afterTranslate = function ( section ) {\n\t// Restore scroll position after changing content\n\tthis.surface.view.$window.scrollTop( this.beforeTranslationData.scrollTop );\n\tsection.emit( 'afterTranslation' );\n};\n\n/* Registration */\n\nve.ui.actionFactory.register( ve.ui.CXTranslationAction );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/tools/ve.ui.CXResetSectionTool.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXDesktopContext.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXInternalLinkAnnotationWidget.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXLinkAnnotationInspector.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXLinkContextItem.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXPublishSettingsDialog.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXPublishSettingsTool.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXPublishTool.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXReferenceContextItem.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXReferenceDialog.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXSurface.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXTextSelectionContextItem.js","messages":[{"ruleId":"jsdoc/check-tag-names","severity":1,"message":"Invalid JSDoc tag name \"mixins\".","line":6,"column":null,"nodeType":"Block","endLine":6,"endColumn":null}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Context item for a CX Text Selections in translation.\n *\n * @class\n * @extends ve.ui.LinearContextItem\n * @mixins ve.ui.CXTranslationUnitContextItem\n *\n * @constructor\n * @param {ve.ui.Context} context Context item is in\n * @param {ve.dm.Model} model Model item is related to\n * @param {Object} config Configuration options\n */\nve.ui.CXTextSelectionContextItem = function VeUiCXTextSelectionContextItem() {\n\t// Parent constructor\n\tve.ui.CXTextSelectionContextItem.super.apply( this, arguments );\n\t// Mixin constructor\n\tve.ui.CXTranslationUnitContextItem.apply( this, arguments );\n\t// Initialization\n\tthis.$element.addClass( 've-ui-CXTextSelectionContextItem' );\n\tthis.$body.addClass( 've-ui-cxLinkContextItem-targetBody' );\n\tthis.$sourceBody = $( '<div>' )\n\t\t.addClass( 've-ui-linearContextItem-body ve-ui-cxLinkContextItem-sourceBody' )\n\t\t.insertAfter( this.$body );\n\tthis.editButton.setLabel( OO.ui.deferMsg( 'cx-tools-link-add' ) );\n\tthis.editButton.setIcon( 'add' );\n\n\tthis.normalizedTitle = null;\n\tthis.sourceLinkCache = ve.init.platform.sourceLinkCache;\n\tthis.targetLinkCache = ve.init.platform.linkCache;\n\tthis.requestManager = ve.init.target.requestManager;\n\n\tthis.surfaceModel = this.context.getSurface().getModel();\n\n\t// Events\n\tthis.surfaceModel.connect( this, { select: 'onSurfaceModelSelect' } );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXTextSelectionContextItem, ve.ui.LinearContextItem );\nOO.mixinClass( ve.ui.CXTextSelectionContextItem, ve.ui.CXTranslationUnitContextItem );\n\n/* Static Properties */\n\nve.ui.CXTextSelectionContextItem.static.name = 'cxtextselection';\n\nve.ui.CXTextSelectionContextItem.static.icon = 'link';\n\nve.ui.CXTextSelectionContextItem.static.commandName = 'linkToggle';\n\nve.ui.CXTextSelectionContextItem.static.label = OO.ui.deferMsg( 'cx-tools-link-title' );\n\nve.ui.CXTextSelectionContextItem.static.deletable = false;\n\nve.ui.CXTextSelectionContextItem.static.editable = true;\n\n// This context will match to any model, so make sure it doesn't\n// exclude a context that is designed specifically for this model (T217081)\nve.ui.CXTextSelectionContextItem.static.exclusive = false;\n\n/* Static Methods */\n\nve.ui.CXTextSelectionContextItem.static.isCompatibleWith = function ( model ) {\n\treturn model.isEditable();\n};\n\nve.ui.CXTextSelectionContextItem.static.generateBody = ve.ui.CXLinkContextItem.static.generateBody;\nve.ui.CXTextSelectionContextItem.static.generateSourceBody = ve.ui.CXLinkContextItem.static.generateSourceBody;\n\nve.ui.CXTextSelectionContextItem.static.getAnnotationAttributes = function ( normalizedTitle ) {\n\tvar title = mw.Title.newFromText( normalizedTitle ),\n\t\tannotation = ve.dm.CXLinkAnnotation.static.newFromTitle( title );\n\n\treturn annotation.element;\n};\n\n/* Methods */\n\n/**\n * Render the body.\n *\n * @param {Object} targetTitleData\n */\nve.ui.CXTextSelectionContextItem.prototype.renderBody = function ( targetTitleData ) {\n\tvar targetLinkInfo, $targetLink,\n\t\tsourceLanguage = this.translation.sourceDoc.getLang(),\n\t\ttargetLanguage = this.translation.targetDoc.getLang();\n\n\ttargetLinkInfo = {\n\t\ttitle: this.normalizedTitle,\n\t\tpagelanguage: targetLanguage,\n\t\tdescription: targetTitleData.description,\n\t\tthumbnail: { source: targetTitleData.imageUrl }\n\t};\n\n\t$targetLink = this.constructor.static.generateBody( targetLinkInfo, this.context );\n\tthis.$body.append( $targetLink );\n\n\t// Find source title for the selected text.\n\tthis.requestManager.getTitlePair( targetLanguage, this.normalizedTitle )\n\t\t.then( function ( titlePairInfo ) {\n\t\t\tvar sourceTitle = titlePairInfo.targetTitle;\n\t\t\tif ( sourceTitle ) {\n\t\t\t\t// Render the source title card for this title.\n\t\t\t\tthis.renderSourceTitle( sourceTitle, sourceLanguage );\n\t\t\t}\n\t\t}.bind( this ) );\n};\n\nve.ui.CXTextSelectionContextItem.prototype.renderSourceTitle = function ( sourceTitle, sourceLanguage ) {\n\tthis.sourceLinkCache.get( sourceTitle ).then( function ( linkData ) {\n\t\tvar sourceLinkInfo, $sourceLink;\n\n\t\tif ( linkData.missing ) {\n\t\t\t// Source title data missing.\n\t\t\t// This is almost impossible since we already found that the source title exist.\n\t\t\treturn;\n\t\t}\n\n\t\tsourceLinkInfo = {\n\t\t\ttitle: sourceTitle,\n\t\t\tpagelanguage: sourceLanguage,\n\t\t\tdescription: linkData.description\n\t\t};\n\n\t\t// Source link\n\t\t$sourceLink = this.constructor.static.generateSourceBody(\n\t\t\tsourceLinkInfo, sourceLanguage\n\t\t);\n\t\tthis.$sourceBody.show().empty().append( $sourceLink );\n\t}.bind( this ) );\n};\n\n/**\n * @inheritdoc\n */\nve.ui.CXTextSelectionContextItem.prototype.setup = function () {\n\tvar fragment = this.getFragment(),\n\t\ttext = fragment.getText().trim();\n\n\tif (\n\t\ttext.length === 0 ||\n\t\ttext.length > 30 ||\n\t\tthis.hasLink() ||\n\t\tthis.context.getSurface().isReadOnly()\n\t) {\n\t\tthis.$element.remove();\n\t\treturn;\n\t}\n\n\t// To avoid flashing of empty card, let us hide the card till we get the link information.\n\tthis.toggle( false );\n\tthis.$sourceBody.hide();\n\tthis.normalizedTitle = ve.init.mw.ApiResponseCache.static.normalizeTitle( text );\n\t// Try to find the selected text as a title in target wiki\n\tthis.targetLinkCache.get( this.normalizedTitle ).then( function ( linkData ) {\n\t\tif ( linkData.missing ) {\n\t\t\t// Title does not exist in target language for the selected text. Do not show the card\n\t\t\tthis.$element.remove();\n\t\t\treturn;\n\t\t}\n\t\tthis.toggle( true );\n\t\tthis.renderBody( linkData );\n\t}.bind( this ) );\n\treturn this;\n};\n\n/**\n * @return {boolean} True if selected text contains a link.\n */\nve.ui.CXTextSelectionContextItem.prototype.hasLink = function () {\n\treturn this.context.getRelatedSources().some( function ( element ) {\n\t\treturn element.model instanceof ve.dm.LinkAnnotation;\n\t} );\n};\n\nve.ui.CXTextSelectionContextItem.prototype.onSurfaceModelSelect = function ( selection ) {\n\tif ( !( selection instanceof ve.dm.LinearSelection ) || selection.isCollapsed() ) {\n\t\treturn;\n\t}\n\n\tthis.context.onContextChange();\n};\n\n/**\n * @inheritdoc\n */\nve.ui.CXTextSelectionContextItem.prototype.teardown = function () {\n\tve.ui.CXTextSelectionContextItem.parent.prototype.teardown.apply( this, arguments );\n\n\t// Disconnect all event listeners\n\tthis.surfaceModel.disconnect( this );\n};\n\n/**\n * @inheritdoc\n */\nve.ui.CXTextSelectionContextItem.prototype.onEditButtonClick = function () {\n\tvar command = this.getCommand(),\n\t\t// We need annotation attributes while annotating links,\n\t\t// so that creation of href does not break.\n\t\tattributes = ve.ui.CXTextSelectionContextItem.static.getAnnotationAttributes( this.normalizedTitle );\n\n\tif ( command ) {\n\t\tcommand.execute( this.context.getSurface(), [ 'cxLink', attributes ] );\n\t\tthis.emit( 'command' );\n\t\t// FIXME: This avoids \"Add link\" card to stick after actually adding link\n\t\tthis.context.getSurface().getModel().setNullSelection();\n\t}\n};\n\n/* Registration */\n\nve.ui.contextItemFactory.register( ve.ui.CXTextSelectionContextItem );\n\nve.ui.commandRegistry.register(\n\tnew ve.ui.Command(\n\t\t'linkToggle', 'annotation', 'toggle',\n\t\t{ supportedSelections: [ 'linear' ] }\n\t)\n);\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXTransclusionContextItem.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXTranslationToolbar.js","messages":[{"ruleId":"no-shadow","severity":1,"message":"'MTManager' is already declared in the upper scope on line 59 column 62.","line":60,"column":65,"nodeType":"Identifier","messageId":"noShadow","endLine":60,"endColumn":74}],"errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @extends ve.ui.Toolbar\n * @constructor\n * @param {ve.init.mw.CXTarget} target\n */\nve.ui.CXTranslationToolbar = function VeUiCXTranslationToolbar() {\n\tvar $title;\n\n\t// TODO: inject\n\tthis.MTManager = ve.init.target.config.MTManager;\n\n\t// Parent constructor\n\tve.ui.CXTranslationToolbar.super.apply( this, arguments );\n\n\t$title = $( '<div>' )\n\t\t.addClass( 've-cx-toolbar-mt-title' )\n\t\t.text( mw.msg( 'cx-tools-mt-title' ) );\n\n\tthis.setAsDefault = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\tclasses: [ 've-cx-toolbar-mt-setdefault' ],\n\t\tlabel: mw.msg( 'cx-tools-mt-set-default' ),\n\t\ticon: 'pushPin'\n\t} ).connect( this, { click: 'onSetAsDefault' } );\n\n\tthis.noMTServices = new OO.ui.LabelWidget( {\n\t\tclasses: [ 've-cx-toolbar-mt-noservices' ],\n\t\tlabel: mw.message( 'cx-tools-mt-noservices' ).parseDom()\n\t} ).toggle( false );\n\tthis.noMTServices.$element.find( 'a' ).prop( 'target', '_blank' );\n\n\tthis.$element\n\t\t.addClass( 've-cx-toolbar-mt' )\n\t\t.prepend( $title, this.noMTServices.$element )\n\t\t.append( this.setAsDefault.$element );\n\n\t// Hide initially, because there is no selection initially\n\tthis.$element.toggle( false );\n\t// Only show this when user has changed selection\n\tthis.setAsDefault.toggle( false );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXTranslationToolbar, ve.ui.Toolbar );\n\n/* Static Methods */\n\n/**\n * @param {mw.cx.MachineTranslationManager} MTManager\n * @return {jQuery.Promise}\n */\nve.ui.CXTranslationToolbar.static.registerTools = function ( MTManager ) {\n\tvar createProviderItem = function ( provider, defaultProvider, MTManager ) {\n\t\tvar toolClassName = provider + 'MTTool';\n\t\tve.ui[ toolClassName ] = function VeCXMTTool() {\n\t\t\tve.ui.Tool.apply( this, arguments );\n\t\t\tthis.MTManager = MTManager;\n\t\t\tthis.setActive( defaultProvider === this.getName() );\n\t\t\tthis.MTManager.getPreferredProvider().then( function ( preferredProvider ) {\n\t\t\t\tthis.setIsPreferred( preferredProvider === this.getName() );\n\t\t\t}.bind( this ) );\n\t\t};\n\n\t\tOO.inheritClass( ve.ui[ toolClassName ], ve.ui.Tool );\n\t\tve.ui[ toolClassName ].static.name = provider;\n\t\tve.ui[ toolClassName ].static.group = 'mt';\n\t\tve.ui[ toolClassName ].static.autoAddToCatchall = false;\n\t\tve.ui[ toolClassName ].static.title = MTManager.getProviderLabel( provider );\n\t\tve.ui[ toolClassName ].static.commandName = provider.toLowerCase();\n\n\t\tve.ui[ toolClassName ].prototype.onSelect = function () {\n\t\t\t// Parent method\n\t\t\tve.ui.Tool.prototype.onSelect.apply( this, arguments );\n\t\t\t// Set all other tools inactive\n\t\t\tthis.toolGroup.items.forEach( function ( tool ) {\n\t\t\t\ttool.setActive( tool === this );\n\t\t\t}, this );\n\t\t};\n\n\t\tve.ui[ toolClassName ].prototype.onUpdateState = function () {\n\t\t\tvar section, source,\n\t\t\t\tsurface = this.toolbar.getSurface(),\n\t\t\t\tselection = surface.getModel().getSelection();\n\n\t\t\t// Parent method\n\t\t\tve.ui.Tool.prototype.onUpdateState.apply( this, arguments );\n\n\t\t\t// Check that we are not getting ve.dm.NullSelection\n\t\t\tif ( !( selection instanceof ve.dm.LinearSelection ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// When changing provides, there is temporarily no parent section\n\t\t\tsection = mw.cx.getParentSectionForSelection( surface, selection );\n\t\t\tif ( !section ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Fall back to defaultProvider (should only happen for drafts stored with\n\t\t\t// older version of CX.\n\t\t\t// TODO: How to handle case that stored provider is no longer valid?\n\t\t\tsource = section.getOriginalContentSource() || defaultProvider;\n\t\t\tthis.setActive( this.getName() === source );\n\t\t\t// Hits localstorage, so caching might be needed if this gets too expensive.\n\t\t\tthis.MTManager.getPreferredProvider().then( function ( preferredProvider ) {\n\t\t\t\tthis.toolbar.setAsDefault.toggle( source !== preferredProvider );\n\t\t\t}.bind( this ) );\n\t\t};\n\n\t\tve.ui[ toolClassName ].prototype.setIsPreferred = function ( toggle ) {\n\t\t\tthis.isPreferred = toggle;\n\t\t\tthis.updateTitle();\n\t\t};\n\n\t\tve.ui[ toolClassName ].prototype.updateTitle = function () {\n\t\t\tve.ui.Tool.prototype.updateTitle.apply( this, arguments );\n\n\t\t\tif ( this.isPreferred ) {\n\t\t\t\tthis.$title.wrapInner( '<span class=\"ve-cx-toolbar-mt-preferred-tool-title\"></span>' );\n\t\t\t\t$( '<span>' )\n\t\t\t\t\t.addClass( 've-cx-toolbar-mt-preferred-tool-indicator' )\n\t\t\t\t\t.text( mw.msg( 'cx-tools-mt-preferred' ) )\n\t\t\t\t\t.appendTo( this.$title );\n\t\t\t}\n\t\t};\n\n\t\tve.ui.toolFactory.register( ve.ui[ toolClassName ] );\n\n\t\tve.ui.commandRegistry.register(\n\t\t\tnew ve.ui.Command(\n\t\t\t\tprovider.toLowerCase(), 'translation', 'translate',\n\t\t\t\t{ args: [ provider ], supportedSelections: [ 'linear' ] }\n\t\t\t)\n\t\t);\n\t};\n\n\treturn MTManager.getDefaultProvider().then( function ( defaultProvider ) {\n\t\treturn MTManager.getAvailableProviders().then( function ( providers ) {\n\t\t\tproviders.forEach( function ( provider ) {\n\t\t\t\tcreateProviderItem( provider, defaultProvider, MTManager );\n\t\t\t} );\n\t\t} );\n\t} );\n};\n\n/* Methods */\n\n/**\n * @inheritDoc\n */\nve.ui.CXTranslationToolbar.prototype.setup = function () {\n\t// Parent method\n\tve.ui.CXTranslationToolbar.super.prototype.setup.apply( this, arguments );\n\n\tthis.toolGroup = this.items[ 0 ];\n\tthis.toolGroup.connect( this, {\n\t\tdisable: 'onGroupDisable'\n\t} );\n\n\t// Toggle the message about non-availability of MT services\n\tthis.noMTServices.toggle( !this.isMTAvailable() );\n};\n\n/**\n * @return {boolean} True if there is MT provider available.\n */\nve.ui.CXTranslationToolbar.prototype.isMTAvailable = function () {\n\treturn this.getToolGroupByName( 'cx-mt' ).getItems().map( function ( item ) {\n\t\treturn item.getName();\n\t} ).some( function ( name ) {\n\t\treturn [ 'ResetSection', 'source', 'scratch' ].indexOf( name ) < 0;\n\t} );\n};\n\n/**\n * Disable event handler for the tool group\n *\n * @param {boolean} disabled\n */\nve.ui.CXTranslationToolbar.prototype.onGroupDisable = function ( disabled ) {\n\t// If the toolgroup is disabled, hide the toolbar\n\tthis.$element.toggle( !disabled );\n\tthis.setAsDefault.toggle( !disabled );\n};\n\n/**\n * @inheritDoc\n */\nve.ui.CXTranslationToolbar.prototype.getCommands = function () {\n\t// The commands added in ve.ui.CXTranslationToolbar.static.registerTools is not updated in the\n\t// commands member property of ve.ui.Surface (which is populated in the constructor)\n\t// ve.ui.Toolbar.prototype.isToolAvailable validates each tool with the known commands and\n\t// if not found, does not add to toolbar. Hence we are overriding this method to give the current\n\t// list of commands for the surface\n\t// TODO: Fix it in ve.ui.Surface#getCommands\n\treturn this.getSurface().commandRegistry.getNames();\n};\n\n/**\n * Save the currently selected provider as the preferred provider for new sections.\n */\nve.ui.CXTranslationToolbar.prototype.onSetAsDefault = function () {\n\t// There should ever be only 0..1 active tools\n\tthis.toolGroup.getItems().forEach( function ( tool ) {\n\t\tif ( tool.isActive() ) {\n\t\t\tthis.MTManager.setPreferredProvider( tool.getName() );\n\t\t}\n\n\t\tif ( tool.setIsPreferred ) {\n\t\t\ttool.setIsPreferred( tool.isActive() );\n\t\t}\n\t}, this );\n\n\tthis.setAsDefault.toggle( false );\n\tthis.$element.addClass( 've-cx-toolbar-mt--highlight' );\n\tsetTimeout(\n\t\tfunction () {\n\t\t\tthis.$element.removeClass( 've-cx-toolbar-mt--highlight' );\n\t\t}.bind( this ),\n\t\t2000 // Leave time for animation to complete.\n\t);\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXTranslationUnitContextItem.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/callout/ext.cx.callout.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/feedback/ext.cx.feedback.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/overlay/ext.cx.overlay.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/pageselector/ext.cx.pageselector.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/progressbar/ext.cx.progressbar.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/spinner/ext.cx.spinner.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/templates/mw.cx.widgets.TemplateParamOptionWidget.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/translator/ext.cx.translator.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/package-lock.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/package.json","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/base/mw.cx.SiteMapper.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/dm/mw.cx.dm.Translation.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/mw.cx.MachineTranslationService.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/mw.cx.TargetArticle.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/mw.cx.TranslationTracker.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/mw.cx.util.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/publish/ext.cx.publish.prepare.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/publish/ext.cx.publish.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/tools/ext.cx.tools.categories.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/tools/ext.cx.tools.mtabuse.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/translation/ext.cx.translation.loader.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/translation/ext.cx.translation.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/ui/mw.cx.ui.Infobar.test.js","messages":[],"errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]}]

$ npm ci
npm WARN prepare removing existing node_modules/ before installation

> core-js@3.9.1 postinstall /src/repo/node_modules/core-js
> node -e "try{require('./postinstall')}catch(e){}"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon: 
> https://opencollective.com/core-js 
> https://www.patreon.com/zloirock 

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> spawn-sync@1.0.15 postinstall /src/repo/node_modules/spawn-sync
> node postinstall


> pre-commit@1.2.2 install /src/repo/node_modules/pre-commit
> node install.js

pre-commit:
pre-commit: Detected an existing git pre-commit hook
pre-commit: Old pre-commit hook backuped to pre-commit.old
pre-commit:
added 661 packages in 15.33s

$ npm test

> @ test /src/repo
> grunt test && npm run test:cxdashboardapp

Running "eslint:all" (eslint) task

/src/repo/Gruntfile.js
  35:11  warning  Object.assign() is not supported in Safari 5.1, iOS Safari 6.0-6.1, IE 11  compat/compat

/src/repo/modules/dashboard/mw.cx.DashboardList.js
  127:7  warning  'language' is already declared in the upper scope on line 122 column 13  no-shadow

/src/repo/modules/dashboard/mw.cx.SuggestionList.js
   12:0   warning  @extends should not have a bracketed type in "jsdoc" mode            jsdoc/valid-types
  141:10  warning  'list' is already declared in the upper scope on line 120 column 57  no-shadow

/src/repo/modules/dashboard/mw.cx.TranslationList.js
  14:0  warning  @extends should not have a bracketed type in "jsdoc" mode  jsdoc/valid-types
  15:0  warning  Invalid JSDoc tag name "mixins"                            jsdoc/check-tag-names

/src/repo/modules/dm/mw.cx.dm.PageTitleModel.js
   9:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  10:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/dm/mw.cx.dm.Translation.js
  7:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/entrypoints/ext.cx.entrypoints.newbytranslation.js
  177:35  warning  navigator.languages() is not supported in Safari 5.1  compat/compat

/src/repo/modules/mw.cx.init.Translation.js
  201:7   warning  'sourceDom' is already declared in the upper scope on line 173 column 6    no-shadow
  201:18  warning  'targetDom' is already declared in the upper scope on line 173 column 17   no-shadow
  203:4   warning  'sourceHtml' is already declared in the upper scope on line 172 column 68  no-shadow

/src/repo/modules/mw.cx.TargetArticle.js
  458:7  warning  'title' is already declared in the upper scope on line 449 column 66  no-shadow

/src/repo/modules/mw.cx.TranslationTracker.js
  594:1  warning  Found more than one @return declaration  jsdoc/require-returns
  594:1  warning  Found more than one @return declaration  jsdoc/require-returns-check

/src/repo/modules/source/mw.cx.SelectedSourcePageDialog.js
  15:0  warning  @extends should not have a bracketed type in "jsdoc" mode  jsdoc/valid-types

/src/repo/modules/stats/ext.cx.stats.js
  617:55  warning  'data' is already declared in the upper scope on line 612 column 7   no-shadow
  629:55  warning  'data' is already declared in the upper scope on line 612 column 7   no-shadow
  641:49  warning  'data' is already declared in the upper scope on line 612 column 7   no-shadow
  662:58  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  674:58  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  686:52  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  698:55  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  718:55  warning  'data' is already declared in the upper scope on line 714 column 7   no-shadow
  727:55  warning  'data' is already declared in the upper scope on line 714 column 7   no-shadow
  736:49  warning  'data' is already declared in the upper scope on line 714 column 7   no-shadow
  757:58  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow
  766:58  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow
  775:52  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow
  784:55  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow

/src/repo/modules/tools/ext.cx.tools.template.js
  107:56  warning  'title' is already declared in the upper scope on line 47 column 7  no-shadow
  631:2   warning  Found more than one @return declaration                             jsdoc/require-returns
  631:2   warning  Found more than one @return declaration                             jsdoc/require-returns-check

/src/repo/modules/tools/ext.cx.tools.validator.js
  8:2  warning  Found more than one @return declaration  jsdoc/require-returns
  8:2  warning  Found more than one @return declaration  jsdoc/require-returns-check

/src/repo/modules/translation/ext.cx.translation.progress.js
  46:2  warning  Found more than one @return declaration  jsdoc/require-returns
  46:2  warning  Found more than one @return declaration  jsdoc/require-returns-check
  93:2  warning  Found more than one @return declaration  jsdoc/require-returns
  93:2  warning  Found more than one @return declaration  jsdoc/require-returns-check

/src/repo/modules/ui/widgets/mw.cx.ui.MessageWidget.js
   9:0  warning  The type 'mediawiki' is undefined  jsdoc/no-undefined-types
  10:0  warning  The type 'mediawiki' is undefined  jsdoc/no-undefined-types

/src/repo/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
  219:31  warning  'pages' is already declared in the upper scope on line 184 column 71  no-shadow

/src/repo/modules/ui/widgets/mw.cx.ui.PageTitleWidget.js
  9:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ui/widgets/mw.cx.ui.PersonalMenuWidget.js
  15:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/ce/ve.ce.CXPlaceholderNode.js
  11:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  12:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/ce/ve.ce.CXSectionNode.js
  7:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  8:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/dm/ve.dm.CXPlaceholderNode.js
  6:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  7:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/dm/ve.dm.CXReferenceNode.js
  12:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/dm/ve.dm.CXSectionNode.js
  12:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  13:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/init/ve.init.mw.CXTarget.js
  724:7   warning  'model' is already declared in the upper scope on line 710 column 3  no-shadow
  802:34  warning  'tx' is already declared in the upper scope on line 784 column 42    no-shadow
  802:38  warning  'doc' is already declared in the upper scope on line 786 column 3    no-shadow

/src/repo/modules/ve-cx/ui/actions/ve.ui.CXTranslationAction.js
  35:0  warning  Missing JSDoc @property "" type  jsdoc/require-property-type

/src/repo/modules/ve-cx/ui/ve.ui.CXTextSelectionContextItem.js
  6:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/ui/ve.ui.CXTranslationToolbar.js
  60:65  warning  'MTManager' is already declared in the upper scope on line 59 column 62  no-shadow

✖ 60 problems (0 errors, 60 warnings)


Running "stylelint:src" (stylelint) task
>> Linted 106 files without errors

Running "banana:ContentTranslation" (banana) task
>> 2 message directories checked.

Done.

> @ test:cxdashboardapp /src/repo
> cd app && npm run test


> cx3@0.1.0 test /src/repo/app
> npm install && npm -s run test:bundle && npm run test:unit


> deasync@0.1.21 install /src/repo/app/node_modules/deasync
> node ./build.js

`linux-x64-node-10` exists; testing
Binary is fine; exiting

> iltorb@2.4.5 install /src/repo/app/node_modules/iltorb
> node ./scripts/install.js || node-gyp rebuild

info looking for cached prebuild @ /cache/_prebuilds/2a34dd-iltorb-v2.4.5-node-v64-linux-x64.tar.gz
info found cached prebuild 
info unpacking @ /cache/_prebuilds/2a34dd-iltorb-v2.4.5-node-v64-linux-x64.tar.gz
info unpack resolved to /src/repo/app/node_modules/iltorb/build/bindings/iltorb.node
info unpack required /src/repo/app/node_modules/iltorb/build/bindings/iltorb.node successfully
info install Successfully installed iltorb binary!

> yorkie@2.0.0 install /src/repo/app/node_modules/yorkie
> node bin/install.js

setting up Git hooks
can't find .git directory, skipping Git hooks installation

> core-js@2.6.12 postinstall /src/repo/app/node_modules/babel-runtime/node_modules/core-js
> node -e "try{require('./postinstall')}catch(e){}"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon: 
> https://opencollective.com/core-js 
> https://www.patreon.com/zloirock 

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> core-js@3.8.3 postinstall /src/repo/app/node_modules/core-js
> node -e "try{require('./postinstall')}catch(e){}"


> core-js-pure@3.9.0 postinstall /src/repo/app/node_modules/core-js-pure
> node -e "try{require('./postinstall')}catch(e){}"


> ejs@2.7.4 postinstall /src/repo/app/node_modules/webpack-bundle-analyzer/node_modules/ejs
> node ./postinstall.js

Thank you for installing EJS: built with the Jake JavaScript build tool (https://jakejs.com/)


> cypress@3.8.3 postinstall /src/repo/app/node_modules/cypress
> node index.js --exec install


Cypress 3.8.3 is installed in /cache/Cypress/3.8.3

npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/webpack-dev-server/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/watchpack-chokidar2/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/jest-runtime/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/jest-runner/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/babel-jest/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/@jest/test-sequencer/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/@jest/reporters/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/@jest/environment/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.13 (node_modules/@jest/core/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.3.2 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.3.2: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

added 2923 packages from 1564 contributors and audited 2936 packages in 105.474s

209 packages are looking for funding
  run `npm fund` for details

found 6 vulnerabilities (4 low, 2 high)
  run `npm audit fix` to fix them, or `npm audit` for details
There are no staged changes to the dist folder in this change.

> cx3@0.1.0 test:unit /src/repo/app
> vue-cli-service test:unit -c config/jest/jest.config.js

PASS src/utils/contentCleaner.test.js

[vue-jest]: Less are not currently compiled by vue-jest


[vue-jest]: Less are not currently compiled by vue-jest


[vue-jest]: Less are not currently compiled by vue-jest

PASS src/components/SXSentenceSelector/SubSection.test.js
PASS src/lib/mediawiki.ui/components/MWButton/MWButton.test.js
PASS src/components/SXSentenceSelector/ProposedTranslationCard.test.js
PASS src/components/SXSentenceSelector/ProposedTranslationHeader.test.js
PASS src/components/SXSentenceSelector/ProposedTranslationActionButtons.test.js (5.46s)
PASS src/utils/segmentedContentConverter.test.js
PASS src/components/SXSectionSelector/SXSectionSelectorSectionList.test.js
PASS src/components/SXSentenceSelector/SXSentenceSelectorSentence.test.js
PASS src/components/SXSentenceSelector/SXSentenceSelector.test.js
PASS src/lib/mediawiki.ui/components/MWSelect/MWSelect.test.js
PASS src/lib/mediawiki.ui/components/MWInput/MWInput.test.js
PASS src/components/SXContentComparator/SXContentComparatorHeaderNavigation.test.js
PASS src/components/SXSectionSelector/SXSectionSelectorSectionListPresent.test.js
PASS src/components/SXArticleSelector/ExistingArticleBanner.test.js
PASS src/components/SXArticleSelector/SXArticleSelectorHeader.test.js
PASS src/components/SXSentenceSelector/SXSentenceSelectorContentHeader.test.js
PASS src/lib/mediawiki.ui/components/MWButtonGroup/MWButtonGroup.test.js
PASS src/lib/mediawiki.ui/components/MWIcon/MWIcon.test.js
PASS src/lib/mediawiki.ui/components/MWDropdown/MWDropdown.test.js
PASS src/lib/mediawiki.ui/components/MWRadioGroup/MWRadioGroup.test.js
PASS src/lib/mediawiki.ui/components/MWMessage/MWMessage.test.js
PASS src/components/SXSectionSelector/SXSectionSelectorViewArticleItem.test.js
PASS src/lib/mediawiki.ui/components/MWThumbnail/MWThumbnail.test.js
PASS src/lib/mediawiki.ui/components/MWCard/MWcard.test.js
PASS src/lib/mediawiki.ui/components/MWRadioGroup/MWRadio.test.js
PASS src/lib/mediawiki.ui/components/MWBottomNavigation/MWBottomNavigation.test.js
PASS src/lib/mediawiki.ui/components/MWDialog/MWDialog.test.js

Test Suites: 28 passed, 28 total
Tests:       62 passed, 62 total
Snapshots:   28 passed, 28 total
Time:        17.083s
Ran all test suites.

$ git add .

$ git commit -F /tmp/tmpye5brg0q
Running "eslint:all" (eslint) task

/src/repo/Gruntfile.js
  35:11  warning  Object.assign() is not supported in Safari 5.1, iOS Safari 6.0-6.1, IE 11  compat/compat

/src/repo/modules/dashboard/mw.cx.DashboardList.js
  127:7  warning  'language' is already declared in the upper scope on line 122 column 13  no-shadow

/src/repo/modules/dashboard/mw.cx.SuggestionList.js
   12:0   warning  @extends should not have a bracketed type in "jsdoc" mode            jsdoc/valid-types
  141:10  warning  'list' is already declared in the upper scope on line 120 column 57  no-shadow

/src/repo/modules/dashboard/mw.cx.TranslationList.js
  14:0  warning  @extends should not have a bracketed type in "jsdoc" mode  jsdoc/valid-types
  15:0  warning  Invalid JSDoc tag name "mixins"                            jsdoc/check-tag-names

/src/repo/modules/dm/mw.cx.dm.PageTitleModel.js
   9:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  10:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/dm/mw.cx.dm.Translation.js
  7:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/entrypoints/ext.cx.entrypoints.newbytranslation.js
  177:35  warning  navigator.languages() is not supported in Safari 5.1  compat/compat

/src/repo/modules/mw.cx.init.Translation.js
  201:7   warning  'sourceDom' is already declared in the upper scope on line 173 column 6    no-shadow
  201:18  warning  'targetDom' is already declared in the upper scope on line 173 column 17   no-shadow
  203:4   warning  'sourceHtml' is already declared in the upper scope on line 172 column 68  no-shadow

/src/repo/modules/mw.cx.TargetArticle.js
  458:7  warning  'title' is already declared in the upper scope on line 449 column 66  no-shadow

/src/repo/modules/mw.cx.TranslationTracker.js
  594:1  warning  Found more than one @return declaration  jsdoc/require-returns
  594:1  warning  Found more than one @return declaration  jsdoc/require-returns-check

/src/repo/modules/source/mw.cx.SelectedSourcePageDialog.js
  15:0  warning  @extends should not have a bracketed type in "jsdoc" mode  jsdoc/valid-types

/src/repo/modules/stats/ext.cx.stats.js
  617:55  warning  'data' is already declared in the upper scope on line 612 column 7   no-shadow
  629:55  warning  'data' is already declared in the upper scope on line 612 column 7   no-shadow
  641:49  warning  'data' is already declared in the upper scope on line 612 column 7   no-shadow
  662:58  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  674:58  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  686:52  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  698:55  warning  'data' is already declared in the upper scope on line 657 column 7   no-shadow
  718:55  warning  'data' is already declared in the upper scope on line 714 column 7   no-shadow
  727:55  warning  'data' is already declared in the upper scope on line 714 column 7   no-shadow
  736:49  warning  'data' is already declared in the upper scope on line 714 column 7   no-shadow
  757:58  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow
  766:58  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow
  775:52  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow
  784:55  warning  'data' is already declared in the upper scope on line 752 column 12  no-shadow

/src/repo/modules/tools/ext.cx.tools.template.js
  107:56  warning  'title' is already declared in the upper scope on line 47 column 7  no-shadow
  631:2   warning  Found more than one @return declaration                             jsdoc/require-returns
  631:2   warning  Found more than one @return declaration                             jsdoc/require-returns-check

/src/repo/modules/tools/ext.cx.tools.validator.js
  8:2  warning  Found more than one @return declaration  jsdoc/require-returns
  8:2  warning  Found more than one @return declaration  jsdoc/require-returns-check

/src/repo/modules/translation/ext.cx.translation.progress.js
  46:2  warning  Found more than one @return declaration  jsdoc/require-returns
  46:2  warning  Found more than one @return declaration  jsdoc/require-returns-check
  93:2  warning  Found more than one @return declaration  jsdoc/require-returns
  93:2  warning  Found more than one @return declaration  jsdoc/require-returns-check

/src/repo/modules/ui/widgets/mw.cx.ui.MessageWidget.js
   9:0  warning  The type 'mediawiki' is undefined  jsdoc/no-undefined-types
  10:0  warning  The type 'mediawiki' is undefined  jsdoc/no-undefined-types

/src/repo/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
  219:31  warning  'pages' is already declared in the upper scope on line 184 column 71  no-shadow

/src/repo/modules/ui/widgets/mw.cx.ui.PageTitleWidget.js
  9:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ui/widgets/mw.cx.ui.PersonalMenuWidget.js
  15:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/ce/ve.ce.CXPlaceholderNode.js
  11:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  12:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/ce/ve.ce.CXSectionNode.js
  7:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  8:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/dm/ve.dm.CXPlaceholderNode.js
  6:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  7:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/dm/ve.dm.CXReferenceNode.js
  12:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/dm/ve.dm.CXSectionNode.js
  12:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names
  13:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/init/ve.init.mw.CXTarget.js
  724:7   warning  'model' is already declared in the upper scope on line 710 column 3  no-shadow
  802:34  warning  'tx' is already declared in the upper scope on line 784 column 42    no-shadow
  802:38  warning  'doc' is already declared in the upper scope on line 786 column 3    no-shadow

/src/repo/modules/ve-cx/ui/actions/ve.ui.CXTranslationAction.js
  35:0  warning  Missing JSDoc @property "" type  jsdoc/require-property-type

/src/repo/modules/ve-cx/ui/ve.ui.CXTextSelectionContextItem.js
  6:0  warning  Invalid JSDoc tag name "mixins"  jsdoc/check-tag-names

/src/repo/modules/ve-cx/ui/ve.ui.CXTranslationToolbar.js
  60:65  warning  'MTManager' is already declared in the upper scope on line 59 column 62  no-shadow

✖ 60 problems (0 errors, 60 warnings)


Running "stylelint:src" (stylelint) task
>> Linted 106 files without errors

Running "banana:ContentTranslation" (banana) task
>> 2 message directories checked.

Done.
audited 2936 packages in 31.153s

209 packages are looking for funding
  run `npm fund` for details

found 6 vulnerabilities (4 low, 2 high)
  run `npm audit fix` to fix them, or `npm audit` for details
There are no staged changes to the dist folder in this change.
PASS src/components/SXSentenceSelector/ProposedTranslationActionButtons.test.js
PASS src/components/SXSentenceSelector/ProposedTranslationHeader.test.js
PASS src/lib/mediawiki.ui/components/MWDialog/MWDialog.test.js
PASS src/components/SXSentenceSelector/SubSection.test.js
PASS src/lib/mediawiki.ui/components/MWBottomNavigation/MWBottomNavigation.test.js
PASS src/utils/contentCleaner.test.js
PASS src/components/SXSentenceSelector/SXSentenceSelector.test.js
PASS src/components/SXSectionSelector/SXSectionSelectorSectionListPresent.test.js
PASS src/components/SXArticleSelector/SXArticleSelectorHeader.test.js
PASS src/components/SXSentenceSelector/ProposedTranslationCard.test.js
PASS src/components/SXArticleSelector/ExistingArticleBanner.test.js
PASS src/components/SXSectionSelector/SXSectionSelectorSectionList.test.js
PASS src/components/SXSentenceSelector/SXSentenceSelectorContentHeader.test.js
PASS src/lib/mediawiki.ui/components/MWButton/MWButton.test.js
PASS src/components/SXSectionSelector/SXSectionSelectorViewArticleItem.test.js
PASS src/lib/mediawiki.ui/components/MWMessage/MWMessage.test.js
PASS src/components/SXContentComparator/SXContentComparatorHeaderNavigation.test.js
PASS src/lib/mediawiki.ui/components/MWRadioGroup/MWRadioGroup.test.js
PASS src/lib/mediawiki.ui/components/MWSelect/MWSelect.test.js
PASS src/lib/mediawiki.ui/components/MWButtonGroup/MWButtonGroup.test.js
PASS src/lib/mediawiki.ui/components/MWDropdown/MWDropdown.test.js
PASS src/components/SXSentenceSelector/SXSentenceSelectorSentence.test.js
PASS src/lib/mediawiki.ui/components/MWThumbnail/MWThumbnail.test.js
PASS src/lib/mediawiki.ui/components/MWInput/MWInput.test.js
PASS src/lib/mediawiki.ui/components/MWIcon/MWIcon.test.js
PASS src/lib/mediawiki.ui/components/MWRadioGroup/MWRadio.test.js
PASS src/utils/segmentedContentConverter.test.js
PASS src/lib/mediawiki.ui/components/MWCard/MWcard.test.js

Test Suites: 28 passed, 28 total
Tests:       62 passed, 62 total
Snapshots:   28 passed, 28 total
Time:        8.213s, estimated 12s
Ran all test suites.
[master 0c5579e] build: Updating eslint-config-wikimedia to 0.18.2
 3 files changed, 238 insertions(+), 389 deletions(-)

$ git format-patch HEAD~1 --stdout
From 0c5579e53bc2ead1e2573666bbe318b7a5a6172f Mon Sep 17 00:00:00 2001
From: libraryupgrader <tools.libraryupgrader@tools.wmflabs.org>
Date: Sat, 6 Mar 2021 11:15:10 +0000
Subject: [PATCH] build: Updating eslint-config-wikimedia to 0.18.2

Change-Id: I9ee4c7adebdb036dcfb1883945c2876e0e0c047a
---
 app/package-lock.json | 123 +++++++----
 package-lock.json     | 502 ++++++++++++++----------------------------
 package.json          |   2 +-
 3 files changed, 238 insertions(+), 389 deletions(-)

diff --git a/app/package-lock.json b/app/package-lock.json
index 7f9df3b..532eeb5 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -5309,17 +5309,6 @@
             "unique-filename": "^1.1.1"
           }
         },
-        "chalk": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
-          "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
         "chownr": {
           "version": "1.1.4",
           "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
@@ -5618,42 +5607,6 @@
             "schema-utils": "^2.5.0"
           }
         },
-        "vue-loader-v16": {
-          "version": "npm:vue-loader@16.1.2",
-          "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
-          "integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "chalk": "^4.1.0",
-            "hash-sum": "^2.0.0",
-            "loader-utils": "^2.0.0"
-          },
-          "dependencies": {
-            "json5": {
-              "version": "2.2.0",
-              "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
-              "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
-              "dev": true,
-              "optional": true,
-              "requires": {
-                "minimist": "^1.2.5"
-              }
-            },
-            "loader-utils": {
-              "version": "2.0.0",
-              "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
-              "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
-              "dev": true,
-              "optional": true,
-              "requires": {
-                "big.js": "^5.2.2",
-                "emojis-list": "^3.0.0",
-                "json5": "^2.1.2"
-              }
-            }
-          }
-        },
         "wrap-ansi": {
           "version": "6.2.0",
           "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@@ -26392,6 +26345,82 @@
         }
       }
     },
+    "vue-loader-v16": {
+      "version": "npm:vue-loader@16.1.2",
+      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
+      "integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "chalk": "^4.1.0",
+        "hash-sum": "^2.0.0",
+        "loader-utils": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+          "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true,
+          "optional": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true,
+          "optional": true
+        },
+        "hash-sum": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz",
+          "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==",
+          "dev": true,
+          "optional": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
     "vue-router": {
       "version": "3.5.1",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.1.tgz",
diff --git a/package-lock.json b/package-lock.json
index 958a685..a2657e9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -552,9 +552,9 @@
 			}
 		},
 		"@eslint/eslintrc": {
-			"version": "0.3.0",
-			"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz",
-			"integrity": "sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==",
+			"version": "0.4.0",
+			"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz",
+			"integrity": "sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog==",
 			"dev": true,
 			"requires": {
 				"ajv": "^6.12.4",
@@ -564,23 +564,10 @@
 				"ignore": "^4.0.6",
 				"import-fresh": "^3.2.1",
 				"js-yaml": "^3.13.1",
-				"lodash": "^4.17.20",
 				"minimatch": "^3.0.4",
 				"strip-json-comments": "^3.1.1"
 			},
 			"dependencies": {
-				"ajv": {
-					"version": "6.12.6",
-					"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-					"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-					"dev": true,
-					"requires": {
-						"fast-deep-equal": "^3.1.1",
-						"fast-json-stable-stringify": "^2.0.0",
-						"json-schema-traverse": "^0.4.1",
-						"uri-js": "^4.2.2"
-					}
-				},
 				"debug": {
 					"version": "4.3.1",
 					"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
@@ -590,12 +577,6 @@
 						"ms": "2.1.2"
 					}
 				},
-				"lodash": {
-					"version": "4.17.20",
-					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-					"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
-					"dev": true
-				},
 				"ms": {
 					"version": "2.1.2",
 					"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -726,31 +707,29 @@
 			"dev": true
 		},
 		"acorn-jsx": {
-			"version": "5.2.0",
-			"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
-			"integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
+			"version": "5.3.1",
+			"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
+			"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
 			"dev": true
 		},
 		"ajv": {
-			"version": "6.12.2",
-			"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
-			"integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
+			"version": "6.12.6",
+			"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+			"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
 			"dev": true,
 			"requires": {
 				"fast-deep-equal": "^3.1.1",
 				"fast-json-stable-stringify": "^2.0.0",
 				"json-schema-traverse": "^0.4.1",
 				"uri-js": "^4.2.2"
-			},
-			"dependencies": {
-				"fast-deep-equal": {
-					"version": "3.1.1",
-					"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
-					"integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
-					"dev": true
-				}
 			}
 		},
+		"ansi-colors": {
+			"version": "4.1.1",
+			"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+			"integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+			"dev": true
+		},
 		"ansi-regex": {
 			"version": "5.0.0",
 			"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
@@ -1191,9 +1170,9 @@
 			"dev": true
 		},
 		"core-js": {
-			"version": "3.8.3",
-			"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz",
-			"integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==",
+			"version": "3.9.1",
+			"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.9.1.tgz",
+			"integrity": "sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==",
 			"dev": true
 		},
 		"core-util-is": {
@@ -1614,14 +1593,6 @@
 			"dev": true,
 			"requires": {
 				"ansi-colors": "^4.1.1"
-			},
-			"dependencies": {
-				"ansi-colors": {
-					"version": "4.1.1",
-					"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
-					"integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
-					"dev": true
-				}
 			}
 		},
 		"entities": {
@@ -1652,13 +1623,13 @@
 			"dev": true
 		},
 		"eslint": {
-			"version": "7.18.0",
-			"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz",
-			"integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==",
+			"version": "7.21.0",
+			"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.21.0.tgz",
+			"integrity": "sha512-W2aJbXpMNofUp0ztQaF40fveSsJBjlSCSWpy//gzfTvwC+USs/nceBrKmlJOiM8r1bLwP2EuYkCqArn/6QTIgg==",
 			"dev": true,
 			"requires": {
-				"@babel/code-frame": "^7.0.0",
-				"@eslint/eslintrc": "^0.3.0",
+				"@babel/code-frame": "7.12.11",
+				"@eslint/eslintrc": "^0.4.0",
 				"ajv": "^6.10.0",
 				"chalk": "^4.0.0",
 				"cross-spawn": "^7.0.2",
@@ -1669,9 +1640,9 @@
 				"eslint-utils": "^2.1.0",
 				"eslint-visitor-keys": "^2.0.0",
 				"espree": "^7.3.1",
-				"esquery": "^1.2.0",
+				"esquery": "^1.4.0",
 				"esutils": "^2.0.2",
-				"file-entry-cache": "^6.0.0",
+				"file-entry-cache": "^6.0.1",
 				"functional-red-black-tree": "^1.0.1",
 				"glob-parent": "^5.0.0",
 				"globals": "^12.1.0",
@@ -1696,213 +1667,88 @@
 				"v8-compile-cache": "^2.0.3"
 			},
 			"dependencies": {
-				"debug": {
-					"version": "4.3.1",
-					"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
-					"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
-					"dev": true,
-					"requires": {
-						"ms": "2.1.2"
-					}
-				},
-				"lodash": {
-					"version": "4.17.20",
-					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-					"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
-					"dev": true
-				},
-				"lru-cache": {
-					"version": "6.0.0",
-					"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-					"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+				"@babel/code-frame": {
+					"version": "7.12.11",
+					"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
+					"integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
 					"dev": true,
 					"requires": {
-						"yallist": "^4.0.0"
+						"@babel/highlight": "^7.10.4"
 					}
 				},
-				"ms": {
-					"version": "2.1.2",
-					"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-					"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+				"@babel/helper-validator-identifier": {
+					"version": "7.12.11",
+					"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
+					"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
 					"dev": true
 				},
-				"semver": {
-					"version": "7.3.4",
-					"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
-					"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
-					"dev": true,
-					"requires": {
-						"lru-cache": "^6.0.0"
-					}
-				},
-				"yallist": {
-					"version": "4.0.0",
-					"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-					"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-					"dev": true
-				}
-			}
-		},
-		"eslint-config-wikimedia": {
-			"version": "0.18.1",
-			"resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.18.1.tgz",
-			"integrity": "sha512-93nHVH4CGxYwelbTjJQLr0xqn4XIe9WwWeGL4wMnELZW/Aceg52aT7AgIdV0659ReLzrCgxLPDvbeqB47LjBTQ==",
-			"dev": true,
-			"requires": {
-				"eslint": "^7.17.0",
-				"eslint-plugin-compat": "^3.9.0",
-				"eslint-plugin-es": "^4.1.0",
-				"eslint-plugin-jsdoc": "^30.7.13",
-				"eslint-plugin-json-es": "^1.5.1",
-				"eslint-plugin-mediawiki": "^0.2.6",
-				"eslint-plugin-mocha": "^8.0.0",
-				"eslint-plugin-no-jquery": "^2.5.0",
-				"eslint-plugin-node": "^11.1.0",
-				"eslint-plugin-qunit": "^5.2.0",
-				"eslint-plugin-vue": "^7.4.1",
-				"eslint-plugin-wdio": "^6.0.12"
-			},
-			"dependencies": {
-				"@eslint/eslintrc": {
-					"version": "0.3.0",
-					"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz",
-					"integrity": "sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==",
+				"@babel/highlight": {
+					"version": "7.13.8",
+					"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.8.tgz",
+					"integrity": "sha512-4vrIhfJyfNf+lCtXC2ck1rKSzDwciqF7IWFhXXrSOUC2O5DrVp+w4c6ed4AllTxhTkUP5x2tYj41VaxdVMMRDw==",
 					"dev": true,
 					"requires": {
-						"ajv": "^6.12.4",
-						"debug": "^4.1.1",
-						"espree": "^7.3.0",
-						"globals": "^12.1.0",
-						"ignore": "^4.0.6",
-						"import-fresh": "^3.2.1",
-						"js-yaml": "^3.13.1",
-						"lodash": "^4.17.20",
-						"minimatch": "^3.0.4",
-						"strip-json-comments": "^3.1.1"
+						"@babel/helper-validator-identifier": "^7.12.11",
+						"chalk": "^2.0.0",
+						"js-tokens": "^4.0.0"
 					},
 					"dependencies": {
-						"ajv": {
-							"version": "6.12.6",
-							"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-							"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+						"chalk": {
+							"version": "2.4.2",
+							"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+							"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
 							"dev": true,
 							"requires": {
-								"fast-deep-equal": "^3.1.1",
-								"fast-json-stable-stringify": "^2.0.0",
-								"json-schema-traverse": "^0.4.1",
-								"uri-js": "^4.2.2"
+								"ansi-styles": "^3.2.1",
+								"escape-string-regexp": "^1.0.5",
+								"supports-color": "^5.3.0"
 							}
 						}
 					}
 				},
-				"acorn-jsx": {
-					"version": "5.3.1",
-					"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
-					"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
-					"dev": true
-				},
-				"debug": {
-					"version": "4.3.1",
-					"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
-					"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
-					"dev": true,
-					"requires": {
-						"ms": "2.1.2"
-					}
-				},
-				"eslint": {
-					"version": "7.18.0",
-					"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz",
-					"integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==",
+				"ansi-styles": {
+					"version": "3.2.1",
+					"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+					"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
 					"dev": true,
 					"requires": {
-						"@babel/code-frame": "^7.0.0",
-						"@eslint/eslintrc": "^0.3.0",
-						"ajv": "^6.10.0",
-						"chalk": "^4.0.0",
-						"cross-spawn": "^7.0.2",
-						"debug": "^4.0.1",
-						"doctrine": "^3.0.0",
-						"enquirer": "^2.3.5",
-						"eslint-scope": "^5.1.1",
-						"eslint-utils": "^2.1.0",
-						"eslint-visitor-keys": "^2.0.0",
-						"espree": "^7.3.1",
-						"esquery": "^1.2.0",
-						"esutils": "^2.0.2",
-						"file-entry-cache": "^6.0.0",
-						"functional-red-black-tree": "^1.0.1",
-						"glob-parent": "^5.0.0",
-						"globals": "^12.1.0",
-						"ignore": "^4.0.6",
-						"import-fresh": "^3.0.0",
-						"imurmurhash": "^0.1.4",
-						"is-glob": "^4.0.0",
-						"js-yaml": "^3.13.1",
-						"json-stable-stringify-without-jsonify": "^1.0.1",
-						"levn": "^0.4.1",
-						"lodash": "^4.17.20",
-						"minimatch": "^3.0.4",
-						"natural-compare": "^1.4.0",
-						"optionator": "^0.9.1",
-						"progress": "^2.0.0",
-						"regexpp": "^3.1.0",
-						"semver": "^7.2.1",
-						"strip-ansi": "^6.0.0",
-						"strip-json-comments": "^3.1.0",
-						"table": "^6.0.4",
-						"text-table": "^0.2.0",
-						"v8-compile-cache": "^2.0.3"
+						"color-convert": "^1.9.0"
 					}
 				},
-				"espree": {
-					"version": "7.3.1",
-					"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
-					"integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
+				"color-convert": {
+					"version": "1.9.3",
+					"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+					"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
 					"dev": true,
 					"requires": {
-						"acorn": "^7.4.0",
-						"acorn-jsx": "^5.3.1",
-						"eslint-visitor-keys": "^1.3.0"
-					},
-					"dependencies": {
-						"eslint-visitor-keys": {
-							"version": "1.3.0",
-							"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
-							"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
-							"dev": true
-						}
+						"color-name": "1.1.3"
 					}
 				},
-				"file-entry-cache": {
-					"version": "6.0.0",
-					"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz",
-					"integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==",
-					"dev": true,
-					"requires": {
-						"flat-cache": "^3.0.4"
-					}
+				"color-name": {
+					"version": "1.1.3",
+					"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+					"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+					"dev": true
 				},
-				"flat-cache": {
-					"version": "3.0.4",
-					"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
-					"integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+				"debug": {
+					"version": "4.3.1",
+					"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+					"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
 					"dev": true,
 					"requires": {
-						"flatted": "^3.1.0",
-						"rimraf": "^3.0.2"
+						"ms": "2.1.2"
 					}
 				},
-				"flatted": {
-					"version": "3.1.1",
-					"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz",
-					"integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
+				"has-flag": {
+					"version": "3.0.0",
+					"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+					"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
 					"dev": true
 				},
 				"lodash": {
-					"version": "4.17.20",
-					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-					"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+					"version": "4.17.21",
+					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+					"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
 					"dev": true
 				},
 				"lru-cache": {
@@ -1929,36 +1775,13 @@
 						"lru-cache": "^6.0.0"
 					}
 				},
-				"table": {
-					"version": "6.0.7",
-					"resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz",
-					"integrity": "sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g==",
+				"supports-color": {
+					"version": "5.5.0",
+					"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+					"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
 					"dev": true,
 					"requires": {
-						"ajv": "^7.0.2",
-						"lodash": "^4.17.20",
-						"slice-ansi": "^4.0.0",
-						"string-width": "^4.2.0"
-					},
-					"dependencies": {
-						"ajv": {
-							"version": "7.0.3",
-							"resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz",
-							"integrity": "sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==",
-							"dev": true,
-							"requires": {
-								"fast-deep-equal": "^3.1.1",
-								"json-schema-traverse": "^1.0.0",
-								"require-from-string": "^2.0.2",
-								"uri-js": "^4.2.2"
-							}
-						},
-						"json-schema-traverse": {
-							"version": "1.0.0",
-							"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
-							"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-							"dev": true
-						}
+						"has-flag": "^3.0.0"
 					}
 				},
 				"yallist": {
@@ -1969,6 +1792,26 @@
 				}
 			}
 		},
+		"eslint-config-wikimedia": {
+			"version": "0.18.2",
+			"resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.18.2.tgz",
+			"integrity": "sha512-OllUgce2qODU/6481jg/a1kT/dygBDY1xhxXuAiQdYxJARV6LXyuiJw+wl1QBQz+huV9NXRxoJGC3L6x/NzC4g==",
+			"dev": true,
+			"requires": {
+				"eslint": "^7.17.0",
+				"eslint-plugin-compat": "^3.9.0",
+				"eslint-plugin-es": "^4.1.0",
+				"eslint-plugin-jsdoc": "^30.7.13",
+				"eslint-plugin-json-es": "^1.5.1",
+				"eslint-plugin-mediawiki": "^0.2.7",
+				"eslint-plugin-mocha": "^8.0.0",
+				"eslint-plugin-no-jquery": "^2.5.0",
+				"eslint-plugin-node": "^11.1.0",
+				"eslint-plugin-qunit": "^5.2.0",
+				"eslint-plugin-vue": "^7.7.0",
+				"eslint-plugin-wdio": "^6.0.12"
+			}
+		},
 		"eslint-plugin-compat": {
 			"version": "3.9.0",
 			"resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-3.9.0.tgz",
@@ -1986,9 +1829,9 @@
 			},
 			"dependencies": {
 				"caniuse-lite": {
-					"version": "1.0.30001180",
-					"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001180.tgz",
-					"integrity": "sha512-n8JVqXuZMVSPKiPiypjFtDTXc4jWIdjxull0f92WLo7e1MSi3uJ3NvveakSh/aCl1QKFAvIz3vIj0v+0K+FrXw==",
+					"version": "1.0.30001196",
+					"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001196.tgz",
+					"integrity": "sha512-CPvObjD3ovWrNBaXlAIGWmg2gQQuJ5YhuciUOjPRox6hIQttu8O+b51dx6VIpIY9ESd2d0Vac1RKpICdG4rGUg==",
 					"dev": true
 				},
 				"semver": {
@@ -2034,9 +1877,9 @@
 					}
 				},
 				"lodash": {
-					"version": "4.17.20",
-					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-					"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+					"version": "4.17.21",
+					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+					"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
 					"dev": true
 				},
 				"lru-cache": {
@@ -2072,67 +1915,29 @@
 			}
 		},
 		"eslint-plugin-json-es": {
-			"version": "1.5.1",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-json-es/-/eslint-plugin-json-es-1.5.1.tgz",
-			"integrity": "sha512-YMzAWMcmKOYWiH0MsN3JOr0AdtZ2Rvmk3YmscsX1rHYJZRsL4KRo+yj9ktRk7S7mgy+G5TORWJ5D3/vH/u7R5A==",
+			"version": "1.5.3",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-json-es/-/eslint-plugin-json-es-1.5.3.tgz",
+			"integrity": "sha512-9wWjwhoN+ipMel70ktkWy0H7jj9sm5OAbAy3N3F3AT0swpIofVsIjDXyjGZJwSzy9tZzDtI/aKIj2WsqMHw2QA==",
 			"dev": true,
 			"requires": {
-				"eslint-visitor-keys": "^1.3.0",
+				"eslint-visitor-keys": "^2.0.0",
 				"espree": "^7.3.1"
-			},
-			"dependencies": {
-				"acorn-jsx": {
-					"version": "5.3.1",
-					"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
-					"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
-					"dev": true
-				},
-				"eslint-visitor-keys": {
-					"version": "1.3.0",
-					"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
-					"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
-					"dev": true
-				},
-				"espree": {
-					"version": "7.3.1",
-					"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
-					"integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
-					"dev": true,
-					"requires": {
-						"acorn": "^7.4.0",
-						"acorn-jsx": "^5.3.1",
-						"eslint-visitor-keys": "^1.3.0"
-					}
-				}
 			}
 		},
 		"eslint-plugin-mediawiki": {
-			"version": "0.2.6",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-mediawiki/-/eslint-plugin-mediawiki-0.2.6.tgz",
-			"integrity": "sha512-e7gx15H39ceam9AnSr6DDyfhMM9L43PVagHzclH3CF33DvWKi/OA+j2dqzJTuJcl5P/EmVIQHG5qoTaepkADsw==",
+			"version": "0.2.7",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-mediawiki/-/eslint-plugin-mediawiki-0.2.7.tgz",
+			"integrity": "sha512-2ZvPvLEwCIqrJxV1349bdX5Q03c30WccuUMCfB1Gh2IVxbBSrY0gbzOk/gPZeYigVhODt9xoFWUCIz8jwTWfrA==",
 			"dev": true,
 			"requires": {
-				"eslint-plugin-vue": "^6.2.2",
+				"eslint-plugin-vue": "^7.7.0",
 				"upath": "^1.2.0"
-			},
-			"dependencies": {
-				"eslint-plugin-vue": {
-					"version": "6.2.2",
-					"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-6.2.2.tgz",
-					"integrity": "sha512-Nhc+oVAHm0uz/PkJAWscwIT4ijTrK5fqNqz9QB1D35SbbuMG1uB6Yr5AJpvPSWg+WOw7nYNswerYh0kOk64gqQ==",
-					"dev": true,
-					"requires": {
-						"natural-compare": "^1.4.0",
-						"semver": "^5.6.0",
-						"vue-eslint-parser": "^7.0.0"
-					}
-				}
 			}
 		},
 		"eslint-plugin-mocha": {
-			"version": "8.0.0",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.0.0.tgz",
-			"integrity": "sha512-n67etbWDz6NQM+HnTwZHyBwz/bLlYPOxUbw7bPuCyFujv7ZpaT/Vn6KTAbT02gf7nRljtYIjWcTxK/n8a57rQQ==",
+			"version": "8.1.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-8.1.0.tgz",
+			"integrity": "sha512-1EgHvXKRl7W3mq3sntZAi5T24agRMyiTPL4bSXe+B4GksYOjAPEWYx+J3eJg4It1l2NMNZJtk0gQyQ6mfiPhQg==",
 			"dev": true,
 			"requires": {
 				"eslint-utils": "^2.1.0",
@@ -2194,15 +1999,15 @@
 			}
 		},
 		"eslint-plugin-vue": {
-			"version": "7.5.0",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.5.0.tgz",
-			"integrity": "sha512-QnMMTcyV8PLxBz7QQNAwISSEs6LYk2LJvGlxalXvpCtfKnqo7qcY0aZTIxPe8QOnHd7WCwiMZLOJzg6A03T0Gw==",
+			"version": "7.7.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.7.0.tgz",
+			"integrity": "sha512-mYz4bpLGv5jx6YG/GvKkqbGSfV7uma2u1P3mLA41Q5vQl8W1MeuTneB8tfsLq6xxxesFubcrOC0BZBJ5R+eaCQ==",
 			"dev": true,
 			"requires": {
 				"eslint-utils": "^2.1.0",
 				"natural-compare": "^1.4.0",
 				"semver": "^7.3.2",
-				"vue-eslint-parser": "^7.4.1"
+				"vue-eslint-parser": "^7.6.0"
 			},
 			"dependencies": {
 				"lru-cache": {
@@ -2321,18 +2126,18 @@
 			"dev": true
 		},
 		"esquery": {
-			"version": "1.3.1",
-			"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz",
-			"integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==",
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+			"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
 			"dev": true,
 			"requires": {
 				"estraverse": "^5.1.0"
 			},
 			"dependencies": {
 				"estraverse": {
-					"version": "5.1.0",
-					"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz",
-					"integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==",
+					"version": "5.2.0",
+					"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+					"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
 					"dev": true
 				}
 			}
@@ -2554,9 +2359,9 @@
 			}
 		},
 		"file-entry-cache": {
-			"version": "6.0.0",
-			"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz",
-			"integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==",
+			"version": "6.0.1",
+			"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+			"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
 			"dev": true,
 			"requires": {
 				"flat-cache": "^3.0.4"
@@ -3413,9 +3218,9 @@
 			"dev": true
 		},
 		"js-yaml": {
-			"version": "3.13.1",
-			"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
-			"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+			"version": "3.14.1",
+			"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+			"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
 			"dev": true,
 			"requires": {
 				"argparse": "^1.0.7",
@@ -5496,9 +5301,9 @@
 			},
 			"dependencies": {
 				"ajv": {
-					"version": "7.0.3",
-					"resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz",
-					"integrity": "sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==",
+					"version": "7.1.1",
+					"resolved": "https://registry.npmjs.org/ajv/-/ajv-7.1.1.tgz",
+					"integrity": "sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ==",
 					"dev": true,
 					"requires": {
 						"fast-deep-equal": "^3.1.1",
@@ -5514,9 +5319,9 @@
 					"dev": true
 				},
 				"lodash": {
-					"version": "4.17.20",
-					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-					"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+					"version": "4.17.21",
+					"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+					"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
 					"dev": true
 				}
 			}
@@ -5783,9 +5588,9 @@
 			"dev": true
 		},
 		"v8-compile-cache": {
-			"version": "2.1.0",
-			"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
-			"integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
+			"version": "2.3.0",
+			"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+			"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
 			"dev": true
 		},
 		"v8flags": {
@@ -5837,16 +5642,16 @@
 			}
 		},
 		"vue-eslint-parser": {
-			"version": "7.4.1",
-			"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.4.1.tgz",
-			"integrity": "sha512-AFvhdxpFvliYq1xt/biNBslTHE/zbEvSnr1qfHA/KxRIpErmEDrQZlQnvEexednRHmLfDNOMuDYwZL5xkLzIXQ==",
+			"version": "7.6.0",
+			"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.6.0.tgz",
+			"integrity": "sha512-QXxqH8ZevBrtiZMZK0LpwaMfevQi9UL7lY6Kcp+ogWHC88AuwUPwwCIzkOUc1LR4XsYAt/F9yHXAB/QoD17QXA==",
 			"dev": true,
 			"requires": {
 				"debug": "^4.1.1",
 				"eslint-scope": "^5.0.0",
 				"eslint-visitor-keys": "^1.1.0",
 				"espree": "^6.2.1",
-				"esquery": "^1.0.1",
+				"esquery": "^1.4.0",
 				"lodash": "^4.17.15"
 			},
 			"dependencies": {
@@ -5876,6 +5681,21 @@
 						"eslint-visitor-keys": "^1.1.0"
 					}
 				},
+				"esquery": {
+					"version": "1.4.0",
+					"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+					"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+					"dev": true,
+					"requires": {
+						"estraverse": "^5.1.0"
+					}
+				},
+				"estraverse": {
+					"version": "5.2.0",
+					"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+					"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
+					"dev": true
+				},
 				"ms": {
 					"version": "2.1.2",
 					"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
diff --git a/package.json b/package.json
index aa33fdd..c476641 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
 	},
 	"pre-commit": "precommit",
 	"devDependencies": {
-		"eslint-config-wikimedia": "0.18.1",
+		"eslint-config-wikimedia": "0.18.2",
 		"grunt": "1.3.0",
 		"grunt-banana-checker": "0.9.0",
 		"grunt-eslint": "23.0.0",
-- 
2.20.1

composer dependencies

Dependencies
Development dependencies

npm dependencies

Development dependencies

Logs

Source code is licensed under the AGPL.