mediawiki/extensions/ContentTranslation: main (log #960307)

sourcepatches

This run took 66 seconds.

$ date
--- stdout ---
Sun Mar 19 21:35:30 UTC 2023

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

--- end ---
$ git config user.name libraryupgrader
--- stdout ---

--- end ---
$ git config user.email tools.libraryupgrader@tools.wmflabs.org
--- stdout ---

--- end ---
$ git submodule update --init
--- stdout ---

--- end ---
$ grr init
--- stdout ---
Installed commit-msg hook.

--- end ---
$ git show-ref refs/heads/master
--- stdout ---
2a98f01b3dfd44dc5395f0fba6880cbe15ade729 refs/heads/master

--- end ---
$ /usr/bin/npm audit --json --legacy-peer-deps
--- stdout ---
{
  "auditReportVersion": 2,
  "vulnerabilities": {},
  "metadata": {
    "vulnerabilities": {
      "info": 0,
      "low": 0,
      "moderate": 0,
      "high": 0,
      "critical": 0,
      "total": 0
    },
    "dependencies": {
      "prod": 1,
      "dev": 359,
      "optional": 0,
      "peer": 0,
      "peerOptional": 0,
      "total": 359
    }
  }
}

--- end ---
$ /usr/bin/composer install
--- stderr ---
No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.
Loading composer repositories with package information
Info from https://repo.packagist.org: #StandWithUkraine
Updating dependencies
Lock file operations: 38 installs, 0 updates, 0 removals
  - Locking composer/installers (v2.2.0)
  - Locking composer/pcre (3.1.0)
  - Locking composer/semver (3.3.2)
  - Locking composer/spdx-licenses (1.5.7)
  - Locking composer/xdebug-handler (3.0.3)
  - Locking doctrine/deprecations (v1.0.0)
  - Locking felixfbecker/advanced-json-rpc (v3.2.1)
  - Locking firebase/php-jwt (v5.5.1)
  - Locking mediawiki/mediawiki-codesniffer (v41.0.0)
  - Locking mediawiki/mediawiki-phan-config (0.12.0)
  - Locking mediawiki/minus-x (1.1.1)
  - Locking mediawiki/phan-taint-check-plugin (4.0.0)
  - Locking microsoft/tolerant-php-parser (v0.1.1)
  - Locking netresearch/jsonmapper (v4.1.0)
  - Locking phan/phan (5.4.1)
  - Locking php-parallel-lint/php-console-color (v1.0.1)
  - Locking php-parallel-lint/php-console-highlighter (v1.0.0)
  - Locking php-parallel-lint/php-parallel-lint (v1.3.2)
  - Locking phpdocumentor/reflection-common (2.2.0)
  - Locking phpdocumentor/reflection-docblock (5.3.0)
  - Locking phpdocumentor/type-resolver (1.7.0)
  - Locking phpstan/phpdoc-parser (1.16.1)
  - Locking psr/container (1.1.2)
  - Locking psr/log (1.1.4)
  - Locking sabre/event (5.1.4)
  - Locking squizlabs/php_codesniffer (3.7.2)
  - Locking symfony/console (v5.4.21)
  - Locking symfony/deprecation-contracts (v2.5.2)
  - Locking symfony/polyfill-ctype (v1.27.0)
  - Locking symfony/polyfill-intl-grapheme (v1.27.0)
  - Locking symfony/polyfill-intl-normalizer (v1.27.0)
  - Locking symfony/polyfill-mbstring (v1.27.0)
  - Locking symfony/polyfill-php73 (v1.27.0)
  - Locking symfony/polyfill-php80 (v1.27.0)
  - Locking symfony/service-contracts (v2.5.2)
  - Locking symfony/string (v5.4.21)
  - Locking tysonandre/var_representation_polyfill (0.1.3)
  - Locking webmozart/assert (1.11.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 38 installs, 0 updates, 0 removals
    0 [>---------------------------]    0 [->--------------------------]    0 [--->------------------------]  - Installing composer/installers (v2.2.0): Extracting archive
  - Installing composer/pcre (3.1.0): Extracting archive
  - Installing firebase/php-jwt (v5.5.1): Extracting archive
  - Installing symfony/polyfill-php80 (v1.27.0): Extracting archive
  - Installing squizlabs/php_codesniffer (3.7.2): Extracting archive
  - Installing symfony/polyfill-mbstring (v1.27.0): Extracting archive
  - Installing composer/spdx-licenses (1.5.7): Extracting archive
  - Installing composer/semver (3.3.2): Extracting archive
  - Installing mediawiki/mediawiki-codesniffer (v41.0.0): Extracting archive
  - Installing tysonandre/var_representation_polyfill (0.1.3): Extracting archive
  - Installing symfony/polyfill-intl-normalizer (v1.27.0): Extracting archive
  - Installing symfony/polyfill-intl-grapheme (v1.27.0): Extracting archive
  - Installing symfony/polyfill-ctype (v1.27.0): Extracting archive
  - Installing symfony/string (v5.4.21): Extracting archive
  - Installing symfony/deprecation-contracts (v2.5.2): Extracting archive
  - Installing psr/container (1.1.2): Extracting archive
  - Installing symfony/service-contracts (v2.5.2): Extracting archive
  - Installing symfony/polyfill-php73 (v1.27.0): Extracting archive
  - Installing symfony/console (v5.4.21): Extracting archive
  - Installing sabre/event (5.1.4): Extracting archive
  - Installing netresearch/jsonmapper (v4.1.0): Extracting archive
  - Installing microsoft/tolerant-php-parser (v0.1.1): Extracting archive
  - Installing webmozart/assert (1.11.0): Extracting archive
  - Installing phpstan/phpdoc-parser (1.16.1): Extracting archive
  - Installing phpdocumentor/reflection-common (2.2.0): Extracting archive
  - Installing doctrine/deprecations (v1.0.0): Extracting archive
  - Installing phpdocumentor/type-resolver (1.7.0): Extracting archive
  - Installing phpdocumentor/reflection-docblock (5.3.0): Extracting archive
  - Installing felixfbecker/advanced-json-rpc (v3.2.1): Extracting archive
  - Installing psr/log (1.1.4): Extracting archive
  - Installing composer/xdebug-handler (3.0.3): Extracting archive
  - Installing phan/phan (5.4.1): Extracting archive
  - Installing mediawiki/phan-taint-check-plugin (4.0.0): Extracting archive
  - Installing mediawiki/mediawiki-phan-config (0.12.0): Extracting archive
  - Installing mediawiki/minus-x (1.1.1): Extracting archive
  - Installing php-parallel-lint/php-console-color (v1.0.1): Extracting archive
  - Installing php-parallel-lint/php-console-highlighter (v1.0.0): Extracting archive
  - Installing php-parallel-lint/php-parallel-lint (v1.3.2): Extracting archive
  0/28 [>---------------------------]   0%
 10/28 [==========>-----------------]  35%
 20/28 [====================>-------]  71%
 27/28 [===========================>]  96%
 28/28 [============================] 100%5 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating autoload files
15 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
--- stdout ---

--- end ---
Upgrading n:eslint-config-wikimedia from 0.22.1 -> 0.24.0
Upgrading n:stylelint-config-wikimedia from 0.13.0 -> 0.14.0
$ /usr/bin/npm install
--- stdout ---

added 353 packages, and audited 354 packages in 9s

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

found 0 vulnerabilities

--- end ---
$ package-lock-lint package-lock.json
--- stdout ---
Checking package-lock.json

--- end ---
$ package-lock-lint package-lock.json
--- stdout ---
Checking package-lock.json

--- end ---
$ ./node_modules/.bin/eslint . --fix
--- stdout ---

/src/repo/modules/.eslintrc.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/base/ext.cx.model.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/base/mw.cx.SiteMapper.js
    1:1   error    Definition for rule 'es/no-promise' was not found  es/no-promise
  143:12  warning  URL is not supported in IE 11                      compat/compat

/src/repo/modules/cache/mw.cx.ApiResponseCache.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/cache/mw.cx.CategoryCache.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/cache/mw.cx.NamespaceCache.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/cache/mw.cx.TitlePairCache.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dashboard/ext.cx.dashboard.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dashboard/ext.cx.recommendtool.client.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dashboard/mw.cx.DashboardList.js
    1:1  error    Definition for rule 'es/no-promise' was not found                        es/no-promise
  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
    1:1   error    Definition for rule 'es/no-promise' was not found                    es/no-promise
  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
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.PageTitleModel.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.SectionState.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.SectionTitleModel.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.Translation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.TranslationIssue.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.WikiPage.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.betafeature.init.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.contributions.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.contributionsmenu.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.languagesearcher.init.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.languagesearcher.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.mffrequentlanguages.js
    1:1  error    Definition for rule 'es/no-promise' was not found  es/no-promise
  148:0  warning  The type 'LanguageSearcher' is undefined           jsdoc/no-undefined-types

/src/repo/modules/entrypoints/ext.cx.entrypoints.newarticle.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.newarticle.veloader.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.newbytranslation.js
    1:1   error    Definition for rule 'es/no-promise' was not found                                es/no-promise
  177:35  warning  navigator.languages() is not supported in Safari 9.1, iOS Safari 9.0-9.2, IE 11  compat/compat

/src/repo/modules/entrypoints/ext.cx.entrypoints.newbytranslation.mobile.js
   1:1   error    Definition for rule 'es/no-promise' was not found                                es/no-promise
  10:35  warning  navigator.languages() is not supported in Safari 9.1, iOS Safari 9.0-9.2, IE 11  compat/compat
  38:10  warning  fetch is not supported in Safari 9.1, IE 11                                      compat/compat

/src/repo/modules/entrypoints/ext.cx.entrypoints.recentedit/RecentEditEntrypointInvitation.vue
    1:1   error    Definition for rule 'es/no-promise' was not found  es/no-promise
    8:7   warning  Don't use 'v-text'                                 vue/no-v-text
   32:6   warning  Don't use 'v-text'                                 vue/no-v-text
   61:7   warning  Don't use 'v-text'                                 vue/no-v-text
   66:6   warning  Don't use 'v-text'                                 vue/no-v-text
  116:29  error    ES2015 'Array.prototype.keys' method is forbidden  es-x/no-array-prototype-keys
  129:11  error    ES2015 'Array.prototype.keys' method is forbidden  es-x/no-array-prototype-keys

/src/repo/modules/entrypoints/ext.cx.entrypoints.recentedit/index.js
   1:1   error    Definition for rule 'es/no-promise' was not found  es/no-promise
  36:10  warning  fetch is not supported in Safari 9.1, IE 11        compat/compat

/src/repo/modules/entrypoints/ext.cx.entrypoints.recenttranslation/RecentTranslationEntrypointDialog.vue
    1:1   error    Definition for rule 'es/no-promise' was not found  es/no-promise
    7:10  warning  Don't use 'v-text'                                 vue/no-v-text
   16:6   warning  Don't use 'v-text'                                 vue/no-v-text
   22:6   warning  Don't use 'v-text'                                 vue/no-v-text
   33:7   warning  Don't use 'v-text'                                 vue/no-v-text
   37:12  warning  Don't use 'v-text'                                 vue/no-v-text
   38:11  warning  Don't use 'v-text'                                 vue/no-v-text
   41:12  warning  Don't use 'v-text'                                 vue/no-v-text
   42:11  warning  Don't use 'v-text'                                 vue/no-v-text
   46:12  warning  Don't use 'v-text'                                 vue/no-v-text
   47:11  warning  Don't use 'v-text'                                 vue/no-v-text
   54:13  warning  Don't use 'v-text'                                 vue/no-v-text
   61:8   warning  Don't use 'v-text'                                 vue/no-v-text
   75:7   warning  Don't use 'v-text'                                 vue/no-v-text
   80:13  warning  Don't use 'v-text'                                 vue/no-v-text
   84:7   warning  Don't use 'v-text'                                 vue/no-v-text
  176:4   warning  fetch is not supported in Safari 9.1, IE 11        compat/compat
  180:7   warning  Promise.reject() is not supported in IE 11         compat/compat
  180:7   error    ES2015 'Promise' class is forbidden                es-x/no-promise
  184:30  error    ES2015 'Array.prototype.keys' method is forbidden  es-x/no-array-prototype-keys

/src/repo/modules/entrypoints/ext.cx.entrypoints.recenttranslation/index.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.uls.relevantlanguages.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.uls.relevantlanguages/CxUlsEntrypoint.vue
   1:1  error    Definition for rule 'es/no-promise' was not found  es/no-promise
  30:6  warning  Don't use 'v-text'                                 vue/no-v-text
  40:5  warning  Don't use 'v-text'                                 vue/no-v-text
  68:7  warning  Don't use 'v-text'                                 vue/no-v-text

/src/repo/modules/entrypoints/ext.cx.interlanguagelink.init.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.interlanguagelink.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.uls.quick.actions.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/eventlogging/ext.cx.eventlogging.campaigns.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/eventlogging/ext.cx.eventlogging.translation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/eventlogging/legacy/ext.cx.eventlogging.translation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.MachineTranslationManager.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.MachineTranslationService.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.MwApiRequestManager.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.TargetArticle.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.TranslationController.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.TranslationTracker.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.init.Translation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.init.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/publish/ext.cx.wikibase.link.js
   1:1   error    Definition for rule 'es/no-promise' was not found  es/no-promise
  30:11  error    ES2015 'Promise' class is forbidden                es-x/no-promise
  43:10  warning  Promise.resolve() is not supported in IE 11        compat/compat
  43:10  error    ES2015 'Promise' class is forbidden                es-x/no-promise

/src/repo/modules/source/mw.cx.SelectedSourcePage.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/source/mw.cx.SelectedSourcePageDialog.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/source/mw.cx.SourcePageSelector.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/stats/ext.cx.stats.js
    1:1   error    Definition for rule 'es/no-promise' was not found                    es/no-promise
  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.validator.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/tools/mw.cx.tools.InstructionsTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/tools/mw.cx.tools.IssueTrackingTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/tools/mw.cx.tools.TranslationTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/tools/mw.cx.tools.TranslationToolFactory.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/translation/ext.cx.translation.conflict.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.ArticleColumn.js
   1:1  error    Definition for rule 'es/no-promise' was not found  es/no-promise
  17:0  warning  Duplicate @param "config.sectionTitle"             jsdoc/check-param-names

/src/repo/modules/ui/mw.cx.ui.CaptchaDialog.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.Categories.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.Infobar.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.LanguageFilter.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.LoginDialog.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.SourceColumn.js
  1:1  error    Definition for rule 'es/no-promise' was not found                                es/no-promise
  6:0  warning  @param path declaration ("config.siteMapper") appears before any real parameter  jsdoc/check-param-names

/src/repo/modules/ui/mw.cx.ui.TargetColumn.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.ToolsColumn.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.TranslationHeader.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.TranslationView.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.CategoryInputWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.CategoryMultiselectWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.CategoryTagItemWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.FeatureDiscoveryWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.MessageWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
    1:1   error    Definition for rule 'es/no-promise' was not found                     es/no-promise
  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
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.SectionTitleWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.TitleOptionWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.TranslationIssueWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.TranslationToolWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/util/mw.cx.util.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/mixins/ve.ce.CXLintableNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/mixins/ve.ce.CXPendingNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXBlockImageNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXImageCaptionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXLinkAnnotation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXPlaceholderNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXReferenceNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXSectionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXSentenceSegmentAnnotation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXTransclusionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/mixins/ve.dm.CXLintableNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXBlockImageNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXImageCaptionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXLinkAnnotation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXPlaceholderNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXReferenceNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXSectionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXSentenceSegmentAnnotation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXTransclusionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXTranslationUnitModel.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/init/ve.init.mw.CXTarget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/actions/ve.ui.CXTranslationAction.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/tools/ve.ui.CXResetSectionTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/tools/ve.ui.CXSaveMTPreferenceTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXDesktopContext.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXInternalLinkAnnotationWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXLinkAnnotationInspector.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXLinkContextItem.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXPublishSettingsDialog.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXPublishSettingsTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXPublishTool.js
   1:1  error  Definition for rule 'es/no-promise' was not found      es/no-promise
  36:1  error  ES2015 'RegExp.prototype.flags' property is forbidden  es-x/no-regexp-prototype-flags

/src/repo/modules/ve-cx/ui/ve.ui.CXReferenceContextItem.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXReferenceDialog.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXSurface.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXTextSelectionContextItem.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXTransclusionContextItem.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXTranslationToolbar.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXTranslationUnitContextItem.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/widgets/callout/ext.cx.callout.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/widgets/feedback/ext.cx.feedback.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/widgets/progressbar/ext.cx.progressbar.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/widgets/spinner/ext.cx.spinner.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/widgets/translator/ext.cx.translator.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/.eslintrc.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/base/mw.cx.SiteMapper.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/dm/mw.cx.dm.Translation.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/mw.cx.MachineTranslationService.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/mw.cx.TargetArticle.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/mw.cx.TranslationTracker.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/mw.cx.util.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/ui/mw.cx.ui.Infobar.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

✖ 192 problems (142 errors, 50 warnings)


--- end ---
$ ./node_modules/.bin/eslint . -f json
--- stdout ---
[{"filePath":"/src/repo/.eslintrc.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/.stylelintrc.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/composer.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/extension.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/i18n/api/en.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/i18n/api/qqq.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/i18n/en.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/i18n/qqq.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/modules/.eslintrc.json","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"{\n\t\"root\": true,\n\t\"extends\": [\n\t\t\"wikimedia/client-es5\",\n\t\t\"wikimedia/jquery\",\n\t\t\"wikimedia/mediawiki\",\n\t\t\"wikimedia/jsduck\"\n\t],\n\t\"globals\": {\n\t\t\"ve\": \"readonly\",\n\t\t\"moment\": \"readonly\",\n\t\t\"Promise\": \"readonly\"\n\t},\n\t\"rules\": {\n\t\t\"max-len\": \"off\",\n\t\t\"no-jquery/no-global-selector\": \"off\",\n\t\t\"no-shadow\": \"warn\",\n\t\t\"compat/compat\": \"warn\",\n\t\t\"es/no-promise\": \"warn\",\n\t\t\"vue/no-v-text\": \"warn\"\n\t}\n}\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/base/ext.cx.model.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation extension\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\t// mw.cx base model\n\tmw.cx = {};\n\t// Source language\n\tmw.cx.sourceLanguage = null;\n\t// Target language\n\tmw.cx.targetLanguage = null;\n\t// Different tools that are available\n\tmw.cx.tools = {};\n\t// Default sitemapper. Only use this if you cannot get access to a sitemapper\n\t// in other way. Do not confuse mw.cx.SiteMapper which is the module.\n\tmw.cx.siteMapper = null;\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/base/mw.cx.SiteMapper.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"compat/compat","severity":1,"message":"URL is not supported in IE 11","line":143,"column":12,"nodeType":"NewExpression","endLine":143,"endColumn":37}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation extension\n * A tool that allows editors to translate pages from one language\n * to another with the help of machine translation and other translation\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n\n'use strict';\n\n/**\n * Handles providing URLs to different wikis.\n *\n * @class\n * @param {Object} [overrides] Configuration overrides (defaults from PHP configuration)\n */\nmw.cx.SiteMapper = function ( overrides ) {\n\tvar config = require( '../config.json' );\n\n\toverrides = overrides || {};\n\n\tvar siteMapperConfig = $.extend( {}, config, overrides );\n\tthis.siteTemplates = siteMapperConfig.SiteTemplates;\n\tthis.codeMap = siteMapperConfig.DomainCodeMapping;\n\tthis.translateInTarget = siteMapperConfig.TranslateInTarget;\n\tthis.targetNamespace = siteMapperConfig.TargetNamespace;\n\n\tthis.languagePairsPromise = null;\n};\n\n/**\n * Some wikis have domain names that do not match the content language.\n * See: wgLanguageCode in operations/mediawiki-config/wmf-config/InitialiseSettings.php\n *\n * @param {string} language Language code\n * @return {string}\n */\nmw.cx.SiteMapper.prototype.getWikiDomainCode = function ( language ) {\n\treturn this.codeMap[ language ] || language;\n};\n\n/**\n * Gets the source language code for current wiki.\n *\n * We can't rely on wgContentLanguage because this will fail for a\n * wiki like simple.wikipedia.org, where the content language is the same as\n * on en.wikipedia.org, as well as some other edge cases. But we use the known\n * mappings to do backwards conversion for known problematic domains, and\n * wgContentLanguage for rest of the cases.\n *\n * @return {string} Source language code\n */\nmw.cx.SiteMapper.prototype.getCurrentWikiLanguageCode = function () {\n\tvar from = mw.config.get( 'wgServerName' ).split( '.', 1 )[ 0 ],\n\t\tfallback = mw.config.get( 'wgContentLanguage' );\n\n\treturn this.getLanguageCodeForWikiDomain( from, fallback );\n};\n\n/**\n * @param {string} domain\n * @param {string} [fallback]\n * @return {string}\n */\nmw.cx.SiteMapper.prototype.getLanguageCodeForWikiDomain = function ( domain, fallback ) {\n\tvar code;\n\n\tfor ( code in this.codeMap ) {\n\t\tif ( this.codeMap[ code ] === domain ) {\n\t\t\treturn code;\n\t\t}\n\t}\n\n\treturn fallback || domain;\n};\n\n/**\n * Get the API for a remote wiki.\n *\n * @param {string} language Language code\n * @param {Object} [options] Api options\n * @return {mw.ForeignApi} api\n */\nmw.cx.SiteMapper.prototype.getApi = function ( language, options ) {\n\tvar url, domain;\n\n\tdomain = this.getWikiDomainCode( language );\n\turl = this.siteTemplates.api.replace( '$1', domain );\n\toptions = $.extend( { anonymous: true }, options );\n\treturn new mw.ForeignApi( url, options );\n};\n\n/**\n * This method returns a boolean indicating whether\n * the current domain is a mobile production wiki domain.\n *\n * Mobile versions of production wiki domains contain the\n * \".m.\" part. The method checks if the \".m.\" part\n * is present inside the domain of the current URL.\n * This method doesn't affect development environments\n * or Mediawiki installations that use a different\n * mobile URL template than the default.\n *\n * @return {boolean}\n */\nmw.cx.SiteMapper.prototype.isMobileDomain = function () {\n\treturn location.hostname.indexOf( '.m.' ) > 0;\n};\n\n/**\n * Get a URL to an article in a wiki for a given language.\n *\n * @param {string} [language] Language code\n * @param {string} title Page title\n * @param {Object} [params] Query parameters\n * @param {string|null} [hash] the hash property of the URL\n * @return {string}\n */\nmw.cx.SiteMapper.prototype.getPageUrl = function ( language, title, params, hash ) {\n\tvar domain,\n\t\tbase = this.siteTemplates.view,\n\t\tprefix;\n\n\t// Use current wiki's content language, if no language given\n\tlanguage = language || mw.config.get( 'wgContentLanguage' );\n\n\tdomain = this.getWikiDomainCode( language );\n\tprefix = domain.replace( /\\$/g, '$$$$' );\n\n\tif ( this.isMobileDomain() ) {\n\t\tprefix += '.m';\n\t}\n\n\tif ( params && !$.isEmptyObject( params ) ) {\n\t\tbase = this.siteTemplates.action || this.siteTemplates.view;\n\t}\n\n\tbase = base.replace( '$1', prefix ).replace( '$2', mw.util.wikiUrlencode( title ).replace( /\\$/g, '$$$$' ) );\n\n\t// use location object as base URL, in order to handle protocol relative paths\n\t// when base includes an absolute path, the location object won't be taken into account\n\tvar url = new URL( base, location );\n\tfor ( var key in params ) {\n\t\turl.searchParams.append( key, params[ key ] );\n\t}\n\n\tif ( hash ) {\n\t\turl.hash = hash;\n\t}\n\n\treturn url.toString();\n};\n\n/**\n * Get the cxserver URL for the current site.\n *\n * @param {string} module CXServer module path\n * @param {Object} [params]\n * @return {string}\n */\nmw.cx.SiteMapper.prototype.getCXServerUrl = function ( module, params ) {\n\tvar paramKey, cxserverURL = this.siteTemplates.cx;\n\n\tif ( params ) {\n\t\tfor ( paramKey in params ) {\n\t\t\tmodule = module.replace( paramKey, encodeURIComponent( params[ paramKey ] ) );\n\t\t}\n\t}\n\n\tif ( mw.cx.getCXVersion() === 2 ) {\n\t\tcxserverURL = cxserverURL.replace( 'v1', 'v2' );\n\t}\n\n\treturn cxserverURL + module;\n};\n\nmw.cx.SiteMapper.prototype.getRestbaseUrl = function ( language, module, params ) {\n\tvar paramKey,\n\t\tdomain = this.getWikiDomainCode( language ),\n\t\turl = this.siteTemplates.restbase.replace( '$1', domain );\n\n\tif ( params ) {\n\t\tfor ( paramKey in params ) {\n\t\t\tmodule = module.replace( paramKey, encodeURIComponent( params[ paramKey ] ) );\n\t\t}\n\t}\n\n\treturn url + module;\n};\n\n/**\n * Get the target title to publish based on the wiki configuration.\n *\n * @param {string} title\n * @return {string} target title\n */\nmw.cx.SiteMapper.prototype.getTargetTitle = function ( title ) {\n\tvar targetTitle;\n\n\t// If the title is already in the user namespace, just return it\n\tif ( new mw.Title( title ).getNamespaceId() === 2 ) {\n\t\treturn title;\n\t}\n\n\tswitch ( this.targetNamespace ) {\n\t\t// Main namespace\n\t\tcase 0:\n\t\t\ttargetTitle = title;\n\t\t\tbreak;\n\t\t// User\n\t\tcase 2:\n\t\t\ttargetTitle = 'User:' + mw.user.getName() + '/' + title;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\ttargetTitle = mw.Title.makeTitle( this.targetNamespace, title ).getPrefixedText();\n\t\t\tbreak;\n\t}\n\n\treturn targetTitle;\n};\n\n/**\n * Get all the source and target languages.\n *\n * @return {jQuery.Promise}\n */\nmw.cx.SiteMapper.prototype.getLanguagePairs = function () {\n\tvar languagePairsAPIUrl,\n\t\tself = this;\n\n\tif ( !this.languagePairsPromise ) {\n\t\tlanguagePairsAPIUrl = this.getCXServerUrl( '/list/languagepairs' );\n\t\tthis.languagePairsPromise = $.get( languagePairsAPIUrl )\n\t\t\t.then( function ( response ) {\n\t\t\t\treturn {\n\t\t\t\t\ttargetLanguages: response.target,\n\t\t\t\t\tsourceLanguages: response.source\n\t\t\t\t};\n\t\t\t}, function ( response ) {\n\t\t\t\tmw.log(\n\t\t\t\t\t'Error getting language pairs from ' + languagePairsAPIUrl + ' . ' +\n\t\t\t\t\tresponse.statusText + ' (' + response.status + '). ' +\n\t\t\t\t\tresponse.responseText\n\t\t\t\t);\n\t\t\t\tself.languagePairsPromise = null;\n\t\t\t\treturn $.Deferred().reject().promise();\n\t\t\t} );\n\t}\n\treturn this.languagePairsPromise;\n};\n\n/**\n * Get the URL for Special:CX on the needed wiki\n * according to given source and target title and the target language.\n *\n * @param {string} sourceTitle\n * @param {string|null} targetTitle\n * @param {string} sourceLanguage\n * @param {string} targetLanguage\n * @param {Object} [extra] Additional query parameters\n * @return {string} URL\n */\nmw.cx.SiteMapper.prototype.getCXUrl = function (\n\tsourceTitle,\n\ttargetTitle,\n\tsourceLanguage,\n\ttargetLanguage,\n\textra\n) {\n\tvar cxPage, queryParams, uri;\n\n\tcxPage = 'Special:ContentTranslation';\n\tqueryParams = $.extend( {\n\t\tfrom: sourceLanguage,\n\t\tto: targetLanguage\n\t}, extra );\n\n\tif ( sourceTitle ) {\n\t\tqueryParams.page = sourceTitle;\n\t}\n\n\tif ( targetTitle ) {\n\t\tqueryParams.targettitle = targetTitle;\n\t}\n\n\tif ( this.translateInTarget ) {\n\t\turi = new mw.Uri( this.getPageUrl( targetLanguage, cxPage ) );\n\t\t// Use mw.Uri().query for current URL also to retain any non-CX params\n\t\t// in URL. A good example is debug=true param.\n\t\turi.query = $.extend( {}, mw.Uri().query, uri.query, queryParams );\n\n\t\treturn uri.toString();\n\t}\n\n\treturn mw.util.getUrl( cxPage, queryParams );\n};\n\n/**\n * Set CX Token in a cookie.\n * This token guarantees that the translator reads the license agreement\n * and starts translating from CX dashboard enabled as beta feature.\n * It is recommended to configure the cookie domain.\n *\n * @param {string} sourceLanguage Source language\n * @param {string} targetLanguage Target language\n * @param {string} sourceTitle Source title\n */\nmw.cx.SiteMapper.prototype.setCXToken = function ( sourceLanguage, targetLanguage, sourceTitle ) {\n\tvar name, options;\n\n\t// base64 encode the name to get cookie name.\n\tname = 'cx_' + btoa( encodeURIComponent( [ sourceTitle, sourceLanguage, targetLanguage ].join( '_' ) ) );\n\t// Remove all characters that are not allowed in cookie name: ( ) < > @ , ; : \\ \" / [ ] ? = { }.\n\tname = name.replace( /[()<>@,;\\\\[\\]?={}]/g, '' );\n\t// sameSite set to None and secure set to true to make the cookie visible on cross-domain requests.\n\toptions = {\n\t\tprefix: '',\n\t\texpires: 3600,\n\t\tsameSite: 'None',\n\t\tsecure: true\n\t};\n\n\t// BC with old default behavior\n\tif ( this.siteTemplates.cookieDomain === null ) {\n\t\t// Save that information in a domain cookie.\n\t\toptions.domain = location.hostname.indexOf( '.' ) > 0 ?\n\t\t\t'.' + location.hostname.split( '.' ).splice( 1 ).join( '.' ) :\n\t\t\tnull; // Mostly for domains like \"localhost\"\n\t} else if ( typeof this.siteTemplates.cookieDomain === 'string' ) {\n\t\t// Explicit domain cookie, preferred way\n\t\toptions.domain = this.siteTemplates.cookieDomain;\n\t}\n\t// Else: use whatever is the default\n\n\t// At this point, the translator saw the license agreement.\n\tmw.cookie.set( name, true, options );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/cache/mw.cx.ApiResponseCache.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation ApiResponseCache class.\n * This is a copy of ApiResponseCache class from VE.\n * When CX is fully integrated with VE, we can reuse VE class directly\n */\n\n'use strict';\n\n/**\n * MediaWiki API batch queue.\n *\n * Used to queue up lists of items centrally to get information about in batches of requests.\n *\n * @class\n * @extends OO.EventEmitter\n * @constructor\n * @param {Object} config Configuration\n */\nmw.cx.ApiResponseCache = function CXMwApiResponseCache( config ) {\n\t// Mixin constructor\n\tOO.EventEmitter.call( this );\n\n\tthis.siteMapper = config.siteMapper;\n\t// Keys are titles, values are deferreds\n\tthis.deferreds = {};\n\n\tthis.language = config.language;\n\t// Keys are page names, values are link data objects\n\t// This is kept for synchronous retrieval of cached values via #getCached\n\tthis.cacheValues = {};\n\n\t// Array of page titles queued to be looked up\n\tthis.queue = [];\n\n\tthis.schedule = OO.ui.debounce( this.processQueue.bind( this ), 0 );\n};\n\n/* Inheritance */\n\nOO.mixinClass( mw.cx.ApiResponseCache, OO.EventEmitter );\n\n/* Static methods */\n\n/**\n * Process each page in the response of an API request\n *\n * @abstract\n * @static\n * @method\n * @param {Object} page The page object\n * @return {Object|undefined} Any relevant info that we want to cache and return.\n */\nmw.cx.ApiResponseCache.static.processPage = null;\n\n/**\n * Normalize the title of the response\n *\n * @param {string} title Title\n * @return {string} Normalized title\n */\nmw.cx.ApiResponseCache.static.normalizeTitle = function ( title ) {\n\tvar titleObj = mw.Title.newFromText( title );\n\tif ( !titleObj ) {\n\t\treturn title;\n\t}\n\treturn titleObj.getPrefixedText();\n};\n\n/* Methods */\n\n/**\n * Look up data about a title. If the data about this title is already in the cache, this\n * returns an already-resolved promise. Otherwise, it returns a pending promise and schedules\n * an request to retrieve the data.\n *\n * @param {string} title Title\n * @return {jQuery.Promise} Promise that will be resolved with the data once it's available\n */\nmw.cx.ApiResponseCache.prototype.get = function ( title ) {\n\tif ( typeof title !== 'string' ) {\n\t\t// Don't bother letting things like undefined or null make it all the way through,\n\t\t// just reject them here. Otherwise they'll cause problems or exceptions at random\n\t\t// other points in this file.\n\t\treturn $.Deferred().reject().promise();\n\t}\n\ttitle = this.constructor.static.normalizeTitle( title );\n\tif ( !Object.prototype.hasOwnProperty.call( this.deferreds, title ) ) {\n\t\tthis.deferreds[ title ] = $.Deferred();\n\t\tthis.queue.push( title );\n\t\tthis.schedule();\n\t}\n\treturn this.deferreds[ title ].promise();\n};\n\n/**\n * Look up data about a page in the cache. If the data about this page is already in the cache,\n * this returns that data. Otherwise, it returns undefined.\n *\n * @param {string} name Normalized page title\n * @return {Object|undefined} Cache data for this name.\n */\nmw.cx.ApiResponseCache.prototype.getCached = function ( name ) {\n\tif ( Object.prototype.hasOwnProperty.call( this.cacheValues, name ) ) {\n\t\treturn this.cacheValues[ name ];\n\t}\n};\n\n/**\n * Fired when a new entry is added to the cache.\n *\n * @event add\n * @param {Object} entries Cache entries that were added. Object mapping names to data objects.\n */\n\n/**\n * Add entries to the cache. Does not overwrite already-set entries.\n *\n * @param {Object} entries Object keyed by page title, with the values being data objects\n * @fires add\n */\nmw.cx.ApiResponseCache.prototype.set = function ( entries ) {\n\tvar name;\n\tfor ( name in entries ) {\n\t\tif ( !Object.prototype.hasOwnProperty.call( this.deferreds, name ) ) {\n\t\t\tthis.deferreds[ name ] = $.Deferred();\n\t\t}\n\t\tif ( this.deferreds[ name ].state() === 'pending' ) {\n\t\t\tthis.deferreds[ name ].resolve( entries[ name ] );\n\t\t\tthis.cacheValues[ name ] = entries[ name ];\n\t\t}\n\t}\n\tthis.emit( 'add', Object.keys( entries ) );\n};\n\n/**\n * Get an API request promise to deal with a list of titles\n *\n * @abstract\n * @method\n * @param subqueue\n * @return {jQuery.Promise}\n */\nmw.cx.ApiResponseCache.prototype.getRequestPromise = null;\n\n/**\n * Perform any scheduled API requests.\n *\n * @private\n * @fires add\n */\nmw.cx.ApiResponseCache.prototype.processQueue = function () {\n\tvar subqueue, queue,\n\t\tcache = this;\n\n\tfunction rejectSubqueue( rejectQueue ) {\n\t\tvar i, len;\n\t\tfor ( i = 0, len = rejectQueue.length; i < len; i++ ) {\n\t\t\tcache.deferreds[ rejectQueue[ i ] ].reject();\n\t\t}\n\t}\n\n\tfunction processResult( data ) {\n\t\tvar pageid, page, i, processedPage,\n\t\t\tpages = ( data.query && data.query.pages ) || data.pages,\n\t\t\tredirects,\n\t\t\tprocessed = {};\n\n\t\tredirects = data.query.redirects || {};\n\t\tif ( pages ) {\n\t\t\tfor ( pageid in pages ) {\n\t\t\t\tpage = pages[ pageid ];\n\t\t\t\tprocessedPage = cache.constructor.static.processPage( page, redirects );\n\t\t\t\tif ( processedPage !== undefined ) {\n\t\t\t\t\tprocessed[ page.title ] = processedPage;\n\t\t\t\t}\n\t\t\t\tfor ( i in redirects ) {\n\t\t\t\t\t// Locate the title in redirects, if any.\n\t\t\t\t\tif ( redirects[ i ].to === page.title ) {\n\t\t\t\t\t\tprocessed[ redirects[ i ].from ] = processedPage;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcache.set( processed );\n\t\t}\n\t}\n\n\tqueue = this.queue;\n\tthis.queue = [];\n\twhile ( queue.length ) {\n\t\tsubqueue = queue.splice( 0, 50 ).map( this.constructor.static.normalizeTitle );\n\t\tthis.getRequestPromise( subqueue )\n\t\t\t.then( processResult )\n\n\t\t\t// Reject everything in subqueue; this will only reject the ones\n\t\t\t// that weren't already resolved above, because .reject() on an\n\t\t\t// already resolved Deferred is a no-op.\n\t\t\t.then( rejectSubqueue.bind( null, subqueue ) );\n\t}\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/cache/mw.cx.CategoryCache.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * ContentTranslation Category request cache\n *\n */\n\n'use strict';\n/**\n * Caches information about title pairs.\n *\n * @class\n * @extends mw.cx.ApiResponseCache\n * @constructor\n * @param {Object} config Configuration\n */\nmw.cx.CategoryCache = function CXCategoryCache( config ) {\n\t// Call parent constructor\n\tmw.cx.CategoryCache.super.call( this, config );\n\tthis.language = config.language;\n\tthis.cacheValues = {};\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.CategoryCache, mw.cx.ApiResponseCache );\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nmw.cx.CategoryCache.static.processPage = function ( page ) {\n\treturn {\n\t\tcategories: ( page.categories || [] ).map( function ( item ) {\n\t\t\treturn item.title;\n\t\t} )\n\t};\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.CategoryCache.prototype.getRequestPromise = function ( subqueue ) {\n\treturn this.siteMapper.getApi( this.language ).get( {\n\t\tformatversion: 2,\n\t\taction: 'query',\n\t\tprop: 'categories',\n\t\tclshow: '!hidden',\n\t\tcllimit: 100,\n\t\ttitles: subqueue.join( '|' )\n\t} );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/cache/mw.cx.NamespaceCache.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * ContentTranslation Namespace request cache\n *\n */\n\n'use strict';\n/**\n * Caches namespace aliases in a language\n *\n * @class\n * @extends mw.cx.ApiResponseCache\n * @constructor\n * @param {Object} config Configuration\n */\nmw.cx.NamespaceCache = function CXNamespaceCache( config ) {\n\tthis.requestPromise = null;\n\tthis.language = config.language;\n\tthis.cacheValues = {};\n\tthis.siteMapper = config.siteMapper;\n};\n\n/* Methods */\n\n/**\n * Get the value from request or cached request\n *\n * @param {string} canonicalNamespace\n * @return {jQuery.Promise}\n */\nmw.cx.NamespaceCache.prototype.get = function ( canonicalNamespace ) {\n\treturn this.getRequestPromise().then( function () {\n\t\treturn this.cacheValues[ canonicalNamespace ];\n\t}.bind( this ) );\n};\n\nmw.cx.NamespaceCache.prototype.processResponse = function ( response ) {\n\tvar namespaceId, namespaceObj;\n\n\tfor ( namespaceId in response.query.namespaces ) {\n\t\tnamespaceObj = response.query.namespaces[ namespaceId ];\n\t\tthis.cacheValues[ namespaceObj.canonical ] = namespaceObj[ '*' ];\n\t}\n};\n\nmw.cx.NamespaceCache.prototype.getRequestPromise = function () {\n\tthis.requestPromise = this.requestPromise ||\n\t\tthis.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\tthis.processResponse( response );\n\t\t}.bind( this ) );\n\treturn this.requestPromise;\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/cache/mw.cx.TitlePairCache.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * ContentTranslation Title pair request cache\n *\n */\n\n'use strict';\n\n/**\n * Caches information about title pairs.\n *\n * @class\n * @extends ve.init.mw.ApiResponseCache\n * @constructor\n * @param {Object} config Configuration\n */\nmw.cx.TitlePairCache = function CXTitlePairCache( config ) {\n\t// Call parent constructor\n\tmw.cx.TitlePairCache.super.call( this, config.siteMapper.getApi( config.sourceLanguage ) );\n\tthis.siteMapper = config.siteMapper;\n\tthis.targetLanguage = config.targetLanguage;\n\t// Keys are page names, values are link data objects\n\t// This is kept for synchronous retrieval of cached values via #getCached\n\tthis.cacheValues = {};\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.TitlePairCache, ve.init.mw.ApiResponseCache );\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nmw.cx.TitlePairCache.static.processPage = function ( page ) {\n\treturn {\n\t\tsourceTitle: OO.getProp( page, 'title' ),\n\t\ttargetTitle: OO.getProp( page, 'langlinks', 0, '*' ),\n\t\tmissing: OO.getProp( page, 'langlinks', 0, '*' ) === undefined\n\t};\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.TitlePairCache.prototype.getRequestPromise = function ( subqueue ) {\n\t// We use post here because the titles when joined will result a longer string\n\t// that GET requests cannot process sometimes.\n\treturn this.api.post( {\n\t\taction: 'query',\n\t\tprop: 'langlinks',\n\t\tlllimit: subqueue.length,\n\t\tlllang: this.siteMapper.getWikiDomainCode( this.targetLanguage ),\n\t\ttitles: subqueue.join( '|' ),\n\t\tredirects: true,\n\t\tcontinue: ''\n\t} );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dashboard/ext.cx.dashboard.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"mediawiki/class-doc","severity":2,"message":"All possible CSS classes should be documented. See https://w.wiki/PS2 for details.","line":258,"column":49,"nodeType":"ObjectExpression","endLine":266,"endColumn":6,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-new","severity":2,"message":"Do not use 'new' for side effects.","line":398,"column":3,"nodeType":"ExpressionStatement","messageId":"noNewStatement","endLine":405,"endColumn":7,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation extension - Dashboard.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\t/**\n\t * CXDashboard\n\t *\n\t * @class\n\t * @param {HTMLElement} element\n\t * @param {mw.cx.SiteMapper} siteMapper\n\t */\n\tfunction CXDashboard( element, siteMapper ) {\n\t\tthis.$container = $( element );\n\t\tthis.siteMapper = siteMapper;\n\n\t\tthis.$sidebar = null;\n\t\tthis.translator = null;\n\t\tthis.$publishedTranslationsButton = null;\n\t\tthis.lists = {};\n\t\tthis.infobar = null;\n\t\tthis.$translationListContainer = null;\n\t\tthis.newTranslationButton = null;\n\t\tthis.filter = null;\n\t\tthis.$listHeader = null;\n\t\tthis.$sourcePageSelector = null;\n\n\t\tthis.narrowLimit = 700;\n\t\tthis.isNarrowScreenSize = false;\n\n\t\tthis.filterLabels = {};\n\t\tif ( mw.config.get( 'wgContentTranslationEnableSuggestions' ) ) {\n\t\t\tthis.filterLabels.suggestions = {\n\t\t\t\twide: {\n\t\t\t\t\tlabel: mw.msg( 'cx-translation-filter-suggested-translations' ),\n\t\t\t\t\ticon: undefined\n\t\t\t\t},\n\t\t\t\tnarrow: {\n\t\t\t\t\tlabel: undefined,\n\t\t\t\t\ticon: 'lightbulb'\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\tthis.filterLabels.draft = {\n\t\t\twide: {\n\t\t\t\tlabel: mw.msg( 'cx-translation-filter-draft-translations' ),\n\t\t\t\ticon: undefined\n\t\t\t},\n\t\t\tnarrow: {\n\t\t\t\tlabel: undefined,\n\t\t\t\ticon: 'edit'\n\t\t\t}\n\t\t};\n\t\tthis.filterLabels.published = {\n\t\t\twide: {\n\t\t\t\tlabel: mw.msg( 'cx-translation-filter-published-translations' ),\n\t\t\t\ticon: undefined\n\t\t\t},\n\t\t\tnarrow: {\n\t\t\t\tlabel: undefined,\n\t\t\t\ticon: 'check'\n\t\t\t}\n\t\t};\n\t}\n\n\tCXDashboard.prototype.init = function () {\n\t\t// Render the main components\n\t\tthis.render();\n\n\t\t// Get acceptable source/target language pairs\n\t\tthis.siteMapper.getLanguagePairs().then(\n\t\t\tfunction ( data ) {\n\t\t\t\t// We store valid source and target languages as \"static\" variables of LanguageFilter\n\t\t\t\tmw.cx.ui.LanguageFilter.static.sourceLanguages = data.sourceLanguages;\n\t\t\t\tmw.cx.ui.LanguageFilter.static.targetLanguages = data.targetLanguages;\n\n\t\t\t\tthis.setDefaultLanguages();\n\n\t\t\t\tthis.initLists();\n\t\t\t\tthis.listen();\n\n\t\t\t\tmw.hook( 'mw.cx.dashboard.ready' ).fire();\n\t\t\t}.bind( this ),\n\t\t\tfunction () {\n\t\t\t\tmw.hook( 'mw.cx.error' ).fire( mw.msg( 'cx-error-server-connection' ) );\n\t\t\t}\n\t\t);\n\t};\n\n\t// Find valid source and target language pair and store them in local storage\n\tCXDashboard.prototype.setDefaultLanguages = function () {\n\t\tvar validDefaultLanguagePair = this.findValidDefaultLanguagePair();\n\n\t\tmw.storage.set( 'cxSourceLanguage', validDefaultLanguagePair.sourceLanguage );\n\t\tmw.storage.set( 'cxTargetLanguage', validDefaultLanguagePair.targetLanguage );\n\t};\n\n\t/**\n\t * Find valid source and target language pair, with different source and target language\n\t *\n\t * @return {Object} languages Valid and different source and target languages\n\t */\n\tCXDashboard.prototype.findValidDefaultLanguagePair = function () {\n\t\tvar sourceLanguage, targetLanguage, currentLang,\n\t\t\tcommonLanguages, i, length,\n\t\t\tquery = new mw.Uri().query,\n\t\t\tsourceLanguages = mw.cx.ui.LanguageFilter.static.sourceLanguages,\n\t\t\ttargetLanguages = mw.cx.ui.LanguageFilter.static.targetLanguages;\n\n\t\tsourceLanguage = query.from || mw.storage.get( 'cxSourceLanguage' );\n\t\ttargetLanguage = query.to || mw.storage.get( 'cxTargetLanguage' );\n\n\t\t// If query.to and local storage have no target language code,\n\t\t// or language code is invalid, fall back to content language as target.\n\t\tif ( targetLanguages.indexOf( targetLanguage ) < 0 ) {\n\t\t\ttargetLanguage = mw.config.get( 'wgContentLanguage' );\n\t\t}\n\n\t\tcommonLanguages = mw.uls.getFrequentLanguageList().filter( function ( lang ) {\n\t\t\treturn sourceLanguages.indexOf( lang ) !== -1;\n\t\t} );\n\n\t\tif ( sourceLanguages.indexOf( sourceLanguage ) < 0 || sourceLanguage === targetLanguage ) {\n\t\t\tfor ( i = 0, length = commonLanguages.length; i < length; i++ ) {\n\t\t\t\tcurrentLang = commonLanguages[ i ];\n\t\t\t\tif ( currentLang !== targetLanguage ) {\n\t\t\t\t\tsourceLanguage = currentLang;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// If wgContentLanguage has invalid language code for any reason, we try to find\n\t\t// some valid language from the list of common languages.\n\t\tif ( targetLanguages.indexOf( targetLanguage ) < 0 || sourceLanguage === targetLanguage ) {\n\t\t\tfor ( i = 0, length = commonLanguages.length; i < length; i++ ) {\n\t\t\t\tcurrentLang = commonLanguages[ i ];\n\t\t\t\tif ( currentLang !== sourceLanguage && targetLanguages.indexOf( currentLang ) !== -1 ) {\n\t\t\t\t\ttargetLanguage = currentLang;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// If the list of frequent languages does not have any valid language code different from\n\t\t// content language, we fall back to Spanish and English. Inability to find a suitable\n\t\t// language for source is most likely when target language is English, so we use most\n\t\t// common source language when translating to English. But, just in case list of frequent\n\t\t// languages only has code for content language, which is non-English, fall back to English\n\t\t// as source language.\n\t\t// Also, if local storage data is corrupted, either due to bug previously existing\n\t\t// in the code, or some manual change of local storage data, apply the same principle.\n\t\t// See T202286#4530740\n\t\tif ( sourceLanguages.indexOf( sourceLanguage ) < 0 || sourceLanguage === targetLanguage ) {\n\t\t\tif ( targetLanguage === 'en' ) {\n\t\t\t\tsourceLanguage = 'es';\n\t\t\t} else {\n\t\t\t\tsourceLanguage = 'en';\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tsourceLanguage: sourceLanguage,\n\t\t\ttargetLanguage: targetLanguage\n\t\t};\n\t};\n\n\t/**\n\t * Initialize the components\n\t */\n\tCXDashboard.prototype.initLists = function () {\n\t\tvar locationHash = location.hash.slice( 1 ),\n\t\t\tlists = [ 'draft', 'published' ];\n\n\t\tthis.renderTranslations();\n\t\tif ( mw.config.get( 'wgContentTranslationEnableSuggestions' ) ) {\n\t\t\tthis.renderTranslationSuggestions();\n\t\t\tlists.push( 'suggestions' );\n\t\t}\n\n\t\tif ( lists.indexOf( locationHash ) > -1 ) {\n\t\t\tthis.setActiveList( locationHash );\n\t\t} else {\n\t\t\tthis.setActiveList( 'draft' );\n\t\t\tthis.lists.draft.once( 'noDrafts', this.onNoDrafts.bind( this ) );\n\t\t}\n\t};\n\n\t/**\n\t * Populates various UI components with data in the given translation suggestions.\n\t */\n\tCXDashboard.prototype.renderTranslationSuggestions = function () {\n\t\tthis.lists.suggestions = new mw.cx.CXSuggestionList(\n\t\t\tthis.$translationListContainer,\n\t\t\tthis.siteMapper\n\t\t);\n\t};\n\n\t/**\n\t * Initiate and render the translation list.\n\t * TODO: Refactor this to move some logic to translationlist module\n\t */\n\tCXDashboard.prototype.renderTranslations = function () {\n\t\tthis.lists.draft = new mw.cx.CXTranslationList(\n\t\t\tthis.$translationListContainer,\n\t\t\tthis.siteMapper,\n\t\t\t'draft'\n\t\t);\n\t\tthis.lists.published = new mw.cx.CXTranslationList(\n\t\t\tthis.$translationListContainer,\n\t\t\tthis.siteMapper,\n\t\t\t'published'\n\t\t);\n\t};\n\n\tCXDashboard.prototype.getSidebarItems = function () {\n\t\treturn [\n\t\t\t{\n\t\t\t\ticon: 'infoFilled',\n\t\t\t\tclasses: [ 'cx-dashboard-sidebar__link', 'cx-dashboard-sidebar__link--information' ],\n\t\t\t\thref: 'https://www.mediawiki.org/wiki/Special:MyLanguage/Content_translation',\n\t\t\t\tlabel: mw.msg( 'cx-dashboard-sidebar-information' )\n\t\t\t},\n\t\t\t{\n\t\t\t\ticon: 'chart',\n\t\t\t\tclasses: [ 'cx-dashboard-sidebar__link', 'cx-dashboard-sidebar__link--stats' ],\n\t\t\t\thref: mw.util.getUrl( 'Special:ContentTranslationStats' ),\n\t\t\t\tlabel: mw.msg( 'cx-dashboard-sidebar-stats' )\n\t\t\t},\n\t\t\t{\n\t\t\t\ticon: 'speechBubbles',\n\t\t\t\tclasses: [ 'cx-dashboard-sidebar__link', 'cx-dashboard-sidebar__link--feedback' ],\n\t\t\t\thref: 'https://www.mediawiki.org/wiki/Talk:Content_translation',\n\t\t\t\tlabel: mw.msg( 'cx-dashboard-sidebar-feedback' )\n\t\t\t}\n\t\t];\n\t};\n\n\tCXDashboard.prototype.buildSidebar = function () {\n\t\tvar $help, i, item, items, $links = [];\n\n\t\tthis.translator = new mw.cx.widgets.CXTranslator();\n\t\tthis.$publishedTranslationsButton = this.translator.$lastMonthButton;\n\n\t\t$help = $( '<div>' )\n\t\t\t.addClass( 'cx-dashboard-sidebar__help' );\n\n\t\titems = this.getSidebarItems();\n\t\t$links = $( '<ul>' );\n\t\tfor ( i = 0; i < items.length; i++ ) {\n\t\t\titem = items[ i ];\n\t\t\t$links.append(\n\t\t\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\t\t\t$( '<li>' ).append( new OO.ui.ButtonWidget( {\n\t\t\t\t\ticon: item.icon,\n\t\t\t\t\tframed: false,\n\t\t\t\t\tclasses: item.classes,\n\t\t\t\t\tflags: [ 'progressive' ],\n\t\t\t\t\tlabel: item.label,\n\t\t\t\t\thref: item.href,\n\t\t\t\t\ttarget: '_blank'\n\t\t\t\t} ).$element )\n\t\t\t);\n\t\t}\n\t\t$help.append(\n\t\t\t$( '<div>' )\n\t\t\t\t.addClass( 'cx-dashboard-sidebar__help-title' )\n\t\t\t\t.text( mw.msg( 'cx-dashboard-sidebar-title' ) ),\n\t\t\t$links\n\t\t);\n\n\t\treturn [ this.translator.$widget, $help ];\n\t};\n\n\tCXDashboard.prototype.render = function () {\n\t\tthis.infobar = new mw.cx.ui.Infobar( this.config );\n\n\t\tthis.$translationListContainer = this.buildTranslationList();\n\t\tthis.$sidebar = $( '<div>' )\n\t\t\t.addClass( 'cx-dashboard-sidebar' )\n\t\t\t.append( this.buildSidebar() );\n\n\t\tthis.$dashboard = $( '<main>' )\n\t\t\t.addClass( 'cx-dashboard' )\n\t\t\t.append( this.$translationListContainer, this.$sidebar );\n\n\t\tthis.$container.append(\n\t\t\tthis.infobar.$element,\n\t\t\tthis.$dashboard\n\t\t);\n\t};\n\n\tCXDashboard.prototype.buildTranslationList = function () {\n\t\tvar size, name, props, $translationList,\n\t\t\tfilterButtons = [];\n\n\t\t// document.documentElement.clientWidth performs faster than $( window ).width()\n\t\tthis.isNarrowScreenSize = document.documentElement.clientWidth < this.narrowLimit;\n\n\t\tsize = this.isNarrowScreenSize ? 'narrow' : 'wide';\n\n\t\tfor ( name in this.filterLabels ) {\n\t\t\tprops = this.filterLabels[ name ];\n\n\t\t\tfilterButtons.push( new OO.ui.ButtonOptionWidget( {\n\t\t\t\tdata: name,\n\t\t\t\tlabel: props[ size ].label,\n\t\t\t\ticon: props[ size ].icon\n\t\t\t} ) );\n\t\t}\n\n\t\tthis.filter = new OO.ui.ButtonSelectWidget( {\n\t\t\titems: filterButtons\n\t\t} );\n\n\t\tthis.newTranslationButton = new OO.ui.ButtonWidget( {\n\t\t\tlabel: mw.msg( 'cx-create-new-translation' ),\n\t\t\ticon: 'add',\n\t\t\tflags: [\n\t\t\t\t'primary',\n\t\t\t\t'progressive'\n\t\t\t]\n\t\t} );\n\n\t\tthis.$listHeader = $( '<div>' ).addClass( 'cx-translation-filter' );\n\t\tthis.$listHeader.append(\n\t\t\tthis.newTranslationButton.$element,\n\t\t\tthis.filter.$element\n\t\t);\n\n\t\tthis.$sourcePageSelector = $( '<div>' )\n\t\t\t.addClass( 'cx-source-page-selector' );\n\n\t\t$translationList = $( '<div>' )\n\t\t\t.addClass( 'cx-translationlist-container' )\n\t\t\t.append( this.$listHeader, this.$sourcePageSelector );\n\n\t\treturn $translationList;\n\t};\n\n\tCXDashboard.prototype.setActiveList = function ( type ) {\n\t\tvar listName, list;\n\n\t\tthis.activeList = type;\n\t\tthis.filter.selectItemByData( type );\n\n\t\tfor ( listName in this.lists ) {\n\t\t\tlist = this.lists[ listName ];\n\n\t\t\tif ( listName === type ) {\n\t\t\t\tlist.show();\n\t\t\t\tlocation.hash = type;\n\t\t\t} else {\n\t\t\t\tlist.hide();\n\t\t\t}\n\t\t}\n\t};\n\n\tCXDashboard.prototype.listen = function () {\n\t\tthis.filter.connect( this, { select: function ( item ) {\n\t\t\tthis.setActiveList( item.getData() );\n\t\t} } );\n\n\t\tthis.$publishedTranslationsButton.on( 'click', function () {\n\t\t\tthis.filter.selectItemByData( 'published' );\n\t\t}.bind( this ) );\n\n\t\tthis.initSourceSelector();\n\t\tthis.newTranslationButton.connect( this, {\n\t\t\tclick: this.hideListHeader\n\t\t} );\n\t\t// Resize handler\n\t\t$( window ).on( 'resize', OO.ui.throttle( this.resize.bind( this ), 250 ) );\n\t};\n\n\t/**\n\t * Handler called when user has no draft translations\n\t */\n\tCXDashboard.prototype.onNoDrafts = function () {\n\t\tif ( mw.config.get( 'wgContentTranslationEnableSuggestions' ) ) {\n\t\t\tthis.setActiveList( 'suggestions' );\n\t\t}\n\t};\n\n\tCXDashboard.prototype.hideListHeader = function () {\n\t\tthis.$listHeader.hide();\n\t\twindow.scrollTo( window.pageXOffset, 0 ); // Equivalent to $( window ).scrollTop( 0 )\n\t};\n\n\tCXDashboard.prototype.initSourceSelector = function () {\n\t\tvar query = new mw.Uri().query;\n\n\t\t// eslint-disable-next-line no-new\n\t\tnew mw.cx.SourcePageSelector( this.newTranslationButton, {\n\t\t\tsiteMapper: this.siteMapper,\n\t\t\tsourceLanguage: query.from,\n\t\t\ttargetLanguage: query.to,\n\t\t\tsourceTitle: query.page,\n\t\t\ttargetTitle: query.targettitle,\n\t\t\t$container: this.$sourcePageSelector\n\t\t} );\n\t\t// Check for conditions that pre-open source page selector\n\t\tif ( query.to && ( ( query.from && query.page ) || query.targettitle ) ) {\n\t\t\tthis.hideListHeader();\n\t\t}\n\n\t\tif ( query.campaign ) {\n\t\t\tmw.hook( 'mw.cx.cta.accept' ).fire( query.campaign, query.from, query.page, query.to );\n\t\t}\n\t};\n\n\tCXDashboard.prototype.resize = function () {\n\t\tvar filterItems = this.filter.getItems(),\n\t\t\tnarrowScreenSize = document.documentElement.clientWidth < this.narrowLimit,\n\t\t\tsize = narrowScreenSize ? 'narrow' : 'wide';\n\n\t\tthis.translator.resize();\n\n\t\t// Exit early if screen size stays above/under narrow screen size limit\n\t\tif ( this.isNarrowScreenSize === narrowScreenSize ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Change filter labels to icons and vice-versa\n\t\tfilterItems.forEach( function ( filter ) {\n\t\t\tvar data = filter.getData(),\n\t\t\t\tlabel = this.filterLabels[ data ][ size ].label,\n\t\t\t\ticon = this.filterLabels[ data ][ size ].icon;\n\n\t\t\tfilter.setIcon( icon );\n\t\t\tfilter.setLabel( label );\n\t\t}, this );\n\t\tthis.isNarrowScreenSize = narrowScreenSize;\n\t};\n\n\t$( function () {\n\t\tvar dashboard;\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\tdashboard = new CXDashboard( document.body, mw.cx.siteMapper );\n\t\tdashboard.init();\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dashboard/ext.cx.recommendtool.client.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * Module to interface with article recommendation tool AND to get seeds for the tool.\n *\n * The recommendation tool is also known as trex in this code.\n *\n * See https://recommend.wmflabs.org/ and research/recommendation-api at Gerrit.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\t/**\n\t * RecommendTool\n\t *\n\t * @class\n\t * @param {string} from Source language\n\t * @param {string} to target language\n\t */\n\tfunction RecommendTool( from, to ) {\n\t\tthis.sourceLanguage = from;\n\t\tthis.targetLanguage = to;\n\t\tthis.seeds = null;\n\t}\n\n\t/**\n\t * Get a seed page for the current language pair.\n\t * The seed page is one of the previous translations (of any status) by the user.\n\t *\n\t * @return {jQuery.Promise}\n\t */\n\tRecommendTool.prototype.getSeedPages = function () {\n\t\tvar params,\n\t\t\tself = this,\n\t\t\tapi = new mw.Api();\n\n\t\tif ( this.seeds ) {\n\t\t\t// We have seeds. So provide first 3.\n\t\t\t// If we ran out of seeds, we will return empty seeds and that is ok.\n\t\t\treturn $.Deferred().resolve( this.seeds.splice( 0, 3 ) );\n\t\t}\n\n\t\t// Collect a bunch of seed pages from previous translations.\n\t\t// This API is also used for fetching the translations of user from translationlist module.\n\t\t// Effectively, the following request is duplicate. But it does not look like a good idea\n\t\t// to have dependency on translation list for this module. There are some complications as\n\t\t// well with sharing data - we are not sure which module gets initated first.\n\t\tparams = {\n\t\t\tassert: 'user',\n\t\t\tlist: 'contenttranslation',\n\t\t\tfrom: this.sourceLanguage,\n\t\t\tto: this.targetLanguage,\n\t\t\tlimit: 50\n\t\t};\n\n\t\treturn api.get( params ).then( function ( response ) {\n\t\t\tself.seeds = response.query.contenttranslation.translations.map( function ( value ) {\n\t\t\t\treturn value.translation.sourceTitle;\n\t\t\t} );\n\n\t\t\treturn self.seeds.splice( 0, 3 );\n\t\t} );\n\t};\n\n\t/**\n\t * @return {jQuery.Promise}\n\t */\n\tRecommendTool.prototype.getSuggestionList = function () {\n\t\tvar self = this;\n\n\t\treturn this.getSeedPages().then( function ( seedPages ) {\n\t\t\tvar algorithms, algorithm;\n\n\t\t\talgorithms = [ 'morelike', 'related_articles' ];\n\t\t\talgorithm = algorithms[ Math.floor( Math.random() * algorithms.length ) ];\n\t\t\treturn $.get( mw.config.get( 'wgRecommendToolAPIURL' ), {\n\t\t\t\tsource: self.sourceLanguage,\n\t\t\t\ttarget: self.targetLanguage,\n\t\t\t\tseed: seedPages.join( '|' ),\n\t\t\t\tsearch: algorithm,\n\t\t\t\tapplication: 'CX'\n\t\t\t} ).then( function ( articles ) {\n\t\t\t\treturn self.adapt( articles, algorithm );\n\t\t\t} );\n\t\t} );\n\t};\n\n\t/**\n\t * Adapt the API response of Recommendation tool to the CX suggestion list format.\n\t *\n\t * @param {Object} articles\n\t * @param {string} algorithm The algorithm used.\n\t * @return {Object}\n\t */\n\tRecommendTool.prototype.adapt = function ( articles, algorithm ) {\n\t\tvar i, title, suggestions = [];\n\n\t\tfor ( i = 0; i < articles.length; i++ ) {\n\t\t\ttitle = mw.Title.newFromText( articles[ i ].title );\n\t\t\tif ( !title ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tsuggestions.push( {\n\t\t\t\ttitle: title.getPrefixedText(),\n\t\t\t\tsourceLanguage: this.sourceLanguage,\n\t\t\t\ttargetLanguage: this.targetLanguage,\n\t\t\t\tpageviews: articles[ i ].pageviews,\n\t\t\t\twikidataId: articles[ i ].wikidata_id,\n\t\t\t\tlistId: 'trex'\n\t\t\t} );\n\t\t}\n\n\t\treturn {\n\t\t\tlists: {\n\t\t\t\ttrex: {\n\t\t\t\t\tname: 'cx-suggestionlist-trex',\n\t\t\t\t\ttype: 5,\n\t\t\t\t\talgorithm: algorithm,\n\t\t\t\t\tsuggestions: suggestions\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t};\n\n\tmw.cx.Recommendtool = RecommendTool;\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dashboard/mw.cx.DashboardList.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"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}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":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":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":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}],"suppressedMessages":[{"ruleId":"no-jquery/no-slide","severity":2,"message":"Prefer CSS transitions to .slideUp","line":606,"column":4,"nodeType":"CallExpression","endLine":608,"endColumn":7,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-jquery/no-class-state","severity":2,"message":"Where possible, maintain application state in JS to avoid slower DOM queries","line":948,"column":2,"nodeType":"CallExpression","endLine":948,"endColumn":81,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-jquery/no-class-state","severity":2,"message":"Where possible, maintain application state in JS to avoid slower DOM queries","line":950,"column":2,"nodeType":"CallExpression","endLine":950,"endColumn":86,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":1,"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.siteMapper.getWikiDomainCode( this.languageFilter.getSourceLanguage() ),\n\t\t\tthis.siteMapper.getWikiDomainCode( this.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( 'cx-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":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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( 'cx-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( 'cx-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( 'cx-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( 'cx-target-title' )\n\t\t\t\t.text( translation.targetTitle );\n\t\t\t$translationLink.append(\n\t\t\t\t$( '<span>' ).text( '\\u00A0' ), // 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( '.cx-translation-status' )\n\t\t.removeClass( 'cx-translation-status-draft cx-translation-status-published' )\n\t\t.addClass( 'cx-translation-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( '.cx-translation-link' )\n\t\t.addClass( 'cx-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":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * CX Section state data model\n *\n * @constructor\n * @param {number} sectionNumber\n */\nmw.cx.dm.SectionState = function MwCxSectionState( sectionNumber ) {\n\t// @var {number}\n\tthis.sectionNumber = sectionNumber;\n\t// @var {Object}\n\tthis.source = {\n\t\thtml: null,\n\t\ttext: null,\n\t\tsaved: false\n\t};\n\t// @var {Object}\n\tthis.unmodifiedMT = {};\n\t// @var {Object}\n\tthis.userTranslation = {\n\t\thtml: null,\n\t\ttext: null,\n\t\tsaved: false\n\t};\n\t// @var {string}\n\tthis.currentMTProvider = null;\n\t// @var {number}\n\tthis.unmodifiedPercentage = 0;\n\t// @var {number}\n\tthis.translationProgressPercentage = 0;\n\t// @var {boolean} Whether the section has any errors while saving\n\tthis.hasSaveError = false;\n\t// @var {number}\n\tthis.saveCount = 0;\n};\n\nmw.cx.dm.SectionState.prototype.setSource = function ( html ) {\n\tthis.source = {\n\t\thtml: html,\n\t\ttext: $( html ).text(),\n\t\tsaved: false\n\t};\n};\n\nmw.cx.dm.SectionState.prototype.getSource = function () {\n\treturn this.source;\n};\n\nmw.cx.dm.SectionState.prototype.setUserTranslation = function ( html ) {\n\tthis.userTranslation = {\n\t\thtml: html,\n\t\ttext: $( html ).text(),\n\t\tsaved: false\n\t};\n};\n\nmw.cx.dm.SectionState.prototype.getUserTranslation = function () {\n\treturn this.userTranslation;\n};\n\nmw.cx.dm.SectionState.prototype.setUnmodifiedMT = function ( html ) {\n\tif ( !this.currentMTProvider ) {\n\t\tthrow new Error( 'Attempting to set unmodified MT without an MT provider' );\n\t}\n\tthis.unmodifiedMT[ this.currentMTProvider ] = {\n\t\thtml: html,\n\t\ttext: $( html ).text(),\n\t\tsaved: false\n\t};\n};\n\nmw.cx.dm.SectionState.prototype.getUnmodifiedMT = function () {\n\treturn this.unmodifiedMT[ this.currentMTProvider ] || {};\n};\n\nmw.cx.dm.SectionState.prototype.setCurrentMTProvider = function ( provider ) {\n\tthis.currentMTProvider = provider;\n};\n\nmw.cx.dm.SectionState.prototype.getCurrentMTProvider = function () {\n\treturn this.currentMTProvider;\n};\n\nmw.cx.dm.SectionState.prototype.getUnmodifiedPercentage = function () {\n\treturn this.unmodifiedPercentage;\n};\n\nmw.cx.dm.SectionState.prototype.setUnmodifiedPercentage = function ( percent ) {\n\tthis.unmodifiedPercentage = percent;\n};\n\nmw.cx.dm.SectionState.prototype.getTranslationProgressPercentage = function () {\n\treturn this.translationProgressPercentage;\n};\n\nmw.cx.dm.SectionState.prototype.setTranslationProgressPercentage = function ( percent ) {\n\tthis.translationProgressPercentage = percent;\n};\n\n/**\n * Whether the section has modifications by user on top of the initial machine translation.\n *\n * @return {boolean}\n */\nmw.cx.dm.SectionState.prototype.isModified = function () {\n\tif ( this.getUserTranslation().text === null ) {\n\t\treturn false;\n\t}\n\treturn this.getUnmodifiedMT().html !== this.getUserTranslation().html;\n};\n\nmw.cx.dm.SectionState.prototype.markSourceSaved = function () {\n\tthis.source.saved = true;\n};\n\nmw.cx.dm.SectionState.prototype.isSourceSaved = function () {\n\treturn this.source.saved;\n};\n\nmw.cx.dm.SectionState.prototype.markUserTranslationSaved = function () {\n\tif ( !this.userTranslation ) {\n\t\tthrow new Error( 'Attempting to set user translation when it is not present.' );\n\t}\n\tthis.userTranslation.saved = true;\n};\n\nmw.cx.dm.SectionState.prototype.markUnmodifiedMTSaved = function () {\n\tif ( !this.currentMTProvider ) {\n\t\tthrow new Error( 'Attempting to set unmodified MT saved without an MT provider' );\n\t}\n\tthis.unmodifiedMT[ this.currentMTProvider ].saved = true;\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dm/mw.cx.dm.SectionTitleModel.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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.SectionTitleModel = function MwCxDmSectionTitleModel() {\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.SectionTitleModel, OO.EventEmitter );\nOO.mixinClass( mw.cx.dm.SectionTitleModel, ve.dm.CXLintableNode );\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nmw.cx.dm.SectionTitleModel.prototype.getId = function () {\n\treturn 'section-title';\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dm/mw.cx.dm.Translation.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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 {string|null} sourceSectionTitle Source section title\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,\n\tsourceSectionTitle,\n\tforTarget,\n\tsavedTranslationUnits,\n\tsourceLanguage\n) {\n\tvar childNodes, restoredContent, sxSectionNumber,\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\tif ( sourceSectionTitle ) {\n\n\t\tvar targetSectionNode = [].slice.call( domDoc.getElementsByTagName( 'h2' ) ).find(\n\t\t\tfunction ( el ) {\n\t\t\t\treturn el.innerText === sourceSectionTitle;\n\t\t\t} );\n\t\tif ( targetSectionNode ) {\n\t\t\tsxSectionNumber = targetSectionNode.parentNode.dataset.mwSectionNumber;\n\t\t}\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, 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\tvar sourceSectionNode = node.cloneNode( true );\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\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\tsectionNode = sourceSectionNode;\n\t\t}\n\n\t\tif ( sxSectionNumber && sxSectionNumber !== mwSectionNumber ) {\n\t\t\tsectionNode.classList.add( 'mw-section-hide' );\n\t\t}\n\n\t\t// if current translation is a section translation, we should hide the H2 header\n\t\t// inside source article column, and its corresponding placeholder inside the\n\t\t// target article column, since the section title is already displayed inside target title widget.\n\t\t// Here we use the source section node to identify and hide both the H2 header and its corresponding\n\t\t// placeholder inside the target column, as the placeholder doesn't have any special attribute\n\t\t// for such identification, itself.\n\t\tif ( sxSectionNumber && sourceSectionNode.querySelector( 'h2' ) ) {\n\t\t\tsectionNode.classList.add( 'mw-section-hide' );\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\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\n/**\n * Check if this translation is Section Translation\n *\n * @return {boolean}\n */\nmw.cx.dm.Translation.prototype.isSectionTranslation = function () {\n\treturn !!this.sourceWikiPage.getSectionTitle();\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\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":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * @class\n * @constructor\n * @param {string} name Unique issue identifier\n * @param {mw.Message|string} message Main message describing the translation issue.\n * Use mw.Message object for registered messages. For messages which are properly parsed,\n * string type can be used.\n * @param {Object} [messageInfo]\n * @cfg {string} [title]\n * @cfg {string} [type='warning'] 'warning' or 'error'\n * @cfg {string} [help]\n * @cfg {boolean} [resolvable=false]\n * @cfg {string} [actionIcon='check']\n * @cfg {string} [actionLabel]\n * @cfg {Function} [action]\n * @cfg {Object[]} [additionalButtons] Array of additional button configurations declaring icon, label and action.\n */\nmw.cx.dm.TranslationIssue = function CXTranslationIssue( name, message, messageInfo ) {\n\tthis.name = name;\n\tthis.message = message;\n\tthis.title = messageInfo && messageInfo.title;\n\tthis.type = messageInfo && messageInfo.type || 'warning';\n\tthis.help = messageInfo && messageInfo.help;\n\tthis.resolvable = messageInfo && messageInfo.resolvable === true;\n\tthis.actionIcon = messageInfo && messageInfo.actionIcon || 'check';\n\tthis.actionLabel = messageInfo && messageInfo.actionLabel;\n\tthis.action = messageInfo && messageInfo.action || this.suppress.bind( this );\n\tthis.additionalButtons = messageInfo && messageInfo.additionalButtons;\n\tthis.suppressed = false;\n\t// @var {Function}\n\tthis.onSuppressedCallback = null;\n};\n\n/* Methods */\n\nmw.cx.dm.TranslationIssue.prototype.getName = function () {\n\treturn this.name;\n};\n\nmw.cx.dm.TranslationIssue.prototype.getTitle = function () {\n\treturn this.title;\n};\n\nmw.cx.dm.TranslationIssue.prototype.getMessage = function () {\n\treturn this.message;\n};\n\n/**\n * Get the HTML content of the message.\n *\n * @return {jQuery|string} If message is registered, it's being parsed as DOM. HTML strings are\n * considered to be already parsed messages.\n */\nmw.cx.dm.TranslationIssue.prototype.getMessageContent = function () {\n\tif ( this.message instanceof mw.Message ) {\n\t\treturn this.message.parseDom();\n\t}\n\n\treturn this.message;\n};\n\nmw.cx.dm.TranslationIssue.prototype.getType = function () {\n\treturn this.type;\n};\n\nmw.cx.dm.TranslationIssue.prototype.getHelpLink = function () {\n\treturn this.help;\n};\n\nmw.cx.dm.TranslationIssue.prototype.isResolvable = function () {\n\treturn this.resolvable;\n};\n\nmw.cx.dm.TranslationIssue.prototype.getAction = function () {\n\treturn this.action;\n};\n\nmw.cx.dm.TranslationIssue.prototype.getIcon = function () {\n\treturn this.actionIcon;\n};\n\nmw.cx.dm.TranslationIssue.prototype.getLabel = function () {\n\treturn this.actionLabel;\n};\n\nmw.cx.dm.TranslationIssue.prototype.getAdditionalButtons = function () {\n\treturn this.additionalButtons;\n};\n\nmw.cx.dm.TranslationIssue.prototype.suppress = function () {\n\tthis.suppressed = true;\n\n\tif ( this.onSuppressedCallback ) {\n\t\tthis.onSuppressedCallback();\n\t}\n};\n\nmw.cx.dm.TranslationIssue.prototype.isSuppressed = function () {\n\treturn this.suppressed;\n};\n\nmw.cx.dm.TranslationIssue.prototype.setSuppressCallback = function ( fn ) {\n\tthis.onSuppressedCallback = fn;\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dm/mw.cx.dm.WikiPage.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * This uniquely identifies a wiki page with (title, language, [revision])\n *\n * @constructor\n * @param {string} title Page name\n * @param {string} language Language code\n * @param {number|null} revision\n * @param {string} sectionTitle Section title\n */\nmw.cx.dm.WikiPage = function MwCxDmWikiPage( title, language, revision, sectionTitle ) {\n\tthis.title = title;\n\tthis.language = language;\n\tthis.revision = revision;\n\tthis.sectionTitle = sectionTitle;\n};\n\nmw.cx.dm.WikiPage.prototype.getTitle = function () {\n\treturn this.title;\n};\n\nmw.cx.dm.WikiPage.prototype.getLanguage = function () {\n\treturn this.language;\n};\n\nmw.cx.dm.WikiPage.prototype.getDirection = function () {\n\treturn $.uls.data.getDir( this.language.toLowerCase() );\n};\n\nmw.cx.dm.WikiPage.prototype.getRevision = function () {\n\treturn this.revision;\n};\n\nmw.cx.dm.WikiPage.prototype.setRevision = function ( revision ) {\n\tthis.revision = revision;\n};\n\nmw.cx.dm.WikiPage.prototype.getSectionTitle = function () {\n\treturn this.sectionTitle;\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/dm/mw.cx.dm.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"mw.cx.dm = {};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.betafeature.init.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"( function () {\n\t'use strict';\n\n\t// This module get loaded when CX beta feature is enabled.\n\t$( function () {\n\t\tmw.hook( 'mw.cx.betafeature.enabled' ).fire();\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.contributions.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"mediawiki/class-doc","severity":2,"message":"All possible CSS classes should be documented. See https://w.wiki/PS2 for details.","line":36,"column":35,"nodeType":"ObjectExpression","endLine":42,"endColumn":5,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"mediawiki/class-doc","severity":2,"message":"All possible CSS classes should be documented. See https://w.wiki/PS2 for details.","line":80,"column":4,"nodeType":"ObjectExpression","endLine":89,"endColumn":5,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-new","severity":2,"message":"Do not use 'new' for side effects.","line":98,"column":3,"nodeType":"ExpressionStatement","messageId":"noNewStatement","endLine":98,"endColumn":54,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * Displays a set of entry points.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\tvar entrypointName = 'contributions-page';\n\n\t/**\n\t * @class\n\t * @param {HTMLElement} element\n\t */\n\tfunction CXContributions( element ) {\n\t\tthis.$element = $( element );\n\t\tthis.init();\n\t}\n\n\t/**\n\t * Initialize the plugin.\n\t */\n\tCXContributions.prototype.init = function () {\n\t\tthis.render();\n\t};\n\n\tCXContributions.prototype.render = function () {\n\t\tvar $sectionHeader, contributionButtons, contributionButtonsGroup;\n\n\t\t$sectionHeader = $( '<h1>' )\n\t\t\t.text( mw.msg( 'cx-contributions-new-contributions' ) );\n\n\t\tcontributionButtons = this.getActivities().map( function ( item ) {\n\t\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\t\treturn new OO.ui.ButtonWidget( {\n\t\t\t\tclasses: [ 'cx-contributions-item' ].concat( item.classes ),\n\t\t\t\tlabel: item.text,\n\t\t\t\ticon: item.icon,\n\t\t\t\ttitle: item.tooltip,\n\t\t\t\thref: item.url\n\t\t\t} );\n\t\t} );\n\t\tcontributionButtonsGroup = new OO.ui.ButtonGroupWidget( {\n\t\t\tclasses: [ 'cx-contributions' ],\n\t\t\titems: contributionButtons\n\t\t} );\n\n\t\tthis.$element.append( $sectionHeader, contributionButtonsGroup.$element );\n\t\tmw.hook( 'mw.cx.cta.shown' ).fire( entrypointName );\n\t};\n\n\t/**\n\t * A weak and inaccurate way to guess if this user has done\n\t * any contribution using CX.\n\t *\n\t * @return {boolean}\n\t */\n\tfunction isNewToCX() {\n\t\treturn $( '.mw-tag-marker-contenttranslation' ).length === 0;\n\t}\n\n\tCXContributions.prototype.getActivities = function () {\n\t\treturn [\n\t\t\t{\n\t\t\t\ttext: mw.msg( 'cx-contributions-new-article' ),\n\t\t\t\tclasses: [ 'cx-contributions-new-article' ],\n\t\t\t\ticon: 'article',\n\t\t\t\turl: mw.util.getUrl( 'Special:WantedPages' ),\n\t\t\t\ttooltip: mw.msg( 'cx-contributions-new-article-tooltip' )\n\t\t\t},\n\t\t\t{\n\t\t\t\ttext: mw.msg( 'cx-contributions-upload' ),\n\t\t\t\tclasses: [ 'cx-contributions-upload' ],\n\t\t\t\ticon: 'upload',\n\t\t\t\turl: 'https://commons.wikimedia.org/wiki/Special:UploadWizard',\n\t\t\t\ttooltip: mw.msg( 'cx-contributions-upload-tooltip' )\n\t\t\t},\n\t\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\t\t{\n\t\t\t\ttext: mw.msg( 'cx-contributions-translation' ),\n\t\t\t\tclasses: [ 'cx-contributions-translation' ]\n\t\t\t\t\t.concat( isNewToCX() ? [ 'cx-contributions-new' ] : [] ),\n\t\t\t\ticon: 'language',\n\t\t\t\turl: mw.util.getUrl( 'Special:ContentTranslation', {\n\t\t\t\t\tcampaign: entrypointName\n\t\t\t\t} ),\n\t\t\t\ttooltip: mw.msg( 'cx-contributions-translation-tooltip' )\n\t\t\t}\n\t\t];\n\t};\n\n\t$( function () {\n\t\tvar contributionsItemsContainer = document.createElement( 'header' );\n\t\tcontributionsItemsContainer.classList.add( 'cx-contributions-header' );\n\n\t\t// eslint-disable-next-line no-new\n\t\tnew CXContributions( contributionsItemsContainer );\n\t\tif ( $( 'header.mw-body-header' ).length ) {\n\t\t\t// Vector 2022\n\t\t\t$( 'header.mw-body-header' ).before( contributionsItemsContainer );\n\t\t} else if ( $( '#firstHeading' ).length ) {\n\t\t\t// Legacy Vector and other skins\n\t\t\t$( '#firstHeading' ).before( contributionsItemsContainer );\n\t\t}\n\t} );\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.contributionsmenu.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * Content Translation invitation from the 'contributions' link in pages.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\tvar CAMPAIGN = 'contributionsmenu',\n\t\tisMenuAttached = false;\n\n\t/**\n\t * @return {boolean}\n\t */\n\tfunction isUserMenuDropdown() {\n\t\treturn [ 'vector', 'vector-2022' ].indexOf( mw.config.get( 'skin', '' ) ) > -1 &&\n\t\t\t$( '.vector-menu-dropdown .mw-portlet-personal' ).length;\n\t}\n\n\t/**\n\t * @return {jQuery.Object}\n\t */\n\tfunction getTranslationsItem() {\n\t\tvar $link, cxUrlParams = {\n\t\t\tcampaign: CAMPAIGN,\n\t\t\tto: mw.config.get( 'wgContentLanguage' )\n\t\t};\n\n\t\t$link = $( '<a>' )\n\t\t\t.prop( 'href', mw.util.getUrl( 'Special:ContentTranslation', cxUrlParams ) )\n\t\t\t.attr( 'title', mw.msg( 'cx-campaign-contributionsmenu-mytranslations-tooltip' ) )\n\t\t\t.append(\n\t\t\t\t$( '<span>' ).text( mw.msg( 'cx-campaign-contributionsmenu-mytranslations' ) )\n\t\t\t);\n\n\t\treturn $( '<li>' )\n\t\t\t.addClass( 'cx-campaign-translations' )\n\t\t\t.append( $link );\n\t}\n\n\t/**\n\t * @param {jQuery.Object} $trigger with 'callout' data value.\n\t * @param {jQuery.Object} $myContributions list item element.\n\t * @param {jQuery.Object} $myTranslations list item element.\n\t * @param {jQuery.Object} $myUploads list item element.\n\t */\n\tfunction attachCallout( $trigger, $myContributions, $myTranslations, $myUploads ) {\n\t\tvar callout,\n\t\t\t$menu = $( '<ul>' )\n\t\t\t\t.append( $myContributions, $myTranslations, $myUploads );\n\n\t\t// Technical debt:\n\t\t// Add icons to call out.\n\t\t// This is not officially supported by legacy Vector and may break in future.\n\t\t// https://phabricator.wikimedia.org/T294656\n\t\t$myContributions.find( 'a' )\n\t\t\t.addClass( 'mw-ui-icon mw-ui-icon-before mw-ui-icon mw-ui-icon-vector-gadget-cx-userContributions' );\n\t\t$myTranslations.find( 'a' )\n\t\t\t.addClass( 'mw-ui-icon mw-ui-icon-before mw-ui-icon-vector-gadget-cx-language' );\n\t\t$myUploads.find( 'a' )\n\t\t\t.addClass( 'mw-ui-icon mw-ui-icon-before mw-ui-icon mw-ui-icon-vector-gadget-cx-imageGallery' );\n\t\t$trigger.callout( {\n\t\t\ttrigger: 'hover',\n\t\t\tclasses: 'cx-campaign-contributionsmenu',\n\t\t\tdirection: $.fn.callout.autoDirection( '1' ),\n\t\t\tcontent: $menu\n\t\t} );\n\n\t\tcallout = $trigger.data( 'callout' );\n\n\t\tmw.hook( 'mw.cx.betafeature.enabled' ).add( function () {\n\t\t\t// Show after a few milliseconds to get all position calculation correct\n\t\t\tsetTimeout( function () {\n\t\t\t\tcallout.show();\n\t\t\t}, 500 );\n\t\t\tmw.hook( 'mw.cx.cta.shown' ).fire( CAMPAIGN );\n\t\t} );\n\n\t}\n\n\tfunction attachMenu( $trigger ) {\n\t\tvar $myContributions, $myTranslations, $myUploads,\n\t\t\tnextNode = document.getElementById( 'pt-mycontris' ),\n\t\t\tuseCallout = !isUserMenuDropdown();\n\n\t\tnextNode = nextNode ? nextNode.nextSibling : null;\n\t\t// Make sure we attach this menu only once\n\t\tif ( isMenuAttached ) {\n\t\t\treturn;\n\t\t}\n\t\tisMenuAttached = true;\n\n\t\t$myContributions = $( '<li>' )\n\t\t\t.addClass( 'cx-campaign-contributions' )\n\t\t\t.append(\n\t\t\t\t$( '<a>' )\n\t\t\t\t\t.attr( 'href', $trigger.find( 'a' ).attr( 'href' ) )\n\t\t\t\t\t.append(\n\t\t\t\t\t\t$( '<span>' ).text( mw.msg( 'cx-campaign-contributionsmenu-mycontributions' ) )\n\t\t\t\t\t)\n\t\t\t);\n\n\t\t$myTranslations = getTranslationsItem( !useCallout );\n\n\t\tif ( $( '.mw-special-Preferences' ).length ) {\n\t\t\t$myTranslations.addClass( 'cx-campaign-new-beta-feature' );\n\t\t}\n\n\t\t$myUploads = $( '<li>' )\n\t\t\t.addClass( 'cx-campaign-uploads' )\n\t\t\t.append( $( '<a>' )\n\t\t\t\t.attr( 'href', '//commons.wikimedia.org/wiki/Special:MyUploads' )\n\t\t\t\t.attr( 'title', mw.msg( 'cx-campaign-contributionsmenu-myuploads-tooltip' ) )\n\t\t\t\t.append(\n\t\t\t\t\t$( '<span>' ).text( mw.msg( 'cx-campaign-contributionsmenu-myuploads' ) )\n\t\t\t\t)\n\t\t\t);\n\n\t\tif ( useCallout ) {\n\n\t\t\tattachCallout( $trigger, $myContributions, $myTranslations, $myUploads );\n\t\t} else {\n\t\t\tmw.util.addPortletLink(\n\t\t\t\t'p-personal',\n\t\t\t\t$myTranslations.find( 'a' ).attr( 'href' ),\n\t\t\t\t$myTranslations.text(),\n\t\t\t\t'cx-language',\n\t\t\t\t$myTranslations.find( 'a' ).attr( 'title' ),\n\t\t\t\tnull,\n\t\t\t\tnextNode\n\t\t\t);\n\t\t\tmw.util.addPortletLink(\n\t\t\t\t'p-personal',\n\t\t\t\t$myUploads.find( 'a' ).attr( 'href' ),\n\t\t\t\t$myUploads.text(),\n\t\t\t\t'cx-imageGallery',\n\t\t\t\t$myUploads.find( 'a' ).attr( 'title' ),\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tnextNode\n\t\t\t);\n\t\t\t// Also add them to the sticky header\n\t\t\t// Icons are missing. See T320448\n\t\t\t/*\n\t\t\tmw.util.addPortletLink(\n\t\t\t\t'p-personal-sticky-header',\n\t\t\t\t$myTranslations.find( 'a' ).attr( 'href' ),\n\t\t\t\t$myTranslations.text(),\n\t\t\t\t'cx-language-sticky',\n\t\t\t\t$myTranslations.find( 'a' ).attr( 'title' ),\n\t\t\t\tnull,\n\t\t\t\tnextNode\n\t\t\t);\n\t\t\tmw.util.addPortletLink(\n\t\t\t\t'p-personal-sticky-header',\n\t\t\t\t$myUploads.find( 'a' ).attr( 'href' ),\n\t\t\t\t$myUploads.text(),\n\t\t\t\t'cx-imageGallery-sticky',\n\t\t\t\t$myUploads.find( 'a' ).attr( 'title' ),\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tnextNode\n\t\t\t);\n\t\t\t*/\n\t\t}\n\t}\n\n\tfunction showFeatureDiscovery( $trigger ) {\n\t\tvar fd, $container = $( '<div>' ).addClass( 'cx-feature-discovery-container' );\n\n\t\t$trigger.append( $container );\n\t\tfd = new mw.cx.ui.FeatureDiscoveryWidget( {\n\t\t\ttitle: mw.msg( 'cx-feature-discovery-title' ),\n\t\t\tcontent: mw.msg( 'cx-feature-discovery-content' ),\n\t\t\tdismissLabel: mw.msg( 'cx-feature-discovery-dismiss' ),\n\t\t\t$container: $container,\n\t\t\tonClose: function () {\n\t\t\t\t// After dismissing the informative dialog, the action should be continued\n\t\t\t\t// and Contributions page opened\n\t\t\t\tlocation.href = $trigger.find( 'a' ).attr( 'href' );\n\t\t\t}\n\t\t} );\n\t\t$container.append( fd.$element );\n\t\t$trigger.one( 'click', function () {\n\t\t\tvar api = new mw.Api();\n\t\t\t// Prevent default click action.\n\t\t\tfd.show();\n\t\t\t// Never show this again.\n\t\t\tapi.postWithToken( 'csrf', {\n\t\t\t\taction: 'globalpreferences',\n\t\t\t\toptionname: 'cx-entrypoint-fd-status',\n\t\t\t\toptionvalue: 'shown'\n\t\t\t} ).then( function ( res ) {\n\t\t\t\tif ( res.error ) {\n\t\t\t\t\tmw.log.error( res.error );\n\t\t\t\t}\n\t\t\t} );\n\t\t\treturn false;\n\t\t} );\n\t}\n\n\t$( function () {\n\t\tvar $trigger = $( '#pt-mycontris' );\n\n\t\tif ( mw.config.get( 'wgContentTranslationEntryPointFD' ) ) {\n\t\t\tmw.loader.using( 'mw.cx.ui.FeatureDiscoveryWidget' ).then( function () {\n\t\t\t\tshowFeatureDiscovery( $trigger );\n\t\t\t} );\n\t\t} else {\n\t\t\tattachMenu( $trigger );\n\t\t}\n\n\t\t// Change the menu when creating a new article using VE\n\t\tmw.hook( 've.activationComplete' ).add( function () {\n\t\t\t// Rebuild menu\n\t\t\t$trigger.removeData( 'callout' );\n\t\t\tattachMenu( $trigger );\n\t\t} );\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.languagesearcher.init.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * Content Translation invitation for editors while searching in mobile frontend language selector.\n * This is initialization module.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\tmw.hook( 'mobileFrontend.languageSearcher.noresults' ).add(\n\t\tfunction () { mw.loader.load( 'ext.cx.entrypoints.languagesearcher' ); }\n\t);\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.languagesearcher.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * Content Translation invitation for editors while searching in mobile frontend language selector\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\tvar CAMPAIGN = 'mflanguagesearcher';\n\n\t/**\n\t * Search the query string for a valid language\n\t *\n\t * @param {string} query\n\t * @return {Promise<string[]>}\n\t */\n\tfunction searchWithAPI( query ) {\n\t\treturn new mw.Api().get( {\n\t\t\taction: 'languagesearch',\n\t\t\tformat: 'json',\n\t\t\torigin: '*',\n\t\t\tsearch: query,\n\t\t\tformatversion: 2\n\t\t} ).then( function ( result ) {\n\t\t\treturn Object.keys( result.languagesearch || {} );\n\t\t} );\n\t}\n\n\t/**\n\t * Handle the matches found for the search query.\n\t * Replace the default empty state of language searcher with custom one.\n\t *\n\t * @param {string[]} results\n\t * @param {HTMLElement} noResultsContainer\n\t */\n\tfunction onLanguageMatch( results, noResultsContainer ) {\n\t\tvar languageIndex, invitationElement, noResultsMsgElement,\n\t\t\tlanguageButtons = [],\n\t\t\tsitemapper = new mw.cx.SiteMapper(),\n\t\t\tmoreButton,\n\t\t\tactionsElement,\n\t\t\tcxUrl;\n\n\t\tinvitationElement = noResultsContainer.querySelector( '.cx-entrypoint-mflanguagesearcher-invite' );\n\t\tif ( !invitationElement ) {\n\t\t\tinvitationElement = document.createElement( 'p' );\n\t\t\tinvitationElement.classList.add(\n\t\t\t\t'cx-entrypoint-mflanguagesearcher-invite', 'empty-results-body' );\n\t\t\tinvitationElement.textContent = mw.msg( 'cx-campaign-mflanguagesearcher-invite' );\n\t\t\tnoResultsContainer.appendChild( invitationElement );\n\t\t} else {\n\t\t\tinvitationElement.classList.remove( 'hidden' );\n\t\t}\n\n\t\tactionsElement = noResultsContainer.querySelector( '.cx-entrypoint-mflanguagesearcher-actions' );\n\t\tif ( !actionsElement ) {\n\t\t\tactionsElement = document.createElement( 'div' );\n\t\t\tactionsElement.classList.add( 'cx-entrypoint-mflanguagesearcher-actions' );\n\t\t\tnoResultsContainer.appendChild( actionsElement );\n\t\t} else {\n\t\t\tactionsElement.classList.remove( 'hidden' );\n\t\t\t// Empty it\n\t\t\twhile ( actionsElement.firstChild ) {\n\t\t\t\tactionsElement.removeChild( actionsElement.firstChild );\n\t\t\t}\n\t\t}\n\n\t\tfor ( languageIndex = 0; languageIndex < results.length; languageIndex++ ) {\n\t\t\tcxUrl = sitemapper.getCXUrl(\n\t\t\t\tmw.config.get( 'wgPageName' ),\n\t\t\t\tnull,\n\t\t\t\tmw.config.get( 'wgContentLanguage' ),\n\t\t\t\tresults[ languageIndex ],\n\t\t\t\t{ campaign: CAMPAIGN, sx: true }\n\t\t\t);\n\t\t\tlanguageButtons.push( new OO.ui.ButtonWidget( {\n\t\t\t\tlabel: $.uls.data.getAutonym( results[ languageIndex ] ),\n\t\t\t\ticon: 'add',\n\t\t\t\thref: cxUrl,\n\t\t\t\tframed: false,\n\t\t\t\tclasses: [ 'cx-entrypoint-mflanguagesearcher-ctabtn' ]\n\t\t\t} )\n\t\t\t);\n\t\t\tif ( languageIndex >= 1 ) {\n\t\t\t\t// We are showing maximum two results\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tnoResultsMsgElement = noResultsContainer.querySelector( '.empty-results-body' );\n\t\tnoResultsMsgElement.classList.add( 'hidden' );\n\n\t\tfor ( languageIndex = 0; languageIndex < languageButtons.length; languageIndex++ ) {\n\t\t\tactionsElement.appendChild( languageButtons[ languageIndex ].$element[ 0 ] );\n\t\t}\n\n\t\tcxUrl = mw.util.getUrl( 'Special:ContentTranslation', {\n\t\t\tcampaign: CAMPAIGN,\n\t\t\tfrom: mw.config.get( 'wgContentLanguage' ),\n\t\t\tpage: mw.config.get( 'wgPageName' ),\n\t\t\tsx: true\n\t\t} );\n\t\tmoreButton = new OO.ui.ButtonWidget( {\n\t\t\ticon: 'ellipsis',\n\t\t\thref: cxUrl,\n\t\t\tframed: false,\n\t\t\tclasses: [ 'cx-entrypoint-mflanguagesearcher-ctabtn-more' ]\n\t\t} );\n\t\tactionsElement.appendChild( moreButton.$element[ 0 ] );\n\t}\n\n\t/**\n\t * If no matches found for the search query, restore\n\t * the rendering to original state\n\t *\n\t * @param {HTMLElement} noResultsContainer\n\t */\n\tfunction onNoLanguageMatch( noResultsContainer ) {\n\t\tvar noResultsMsgElement, invitationElement, actionsElement;\n\n\t\tinvitationElement = noResultsContainer.querySelector( '.cx-entrypoint-mflanguagesearcher-invite' );\n\t\tif ( invitationElement ) {\n\t\t\tinvitationElement.classList.add( 'hidden' );\n\t\t}\n\t\tnoResultsMsgElement = noResultsContainer.querySelector( '.empty-results-body' );\n\t\tnoResultsMsgElement.classList.remove( 'hidden' );\n\n\t\tactionsElement = noResultsContainer.querySelector( '.cx-entrypoint-mflanguagesearcher-actions' );\n\t\tif ( actionsElement ) {\n\t\t\tactionsElement.classList.add( 'hidden' );\n\t\t}\n\t}\n\n\t/**\n\t * @param {string} searchQuery\n\t * @param {HTMLElement} noResultsContainer\n\t */\n\tfunction showTranslationCTA( searchQuery, noResultsContainer ) {\n\t\tsearchWithAPI( searchQuery ).then( function ( results ) {\n\t\t\tvar matches, enabledTargets = mw.config.get( 'wgSectionTranslationTargetLanguages' );\n\n\t\t\tmatches = results.filter( function ( code ) {\n\t\t\t\treturn enabledTargets.indexOf( code ) >= 0 &&\n\t\t\t\t\tcode !== mw.config.get( 'wgContentLanguage' );\n\t\t\t} );\n\t\t\tif ( !matches.length ) {\n\t\t\t\tonNoLanguageMatch( noResultsContainer );\n\t\t\t} else {\n\t\t\t\tonLanguageMatch( matches, noResultsContainer );\n\t\t\t}\n\t\t} );\n\t}\n\n\tmw.hook( 'mobileFrontend.languageSearcher.noresults' ).add( showTranslationCTA );\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.mffrequentlanguages.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'LanguageSearcher' is undefined.","line":148,"column":null,"nodeType":"Block","endLine":148,"endColumn":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"( function () {\n\t/**\n\t * @param {{ lang: string, autonym: string, dir: string }[]} sxMissingFrequentLanguages array of missing frequent languages\n\t * @return {HTMLDivElement}\n\t */\n\tfunction createPanelTextElement( sxMissingFrequentLanguages ) {\n\t\tvar missingLanguagesPanelTextElement = document.createElement( 'div' );\n\t\tmissingLanguagesPanelTextElement.className = 'cx-entrypoint-missing-frequent-languages__text';\n\n\t\tvar missingFrequentLanguagesCount = sxMissingFrequentLanguages.length;\n\t\t// This method is only called when frequent, missing and SX-enabled languages DO exist,\n\t\t// so the length is guaranteed to be greater than 0\n\t\tif ( missingFrequentLanguagesCount === 1 ) {\n\t\t\tmissingLanguagesPanelTextElement.innerHTML = mw.message(\n\t\t\t\t'sx-missing-languages-entrypoint-panel-text-one-missing',\n\t\t\t\tsxMissingFrequentLanguages[ 0 ].autonym\n\t\t\t).parse();\n\t\t} else if ( missingFrequentLanguagesCount === 2 ) {\n\t\t\tmissingLanguagesPanelTextElement.innerHTML = mw.message(\n\t\t\t\t'sx-missing-languages-entrypoint-panel-text-two-missing',\n\t\t\t\tsxMissingFrequentLanguages[ 0 ].autonym,\n\t\t\t\tsxMissingFrequentLanguages[ 1 ].autonym\n\t\t\t).parse();\n\t\t} else {\n\t\t\t// more than two frequent languages that are enabled for\n\t\t\t// Section Translation are missing\n\t\t\tmissingLanguagesPanelTextElement.innerHTML = mw.message(\n\t\t\t\t'sx-missing-languages-entrypoint-panel-text-more-missing',\n\t\t\t\tsxMissingFrequentLanguages[ 0 ].autonym,\n\t\t\t\tsxMissingFrequentLanguages[ 1 ].autonym\n\t\t\t).parse();\n\t\t}\n\n\t\tvar languageSpans = missingLanguagesPanelTextElement.getElementsByTagName( 'span' );\n\t\tfor ( var i = 0; i < languageSpans.length; i++ ) {\n\t\t\tlanguageSpans[ i ].setAttribute( 'lang', sxMissingFrequentLanguages[ i ].lang );\n\t\t\tlanguageSpans[ i ].setAttribute( 'dir', sxMissingFrequentLanguages[ i ].dir );\n\t\t}\n\n\t\treturn missingLanguagesPanelTextElement;\n\t}\n\n\tfunction createArrowIcon() {\n\t\tvar span = document.createElement( 'span' );\n\t\tspan.className = 'cx-entrypoint-missing-frequent-languages__icon mw-ui-icon';\n\t\treturn span;\n\t}\n\n\t/**\n\t * This method creates and returns an H3 header element that contains the entrypoint banner.\n\t * The returned element should strictly be an H3 element, so that this banner element is hidden,\n\t * along with the other H3 header elements inside mobile Language Searcher, when a search query\n\t * exists inside the mobile Language Searcher search input. This is required so that we avoid to\n\t * display multiple SX entrypoints at the same time (https://phabricator.wikimedia.org/T298032#7844926).\n\t *\n\t * @param {{ lang: string, autonym: string, dir: string }[]} sxMissingFrequentLanguages array of missing frequent languages\n\t * @return {HTMLHeadingElement}\n\t */\n\tfunction createMissingLanguagesPanel( sxMissingFrequentLanguages ) {\n\t\t// Wrap the banner inside an h3 element, so that it is hidden along with other h3 elements inside mobile\n\t\t// Language Searcher, when a search query exists inside the Language Searcher search input.\n\t\tvar missingLanguagesPanelContainer = document.createElement( 'h3' );\n\t\tmissingLanguagesPanelContainer.className = 'cx-entrypoint-missing-frequent-languages-container';\n\n\t\tvar missingLanguagesPanel = document.createElement( 'a' );\n\t\tmissingLanguagesPanel.className = 'cx-entrypoint-missing-frequent-languages';\n\n\t\tvar missingLanguagesPanelText = createPanelTextElement( sxMissingFrequentLanguages );\n\t\tvar missingLanguagesPanelIcon = createArrowIcon();\n\t\tmissingLanguagesPanel.appendChild( missingLanguagesPanelText );\n\t\tmissingLanguagesPanel.appendChild( missingLanguagesPanelIcon );\n\n\t\tvar siteMapper = new mw.cx.SiteMapper();\n\t\tmissingLanguagesPanel.href = siteMapper.getCXUrl(\n\t\t\tmw.config.get( 'wgPageName' ),\n\t\t\tnull,\n\t\t\tsiteMapper.getCurrentWikiLanguageCode(),\n\t\t\tsxMissingFrequentLanguages[ 0 ].lang,\n\t\t\t{ campaign: 'mffrequentlanguages', sx: true }\n\t\t);\n\n\t\tmissingLanguagesPanelContainer.appendChild( missingLanguagesPanel );\n\t\treturn missingLanguagesPanelContainer;\n\t}\n\n\t/**\n\t * @param {Object} frequentLanguages object containing the frequently used languages (codes)\n\t * mapped to their respective frequency\n\t * @param {string|undefined} deviceLanguage the device language (can be a variant too, e.g. en-gb)\n\t * @return {{autonym: string, lang: string, dir: string}[]} array of objects representing languages, ordered by their frequency\n\t */\n\tfunction getMissingFrequentLanguages( frequentLanguages, deviceLanguage ) {\n\t\tvar targetedLanguages,\n\t\t\tdeviceParentLanguage,\n\t\t\tindex;\n\n\t\t/** @type {{lang: string, frequency: number}[]} */\n\t\ttargetedLanguages = Object.keys( frequentLanguages ).map( function ( languageCode ) {\n\t\t\treturn { lang: languageCode, frequency: frequentLanguages[ languageCode ] };\n\t\t} ).sort( function ( a, b ) {\n\t\t\treturn b.frequency - a.frequency;\n\t\t} );\n\n\t\t// add device language/variant and parent device language (if exist) on top of this list\n\t\tif ( deviceLanguage ) {\n\t\t\tindex = deviceLanguage.indexOf( '-' );\n\t\t\tif ( index !== -1 ) {\n\t\t\t\tdeviceParentLanguage = deviceLanguage.slice( 0, index );\n\t\t\t}\n\n\t\t\ttargetedLanguages = targetedLanguages.filter( function ( language ) {\n\t\t\t\treturn language.lang !== deviceLanguage && language.lang !== deviceParentLanguage;\n\t\t\t} );\n\t\t\tif ( deviceParentLanguage ) {\n\t\t\t\ttargetedLanguages.unshift( { lang: deviceParentLanguage } );\n\t\t\t}\n\t\t\ttargetedLanguages.unshift( { lang: deviceLanguage } );\n\t\t}\n\t\t// Remove current wiki language from targetedLanguages\n\t\ttargetedLanguages = targetedLanguages.filter( function ( language ) {\n\t\t\treturn language.lang !== mw.config.get( 'wgContentLanguage' );\n\t\t} );\n\t\t/**\n\t\t * @type {{lang: string, autonym: string, dir: string}[]} missingSXLanguages array containing the\n\t\t * enabled language codes for SX that are missing for the specific article\n\t\t */\n\t\tvar missingSXLanguages = mw.config.get( 'wgSectionTranslationMissingLanguages', [] );\n\t\treturn missingSXLanguages.filter( function ( missingSXLanguage ) {\n\t\t\treturn targetedLanguages.some( function ( targetLanguage ) {\n\t\t\t\treturn missingSXLanguage.lang === targetLanguage.lang;\n\t\t\t} );\n\t\t} );\n\t}\n\n\t/**\n\t * Copied from MobileFrontend/src/mobile.languages.structured/util.js\n\t *\n\t * @return {Object} object containing the frequently used languages (codes)\n\t * mapped to their respective frequency (e.g. { en: 3, el: 5 })\n\t */\n\tfunction getFrequentlyUsedLanguages() {\n\t\tvar languageMap = mw.storage.get( 'langMap' );\n\n\t\treturn languageMap ? JSON.parse( languageMap ) : {};\n\t}\n\n\tmw.hook( 'mobileFrontend.languageSearcher.onOpen' ).add(\n\t\t/** @param {LanguageSearcher} languageSearcher */\n\t\tfunction ( languageSearcher ) {\n\t\t\tvar frequentLanguages = getFrequentlyUsedLanguages(),\n\t\t\t\tdeviceLanguage = languageSearcher.options.deviceLanguage;\n\n\t\t\tvar sxMissingFrequentLanguages = getMissingFrequentLanguages( frequentLanguages, deviceLanguage );\n\t\t\tif ( !sxMissingFrequentLanguages.length ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar missingLanguagesPanel = createMissingLanguagesPanel( sxMissingFrequentLanguages );\n\t\t\tlanguageSearcher.addBanner( missingLanguagesPanel.outerHTML );\n\t\t}\n\t);\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.newarticle.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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\n\tvar campaign = 'newarticle';\n\n\tfunction showInvitation() {\n\t\tvar $banner, $trigger, cxLink, $cancel, $tryCX;\n\n\t\t$trigger = $( '#pt-betafeatures' );\n\t\tcxLink = mw.util.getUrl( 'Special:ContentTranslation', {\n\t\t\tcampaign: campaign,\n\t\t\ttargettitle: mw.config.get( 'wgTitle' ),\n\t\t\tto: mw.config.get( 'wgContentLanguage' )\n\t\t} );\n\n\t\t$cancel = $( '<button>' )\n\t\t\t.addClass( 'mw-ui-button mw-ui-quiet cancel' ).text( mw.msg( 'cx-campaign-no-thanks' ) );\n\t\t$tryCX = $( '<button>' )\n\t\t\t.addClass( 'mw-ui-button mw-ui-progressive try' ).text( mw.msg( 'cx-campaign-try' ) );\n\t\t$banner = $( '<div>' )\n\t\t\t.addClass( 'cx-campaign-newarticle' )\n\t\t\t.append(\n\t\t\t\t$( '<div>' ).addClass( 'cx-campaign-newarticle__logo' ),\n\t\t\t\t$( '<div>' ).addClass( 'cx-campaign-newarticle__message' ).append(\n\t\t\t\t\tmw.message( 'cx-campaign-newarticle-notice' ).parseDom()\n\t\t\t\t),\n\t\t\t\t$( '<div>' ).addClass( 'cx-campaign-newarticle__actions' ).append( $cancel, $tryCX )\n\t\t\t);\n\n\t\t$trigger.callout( {\n\t\t\ttrigger: 'auto',\n\t\t\tdirection: $.fn.callout.autoDirection( '1' ),\n\t\t\tcontent: $banner\n\t\t} );\n\n\t\t$cancel.on( 'click', function () {\n\t\t\t$trigger.callout( 'hide' );\n\t\t\t$.cookie(\n\t\t\t\t'cx_campaign_' + campaign + '_hide', 1, {\n\t\t\t\t\texpires: 300,\n\t\t\t\t\tpath: '/'\n\t\t\t\t}\n\t\t\t);\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\t$tryCX.on( 'click', function () {\n\t\t\tlocation.href = cxLink;\n\t\t\t// We need to log this using eventlogging, but since we are navigating away\n\t\t\t// we cannot do it reliably here(See https://phabricator.wikimedia.org/T44815).\n\t\t\t// We will do it at the resulting page\n\t\t} );\n\t\tmw.hook( 'mw.cx.cta.shown' ).fire( campaign );\n\t}\n\n\tif ( !( new URL( location.href ).searchParams.get( 'cxhidebetapopup' ) ) ) {\n\t\t$( showInvitation );\n\t}\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.newarticle.veloader.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * Content Translation: Loader for the new article campaign for VisualEditor.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\t$( function () {\n\t\tmw.hook( 've.activationComplete' ).add( function () {\n\t\t\tmw.loader.load( 'ext.cx.entrypoints.newarticle' );\n\t\t} );\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.newbytranslation.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"compat/compat","severity":1,"message":"navigator.languages() is not supported in Safari 9.1, iOS Safari 9.0-9.2, IE 11","line":177,"column":35,"nodeType":"MemberExpression","endLine":177,"endColumn":54}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":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( 'cx-suggestion-image oo-ui-icon-article' );\n\t\t\t$suggestionDetails = $( '<div>' ).addClass( 'cx-suggestion-details' );\n\n\t\t\t$suggestionDetails.append(\n\t\t\t\t$( '<div>' ).addClass( 'cx-suggestion-title' ).text( this.suggestion.title ),\n\t\t\t\t$( '<div>' ).addClass( 'cx-suggestion-desc' ).text( this.suggestion.description ),\n\t\t\t\t$( '<div>' ).addClass( 'cx-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( 'cx-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.entrypoints.newbytranslation.mobile.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"compat/compat","severity":1,"message":"navigator.languages() is not supported in Safari 9.1, iOS Safari 9.0-9.2, IE 11","line":10,"column":35,"nodeType":"MemberExpression","endLine":10,"endColumn":54},{"ruleId":"compat/compat","severity":1,"message":"fetch is not supported in Safari 9.1, IE 11","line":38,"column":10,"nodeType":"CallExpression","endLine":38,"endColumn":38}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"( function () {\n\t'use strict';\n\tvar siteMapper = new mw.cx.SiteMapper();\n\tvar targetTitle = mw.config.get( 'wgPageName' );\n\tvar targetLanguage = siteMapper.getCurrentWikiLanguageCode();\n\n\t// Copied from ext.cx.entrypoints.newbytranslation.js\n\tfunction getCandidateSourceLanguages() {\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.map( function ( lang ) {\n\t\t\tif ( lang ) {\n\t\t\t\t// Remove country codes\n\t\t\t\treturn lang.split( '-' )[ 0 ];\n\t\t\t}\n\t\t\treturn null;\n\t\t} ).filter( function ( lang, index, self ) {\n\t\t\t// Remove target language and duplicates\n\t\t\treturn lang && lang !== targetLanguage && self.indexOf( lang ) === index;\n\t\t} );\n\t\treturn candidates.splice( 0, 5 );\n\t}\n\n\t// Copied from ext.cx.entrypoints.newbytranslation.js\n\tfunction getSourceSuggestions() {\n\t\tvar candidateSourceLanguages = getCandidateSourceLanguages();\n\t\tvar sourceSuggestionApi = 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 fetch( sourceSuggestionApi )\n\t\t\t.then( function ( response ) {\n\t\t\t\treturn response.json();\n\t\t\t} )\n\t\t\t.then( function ( response ) {\n\t\t\t\treturn response.suggestions || [];\n\t\t\t} );\n\t}\n\n\tfunction removeInvite( invitePanel ) {\n\t\tif ( document.body.contains( invitePanel ) ) {\n\t\t\tdocument.body.removeChild( invitePanel );\n\t\t}\n\t}\n\n\t/**\n\t * @param {{description: string|undefined, language: string, thumbnail: object|undefined, title: string }} suggestion\n\t * @return {HTMLAnchorElement}\n\t */\n\tfunction createNewByTranslationPanel( suggestion ) {\n\t\tvar invitePanel = document.createElement( 'a' );\n\t\tinvitePanel.classList.add( 'sx-new-by-translation-entrypoint' );\n\n\t\tinvitePanel.href = siteMapper.getCXUrl(\n\t\t\tsuggestion.title,\n\t\t\ttargetTitle,\n\t\t\tsuggestion.language,\n\t\t\ttargetLanguage,\n\t\t\t{ sx: true, campaign: 'newbytranslationmobile' }\n\t\t);\n\n\t\tvar thumbnailContainer = document.createElement( 'div' );\n\t\tthumbnailContainer.classList.add( 'sx-suggestion__thumbnail-container' );\n\t\tvar thumbnail = document.createElement( 'div' );\n\t\tthumbnail.classList.add( 'sx-suggestion__thumbnail' );\n\n\t\t// if thumbnail exists for the current suggestion, display it\n\t\tif ( suggestion.thumbnail ) {\n\t\t\tthumbnail.style.backgroundImage = \"url('\" + suggestion.thumbnail.source + \"')\";\n\t\t} else {\n\t\t\t// if thumbnail doesn't exist, display the article icon as thumbnail placeholder\n\t\t\tvar thumbnailPlaceholderIcon = document.createElement( 'span' );\n\t\t\tthumbnailPlaceholderIcon.className = 'sx-suggestion__thumbnail-placeholder mw-ui-icon mw-ui-icon-article';\n\t\t\tthumbnail.appendChild( thumbnailPlaceholderIcon );\n\t\t}\n\t\tthumbnailContainer.appendChild( thumbnail );\n\t\tinvitePanel.appendChild( thumbnailContainer );\n\n\t\tvar bodyContainer = document.createElement( 'div' );\n\t\tbodyContainer.classList.add( 'sx-suggestion__body-container' );\n\n\t\tvar header = document.createElement( 'h6' );\n\t\theader.classList.add( 'sx-suggestion__body-container__header' );\n\t\theader.innerText = 'Start with a translation';\n\t\tbodyContainer.appendChild( header );\n\n\t\tvar title = document.createElement( 'h5' );\n\t\ttitle.classList.add( 'sx-suggestion__body-container__title' );\n\t\ttitle.innerText = suggestion.title;\n\t\tbodyContainer.appendChild( title );\n\n\t\tif ( suggestion.description ) {\n\t\t\tvar description = document.createElement( 'span' );\n\t\t\tdescription.classList.add( 'sx-suggestion__body-container__description' );\n\t\t\tdescription.innerText = suggestion.description;\n\t\t\tbodyContainer.appendChild( description );\n\t\t}\n\n\t\tinvitePanel.appendChild( bodyContainer );\n\n\t\tvar actionContainer = document.createElement( 'div' );\n\t\tactionContainer.classList.add( 'sx-suggestion__action-container' );\n\n\t\tvar closeIcon = document.createElement( 'span' );\n\t\tcloseIcon.className = 'sx-suggestion__action-container__close-icon';\n\t\tcloseIcon.addEventListener( 'click', function ( event ) {\n\t\t\tremoveInvite( invitePanel );\n\t\t\tevent.stopPropagation();\n\t\t} );\n\n\t\tactionContainer.appendChild( closeIcon );\n\t\tinvitePanel.appendChild( actionContainer );\n\n\t\treturn invitePanel;\n\t}\n\n\t// Here we need to detect the editor open event in MobileFrontend.\n\t// VE provides \"ve.activationComplete\" hook but that is only for VisualEdit mode.\n\t// To cover both wikitext and VisualEdit mode, we are using \"mobileFrontend.editorOpened\" hook\n\tmw.hook( 'mobileFrontend.editorOpened' ).add( function () {\n\t\tvar sxInvite;\n\t\tgetSourceSuggestions().then( function ( suggestions ) {\n\t\t\tif ( !suggestions.length ) {\n\t\t\t\t// No suggestions. Nothing to do.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar suggestion = suggestions[ 0 ];\n\t\t\tsxInvite = createNewByTranslationPanel( suggestion );\n\t\t\tdocument.body.appendChild( sxInvite );\n\n\t\t\tmw.hook( 'mobileFrontend.editorClosed' ).add( function () {\n\t\t\t\tremoveInvite( sxInvite );\n\t\t\t} );\n\n\t\t\tvar wikitextEditor = document.getElementById( 'wikitext-editor' );\n\t\t\twikitextEditor.addEventListener( 'input', function () {\n\t\t\t\tremoveInvite( sxInvite );\n\t\t\t} );\n\t\t} );\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.recentedit/RecentEditEntrypointInvitation.vue","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":8,"column":7,"nodeType":"VAttribute","endLine":8,"endColumn":70},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":32,"column":6,"nodeType":"VAttribute","endLine":32,"endColumn":116},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":61,"column":7,"nodeType":"VAttribute","endLine":61,"endColumn":103},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":66,"column":6,"nodeType":"VAttribute","endLine":72,"endColumn":7},{"ruleId":"es-x/no-array-prototype-keys","severity":2,"message":"ES2015 'Array.prototype.keys' method is forbidden.","line":116,"column":29,"nodeType":"MemberExpression","messageId":"forbidden","endLine":116,"endColumn":40},{"ruleId":"es-x/no-array-prototype-keys","severity":2,"message":"ES2015 'Array.prototype.keys' method is forbidden.","line":129,"column":11,"nodeType":"MemberExpression","messageId":"forbidden","endLine":129,"endColumn":22}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":0,"source":"<template>\n\t<section v-if=\"showPanel\" class=\"sx-recent-edit-entrypoint\">\n\t\t<div class=\"sx-recent-edit-entrypoint__top-banner row pa-4\">\n\t\t\t<div class=\"col\">\n\t\t\t\t<div class=\"row sx-recent-edit-entrypoint__top-banner__header\">\n\t\t\t\t\t<h5\n\t\t\t\t\t\tclass=\"col\"\n\t\t\t\t\t\tv-text=\"$i18n( 'sx-recent-edit-entrypoint-top-banner-header' )\"\n\t\t\t\t\t></h5>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"sx-recent-edit-entrypoint__top-banner__close-icon col shrink\"\n\t\t\t\t\t\t@click=\"closeInvite\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\t\t\t:width=\"size\"\n\t\t\t\t\t\t\t\t:height=\"size\"\n\t\t\t\t\t\t\t\tviewBox=\"0 0 20 20\"\n\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<g :fill=\"iconColor\">\n\t\t\t\t\t\t\t\t\t<path :d=\"closeIconPath\" />\n\t\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<p\n\t\t\t\t\tclass=\"sx-recent-edit-entrypoint__top-banner__content\"\n\t\t\t\t\tv-text=\"$i18n( 'sx-recent-edit-entrypoint-top-banner-content', sourceLanguageAutonym, targetLanguageAutonym )\"\n\t\t\t\t></p>\n\t\t\t</div>\n\t\t</div>\n\t\t<hr class=\"sx-recent-edit-entrypoint__separation-line\">\n\t\t<a\n\t\t\tclass=\"sx-recent-edit-entrypoint__invitation row pa-4\"\n\t\t\t:href=\"sxUrl\"\n\t\t>\n\t\t\t<div class=\"sx-recent-edit-entrypoint__invitation__icon col shrink\">\n\t\t\t\t<span>\n\t\t\t\t\t<svg\n\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\t:width=\"size\"\n\t\t\t\t\t\t:height=\"size\"\n\t\t\t\t\t\tviewBox=\"0 0 20 20\"\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<g :fill=\"iconColor\">\n\t\t\t\t\t\t\t<path :d=\"plusIconPath\" />\n\t\t\t\t\t\t</g>\n\t\t\t\t\t</svg>\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t\t<div class=\"col\">\n\t\t\t\t<div class=\"row sx-recent-edit-entrypoint__invitation__header\">\n\t\t\t\t\t<h5\n\t\t\t\t\t\tclass=\"col\"\n\t\t\t\t\t\tv-text=\"$i18n( 'sx-recent-edit-entrypoint-invitation-button-label', firstMissingEditedSection )\"\n\t\t\t\t\t></h5>\n\t\t\t\t</div>\n\t\t\t\t<p\n\t\t\t\t\tclass=\"sx-recent-edit-entrypoint__invitation__details\"\n\t\t\t\t\tv-text=\"\n\t\t\t\t\t\t$i18n(\n\t\t\t\t\t\t\t'sx-recent-edit-entrypoint-invitation-button-details',\n\t\t\t\t\t\t\tmissingSectionLength,\n\t\t\t\t\t\t\ttargetLanguageAutonym\n\t\t\t\t\t\t)\n\t\t\t\t\t\"\n\t\t\t\t></p>\n\t\t\t</div>\n\t\t</a>\n\t</section>\n</template>\n\n<script>\n// @vue/component\nmodule.exports = {\n\tname: 'RecentEditEntrypointInvitation',\n\tprops: {\n\t\t/**\n\t\t * @type {{ language: string, page: string, sections: Array }}\n\t\t */\n\t\trecentEdit: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t\tvalidator: function ( edit ) {\n\t\t\t\treturn edit.language && edit.page && edit.sections;\n\t\t\t}\n\t\t},\n\t\tmissingSections: {\n\t\t\ttype: Object,\n\t\t\trequired: true\n\t\t},\n\t\thiddenInvitationStorageKey: {\n\t\t\ttype: String,\n\t\t\trequired: true\n\t\t}\n\t},\n\tdata: function () {\n\t\treturn {\n\t\t\tsize: 20,\n\t\t\tplusIconPath: 'M11 9V4H9v5H4v2h5v5h2v-5h5V9z',\n\t\t\tcloseIconPath: 'M4.34 2.93l12.73 12.73-1.41 1.41L2.93 4.35z M17.07 4.34L4.34 17.07l-1.41-1.41L15.66 2.93z',\n\t\t\ticonColor: 'currentColor',\n\t\t\tshowPanel: true,\n\t\t\tsiteMapper: new mw.cx.SiteMapper()\n\t\t};\n\t},\n\tcomputed: {\n\t\tfirstMissingEditedSection: function () {\n\t\t\tvar i,\n\t\t\t\tsourceMissingSections = Object.keys( this.missingSections ),\n\t\t\t\tmissingSection,\n\t\t\t\teditedSections = ( this.recentEdit && this.recentEdit.sections ) || [];\n\n\t\t\tfor ( i = 0; i < sourceMissingSections.length; i++ ) {\n\t\t\t\tmissingSection = sourceMissingSections[ i ];\n\t\t\t\tif ( editedSections.indexOf( missingSection ) > -1 ) {\n\t\t\t\t\treturn missingSection;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t\tmissingSectionLength: function () {\n\t\t\treturn Object.keys( this.missingSections ).length;\n\t\t},\n\t\tsourceLanguage: function () {\n\t\t\treturn this.recentEdit && this.recentEdit.language;\n\t\t},\n\t\tsourceTitle: function () {\n\t\t\treturn this.recentEdit && this.recentEdit.page;\n\t\t},\n\t\ttargetLanguage: function () {\n\t\t\treturn this.siteMapper.getCurrentWikiLanguageCode();\n\t\t},\n\t\tsourceLanguageAutonym: function () {\n\t\t\treturn $.uls.data.getAutonym( this.sourceLanguage );\n\t\t},\n\t\ttargetLanguageAutonym: function () {\n\t\t\treturn $.uls.data.getAutonym( this.targetLanguage );\n\t\t},\n\t\tsxUrl: function () {\n\t\t\treturn this.siteMapper.getCXUrl(\n\t\t\t\tthis.sourceTitle,\n\t\t\t\t'',\n\t\t\t\tthis.sourceLanguage,\n\t\t\t\tthis.targetLanguage,\n\t\t\t\t{ sx: true }\n\t\t\t);\n\t\t}\n\t},\n\tmethods: {\n\t\tcloseInvite: function () {\n\t\t\tthis.showPanel = false;\n\t\t\tvar hiddenInvitations = mw.storage.getObject( this.hiddenInvitationStorageKey ) || [];\n\t\t\thiddenInvitations.push( {\n\t\t\t\tlanguage: this.sourceLanguage,\n\t\t\t\tpage: this.sourceTitle,\n\t\t\t\tsection: this.firstMissingEditedSection\n\t\t\t} );\n\t\t\tmw.storage.setObject( this.hiddenInvitationStorageKey, hiddenInvitations );\n\t\t}\n\t}\n};\n</script>\n\n<style lang=\"less\">\n// Copying variables from \"mediawiki.ui/variables.less\"\n@color-base: #202122;\n@colorGray5: #54595d;\n@colorGray14: #eaecf0;\n@color-primary: #36c;\n\n.sx-recent-edit-entrypoint {\n  width: 100%;\n  position: fixed;\n  bottom: 0;\n  background: white;\n  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.25);\n  .row {\n    box-sizing: border-box;\n    display: flex;\n    flex: 0 1 auto;\n    flex-wrap: wrap;\n  }\n  .col {\n    flex-basis: 0;\n    flex-grow: 1;\n    max-width: 100%;\n  }\n  .pa-4 {\n    padding: 16px;\n  }\n  .shrink {\n    flex-grow: 0 !important;\n    flex-shrink: 1 !important;\n  }\n  &__top-banner {\n    &__header {\n      font-weight: 600;\n      color: @color-base;\n    }\n    &__close-icon {\n      cursor: pointer;\n    }\n    &__content {\n      margin-top: 8px;\n      font-size: 14px;\n      color: @color-base;\n    }\n  }\n\n  &__separation-line {\n    margin: 0;\n    border-top: 0;\n    color: @colorGray14;\n  }\n\n  &__invitation {\n    cursor: pointer;\n    &:hover {\n      background: #eaf3ff;\n    }\n    &__icon {\n      margin-right: 8px;\n      color: @color-primary;\n    }\n    &__header {\n      font-weight: 600;\n      color: #36c;\n    }\n    &__details {\n      margin-top: 4px;\n      font-size: 14px;\n      color: @colorGray5;\n    }\n  }\n\n}\n</style>\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.recentedit/index.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"compat/compat","severity":1,"message":"fetch is not supported in Safari 9.1, IE 11","line":36,"column":10,"nodeType":"CallExpression","endLine":36,"endColumn":50}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"( function () {\n\t/**\n\t * This variable contains an array of objects that contain all\n\t * edited sections by the user in languages other than the current one.\n\t *\n\t * @type {{ language: string, page: string, sections: string[]}[]}\n\t */\n\tvar recentEdits = mw.config.get( 'wgSectionTranslationRecentlyEditedSections' );\n\tvar isRecentEditInvitationSuppressed =\n\t\tmw.config.get( 'wgCXSectionTranslationRecentEditInvitationSuppressed', false );\n\tvar hiddenInvitationStorageKey = 'sx_recent_edit_invitation_hidden';\n\t/**\n\t * Given a title and a source language, this method fetches section\n\t * suggestions from cxserver with current wiki language code as target\n\t * language and returns an object containing the section suggestions response\n\t * or null in case of failed request.\n\t *\n\t * @param {string} title\n\t * @param {string} sourceLanguage\n\t * @return {Promise<Object|null>}\n\t */\n\tfunction fetchSectionSuggestions( title, sourceLanguage ) {\n\t\tvar siteMapper = new mw.cx.SiteMapper(),\n\t\t\ttargetLanguage = siteMapper.getCurrentWikiLanguageCode(),\n\t\t\tcxServerParams = [\n\t\t\t\ttitle,\n\t\t\t\tsourceLanguage,\n\t\t\t\ttargetLanguage\n\t\t\t].map( function ( param ) {\n\t\t\t\treturn encodeURIComponent( param );\n\t\t\t} ),\n\t\t\tcxServerSectionSuggestionApiUrl = siteMapper.getCXServerUrl(\n\t\t\t\t'/suggest/sections/' + cxServerParams.join( '/' )\n\t\t\t);\n\n\t\treturn fetch( cxServerSectionSuggestionApiUrl )\n\t\t\t.then( function ( response ) {\n\t\t\t\treturn response.ok ?\n\t\t\t\t\tresponse.json() :\n\t\t\t\t\tnull;\n\t\t\t} )\n\t\t\t.then( function ( suggestionResult ) {\n\t\t\t\tif ( suggestionResult && suggestionResult.sections ) {\n\t\t\t\t\treturn suggestionResult.sections;\n\t\t\t\t}\n\t\t\t\treturn null;\n\t\t\t} );\n\t}\n\n\t/**\n\t * Given an object containing the recent edit and an object containing\n\t * the missing sections, this method mounts an instance of\n\t * RecentEditEntrypointInvitation component to render the entrypoint\n\t * invitation\n\t *\n\t * @param {{ language: string, page: string, sections: string[] }} recentEdit\n\t * @param {Object} missingSections\n\t */\n\tfunction renderInvitation( recentEdit, missingSections ) {\n\t\tvar Vue = require( 'vue' ),\n\t\t\tInvitationComponent = require( './RecentEditEntrypointInvitation.vue' ),\n\t\t\tpanel = document.createElement( 'section' ),\n\t\t\tcontainer = document.getElementById( 'mw-mf-page-center' );\n\t\tpanel.className = 'sx-recentedit-entrypoint-banners';\n\t\tcontainer = container || document.body;\n\t\tcontainer.appendChild( panel );\n\t\tVue.createMwApp( InvitationComponent, {\n\t\t\trecentEdit: recentEdit,\n\t\t\tmissingSections: missingSections,\n\t\t\thiddenInvitationStorageKey: hiddenInvitationStorageKey\n\t\t} ).mount( panel );\n\t}\n\n\t/**\n\t * Given an object representing a recent significant edit,\n\t * this method fetches section suggestions for the related page\n\t * and language pair. If missing sections contain at least one\n\t * of the edited sections in this recent edit, then the entrypoint\n\t * invitation is being displayed to the user.\n\t *\n\t * Finally, this method returns a promise that resolves to a boolean\n\t * indicating whether the invitation has been displayed or not\n\t *\n\t * @param {{ language: string, page: string, sections: string[] }} recentEdit\n\t * @return {Promise<boolean>}\n\t */\n\tfunction handleSectionSuggestionsByEdit( recentEdit ) {\n\t\tvar sourceLanguage = recentEdit.language,\n\t\t\tpageTitle = recentEdit.page,\n\t\t\tsectionTitles = recentEdit.sections;\n\n\t\treturn fetchSectionSuggestions( pageTitle, sourceLanguage )\n\t\t\t.then( function ( sections ) {\n\t\t\t\tvar firstMissingEditedSection, i, sourceMissingSections, missingSection;\n\t\t\t\tif ( !sections || !sections.missing ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tsourceMissingSections = Object.keys( sections.missing );\n\t\t\t\tfor ( i = 0; i < sourceMissingSections.length; i++ ) {\n\t\t\t\t\tmissingSection = sourceMissingSections[ i ];\n\t\t\t\t\tif ( sectionTitles.indexOf( missingSection ) > -1 ) {\n\t\t\t\t\t\tfirstMissingEditedSection = missingSection;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ( !firstMissingEditedSection ) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tvar hiddenInvitations = mw.storage.getObject( hiddenInvitationStorageKey ) || [];\n\t\t\t\tvar isInvitationHidden = function ( hiddenInvitation ) {\n\t\t\t\t\treturn hiddenInvitation.language === sourceLanguage && hiddenInvitation.page === pageTitle && hiddenInvitation.section === firstMissingEditedSection;\n\t\t\t\t};\n\t\t\t\tfor ( i = 0; i < hiddenInvitations.length; i++ ) {\n\t\t\t\t\tif ( isInvitationHidden( hiddenInvitations[ i ] ) ) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\trenderInvitation( recentEdit, sections.missing );\n\t\t\t\treturn true;\n\t\t\t} );\n\t}\n\n\t/**\n\t * Given an index indicating the position of the current\n\t * recent significant edit inside the \"recentEdits\" array\n\t * this method recursively tries to fetch section suggestions\n\t * for all recent edits, until the requirements for the invitation\n\t * has been met (and thus the invitation is rendered) or it\n\t * reaches the end of the array.\n\t *\n\t * @param {number} editIndex\n\t */\n\tfunction handleRecentEditsRecursively( editIndex ) {\n\t\tif ( editIndex >= recentEdits.length ) {\n\t\t\treturn;\n\t\t}\n\n\t\thandleSectionSuggestionsByEdit( recentEdits[ editIndex ] ).then( function ( success ) {\n\t\t\tif ( success ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\thandleRecentEditsRecursively( editIndex + 1 );\n\t\t} );\n\t}\n\n\tif ( !isRecentEditInvitationSuppressed ) {\n\t\t// Start the recursion from the first index\n\t\thandleRecentEditsRecursively( 0 );\n\t}\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.recenttranslation/RecentTranslationEntrypointDialog.vue","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":7,"column":10,"nodeType":"VAttribute","endLine":7,"endColumn":76},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":16,"column":6,"nodeType":"VAttribute","endLine":16,"endColumn":84},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":22,"column":6,"nodeType":"VAttribute","endLine":22,"endColumn":89},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":33,"column":7,"nodeType":"VAttribute","endLine":33,"endColumn":83},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":37,"column":12,"nodeType":"VAttribute","endLine":37,"endColumn":105},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":38,"column":11,"nodeType":"VAttribute","endLine":38,"endColumn":105},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":41,"column":12,"nodeType":"VAttribute","endLine":41,"endColumn":99},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":42,"column":11,"nodeType":"VAttribute","endLine":42,"endColumn":99},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":46,"column":12,"nodeType":"VAttribute","endLine":46,"endColumn":105},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":47,"column":11,"nodeType":"VAttribute","endLine":47,"endColumn":105},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":54,"column":13,"nodeType":"VAttribute","endLine":54,"endColumn":90},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":61,"column":8,"nodeType":"VAttribute","endLine":61,"endColumn":111},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":75,"column":7,"nodeType":"VAttribute","endLine":75,"endColumn":89},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":80,"column":13,"nodeType":"VAttribute","endLine":80,"endColumn":95},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":84,"column":7,"nodeType":"VAttribute","endLine":84,"endColumn":38},{"ruleId":"compat/compat","severity":1,"message":"fetch is not supported in Safari 9.1, IE 11","line":176,"column":4,"nodeType":"CallExpression","endLine":176,"endColumn":44},{"ruleId":"compat/compat","severity":1,"message":"Promise.reject() is not supported in IE 11","line":180,"column":7,"nodeType":"MemberExpression","endLine":180,"endColumn":21},{"ruleId":"es-x/no-promise","severity":2,"message":"ES2015 'Promise' class is forbidden.","line":180,"column":7,"nodeType":"Identifier","messageId":"forbidden","endLine":180,"endColumn":14},{"ruleId":"es-x/no-array-prototype-keys","severity":2,"message":"ES2015 'Array.prototype.keys' method is forbidden.","line":184,"column":30,"nodeType":"MemberExpression","messageId":"forbidden","endLine":184,"endColumn":41}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":17,"fixableErrorCount":0,"fixableWarningCount":0,"source":"<template>\n\t<div v-if=\"showDialog\" class=\"sx-recent-translation-dialog\">\n\t\t<div class=\"sx-recent-translation-dialog__overlay\"></div>\n\t\t<div class=\"sx-recent-translation-dialog__shell\">\n\t\t\t<div class=\"sx-recent-translation-dialog__header row\">\n\t\t\t\t<div class=\"sx-recent-translation-dialog__header-text col\">\n\t\t\t\t\t<h3 v-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-header' )\"></h3>\n\t\t\t\t</div>\n\t\t\t\t<span class=\"sx-recent-translation-dialog__header-close-icon mw-ui-icon\" @click=\"closeDialog\"></span>\n\t\t\t</div>\n\t\t\t<div v-if=\"missingSections.length > 0\" class=\"sx-recent-translation-dialog__action-switches row\">\n\t\t\t\t<button\n\t\t\t\t\tclass=\"sx-recent-translation-dialog__action-switch\"\n\t\t\t\t\t:class=\"{ 'sx-recent-translation-dialog__action-switch--enabled': isReviewSelected }\"\n\t\t\t\t\t@click=\"selectedOption = 'review'\"\n\t\t\t\t\tv-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-review-switch-text' )\"\n\t\t\t\t></button>\n\t\t\t\t<button\n\t\t\t\t\tclass=\"sx-recent-translation-dialog__action-switch\"\n\t\t\t\t\t:class=\"{ 'sx-recent-translation-dialog__action-switch--enabled': isAddSelected }\"\n\t\t\t\t\t@click=\"selectedOption = 'add'\"\n\t\t\t\t\tv-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-add-section-switch-text' )\"\n\t\t\t\t></button>\n\t\t\t</div>\n\t\t\t<div\n\t\t\t\tref=\"content\"\n\t\t\t\tclass=\"sx-recent-translation-dialog__content\"\n\t\t\t\t:style=\"contentStyle\"\n\t\t\t>\n\t\t\t\t<div v-show=\"isReviewSelected\" class=\"sx-recent-translation-dialog__review-tab\">\n\t\t\t\t\t<p\n\t\t\t\t\t\tclass=\"sx-recent-translation-dialog__review-lead-text\"\n\t\t\t\t\t\tv-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-review-lead-text' )\"\n\t\t\t\t\t></p>\n\t\t\t\t\t<ol class=\"sx-recent-translation-dialog__review-list\">\n\t\t\t\t\t\t<li class=\"sx-recent-translation-dialog__review-list-item\">\n\t\t\t\t\t\t\t<h4 v-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-natural-contents-list-item-header' )\"></h4>\n\t\t\t\t\t\t\t<p v-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-natural-contents-list-item-details' )\"></p>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t<li class=\"sx-recent-translation-dialog__review-list-item\">\n\t\t\t\t\t\t\t<h4 v-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-references-list-item-header' )\"></h4>\n\t\t\t\t\t\t\t<p v-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-references-list-item-details' )\">\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t<li class=\"sx-recent-translation-dialog__review-list-item\">\n\t\t\t\t\t\t\t<h4 v-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-compare-original-list-item-header' )\"></h4>\n\t\t\t\t\t\t\t<p v-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-compare-original-list-item-details' )\">\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ol>\n\t\t\t\t\t<button class=\"sx-recent-translation-dialog__edit-button mw-ui-button mw-ui-progressive\" @click=\"openVE\">\n\t\t\t\t\t\t<span class=\"sx-recent-translation-dialog__edit-icon mw-ui-icon\">\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<span v-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-edit-button-label' )\"></span>\n\t\t\t\t\t</button>\n\t\t\t\t\t<div class=\"sx-recent-translation-dialog__original-page-link row\">\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\tclass=\"sx-recent-translation-dialog__original-page-anchor col\"\n\t\t\t\t\t\t\t:href=\"originalPageUrl\"\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\tv-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-original-page-anchor', sourceLanguageAutonym )\"\n\t\t\t\t\t\t></a>\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\tclass=\"sx-recent-translation-dialog__original-page-icon-anchor col shrink\"\n\t\t\t\t\t\t\t:href=\"originalPageUrl\"\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span class=\"sx-recent-translation-dialog__original-page-icon mw-ui-icon\"></span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div v-show=\"isAddSelected\" class=\"sx-recent-translation-dialog__add-sections-tab\">\n\t\t\t\t\t<p\n\t\t\t\t\t\tclass=\"sx-recent-translation-dialog__add-sections-tab__lead-text\"\n\t\t\t\t\t\tv-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-add-sections-lead-text' )\"\n\t\t\t\t\t></p>\n\t\t\t\t\t<a :href=\"sxUrl\" class=\"sx-recent-translation-dialog__translate-button mw-ui-button mw-ui-progressive\">\n\t\t\t\t\t\t<span class=\"sx-recent-translation-dialog__language-icon mw-ui-icon\">\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<span v-text=\"$i18n( 'sx-recent-translation-entrypoint-dialog-translate-button-label' )\"></span>\n\t\t\t\t\t</a>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"sx-recent-translation-dialog__translate-secondary-notice\"\n\t\t\t\t\t\tv-text=\"translateSecondaryText\"\n\t\t\t\t\t></div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nvar siteMapper = new mw.cx.SiteMapper();\n\n// @vue/component\nmodule.exports = {\n\tname: 'RecentTranslationEntrypointDialog',\n\tdata: function () {\n\t\treturn {\n\t\t\tshowDialog: true,\n\t\t\tcontentHeight: 0,\n\t\t\tmissingSections: [],\n\t\t\tselectedOption: 'review',\n\t\t\tsourceLanguage: mw.config.get( 'wgSectionTranslationSourceLanguage' ),\n\t\t\tsourceTitle: mw.config.get( 'wgSectionTranslationSourceTitle' ),\n\t\t\ttargetLanguage: siteMapper.getCurrentWikiLanguageCode()\n\t\t};\n\t},\n\tcomputed: {\n\t\tcontentStyle: function () {\n\t\t\treturn {\n\t\t\t\t'min-height': String( this.contentHeight ) + 'px'\n\t\t\t};\n\t\t},\n\t\tisReviewSelected: function () {\n\t\t\treturn this.selectedOption === 'review';\n\t\t},\n\t\tisAddSelected: function () {\n\t\t\treturn this.selectedOption === 'add';\n\t\t},\n\t\toriginalPageUrl: function () {\n\t\t\treturn siteMapper.getPageUrl(\n\t\t\t\tthis.sourceLanguage,\n\t\t\t\tthis.sourceTitle\n\t\t\t);\n\t\t},\n\t\tsourceLanguageAutonym: function () {\n\t\t\treturn $.uls.data.getAutonym( this.sourceLanguage );\n\t\t},\n\t\ttranslateSecondaryText: function () {\n\t\t\tswitch ( this.missingSections.length ) {\n\t\t\t\tcase 1:\n\t\t\t\t\treturn this.$i18n( 'sx-recent-translation-entrypoint-dialog-translate-notice-text-one-missing', this.missingSections[ 0 ] );\n\t\t\t\tcase 2:\n\t\t\t\t\treturn this.$i18n( 'sx-recent-translation-entrypoint-dialog-translate-notice-text-two-missing', this.missingSections[ 0 ], this.missingSections[ 1 ] );\n\t\t\t\tdefault:\n\t\t\t\t\treturn this.$i18n( 'sx-recent-translation-entrypoint-dialog-translate-notice-text-more-missing', this.missingSections[ 0 ], this.missingSections[ 1 ] );\n\t\t\t}\n\t\t},\n\t\tsxUrl: function () {\n\t\t\treturn siteMapper.getCXUrl(\n\t\t\t\tthis.sourceTitle,\n\t\t\t\tnull,\n\t\t\t\tthis.sourceLanguage,\n\t\t\t\tthis.targetLanguage,\n\t\t\t\t{ campaign: 'mfrecenttranslation', sx: true }\n\t\t\t);\n\t\t}\n\t},\n\tmethods: {\n\t\tcloseDialog: function () {\n\t\t\tthis.showDialog = false;\n\t\t},\n\t\topenVE: function () {\n\t\t\tvar router = mw.loader.require( 'mediawiki.router' );\n\t\t\trouter.navigate( '/editor/all' );\n\t\t\tthis.closeDialog();\n\t\t},\n\t\tgetSectionSuggestions: function () {\n\t\t\tvar cxServerParams, that, cxServerSectionSuggestionApiUrl;\n\n\t\t\tcxServerParams = [\n\t\t\t\tthis.sourceTitle,\n\t\t\t\tthis.sourceLanguage,\n\t\t\t\tthis.targetLanguage\n\t\t\t].map( function ( param ) {\n\t\t\t\treturn encodeURIComponent( param );\n\t\t\t} );\n\n\t\t\tcxServerSectionSuggestionApiUrl = siteMapper.getCXServerUrl(\n\t\t\t\t'/suggest/sections/' + cxServerParams.join( '/' )\n\t\t\t);\n\n\t\t\tthat = this;\n\n\t\t\tfetch( cxServerSectionSuggestionApiUrl )\n\t\t\t\t.then( function ( response ) {\n\t\t\t\t\treturn response.ok ?\n\t\t\t\t\t\tresponse.json() :\n\t\t\t\t\t\tPromise.reject( new Error( 'Failed to load data from server' ) );\n\t\t\t\t} )\n\t\t\t\t.then( function ( suggestionResult ) {\n\t\t\t\t\tif ( suggestionResult.sections ) {\n\t\t\t\t\t\tthat.missingSections = Object.keys( suggestionResult.sections.missing );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t}\n\t},\n\tmounted: function () {\n\t\tvar that = this;\n\t\tthis.$nextTick( function () {\n\t\t\tthat.contentHeight = Math.max( that.contentHeight, that.$refs.content.clientHeight );\n\t\t} );\n\t\tthis.getSectionSuggestions();\n\t}\n};\n</script>\n\n<style lang=\"less\">\n// Copying variables from \"mediawiki.ui/variables.less\"\n@background-color-base: #fff;\n@color-base: #202122;\n@color-base--inverted: #fff;\n@color-primary: #36c;\n@color-base--subtle: #72777d;\n@colorGray14: #eaecf0;\n@colorGray15: #f8f9fa;\n@border-style-base: solid;\n@border-width-base: 1px;\n\n.icon(@path) {\n  background: center / contain no-repeat url( @path );\n}\n\n.sx-recent-translation-dialog {\n  display: flex;\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  align-items: center;\n  justify-content: center;\n  z-index: 100;\n\n  .row {\n    box-sizing: border-box;\n    display: flex;\n    flex: 0 1 auto;\n    flex-wrap: wrap;\n  }\n\n  .col {\n    flex-basis: 0;\n    flex-grow: 1;\n    max-width: 100%;\n  }\n\n  .shrink {\n    flex-grow: 0 !important;\n    flex-shrink: 1 !important;\n  }\n\n  p {\n    color: @color-base;\n  }\n\n  &__overlay {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background-color: rgba(0, 0, 0, 0.5);\n  }\n\n  &__shell {\n    background-color: @background-color-base;\n    position: relative;\n    max-width: 400px;\n    margin: 0 12px;\n  }\n\n  &__header {\n    padding: 12px;\n    &-close-icon {\n      cursor: pointer;\n      .icon(\"../images/close-icon.svg\")\n    }\n    h3 {\n      padding: 0;\n      font-size: 16px;\n    }\n  }\n\n  &__action-switches {\n    padding: 0 16px;\n    background-color: @colorGray15;\n    border-top: @border-style-base @border-width-base @colorGray14;\n    border-bottom: @border-style-base @border-width-base @colorGray14;\n  }\n  &__action-switch {\n    padding: 12px 0;\n    font-weight: bold;\n    &--enabled {\n      border-bottom: solid 3px @color-base;\n    }\n    &:not(:last-of-type) {\n      [ dir='ltr' ] & {\n        margin-right: 16px;\n      }\n\n      [ dir='rtl' ] & {\n        margin-left: 16px;\n      }\n    }\n  }\n\n  &__content {\n    padding: 16px;\n    box-sizing: border-box;\n  }\n  &__review-tab {\n    height: 100%;\n  }\n  & &__review-lead-text {\n    margin: 0 0 20px 0;\n  }\n  & &__review-list {\n    // reset list numbering\n    counter-reset: review-list-counter;\n    list-style: none;\n    padding-left: 0;\n    position: relative;\n\n    &-item {\n      [ dir='ltr' ] & {\n        padding-left: 20px;\n      }\n\n      [ dir='rtl' ] & {\n        padding-right: 20px;\n      }\n      // Increase counter for each item\n      counter-increment: review-list-counter;\n      list-style-type: none;\n      position: relative;\n      // Styles for list number\n      &:before {\n        font-family: sans-serif;\n        content: counter(review-list-counter) \".\";\n        display: inline-block;\n        font-weight: bold;\n        position: absolute;\n        left: 0;\n        top: -2px;\n      }\n      h4 {\n        padding: 0;\n        font-size: 16px;\n        font-weight: bold;\n      }\n      p {\n        font-size: 14px;\n        margin: 0;\n      }\n    }\n  }\n  & &__edit-button {\n    margin-top: 36px;\n    [ dir='ltr' ] & {\n      margin-left: 12px;\n    }\n\n    [ dir='rtl' ] & {\n      margin-right: 12px;\n    }\n  }\n  & &__translate-button {\n    margin-top: 24px;\n  }\n  &__edit-icon {\n    .icon(\"../images/edit-icon.svg\");\n  }\n  &__language-icon {\n    .icon(\"../images/language-icon.svg\");\n  }\n  &__original-page {\n    &-link {\n      margin-top: 20px;\n      margin-bottom: 8px;\n      [ dir='ltr' ] & {\n        margin-left: 8px;\n      }\n\n      [ dir='rtl' ] & {\n        margin-right: 8px;\n      }\n    }\n    &-anchor {\n      font-weight: 600;\n      color: @color-primary;\n    }\n    &-icon-anchor {\n      span {\n        .icon(\"../images/external-link-icon.svg\");\n      }\n    }\n  }\n  &__add-sections-tab {\n    height: 100%;\n    & &__lead-text {\n      margin: 0;\n    }\n  }\n  &__translate-secondary-notice {\n    margin-top: 16px;\n    color: @color-base--subtle;\n    font-size: 14px;\n  }\n}\n</style>\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.recenttranslation/index.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"( function () {\n\tvar mainContent,\n\t\tentrypointContainer = document.createElement( 'div' ),\n\t\tbannerContainer = document.createElement( 'div' ),\n\t\ticonContainer = document.createElement( 'span' ),\n\t\tcontentContainer = document.createElement( 'div' ),\n\t\tdialogContainer = document.createElement( 'div' ),\n\t\tdialogInstance,\n\t\tdialogInitialized = false;\n\n\t/**\n\t * @return {HTMLDivElement}\n\t */\n\tfunction createContent() {\n\t\tvar headerText = document.createElement( 'h3' ),\n\t\t\tsecondaryText = document.createElement( 'p' );\n\n\t\tcontentContainer.className = 'sx-recent-translation-banner__content';\n\t\theaderText.className = 'sx-recent-translation-banner__content-header';\n\t\theaderText.innerText = mw.msg( 'sx-recent-translation-entrypoint-banner-header' );\n\n\t\tsecondaryText.className = 'sx-recent-translation-banner__content-details';\n\t\tsecondaryText.innerText = mw.msg( 'sx-recent-translation-entrypoint-banner-details' );\n\n\t\tcontentContainer.appendChild( headerText );\n\t\tcontentContainer.appendChild( secondaryText );\n\t\treturn contentContainer;\n\t}\n\n\tfunction createDialogInstance() {\n\t\tvar Vue = require( 'vue' ),\n\t\t\tRecentTranslationEntrypointDialogComponent =\n\t\t\t\trequire( './RecentTranslationEntrypointDialog.vue' );\n\n\t\treturn Vue.createMwApp( RecentTranslationEntrypointDialogComponent ).mount( dialogContainer );\n\t}\n\n\t// If there are any other existing notices, we should not add the entrypoint banner.\n\t// For this reason we check for all elements with .hatnote and .ambox class that exist\n\t// inside the lead section of the article. We can expand this list to add more class names\n\t// that are considered to belong to \"notice\" elements.\n\t// If not such elements exist, continue with adding the entrypoint banner\n\tif ( !document.querySelector( '#mf-section-0 > .hatnote, #mf-section-0 > .ambox' ) ) {\n\t\tmainContent = document.querySelector( '.mw-parser-output' );\n\t\tentrypointContainer.className = 'sx-recent-translation-entrypoint';\n\n\t\tbannerContainer.className = 'sx-recent-translation-banner';\n\t\ticonContainer.className = 'sx-recent-translation-banner__icon mw-ui-icon';\n\t\tcontentContainer = createContent();\n\t\tbannerContainer.appendChild( iconContainer );\n\t\tbannerContainer.appendChild( contentContainer );\n\n\t\tentrypointContainer.appendChild( bannerContainer );\n\n\t\tdialogContainer.className = 'sx-recent-translation-dialog-container';\n\n\t\tbannerContainer.addEventListener( 'click', function () {\n\t\t\tif ( !dialogInitialized ) {\n\t\t\t\tdialogInstance = createDialogInstance();\n\t\t\t\tdialogInitialized = true;\n\t\t\t} else {\n\t\t\t\tdialogInstance.showDialog = true;\n\t\t\t}\n\t\t} );\n\n\t\tdocument.body.appendChild( dialogContainer );\n\t\tmainContent.prepend( entrypointContainer );\n\t}\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.uls.relevantlanguages.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"( function () {\n\tvar entrypointRendered = false;\n\t/**\n\t * @param {string[]} missingRelevantLanguages array of missing frequent language autonyms\n\t * @return {string|null}\n\t */\n\tfunction getSXMissingLanguagesText( missingRelevantLanguages ) {\n\t\tvar missingRelevantLanguagesCount = Object.keys( missingRelevantLanguages ).length;\n\t\tif ( missingRelevantLanguagesCount === 1 ) {\n\t\t\treturn mw.message(\n\t\t\t\t'cx-uls-relevant-languages-banner-text-single-missing',\n\t\t\t\t$.uls.data.getAutonym( missingRelevantLanguages[ 0 ] )\n\t\t\t).parse();\n\t\t} else {\n\t\t\treturn mw.message(\n\t\t\t\t'cx-uls-relevant-languages-banner-text-multiple-missing',\n\t\t\t\t$.uls.data.getAutonym( missingRelevantLanguages[ 0 ] ),\n\t\t\t\t$.uls.data.getAutonym( missingRelevantLanguages[ 1 ] )\n\t\t\t).parse();\n\t\t}\n\t}\n\n\t/**\n\t * @param {string[]} missingRelevantLanguages array of missing frequent language autonyms\n\t * @return {HTMLDivElement}\n\t */\n\tfunction createPanelTextElement( missingRelevantLanguages ) {\n\t\tvar missingLanguagesPanelTextElement = document.createElement( 'div' );\n\t\tmissingLanguagesPanelTextElement.className = 'cx-uls-relevant-languages-banner__text';\n\t\tmissingLanguagesPanelTextElement.innerHTML = getSXMissingLanguagesText( missingRelevantLanguages );\n\n\t\treturn missingLanguagesPanelTextElement;\n\t}\n\n\t/**\n\t * @return {HTMLSpanElement}\n\t */\n\tfunction createArrowIcon() {\n\t\tvar span = document.createElement( 'span' );\n\t\tspan.className = 'cx-uls-relevant-languages-banner__icon mw-ui-icon';\n\t\treturn span;\n\t}\n\n\t/**\n\t * @param {string[]} missingRelevantLanguages array of missing frequent language autonyms\n\t * @param {Object} uls The ULS object\n\t * @return {HTMLDivElement}\n\t */\n\tfunction createMissingLanguagesPanel( missingRelevantLanguages, uls ) {\n\t\tvar missingLanguagesPanel = document.createElement( 'div' ),\n\t\t\tmissingLanguagesPanelText = createPanelTextElement( missingRelevantLanguages ),\n\t\t\tmissingLanguagesPanelIcon = createArrowIcon();\n\n\t\tmissingLanguagesPanel.className = 'cx-uls-relevant-languages-banner';\n\t\tmissingLanguagesPanel.appendChild( missingLanguagesPanelText );\n\t\tmissingLanguagesPanel.appendChild( missingLanguagesPanelIcon );\n\n\t\tmissingLanguagesPanel.addEventListener( 'click', function () {\n\t\t\tvar Vue = require( 'vue' );\n\t\t\tvar CxUlsEntrypoint = require( './ext.cx.entrypoints.uls.relevantlanguages/CxUlsEntrypoint.vue' );\n\t\t\tvar position = uls.position(), width = uls.$menu.width();\n\t\t\tuls.hide();\n\n\t\t\tvar panel = document.createElement( 'div' );\n\t\t\tpanel.className = 'cx-uls-entrypoint__panel-container';\n\t\t\t// Copy the positioning from parent ULS Menu\n\t\t\tObject.keys( position ).forEach( function ( property ) {\n\t\t\t\tpanel.style[ property ] = position[ property ] + 'px';\n\t\t\t} );\n\t\t\tpanel.style.width = width + 'px';\n\t\t\tpanel.style.zIndex = uls.$menu.css( 'z-index' );\n\t\t\tpanel.style.position = uls.$menu.css( 'position' );\n\t\t\tdocument.body.appendChild( panel );\n\n\t\t\tVue.createMwApp( CxUlsEntrypoint, {\n\t\t\t\tlanguages: missingRelevantLanguages,\n\t\t\t\tonClose: uls.show.bind( uls )\n\t\t\t} ).mount( panel );\n\t\t} );\n\n\t\treturn missingLanguagesPanel;\n\t}\n\n\tmw.hook( 'mw.uls.compact_language_links.open' ).add(\n\t\tfunction ( $trigger ) {\n\t\t\tif ( entrypointRendered ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar uls = $trigger.data( 'uls' );\n\t\t\t// Remove variants, Remove current language\n\t\t\tvar frequentLanguages = mw.uls.getFrequentLanguageList().map( function ( language ) {\n\t\t\t\treturn language.split( '-' )[ 0 ];\n\t\t\t} ).filter( function ( language, index, frequentLangs ) {\n\t\t\t\treturn frequentLangs.indexOf( language ) === index && language !== mw.config.get( 'wgContentLanguage' );\n\t\t\t} );\n\n\t\t\tvar existingLanguages = Object.keys( uls.languages );\n\n\t\t\tif ( !existingLanguages.length ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar missingRelevantLanguages = frequentLanguages.filter( function ( language ) {\n\t\t\t\treturn existingLanguages.indexOf( language ) === -1;\n\t\t\t} );\n\n\t\t\tif ( !missingRelevantLanguages.length ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tvar missingLanguagesPanel = createMissingLanguagesPanel( missingRelevantLanguages, uls );\n\t\t\tuls.$resultsView.before( missingLanguagesPanel );\n\t\t\tuls.$languageFilter.on( 'input', function ( event ) {\n\t\t\t\t// when user types inside the search input, add \".cx-uls-entrypoint--hidden\" class to the\n\t\t\t\t// entrypoint banner element the CSS rule that hides the entrypoint when this class is applied,\n\t\t\t\t// lives inside CxUlsEntrypoint.vue file\n\t\t\t\tif ( event.currentTarget.value ) {\n\t\t\t\t\tmissingLanguagesPanel.classList.add( 'cx-uls-entrypoint--hidden' );\n\t\t\t\t} else {\n\t\t\t\t\t// when user empties the search input, remove the \".cx-uls-entrypoint--hidden\" class (if exists),\n\t\t\t\t\t// so that the entrypoint is visible again.\n\t\t\t\t\tmissingLanguagesPanel.classList.remove( 'cx-uls-entrypoint--hidden' );\n\t\t\t\t}\n\t\t\t} );\n\t\t\tentrypointRendered = true;\n\t\t}\n\t);\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.entrypoints.uls.relevantlanguages/CxUlsEntrypoint.vue","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":30,"column":6,"nodeType":"VAttribute","endLine":30,"endColumn":64},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":40,"column":5,"nodeType":"VAttribute","endLine":40,"endColumn":64},{"ruleId":"vue/no-v-text","severity":1,"message":"Don't use 'v-text'.","line":68,"column":7,"nodeType":"VAttribute","endLine":68,"endColumn":38}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"<template>\n\t<div\n\t\tv-show=\"showPanel\"\n\t\tclass=\"cx-uls-entrypoint\"\n\t>\n\t\t<div class=\"cx-uls-entrypoint__header row\">\n\t\t\t<div class=\"col shrink\">\n\t\t\t\t<button class=\"cx-uls-entrypoint__close-button\" @click.stop=\"close\">\n\t\t\t\t\t<span>\n\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\t\t:width=\"size\"\n\t\t\t\t\t\t\t:height=\"size\"\n\t\t\t\t\t\t\tviewBox=\"0 0 20 20\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<g :fill=\"iconColor\">\n\t\t\t\t\t\t\t\t<path :d=\"previousIconPath\" />\n\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</span>\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<div class=\"col grow\">\n\t\t\t\t<h5\n\t\t\t\t\tclass=\"cx-uls-entrypoint__header-title\"\n\t\t\t\t\t:lang=\"sourceLanguage\"\n\t\t\t\t\t:dir=\"getDir( sourceLanguage )\"\n\t\t\t\t\tv-text=\"$i18n( 'cx-uls-relevant-languages-panel-header' )\"\n\t\t\t\t>\n\t\t\t\t</h5>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"cx-uls-entrypoint__body\">\n\t\t\t<p\n\t\t\t\tclass=\"cx-uls-entrypoint__body__message\"\n\t\t\t\t:lang=\"sourceLanguage\"\n\t\t\t\t:dir=\"getDir( sourceLanguage )\"\n\t\t\t\tv-text=\"$i18n( 'cx-uls-relevant-languages-panel-message' )\"\n\t\t\t>\n\t\t\t</p>\n\t\t\t<div class=\"cx-uls-entrypoint__body__translation-links row\">\n\t\t\t\t<a\n\t\t\t\t\tv-for=\"language in slicedLanguages\"\n\t\t\t\t\t:key=\"'link-' + language\"\n\t\t\t\t\tclass=\"cx-uls-entrypoint__body__translation-link\"\n\t\t\t\t\t:href=\"getCXUrlByTargetLanguage( language )\"\n\t\t\t\t>\n\t\t\t\t\t<span class=\"cx-uls-entrypoint__body__translation-link-icon\">\n\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\t\t:width=\"size\"\n\t\t\t\t\t\t\t:height=\"size\"\n\t\t\t\t\t\t\tviewBox=\"0 0 20 20\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<g :fill=\"iconColor\">\n\t\t\t\t\t\t\t\t<path :d=\"plusIconPath\" />\n\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"cx-uls-entrypoint__body__translation-link-text\"\n\t\t\t\t\t\t:lang=\"language\"\n\t\t\t\t\t\t:dir=\"getDir( language )\"\n\t\t\t\t\t\tv-text=\"getAutonym( language )\"\n\t\t\t\t\t></span>\n\t\t\t\t</a>\n\t\t\t\t<a\n\t\t\t\t\tclass=\"cx-uls-entrypoint__body__translation-link\"\n\t\t\t\t\t:href=\"getCXUrlByTargetLanguage( null )\"\n\t\t\t\t>\n\t\t\t\t\t<span class=\"cx-uls-entrypoint__body__translation-link-icon\">\n\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\t\t:width=\"size\"\n\t\t\t\t\t\t\t:height=\"size\"\n\t\t\t\t\t\t\tviewBox=\"0 0 20 20\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<g :fill=\"iconColor\">\n\t\t\t\t\t\t\t\t<path :d=\"ellipsisIconPath\" />\n\t\t\t\t\t\t\t</g>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nvar Vue = require( 'vue' );\n\n// @vue/component\nmodule.exports = {\n\tcompatConfig: { MODE: 3 },\n\tname: 'CxUlsEntrypoint',\n\tprops: {\n\t\tlanguages: {\n\t\t\ttype: Array,\n\t\t\trequired: true\n\t\t},\n\t\tonClose: {\n\t\t\ttype: Function,\n\t\t\trequired: true\n\t\t}\n\t},\n\tsetup: function ( props ) {\n\t\tvar showPanel = Vue.ref( true );\n\t\tvar siteMapper = new mw.cx.SiteMapper();\n\t\tvar slicedLanguages = Vue.computed( function () {\n\t\t\treturn props.languages.slice( 0, 2 );\n\t\t} );\n\n\t\tvar close = function () {\n\t\t\tshowPanel.value = false;\n\t\t\tprops.onClose();\n\t\t};\n\n\t\tvar sourceLanguage = siteMapper.getCurrentWikiLanguageCode();\n\n\t\tvar getCXUrlByTargetLanguage = function ( targetLanguage ) {\n\t\t\tvar sourceTitle = mw.config.get( 'wgTitle' );\n\n\t\t\treturn siteMapper.getCXUrl(\n\t\t\t\tsourceTitle,\n\t\t\t\t'',\n\t\t\t\tsourceLanguage,\n\t\t\t\ttargetLanguage || null\n\t\t\t);\n\t\t};\n\n\t\treturn {\n\t\t\tclose: close,\n\t\t\tellipsisIconPath: 'M 19,10 a 2,2 0 0 1 -2,2 2,2 0 0 1 -2,-2 2,2 0 0 1 2,-2 2,2 0 0 1 2,2 M 5,10 A 2,2 0 0 1 3,12 2,2 0 0 1 1,10 2,2 0 0 1 3,8 2,2 0 0 1 5,10 m 7,0 a 2,2 0 0 1 -2,2 2,2 0 0 1 -2,-2 2,2 0 0 1 2,-2 2,2 0 0 1 2,2',\n\t\t\tgetDir: $.uls.data.getDir,\n\t\t\tgetAutonym: $.uls.data.getAutonym,\n\t\t\ticonColor: 'currentColor',\n\t\t\tplusIconPath: 'M11 9V4H9v5H4v2h5v5h2v-5h5V9z',\n\t\t\tpreviousIconPath: 'M5.83 9l5.58-5.58L10 2l-8 8 8 8 1.41-1.41L5.83 11H18V9z',\n\t\t\tgetCXUrlByTargetLanguage: getCXUrlByTargetLanguage,\n\t\t\tshowPanel: showPanel,\n\t\t\tsize: 20,\n\t\t\tslicedLanguages: slicedLanguages,\n\t\t\tsourceLanguage: sourceLanguage\n\t\t};\n\t}\n};\n</script>\n\n<style lang=\"less\">\n@import 'mediawiki.ui/variables';\n\n.cx-uls-entrypoint {\n  border: @border-base;\n  color: @color-base;\n  &--hidden {\n    display: none;\n  }\n  .row {\n    box-sizing: border-box;\n    display: flex;\n    flex: 0 1 auto;\n    flex-wrap: wrap;\n  }\n  .col {\n    flex-basis: 0;\n    flex-grow: 1;\n    max-width: 100%;\n  }\n  .shrink {\n    flex-grow: 0 !important;\n    flex-shrink: 1 !important;\n  }\n  button {\n    cursor: pointer;\n  }\n  &__header {\n    align-items: center;\n    padding: 12px;\n    border-bottom: @border-base;\n    &-title {\n      font-size: 16px;\n      padding-block: 0;\n      padding-inline: 8px;\n      margin: 0;\n    }\n  }\n  &__close-button {\n    background: none;\n    border: none;\n    align-self: center;\n  }\n  &__body {\n    padding: 16px;\n    &__message {\n      margin: 0;\n      padding-bottom: 8px;\n      padding-inline: 8px;\n      line-height: 1.4;\n    }\n    &__translation-links {\n      align-items: baseline;\n      padding-inline: 8px;\n    }\n    &__translation-link {\n      padding: 4px;\n      background: none;\n      border: none;\n      align-items: center;\n      display: flex;\n      // set color to \"inherit\", so that the CX links have the same color as the rest of the text body (@color-base)\n      color: inherit;\n      &-icon {\n        // set display to \"flex\", so that the height of the outer span is equal to the icon height (20px)\n        display: flex;\n        padding-inline-end: 4px;\n      }\n      &-text {\n        font-size: 16px;\n        margin-inline-end: 4px;\n      }\n    }\n  }\n}\n</style>\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.interlanguagelink.init.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"no-implicit-globals","severity":2,"message":"Unexpected 'var' declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable.","line":9,"column":5,"nodeType":"VariableDeclarator","messageId":"globalNonLexicalBinding","endLine":9,"endColumn":35,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-implicit-globals","severity":2,"message":"Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable.","line":17,"column":1,"nodeType":"FunctionDeclaration","messageId":"globalNonLexicalBinding","endLine":22,"endColumn":2,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-implicit-globals","severity":2,"message":"Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable.","line":34,"column":1,"nodeType":"FunctionDeclaration","messageId":"globalNonLexicalBinding","endLine":75,"endColumn":2,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-implicit-globals","severity":2,"message":"Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable.","line":77,"column":1,"nodeType":"FunctionDeclaration","messageId":"globalNonLexicalBinding","endLine":91,"endColumn":2,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-implicit-globals","severity":2,"message":"Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable.","line":93,"column":1,"nodeType":"FunctionDeclaration","messageId":"globalNonLexicalBinding","endLine":111,"endColumn":2,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n\n'use strict';\n/* eslint-disable no-implicit-globals */\n\nvar $pLangList = $( '#p-lang ul' );\n\n/**\n\t* Checks if there is a page in the target language.\n\t*\n\t* @param {string} code\n\t* @return {boolean}\n\t*/\nfunction pageInLanguageExists( code ) {\n\tvar map = require( '../config.json' ).ContentTranslationDomainCodeMapping,\n\t\tdomainCode = map[ code ] || code;\n\n\treturn $( 'li.interlanguage-link.interwiki-' + domainCode ).length === 1;\n}\n\n/**\n\t* Get the list of target languages that should be suggested to the current user:\n\t* - The MediaWiki user interface language.\n\t* - Accept-Language.\n\t* - Browser interface language.\n\t*\n\t* Page language is ignored. Only languages in which article does not exist are suggested.\n\t*\n\t* @return {string[]} Target languages\n\t*/\nfunction getSuggestedTargetLanguages() {\n\tvar splitCode, splitCodes, specialCodeIndex,\n\t\tpossibleTargetLanguages = [],\n\t\tpageLanguage = mw.config.get( 'wgPageContentLanguage' ).split( '-' )[ 0 ];\n\n\tpossibleTargetLanguages.push( mw.config.get( 'wgUserLanguage' ) );\n\tpossibleTargetLanguages.push( mw.uls.getBrowserLanguage() );\n\n\tArray.prototype.push.apply( possibleTargetLanguages, mw.uls.getAcceptLanguageList() );\n\tArray.prototype.push.apply( possibleTargetLanguages, mw.uls.getPreviousLanguages() );\n\n\t// Language codes can have country extensions like en-US.\n\t// Remove them so that it is like domain code format.\n\tpossibleTargetLanguages = possibleTargetLanguages.map( function ( language ) {\n\t\treturn language.split( '-' )[ 0 ];\n\t} );\n\n\t// Replace possibly non-standard, macro and duplicate language codes\n\t// with normalized counterparts\n\tsplitCodes = {\n\t\t// Suggest both varieties of Belarusian when requesting 'be'\n\t\tbe: [ 'be', 'be-tarask' ],\n\t\t// Suggest both varieties of Norwegian when requesting 'no'\n\t\tno: [ 'nb', 'nn' ]\n\t};\n\n\tfor ( splitCode in splitCodes ) {\n\t\tspecialCodeIndex = possibleTargetLanguages.indexOf( splitCode );\n\t\tif ( specialCodeIndex > -1 ) {\n\t\t\tpossibleTargetLanguages.splice( specialCodeIndex, 1 );\n\t\t\tArray.prototype.push.apply( possibleTargetLanguages, splitCodes[ splitCode ] );\n\t\t}\n\t}\n\n\treturn possibleTargetLanguages.filter( function ( language ) {\n\t\t// Code should not be a language in which page exists.\n\t\t// Also it should be a known language for ULS\n\t\treturn language !== pageLanguage &&\n\t\t\t!pageInLanguageExists( language ) &&\n\t\t\tlanguage !== $.uls.data.getAutonym( language );\n\t} );\n}\n\nfunction prepareCXInterLanguageLinks( suggestedTargetLanguages ) {\n\tvar $newItem, count = 0, maxListSize = 3;\n\n\t// Remove duplicates\n\tsuggestedTargetLanguages = suggestedTargetLanguages.filter( function ( element, index ) {\n\t\treturn suggestedTargetLanguages.indexOf( element ) === index;\n\t} );\n\n\tsuggestedTargetLanguages.some( function ( code ) {\n\t\t$newItem = mw.cx.createCXInterlanguageItem( code );\n\t\t$pLangList.prepend( $newItem );\n\t\t// Array.prototype.some breaks the iteration first time `true` is returned\n\t\treturn ++count === maxListSize;\n\t} );\n}\n\nfunction init() {\n\tvar suggestedTargetLanguages;\n\n\t// No language links on the page\n\tif ( $pLangList.length === 0 ) {\n\t\treturn;\n\t}\n\n\tsuggestedTargetLanguages = getSuggestedTargetLanguages();\n\n\tif ( !suggestedTargetLanguages.length ) {\n\t\treturn;\n\t}\n\n\tmw.loader.using( 'ext.cx.interlanguagelink' ).then( function () {\n\t\tmw.cx.siteMapper = new mw.cx.SiteMapper();\n\t\tprepareCXInterLanguageLinks( suggestedTargetLanguages );\n\t} );\n}\n\n// Early execute of init\nif ( document.readyState === 'interactive' ) {\n\tinit();\n} else {\n\t$( init );\n}\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.interlanguagelink.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\tvar CAMPAIGN = 'interlanguagelink';\n\n\t/**\n\t * Start a new page translation in Special:CX.\n\t *\n\t * @param {string} targetLanguage\n\t */\n\tfunction startPageInCX( targetLanguage ) {\n\t\tvar sourceLanguage = mw.cx.siteMapper.getCurrentWikiLanguageCode(),\n\t\t\tsourceTitle = mw.config.get( 'wgTitle' );\n\n\t\tmw.cx.siteMapper.setCXToken( sourceLanguage, targetLanguage, sourceTitle );\n\t\tlocation.href = mw.cx.siteMapper.getCXUrl(\n\t\t\tsourceTitle,\n\t\t\tnull,\n\t\t\tsourceLanguage,\n\t\t\ttargetLanguage,\n\t\t\t{ campaign: CAMPAIGN }\n\t\t);\n\t}\n\n\t/**\n\t * Render the CX entry point dialog.\n\t *\n\t * @param {string} targetLanguage\n\t * @return {jQuery}\n\t */\n\tfunction getDialogContent( targetLanguage ) {\n\t\tvar settingsButton, descriptionLabel, actionTranslate, $license, actions;\n\n\t\tdescriptionLabel = new OO.ui.LabelWidget( {\n\t\t\tclasses: [ 'cx-entrypoint-dialog__desc' ],\n\t\t\tlabel: mw.msg( 'cx-entrypoint-dialog-desc' )\n\t\t} );\n\n\t\tactionTranslate = new OO.ui.ButtonWidget( {\n\t\t\tlabel: mw.msg(\n\t\t\t\t'cx-entrypoint-dialog-button-translate-from',\n\t\t\t\t$.uls.data.getAutonym( mw.config.get( 'wgContentLanguage' ) )\n\t\t\t),\n\t\t\tflags: [ 'primary', 'progressive' ]\n\t\t} );\n\n\t\tactionTranslate.on( 'click', function () {\n\t\t\tstartPageInCX( targetLanguage );\n\t\t} );\n\n\t\t$license = $( '<div>' )\n\t\t\t.addClass( 'cx-entrypoint-dialog__license' )\n\t\t\t.append( mw.message( 'cx-license-agreement' ).parseDom() );\n\n\t\tactions = new OO.ui.HorizontalLayout( {\n\t\t\tclasses: [ 'cx-entrypoint-dialog__actions' ],\n\t\t\titems: [ actionTranslate ]\n\t\t} );\n\n\t\tif ( !mw.config.get( 'wgContentTranslationAsBetaFeature' ) ) {\n\t\t\tsettingsButton = new OO.ui.ButtonWidget( {\n\t\t\t\tclasses: [ 'cx-entrypoint-dialog__settings' ],\n\t\t\t\ticon: 'settings',\n\t\t\t\tframed: false,\n\t\t\t\thref: mw.util.getUrl( 'Special:Preferences#mw-prefsection-rendering-languages' ),\n\t\t\t\ttarget: '_blank'\n\t\t\t} );\n\t\t\tactions.addItems( [ settingsButton ] );\n\t\t}\n\t\treturn $( '<div>' ).append( descriptionLabel.$element, actions.$element, $license );\n\t}\n\n\tfunction createCXInterlanguageItem( code ) {\n\t\tvar $languageLink, popup, autonym = $.uls.data.getAutonym( code );\n\n\t\t$languageLink = $( '<a>' )\n\t\t\t.addClass( 'new' )\n\t\t\t.prop( {\n\t\t\t\ttitle: mw.msg( 'cx-entrypoint-title', autonym ),\n\t\t\t\tlang: code,\n\t\t\t\thref: mw.util.getUrl( 'Special:ContentTranslation', {\n\t\t\t\t\tpage: mw.config.get( 'wgTitle' ),\n\t\t\t\t\tfrom: mw.cx.siteMapper.getCurrentWikiLanguageCode(),\n\t\t\t\t\tto: code\n\t\t\t\t} )\n\t\t\t} )\n\t\t\t.text( autonym );\n\n\t\tpopup = new OO.ui.PopupWidget( {\n\t\t\tlabel: mw.msg( 'cx-entrypoint-dialog-page-doesnt-exist-yet', autonym ),\n\t\t\tclasses: [ 'cx-entrypoint-dialog' ],\n\t\t\t$content: getDialogContent( code ),\n\t\t\thead: true,\n\t\t\twidth: 500,\n\t\t\tposition: 'after',\n\t\t\tautoFlip: false,\n\t\t\tautoClose: true\n\t\t} );\n\n\t\t// HACK: We attached PopupWidget to element in side panel, which is absolutely\n\t\t// positioned and often below the fold. Vector skin sets `height: 100%` to\n\t\t// <html> and <body> elements, and <html> element is used as container for popup.\n\t\t// <html> element is not covering whole document, just the viewport, which messes\n\t\t// up the calculation in PopupWidget's computePosition method. See T224880.\n\t\t// As a workaround, set <html> element's height to `auto` temporarily while doing\n\t\t// position calculations and reset later. Also, check if that height is enough,\n\t\t// since our gray interlanguage link is in absolutely positioned side panel.\n\t\tpopup.computePosition = function () {\n\t\t\tvar oldPositionObj,\n\t\t\t\t$html = $( 'html' ),\n\t\t\t\tpanelHeight = $( '#mw-panel' ).outerHeight();\n\n\t\t\t$html.css( 'height', 'auto' );\n\t\t\tif ( $html.outerHeight() < panelHeight ) {\n\t\t\t\t$html.css( 'height', panelHeight );\n\t\t\t}\n\t\t\toldPositionObj = OO.ui.PopupWidget.prototype.computePosition.call( popup );\n\t\t\t$html.css( 'height', '' );\n\n\t\t\treturn oldPositionObj;\n\t\t};\n\n\t\t$languageLink.on( 'click', function () {\n\t\t\tpopup.toggle( true );\n\t\t\treturn false;\n\t\t} );\n\n\t\treturn $( '<li>' )\n\t\t\t.addClass( 'cx-new-interlanguage-link' )\n\t\t\t.append( $languageLink, popup.$element );\n\t}\n\n\tmw.cx.createCXInterlanguageItem = createCXInterlanguageItem;\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/entrypoints/ext.cx.uls.quick.actions.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"( function () {\n\t// Here we override \"TranslateInTarget\" configuration parameter for the SiteMapper class,\n\t// to always be false. This is required since we don't know beforehand the target language\n\t// for this CX entrypoint, and thus we want to avoid redirection to target wiki, where the\n\t// publishing happens in production wikis. Ultimately, we want to redirect the user to CX\n\t// within the current wiki with source page, source language and target language prefilled.\n\t// The target language is the first among the suggested target languages for the current\n\t// user. In case no such suggested target languages exist, the target language is null\n\t// but the redirection to CX still happens with the target language missing.\n\tvar siteMapper = new mw.cx.SiteMapper( { TranslateInTarget: false } );\n\tvar sourceLanguage = siteMapper.getCurrentWikiLanguageCode();\n\n\t/**\n\t * Checks if there is a page in the target language.\n\t *\n\t * @param {string} code\n\t * @return {boolean}\n\t */\n\tfunction pageInLanguageExists( code ) {\n\t\tvar domainCode = siteMapper.getWikiDomainCode( code );\n\n\t\treturn $( 'li.interlanguage-link.interwiki-' + domainCode ).length === 1;\n\t}\n\n\t/**\n\t * Copied from ext.cx.interlanguagelink.init.js\n\t *\n\t * This method creates a list of target languages that could be suggested to the current user:\n\t * - The MediaWiki user interface language.\n\t * - Accept-Language.\n\t * - Browser interface language.\n\t * It filters out page language and languages in which the article DOES exist, and returns\n\t * the first language in the array if the array is not empty or null elsewise.\n\t *\n\t * @return {string|null} Target language\n\t */\n\tfunction getSuggestedTargetLanguage() {\n\t\tvar pageLanguage = mw.config.get( 'wgPageContentLanguage' ).split( '-' )[ 0 ];\n\t\tvar possibleTargetLanguages = [];\n\t\tpossibleTargetLanguages.push( mw.config.get( 'wgUserLanguage' ) );\n\t\tpossibleTargetLanguages.push( mw.uls.getBrowserLanguage() );\n\n\t\tArray.prototype.push.apply( possibleTargetLanguages, mw.uls.getAcceptLanguageList() );\n\t\tArray.prototype.push.apply( possibleTargetLanguages, mw.uls.getPreviousLanguages() );\n\n\t\t// Language codes can have country extensions like en-US.\n\t\t// Remove them so that it is like domain code format.\n\t\tpossibleTargetLanguages = possibleTargetLanguages.map( function ( language ) {\n\t\t\treturn language.split( '-' )[ 0 ];\n\t\t} );\n\n\t\t// Replace possibly non-standard, macro and duplicate language codes\n\t\t// with normalized counterparts\n\t\tvar splitCodes = {\n\t\t\t// Suggest both varieties of Belarusian when requesting 'be'\n\t\t\tbe: [ 'be', 'be-tarask' ],\n\t\t\t// Suggest both varieties of Norwegian when requesting 'no'\n\t\t\tno: [ 'nb', 'nn' ]\n\t\t};\n\n\t\tfor ( var splitCode in splitCodes ) {\n\t\t\tvar specialCodeIndex = possibleTargetLanguages.indexOf( splitCode );\n\t\t\tif ( specialCodeIndex > -1 ) {\n\t\t\t\tpossibleTargetLanguages.splice( specialCodeIndex, 1 );\n\t\t\t\tArray.prototype.push.apply( possibleTargetLanguages, splitCodes[ splitCode ] );\n\t\t\t}\n\t\t}\n\n\t\tpossibleTargetLanguages = possibleTargetLanguages.filter( function ( language ) {\n\t\t\t// Code should not be a language in which page exists.\n\t\t\t// Also it should be a known language for ULS\n\t\t\treturn language !== pageLanguage &&\n\t\t\t\t\t!pageInLanguageExists( language ) &&\n\t\t\t\t\tlanguage !== $.uls.data.getAutonym( language );\n\t\t} );\n\n\t\treturn possibleTargetLanguages.length ? possibleTargetLanguages[ 0 ] : null;\n\t}\n\n\tvar cxEntrypointUrl = siteMapper.getCXUrl(\n\t\tmw.config.get( 'wgTitle' ),\n\t\tnull,\n\t\tsourceLanguage,\n\t\tgetSuggestedTargetLanguage()\n\t);\n\n\tvar translateActionItem = {\n\t\tname: 'cxTranslate',\n\t\ticon: 'add',\n\t\ttext: mw.msg( 'cx-uls-translate-page-quick-action-label' ),\n\t\thref: cxEntrypointUrl\n\t};\n\n\tmw.uls.ActionsMenuItemsRegistry.register( translateActionItem );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/eventlogging/ext.cx.eventlogging.campaigns.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation event logging for campaigns.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n\n( function () {\n\t'use strict';\n\n\t$( function () {\n\t\tmw.hook( 'mw.cx.cta.accept' ).add( function ( campaign ) {\n\t\t\tmw.track( 'counter.MediaWiki.cx.campaign.' + campaign + '.accept', 1 );\n\t\t} );\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/eventlogging/ext.cx.eventlogging.translation.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"no-new","severity":2,"message":"Do not use 'new' for side effects.","line":109,"column":3,"nodeType":"ExpressionStatement","messageId":"noNewStatement","endLine":109,"endColumn":40,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation event logging for translation view.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n\n( function () {\n\t'use strict';\n\n\t/**\n\t * ContentTranslation event logger\n\t */\n\tfunction ContentTranslationEventLogging() {\n\t\tthis.listen();\n\t}\n\n\tContentTranslationEventLogging.prototype = {\n\t\t/**\n\t\t * Listen for event logging.\n\t\t */\n\t\tlisten: function () {\n\t\t\t// Register handlers for event logging triggers\n\t\t\tmw.hook( 'mw.cx.translation.published' ).add( this.published );\n\t\t\tmw.hook( 'mw.cx.translation.publish.error' ).add( this.publishFailed.bind( this ) );\n\t\t\tmw.hook( 'mw.cx.translation.abusefilter' ).add( this.logAbuseFilter );\n\t\t},\n\n\t\t/**\n\t\t * Count creation of translated page.\n\t\t */\n\t\tpublished: function () {\n\t\t\tmw.track( 'counter.MediaWiki.cx.publish.success', 1 );\n\t\t},\n\n\t\t/**\n\t\t * Log publish failures\n\t\t *\n\t\t * @param {string} sourceLanguage source language\n\t\t * @param {string} targetLanguage Target language code\n\t\t * @param {string} sourceTitle Source title\n\t\t * @param {string} targetTitle Target title\n\t\t * @param {Object} trace Error trace\n\t\t */\n\t\tpublishFailed: function ( sourceLanguage, targetLanguage, sourceTitle, targetTitle, trace ) {\n\t\t\tmw.track( 'counter.MediaWiki.cx.publish.fail', 1 );\n\t\t\tthis.handleAbuseFilter( sourceLanguage, targetLanguage, sourceTitle, targetTitle, trace, 'publishing' );\n\t\t},\n\n\t\t/**\n\t\t * Log AbuseFilter event.\n\t\t *\n\t\t * @param {string} sourceLanguage Source language code\n\t\t * @param {string} targetLanguage Target language code\n\t\t * @param {string} sourceTitle Source title\n\t\t * @param {string} targetTitle Target title\n\t\t * @param {string} context \"saving\" or \"publishing\"\n\t\t * @param {string} filterType Filter type: \"warn\", \"disallow\", etc.\n\t\t * @param {string} filterId Filter number, as on Special:AbuseFilter\n\t\t */\n\t\tlogAbuseFilter: function ( sourceLanguage, targetLanguage, sourceTitle, targetTitle, context, filterType, filterId ) {\n\t\t\tmw.track( 'event.ContentTranslationAbuseFilter', {\n\t\t\t\ttoken: mw.user.id(),\n\t\t\t\tcontext: context,\n\t\t\t\tsourceLanguage: sourceLanguage,\n\t\t\t\ttargetLanguage: targetLanguage,\n\t\t\t\tsourceTitle: sourceTitle,\n\t\t\t\ttargetTitle: targetTitle,\n\t\t\t\tcxVersion: mw.cx.getCXVersion(),\n\t\t\t\tfilterType: filterType,\n\t\t\t\tfilterId: parseInt( filterId )\n\t\t\t} );\n\t\t},\n\n\t\t/**\n\t\t * Check whether the event is an AbuseFilter event, and log it if needed.\n\t\t *\n\t\t * @param {string} sourceLanguage Source language code\n\t\t * @param {string} targetLanguage Target language code\n\t\t * @param {string} sourceTitle Source title\n\t\t * @param {string} targetTitle Target title\n\t\t * @param {Object} trace The event trace, possibly describing an AbuseFilter response\n\t\t * @param {string} context \"saving\" or \"publishing\"\n\t\t */\n\t\thandleAbuseFilter: function ( sourceLanguage, targetLanguage, sourceTitle, targetTitle, trace, context ) {\n\t\t\tvar abuseFilterCodes = [ 'abusefilter-warning', 'abusefilter-disallowed' ];\n\n\t\t\tif ( trace &&\n\t\t\t\ttrace.error &&\n\t\t\t\ttrace.error.code &&\n\t\t\t\tabuseFilterCodes.indexOf( trace.error.code ) > -1 &&\n\t\t\t\ttrace.error.abusefilter\n\t\t\t) {\n\t\t\t\tthis.logAbuseFilter(\n\t\t\t\t\tsourceLanguage,\n\t\t\t\t\ttargetLanguage,\n\t\t\t\t\tsourceTitle,\n\t\t\t\t\ttargetTitle,\n\t\t\t\t\tcontext,\n\t\t\t\t\ttrace.error.code,\n\t\t\t\t\ttrace.error.abusefilter.id\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t};\n\n\t$( function () {\n\t\t// eslint-disable-next-line no-new\n\t\tnew ContentTranslationEventLogging();\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/eventlogging/legacy/ext.cx.eventlogging.translation.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"no-new","severity":2,"message":"Do not use 'new' for side effects.","line":145,"column":3,"nodeType":"ExpressionStatement","messageId":"noNewStatement","endLine":145,"endColumn":40,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation event logging for translation view.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n\n( function () {\n\t'use strict';\n\n\tvar saveCount = 0;\n\t/**\n\t * ContentTranslation event logger\n\t */\n\tfunction ContentTranslationEventLogging() {\n\t\tthis.listen();\n\t}\n\n\tContentTranslationEventLogging.prototype = {\n\t\t/**\n\t\t * Listen for event logging.\n\t\t */\n\t\tlisten: function () {\n\t\t\t// Register handlers for event logging triggers\n\t\t\tmw.hook( 'mw.cx.translation.published' ).add( this.published );\n\t\t\tmw.hook( 'mw.cx.translation.publish.error' ).add( this.publishFailed.bind( this ) );\n\t\t\tmw.hook( 'mw.cx.translation.saved' ).add( this.saved );\n\t\t\tmw.hook( 'mw.cx.translation.save-failed' ).add( this.saveFailed );\n\t\t\tmw.hook( 'mw.cx.translation.continued' ).add( this.continued );\n\t\t\tmw.hook( 'mw.cx.draft.restore-failed' ).add( this.restoreFailed );\n\t\t},\n\n\t\t/**\n\t\t * Count creation of translated page.\n\t\t */\n\t\tpublished: function () {\n\t\t\tmw.track( 'counter.MediaWiki.cx.publish.success', 1 );\n\t\t},\n\n\t\t/**\n\t\t * Log publish failures\n\t\t *\n\t\t * @param {string} sourceLanguage source language\n\t\t * @param {string} targetLanguage Target language code\n\t\t * @param {string} sourceTitle Source title\n\t\t * @param {string} targetTitle Target title\n\t\t * @param {Object} trace Error trace\n\t\t */\n\t\tpublishFailed: function ( sourceLanguage, targetLanguage, sourceTitle, targetTitle, trace ) {\n\t\t\tmw.track( 'counter.MediaWiki.cx.publish.fail', 1 );\n\t\t\tthis.handleAbuseFilter( sourceLanguage, targetLanguage, sourceTitle, targetTitle, trace, 'publishing' );\n\t\t},\n\n\t\t/**\n\t\t * Count save failures\n\t\t */\n\t\tsaveFailed: function () {\n\t\t\tmw.track( 'counter.MediaWiki.cx.save.fail', 1 );\n\t\t},\n\n\t\t/**\n\t\t * Count translation restore failures\n\t\t */\n\t\trestoreFailed: function () {\n\t\t\tmw.track( 'counter.MediaWiki.cx.restore.fail', 1 );\n\t\t},\n\n\t\t/**\n\t\t * Count saving (draft) of translated page.\n\t\t */\n\t\tsaved: function () {\n\t\t\tif ( saveCount ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsaveCount++;\n\t\t\tmw.track( 'counter.MediaWiki.cx.save.success', 1 );\n\t\t},\n\n\t\t/**\n\t\t * Count continuing translation.\n\t\t */\n\t\tcontinued: function () {\n\t\t\tmw.track( 'counter.MediaWiki.cx.restore.success', 1 );\n\t\t},\n\n\t\t/**\n\t\t * Log AbuseFilter event.\n\t\t *\n\t\t * @param {string} sourceLanguage Source language code\n\t\t * @param {string} targetLanguage Target language code\n\t\t * @param {string} sourceTitle Source title\n\t\t * @param {string} targetTitle Target title\n\t\t * @param {string} context \"saving\" or \"publishing\"\n\t\t * @param {string} filterType Filter type: \"warn\", \"disallow\", etc.\n\t\t * @param {string} filterId Filter number, as on Special:AbuseFilter\n\t\t */\n\t\tlogAbuseFilter: function ( sourceLanguage, targetLanguage, sourceTitle, targetTitle, context, filterType, filterId ) {\n\t\t\tmw.track( 'event.ContentTranslationAbuseFilter', {\n\t\t\t\ttoken: mw.user.id(),\n\t\t\t\tcontext: context,\n\t\t\t\tsourceLanguage: sourceLanguage,\n\t\t\t\ttargetLanguage: targetLanguage,\n\t\t\t\tsourceTitle: sourceTitle,\n\t\t\t\ttargetTitle: targetTitle,\n\t\t\t\tcxVersion: mw.cx.getCXVersion(),\n\t\t\t\tfilterType: filterType,\n\t\t\t\tfilterId: parseInt( filterId )\n\t\t\t} );\n\t\t},\n\n\t\t/**\n\t\t * Check whether the event is an AbuseFilter event, and log it if needed.\n\t\t *\n\t\t * @param {string} sourceLanguage Source language code\n\t\t * @param {string} targetLanguage Target language code\n\t\t * @param {string} sourceTitle Source title\n\t\t * @param {string} targetTitle Target title\n\t\t * @param {Object} trace The event trace, possibly describing an AbuseFilter response\n\t\t * @param {string} context \"saving\" or \"publishing\"\n\t\t */\n\t\thandleAbuseFilter: function ( sourceLanguage, targetLanguage, sourceTitle, targetTitle, trace, context ) {\n\t\t\tvar abuseFilterCodes = [ 'abusefilter-warning', 'abusefilter-disallowed' ];\n\n\t\t\tif ( trace &&\n\t\t\t\ttrace.error &&\n\t\t\t\ttrace.error.code &&\n\t\t\t\tabuseFilterCodes.indexOf( trace.error.code ) > -1 &&\n\t\t\t\ttrace.error.abusefilter\n\t\t\t) {\n\t\t\t\tthis.logAbuseFilter(\n\t\t\t\t\tsourceLanguage,\n\t\t\t\t\ttargetLanguage,\n\t\t\t\t\tsourceTitle,\n\t\t\t\t\ttargetTitle,\n\t\t\t\t\tcontext,\n\t\t\t\t\ttrace.error.code,\n\t\t\t\t\ttrace.error.abusefilter.id\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t};\n\n\t$( function () {\n\t\t// eslint-disable-next-line no-new\n\t\tnew ContentTranslationEventLogging();\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.MachineTranslationManager.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * This class takes care of managing which machine translation service to use.\n * Basically what happens when user clicks a section to translate, or uses the\n * machine translation tool card to change provider.\n *\n * Here provides means either a named machine translation services that is used\n * via the cxserver backend using the mw.cx.MachineTranslationService class or\n * an always present option `scratch` or `source`. By convention named providers\n * should start with a capital letter.\n */\n\n'use strict';\n\n/**\n * @class\n * @param {string} sourceLanguage Language code\n * @param {string} targetLanguage Language code\n * @param {mw.cx.MachineTranslationService} MTService\n */\nmw.cx.MachineTranslationManager = function MwCxMachineTranslationManager(\n\tsourceLanguage, targetLanguage, MTService\n) {\n\tthis.sourceLanguage = sourceLanguage;\n\tthis.targetLanguage = targetLanguage;\n\tthis.MT = MTService;\n};\n\n/**\n * Map provider id to human readable label.\n *\n * @param {string} provider Id of the provider\n * @return {string} Translated label\n */\nmw.cx.MachineTranslationManager.prototype.getProviderLabel = function ( provider ) {\n\treturn mw.msg.apply( null, {\n\t\tElia: [ 'cx-tools-mt-provider-title', 'Elia.eus' ],\n\t\tFlores: [ 'cx-tools-mt-provider-title', 'NLLB-200' ],\n\t\tGoogle: [ 'cx-tools-mt-provider-title', 'Google Translate' ],\n\t\tYandex: [ 'cx-tools-mt-provider-title', 'Yandex.Translate' ],\n\t\tscratch: [ 'cx-tools-mt-dont-use' ],\n\t\tsource: [ 'cx-tools-mt-use-source' ],\n\t\treset: [ 'cx-tools-mt-reset' ]\n\t}[ provider ] || [ 'cx-tools-mt-provider-title', provider ] );\n};\n\n/* Public methods */\n\n/**\n * Get the preferred provider, also taking into account user preference.\n *\n * @return {jQuery.Promise}\n */\nmw.cx.MachineTranslationManager.prototype.getPreferredProvider = function () {\n\tvar\n\t\tkey = this.getStorageKey(),\n\t\tvalue = mw.storage.get( key );\n\n\treturn this.getAvailableProviders().then( function ( providers ) {\n\t\tif ( value && providers.indexOf( value ) >= 0 ) {\n\t\t\treturn value;\n\t\t}\n\n\t\t// Stored provider is invalid or not available right now\n\t\treturn this.getDefaultProvider();\n\t}.bind( this ) );\n\n};\n\nmw.cx.MachineTranslationManager.prototype.setPreferredProvider = function ( value ) {\n\tvar key = this.getStorageKey();\n\n\tmw.storage.set( key, value );\n};\n\nmw.cx.MachineTranslationManager.prototype.getAvailableProviders = function () {\n\treturn this.MT.getProviders().then(\n\t\tfunction ( providers ) {\n\t\t\treturn providers.concat( [ 'source', 'scratch' ] );\n\t\t},\n\t\tfunction () {\n\t\t\t// Allow to continue translation even if this fails\n\t\t\treturn $.Deferred().resolve( [ 'source', 'scratch' ] );\n\t\t}\n\t);\n};\n\n/* Private methods */\n\n/**\n * Determines whether `source` or `scratch` should be used. Since mixing\n * left-to-right and right-to-left is complex and confusing, default to\n * `scratch` translation if directions are different.\n *\n * @return {jQuery.Promise} Resolves to provider id.\n */\nmw.cx.MachineTranslationManager.prototype.getDefaultNonMTProvider = function () {\n\treturn mw.loader.using( 'jquery.uls.data' ).then(\n\t\tfunction () {\n\t\t\tvar\n\t\t\t\tsourceDir = $.uls.data.getDir( this.sourceLanguage ),\n\t\t\t\ttargetDir = $.uls.data.getDir( this.targetLanguage );\n\n\t\t\treturn sourceDir === targetDir ? 'source' : 'scratch';\n\t\t}.bind( this ),\n\t\tfunction () {\n\t\t\t// Convert failure to success\n\t\t\treturn $.Deferred().resolve( 'source' ).promise();\n\t\t}\n\t);\n};\n\n/**\n * Get the default MT provider.\n *\n * @return {jQuery.Promise} Resolves to a provider id.\n */\nmw.cx.MachineTranslationManager.prototype.getDefaultProvider = function () {\n\treturn this.MT.getSuggestedDefaultProvider().then(\n\t\tfunction ( provider ) {\n\t\t\treturn provider || this.getDefaultNonMTProvider();\n\t\t}.bind( this ),\n\t\tfunction () {\n\t\t\treturn this.getDefaultNonMTProvider();\n\t\t}.bind( this )\n\t);\n};\n\nmw.cx.MachineTranslationManager.prototype.getStorageKey = function () {\n\t// This format was used by CX1, so keeping it for compatibility.\n\treturn [ 'cxMTProvider', this.sourceLanguage, this.targetLanguage ].join( '-' );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.MachineTranslationService.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * This class handles translating text and HTML using cxserver while taking\n * care of creating the requests, authorization etc.\n */\n\n'use strict';\n\n/**\n * @class\n * @param {string} sourceLanguage Language code\n * @param {string} targetLanguage Language code\n * @param {mw.cx.SiteMapper} siteMapper\n */\nmw.cx.MachineTranslationService = function MwCxMachineTranslationService(\n\tsourceLanguage, targetLanguage, siteMapper\n) {\n\tthis.sourceLanguage = sourceLanguage;\n\tthis.targetLanguage = targetLanguage;\n\tthis.siteMapper = siteMapper;\n\n\tthis.providers = null;\n};\n\n/* Public methods */\n\n/**\n * Translate a piece of text. The main entry point for this class.\n *\n * @param {string} content HTML to translate.\n * @param {string} provider Which provider to use.\n * @return {jQuery.Promise} Returns the translated HTML as a string.\n */\nmw.cx.MachineTranslationService.prototype.translate = function ( content, provider ) {\n\tif ( provider === 'source' ) {\n\t\t// Adapt without translation.\n\t\treturn this.fetchTranslation( content );\n\t} else if ( provider === 'scratch' ) {\n\t\treturn $.Deferred().resolve( this.prepareContentForScratch( content ) );\n\t}\n\n\treturn this.getCXServerToken().then( this.fetchTranslation.bind( this, content, provider ) );\n};\n\n/**\n * Given a source page title, this method sends a request to the cxserver,\n * and more specifically to the /suggest/title endpoint, and it returns\n * the \"targetTitle\" property of the response as suggested target title\n *\n * @param {string} title Title to translate.\n * @return {jQuery.Promise} Returns the suggested title\n */\nmw.cx.MachineTranslationService.prototype.getSuggestedTitle = function ( title ) {\n\tvar mtURL = this.siteMapper.getCXServerUrl( '/suggest/title/$title/$from/$to', {\n\t\t$title: title,\n\t\t$from: this.sourceLanguage,\n\t\t$to: this.targetLanguage\n\t} );\n\n\tvar fetchTitleSuggestion = function ( token ) {\n\t\tvar request = {\n\t\t\ttype: 'get',\n\t\t\turl: mtURL,\n\t\t\theaders: { Authorization: token }\n\t\t};\n\n\t\treturn $.ajax( request ).then( function ( response ) {\n\t\t\treturn response.targetTitle;\n\t\t} );\n\t};\n\n\treturn this.getCXServerToken().then( fetchTitleSuggestion );\n};\n\n/**\n * Surgically empty a piece of content to enable translation from scratch.\n *\n * @param {string} content HTML\n * @return {string} HTML\n */\nmw.cx.MachineTranslationService.prototype.prepareContentForScratch = function ( content ) {\n\tvar $content = $( $.parseHTML( content ) );\n\t$content.children().each( function () {\n\t\tif ( $( this ).is( 'p, h1, h2, h3, h4, h5, h6' ) ) {\n\t\t\t$( this ).empty();\n\t\t} else {\n\t\t\t$( this ).remove();\n\t\t}\n\t} );\n\n\tif ( !$content.children().length ) {\n\t\t$content.append( $( '<p>' ) );\n\t}\n\n\treturn $content.prop( 'outerHTML' );\n};\n\n/**\n * Get a list of available machine translation providers.\n *\n * @return {jQuery.Promise}\n */\nmw.cx.MachineTranslationService.prototype.getProviders = function () {\n\treturn this.getProvidersCached().then( function ( providers ) {\n\t\treturn providers.filter( function ( item ) {\n\t\t\treturn item !== 'source-mt';\n\t\t} );\n\t} );\n};\n\nmw.cx.MachineTranslationService.prototype.getSuggestedDefaultProvider = function () {\n\treturn this.getProvidersCached().then( function ( providers ) {\n\t\tif ( providers.length === 0 || providers[ 0 ] === 'source-mt' ) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn providers[ 0 ];\n\t} );\n};\n\n/* Private methods */\n\nmw.cx.MachineTranslationService.prototype.getProvidersCached = function () {\n\tif ( this.providers !== null ) {\n\t\treturn $.Deferred().resolve( this.providers );\n\t}\n\n\treturn this.fetchProviders()\n\t\t.fail( this.fetchProvidersError.bind( this ) )\n\t\t.done( function ( providers ) {\n\t\t\tthis.providers = providers;\n\t\t}.bind( this ) );\n};\n\n/**\n * Fetch available providers from cxserver.\n *\n * @private\n * @return {jQuery.Promise}\n */\nmw.cx.MachineTranslationService.prototype.fetchProviders = function () {\n\tif ( mw.config.get( 'wgContentTranslationEnableMT' ) === false ) {\n\t\t// MT services are not enabled for this wiki.\n\t\treturn $.Deferred().resolve( [] );\n\t}\n\n\tvar fetchProvidersUrl = this.siteMapper.getCXServerUrl( '/list/mt/$from/$to', {\n\t\t$from: this.sourceLanguage,\n\t\t$to: this.targetLanguage\n\t} );\n\n\treturn $.get( fetchProvidersUrl ).then( function ( response ) {\n\t\treturn response.mt || [];\n\t} );\n};\n\nmw.cx.MachineTranslationService.prototype.fetchProvidersError = function () {\n\tmw.hook( 'mw.cx.error' ).fire( 'Unable to fetch machine translation providers.' );\n\tmw.log.error( '[CX]', 'Unable to fetch machine translation providers.', arguments );\n};\n\nmw.cx.MachineTranslationService.prototype.fetchCXServerToken = function () {\n\treturn new mw.Api().postWithToken( 'csrf', {\n\t\taction: 'cxtoken',\n\t\tassert: 'user'\n\t} );\n};\n\nmw.cx.MachineTranslationService.prototype.getCXServerToken = function () {\n\tthis.tokenPromise = this.tokenPromise ||\n\t\tthis.fetchCXServerToken().then(\n\t\t\tfunction ( token ) {\n\t\t\t\tvar now = Math.floor( Date.now() / 1000 );\n\t\t\t\t// We use `age` instead of `exp` because it is more reliable, as user's\n\t\t\t\t// clocks might be set to wrong time.\n\t\t\t\ttoken.refreshAt = now + token.age - 30;\n\t\t\t\treturn token;\n\t\t\t},\n\t\t\tfunction ( errorCode, errorObj ) {\n\t\t\t\tif ( errorCode === 'token-impossible' ) {\n\t\t\t\t\t// Likely CX extension has not been configured properly.\n\t\t\t\t\t// To make development and testing easier, assume that\n\t\t\t\t\t// no token is needed.\n\t\t\t\t\tmw.log.warn( '[CX] Unable to get cxserver token (ignored).', errorObj );\n\t\t\t\t\treturn $.Deferred().resolve( {} ).promise();\n\t\t\t\t}\n\t\t\t\tmw.hook( 'mw.cx.error' ).fire( 'Unable to fetch machine translation token.' );\n\t\t\t\tmw.log.error( '[CX] Unable to get cxserver token.', errorObj );\n\t\t\t}\n\t\t);\n\n\treturn this.tokenPromise.then( function ( token ) {\n\t\tvar now = Math.floor( Date.now() / 1000 );\n\t\tif ( 'refreshAt' in token && token.refreshAt <= now ) {\n\t\t\tthis.tokenPromise = undefined;\n\t\t\treturn this.getCXServerToken();\n\t\t}\n\n\t\t// Return the cached token\n\t\treturn token.jwt || '';\n\t}.bind( this ) );\n};\n\n/**\n * Calls cxserver to do the translation.\n *\n * @private\n * @param {string} content HTML to translate.\n * @param {string} [provider] Provider to use. If not given,\n *  content is adapted without machine translation.\n * @param {string} [token] Authorization token. Required only when the provider needs it.\n * @return {jQuery.Promise} Returns the translated HTML as a string.\n */\nmw.cx.MachineTranslationService.prototype.fetchTranslation = function ( content, provider, token ) {\n\tvar mtURL = this.siteMapper.getCXServerUrl( '/translate/$from/$to/$provider', {\n\t\t$from: this.sourceLanguage,\n\t\t$to: this.targetLanguage,\n\t\t$provider: provider || ''\n\t} );\n\n\tvar request = {\n\t\ttype: 'post',\n\t\turl: mtURL,\n\t\tdata: {\n\t\t\thtml: content\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: token\n\t\t}\n\t};\n\n\treturn $.ajax( request ).then( function ( response ) {\n\t\treturn response.contents;\n\t} );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.MwApiRequestManager.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * ContentTranslation MediaWiki API request manager class.\n *\n * This class abstracts API requests to MediaWiki instance for title, image,\n * template and such information. The caching of such requests is taken care.\n * Pagination, request queues are also implemented. The requests will be created\n * using SiteMapper so that source wiki, target wiki API end points are correctly\n * handled.\n *\n * This abstraction also helps to write unit tests. We can mock the network requests\n * by adding entries to the cache before running tests so that the real network\n * requests won't be initiated.\n */\n\n'use strict';\n\n/**\n * @class\n * @param {string} sourceLanguage Language code\n * @param {string} targetLanguage Language code\n * @param {mw.cx.SiteMapper} siteMapper\n */\nmw.cx.MwApiRequestManager = function MwCxMwApiRequestManager( sourceLanguage, targetLanguage, siteMapper ) {\n\tthis.sourceLanguage = sourceLanguage;\n\tthis.targetLanguage = targetLanguage;\n\tthis.siteMapper = siteMapper;\n\tthis.titlePairCache = {};\n\tthis.categoryCache = {};\n\tthis.namespaceCache = {};\n\tthis.init();\n};\n\n/**\n * Initialize or reset all caches.\n */\nmw.cx.MwApiRequestManager.prototype.init = function () {\n\tthis.titlePairCache[ this.sourceLanguage ] = new mw.cx.TitlePairCache( {\n\t\tsourceLanguage: this.sourceLanguage,\n\t\ttargetLanguage: this.targetLanguage,\n\t\tsiteMapper: this.siteMapper\n\t} );\n\tthis.titlePairCache[ this.targetLanguage ] = new mw.cx.TitlePairCache( {\n\t\tsourceLanguage: this.targetLanguage,\n\t\ttargetLanguage: this.sourceLanguage,\n\t\tsiteMapper: this.siteMapper\n\t} );\n\tthis.categoryCache[ this.sourceLanguage ] = new mw.cx.CategoryCache( {\n\t\tlanguage: this.sourceLanguage,\n\t\tsiteMapper: this.siteMapper\n\t} );\n\tthis.categoryCache[ this.targetLanguage ] = new mw.cx.CategoryCache( {\n\t\tlanguage: this.targetLanguage,\n\t\tsiteMapper: this.siteMapper\n\t} );\n\tthis.namespaceCache[ this.sourceLanguage ] = new mw.cx.NamespaceCache( {\n\t\tlanguage: this.targetLanguage,\n\t\tsiteMapper: this.siteMapper\n\t} );\n\tthis.namespaceCache[ this.targetLanguage ] = new mw.cx.NamespaceCache( {\n\t\tlanguage: this.targetLanguage,\n\t\tsiteMapper: this.siteMapper\n\t} );\n};\n\n/**\n * Look up pairing data about a title. If the data about this title is already in the cache, this\n * returns an already-resolved promise. Otherwise, it returns a pending promise and schedules\n * an request to retrieve the data.\n *\n * @param {string} language Language code\n * @param {string} title Title\n * @return {jQuery.Promise} Promise that will be resolved with the data once it's available\n */\nmw.cx.MwApiRequestManager.prototype.getTitlePair = function ( language, title ) {\n\tif ( !this.titlePairCache[ language ] ) {\n\t\tthrow new Error( '[CX] TitlePairCache not initialized for ' + language );\n\t}\n\treturn this.titlePairCache[ language ].get( title );\n};\n\n/**\n * @param {string} language Language code\n * @param {string} title Title\n * @return {jQuery.Promise} Promise that will be resolved with the data once it's available\n */\nmw.cx.MwApiRequestManager.prototype.getCategories = function ( language, title ) {\n\tif ( !this.categoryCache[ language ] ) {\n\t\tthrow new Error( '[CX] CategoryCache not initialized for ' + language );\n\t}\n\treturn this.categoryCache[ language ].get( title );\n};\n\n/**\n * @param {string} language Language code\n * @param {string} canonicalNamespace Canonical namespace\n * @return {jQuery.Promise} Promise that will be resolved with the data once it's available\n */\nmw.cx.MwApiRequestManager.prototype.getNamespaceAlias = function ( language, canonicalNamespace ) {\n\tif ( !this.namespaceCache[ language ] ) {\n\t\tthrow new Error( '[CX] namespaceCache not initialized for ' + language );\n\t}\n\treturn this.namespaceCache[ language ].get( canonicalNamespace );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.TargetArticle.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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 * @param {mw.cx.SiteMapper} config.siteMapper SiteMapper instance\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 = translation.getSourceTitle();\n\tthis.sourceLanguage = translation.getSourceLanguage();\n\tthis.targetLanguage = translation.getTargetLanguage();\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 pathological transclusions if any. Transclusion without any definition can cause\n\t// Parsoid errors and hence failing the whole publishing workflow.\n\t// Example `<span typeof=\"mw:Transclusion\" data-mw=\"{}\" data-cx=\"[{&quot;adapted&quot;:false}]\" id=\"mwCH0\"></span>`\n\t// These are not necessarily generated by CX adaptation, but resulted form editing workflow.\n\tArray.prototype.forEach.call( doc.querySelectorAll( '[typeof=\"mw:Transclusion\"]' ), function ( element ) {\n\t\tvar dataMw = JSON.parse( element.getAttribute( 'data-mw' ) || '{}' );\n\n\t\tif ( !dataMw.parts ) {\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\tthis.getContent( true ).then( function ( html ) {\n\t\tvar apiParams = {\n\t\t\tassert: 'user',\n\t\t\taction: 'cxpublish',\n\t\t\tfrom: this.sourceLanguage,\n\t\t\tto: this.targetLanguage,\n\t\t\tsourcetitle: this.sourceTitle,\n\t\t\ttitle: this.getTargetTitle(),\n\t\t\thtml: html,\n\t\t\tcategories: this.getTargetCategories( shouldAddHighMTCategory ),\n\t\t\tpublishtags: this.getTags(),\n\t\t\twpCaptchaId: this.captcha && this.captcha.id,\n\t\t\twpCaptchaWord: this.captcha && this.captcha.input.getValue(),\n\t\t\tcxversion: 2\n\t\t};\n\n\t\t// Check for title conflicts\n\t\tthis.checkForPublishAnyway( this.getTargetTitle(), hasIssues ).then( function () {\n\t\t\treturn new mw.Api().postWithToken( 'csrf', apiParams, {\n\t\t\t\t// A bigger timeout since publishing after converting html to wikitext\n\t\t\t\t// parsoid is not a fast operation.\n\t\t\t\ttimeout: 100 * 1000 // in milliseconds\n\t\t\t} ).then( this.publishSuccess.bind( this ), this.publishFail.bind( this ) );\n\t\t}.bind( this ), function () {\n\t\t\tthis.emit( 'publishCancel' );\n\t\t}.bind( this ) );\n\t}.bind( this ) );\n};\n\n/**\n * Publish the translated section to target wiki\n */\nmw.cx.TargetArticle.prototype.publishSection = function () {\n\tthis.getContent( false ).then( function ( html ) {\n\t\tvar isSandbox = this.veTarget.getPublishNamespace() === mw.config.get( 'wgNamespaceIds' ).user;\n\t\tvar params = {\n\t\t\taction: 'cxpublishsection',\n\t\t\ttitle: this.getTargetTitle(),\n\t\t\thtml: html,\n\t\t\tsourcetitle: this.sourceTitle,\n\t\t\tsourcerevid: this.translation.sourceWikiPage.getRevision(),\n\t\t\tsourcesectiontitle: this.translation.sourceWikiPage.getSectionTitle(),\n\t\t\ttargetsectiontitle: this.veTarget.translationView.targetColumn.getTitle(),\n\t\t\tsourcelanguage: this.sourceLanguage,\n\t\t\ttargetlanguage: this.targetLanguage,\n\t\t\tissandbox: isSandbox\n\t\t};\n\n\t\tif ( this.captcha ) {\n\t\t\tparams.captchaid = this.captcha.id;\n\t\t\tparams.captchaword = this.captcha.input.getValue();\n\t\t}\n\n\t\treturn new mw.Api()\n\t\t\t.postWithToken( 'csrf', params )\n\t\t\t.then( this.publishSuccess.bind( this ), this.publishFail.bind( this ) );\n\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 publishAction = this.translation.isSectionTranslation() ? 'cxpublishsection' : 'cxpublish';\n\tvar publishResult = response[ publishAction ];\n\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\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\tvar editError = 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.msg( 'title-invalid-characters', this.getTargetTitle() ),\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\tvar editResult = 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\tvar publishAction = this.translation.isSectionTranslation() ? 'publishSection' : 'publish';\n\n\tthis.captchaDialog.connect( this, {\n\t\tpublish: publishAction,\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 errorMsg = ( 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\tvar errorDetails = 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 {jQuery.Promise} Promise which resolves with content for publishing, may be deflated\n */\nmw.cx.TargetArticle.prototype.getContent = function ( deflate ) {\n\tvar doc = this.veTarget.getSurface().getDom();\n\tvar cleanupHtml = mw.libs.ve.targetSaver.getHtml( this.constructor.static.getCleanedupContent( doc ) );\n\n\tif ( deflate ) {\n\t\treturn mw.loader.using( 'mediawiki.deflate' ).then( function () {\n\t\t\treturn mw.deflate( cleanupHtml );\n\t\t} );\n\t} else {\n\t\treturn $.Deferred().resolve( cleanupHtml ).promise();\n\t}\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} pageTitle The title to check\n * @param {boolean} hasIssues Whether the translation has issues\n * @return {jQuery.Promise}\n */\nmw.cx.TargetArticle.prototype.checkForPublishAnyway = function ( pageTitle, 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( pageTitle ).then( function ( result ) {\n\t\tvar targetExists = !result.missing;\n\n\t\tvar title, message;\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 maintenanceCategoryMsg = 'cx-unreviewed-translation-category';\n\n\tvar targetCategories = this.translation.getTargetCategories();\n\tvar index = 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":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * CX Translation - save, fetch controller\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 {mw.cx.SiteMapper} siteMapper\n * @param {Object} [config] Configuration for mw.cx.TargetArticle\n */\nmw.cx.TranslationController = function MwCxTranslationController(\n\ttranslation, veTarget, siteMapper, config\n) {\n\tthis.translation = translation;\n\tthis.veTarget = veTarget;\n\tthis.siteMapper = siteMapper;\n\tthis.translationView = this.veTarget.translationView;\n\n\t// Mixin constructors\n\tOO.EventEmitter.call( this );\n\n\tthis.hasDeletedTranslations = null;\n\n\t// Properties\n\tthis.saveRequest = null;\n\tthis.failCounter = 0;\n\tthis.isFailedUnrecoverably = false; // TODO: This is still unused\n\tthis.mtAbusePublishingStopped = false;\n\tthis.saveStatusTimer = null;\n\tthis.retryTimer = null;\n\tthis.loginDialog = null;\n\tthis.sourceCategoriesSaved = false;\n\tthis.targetCategoriesChanged = 0;\n\tthis.savedTargetTitle = this.translation.getTargetTitle();\n\tthis.targetArticle = new mw.cx.TargetArticle( this.translation, this.veTarget, {\n\t\tsiteMapper: this.siteMapper\n\t} );\n\tthis.translationTracker = new mw.cx.TranslationTracker( this.veTarget, config );\n\tthis.saveScheduler = OO.ui.debounce( this.processSaveQueue.bind( this ), 5 * 1000 );\n\t// See also ve.ui.CXResetSectionTool that depends on the timeout value\n\tthis.changeTrackerScheduler = OO.ui.debounce( this.processChangeQueue.bind( this ), 100 );\n\n\t// Events\n\tthis.listen();\n};\n\n/* Inheritance */\n\nOO.mixinClass( mw.cx.TranslationController, OO.EventEmitter );\n\n/* Methods */\n\nmw.cx.TranslationController.prototype.listen = function () {\n\tthis.translation.connect( this, {\n\t\ttargetCategoriesChange: 'onTargetCategoriesChange',\n\t\tissuesResolved: 'onIssuesResolved',\n\t\ttranslationIssues: 'onTranslationIssues',\n\t\tsectionChange: 'onSectionChange'\n\t} );\n\n\tthis.veTarget.connect( this, {\n\t\tsurfaceReady: 'onSurfaceReady',\n\t\tpublish: 'publish',\n\t\ttargetTitleChange: 'onTargetTitleChange', // emitted only for article translations\n\t\ttargetSectionTitleChange: 'onTargetSectionTitleChange' // emitted only for section translations\n\t} );\n\n\tthis.targetArticle.connect( this, {\n\t\tcaptchaCancel: 'onPublishCancel',\n\t\tpublishCancel: 'onPublishCancel',\n\t\tpublishSuccess: 'onPublishSuccess',\n\t\tpublishError: 'onPublishFailure'\n\t} );\n\n\t// Save when CTRL+S is pressed.\n\t// TODO: This should use VE's Trigger/Command system, and be registered with the help dialog\n\tdocument.onkeydown = function ( e ) {\n\t\t// See https://medium.engineering/the-curious-case-of-disappearing-polish-s-fa398313d4df\n\t\tif ( ( e.metaKey || e.ctrlKey && !e.altKey ) && e.which === 83 ) {\n\t\t\tthis.processSaveQueue();\n\t\t\treturn false;\n\t\t}\n\t}.bind( this );\n\n\twindow.onbeforeunload = this.onPageUnload.bind( this );\n};\n\n/**\n * Add the section changes to save queue and change queue.\n * These two queues are processed in different interevals and different\n * triggers. Hence two queues.\n *\n * @param {string} sectionId\n */\nmw.cx.TranslationController.prototype.onSectionChange = function ( sectionId ) {\n\tvar sectionNumber = mw.cx.getSectionNumberFromSectionId( sectionId );\n\tthis.translationTracker.pushToChangeQueue( sectionNumber );\n\tthis.translationTracker.pushToSaveQueue( sectionNumber );\n\t// Schedule processing the change and save queues\n\tthis.changeTrackerScheduler();\n\tthis.saveScheduler();\n\n\tif ( this.mtAbusePublishingStopped ) {\n\t\tthis.mtAbusePublishingStopped = false;\n\t\t// Resolve MT abuse error, if it is registered\n\t\tthis.translation.resolveIssueByName( 'mt-abuse-publish' );\n\t}\n};\n\n/**\n * Return true if there are any new changes to be saved.\n *\n * @return {boolean}\n */\nmw.cx.TranslationController.prototype.hasUnsavedChanges = function () {\n\treturn this.translationTracker.getSaveQueue().length ||\n\t\tthis.targetTitleChanged() ||\n\t\tthis.targetCategoriesChanged !== 0;\n};\n\n/**\n * Return true if target title is changed and needs to be saved.\n *\n * @return {boolean}\n */\nmw.cx.TranslationController.prototype.targetTitleChanged = function () {\n\treturn this.savedTargetTitle !== this.translation.getTargetTitle();\n};\n\nmw.cx.TranslationController.prototype.processChangeQueue = function () {\n\tthis.translationTracker.processChangeQueue();\n};\n\n/**\n * Process the save queue. Save the changed translation units.\n *\n * @param {boolean} [isRetry] Whether this is a retry or not.\n */\nmw.cx.TranslationController.prototype.processSaveQueue = function ( isRetry ) {\n\tvar apiOptions = {},\n\t\tapi = new mw.Api();\n\n\t// Before save starts, make sure all changes are processed and section states are\n\t// up to date with latest content.\n\tthis.processChangeQueue();\n\n\tif ( !this.hasUnsavedChanges() ) {\n\t\treturn;\n\t}\n\n\tif ( this.failCounter > 0 && isRetry !== true ) {\n\t\t// Last save failed, and a retry has been scheduled. Don't allow starting new\n\t\t// save requests to avoid overloading the servers, unless this is the retry.\n\t\tmw.log( '[CX] Save request skipped because a retry has been scheduled' );\n\t\treturn;\n\t}\n\n\t// Starting the real save API call.\n\tthis.translationView.setStatusMessage( mw.msg( 'cx-save-draft-saving' ) );\n\n\tif ( this.saveRequest ) {\n\t\tmw.log( '[CX] Aborted active save request' );\n\t\t// This causes failCounter to increase because the in-flight request fails.\n\t\t// The new request we do below will reset the fail counter on success.\n\t\t// If it does not succeed, the retry timer that was set by the failed request\n\t\t// prevents further saves before the retry has completed successfully or given up.\n\t\tthis.saveRequest.abort();\n\t}\n\n\t// Copy the current save queue by value.\n\tvar savedSections = this.translationTracker.getSaveQueue().slice();\n\n\tvar numOfChangedCategories;\n\tthis.getContentToSave( savedSections ).then( function ( content ) {\n\t\tvar params = {\n\t\t\taction: 'cxsave',\n\t\t\tassert: 'user',\n\t\t\tcontent: content,\n\t\t\tfrom: this.translation.getSourceLanguage(),\n\t\t\tto: this.translation.getTargetLanguage(),\n\t\t\tsourcetitle: this.translation.getSourceTitle(),\n\t\t\ttitle: this.translation.getTargetTitle(),\n\t\t\tsourcerevision: this.translation.getSourceRevisionId(),\n\t\t\tprogress: JSON.stringify( this.translationTracker.getTranslationProgress() ),\n\t\t\tcxversion: 2\n\t\t};\n\n\t\tif ( this.targetCategoriesChanged > 0 ) {\n\t\t\t// Use counter for number of changes in target categories which are attempted to be saved.\n\t\t\t// Once saving is successful, that number is subtracted from current number of changes in\n\t\t\t// target categories, which maybe got bigger while first change was being saved.\n\t\t\tnumOfChangedCategories = this.targetCategoriesChanged;\n\t\t\tparams.targetcategories = JSON.stringify( this.translation.getTargetCategories() );\n\n\t\t\t// Only save source categories once per session, the first time user changes target\n\t\t\t// categories. Both source and target categories are saved in cx_corpora table, but\n\t\t\t// only target categories are retrieved when saved draft is being restored. Source\n\t\t\t// categories are saved for completeness of cx_corpora, which pairs source and target.\n\t\t\t// Source categories are saved once per session, because there may have been changes\n\t\t\t// to source categories in the mean time.\n\t\t\tif ( !this.sourceCategoriesSaved ) {\n\t\t\t\tparams.sourcecategories = JSON.stringify( this.translation.getSourceCategories() );\n\t\t\t}\n\t\t}\n\n\t\tif ( this.failCounter > 0 ) {\n\t\t\tmw.log( '[CX] Retrying to save the translation. Failed ' + this.failCounter + ' times so far.' );\n\t\t}\n\n\t\tif ( isRetry ) {\n\t\t\t// Default timeout is 30s. Double it while retrying to increase the chance for success.\n\t\t\tapiOptions = { timeout: 60 * 1000 };\n\t\t}\n\n\t\tthis.saveRequest = api.postWithToken( 'csrf', params, apiOptions )\n\t\t\t.done( function ( saveResult ) {\n\t\t\t\tthis.onSaveComplete( savedSections, saveResult );\n\n\t\t\t\tif ( this.targetTitleChanged() ) {\n\t\t\t\t\tmw.log( '[CX] Target title saved.' );\n\t\t\t\t}\n\t\t\t\tthis.savedTargetTitle = params.title;\n\n\t\t\t\tif ( params.sourcecategories ) {\n\t\t\t\t\tthis.sourceCategoriesSaved = true;\n\t\t\t\t}\n\n\t\t\t\tif ( numOfChangedCategories ) {\n\t\t\t\t\tthis.targetCategoriesChanged -= numOfChangedCategories;\n\t\t\t\t}\n\n\t\t\t\t// Remove saved sections from the queue\n\t\t\t\tsavedSections.forEach( function ( sectionNumber ) {\n\t\t\t\t\tthis.translationTracker.removeSectionFromSaveQueue( sectionNumber );\n\t\t\t\t}, this );\n\n\t\t\t\t// Reset fail counter.\n\t\t\t\tif ( this.failCounter > 0 ) {\n\t\t\t\t\tthis.failCounter = 0;\n\t\t\t\t\tmw.log( '[CX] Retry successful. Save succeeded.' );\n\t\t\t\t}\n\n\t\t\t\tthis.emit( 'saveSuccess' );\n\t\t\t}.bind( this ) ).fail( function ( errorCode, details ) {\n\t\t\t\tthis.failCounter++;\n\n\t\t\t\tmw.log.warn( '[CX] Saving Failed. Error code: ' + errorCode );\n\t\t\t\tif ( details.exception !== 'abort' ) {\n\t\t\t\t\tthis.onSaveFailure( errorCode, details );\n\t\t\t\t}\n\n\t\t\t\tif ( this.failCounter > 5 ) {\n\t\t\t\t\t// If there are more than a few errors, stop autosave at timer triggers.\n\t\t\t\t\t// Show a bigger error message at this point.\n\t\t\t\t\tthis.translationView.showMessage( 'error', mw.msg( 'cx-save-draft-error' ) );\n\t\t\t\t\t// This will allow any change to trigger save again\n\t\t\t\t\tthis.failCounter = 0;\n\t\t\t\t\tmw.log.error( '[CX] Saving failed repeatedly. Stopping retries.' );\n\t\t\t\t} else {\n\t\t\t\t\t// Delay in seconds, failCounter is [1,5]\n\t\t\t\t\tvar delay = 60 * this.failCounter;\n\t\t\t\t\t// Schedule retry.\n\t\t\t\t\tthis.retryTimer = setTimeout( this.processSaveQueue.bind( this, true ), delay * 1000 );\n\t\t\t\t\tmw.log( '[CX] Retry scheduled in ' + delay / 60 + ' minutes.' );\n\t\t\t\t}\n\n\t\t\t\tthis.emit( 'saveFailure' );\n\t\t\t}.bind( this ) ).always( function () {\n\t\t\t\tthis.saveRequest = null;\n\t\t\t}.bind( this ) );\n\t}.bind( this ) );\n};\n\n/**\n * Find out if there is any \"dirty\" section translation units.\n * Inform about sections not saved to the user.\n *\n * @return {string|undefined} The message to be shown to the user\n */\nmw.cx.TranslationController.prototype.onPageUnload = function () {\n\tif ( this.hasUnsavedChanges() ) {\n\t\t// Immediately start processing the save queue.\n\t\tthis.processSaveQueue();\n\t\treturn mw.msg( 'cx-warning-unsaved-translation' );\n\t}\n};\n\nmw.cx.TranslationController.prototype.onSaveComplete = function ( savedSections, saveResult ) {\n\tif ( this.targetCategoriesChanged > 0 ) {\n\t\tmw.log( '[CX] Target categories saved.' );\n\t}\n\n\tvar validations = saveResult.cxsave.validations;\n\n\tsavedSections.forEach( function ( sectionNumber ) {\n\t\tvar sectionState = this.translationTracker.getSectionState( sectionNumber );\n\n\t\tif ( this.shouldUnmodifiedMTBeSavedForSection( sectionState ) ) {\n\t\t\tsectionState.markUnmodifiedMTSaved();\n\t\t}\n\t\tif ( !this.isSourceSavedForSection( sectionState ) ) {\n\t\t\tsectionState.markSourceSaved();\n\t\t}\n\n\t\tvar validation = validations[ sectionNumber ];\n\n\t\tif ( !validation ) {\n\t\t\treturn;\n\t\t}\n\t\tvar section = this.veTarget.getTargetSectionNodeFromSectionNumber( sectionNumber );\n\n\t\tif ( section instanceof ve.dm.CXSectionNode ) {\n\t\t\t// Annotate the section with errors, if any.\n\t\t\tthis.onSaveValidation( section, validation );\n\t\t}\n\n\t\tmw.log( '[CX] Section ' + sectionNumber + ' saved.' );\n\t}, this );\n\n\t// Show saved status with a time after last save.\n\tclearInterval( this.saveStatusTimer );\n\tthis.translationView.setStatusMessage( mw.msg( 'cx-save-draft-save-success', 0 ) );\n\n\tvar minutes = 0;\n\tthis.saveStatusTimer = setInterval( function () {\n\t\tif ( this.failCounter > 0 ) {\n\t\t\t// Don't overwrite error message of failure with this timer controlled message.\n\t\t\treturn;\n\t\t}\n\n\t\tminutes++;\n\t\tthis.translationView.setStatusMessage(\n\t\t\tmw.msg( 'cx-save-draft-save-success', mw.language.convertNumber( minutes ) )\n\t\t);\n\t}.bind( this ), 60 * 1000 );\n};\n\nmw.cx.TranslationController.prototype.onSaveFailure = function ( errorCode, details ) {\n\tif ( errorCode === 'assertuserfailed' ) {\n\t\tthis.showLoginDialog();\n\t}\n\n\tif ( details && details.exception instanceof Error ) {\n\t\tdetails.exception = details.exception.toString();\n\t\tdetails.errorCode = errorCode;\n\t}\n\n\tthis.translationView.setStatusMessage( mw.msg( 'cx-save-draft-error' ) );\n};\n\nmw.cx.TranslationController.prototype.showLoginDialog = function () {\n\tmw.loader.using( 'mw.cx.ui.LoginDialog' ).then( function () {\n\t\tvar windowManager = OO.ui.getWindowManager();\n\n\t\tif ( !this.loginDialog ) {\n\t\t\tthis.loginDialog = new mw.cx.ui.LoginDialog();\n\t\t\twindowManager.addWindows( [ this.loginDialog ] );\n\t\t}\n\n\t\tthis.failCounter = 0;\n\t\tclearTimeout( this.retryTimer );\n\n\t\twindowManager\n\t\t\t.openWindow( this.loginDialog.constructor.static.name )\n\t\t\t.closed.then( this.processSaveQueue.bind( this ) );\n\t}.bind( this ) );\n};\n\n/**\n * Validation result handler\n *\n * @param {ve.dm.CXSectionNode} section\n * @param {Object[]} validations\n */\nmw.cx.TranslationController.prototype.onSaveValidation = function ( section, validations ) {\n\tvar counter = 1, results = [],\n\t\thelpLink = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Content_translation/Abuse_filter';\n\n\t// Resolve old issues, so that we don't get duplicates when adding issues to this section\n\tsection.resolveTranslationIssues( 'validation' );\n\n\tvar sectionState = this.translationTracker.getSectionState( section.getSectionNumber() );\n\n\t// If there are no validations, don't proceed\n\tif ( !validations || validations.length === 0 ) {\n\t\tsectionState.hasSaveError = false;\n\t\treturn;\n\t}\n\n\tsectionState.hasSaveError = true;\n\n\tfor ( var id in validations ) {\n\t\tvar validation = validations[ id ];\n\n\t\t// To EventLogging\n\t\tmw.hook( 'mw.cx.translation.abusefilter' ).fire(\n\t\t\tthis.translation.getSourceLanguage(),\n\t\t\tthis.translation.getTargetLanguage(),\n\t\t\tthis.translation.getSourceTitle(),\n\t\t\tthis.translation.getTargetTitle(),\n\t\t\t'saving',\n\t\t\tObject.keys( validation ).sort().join( ',' ), // A filter may have several actions\n\t\t\tid\n\t\t);\n\n\t\tvar message = validation.warn && validation.warn.messageHtml;\n\t\tvar error = validation.disallow;\n\n\t\tif ( message ) {\n\t\t\tresults.push( {\n\t\t\t\tname: 'validation' + counter++,\n\t\t\t\tmessage: message,\n\t\t\t\tmessageInfo: {\n\t\t\t\t\ttitle: mw.msg( 'cx-tools-linter-abuse-filter' ),\n\t\t\t\t\ttype: error ? 'error' : 'warning',\n\t\t\t\t\thelp: helpLink\n\t\t\t\t}\n\t\t\t} );\n\t\t} else if ( error ) {\n\t\t\t// If \"Trigger these actions after giving the user a warning\" is not checked\n\t\t\t// for particular abuse filter rule, we will not have `validation.warn.messageHtml`.\n\t\t\t// But if \"Prevent the user from performing the action in question\" is checked,\n\t\t\t// error should be displayed, even if there is no message.\n\t\t\tresults.push( {\n\t\t\t\tname: 'validation' + counter++,\n\t\t\t\tmessage: mw.msg( 'cx-tools-linter-abuse-filter' ),\n\t\t\t\tmessageInfo: {\n\t\t\t\t\ttype: 'error',\n\t\t\t\t\thelp: helpLink\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\t}\n\n\tif ( results.length > 0 ) {\n\t\tsection.addTranslationIssues( results );\n\t}\n};\n\n/**\n * Get the deflated content to save from save queue.\n *\n * @param {number[]} saveQueue\n * @return {jQuery.Promise} Promise which resolve with deflated content\n */\nmw.cx.TranslationController.prototype.getContentToSave = function ( saveQueue ) {\n\tvar records = [];\n\n\tsaveQueue.forEach( function ( sectionNumber ) {\n\t\tthis.getSectionRecords( sectionNumber ).forEach( function ( data ) {\n\t\t\trecords.push( data );\n\t\t} );\n\t}, this );\n\n\treturn mw.loader.using( 'mediawiki.deflate' ).then( function () {\n\t\treturn mw.deflate( JSON.stringify( records ) );\n\t} );\n};\n\n/**\n * Get the records for saving the translation unit.\n *\n * @param {number} sectionNumber\n * @return {Object[]} Objects to save\n */\nmw.cx.TranslationController.prototype.getSectionRecords = function ( sectionNumber ) {\n\tvar records = [];\n\n\tvar sectionState = this.translationTracker.getSectionState( sectionNumber );\n\n\tif ( !sectionState ) {\n\t\tthrow new Error( 'Attempting to save section ' + sectionNumber + ' having no section state.' );\n\t}\n\n\t// Because validation is computationally heavy and slow operation (server side),\n\t// do not perform validation on every request, unless there is a known validation\n\t// issue that should go away immediately when fixed by the user. Validation means\n\t// checking whether the content matches AbuseFilter rules defined in the target wiki.\n\tvar validate = sectionState.hasSaveError || sectionState.saveCount % 5 === 0 || !sectionState.isModified();\n\n\tvar translationSource = sectionState.getCurrentMTProvider();\n\tvar content;\n\tif ( sectionState.isModified() || translationSource === 'source' || translationSource === 'scratch' ) {\n\t\tcontent = sectionState.getUserTranslation().html;\n\t\tif ( content ) {\n\t\t\trecords.push( {\n\t\t\t\tcontent: content,\n\t\t\t\tsectionId: sectionNumber,\n\t\t\t\tvalidate: validate,\n\t\t\t\torigin: 'user'\n\t\t\t} );\n\t\t\tmw.log( '[CX] Saving user translation for section ' + sectionNumber +\n\t\t\t' [validate:' + validate + ']' );\n\t\t} else {\n\t\t\tthrow new Error( 'Attempting to save section ' + sectionNumber + ' with blank content.' );\n\t\t}\n\t}\n\n\tif ( this.shouldUnmodifiedMTBeSavedForSection( sectionState ) ) {\n\t\tcontent = sectionState.getUnmodifiedMT().html;\n\t\tif ( content ) {\n\t\t\trecords.push( {\n\t\t\t\tcontent: content,\n\t\t\t\tsectionId: sectionNumber,\n\t\t\t\tvalidate: false,\n\t\t\t\torigin: translationSource\n\t\t\t} );\n\t\t\tmw.log( '[CX] Saving unmodified MT for section ' + sectionNumber +\n\t\t\t' [validate:' + validate + ']' );\n\t\t} else {\n\t\t\tthrow new Error( 'Attempting to save section ' + sectionNumber + ' with blank content.' );\n\t\t}\n\t}\n\n\t// Save source sections only once because they do not change.\n\tif ( !this.isSourceSavedForSection( sectionState ) ) {\n\t\trecords.push( {\n\t\t\tcontent: sectionState.getSource().html,\n\t\t\tsectionId: sectionNumber,\n\t\t\t// It makes no sense to validate source sections.\n\t\t\tvalidate: false,\n\t\t\torigin: 'source'\n\t\t} );\n\t\tmw.log( '[CX] Saving source content of section ' + sectionNumber );\n\t}\n\n\tsectionState.saveCount++;\n\n\treturn records;\n};\n\n/**\n * @param {mw.cx.dm.SectionState} sectionState\n * @return {boolean} True if unmodified MT should be saved for section.\n */\nmw.cx.TranslationController.prototype.shouldUnmodifiedMTBeSavedForSection = function ( sectionState ) {\n\treturn !sectionState.getUnmodifiedMT().saved && sectionState.getCurrentMTProvider() !== 'source';\n};\n\n/**\n * @param {mw.cx.dm.SectionState} sectionState\n * @return {boolean} True if source is saved for section.\n */\nmw.cx.TranslationController.prototype.isSourceSavedForSection = function ( sectionState ) {\n\treturn sectionState.isSourceSaved();\n};\n\n/**\n * Publish the translation\n */\nmw.cx.TranslationController.prototype.publish = function () {\n\n\tif ( this.translation.isSectionTranslation() ) {\n\t\treturn this.publishSection();\n\t}\n\n\tvar numOfHighMTSections = this.translationTracker.sectionsWithMTAbuse().length,\n\t\tmtAbuseMsg = this.getMTAbuseMsg( numOfHighMTSections );\n\n\tmw.log( '[CX] Publishing translation...' );\n\n\t// Scroll to the top of the page, so success/fail messages become visible\n\t$( 'html, body' ).animate( { scrollTop: 0 }, 'fast' );\n\n\tif ( mtAbuseMsg instanceof mw.Message ) {\n\t\tthis.translationView.showViewIssuesMessage(\n\t\t\tmw.msg( 'cx-mt-abuse-publish-error' ), 'mt-abuse-publish', 'error'\n\t\t);\n\t\tthis.showMTAbusePublishError( mtAbuseMsg.toString() );\n\t\tthis.onPublishCancel();\n\t\tthis.mtAbusePublishingStopped = true;\n\t\treturn;\n\t}\n\n\t// Disable categories to prevent editing\n\tthis.translationView.categoryUI.disableCategoryUI( true );\n\n\tif ( !this.hasUnsavedChanges() ) {\n\t\tthis.publishArticle( numOfHighMTSections );\n\t\treturn;\n\t}\n\n\t// At this point, there is certainly a scheduled saving about to happen.\n\t// We wait for successful saving, before proceeding with publishing.\n\tthis.once( 'saveSuccess', this.saveBeforePublishingSucceeded.bind( this, numOfHighMTSections ) );\n\tthis.once( 'saveFailure', this.saveBeforePublishingFailed.bind( this ) );\n};\n\n/**\n * Publish the section. Used in section translation mode\n */\nmw.cx.TranslationController.prototype.publishSection = function () {\n\tmw.log( '[CX] Publishing section translation...' );\n\t// Clear the status message\n\tthis.translationView.setStatusMessage( '' );\n\tthis.targetArticle.publishSection();\n};\n\nmw.cx.TranslationController.prototype.showMTAbusePublishError = function ( title ) {\n\tthis.translation.resolveIssueByName( 'mt-abuse-publish' );\n\tthis.translation.addUnattachedIssues( [\n\t\tnew mw.cx.dm.TranslationIssue(\n\t\t\t'mt-abuse-publish', // Issue name\n\t\t\tmw.msg( 'cx-mt-abuse-error-text' ), // message body\n\t\t\t{\n\t\t\t\ttitle: title,\n\t\t\t\ttype: 'error',\n\t\t\t\thelp: 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Content_translation/Translating/Translation_quality'\n\t\t\t}\n\t\t)\n\t] );\n};\n\n/**\n * @param {number} numOfHighMTSections\n */\nmw.cx.TranslationController.prototype.publishArticle = function ( numOfHighMTSections ) {\n\tvar shouldAddHighMTCategory = numOfHighMTSections >= ( this.hasDeletedTranslations ? 1 : 10 );\n\n\t// Clear the status message\n\tthis.translationView.setStatusMessage( '' );\n\tthis.targetArticle.publish( this.translationHasIssues( [ 'title' ] ), shouldAddHighMTCategory );\n};\n\n/**\n * @param {Array} ignore Array of IDs of nodes which should be excluded from issue checking.\n * @return {boolean} True if translation has any non-suppressed issue.\n */\nmw.cx.TranslationController.prototype.translationHasIssues = function ( ignore ) {\n\treturn this.translation.getTranslationIssues().length > 0 ||\n\t\tthis.translationTracker.getNodesWithIssues().some( function ( node ) {\n\t\t\treturn ignore.indexOf( node ) === -1;\n\t\t} );\n};\n\nmw.cx.TranslationController.prototype.saveBeforePublishingSucceeded = function ( numOfHighMTSections ) {\n\tthis.publishArticle( numOfHighMTSections );\n\tthis.off( 'saveFailure', this.saveBeforePublishingFailed.bind( this ) );\n};\n\nmw.cx.TranslationController.prototype.saveBeforePublishingFailed = function () {\n\tthis.onPublishCancel();\n\tthis.off( 'saveSuccess', this.saveBeforePublishingSucceeded.bind( this ) );\n};\n\nmw.cx.TranslationController.prototype.enableEditing = function () {\n\t// categoryUI is null for section translations. Check for value before re-enabling it\n\tif ( this.translationView.categoryUI ) {\n\t\tthis.translationView.categoryUI.disableCategoryUI( false );\n\t}\n\n\tclearInterval( this.saveStatusTimer );\n};\n\n/**\n * Publish cancel handler\n */\nmw.cx.TranslationController.prototype.onPublishCancel = function () {\n\tmw.log( '[CX] Publishing canceled' );\n\n\tthis.veTarget.onPublishCancel();\n\tthis.enableEditing();\n};\n\n/**\n * Publish success handler\n */\nmw.cx.TranslationController.prototype.onPublishSuccess = function () {\n\tmw.log( '[CX] Publishing finished successfully' );\n\n\tthis.veTarget.onPublishSuccess( this.targetArticle.getTargetURL() );\n\tthis.enableEditing();\n\n\t// Event logging and Wikibase linking\n\tmw.hook( 'mw.cx.translation.published' ).fire(\n\t\tthis.translation.getSourceLanguage(),\n\t\tthis.translation.getTargetLanguage(),\n\t\tthis.translation.getSourceTitle(),\n\t\tthis.translation.getTargetTitle()\n\t);\n};\n\n/**\n * Publish error handler\n *\n * @param {OO.ui.Error} error\n */\nmw.cx.TranslationController.prototype.onPublishFailure = function ( error ) {\n\tthis.isFailedUnrecoverably = !error.isRecoverable();\n\tthis.veTarget.onPublishFailure( error.getMessageText() );\n\tthis.enableEditing();\n};\n\n/**\n * Target categories change handler.\n */\nmw.cx.TranslationController.prototype.onTargetCategoriesChange = function () {\n\tthis.targetCategoriesChanged++;\n\tthis.saveScheduler();\n};\n\n/**\n * Target title change handler\n */\nmw.cx.TranslationController.prototype.onTargetTitleChange = function () {\n\tvar currentTitle = this.translation.getTargetTitle(),\n\t\tnewTitle = this.translationView.targetColumn.getTitle();\n\n\t// if nothing changed return without doing anything\n\tif ( currentTitle === newTitle ) {\n\t\treturn;\n\t}\n\n\tthis.translation.setTargetTitle( newTitle );\n\tthis.saveScheduler();\n\n\tvar currentTitleObj = mw.Title.newFromUserInput( currentTitle );\n\tvar newTitleObj = mw.Title.newFromUserInput( newTitle );\n\n\tif (\n\t\tcurrentTitleObj && newTitleObj &&\n\t\tcurrentTitleObj.getNamespaceId() !== newTitleObj.getNamespaceId()\n\t) {\n\t\tthis.veTarget.emitNamespaceChange( newTitleObj.getNamespaceId() );\n\t}\n};\n\n/**\n * Target section title change handler. Currently not used\n */\nmw.cx.TranslationController.prototype.onTargetSectionTitleChange = function () {};\n\nmw.cx.TranslationController.prototype.onSurfaceReady = function () {\n\tvar api = new mw.Api();\n\n\tthis.translationTracker.init( this.translation );\n\n\tapi.get( {\n\t\taction: 'query',\n\t\tmeta: 'cxdeletedtranslations',\n\t\tdtafter: this.getTimestamp()\n\t} ).then( function ( result ) {\n\t\tthis.hasDeletedTranslations = OO.getProp( result, 'query', 'cxdeletedtranslations', 'deleted' ) > 0;\n\t}.bind( this ) );\n};\n\n/**\n * Get ISO formatted Date string for current date minus 30 days, signifying last month period.\n *\n * @return {string}\n */\nmw.cx.TranslationController.prototype.getTimestamp = function () {\n\tvar date = new Date();\n\tdate.setDate( date.getDate() - 30 );\n\n\treturn date.toISOString();\n};\n\n/**\n * Check if the translation has too much MT usage and get appropriate message.\n *\n * @param {number} numOfHighMTSections\n * @return {mw.Message|null}\n */\nmw.cx.TranslationController.prototype.getMTAbuseMsg = function ( numOfHighMTSections ) {\n\tvar highMTSectionsThreshold = this.hasDeletedTranslations ? 10 : 50;\n\n\tif ( numOfHighMTSections >= highMTSectionsThreshold ) {\n\t\treturn mw.message( 'cx-mt-abuse-error-sections' );\n\t}\n\n\tvar mtPercentage = this.translationTracker.getUnmodifiedMTPercentageInTranslation();\n\tmw.log( 'Unmodified MT percentage: ' + mtPercentage );\n\tvar threshold = mw.config.get( 'wgContentTranslationUnmodifiedMTThresholdForPublish' );\n\n\tif ( mtPercentage > parseFloat( threshold ) ) {\n\t\treturn mw.message(\n\t\t\t'cx-mt-abuse-error-title',\n\t\t\tmw.language.convertNumber( Math.round( mtPercentage ) )\n\t\t);\n\t}\n\n\treturn null;\n};\n\n/**\n * Triggered when all issues are resolved on node with a given ID.\n *\n * @param {number|string} id ID of a node which issues are resolved\n */\nmw.cx.TranslationController.prototype.onIssuesResolved = function ( id ) {\n\tthis.translationTracker.setTranslationIssues( id, false );\n\tthis.translationView.onIssuesResolved( this.translationTracker.getNodesWithIssues() );\n};\n\n/**\n * Triggered when node with given ID has issues.\n *\n * @param {number|string} id ID of a node with issues\n * @param {boolean} hasErrors True if any of the issues is error. False if all issues are warnings\n */\nmw.cx.TranslationController.prototype.onTranslationIssues = function ( id, hasErrors ) {\n\tthis.translationTracker.setTranslationIssues( id, true );\n\tthis.translationView.onTranslationIssues( this.translationTracker.getNodesWithIssues(), hasErrors );\n};\n\n/* Registration */\n\nve.ui.commandHelpRegistry.register( 'other', 'autoSave', {\n\tshortcuts: [ {\n\t\tmac: 'cmd+s',\n\t\tpc: 'ctrl+s'\n\t} ],\n\tlabel: OO.ui.deferMsg( 'cx-save-draft-shortcut-label' )\n} );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/mw.cx.TranslationTracker.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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\tif ( string1 === string2 ) {\n\t\treturn 1;\n\t}\n\tif ( !string1 || !string2 ) {\n\t\treturn 0;\n\t}\n\n\tvar tokens1 = this.tokenise( string1, language );\n\tvar tokens2 = 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\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\tvar tokens1, tokens2;\n\tvar bigSet = tokens1 = this.tokenise( string1, language );\n\tvar smallSet = 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\tvar unmodifiedTokens = 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 excludedTypes = [\n\t\t'cxBlockImage', 'mwBlockImage', // Both are required since new images can be inserted too.\n\t\t'cxTransclusionBlock', 'mwTransclusionBlock',\n\t\t'mwReferencesList',\n\t\t'mwMath',\n\t\t'definitionList',\n\t\t'mwAlienBlockExtension', 'mwAlienInlineExtension',\n\t\t'mwTable', 'list', 'mwHeading'\n\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\tvar children;\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\tvar childType = 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 nodesToBeValidated = [];\n\n\tvar children;\n\tif ( nodeModel && nodeModel.getChildren ) {\n\t\tchildren = nodeModel.getChildren();\n\t}\n\n\tif ( !children ) {\n\t\treturn [];\n\t}\n\n\tfor ( var i = 0; i < children.length; i++ ) {\n\t\tvar currentNode = 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\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 ( var i = 0; i < validationTree.length; i++ ) {\n\t\tvar validationNode = validationTree[ i ];\n\t\tvar sourceText = $( 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 restoredSections = 0;\n\n\tvar sectionModels = translationModel.sourceDoc.getNodesByType( 'cxSection' );\n\tvar savedTranslationUnits = translationModel.savedTranslationUnits || [];\n\tfor ( var i = 0; i < sectionModels.length; i++ ) {\n\t\tvar sectionModel = sectionModels[ i ];\n\n\t\tvar sectionNumber = sectionModel.getSectionNumber();\n\t\tvar sectionState = new mw.cx.dm.SectionState( sectionNumber );\n\t\tsectionState.setSource( ve.dm.converter.getDomFromNode( sectionModel ).body.innerHTML );\n\t\tvar savedTranslationUnit = 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\tvar progress = 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.attachEventListeners( translationModel.targetDoc.getNodesByType( 'cxSection' ) );\n};\n\n/**\n * Attach listeners for events on restored sections as well as on newly added sections.\n *\n * @param {ve.dm.CXSectionNode[]} sections\n */\nmw.cx.TranslationTracker.prototype.attachEventListeners = function ( sections ) {\n\t// Register event listeners for 'focus' and 'update' events on restored sections\n\tsections.map( function ( sectionModel ) {\n\t\treturn sectionModel.getId();\n\t} ).forEach( this.registerEventListenersForSection.bind( this ) );\n\n\t// Register event listeners for 'focus' event for every newly added section\n\tthis.veTarget.connect( this, { changeContentSource: 'registerEventListenersForSection' } );\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 = 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\tvar sectionState = 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\tvar currentMTProvider = sectionState.getCurrentMTProvider();\n\tvar newMTProvider = sectionModel.getOriginalContentSource();\n\tvar freshTranslation = false;\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\t// We use CLIPBOARD_MODE below because in PARSER_MODE, we exclude template renderings\n\t// and other view-only information that Parsoid doesn't care about.\n\t// CLIPBOARD_MODE helps to keep the rendering in the content to be saved and hence helping to restore\n\t// them with rendering.\n\tvar newContent = ve.dm.converter.getDomFromNode( sectionModel, ve.dm.Converter.static.CLIPBOARD_MODE ).body.innerHTML;\n\tvar existingContent = sectionState.getUserTranslation();\n\tvar unmodifiedMTContent = 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 sectionState = this.sections[ sectionNumber ],\n\t\tunmodifiedContent = sectionState.getUnmodifiedMT(),\n\t\tuserTranslation = sectionState.getUserTranslation();\n\n\tvar unmodifiedPercentage = 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\tvar progress = 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\tif ( !sectionModel ) {\n\t\treturn;\n\t}\n\n\tvar sectionState = this.sections[ sectionModel.getSectionNumber() ];\n\tvar percentage = 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 index = sectionModel.findIssueIndex( 'mt-abuse' );\n\n\t\tif ( index < 0 ) {\n\t\t\treturn false;\n\t\t}\n\n\t\tvar issue = sectionModel.translationIssues[ index ];\n\t\tif ( !issue.isSuppressed() ) {\n\t\t\treturn true;\n\t\t}\n\n\t\tvar sectionState = this.sections[ sectionModel.getSectionNumber() ];\n\t\tvar unmodifiedPercentage = sectionState.getUnmodifiedPercentage();\n\t\tvar threshold = 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 sourceSectionCount = 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\tvar unmodifiedMTPercentage = 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.registerEventListenersForSection = function ( sectionNumber ) {\n\t/* @type {ve.ce.CXSectionNode} */\n\tvar sectionNode = this.veTarget.getTargetSectionElementFromSectionNumber( sectionNumber );\n\t/* @type {ve.dm.CXSectionNode} */\n\tvar sectionModel = this.veTarget.getTargetSectionNodeFromSectionNumber( sectionNumber );\n\n\tvar focusHandler = function () {\n\t\t// Validate every sections except the current section\n\t\tvar index = this.validationDelayQueue.indexOf( sectionNumber );\n\t\tif ( index > -1 ) {\n\t\t\tthis.validationDelayQueue.splice( index, 1 );\n\t\t}\n\t\tthis.processValidationQueue();\n\t\t// Put the section number back in queue\n\t\tthis.validationDelayQueue.push( sectionNumber );\n\t};\n\n\tvar changeHandler = function () {\n\t\t// If the setion has existing issues, validate the issues on every change\n\t\t// So that the translator know when it is getting resolved\n\t\tif ( sectionModel.hasTranslationIssues() ) {\n\t\t\t// Change events need to be debounced\n\t\t\tOO.ui.debounce( this.processValidationQueue.bind( this ), 3 * 1000 )();\n\t\t}\n\t};\n\n\tsectionModel.connect( this, { update: changeHandler } );\n\tsectionNode.connect( this, { focus: focusHandler } );\n};\n\n/**\n * Process any delayed validations on sections.\n */\nmw.cx.TranslationTracker.prototype.processValidationQueue = function () {\n\tvar i = this.validationDelayQueue.length;\n\twhile ( i-- ) {\n\t\tvar sectionNumber = this.validationDelayQueue[ i ];\n\t\tvar sectionModel = 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":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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.sourceSectionTitle = sourceWikiPage.getSectionTitle();\n\n\tthis.config.targetTitle = targetWikiPage.getTitle();\n\tthis.config.targetLanguage = targetWikiPage.getLanguage();\n\tthis.config.targetSectionTitle = targetWikiPage.getSectionTitle();\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\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\tvar platformPromise = new ve.init.mw.Platform().initialize();\n\tvar translationPromise = this.fetchTranslationData();\n\tvar pluginModules = mw.config.get( 'wgVisualEditorConfig' ).pluginModules;\n\tvar modulePromise = mw.loader.using( [ 'mw.cx.visualEditor' ].concat( pluginModules ) );\n\t$.when( translationPromise, modulePromise, platformPromise ).then( function ( translationData ) {\n\t\tvar sourcePageContent = 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\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\tif ( translationModel.isSectionTranslation() ) {\n\t\t\t\tthis.translationView.markSectionTranslation();\n\t\t\t} else {\n\t\t\t\ttranslationModel.initCategories(\n\t\t\t\t\tthis.processCategories( sourcePageContent.categories )\n\t\t\t\t);\n\t\t\t\tvar categoryUI = new mw.cx.ui.Categories( translationModel, this.config );\n\t\t\t\tthis.translationView.showCategories( categoryUI );\n\t\t\t}\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 ), this.initializationError.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\tmw.log( '[CX] Fetching Source page...' );\n\tvar sourcePageFetchDeferred = 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\tvar draftFetchDeferred = 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] Saved translation if any.\n * @return {jQuery.Promise}\n */\nmw.cx.init.Translation.prototype.initTranslationModel = function ( sourceHtml, draft ) {\n\tvar translationUnits = draft && draft.translationUnits,\n\t\tnumberOfUnrestoredSections = 0;\n\n\tvar targetDom = mw.cx.dm.Translation.static.getSourceDom(\n\t\tsourceHtml,\n\t\tthis.sourceWikiPage.getSectionTitle(),\n\t\ttrue,\n\t\ttranslationUnits,\n\t\tthis.sourceWikiPage.getLanguage()\n\t);\n\n\tfor ( var 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\tvar sourceDom = mw.cx.dm.Translation.static.getSourceDom( sourceHtml, this.sourceWikiPage.getSectionTitle() );\n\n\t\tvar translationModel = 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 uri = new mw.Uri(),\n\t\t\tupdatedSourceHtml = sourcePageContent.segmentedContent;\n\n\t\t// Reset restoration status for all translation units\n\t\tfor ( var id in translationUnits ) {\n\t\t\ttranslationUnits[ id ].restored = false;\n\t\t}\n\n\t\tvar updatedSourceDom = mw.cx.dm.Translation.static.getSourceDom( updatedSourceHtml, this.sourceWikiPage.getSectionTitle() );\n\t\tvar updatedTargetDom = mw.cx.dm.Translation.static.getSourceDom(\n\t\t\tupdatedSourceHtml,\n\t\t\tthis.sourceWikiPage.getSectionTitle(),\n\t\t\ttrue,\n\t\t\ttranslationUnits,\n\t\t\tthis.sourceWikiPage.getLanguage()\n\t\t);\n\n\t\tvar updatedTranslationModel = new mw.cx.dm.Translation( this.sourceWikiPage, this.targetWikiPage, updatedSourceDom, updatedTargetDom );\n\t\tupdatedTranslationModel.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 updatedTranslationModel;\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 = {\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\tvar apiURL = '/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\tvar fetchPageUrl = 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 in last 24 hours by another translator found.' );\n\t\tthis.translationView.showConflictWarning( draft );\n\t\t// Stop further processing\n\t\treturn $.Deferred().resolve( null ).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 categories = {},\n\t\tlength = fetchedCategories.length;\n\n\twhile ( length-- ) {\n\t\tvar category = 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\tthis.translationView.showViewIssuesMessage(\n\t\tmw.msg( 'cx-infobar-old-version' ), 'old-version', 'warning'\n\t);\n\n\tvar diff = 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\tvar translationIssuesParams = {\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\tif ( !data || data.action !== 'restart' ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar sourceLanguage = this.translationModel.getSourceLanguage();\n\t\tvar targetLanguage = this.translationModel.getTargetLanguage();\n\t\tvar sourceTitle = this.translationModel.getSourceTitle();\n\t\tvar apiParams = {\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' ), // 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.publishToolbar\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":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * ContentTranslation initialization module.\n */\n\n( function () {\n\t'use strict';\n\n\t/**\n\t * This method receives the \"targettitle\" URL param, the sourceTitle and the mtService as arguments,\n\t * and returns the appropriate target title for this translation, according to the following logic:\n\t * 1. If the \"targettitle\" URL param is set, the method returns it\n\t * 2. If not, then a request is sent to the /suggest/title cxserver endpoint and the target title\n\t *    that is given in the HTTP response, is returned\n\t * 3. If that request fails, the source title is returned as fallback\n\t *\n\t * @param {string|null} targetTitleUrlParam\n\t * @param {string} sourceTitle\n\t * @param {mw.cx.MachineTranslationService} mtService\n\t * @return {jQuery.Promise}\n\t */\n\tfunction getTargetTitle( targetTitleUrlParam, sourceTitle, mtService ) {\n\t\tif ( targetTitleUrlParam ) {\n\t\t\treturn $.Deferred().resolve( targetTitleUrlParam );\n\t\t}\n\n\t\treturn mtService.getSuggestedTitle( sourceTitle ).then(\n\t\t\tfunction ( suggestedTitle ) {\n\t\t\t\treturn mw.cx.getTitleForNamespace( suggestedTitle, mw.cx.getDefaultTargetNamespace() );\n\t\t\t},\n\t\t\tfunction () {\n\t\t\t\treturn sourceTitle;\n\t\t\t}\n\t\t);\n\t}\n\n\tfunction initCX() {\n\t\tvar query, services = {}, sourceWikiPage, targetWikiPage, translation, VEConfig;\n\n\t\tquery = new mw.Uri().query;\n\t\tif (\n\t\t\t!query.page || !query.from || !query.to ||\n\t\t\t( mw.Title.newFromText( query.page ) === null )\n\t\t) {\n\t\t\tlocation.href = mw.util.getUrl( 'Special:ContentTranslation' );\n\t\t\treturn;\n\t\t}\n\n\t\t// Set the global siteMapper for code which we cannot inject it\n\t\t// All these configuration in mw.cx is just for supporting legacy code.\n\t\t// New code should get them from config injected to classes.\n\t\tmw.cx.siteMapper = new mw.cx.SiteMapper();\n\t\tvar sourceTitle = query.page;\n\t\tvar sourceRevision = query.revision;\n\t\tvar sourceSectionTitle = query.sourcesection;\n\t\tvar targetSectionTitle = query.targetsection || query.sourcesection;\n\t\tmw.cx.targetLanguage = query.to;\n\t\tmw.cx.sourceLanguage = query.from;\n\t\t// Global services that every class can expect to have\n\t\tservices = {\n\t\t\tsiteMapper: mw.cx.siteMapper\n\t\t};\n\n\t\tservices.requestManager = new mw.cx.MwApiRequestManager( mw.cx.sourceLanguage, mw.cx.targetLanguage, services.siteMapper );\n\t\tservices.MTService = new mw.cx.MachineTranslationService( mw.cx.sourceLanguage, mw.cx.targetLanguage, services.siteMapper );\n\t\tservices.MTManager = new mw.cx.MachineTranslationManager( mw.cx.sourceLanguage, mw.cx.targetLanguage, services.MTService );\n\n\t\tgetTargetTitle( query.targettitle, sourceTitle, services.MTService ).then( function ( targetTitle ) {\n\t\t\tsourceWikiPage = new mw.cx.dm.WikiPage( sourceTitle, mw.cx.sourceLanguage, sourceRevision, sourceSectionTitle );\n\t\t\ttargetWikiPage = new mw.cx.dm.WikiPage( targetTitle, mw.cx.targetLanguage, null, targetSectionTitle );\n\t\t\ttranslation = new mw.cx.init.Translation( sourceWikiPage, targetWikiPage, services );\n\t\t\ttranslation.init();\n\n\t\t\tif ( query.campaign ) {\n\t\t\t\tmw.hook( 'mw.cx.cta.accept' ).fire( query.campaign, mw.cx.sourceLanguage, sourceTitle, mw.cx.targetLanguage );\n\t\t\t}\n\n\t\t\tif ( mw.config.get( 'wgContentTranslationBetaFeatureEnabled' ) ) {\n\t\t\t\tmw.notify( mw.msg( 'cx-beta-feature-enabled-notification' ) );\n\t\t\t}\n\n\t\t\t// The default values for these options depend on PageImages and Wikibase Client\n\t\t\t// being installed on this wiki. Because we are querying remote wikis, this makes\n\t\t\t// no sense, and hence overwrite the values.\n\t\t\tVEConfig = mw.config.get( 'wgVisualEditorConfig' );\n\t\t\tVEConfig.usePageImages = true;\n\t\t\tVEConfig.usePageDescriptions = true;\n\t\t\tmw.config.set( 'wgVisualEditorConfig', VEConfig );\n\t\t} );\n\n\t}\n\n\t// On document ready, initialize, but not during QUnit tests, when this code is loaded\n\t// only because another file in this module has tests\n\tif ( !window.QUnit ) {\n\t\t$( initCX );\n\t}\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/publish/ext.cx.wikibase.link.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"es-x/no-promise","severity":2,"message":"ES2015 'Promise' class is forbidden.","line":30,"column":11,"nodeType":"Identifier","messageId":"forbidden","endLine":30,"endColumn":18},{"ruleId":"compat/compat","severity":1,"message":"Promise.resolve() is not supported in IE 11","line":43,"column":10,"nodeType":"MemberExpression","endLine":43,"endColumn":25},{"ruleId":"es-x/no-promise","severity":2,"message":"ES2015 'Promise' class is forbidden.","line":43,"column":10,"nodeType":"Identifier","messageId":"forbidden","endLine":43,"endColumn":17}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation - Link articles using Wikibase\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\t/**\n\t * Link the source and target articles in the Wikibase repo\n\t *\n\t * @param {string} sourceLanguage\n\t * @param {string} targetLanguage\n\t * @param {string} sourceTitle\n\t * @param {string} targetTitle\n\t * @return {Promise}\n\t */\n\tfunction addWikibaseLink(\n\t\tsourceLanguage,\n\t\ttargetLanguage,\n\t\tsourceTitle,\n\t\ttargetTitle\n\t) {\n\t\tvar title, params, targetWikiId, api;\n\n\t\t// Link only pages in the main space\n\t\ttitle = new mw.Title( targetTitle );\n\t\tif ( title.getNamespaceId() !== 0 ) {\n\t\t\treturn Promise.resolve();\n\t\t}\n\n\t\t// Current wiki is target wiki since publishing happens at target wiki\n\t\ttargetWikiId = mw.config.get( 'wgWikiID' );\n\t\tparams = {\n\t\t\taction: 'wblinktitles',\n\t\t\tfromsite: targetWikiId.replace( targetLanguage, sourceLanguage ),\n\t\t\tfromtitle: sourceTitle,\n\t\t\ttosite: targetWikiId,\n\t\t\ttotitle: targetTitle\n\t\t};\n\t\tapi = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php' );\n\t\treturn Promise.resolve( api.postWithToken( 'csrf', params ).then( function () {\n\t\t\tvar mwApi = new mw.Api();\n\n\t\t\t// Purge the newly-created page after adding the link,\n\t\t\t// so that they will appear as soon as possible without manual purging\n\t\t\treturn mwApi.post( {\n\t\t\t\taction: 'purge',\n\t\t\t\ttitles: targetTitle\n\t\t\t} );\n\t\t} ) );\n\t}\n\n\t$( function () {\n\t\tmw.hook( 'mw.cx.translation.published' ).add( addWikibaseLink );\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/source/mw.cx.SelectedSourcePage.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"mediawiki/class-doc","severity":2,"message":"All possible CSS classes should be documented. See https://w.wiki/PS2 for details.","line":355,"column":3,"nodeType":"CallExpression","endLine":355,"endColumn":82,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/*!\n * SelectedSourcePage - widget that displays selected page info:\n * - title\n * - image\n * - number of different language versions\n * - weekly page views count\n * and language selector that allows to change source and target language before starting the translation.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n\n/**\n * SelectedSourcePage\n *\n * @class\n * @param {mw.cx.SiteMapper} siteMapper\n * @param {Object} config\n * @cfg {Function} [onDiscard] Callback triggered after selected source page is discarded\n */\nmw.cx.SelectedSourcePage = function ( siteMapper, config ) {\n\tthis.siteMapper = siteMapper;\n\tthis.config = $.extend( {\n\t\tcampaign: new mw.Uri().query.campaign\n\t}, config );\n\n\tthis.onDiscard = this.config.onDiscard;\n\tthis.sourceTitle = null;\n\tthis.targetTitle = null;\n\t// this.sourcePageTitles are titles of the selected source page in different languages\n\tthis.sourcePageTitles = {};\n\n\tthis.$element = null;\n\n\tthis.$image = null;\n\tthis.$link = null;\n\tthis.languageCount = null;\n\tthis.viewsCount = null;\n\tthis.bookmarkButton = null;\n\tthis.languageFilter = null;\n\tthis.discardButton = null;\n\tthis.startTranslationButton = null;\n\tthis.$messageBar = null;\n\tthis.$messageText = null;\n\n\tthis.alreadyFavorite = false;\n\n\tthis.init();\n};\n\nmw.cx.SelectedSourcePage.prototype.init = function () {\n\tthis.validator = new mw.cx.ContentTranslationValidator( this.siteMapper );\n\n\tthis.languageFilter = new mw.cx.ui.LanguageFilter( {\n\t\tonSourceLanguageChange: this.sourceLanguageChangeHandler.bind( this ),\n\t\tonTargetLanguageChange: this.targetLanguageChangeHandler.bind( this )\n\t} );\n\tthis.bookmarkButton = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\ticon: 'bookmarkOutline'\n\t} );\n\tthis.discardButton = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\ticon: 'close',\n\t\tclasses: [ 'cx-selected-source-page__discard' ]\n\t} );\n\n\tthis.render();\n\tthis.listen();\n};\n\nmw.cx.SelectedSourcePage.prototype.render = function () {\n\tvar $linkContainer, $container, $info, $metrics, $license, $actions,\n\t\ttranslateButtonLabel, languageCountIcon;\n\n\tthis.$image = $( '<div>' )\n\t\t.addClass( 'cx-selected-source-page__image' );\n\n\tthis.$link = $( '<a>' )\n\t\t.addClass( 'cx-selected-source-page__link' )\n\t\t.prop( {\n\t\t\tlang: this.languageFilter.getSourceLanguage(),\n\t\t\tdir: $.uls.data.getDir( this.languageFilter.getSourceLanguage() )\n\t\t} );\n\t$linkContainer = $( '<span>' )\n\t\t.append( this.$link );\n\n\tlanguageCountIcon = new OO.ui.IconWidget( {\n\t\ticon: 'language',\n\t\tclasses: [ 'cx-selected-source-page__language-count' ]\n\t} );\n\tthis.languageCount = new OO.ui.LabelWidget();\n\tthis.viewsCount = new OO.ui.LabelWidget( {\n\t\tclasses: [ 'cx-selected-source-page__views-count' ]\n\t} );\n\t$metrics = $( '<div>' )\n\t\t.addClass( 'cx-selected-source-page__metrics' )\n\t\t.append( languageCountIcon.$element, this.languageCount.$element, this.viewsCount.$element );\n\n\t$info = $( '<div>' )\n\t\t.addClass( 'cx-selected-source-page__info' )\n\t\t.append( $linkContainer, $metrics );\n\n\t$container = $( '<div>' )\n\t\t.addClass( 'cx-selected-source-page__container' )\n\t\t.append(\n\t\t\tthis.$image,\n\t\t\t$info,\n\t\t\t$( '<div>' ).addClass( 'cx-selected-source-page__spacer' ),\n\t\t\tthis.bookmarkButton.$element,\n\t\t\tthis.languageFilter.$element,\n\t\t\tthis.discardButton.$element\n\t\t);\n\n\tthis.$messageBar = $( '<div>' )\n\t\t.addClass( 'cx-selected-source-page__messagebar' );\n\tthis.$messageText = $( '<span>' )\n\t\t.addClass( 'cx-selected-source-page__messagebar-text' );\n\tthis.$messageBar\n\t\t.append( this.$messageText )\n\t\t.hide();\n\n\ttranslateButtonLabel = mw.msg( 'cx-selected-source-page-start-translation-button' );\n\tthis.startTranslationButton = new OO.ui.ButtonWidget( {\n\t\tflags: [ 'primary', 'progressive' ],\n\t\tlabel: translateButtonLabel\n\t} );\n\n\t$license = $( '<div>' )\n\t\t.addClass( 'cx-selected-source-page__license' )\n\t\t.append( mw.message( 'cx-license-agreement', translateButtonLabel ).parseDom() );\n\n\t$license.find( 'a' ).prop( 'target', '_blank' );\n\n\t$actions = $( '<div>' )\n\t\t.addClass( 'cx-selected-source-page__actions' )\n\t\t.append( this.startTranslationButton.$element );\n\n\tthis.$element = $( '<div>' )\n\t\t.addClass( 'cx-selected-source-page' )\n\t\t.append( $container, this.$messageBar, $license, $actions );\n};\n\nmw.cx.SelectedSourcePage.prototype.hide = function () {\n\tthis.$element.remove();\n};\n\nmw.cx.SelectedSourcePage.prototype.focusStartTranslationButton = function () {\n\tthis.startTranslationButton.$button.trigger( 'focus' );\n};\n\nmw.cx.SelectedSourcePage.prototype.listen = function () {\n\tthis.startTranslationButton.connect( this, { click: 'startPageInCX' } );\n\tthis.discardButton.connect( this, { click: 'discardDialog' } );\n\n\tthis.bookmarkButton.connect( this, { click: 'onBookmarkButtonClick' } );\n};\n\n/**\n * Change \"favorite\" button icon to filled bookmark\n */\nmw.cx.SelectedSourcePage.prototype.setFilledIcon = function () {\n\tthis.bookmarkButton.setFlags( 'progressive' );\n\tthis.bookmarkButton.setIcon( 'bookmark' );\n};\n\n/**\n * Change \"favorite\" button icon to bookmark outline\n */\nmw.cx.SelectedSourcePage.prototype.setOutlineIcon = function () {\n\tthis.bookmarkButton.clearFlags();\n\tthis.bookmarkButton.setIcon( 'bookmarkOutline' );\n};\n\n/**\n * Change \"favorite\" button icon to filled bookmark and register mouse events\n */\nmw.cx.SelectedSourcePage.prototype.toggleFilledIcon = function () {\n\tthis.setFilledIcon();\n\tthis.bookmarkButton.$element.on( {\n\t\tmouseenter: this.setOutlineIcon.bind( this ),\n\t\tmouseleave: this.setFilledIcon.bind( this )\n\t} );\n};\n\n/**\n * Change \"favorite\" button icon to bookmark outline and register mouse events\n */\nmw.cx.SelectedSourcePage.prototype.toggleOutlineIcon = function () {\n\tthis.setOutlineIcon();\n\tthis.bookmarkButton.$element.on( {\n\t\tmouseenter: this.setFilledIcon.bind( this ),\n\t\tmouseleave: this.setOutlineIcon.bind( this )\n\t} );\n};\n\nmw.cx.SelectedSourcePage.prototype.onBookmarkButtonClick = function () {\n\tvar params, api = new mw.Api();\n\n\tparams = {\n\t\tassert: 'user',\n\t\taction: 'cxsuggestionlist',\n\t\tlistname: 'cx-suggestionlist-favorite',\n\t\tlistaction: this.alreadyFavorite ? 'remove' : 'add',\n\t\ttitles: this.sourceTitle,\n\t\tfrom: this.languageFilter.sourceLanguage,\n\t\tto: this.languageFilter.targetLanguage\n\t};\n\n\tapi.postWithToken( 'csrf', params ).done( function ( response ) {\n\t\tif ( response.cxsuggestionlist.result !== 'success' ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( this.alreadyFavorite ) {\n\t\t\tmw.notify( mw.msg( 'cx-favorite-removed' ) );\n\t\t\tthis.toggleOutlineIcon();\n\t\t} else {\n\t\t\tmw.notify( this.getNotifyMessage() );\n\t\t\tthis.toggleFilledIcon();\n\t\t}\n\t\tthis.alreadyFavorite = !this.alreadyFavorite;\n\t}.bind( this ) );\n};\n\n/**\n * Get message to show to user as notification after adding an article to \"For later\",\n * depending on currently selected list. See T188634#5384861\n * There's no reference to dashboard and its suggestion/translation lists,\n * therefore we query DOM in order to get selected list.\n *\n * @return {string} Messsage to notify user after keeping an article for later.\n */\nmw.cx.SelectedSourcePage.prototype.getNotifyMessage = function () {\n\tvar selectedOptionWidget = $( '.cx-translation-filter .oo-ui-optionWidget-selected' ).data(),\n\t\tselectedView = selectedOptionWidget && selectedOptionWidget.ooUiOptionWidget;\n\n\treturn selectedView && selectedView.getData() === 'suggestions' ?\n\t\tmw.msg( 'cx-favorite-added-for-later' ) :\n\t\tmw.msg( 'cx-favorite-added-for-later-detail' );\n};\n\nmw.cx.SelectedSourcePage.prototype.discardDialog = function () {\n\tthis.$messageBar.hide(); // Hide any previous messages\n\n\t// Discard selected source image\n\tthis.$image\n\t\t.removeAttr( 'style' )\n\t\t.removeClass( 'oo-ui-iconElement-icon' )\n\t\t.attr( 'class', function ( i, className ) {\n\t\t\treturn className.replace( /oo-ui-icon-\\S+/, '' );\n\t\t} );\n\n\tthis.alreadyFavorite = false;\n\tthis.bookmarkButton.toggle( true );\n\tthis.setOutlineIcon();\n\t// Reset source titles, as there is no selected source\n\tthis.sourcePageTitles = {};\n\t// Reset source and target ULS to show all source and target languages\n\tthis.languageFilter.fillSourceLanguages( null, true );\n\tthis.languageFilter.fillTargetLanguages( null, true );\n\n\t$( 'html' ).trigger( 'click' ); // Not sure why click doesn't pass through OOUI button to HTML element\n\t// where listener is closing the ULS on outside clicks. Maybe some OOUI change?\n\n\tif ( this.onDiscard ) {\n\t\tthis.onDiscard();\n\t}\n};\n\n/**\n * Change the title of selected source page to title in other language\n *\n * @param {string} language Language code\n */\nmw.cx.SelectedSourcePage.prototype.changeSelectedSourceTitle = function ( language ) {\n\tvar href, title = this.sourcePageTitles[ language ];\n\n\tif ( title ) {\n\t\thref = this.siteMapper.getPageUrl( language, title );\n\t\tthis.$link.prop( {\n\t\t\thref: href,\n\t\t\ttitle: title,\n\t\t\ttext: title\n\t\t} ).toggleClass( 'cx-selected-source-page__link--long', title.length >= 60 );\n\t\tthis.sourceTitle = title;\n\t}\n};\n\n/**\n * Handles source language change.\n *\n * @param {string} language Language code.\n */\nmw.cx.SelectedSourcePage.prototype.sourceLanguageChangeHandler = function ( language ) {\n\tthis.changeSelectedSourceTitle( language );\n\tthis.getPageInfo( this.sourcePageTitles[ language ] ).done( function ( data ) {\n\t\tthis.renderPageViews( data.pageviews );\n\t}.bind( this ) ).fail( function ( error ) {\n\t\tmw.log( 'Error getting page info for ' + this.sourcePageTitles[ language ] + '. ' + error );\n\t}.bind( this ) );\n\n\tthis.initBookmark();\n\tthis.check();\n};\n\nmw.cx.SelectedSourcePage.prototype.setSourceTitle = function ( sourceTitle ) {\n\tthis.sourceTitle = sourceTitle;\n};\n\n/**\n * Handles target language change.\n *\n * @param {string} language Language code.\n */\nmw.cx.SelectedSourcePage.prototype.targetLanguageChangeHandler = function () {\n\tthis.initBookmark();\n\tthis.check();\n};\n\nmw.cx.SelectedSourcePage.prototype.setTargetTitle = function ( targetTitle ) {\n\tthis.targetTitle = targetTitle;\n};\n\n/**\n * Sets all the info for selected page\n *\n * @param {string} pageTitle\n * @param {string} href\n * @param {Object} config\n * @cfg {string} sourceLanguage Source language code\n * @cfg {string} targetLanguage Target language code\n * @cfg {Object} [params] Parameters used for API call to get page info\n * @cfg {string} [imageUrl] URL for selected source page image\n * @cfg {string} [imageIcon] OOUI class of selected page placeholder icon\n * @cfg {number} [numOfLanguages] Number of different language versions for selected source page\n */\nmw.cx.SelectedSourcePage.prototype.setData = function ( pageTitle, href, config ) {\n\tvar params;\n\tthis.languageFilter.setSourceLanguageNoChecks( config.sourceLanguage );\n\tthis.languageFilter.setTargetLanguageNoChecks( config.targetLanguage );\n\n\tparams = $.extend( {\n\t\tprop: [ 'langlinks', 'pageviews' ],\n\t\tredirects: true,\n\t\tlllimit: 'max'\n\t}, config.params );\n\n\tif ( config.imageUrl ) {\n\t\tthis.$image.css( 'background-image', 'url( ' + config.imageUrl + ')' );\n\t} else {\n\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\tthis.$image.addClass( 'oo-ui-iconElement-icon oo-ui-icon-' + config.imageIcon );\n\t}\n\n\tthis.$link.prop( {\n\t\thref: href,\n\t\ttitle: pageTitle,\n\t\ttarget: '_blank',\n\t\ttext: pageTitle\n\t} );\n\tthis.$link.toggleClass( 'cx-selected-source-page__link--long', pageTitle.length >= 60 );\n\n\tthis.getPageInfo( pageTitle, params ).done( function ( data ) {\n\t\tvar langCode, title, languagesPageExistsIn, languageDecorator, numOfLanguages;\n\n\t\tthis.renderPageViews( data.pageviews );\n\n\t\tnumOfLanguages =\n\t\t\tconfig.numOfLanguages ||\n\t\t\t( OO.getProp( data, 'langlinkscount' ) || 0 ) + 1;\n\t\tthis.languageCount.setLabel( mw.language.convertNumber( numOfLanguages ) );\n\n\t\t// Reset source page titles\n\t\tthis.sourcePageTitles = {};\n\t\t// Extract results data and create sourcePageTitles mapping\n\t\tif ( data.langlinks ) {\n\t\t\tdata.langlinks.forEach( function ( element ) {\n\t\t\t\tlangCode = element.lang;\n\t\t\t\ttitle = element[ '*' ];\n\n\t\t\t\tthis.sourcePageTitles[ langCode ] = title;\n\t\t\t}, this );\n\t\t}\n\t\t// Include chosen source page title (not returned by langlinks API)\n\t\tthis.sourcePageTitles[ this.languageFilter.getSourceLanguage() ] = pageTitle;\n\n\t\tlanguagesPageExistsIn = Object.keys( this.sourcePageTitles );\n\t\tlanguageDecorator = function ( $language, languageCode ) {\n\t\t\tif ( languagesPageExistsIn.indexOf( languageCode ) < 0 ) {\n\t\t\t\t$language.css( 'font-weight', 'bold' );\n\t\t\t}\n\t\t};\n\n\t\tthis.languageFilter.fillSourceLanguages( languagesPageExistsIn, true, {\n\t\t\tulsPurpose: 'cx-selectedpage-source'\n\t\t} );\n\t\tthis.languageFilter.fillTargetLanguages( null, true, {\n\t\t\tulsPurpose: 'cx-selectedpage-target',\n\t\t\tlanguageDecorator: languageDecorator\n\t\t} );\n\t\tthis.languageFilter.setValidSourceLanguages( languagesPageExistsIn );\n\t}.bind( this ) ).fail( function ( error ) {\n\t\tmw.log( 'Error getting page info for ' + pageTitle + '. ' + error );\n\t} );\n\n\tthis.sourceTitle = pageTitle;\n\t// Reset target title.\n\tthis.targetTitle = null;\n\tthis.initBookmark();\n\tthis.check();\n};\n\n/**\n * Gets data for the selected page.\n * Gets pageviews by default, and langlinks if specified through optional params.\n *\n * @param {string} title Title of the page for which data is fetched.\n * @param {Object} [params] Optional parameter used for fetching additional source data.\n * @return {jQuery.Promise} Returns thenable promise, so langlinks can be processed if necessary.\n */\nmw.cx.SelectedSourcePage.prototype.getPageInfo = function ( title, params ) {\n\tvar api;\n\n\tif ( !title ) {\n\t\tthrow new Error( 'Title is mandatory parameter' );\n\t}\n\n\tapi = this.siteMapper.getApi( this.languageFilter.getSourceLanguage() );\n\tparams = $.extend( {\n\t\taction: 'query',\n\t\t// If new prop array is provided in params, this one is overridden\n\t\tprop: [ 'pageviews' ],\n\t\ttitles: title,\n\t\tpvipdays: 7\n\t}, params );\n\n\treturn api.get( params ).then( function ( data ) {\n\t\tvar pageId,\n\t\t\tpage = OO.getProp( data, 'query', 'pages' );\n\n\t\tif ( !page ) {\n\t\t\treturn $.Deferred().reject( 'No page data' ).promise();\n\t\t}\n\n\t\t// Only one title was passed in titles params, so we expect one result\n\t\tpageId = Object.keys( page )[ 0 ];\n\t\tif ( pageId === '-1' ) {\n\t\t\t// Page does not exist\n\t\t\treturn $.Deferred().reject( 'Requested page does not exist' ).promise();\n\t\t}\n\n\t\treturn page[ pageId ];\n\t}, function ( response ) {\n\t\t// In case of failure, fallback to all source and target languages\n\t\tthis.sourcePageTitles = {};\n\t\tthis.languageFilter.fillSourceLanguages( null, true );\n\t\tthis.languageFilter.fillTargetLanguages( null, true );\n\n\t\treturn $.Deferred().reject( 'Reason: ' + response ).promise();\n\t}.bind( this ) );\n};\n\nmw.cx.SelectedSourcePage.prototype.renderPageViews = function ( pageViewData ) {\n\tvar date, pageViews = 0;\n\n\tif ( !pageViewData ) {\n\t\treturn;\n\t}\n\n\tfor ( date in pageViewData ) {\n\t\tpageViews += pageViewData[ date ];\n\t}\n\n\tthis.viewsCount.setLabel(\n\t\tmw.msg( 'cx-selected-source-page-view-count', mw.language.convertNumber( pageViews ) )\n\t);\n};\n\nmw.cx.SelectedSourcePage.prototype.initBookmark = function () {\n\tthis.alreadyFavorite = false;\n\tthis.bookmarkButton.toggle( true );\n\tthis.setOutlineIcon();\n\n\tthis.isAlreadyFavorite(\n\t\tthis.languageFilter.getSourceLanguage(),\n\t\tthis.languageFilter.getTargetLanguage(),\n\t\tthis.sourceTitle\n\t).then( function ( alreadyFavorite ) {\n\t\tthis.alreadyFavorite = alreadyFavorite;\n\n\t\tif ( alreadyFavorite ) {\n\t\t\tthis.toggleFilledIcon();\n\t\t} else {\n\t\t\tthis.toggleOutlineIcon();\n\t\t}\n\t}.bind( this ) );\n};\n\nmw.cx.SelectedSourcePage.prototype.isAlreadyFavorite = function ( sourceLanguage, targetLanguage, title ) {\n\tvar params,\n\t\tapi = new mw.Api();\n\n\tparams = {\n\t\tassert: 'user',\n\t\tformatversion: 2,\n\t\taction: 'cxsuggestionlist',\n\t\tlistname: 'cx-suggestionlist-favorite',\n\t\tlistaction: 'view',\n\t\ttitles: title,\n\t\tfrom: sourceLanguage,\n\t\tto: targetLanguage\n\t};\n\n\treturn api.postWithToken( 'csrf', params ).then( function ( response ) {\n\t\treturn response.cxsuggestionlist.listaction;\n\t} );\n};\n\n/**\n * Start a new page translation in Special:CX.\n */\nmw.cx.SelectedSourcePage.prototype.startPageInCX = function () {\n\tvar targetTitle, originalSourceTitle, sourceLanguage, targetLanguage, siteMapper;\n\n\tsiteMapper = this.siteMapper;\n\tsourceLanguage = this.languageFilter.getSourceLanguage();\n\ttargetLanguage = this.languageFilter.getTargetLanguage();\n\toriginalSourceTitle = this.sourceTitle;\n\ttargetTitle = this.targetTitle;\n\n\tthis.validator.isTitleExistInLanguage(\n\t\tsourceLanguage,\n\t\toriginalSourceTitle\n\t).done( function ( sourceTitle ) {\n\t\t// Set CX token as cookie.\n\t\tsiteMapper.setCXToken( sourceLanguage, targetLanguage, sourceTitle );\n\n\t\tlocation.href = siteMapper.getCXUrl(\n\t\t\tsourceTitle,\n\t\t\ttargetTitle,\n\t\t\tsourceLanguage,\n\t\t\ttargetLanguage,\n\t\t\t{ campaign: this.config.campaign }\n\t\t);\n\t}.bind( this ) );\n};\n\n/**\n * Checks selected source page for problems with chosen source and target language pair.\n */\nmw.cx.SelectedSourcePage.prototype.check = function () {\n\tvar sourceLanguage = this.languageFilter.getSourceLanguage(),\n\t\ttargetLanguage = this.languageFilter.getTargetLanguage(),\n\t\ttargetTitle = this.targetTitle || '',\n\t\ttitleCheck, translationCheck;\n\n\tthis.$messageBar.hide();\n\n\t// Whether the target title, if given, exists in the target wiki\n\ttitleCheck = this.validator.isTitleExistInLanguage( targetLanguage, targetTitle );\n\t// Whether the source already has a translation linked via language links\n\ttranslationCheck = this.validator.isTitleConnectedInLanguages(\n\t\tsourceLanguage,\n\t\ttargetLanguage,\n\t\tthis.sourceTitle\n\t);\n\n\t$.when(\n\t\ttranslationCheck,\n\t\ttitleCheck\n\t).done( function ( existingTranslation, existingTargetTitle ) {\n\t\t// If there is an existing translation and\n\t\t// the specified target title is in use\n\t\tif ( existingTranslation && existingTargetTitle ) {\n\t\t\tthis.showPageExistsAndTitleInUseError(\n\t\t\t\texistingTranslation,\n\t\t\t\texistingTargetTitle,\n\t\t\t\ttargetLanguage\n\t\t\t);\n\t\t} else if ( existingTranslation ) {\n\t\t\t// If there is just an existing translation\n\t\t\tthis.showPageExistsError( existingTranslation, targetLanguage );\n\t\t} else if ( existingTargetTitle ) {\n\t\t\t// If the specified target title is in use\n\t\t\tthis.showTitleInUseError( existingTargetTitle, targetLanguage );\n\t\t}\n\n\t\t// Page exists in target language\n\t\tif ( existingTranslation ) {\n\t\t\t// Hide bookmark button if page already exists\n\t\t\tthis.bookmarkButton.toggle( false );\n\t\t\tthis.setTargetTitle( existingTranslation );\n\t\t}\n\t}.bind( this ) );\n};\n\n/**\n * Shows error for target page existing and target title in use.\n *\n * @param {string} equivalentTargetPage the title of the existing page\n * @param {string} existingTargetTitle the title already in use\n * @param {string} targetLanguage\n */\nmw.cx.SelectedSourcePage.prototype.showPageExistsAndTitleInUseError = function (\n\tequivalentTargetPage,\n\texistingTargetTitle,\n\ttargetLanguage\n) {\n\tvar equivalentTargetPageLink, targetLanguageDisplay,\n\t\texistingTargetTitleLink, message;\n\n\tequivalentTargetPageLink = this.siteMapper.getPageUrl( targetLanguage, equivalentTargetPage );\n\ttargetLanguageDisplay = $.uls.data.getAutonym( targetLanguage );\n\n\texistingTargetTitleLink = this.siteMapper.getPageUrl( targetLanguage, existingTargetTitle );\n\n\tmessage = mw.message(\n\t\t'cx-selected-source-page-error-page-and-title-exist',\n\t\tequivalentTargetPageLink,\n\t\ttargetLanguageDisplay,\n\t\texistingTargetTitleLink\n\t);\n\n\tthis.showMessage( message );\n};\n\n/**\n * Shows error for page already existing in target.\n *\n * @param {string} equivalentTargetPage the title of the existing page\n * @param {string} targetLanguage\n */\nmw.cx.SelectedSourcePage.prototype.showPageExistsError = function ( equivalentTargetPage, targetLanguage ) {\n\tvar equivalentTargetPageLink, targetLanguageDisplay, message;\n\n\tequivalentTargetPageLink = this.siteMapper.getPageUrl( targetLanguage, equivalentTargetPage );\n\ttargetLanguageDisplay = $.uls.data.getAutonym( targetLanguage );\n\n\tmessage = mw.message(\n\t\t'cx-selected-source-page-error-page-exists',\n\t\tequivalentTargetPageLink, targetLanguageDisplay\n\t);\n\n\tthis.showMessage( message );\n};\n\n/**\n * Shows error for title already in use in target wiki.\n *\n * @param {string} existingTargetTitle The title already in use\n * @param {string} targetLanguage\n */\nmw.cx.SelectedSourcePage.prototype.showTitleInUseError = function ( existingTargetTitle, targetLanguage ) {\n\tvar existingTargetTitleLink, message;\n\n\texistingTargetTitleLink = this.siteMapper.getPageUrl( targetLanguage, existingTargetTitle );\n\n\tmessage = mw.message(\n\t\t'cx-selected-source-page-error-title-in-use',\n\t\texistingTargetTitleLink\n\t);\n\n\tthis.showMessage( message );\n};\n\n/**\n * Shows error message for dialog.\n *\n * @param {mw.Message|string} message the message to show\n */\nmw.cx.SelectedSourcePage.prototype.showMessage = function ( message ) {\n\tif ( message instanceof mw.Message ) {\n\t\tthis.$messageText.append( message.parseDom() );\n\t} else {\n\t\tthis.$messageText.text( message );\n\t}\n\n\tthis.$messageBar.find( 'a' )\n\t\t.attr( 'target', '_blank' );\n\n\tthis.$messageBar.show();\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/source/mw.cx.SelectedSourcePageDialog.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"no-jquery/no-slide","severity":2,"message":"Prefer CSS transitions to .slideDown","line":200,"column":2,"nodeType":"CallExpression","endLine":200,"endColumn":37,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-jquery/no-slide","severity":2,"message":"Prefer CSS transitions to .slideDown","line":209,"column":2,"nodeType":"CallExpression","endLine":209,"endColumn":51,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @param {OO.ui.ButtonWidget} triggerButton\n * @param {Object} options\n * @cfg {mw.cx.SiteMapper} siteMapper\n * @cfg {jQuery} $container Container for source page selector\n * @cfg {string} sourceLanguage Source language\n * @cfg {string} targetLanguage Target language\n * @cfg {string} sourceTitle Source title\n * @cfg {string} targetTitle Target title\n */\nmw.cx.SourcePageSelector = function ( triggerButton, options ) {\n\tthis.options = options || {};\n\tthis.siteMapper = options.siteMapper;\n\tthis.triggerButton = triggerButton;\n\tthis.$container = options.$container;\n\tthis.pageSelector = null;\n\tthis.selectedSourcePage = null;\n\tthis.languageFilter = null;\n\tthis.discardButton = null;\n\tthis.$noResultsMessage = null;\n\tthis.onDocumentMouseUpHandler = this.onDocumentMouseUp.bind( this );\n\tthis.init();\n};\n\n/**\n * Initialize the plugin.\n */\nmw.cx.SourcePageSelector.prototype.init = function () {\n\tthis.render();\n\tthis.prefill();\n\tthis.pageSelector.populateSuggestions();\n\tthis.listen();\n};\n\n/**\n * Prefill the selector if values are passed as options.\n */\nmw.cx.SourcePageSelector.prototype.prefill = function () {\n\tif ( this.options.sourceLanguage ) {\n\t\tthis.languageFilter.setSourceLanguage( this.options.sourceLanguage );\n\t}\n\n\tif ( this.options.targetLanguage ) {\n\t\tthis.languageFilter.setTargetLanguage( this.options.targetLanguage );\n\t}\n\n\tif ( this.options.sourceTitle ) {\n\t\tthis.selectedSourcePage.setSourceTitle( this.options.sourceTitle );\n\t\tthis.pageSelector.setValue( this.options.sourceTitle );\n\t}\n\n\tif ( this.options.targetTitle ) {\n\t\tthis.selectedSourcePage.setTargetTitle( this.options.targetTitle );\n\t}\n\n\tif ( this.options.targetLanguage ) {\n\t\t// If all of the values are already present, show the dialog and initiate a validation.\n\t\tif ( this.options.sourceLanguage && this.options.sourceTitle ) {\n\t\t\tthis.pageSelector.lookupChooseFirstItem = true;\n\t\t\tthis.show();\n\t\t} else if ( this.options.targetTitle ) {\n\t\t\tthis.show();\n\t\t}\n\t}\n};\n\n/**\n * Listen for events.\n */\nmw.cx.SourcePageSelector.prototype.listen = function () {\n\tvar proxied;\n\t// Open or close the dialog when clicking the trigger link.\n\t// The dialog will be uninitialized until the first click.\n\tthis.triggerButton.connect( this, {\n\t\tclick: this.show\n\t} );\n\n\tthis.pageSelector.connect( this, { noResults: 'updateNoResultsMessage' } );\n\n\tthis.languageFilter.on( 'resize', this.pageSelector.positionLabel.bind( this.pageSelector ) );\n\n\tthis.discardButton.connect( this, { click: 'discardDialog' } );\n\tthis.pageSelector.onLookupMenuChoose = function ( source ) {\n\t\tthis.selectedSourcePage.setData(\n\t\t\tsource.getData(),\n\t\t\tsource.$label.prop( 'href' ),\n\t\t\t{\n\t\t\t\tnumOfLanguages: source.getNumberOfLanguages(),\n\t\t\t\timageUrl: source.imageUrl,\n\t\t\t\timageIcon: source.icon,\n\t\t\t\tsourceLanguage: this.languageFilter.getSourceLanguage(),\n\t\t\t\ttargetLanguage: this.languageFilter.getTargetLanguage()\n\t\t\t}\n\t\t);\n\t\tthis.$container.addClass( 'cx-source-page-selector--selected' );\n\t\tthis.selectedSourcePage.focusStartTranslationButton();\n\t}.bind( this );\n\n\tthis.$container.on( 'keydown', function ( e ) {\n\t\tif ( e.keyCode !== OO.ui.Keys.ESCAPE ) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.discardDialog();\n\t}.bind( this ) );\n\n\tproxied = this.pageSelector.lookupMenu.onDocumentKeyDownHandler;\n\tthis.pageSelector.lookupMenu.onDocumentKeyDownHandler = function ( e ) {\n\t\tif ( e.keyCode === OO.ui.Keys.TAB || e.keyCode === OO.ui.Keys.ESCAPE ) {\n\t\t\treturn;\n\t\t}\n\n\t\treturn proxied.apply( this, arguments );\n\t};\n};\n\n/**\n * Handle the document mouse up handler\n *\n * @param {boolean} bind Bind the mouse up handler, otherwise unbind\n */\nmw.cx.SourcePageSelector.prototype.toggleMouseUpHandler = function ( bind ) {\n\tif ( bind ) {\n\t\tdocument.addEventListener( 'mouseup', this.onDocumentMouseUpHandler, true );\n\t} else {\n\t\tdocument.removeEventListener( 'mouseup', this.onDocumentMouseUpHandler, true );\n\t}\n};\n\n/**\n * Handles document mouse up events.\n *\n * Mimics OO.ui.MenuSelectWidget\n * Usually this would be a mouse *down* handler, but as it causes a change\n * in the page height we use mouseup to avoid buttons moving in the middle\n * of a mouse click.\n *\n * @param {MouseEvent} e Mouse up event\n */\nmw.cx.SourcePageSelector.prototype.onDocumentMouseUp = function ( e ) {\n\tif (\n\t// Ignore clicks inside the selector and its lookupMenu & ULS popups\n\t\t!OO.ui.contains(\n\t\t\tthis.pageSelector.lookupMenu.$element.add( this.$container ).get(),\n\t\t\te.target,\n\t\t\ttrue\n\t\t) &&\n\t\t\t!$( e.target ).closest( '.uls-menu' ).length\n\t) {\n\t\tthis.discardDialog();\n\t}\n};\n\n/**\n * Updates the message displayed when there are no search results\n */\nmw.cx.SourcePageSelector.prototype.updateNoResultsMessage = function () {\n\tvar message = mw.msg( 'cx-source-page-selector-no-search-results',\n\t\tthis.pageSelector.getQueryValue(),\n\t\t$.uls.data.getAutonym( this.languageFilter.getSourceLanguage() )\n\t);\n\n\tthis.$noResultsMessage.text( message );\n};\n\n/**\n * Handles source language change.\n *\n * @param {string} language Language code.\n */\nmw.cx.SourcePageSelector.prototype.sourceLanguageChangeHandler = function ( language ) {\n\tthis.pageSelector.setLanguage( language );\n\n\tthis.pageSelector.toggle( true );\n\tthis.getExcludedSourceNamespaces( language )\n\t\t.then( this.pageSelector.setExcludedNamespaces.bind( this.pageSelector ) );\n};\n\n/**\n * Handles target language change.\n *\n * @param {string} language Language code.\n */\nmw.cx.SourcePageSelector.prototype.targetLanguageChangeHandler = function ( language ) {\n\tthis.pageSelector.setTargetLanguage( language );\n};\n\n/**\n * Show the SourcePageSelector.\n */\nmw.cx.SourcePageSelector.prototype.show = function () {\n\tthis.pageSelector.populateLookupMenu();\n\tthis.pageSelector.lookupMenu.toggle( true );\n\t// FIXME: Use CSS transition\n\t// eslint-disable-next-line no-jquery/no-slide\n\tthis.$container.slideDown( 'fast' );\n\tthis.pageSelector.focus();\n\tthis.pageSelector.positionLabel();\n\tthis.toggleMouseUpHandler( true );\n};\n\nmw.cx.SourcePageSelector.prototype.resetDialog = function () {\n\t// FIXME: Use CSS transition\n\t// eslint-disable-next-line no-jquery/no-slide\n\t$( '.cx-translation-filter' ).slideDown( 'fast' );\n\tthis.$container.removeClass( 'cx-source-page-selector--selected' ).toggle();\n\tthis.toggleMouseUpHandler( false );\n\n\tthis.pageSelector.setValue( '' );\n};\n\nmw.cx.SourcePageSelector.prototype.discardDialog = function () {\n\tthis.selectedSourcePage.discardDialog();\n};\n\nmw.cx.SourcePageSelector.prototype.render = function () {\n\tvar $searchResults, $noSuggestionsMessage;\n\n\tthis.$container.hide(); // Starts as hidden, shown on this.triggerButton click\n\n\t$noSuggestionsMessage = $( '<div>' )\n\t\t.addClass( 'cx-source-page-selector__no-suggestions-message' )\n\t\t.text( mw.msg( 'cx-source-page-selector-no-suggestions' ) );\n\n\tthis.$noResultsMessage = $( '<div>' )\n\t\t.addClass( 'cx-source-page-selector__search-message' );\n\n\t$searchResults = $( '<div>' )\n\t\t.addClass( 'cx-source-page-selector__search-results' )\n\t\t.append( $noSuggestionsMessage, this.$noResultsMessage );\n\n\tthis.languageFilter = new mw.cx.ui.LanguageFilter( {\n\t\tonSourceLanguageChange: this.sourceLanguageChangeHandler.bind( this ),\n\t\tonTargetLanguageChange: this.targetLanguageChangeHandler.bind( this ),\n\t\tupdateLocalStorage: true\n\t} );\n\n\tthis.discardButton = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\ticon: 'close',\n\t\tclasses: [ 'cx-source-page-selector__discard' ]\n\t} );\n\n\tthis.pageSelector = new mw.cx.ui.PageSelectorWidget( {\n\t\tclasses: [ 'cx-source-page-selector__page-title' ],\n\t\tlanguage: this.languageFilter.getSourceLanguage(),\n\t\ttargetLanguage: this.languageFilter.getTargetLanguage(),\n\t\tsiteMapper: this.siteMapper,\n\t\tvalue: this.options.sourceTitle,\n\t\tvalidateTitle: false,\n\t\tplaceholder: mw.msg( 'cx-source-page-selector-input-placeholder' ),\n\t\tallowSuggestionsWhenEmpty: true,\n\t\tshowRedirectTargets: true,\n\t\t$overlay: $searchResults,\n\t\t$container: $searchResults,\n\t\tlabel: this.languageFilter.$element.add( this.discardButton.$element )\n\t} );\n\n\tthis.selectedSourcePage = new mw.cx.SelectedSourcePage( this.siteMapper, {\n\t\tonDiscard: this.resetDialog.bind( this )\n\t} );\n\n\tthis.$container.append(\n\t\tthis.pageSelector.$element,\n\t\t$searchResults,\n\t\tthis.selectedSourcePage.$element\n\t);\n\n\tthis.getExcludedSourceNamespaces( this.languageFilter.getSourceLanguage() )\n\t\t.then( this.pageSelector.setExcludedNamespaces.bind( this.pageSelector ) );\n};\n\n/**\n * @param {string} sourceLanguage\n * @return {jQuery.Promise}\n */\nmw.cx.SourcePageSelector.prototype.getExcludedSourceNamespaces = function ( sourceLanguage ) {\n\tvar excludedNamespacesConfig = Object.keys(\n\t\tmw.config.get( 'wgContentTranslationExcludedNamespaces' ) || {}\n\t);\n\n\treturn this.siteMapper.getApi( sourceLanguage ).get( {\n\t\taction: 'query',\n\t\tmeta: 'siteinfo',\n\t\tsiprop: 'namespaces'\n\t} ).then( function ( response ) {\n\t\tvar isTalkPage, namespaceId, namespaceObj, excludedNamespaces = [];\n\n\t\tfor ( namespaceId in response.query.namespaces ) {\n\t\t\tnamespaceObj = response.query.namespaces[ namespaceId ];\n\t\t\t// Odd namespace ids are talk pages\n\t\t\tisTalkPage = ( namespaceId > 0 && namespaceId % 2 === 1 );\n\t\t\tif ( isTalkPage ||\n\t\t\t\texcludedNamespacesConfig.indexOf( namespaceObj.canonical ) >= 0\n\t\t\t) {\n\t\t\t\t// Exclude both the canonical name and localized name.\n\t\t\t\texcludedNamespaces.push( namespaceObj.canonical );\n\t\t\t\texcludedNamespaces.push( namespaceObj[ '*' ] );\n\t\t\t}\n\t\t}\n\n\t\treturn excludedNamespaces;\n\t} );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/stats/ext.cx.stats.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"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}],"suppressedMessages":[{"ruleId":"mediawiki/class-doc","severity":2,"message":"All possible CSS classes should be documented. See https://w.wiki/PS2 for details.","line":194,"column":6,"nodeType":"CallExpression","endLine":196,"endColumn":59,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-new","severity":2,"message":"Do not use 'new' for side effects.","line":649,"column":3,"nodeType":"ExpressionStatement","messageId":"noNewStatement","endLine":653,"endColumn":7,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-new","severity":2,"message":"Do not use 'new' for side effects.","line":706,"column":3,"nodeType":"ExpressionStatement","messageId":"noNewStatement","endLine":710,"endColumn":7,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-new","severity":2,"message":"Do not use 'new' for side effects.","line":744,"column":3,"nodeType":"ExpressionStatement","messageId":"noNewStatement","endLine":748,"endColumn":7,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-new","severity":2,"message":"Do not use 'new' for side effects.","line":792,"column":3,"nodeType":"ExpressionStatement","messageId":"noNewStatement","endLine":796,"endColumn":7,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":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 = 'cx-stats-trend-increase';\n\t\t} else {\n\t\t\tweekTrendClass = 'cx-stats-trend-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.text( '\\u00A0' ),\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.msg(\n\t\t\t'cx-trend-translations-to',\n\t\t\t$.uls.data.getAutonym( mw.config.get( 'wgContentLanguage' ) )\n\t\t) ) );\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 cx-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 cx-stats-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, $container;\n\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$( '.mw-body-content' ).append(\n\t\t\t$( '<div>' ).addClass( 'cx-widget' ).append(\n\t\t\t\t$container\n\t\t\t)\n\t\t);\n\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.validator.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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 ( title.indexOf( '|' ) !== -1 ) {\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\tmw.cx.ContentTranslationValidator = ContentTranslationValidator;\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/mw.cx.tools.InstructionsTool.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Instructions Tool\n *\n * @class\n * @extends mw.cx.tools.TranslationTool\n * @constructor\n * @param {mw.cx.dm.TranslationUnit} model\n * @param {Object} config\n */\nmw.cx.tools.InstructionsTool = function CXInstructionsTool( model, config ) {\n\tconfig.order = 30;\n\tmw.cx.tools.InstructionsTool.super.call( this, model, config );\n};\n\n/* Inheritance */\nOO.inheritClass( mw.cx.tools.InstructionsTool, mw.cx.tools.TranslationTool );\n\nmw.cx.tools.InstructionsTool.static.name = 'instructions';\n\n/**\n * @inheritDoc\n */\nmw.cx.tools.InstructionsTool.prototype.getActions = function () {\n\treturn [];\n};\n\n/**\n * @inheritDoc\n */\nmw.cx.tools.InstructionsTool.prototype.getContent = function () {\n\tvar i, count = 1,\n\t\trenderParams = {\n\t\t\thref: mw.msg( 'cx-tools-view-guidelines-link' ),\n\t\t\ttext: mw.msg( 'cx-tools-view-guidelines' )\n\t\t};\n\n\tfor ( i = 1; i <= 3; i++ ) {\n\t\trenderParams[ 'count' + i ] = mw.language.convertNumber( i );\n\t\t// The following messages are used here:\n\t\t// * cx-tools-instructions-text1\n\t\t// * cx-tools-instructions-text3\n\t\t// * cx-tools-instructions-text5\n\t\trenderParams[ 'heading' + i ] = mw.msg( 'cx-tools-instructions-text' + count++ );\n\t\t// The following messages are used here:\n\t\t// * cx-tools-instructions-text2\n\t\t// * cx-tools-instructions-text4\n\t\t// * cx-tools-instructions-text6\n\t\trenderParams[ 'description' + i ] = mw.msg( 'cx-tools-instructions-text' + count++ );\n\t}\n\treturn mw.template.get( 'mw.cx.tools.InstructionsTool', 'tools/instructions.mustache' )\n\t\t.render( renderParams );\n};\n\nmw.cx.tools.InstructionsTool.prototype.getData = function () {\n\treturn this.constructor.static.name + '::' + 1;\n};\n\n/* Register */\nmw.cx.tools.translationToolFactory.register( mw.cx.tools.InstructionsTool );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/mw.cx.tools.IssueTrackingTool.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"no-jquery/no-class-state","severity":2,"message":"Where possible, maintain application state in JS to avoid slower DOM queries","line":126,"column":15,"nodeType":"CallExpression","endLine":126,"endColumn":72,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-restricted-properties","severity":2,"message":"'parentElement' is restricted from being used. Prefer parentNode to parentElement as Node.parentElement is not supported by IE11.","line":206,"column":19,"nodeType":"MemberExpression","messageId":"restrictedProperty","endLine":206,"endColumn":46,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Issue tracking tool class\n *\n * @class\n * @constructor\n * @extends mw.cx.tools.TranslationTool\n */\nmw.cx.tools.IssueTrackingTool = function CXIssueTrackingTool() {\n\t// Parent constructor\n\tmw.cx.tools.IssueTrackingTool.super.call( this, null, {} ); // Third param is empty object to avoid JS errors\n\n\tthis.actionButtons = new OO.ui.ButtonGroupWidget();\n\n\tthis.numberOfIssues = 0;\n\tthis.numberOfWarnings = 0;\n\tthis.numberOfErrors = 0;\n\n\t// Array of plain objects with ID and translation issue model\n\tthis.allIssues = [];\n\n\tthis.currentIssue = 1;\n\tthis.currentNode = null;\n\n\tthis.expandButton = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\ticon: 'expand'\n\t} );\n\tthis.collapseButton = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\ticon: 'collapse'\n\t} );\n\tthis.numberOfIssuesLabel = new OO.ui.LabelWidget();\n\tthis.previousButton = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\tdisabled: true,\n\t\ticon: 'previous'\n\t} );\n\tthis.nextButton = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\ticon: 'next',\n\t\tclasses: [ 'cx-tools-linter-next-issue' ]\n\t} );\n\n\tthis.issuesLayout = new OO.ui.IndexLayout( {\n\t\texpanded: false,\n\t\tscrollable: false,\n\t\tautoFocus: false,\n\t\tclasses: [ 'cx-tools-linter-issues-layout' ]\n\t} );\n\tthis.warningsCount = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\tclasses: [ 'cx-tools-linter-warnings-count' ]\n\t} );\n\tthis.errorsCount = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\tclasses: [ 'cx-tools-linter-errors-count' ]\n\t} );\n\n\t// Events\n\tthis.expandButton.connect( this, { click: 'showExpanded' } );\n\tthis.collapseButton.connect( this, { click: 'showCollapsed' } );\n\tthis.previousButton.connect( this, { click: [ 'navigateIssues', -1 ] } );\n\tthis.nextButton.connect( this, { click: [ 'navigateIssues', 1 ] } );\n\n\tthis.card = this.getCard();\n\tthis.card.$header\n\t\t.addClass( 'mw-cx-tools-IssueTracking-head' )\n\t\t.empty()\n\t\t.append( this.getHeader() );\n\tthis.card.$element\n\t\t.addClass( 'mw-cx-tools-IssueTracking' )\n\t\t.on( 'mousedown', false );\n\tthis.card.$information\n\t\t.addClass( 'mw-cx-tools-IssueTracking-body' )\n\t\t.on( 'click', this.showExpanded.bind( this ) );\n\tthis.warningsCount.connect( this, { click: [ 'openFirstOfType', 'warning' ] } );\n\tthis.errorsCount.connect( this, { click: [ 'openFirstOfType', 'error' ] } );\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.tools.IssueTrackingTool, mw.cx.tools.TranslationTool );\n\n/* Static properties */\n\nmw.cx.tools.IssueTrackingTool.static.name = 'issues';\nmw.cx.tools.IssueTrackingTool.static.label = OO.ui.deferMsg( 'cx-tools-linter-issues' );\n\n/* Methods */\n\n/**\n * Initialize the card as collapsed or expanded and show the current panel, if possible.\n */\nmw.cx.tools.IssueTrackingTool.prototype.init = function () {\n\tvar currentPanel = this.getCurrentPanel();\n\n\tif ( !currentPanel ) {\n\t\tthis.showCollapsed();\n\t} else {\n\t\tthis.currentIssue = currentPanel;\n\t\tthis.showDetails();\n\t\tthis.openCurrentPanel();\n\t}\n};\n\n/**\n * Highlight the current node with higher opacity marker.\n */\nmw.cx.tools.IssueTrackingTool.prototype.updateCurrentNode = function () {\n\tvar node, $highlightableElement, hasErrors, id = this.allIssues[ this.currentIssue - 1 ].id;\n\n\tthis.removeCurrentNodeHighlight();\n\tthis.currentNode = null;\n\n\tif ( id === 'global' ) {\n\t\treturn;\n\t}\n\n\tnode = this.getNodeForId( id );\n\tif ( node ) {\n\t\tthis.currentNode = node;\n\t\t$highlightableElement = node.getHighlightableElement();\n\t\t// eslint-disable-next-line no-jquery/no-class-state\n\t\thasErrors = $highlightableElement.hasClass( 'mw-cx-lintIssue-error' );\n\t\t$highlightableElement.addClass( hasErrors ? 'mw-cx-current-issue-error' : 'mw-cx-current-issue-warning' );\n\t}\n};\n\n/**\n * Remove the higher opacity marker from the current node.\n */\nmw.cx.tools.IssueTrackingTool.prototype.removeCurrentNodeHighlight = function () {\n\tif ( this.currentNode ) {\n\t\tthis.currentNode.getHighlightableElement().removeClass( 'mw-cx-current-issue-warning mw-cx-current-issue-error' );\n\t}\n};\n\n/**\n * @inheritDoc\n */\nmw.cx.tools.IssueTrackingTool.prototype.getContent = function () {\n\treturn this.getBody();\n};\n\n/**\n * Get the index for the first issue of the current focused node.\n *\n * @return {number|boolean} Index of current panel or false\n */\nmw.cx.tools.IssueTrackingTool.prototype.getCurrentPanel = function () {\n\tvar i, length, issue, number,\n\t\tid = this.getCurrentNodeId();\n\n\tif ( !id ) {\n\t\treturn false;\n\t}\n\n\tif ( id === 'title' ) {\n\t\tnumber = id;\n\t} else {\n\t\tnumber = mw.cx.getSectionNumberFromSectionId( id );\n\t}\n\n\t// If last issue gets marked as resolved, we can end up with current issue being\n\t// bigger than the number of issues, which can break various expectations.\n\tif ( this.currentIssue > this.numberOfIssues ) {\n\t\treturn this.numberOfIssues;\n\t}\n\n\t// When issue is opened for current node, return that same card\n\tif ( this.allIssues[ this.currentIssue - 1 ].id === number ) {\n\t\treturn this.currentIssue;\n\t}\n\n\tfor ( i = 0, length = this.allIssues.length; i < length; i++ ) {\n\t\tissue = this.allIssues[ i ];\n\n\t\tif ( issue.id === number ) {\n\t\t\treturn i + 1;\n\t\t}\n\t}\n\n\treturn false;\n};\n\n/**\n * Get ID of currently active node\n *\n * @return {string}\n */\nmw.cx.tools.IssueTrackingTool.prototype.getCurrentNodeId = function () {\n\tvar activeElement = document.activeElement;\n\n\twhile ( activeElement ) {\n\t\tif ( activeElement.classList.contains( 've-ce-cxSectionNode' ) ) {\n\t\t\treturn activeElement.id;\n\t\t}\n\n\t\tif ( activeElement.type === 'textarea' && !activeElement.readOnly ) {\n\t\t\treturn 'title';\n\t\t}\n\n\t\t// eslint-disable-next-line no-restricted-properties\n\t\tactiveElement = activeElement.parentElement;\n\t}\n\n\treturn null;\n};\n\n/**\n * Create elements for card header.\n *\n * @return {jQuery[]}\n */\nmw.cx.tools.IssueTrackingTool.prototype.getHeader = function () {\n\tvar $title, $actions;\n\n\t$title = $( '<div>' )\n\t\t.addClass( 'mw-cx-tools-IssueTracking-title' )\n\t\t.append( new OO.ui.LabelWidget( { label: this.constructor.static.label } ).$element );\n\n\t$actions = $( '<div>' )\n\t\t.addClass( 'mw-cx-tools-IssueTracking-actions' )\n\t\t.append( this.actionButtons.$element );\n\n\treturn [ $title, $actions ];\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.tools.IssueTrackingTool.prototype.getData = function () {\n\treturn this.constructor.static.name;\n};\n\n/**\n * Render the body.\n *\n * @return {jQuery}\n */\nmw.cx.tools.IssueTrackingTool.prototype.getBody = function () {\n\t// IndexLayout offers config option showMenu, but does not respect setting it to false\n\tthis.issuesLayout.toggleMenu( false );\n\n\treturn this.errorsCount.$element\n\t\t.add( this.warningsCount.$element )\n\t\t.add( this.issuesLayout.$element );\n};\n\n/**\n * Register focus and blur events for nodes with issues.\n *\n * @param {ve.ce.CXSectionNode|mw.cx.ui.PageTitleWidget} node\n */\nmw.cx.tools.IssueTrackingTool.prototype.registerEvents = function ( node ) {\n\tvar onFocus = OO.ui.debounce( this.init.bind( this ) ),\n\t\tonBlur = OO.ui.debounce( this.onBlur.bind( this ) );\n\n\tif ( node.isFocusListenerAttached() ) {\n\t\treturn;\n\t}\n\n\tnode.connect( this, {\n\t\tfocus: onFocus,\n\t\tblur: onBlur\n\t} );\n\tnode.setFocusListenerAttached( true );\n\n\t// When all issues are resolved, disconnect the focus and blur listeners\n\tnode.getModel().connect( this, {\n\t\tallIssuesResolved: function () {\n\t\t\tnode.disconnect( this, {\n\t\t\t\tfocus: onFocus,\n\t\t\t\tblur: onBlur\n\t\t\t} );\n\t\t\tnode.setFocusListenerAttached( false );\n\t\t}.bind( this )\n\t} );\n};\n\n/**\n * Display the card as collapsed.\n */\nmw.cx.tools.IssueTrackingTool.prototype.showCollapsed = function () {\n\tthis.actionButtons.clearItems().addItems( [ this.expandButton ] );\n\n\tthis.issuesLayout.toggle( false );\n\tthis.warningsCount\n\t\t.toggle( this.numberOfWarnings > 0 )\n\t\t.setLabel( mw.msg( 'cx-tools-linter-warnings-count', this.numberOfWarnings ) );\n\tthis.errorsCount\n\t\t.toggle( this.numberOfErrors > 0 )\n\t\t.setLabel( mw.msg( 'cx-tools-linter-errors-count', this.numberOfErrors ) );\n\tthis.removeCurrentNodeHighlight();\n};\n\n/**\n * Display expanded card.\n */\nmw.cx.tools.IssueTrackingTool.prototype.showDetails = function () {\n\tthis.actionButtons.clearItems();\n\tif ( this.numberOfIssues > 1 ) {\n\t\tthis.actionButtons.addItems( [ this.numberOfIssuesLabel, this.previousButton, this.nextButton ] );\n\t}\n\tthis.actionButtons.addItems( [ this.collapseButton ] );\n\n\tthis.issuesLayout.toggle( true );\n\tthis.warningsCount.toggle( false );\n\tthis.errorsCount.toggle( false );\n};\n\n/**\n * Navigate to the next or previous issue.\n *\n * @param {number} increment +1 for next issue or -1 for the previous one\n */\nmw.cx.tools.IssueTrackingTool.prototype.navigateIssues = function ( increment ) {\n\tthis.currentIssue += increment;\n\tthis.focusCurrentElement();\n\tthis.openCurrentPanel();\n\n\tthis.correctFocus( increment );\n};\n\n/**\n * Open current panel, which is determined by this.currentIssue.\n * Set disabled state for previous/next button, as needed.\n */\nmw.cx.tools.IssueTrackingTool.prototype.openCurrentPanel = function () {\n\tthis.issuesLayout.setTabPanel( this.currentIssue - 1 );\n\tthis.numberOfIssuesLabel.setLabel(\n\t\tmw.msg( 'cx-tools-linter-issues-count', this.currentIssue, this.numberOfIssues )\n\t);\n\n\tthis.previousButton.setDisabled( false );\n\tthis.nextButton.setDisabled( false );\n\tif ( this.currentIssue === 1 ) {\n\t\tthis.previousButton.setDisabled( true );\n\t}\n\tif ( this.currentIssue === this.numberOfIssues ) {\n\t\tthis.nextButton.setDisabled( true );\n\t}\n\tthis.updateCurrentNode();\n};\n\n/**\n * Show detailed version of a card and focus on a current element.\n */\nmw.cx.tools.IssueTrackingTool.prototype.showExpanded = function () {\n\tthis.showDetails();\n\tthis.updateCurrentNode();\n\tthis.focusCurrentElement();\n\n\t// HACK: Used to avoid problems described in the comments for this.correctFocus()\n\tOO.ui.debounce( function () {\n\t\tthis.focusCurrentElement();\n\t} ).call( this );\n};\n\n/**\n * Show collapsed version of a card if current focused element doesn't have a translation issue.\n */\nmw.cx.tools.IssueTrackingTool.prototype.onBlur = function () {\n\tif ( !this.getCurrentPanel() ) {\n\t\tthis.showCollapsed();\n\t}\n};\n\nmw.cx.tools.IssueTrackingTool.prototype.focusCurrentElement = function () {\n\tvar node, id = this.allIssues[ this.currentIssue - 1 ].id;\n\n\t// If issue is global, i.e. not attached to any DOM node, we have nothing to focus\n\tif ( id === 'global' ) {\n\t\treturn;\n\t}\n\n\tnode = this.getNodeForId( id );\n\tif ( node ) {\n\t\tnode.getFocusableElement().focus();\n\t}\n};\n\n/**\n * This method is a trick used to give focus to the correct element, when focus goes from blurred\n * state to focus of some section inside VE editing surface. VE surface gives focus to the first\n * section in that case. See ve.ce.Surface#onDocumentFocus.\n *\n * This method essentially focuses the same node that tries to get focus in\n * this.focusCurrentElement, but gets its focus stolen by VE code.\n *\n * @param {number} increment +1 or -1 indicating what was the previous issue in navigation\n */\nmw.cx.tools.IssueTrackingTool.prototype.correctFocus = function ( increment ) {\n\tvar previousNode, currentNode, bluring, focusableElement,\n\t\tpreviousIssueId = this.allIssues[ this.currentIssue - increment - 1 ].id,\n\t\tcurrentIssueId = this.allIssues[ this.currentIssue - 1 ].id;\n\n\t// If current issue is global, we don't steal focus from the previous node, because\n\t// we don't have any node which we can focus for unattached issues.\n\tif ( currentIssueId === 'global' ) {\n\t\treturn;\n\t}\n\n\tpreviousNode = this.getNodeForId( previousIssueId );\n\tcurrentNode = this.getNodeForId( currentIssueId );\n\n\t// If previous issue was global, i.e. not attached to any DOM node, that is considerred\n\t// as case of blurred context, so correction of focus might be needed.\n\tbluring = ( previousIssueId === 'global' ) || ( previousNode && previousNode.blursEditingSurface() );\n\tfocusableElement = currentNode && currentNode.getFocusableElement();\n\n\tif ( bluring && focusableElement ) {\n\t\tOO.ui.debounce( function () {\n\t\t\tfocusableElement.focus();\n\t\t\tthis.showDetails();\n\t\t\tthis.openCurrentPanel();\n\t\t} ).call( this );\n\t}\n};\n\n/**\n * @param {number|string} id Section number or special values of 'title' and 'global'\n * @return {ve.ce.CXSectionNode|mw.cx.ui.PageTitleWidget|mw.cx.dm.Translation|null}\n */\nmw.cx.tools.IssueTrackingTool.prototype.getNodeForId = function ( id ) {\n\tvar node = this.getVeTarget().getTargetSectionElementFromSectionNumber( id );\n\n\tif ( !node && id === 'title' ) {\n\t\tnode = this.getVeTarget().translationView.targetColumn.getTitleWidget();\n\t}\n\n\tif ( !node && id === 'global' ) {\n\t\tnode = this.getVeTarget().getTranslation();\n\t}\n\n\treturn node;\n};\n\n/**\n * Getter for VE target global object.\n * We cannot assign this object in constructor because it is not yet available at that point.\n *\n * @return {ve.init.mw.CXTarget}\n */\nmw.cx.tools.IssueTrackingTool.prototype.getVeTarget = function () {\n\treturn ve.init.target;\n};\n\n/**\n * Go through the list of IDs of nodes with issues,\n * extract the translation issues and process them:\n * - Set number of warnings/errors and total issue count\n * - Register events for nodes with issues\n * - Set array of all issues, which is array of plain\n * objects, that have ID and model properties.\n *\n * @param {Mixed[]} nodesWithIssues IDs of nodes with issues\n */\nmw.cx.tools.IssueTrackingTool.prototype.showIssues = function ( nodesWithIssues ) {\n\tvar i, length, j, numOfIssues, id, node, issue, issues, allIssues = [];\n\n\tthis.numberOfIssues = 0;\n\tthis.numberOfErrors = 0;\n\tthis.numberOfWarnings = 0;\n\n\tfor ( i = 0, length = nodesWithIssues.length; i < length; i++ ) {\n\t\tid = nodesWithIssues[ i ];\n\t\tnode = this.getNodeForId( id );\n\n\t\tif ( !node ) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// If there are issues not stored to any DOM element, they're stored\n\t\t// in mw.cx.dm.Translation\n\t\tif ( id === 'global' ) {\n\t\t\tissues = node.getTranslationIssues();\n\t\t} else {\n\t\t\tissues = node.getModel().getTranslationIssues();\n\t\t}\n\n\t\tnumOfIssues = issues.length;\n\t\tthis.numberOfIssues += numOfIssues;\n\n\t\tfor ( j = 0; j < numOfIssues; j++ ) {\n\t\t\tissue = issues[ j ];\n\n\t\t\tif ( issue.getType() === 'error' ) {\n\t\t\t\tthis.numberOfErrors++;\n\t\t\t} else if ( issue.getType() === 'warning' ) {\n\t\t\t\tthis.numberOfWarnings++;\n\t\t\t}\n\n\t\t\tallIssues.push( {\n\t\t\t\tid: id,\n\t\t\t\tissue: issue\n\t\t\t} );\n\t\t}\n\n\t\t// Global issues aren't attached to any DOM node, so we cannot register events\n\t\tif ( id !== 'global' ) {\n\t\t\tthis.registerEvents( node );\n\t\t}\n\t}\n\n\tthis.allIssues = allIssues;\n\tissues = allIssues.map( function ( element, index ) {\n\t\treturn new mw.cx.ui.TranslationIssueWidget( index, element.issue );\n\t} );\n\tthis.issuesLayout.clearTabPanels().addTabPanels( issues );\n\tthis.numberOfIssuesLabel.setLabel(\n\t\tmw.msg( 'cx-tools-linter-issues-count', this.currentIssue, this.numberOfIssues )\n\t);\n\tthis.init();\n};\n\n/**\n * Open the details panel for translation issue with a given name.\n *\n * @param {string} name\n */\nmw.cx.tools.IssueTrackingTool.prototype.openIssueByName = function ( name ) {\n\tthis.openIssue( function ( issue ) {\n\t\treturn issue.getName() === name;\n\t} );\n};\n\n/**\n * Open the details panel for the first translation issue of a given type.\n *\n * @param {string} type\n */\nmw.cx.tools.IssueTrackingTool.prototype.openFirstOfType = function ( type ) {\n\tthis.openIssue( function ( issue ) {\n\t\treturn issue.getType() === type;\n\t} );\n};\n\n/**\n * Open the details panel for the first translation issue that satisfies the condition.\n *\n * @param {Function} checkCondition\n */\nmw.cx.tools.IssueTrackingTool.prototype.openIssue = function ( checkCondition ) {\n\tvar i, length, issueObj, index;\n\n\tfor ( i = 0, length = this.allIssues.length; i < length; i++ ) {\n\t\tissueObj = this.allIssues[ i ];\n\n\t\tif ( checkCondition( issueObj.issue ) ) {\n\t\t\tindex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tif ( index === undefined ) {\n\t\treturn;\n\t}\n\n\tthis.currentIssue = index + 1;\n\tthis.showExpanded();\n\tthis.openCurrentPanel();\n};\n\n/* Register */\nmw.cx.tools.translationToolFactory.register( mw.cx.tools.IssueTrackingTool );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/mw.cx.tools.TranslationTool.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Translation tool\n *\n * @class\n * @abstract\n * @constructor\n * @param {mw.cx.dm.TranslationUnit} model\n * @param {Object} config\n * @cfg {string} title The title to be displayed for the tool card. If missing, header wont be displayed\n * @cfg {string} language The language name to be displayed in header of tool card\n * @cfg {number} order The position of the card in tools column. Cards will be arranged in this order.\n */\nmw.cx.tools.TranslationTool = function CXTranslationTool( model, config ) {\n\t// Mixin constructor\n\tOO.EventEmitter.call( this );\n\n\tthis.card = null;\n\tthis.title = config.title;\n\tthis.language = config.language;\n\tthis.order = config.order;\n\tthis.model = model;\n\tthis.actions = [];\n};\n\n/* Setup */\nOO.mixinClass( mw.cx.tools.TranslationTool, OO.EventEmitter );\n\n/* Events */\n\n/**\n * @event contentChange\n *\n * A 'contentChange' event is emitted when the content for the card associated\n * with the tool changed. This is demanding a re-render of card\n */\n\n/**\n * @event actionsChange\n *\n * An 'actionsChange' event is emitted when the actions associated with the tool\n * changed while processing the data. This demands re-rendering of actions if any\n * shown in the card associated with the tool\n */\n\n/**\n * @event backgroundChange\n *\n * A 'backgroundChange' event is emitted when the background image for the card associated\n * with the tool changed. This is demanding a re-render of card's background image.\n */\n\n/* Methods */\n\n/**\n * Build the tool card widget associated with the current translation unit\n *\n * @return {mw.cx.widgets.TranslationToolWidget} The tool card\n */\nmw.cx.tools.TranslationTool.prototype.getCard = function () {\n\tthis.card = this.card || new mw.cx.widgets.TranslationToolWidget( this, {\n\t\ttitle: this.title,\n\t\tlanguage: this.language,\n\t\tname: this.constructor.static.name\n\t} );\n\treturn this.card;\n};\n\n/**\n * Get all possible actions with this tool widget\n *\n * @method\n * @return {OO.ui.Element[]} Array of OOUI Elements\n */\nmw.cx.tools.TranslationTool.prototype.getActions = function () {\n\treturn [];\n};\n\n/**\n * Get the optional background image for the card\n *\n * @return {string|null} The background image URL\n */\nmw.cx.tools.TranslationTool.prototype.getBackgroundImage = function () {\n\treturn null;\n};\n\n/**\n * Get the tools information content.\n *\n * @method\n * @return {string|jQuery} Content as HTML or jQuery\n */\nmw.cx.tools.TranslationTool.prototype.getContent = function () {\n\treturn $( [] );\n};\n\nmw.cx.tools.TranslationTool.prototype.getData = function () {\n\treturn this.constructor.static.name + '::' + this.model.getSectionId();\n};\n\n/**\n * Refresh the card rendering.\n */\nmw.cx.tools.TranslationTool.prototype.refresh = function () {\n\tvar card = this.getCard();\n\tif ( card ) {\n\t\tcard.render();\n\t}\n};\n\nmw.cx.tools.TranslationTool.prototype.destroy = function () {\n\tvar card = this.getCard();\n\tif ( card ) {\n\t\t// Remove will not work, until this is attached to DOM\n\t\tcard.$element.remove();\n\t\tcard.$element = $( [] );\n\t}\n\tdelete this;\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/tools/mw.cx.tools.TranslationToolFactory.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * TranslationTool Factory.\n *\n * @class\n * @abstract\n * @extends OO.Factory\n * @constructor\n */\nmw.cx.tools.TranslationToolFactory = function CXTranslationToolFactory() {\n\tmw.cx.tools.TranslationToolFactory.super.call( this );\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.tools.TranslationToolFactory, OO.Factory );\n\n/* Initialization */\n\nmw.cx.tools.translationToolFactory = new mw.cx.tools.TranslationToolFactory();\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/translation/ext.cx.translation.conflict.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation - Translation conflict handler\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\t/**\n\t * Show the information about translation conflict\n\t *\n\t * @param {Object} translation\n\t */\n\tfunction showConflictHandler( translation ) {\n\t\tvar message, $userPage;\n\n\t\tif ( translation.translatorName ) {\n\t\t\t$userPage = $( '<a>' )\n\t\t\t\t.text( translation.translatorName )\n\t\t\t\t.prop( 'href', mw.util.getUrl( 'User:' + translation.translatorName ) );\n\n\t\t\tmessage = mw.message( 'cx-translation-already-in-progress',\n\t\t\t\t$userPage,\n\t\t\t\ttranslation.translatorGender\n\t\t\t);\n\t\t} else {\n\t\t\tmessage = mw.message( 'cx-translation-already-in-progress-unknown' );\n\t\t}\n\n\t\treturn OO.ui.getWindowManager().openWindow( 'message', {\n\t\t\tmessage: message.parseDom(),\n\t\t\tactions: [\n\t\t\t\t{ action: 'cancel', label: mw.msg( 'cx-create-new-translation' ), flags: 'primary' }\n\t\t\t]\n\t\t} ).closed.then( function ( data ) {\n\t\t\tif ( !data || data.action === 'cancel' ) {\n\t\t\t\t// Go to dashboard\n\t\t\t\tvar uri = new mw.Uri();\n\n\t\t\t\tdelete uri.query.page;\n\t\t\t\tlocation.href = uri.toString();\n\t\t\t}\n\t\t} );\n\t}\n\n\t$( function () {\n\t\tmw.hook( 'mw.cx.translation.conflict' ).add( function ( translation ) {\n\t\t\tshowConflictHandler( translation );\n\t\t} );\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.ArticleColumn.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"jsdoc/check-param-names","severity":1,"message":"Duplicate @param \"config.sectionTitle\"","line":17,"column":null,"nodeType":"Block","endLine":17,"endColumn":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Article column container\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @abstract\n *\n * @param {Object} [config] Configuration\n * @param {string} config.title The title of the article for the column\n * @param {string|null} config.sectionTitle The title of the section for the column\n * @param {string} config.language The language of the article for the column\n * @param {mw.cx.SiteMapper} config.siteMapper SiteMapper instance\n * @param {string} [config.sectionTitle] The section title of the article for the column\n */\nmw.cx.ui.ArticleColumn = function ( config ) {\n\tvar languageLabel;\n\n\t// Configuration initialization\n\tconfig = $.extend( {}, config, {\n\t\tcontinuous: true,\n\t\tclasses: [ 'cx-column' ],\n\t\texpanded: false,\n\t\tscrollable: false\n\t} );\n\t// Parent constructor\n\tmw.cx.ui.ArticleColumn.super.call( this, config );\n\n\tthis.translation = null;\n\tthis.siteMapper = config.siteMapper;\n\tthis.language = config.language;\n\t// if this is a section translation\n\tif ( config.sectionTitle ) {\n\t\tthis.addItems( [\n\t\t\tnew OO.ui.LabelWidget( {\n\t\t\t\tlabel: config.title,\n\t\t\t\tdir: this.direction,\n\t\t\t\tclasses: [ 'cx-column-page-title' ]\n\t\t\t} )\n\t\t] );\n\t\tthis.titleWidget = new mw.cx.ui.SectionTitleWidget(\n\t\t\tnew mw.cx.dm.SectionTitleModel(),\n\t\t\t{ value: config.sectionTitle }\n\t\t);\n\t} else {\n\t\tthis.titleWidget = new mw.cx.ui.PageTitleWidget(\n\t\t\tnew mw.cx.dm.PageTitleModel(),\n\t\t\t{ value: config.title }\n\t\t);\n\t}\n\tthis.direction = $.uls.data.getDir( config.language );\n\n\tlanguageLabel = new OO.ui.LabelWidget( {\n\t\tlabel: $.uls.data.getAutonym( config.language ),\n\t\tdir: this.direction,\n\t\tclasses: [ 'cx-column-language-label' ]\n\t} );\n\n\tthis.subHeading = new OO.ui.HorizontalLayout( {\n\t\tclasses: [ 'cx-column-sub-heading' ],\n\t\titems: [ languageLabel ]\n\t} );\n\n\tthis.$element.prop( {\n\t\tlang: this.language,\n\t\tdir: this.direction\n\t} );\n\n\tthis.addItems( [ this.titleWidget, this.subHeading ] );\n\n\tthis.$content = $( '<div>' ).addClass( 'cx-column__content' );\n\tthis.$element.append( this.$content );\n};\n\n/* Setup */\n\nOO.inheritClass( mw.cx.ui.ArticleColumn, OO.ui.StackLayout );\n\n/**\n * Attach the source surface's element to the DOM\n *\n * @param {ve.ui.CXSourceSurface} surface The source surface\n */\nmw.cx.ui.ArticleColumn.prototype.attachSurface = function ( surface ) {\n\tthis.$content.empty().append( surface.$element );\n};\n\n/**\n * Set the category count UI element\n *\n * @param {jQuery} $categoryCount\n */\nmw.cx.ui.ArticleColumn.prototype.setCategoryCount = function ( $categoryCount ) {\n\tthis.$content.before( $categoryCount );\n};\n\n/**\n * Set the category list UI element\n *\n * @param {jQuery} $categoryListing\n */\nmw.cx.ui.ArticleColumn.prototype.setCategoryListing = function ( $categoryListing ) {\n\tthis.$content.after( $categoryListing );\n};\n\n/**\n * Set the translation data model\n *\n * @param {mw.cx.dm.Translation} translation\n */\nmw.cx.ui.ArticleColumn.prototype.setTranslation = function ( translation ) {\n\tthis.translation = translation;\n};\n\n/**\n * Set the main title for the column.\n *\n * @param {string} title\n */\nmw.cx.ui.ArticleColumn.prototype.setTitle = function ( title ) {\n\tthis.titleWidget.setValue( title );\n\tthis.titleWidget.validateTitle( title );\n};\n\n/**\n * Get the current main title for the column.\n *\n * @return {string}\n */\nmw.cx.ui.ArticleColumn.prototype.getTitle = function () {\n\treturn this.titleWidget.getValue();\n};\n\n/**\n * @return {mw.cx.ui.PageTitleWidget}\n */\nmw.cx.ui.ArticleColumn.prototype.getTitleWidget = function () {\n\treturn this.titleWidget;\n};\n\nmw.cx.ui.ArticleColumn.prototype.isSectionTranslation = function () {\n\treturn this.titleWidget instanceof mw.cx.ui.SectionTitleWidget;\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.CaptchaDialog.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"mediawiki/msg-doc","severity":2,"message":"All possible message keys should be documented. See https://w.wiki/4r9a for details.","line":139,"column":22,"nodeType":"CallExpression","endLine":139,"endColumn":43,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Dialog for displaying CAPTCHA for Content Translation.\n * Note that methods are not safe to call before the dialog has initialized.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @extends OO.ui.ProcessDialog\n *\n * @constructor\n * @param {Object} [config] Configuration options\n */\nmw.cx.ui.CaptchaDialog = function ( config ) {\n\t// Parent constructor\n\tmw.cx.ui.CaptchaDialog.super.call( this, config );\n\n\t// Initialization\n\tthis.$element.addClass( 'mw-cx-ui-captcha-dialog' );\n\tthis.$intro = $( '<p>' ).addClass( 'mw-cx-ui-captcha-dialog-intro' );\n\tthis.$captchaContent = $( '<div>' ).addClass( 'mw-cx-ui-captcha-dialog-content' );\n\tthis.input = new OO.ui.TextInputWidget( {\n\t\tclasses: [ 'mw-cx-ui-captcha-dialog-input' ]\n\t} );\n\n\t// Publish on \"Enter\" key in captcha input field, as it is single line.\n\tthis.input.connect( this, { enter: function () {\n\t\tthis.executeAction( 'publish' );\n\t}.bind( this ) } );\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.ui.CaptchaDialog, OO.ui.ProcessDialog );\n\n/* Static Properties */\n\nmw.cx.ui.CaptchaDialog.static.name = 'cxCaptcha';\n\nmw.cx.ui.CaptchaDialog.static.title = mw.msg( 'cx-publish-captcha-title' );\n\nmw.cx.ui.CaptchaDialog.static.actions = [\n\t{ action: 'publish', label: mw.msg( 'cx-publish-button' ), flags: [ 'primary', 'progressive' ] },\n\t{ action: 'cancel', label: mw.msg( 'cx-captcha-dialog-cancel' ), flags: 'safe' }\n];\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.CaptchaDialog.prototype.initialize = function () {\n\t// Parent method\n\tmw.cx.ui.CaptchaDialog.super.prototype.initialize.call( this );\n\n\tthis.$body.append( this.$errors.empty(), this.$intro, this.$captchaContent, this.input.$element );\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.CaptchaDialog.prototype.getReadyProcess = function () {\n\tthis.input.setValue( '' ).focus();\n\n\t// Parent method\n\treturn mw.cx.ui.CaptchaDialog.super.prototype.getReadyProcess.call( this, arguments );\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.CaptchaDialog.prototype.getActionProcess = function ( action ) {\n\treturn new OO.ui.Process( function () {\n\t\tthis.clearContent();\n\t\tthis.close();\n\t\tthis.emit( action );\n\t}, this );\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.CaptchaDialog.prototype.onDialogKeyDown = function ( e ) {\n\tif ( e.which === OO.ui.Keys.ESCAPE && this.constructor.static.escapable ) {\n\t\tthis.executeAction( 'cancel' );\n\t\te.preventDefault();\n\t\te.stopPropagation();\n\t\treturn;\n\t}\n\n\treturn mw.cx.ui.CaptchaDialog.super.prototype.onDialogKeyDown.call( this, e );\n};\n\n/**\n * Override parent showErrors() method to display errors inline and not inside overlay.\n *\n * @param {string} error\n */\nmw.cx.ui.CaptchaDialog.prototype.showErrors = function ( error ) {\n\tthis.$errorItems = $( '<div>' )\n\t\t.addClass( 'oo-ui-processDialog-error' )\n\t\t.text( error );\n\n\tthis.$errors\n\t\t.removeClass( 'oo-ui-element-hidden' )\n\t\t.append( this.$errorItems );\n};\n\nmw.cx.ui.CaptchaDialog.prototype.clearContent = function () {\n\tthis.$intro.empty();\n\tthis.$captchaContent.empty();\n};\n\nmw.cx.ui.CaptchaDialog.prototype.initIntro = function () {\n\tthis.$intro.append(\n\t\t$( '<strong>' ).text( mw.msg( 'captcha-label' ) ),\n\t\tdocument.createTextNode( mw.msg( 'colon-separator' ) )\n\t);\n};\n\n/**\n * @param {string} message Message key for intro sentence.\n * @param {string} question Plain string of HTML string of captcha question.\n * @param {string} mimeType Mime type of question (plain text or HTML).\n */\nmw.cx.ui.CaptchaDialog.prototype.setCaptcha = function ( message, question, mimeType ) {\n\tthis.initIntro();\n\n\tif ( mimeType === 'text/html' ) {\n\t\tquestion = $.parseHTML( question );\n\t} else if ( mimeType === 'text/plain' ) {\n\t\tquestion = document.createTextNode( question );\n\t} else {\n\t\tmw.log.error( '[CX] Unexpected mime type for captcha: ' + mimeType );\n\t\treturn;\n\t}\n\n\t// eslint-disable-next-line mediawiki/msg-doc\n\tthis.$intro.append( mw.message( message ).parseDom(), '<br>', question );\n};\n\n/**\n * Display the FancyCaptcha.\n *\n * @param {string} url URL to the image captcha.\n */\nmw.cx.ui.CaptchaDialog.prototype.setFancyCaptcha = function ( url ) {\n\tvar $captchaImg,\n\t\treloadButton = new OO.ui.ButtonWidget( {\n\t\t\tframed: false,\n\t\t\tclasses: [ 'fancycaptcha-reload' ],\n\t\t\tlabel: mw.msg( 'fancycaptcha-reload-text' ),\n\t\t\ticon: 'reload',\n\t\t\tflags: 'progressive'\n\t\t} );\n\n\tthis.initIntro();\n\n\tthis.$intro.append(\n\t\t$( mw.message( 'fancycaptcha-create' ).parseDom() )\n\t\t\t.filter( 'a' ).prop( 'target', '_blank' ).end()\n\t);\n\n\t$captchaImg = $( '<img>' )\n\t\t.prop( 'src', url )\n\t\t.addClass( 'fancycaptcha-image' )\n\t\t.on( 'load', this.updateSize.bind( this ) );\n\tthis.$captchaContent.addClass( 'fancycaptcha-captcha-container' ).append(\n\t\t$captchaImg,\n\t\t' ',\n\t\treloadButton.$element\n\t);\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.Categories.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * CX Categories UI component.\n *\n * @class\n * @constructor\n *\n * @param {mw.cx.dm.Translation} translationModel\n * @param {Object} [config] Configuration options\n */\nmw.cx.ui.Categories = function ( translationModel, config ) {\n\tthis.translationModel = translationModel;\n\tthis.config = config;\n\n\t// @var {OO.ui.ButtonWidget}\n\tthis.sourceCategoryCount = null;\n\t// @var {OO.ui.ButtonWidget}\n\tthis.targetCategoryCount = null;\n\t// @var {mw.cx.ui.CategoryMultiselectWidget}\n\tthis.sourceCategoryListing = null;\n\t// @var {mw.cx.ui.CategoryMultiselectWidget}\n\tthis.targetCategoryListing = null;\n\n\tthis.render();\n\tthis.listen();\n};\n\n/**\n * Example: \"Category:Cakes\" -> \"Cakes\"\n *\n * @param {string} fullCategoryName Category name with namespace prefix\n * @return {string} Category name, without namespace prefix\n */\nmw.cx.ui.Categories.prototype.removeCategoryNamespace = function ( fullCategoryName ) {\n\t// mw.Title cannot be used because of T106644\n\tvar match;\n\n\tmatch = fullCategoryName.match( /^.+?:(.*)$/ );\n\tif ( !match ) {\n\t\tmw.log.error( '[CX] Expected category page title, got ' + fullCategoryName );\n\t\treturn fullCategoryName;\n\t}\n\treturn match[ 1 ];\n};\n\n/**\n * Create UI elements for category count buttons and category listings.\n */\nmw.cx.ui.Categories.prototype.render = function () {\n\tvar sourceCategories = this.translationModel.getSourceCategories(),\n\t\ttargetCategories = this.translationModel.getTargetCategories();\n\n\tthis.sourceCategoryCount = this.createCategoryCount( sourceCategories.length );\n\tthis.targetCategoryCount = this.createCategoryCount( targetCategories.length );\n\n\tthis.sourceCategoryListing = this.createCategoryListing(\n\t\tsourceCategories,\n\t\ttrue,\n\t\t{\n\t\t\tallowReordering: false,\n\t\t\tinputPosition: 'none'\n\t\t}\n\t);\n\tthis.targetCategoryListing = this.createCategoryListing(\n\t\ttargetCategories,\n\t\tfalse,\n\t\t{\n\t\t\tallowedValues: targetCategories,\n\t\t\tinputPosition: 'outline'\n\t\t}\n\t);\n\n\tthis.checkForEmptySourceCategories();\n\tthis.checkForEmptyTargetCategories();\n\tthis.addMissingCategoriesToMenu();\n};\n\n/**\n * Create the category count button. To be displayed above source and translation contents.\n *\n * @param {number} count\n * @return {OO.ui.ButtonWidget}\n */\nmw.cx.ui.Categories.prototype.createCategoryCount = function ( count ) {\n\treturn new OO.ui.ButtonWidget( {\n\t\tlabel: mw.msg( 'cx-tools-categories-count-message', mw.language.convertNumber( count ) ),\n\t\ticon: 'tag',\n\t\tframed: false\n\t} );\n};\n\n/**\n * @return {jQuery}\n */\nmw.cx.ui.Categories.prototype.getSourceCategoryCount = function () {\n\treturn this.sourceCategoryCount.$element;\n};\n\n/**\n * @return {jQuery}\n */\nmw.cx.ui.Categories.prototype.getTargetCategoryCount = function () {\n\treturn this.targetCategoryCount.$element;\n};\n\n/**\n * @param {Array} categories\n * @return {Array} Array of objects with data (full category name) and label (category name) properties\n */\nmw.cx.ui.Categories.prototype.mapCategories = function ( categories ) {\n\treturn categories.map( function ( category ) {\n\t\treturn {\n\t\t\tdata: category,\n\t\t\tlabel: this.removeCategoryNamespace( category )\n\t\t};\n\t}, this );\n};\n\n/**\n * Get the category listing\n *\n * @param {Array} categories\n * @param {boolean} isSource True if source category listing should be created.\n * @param {Object} [config]\n * @return {mw.cx.ui.CategoryMultiselectWidget}\n */\nmw.cx.ui.Categories.prototype.createCategoryListing = function ( categories, isSource, config ) {\n\tvar categoryItems = this.mapCategories( categories );\n\n\treturn new mw.cx.ui.CategoryMultiselectWidget( $.extend( {\n\t\ticon: 'tag',\n\t\toptions: isSource ? categoryItems : [],\n\t\tselected: categoryItems.map( function ( item ) {\n\t\t\treturn {\n\t\t\t\tlabel: item.label,\n\t\t\t\tdata: item.data,\n\t\t\t\tconfig: {\n\t\t\t\t\tdraggable: !isSource,\n\t\t\t\t\thideRemoveButton: isSource,\n\t\t\t\t\tdisabled: isSource && !this.translationModel.getCorrespondingTargetCategory( item.data )\n\t\t\t\t}\n\t\t\t};\n\t\t}, this ),\n\t\tclasses: [ 'cx-category-listing' ]\n\t}, this.config, config ) );\n};\n\n/**\n * @return {jQuery}\n */\nmw.cx.ui.Categories.prototype.getSourceCategoryListing = function () {\n\treturn this.sourceCategoryListing.$element;\n};\n\n/**\n * @return {jQuery}\n */\nmw.cx.ui.Categories.prototype.getTargetCategoryListing = function () {\n\treturn this.targetCategoryListing.$element;\n};\n\n/**\n * Set disabled state for all category UI elements.\n *\n * @param {boolean} state\n */\nmw.cx.ui.Categories.prototype.disableCategoryUI = function ( state ) {\n\tthis.sourceCategoryCount.setDisabled( state );\n\tthis.targetCategoryCount.setDisabled( state );\n\tthis.sourceCategoryListing.setDisabled( state );\n\tthis.targetCategoryListing.setDisabled( state );\n};\n\n/**\n * Event handling\n */\nmw.cx.ui.Categories.prototype.listen = function () {\n\tthis.sourceCategoryCount.connect( this, {\n\t\tclick: function () {\n\t\t\tthis.sourceCategoryListing.scrollElementIntoView();\n\t\t}\n\t} );\n\tthis.targetCategoryCount.connect( this, {\n\t\tclick: function () {\n\t\t\tthis.targetCategoryListing.scrollElementIntoView();\n\t\t}\n\t} );\n\n\tthis.sourceCategoryListing.connect( this, {\n\t\titemSelect: 'onSourceCategoryClick',\n\t\tmouseEnter: [ 'toggleCategoryHighlight', this.targetCategoryListing, true ],\n\t\tmouseLeave: [ 'toggleCategoryHighlight', this.targetCategoryListing, false ]\n\t} );\n\n\tthis.targetCategoryListing.connect( this, {\n\t\tchange: 'onTargetCategoriesChange',\n\t\tmouseEnter: [ 'toggleCategoryHighlight', this.sourceCategoryListing, true ],\n\t\tmouseLeave: [ 'toggleCategoryHighlight', this.sourceCategoryListing, false ],\n\t\titemRemove: [ 'toggleCategoryHighlight', this.sourceCategoryListing, false ]\n\t} );\n};\n\n/**\n * @param {mw.cx.ui.CategoryTagItemWidget} sourceTagItem\n */\nmw.cx.ui.Categories.prototype.onSourceCategoryClick = function ( sourceTagItem ) {\n\tvar sourceCategoryName = sourceTagItem.getData(),\n\t\ttargetCategoryName = this.translationModel.getCorrespondingTargetCategory( sourceCategoryName );\n\n\tthis.targetCategoryListing.addTag(\n\t\ttargetCategoryName,\n\t\tthis.removeCategoryNamespace( targetCategoryName )\n\t);\n};\n\n/**\n * @param {mw.cx.ui.CategoryMultiselectWidget} categoryListing\n * @param {boolean} toggle Toggle highlight on/off (true/false)\n * @param {mw.cx.ui.CategoryTagItemWidget} tagItem\n */\nmw.cx.ui.Categories.prototype.toggleCategoryHighlight = function ( categoryListing, toggle, tagItem ) {\n\tvar correspondingTagItem,\n\t\tcategoryName = tagItem.getData(),\n\t\tcorrespondingCategoryName =\n\t\t\tthis.translationModel.getCorrespondingTargetCategory( categoryName ) ||\n\t\t\tthis.translationModel.getCorrespondingSourceCategory( categoryName );\n\n\tcorrespondingTagItem = categoryListing.findItemFromData( correspondingCategoryName );\n\n\tif ( !correspondingTagItem ) {\n\t\treturn;\n\t}\n\n\tif ( toggle ) {\n\t\ttagItem.setFlags( 'highlight' );\n\t\tcorrespondingTagItem.setFlags( 'highlight' );\n\t} else {\n\t\ttagItem.clearFlags();\n\t\tcorrespondingTagItem.clearFlags();\n\t}\n};\n\n/**\n * @param {Array} selectedTargetCategories\n */\nmw.cx.ui.Categories.prototype.onTargetCategoriesChange = function ( selectedTargetCategories ) {\n\tvar targetCategories = selectedTargetCategories.map( function ( item ) {\n\t\treturn item.data;\n\t} );\n\n\tthis.targetCategoryCount.setLabel(\n\t\tmw.msg(\n\t\t\t'cx-tools-categories-count-message',\n\t\t\tmw.language.convertNumber( targetCategories.length )\n\t\t)\n\t);\n\n\tthis.translationModel.setTargetCategories( targetCategories );\n\tthis.addMissingCategoriesToMenu();\n\tthis.checkForEmptyTargetCategories();\n};\n\n/**\n * Check if source categories listing is empty and display the informative message.\n */\nmw.cx.ui.Categories.prototype.checkForEmptySourceCategories = function () {\n\tif ( this.sourceCategoryListing.getItemCount() === 0 ) {\n\t\tthis.sourceCategoryListing.setLabel( mw.msg( 'cx-no-source-categories' ) );\n\t}\n};\n\n/**\n * Display the informative message if target categories listing is empty.\n */\nmw.cx.ui.Categories.prototype.checkForEmptyTargetCategories = function () {\n\tif ( this.targetCategoryListing.getItemCount() > 0 ) {\n\t\tthis.targetCategoryListing.setLabel( '' );\n\t\treturn;\n\t}\n\n\tif ( this.sourceCategoryListing.allDisabled() ) {\n\t\tthis.targetCategoryListing.setLabel( mw.msg( 'cx-no-adapted-categories' ) );\n\t} else {\n\t\tthis.targetCategoryListing.setLabel( mw.msg( 'cx-no-target-categories' ) );\n\t}\n};\n\n/**\n * Find difference between all adapted target categories and selected adapted target categories.\n * Used to populate \"Add category\" menu in target category listing when input query is empty.\n */\nmw.cx.ui.Categories.prototype.addMissingCategoriesToMenu = function () {\n\tvar missingCategories = this.translationModel.getRemovedCategories();\n\n\tthis.targetCategoryListing.menu.clearItems();\n\tthis.targetCategoryListing.addOptions( this.mapCategories( missingCategories ) );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.Infobar.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * @class\n * @param {Object} [config] Configuration object\n */\nmw.cx.ui.Infobar = function ( config ) {\n\t// Configuration initialization\n\tthis.config = $.extend( {}, config, {\n\t\tcontinuous: true,\n\t\texpanded: false,\n\t\t$content: this.getContent(),\n\t\tclasses: [ 'cx-header-infobar' ]\n\t} );\n\t// Parent constructor\n\tmw.cx.ui.Infobar.super.call( this, this.config );\n\tthis.listen();\n};\n\n/* Setup */\n\nOO.inheritClass( mw.cx.ui.Infobar, OO.ui.PanelLayout );\n\nmw.cx.ui.Infobar.prototype.getContent = function () {\n\tthis.messageLayout = new OO.ui.FieldsetLayout();\n\treturn this.messageLayout.$element;\n};\n\nmw.cx.ui.Infobar.prototype.listen = function () {\n\tmw.hook( 'mw.cx.error' ).add( this.showError.bind( this ) );\n\tmw.hook( 'mw.cx.warning' ).add( this.showWarning.bind( this ) );\n\tmw.hook( 'mw.cx.success' ).add( this.showSuccess.bind( this ) );\n\tmw.hook( 'mw.cx.error.anonuser' ).add( this.showLoginMessage.bind( this ) );\n\tmw.hook( 'mw.cx.translation.title.change' ).add( this.clearMessages.bind( this ) );\n};\n\nmw.cx.ui.Infobar.prototype.showSuccess = function ( message, details ) {\n\tthis.showMessage( 'success', message, details );\n};\n\nmw.cx.ui.Infobar.prototype.showError = function ( message, details ) {\n\tthis.showMessage( 'error', message, details );\n};\n\nmw.cx.ui.Infobar.prototype.showWarning = function ( message, details ) {\n\tthis.showMessage( 'warning', message, details );\n};\n\n/**\n * Shows a message in the info bar.\n *\n * @param {string} type Message class.\n * @param {mw.Message|string} message Message objects are parsed, strings are plain text.\n * @param {mw.Message|string} details The details of error in HTML.\n * @param {Mixed} data Custom data of any type or combination of types (e.g., string, number, array, object).\n * @param {OO.ui.ButtonWidget[]} buttons Additional buttons to add to the message widget.\n */\nmw.cx.ui.Infobar.prototype.showMessage = function ( type, message, details, data, buttons ) {\n\tvar messageWidget;\n\n\tmessageWidget = new mw.cx.ui.MessageWidget( {\n\t\tmessage: message,\n\t\tdetails: details,\n\t\ttype: type,\n\t\tdata: data,\n\t\tbuttons: buttons\n\t} );\n\tthis.clearMessages();\n\tthis.messageLayout.addItems( [ messageWidget ] );\n};\n\n/**\n * Show login message.\n */\nmw.cx.ui.Infobar.prototype.showLoginMessage = function () {\n\tthis.showError( mw.message( 'cx-special-login-error', mw.cx.getLoginHref() ) );\n\n\t// Do not show the columns\n\t// TODO: use events\n\t$( '.cx-widget__columns' ).remove();\n};\n\nmw.cx.ui.Infobar.prototype.clearMessages = function () {\n\tthis.messageLayout.clearItems();\n};\n\n/**\n * @param {Mixed} messageData\n */\nmw.cx.ui.Infobar.prototype.removeMessage = function ( messageData ) {\n\tvar item = this.messageLayout.findItemFromData( messageData );\n\n\tif ( item ) {\n\t\tthis.messageLayout.removeItems( [ item ] );\n\t}\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.LanguageFilter.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n// Register 'All languages' in ULS data\n// 'all' could be valid language code, so we use extension mechanism and go with 'x-all'\n$.uls.data.addLanguage( 'x-all', {\n\tscript: 'Latn',\n\tregions: [ 'WW' ],\n\tautonym: mw.msg( 'cx-translation-filter-uls-all-languages' )\n} );\n\n/**\n * Language filter\n *\n * @class\n * @extends OO.ui.Widget\n * @param {Object} [config] Configuration object\n * @cfg {boolean} [canBeSame=false] True if source and target language can be the same language\n * @cfg {boolean} [canBeUndefined=false] True if source or target language can be unset\n * @cfg {boolean} [updateLocalStorage=false] True if this language selector can update local storage,\n * when source or target language changes\n * @cfg {Function} [onSourceLanguageChange] Callback invoked when source language changes\n * @cfg {Function} [onTargetLanguageChange] Callback invoked when target language changes\n */\nmw.cx.ui.LanguageFilter = function ( config ) {\n\t// Configuration initialization\n\tthis.config = config || {};\n\n\t// Parent method\n\tmw.cx.ui.LanguageFilter.super.call( this, config );\n\n\tthis.canBeSame = this.config.canBeSame || false;\n\tthis.canBeUndefined = this.config.canBeUndefined || false;\n\tthis.updateLocalStorage = this.config.updateLocalStorage || false;\n\tthis.onSourceLanguageChange = this.config.onSourceLanguageChange;\n\tthis.onTargetLanguageChange = this.config.onTargetLanguageChange;\n\t// Represents all possible source/target language codes\n\tthis.sourceLanguages = null;\n\tthis.targetLanguages = null;\n\t// this.sourceLanguage and this.targetLanguage are selected source/target languages respectively\n\tthis.sourceLanguage = null;\n\tthis.targetLanguage = null;\n\tthis.sourceLanguageButton = null;\n\tthis.targetLanguageButton = null;\n\n\tthis.narrowLimit = 700;\n\tthis.isNarrowScreenSize = false;\n\n\tthis.init();\n};\n\nOO.inheritClass( mw.cx.ui.LanguageFilter, OO.ui.Widget );\n\n/* Static Properties */\n\n/**\n * @static\n */\nmw.cx.ui.LanguageFilter.static.sourceLanguages = null;\nmw.cx.ui.LanguageFilter.static.targetLanguages = null;\n\n/* Methods */\n\nmw.cx.ui.LanguageFilter.prototype.init = function () {\n\tvar sourceLanguage = mw.storage.get( 'cxSourceLanguage' ),\n\t\ttargetLanguage = mw.storage.get( 'cxTargetLanguage' );\n\n\tthis.isNarrowScreenSize = document.documentElement.clientWidth < this.narrowLimit;\n\n\tif ( !this.canBeUndefined ) {\n\t\tthis.sourceLanguage = sourceLanguage;\n\t\tthis.targetLanguage = targetLanguage;\n\t}\n\n\t// Load source languages into instance variable from static variable\n\tthis.sourceLanguages = this.constructor.static.sourceLanguages;\n\tthis.targetLanguages = this.constructor.static.targetLanguages;\n\n\tthis.render();\n\tthis.setFilterLabel( this.sourceLanguageButton, this.sourceLanguage );\n\tthis.setFilterLabel( this.targetLanguageButton, this.targetLanguage );\n\tthis.listen();\n};\n\n/**\n * Return an object of languages indexed by language code.\n *\n * @param {Array} languages An array of language codes.\n * @return {Object} autonyms indexed by language code.\n */\nmw.cx.ui.LanguageFilter.prototype.getAutonyms = function ( languages ) {\n\treturn languages.reduce( function ( prevObject, element ) {\n\t\tprevObject[ element ] = $.uls.data.getAutonym( element );\n\n\t\treturn prevObject;\n\t}, {} );\n};\n\n/**\n * Calculate position for ULS, to display it next to the language filter\n * and use the appropriate size, not to go outside of viewport.\n */\nmw.cx.ui.LanguageFilter.prototype.calculateUlsPosition = function () {\n\tvar ulsTriggerLeft = this.$element.offset().left,\n\t\ttriggerWidth = this.$element.outerWidth(),\n\t\tmenuWidth = this.$menu.width(),\n\t\tisRtl = $( 'html' ).prop( 'dir' ) === 'rtl',\n\t\tleft = isRtl ? ulsTriggerLeft : ( ulsTriggerLeft + triggerWidth - menuWidth ),\n\t\tisInsideViewport = isRtl ? ( left + menuWidth ) < document.documentElement.clientWidth : left > 0;\n\n\tif ( isInsideViewport ) {\n\t\tthis.left = left;\n\t\tthis.$menu.css( this.position() );\n\t\treturn;\n\t}\n\n\tthis.menuWidth = this.getMenuWidth() === 'wide' ? 'medium' : 'narrow';\n\tthis.recreateLanguageFilter();\n\t// The following classes are used here:\n\t// * uls-medium\n\t// * uls-narrow\n\tthis.$menu\n\t\t.removeClass( 'uls-wide uls-medium uls-narrow' )\n\t\t.addClass( 'uls-' + this.menuWidth );\n\n\t// HACK: This is a recursive call to this function, because this\n\t// method is registered as onVisible when ULS menu is created.\n\tthis.visible();\n};\n\n/**\n * Check whether a language is available as a target language\n * for the specified source language.\n *\n * @param {string} targetLanguage A language code.\n * @return {boolean} true if the target language is valid for the source language.\n */\nmw.cx.ui.LanguageFilter.prototype.isValidTarget = function ( targetLanguage ) {\n\treturn this.targetLanguages.indexOf( targetLanguage ) !== -1;\n};\n\n/**\n * Check whether a language is available as a source language\n * for the specified target language.\n *\n * @param {string} sourceLanguage A language code.\n * @return {boolean} true if the target language is valid for the source language.\n */\nmw.cx.ui.LanguageFilter.prototype.isValidSource = function ( sourceLanguage ) {\n\treturn this.sourceLanguages.indexOf( sourceLanguage ) !== -1;\n};\n\nmw.cx.ui.LanguageFilter.prototype.setValidSourceLanguages = function ( sourceLanguages ) {\n\tthis.sourceLanguages = sourceLanguages;\n};\n\n/**\n * Get the current source language code.\n *\n * @return {string} Language code. Empty string if not set.\n */\nmw.cx.ui.LanguageFilter.prototype.getSourceLanguage = function () {\n\treturn this.sourceLanguage;\n};\n\n/**\n * Sets the source language.\n *\n * @param {string} language A language code\n */\nmw.cx.ui.LanguageFilter.prototype.setSourceLanguage = function ( language ) {\n\tvar currentSource, i, length, quickListLanguages, quickListLang;\n\n\tif ( language === 'x-all' ) {\n\t\tlanguage = null;\n\t}\n\n\t// Do not allow selection of invalid source languages, unless specified with canBeUndefined tag\n\tif ( ( !this.canBeUndefined && !this.isValidSource( language ) ) || language === this.getSourceLanguage() ) {\n\t\treturn;\n\t}\n\n\t// Don't let the same languages be selected as source and target.\n\t// Instead, do what the user probably means: either swap them if\n\t// it's valid, or pick the first of the common languages in ULS.\n\tif ( !this.canBeSame && language === this.getTargetLanguage() ) {\n\t\tcurrentSource = this.getSourceLanguage();\n\n\t\tif ( this.isValidTarget( currentSource ) ) {\n\t\t\tthis.sourceLanguage = language;\n\t\t\tthis.setTargetLanguage( currentSource );\n\t\t} else {\n\t\t\tquickListLanguages = this.targetLanguageButton.$button.data( 'uls' ).options.quickList();\n\t\t\tfor ( i = 0, length = quickListLanguages.length; i < length; i++ ) {\n\t\t\t\tquickListLang = quickListLanguages[ i ];\n\n\t\t\t\tif ( this.isValidTarget( quickListLang ) && quickListLang !== language ) {\n\t\t\t\t\tthis.setTargetLanguage( quickListLang );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// If we still don't have a valid source language, return,\n\t// so we prevent same source and target language\n\tif ( !this.canBeSame && language === this.getTargetLanguage() ) {\n\t\treturn;\n\t}\n\n\tthis.sourceLanguage = language;\n\tthis.setFilterLabel( this.sourceLanguageButton, this.sourceLanguage );\n\n\tif ( this.updateLocalStorage ) {\n\t\tmw.storage.set( 'cxSourceLanguage', this.sourceLanguage );\n\t}\n\n\tif ( this.onSourceLanguageChange ) {\n\t\tthis.onSourceLanguageChange( this.sourceLanguage );\n\t}\n};\n\n/**\n * Set source language and update Language filter label without any checks and side effects on\n * local storage or invoked callbacks when language changes\n *\n * @param {string} language Language code\n */\nmw.cx.ui.LanguageFilter.prototype.setSourceLanguageNoChecks = function ( language ) {\n\tthis.sourceLanguage = language;\n\tthis.setFilterLabel( this.sourceLanguageButton, this.sourceLanguage );\n};\n\n/**\n * Get the current target language code.\n *\n * @return {string} Language code. Empty string if not set.\n */\nmw.cx.ui.LanguageFilter.prototype.getTargetLanguage = function () {\n\treturn this.targetLanguage;\n};\n\n/**\n * Sets the target language.\n *\n * @param {string} language A language code\n */\nmw.cx.ui.LanguageFilter.prototype.setTargetLanguage = function ( language ) {\n\tvar currentTarget, i, length, quickListLanguages, quickListLang;\n\n\tif ( language === 'x-all' ) {\n\t\tlanguage = null;\n\t}\n\n\tif ( ( !this.canBeUndefined && !this.isValidTarget( language ) ) || this.targetLanguage === language ) {\n\t\treturn;\n\t}\n\n\t// Don't let the same languages be selected as source and target.\n\t// Instead, do what the user probably means: either swap them if\n\t// it's valid, or pick the first valid language of the common languages in ULS.\n\tif ( !this.canBeSame && language === this.getSourceLanguage() ) {\n\t\tcurrentTarget = this.getTargetLanguage();\n\n\t\tif ( this.isValidSource( currentTarget ) ) {\n\t\t\tthis.targetLanguage = language;\n\t\t\tthis.setSourceLanguage( currentTarget );\n\t\t} else {\n\t\t\t// When swapping languages, there can sometimes be only one available source language,\n\t\t\t// and we don't have options to swap languages.\n\t\t\t// This can rarely happen, but we return early to prevent\n\t\t\t// any errors and unexpected behavior in code that uses this class.\n\t\t\tif ( this.sourceLanguages.length === 1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tquickListLanguages = this.sourceLanguageButton.$button.data( 'uls' ).options.quickList();\n\t\t\tfor ( i = 0, length = quickListLanguages.length; i < length; i++ ) {\n\t\t\t\tquickListLang = quickListLanguages[ i ];\n\n\t\t\t\tif ( this.isValidSource( quickListLang ) && quickListLang !== language ) {\n\t\t\t\t\tthis.setSourceLanguage( quickListLang );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// If we still don't have a valid source language, return,\n\t// so we prevent same source and target language\n\tif ( !this.canBeSame && language === this.getSourceLanguage() ) {\n\t\treturn;\n\t}\n\n\tthis.targetLanguage = language;\n\tthis.setFilterLabel( this.targetLanguageButton, this.targetLanguage );\n\n\tif ( this.updateLocalStorage ) {\n\t\tmw.storage.set( 'cxTargetLanguage', this.targetLanguage );\n\t}\n\n\tif ( this.onTargetLanguageChange ) {\n\t\tthis.onTargetLanguageChange( this.targetLanguage );\n\t}\n};\n\n/**\n * Set target language and update Language filter label without any checks and side effects on\n * local storage or invoked callbacks when language changes.\n *\n * @param {string} language Language code\n */\nmw.cx.ui.LanguageFilter.prototype.setTargetLanguageNoChecks = function ( language ) {\n\tthis.targetLanguage = language;\n\tthis.setFilterLabel( this.targetLanguageButton, this.targetLanguage );\n};\n\n/**\n * Set the label for language filter button that acts as a trigger for displaying ULS language filter.\n *\n * @param {OO.ui.ButtonWidget} filterButton\n * @param {string} language\n */\nmw.cx.ui.LanguageFilter.prototype.setFilterLabel = function ( filterButton, language ) {\n\tvar langProps, label;\n\n\tif ( this.canBeUndefined && !language ) {\n\t\tfilterButton.setLabel( mw.msg( 'cx-translation-filter-label-all-languages' ) );\n\t\treturn;\n\t}\n\n\tlangProps = {\n\t\tlang: language,\n\t\tdir: $.uls.data.getDir( language )\n\t};\n\tlabel = this.isNarrowScreenSize ?\n\t\tmw.language.bcp47( language ) :\n\t\t$.uls.data.getAutonym( language );\n\n\tfilterButton.$button.prop( langProps );\n\tfilterButton.setLabel( label );\n\n\tthis.emit( 'resize' );\n};\n\n/**\n * Fill the source language dropdown with source languages for which selected article exists\n *\n * @param {Array} [sourceLanguages] Array of language codes used to populate ULS\n * @param {boolean} [replace=false] Whether to destroy the ULS instance before recreating it\n * @param {Object} [ulsOptions] ULS options that are added to the ULS\n */\nmw.cx.ui.LanguageFilter.prototype.fillSourceLanguages = function ( sourceLanguages, replace, ulsOptions ) {\n\tvar self = this;\n\n\t// Default to all valid source languages\n\tif ( !sourceLanguages ) {\n\t\tsourceLanguages = this.constructor.static.sourceLanguages;\n\t}\n\n\tif ( replace ) {\n\t\t// Delete the old source ULS data\n\t\tthis.sourceLanguageButton.$button.data( 'uls', null );\n\t\tthis.sourceLanguageButton.$button.off( 'click' );\n\t}\n\n\tthis.sourceLanguageButton.$button.uls( $.extend( {\n\t\tlanguages: this.getAutonyms( sourceLanguages ),\n\t\tulsPurpose: 'cx-languagefilter-source',\n\t\tonSelect: function ( language ) {\n\t\t\tself.setSourceLanguage( language );\n\t\t\tmw.uls.addPreviousLanguage( language );\n\t\t},\n\t\tonReady: function () {\n\t\t\tthis.$menu.addClass( 'cx-language-filter-source-language' );\n\t\t},\n\t\tonVisible: this.calculateUlsPosition,\n\t\tquickList: function () {\n\t\t\treturn mw.uls.getFrequentLanguageList().filter( function ( n ) {\n\t\t\t\treturn sourceLanguages.indexOf( n ) !== -1;\n\t\t\t} );\n\t\t}\n\t}, ulsOptions ) );\n};\n\n/**\n * Fill the target language dropdown with target languages that have\n * language tools compatible with the source language.\n *\n * @param {Array} [targetLanguages] Array of language codes used to populate ULS\n * @param {boolean} [replace=false] Whether to destroy the ULS instance before recreating it\n * @param {Object} [ulsOptions] ULS options that are added to the ULS\n */\nmw.cx.ui.LanguageFilter.prototype.fillTargetLanguages = function ( targetLanguages, replace, ulsOptions ) {\n\tvar self = this;\n\n\t// Default to all valid target languages\n\tif ( !targetLanguages ) {\n\t\ttargetLanguages = this.constructor.static.targetLanguages;\n\t}\n\n\tif ( replace ) {\n\t\t// Delete the old target ULS data\n\t\tthis.targetLanguageButton.$button.data( 'uls', null );\n\t\tthis.targetLanguageButton.$button.off( 'click' );\n\t}\n\n\tthis.targetLanguageButton.$button.uls( $.extend( {\n\t\tlanguages: this.getAutonyms( targetLanguages ),\n\t\tulsPurpose: 'cx-languagefilter-target',\n\t\tonSelect: function ( language ) {\n\t\t\tself.setTargetLanguage( language );\n\t\t\tmw.uls.addPreviousLanguage( language );\n\t\t},\n\t\tonReady: function () {\n\t\t\tthis.$menu.addClass( 'cx-language-filter-target-language' );\n\t\t},\n\t\tonVisible: this.calculateUlsPosition,\n\t\tquickList: function () {\n\t\t\treturn mw.uls.getFrequentLanguageList().filter( function ( n ) {\n\t\t\t\treturn targetLanguages.indexOf( n ) !== -1;\n\t\t\t} );\n\t\t},\n\t\tlanguageDecorator: this.config.languageDecorator\n\t}, ulsOptions ) );\n};\n\nmw.cx.ui.LanguageFilter.prototype.render = function () {\n\tthis.sourceLanguageButton = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\tclasses: [ 'cx-language-filter-source-language' ],\n\t\t$button: $( '<a>' ).addClass( 'cx-language-filter-button' )\n\t} );\n\n\tthis.targetLanguageButton = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\tclasses: [ 'cx-language-filter-target-language' ],\n\t\t$button: $( '<a>' ).addClass( 'cx-language-filter-button' )\n\t} );\n\n\tthis.fillSourceLanguages( this.sourceLanguages );\n\tthis.fillTargetLanguages( this.targetLanguages );\n\n\tthis.$element\n\t\t.addClass( 'cx-language-filter' )\n\t\t.append(\n\t\t\tthis.sourceLanguageButton.$element,\n\t\t\t$( '<div>' ).addClass( 'cx-language-filter-arrow' ),\n\t\t\tthis.targetLanguageButton.$element\n\t\t);\n};\n\nmw.cx.ui.LanguageFilter.prototype.listen = function () {\n\tthis.sourceLanguageButton.$button.on( {\n\t\tkeypress: function () {\n\t\t\tthis.click();\n\t\t}\n\t} );\n\tthis.targetLanguageButton.$button.on( {\n\t\tkeypress: function () {\n\t\t\tthis.click();\n\t\t}\n\t} );\n\n\t// Resize handler\n\t$( window ).on( 'resize', OO.ui.throttle( this.resize.bind( this ), 250 ) );\n};\n\nmw.cx.ui.LanguageFilter.prototype.resize = function () {\n\tvar isNarrowScreenSize = document.documentElement.clientWidth < this.narrowLimit;\n\n\t// Exit early if screen size stays above/under narrow screen size limit\n\tif ( this.isNarrowScreenSize === isNarrowScreenSize ) {\n\t\treturn;\n\t}\n\n\tthis.isNarrowScreenSize = isNarrowScreenSize;\n\tthis.setFilterLabel( this.sourceLanguageButton, this.sourceLanguage );\n\tthis.setFilterLabel( this.targetLanguageButton, this.targetLanguage );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.LoginDialog.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Login dialog, which does not close on button click, but when user starts new session.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @constructor\n * @extends OO.ui.MessageDialog\n *\n * @param {Object} [config] Parent class configuration options\n */\nmw.cx.ui.LoginDialog = function () {\n\tthis.intervalTimer = null;\n\tthis.bindedListener = null;\n\n\t// Parent constructor\n\tmw.cx.ui.LoginDialog.parent.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.ui.LoginDialog, OO.ui.MessageDialog );\n\n/* Static Properties */\n\n/**\n * @static\n * @inheritdoc\n */\nmw.cx.ui.LoginDialog.static.name = 'login';\n\n/**\n * @static\n * @inheritdoc\n */\nmw.cx.ui.LoginDialog.static.title = OO.ui.deferMsg( 'cx-lost-session' );\n\n/**\n * @static\n * @inheritdoc\n */\nmw.cx.ui.LoginDialog.static.message = OO.ui.deferMsg( 'cx-lost-session-draft' );\n\n/**\n * @static\n * @inheritdoc\n */\nmw.cx.ui.LoginDialog.static.actions = [\n\t{ action: 'login', label: OO.ui.deferMsg( 'login' ), flags: [ 'primary' ] }\n];\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.LoginDialog.prototype.getActionProcess = function ( action ) {\n\tthis.removeExistingListeners();\n\t// We need to store method which uses Function.prototype.bind() inside variable,\n\t// so that we can detach it later by using removeEventListener()\n\tthis.bindedListener = this.onDocumentVisibilityChange.bind( this, action );\n\n\treturn new OO.ui.Process( function () {\n\t\t// Open user login special page inside a new tab\n\t\twindow.open( mw.util.getUrl( 'Special:UserLogin' ), '_blank' );\n\t\t// Check login status immediately when user returns to this tab\n\t\tdocument.addEventListener( 'visibilitychange', this.bindedListener );\n\t\t// Check periodically if user logged back in\n\t\tthis.intervalTimer = setInterval( this.closeIfLoggedIn.bind( this, action ), 5000 );\n\t}, this );\n};\n\n/**\n * @param {string} action Symbolic name of action\n */\nmw.cx.ui.LoginDialog.prototype.onDocumentVisibilityChange = function ( action ) {\n\tif ( document.visibilityState === 'visible' ) {\n\t\tthis.closeIfLoggedIn( action );\n\t}\n};\n\n/**\n * @param {string} action Symbolic name of action\n * @return {jQuery.Promise}\n */\nmw.cx.ui.LoginDialog.prototype.closeIfLoggedIn = function ( action ) {\n\treturn new mw.Api()\n\t\t.get( { assert: 'user' } )\n\t\t.then( function () {\n\t\t\tthis.close( { action: action } );\n\t\t\tthis.removeExistingListeners();\n\t\t}.bind( this ) );\n};\n\n/**\n * Remove 'visibilitychange' listener and stop checking periodically if user logged back in.\n */\nmw.cx.ui.LoginDialog.prototype.removeExistingListeners = function () {\n\tclearInterval( this.intervalTimer );\n\tdocument.removeEventListener( 'visibilitychange', this.bindedListener );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.SourceColumn.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"jsdoc/check-param-names","severity":1,"message":"@param path declaration (\"config.siteMapper\") appears before any real parameter.","line":6,"column":null,"nodeType":"Block","endLine":6,"endColumn":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Source article container\n *\n * @class\n * @extends mw.cx.ui.ArticleColumn\n * @param {mw.cx.SiteMapper} config.siteMapper\n * @param {string} config.language\n * @param {string} config.title\n * @param {string|null} config.sectionTitle\n * @param {Object} [config] Configuration object\n */\nmw.cx.ui.SourceColumn = function ( config ) {\n\tvar articleLink;\n\n\t// Parent constructor\n\tmw.cx.ui.SourceColumn.super.apply( this, arguments );\n\n\tthis.$element.addClass( 'cx-column--source' );\n\n\tthis.loading = true;\n\tthis.$loadingIndicator = null;\n\n\t// Try to load Cite styles. Silently ignored if not installed.\n\tif ( mw.loader.getState( 'ext.cite.style' ) !== null ) {\n\t\tmw.loader.load( 'ext.cite.style' );\n\t}\n\tmw.hook( 'mw.cx.error' ).add( this.removeLoadingIndicator.bind( this ) );\n\n\tthis.titleWidget\n\t\t.setReadOnly( true )\n\t\t.setValidation( null );\n\n\tvar linkHref = this.siteMapper.getPageUrl(\n\t\tthis.language,\n\t\tconfig.title,\n\t\tnull,\n\t\tconfig.sectionTitle && mw.util.escapeIdForLink( config.sectionTitle )\n\t);\n\tarticleLink = new OO.ui.ButtonWidget( {\n\t\tlabel: mw.msg( 'cx-source-view-page' ),\n\t\thref: linkHref,\n\t\ttarget: '_blank',\n\t\tclasses: [ 'cx-column-sub-heading-view-page' ],\n\t\tframed: false,\n\t\tflags: [ 'progressive' ]\n\t} );\n\n\tthis.subHeading.addItems( [ articleLink ] );\n\n\tthis.showLoadingIndicator();\n};\n\n/* Setup */\n\nOO.inheritClass( mw.cx.ui.SourceColumn, mw.cx.ui.ArticleColumn );\n\n/**\n * @param {jQuery} $translationUnit\n * @param {number} position\n */\nmw.cx.ui.SourceColumn.prototype.add = function ( $translationUnit, position ) {\n\tthis.insertAt( position, $translationUnit );\n};\n\nmw.cx.ui.SourceColumn.prototype.insertAt = function ( index, $element ) {\n\tvar lastIndex = this.$content.children().length;\n\n\tif ( index < 0 ) {\n\t\tindex = Math.max( 0, lastIndex + 1 + index );\n\t}\n\tthis.$content.append( $element );\n\tif ( index < lastIndex ) {\n\t\tthis.$content.children().eq( index ).before( this.$content.children().last() );\n\t}\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.SourceColumn.prototype.setTranslation = function () {\n\t// Parent method\n\tmw.cx.ui.TargetColumn.super.prototype.setTranslation.apply( this, arguments );\n\n\tthis.removeLoadingIndicator();\n};\n\nmw.cx.ui.SourceColumn.prototype.showLoadingIndicator = function () {\n\tvar $title, userLanguage, $loadingIndicatorContent,\n\t\t$loadingIndicatorSpinner;\n\n\tthis.$loadingIndicator = $( '<div>' )\n\t\t.addClass( 'cx-column__loading-indicator' );\n\n\t$title = $( '<span>' )\n\t\t.prop( {\n\t\t\tlang: this.language,\n\t\t\tdir: this.direction\n\t\t} )\n\t\t.text( this.getTitle() );\n\n\tuserLanguage = mw.config.get( 'wgUserLanguage' );\n\t$loadingIndicatorContent = $( '<div>' )\n\t\t.prop( {\n\t\t\tlang: userLanguage,\n\t\t\tdir: $.uls.data.getDir( userLanguage )\n\t\t} )\n\t\t.addClass( 'cx-column__loading-indicator--text' )\n\t\t.append( mw.message( 'cx-source-loading', $title ).parseDom() );\n\n\t$loadingIndicatorSpinner = mw.cx.widgets.spinner();\n\tthis.$loadingIndicator.append( $loadingIndicatorSpinner, $loadingIndicatorContent );\n\tthis.$element.append( this.$loadingIndicator );\n};\n\nmw.cx.ui.SourceColumn.prototype.removeLoadingIndicator = function () {\n\tthis.loading = false;\n\tthis.$loadingIndicator.remove();\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.TargetColumn.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Target column\n *\n * @class\n * @extends mw.cx.ui.ArticleColumn\n * @param {Object} [config] Configuration object\n * @param {mw.cx.SiteMapper} [config.siteMapper]\n * @param {string} [config.language]\n * @param {string} [config.title]\n * @param {string|null} [config.sectionTitle]\n */\nmw.cx.ui.TargetColumn = function ( config ) {\n\t// Parent constructor\n\tmw.cx.ui.TargetColumn.super.apply( this, arguments );\n\n\t// Propagate title change events\n\tif ( !this.isSectionTranslation() ) {\n\t\t// if this is an article translation, the title widget is an instance\n\t\t// of PageTitleWidget and \"titleChange\" event should be fired on change\n\t\tthis.titleWidget.connect( this, { change: [ 'emit', 'titleChange' ] } );\n\t} else {\n\t\t// if this is a section translation, the title widget is an instance\n\t\t// of SectionTitleWidget and \"sectionTitleChange\" event should be fired on change\n\t\tthis.titleWidget.connect( this, { change: [ 'emit', 'sectionTitleChange' ] } );\n\n\t\tvar linkHref = this.siteMapper.getPageUrl( this.language, config.title );\n\n\t\tvar articleLink = new OO.ui.ButtonWidget( {\n\t\t\tlabel: mw.msg( 'cx-target-view-page' ),\n\t\t\thref: linkHref,\n\t\t\ttarget: '_blank',\n\t\t\tclasses: [ 'cx-column-sub-heading-view-page' ],\n\t\t\tframed: false,\n\t\t\tflags: [ 'progressive' ]\n\t\t} );\n\n\t\tthis.subHeading.addItems( [ articleLink ] );\n\t}\n\n\tthis.$element.addClass( 'cx-column--translation' );\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.ui.TargetColumn, mw.cx.ui.ArticleColumn );\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.TargetColumn.prototype.setTranslation = function () {\n\t// Parent method\n\tmw.cx.ui.TargetColumn.super.prototype.setTranslation.apply( this, arguments );\n\n\tif ( !this.isSectionTranslation() ) {\n\t\tthis.setTitle( this.translation.getTargetTitle() );\n\t}\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.ToolsColumn.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Tools column\n *\n * @class\n * @param {Object} [config] Configuration object\n */\nmw.cx.ui.ToolsColumn = function ( config ) {\n\tthis.config = config;\n\n\tthis.editingToolbarContainer = new OO.ui.PanelLayout( {\n\t\tclasses: [ 'cx-tools-editing-toolbar-container', 'cx-card' ],\n\t\texpanded: false,\n\t\tframed: false,\n\t\tpadded: false\n\t} );\n\n\tthis.mtToolbarContainer = new OO.ui.PanelLayout( {\n\t\tclasses: [ 'cx-tools-mt-toolbar-container', 'cx-card' ],\n\t\texpanded: false,\n\t\tframed: false,\n\t\tpadded: false\n\t} );\n\n\tthis.toolContainer = new OO.ui.StackLayout( {\n\t\tcontinuous: true,\n\t\tclasses: [ 'cx-column-tools-container' ],\n\t\texpanded: false,\n\t\tscrollable: false,\n\t\tpadded: false,\n\t\titems: [ this.editingToolbarContainer, this.mtToolbarContainer ]\n\t} );\n\n\tthis.issueCard = mw.cx.tools.translationToolFactory.create( 'issues' );\n\n\t// Configuration initialization\n\tthis.config = $.extend( {}, config, {\n\t\tcontinuous: true,\n\t\tclasses: [ 'cx-column', 'cx-column-tools' ],\n\t\texpanded: false,\n\t\tscrollable: false,\n\t\tpadded: false,\n\t\titems: [ this.toolContainer ]\n\t} );\n\tthis.translation = null;\n\t// Parent constructor\n\tmw.cx.ui.ToolsColumn.super.call( this, this.config );\n\tthis.init();\n};\n\n/* Setup */\n\nOO.inheritClass( mw.cx.ui.ToolsColumn, OO.ui.StackLayout );\n\nmw.cx.ui.ToolsColumn.prototype.init = function () {\n\tmw.loader.using( [\n\t\t'mw.cx.tools.InstructionsTool'\n\t], function () {\n\t\tmw.log( '[CX] Initializing translation tool system' );\n\t\tthis.showInstructions();\n\t}.bind( this ) );\n\n};\n\n/**\n * Set the translation data model\n *\n * @param {mw.cx.dm.Translation} translation\n */\nmw.cx.ui.ToolsColumn.prototype.setTranslation = function ( translation ) {\n\tthis.translation = translation;\n};\n\n/**\n * Show the instructions card when translation is loaded.\n */\nmw.cx.ui.ToolsColumn.prototype.showInstructions = function () {\n\tvar instructions = mw.cx.tools.translationToolFactory.create(\n\t\t'instructions', this, this.config\n\t);\n\n\tthis.showTool( instructions );\n};\n\n/**\n * Show translation issues.\n *\n * @param {Mixed[]} nodesWithIssues IDs of nodes with issues\n */\nmw.cx.ui.ToolsColumn.prototype.showIssues = function ( nodesWithIssues ) {\n\tthis.issueCard.showIssues( nodesWithIssues );\n\tthis.showTool( this.issueCard );\n};\n\nmw.cx.ui.ToolsColumn.prototype.hideIssues = function () {\n\tif ( this.issueCard ) {\n\t\tthis.hideTool( this.issueCard );\n\t}\n};\n\n/**\n * Show the tools associated with the translationUnit.\n *\n * @param {mw.cx.ui.TranslationUnit} translationUnit\n */\nmw.cx.ui.ToolsColumn.prototype.showTools = function ( translationUnit ) {\n\tvar i, tools;\n\n\ttools = translationUnit.getTools();\n\tfor ( i = 0; i < tools.length; i++ ) {\n\t\tthis.toolContainer.addItems( [ tools[ i ].getCard() ] );\n\t}\n};\n\n/**\n * Show a single tool in tools container.\n * Avoid duplicates by checking if the tool is already in the container.\n *\n * @param {mw.cx.tools.TranslationTool} tool The translation tool instance\n */\nmw.cx.ui.ToolsColumn.prototype.showTool = function ( tool ) {\n\tif ( !this.toolContainer.findItemsFromData( tool.getData() ).length ) {\n\t\tthis.toolContainer.addItems( [ tool.getCard() ] );\n\t}\n};\n\n/**\n * Hide this given tool\n *\n * @param {mw.cx.tools.TranslationTool} tool The translation tool instance\n */\nmw.cx.ui.ToolsColumn.prototype.hideTool = function ( tool ) {\n\tvar items = this.toolContainer.findItemsFromData( tool.getData() );\n\tthis.toolContainer.removeItems( items );\n};\n\n/**\n * Hide all tools shown in the tools container\n */\nmw.cx.ui.ToolsColumn.prototype.hideAllTools = function () {\n\tthis.toolContainer.clearItems();\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.TranslationHeader.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"mediawiki/class-doc","severity":2,"message":"All possible CSS classes should be documented. See https://w.wiki/PS2 for details.","line":18,"column":74,"nodeType":"ObjectExpression","endLine":23,"endColumn":3,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * TranslationView Header\n *\n * @class\n * @extends OO.ui.PanelLayout\n * @param {Object} [config] Configuration object\n */\nmw.cx.ui.TranslationHeader = function ( config ) {\n\t// Configuration initialization\n\tthis.config = config || {};\n\tthis.statusbar = null;\n\tthis.$toolbar = null;\n\n\t// Parent constructor\n\t// eslint-disable-next-line mediawiki/class-doc\n\tmw.cx.ui.TranslationHeader.super.call( this, $.extend( {}, this.config, {\n\t\tcontinuous: true,\n\t\texpanded: false,\n\t\t$content: this.getContent(),\n\t\tclasses: [ 'cx-translation-view-header' ].concat( this.config.classes )\n\t} ) );\n\n\tthis.listen();\n};\n\n/* Setup */\n\nOO.inheritClass( mw.cx.ui.TranslationHeader, OO.ui.PanelLayout );\n\nmw.cx.ui.TranslationHeader.prototype.getContent = function () {\n\tvar translationCenter = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\ticon: 'previous',\n\t\tclasses: [ 'cx-header__translation-center' ],\n\t\tlabel: mw.msg( 'cx-header-all-translations' ),\n\t\thref: mw.util.getUrl( 'Special:ContentTranslation' )\n\t} );\n\n\tthis.statusbar = new OO.ui.LabelWidget( {\n\t\tclasses: [ 'cx-header-draft-status' ],\n\t\ttitle: mw.msg( 'cx-save-draft-tooltip' )\n\t} );\n\n\tthis.$toolbar = $( '<div>' ).addClass( 'cx-header-tools-container oo-ui-toolbar-bar' );\n\n\treturn [ translationCenter.$element, this.statusbar.$element, this.$toolbar ];\n};\n\nmw.cx.ui.TranslationHeader.prototype.listen = function () {\n\tmw.hook( 'mw.cx.draft.restored' ).add(\n\t\tthis.setStatusMessage.bind( this, mw.msg( 'cx-draft-restored' ) )\n\t);\n};\n\nmw.cx.ui.TranslationHeader.prototype.setStatusMessage = function ( message ) {\n\tthis.statusbar.setLabel( message );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.TranslationView.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * TranslationView\n *\n * @class\n * @param {Object} [config] Configuration object\n * @param {string} config.sourceTitle The title of the source page\n * @param {string} config.targetTitle The title of the target page\n * @param {string} config.sourceLanguage The language of the source page\n * @param {string} config.targetLanguage The language of the target page\n * @param {mw.cx.SiteMapper} config.siteMapper SiteMapper instance\n * @param {string} [config.sourceSectionTitle] The title of the source section\n * @param {string} [config.targetSectionTitle] The title of the target section\n */\nmw.cx.ui.TranslationView = function ( config ) {\n\tthis.infobar = new mw.cx.ui.Infobar( config );\n\tthis.sourceColumn = new mw.cx.ui.SourceColumn( {\n\t\tsiteMapper: config.siteMapper,\n\t\tlanguage: config.sourceLanguage,\n\t\ttitle: config.sourceTitle,\n\t\tsectionTitle: config.sourceSectionTitle\n\t} );\n\tthis.targetColumn = new mw.cx.ui.TargetColumn( {\n\t\tsiteMapper: config.siteMapper,\n\t\tlanguage: config.targetLanguage,\n\t\ttitle: config.targetTitle,\n\t\tsectionTitle: config.targetSectionTitle || config.sourceSectionTitle\n\t} );\n\tthis.toolsColumn = new mw.cx.ui.ToolsColumn( config );\n\tthis.translationHeader = new mw.cx.ui.TranslationHeader( config );\n\tthis.titleValidationTool = null;\n\t// @var {mw.cx.ui.Categories}\n\tthis.categoryUI = null;\n\n\tthis.pageName = this.targetColumn.getTitle();\n\tthis.contentContainer = new OO.ui.HorizontalLayout( $.extend( {}, config, {\n\t\tcontinuous: true,\n\t\texpanded: true,\n\t\tclasses: [ 'cx-content-container' ],\n\t\titems: [ this.sourceColumn, this.targetColumn ]\n\t} ) );\n\n\tthis.translationViewContainer = new OO.ui.StackLayout( $.extend( {}, config, {\n\t\tcontinuous: true,\n\t\texpanded: false,\n\t\tclasses: [ 'cx-translation-view-container' ],\n\t\tscrollable: false,\n\t\tpadded: false,\n\t\titems: [ this.translationHeader, this.infobar, this.contentContainer ]\n\t} ) );\n\n\tthis.columns = new OO.ui.HorizontalLayout( $.extend( {}, config, {\n\t\tcontinuous: true,\n\t\texpanded: true,\n\t\tclasses: [ 'cx-widget__columns' ],\n\t\titems: [ this.translationViewContainer, this.toolsColumn ]\n\t} ) );\n\n\t// Configuration initialization\n\tthis.config = $.extend( {}, config, {\n\t\tcontinuous: true,\n\t\texpanded: false,\n\t\titems: [ this.columns ],\n\t\tclasses: [ 'cx-translation-view' ],\n\t\tscrollable: false,\n\t\tpadded: false\n\t} );\n\n\t// Parent constructor\n\tmw.cx.ui.TranslationView.super.call( this, this.config );\n\n\t// Events\n\tthis.targetColumn.titleWidget.connect( this, {\n\t\tfocus: 'onFocus',\n\t\tblur: 'onBlur'\n\t} );\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.ui.TranslationView, OO.ui.StackLayout );\n\n/* Events */\n\n/**\n * @event hasTranslationIssues\n *\n * @param {boolean} hasErrors True if any of the translation issues is of type 'error'\n */\n\n/* Static methods */\n\n/**\n * Align a source+target section pair by adjusting their paddingTop\n *\n * @param {number} sourceOffsetTop Pixel offset of the source section\n * @param {number} targetOffsetTop Pixel offset of the target section\n * @param {number} sectionNumber The number in the source/target section id attribute\n */\nmw.cx.ui.TranslationView.static.alignSectionPair = function ( sourceOffsetTop, targetOffsetTop, sectionNumber ) {\n\tvar offsetTop, viewNode,\n\t\tsourceNode = document.getElementById( 'cxSourceSection' + sectionNumber ),\n\t\ttargetNode = document.getElementById( 'cxTargetSection' + sectionNumber );\n\n\tfunction isSubclass( x, y ) {\n\t\treturn x && ( x.constructor === y || x.constructor instanceof y );\n\t}\n\tif ( !sourceNode || !targetNode ) {\n\t\treturn;\n\t}\n\tviewNode = $.data( targetNode, 'view' );\n\tsourceNode.style.marginTop = '';\n\ttargetNode.style.marginTop = '';\n\t// Reset heights before we do calculations.\n\tsourceNode.style.height = '';\n\ttargetNode.style.height = '';\n\toffsetTop = Math.max(\n\t\tsourceOffsetTop + sourceNode.offsetTop,\n\t\ttargetOffsetTop + targetNode.offsetTop\n\t);\n\tsourceNode.style.marginTop = ( offsetTop - sourceOffsetTop - sourceNode.offsetTop ) + 'px';\n\ttargetNode.style.marginTop = ( offsetTop - targetOffsetTop - targetNode.offsetTop ) + 'px';\n\tif ( isSubclass( viewNode, ve.ce.CXPlaceholderNode ) || isSubclass( viewNode, ve.ce.CXSectionNode ) ) {\n\t\tif ( sourceNode.offsetHeight > targetNode.offsetHeight ) {\n\t\t\ttargetNode.style.height = sourceNode.offsetHeight + 'px';\n\t\t} else {\n\t\t\tsourceNode.style.height = targetNode.offsetHeight + 'px';\n\t\t}\n\t}\n};\n\n/* Methods */\n\nmw.cx.ui.TranslationView.prototype.showCategories = function ( categoryUI ) {\n\tthis.categoryUI = categoryUI;\n\n\tthis.sourceColumn.setCategoryCount( categoryUI.getSourceCategoryCount() );\n\tthis.sourceColumn.setCategoryListing( categoryUI.getSourceCategoryListing() );\n\tthis.targetColumn.setCategoryCount( categoryUI.getTargetCategoryCount() );\n\tthis.targetColumn.setCategoryListing( categoryUI.getTargetCategoryListing() );\n};\n\n/**\n * Show a success/error message in the view\n *\n * @param {string} type Message class.\n * @param {mw.Message|string} message Message objects are parsed, strings are plain text.\n * @param {mw.Message|string} details The details of error in HTML.\n * @param {Mixed} data Element data.\n * @param {OO.ui.ButtonWidget[]} buttons Array of additional buttons to add to infobar.\n */\nmw.cx.ui.TranslationView.prototype.showMessage = function ( type, message, details, data, buttons ) {\n\tthis.infobar.showMessage( type, message, details, data, buttons );\n};\n\n/**\n * @param {string} message Message to display inside infobar\n * @param {string} issueName Name of the issue to be displayed when infobar message is closed\n * @param {string} type 'error' or 'warning'\n */\nmw.cx.ui.TranslationView.prototype.showViewIssuesMessage = function ( message, issueName, type ) {\n\tvar button = new OO.ui.ButtonWidget( {\n\t\tframed: false,\n\t\tflags: [ 'progressive' ],\n\t\tlabel: mw.msg( 'cx-infobar-view-issues' )\n\t} );\n\n\tbutton.connect( this, { click: [ 'displayIssueDetails', issueName ] } );\n\n\tthis.showMessage( type, message, null, issueName, [ button ] );\n};\n\n/**\n * @param {string} issueName Name of the issue to be displayed when infobar message is closed\n */\nmw.cx.ui.TranslationView.prototype.displayIssueDetails = function ( issueName ) {\n\tvar issueCard = this.toolsColumn.issueCard;\n\n\tif ( !issueCard ) {\n\t\tthrow new Error( 'Issue card is not initialized' );\n\t}\n\n\tissueCard.openIssueByName( issueName );\n\tthis.clearMessages();\n};\n\n/**\n * @param {Mixed} messageData\n */\nmw.cx.ui.TranslationView.prototype.removeMessage = function ( messageData ) {\n\tthis.infobar.removeMessage( messageData );\n};\n\n/**\n * Hide all infobar messages.\n */\nmw.cx.ui.TranslationView.prototype.clearMessages = function () {\n\tthis.infobar.clearMessages();\n};\n\nmw.cx.ui.TranslationView.prototype.setStatusMessage = function ( message ) {\n\tthis.translationHeader.setStatusMessage( message );\n};\n\nmw.cx.ui.TranslationView.prototype.showConflictWarning = function ( translation ) {\n\tmw.loader.using( 'ext.cx.translation.conflict' ).then( function () {\n\t\tmw.hook( 'mw.cx.translation.conflict' ).fire( translation );\n\t} );\n};\n\n/**\n * @param {Mixed[]} nodesWithIssues\n * @param {boolean} hasErrors\n * @fires hasTranslationIssues\n */\nmw.cx.ui.TranslationView.prototype.onTranslationIssues = function ( nodesWithIssues, hasErrors ) {\n\tif ( hasErrors ) {\n\t\t// TODO: Do this in target#onTranslationIssues\n\t\tve.init.target.publishButton.setDisabled( true );\n\t}\n\n\tthis.emit( 'hasTranslationIssues', hasErrors );\n\tthis.toolsColumn.showIssues( nodesWithIssues );\n};\n\n/**\n * @param {Mixed[]} nodesWithIssues\n * @fires hasTranslationIssues\n */\nmw.cx.ui.TranslationView.prototype.onIssuesResolved = function ( nodesWithIssues ) {\n\tif ( nodesWithIssues.length === 0 ) {\n\t\tthis.toolsColumn.hideIssues();\n\t\tthis.emit( 'hasTranslationIssues', false );\n\t\treturn;\n\t}\n\n\tthis.toolsColumn.showIssues( nodesWithIssues );\n};\n\n/**\n * Set the height for both title widgets to whichever\n * is the bigger height between source and target titles.\n */\nmw.cx.ui.TranslationView.prototype.alignTitles = function () {\n\tvar height,\n\t\t$sourceTitleWidget = this.sourceColumn.getTitleWidget().$element,\n\t\t$targetTitleWidget = this.targetColumn.getTitleWidget().$element;\n\n\t$sourceTitleWidget.css( 'min-height', '' );\n\t$targetTitleWidget.css( 'min-height', '' );\n\n\theight = Math.max(\n\t\t$sourceTitleWidget.outerHeight(),\n\t\t$targetTitleWidget.outerHeight()\n\t);\n\n\t$sourceTitleWidget.css( 'min-height', height );\n\t$targetTitleWidget.css( 'min-height', height );\n};\n\nmw.cx.ui.TranslationView.prototype.onFocus = function () {\n\tthis.toolsColumn.toolContainer.$element.addClass( 'cx-column-tools-container--contextual' );\n};\n\nmw.cx.ui.TranslationView.prototype.onBlur = function () {\n\tthis.toolsColumn.toolContainer.$element.removeClass( 'cx-column-tools-container--contextual' );\n};\n\n/**\n * Add a CSS class to translation view that marks the current mode as section translation\n */\nmw.cx.ui.TranslationView.prototype.markSectionTranslation = function () {\n\tthis.$element.addClass( 'cx-sx-mode' );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/mw.cx.ui.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation extension UI base module\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\tmw.cx.ui = {\n\t\tmixin: {}\n\t};\n\t// FIXME: Remove after the widgets files moved to ui folder.\n\tmw.cx.widgets = mw.cx.widgets || {};\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.CategoryInputWidget.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n* Content Translation UserInterface CategoryInputWidget class.\n*\n* @copyright See AUTHORS.txt\n* @license GPL-2.0-or-later\n*/\n\n'use strict';\n\n/**\n * Creates a mw.cx.ui.CategoryInputWidget object.\n *\n * @class\n * @extends ve.ui.MWCategoryInputWidget\n *\n * @constructor\n * @param {mw.cx.ui.CategoryMultiselectWidget} categoryWidget\n * @param {Object} [config] Configuration options\n * @cfg {mw.cx.MwApiRequestManager} [requestManager]\n * @cfg {mw.cx.SiteMapper} [siteMapper]\n */\nmw.cx.ui.CategoryInputWidget = function CategoryInputWidget( categoryWidget, config ) {\n\tvar siteMapper = config.siteMapper || mw.cx.siteMapper,\n\t\ttargetLanguage = mw.cx.targetLanguage,\n\t\trequestManager = config.requestManager || new mw.cx.MwApiRequestManager(\n\t\t\tmw.cx.sourceLanguage, targetLanguage, siteMapper\n\t\t);\n\n\t// Parent constructor\n\tmw.cx.ui.CategoryInputWidget.super.call( this, categoryWidget, $.extend( {\n\t\tapi: siteMapper.getApi( targetLanguage, { parameters: { formatversion: 2 } } )\n\t}, config.input ) );\n\n\trequestManager.getNamespaceAlias( targetLanguage, 'Category' ).done( function ( prefix ) {\n\t\t// This is likely to be resolved before first usage of variable,\n\t\t// but we may want some error handling\n\t\tthis.namespacePrefix = prefix + ':';\n\t}.bind( this ) );\n\n\t// Initialization\n\tthis.$element.addClass( 'mw-cx-ui-CategoryInputWidget' );\n\tthis.$element.removeClass( 've-ui-mwCategoryInputWidget' );\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.ui.CategoryInputWidget, ve.ui.MWCategoryInputWidget );\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.CategoryInputWidget.prototype.getLookupCacheDataFromResponse = function () {\n\tvar result = mw.cx.ui.CategoryInputWidget.super.prototype.getLookupCacheDataFromResponse.apply( this, arguments );\n\n\treturn result.map( function ( category ) {\n\t\tvar hasNamespacePrefix = category.indexOf( this.namespacePrefix ) === 0;\n\n\t\treturn hasNamespacePrefix ? category.slice( this.namespacePrefix.length ) : category;\n\t}, this );\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.CategoryInputWidget.prototype.onLookupMenuChoose = function () {\n\tmw.cx.ui.CategoryInputWidget.super.prototype.onLookupMenuChoose.apply( this, arguments );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.CategoryMultiselectWidget.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n* Content Translation UserInterface CategoryMultiselectWidget class.\n*\n* @copyright See AUTHORS.txt\n* @license GPL-2.0-or-later\n*/\n\n'use strict';\n\n/**\n * Creates a mw.cx.ui.CategoryMultiselectWidget object.\n *\n * @class\n * @extends OO.ui.MenuTagMultiselectWidget\n *\n * @constructor\n * @param {Object} [config] Configuration options\n */\nmw.cx.ui.CategoryMultiselectWidget = function CategoryMultiselectWidget( config ) {\n\tif ( config.inputPosition === 'outline' ) {\n\t\tconfig = config || {};\n\t\tconfig.inputWidget = new mw.cx.ui.CategoryInputWidget( this, config );\n\t}\n\n\t// Parent constructor\n\tmw.cx.ui.CategoryMultiselectWidget.super.call( this, config );\n\n\t// Mixin constructors\n\tOO.ui.mixin.LabelElement.call( this, config );\n\n\t// Initialize\n\tthis.$element.addClass( 'mw-cx-ui-CategoryMultiselectWidget' );\n\tthis.setHeaderLabel( mw.msg( 'categories' ) );\n\tthis.$content.prepend( this.$label );\n\n\t// Aggregate mouse hover events from mw.cx.ui.CategoryTagItemWidget\n\tthis.aggregate( { mouseenter: 'mouseEnter' } );\n\tthis.aggregate( { mouseleave: 'mouseLeave' } );\n\tif ( this.hasInput ) {\n\t\tthis.input.connect( this, { choose: 'onInputChoose' } );\n\t}\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.ui.CategoryMultiselectWidget, OO.ui.MenuTagMultiselectWidget );\nOO.mixinClass( mw.cx.ui.CategoryMultiselectWidget, OO.ui.mixin.LabelElement );\n\n/* Methods */\n\nmw.cx.ui.CategoryMultiselectWidget.prototype.setHeaderLabel = function ( label ) {\n\tthis.$icon.text( label );\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.CategoryMultiselectWidget.prototype.setDisabled = function ( isDisabled ) {\n\t// Parent method\n\tmw.cx.ui.CategoryMultiselectWidget.parent.prototype.setDisabled.call( this, isDisabled );\n\n\t// When widget is (re)enabled, restore tag item's original disabled state\n\tif ( !isDisabled && this.items ) {\n\t\tthis.getItems().forEach( function ( item ) {\n\t\t\titem.restoreOriginalDisabledState();\n\t\t} );\n\t}\n};\n\n/**\n * Check whether all tag items in the widget are disabled. If there are no tag items, return false.\n *\n * @return {boolean}\n */\nmw.cx.ui.CategoryMultiselectWidget.prototype.allDisabled = function () {\n\tif ( this.getItemCount() === 0 ) {\n\t\treturn false;\n\t}\n\n\treturn this.getItems().every( function ( item ) {\n\t\treturn item.isDisabled();\n\t} );\n};\n\n/**\n * Construct a mw.cx.ui.CategoryTagItemWidget from given data, label and config.\n *\n * @param {string} data Item data\n * @param {string} label The label text\n * @param {Object} [config] Configuration options\n * @cfg {boolean} [draggable]\n * @cfg {boolean} [hideRemoveButton]\n * @cfg {boolean} [disabled]\n * @return {mw.cx.ui.CategoryTagItemWidget}\n */\nmw.cx.ui.CategoryMultiselectWidget.prototype.createTagItemWidget = function ( data, label, config ) {\n\tlabel = label || data;\n\n\treturn new mw.cx.ui.CategoryTagItemWidget( $.extend( {\n\t\tdata: data,\n\t\tlabel: label\n\t}, config ) );\n};\n\n/**\n * Set selected items.\n *\n * @param {Object[]} values Array of objects representing the data, label and config of the value.\n */\nmw.cx.ui.CategoryMultiselectWidget.prototype.setValue = function ( values ) {\n\tthis.clearItems();\n\n\tvalues.forEach( function ( val ) {\n\t\tthis.addTag( val.data, val.label, val.config );\n\t}, this );\n};\n\n/**\n * Add tag to the display area.\n *\n * @param {string|Object} data Tag data\n * @param {string} [label] Tag label. If no label is provided, the\n *  stringified version of the data will be used instead.\n * @param {Object} [config] Configuration options\n * @return {boolean} Item was added successfully\n */\nmw.cx.ui.CategoryMultiselectWidget.prototype.addTag = function ( data, label, config ) {\n\tvar newItemWidget, isValid = this.isAllowedData( data );\n\n\tif ( isValid || this.allowDisplayInvalidTags ) {\n\t\tnewItemWidget = this.createTagItemWidget( data, label, config );\n\t\tnewItemWidget.toggleValid( isValid );\n\t\tthis.addItems( [ newItemWidget ] );\n\n\t\treturn true;\n\t}\n\n\treturn false;\n};\n\n/**\n * Method required by mw.cx.ui.CategoryInputWidget that returns existing categories,\n * which are added to \"Move this category here\" section in search menu, which is a\n * functionality we opt out from.\n *\n * @return {Array} Array of existing categories\n */\nmw.cx.ui.CategoryMultiselectWidget.prototype.getCategories = function () {\n\treturn [];\n};\n\n/**\n * @param {mw.cx.ui.CategoryInputWidget} item\n */\nmw.cx.ui.CategoryMultiselectWidget.prototype.onInputChoose = function ( item ) {\n\tvar title = item.getLabel(),\n\t\ttitleWithPrefix = item.getData();\n\n\tif ( !title ) {\n\t\treturn;\n\t}\n\n\t// By utilizing allowedValues, we prevent user from adding\n\t// arbitrary category, which isn't coming from lookup menu.\n\t// That prevents situations when you press one character,\n\t// followed quickly by Enter key, before first pending lookup\n\t// request is resolved.\n\tthis.allowedValues.push( titleWithPrefix );\n\tthis.addTag( titleWithPrefix, title );\n};\n\n/**\n * Menu with missing adapted categories is displayed only when there is no user input.\n *\n * @param {string} inputValue\n */\nmw.cx.ui.CategoryMultiselectWidget.prototype.onInputChange = function ( inputValue ) {\n\tif ( !inputValue ) {\n\t\tthis.menu.toggle( true );\n\t\tthis.initializeMenuSelection();\n\t} else {\n\t\tthis.menu.toggle( false );\n\t}\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.CategoryTagItemWidget.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n* Content Translation UserInterface CategoryTagItemWidget class.\n*\n* @copyright See AUTHORS.txt\n* @license GPL-2.0-or-later\n*/\n\n'use strict';\n\n/**\n * Creates a mw.cx.ui.CategoryTagItemWidget object.\n *\n * @class\n * @extends OO.ui.TagItemWidget\n *\n * @constructor\n * @param {Object} [config] Configuration options\n */\nmw.cx.ui.CategoryTagItemWidget = function CategoryTagItemWidget( config ) {\n\t// Parent constructor\n\tmw.cx.ui.CategoryTagItemWidget.super.call( this, config );\n\n\tthis.originalDisabledState = config.disabled === true;\n\n\tthis.$element.addClass( 'mw-cx-ui-CategoryTagItemWidget' );\n\tif ( config.hideRemoveButton ) {\n\t\tthis.closeButton.$element.remove();\n\t\tthis.$element.addClass( 'mw-cx-ui-CategoryTagItemWidget--no-remove' );\n\t}\n\n\t// Events\n\tthis.$element.on( 'mouseenter', this.onMouseEnter.bind( this ) );\n\tthis.$element.on( 'mouseleave', this.onMouseLeave.bind( this ) );\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.ui.CategoryTagItemWidget, OO.ui.TagItemWidget );\n\n/* Methods */\n\n/**\n * Disabling individual tag items is no longer supported in OOUI since T193571.\n * This setDisabled() method is mirror of OOUI v0.26.5, before disabling of\n * individual tag items was thrown out.\n *\n * @inheritdoc\n */\nmw.cx.ui.CategoryTagItemWidget.prototype.setDisabled = function ( state ) {\n\t// Grandparent method\n\tOO.ui.Widget.prototype.setDisabled.call( this, state );\n\n\tif ( this.closeButton ) {\n\t\tthis.closeButton.setDisabled( state );\n\t}\n\n\treturn this;\n};\n\nmw.cx.ui.CategoryTagItemWidget.prototype.restoreOriginalDisabledState = function () {\n\tthis.setDisabled( this.originalDisabledState );\n};\n\n/**\n * @fires mouseenter\n */\nmw.cx.ui.CategoryTagItemWidget.prototype.onMouseEnter = function () {\n\tif ( !this.isDisabled() ) {\n\t\tthis.emit( 'mouseenter' );\n\t}\n};\n\n/**\n * @fires mouseleave\n */\nmw.cx.ui.CategoryTagItemWidget.prototype.onMouseLeave = function () {\n\tif ( !this.isDisabled() ) {\n\t\tthis.emit( 'mouseleave' );\n\t}\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.FeatureDiscoveryWidget.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * UserInterface Feature Discovery widget. Used to show a pulsating blue dot\n * which, when you click, reveals a popup with useful information.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @constructor\n * @param {Object} [config] Configuration options\n */\nmw.cx.ui.FeatureDiscoveryWidget = function MwCxUiFeatureDiscoveryWidget( config ) {\n\tvar popupCloseButton, $popupContent;\n\n\tconfig = config || {};\n\tthis.$element = config.$container;\n\tthis.onClose = config.onClose;\n\tpopupCloseButton = new OO.ui.ButtonWidget( {\n\t\tlabel: config.dismissLabel,\n\t\tflags: [ 'progressive', 'primary' ],\n\t\tclasses: [ 'mw-cx-ui-featureDiscoveryPopup-dismiss' ]\n\t} );\n\tpopupCloseButton.connect( this, { click: 'onPopupCloseButtonClick' } );\n\t$popupContent = $( '<div>' ).append(\n\t\t$( '<div>' ).addClass( 'mw-cx-ui-featureDiscoveryPopup-header' ),\n\t\t$( '<h3>' ).text( config.title ),\n\t\t$( '<p>' ).text( config.content ),\n\t\tpopupCloseButton.$element\n\t);\n\n\tthis.popup = new OO.ui.PopupWidget( {\n\t\t$floatableContainer: this.$element,\n\t\t$content: $popupContent,\n\t\tclasses: [ 'mw-cx-ui-featureDiscoveryPopup' ],\n\t\tpadded: true,\n\t\twidth: 300\n\t} );\n\n\tthis.$pulsatingDot = $( '<div>' ).addClass( 'mw-pulsating-dot' );\n\tthis.$element\n\t\t.addClass( 'mw-cx-ui-featureDiscoveryPopup' )\n\t\t.append( this.popup.$element, this.$pulsatingDot );\n\tthis.$element.on( 'click', this.show.bind( this ) );\n};\n\n/* Initialization */\n\nOO.initClass( mw.cx.ui.FeatureDiscoveryWidget );\n\n/* Methods */\n\n/**\n * Click handler for the popup close button\n */\nmw.cx.ui.FeatureDiscoveryWidget.prototype.onPopupCloseButtonClick = function () {\n\tthis.popup.toggle( false );\n\tif ( this.onClose ) {\n\t\tthis.onClose();\n\t}\n};\n\nmw.cx.ui.FeatureDiscoveryWidget.prototype.show = function () {\n\tthis.$pulsatingDot.addClass( 'oo-ui-element-hidden' );\n\tthis.popup.toggle( true );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.MessageWidget.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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 {mw.Message|string} [message] Main message.\n * @cfg {mw.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 = $.extend( {\n\t\tshowClose: true\n\t}, 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// Initialization\n\tthis.$element.addClass( 'cx-message-widget' );\n\n\tthis.addButtons( config.buttons );\n};\n\n/* Setup */\n\nOO.inheritClass( mw.cx.ui.MessageWidget, OO.ui.MessageWidget );\n\n/* Methods */\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.append( message.parseDom() );\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.append( details.parseDom() );\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":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"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}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":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.siteMapper.getWikiDomainCode( 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: 120,\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: 120,\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":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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.SectionTitleWidget.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Section title widget. This is the header for source and translation columns.\n * It is editable (contenteditable) for translation and readonly for source section title.\n *\n * @class\n * @extends OO.ui.MultilineTextInputWidget\n * @mixins ve.ce.CXLintableNode\n * @param {mw.cx.dm.SectionTitleModel} model\n * @param {Object} [config] Configuration object\n */\nmw.cx.ui.SectionTitleWidget = function ( model, config ) {\n\t// Configuration initialization\n\tconfig = $.extend( config, {\n\t\tclasses: [ 'cx-pagetitle cx-sectiontitle' ],\n\t\ttype: 'text',\n\t\tautosize: true\n\t} );\n\n\tthis.model = model;\n\n\t// Parent constructor\n\tmw.cx.ui.SectionTitleWidget.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-sectiontitle' )\n\t\t.on( 'blur', '.cx-sectiontitle', 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.SectionTitleWidget, OO.ui.MultilineTextInputWidget );\nOO.mixinClass( mw.cx.ui.SectionTitleWidget, ve.ce.CXLintableNode );\n\n/* Methods */\n\n/**\n * @return {mw.cx.dm.SectionTitleModel}\n */\nmw.cx.ui.SectionTitleWidget.prototype.getModel = function () {\n\treturn this.model;\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.SectionTitleWidget.prototype.getFocusableElement = function () {\n\treturn this.$tabIndexed;\n};\n\n/**\n * @inheritdoc\n */\nmw.cx.ui.SectionTitleWidget.prototype.blursEditingSurface = function () {\n\treturn true;\n};\n\nmw.cx.ui.SectionTitleWidget.prototype.validateTitle = function ( value ) {\n\t// Empty array in param resolves all issues with the title\n\tthis.model.resolveTranslationIssues( [] );\n\n\tif ( value === '' ) {\n\t\tthis.model.addTranslationIssues( [ this.getEmptyTitleError() ] );\n\t}\n};\n\nmw.cx.ui.SectionTitleWidget.prototype.getEmptyTitleError = function () {\n\treturn {\n\t\tname: 'empty-title',\n\t\tmessage: mw.message( 'cx-tools-linter-empty-title-message' ), // TODO: Fix this message\n\t\tmessageInfo: {\n\t\t\ttitle: mw.msg( 'cx-tools-linter-empty-title' ), // TODO: Fix this message\n\t\t\ttype: 'error'\n\t\t}\n\t};\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.SectionTitleWidget.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.SectionTitleWidget.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.TitleOptionWidget.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * Content Translation UserInterface adaptation of MediaWiki Widget TitleOptionWidget.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n\n'use strict';\n\n( function () {\n\n\t/**\n\t * Creates an mw.cx.ui.TitleOptionWidget object.\n\t *\n\t * @class\n\t * @extends mw.widgets.TitleOptionWidget\n\t *\n\t * @constructor\n\t * @param {Object} [config] Configuration options\n\t * @cfg {number} [numOfLanguages] Number of languages matched article exists in.\n\t * @cfg {boolean} [missingInTargetLanguage] Article is missing in target language\n\t */\n\tmw.cx.ui.TitleOptionWidget = function MwCxTitleOptionWidget( config ) {\n\t\tvar languageIcon, languageLabel;\n\n\t\t// Parent constructor\n\t\tmw.cx.ui.TitleOptionWidget.parent.call( this, config );\n\n\t\tthis.$element.addClass( 'mw-cx-widget-titleOptionWidget' );\n\t\t// TODO: Consider upstreaming this\n\t\tthis.imageUrl = config.imageUrl;\n\n\t\t// This is sometimes wrong, but at least consistently wrong with other places.\n\t\t// For example: A title of a Song in English in Arabic Wikipedia.\n\t\tthis.$label.prop( {\n\t\t\tlang: config.sourceLanguage,\n\t\t\tdir: $.uls.data.getDir( config.sourceLanguage )\n\t\t} );\n\n\t\t// The descriptions are always in the source language...\n\t\t// ...except for redirect pages it is in the ui language\n\t\tif ( !config.redirect ) {\n\t\t\tthis.$element.find( '.mw-widget-titleOptionWidget-description' )\n\t\t\t\t.prop( {\n\t\t\t\t\tlang: config.sourceLanguage,\n\t\t\t\t\tdir: $.uls.data.getDir( config.sourceLanguage )\n\t\t\t\t} );\n\t\t}\n\n\t\tif ( config.numOfLanguages ) {\n\t\t\tthis.numOfLanguages = config.numOfLanguages;\n\n\t\t\tlanguageIcon = new OO.ui.IconWidget( {\n\t\t\t\ticon: 'language',\n\t\t\t\ttitle: mw.msg( 'cx-page-number-of-languages' )\n\t\t\t} );\n\t\t\tlanguageLabel = new OO.ui.LabelWidget( {\n\t\t\t\tlabel: mw.language.convertNumber( config.numOfLanguages )\n\t\t\t} );\n\t\t\tthis.$element.append(\n\t\t\t\t$( '<span>' )\n\t\t\t\t\t.addClass( 'mw-cx-widget-titleOptionWidget-numOfLanguages' )\n\t\t\t\t\t.append(\n\t\t\t\t\t\tlanguageIcon.$element,\n\t\t\t\t\t\tlanguageLabel.$element\n\t\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\tif ( config.missingInTargetLanguage ) {\n\t\t\tthis.$element.append(\n\t\t\t\t$( '<span>' )\n\t\t\t\t\t.addClass( 'mw-cx-widget-titleOptionWidget-missing' )\n\t\t\t\t\t.text( mw.msg( 'cx-page-missing-in-target-language',\n\t\t\t\t\t\t$.uls.data.getAutonym( config.targetLanguage ) )\n\t\t\t\t\t)\n\t\t\t);\n\t\t}\n\t};\n\n\t/* Setup */\n\n\tOO.inheritClass( mw.cx.ui.TitleOptionWidget, mw.widgets.TitleOptionWidget );\n\n\tmw.cx.ui.TitleOptionWidget.prototype.getNumberOfLanguages = function () {\n\t\treturn this.numOfLanguages;\n\t};\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.TranslationIssueWidget.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Widget for CX translation issues\n *\n * @class\n * @extends OO.ui.TabPanelLayout\n * @constructor\n *\n * @param {string} name Unique name of tab panel\n * @param {mw.cx.dm.TranslationIssue} model\n * @param {Object} [config] Configuration options\n */\nmw.cx.ui.TranslationIssueWidget = function TranslationIssueWidget( name, model, config ) {\n\tconfig = $.extend( {\n\t\texpanded: false,\n\t\tscrollable: false\n\t}, config );\n\tthis.model = model;\n\n\t// Parent constructor\n\tmw.cx.ui.TranslationIssueWidget.super.call( this, name, config );\n\n\t// Initialization\n\tthis.$element.addClass( 'cx-ui-translationIssue' );\n\n\tthis.icon = new OO.ui.IconWidget( this.getIconConfig() );\n\tthis.$title = $( '<h4>' )\n\t\t.addClass( 'cx-ui-translationIssue-title' )\n\t\t.text( this.model.getTitle() || mw.msg( 'cx-tools-linter-generic-title' ) );\n\tthis.$message = $( '<p>' )\n\t\t.addClass( 'cx-ui-translationIssue-message' )\n\t\t.append( this.model.getMessageContent() );\n\tthis.$message.find( 'a' ).prop( 'target', '_blank' );\n\tthis.$foot = $( '<div>' ).addClass( 'cx-ui-translationIssue-foot' );\n\n\tthis.$element.append( this.icon.$element, this.$title, this.$message, this.$foot );\n\n\tif ( this.model.getHelpLink() ) {\n\t\tthis.helpLink = new OO.ui.ButtonWidget( {\n\t\t\tframed: false,\n\t\t\tlabel: mw.msg( 'cx-tools-linter-learn-more' ),\n\t\t\tflags: [ 'progressive' ],\n\t\t\tclasses: [ 'cx-ui-translationIssue-help' ],\n\t\t\thref: this.model.getHelpLink(),\n\t\t\ttarget: '_blank'\n\t\t} );\n\t\tthis.$foot.before( this.helpLink.$element );\n\t}\n\n\tif ( this.model.isResolvable() ) {\n\t\tthis.resolveButton = new OO.ui.ButtonWidget( {\n\t\t\tframed: false,\n\t\t\ticon: this.model.getIcon(),\n\t\t\tlabel: this.model.getLabel() || mw.msg( 'cx-tools-linter-mark-as-resolved' )\n\t\t} );\n\t\tthis.resolveButton.connect( this, { click: this.model.getAction() } );\n\t\tthis.$foot.append( this.resolveButton.$element );\n\t}\n\n\tthis.buildAdditionalButtons();\n};\n\n/* Inheritance */\n\nOO.inheritClass( mw.cx.ui.TranslationIssueWidget, OO.ui.TabPanelLayout );\n\n/* Methods */\n\nmw.cx.ui.TranslationIssueWidget.prototype.getIconConfig = function () {\n\tvar isError = this.model.getType() === 'error';\n\n\treturn {\n\t\tflags: isError ? [ 'error' ] : [ 'warning' ],\n\t\ticon: isError ? 'error' : 'alert',\n\t\tclasses: [ 'cx-ui-translationIssue-icon' ]\n\t};\n};\n\nmw.cx.ui.TranslationIssueWidget.prototype.buildAdditionalButtons = function () {\n\tvar additionalButtons = this.model.getAdditionalButtons() || [];\n\n\tif ( !additionalButtons.length ) {\n\t\treturn;\n\t}\n\n\tthis.$foot.addClass( 'cx-ui-translationIssue-foot-additional' );\n\n\tadditionalButtons.forEach( function ( buttonConfig ) {\n\t\tvar button = new OO.ui.ButtonWidget( {\n\t\t\tframed: false,\n\t\t\ticon: buttonConfig.icon,\n\t\t\tlabel: buttonConfig.label\n\t\t} );\n\n\t\tbutton.connect( this, { click: buttonConfig.action } );\n\n\t\tthis.$foot.prepend( button.$element );\n\t}.bind( this ) );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ui/widgets/mw.cx.ui.TranslationToolWidget.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n/**\n * Creates a mw.cx.widgets.TranslationToolWidget object.\n *\n * @class\n * @extends OO.ui.PanelLayout\n *\n * @constructor\n * @param {mw.cx.tools.TranslationTool} translationTool\n * @param {Object} config Configuration options\n * @cfg {string} title The tool title\n */\nmw.cx.widgets.TranslationToolWidget = function CXTranslationToolWidget( translationTool, config ) {\n\tthis.translationTool = translationTool;\n\tconfig = $.extend( {}, config, {\n\t\t// The following classes are used here:\n\t\t// * cx-card-instructions\n\t\t// * cx-card-issues\n\t\t// * cx-card-search\n\t\t// * cx-card-template\n\t\tclasses: [ 'cx-card', 'cx-card-' + config.name ],\n\t\texpanded: false,\n\t\tframed: true,\n\t\tpadded: false,\n\t\tdata: this.translationTool.getData()\n\t} );\n\n\t// Parent constructor\n\tmw.cx.widgets.TranslationToolWidget.super.call( this, config );\n\n\t// Initialization\n\tthis.$element.addClass( 'cx-widget-translationtool' );\n\n\tthis.$title = $( '<div>' )\n\t\t.addClass( 'card__title' )\n\t\t.text( config.title || '' );\n\tthis.$language = $( '<div>' )\n\t\t.addClass( 'card__title--language' )\n\t\t.text( $.uls.data.getAutonym( config.language ) || '' );\n\tthis.$header = $( '<div>' )\n\t\t.addClass( 'cx-widget-translationtool-header' )\n\t\t.append( this.$title, this.$language );\n\n\t// It is not always possible to provide the toolContent at this point. The tools can update this widget\n\tthis.$information = $( '<div>' )\n\t\t.addClass( 'cx-widget-translationtool-container' );\n\n\tthis.$actions = $( '<div>' )\n\t\t.addClass( 'cx-widget-translationtool-actions' );\n\n\tthis.$element.append( this.$header, this.$information, this.$actions );\n\t// Set the display order in tools column\n\tthis.$element.css( 'order', translationTool.order );\n\tthis.render();\n\tthis.listen();\n};\n\n/* Inheritance */\nOO.inheritClass( mw.cx.widgets.TranslationToolWidget, OO.ui.Widget );\n\n/**\n * Render the card content and actions if any\n */\nmw.cx.widgets.TranslationToolWidget.prototype.render = function () {\n\tthis.renderContent();\n\tthis.renderActions();\n\tthis.renderBackground();\n};\n\nmw.cx.widgets.TranslationToolWidget.prototype.listen = function () {\n\tthis.translationTool.connect( this, {\n\t\tactionsChange: 'renderActions',\n\t\tcontentChange: 'renderContent',\n\t\tbackgroundChange: 'renderBackground'\n\t} );\n};\n\nmw.cx.widgets.TranslationToolWidget.prototype.renderContent = function () {\n\tthis.setContent( this.translationTool.getContent() );\n};\n\nmw.cx.widgets.TranslationToolWidget.prototype.renderActions = function () {\n\tthis.setActions( this.translationTool.getActions() );\n};\n\nmw.cx.widgets.TranslationToolWidget.prototype.renderBackground = function () {\n\tthis.setBackgroundImage( this.translationTool.getBackgroundImage() );\n};\n\n/**\n * Set the content of card\n *\n * @param {string|jQuery} content Content as HTML or jQuery\n */\nmw.cx.widgets.TranslationToolWidget.prototype.setContent = function ( content ) {\n\tthis.$information.empty();\n\n\tif ( content instanceof $ ) {\n\t\tthis.$information.append( content );\n\t} else {\n\t\tthis.$information.text( content );\n\t}\n};\n\n/**\n * Set the action widgets for the card.\n *\n * @param {OO.ui.Element[]} actions Array of action widgets\n */\nmw.cx.widgets.TranslationToolWidget.prototype.setActions = function ( actions ) {\n\tvar i;\n\n\tthis.$actions.empty();\n\tif ( !actions || actions.length === 0 ) {\n\t\tthis.$actions.hide();\n\t} else {\n\t\tfor ( i = 0; i < actions.length; i++ ) {\n\t\t\tthis.$actions.append( actions[ i ].$element );\n\t\t}\n\t}\n};\n\n/**\n * Set the background image for the card.\n *\n * @param {string} imageUrl\n */\nmw.cx.widgets.TranslationToolWidget.prototype.setBackgroundImage = function ( imageUrl ) {\n\tif ( imageUrl ) {\n\t\tthis.$element.css( 'background-image', 'url(' + imageUrl + ')' );\n\t} else {\n\t\tthis.$element.css( 'background-image', '' );\n\t}\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/util/mw.cx.util.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * A set of utility methods for Content Translation\n */\n\n'use strict';\n\n/**\n * Align two sections horizontally\n *\n * @param {jQuery} $sourceSection Source section\n * @param {jQuery} $targetSection Target section\n */\nmw.cx.alignSections = function ( $sourceSection, $targetSection ) {\n\tvar sourceHeight, targetHeight,\n\t\tsteps = 0;\n\n\t// Remove min-heights\n\t$sourceSection.css( 'min-height', '' );\n\t$targetSection.css( 'min-height', '' );\n\n\tsourceHeight = +$sourceSection[ 0 ].scrollHeight;\n\ttargetHeight = +$targetSection[ 0 ].scrollHeight;\n\n\twhile ( sourceHeight !== targetHeight ) {\n\t\tif ( targetHeight > sourceHeight ) {\n\t\t\t$sourceSection.css( 'min-height', targetHeight );\n\t\t} else {\n\t\t\t$targetSection.css( 'min-height', sourceHeight );\n\t\t}\n\t\tsourceHeight = +$sourceSection[ 0 ].scrollHeight;\n\t\ttargetHeight = +$targetSection[ 0 ].scrollHeight;\n\t\tif ( steps++ === 5 ) {\n\t\t\tmw.track( 'Alignment attempt is not succeeding. Aborting.' );\n\t\t\tbreak;\n\t\t}\n\t}\n};\n\n/**\n * Get the title after changing its namespace to new one.\n * Expects valid title, if not, will throw exception.\n *\n * @param {string} currentTitle Original title string\n * @param {number} newNamespaceId New namespace id\n * @return {string} New title with changed namespace\n */\nmw.cx.getTitleForNamespace = function ( currentTitle, newNamespaceId ) {\n\tvar currentTitleObj, currentNamespace, username;\n\n\tcurrentTitleObj = new mw.Title( currentTitle );\n\tcurrentNamespace = currentTitleObj.getNamespaceId();\n\tif ( newNamespaceId === currentNamespace ) {\n\t\t// No change.\n\t\treturn currentTitle;\n\t}\n\n\t// Get the current title string\n\tcurrentTitle = currentTitleObj.getMainText();\n\tif ( currentNamespace === mw.config.get( 'wgNamespaceIds' ).user ) {\n\t\t// User namespace. Get the title part alone after removing User:username/ part\n\t\tcurrentTitle = currentTitle.slice( currentTitle.indexOf( '/' ) + 1 );\n\t}\n\n\tif ( newNamespaceId === mw.config.get( 'wgNamespaceIds' ).user ) {\n\t\tusername = mw.user.getName();\n\t\tcurrentTitle = mw.Title.newFromText( username + '/' + currentTitle, newNamespaceId ).toText();\n\t}\n\treturn mw.Title.newFromText( currentTitle, newNamespaceId ).toText();\n};\n\n/**\n * Get the default publishing target namespace\n *\n * @return {number} Target namespace id\n */\nmw.cx.getDefaultTargetNamespace = function () {\n\tvar config = require( '../config.json' ),\n\t\ttargetNamespace = config.TargetNamespace;\n\n\t// Validate the configuration.\n\tif ( !( targetNamespace in mw.config.get( 'wgFormattedNamespaces' ) ) ) {\n\t\tmw.log.error( '[CX] Invalid publishing namespace configuration. Namespace does not exist: ' + targetNamespace );\n\t\ttargetNamespace = 0;\n\t}\n\n\treturn targetNamespace;\n};\n\n/**\n * Given a section Id, get the section number for it.\n * Section id is like cxSourceSection15 or cxTargetSection15. 15 is the section number.\n *\n * @param {string} sectionId Section id\n * @return {number} section number\n */\nmw.cx.getSectionNumberFromSectionId = function ( sectionId ) {\n\treturn Number( sectionId.match( /^cx(Target|Source)Section([0-9]+)$/ )[ 2 ] );\n};\n\nmw.cx.getCXVersion = function () {\n\tvar config = require( '../config.json' );\n\treturn Number( config.Version );\n};\n\n/**\n * Get the parent section model for the given seletion in the surface.\n *\n * @param {ve.dm.Surface} surface\n * @param {ve.dm.LinearSelection} selection\n * @return {ve.dm.CXSectionNode}\n */\nmw.cx.getParentSectionForSelection = function ( surface, selection ) {\n\tvar section, parentBranchNode, documentModel;\n\n\tdocumentModel = surface.getModel().getDocument();\n\tparentBranchNode = documentModel.getBranchNodeFromOffset( selection.getRange().start );\n\n\twhile ( parentBranchNode ) {\n\t\tif ( parentBranchNode.type === 'cxSection' ) {\n\t\t\tsection = parentBranchNode;\n\t\t\tbreak;\n\t\t}\n\n\t\tparentBranchNode = parentBranchNode.parent;\n\t}\n\n\treturn section;\n};\n\n/**\n * @return {string} Login href.\n */\nmw.cx.getLoginHref = function () {\n\tvar currentUri = new mw.Uri();\n\n\t// Remove the title from return-to query parameters, so duplication is avoided\n\tdelete currentUri.query.title;\n\n\treturn mw.util.getUrl( 'Special:UserLogin', {\n\t\treturnto: 'Special:ContentTranslation',\n\t\treturntoquery: currentUri.getQueryString()\n\t} );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/mixins/ve.ce.CXLintableNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Mixin class for editable content nodes which can have translation issues.\n *\n * @class\n * @constructor\n */\nve.ce.CXLintableNode = function VeCeCXLintableNode() {\n\tthis.focusListenerAttached = false;\n\n\tthis.getHighlightableElement().addClass( 've-ce-cxLintableNode' );\n\n\tthis.model.connect( this, {\n\t\tallIssuesResolved: 'onAllIssuesResolved',\n\t\ttranslationIssues: 'highlightNode'\n\t} );\n};\n\n/* Methods */\n\n/**\n * Get element which should be focused when going through issues in issue card\n * and getting to this node's issue. See T189488 for issue card interactions.\n *\n * @return {jQuery}\n */\nve.ce.CXLintableNode.prototype.getFocusableElement = function () {\n\treturn this.$element;\n};\n\n/**\n * Get element which should be visually highlighted with a marker when there\n * are translation issues with this particular node.\n *\n * @return {jQuery}\n */\nve.ce.CXLintableNode.prototype.getHighlightableElement = function () {\n\treturn this.$element;\n};\n\n/**\n * @return {boolean} True if opening issue related to this node blurs the VE editing surface.\n */\nve.ce.CXLintableNode.prototype.blursEditingSurface = function () {\n\treturn false;\n};\n\n/**\n * Handler triggered when translation issues of this node are resolved.\n */\nve.ce.CXLintableNode.prototype.onAllIssuesResolved = function () {\n\tthis.removeHighlight();\n};\n\n/**\n * Remove the CSS highlights for warnings and errors.\n */\nve.ce.CXLintableNode.prototype.removeHighlight = function () {\n\tthis.getHighlightableElement()\n\t\t.removeClass( 'mw-cx-lintIssue-error mw-cx-lintIssue-warning' )\n\t\t.removeClass( 'mw-cx-current-issue-warning mw-cx-current-issue-error' );\n};\n\n/**\n * Set warning or error marker for a node.\n *\n * @param {boolean} hasErrors\n */\nve.ce.CXLintableNode.prototype.highlightNode = function ( hasErrors ) {\n\tthis.removeHighlight();\n\tvar type = hasErrors ? 'error' : 'warning';\n\t// The following classes are used here:\n\t// * mw-cx-lintIssue-error\n\t// * mw-cx-lintIssue-warning\n\tthis.getHighlightableElement().addClass( 'mw-cx-lintIssue-' + type );\n};\n\nve.ce.CXLintableNode.prototype.isFocusListenerAttached = function () {\n\treturn this.focusListenerAttached;\n};\n\nve.ce.CXLintableNode.prototype.setFocusListenerAttached = function ( isFocusListenerAttached ) {\n\tthis.focusListenerAttached = isFocusListenerAttached;\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/mixins/ve.ce.CXPendingNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * An abstract class used as mixin to provide pending indicator support\n * for the nodes. An overlay on top of the node is shown.\n *\n * @abstract\n * @class\n *\n * @constructor\n * @param {Object} [config] Configuration options\n * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element\n */\nve.ce.CXPendingNode = function VeCeMixinsCxPendingNode( config ) {\n\t// Configuration initialization\n\tconfig = config || {};\n\tthis.pending = false;\n\t// Initialisation\n\tthis.$pending = config.$pending || this.$element;\n};\n\n/* Methods */\n\n/**\n * Check if an element is pending.\n *\n * @return {boolean} Element is pending\n */\nve.ce.CXPendingNode.prototype.isPending = function () {\n\treturn this.pending;\n};\n\n/**\n * Mark the element as pending or not based on boolean pending argument\n *\n * @param {boolean} pending\n */\nve.ce.CXPendingNode.prototype.setPending = function ( pending ) {\n\tthis.pending = pending;\n\n\tif ( this.pending ) {\n\t\tthis.$pending.append( this.getPendingIndicator() );\n\t\tthis.$pending.addClass( 've-ce-cxPendingNode-pending' );\n\t} else {\n\t\t// The pending indicator might be already overwritten when the content changed\n\t\t// But in case of failures or errors, if that did not happen, remove it.\n\t\tthis.$pending.find( '.ve-ce-cxPendingNode-indicator' ).remove();\n\t\tthis.$pending.removeClass( 've-ce-cxPendingNode-pending' );\n\n\t\t// Highlight the resulting paragraph for a second.\n\t\tthis.$pending.addClass( 've-ce-cxPendingNode-flash' );\n\t\tsetTimeout( function () {\n\t\t\tthis.$pending.removeClass( 've-ce-cxPendingNode-flash' );\n\t\t}.bind( this ), 2000 ); // More than 1s to wait for finish animation\n\t}\n};\n\n/**\n * Get the indicator element to be used in the overlay\n *\n * @return {jQuery} Pending indicator\n */\nve.ce.CXPendingNode.prototype.getPendingIndicator = function () {\n\treturn $( '<div>' )\n\t\t.addClass( 've-ce-cxPendingNode-indicator' )\n\t\t.append( mw.cx.widgets.spinner() );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXBlockImageNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * CX Block image node\n *\n * @class\n * @extends ve.ce.MWBlockImageNode\n * @constructor\n * @param {ve.dm.CXImageCaptionNode} model Model to observe\n */\nve.ce.CXBlockImageNode = function CXBlockImageNode() {\n\t// Parent constructor\n\tve.ce.CXBlockImageNode.super.apply( this, arguments );\n\tthis.$element.addClass( 've-ce-cxBlockImageNode' );\n\n\tvar adaptationInfo = this.getAdaptationInfo();\n\tif ( adaptationInfo && !adaptationInfo.adapted ) {\n\t\tthis.$element.addClass( 'cx-image-unadapted' );\n\t}\n\n\t// Fix the link target\n\tthis.$a.attr( 'target', '_blank' );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ce.CXBlockImageNode, ve.ce.MWBlockImageNode );\n\n/* Static Properties */\n\nve.ce.CXBlockImageNode.static.name = 'cxBlockImage';\n\n/* Methods */\n\n/**\n * Get the adaptation info supplied by cxserver\n *\n * @return {Object} adptationInfo\n */\nve.ce.CXBlockImageNode.prototype.getAdaptationInfo = function () {\n\treturn this.model.getAttribute( 'cx' );\n};\n\n/* Registration */\n\nve.ce.nodeFactory.register( ve.ce.CXBlockImageNode );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXImageCaptionNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * CX Block image caption node\n *\n * @class\n * @extends ve.ce.MWImageCaptionNode\n * @constructor\n * @param {ve.dm.CXImageCaptionNode} model Model to observe\n */\nve.ce.CXImageCaptionNode = function VeCeCXImageCaptionNode( model ) {\n\tthis.model = model;\n\t// Parent constructor\n\tve.ce.CXImageCaptionNode.super.apply( this, arguments );\n\n\tthis.$element.addClass( 've-ce-cxImageCaptionNode' );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ce.CXImageCaptionNode, ve.ce.MWImageCaptionNode );\n\n/* Static Properties */\n\nve.ce.CXImageCaptionNode.static.name = 'cxImageCaption';\n\n/* Registration */\n\nve.ce.nodeFactory.register( ve.ce.CXImageCaptionNode );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXLinkAnnotation.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Annotation representing an adapted or unadapted link\n *\n * @class\n * @extends ve.ce.MWInternalLinkAnnotation\n * @constructor\n * @param {ve.dm.CXLinkAnnotation} model\n */\nve.ce.CXLinkAnnotation = function VeCeCXLinkAnnotation() {\n\t// Parent constructor\n\tve.ce.CXLinkAnnotation.super.apply( this, arguments );\n\n\tthis.$anchor\n\t\t.addClass( 've-ce-cxLinkAnnotation' )\n\t\t.data( 'linkid', this.model.getAttribute( 'linkid' ) );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ce.CXLinkAnnotation, ve.ce.MWInternalLinkAnnotation );\n\n/* Static Properties */\n\nve.ce.CXLinkAnnotation.static.name = 'cxLink';\n\n/* Methods */\n\n/**\n * Get the adaptation info supplied by cxserver\n *\n * @return {Object} adaptationInfo\n */\nve.ce.CXLinkAnnotation.prototype.getAdaptationInfo = function () {\n\treturn this.model.getAttribute( 'cx' );\n};\n\n/**\n * @inheritdoc\n */\nve.ce.CXLinkAnnotation.prototype.updateClasses = function () {\n\tvar adaptationInfo = this.getAdaptationInfo();\n\tif ( adaptationInfo && !adaptationInfo.adapted ) {\n\t\tif ( adaptationInfo.targetTitle ) {\n\t\t\tthis.$anchor.addClass( 'new' );\n\t\t} else {\n\t\t\tthis.$anchor.addClass( 'cx-target-link-unadapted' );\n\t\t}\n\t}\n};\n\n/* Registration */\n\nve.ce.annotationFactory.register( ve.ce.CXLinkAnnotation );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXPlaceholderNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Handling of unadapted references in translations.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @extends ve.ce.MWReferenceNode\n * @constructor\n */\nve.ce.CXReferenceNode = function VeCeCXReferenceNode() {\n\t// Parent constructor\n\tve.ce.CXReferenceNode.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ce.CXReferenceNode, ve.ce.MWReferenceNode );\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ce.CXReferenceNode.prototype.onAttributeChange = function ( key ) {\n\t// Parent method\n\tve.ce.CXReferenceNode.super.prototype.onAttributeChange.apply( this, arguments );\n\n\tif ( key === 'cx' ) {\n\t\tthis.update();\n\t}\n};\n\n/**\n * @inheritdoc\n */\nve.ce.CXReferenceNode.prototype.update = function () {\n\t// Parent method\n\tve.ce.CXReferenceNode.super.prototype.update.apply( this, arguments );\n\n\tvar adaptationInfo = this.model.getAdaptationInfo();\n\tif ( adaptationInfo && adaptationInfo.adapted === false ) {\n\t\tthis.$link.addClass( 've-ce-cxReferenceNode-unadapted' );\n\t} else {\n\t\tthis.$link.removeClass( 've-ce-cxReferenceNode-unadapted' );\n\t}\n};\n\n/* Registration */\n\nve.ce.nodeFactory.register( ve.ce.CXReferenceNode );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXSectionNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * VisualEditor ContentEditable CXSentenceSegmentAnnotation class.\n *\n * @copyright 2011-2017 VisualEditor Team and others; see http://ve.mit-license.org\n */\n\n/**\n * ContentEditable CX sentence segment annotation\n *\n * @class\n * @extends ve.ce.Annotation\n * @constructor\n * @param {ve.dm.CXSentenceSegmentAnnotation} model Model to observe\n * @param {ve.ce.ContentBranchNode} [parentNode] Node rendering this annotation\n * @param {Object} [config] Configuration options\n */\nve.ce.CXSentenceSegmentAnnotation = function VeCeCXSentenceSegmentAnnotation() {\n\t// Parent constructor\n\tve.ce.CXSentenceSegmentAnnotation.super.apply( this, arguments );\n\n\t// DOM changes\n\tthis.$element\n\t\t.addClass( 've-ce-cxSentenceSegmentAnnotation' )\n\t\t.attr( {\n\t\t\t'data-segmentid': this.model.getAttribute( 'segmentid' ) || undefined\n\t\t} );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ce.CXSentenceSegmentAnnotation, ve.ce.Annotation );\n\n/* Static Properties */\n\nve.ce.CXSentenceSegmentAnnotation.static.name = 'cxSegment';\n\nve.ce.CXSentenceSegmentAnnotation.static.tagName = 'span';\n\n/* Registration */\n\nve.ce.annotationFactory.register( ve.ce.CXSentenceSegmentAnnotation );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ce/ve.ce.CXTransclusionNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Handling of unadapted templates in translations.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n * @file\n */\n\n/**\n * @class\n * @extends ve.ce.MWTransclusionNode\n * @constructor\n */\nve.ce.CXTransclusionNode = function VeCeCXTransclusionNode() {\n\t// Parent constructor\n\tve.ce.CXTransclusionNode.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ce.CXTransclusionNode, ve.ce.MWTransclusionNode );\n\n/* Static Properties */\n\nve.ce.CXTransclusionNode.static.name = 'cxTransclusion';\n\n/**\n * @return {boolean} True if transclusion node is not adapted by cxserver.\n */\nve.ce.CXTransclusionNode.prototype.isUnadapted = function () {\n\treturn this.getModel() && this.getModel().missingInTargetLanguage();\n};\n\n/**\n * @inheritdoc\n */\nve.ce.CXTransclusionNode.prototype.update = function () {\n\tif ( !this.getModel() || !this.getModel().isValid() || this.isUnadapted() ) {\n\t\treturn;\n\t}\n\n\treturn ve.ce.CXTransclusionNode.super.prototype.update.apply( this, arguments );\n};\n\n/**\n * XXX: ContentEditable MediaWiki transclusion block node.\n *\n * @class\n * @extends ve.ce.CXTransclusionNode\n *\n * @constructor\n * @param {Object} [element] Reference to element in linear model\n */\nve.ce.CXTransclusionBlockNode = function VeCeCXTransclusionBlockNode() {\n\t// Parent constructor\n\tve.ce.CXTransclusionBlockNode.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ce.CXTransclusionBlockNode, ve.ce.CXTransclusionNode );\n\n/* Static Properties */\n\nve.ce.CXTransclusionBlockNode.static.name = 'cxTransclusionBlock';\n\nve.ce.CXTransclusionBlockNode.static.tagName = 'div';\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ce.CXTransclusionBlockNode.prototype.afterRender = function () {\n\tve.ce.CXTransclusionBlockNode.super.prototype.afterRender.apply( this, arguments );\n\tvar parentSection = this.model.findParent( ve.dm.CXSectionNode );\n\t// Emit an event so that the parent section can do visual re-alignment if needed.\n\tparentSection.emit( 'afterRender' );\n\t// Emit change for the parent section, so that saving is queued.\n\tparentSection.emit( 'update' );\n};\n\n/**\n * @inheritdoc\n */\nve.ce.CXTransclusionBlockNode.prototype.onFocusableSetup = function () {\n\tif ( !this.getModel() || !this.getModel().isValid() ) {\n\t\treturn;\n\t}\n\n\tvar iconWhenInvisible = this.constructor.static.iconWhenInvisible;\n\n\tif ( this.isUnadapted() ) {\n\t\t// Temporarily set static property to null to avoid displaying icon\n\t\t// while generating transclusion node content.\n\t\tthis.constructor.static.iconWhenInvisible = null;\n\t}\n\n\tve.ce.CXTransclusionBlockNode.super.prototype.onFocusableSetup.apply( this, arguments );\n\n\t// Reset the icon static property\n\tthis.constructor.static.iconWhenInvisible = iconWhenInvisible;\n};\n\n/**\n * @inheritdoc\n */\nve.ce.CXTransclusionBlockNode.prototype.setFocused = function ( value ) {\n\tif ( !this.getModel() || !this.getModel().isValid() ) {\n\t\treturn;\n\t}\n\tif ( this.isUnadapted() ) {\n\t\tvalue = false;\n\t}\n\n\treturn ve.ce.CXTransclusionBlockNode.super.prototype.setFocused.call( this, value );\n};\n\n/**\n * XXX: ContentEditable MediaWiki transclusion inline node.\n *\n * @class\n * @extends ve.ce.CXTransclusionNode\n *\n * @constructor\n * @param {Object} [element] Reference to element in linear model\n */\nve.ce.CXTransclusionInlineNode = function VeCeCXTransclusionInlineNode() {\n\t// Parent constructor\n\tve.ce.CXTransclusionInlineNode.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ce.CXTransclusionInlineNode, ve.ce.CXTransclusionNode );\n\n/* Static Properties */\n\nve.ce.CXTransclusionInlineNode.static.name = 'cxTransclusionInline';\n\nve.ce.CXTransclusionInlineNode.static.tagName = 'span';\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ce.CXTransclusionInlineNode.prototype.afterRender = function () {\n\tvar parentSection;\n\tve.ce.CXTransclusionInlineNode.super.prototype.afterRender.apply( this, arguments );\n\tparentSection = this.model.findParent( ve.dm.CXSectionNode );\n\t// For citations, the corresponding template is not rendered inside the section, but\n\t// in the reference context item. So there is no parent section.\n\tif ( parentSection ) {\n\t\tparentSection.emit( 'afterRender' );\n\t}\n};\n\n/* Registration */\n\nve.ce.nodeFactory.register( ve.ce.CXTransclusionNode );\nve.ce.nodeFactory.register( ve.ce.CXTransclusionBlockNode );\nve.ce.nodeFactory.register( ve.ce.CXTransclusionInlineNode );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/mixins/ve.dm.CXLintableNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * @class\n * @abstract\n * @constructor\n */\nve.dm.CXLintableNode = function VeDmCXLintableNode() {\n\t// @var {mw.cx.dm.TranslationIssue[]}\n\tthis.translationIssues = [];\n\n\t// It is assumed that CXLintableNode will be mixed into a\n\t// node and therefore OO.EventEmitter is already available.\n};\n\n/* Methods */\n\n/**\n * @method\n * @return {string|number}\n */\nve.dm.CXLintableNode.prototype.getId = null;\n\n/**\n * @return {mw.cx.dm.Translation}\n */\nve.dm.CXLintableNode.prototype.getTranslation = function () {\n\treturn ve.init.target.getTranslation();\n};\n\n/**\n * @return {mw.cx.dm.TranslationIssue[]}\n */\nve.dm.CXLintableNode.prototype.getTranslationIssues = function () {\n\treturn this.translationIssues.filter( function ( issue ) {\n\t\treturn !issue.isSuppressed();\n\t} );\n};\n\n/**\n * Find names of all issues that have some pattern in their name.\n *\n * @param {string} name Part of a name used in a regex\n * @return {string[]} All issues with some pattern in their name\n */\nve.dm.CXLintableNode.prototype.findMatchingIssues = function ( name ) {\n\t// Prevent matching all issues when empty string is given\n\tif ( !name.length ) {\n\t\treturn [];\n\t}\n\n\t// If nothing is found, empty array is returned\n\treturn this.translationIssues.filter( function ( issue ) {\n\t\treturn ( new RegExp( name ) ).test( issue.getName() );\n\t} ).map( function ( issue ) {\n\t\treturn issue.getName();\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 */\nve.dm.CXLintableNode.prototype.findIssueIndex = function ( name ) {\n\tfor ( var 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 * @param {mw.cx.dm.TranslationIssue[]|string[]|Object[]} issues\n */\nve.dm.CXLintableNode.prototype.addTranslationIssues = function ( issues ) {\n\tif ( issues.length < 1 ) {\n\t\treturn;\n\t}\n\n\t// Load the translation issue DM module, now that it's required.\n\t// There is no use of adding this to the initial JS payload\n\tissues.map( this.processTranslationIssues ).forEach( function ( issue ) {\n\t\tvar existingIssueIndex = this.findIssueIndex( issue.name );\n\n\t\t// When issue is suppressed, emit events about the current state\n\t\tissue.setSuppressCallback( this.notify.bind( this ) );\n\n\t\tif ( existingIssueIndex > -1 ) {\n\t\t\t// Replace existing issue\n\t\t\tthis.translationIssues[ existingIssueIndex ] = issue;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.translationIssues.push( issue );\n\t}, this );\n\n\tthis.emit( 'translationIssues', this.hasErrors() );\n\tthis.getTranslation().emit( 'translationIssues', this.getId(), this.hasErrors() );\n};\n\n/**\n * Resolve issues by name:\n * - Array of strings resolves issues that have those names\n * - Empty array resolves all issues of this node\n * - Plain string is used as regex to find resolvable issues\n *\n * @param {string|string[]} names\n */\nve.dm.CXLintableNode.prototype.resolveTranslationIssues = function ( names ) {\n\tif ( Array.isArray( names ) && names.length === 0 ) {\n\t\tthis.translationIssues = [];\n\t}\n\n\tif ( typeof names === 'string' || names instanceof String ) {\n\t\tnames = this.findMatchingIssues( names );\n\t}\n\n\tnames.forEach( function ( name ) {\n\t\tvar index = this.findIssueIndex( name );\n\n\t\tif ( index > -1 ) {\n\t\t\tthis.translationIssues.splice( index, 1 );\n\t\t}\n\t}, this );\n\n\tthis.notify();\n};\n\n/**\n * Emit events about the state of issues.\n */\nve.dm.CXLintableNode.prototype.notify = function () {\n\tif ( !this.hasTranslationIssues() ) {\n\t\tthis.emit( 'allIssuesResolved' );\n\t\tthis.getTranslation().emit( 'issuesResolved', this.getId() );\n\t} else {\n\t\tthis.emit( 'translationIssues', this.hasErrors() );\n\t\tthis.getTranslation().emit( 'translationIssues', this.getId(), this.hasErrors() );\n\t}\n};\n\n/**\n * Transform the issue into mw.cx.dm.TranslationIssue object.\n *\n * @param {mw.cx.dm.TranslationIssue|string|Object} issue\n * @return {mw.cx.dm.TranslationIssue}\n */\nve.dm.CXLintableNode.prototype.processTranslationIssues = function ( issue ) {\n\tif ( issue instanceof mw.cx.dm.TranslationIssue ) {\n\t\treturn issue;\n\t}\n\n\tif ( typeof issue === 'string' || issue instanceof String ) {\n\t\treturn new mw.cx.dm.TranslationIssue( issue );\n\t}\n\n\t// If issue is object with properties name and message and optional property messageInfo\n\tif ( issue === Object( issue ) && issue.name && issue.message ) {\n\t\treturn new mw.cx.dm.TranslationIssue( issue.name, issue.message, issue.messageInfo );\n\t}\n\n\tmw.log.error( 'Lint result cannot be processed' );\n};\n\n/**\n * True if this node has at least one issue that is an error. Number of warnings is irrelevant.\n *\n * @return {boolean}\n */\nve.dm.CXLintableNode.prototype.hasErrors = function () {\n\treturn this.getTranslationIssues().some( function ( issue ) {\n\t\treturn issue.type === 'error';\n\t} );\n};\n\n/**\n * Check if this node has at least one issue.\n *\n * @return {boolean}\n */\nve.dm.CXLintableNode.prototype.hasTranslationIssues = function () {\n\treturn this.getTranslationIssues().length > 0;\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXBlockImageNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * DataModel CX image node.\n *\n * @class\n * @extends ve.dm.MWBlockImageNode\n * @constructor\n * @param {Object} [element] Reference to element in linear model\n * @param {ve.dm.Node[]} [children]\n */\nve.dm.CXBlockImageNode = function VeDmCXBlockImageNode() {\n\t// Parent constructor\n\tve.dm.CXBlockImageNode.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.dm.CXBlockImageNode, ve.dm.MWBlockImageNode );\n\n/* Static Properties */\n\nve.dm.CXBlockImageNode.static.name = 'cxBlockImage';\n\n/* Static Methods */\n\n// Increase the specificity of class match over the parent class\nve.dm.CXBlockImageNode.static.matchFunction = function ( node ) {\n\treturn node.getAttribute( 'rel' ) === 'cx:Figure';\n};\n\nve.dm.CXBlockImageNode.static.childNodeTypes = [ 'cxImageCaption' ];\n\nve.dm.CXBlockImageNode.static.toDataElement = function ( domElements, converter ) {\n\tvar figure = domElements[ 0 ];\n\n\tvar dataElements = ve.dm.CXBlockImageNode.super.static.toDataElement.call( this, domElements, converter );\n\n\tvar rel = domElements[ 0 ].getAttribute( 'rel' );\n\tif ( rel ) {\n\t\tfigure.setAttribute( 'rel', rel );\n\t}\n\tvar dataCX = figure.getAttribute( 'data-cx' );\n\tif ( dataCX ) {\n\t\tdataElements[ 0 ].attributes.cx = JSON.parse( domElements[ 0 ].getAttribute( 'data-cx' ) );\n\t}\n\n\tfor ( var i = 0; i < dataElements.length; i++ ) {\n\t\tif ( dataElements[ i ].type === 'mwImageCaption' ) {\n\t\t\tdataElements[ i ].type = 'cxImageCaption';\n\t\t}\n\t\tif ( dataElements[ i ].type === '/mwImageCaption' ) {\n\t\t\tdataElements[ i ].type = '/cxImageCaption';\n\t\t}\n\t}\n\n\treturn dataElements;\n};\n\nve.dm.CXBlockImageNode.static.toDomElements = function ( dataElements, doc, converter ) {\n\tvar domElements = ve.dm.CXBlockImageNode.super.static.toDomElements.call( this, dataElements, doc, converter );\n\tif ( dataElements[ 0 ].attributes.cx ) {\n\t\tdomElements[ 0 ].setAttribute( 'data-cx', JSON.stringify( dataElements[ 0 ].attributes.cx ) );\n\t}\n\n\tvar rel = dataElements[ 0 ].attributes.rel;\n\tif ( rel ) {\n\t\tdomElements[ 0 ].setAttribute( 'rel', dataElements[ 0 ].attributes.rel );\n\t}\n\treturn domElements;\n};\n\nve.dm.CXBlockImageNode.static.getMatchRdfaTypes = function () {\n\t// Parent method\n\tvar types = ve.dm.CXBlockImageNode.super.static.getMatchRdfaTypes();\n\treturn [ 'cx:Figure' ].concat( types );\n};\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.dm.CXBlockImageNode.prototype.getScalable = function () {\n\t// VE uses its image cache to get image dimensions.\n\t// CX has it in the adaptation info. Use that.\n\t// Mixin method, bypass ve.dm.MWBlockImageNode.prototype.getScalable\n\treturn ve.dm.ResizableNode.prototype.getScalable.call( this );\n};\n\n/**\n * Get the caption node of the image.\n *\n * @method\n * @return {ve.dm.CXImageCaptionNode|null} Caption node, if present\n */\nve.dm.CXBlockImageNode.prototype.getCaptionNode = function () {\n\tvar node = this.children[ 0 ];\n\treturn node instanceof ve.dm.CXImageCaptionNode ? node : null;\n};\n\n/* Registration */\n\nve.dm.modelRegistry.register( ve.dm.CXBlockImageNode );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXImageCaptionNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * DataModel CX image caption node.\n *\n * @class\n * @extends ve.dm.MWImageCaptionNode\n * @constructor\n * @param {Object} [element] Reference to element in linear model\n * @param {ve.dm.Node[]} [children]\n */\nve.dm.CXImageCaptionNode = function VeDmCXImageCaptionNode() {\n\t// Parent constructor\n\tve.dm.MWImageCaptionNode.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.dm.CXImageCaptionNode, ve.dm.MWImageCaptionNode );\n\n/* Static Properties */\n\nve.dm.CXImageCaptionNode.static.name = 'cxImageCaption';\n\n/* Static Methods */\n\n// Set cxBlockImage as the parent type for this node\nve.dm.CXImageCaptionNode.static.parentNodeTypes = [ 'cxBlockImage' ];\n\n/* Registration */\n\nve.dm.modelRegistry.register( ve.dm.CXImageCaptionNode );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXLinkAnnotation.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * @class\n * @extends ve.dm.MWInternalLinkAnnotation\n * @constructor\n * @param {Object} element\n */\nve.dm.CXLinkAnnotation = function VeDmCXLinkAnnotation() {\n\t// Parent constructor\n\tve.dm.CXLinkAnnotation.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.dm.CXLinkAnnotation, ve.dm.MWInternalLinkAnnotation );\n\n/* Static Properties */\n\nve.dm.CXLinkAnnotation.static.name = 'cxLink';\n\n/* Static Methods */\n\nve.dm.CXLinkAnnotation.static.matchFunction = function ( domElement ) {\n\treturn domElement.classList.contains( 'cx-link' );\n};\n\nve.dm.CXLinkAnnotation.static.toDataElement = function ( domElements, converter ) {\n\tvar dataElement = ve.dm.CXLinkAnnotation.super.static.toDataElement.call( this, domElements, converter );\n\n\tif ( !dataElement ) {\n\t\t// ve.dm.CXLinkAnnotation.super.static.toDataElement has assumptions about document.baseURI.\n\t\t// baseURI can use URL patterns like /wiki/$1 or w/index.php?title=$1. It also uses\n\t\t// current wikis 'wgScript', 'wgArticlePath' configuration values which can\n\t\t// totally be different when running on a local dev wiki.\n\t\t// Because of this dataElement can be null as toDataElement fails to parse an internal link\n\t\t// So make this dataElement calculation agnostic of all of the above mentioned factors.\n\t\tdataElement = ve.dm.CXLinkAnnotation.super.static.dataElementFromTitle.call(\n\t\t\tthis,\n\t\t\tmw.Title.newFromText( domElements[ 0 ].getAttribute( 'title' ) )\n\t\t);\n\t}\n\n\tdataElement.attributes.linkid = domElements[ 0 ].getAttribute( 'data-linkid' );\n\n\tvar dataCX = domElements[ 0 ].getAttribute( 'data-cx' );\n\tif ( dataCX ) {\n\t\tdataElement.attributes.cx = JSON.parse( domElements[ 0 ].getAttribute( 'data-cx' ) );\n\t}\n\treturn dataElement;\n};\n\nve.dm.CXLinkAnnotation.static.toDomElements = function ( dataElement, doc ) {\n\tvar domElements = ve.dm.CXLinkAnnotation.super.static.toDomElements.call( this, dataElement, doc );\n\tdomElements[ 0 ].setAttribute( 'data-linkid', dataElement.attributes.linkid );\n\tif ( dataElement.attributes.cx ) {\n\t\tdomElements[ 0 ].setAttribute( 'data-cx', JSON.stringify( dataElement.attributes.cx ) );\n\t}\n\treturn domElements;\n};\n\n/**\n * @inheritdoc\n * @return {ve.dm.CXLinkAnnotation} The annotation.\n */\nve.dm.CXLinkAnnotation.static.newFromTitle = function ( title, rawTitle ) {\n\tvar element = this.dataElementFromTitle( title, rawTitle );\n\n\telement.attributes.cx = {\n\t\tuserAdded: true,\n\t\tadapted: true\n\t};\n\n\treturn new ve.dm.CXLinkAnnotation( element );\n};\n\n/* Methods */\n\nve.dm.CXLinkAnnotation.prototype.getComparableObject = function () {\n\tvar comparableObject = ve.dm.CXLinkAnnotation.super.prototype.getComparableObject.call( this );\n\tcomparableObject.linkid = this.getAttribute( 'linkid' );\n\n\treturn comparableObject;\n};\n\n/* Registration */\n\nve.dm.modelRegistry.register( ve.dm.CXLinkAnnotation );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXPlaceholderNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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 cxDataJSON = domElements[ 0 ].getAttribute( 'data-cx' );\n\n\t// Parent method\n\tvar dataElement = ve.dm.CXReferenceNode.super.static.toDataElement.apply( this, arguments );\n\n\tvar cxData;\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":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * VisualEditor DataModel CXSentenceSegmentAnnotation class.\n *\n * @copyright 2011-2017 VisualEditor Team and others; see http://ve.mit-license.org\n */\n\n/**\n * ContentTranslation sentence segment annotation\n *\n * Represents `<span class=\"cx-segment\">` tags with data-segmentid attribute\n *\n * @class\n * @extends ve.dm.Annotation\n * @constructor\n * @param {Object} element\n */\nve.dm.CXSentenceSegmentAnnotation = function VeDmCXSentenceSegmentAnnotation() {\n\t// Parent constructor\n\tve.dm.CXSentenceSegmentAnnotation.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.dm.CXSentenceSegmentAnnotation, ve.dm.Annotation );\n\n/* Static Properties */\n\nve.dm.CXSentenceSegmentAnnotation.static.name = 'cxSegment';\n\nve.dm.CXSentenceSegmentAnnotation.static.matchTagNames = [ 'span' ];\n\n/* Static Methods */\n\nve.dm.CXSentenceSegmentAnnotation.static.matchFunction = function ( domElement ) {\n\treturn domElement.classList.contains( 'cx-segment' ) &&\n\t\tdomElement.getAttribute( 'data-segmentid' );\n};\n\nve.dm.CXSentenceSegmentAnnotation.static.applyToAppendedContent = true;\n\nve.dm.CXSentenceSegmentAnnotation.static.toDataElement = function ( domElements ) {\n\treturn {\n\t\ttype: this.name,\n\t\tattributes: {\n\t\t\tsegmentid: domElements[ 0 ].getAttribute( 'data-segmentid' )\n\t\t}\n\t};\n};\n\nve.dm.CXSentenceSegmentAnnotation.static.toDomElements = function ( dataElement, doc, converter ) {\n\t// We only need these segments when converting the document for saving\n\t// or for translating, not for the clipboard (or previewing) (T220495)\n\tif ( converter.isForParser() || converter.isForTranslation ) {\n\t\tvar domElement = doc.createElement( 'span' );\n\t\tif ( dataElement.attributes.segmentid ) {\n\t\t\tdomElement.setAttribute( 'data-segmentid', dataElement.attributes.segmentid );\n\t\t}\n\n\t\treturn [ domElement ];\n\t} else {\n\t\treturn [];\n\t}\n};\n\n/* Methods */\n\n/**\n * @return {Object}\n */\nve.dm.CXSentenceSegmentAnnotation.prototype.getComparableObject = function () {\n\treturn {\n\t\ttype: 'cxSegment',\n\t\tsegmentid: this.getAttribute( 'segmentid' )\n\t};\n};\n\n/* Registration */\n\nve.dm.modelRegistry.register( ve.dm.CXSentenceSegmentAnnotation );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXTransclusionNode.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Handling of unadapted templates in translations.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n * @file\n */\n\n/**\n * This class exists to override inlineType and blockType. Those actually inherit\n * the ve.dm.MW* classes, so this is not part of the inheritance tree. Also the\n * static methods are partially \"inherited\" from this class.\n *\n * @class\n * @extends ve.dm.MWTransclusionNode\n * @constructor\n */\nve.dm.CXTransclusionNode = function VeDmCXTransclusionNode() {\n\t// Parent constructor\n\tve.dm.CXTransclusionNode.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.dm.CXTransclusionNode, ve.dm.MWTransclusionNode );\n\n/* Static Properties */\n\nve.dm.CXTransclusionNode.static.name = 'cxTransclusion';\nve.dm.CXTransclusionNode.static.inlineType = 'cxTransclusionInline';\nve.dm.CXTransclusionNode.static.blockType = 'cxTransclusionBlock';\n\n/* Static Methods */\n\nve.dm.CXTransclusionNode.static.toDataElement = function ( domElements ) {\n\tvar cxDataJSON = domElements[ 0 ].getAttribute( 'data-cx' ),\n\t\tcxData = cxDataJSON ? JSON.parse( cxDataJSON ) : {};\n\n\t// Parent method\n\tvar dataElement = ve.dm.CXTransclusionNode.super.static.toDataElement.apply( this, arguments );\n\tdataElement.attributes.cx = cxData;\n\treturn dataElement;\n};\n\nve.dm.CXTransclusionNode.static.toDomElements = function ( dataElement ) {\n\tvar elements = ve.dm.CXTransclusionNode.super.static.toDomElements.apply( this, arguments );\n\tif ( Object.keys( dataElement.attributes.cx ).length ) {\n\t\t// Do not add empty data for data-cx. For example, nodes in source page has no data for cx.\n\t\telements[ 0 ].setAttribute( 'data-cx', JSON.stringify( dataElement.attributes.cx ) );\n\t}\n\treturn elements;\n};\n\n/**\n * Due to cxserver bugs in translating and adapting content, it is possible to have\n * some templates having no data-mw attributes. This can cause various js errors\n * in VE and prevent translation restore. Prevent such errors by ignoring them\n * by validating. See T307967\n *\n * @return {boolean} True if transclusion has valid definition\n */\nve.dm.CXTransclusionNode.prototype.isValid = function () {\n\treturn this.getAttribute( 'mw' ) && this.getAttribute( 'mw' ).parts;\n};\n\n/**\n * Check whether the target title is missing for this template\n *\n * @return {boolean} Whether the target template is missing or not.\n */\nve.dm.CXTransclusionNode.prototype.missingInTargetLanguage = function () {\n\tvar cxData = this.getAttribute( 'cx' ) || {};\n\treturn ve.getProp( cxData, 0, 'targetExists' ) === false;\n};\n\n/**\n * Communicate warnings about unadapted templates.\n *\n * @class\n * @extends ve.dm.CXTransclusionNode\n *\n * @constructor\n * @param {Object} [element] Reference to element in linear model\n */\nve.dm.CXTransclusionBlockNode = function VeDmCXTransclusionBlockNode() {\n\t// Parent constructor\n\tve.dm.CXTransclusionBlockNode.super.apply( this, arguments );\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.CXTransclusionBlockNode, ve.dm.CXTransclusionNode );\n\n/* Static Properties */\n\nve.dm.CXTransclusionBlockNode.static.name = 'cxTransclusionBlock';\n\n/* Only ve.dm.CXTransclusionNode matches, then creates block/inline nodes dynamically */\nve.dm.CXTransclusionBlockNode.static.matchTagNames = [];\n\n/* Methods */\n\nve.dm.CXTransclusionBlockNode.prototype.onAttach = function () {\n\tvar additionalButtons = [],\n\t\tsectionNode = this.findParent( ve.dm.CXSectionNode );\n\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\tvar title, message;\n\tif ( this.missingInTargetLanguage() ) {\n\t\t// TODO: Add help link\n\t\ttitle = mw.msg( 'cx-tools-linter-template' );\n\t\tmessage = mw.message( 'cx-tools-linter-template-block-message' );\n\t\tadditionalButtons.push( {\n\t\t\ticon: 'puzzle',\n\t\t\tlabel: mw.msg( 'cx-tools-linter-template-add-new' ),\n\t\t\taction: this.addNewTemplate\n\t\t} );\n\t} else if ( !this.isAdapted() ) {\n\t\ttitle = mw.msg( 'cx-tools-linter-incomplete-template' );\n\t\tmessage = mw.message( 'cx-tools-linter-empty-template' );\n\t} else if ( this.isPartiallyAdapted() ) {\n\t\ttitle = mw.msg( 'cx-tools-linter-incomplete-template' );\n\t\tmessage = mw.message( 'cx-tools-linter-template-missing-mandatory' );\n\t} else {\n\t\tif ( !this.hasAdaptationInfo() ) {\n\t\t\tmw.log.warn(\n\t\t\t\t'[CX] Template adaptation status is missing for a block template in section ' +\n\t\t\t\tsectionNode.getId()\n\t\t\t);\n\t\t}\n\n\t\treturn;\n\t}\n\n\t// TODO: Add help link\n\tsectionNode.addTranslationIssues( [ {\n\t\tname: 'block-template',\n\t\tmessage: message,\n\t\tmessageInfo: {\n\t\t\ttitle: title,\n\t\t\tresolvable: true,\n\t\t\tadditionalButtons: additionalButtons\n\t\t}\n\t} ] );\n};\n\nve.dm.CXTransclusionBlockNode.prototype.addNewTemplate = function () {\n\tvar targetSurface = ve.init.target.targetSurface,\n\t\tcommand = targetSurface.commandRegistry.lookup( 'transclusion' );\n\n\tcommand.execute( targetSurface );\n};\n\nve.dm.CXTransclusionBlockNode.prototype.onDetach = function ( parent ) {\n\tif ( parent instanceof ve.dm.CXSectionNode && parent.isTargetSection() ) {\n\t\tparent.resolveTranslationIssues( [ 'block-template' ] );\n\t}\n};\n\n/**\n * @return {boolean} Whether cxserver provided adaptation info or not.\n */\nve.dm.CXTransclusionBlockNode.prototype.hasAdaptationInfo = function () {\n\tif ( !this.isValid() ) {\n\t\treturn false;\n\t}\n\tvar cxData = this.getAttribute( 'cx' ) || {};\n\treturn ve.getProp( cxData, 0, 'adapted' ) !== undefined;\n};\n\n/**\n * Check whether the template was adapted by cxserver successfully.\n * It means, some parameters are mapped to target template\n *\n * @return {boolean} Whether template is adapted or not.\n */\nve.dm.CXTransclusionBlockNode.prototype.isAdapted = function () {\n\tif ( !this.isValid() ) {\n\t\treturn false;\n\t}\n\tvar cxData = this.getAttribute( 'cx' ) || {};\n\treturn ve.getProp( cxData, 0, 'adapted' ) === true;\n};\n\n/**\n * Check whether all mandatory parameters are mapped to target template\n *\n * @return {boolean} Whether some mandatory parameters are not mapped\n */\nve.dm.CXTransclusionBlockNode.prototype.isPartiallyAdapted = function () {\n\tvar cxData = this.getAttribute( 'cx' ) || {};\n\treturn ve.getProp( cxData, 0, 'partial' ) === true;\n};\n\n/**\n * Communicate warnings about unadapted inline templates.\n *\n * @class\n * @extends ve.dm.CXTransclusionNode\n *\n * @constructor\n * @param {Object} [element] Reference to element in linear model\n */\nve.dm.CXTransclusionInlineNode = function VeDmCXTransclusionInlineNode() {\n\t// Parent constructor\n\tve.dm.CXTransclusionInlineNode.super.apply( this, arguments );\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.CXTransclusionInlineNode, ve.dm.CXTransclusionNode );\n\n/* Static Properties */\n\nve.dm.CXTransclusionInlineNode.static.name = 'cxTransclusionInline';\n\n/* Only ve.dm.CXTransclusionNode matches, then creates block/inline nodes dynamically */\nve.dm.CXTransclusionInlineNode.static.matchTagNames = [];\n\nve.dm.CXTransclusionInlineNode.static.isContent = true;\n\n/* Methods */\n\nve.dm.CXTransclusionInlineNode.prototype.onAttach = function () {\n\tvar index, part,\n\t\tcxData = this.getAttribute( 'cx' ) || [],\n\t\tsectionNode = this.findParent( ve.dm.CXSectionNode );\n\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\tfor ( index = 0; index < cxData.length; index++ ) {\n\t\tpart = cxData[ index ];\n\n\t\tif ( part.adapted ) {\n\t\t\t// Continue looping to check other parts\n\t\t\tcontinue;\n\t\t}\n\n\t\tif ( part.adapted !== false ) {\n\t\t\tmw.log.warn(\n\t\t\t\t'[CX] Template adaptation status is missing for an inline template ' +\n\t\t\t\t'`%1` in section `%2` for part `$3`'\n\t\t\t\t\t.replace( '%1', OO.getProp( cxData, 'sourceTitle', 'title' ) )\n\t\t\t\t\t.replace( '%2', sectionNode.getId() )\n\t\t\t\t\t.replace( '%3', index )\n\t\t\t);\n\t\t\t// Stop looping, this seems broken enough.\n\t\t\tbreak;\n\t\t}\n\n\t\t// Convert the unadapted inline template to plain text value. T204942\n\t\tsectionNode.on( 'afterTranslation', this.convertToPlainText.bind( this ) );\n\n\t\t// TODO: Add help link\n\t\tsectionNode.addTranslationIssues( [ {\n\t\t\tname: 'inline-template',\n\t\t\tmessage: mw.message( 'cx-tools-linter-template-inline-message' ),\n\t\t\tmessageInfo: {\n\t\t\t\ttitle: mw.msg( 'cx-tools-linter-template' ),\n\t\t\t\tresolvable: true\n\t\t\t}\n\t\t} ] );\n\n\t\t// Stop looping because we already added an issue warning\n\t\tbreak;\n\t}\n};\n\nve.dm.CXTransclusionInlineNode.prototype.onDetach = function ( parent ) {\n\t// FIXME: There might be multiple unadapted inline templates. How to track?\n\tif ( parent instanceof ve.dm.CXSectionNode && parent.isTargetSection() ) {\n\t\tparent.resolveTranslationIssues( [ 'inline-template' ] );\n\t}\n};\n\nve.dm.CXTransclusionInlineNode.prototype.convertToPlainText = function () {\n\tvar fragment, originalDomElements, surfaceModel, textValue = '';\n\n\tsurfaceModel = ve.init.target.targetSurface.getModel();\n\toriginalDomElements = this.getOriginalDomElements( this.doc.store ) || [];\n\tfragment = surfaceModel.getLinearFragment( this.getOuterRange(), true );\n\ttextValue = originalDomElements.map( function ( elem ) {\n\t\treturn elem.innerText || '';\n\t} ).join( '' );\n\tfragment.insertContent( textValue, true );\n};\n\n/* Registration */\n\nve.dm.modelRegistry.register( ve.dm.CXTransclusionNode );\nve.dm.modelRegistry.register( ve.dm.CXTransclusionBlockNode );\nve.dm.modelRegistry.register( ve.dm.CXTransclusionInlineNode );\n\n// Re-register ve.dm.MWReferencesListNode so that it gets high rank in\n// ve.dm.modelRegistry.matchElement. See T206756\nif ( ve.dm.MWReferencesListNode ) {\n\tve.dm.modelRegistry.unregister( ve.dm.MWReferencesListNode );\n\tve.dm.modelRegistry.register( ve.dm.MWReferencesListNode );\n}\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/dm/ve.dm.CXTranslationUnitModel.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * It is assumed that `this` is a ve.dm.Model that belongs to a translation unit\n *\n * @class\n * @abstract\n * @constructor\n */\nve.dm.CXTranslationUnitModel = function VeDmCXTranslationUnitModel() {\n\t//\n};\n\n/* Inheritance */\n\nOO.initClass( ve.dm.CXTranslationUnitModel );\n\n/* Methods */\n\n/**\n * Get the section id for this section.\n * Example: cxTargetSection34\n *\n * @return {string}\n */\nve.dm.CXTranslationUnitModel.prototype.getSectionId = function () {\n\treturn this.getAttribute( 'cxid' );\n};\n\n/**\n * Get the section number for the section. It is common for both\n * source and target section. Examples: 45, 12 etc.\n *\n * @return {number} section number\n */\nve.dm.CXTranslationUnitModel.prototype.getSectionNumber = function () {\n\treturn mw.cx.getSectionNumberFromSectionId( this.getSectionId() );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/init/ve.init.mw.CXTarget.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"mediawiki/class-doc","severity":2,"message":"All possible CSS classes should be documented. See https://w.wiki/PS2 for details.","line":341,"column":2,"nodeType":"CallExpression","endLine":341,"endColumn":52,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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\tthis.mtToolbar = null;\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', // only emitted for article translations\n\t\tsectionTitleChange: 'onTargetSectionTitleChange' // only emitted for section translations\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 targetSectionTitleChange\n *\n * Target section title of the translation has changed. Only used for section translations.\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.integrationType = 'contenttranslation';\n\nve.init.mw.CXTarget.static.publishToolbarGroups = [\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\t{\n\t\tname: 'cx-mt-set-default',\n\t\ttype: 'bar',\n\t\tinvisibleLabel: false,\n\t\tinclude: [ 'save-mt-preference' ]\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\talign: 'after',\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 ( surface ) {\n\t// Parent method\n\tve.init.mw.CXTarget.super.prototype.setupToolbar.apply( this, arguments );\n\n\tthis.publishToolbar = new ve.ui.TargetToolbar( this );\n\tthis.publishToolbar.setup( this.constructor.static.publishToolbarGroups, surface );\n\n\tthis.publishButton = this.publishToolbar.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.publishToolbar.$element );\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\tthis.translation = translation;\n\tvar sourceSurface = this.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\tvar targetSurface = this.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\tif ( this.classList.contains( 'cx-sentence-highlight' ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tvar segmentSelector = '[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\tif ( this.classList.contains( 'cx-section-highlight' ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tvar sectionNumber = 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 = new ve.ui.CXSurface( this, 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\tvar documentView = 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\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\tvar headerHeight = $( 'header.cx-header' ).outerHeight();\n\tvar scrollPosition = $( 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 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\tvar process;\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\n/**\n * @fires targetSectionTitleChange\n */\nve.init.mw.CXTarget.prototype.onTargetSectionTitleChange = function () {\n\tthis.emit( 'targetSectionTitleChange' );\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 isSectionTranslation = this.translationView.targetColumn.isSectionTranslation();\n\n\tif ( !isSectionTranslation ) {\n\t\tvar newTitle = mw.cx.getTitleForNamespace( this.pageName, namespaceId );\n\t\t// Setting title in targetColumn will take care of necessary event firing for title change.\n\t\tthis.translationView.targetColumn.setTitle( newTitle );\n\t\tmw.log( '[CX] Target title changed to ' + newTitle );\n\t}\n\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\tif ( this.publishToolbar ) {\n\t\tthis.publishToolbar.updateToolState();\n\t}\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\tthis.mtToolbar = mtToolbar;\n\t}.bind( this ) );\n};\n\n/**\n * @param {ve.dm.Transaction} transaction\n * @fires contentChange\n */\nve.init.mw.CXTarget.prototype.onDocumentTransact = function ( transaction ) {\n\tthis.emit( 'contentChange' );\n\tthis.debounceAlignSectionPairs();\n\n\t/** @type {ve.dm.Document} */\n\tvar docModel = this.targetSurface.getModel().getDocument();\n\tvar changedRange = transaction.getModifiedRange( docModel, { includeInternalList: true } );\n\tif ( !changedRange ) {\n\t\treturn;\n\t}\n\tvar changedNode = docModel.getBranchNodeFromOffset( changedRange.start );\n\tvar changedSectionNode;\n\tif ( changedNode ) {\n\t\tchangedSectionNode = changedNode.findParent( ve.dm.CXSectionNode );\n\t}\n\tif ( changedSectionNode ) {\n\t\tchangedSectionNode.emitSectionChange();\n\t} else {\n\t\t// In case of references, the node affected is internal list item.\n\t\t// It is possible that the reference is used in multiple sections too.\n\t\t// Register change to all sections.\n\t\tdocModel.getNodesByType( 'cxSection' ).forEach( function ( section ) {\n\t\t\tsection.emitSectionChange();\n\t\t} );\n\t}\n};\n\nve.init.mw.CXTarget.prototype.alignSectionPairs = function () {\n\tvar sourceDocumentNode = this.sourceSurface.getView().getDocument().getDocumentNode();\n\tvar targetDocumentNode = 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\tvar sourceOffsetTop = sourceDocumentNode.$element.offset().top;\n\tvar targetOffsetTop = targetDocumentNode.$element.offset().top;\n\tvar documentNodeChildren = sourceDocumentNode.getChildren();\n\n\tvar articleNode;\n\tfor ( var 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\tvar alignSectionPair = this.translationView.constructor.static.alignSectionPair;\n\t// Save the scroll position. The alignment openration need to reset the heights\n\t// of sections. If the asynchronous content changes from templates happen\n\t// during that time, we will have a different scroll position at the end\n\t// of this alignment. So we lock the scroll position.\n\tvar scrollPosition = $( this.getElementWindow() ).scrollTop();\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\t// Restore scroll position\n\t$( this.getElementWindow() ).scrollTop( scrollPosition );\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 = mw.cx.getSectionNumberFromSectionId( sectionId );\n\tvar sourceId = '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 = mw.cx.getSectionNumberFromSectionId( sectionId );\n\tvar targetId = 'cxTargetSection' + sectionNumber;\n\tvar view = 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 model = 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 $sourceElement = this.getSourceSectionElement( cxid );\n\t\t$sourceElement.removeClass( 'cx-section-highlight' );\n\t\tvar sectionNode = this.getTargetSectionNode( cxid );\n\t\tif ( sectionNode ) {\n\t\t\tsectionNode.emit( 'afterTranslation' );\n\t\t\tthis.prefetchTranslationForSection( sectionNode.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 surfaceModel = this.getSurface().getModel(),\n\t\tdoc = surfaceModel.getDocument(),\n\t\tcxid = section.getSectionId(),\n\t\tfragment = surfaceModel.getLinearFragment( section.getOuterRange(), true /* noAutoSelect */ );\n\n\tvar scrollTop = this.getSurface().view.$window.scrollTop();\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} refTx Transaction generated by newFromDocumentInsertion()\n\t */\n\tfunction deduplicateReferences( refTx ) {\n\t\tfor ( var o = 0; o < refTx.operations.length; o++ ) {\n\t\t\tif ( refTx.operations[ o ].type !== 'replace' ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor ( var i = 0; i < refTx.operations[ o ].insert.length; i++ ) {\n\t\t\t\tvar element = refTx.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\tvar nodeGroup = doc.getInternalList().getNodeGroup( element.attributes.listGroup );\n\t\t\t\tvar kinNodes = 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\tvar pasteDoc = ve.dm.converter.getModelFromDom( ve.createDocumentFromHtml( content ) );\n\tvar docLen = 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\tvar tx = 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 );\n\tvar newRange = tx.getModifiedRange( doc );\n\tsurfaceModel.change( tx, new ve.dm.LinearSelection( newRange ) );\n\n\t// Select first content offset within new content\n\tvar newCursorRange = new ve.Range( surfaceModel.getDocument().data.getNearestContentOffset( newRange.start, 1 ) );\n\tif ( newRange.containsRange( newCursorRange ) ) {\n\t\tsurfaceModel.setLinearSelection( newCursorRange );\n\t}\n\t// Restore scroll top\n\tif ( this.getSurface().view.$window.scrollTop() !== scrollTop ) {\n\t\tthis.getSurface().view.$window.scrollTop( scrollTop );\n\t\tmw.log( '[CX] Scroll position restored to ' + scrollTop );\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 mode = ve.dm.Converter.static.CLIPBOARD_MODE,\n\t\tsourceNodeModel = this.getSourceSectionNode( sectionId ),\n\t\tsectionNumber = sourceNodeModel.getSectionNumber();\n\n\tvar mtRequest = 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\tvar sourceNode = 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\toptions = options || {};\n\tvar cxid = section.getSectionId();\n\tve.dm.converter.isForTranslation = true;\n\tvar html = 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\tvar cachedContent = 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":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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};\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 {string[]}\n */\nve.ui.CXTranslationAction.static.methods = [ 'translate', 'savePreference' ];\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 target = 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\tvar section = 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\tvar originalSource = section.getOriginalContentSource();\n\n\t// Emit Pre-translate event\n\tsection.emit( 'beforeTranslation' );\n\n\tvar promise;\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 destroyed in content change\n\t\t\tsection = target.getTargetSectionNode( section.getSectionId() );\n\t\t\tif ( section ) {\n\t\t\t\t// Emit Post-translate event\n\t\t\t\tsection.emit( 'afterTranslation' );\n\t\t\t}\n\t\t} ).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 * Save the currently selected provider as the preferred provider for new sections.\n *\n * @return {boolean} False if action is cancelled.\n */\nve.ui.CXTranslationAction.prototype.savePreference = function () {\n\tvar mtManager = ve.init.target.config.MTManager,\n\t\tmtToolbar = ve.init.target.mtToolbar,\n\t\tmtProviderTools = {},\n\t\tselection = this.surface.getModel().getSelection();\n\n\tif ( !( selection instanceof ve.dm.LinearSelection ) ) {\n\t\treturn false;\n\t}\n\n\tvar section = 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\tvar currentMTProvider = section.getOriginalContentSource();\n\tmtManager.setPreferredProvider( currentMTProvider );\n\n\t// Fix up the default provider indicator in the MT menu.\n\tmtProviderTools = mtToolbar.getToolGroupByName( 'cx-mt' ).tools;\n\tfor ( var toolName in mtProviderTools ) {\n\t\tif ( mtProviderTools[ toolName ].setIsPreferred ) {\n\t\t\tmtProviderTools[ toolName ].setIsPreferred( toolName === currentMTProvider );\n\t\t}\n\t}\n\t// Toggle the tool\n\tmtToolbar.getToolGroupByName( 'cx-mt-set-default' ).tools[ 'save-mt-preference' ].toggle();\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":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * @license GPL-2.0-or-later\n */\n'use strict';\n\n/**\n * CX specific tool to reset current section to state before user modifications.\n *\n * @class\n * @extends ve.ui.Tool\n */\nve.ui.CXResetSectionTool = function VeUiCXResetSectionTool() {\n\t// Parent constructor\n\tve.ui.CXResetSectionTool.super.apply( this, arguments );\n\t// Hide the tool initially. Need to show only if the section has modifications\n\tthis.toggle( false );\n\n\t// Since state changes are processed asynchronously, we need to wait here\n\t// for a bit to ensure we get the correct state right after first event.\n\t// See this.changeTrackerScheduler in mw.cxTranslationController where the\n\t// timeout is defined\n\tthis.onUpdateStateDebounced = OO.ui.debounce( this.onUpdateState, 200 );\n\n\t// When running on ve.ui.CXSurface, use the transaction event to update the tool state.\n\t// Without this, only surface or surface fragment change would trigger an update, but\n\t// the reset tool really depends on the document state instead. For performance, this\n\t// is only applied for the reset tool, not for the toolbar itself.\n\tthis.toolbar.getSurface().connect( this, {\n\t\tdocumentTransactCX: 'onUpdateStateDebounced'\n\t} );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXResetSectionTool, ve.ui.Tool );\n\n/* Static Properties */\n\nve.ui.CXResetSectionTool.static.name = 'ResetSection';\nve.ui.CXResetSectionTool.static.group = 'mt';\nve.ui.CXResetSectionTool.static.title = mw.msg( 'cx-tools-mt-reset' );\nve.ui.CXResetSectionTool.static.commandName = 'reset-translation';\nve.ui.CXResetSectionTool.static.deactivateOnSelect = true;\nve.ui.CXResetSectionTool.static.autoAddToCatchall = false;\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ui.CXResetSectionTool.prototype.onUpdateState = function () {\n\tvar surface = this.toolbar.getSurface(),\n\t\tselection = surface.getModel().getSelection();\n\n\t// Parent method\n\tve.ui.CXResetSectionTool.super.prototype.onUpdateState.apply( this, arguments );\n\n\t// Check that we are not getting ve.dm.NullSelection\n\tif ( !( selection instanceof ve.dm.LinearSelection ) ) {\n\t\treturn;\n\t}\n\n\t// When changing providers, temporarily, there is no parent section\n\tvar section = mw.cx.getParentSectionForSelection( surface, selection );\n\tif ( !section ) {\n\t\treturn;\n\t}\n\n\tvar oldState = this.isVisible();\n\tvar newState = section.hasUserModifications();\n\n\t// For some reason this tool is set to disabled frequently. Make sure it is enabled when visible.\n\tthis.setDisabled( !newState );\n\n\tif ( oldState === newState ) {\n\t\treturn;\n\t}\n\n\t// If section has user modifications, show the tool (or hide if the user used reset)\n\tthis.toggle( newState );\n};\n\n/* Registration */\n\nve.ui.commandRegistry.register(\n\tnew ve.ui.Command(\n\t\t'reset-translation', 'translation', 'translate',\n\t\t{ args: [ 'reset-translation' ], supportedSelections: [ 'linear' ] }\n\t)\n);\n\nve.ui.toolFactory.register( ve.ui.CXResetSectionTool );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/tools/ve.ui.CXSaveMTPreferenceTool.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * @license GPL-2.0-or-later\n */\n'use strict';\n\n/**\n * CX specific tool to save current MT engine as default preference\n *\n * @class\n * @extends ve.ui.Tool\n */\nve.ui.CXSaveMTPreferenceTool = function VeUiCXSaveMTPreferenceTool() {\n\t// Parent constructor\n\tve.ui.CXSaveMTPreferenceTool.super.apply( this, arguments );\n\t// Hide the tool initially. Need to show only if the section has modifications\n\tthis.toggle( false );\n\n\tthis.MTManager = ve.init.target.config.MTManager;\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXSaveMTPreferenceTool, ve.ui.Tool );\n\n/* Static Properties */\n\nve.ui.CXSaveMTPreferenceTool.static.name = 'save-mt-preference';\nve.ui.CXSaveMTPreferenceTool.static.group = 'mt-default';\nve.ui.CXSaveMTPreferenceTool.static.icon = 'pushPin';\nve.ui.CXSaveMTPreferenceTool.static.title = OO.ui.deferMsg( 'cx-tools-mt-set-default' );\nve.ui.CXSaveMTPreferenceTool.static.label = OO.ui.deferMsg( 'cx-tools-mt-set-default' );\nve.ui.CXSaveMTPreferenceTool.static.displayBothIconAndLabel = true;\nve.ui.CXSaveMTPreferenceTool.static.commandName = 'save-mt-preference';\nve.ui.CXSaveMTPreferenceTool.static.deactivateOnSelect = true;\nve.ui.CXSaveMTPreferenceTool.static.autoAddToCatchall = false;\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ui.CXSaveMTPreferenceTool.prototype.onUpdateState = function () {\n\tvar surface = this.toolbar.getSurface(),\n\t\tselection = surface.getModel().getSelection();\n\n\t// Parent method\n\tve.ui.CXSaveMTPreferenceTool.super.prototype.onUpdateState.apply( this, arguments );\n\n\t// Check that we are not getting ve.dm.NullSelection\n\tif ( !( selection instanceof ve.dm.LinearSelection ) ) {\n\t\treturn;\n\t}\n\n\t// When changing provides, there is temporarily no parent section\n\tvar section = mw.cx.getParentSectionForSelection( surface, selection );\n\tif ( !section ) {\n\t\treturn;\n\t}\n\n\tthis.MTManager.getDefaultProvider().then( function ( defaultProvider ) {\n\t\tvar source = section.getOriginalContentSource() || defaultProvider;\n\t\tthis.MTManager.getPreferredProvider().then( function ( preferredProvider ) {\n\t\t\tthis.toggle( source !== preferredProvider );\n\t\t}.bind( this ) );\n\t}.bind( this ) );\n\n};\n\n/* Registration */\n\nve.ui.commandRegistry.register(\n\tnew ve.ui.Command(\n\t\t'save-mt-preference', 'translation', 'savePreference',\n\t\t{ supportedSelections: [ 'linear' ] }\n\t)\n);\n\nve.ui.toolFactory.register( ve.ui.CXSaveMTPreferenceTool );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXDesktopContext.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * CX Context menu and inspectors.\n *\n * @class\n * @extends ve.ui.DesktopContext\n *\n * @constructor\n * @param {ve.ui.CXSurface} surface\n * @param {Object} [config]\n */\nve.ui.CXDesktopContext = function VeUiCXDesktopContext() {\n\t// Parent constructor\n\tve.ui.CXDesktopContext.super.apply( this, arguments );\n\n\tthis.popup.$element.remove();\n\n\t// Initialization\n\tthis.$element.addClass( 've-ui-cxDesktopContext' ).append(\n\t\tthis.$group,\n\t\tthis.inspectors.$element\n\t\t\t.addClass( 've-ui-cxDesktopContext-inspectors' )\n\t);\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXDesktopContext, ve.ui.DesktopContext );\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ui.CXDesktopContext.prototype.afterContextChange = function () {\n\t// Parent method\n\tve.ui.CXDesktopContext.super.prototype.afterContextChange.call( this );\n\n\tthis.emit( 'afterContextChange' );\n};\n\nve.ui.CXDesktopContext.prototype.updateDimensions = function () {};\n\nve.ui.CXDesktopContext.prototype.setPopupSizeAndPosition = function () {};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXInternalLinkAnnotationWidget.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Creates an ve.ui.CXInternalLinkAnnotationWidget object.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @extends ve.ui.MWInternalLinkAnnotationWidget\n *\n * @constructor\n * @param {Object} [config] Configuration options\n */\nve.ui.CXInternalLinkAnnotationWidget = function VeUiCXInternalLinkAnnotationWidget() {\n\t// Parent constructor\n\tve.ui.CXInternalLinkAnnotationWidget.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXInternalLinkAnnotationWidget, ve.ui.MWInternalLinkAnnotationWidget );\n\n/* Static Methods */\n\n/**\n * @inheritdoc\n */\nve.ui.CXInternalLinkAnnotationWidget.static.getAnnotationFromText = function ( value ) {\n\tvar trimmed = value.trim(),\n\t\ttitle = mw.Title.newFromText( trimmed );\n\n\tif ( !title ) {\n\t\treturn null;\n\t}\n\n\treturn ve.dm.CXLinkAnnotation.static.newFromTitle( title, trimmed );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXLinkAnnotationInspector.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Inspector for applying and editing labeled MediaWiki internal and external links.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @constructor\n * @extends ve.ui.MWLinkAnnotationInspector\n *\n * @param {Object} [config] Configuration options\n */\nve.ui.CXLinkAnnotationInspector = function VeUiCXLinkAnnotationInspector( config ) {\n\t// Parent constructor\n\tve.ui.CXLinkAnnotationInspector.super.call( this, config );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXLinkAnnotationInspector, ve.ui.MWLinkAnnotationInspector );\n\n/* Static properties */\n\nve.ui.CXLinkAnnotationInspector.static.modelClasses = [\n\tve.dm.MWExternalLinkAnnotation,\n\tve.dm.CXLinkAnnotation\n];\n\n/* Methods */\n\n/**\n * @return {ve.ui.CXInternalLinkAnnotationWidget}\n */\nve.ui.CXLinkAnnotationInspector.prototype.createInternalAnnotationInput = function () {\n\treturn new ve.ui.CXInternalLinkAnnotationWidget();\n};\n\n/**\n * @inheritdoc\n * @return {ve.dm.CXLinkAnnotation} The annotation.\n */\nve.ui.CXLinkAnnotationInspector.prototype.newInternalLinkAnnotationFromTitle = function ( title ) {\n\treturn ve.dm.CXLinkAnnotation.static.newFromTitle( title );\n};\n\n/**\n * @inheritdoc\n */\nve.ui.CXLinkAnnotationInspector.prototype.ready = function ( data ) {\n\treturn this.getReadyProcess( data ).execute().then( function () {\n\t\tthis.$element.addClass( 'oo-ui-window-ready' ).width();\n\t\tthis.$content.addClass( 'oo-ui-window-content-ready' ).width();\n\t}.bind( this ) );\n};\n\n/* Registration */\n\nve.ui.windowFactory.register( ve.ui.CXLinkAnnotationInspector );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXLinkContextItem.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Context item for CXLinkAnnotation\n *\n * @class\n * @extends ve.ui.MWInternalLinkContextItem\n * @constructor\n */\nve.ui.CXLinkContextItem = function VeUiCXLinkContextItem() {\n\t// Parent constructor\n\tve.ui.CXLinkContextItem.super.apply( this, arguments );\n\t// Mixin constructor\n\tve.ui.CXTranslationUnitContextItem.apply( this, arguments );\n\tthis.$sourceBody = $( '<div>' )\n\t\t.addClass( 've-ui-linearContextItem-body ve-ui-cxLinkContextItem-sourceBody' )\n\t\t.insertAfter( this.$body );\n\tthis.$body.addClass( 've-ui-cxLinkContextItem-targetBody' );\n\tthis.$element.addClass( 've-ui-cxLinkContextItem' );\n\n\tthis.inTargetSurface = this.context.surface === ve.init.target.targetSurface;\n\tthis.thisLinkCache = this.inTargetSurface ? ve.init.platform.linkCache : ve.init.platform.sourceLinkCache;\n\tthis.otherLinkCache = this.inTargetSurface ? ve.init.platform.sourceLinkCache : ve.init.platform.linkCache;\n\tthis.requestManager = ve.init.target.requestManager;\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXLinkContextItem, ve.ui.MWInternalLinkContextItem );\nOO.mixinClass( ve.ui.CXLinkContextItem, ve.ui.CXTranslationUnitContextItem );\n\n/* Static Properties */\n\nve.ui.CXLinkContextItem.static.name = 'cxLink';\n\nve.ui.CXLinkContextItem.static.modelClasses = [ ve.dm.CXLinkAnnotation ];\n\n/* Static Methods */\n\nve.ui.CXLinkContextItem.static.generateSourceBody = function ( linkInfo, language ) {\n\tvar $wrapper = $( '<div>' );\n\n\tvar $linkTitle = $( '<a>' )\n\t\t.addClass( 've-ui-cxLinkContextItem-title' )\n\t\t.text( linkInfo.title )\n\t\t.prop( {\n\t\t\ttarget: '_blank',\n\t\t\ttitle: linkInfo.description || linkInfo.title,\n\t\t\thref: ve.init.target.config.siteMapper.getPageUrl( linkInfo.pagelanguage, linkInfo.title )\n\t\t} );\n\tvar $languageLabel = $( '<span>' )\n\t\t.addClass( 've-ui-cxLinkContextItem-language' )\n\t\t.text( ve.init.platform.getLanguageAutonym( language ) );\n\t$wrapper.append( $linkTitle, $languageLabel );\n\treturn $wrapper;\n};\n\n/**\n * Generate the body of the link context item\n *\n * @param {Object} linkInfo The object with title meta data originally served by\n *   cxserver, but later extra properties added to help the rendering.\n * @param {ve.ui.Context} context Context (for resizing)\n * @return {jQuery} The jQuery object of the link context item\n */\nve.ui.CXLinkContextItem.static.generateBody = function ( linkInfo, context ) {\n\tvar $wrapper = $( '<div>' ),\n\t\t$linkTitle = $( '<a>' ),\n\t\tsiteMapper = ve.init.target.siteMapper;\n\n\tvar linkHref;\n\tif ( linkInfo.missing ) {\n\t\t// Link to start a new translation\n\t\tlinkHref = siteMapper.getCXUrl(\n\t\t\tlinkInfo.title, // Source title\n\t\t\tlinkInfo.title, // Target title\n\t\t\tlinkInfo.sourceLanguage,\n\t\t\tlinkInfo.pagelanguage\n\t\t);\n\t\t$linkTitle.addClass( 'new' );\n\t} else {\n\t\tlinkHref = siteMapper.getPageUrl( linkInfo.pagelanguage, linkInfo.title );\n\t}\n\n\t$linkTitle\n\t\t.addClass( 've-ui-linkContextItem-link ve-ui-cxLinkContextItem-title' )\n\t\t.text( linkInfo.title )\n\t\t.prop( {\n\t\t\ttarget: '_blank',\n\t\t\ttitle: linkInfo.title,\n\t\t\thref: linkHref\n\t\t} );\n\n\tvar icon = new OO.ui.IconWidget( { icon: 'article' } );\n\t$wrapper\n\t\t.addClass( 've-ui-mwInternalLinkContextItem-withImage' )\n\t\t.addClass( 've-ui-mwInternalLinkContextItem-withDescription' )\n\t\t.append( icon.$element );\n\n\t$wrapper.append( $linkTitle );\n\n\tvar description = linkInfo.description;\n\tif ( description ) {\n\t\tvar $description = $( '<span>' )\n\t\t\t.addClass( 've-ui-mwInternalLinkContextItem-description' )\n\t\t\t.text( description );\n\t\t$wrapper.append( $description );\n\t\t// Multiline descriptions may make the context bigger (T183650)\n\t\tcontext.updateDimensions();\n\t}\n\n\tvar imageUrl = OO.getProp( linkInfo, 'thumbnail', 'source' );\n\tif ( imageUrl ) {\n\t\ticon.$element\n\t\t\t.addClass( 've-ui-mwInternalLinkContextItem-hasImage' )\n\t\t\t.css( 'background-image', 'url(' + imageUrl + ')' );\n\t}\n\n\treturn $wrapper;\n};\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ui.CXLinkContextItem.prototype.isEditable = function () {\n\t// adaptationInfo will be empty in source surface\n\tvar adaptationInfo = this.model.getAttribute( 'cx' );\n\t// Parent method\n\treturn ve.ui.CXLinkContextItem.super.prototype.isEditable.apply( this, arguments ) &&\n\t\t( !adaptationInfo || adaptationInfo.adapted || adaptationInfo.targetTitle );\n};\n\n/**\n * @inheritdoc\n */\nve.ui.CXLinkContextItem.prototype.renderBody = function () {\n\tvar adaptationInfo = this.model.getAttribute( 'cx' );\n\n\t// Case 1: Server-side adapted blue or red link in the target column. Information\n\t// is present on the link attributes, so additional requests or caching are not needed.\n\tif ( adaptationInfo && !adaptationInfo.userAdded ) {\n\t\tthis.$body.empty().append( this.generateBody( adaptationInfo ) );\n\t\treturn;\n\t}\n\n\t// Case 2: Cached hit on a manually added link or a link in the source column.\n\tvar store = this.model.getStore();\n\tvar normalizedTitle = this.model.getAttribute( 'normalizedTitle' );\n\tadaptationInfo = store.value( store.hashOfValue( null, OO.getHash( normalizedTitle ) ) );\n\tif ( adaptationInfo ) {\n\t\tthis.$body.empty().append( this.generateBody( adaptationInfo ) );\n\t\treturn;\n\t}\n\n\t// ve.ui.LinearContextItem#setup assumes renderBody is synchronous, so add\n\t// some content so it isn't thought to be empty.\n\t// TODO: This could be a loading message.\n\tthis.$body.append( $( '<span>' ) );\n\n\t// Case 3: First click on a link in the source column, or a first click on\n\t// a link in the target column added manually by the translator. This will\n\t// cache the results so that case 2 is hit on subsequent hits.\n\tthis.getLinkInfo().then( function ( linkAdaptationInfo ) {\n\t\tthis.$body.empty().append( this.generateBody( linkAdaptationInfo ) );\n\t}.bind( this ) );\n};\n\n/**\n * Get info about user-added links. Once fetched, link info is stored\n * inside the model, so this method should execute once per user-added link.\n *\n * See renderBody method on this object, to see how future creation of\n * link context item body uses the info stored as attribute of the model.\n *\n * @return {jQuery.Promise}\n */\nve.ui.CXLinkContextItem.prototype.getLinkInfo = function () {\n\tvar thisTitle = this.model.getAttribute( 'normalizedTitle' ),\n\t\tthisLanguage = this.inTargetSurface ? this.translation.getTargetLanguage() : this.translation.getSourceLanguage(),\n\t\totherLanguage = this.inTargetSurface ? this.translation.getSourceLanguage() : this.translation.getTargetLanguage(),\n\t\tthisTitleKey = this.inTargetSurface ? 'targetTitle' : 'sourceTitle',\n\t\totherTitleKey = this.inTargetSurface ? 'sourceTitle' : 'targetTitle',\n\t\tadaptationInfo = {};\n\n\tvar thisTitlePromise = this.thisLinkCache.get( thisTitle ).then( function ( thisLinkData ) {\n\t\tadaptationInfo[ thisTitleKey ] = {\n\t\t\ttitle: thisTitle,\n\t\t\tpagelanguage: thisLanguage,\n\t\t\tdescription: thisLinkData.description,\n\t\t\tthumbnail: { source: thisLinkData.imageUrl }\n\t\t};\n\t} );\n\n\tvar otherTitlePromise = this.requestManager.getTitlePair( thisLanguage, thisTitle ).then( function ( titlePairInfo ) {\n\t\tvar otherTitle = titlePairInfo.targetTitle;\n\n\t\tadaptationInfo.adapted = true;\n\n\t\tif ( titlePairInfo.missing ) {\n\t\t\treturn;\n\t\t}\n\n\t\treturn this.otherLinkCache.get( otherTitle ).then( function ( otherLinkData ) {\n\t\t\tadaptationInfo[ otherTitleKey ] = {\n\t\t\t\ttitle: otherTitle,\n\t\t\t\tpagelanguage: otherLanguage,\n\t\t\t\tdescription: otherLinkData.description,\n\t\t\t\tthumbnail: { source: otherLinkData.imageUrl }\n\t\t\t};\n\t\t} );\n\t}.bind( this ) );\n\n\treturn $.when( thisTitlePromise, otherTitlePromise ).then( function () {\n\t\tthis.model.getStore().hash( adaptationInfo, OO.getHash( thisTitle ) );\n\n\t\treturn adaptationInfo;\n\t}.bind( this ) );\n};\n\nve.ui.CXLinkContextItem.prototype.generateBody = function ( adaptationInfo ) {\n\tvar $sourceLink;\n\tif ( adaptationInfo.sourceTitle ) {\n\t\t// Source link\n\t\t$sourceLink = ve.ui.CXLinkContextItem.static.generateSourceBody(\n\t\t\tadaptationInfo.sourceTitle,\n\t\t\tthis.translation.getSourceLanguage()\n\t\t);\n\t\tthis.$sourceBody.empty().append( $sourceLink );\n\t}\n\n\tif ( !adaptationInfo.adapted && adaptationInfo.targetTitle ) {\n\t\tthis.setLabel( mw.msg( 'cx-linkcontextitem-missing-link-title' ) );\n\t}\n\n\t// Target link\n\tvar $targetLinkCard;\n\tif ( adaptationInfo.targetTitle ) {\n\t\t$targetLinkCard = ve.ui.CXLinkContextItem.static.generateBody(\n\t\t\tadaptationInfo.targetTitle, this.context\n\t\t);\n\t} else {\n\t\tthis.setLabel( mw.msg( 'cx-linkcontextitem-missing-link-title' ) );\n\t\t// TODO: Support mark-as-missing button on source surface.\n\t\tif ( this.inTargetSurface ) {\n\t\t\tvar markAsMissingButton = new OO.ui.ButtonWidget( {\n\t\t\t\tlabel: mw.msg( 'cx-tools-missing-link-mark-link' ),\n\t\t\t\tclasses: [ 've-ui-cxLinkContextItem-mark-missing-button' ],\n\t\t\t\tflags: [ 'progressive' ]\n\t\t\t} );\n\t\t\tvar $markAsMissingInfo = $( '<div>' )\n\t\t\t\t.addClass( 've-ui-cxLinkContextItem-mark-missing-text' )\n\t\t\t\t.text( mw.msg( 'cx-tools-missing-link-text' ) );\n\t\t\t$targetLinkCard = $( '<div>' )\n\t\t\t\t.addClass( 've-ui-cxLinkContextItem-mark-missing' )\n\t\t\t\t.append( $markAsMissingInfo, markAsMissingButton.$element );\n\n\t\t\tmarkAsMissingButton.on( 'click', this.createRedLink.bind( this, adaptationInfo ) );\n\t\t}\n\t}\n\n\treturn $targetLinkCard;\n};\n\n/**\n * Change the grey link to red link.\n *\n * @param {Object} adaptationInfo\n */\nve.ui.CXLinkContextItem.prototype.createRedLink = function ( adaptationInfo ) {\n\tvar targetLanguage = this.translation.getTargetLanguage(),\n\t\tsourceLanguage = this.translation.getSourceLanguage();\n\n\t// Handle a red link for which we have no idea what is the target page, as determined by\n\t// cxserver. Per T224408, open the link target selection widget to let the user confirm\n\t// the right target.\n\tif ( adaptationInfo.targetFrom === 'source' ) {\n\t\tthis.context.getSurface().commandRegistry.registry.link.execute(\n\t\t\tthis.context.getSurface()\n\t\t);\n\t\treturn;\n\t}\n\n\t// See ve.ui.AnnotationContextItem#applyToAnnotations\n\tthis.applyToAnnotations( function ( fragment, annotation ) {\n\t\t// Clear the annotation from fragment\n\t\tfragment.annotateContent( 'clear', annotation );\n\n\t\t// Clone the old element data to avoid modifying the old state (T202440)\n\t\tvar newElement = ve.copy( annotation.element );\n\t\t// Update the adaptation info.\n\t\tnewElement.attributes.cx.targetTitle = {\n\t\t\ttitle: newElement.attributes.cx.sourceTitle.title,\n\t\t\tpagelanguage: targetLanguage,\n\t\t\tsourceLanguage: sourceLanguage, // Required to provide CX link\n\t\t\tmissing: true,\n\t\t\tdescription: mw.msg( 'cx-linkcontextitem-missing-title-description' )\n\t\t};\n\n\t\t// Set the updated annotation to the fragment\n\t\tfragment.annotateContent( 'set', ve.dm.annotationFactory.createFromElement( newElement ) );\n\t} );\n};\n\n/* Registration */\n\nve.ui.contextItemFactory.register( ve.ui.CXLinkContextItem );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXPublishSettingsDialog.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Creates an ve.ui.CXPublishSettingsDialog object.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @constructor\n * @extends OO.ui.ProcessDialog\n */\nve.ui.CXPublishSettingsDialog = function VeUiCXPublishSettingsDialog() {\n\t// Parent constructor\n\tve.ui.CXPublishSettingsDialog.parent.apply( this, arguments );\n\n\tthis.namespaceSelector = null;\n\tthis.initialNamespace = null;\n\tthis.mainNamespaceId = mw.config.get( 'wgNamespaceIds' )[ '' ];\n\tthis.targetTitleExists = false;\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXPublishSettingsDialog, OO.ui.ProcessDialog );\n\n/* Static properties */\n\nve.ui.CXPublishSettingsDialog.static.name = 'publishSettings';\n\nve.ui.CXPublishSettingsDialog.static.title = OO.ui.deferMsg( 'cx-publish-settings' );\n\nve.ui.CXPublishSettingsDialog.static.actions = [\n\t{\n\t\tlabel: OO.ui.deferMsg( 'visualeditor-dialog-action-cancel' ),\n\t\tflags: [ 'safe', 'back' ]\n\t},\n\t{\n\t\taction: 'done',\n\t\tlabel: OO.ui.deferMsg( 'visualeditor-dialog-action-apply' ),\n\t\tflags: [ 'progressive', 'primary' ],\n\t\tdisabled: true\n\t}\n];\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ui.CXPublishSettingsDialog.prototype.getActionProcess = function ( action ) {\n\tif ( action ) {\n\t\treturn new OO.ui.Process( function () {\n\t\t\tif ( action === 'done' ) {\n\t\t\t\tve.init.target.onPublishNamespaceChange(\n\t\t\t\t\tthis.namespaceSelector.findSelectedItem().getData()\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthis.close();\n\t\t}, this );\n\t}\n\n\t// Parent method\n\treturn ve.ui.CXPublishSettingsDialog.parent.prototype.getActionProcess.call( this, action );\n};\n\n/**\n * Update the 'done' action according to whether there are changes\n *\n * @param {OO.ui.OptionWidget|null} selectedItem\n */\nve.ui.CXPublishSettingsDialog.prototype.updateActions = function ( selectedItem ) {\n\tvar namespace = selectedItem ? selectedItem.getData() : null;\n\tthis.actions.setAbilities( { done: this.initialNamespace !== namespace } );\n};\n\n/**\n * @inheritdoc\n */\nve.ui.CXPublishSettingsDialog.prototype.initialize = function () {\n\tvar namespaceIds = mw.config.get( 'wgNamespaceIds' );\n\n\t// Parent method\n\tve.ui.CXPublishSettingsDialog.parent.prototype.initialize.apply( this, arguments );\n\n\tvar publishDestinationLabel = new OO.ui.LabelWidget( {\n\t\tlabel: mw.msg( 'cx-publish-destination-header' ),\n\t\tclasses: [ 've-ui-cxPublishSettings-destination' ]\n\t} );\n\n\tvar helpIcon = new OO.ui.ButtonWidget( {\n\t\ticon: 'helpNotice',\n\t\tframed: false,\n\t\thref: 'https://www.mediawiki.org/wiki/Help:Content_translation/Publishing',\n\t\ttarget: '_blank'\n\t} );\n\n\tvar $publishSettingsContainer = $( '<div>' )\n\t\t.addClass( 've-ui-cxPublishSettings-destination-container' )\n\t\t.append( publishDestinationLabel.$element, helpIcon.$element );\n\n\tthis.namespaceSelector = new OO.ui.RadioSelectWidget( {\n\t\tclasses: [ 've-ui-cxPublishSettings-selector' ],\n\t\titems: [\n\t\t\tthis.createRadioOptionWidget( 'main', namespaceIds[ '' ] ),\n\t\t\tthis.createRadioOptionWidget( 'user', namespaceIds.user )\n\t\t]\n\t} );\n\n\tif ( namespaceIds.draft ) {\n\t\tthis.namespaceSelector.addItems( this.createRadioOptionWidget( 'draft', namespaceIds.draft ) );\n\t}\n\n\tthis.$body.append( $publishSettingsContainer, this.namespaceSelector.$element );\n};\n\n/**\n * @inheritdoc\n */\nve.ui.CXPublishSettingsDialog.prototype.getSetupProcess = function ( data ) {\n\t// Parent method\n\treturn ve.ui.CXPublishSettingsDialog.parent.prototype.getSetupProcess.call( this, data )\n\t\t.next( function () {\n\t\t\tthis.initialNamespace = ve.init.target.getPublishNamespace();\n\t\t\tthis.namespaceSelector.connect( this, { select: 'updateActions' } );\n\t\t\tthis.namespaceSelector.selectItemByData( this.initialNamespace );\n\t\t\tthis.doesTargetTitleExist().then( this.changeMainNamespaceLabel.bind( this ) );\n\t\t}, this );\n};\n\n/**\n * @inheritdoc\n */\nve.ui.CXPublishSettingsDialog.prototype.getReadyProcess = function ( data ) {\n\t// Parent method\n\treturn ve.ui.CXPublishSettingsDialog.parent.prototype.getReadyProcess.call( this, data )\n\t\t.next( function () {\n\t\t\tif ( this.namespaceSelector.findSelectedItem() ) {\n\t\t\t\t// Focus causes the first item to become selected\n\t\t\t\tthis.namespaceSelector.focus();\n\t\t\t}\n\t\t}, this );\n};\n\n/**\n * @inheritdoc\n */\nve.ui.CXPublishSettingsDialog.prototype.getTeardownProcess = function ( data ) {\n\t// Parent method\n\treturn ve.ui.CXPublishSettingsDialog.parent.prototype.getTeardownProcess.call( this, data )\n\t\t.next( function () {\n\t\t\tthis.namespaceSelector.disconnect( this );\n\t\t\tthis.namespaceSelector.selectItem();\n\t\t}, this );\n};\n\n/**\n * Create OO.ui.RadioOptionWidget with description\n *\n * @param {string} namespace Name of the namespace. 'main', 'user' or 'draft'.\n *  Also used as message key for label and description:\n *  - Label: `cx-publish-destination-namespace-${namespace}`\n *  - Description: `cx-publish-destination-namespace-${namespace}-description`\n * @param {Mixed} data Option widget data\n * @return {OO.ui.RadioOptionWidget}\n */\nve.ui.CXPublishSettingsDialog.prototype.createRadioOptionWidget = function ( namespace, data ) {\n\tvar radioOption = new OO.ui.RadioOptionWidget( {\n\t\t// Messages used here:\n\t\t// * cx-publish-destination-namespace-main\n\t\t// * cx-publish-destination-namespace-user\n\t\t// * cx-publish-destination-namespace-draft\n\t\tlabel: mw.msg( 'cx-publish-destination-namespace-' + namespace ),\n\t\tdata: data\n\t} );\n\tvar description = new OO.ui.LabelWidget( {\n\t\t// Messages used here:\n\t\t// * cx-publish-destination-namespace-main-description\n\t\t// * cx-publish-destination-namespace-user-description\n\t\t// * cx-publish-destination-namespace-draft-description\n\t\tlabel: mw.msg( 'cx-publish-destination-namespace-' + namespace + '-description' ),\n\t\tclasses: [ 'oo-ui-inline-help' ]\n\t} );\n\n\tradioOption.$element.append( description.$element );\n\n\treturn radioOption;\n};\n\n/**\n * @return {jQuery.Promise}\n */\nve.ui.CXPublishSettingsDialog.prototype.doesTargetTitleExist = function () {\n\tvar pageTitle = mw.cx.getTitleForNamespace( ve.init.target.getPageName(), this.mainNamespaceId );\n\n\treturn ve.init.platform.linkCache.get( pageTitle ).then( function ( result ) {\n\t\treturn !result.missing;\n\t} );\n};\n\n/**\n * @param {boolean} targetTitleExists True if target title exists in main namespace\n */\nve.ui.CXPublishSettingsDialog.prototype.changeMainNamespaceLabel = function ( targetTitleExists ) {\n\tvar msgKey = 'cx-publish-destination-namespace-main';\n\n\tif ( this.targetTitleExists === targetTitleExists ) {\n\t\treturn;\n\t}\n\n\tthis.targetTitleExists = targetTitleExists;\n\n\tmsgKey += targetTitleExists ? '-exists' : '';\n\t// Messages used here:\n\t// * cx-publish-destination-namespace-main\n\t// * cx-publish-destination-namespace-main-exists\n\tthis.namespaceSelector.findItemFromData( this.mainNamespaceId ).setLabel( mw.msg( msgKey ) );\n};\n\n/* Registration */\n\nve.ui.windowFactory.register( ve.ui.CXPublishSettingsDialog );\n\nve.ui.commandRegistry.register(\n\tnew ve.ui.Command(\n\t\t'publishSettings', 'window', 'open',\n\t\t{ args: [ 'publishSettings' ] }\n\t)\n);\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXPublishSettingsTool.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Publishing settings tool\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @extends ve.ui.WindowTool\n * @constructor\n * @param {OO.ui.ToolGroup} toolGroup\n * @param {Object} [config] Configuration options\n */\nve.ui.CXPublishSettingsTool = function VeUiCXPublishSettingsTool() {\n\t// Parent constructor\n\tve.ui.CXPublishSettingsTool.super.apply( this, arguments );\n\n\t// Make the tool findable\n\tthis.setData( 'publishSettings' );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXPublishSettingsTool, ve.ui.WindowTool );\n\n/* Static Properties */\n\nve.ui.CXPublishSettingsTool.static.name = 'publishSettings';\n\nve.ui.CXPublishSettingsTool.static.icon = 'settings';\n\nve.ui.CXPublishSettingsTool.static.title = OO.ui.deferMsg( 'cx-publish-destination-tooltip' );\n\nve.ui.CXPublishSettingsTool.static.commandName = 'publishSettings';\n\nve.ui.CXPublishSettingsTool.static.autoAddToCatchall = false;\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ui.CXPublishSettingsTool.prototype.onUpdateState = function () {\n\t// Parent method\n\tve.ui.CXPublishSettingsTool.super.prototype.onUpdateState.apply( this, arguments );\n\n\tthis.setDisabled(\n\t\tve.init.target.targetSurface.isReadOnly() ||\n\t\t!mw.Title.newFromText( ve.init.target.pageName )\n\t);\n};\n\n/* Register */\n\nve.ui.toolFactory.register( ve.ui.CXPublishSettingsTool );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXPublishTool.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null},{"ruleId":"es-x/no-regexp-prototype-flags","severity":2,"message":"ES2015 'RegExp.prototype.flags' property is forbidden.","line":36,"column":1,"nodeType":"MemberExpression","messageId":"forbidden","endLine":36,"endColumn":33}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Publishing tool\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @extends ve.ui.Tool\n * @constructor\n * @param {OO.ui.ToolGroup} toolGroup\n * @param {Object} [config] Configuration options\n */\nve.ui.CXPublishTool = function VeUiCXPublishTool() {\n\t// Parent constructor\n\tve.ui.CXPublishTool.super.apply( this, arguments );\n\n\tthis.$link.addClass( 'cx-header__publish-button' );\n\t// Make the tool find-able\n\tthis.setData( 'publish' );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXPublishTool, ve.ui.Tool );\n\n/* Static Properties */\n\nve.ui.CXPublishTool.static.name = 'publish';\n\nve.ui.CXPublishTool.static.title = OO.ui.deferMsg( 'cx-publish-button' );\n\nve.ui.CXPublishTool.static.displayBothIconAndLabel = true;\n\nve.ui.CXPublishTool.static.autoAddToCatchall = false;\n\nve.ui.CXPublishTool.static.flags = [ 'primary', 'progressive' ];\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ui.CXPublishTool.prototype.onSelect = function () {\n\tthis.setActive( false );\n\tthis.toolbar.getTarget().onPublishButtonClick();\n};\n\n/* Register */\n\nve.ui.toolFactory.register( ve.ui.CXPublishTool );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXReferenceContextItem.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"( function () {\n\t'use strict';\n\n\t/* Override for ve.ui.MWReferenceContextItem.prototype.getRendering as it creates\n\t * a ve.ui.MWPreviewElement with default parameters. ve.ui.MWPreviewElement has\n\t * useView configuration as false by default and causes not rendering the references\n\t * from its DOM model as required for content translation.\n\t *\n\t * Get a DOM rendering of the reference.\n\t *\n\t * @private\n\t * @return {jQuery} DOM rendering of reference\n\t**/\n\tve.ui.MWReferenceContextItem.prototype.getRendering = function getReferenceRendering() {\n\t\tvar refNode = this.getReferenceNode();\n\t\tif ( refNode ) {\n\t\t\tthis.view = new ve.ui.MWPreviewElement( refNode, {\n\t\t\t\tuseView: true\n\t\t\t} );\n\t\t\t// The $element property may be rendered into asynchronously, update the context's size when the\n\t\t\t// rendering is complete if that's the case\n\t\t\tthis.view.once( 'render', this.context.updateDimensions.bind( this.context ) );\n\t\t\treturn this.view.$element;\n\t\t} else {\n\t\t\treturn $( '<div>' )\n\t\t\t\t.addClass( 've-ui-mwReferenceContextItem-muted' )\n\t\t\t\t.text( ve.msg( 'cite-ve-referenceslist-missingref' ) );\n\t\t}\n\t};\n\n\tfunction addCxSubclass( parentContextItem ) {\n\t\tvar contextItem = function CXGeneratedMWCitationContextItem() {\n\t\t\t// Parent constructor\n\t\t\tparentContextItem.apply( this, arguments );\n\t\t};\n\n\t\tOO.inheritClass( contextItem, parentContextItem );\n\n\t\tcontextItem.prototype.getRendering = function () {\n\t\t\tvar cxData = this.model.getAttribute( 'cx' ) || {};\n\n\t\t\tif ( cxData.adapted === false ) {\n\t\t\t\t// Reference is not adapted. Use an empty div as content with the same\n\t\t\t\t// CSS class that parent class uses for such empty content.\n\t\t\t\treturn $( '<div>' )\n\t\t\t\t\t.addClass( 've-ui-mwReferenceContextItem-muted' );\n\t\t\t}\n\t\t\treturn contextItem.super.prototype.getRendering.apply( this, arguments );\n\t\t};\n\n\t\tve.ui.contextItemFactory.register( contextItem );\n\t}\n\n\tve.ui.mwCitationTools.forEach( function ( tool ) {\n\t\tvar contextName = 'cite-' + tool.name;\n\t\tvar parentContextItem = ve.ui.contextItemFactory.lookup( contextName );\n\t\taddCxSubclass( parentContextItem );\n\t} );\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXReferenceDialog.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\n/**\n * Creates an ve.ui.CXReferenceDialog object.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n *\n * @class\n * @constructor\n * @extends ve.ui.MWReferenceDialog\n */\nve.ui.CXReferenceDialog = function VeUiCXReferenceDialog() {\n\t// Parent constructor\n\tve.ui.CXReferenceDialog.parent.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXReferenceDialog, ve.ui.MWReferenceDialog );\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ui.CXReferenceDialog.prototype.getActionProcess = function ( action ) {\n\tif ( action === 'done' ) {\n\t\tif ( this.selectedNode instanceof ve.dm.MWReferenceNode ) {\n\t\t\tvar cxData = this.selectedNode.getAttribute( 'cx' ) || {};\n\n\t\t\tif ( cxData.adapted === false ) {\n\t\t\t\tvar targetDoc = this.selectedNode.getDocument();\n\t\t\t\tvar attributeTx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(\n\t\t\t\t\ttargetDoc,\n\t\t\t\t\tthis.selectedNode.getOuterRange().start,\n\t\t\t\t\t{ cx: {} }\n\t\t\t\t);\n\t\t\t\ttargetDoc.commit( attributeTx );\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ve.ui.CXReferenceDialog.super.prototype.getActionProcess.call( this, action );\n};\n\n/* Registration */\n\nve.ui.windowFactory.register( ve.ui.CXReferenceDialog );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXSurface.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Content Translation VE Surface\n *\n * @class\n * @extends ve.ui.Surface\n *\n * @constructor\n * @param {ve.init.Target} target Target the surface belongs to\n * @param {HTMLDocument|Array|ve.dm.ElementLinearData|ve.dm.Document} dataOrDoc Document data to edit\n * @param {mw.cx.ui.ToolsColumn} toolsColumn Tools column\n * @param {Object} [config] Configuration options\n */\nve.ui.CXSurface = function VeUiCXSurface( target, dataOrDoc, toolsColumn, config ) {\n\t// Parent constructor\n\tve.ui.CXSurface.super.call( this, target, dataOrDoc, config );\n\n\tthis.toolsContainer = toolsColumn.toolContainer;\n\t// Move context to toolsContainer\n\tthis.toolsContainer.$element.append( this.context.$element );\n\tthis.getView().connect( this, {\n\t\tfocus: 'onFocus',\n\t\tblur: 'onBlur'\n\t} );\n\n\tif ( config.inDialog ) {\n\t\tthis.toggleGlobalOverlayClass( true );\n\t\tthis.connect( this, { destroy: [ 'toggleGlobalOverlayClass', false ] } );\n\t}\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXSurface, ve.ui.Surface );\n\n/* Events */\n\n/**\n * @event documentTransactCX\n */\n\n/* Methods */\n\n/**\n * Create a context.\n *\n * @method\n * @return {ve.ui.DesktopContext} Context\n */\nve.ui.CXSurface.prototype.createContext = function () {\n\treturn new ve.ui.CXDesktopContext( this );\n};\n\nve.ui.CXSurface.prototype.onBlur = function () {\n\tthis.toolsContainer.$element.removeClass( 'cx-column-tools-container--contextual' );\n};\n\nve.ui.CXSurface.prototype.onFocus = function () {\n\tthis.toolsContainer.$element.addClass( 'cx-column-tools-container--contextual' );\n};\n\n/**\n * Overriden to create documentTransactCX for tools (ResetSectionTool) to be able to\n * react to changes on the document state.\n *\n * @inheritDoc\n * @fires documentTransactCX\n */\nve.ui.CXSurface.prototype.onDocumentTransact = function () {\n\tve.ui.CXSurface.super.prototype.onDocumentTransact.apply( this, arguments );\n\n\tthis.emit( 'documentTransactCX' );\n};\n\nve.ui.CXSurface.prototype.toggleGlobalOverlayClass = function ( state ) {\n\tthis.getGlobalOverlay().$element.toggleClass( 've-cx-ui-overlay-global', state );\n};\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXTextSelectionContextItem.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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.LinkContextItem );\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.clearable = 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 sourceLanguage = this.translation.sourceDoc.getLang(),\n\t\ttargetLanguage = this.translation.targetDoc.getLang();\n\n\tvar targetLinkInfo = {\n\t\ttitle: this.normalizedTitle,\n\t\tpagelanguage: targetLanguage,\n\t\tdescription: targetTitleData.description,\n\t\tthumbnail: { source: targetTitleData.imageUrl }\n\t};\n\n\tvar $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\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\tvar sourceLinkInfo = {\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\tvar $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.getFragment().getAnnotations( true ).filter( function ( ann ) {\n\t\treturn ann instanceof ve.dm.LinkAnnotation;\n\t} ).isEmpty();\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// Force selection inside the link, as in ve.ui.AnnotationInspector#getTeardownProcess\n\t\tthis.context.getSurface().getView().selectAnnotation( function ( annView ) {\n\t\t\treturn annView.getModel() instanceof ve.dm.LinkAnnotation;\n\t\t} );\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":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Context item for transclusion nodes.\n *\n * @class\n * @extends ve.ui.MWTransclusionContextItem\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.CXTransclusionContextItem = function VeUiCXTransclusionContextItem() {\n\t// Parent constructor\n\tve.ui.CXTransclusionContextItem.super.apply( this, arguments );\n};\n\n/* Inheritance */\n\nOO.inheritClass( ve.ui.CXTransclusionContextItem, ve.ui.MWTransclusionContextItem );\n\n/* Methods */\n\n/**\n * @inheritdoc\n */\nve.ui.CXTransclusionContextItem.prototype.setup = function () {\n\tif ( this.model instanceof ve.dm.CXTransclusionBlockNode &&\n\t\tthis.model.missingInTargetLanguage()\n\t) {\n\t\tthis.$element.remove();\n\t\treturn;\n\t}\n\n\tve.ui.CXTransclusionContextItem.super.prototype.setup.apply( this, arguments );\n};\n\n/* Registration */\n\nve.ui.contextItemFactory.register( ve.ui.CXTransclusionContextItem );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXTranslationToolbar.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"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\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\tvar $title = $( '<div>' )\n\t\t.addClass( 've-cx-toolbar-mt-title' )\n\t\t.text( mw.msg( 'cx-tools-mt-title' ) );\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\n\t// Hide initially, because there is no selection initially\n\tthis.$element.toggle( false );\n\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 ) {\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 surface = 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\tvar section = 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\tvar source = section.getOriginalContentSource() || defaultProvider;\n\t\t\tthis.setActive( this.getName() === source );\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 );\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};\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","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/ve-cx/ui/ve.ui.CXTranslationUnitContextItem.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * VisualEditor ContentTranslation translation unit ContextItem mixin.\n *\n * It is assumed that `this` is already a ve.ui.ContextItem whose model belongs to a translation\n * unit.\n *\n * @class\n * @abstract\n * @constructor\n * @param {ve.ui.Context} context Context item is in\n * @param {ve.dm.Model} model Model item is related to\n */\nve.ui.CXTranslationUnitContextItem = function VeUiCXTranslationUnitContextItem( context, model ) {\n\tthis.model = model;\n\tthis.context = context;\n\tthis.translation = ve.init.target.getTranslation();\n};\n\nOO.initClass( ve.ui.CXTranslationUnitContextItem );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/callout/ext.cx.callout.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[{"ruleId":"no-jquery/no-animate","severity":2,"message":"Prefer CSS transitions to .animate","line":86,"column":4,"nodeType":"CallExpression","endLine":92,"endColumn":12,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"mediawiki/class-doc","severity":2,"message":"All possible CSS classes should be documented. See https://w.wiki/PS2 for details.","line":123,"column":4,"nodeType":"CallExpression","endLine":123,"endColumn":44,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * A Callout dialog widget with jQuery.\n * Copyright (c) 2015 Santhosh Thottingal <santhosh.thottingal@gmail.com>\n * released under the MIT license.\n */\n( function () {\n\t'use strict';\n\n\t/**\n\t * Depending on type, return function call result or just the value.\n\t *\n\t * @param {Mixed} funcOrVal\n\t * @param {Object} context\n\t * @return {Mixed} Function call result or value.\n\t */\n\tfunction maybeCall( funcOrVal, context ) {\n\t\treturn typeof funcOrVal === 'function' ? funcOrVal.call( context ) : funcOrVal;\n\t}\n\n\t/**\n\t * Callout class\n\t *\n\t * @param {HTMLElement} element The trigger element to which callout attaches.\n\t * @param {Object} options Options object\n\t * @param {string|Function} options.direction Direction of callout.\n\t *     Imagine the callout is along with the direction of a clock handle with the tip at top of\n\t *     the handle.\n\t *         11  12  1\n\t *         ________\n\t *     10 |        | 2\n\t *     9  |    .   | 3\n\t *     8  |________| 4\n\t *         7  6   5\n\t *     Possible directions are 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12.\n\t *     Direction 12 is same as position 0.\n\t *     It is also possible to give a preferred direction using $.fn.callout.autoDirection\n\t *     Example: $.fn.callout.autoDirection( '0' )\n\t *     This will make the callout below the trigger with its tip pointing upwards\n\t *            +\n\t *     ______/\\______\n\t *     |             |\n\t *     _______________\n\t *     This will make the callout left of the trigger with its tip pointing 3'O clock direction\n\t *     Example: $.fn.callout.autoDirection( '0' )\n\t *      ______\n\t *      |     |\n\t *      |      > +\n\t *      |_____|\n\t *     Auto direction will change direction if screen space is not available.\n\t * @param {number} options.offset Offset of the dialog from the trigger\n\t * @param {number} options.opacity Opacity of callout\n\t * @param {string} options.trigger Trigger: available options: hover, click,\n\t *     and auto(show automatically without any trigger).\n\t *     Any other value will assume the client of this libary will take care of show/hide\n\t */\n\tfunction Callout( element, options ) {\n\t\tthis.$element = $( element );\n\t\tthis.options = $.extend( {}, $.fn.callout.defaults, options );\n\t\tthis.shown = false;\n\t\tthis.listen();\n\t}\n\n\t/**\n\t * Show the callout\n\t */\n\tCallout.prototype.show = function () {\n\t\tvar content = maybeCall( this.options.content );\n\t\tif ( !content ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar $dialog = this.dialog();\n\t\t$dialog.find( '.cx-callout-content' ).empty().append( content );\n\t\t$dialog.remove().css( {\n\t\t\ttop: 0,\n\t\t\tleft: 0,\n\t\t\tvisibility: 'hidden',\n\t\t\tdisplay: 'block'\n\t\t} ).appendTo( document.body );\n\n\t\tthis.position();\n\n\t\tif ( this.options.fade ) {\n\t\t\t// FIXME: Use CSS transition\n\t\t\t// eslint-disable-next-line no-jquery/no-animate\n\t\t\t$dialog.stop().css( {\n\t\t\t\topacity: 0,\n\t\t\t\tdisplay: 'block',\n\t\t\t\tvisibility: 'visible'\n\t\t\t} ).animate( {\n\t\t\t\topacity: this.options.opacity\n\t\t\t}, 100 );\n\t\t} else {\n\t\t\t$dialog.css( {\n\t\t\t\tvisibility: 'visible',\n\t\t\t\topacity: this.options.opacity\n\t\t\t} );\n\t\t}\n\t\tthis.shown = true;\n\t};\n\n\t/**\n\t * Position the callout\n\t */\n\tCallout.prototype.position = function () {\n\t\tvar pos = $.extend( {}, this.$element.offset(), {\n\t\t\twidth: this.$element[ 0 ].offsetWidth,\n\t\t\theight: this.$element[ 0 ].offsetHeight\n\t\t} );\n\n\t\tvar $dialog = this.dialog();\n\t\tvar direction = maybeCall( this.options.direction, this.$element[ 0 ] );\n\t\t// Attach css classes before checking height/width so they\n\t\t// can be applied.\n\t\t// The following classes are used here:\n\t\t// * cx-callout-0\n\t\t// * cx-callout-1\n\t\t// * ...\n\t\t// * cx-callout-12\n\t\t$dialog.removeClass().addClass( 'cx-callout cx-callout-' + direction );\n\t\tif ( this.options.classes ) {\n\t\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\t\t$dialog.addClass( this.options.classes );\n\t\t}\n\t\tvar actualWidth = $dialog[ 0 ].offsetWidth;\n\t\tvar actualHeight = $dialog[ 0 ].offsetHeight;\n\t\tvar position;\n\t\tswitch ( direction ) {\n\t\t\tcase '0':\n\t\t\tcase '12':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top + pos.height + this.options.offset,\n\t\t\t\t\tleft: pos.left + pos.width / 2 - actualWidth / 2\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase '1':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top + pos.height + this.options.offset,\n\t\t\t\t\tleft: pos.left + pos.width - actualWidth\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase '11':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top + pos.height + this.options.offset,\n\t\t\t\t\tleft: pos.left\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase '2':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top - pos.height / 2,\n\t\t\t\t\tleft: pos.left - actualWidth - this.options.offset\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase '3':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top + pos.height / 2 - actualHeight / 2,\n\t\t\t\t\tleft: pos.left - actualWidth - this.options.offset\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase '4':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top + pos.height - actualHeight + this.options.offset,\n\t\t\t\t\tleft: pos.left - actualWidth - this.options.offset\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase '5':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top - actualHeight - this.options.offset,\n\t\t\t\t\tleft: pos.left + pos.width - actualWidth\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase '6':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top - actualHeight - this.options.offset,\n\t\t\t\t\tleft: pos.left + pos.width / 2 - actualWidth / 2\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase '7':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top - actualHeight - this.options.offset,\n\t\t\t\t\tleft: pos.left\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase '8':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top + pos.height - actualHeight + this.options.offset,\n\t\t\t\t\tleft: pos.left + pos.width + this.options.offset\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase '9':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top + pos.height / 2 - actualHeight / 2,\n\t\t\t\t\tleft: pos.left + pos.width + this.options.offset\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase '10':\n\t\t\t\tposition = {\n\t\t\t\t\ttop: pos.top - pos.height / 2,\n\t\t\t\t\tleft: pos.left + pos.width + this.options.offset\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t}\n\n\t\t$dialog.css( position );\n\t};\n\n\t/**\n\t * Listen for event\n\t */\n\tCallout.prototype.listen = function () {\n\t\tvar self = this;\n\n\t\tif ( this.options.trigger === 'auto' ) {\n\t\t\tself.show();\n\t\t}\n\n\t\tif ( this.options.trigger === 'hover' ) {\n\t\t\tthis.$element.on( 'mouseenter', function () {\n\t\t\t\t// Hide all other cx-callouts\n\t\t\t\t$( '.cx-callout' ).hide();\n\t\t\t\tself.show();\n\t\t\t\t// On mouse enter of siblings, hide.\n\t\t\t\tself.$element.siblings().one( 'mouseenter', self.hide.bind( self ) );\n\t\t\t\tself.$dialog.one( 'mouseleave', self.hide.bind( self ) );\n\t\t\t\t$( document ).one( 'click', self.hide.bind( self ) );\n\t\t\t} );\n\t\t}\n\t\tif ( this.options.trigger === 'click' ) {\n\t\t\tthis.$element.on( 'click', function () {\n\t\t\t\tif ( self.shown ) {\n\t\t\t\t\tself.hide();\n\t\t\t\t} else {\n\t\t\t\t\tself.show();\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\n\t\tvar timer;\n\t\t$( window ).on( 'resize', function () {\n\t\t\tclearTimeout( timer );\n\t\t\ttimer = setTimeout( function () {\n\t\t\t\tself.position();\n\t\t\t}, 200 );\n\t\t} );\n\t};\n\n\t/**\n\t * Hide the callout\n\t */\n\tCallout.prototype.hide = function () {\n\t\tif ( this.options.fade ) {\n\t\t\tthis.dialog().stop().fadeOut( 100, function () {\n\t\t\t\t$( this ).remove();\n\t\t\t} );\n\t\t} else {\n\t\t\tthis.dialog().remove();\n\t\t}\n\t\tthis.shown = false;\n\t};\n\n\t/**\n\t * Construct and return the dialog\n\t *\n\t * @return {jQuery}\n\t */\n\tCallout.prototype.dialog = function () {\n\t\tif ( !this.$dialog ) {\n\t\t\tthis.$dialog = $( '<div>' ).addClass( 'cx-callout' )\n\t\t\t\t.append( $( '<div>' ).addClass( 'cx-callout-content' ) );\n\t\t}\n\t\treturn this.$dialog;\n\t};\n\n\t/**\n\t * The callout plugin\n\t *\n\t * @param {Object} options\n\t * @return {jQuery}\n\t */\n\t$.fn.callout = function ( options ) {\n\t\treturn this.each( function () {\n\t\t\tvar $this = $( this ),\n\t\t\t\tdata = $this.data( 'callout' );\n\n\t\t\tif ( !data ) {\n\t\t\t\tdata = new Callout( this, options );\n\t\t\t\t$this.data( 'callout', data );\n\t\t\t}\n\n\t\t\tif ( typeof options === 'string' ) {\n\t\t\t\tdata[ options ]();\n\t\t\t}\n\t\t} );\n\t};\n\n\t/**\n\t * Return a closure that can calculate a good direction based on the arguments.\n\t *\n\t * @param {string} prefer - the direction to prefer. if there are no viewable region\n\t *     edges effecting the callouts's direction.  e.g. '3', '6', '9'\n\t * @return {Function}\n\t */\n\t$.fn.callout.autoDirection = function ( prefer ) {\n\t\treturn function () {\n\t\t\tvar direction = prefer,\n\t\t\t\tboundTop = window.pageYOffset + document.documentElement.clientHeight / 2,\n\t\t\t\tboundLeft = window.pageXOffset + document.documentElement.clientWidth / 2,\n\t\t\t\t$this = $( this );\n\n\t\t\tvar leftFlips = {\n\t\t\t\t1: '11',\n\t\t\t\t2: '10',\n\t\t\t\t3: '9',\n\t\t\t\t4: '8',\n\t\t\t\t5: '7'\n\t\t\t};\n\t\t\tvar rightFlips = {\n\t\t\t\t11: '1',\n\t\t\t\t10: '2',\n\t\t\t\t9: '3',\n\t\t\t\t8: '4',\n\t\t\t\t7: '5'\n\t\t\t};\n\t\t\tvar topFlips = {\n\t\t\t\t3: '2',\n\t\t\t\t4: '2',\n\t\t\t\t5: '1',\n\t\t\t\t6: '0',\n\t\t\t\t7: '11',\n\t\t\t\t8: '10',\n\t\t\t\t9: '10'\n\t\t\t};\n\t\t\tvar bottomFlips = {\n\t\t\t\t0: '6',\n\t\t\t\t12: '6',\n\t\t\t\t1: '5',\n\t\t\t\t2: '4',\n\t\t\t\t3: '4',\n\t\t\t\t11: '7',\n\t\t\t\t10: '8',\n\t\t\t\t9: '8'\n\t\t\t};\n\n\t\t\tif ( $this.offset().top < boundTop ) {\n\t\t\t\tdirection = topFlips[ direction ] || direction;\n\t\t\t}\n\t\t\tif ( $this.offset().top > boundTop ) {\n\t\t\t\tdirection = bottomFlips[ direction ] || direction;\n\t\t\t}\n\t\t\tif ( $( this ).offset().left < boundLeft ) {\n\t\t\t\tdirection = leftFlips[ direction ] || direction;\n\t\t\t}\n\t\t\tif ( $( this ).offset().left > boundLeft ) {\n\t\t\t\tdirection = rightFlips[ direction ] || direction;\n\t\t\t}\n\t\t\treturn direction;\n\t\t};\n\t};\n\n\t$.fn.callout.defaults = {\n\t\tdirection: $.fn.callout.autoDirection( '0' ),\n\t\toffset: 10,\n\t\topacity: 1.0,\n\t\ttrigger: 'hover'\n\t};\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/feedback/ext.cx.feedback.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation feedback link\n * Adds an icon and a call to action to leave feedback.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\n\t/**\n\t * ContentTranslationFeedback\n\t *\n\t * @class\n\t *\n\t * @param {Element} element\n\t */\n\tfunction ContentTranslationFeedback( element ) {\n\t\tthis.$container = $( element );\n\n\t\tthis.render();\n\t}\n\n\tContentTranslationFeedback.prototype.render = function () {\n\t\tvar $feedbackLink = $( '<a>' )\n\t\t\t.addClass( 'cx-feedback__link' )\n\t\t\t.attr( {\n\t\t\t\thref: '//www.mediawiki.org/wiki/Talk:Content_translation',\n\t\t\t\ttarget: '_blank'\n\t\t\t} )\n\t\t\t.text( mw.msg( 'cx-feedback-link' ) );\n\n\t\tvar $feedbackContainer = $( '<div>' )\n\t\t\t.addClass( 'cx-feedback' )\n\t\t\t.append( $feedbackLink );\n\n\t\tthis.$container.append( $feedbackContainer );\n\t};\n\n\t$.fn.cxFeedback = function () {\n\t\treturn this.each( function () {\n\t\t\tvar $this = $( this ),\n\t\t\t\tdata = $this.data( 'cxFeedback' );\n\n\t\t\tif ( !data ) {\n\t\t\t\t$this.data( 'cxFeedback', ( data = new ContentTranslationFeedback( this ) ) );\n\t\t\t}\n\t\t} );\n\t};\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/progressbar/ext.cx.progressbar.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation Progress Indicator\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\t/**\n\t * ProgressBar\n\t *\n\t * @class\n\t *\n\t * @param {Element} element\n\t * @param {Object} options\n\t */\n\tfunction ProgressBar( element, options ) {\n\t\tthis.$container = $( element );\n\t\tthis.options = $.extend( true, {}, $.fn.cxProgressBar.defaults, options );\n\t\tthis.init();\n\t}\n\n\tProgressBar.prototype.init = function () {\n\t\tthis.render();\n\t\tthis.listen();\n\t\tif ( this.options.weights ) {\n\t\t\tthis.update( this.options.weights, this.options.version );\n\t\t}\n\t};\n\n\tProgressBar.prototype.render = function () {\n\t\tthis.$container.append( $( '<div>' )\n\t\t\t.addClass( 'cx-progressbar' )\n\t\t\t.append(\n\t\t\t\t$( '<span>' ).addClass( 'cx-progressbar__bar' ),\n\t\t\t\t$( '<span>' ).addClass( 'cx-progressbar__bar--mt' )\n\t\t\t)\n\t\t);\n\n\t\tthis.$bar = this.$container.find( '.cx-progressbar__bar' );\n\t\tthis.$mtbar = this.$container.find( '.cx-progressbar__bar--mt' );\n\t\tthis.update( {\n\t\t\tmaximum: 0,\n\t\t\tany: 0,\n\t\t\thuman: 0,\n\t\t\tmt: 0\n\t\t} );\n\t};\n\n\tProgressBar.prototype.listen = function () {\n\t\tmw.hook( 'mw.cx.progress' ).add( this.update.bind( this ) );\n\t};\n\n\tProgressBar.prototype.update = function ( weights, version ) {\n\t\tvar progress = weights.any * 100,\n\t\t\tmtProgress = weights.mt * 100,\n\t\t\tmtPercentage = weights.mt / weights.any * 100 || 0,\n\t\t\tmtText = version === 2 ? mtProgress : mtPercentage,\n\t\t\tmtBarPercentage = version === 2 ? ( progress * weights.mt ) : mtProgress;\n\n\t\tthis.$bar.css( 'width', progress + '%' );\n\t\tthis.$mtbar.css( 'width', mtBarPercentage + '%' );\n\n\t\tthis.$container.attr( 'title',\n\t\t\tmw.msg(\n\t\t\t\t'cx-header-progressbar-text',\n\t\t\t\tmw.language.convertNumber( Math.round( progress ) ) ) +\n\t\t\t'\\n' +\n\t\t\tmw.msg(\n\t\t\t\t'cx-header-progressbar-text-mt',\n\t\t\t\tmw.language.convertNumber( Math.round( mtText ) )\n\t\t\t) );\n\t};\n\n\t$.fn.cxProgressBar = function ( options ) {\n\t\treturn this.each( function () {\n\t\t\tvar $this = $( this ),\n\t\t\t\tdata = $this.data( 'cxProgressBar' );\n\n\t\t\tif ( !data ) {\n\t\t\t\t$this.data( 'cxProgressBar', ( data = new ProgressBar( this, options ) ) );\n\t\t\t}\n\n\t\t\tif ( typeof options === 'string' ) {\n\t\t\t\tdata[ options ].call( $this );\n\t\t\t}\n\t\t} );\n\t};\n\n\t$.fn.cxProgressBar.defaults = {\n\t\tversion: 1\n\t};\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/spinner/ext.cx.spinner.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\tmw.cx.widgets = mw.cx.widgets || {};\n\n\tmw.cx.widgets.spinner = function () {\n\t\treturn $( '<div>' )\n\t\t\t.addClass( 'cx-spinner' )\n\t\t\t.append(\n\t\t\t\t$( '<div>' ).addClass( 'cx-bounce' )\n\t\t\t);\n\t};\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/modules/widgets/translator/ext.cx.translator.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * ContentTranslation extension - Translator Widget.\n *\n * @copyright See AUTHORS.txt\n * @license GPL-2.0-or-later\n */\n( function () {\n\t'use strict';\n\tvar statsRequest;\n\n\tmw.cx.widgets = mw.cx.widgets || {};\n\n\t/**\n\t * CXTranslator constructor\n\t *\n\t * @param {string} translatorName Username of the translator for which statistics are loaded\n\t */\n\tmw.cx.widgets.CXTranslator = function ( translatorName ) {\n\t\tthis.translatorName = translatorName;\n\t\tthis.data = [];\n\t\tthis.max = -1;\n\n\t\tthis.$lastMonthButton = null;\n\t\tthis.$widget = null;\n\t\tthis.$canvas = null;\n\n\t\tthis.render();\n\t};\n\n\tmw.cx.widgets.CXTranslator.prototype.render = function () {\n\t\tvar $header, $monthStats, $total,\n\t\t\tapi = new mw.Api(),\n\t\t\tself = this;\n\n\t\t$header = $( '<div>' ).addClass( 'cx-translator__header' );\n\t\tthis.$lastMonthButton = $( '<div>' )\n\t\t\t.addClass( 'cx-translator__month-stats-button' )\n\t\t\t.append(\n\t\t\t\t$( '<div>' )\n\t\t\t\t\t.addClass( 'cx-translator__month-stats-count' ),\n\t\t\t\t$( '<div>' )\n\t\t\t\t\t.addClass( 'cx-translator__month-stats-label' )\n\t\t\t\t\t.text( mw.msg( 'cx-translator-month-stats-label' ) )\n\t\t\t);\n\t\t$monthStats = $( '<div>' )\n\t\t\t.addClass( 'cx-translator__month-stats' )\n\t\t\t.append( this.$lastMonthButton );\n\t\t$total = $( '<div>' ).addClass( 'cx-translator__total-translations' ).append(\n\t\t\t$( '<div>' )\n\t\t\t\t.addClass( 'cx-translator__total-translations-count' ),\n\t\t\t$( '<div>' )\n\t\t\t\t.addClass( 'cx-translator__total-translations-label' )\n\t\t\t\t.text( mw.msg( 'cx-translator-total-translations-label' ) )\n\t\t);\n\t\tthis.$canvas = $( '<canvas>' )\n\t\t\t.addClass( 'cx-translatorstats' )\n\t\t\t.prop( 'height', '50' );\n\t\tstatsRequest = statsRequest || api.get( {\n\t\t\taction: 'query',\n\t\t\tlist: 'cxtranslatorstats',\n\t\t\ttranslator: this.translatorName\n\t\t} );\n\t\tthis.$widget = $( '<div>' )\n\t\t\t.addClass( 'cx-translator' )\n\t\t\t.append( $header, $monthStats, $total, this.$canvas );\n\t\tstatsRequest.then( function ( stats ) {\n\t\t\tvar total, thisMonthStats,\n\t\t\t\tpublishTrend = stats.cxtranslatorstats.publishTrend,\n\t\t\t\t// Sorted months for ordered display on bar chart\n\t\t\t\tmonthKeys = Object.keys( publishTrend ).sort(),\n\t\t\t\tthisMonthKey = new Date().toISOString().slice( 0, 7 ) + '-01';\n\n\t\t\ttotal = publishTrend[ thisMonthKey ].count || 0;\n\t\t\tthisMonthStats = publishTrend[ thisMonthKey ].delta || 0;\n\n\t\t\t// Don't display statistics if there are no translations yet\n\t\t\tif ( total === 0 ) {\n\t\t\t\tself.$widget.remove();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$header.text( mw.msg( 'cx-translator-header' ) );\n\t\t\t$total.find( '.cx-translator__total-translations-count' )\n\t\t\t\t.text( mw.language.convertNumber( total ) );\n\t\t\t$monthStats.find( '.cx-translator__month-stats-count' )\n\t\t\t\t.text( mw.language.convertNumber( thisMonthStats ) );\n\n\t\t\tmonthKeys.forEach( function ( month ) {\n\t\t\t\tself.max = Math.max( self.max, publishTrend[ month ].delta );\n\t\t\t\tself.data.push( publishTrend[ month ].delta );\n\t\t\t} );\n\n\t\t\tself.$canvas.prop( 'width', self.$widget.width() );\n\t\t\tself.draw();\n\n\t\t\t// Make statistics visible in dashboard sidebar,\n\t\t\t// after all data is fetched and drawn on canvas\n\t\t\tself.$widget.addClass( 'cx-translator--visible' );\n\t\t} ).fail( function () {\n\t\t\tself.$widget.remove();\n\t\t} );\n\t};\n\n\tmw.cx.widgets.CXTranslator.prototype.draw = function () {\n\t\tvar i, numOfBars, dataLength,\n\t\t\tcontext = this.$canvas[ 0 ].getContext( '2d' ),\n\t\t\tbarWidth = 6,\n\t\t\theight = 50,\n\t\t\t// Spacing between bars in bar chart\n\t\t\tspacing = 4,\n\t\t\toffsetX = spacing,\n\t\t\t// One height unit relative to maximum number of contributions\n\t\t\tsegment = ( height - spacing ) / this.max,\n\t\t\tdata = this.data,\n\t\t\tcanvasWidth = this.$canvas.parent().width();\n\n\t\t// Limit the number of bars displayed\n\t\tnumOfBars = Math.floor( ( canvasWidth - spacing ) / ( barWidth + spacing ) );\n\t\tdata = this.data.slice( Math.max( this.data.length - numOfBars, 0 ) );\n\n\t\tdataLength = data.length;\n\t\tcontext.fillStyle = '#a2a9b1';\n\t\tfor ( i = 0; i < dataLength; i++ ) {\n\t\t\t// Last bar in chart is displayed using progressive color (Accent50) from WikimediaUI color palette\n\t\t\tif ( i === dataLength - 1 ) {\n\t\t\t\tcontext.fillStyle = '#36c';\n\t\t\t}\n\t\t\tcontext.fillRect( offsetX, height - data[ i ] * segment, barWidth, data[ i ] * segment );\n\t\t\toffsetX += barWidth + spacing;\n\t\t}\n\t};\n\n\tmw.cx.widgets.CXTranslator.prototype.resize = function () {\n\t\tthis.$canvas.prop( 'width', this.$canvas.parent().width() );\n\n\t\t// When resized, canvas needs to be redrawn\n\t\tthis.draw();\n\t};\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/package-lock.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/package.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/sql/abstractSchemaChanges/patch-cx_translators-unique-to-pk.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/sql/tables.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"no-process-exit","replacedBy":[]}]},{"filePath":"/src/repo/tests/qunit/.eslintrc.json","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"{\n\t\"root\": true,\n\t\"extends\": [\n\t\t\"../../modules/.eslintrc.json\",\n\t\t\"wikimedia/qunit\"\n\t]\n}\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/base/mw.cx.SiteMapper.test.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * @author Niklas Laxström\n * @license GPL-2.0-or-later\n */\n\n( function () {\n\t'use strict';\n\n\tQUnit.module( 'mw.cx.SiteMapper', QUnit.newMwEnvironment( {\n\t\tbeforeEach: function () {\n\t\t\tthis.siteMapper = new mw.cx.SiteMapper( {\n\t\t\t\tSiteTemplates: {\n\t\t\t\t\taction: '//$1.wikipedia.org/w/index.php?title=$2',\n\t\t\t\t\tview: 'https://$1.wikipedia.org/wiki/$2',\n\t\t\t\t\tapi: 'https://$1.wikipedia.org/w/api.php',\n\t\t\t\t\tcx: 'http://localhost:8080/page/$1/$2',\n\t\t\t\t\trestbase: '//$1.wikipedia.org/api/rest_v1'\n\t\t\t\t},\n\t\t\t\tDomainCodeMapping: {\n\t\t\t\t\tnb: 'no'\n\t\t\t\t}\n\t\t\t} );\n\t\t}\n\t} ) );\n\n\tQUnit.test( 'getLanguageCodeForWikiDomain', function ( assert ) {\n\t\tassert.strictEqual(\n\t\t\tthis.siteMapper.getLanguageCodeForWikiDomain( 'no' ),\n\t\t\t'nb',\n\t\t\t'no is mapped to nb'\n\t\t);\n\n\t\tassert.strictEqual(\n\t\t\tthis.siteMapper.getLanguageCodeForWikiDomain( 'fi' ),\n\t\t\t'fi',\n\t\t\t'fi stays fi'\n\t\t);\n\t} );\n\n\tQUnit.test( 'getPageUrl', function ( assert ) {\n\t\tassert.strictEqual(\n\t\t\tthis.siteMapper.getPageUrl( 'es', 'Title' ),\n\t\t\t'https://es.wikipedia.org/wiki/Title',\n\t\t\t'Simple title'\n\t\t);\n\n\t\tassert.strictEqual(\n\t\t\tthis.siteMapper.getPageUrl( 'fi', 'Longer title' ),\n\t\t\t'https://fi.wikipedia.org/wiki/Longer_title',\n\t\t\t'Title with space'\n\t\t);\n\n\t\tassert.strictEqual(\n\t\t\tthis.siteMapper.getPageUrl( 'ml', 'Random title', { q1: 'test' } ),\n\t\t\t'http://ml.wikipedia.org/w/index.php?title=Random_title&q1=test',\n\t\t\t'Protocol relative base URL with params'\n\t\t);\n\n\t\tassert.strictEqual(\n\t\t\tthis.siteMapper.getPageUrl( 'el', 'Random title', null, 'myhash' ),\n\t\t\t'https://el.wikipedia.org/wiki/Random_title#myhash',\n\t\t\t'URL with hash'\n\t\t);\n\t} );\n\n\tQUnit.test( 'getApi', function ( assert ) {\n\t\tvar api = this.siteMapper.getApi( 'he' );\n\t\tassert.strictEqual( api.apiUrl, 'https://he.wikipedia.org/w/api.php' );\n\t} );\n\n\tQUnit.test( 'getRestbaseUrl', function ( assert ) {\n\t\tvar url;\n\n\t\turl = this.siteMapper.getRestbaseUrl(\n\t\t\t'he',\n\t\t\t'/transform/wikitext/to/html/$title',\n\t\t\t{ $title: 'User:KartikMistry/Who?_(movie)_ä&ö' }\n\t\t);\n\t\tassert.strictEqual( url, '//he.wikipedia.org/api/rest_v1/transform/wikitext/to/html/User%3AKartikMistry%2FWho%3F_(movie)_%C3%A4%26%C3%B6' );\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/dm/mw.cx.dm.Translation.test.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * @author Santhosh Thottingal\n * @license GPL-2.0-or-later\n */\n\n( function () {\n\t'use strict';\n\n\tvar restoreTestData,\n\t\ttestDataPath = mw.config.get( 'wgExtensionAssetsPath' ) +\n\t\t'/ContentTranslation/tests/qunit/data/dm-translation-source-article.html';\n\n\tQUnit.module( 'mw.cx.dm.Translation', QUnit.newMwEnvironment() );\n\n\tQUnit.skip( 'Source and target dom build test', function ( assert ) {\n\t\tvar $fixture = $( '#qunit-fixture' ),\n\t\t\tdone = assert.async();\n\n\t\t$fixture.load( testDataPath, function () {\n\t\t\tvar sourceHTML = $fixture.find( '#source-page-content' ).html(),\n\t\t\t\tsourceDom, targetDom;\n\n\t\t\tsourceDom = mw.cx.dm.Translation.static.getSourceDom( sourceHTML, false );\n\t\t\tassert.strictEqual( $( sourceDom ).find( 'article' ).length, 1,\n\t\t\t\t'Source DOM is wrapped in article tag' );\n\t\t\tassert.strictEqual( $( sourceDom ).find( 'section' ).length, 2,\n\t\t\t\t'There are 3 sections in source' );\n\t\t\tassert.strictEqual( $( sourceDom ).find( '[rel=\"cx:Section\"]' ).length, 2,\n\t\t\t\t'Two sections are constructed in source dom' );\n\t\t\ttargetDom = mw.cx.dm.Translation.static.getSourceDom( sourceHTML, true );\n\t\t\tassert.strictEqual( $( targetDom ).find( '[rel=\"cx:Placeholder\"]' ).length, 2,\n\t\t\t\t'Two sections are constructed in target dom' );\n\t\t\tdone();\n\t\t} );\n\t} );\n\n\trestoreTestData = {\n\t\tsavedTranslationUnits: {\n\t\t\t12: {\n\t\t\t\tsource: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection12\"><p id=\"mwAz\">Content new</p></section>'\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tcontent: '<section id=\"cxTargetSection12\"><p id=\"mwAz\">Translated content</p></section>'\n\t\t\t\t}\n\t\t\t},\n\t\t\tmwAx: {\n\t\t\t\tsource: {\n\t\t\t\t\tcontent: '<p id=\"mwAx\">Content for mwAx</p>'\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tcontent: '<p id=\"mwAx\">Translated content</p>'\n\t\t\t\t}\n\t\t\t},\n\t\t\t16: {\n\t\t\t\tsource: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection16\"><p id=\"mwAp\">Content mwAp</p></section>'\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tcontent: '<section id=\"cxTargetSection16\"><p id=\"mwAp\">Translated content mwAp</p></section>'\n\t\t\t\t}\n\t\t\t},\n\t\t\t17: {\n\t\t\t\tsource: {\n\t\t\t\t\tcontent: '<p id=\"mwAr\">Content mwAr</p>'\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tcontent: '<p id=\"mwAr\">Translated content mwAr</p>'\n\t\t\t\t}\n\t\t\t},\n\t\t\t21: {\n\t\t\t\tsource: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection201\"><p id=\"mwBA\"><b>Phantosmia</b> (phantom smell), also called an olfactory hallucination, is smelling an odor that is not actually there</p></section>'\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection201\"><p id=\"mwBA\">Translated content mwAr</p></section>'\n\t\t\t\t}\n\t\t\t},\n\t\t\t22: {\n\t\t\t\tsource: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection201\"><p id=\"mwBB\"><b>A</b> B C D E F</p></section>'\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection201\"><p id=\"mwBB\"><b>A</b> B C D E F</p></section>'\n\t\t\t\t}\n\t\t\t},\n\t\t\t23: {\n\t\t\t\tsource: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection201\"><h2 id=\"mwBC\"><b>A</b> B C D E F</h2></section>'\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection201\"><h2 id=\"mwBC\"><b>A</b> B C D E F</h2></section>'\n\t\t\t\t}\n\t\t\t},\n\t\t\t24: {\n\t\t\t\tsource: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection20\"><p id=\"mwBD\"><b>A</b> B C G H I</p></section>'\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection20\"><p id=\"mwBD\"><b>A</b> B C G H I</p></section>'\n\t\t\t\t}\n\t\t\t},\n\t\t\t25: {\n\t\t\t\tsource: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection20\"><p id=\"mwBD\"><b>Sed</b> congue augue a eros tristique, nec interdum ipsum consequat. Maecenas id magna id nisi dapibus tristique</p></section>'\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection20\"><p id=\"mwBD\">Seds conguee auguee aa eross tristiquee, necc interduum ipsuum conssequat. Maeceenas id magna id niisi dapibus tristique.</p></section>'\n\t\t\t\t}\n\t\t\t},\n\t\t\t26: {\n\t\t\t\tsource: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection20\"><p id=\"mwBD\">Praesent auctor tincidunt risus, vitae sollicitudin tellus sodales id. Aenean a augue vitae neque lacinia euismod. Proin tincidunt dolor tincidunt, sagittis dui in, eleifend justo. <b>Sed</b> congue augue a eros tristique, nec interdum ipsum consequat!. Maecenas id \"magna\" id nisi dapibus Tristique.</p></section>'\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tcontent: '<section id=\"cxSourceSection20\"><p id=\"mwBD\">Seds conguee auguee aa eross tristiquee, necc interduum ipsuum conssequat. Maeceenas id magna id niisi dapibus tristique.</p></section>'\n\t\t\t\t}\n\t\t\t},\n\t\t\t27: {\n\t\t\t\tsource: {\n\t\t\t\t\tcontent: '<section rel=\"cx:Section\" id=\"cxSourceSection3\" data-mw-cx-source=\"undefined\"><p id=\"mwEA\"><span data-segmentid=\"14\" class=\"cx-segment\"><a href=\"./Barbados\" rel=\"mw:WikiLink\" data-linkid=\"15\" class=\"cx-link\" id=\"mwEQ\" title=\"Barbados\">Barbados</a> is a moderate <a href=\"./Political\" rel=\"mw:WikiLink\" data-linkid=\"16\" class=\"mw-redirect cx-link\" id=\"mwEg\" title=\"Political\">political</a> and <a href=\"./Economic\" rel=\"mw:WikiLink\" data-linkid=\"17\" class=\"mw-redirect cx-link\" id=\"mwEw\" title=\"Economic\">economic</a> power in the Caribbean region.</span></p>\\n\\n</section>'\n\t\t\t\t},\n\t\t\t\tuser: {\n\t\t\t\t\tcontent: '<section rel=\"cx:Section\" id=\"cxTargetSection3\" data-mw-cx-source=\"Apertium\"><p id=\"mwEA\"><span data-segmentid=\"14\" class=\"cx-segment\">Barbados es un poder político y económico moderado en la región de Caribes.</span></p></section>'\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\tsourceSections: {\n\t\t\t12: {\n\t\t\t\tsource: '<section id=\"cxSourceSection12\"><p id=\"mwAz\">Content new</p></section>',\n\t\t\t\tdescription: 'Ideal case: A CX2 saved translation with same source section content',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: 12\n\t\t\t},\n\t\t\t13: {\n\t\t\t\tsource: '<section id=\"cxSourceSection13\"><p id=\"mwAx\">Content new</p></section>',\n\t\t\t\tdescription: 'CX1 saved translation restoring against a CX2 translation.',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: 'mwAx'\n\t\t\t},\n\t\t\t18: {\n\t\t\t\tsource: '<section id=\"cxSourceSection18\"><p id=\"mwAp\">Content mwAp</p></section>',\n\t\t\t\tdescription: 'CX2 translation, section number mismatch, but content matched.',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: 16\n\t\t\t},\n\t\t\t19: {\n\t\t\t\tsource: '<section id=\"cxSourceSection19\"><p id=\"mwAr\">Content mwAp</p></section>',\n\t\t\t\tdescription: 'CX1 translation, section number mismatch, but parsoid id matched.',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: 17\n\t\t\t},\n\t\t\t20: {\n\t\t\t\tsource: '<section id=\"cxSourceSection20\"><p id=\"mwAq\">Content mwAq</p></section>',\n\t\t\t\tdescription: 'No translation found.',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: null\n\t\t\t},\n\t\t\t21: {\n\t\t\t\tsource: '<section id=\"cxSourceSection20\"><p id=\"mwBa\"><b>Phantosmia</b> (phantom smell), also called an olfactory hallucination or a phantom odor[1] is smelling an odor that is not actually there</p></section>',\n\t\t\t\tdescription: 'CX2 translation, Recover using content matching',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: 21\n\t\t\t},\n\t\t\t22: {\n\t\t\t\tsource: '<section id=\"cxSourceSection20\"><p id=\"mwBb\"><b>A</b> B C D E F</p></section>',\n\t\t\t\tdescription: 'CX2 translation, Recover using content matching',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: 22\n\t\t\t},\n\t\t\t23: {\n\t\t\t\tsource: '<section id=\"cxSourceSection20\"><h1 id=\"mwBc\"><b>A</b> B C D E F</h1></section>',\n\t\t\t\tdescription: 'CX2 translation, Recover using content matching failed due to tag mismatch',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: null\n\t\t\t},\n\t\t\t24: {\n\t\t\t\tsource: '<section id=\"cxSourceSection20\"><p id=\"mwBd\"><b>A</b> B C D E F</p></section>',\n\t\t\t\tdescription: 'CX2 translation, Recover using content matching failed due to insufficient common token',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: null\n\t\t\t},\n\t\t\t25: {\n\t\t\t\tsource: '<section id=\"cxSourceSection24\"><p id=\"24\">quick fox jumps over the lazy brown dog</section>',\n\t\t\t\tdescription: 'CX2 translation, Try to recover using content matching, but never attempt for a parsoid id match',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: null\n\t\t\t},\n\t\t\t26: {\n\t\t\t\tsource: '<section id=\"cxSourceSection20\"><p id=\"mwBD\"><b>Sed</b> congue augue a eros tristique, nec interdum ipsum consequat. Maecenas id magna id nisi dapibus tristique. Praesent auctor tincidunt risus, vitae sollicitudin tellus sodales id. Aenean a augue vitae neque lacinia euismod. Proin tincidunt dolor tincidunt, sagittis dui in, eleifend justo.</p></section>',\n\t\t\t\tdescription: 'CX2 translation, Test whether big content has small content',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: 25\n\t\t\t},\n\t\t\t27: {\n\t\t\t\tsource: '<section id=\"cxSourceSection20\"><p id=\"mwBD\"><b>Sed</b> congue augue a eros tristique, nec interdum ipsum consequat. Maecenas id magna id nisi dapibus tristique</p></section>',\n\t\t\t\tdescription: 'CX2 translation, Test whether big content has small content, ignore punctuations, case changes',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: 26\n\t\t\t},\n\t\t\t28: {\n\t\t\t\tsource: '<section id=\"cxSourceSection0\" rel=\"cx:Section\"><p id=\"mwAg\"></p>\\n\\n</section>',\n\t\t\t\tdescription: 'CX2 translation, test whether big content has small content when small content consists of newline characters',\n\t\t\t\tsourceLanguage: 'en',\n\t\t\t\texpectedSavedUnit: null\n\t\t\t}\n\t\t}\n\t};\n\n\tQUnit.test( 'Saved translation restore test', function ( assert ) {\n\t\tvar sectionNumber, sourceSectionDom, sourceSection, savedUnit;\n\n\t\tfor ( sectionNumber in restoreTestData.sourceSections ) {\n\t\t\tsourceSection = restoreTestData.sourceSections[ sectionNumber ];\n\t\t\tsourceSectionDom = $( sourceSection.source )[ 0 ];\n\n\t\t\tsavedUnit = mw.cx.dm.Translation.static.getSavedSection(\n\t\t\t\trestoreTestData.savedTranslationUnits,\n\t\t\t\tsourceSectionDom,\n\t\t\t\tsourceSection.sourceLanguage\n\t\t\t);\n\t\t\tassert.strictEqual(\n\t\t\t\tsavedUnit,\n\t\t\t\trestoreTestData.savedTranslationUnits[ sourceSection.expectedSavedUnit ],\n\t\t\t\tsourceSection.description\n\t\t\t);\n\t\t}\n\t} );\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/mw.cx.MachineTranslationService.test.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"'use strict';\n\nQUnit.module( 'mw.cx.MachineTranslationService', QUnit.newMwEnvironment( {\n\tbeforeEach: function () {\n\t\tthis.server = this.sandbox.useFakeServer();\n\t\tthis.server.respondImmediately = true;\n\t\tthis.siteMapper = new mw.cx.SiteMapper();\n\t}\n} ) );\n\nQUnit.test( 'fetchProviders [Success with results]', function ( assert ) {\n\tthis.server.respondWith( /list\\/mt\\/es\\/ca/, [\n\t\t200,\n\t\t{ 'Content-Type': 'application/json' },\n\t\t'{ \"mt\": [ \"Provider1\", \"Provider2\" ] }'\n\t] );\n\n\treturn new mw.cx.MachineTranslationService( 'es', 'ca', this.siteMapper )\n\t\t.fetchProviders()\n\t\t.then( function ( providers ) {\n\t\t\tassert.deepEqual( providers, [ 'Provider1', 'Provider2' ], 'Correct providers are returned' );\n\t\t} );\n} );\n\nQUnit.test( 'fetchProviders [Success without results]', function ( assert ) {\n\tthis.server.respondWith( /list\\/mt\\/se\\/ja/, [\n\t\t200,\n\t\t{ 'Content-Type': 'application/json' },\n\t\t'{}'\n\t] );\n\n\treturn new mw.cx.MachineTranslationService( 'se', 'ja', this.siteMapper )\n\t\t.fetchProviders()\n\t\t.then( function ( providers ) {\n\t\t\tassert.deepEqual( providers, [], 'Case of no providers is handled.' );\n\t\t} );\n} );\n\nQUnit.test( 'fetchProviders [Service is down]', function ( assert ) {\n\tthis.server.respondWith( /list\\/fi\\/sv\\/ca/, [\n\t\t500,\n\t\t{ 'Content-Type': 'text/html' },\n\t\t'Temporary failure'\n\t] );\n\n\tassert.rejects(\n\t\tnew mw.cx.MachineTranslationService( 'fi', 'sv', this.siteMapper )\n\t\t\t.fetchProviders(),\n\t\t'Failure causes promise to be rejected'\n\t);\n} );\n\nQUnit.test( 'getSuggestedDefaultProvider [Success with results]', function ( assert ) {\n\tthis.server.respondWith( /list\\/mt\\/source\\/target/, [\n\t\t200,\n\t\t{ 'Content-Type': 'application/json' },\n\t\t'{ \"mt\": [ \"Provider1\", \"Provider2\" ] }'\n\t] );\n\n\treturn new mw.cx.MachineTranslationService( 'source', 'target', this.siteMapper )\n\t\t.getSuggestedDefaultProvider()\n\t\t.then( function ( provider ) {\n\t\t\tassert.strictEqual( provider, 'Provider1', 'The first provider is suggested.' );\n\t\t} );\n} );\n\nQUnit.test( 'getSuggestedDefaultProvider [Success without results]', function ( assert ) {\n\tthis.server.respondWith( /list\\/mt\\/source\\/target/, [\n\t\t200,\n\t\t{ 'Content-Type': 'application/json' },\n\t\t'{}'\n\t] );\n\n\treturn new mw.cx.MachineTranslationService( 'source', 'target', this.siteMapper )\n\t\t.getSuggestedDefaultProvider()\n\t\t.then( function ( provider ) {\n\t\t\tassert.strictEqual( provider, null, 'If no providers, no suggested provider.' );\n\t\t} );\n} );\n\nQUnit.test( 'getSuggestedDefaultProvider [Success without source-mt in result]', function ( assert ) {\n\tthis.server.respondWith( /list\\/mt\\/source\\/target/, [\n\t\t200,\n\t\t{ 'Content-Type': 'application/json' },\n\t\t'{ \"mt\": [ \"source-mt\", \"Provider1\", \"Provider2\" ] }'\n\t] );\n\n\treturn new mw.cx.MachineTranslationService( 'source', 'target', this.siteMapper )\n\t\t.getSuggestedDefaultProvider()\n\t\t.then( function ( provider ) {\n\t\t\tassert.strictEqual( provider, null, 'Source mt suggested by the server respected' );\n\t\t} );\n} );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/mw.cx.TargetArticle.test.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * @author Santhosh Thottingal\n * @license GPL-2.0-or-later\n */\n\n( function () {\n\t'use strict';\n\n\tQUnit.module( 'mw.cx.TargetArticle', QUnit.newMwEnvironment() );\n\n\tQUnit.test( 'cleanupContent', function ( assert ) {\n\t\tvar i, inputDoc, tests = [ {\n\t\t\tinputHtml: '<section re=\"cx:Section\"><p id=\"mwaA\"><span class=\"cx-segment\" data-segmentid=\"10\" id=\"34\">sentence content</span></p><section>',\n\t\t\toutput: '<p id=\"mwaA\">sentence content</p>',\n\t\t\tdesc: 'Sentence segment markup removed'\n\t\t},\n\t\t{\n\t\t\tinputHtml: '<section re=\"cx:Section\"><table id=\"mwaA\"><th id=\"9\"><td id=\"10\">T1<td></th><tr id=\"10\"><td id=\"10\">T1<td></tr></table><section>',\n\t\t\toutput: '<table><tbody><tr><th></th><td>T1</td><td></td></tr><tr><td>T1</td><td></td></tr></tbody></table>',\n\t\t\tdesc: 'Ids from table markup removed'\n\t\t},\n\t\t{\n\t\t\tinputHtml: '<section rel=\"cx:Section\" id=\"cxTargetSection112\" data-mw-cx-source=\"Google\"><span typeof=\"mw:Transclusion\" data-mw=\"{}\" data-cx=\"[{&quot;adapted&quot;:false}]\" id=\"mwCH0\"></span></section>',\n\t\t\toutput: '',\n\t\t\tdesc: 'Pathological template removed'\n\t\t} ];\n\t\tfor ( i = 0; i < tests.length; i++ ) {\n\t\t\tinputDoc = ve.createDocumentFromHtml( tests[ i ].inputHtml );\n\t\t\tassert.strictEqual(\n\t\t\t\tmw.cx.TargetArticle.static.getCleanedupContent( inputDoc ).getElementsByTagName( 'body' )[ 0 ].innerHTML,\n\t\t\t\ttests[ i ].output,\n\t\t\t\ttests[ i ].desc\n\t\t\t);\n\t\t}\n\t} );\n\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/mw.cx.TranslationTracker.test.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * @author Santhosh Thottingal\n * @license GPL-2.0-or-later\n */\n\n( function () {\n\t'use strict';\n\n\tQUnit.module( 'mw.cx.TranslationTracker', QUnit.newMwEnvironment() );\n\n\tQUnit.test( 'calculateUnmodifiedContent', function ( assert ) {\n\t\tvar i, tests = [\n\t\t\t{\n\t\t\t\tstring1: 'a b c d',\n\t\t\t\tstring2: 'a b c d',\n\t\t\t\tlanguage: 'en',\n\t\t\t\tresult: 1,\n\t\t\t\tdesc: 'No modification'\n\t\t\t},\n\t\t\t{\n\t\t\t\tstring1: 'a b c d',\n\t\t\t\tstring2: 'a b c d e',\n\t\t\t\tlanguage: 'en',\n\t\t\t\tresult: 0.8,\n\t\t\t\tdesc: 'A token was added'\n\t\t\t},\n\t\t\t{\n\t\t\t\tstring1: 'a b c d',\n\t\t\t\tstring2: 'a b c',\n\t\t\t\tlanguage: 'en',\n\t\t\t\tresult: 0.75,\n\t\t\t\tdesc: 'A token was deleted'\n\t\t\t},\n\t\t\t{\n\t\t\t\tstring1: 'a b c d e',\n\t\t\t\tstring2: 'A B C D E',\n\t\t\t\tlanguage: 'en',\n\t\t\t\tresult: 0,\n\t\t\t\tdesc: 'All tokens modified'\n\t\t\t},\n\t\t\t{\n\t\t\t\tstring1: '典范条目',\n\t\t\t\tstring2: '典闻动态',\n\t\t\t\tlanguage: 'zh',\n\t\t\t\tresult: 0.25,\n\t\t\t\tdesc: 'A character modified for Chinese'\n\t\t\t},\n\t\t\t{\n\t\t\t\tstring1: 'a b c d e.',\n\t\t\t\tstring2: 'a B c d e. f g h',\n\t\t\t\tlanguage: 'en',\n\t\t\t\tresult: 0.5,\n\t\t\t\tdesc: '3 token were added, one modified'\n\t\t\t},\n\t\t\t{\n\t\t\t\tstring1: 'foo',\n\t\t\t\tstring2: '   ',\n\t\t\t\tlanguage: 'en',\n\t\t\t\tresult: 0,\n\t\t\t\tdesc: 'whitespace does not count as a token'\n\t\t\t},\n\t\t\t{\n\t\t\t\tstring1: '',\n\t\t\t\tstring2: '',\n\t\t\t\tlanguage: 'en',\n\t\t\t\tresult: 0,\n\t\t\t\tdesc: 'If both are blank, return 0'\n\t\t\t}\n\t\t];\n\t\tfor ( i = 0; i < tests.length; i++ ) {\n\t\t\tassert.strictEqual( mw.cx.TranslationTracker.static.calculateUnmodifiedContent(\n\t\t\t\ttests[ i ].string1, tests[ i ].string2, tests[ i ].language\n\t\t\t),\n\t\t\ttests[ i ].result,\n\t\t\ttests[ i ].desc\n\t\t\t);\n\t\t}\n\t} );\n\n\tQUnit.test( 'getSectionNodeValidationTokens', function ( assert ) {\n\t\tvar i, model, node, tokens,\n\t\t\t// The sample HTML below matches ve.dm.SectionNode instead of ve.dm.CXSectionNode,\n\t\t\t// because the latter is not loaded in unit tests. T303298#7816113\n\t\t\t// (If it was loaded, it would crash because it relies on ve.init.target being set.)\n\t\t\ttestSet = [\n\t\t\t\t{\n\t\t\t\t\thtml: '<section class=\"ve-ce-branchNode ve-ce-activeNode ve-ce-sectionNode ve-ce-cxLintableNode ve-ce-cxSectionNode ve-ce-activeNode-active noime\" spellcheck=\"false\" style=\"margin-top: 0px; height: 62px;\" contenteditable=\"true\"><p id=\"mwlg\" class=\"ve-ce-branchNode ve-ce-contentBranchNode ve-ce-paragraphNode\"><span class=\"cx-segment ve-ce-annotation ve-ce-cxSentenceSegmentAnnotation\" data-segmentid=\"85\"><span class=\"ve-ce-branchNode-slug ve-ce-branchNode-inlineSlug\"><img role=\"none\" alt=\"\" class=\"ve-ce-chimera ve-ce-chimera-gecko\" src=\"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=\"></span><span about=\"#mwt180\" class=\"mwe-math-element ve-ce-leafNode ve-ce-mwLatexNode ve-ce-focusableNode\" data-mw=\"{&quot;name&quot;:&quot;math&quot;,&quot;attrs&quot;:{},&quot;body&quot;:{&quot;extsrc&quot;:&quot;\\\\\\\\sum_i \\\\\\\\gamma_i c_{V,i} = \\\\\\\\alpha V K_T&quot;}}\" id=\"mwlw\" typeof=\"mw:Extension/math\" contenteditable=\"false\"><span class=\"mwe-math-mathml-inline mwe-math-mathml-a11y\" style=\"display: none;\"><math alttext=\"{\\\\displaystyle \\\\sum _{i}\\\\gamma _{i}c_{V,i}=\\\\alpha VK_{T}}\" xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow class=\"MJX-TeXAtom-ORD\"><mstyle displaystyle=\"true\" scriptlevel=\"0\"><munder><mo>∑</mo><mrow class=\"MJX-TeXAtom-ORD\"><mi>i</mi></mrow></munder><msub><mi>γ</mi><mrow class=\"MJX-TeXAtom-ORD\"><mi>i</mi></mrow></msub><msub><mi>c</mi><mrow class=\"MJX-TeXAtom-ORD\"><mi>V</mi><mo>,</mo><mi>i</mi></mrow></msub><mo>=</mo><mi>α</mi><mi>V</mi><msub><mi>K</mi><mrow class=\"MJX-TeXAtom-ORD\"><mi>T</mi></mrow></msub></mstyle></mrow><annotation encoding=\"application/x-tex\">{\\\\displaystyle \\\\sum _{i}\\\\gamma _{i}c_{V,i}=\\\\alpha VK_{T}}</annotation></semantics></math></span><img alt=\"{\\\\displaystyle \\\\sum _{i}\\\\gamma _{i}c_{V,i}=\\\\alpha VK_{T}}\" aria-hidden=\"true\" class=\"mwe-math-fallback-image-inline\" src=\"https://wikimedia.org/api/rest_v1/media/math/render/svg/e27f4bb563eb88ea6a36675ef119c347738fc3ea\" style=\"vertical-align: -3.005ex; width:19.009ex; height:5.509ex;\"></span>.</span></p></section>',\n\t\t\t\t\tresultTokens: 1,\n\t\t\t\t\tfailureMessage: 'Tokens for section with math expression not calculated correctly'\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\thtml: '<section class=\"ve-ce-branchNode ve-ce-activeNode ve-ce-sectionNode ve-ce-cxLintableNode ve-ce-cxSectionNode noime ve-ce-activeNode-active\" spellcheck=\"false\" style=\"margin-top: 0px; height: 93px;\" contenteditable=\"true\"><p id=\"mwIw\" class=\"ve-ce-branchNode ve-ce-contentBranchNode ve-ce-paragraphNode\"><span class=\"cx-segment ve-ce-annotation ve-ce-cxSentenceSegmentAnnotation\" data-segmentid=\"22\">The expression for the Grüneisen constant of a perfect crystal with pair interactions in<span about=\"#mwt36\" class=\"mwe-math-element ve-ce-leafNode ve-ce-mwLatexNode ve-ce-focusableNode\" data-mw=\"{&quot;name&quot;:&quot;math&quot;,&quot;attrs&quot;:{},&quot;body&quot;:{&quot;extsrc&quot;:&quot;d&quot;}}\" id=\"mwJA\" typeof=\"mw:Extension/math\" contenteditable=\"false\"><span class=\"mwe-math-mathml-inline mwe-math-mathml-a11y\" style=\"display: none;\"><math alttext=\"{\\\\displaystyle d}\" xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow class=\"MJX-TeXAtom-ORD\"><mstyle displaystyle=\"true\" scriptlevel=\"0\"><mi>d</mi></mstyle></mrow><annotation encoding=\"application/x-tex\">{\\\\displaystyle d}</annotation></semantics></math></span><img alt=\"d\" aria-hidden=\"true\" class=\"mwe-math-fallback-image-inline\" src=\"https://wikimedia.org/api/rest_v1/media/math/render/svg/e85ff03cbe0c7341af6b982e47e9f90d235c66ab\" style=\"vertical-align: -0.338ex; width:1.216ex; height:2.176ex;\"></span>-dimensional space has the form:<span about=\"#mwt42\" class=\"mw-ref ve-ce-leafNode ve-ce-focusableNode ve-ce-mwReferenceNode\" id=\"cite_ref-Krivtsov_Kuzkin_2-0\" rel=\"dc:references\" typeof=\"mw:Extension/ref\" contenteditable=\"false\"><a href=\"#\" style=\"counter-reset: mw-Ref 2;\"><span class=\"mw-reflink-text\">[2]</span></a></span></span><span class=\"ve-ce-branchNode-slug ve-ce-branchNode-inlineSlug\"><img role=\"none\" alt=\"\" class=\"ve-ce-chimera ve-ce-chimera-gecko\" src=\"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=\"></span></p></section>',\n\t\t\t\t\tresultTokens: 20,\n\t\t\t\t\tfailureMessage: 'Tokens for section with reference not calculated correctly'\n\t\t\t\t}\n\t\t\t];\n\n\t\tfor ( i = 0; i < testSet.length; i++ ) {\n\t\t\tmodel = ve.dm.converter.getModelFromDom( ve.createDocumentFromHtml( testSet[ i ].html ) );\n\t\t\tnode = model.getDocumentNode().getChildren()[ 0 ];\n\t\t\ttokens = mw.cx.TranslationTracker.static.getSectionNodeValidationTokens( node, 'en' );\n\t\t\tassert.strictEqual( tokens.length, testSet[ i ].resultTokens, testSet[ i ].failureMessage );\n\t\t}\n\n\t} );\n}() );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/mw.cx.util.test.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"QUnit.module( 'mw.cx.util', QUnit.newMwEnvironment( {\n\tbeforeEach: function () {\n\t\tmw.config.set( 'wgUserName', 'TestUser' );\n\t}\n} ) );\n\nQUnit.test.each( '.getTitleForNamespace()', {\n\t'Main namespace to main namespace': {\n\t\ttitle: 'Oxygen',\n\t\tnamespace: 0,\n\t\ttargetTitle: 'Oxygen'\n\t},\n\t'User namespace.': {\n\t\ttitle: 'Oxygen',\n\t\tnamespace: 2,\n\t\ttargetTitle: 'User:TestUser/Oxygen'\n\t},\n\t'User namespace to main': {\n\t\ttitle: 'User:TestUser/Oxygen',\n\t\tnamespace: 0,\n\t\ttargetTitle: 'Oxygen'\n\t},\n\t'User namespace to Talk namespace': {\n\t\ttitle: 'User:TestUser/Oxygen',\n\t\tnamespace: 1,\n\t\ttargetTitle: 'Talk:Oxygen'\n\t},\n\t'User namespace to User namespace': {\n\t\ttitle: 'User:TestUser/Oxygen',\n\t\tnamespace: 2,\n\t\ttargetTitle: 'User:TestUser/Oxygen'\n\t},\n\t'Talk namespace to User namespace': {\n\t\ttitle: 'Talk:Oxygen',\n\t\tnamespace: 2,\n\t\ttargetTitle: 'User:TestUser/Oxygen'\n\t},\n\t'Talk namespace to Talk namespace': {\n\t\ttitle: 'Talk:Oxygen',\n\t\tnamespace: 1,\n\t\ttargetTitle: 'Talk:Oxygen'\n\t}\n}, function ( assert, data ) {\n\tassert.deepEqual(\n\t\tmw.cx.getTitleForNamespace( data.title, data.namespace ),\n\t\tdata.targetTitle\n\t);\n} );\n\nQUnit.test( '.getTitleForNamespace() [invalid]', function ( assert ) {\n\tassert.throws(\n\t\tmw.cx.getTitleForNamespace.bind( this, '::', 0 ),\n\t\t'Error thrown for invalid title'\n\t);\n} );\n","usedDeprecatedRules":[]},{"filePath":"/src/repo/tests/qunit/ui/mw.cx.ui.Infobar.test.js","messages":[{"ruleId":"es/no-promise","message":"Definition for rule 'es/no-promise' was not found.","line":1,"column":1,"endLine":1,"endColumn":2,"severity":2,"nodeType":null}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/*!\n * QUnit tests for Content Translation.\n *\n * @licence GPL-2.0-or-later\n */\n\n( function () {\n\t'use strict';\n\n\tQUnit.module( 'mw.cx.ui.Infobar', QUnit.newMwEnvironment() );\n\n\tQUnit.test( 'Show message with a string', function ( assert ) {\n\t\tvar infobar, $fixture = $( '#qunit-fixture' );\n\n\t\tinfobar = new mw.cx.ui.Infobar();\n\t\t$fixture.append( infobar.$element );\n\n\t\tinfobar.showMessage( 'test-class', 'Test <b>message</b>' );\n\t\t// infobar.$element also contains hidden label for close button\n\t\tassert.true( infobar.$element.text().indexOf( 'Test <b>message</b>' ) !== -1, 'Strings as escaped' );\n\n\t\tinfobar.showMessage( 'changed-class', 'New message' );\n\t\tassert.true( infobar.$element.text().indexOf( 'New message' ) !== -1, 'Message is updated' );\n\t} );\n\n\tQUnit.test( 'Show message with a Message object', function ( assert ) {\n\t\tvar infobar, $fixture = $( '#qunit-fixture' );\n\n\t\tinfobar = new mw.cx.ui.Infobar();\n\t\t$fixture.append( infobar.$element );\n\n\t\tmw.messages.set( 'cx-header-test', '[http://example.com $1] is <b>here</b>' );\n\n\t\tinfobar.showMessage( 'test-class', mw.message( 'cx-header-test', 'Kissa' ) );\n\t\t// Taking a shortcut by testing the text content where html is dropped\n\t\tassert.true( infobar.$element.text().indexOf( 'Kissa is here' ) !== -1, 'Html is accepted' );\n\t} );\n\n}() );\n","usedDeprecatedRules":[]}]

--- end ---
$ ./node_modules/.bin/stylelint modules/widgets/feedback/styles/ext.cx.feedback.less modules/ui/styles/mw.cx.highlight.less modules/ve-cx/ce/mixins/ve.ce.CXPendingNode.less app/src/lib/mediawiki.ui/components/MWLayout/animations.less modules/entrypoints/styles/ext.cx.entrypoints.mffrequentlanguages.less modules/ui/styles/widgets/mw.cx.ui.CategoryTagItemWidget.less modules/ui/styles/mw.cx.ui.TargetColumn.less modules/ui/styles/mw.cx.init.Translation.less modules/dashboard/styles/mw.cx.SuggestionList.less modules/ve-cx/ce/styles/ve.ce.CXLintableNode.less modules/ui/styles/grid/grid-core.less modules/ui/styles/mw.cx.ui.Header.less modules/tools/styles/mw.cx.tools.InstructionsTool.less modules/ui/styles/grid/grid-responsive.less modules/ui/styles/widgets/mw.cx.ui.TranslationToolWidget.less modules/tools/styles/mw.cx.tools.IssueTrackingTool.less app/src/styles/page.less modules/entrypoints/styles/ext.cx.entrypoints.newbytranslation.mobile.less modules/ui/styles/grid/grid-settings.less modules/ui/styles/widgets/mw.cx.ui.TranslationIssueWidget.less modules/ui/styles/grid/agora-grid.less modules/source/styles/mw.cx.SourcePageSelector.less modules/ve-cx/ce/styles/ve.ce.CXPlaceholderNode.less modules/ui/styles/mw.cx.ui.LanguageFilter.less modules/dashboard/styles/ext.cx.dashboard.less app/src/lib/mediawiki.ui/components/MWButton/buttons.less modules/entrypoints/styles/ext.cx.contributions.vector.less modules/ve-cx/ce/styles/ve.ce.CXSectionNode.less modules/ve-cx/ce/styles/ve.ce.CXSentenceSegmentAnnotation.less modules/entrypoints/styles/ext.cx.contributions.less modules/ui/styles/mw.cx.mixins.less app/src/lib/mediawiki.ui/components/MWLayout/grid-story.less modules/ui/styles/widgets/mw.cx.ui.FeatureDiscoveryWidget.less modules/ui/styles/mw.cx.variables.less modules/ui/styles/mw.cx.common.less modules/entrypoints/styles/ext.cx.entrypoints.newbytranslation.less app/dist/style.css modules/dashboard/styles/ext.cx.lists.common.less modules/ui/styles/widgets/mw.cx.ui.PageTitleWidget.less modules/ui/styles/widgets/mw.cx.ui.TitleOptionWidget.less modules/ve-cx/ui/tools/ve.ui.CXSaveMTPreferenceTool.less modules/ui/styles/mw.cx.ui.Columns.less modules/ve-cx/ui/tools/ve.ui.CXResetSectionTool.less modules/entrypoints/styles/ext.cx.entrypoints.newarticle.less modules/ve-cx/ui/styles/ve.ui.CXPublishSettingsDialog.less modules/ui/styles/mw.cx.ui.Infobar.less modules/ui/styles/mw.cx.ui.TranslationHeader.less modules/ve-cx/ui/styles/ve.ui.CXSurface.less modules/widgets/callout/ext.cx.callout.vector.less modules/entrypoints/styles/ext.cx.entrypoints.recenttranslation.less modules/dashboard/styles/ext.cx.dashboard.monobook.less skin/styles/menu.less modules/ui/styles/widgets/mw.cx.ui.MessageWidget.less modules/tools/styles/mw.cx.tools.SearchTool.less modules/ui/styles/mw.cx.ui.TranslationView.less modules/dashboard/styles/mw.cx.TranslationList.less modules/entrypoints/styles/ext.cx.interlanguagelink.less app/src/lib/mediawiki.ui/variables/wikimedia-ui-base.less modules/ve-cx/ui/styles/ve.ui.CXDesktopContext.less modules/stats/styles/ext.cx.stats.less modules/widgets/progressbar/ext.cx.progressbar.less modules/entrypoints/styles/ext.cx.entrypoints.uls.relevantlanguages.less modules/ui/styles/widgets/mw.cx.ui.CategoryMultiselectWidget.less modules/ve-cx/ui/styles/ve.ui.CXTranslationToolbar.less modules/ui/styles/mw.cx.ui.Categories.less modules/ve-cx/ce/styles/ve.ce.CXLinkAnnotation.less modules/ui/styles/mw.cx.ui.CaptchaDialog.less modules/ui/styles/mw.cx.ui.SourceColumn.less modules/entrypoints/styles/ext.cx.entrypoints.contributionsmenu.less modules/widgets/translator/ext.cx.translator.less modules/ve-cx/ui/styles/ve.ui.CXLinkContextItem.less modules/ui/styles/widgets/mw.cx.ui.CategoryTagItemWidget.monobook.less modules/ve-cx/ce/styles/ve.ce.CXReferenceNode.less modules/ui/styles/mw.cx.ui.ToolsColumn.less modules/widgets/callout/ext.cx.callout.css skin/styles/skin.less modules/widgets/spinner/ext.cx.spinner.less modules/source/styles/mw.cx.SelectedSourcePage.less -f json
--- stdout ---
[{"source":"/src/repo/modules/widgets/feedback/styles/ext.cx.feedback.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.highlight.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ce/mixins/ve.ce.CXPendingNode.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/entrypoints/styles/ext.cx.entrypoints.mffrequentlanguages.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/widgets/mw.cx.ui.CategoryTagItemWidget.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.ui.TargetColumn.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.init.Translation.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/dashboard/styles/mw.cx.SuggestionList.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":89,"column":19,"endLine":89,"endColumn":20,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":129,"column":4,"endLine":129,"endColumn":5,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":137,"column":4,"endLine":137,"endColumn":5,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"text":"Needless disable for \"indentation\"","rule":"--report-needless-disables","line":22,"column":1,"endLine":22,"endColumn":81,"severity":"error"},{"text":"Needless disable for \"selector-type-case\"","rule":"--report-needless-disables","line":22,"column":1,"endLine":22,"endColumn":81,"severity":"error"}]},{"source":"/src/repo/modules/ve-cx/ce/styles/ve.ce.CXLintableNode.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/grid/grid-core.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.ui.Header.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/tools/styles/mw.cx.tools.InstructionsTool.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/grid/grid-responsive.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/widgets/mw.cx.ui.TranslationToolWidget.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/tools/styles/mw.cx.tools.IssueTrackingTool.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/entrypoints/styles/ext.cx.entrypoints.newbytranslation.mobile.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":23,"column":5,"endLine":23,"endColumn":6,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"}]},{"source":"/src/repo/modules/ui/styles/grid/grid-settings.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/widgets/mw.cx.ui.TranslationIssueWidget.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/grid/agora-grid.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/source/styles/mw.cx.SourcePageSelector.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ce/styles/ve.ce.CXPlaceholderNode.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.ui.LanguageFilter.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":81,"column":27,"endLine":81,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":82,"column":27,"endLine":82,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":93,"column":27,"endLine":93,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":101,"column":27,"endLine":101,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"}]},{"source":"/src/repo/modules/dashboard/styles/ext.cx.dashboard.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/entrypoints/styles/ext.cx.contributions.vector.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ce/styles/ve.ce.CXSectionNode.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ce/styles/ve.ce.CXSentenceSegmentAnnotation.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/entrypoints/styles/ext.cx.contributions.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":9,"column":4,"endLine":9,"endColumn":5,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":21,"column":4,"endLine":21,"endColumn":5,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"}]},{"source":"/src/repo/modules/ui/styles/mw.cx.mixins.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":100,"column":3,"endLine":100,"endColumn":4,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"text":"Needless disable for \"at-rule-no-unknown\"","rule":"--report-needless-disables","line":92,"column":2,"endLine":92,"endColumn":43,"severity":"error"}]},{"source":"/src/repo/modules/ui/styles/widgets/mw.cx.ui.FeatureDiscoveryWidget.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.variables.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.common.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/entrypoints/styles/ext.cx.entrypoints.newbytranslation.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/dashboard/styles/ext.cx.lists.common.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":19,"column":42,"endLine":19,"endColumn":43,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":20,"column":42,"endLine":20,"endColumn":43,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":116,"column":13,"endLine":116,"endColumn":14,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":117,"column":13,"endLine":117,"endColumn":14,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":128,"column":21,"endLine":128,"endColumn":22,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":135,"column":21,"endLine":135,"endColumn":22,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"}]},{"source":"/src/repo/modules/ui/styles/widgets/mw.cx.ui.PageTitleWidget.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/widgets/mw.cx.ui.TitleOptionWidget.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ui/tools/ve.ui.CXSaveMTPreferenceTool.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.ui.Columns.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":10,"column":3,"endLine":10,"endColumn":4,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"}]},{"source":"/src/repo/modules/ve-cx/ui/tools/ve.ui.CXResetSectionTool.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/entrypoints/styles/ext.cx.entrypoints.newarticle.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ui/styles/ve.ui.CXPublishSettingsDialog.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.ui.Infobar.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.ui.TranslationHeader.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ui/styles/ve.ui.CXSurface.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/widgets/callout/ext.cx.callout.vector.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/entrypoints/styles/ext.cx.entrypoints.recenttranslation.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/dashboard/styles/ext.cx.dashboard.monobook.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/skin/styles/menu.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":79,"column":5,"endLine":79,"endColumn":6,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"}]},{"source":"/src/repo/modules/ui/styles/widgets/mw.cx.ui.MessageWidget.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/tools/styles/mw.cx.tools.SearchTool.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.ui.TranslationView.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/dashboard/styles/mw.cx.TranslationList.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/entrypoints/styles/ext.cx.interlanguagelink.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ui/styles/ve.ui.CXDesktopContext.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/stats/styles/ext.cx.stats.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":174,"column":4,"endLine":174,"endColumn":5,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"}]},{"source":"/src/repo/modules/widgets/progressbar/ext.cx.progressbar.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/entrypoints/styles/ext.cx.entrypoints.uls.relevantlanguages.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/widgets/mw.cx.ui.CategoryMultiselectWidget.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ui/styles/ve.ui.CXTranslationToolbar.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.ui.Categories.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ce/styles/ve.ce.CXLinkAnnotation.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.ui.CaptchaDialog.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.ui.SourceColumn.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/entrypoints/styles/ext.cx.entrypoints.contributionsmenu.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/widgets/translator/ext.cx.translator.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ui/styles/ve.ui.CXLinkContextItem.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/widgets/mw.cx.ui.CategoryTagItemWidget.monobook.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ve-cx/ce/styles/ve.ce.CXReferenceNode.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/ui/styles/mw.cx.ui.ToolsColumn.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/widgets/callout/ext.cx.callout.css","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":17,"column":12,"endLine":17,"endColumn":13,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":18,"column":12,"endLine":18,"endColumn":13,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":27,"column":27,"endLine":27,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":28,"column":14,"endLine":28,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":33,"column":27,"endLine":33,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":34,"column":14,"endLine":34,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":35,"column":14,"endLine":35,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":42,"column":27,"endLine":42,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":43,"column":14,"endLine":43,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":44,"column":14,"endLine":44,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":51,"column":27,"endLine":51,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":52,"column":14,"endLine":52,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":57,"column":27,"endLine":57,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":62,"column":27,"endLine":62,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":67,"column":27,"endLine":67,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":72,"column":27,"endLine":72,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":77,"column":27,"endLine":77,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":78,"column":14,"endLine":78,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":79,"column":15,"endLine":79,"endColumn":16,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":85,"column":27,"endLine":85,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":86,"column":14,"endLine":86,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":87,"column":15,"endLine":87,"endColumn":16,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":93,"column":27,"endLine":93,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":94,"column":14,"endLine":94,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":98,"column":28,"endLine":98,"endColumn":29,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":99,"column":15,"endLine":99,"endColumn":16,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":104,"column":27,"endLine":104,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":105,"column":14,"endLine":105,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":110,"column":28,"endLine":110,"endColumn":29,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":111,"column":15,"endLine":111,"endColumn":16,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":116,"column":28,"endLine":116,"endColumn":29,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":117,"column":14,"endLine":117,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":124,"column":28,"endLine":124,"endColumn":29,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":125,"column":14,"endLine":125,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":132,"column":29,"endLine":132,"endColumn":30,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":133,"column":15,"endLine":133,"endColumn":16,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":138,"column":28,"endLine":138,"endColumn":29,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":145,"column":28,"endLine":145,"endColumn":29,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":152,"column":27,"endLine":152,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":160,"column":27,"endLine":160,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":168,"column":27,"endLine":168,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":169,"column":14,"endLine":169,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":174,"column":27,"endLine":174,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":180,"column":27,"endLine":180,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":186,"column":27,"endLine":186,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":187,"column":14,"endLine":187,"endColumn":15,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":192,"column":27,"endLine":192,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":198,"column":27,"endLine":198,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":204,"column":27,"endLine":204,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":212,"column":27,"endLine":212,"endColumn":28,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"}]},{"source":"/src/repo/skin/styles/skin.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]},{"source":"/src/repo/modules/widgets/spinner/ext.cx.spinner.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":true,"warnings":[{"line":10,"column":3,"endLine":10,"endColumn":4,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":11,"column":3,"endLine":11,"endColumn":4,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":22,"column":3,"endLine":22,"endColumn":4,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"},{"line":27,"column":3,"endLine":27,"endColumn":4,"rule":"selector-pseudo-element-colon-notation","severity":"error","text":"Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)"}]},{"source":"/src/repo/modules/source/styles/mw.cx.SelectedSourcePage.less","deprecations":[],"invalidOptionWarnings":[],"parseErrors":[],"errored":false,"warnings":[]}]
--- end ---
$ /usr/bin/npm ci --legacy-peer-deps
--- stdout ---

added 353 packages, and audited 354 packages in 4s

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

found 0 vulnerabilities

--- end ---
$ /usr/bin/npm test
--- stdout ---

> test
> npm run test:cx2 && npm run test:cx3


> test:cx2
> npm -s run lint

Checked 1 message directory.

/src/repo/.eslintrc.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/.stylelintrc.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/composer.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/extension.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/i18n/api/en.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/i18n/api/qqq.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/i18n/en.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/i18n/qqq.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/.eslintrc.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/base/ext.cx.model.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/base/mw.cx.SiteMapper.js
    1:1   error    Definition for rule 'es/no-promise' was not found  es/no-promise
  143:12  warning  URL is not supported in IE 11                      compat/compat

/src/repo/modules/cache/mw.cx.ApiResponseCache.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/cache/mw.cx.CategoryCache.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/cache/mw.cx.NamespaceCache.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/cache/mw.cx.TitlePairCache.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dashboard/ext.cx.dashboard.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dashboard/ext.cx.recommendtool.client.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dashboard/mw.cx.DashboardList.js
    1:1  error    Definition for rule 'es/no-promise' was not found                        es/no-promise
  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
    1:1   error    Definition for rule 'es/no-promise' was not found                    es/no-promise
  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
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.PageTitleModel.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.SectionState.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.SectionTitleModel.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.Translation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.TranslationIssue.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.WikiPage.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/dm/mw.cx.dm.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.betafeature.init.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.contributions.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.contributionsmenu.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.languagesearcher.init.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.languagesearcher.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.mffrequentlanguages.js
    1:1  error    Definition for rule 'es/no-promise' was not found  es/no-promise
  148:0  warning  The type 'LanguageSearcher' is undefined           jsdoc/no-undefined-types

/src/repo/modules/entrypoints/ext.cx.entrypoints.newarticle.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.newarticle.veloader.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.newbytranslation.js
    1:1   error    Definition for rule 'es/no-promise' was not found                                es/no-promise
  177:35  warning  navigator.languages() is not supported in Safari 9.1, iOS Safari 9.0-9.2, IE 11  compat/compat

/src/repo/modules/entrypoints/ext.cx.entrypoints.newbytranslation.mobile.js
   1:1   error    Definition for rule 'es/no-promise' was not found                                es/no-promise
  10:35  warning  navigator.languages() is not supported in Safari 9.1, iOS Safari 9.0-9.2, IE 11  compat/compat
  38:10  warning  fetch is not supported in Safari 9.1, IE 11                                      compat/compat

/src/repo/modules/entrypoints/ext.cx.entrypoints.recentedit/RecentEditEntrypointInvitation.vue
    1:1   error    Definition for rule 'es/no-promise' was not found  es/no-promise
    8:7   warning  Don't use 'v-text'                                 vue/no-v-text
   32:6   warning  Don't use 'v-text'                                 vue/no-v-text
   61:7   warning  Don't use 'v-text'                                 vue/no-v-text
   66:6   warning  Don't use 'v-text'                                 vue/no-v-text
  116:29  error    ES2015 'Array.prototype.keys' method is forbidden  es-x/no-array-prototype-keys
  129:11  error    ES2015 'Array.prototype.keys' method is forbidden  es-x/no-array-prototype-keys

/src/repo/modules/entrypoints/ext.cx.entrypoints.recentedit/index.js
   1:1   error    Definition for rule 'es/no-promise' was not found  es/no-promise
  36:10  warning  fetch is not supported in Safari 9.1, IE 11        compat/compat

/src/repo/modules/entrypoints/ext.cx.entrypoints.recenttranslation/RecentTranslationEntrypointDialog.vue
    1:1   error    Definition for rule 'es/no-promise' was not found  es/no-promise
    7:10  warning  Don't use 'v-text'                                 vue/no-v-text
   16:6   warning  Don't use 'v-text'                                 vue/no-v-text
   22:6   warning  Don't use 'v-text'                                 vue/no-v-text
   33:7   warning  Don't use 'v-text'                                 vue/no-v-text
   37:12  warning  Don't use 'v-text'                                 vue/no-v-text
   38:11  warning  Don't use 'v-text'                                 vue/no-v-text
   41:12  warning  Don't use 'v-text'                                 vue/no-v-text
   42:11  warning  Don't use 'v-text'                                 vue/no-v-text
   46:12  warning  Don't use 'v-text'                                 vue/no-v-text
   47:11  warning  Don't use 'v-text'                                 vue/no-v-text
   54:13  warning  Don't use 'v-text'                                 vue/no-v-text
   61:8   warning  Don't use 'v-text'                                 vue/no-v-text
   75:7   warning  Don't use 'v-text'                                 vue/no-v-text
   80:13  warning  Don't use 'v-text'                                 vue/no-v-text
   84:7   warning  Don't use 'v-text'                                 vue/no-v-text
  176:4   warning  fetch is not supported in Safari 9.1, IE 11        compat/compat
  180:7   warning  Promise.reject() is not supported in IE 11         compat/compat
  180:7   error    ES2015 'Promise' class is forbidden                es-x/no-promise
  184:30  error    ES2015 'Array.prototype.keys' method is forbidden  es-x/no-array-prototype-keys

/src/repo/modules/entrypoints/ext.cx.entrypoints.recenttranslation/index.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.uls.relevantlanguages.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.entrypoints.uls.relevantlanguages/CxUlsEntrypoint.vue
   1:1  error    Definition for rule 'es/no-promise' was not found  es/no-promise
  30:6  warning  Don't use 'v-text'                                 vue/no-v-text
  40:5  warning  Don't use 'v-text'                                 vue/no-v-text
  68:7  warning  Don't use 'v-text'                                 vue/no-v-text

/src/repo/modules/entrypoints/ext.cx.interlanguagelink.init.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.interlanguagelink.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/entrypoints/ext.cx.uls.quick.actions.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/eventlogging/ext.cx.eventlogging.campaigns.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/eventlogging/ext.cx.eventlogging.translation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/eventlogging/legacy/ext.cx.eventlogging.translation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.MachineTranslationManager.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.MachineTranslationService.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.MwApiRequestManager.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.TargetArticle.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.TranslationController.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.TranslationTracker.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.init.Translation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/mw.cx.init.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/publish/ext.cx.wikibase.link.js
   1:1   error    Definition for rule 'es/no-promise' was not found  es/no-promise
  30:11  error    ES2015 'Promise' class is forbidden                es-x/no-promise
  43:10  warning  Promise.resolve() is not supported in IE 11        compat/compat
  43:10  error    ES2015 'Promise' class is forbidden                es-x/no-promise

/src/repo/modules/source/mw.cx.SelectedSourcePage.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/source/mw.cx.SelectedSourcePageDialog.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/source/mw.cx.SourcePageSelector.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/stats/ext.cx.stats.js
    1:1   error    Definition for rule 'es/no-promise' was not found                    es/no-promise
  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.validator.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/tools/mw.cx.tools.InstructionsTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/tools/mw.cx.tools.IssueTrackingTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/tools/mw.cx.tools.TranslationTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/tools/mw.cx.tools.TranslationToolFactory.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/translation/ext.cx.translation.conflict.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.ArticleColumn.js
   1:1  error    Definition for rule 'es/no-promise' was not found  es/no-promise
  17:0  warning  Duplicate @param "config.sectionTitle"             jsdoc/check-param-names

/src/repo/modules/ui/mw.cx.ui.CaptchaDialog.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.Categories.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.Infobar.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.LanguageFilter.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.LoginDialog.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.SourceColumn.js
  1:1  error    Definition for rule 'es/no-promise' was not found                                es/no-promise
  6:0  warning  @param path declaration ("config.siteMapper") appears before any real parameter  jsdoc/check-param-names

/src/repo/modules/ui/mw.cx.ui.TargetColumn.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.ToolsColumn.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.TranslationHeader.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.TranslationView.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/mw.cx.ui.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.CategoryInputWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.CategoryMultiselectWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.CategoryTagItemWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.FeatureDiscoveryWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.MessageWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
    1:1   error    Definition for rule 'es/no-promise' was not found                     es/no-promise
  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
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.SectionTitleWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.TitleOptionWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.TranslationIssueWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ui/widgets/mw.cx.ui.TranslationToolWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/util/mw.cx.util.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/mixins/ve.ce.CXLintableNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/mixins/ve.ce.CXPendingNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXBlockImageNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXImageCaptionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXLinkAnnotation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXPlaceholderNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXReferenceNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXSectionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXSentenceSegmentAnnotation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ce/ve.ce.CXTransclusionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/mixins/ve.dm.CXLintableNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXBlockImageNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXImageCaptionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXLinkAnnotation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXPlaceholderNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXReferenceNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXSectionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXSentenceSegmentAnnotation.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXTransclusionNode.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/dm/ve.dm.CXTranslationUnitModel.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/init/ve.init.mw.CXTarget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/actions/ve.ui.CXTranslationAction.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/tools/ve.ui.CXResetSectionTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/tools/ve.ui.CXSaveMTPreferenceTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXDesktopContext.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXInternalLinkAnnotationWidget.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXLinkAnnotationInspector.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXLinkContextItem.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXPublishSettingsDialog.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXPublishSettingsTool.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXPublishTool.js
   1:1  error  Definition for rule 'es/no-promise' was not found      es/no-promise
  36:1  error  ES2015 'RegExp.prototype.flags' property is forbidden  es-x/no-regexp-prototype-flags

/src/repo/modules/ve-cx/ui/ve.ui.CXReferenceContextItem.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXReferenceDialog.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXSurface.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXTextSelectionContextItem.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXTransclusionContextItem.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXTranslationToolbar.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/ve-cx/ui/ve.ui.CXTranslationUnitContextItem.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/widgets/callout/ext.cx.callout.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/widgets/feedback/ext.cx.feedback.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/widgets/progressbar/ext.cx.progressbar.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/widgets/spinner/ext.cx.spinner.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/modules/widgets/translator/ext.cx.translator.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/package-lock.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/package.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/sql/abstractSchemaChanges/patch-cx_translators-unique-to-pk.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/sql/tables.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/.eslintrc.json
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/base/mw.cx.SiteMapper.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/dm/mw.cx.dm.Translation.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/mw.cx.MachineTranslationService.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/mw.cx.TargetArticle.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/mw.cx.TranslationTracker.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/mw.cx.util.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

/src/repo/tests/qunit/ui/mw.cx.ui.Infobar.test.js
  1:1  error  Definition for rule 'es/no-promise' was not found  es/no-promise

✖ 204 problems (154 errors, 50 warnings)


--- end ---
Traceback (most recent call last):
  File "/venv/lib/python3.9/site-packages/runner-0.1.0-py3.9.egg/runner/__init__.py", line 1400, in main
    libup.run(args.repo, args.output, args.branch)
  File "/venv/lib/python3.9/site-packages/runner-0.1.0-py3.9.egg/runner/__init__.py", line 1338, in run
    self.npm_upgrade(plan)
  File "/venv/lib/python3.9/site-packages/runner-0.1.0-py3.9.egg/runner/__init__.py", line 1049, in npm_upgrade
    self.npm_test()
  File "/venv/lib/python3.9/site-packages/runner-0.1.0-py3.9.egg/runner/__init__.py", line 287, in npm_test
    self.check_call(['npm', 'test'])
  File "/venv/lib/python3.9/site-packages/runner-0.1.0-py3.9.egg/runner/shell2.py", line 54, in check_call
    res.check_returncode()
  File "/usr/lib/python3.9/subprocess.py", line 460, in check_returncode
    raise CalledProcessError(self.returncode, self.args, self.stdout,
subprocess.CalledProcessError: Command '['/usr/bin/npm', 'test']' returned non-zero exit status 1.
Source code is licensed under the AGPL.