Merge "Add password validation to Special:ChangeCredentials"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 17 Mar 2017 08:35:06 +0000 (08:35 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 17 Mar 2017 08:35:07 +0000 (08:35 +0000)
42 files changed:
docs/hooks.txt
includes/changes/ChangesListFilter.php
includes/changes/ChangesListFilterGroup.php
includes/installer/i18n/hu.json
includes/libs/CryptRand.php
includes/resourceloader/ResourceLoaderImageModule.php
includes/specialpage/ChangesListSpecialPage.php
includes/specials/SpecialEmailuser.php
languages/i18n/be-tarask.json
languages/i18n/br.json
languages/i18n/ceb.json
languages/i18n/cs.json
languages/i18n/et.json
languages/i18n/fa.json
languages/i18n/fr.json
languages/i18n/frr.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/hu-formal.json [new file with mode: 0644]
languages/i18n/it.json
languages/i18n/lb.json
languages/i18n/nan.json
languages/i18n/nl.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/tr.json
languages/i18n/zh-hans.json
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FiltersListWidget.less
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
tests/phpunit/includes/changes/ChangesListFilterGroupTest.php
tests/phpunit/includes/changes/ChangesListFilterTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php
tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js
tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js

index 4b356dd..f307f45 100644 (file)
@@ -1015,10 +1015,11 @@ $opts: FormOptions for this request
 'ChangesListSpecialPageStructuredFilters': Called to allow extensions to register
 filters for pages inheriting from ChangesListSpecialPage (in core: RecentChanges,
 RecentChangesLinked, and Watchlist).  Generally, you will want to construct
-new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects.  You can
-then either add them to existing ChangesListFilterGroup objects (accessed through
-$special->getFilterGroup), or create your own.  If you create new groups, you
-must register them with $special->registerFilterGroup.
+new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects.
+
+When constructing them, you specify which group they belong to.  You can reuse
+existing groups (accessed through $special->getFilterGroup), or create your own.
+If you create new groups, you must register them with $special->registerFilterGroup.
 $special: ChangesListSpecialPage instance
 
 'ChangeTagAfterDelete': Called after a change tag has been deleted (that is,
index cd74154..22e797d 100644 (file)
@@ -113,7 +113,8 @@ abstract class ChangesListFilter {
        const RESERVED_NAME_CHAR = '_';
 
        /**
-        * Create a new filter with the specified configuration.
+        * Creates a new filter with the specified configuration, and registers it to the
+        * specified group.
         *
         * It infers which UI (it can be either or both) to display the filter on based on
         * which messages are provided.
@@ -161,6 +162,11 @@ abstract class ChangesListFilter {
                        );
                }
 
+               if ( $this->group->getFilter( $filterDefinition['name'] ) ) {
+                       throw new MWException( 'Two filters in a group cannot have the ' .
+                               "same name: '{$filterDefinition['name']}'" );
+               }
+
                $this->name = $filterDefinition['name'];
 
                if ( isset( $filterDefinition['cssClassSuffix'] ) ) {
index 30ab552..d2ad204 100644 (file)
@@ -315,10 +315,10 @@ abstract class ChangesListFilterGroup {
         * Get filter by name
         *
         * @param string $name Filter name
-        * @return ChangesListFilter Specified filter
+        * @return ChangesListFilter|null Specified filter, or null if it is not registered
         */
        public function getFilter( $name ) {
-               return $this->filters[$name];
+               return isset( $this->filters[$name] ) ? $this->filters[$name] : null;
        }
 
        /**
index 1c5c2ee..5aece05 100644 (file)
        "config-missing-db-name": "Meg kell adnod a(z) „{{int:config-db-name}}” értékét.",
        "config-missing-db-host": "Meg kell adnod az „{{int:config-db-host}}” értékét.",
        "config-missing-db-server-oracle": "Meg kell adnod az „{{int:config-db-host-oracle}}” értékét.",
-       "config-invalid-db-server-oracle": "Érvénytelen adatbázis TNS: „$1”\nCsak ASCII betűk (a-z, A-Z), számok (0-9), alulvonás (_) és pont (.) használható.",
+       "config-invalid-db-server-oracle": "Érvénytelen adatbázis TNS: „$1”\nHasználd a „TNS Name” vagy az Easy Connect” sztringet!\n([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).",
        "config-invalid-db-name": "Érvénytelen adatbázisnév: „$1”.\nCsak ASCII-karakterek (a-z, A-Z), számok (0-9), alulvonás (_) és kötőjel (-) használható.",
        "config-invalid-db-prefix": "Érvénytelen adatbázisnév-előtag: „$1”.\nCsak ASCII-karakterek (a-z, A-Z), számok (0-9), alulvonás (_) és kötőjel (-) használható.",
        "config-connection-error": "$1.\n\nEllenőrizd a hosztot, felhasználónevet és jelszót, majd próbáld újra.",
        "config-nofile": "\"$1\" fájl nem található. Törölve lett?",
        "config-extension-link": "Tudtad, hogy a wikid támogat [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions kiterjesztéseket]?\n\nBöngészhetsz [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category kiterjesztéseket kategóriánként] vagy válogathatsz a [https://www.mediawiki.org/wiki/Extension_Matrix kiterjesztésmátrixból] az összes kiterjesztés áttekintéséhez.",
        "mainpagetext": "<strong>A MediaWiki telepítése sikeresen befejeződött.</strong>",
-       "mainpagedocfooter": "Ha segítségre van szükséged a wikiszoftver használatához, akkor keresd fel a [https://meta.wikimedia.org/wiki/Help:Contents User's Guide] oldalt.\n\n== Alapok (angol nyelven) ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Beállítások listája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki GyIK]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources A MediaWiki fordítása a saját nyelvedre]"
+       "mainpagedocfooter": "Ha segítségre van szükséged a wikiszoftver használatához, akkor keresd fel a [https://meta.wikimedia.org/wiki/Help:Contents User's Guide] oldalt.\n\n== Alapok (angol nyelven) ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Beállítások listája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki GyIK]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources A MediaWiki fordítása a saját nyelvedre]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Tudd meg többet, hogyan küzdhetsz a kéretlen levelek ellen a wikiden]"
 }
index 10088f2..0d3613a 100644 (file)
@@ -242,6 +242,21 @@ class CryptRand {
                        $this->strong = true;
                }
 
+               if ( strlen( $buffer ) < $bytes ) {
+                       // If available make use of PHP 7's random_bytes
+                       // On Linux, getrandom syscall will be used if available.
+                       // On Windows CryptGenRandom will always be used
+                       // On other platforms, /dev/urandom will be used.
+                       // All error situations will throw Exceptions and or Errors
+                       if ( function_exists( 'random_bytes' ) ) {
+                               $rem = $bytes - strlen( $buffer );
+                               $buffer .= random_bytes( $rem );
+                       }
+                       if ( strlen( $buffer ) >= $bytes ) {
+                               $this->strong = true;
+                       }
+               }
+
                if ( strlen( $buffer ) < $bytes ) {
                        // If available make use of mcrypt_create_iv URANDOM source to generate randomness
                        // On unix-like systems this reads from /dev/urandom but does it without any buffering
index ff1b7b1..1a3463f 100644 (file)
@@ -315,11 +315,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
                $selectors = $this->getSelectors();
 
                foreach ( $this->getImages( $context ) as $name => $image ) {
-                       $declarations = $this->getCssDeclarations(
-                               $image->getDataUri( $context, null, 'original' ),
-                               $image->getUrl( $context, $script, null, 'rasterized' )
-                       );
-                       $declarations = implode( "\n\t", $declarations );
+                       $declarations = $this->getStyleDeclarations( $context, $image, $script );
                        $selector = strtr(
                                $selectors['selectorWithoutVariant'],
                                [
@@ -331,11 +327,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
                        $rules[] = "$selector {\n\t$declarations\n}";
 
                        foreach ( $image->getVariants() as $variant ) {
-                               $declarations = $this->getCssDeclarations(
-                                       $image->getDataUri( $context, $variant, 'original' ),
-                                       $image->getUrl( $context, $script, $variant, 'rasterized' )
-                               );
-                               $declarations = implode( "\n\t", $declarations );
+                               $declarations = $this->getStyleDeclarations( $context, $image, $script, $variant );
                                $selector = strtr(
                                        $selectors['selectorWithVariant'],
                                        [
@@ -352,6 +344,28 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
                return [ 'all' => $style ];
        }
 
+       /**
+        * @param ResourceLoaderContext $context
+        * @param ResourceLoaderImage $image Image to get the style for
+        * @param string $script URL to load.php
+        * @param string|null $variant Variant to get the style for
+        * @return string
+        */
+       private function getStyleDeclarations(
+               ResourceLoaderContext $context,
+               ResourceLoaderImage $image,
+               $script,
+               $variant = null
+       ) {
+               $imageDataUri = $image->getDataUri( $context, $variant, 'original' );
+               $primaryUrl = $imageDataUri ?: $image->getUrl( $context, $script, $variant, 'original' );
+               $declarations = $this->getCssDeclarations(
+                       $primaryUrl,
+                       $image->getUrl( $context, $script, $variant, 'rasterized' )
+               );
+               return implode( "\n\t", $declarations );
+       }
+
        /**
         * SVG support using a transparent gradient to guarantee cross-browser
         * compatibility (browsers able to understand gradient syntax support also SVG).
index e92f697..8f702ba 100644 (file)
@@ -663,10 +663,12 @@ abstract class ChangesListSpecialPage extends SpecialPage {
         *
         * @param string $groupName Name of group
         *
-        * @return ChangesListFilterGroup
+        * @return ChangesListFilterGroup|null Group, or null if not registered
         */
        public function getFilterGroup( $groupName ) {
-               return $this->filterGroups[$groupName];
+               return isset( $this->filterGroups[$groupName] ) ?
+                       $this->filterGroups[$groupName] :
+                       null;
        }
 
        // Currently, this intentionally only includes filters that display
index 085b68d..a69406c 100644 (file)
@@ -231,14 +231,15 @@ class SpecialEmailUser extends UnlistedSpecialPage {
                        return 'usermaildisabled';
                }
 
-               if ( !$user->isAllowed( 'sendemail' ) ) {
-                       return 'badaccess';
-               }
-
+               // Run this before $user->isAllowed, to show appropriate message to anons (T160309)
                if ( !$user->isEmailConfirmed() ) {
                        return 'mailnologin';
                }
 
+               if ( !$user->isAllowed( 'sendemail' ) ) {
+                       return 'badaccess';
+               }
+
                if ( $user->isBlockedFromEmailuser() ) {
                        wfDebug( "User is blocked from sending e-mail.\n" );
 
index ebc449b..a1625b8 100644 (file)
        "revdelete-log": "Прычына:",
        "revdelete-submit": "Ужыць для {{PLURAL:$1|1=выбранай вэрсіі|выбраных вэрсіяў}}",
        "revdelete-success": "Бачнасьць вэрсіі абноўленая.",
-       "revdelete-failure": "'''Немагчыма абнавіць бачнасьць вэрсіі:'''\n$1",
+       "revdelete-failure": "Немагчыма абнавіць бачнасьць вэрсіі:\n$1",
        "logdelete-success": "Бачнасьць падзеі ўсталяваная.",
        "logdelete-failure": "'''Немагчыма ўстанавіць бачнасьць у журнале:'''\n$1",
        "revdel-restore": "Зьмяніць бачнасьць",
        "rcfilters-filter-patrolled-label": "Правераныя",
        "rcfilters-filter-patrolled-description": "Праўкі, пазначаныя як правераныя.",
        "rcfilters-filter-unpatrolled-label": "Неправераныя",
+       "rcfilters-filter-unpatrolled-description": "Рэдагаваньні, не пазначаныя як правераныя.",
        "rcfilters-filtergroup-significance": "Значэньне",
        "rcfilters-filter-minor-label": "Дробныя праўкі",
        "rcfilters-filter-minor-description": "Праўкі, якія аўтар пазначыў як дробныя.",
index ac9220c..684020e 100644 (file)
        "page_last": "diwezhañ",
        "histlegend": "Sellet ouzh an diforc'hioù : lakait un ask adal d'ar stummoù a fell deoc'h keñveriañ ha pouezit war kadarnaat pe war ar bouton en traoñ.<br />\nAlc'hwez : (red) = diforc'hioù gant ar stumm a-vremañ,\n(diwez) = diforc'hioù gant ar stumm kent, D = kemm dister",
        "history-fieldset-title": "Furchal en istor",
-       "history-show-deleted": "Diverket hepken",
+       "history-show-deleted": "Stumm diverket hepken",
        "histfirst": "koshañ",
        "histlast": "nevezañ",
        "historysize": "({{PLURAL:$1|$1 okted|$1 okted}})",
        "tags-delete-explanation-initial": "Emaoc'h o vont da ziverkañ an dikedenn \"$1\" a-ziwar an diaz roadennoù.",
        "tags-delete-explanation-in-use": "Dilamet e vo diouzh an {{PLURAL:$2|$2 stumm pe moned marilh|holl $2 stumm ha/pe monedoù marilh}} m'emañ bet lakaet evit ar mare.",
        "tags-delete-explanation-warning": "<strong>Ne vo ket posupl distreiñ</strong> war an ober-mañ <strong>n'haller ket nullañ</strong>, ha pa vefe gant merourien an diaz roadennoù zoken. Bezit peursur eo homañ an dikedenn a fell deoc'h diverkañ.",
+       "tags-delete-explanation-active": "Oberiant eo c'hoazh <strong>an dikedenn \"$1\" ha kenderc'hel a raio da vezañ kemeret e kont en dazont.</strong> Evit mirout na c'hoarvezfe, mont d'al lec'h(ioù) m'emañ an dikedenn da vezañ lakaet da dalvezout ha diweredekaat anezhi eno.",
        "tags-delete-reason": "Abeg :",
+       "tags-delete-submit": "Diverkañ an dikedenn-mañ da viken",
+       "tags-delete-not-allowed": "N'haller ket diverkañ an tikedennoù termenet gant un astenn nemet e vefe aotreet en un doare sklaer d'en ober gant an astenn.",
        "tags-delete-not-found": "N'eus ket eus an dikedenn \"$1\".",
+       "tags-delete-too-many-uses": "Talvezout a ra an dikedenn \"$1\" evit muioc'h eget $2 {{PLURAL:$2|stumm}}, ar pezh a dalvez ne c'hall ket bezañ diverket.",
+       "tags-delete-warnings-after-delete": "Diverket eo bet an dikedenn \"$1\" met kavet eo bet ar {{PLURAL:$2|c'hemenn|c'hemennoù}} da-heul :",
+       "tags-delete-no-permission": "N'oc'h ket aotreet da ziverkañ an dikedennoù kemmañ.",
        "tags-activate-title": "Gweredekaat an dikedenn",
+       "tags-activate-question": "Emaoc'h o vont da weredekaat an dikedenn \"$1\".",
        "tags-activate-reason": "Abeg :",
+       "tags-activate-not-allowed": "N'haller ket gweredekaat an dikedenn \"$1\".",
+       "tags-activate-not-found": "N'eus ket eus an dikedenn \"$1\".",
        "tags-activate-submit": "Gweredekaat",
        "tags-deactivate-title": "Diweredekaat an dikedenn",
+       "tags-deactivate-question": "Emaoc'h o vont da ziweredekaat an dikedenn \"$1\".",
        "tags-deactivate-reason": "Abeg :",
+       "tags-deactivate-not-allowed": "N'haller ket diweredekaat an dikedenn \"$1\".",
        "tags-deactivate-submit": "Diweredekaat",
+       "tags-apply-no-permission": "N'oc'h ket aotreet da lakaat tikedennoù kemmañ da dalvezout a-gevret gant ho kemmoù.",
+       "tags-apply-blocked": "N'hallit ket lakaat da dalvezout ar c'hemmañ tikedennoù hag ho kemmoù e-keit hag {{GENDER:$1|emaoc'h}} stanket.",
+       "tags-apply-not-allowed-one": "N'haller ket lakaat an dikedenn \"$1\" da dalvezout gant an dorn.",
        "tags-edit-title": "Kemmañ an tikedennoù",
        "tags-edit-manage-link": "Merañ an tikedennoù",
        "tags-edit-existing-tags": "Tikedennoù zo anezho :",
index 6d2c938..4806964 100644 (file)
        "searchprofile-articles": "Mga panid sa sulod",
        "searchprofile-images": "Multimedia",
        "searchprofile-everything": "Tanan",
+       "searchprofile-advanced": "Abanse",
        "searchprofile-articles-tooltip": "Pangita sa $1",
        "searchprofile-images-tooltip": "Pangita og mga payl",
        "searchprofile-everything-tooltip": "Pangita sa tanang sulod (lakip ang mga panid sa hisgot)",
        "undeletelink": "tan-awa/ibalik",
        "namespace": "Ngalang espasyo:",
        "invert": "Baliha ang gipili",
+       "tooltip-invert": "I-tsek kini nga kahon aron tagoon ang mga kausaban sa mga panid sa gipili nga <i>namespace</i> (ug sa <i>associated namespace</i> kon gi-tsekan)",
+       "tooltip-namespace_association": "I-tsek kini nga kahon aron iapil ang mga panid sa hisgot o <i>subject namespace</i> nga may kalabotan sa gipili nga <i>namespace</i>",
        "blanknamespace": "(Meyn)",
        "contributions": "Mga tampo ning {{GENDER:$1|gumagamit}}",
        "contributions-title": "Mga tampo sa gumagamit para kang $1",
        "svg-long-desc": "SVG nga payl, nominally $1 × $2 pixels, size sa payl: $3",
        "show-big-image": "Tibuok resolusyon",
        "show-big-image-preview": "Gidak-on ning maong paunang tan-aw: $1.",
+       "show-big-image-other": "Uban pang {{PLURAL:$2|resolusyon|mga resoluyon}}: $1.",
        "show-big-image-size": "$1 x $2 ka mga piksel",
        "bad_image_list": "Ang pormat mao ang mosunod:\n\nAng mga list items (mga linya nga nagsugod sa*) ang gikonsiderar.\nAng unang sumpay sa linya kinahanglang sumpay sa payl nga daot.\nAng bisan unsang mosunod nga mga sumpay sa parehong linya gikonsiderar nga mga eksepsyon, i.e. mga panid diin ang payl mahimong inline.",
        "metadata": "Metadata",
index 9cf8622..996161a 100644 (file)
        "login": "Přihlaste se",
        "login-security": "Ověřte svou identitu",
        "nav-login-createaccount": "Přihlášení / vytvoření účtu",
-       "userlogin": "Přihlášení / vytvoření účtu",
-       "userloginnocreate": "Přihlášení",
        "logout": "Odhlásit se",
        "userlogout": "Odhlášení",
        "notloggedin": "Nejste přihlášen(a)",
        "userlogin-noaccount": "Nemáte účet?",
        "userlogin-joinproject": "Přidejte se k {{grammar:3sg|{{SITENAME}}}}",
-       "nologin": "Dosud nemáte účet? $1.",
-       "nologinlink": "Zaregistrujte se",
        "createaccount": "Vytvořit účet",
-       "gotaccount": "Už jste registrováni? $1.",
-       "gotaccountlink": "Přihlaste se",
-       "userlogin-resetlink": "Zapomněli jste přihlašovací údaje?",
        "userlogin-resetpassword-link": "Zapomněli jste heslo?",
        "userlogin-helplink2": "Nápověda k přihlašování",
        "userlogin-loggedin": "Již jste {{GENDER:$1|přihlášen|přihlášena}} jako $1.\nPomocí formuláře níže se můžete přihlásit jako jiný uživatel.",
        "createaccountmail": "Použít dočasné náhodné heslo a odeslat ho na uvedenou e-mailovou adresu",
        "createaccountmail-help": "Lze využít k založení účtu pro jinou osobu bez prozrazení hesla.",
        "createacct-realname": "Skutečné jméno (nepovinné)",
-       "createaccountreason": "Důvod:",
        "createacct-reason": "Důvod",
        "createacct-reason-ph": "Proč si vytváříte další účet",
        "createacct-reason-help": "Zpráva zobrazená v knize nových uživatelů",
        "prefs-help-prefershttps": "Toto nastavení se projeví při příštím přihlášení.",
        "prefswarning-warning": "Provedli jste změny nastavení, které dosud nejsou uloženy. Pokud tuto stránku opustíte, aniž byste klikli na „$1“, vaše nastavení se nezmění.",
        "prefs-tabs-navigation-hint": "Tip: Pro přepínání mezi záložkami můžete používat šipky vlevo a vpravo.",
-       "userrights": "Správa uživatelských skupin",
+       "userrights": "Uživatelská práva",
        "userrights-lookup-user": "Vybrat uživatele",
        "userrights-user-editname": "Zadejte uživatelské jméno:",
        "editusergroup": "Načíst uživatelské skupiny",
        "logentry-tag-update-revision": "$1 {{GENDER:$2|změnil|změnila}} značky na revizi $4 stránky $3 ({{PLURAL:$7|přidáno}} $6; {{PLURAL:$9|odebráno}} $8)",
        "logentry-tag-update-logentry": "$1 {{GENDER:$2|změnil|změnila}} značky na protokolovacím záznamu $5 k stránce $3 ({{PLURAL:$7|přidáno}} $6; {{PLURAL:$9|odebráno}} $8)",
        "rightsnone": "(žádné)",
-       "revdelete-summary": "shrnutí editace",
        "rightslogentry-temporary-group": "$1 (dočasně, do $2)",
        "feedback-adding": "Komentář se přidává na stránku…",
        "feedback-back": "Zpět",
index d82602f..28a2d34 100644 (file)
        "page_last": "viimane",
        "histlegend": "Märgi versioonid, mida tahad võrrelda ja vajuta võrdlemisnupule.\nLegend: (viim) = erinevused võrreldes viimase redaktsiooniga,\n(eel) = erinevused võrreldes eelmise redaktsiooniga, P = pisimuudatus",
        "history-fieldset-title": "Ajaloo sirvimine",
-       "history-show-deleted": "Üksnes kustutatud",
+       "history-show-deleted": "Üksnes kustutatud redaktsioonid",
        "histfirst": "vanimad",
        "histlast": "uusimad",
        "historysize": "({{PLURAL:$1|1 bait|$1 baiti}})",
        "prefs-help-prefershttps": "See eelistus jõustub pärast järgmist sisselogimist.",
        "prefswarning-warning": "Oled teinud eelistustes muudatusi, mida pole veel salvestatud.\nKui lahkud sellelt leheküljelt ilma nupul \"$1\" klõpsamata, jäävad kehtima senised eelistused.",
        "prefs-tabs-navigation-hint": "Spikker: Kaardiloendis toodud kaartide vahel liikumiseks saad kasutada vasakut ja paremat nooleklahvi.",
-       "userrights": "Kasutajaõiguste haldus",
+       "userrights": "Kasutajaõigused",
        "userrights-lookup-user": "Kasutaja valimine",
        "userrights-user-editname": "Sisesta kasutajanimi:",
        "editusergroup": "Laadi kasutajarühmad",
index a228ed8..aa7e5e6 100644 (file)
        "prefs-help-prefershttps": "تأثیر این ترجیح بعد از ورود بعدی شما اعمال خواهد شد.",
        "prefswarning-warning": "تغییراتتان به ترجیحات هنوز ذحیره نشده است.\nاگر این صفحه بدون کلیک بر «$1» ترک کنید ترجیحاتتان ذخیره نخواهد شد.",
        "prefs-tabs-navigation-hint": "نکته: شما می توانید از کلیدهای جهت‌نمای چپ و راست برای حرکت بین زبانه‌ها در فهرست زبانه‌ها استفاده کنید.",
-       "userrights": "مدیریت اختیارات کاربر",
+       "userrights": "اختیارات کاربر",
        "userrights-lookup-user": "انتخاب یک کاربر",
        "userrights-user-editname": "یک نام کاربری وارد کنید:",
        "editusergroup": "بارگیری گروه‌های کاربر",
index dd5f348..72264e4 100644 (file)
        "page_last": "dernière",
        "histlegend": "Sélection du diff : cochez les boutons radio des versions à comparer et appuyez sur entrée ou sur le bouton en bas.<br />\nLégende : <strong>({{int:cur}})</strong> = différence avec la dernière version, <strong>({{int:last}})</strong> = différence avec la version précédente, <strong>{{int:minoreditletter}}</strong> = modification mineure.",
        "history-fieldset-title": "Naviguer dans l’historique",
-       "history-show-deleted": "Supprimés seulement",
+       "history-show-deleted": "Révision supprimée uniquement",
        "histfirst": "les plus anciennes",
        "histlast": "les plus récentes",
        "historysize": "($1 octet{{PLURAL:$1||s}})",
        "movelogpagetext": "Voici la liste de toutes les pages renommées ou déplacées.",
        "movesubpage": "Sous-page{{PLURAL:$1||s}}",
        "movesubpagetext": "Cette page a $1 {{PLURAL:$1|sous-page affichée|sous-pages affichées}} ci-dessous.",
-       "movesubpagetalktext": "La page de discussion correspodnante a $1 {{PLURAL:$1|sous-page|sous-pages}} affichées ci-dessous.",
+       "movesubpagetalktext": "La page de discussion correspondante a $1 {{PLURAL:$1|sous-page|sous-pages}} affichées ci-dessous.",
        "movenosubpage": "Cette page n'a aucune sous-page.",
        "movereason": "Motif :",
        "revertmove": "rétablir",
index a022551..6a81448 100644 (file)
        "newwindow": "(woort uun en nei wönang eeben maaget)",
        "cancel": "Ufbreeg",
        "moredotdotdot": "Muar ...",
-       "morenotlisted": "Detdiar list as ei komplet.",
+       "morenotlisted": "Detdiar list küd ei komplet wees.",
        "mypage": "Sidj",
        "mytalk": "Diskuschuun",
        "anontalk": "Diskuschuun",
        "externaldberror": "Deer läit en fäägel bai jü äkstärn autentifisiiring for, unti dü möist din äkstärn brükerkonto äi aktualisiire.",
        "login": "Uunmelde",
        "nav-login-createaccount": "Melde di uun of skriiw di iin",
-       "userlogin": "Melde di uun of skriiw di iin",
-       "userloginnocreate": "Uunmelde",
        "logout": "Ufmelde",
        "userlogout": "Ufmelde",
        "notloggedin": "Ei uunmeldet",
        "userlogin-noaccount": "Dü heest noch nian brükerkonto ?",
        "userlogin-joinproject": "Bi {{SITENAME}} mämaage",
-       "nologin": "Dü heest nian brükerkonto? $1.",
-       "nologinlink": "Nei brükerkonto iinracht",
        "createaccount": "Brükerkonto iinracht",
-       "gotaccount": "Dü hääst ål en brükerkonto? '''$1'''.",
-       "gotaccountlink": "Uunmelde",
-       "userlogin-resetlink": "Heest dü din login dooten ferjiden?",
        "userlogin-resetpassword-link": "Paaswurd ferjiden?",
        "userlogin-helplink2": "Halep bi't uunmeldin",
        "userlogin-loggedin": "Du beest al üs {{GENDER:$1|$1}} uunmeldet.\nBrük det formulaar diar oner, am di mä en öödern nööm uuntumeldin.",
        "createacct-another-email-ph": "E-Mail-adres uundu",
        "createaccountmail": "E-mail tu detdiar adres ferschüür mä en tidjwiis tufelag paaswurd",
        "createacct-realname": "Rocht nööm (optional)",
-       "createaccountreason": "Grünj:",
        "createacct-reason": "Grünj",
        "createacct-reason-ph": "Huaram dü en ööder brükerkonto iinrachtst",
        "createacct-submit": "Din brükerkonto iinracht",
        "nocookieslogin": "{{SITENAME}} brükt cookies för't uunmeldin faan brükern.\nDü heest cookies deaktiwiaret.\nWees so gud an aktiwiare jo uun dan browser, an do ferschük det noch ans.",
        "nocookiesfornew": "Det brükerkonto as ei iinracht wurden, auer wi ei witj, huar a dooten faan kem.\nÜüb dan kompjuuter skel cookies aktiwiaret wees. Do rep detheer sidj noch ans nei ap.",
        "noname": "Dü skel en rochten brükernööm uundu.",
-       "loginsuccesstitle": "Uunmeldin hää loket.",
+       "loginsuccesstitle": "Uunmeldet",
        "loginsuccess": "'''Dü beest nü üs „$1“ bi {{SITENAME}} uunmeldet.'''",
-       "nosuchuser": "Di brükernööm „$1“ jaft at ei. Aachte üüb det skriiwwiis (an uk üüb grat- an letjskriiwang), an do [[Special:CreateAccount|melde di nei uun]].",
+       "nosuchuser": "Di brükernööm „$1“ jaft at ei. Aachte üüb det skriiwwiis (an uk üüb grat- an letjskriiwang), of [[Special:CreateAccount|melde di nei uun]].",
        "nosuchusershort": "Diar as nään brüker mä di nööm \"$1\".\nHeest dü ham uk rocht skrewen?",
        "nouserspecified": "Dü skel en brükernööm uundu.",
        "login-userblocked": "Didiar brüker as speret wurden. Hi mut ham ei uunmelde.",
        "eauthentsent": "Diar as en e-mail tu det uunjiwen adres schüürd wurden.\n\nIar en e-mail faan ööder brükern auer det e-mail-funktjuun uunnimen wurd koon, skal seeker steld wurd, dat det e-mail-adres uk würelk tu di brüker hiart. Wees so gud an befulge jo uunwisangen uun det e-mail, wat dü jüst füngen heest.",
        "throttled-mailpassword": "Diar as uun a leetst {{PLURAL:$1|stünj|$1 stünj}} al ans am en nei paaswurd uunfraaget wurden. Am dat diar nään masbrük mä drewen woort, koon bluas {{PLURAL:$1|iansis per stünj|iansis per $1 stünj}} am en nei paaswurd uunfraaget wurd.",
        "mailerror": "Fäägel bai dåt siinjen foon e e-mail: $1",
-       "acct_creation_throttle_hit": "Beschükern faan detheer wiki mä din IP-adres haa di leetst dai {{PLURAL:$1|1 brükerkonto|$1 brükerkontos}} iinracht. Muar san ei tuläät.\n\nBeschükern mä detdiar IP-adres kön daalang nian brükerkontos muar iinracht.",
+       "acct_creation_throttle_hit": "Beschükern faan detheer wiki mä din IP-adres haa a leetst $2 {{PLURAL:$1|1 brükerkonto|$1 brükerkontos}} iinracht. Muar san ei tuläät.\n\nBeschükern mä detdiar IP-adres kön daalang nian brükerkontos muar iinracht.",
        "emailauthenticated": "Din e-mail-adres as di $2 am a klook $3 gudkäänd wurden.",
        "emailnotauthenticated": "Din e-mail-adres as noch ei gudkäänd. Jodiar e-mail-funktjuunen kön iarst brükt wurd, wan det adres gudkäänd wurden as.",
        "noemailprefs": "Du en e-mail-adres uun din iinstelangen iin, amdat dü jodiar funktjuunen brük könst.",
        "botpasswords-insert-failed": "Di bot-nööm \"$1\" küd ei apnimen wurd. Ferlicht as hi al diar?",
        "botpasswords-update-failed": "Di bot-nööm \"$1\" küd ei apnimen wurd. As hi stregen wurden?",
        "botpasswords-created-title": "Bot-paaswurd as iinracht wurden.",
-       "botpasswords-created-body": "Det bot-paaswurd \"$1\" as iinracht wurden an uun funktjuun.",
+       "botpasswords-created-body": "Det bot-paaswurd för di bot \"$1\" faan di brüker \"$2\" as iinracht wurden.",
        "botpasswords-updated-title": "Bot-paaswurd as aktualisiaret wurden.",
-       "botpasswords-updated-body": "Det bot-paaswurd \"$1\" as aktualisiaret wurden an uun funktjuun.",
+       "botpasswords-updated-body": "Det bot-paaswurd för di bot \"$1\" faan di brüker \"$2\" as aktualisiaret wurden.",
        "botpasswords-deleted-title": "Bot-paaswurd as stregen wurden.",
-       "botpasswords-deleted-body": "Det bot-paaswurd \"$1\" as stregen wurden.",
+       "botpasswords-deleted-body": "Det bot-paaswurd för di bot \"$1\" faan di brüker \"$2\" as stregen wurden.",
        "resetpass_forbidden": "Det paaswurd koon ei feranert wurd.",
        "resetpass-no-info": "Dü skel di uunmelde, am üüb det sidj tutugripen.",
        "resetpass-submit-loggedin": "Paaswurd feranre",
        "minoredit": "Det as man en letj feranrang",
        "watchthis": "Detdiar sidj uun't uug behual",
        "savearticle": "Sidj seekre",
+       "savechanges": "Sidj seekre",
+       "publishpage": "Sidj seekre",
+       "publishchanges": "Sidj seekre",
        "preview": "Iarst ans luke",
        "showpreview": "Iarst ans luke",
        "showdiff": "Feranrangen wise",
        "tooltip-ca-nstab-category": "Kategoriisidj uunluke",
        "tooltip-minoredit": "Detdiar feranrang üs letj kääntiakne.",
        "tooltip-save": "Feranrangen seekre",
+       "tooltip-publish": "Din feranrangen seekre",
        "tooltip-preview": "Föörskau faan feranrangen üüb detdiar sidj. Iarst noch ans luke, iar dü det sidj seekerst!",
        "tooltip-diff": "Feranrangen bi a tekst wise",
        "tooltip-compareselectedversions": "Ferskeel tesken tau werjuunen faan detdiar sidj uunwise.",
        "logentry-managetags-deactivate": "$1 {{GENDER:$2|hää}} det markiarang \"$4\" för't bewerkin faan brükern of bots de-aktiwiaret.",
        "log-name-tag": "Markiarang-logbuk",
        "rightsnone": "(-)",
-       "revdelete-summary": "tuhuupefootings-komäntoor",
        "feedback-adding": "Komentaar woort tu det sidj skrewen ...",
        "feedback-back": "Turag",
        "feedback-bugcheck": "Gud! Luke noch ans efter, of det ei ferlicht en [$1 bekäänden feeler] as.",
index e0c1524..59593c6 100644 (file)
        "page_last": "אחרון",
        "histlegend": "בחירת גרסאות להשוואה: {{GENDER:|בחר|בחרי|בחרו}} את הגרסאות ש{{GENDER:|ברצונך|ברצונך|ברצונכם}} להשוות ולאחר מכן {{GENDER:|הקש|הקישי|הקישו}} על Enter או {{GENDER:|לחץ|לחצי|לחצו}} על הכפתור שלמטה.<br />\nמקרא: <strong>({{int:cur}})</strong> = השוואה עם הגרסה הנוכחית, <strong>({{int:last}})</strong> = השוואה עם הגרסה הקודמת, <strong>{{int:minoreditletter}}</strong> = עריכה משנית.",
        "history-fieldset-title": "חיפוש בהיסטוריית הדף",
-       "history-show-deleted": "ער×\99×\9bות מוסתרות בלבד",
+       "history-show-deleted": "×\92רס×\90ות מוסתרות בלבד",
        "histfirst": "הישנות ביותר",
        "histlast": "החדשות ביותר",
        "historysize": "({{PLURAL:$1|בייט אחד|$1 בייטים}})",
index 6197bae..9773e67 100644 (file)
        "datedefault": "Nemoj postaviti",
        "prefs-labs": "Labs mogućnosti",
        "prefs-user-pages": "Suradničke stranice",
-       "prefs-personal": "Podaci o suradniku",
+       "prefs-personal": "Podatci o suradniku",
        "prefs-rc": "Nedavne promjene",
        "prefs-watchlist": "Praćene stranice",
        "prefs-editwatchlist": "Uredi popis praćenja",
diff --git a/languages/i18n/hu-formal.json b/languages/i18n/hu-formal.json
new file mode 100644 (file)
index 0000000..c098a2f
--- /dev/null
@@ -0,0 +1,255 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Dani",
+                       "Futhark1988",
+                       "Máté",
+                       "R-Joe",
+                       "Tacsipacsi"
+               ]
+       },
+       "tog-underline": "Hivatkozások aláhúzása:",
+       "tog-hideminor": "Apró változtatások elrejtése a friss változtatások lapon",
+       "tog-hidepatrolled": "Az ellenőrzött szerkesztések elrejtése a friss változtatások lapon",
+       "tog-newpageshidepatrolled": "Ellenőrzött lapok elrejtése az új lapok listájáról",
+       "tog-extendwatchlist": "A figyelőlistán az összes változtatás látszódjon, ne csak az utolsó",
+       "tog-usenewrc": "Szerkesztések csoportosítása oldal szerint a friss változtatásokban és a figyelőlistán",
+       "tog-numberheadings": "Fejezetcímek automatikus számozása",
+       "tog-showtoolbar": "Szerkesztőeszközsor megjelenítése",
+       "tog-editondblclick": "A lapok szerkesztése dupla kattintásra",
+       "tog-editsectiononrightclick": "Szakaszok szerkesztése a szakaszcímre való jobb kattintással",
+       "tog-watchcreations": "Az Ön által létrehozott lapok és feltöltött fájlok felvétele a figyelőlistára",
+       "tog-watchdefault": "Az Ön által szerkesztett lapok és fájlok felvétele a figyelőlistájára",
+       "tog-watchmoves": "Az Ön által átnevezett lapok és fájlok felvétele a figyelőlistájára",
+       "tog-watchdeletion": "Az Ön által törölt lapok és fájlok hozzáadása a figyelőlistájához",
+       "tog-minordefault": "Alapértelmezésben az összes szerkesztése legyen aprónak jelölve",
+       "tog-previewontop": "Előnézet megjelenítése a szerkesztőablak előtt",
+       "tog-previewonfirst": "Előnézet első szerkesztésnél",
+       "tog-enotifwatchlistpages": "Értesítés küldése e-mailben, ha egy Ön által figyelt lap vagy fájl megváltozik",
+       "tog-enotifusertalkpages": "Értesítés e-mailben, ha megváltozik a vitalapja",
+       "tog-enotifminoredits": "Értesítés küldése e-mailben akkor is, ha egy Ön által figyelt lap vagy fájl megváltozik (apró változtatás)",
+       "tog-enotifrevealaddr": "Jelenítse meg az e-mail címét a figyelmeztető e-mailekben",
+       "tog-shownumberswatching": "A lapot figyelő szerkesztők számának megjelenítése",
+       "tog-oldsig": "A jelenlegi aláírás:",
+       "tog-fancysig": "Az aláírás wikiszöveg (nem lesz automatikusan hivatkozásba rakva)",
+       "tog-uselivepreview": "Élő előnézet használata (kísérleti)",
+       "tog-forceeditsummary": "Figyelmeztetés megjelenítése, ha nem ad meg szerkesztési összefoglalót",
+       "tog-watchlisthideown": "Az Ön szerkesztéseinek elrejtése a figyelőlistáról",
+       "tog-watchlisthidebots": "Robotok szerkesztéseinek elrejtése",
+       "tog-watchlisthideminor": "Apró változtatások elrejtése",
+       "tog-watchlisthideliu": "Bejelentkezett szerkesztők módosításainak elrejtése a figyelőlistáról",
+       "tog-watchlisthideanons": "Névtelen szerkesztések elrejtése",
+       "tog-watchlisthidepatrolled": "Az ellenőrzött szerkesztések elrejtése",
+       "tog-ccmeonemails": "A másoknak küldött e-mailjeiről kapon Ön is másolatot",
+       "tog-diffonly": "Ne mutassa a lap tartalmát a lapváltozatok közötti eltérések megtekintésekor",
+       "tog-showhiddencats": "Rejtett kategóriák megjelenítése",
+       "tog-norollbackdiff": "Ne jelenjenek meg az eltérések visszaállítás után",
+       "underline-always": "mindig",
+       "underline-never": "soha",
+       "underline-default": "Felület és böngésző alapértelmezése szerint",
+       "editfont-style": "A szerkesztőterület betűtípusa:",
+       "editfont-default": "a böngésző alapértelmezett beállítása",
+       "editfont-monospace": "fix szélességű betűtípus",
+       "editfont-sansserif": "talpatlan (sans-serif) betűtípus",
+       "editfont-serif": "talpas (serif) betűtípus",
+       "sunday": "vasárnap",
+       "monday": "hétfő",
+       "tuesday": "kedd",
+       "wednesday": "szerda",
+       "thursday": "csütörtök",
+       "friday": "péntek",
+       "saturday": "szombat",
+       "sun": "vas",
+       "mon": "hét",
+       "tue": "kedd",
+       "wed": "sze",
+       "thu": "csü",
+       "fri": "pén",
+       "sat": "szo",
+       "january": "január",
+       "february": "február",
+       "march": "március",
+       "april": "április",
+       "may_long": "május",
+       "june": "június",
+       "july": "július",
+       "august": "augusztus",
+       "september": "szeptember",
+       "october": "október",
+       "november": "november",
+       "december": "december",
+       "january-gen": "január",
+       "february-gen": "február",
+       "march-gen": "március",
+       "april-gen": "április",
+       "may-gen": "május",
+       "june-gen": "június",
+       "july-gen": "július",
+       "august-gen": "augusztus",
+       "september-gen": "szeptember",
+       "october-gen": "október",
+       "november-gen": "november",
+       "december-gen": "december",
+       "jan": "jan",
+       "feb": "febr",
+       "mar": "márc",
+       "apr": "ápr",
+       "may": "máj",
+       "jun": "jún",
+       "jul": "júl",
+       "aug": "aug",
+       "sep": "szept",
+       "oct": "okt",
+       "nov": "nov",
+       "dec": "dec",
+       "pagecategories": "{{PLURAL:$1|Kategória}}",
+       "category_header": "A(z) „$1” kategóriába tartozó lapok",
+       "subcategories": "Alkategóriák",
+       "category-media-header": "A(z) „$1” kategóriába tartozó médiafájlok",
+       "category-empty": "''Ebben a kategóriában pillanatnyilag egyetlen lap vagy médiafájl sem szerepel.''",
+       "hidden-categories": "{{PLURAL:$1|Rejtett kategória}}",
+       "hidden-category-category": "Rejtett kategóriák",
+       "category-subcat-count": "''{{PLURAL:$2|1=Ennek a kategóriának csak egyetlen alkategóriája van.|2=Ez a kategória az alábbi {{PLURAL:$1|1=alkategóriával|2=$1 alkategóriával}} rendelkezik (összesen $2 alkategóriája van).}}''",
+       "category-subcat-count-limited": "Ebben a kategóriában {{PLURAL:$1|egy|$1}} alkategória található.",
+       "category-article-count": "{{PLURAL:$2|A kategóriában csak a következő lap található.|A következő $1 lap található a kategóriában, összesen $2 lapból.}}",
+       "category-article-count-limited": "Ebben a kategóriában a következő {{PLURAL:$1|lap|$1 lap}} található:",
+       "category-file-count": "{{PLURAL:$2|Csak a következő fájl található ebben a kategóriában.|Az összesen $2 fájlból a következő $1-t listázza ez a kategórialap, a többi a további oldalakon található.}}",
+       "category-file-count-limited": "Ebben a kategóriában a következő {{PLURAL:$1|lap|$1 lap}} található:",
+       "listingcontinuesabbrev": "folyt.",
+       "index-category": "Indexelt lapok",
+       "noindex-category": "Nem indexelt lapok",
+       "broken-file-category": "Hibás fájlhivatkozásokat tartalmazó lapok",
+       "about": "Névjegy",
+       "article": "Szócikk",
+       "newwindow": "(új ablakban nyílik meg)",
+       "cancel": "Mégse",
+       "moredotdotdot": "Tovább…",
+       "mypage": "‎Lapom",
+       "mytalk": "Vitalap",
+       "anontalk": "Az IP-címhez tartozó vitalap",
+       "navigation": "Navigáció",
+       "and": "&#32;és",
+       "qbfind": "Keresés",
+       "qbbrowse": "Böngészés",
+       "qbedit": "Szerkesztés",
+       "qbpageoptions": "Lapbeállítások",
+       "qbmyoptions": "Lapjai",
+       "faq": "GyIK",
+       "faqpage": "Project:GyIK",
+       "actions": "Műveletek",
+       "namespaces": "Névterek",
+       "variants": "Változatok",
+       "errorpagetitle": "Hiba",
+       "returnto": "Vissza a(z) $1 laphoz.",
+       "tagline": "A {{SITENAME}} wikiből",
+       "help": "Segítség",
+       "search": "Keresés",
+       "searchbutton": "Keresés",
+       "go": "Menjen",
+       "searcharticle": "Menjen",
+       "history": "Laptörténet",
+       "history_short": "Laptörténet",
+       "updatedmarker": "az utolsó látogatása óta frissítették",
+       "printableversion": "Nyomtatható változat",
+       "permalink": "Hivatkozás erre a változatra",
+       "print": "Nyomtatás",
+       "view": "Olvasás",
+       "edit": "Szerkesztés",
+       "create": "Létrehozás",
+       "editthispage": "Lap szerkesztése",
+       "create-this-page": "Oldal létrehozása",
+       "delete": "Törlés",
+       "deletethispage": "Lap törlése",
+       "undelete_short": "{{PLURAL:$1|Egy|$1}} szerkesztés helyreállítása",
+       "viewdeleted_short": "{{PLURAL:$1|Egy|$1}} törölt szerkesztés megtekintése",
+       "protect": "Lapvédelem",
+       "protect_change": "módosítás",
+       "protectthispage": "Lapvédelem",
+       "unprotect": "Védelem módosítása",
+       "unprotectthispage": "Védelem módosítása",
+       "newpage": "Új lap",
+       "talkpage": "A lappal kapcsolatos megbeszélés",
+       "talkpagelinktext": "vitalap",
+       "specialpage": "Speciális lap",
+       "personaltools": "Személyes eszközök",
+       "articlepage": "Szócikk megtekintése",
+       "talk": "Vitalap",
+       "views": "Nézetek",
+       "toolbox": "Eszközök",
+       "userpage": "Felhasználó lapjának megtekintése",
+       "projectpage": "Projektlap megtekintése",
+       "imagepage": "A fájl leírólapjának megtekintése",
+       "mediawikipage": "Üzenetlap megtekintése",
+       "templatepage": "Sablon lapjának megtekintése",
+       "viewhelppage": "Súgólap megtekintése",
+       "categorypage": "Kategórialap megtekintése",
+       "viewtalkpage": "Beszélgetés megtekintése",
+       "otherlanguages": "Más nyelveken",
+       "redirectedfrom": "($1 szócikkből átirányítva)",
+       "redirectpagesub": "Átirányító lap",
+       "lastmodifiedat": "A lap utolsó módosítása: $1, $2",
+       "viewcount": "Ezt a lapot {{PLURAL:$1|egy|$1}} alkalommal keresték fel.",
+       "protectedpage": "Védett lap",
+       "jumpto": "Ugrás:",
+       "jumptonavigation": "navigáció",
+       "jumptosearch": "keresés",
+       "view-pool-error": "Sajnos a szerverek jelen pillanatban túl vannak terhelve, mert\ntúl sok felhasználó próbálta megtekinteni ezt az oldalt.\nKérjük, várjon egy kicsit, mielőtt újrapróbálkozna a lap megtekintésével!\n\n$1",
+       "pool-timeout": "Letelt a zárolás feloldására szánt várakozási idő",
+       "pool-queuefull": "A pool sor megtelt",
+       "pool-errorunknown": "Ismeretlen hiba",
+       "aboutsite": "A {{SITENAME}} wikiről",
+       "aboutpage": "Project:Rólunk",
+       "copyright": "A tartalom további jelölés hiányában a(z) $1 feltételei szerint használható fel.",
+       "copyrightpage": "{{ns:project}}:Szerzői jogok",
+       "currentevents": "Aktuális események",
+       "currentevents-url": "Project:Friss események",
+       "disclaimers": "Jogi nyilatkozat",
+       "disclaimerpage": "Project:Jogi nyilatkozat",
+       "edithelp": "Szerkesztési segítség",
+       "mainpage": "Kezdőlap",
+       "mainpage-description": "Kezdőlap",
+       "policy-url": "Project:Irányelvek",
+       "portal": "Közösségi portál",
+       "portal-url": "Project:Közösségi portál",
+       "privacy": "Adatvédelmi irányelvek",
+       "privacypage": "Project:Adatvédelmi irányelvek",
+       "badaccess": "Engedélyezési hiba",
+       "badaccess-group0": "Ezt a tevékenységet Ön nem végezheti el.",
+       "badaccess-groups": "Ezt a tevékenységet csak a(z) $1 {{PLURAL:$2|csoportba|csoportok valamelyikébe}} tartozó felhasználó végezheti el.",
+       "versionrequired": "A MediaWiki $1 verziója szükséges",
+       "versionrequiredtext": "A lap használatához a MediaWiki $1-s verziójára van szükség.\nTovábbi információkat a [[Special:Version|verzióinformációs lapon]] találhat.",
+       "ok": "OK",
+       "retrievedfrom": "A lap eredeti címe: „$1”",
+       "youhavenewmessages": "Új üzenet várja $1! (Az üzenetet $2.)",
+       "youhavenewmessagesmulti": "Új üzenetet várja a(z) $1 wikin",
+       "editsection": "szerkesztés",
+       "editold": "szerkesztés",
+       "viewsourceold": "lapforrás",
+       "editlink": "szerkesztés",
+       "viewsourcelink": "forráskód megtekintése",
+       "editsectionhint": "Szakasz szerkesztése: $1",
+       "toc": "Tartalomjegyzék",
+       "showtoc": "megjelenítés",
+       "hidetoc": "elrejtés",
+       "collapsible-collapse": "becsuk",
+       "collapsible-expand": "kinyit",
+       "nstab-main": "Lap",
+       "nosuchactiontext": "Az URL-ben megadott műveletet érvénytelen.\nValószínűleg elgépelte, hibás hivatkozásra kattintott, vagy a\na(z) {{SITENAME}} által használt szoftver hibája is lehet.",
+       "nospecialpagetext": "<strong>Érvénytelen speciális lapot akart megtekinteni.</strong>\n\nAz érvényes speciális lapok listáját a [[Special:SpecialPages|Speciális lapok]] oldalon találja meg.",
+       "enterlockreason": "Adja meg a lezárás okát, valamint egy becslést, hogy mikor kerül a lezárás feloldásra",
+       "exception-nologin-text": "Ezen lap vagy művelet eléréséhez, kérjük, [[Special:Userlogin|jelentkezzen be]].",
+       "login-security": "Személyazonosságának igazolása",
+       "userlogin-reauth": "Újra be kell jelentkeznie, hogy igazolja, ön $1.",
+       "createacct-loginerror": "A fiók sikeresen létrejött, de nem tudott automatikusan bejelentkezni. Kérjük, [[Special:UserLogin|jelentkezzen be manuálisan]]!",
+       "nosuchuser": "Nem létezik „$1” nevű szerkesztő.\nEllenőrizze, hogy helyesen írta-e be, vagy [[Special:CreateAccount|hozzon létre egy új fiókot]].",
+       "changeemail-newemail-help": "Ha el akarja távolítani az e-mail-címét, ezt a mezőt üresen kell hagynia. Ha eltávolítja az e-mail-címét, nem fogja tudni visszaállítani a jelszavát, és nem fog tudni e-maileket fogadni erről a wikiről.",
+       "changeemail-nochange": "Kérjük, adjon meg egy másik új e-mail-címet.",
+       "noarticletext": "Ez a lap jelenleg nem tartalmaz szöveget.\n[[Special:Search/{{PAGENAME}}|Rákereshet erre a címszóra]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} megtekintheti a kapcsolódó naplókat],\nvagy [{{fullurl:{{FULLPAGENAME}}|action=edit}} létrehozhatja a lapot].</span>",
+       "recentchangeslinked-toolbox": "Kapcsolódó változtatások",
+       "upload-form-label-not-own-work-local-generic-local": "Az [[Special:Upload|alapértelmezett feltöltőoldalt]] is kipróbálhatja.",
+       "filehist": "Fájl történet",
+       "newpages": "Új lapok",
+       "changecontentmodel-submit": "Módosít",
+       "specialpages": "Speciális lapok",
+       "sessionprovider-nocookies": "A sütik le lehetnek tiltva. Engedélyezze a sütiket, és próbálja meg újra!"
+}
index 96d872f..6bac1e8 100644 (file)
        "page_last": "ultima",
        "histlegend": "Confronto tra versioni: selezionare le caselle corrispondenti alle versioni desiderate e premere Invio o il pulsante in basso.\n\nLegenda: '''({{int:cur}})''' = differenze con la versione attuale, '''({{int:last}})''' = differenze con la versione precedente, '''{{int:minoreditletter}}''' = modifica minore",
        "history-fieldset-title": "Naviga nella cronologia",
-       "history-show-deleted": "Solo quelli cancellati",
+       "history-show-deleted": "Solo versioni cancellate",
        "histfirst": "prima",
        "histlast": "ultima",
        "historysize": "({{PLURAL:$1|1 byte|$1 byte}})",
index d572833..511e962 100644 (file)
        "prefs-help-prefershttps": "Dës Astellung gëtt déi nächste Kéier wierksam wou Dir Iech ageloggt.",
        "prefswarning-warning": "Dir hutt Ännerunge vun Ären Astellunge gemaach, déi nach net gespäichert goufen.\n\nWann Dir vun dëser Säit erof gitt ouni op \"$1\" ze klicken, da ginn Är Astellungen net aktualiséiert.",
        "prefs-tabs-navigation-hint": "Tipp: Dir kënnt d'Feiler no lénks an no riets benotze fir tëscht den Tabs an der Lëscht vun den Tabs ze navigéieren.",
-       "userrights": "Benotzerrechterverwaltung",
+       "userrights": "Benotzerrechter",
        "userrights-lookup-user": "E Benotzer eraussichen",
        "userrights-user-editname": "Benotzernumm uginn:",
        "editusergroup": "Benotzergruppe lueden",
        "rcfilters-filter-user-experience-level-learner-description": "Aktivitéit vu méi Deeg a méi Ännerunge wéi ''Nei Benotzer'' awer manner wéi ''Erfuere Benotzer''.",
        "rcfilters-filter-user-experience-level-experienced-label": "Erfuere Benotzer.",
        "rcfilters-filter-user-experience-level-experienced-description": "Méi wéi 30 Deeg Aktivitéit a méi wéi 500 Ännerungen.",
+       "rcfilters-filtergroup-reviewstatus": "Status nokucken",
        "rcfilters-filter-patrolled-label": "Nogekuckt",
        "rcfilters-filter-patrolled-description": "Ännerungen déi als nogekuckt markéiert sinn.",
        "rcfilters-filter-unpatrolled-label": "Net nogekuckt",
index ede7e04..4da94e4 100644 (file)
@@ -36,7 +36,7 @@
        "tog-enotifminoredits": "Sió pian-chi̍p mā kià tiān-tsú-phue hōo guá.",
        "tog-enotifrevealaddr": "Hō͘ pat-lâng khoàⁿ ê tio̍h oá ê tiān-chú-phoe tē-chí",
        "tog-shownumberswatching": "Hián-sī tng leh khoàⁿ ê iōng-chiá sò͘-bo̍k",
-       "tog-oldsig": "Chit-má ê chhiam-miâ:",
+       "tog-oldsig": "Lí kì-sêng ê chhiam-miâ:",
        "tog-fancysig": "共我的簽名當做文章文字,(無需要自動連結)",
        "tog-uselivepreview": "Ēng sui khoàⁿ-māi",
        "tog-forceeditsummary": "Pian-chi̍p khài-iàu bô thiⁿ ê sî-chūn, kā goá thê-chhéⁿ",
@@ -51,9 +51,9 @@
        "tog-ccmeonemails": "Kià hō͘ pa̍t-lâng ê email sūn-soà kià copy hō͘ goá",
        "tog-diffonly": "Diff ē-pêng bián hián-sī ia̍h ê loē-iông",
        "tog-showhiddencats": "Hián-sī chhàng khí--lâi ê lūi-pia̍t",
-       "tog-norollbackdiff": "ká tńg-khí liáu bián-koán cheng-chha goā-chē",
+       "tog-norollbackdiff": "Ká tńg--khí liáu-āu mái tián-sī cheng-chha",
        "tog-useeditwarning": "Goá nā iáu-boē pó-chûn siu-kái--ê ia̍h tō thiàu khai, ài kā goá kóng.",
-       "tog-prefershttps": "我登入的時陣愛用安全連線",
+       "tog-prefershttps": "Teng-ji̍p ê sî lóng ēng an-choân liân-chiap",
        "underline-always": "Tiāⁿ-tio̍h",
        "underline-never": "Tiāⁿ-tio̍h mài",
        "underline-default": "Tòe liû-lám-khì ê siat-piān",
        "newwindow": "(ē khui sin thang-á hián-sī)",
        "cancel": "Chhú-siau",
        "moredotdotdot": "Iáu-ū",
-       "morenotlisted": "這毋是完整的表",
+       "morenotlisted": "Chit-ê lia̍t-toaⁿ khó-lêng iáu bô-chiâu",
        "mypage": "Ia̍h",
        "mytalk": "Thó-lūn",
        "anontalk": "Thó-lūn",
        "tagline": "Ùi {{SITENAME}}",
        "help": "Soat-bêng-su",
        "search": "Chhiau-chhoē",
+       "search-ignored-headings": " #<!-- leave this line exactly as it is --> <pre>\n# Tī chhiau-chhōe tang-tiong m̄-chhap ê thâu-pō͘.\n# Tùi che ê piàn-keng ē tòe thâu-pō͘ hông sek-ín ê sî khai-sí chok-iōng.\n# Lí thang lī-ēng khang pian-chi̍p lâi kiông-chè ia̍h-bīn têng-sin sek-ín.\n# Àn-chiàu ē-té ê kù-hoat:\n#   * Só͘-ū tùi \"#\" khai-sí kàu jī-hâng bóe ê jī sī chi̍t-ê chù-kái.\n#   * Só͘-ū hui khang-pe̍h jī-hâng kui--ê tio̍h sī m̄ hông chhap ê tê-bo̍k, hâm tōa-sió-jī chāi-lāi ta̍k hāng lóng sī.\nChham-khó\nHiòng-gōa liân-kiat\nSiong-koan\n #</pre> <!-- leave this line exactly as it is -->",
        "searchbutton": "Chhoē",
        "go": "Lâi-khì",
        "searcharticle": "Lâi-khì",
        "history": "Ia̍h le̍k-sú",
        "history_short": "le̍k-sú",
+       "history_small": "le̍k-sú",
        "updatedmarker": "Téng hoê goá lâi chiah liáu ū kái koè--ê",
        "printableversion": "Ìn-soat pán-pún",
        "permalink": "Éng-kiú liân-kiat",
        "talk": "Thó-lūn",
        "views": "Khoàⁿ",
        "toolbox": "Ke-si",
-       "tool-link-userrights": "Piàn-keng {{GENDER:$1|ēng-chiá}} hun-cho͘",
+       "tool-link-userrights": "Piàn-keng {{GENDER:$1|iōng-chiá}} hun-cho͘",
+       "tool-link-userrights-readonly": "Khòaⁿ {{GENDER:$1|iōng-chiá}} hun-cho͘",
+       "tool-link-emailuser": "Email hō͘ chit ūi {{GENDER:$1|iōng-chiá}}",
        "userpage": "Khoàⁿ iōng-chiá ê Ia̍h",
        "projectpage": "Khoàⁿ sū-kang ia̍h",
        "imagepage": "Khoàⁿ tóng-àn ia̍h",
index 2a96c1e..c6cbc54 100644 (file)
        "page_last": "laatste",
        "histlegend": "Selectie voor verschillen: selecteer de te vergelijken versies en toets ENTER of de knop onderaan.<br />\nVerklaring afkortingen: '''({{int:cur}})''' = verschil met huidige versie, '''({{int:last}})''' = verschil met voorgaande versie, '''{{int:minoreditletter}}''' = kleine wijziging",
        "history-fieldset-title": "Door geschiedenis bladeren",
-       "history-show-deleted": "Alleen verwijderd",
+       "history-show-deleted": "Alleen verwijderde versies",
        "histfirst": "oudste",
        "histlast": "nieuwste",
        "historysize": "({{PLURAL:$1|1 byte|$1 bytes}})",
index d8a1174..89560de 100644 (file)
        "page_last": "zadnja",
        "histlegend": "Izbira primerjave: označite okroglo polje ob redakciji za primerjavo in stisnite enter ali gumb na dnu strani.<br />\nLegenda: '''({{int:cur}})''' = primerjava s trenutno redakcijo, '''({{int:last}})''' = primerjava s prejšnjo redakcijo, '''{{int:minoreditletter}}''' = manjše urejanje.",
        "history-fieldset-title": "Zgodovina poizvedovanj",
-       "history-show-deleted": "Samo izbrisani",
+       "history-show-deleted": "Samo izbrisana redakcija",
        "histfirst": "najstarejše",
        "histlast": "najnovejše",
        "historysize": "({{PLURAL:$1|$1 zlog|$1 zloga|$1 zlogi|$1 zlogov}})",
        "prefs-help-prefershttps": "Nastavitev bo začela veljati ob vaši naslednji prijavi.",
        "prefswarning-warning": "V svojih nastavitvah ste naredili spremembe, ki jih še niste shranili. Če odidete s strani brez da bi kliknili »$1«, vaših nastavitev ne bomo posodobili.",
        "prefs-tabs-navigation-hint": "Namig: Za krmarjenje med zavihki na seznamu zavihkov lahko uporabite levo in desno smerno tipko.",
-       "userrights": "Upravljanje s pravicami uporabnikov",
+       "userrights": "Pravice uporabnika",
        "userrights-lookup-user": "Izberite uporabnika",
        "userrights-user-editname": "Vpišite uporabniško ime:",
        "editusergroup": "Naloži uporabniške skupine",
index ca96c7a..465d6fa 100644 (file)
        "previewnote": "<strong>Не заборавите да је ово само претпреглед.</strong>\nВаше измене још нису сачуване!",
        "continue-editing": "Иди на уређивачки оквир",
        "previewconflict": "Овај преглед осликава како ће текст у текстуалном оквиру изгледати.",
-       "session_fail_preview": "Ð\9dиÑ\81мо Ð¼Ð¾Ð³Ð»Ð¸ Ð´Ð° Ð¾Ð±Ñ\80адимо Ð²Ð°Ñ\88Ñ\83 Ð¸Ð·Ð¼ÐµÐ½Ñ\83 Ð·Ð±Ð¾Ð³ Ð³Ñ\83биÑ\82ка Ð¿Ð¾Ð´Ð°Ñ\82ака Ñ\81еÑ\81иÑ\98е.\n\nÐ\9cожда Ñ\81Ñ\82е Ð¾Ð´Ñ\98авÑ\99ени. <strong>Ð\9fÑ\80овеÑ\80иÑ\82е Ð´Ð° Ð»Ð¸ Ñ\81Ñ\82е Ð¿Ñ\80иÑ\98авÑ\99ен Ð¸ Ð¿Ð¾ÐºÑ\83Ñ\88аÑ\98Ñ\82е Ð¿Ð¾Ð½Ð¾Ð²Ð¾</strong>.\n\nАко и даље не ради, покушајте да се [[Special:UserLogout|одјавите]] и поново пријавите и проверите да ли су на Вашем претраживачу дозвољени колачићи са овог сајта.",
+       "session_fail_preview": "Ð\98звиÑ\9aавамо Ñ\81е! Ð\9dиÑ\81мо Ð¼Ð¾Ð³Ð»Ð¸ Ð´Ð° Ð¾Ð±Ñ\80адимо Ð\92аÑ\88Ñ\83 Ð¸Ð·Ð¼ÐµÐ½Ñ\83 Ð·Ð±Ð¾Ð³ Ð³Ñ\83биÑ\82ка Ð¿Ð¾Ð´Ð°Ñ\82ака Ñ\81еÑ\81иÑ\98е.\n\nÐ\9cожда Ñ\81Ñ\82е Ð¾Ð´Ñ\98авÑ\99ени. <strong>Ð\9fÑ\80овеÑ\80иÑ\82е Ð´Ð° Ð»Ð¸ Ñ\81Ñ\82е Ð¿Ñ\80иÑ\98авÑ\99ен Ð¸ Ð¿Ð¾ÐºÑ\83Ñ\88аÑ\98Ñ\82е Ð¿Ð¾Ð½Ð¾Ð²Ð¾</strong>.\nАко и даље не ради, покушајте да се [[Special:UserLogout|одјавите]] и поново пријавите и проверите да ли су на Вашем претраживачу дозвољени колачићи са овог сајта.",
        "session_fail_preview_html": "Нисмо могли да обрадимо вашу измену због губитка података сесије.\n\n<em>Будући да је на овом викију омогућен унос HTML ознака, преглед је сакривен као мера предострожности против напада преко јаваскрипта.</em>\n\n<strong>Ако сте покушали да направите праву измену, покушајте поново.<strong>\nАко и даље не ради, покушајте да се [[Special:UserLogout|одјавите]] и поново пријавите и проверите да ли Ваш претраживач дозвољава колачиће са овог сајта.",
        "token_suffix_mismatch": "'''Ваша измена је одбачена јер је ваш прегледач убацио знакове интерпункције у новчић уређивања.\nТо се понекад догађа када се користи неисправан посредник.'''",
        "edit_form_incomplete": "<strong>Неки делови обрасца за уређивање нису стигли до сервера. Проверите да ли су ваше измене непромењене и покушајте поново.</strong>",
index c47fb72..6ff1b87 100644 (file)
        "previewnote": "<strong>Ne zaboravite da je ovo samo pretpregled.</strong>\nVaše izmene još nisu sačuvane!",
        "continue-editing": "Idi na uređivački okvir",
        "previewconflict": "Ovaj pregled oslikava kako će tekst u tekstualnom okviru izgledati.",
-       "session_fail_preview": "'''Nismo mogli da obradimo vašu izmenu zbog gubitka podataka sesije.'''\nPokušajte ponovo.\nAko i dalje ne radi, pokušajte da se [[Special:UserLogout|odjavite]] i ponovo prijavite.",
+       "session_fail_preview": "Izvinjavamo se! Nismo mogli da obradimo Vašu izmenu zbog gubitka podataka sesije.\n\nMožda ste odjavljeni. <strong>Proverite da li ste prijavljen i pokušajte ponovo</strong>.\nAko i dalje ne radi, pokušajte da se [[Special:UserLogout|odjavite]] i ponovo prijavite i proverite da li su na Vašem pretraživaču dozvoljeni kolačići sa ovog sajta.",
        "session_fail_preview_html": "'''Nismo mogli da obradimo vašu izmenu zbog gubitka podataka sesije.'''\n\n''Budući da je na ovom vikiju omogućen unos HTML oznaka, pregled je sakriven kao mera predostrožnosti protiv napada preko javaskripta.''\n\n'''Ako ste pokušali da napravite pravu izmenu, pokušajte ponovo.\nAko i dalje ne radi, pokušajte da se [[Special:UserLogout|odjavite]] i ponovo prijavite.'''",
        "token_suffix_mismatch": "'''Vaša izmena je odbačena jer je vaš pregledač ubacio znakove interpunkcije u novčić uređivanja.\nTo se ponekad događa kada se koristi neispravan posrednik.'''",
        "edit_form_incomplete": "<strong>Neki delovi obrasca za uređivanje nisu stigli do servera. Proverite da li su vaše izmene nepromenjene i pokušajte ponovo.</strong>",
index f3acb6e..3af1872 100644 (file)
        "table_pager_limit_label": "Sayfa başına öğe:",
        "table_pager_limit_submit": "Git",
        "table_pager_empty": "Sonuç yok",
-       "autosumm-blank": "Sayfayı boşalttı",
+       "autosumm-blank": "Sayfa boşaltıldı",
        "autosumm-replace": "Sayfa içeriği '$1' ile değiştiriliyor",
        "autoredircomment": "[[$1]] sayfasına yönlendirildi",
        "autosumm-new": "Yeni sayfa: \"$1\"",
index 301fb18..d6eed0a 100644 (file)
        "page_last": "末页",
        "histlegend": "差异选择:选中要对比的版本的单选按钮,按Enter键或下方的按钮。<br />说明:<strong>({{int:cur}})</strong>=与最后版本之间的差异,<strong>({{int:last}})</strong>=与上一版本之间的差异,<strong>{{int:minoreditletter}}</strong>=小编辑。",
        "history-fieldset-title": "浏览历史",
-       "history-show-deleted": "仅显示已删除的版本",
+       "history-show-deleted": "仅限修订版本删除",
        "histfirst": "最旧",
        "histlast": "最新",
        "historysize": "($1字节)",
index 14a610b..1070b98 100644 (file)
@@ -13,6 +13,7 @@
         * @cfg {string} [separator='|'] Value separator for 'string_options' groups
         * @cfg {boolean} [active] Group is active
         * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
+        * @cfg {Object} [conflicts] Defines the conflicts for this filter group
         */
        mw.rcfilters.dm.FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
                config = config || {};
@@ -29,6 +30,8 @@
                this.active = !!config.active;
                this.fullCoverage = !!config.fullCoverage;
 
+               this.conflicts = config.conflicts || {};
+
                this.aggregate( { update: 'filterItemUpdate' } );
                this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
        };
                return this.name;
        };
 
+       /**
+        * Get the conflicts associated with the entire group.
+        * Conflict object is set up by filter name keys and conflict
+        * definition. For example:
+        * [
+        *              {
+        *                      filterName: {
+        *                              filter: filterName,
+        *                              group: group1
+        *                      }
+        *              },
+        *              {
+        *                      filterName2: {
+        *                              filter: filterName2,
+        *                              group: group2
+        *                      }
+        *              }
+        * ]
+        * @return {Object} Conflict definition
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.getConflicts = function () {
+               return this.conflicts;
+       };
+
+       /**
+        * Set conflicts for this group. See #getConflicts for the expected
+        * structure of the definition.
+        *
+        * @param {Object} conflicts Conflicts for this group
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.setConflicts = function ( conflicts ) {
+               this.conflicts = conflicts;
+       };
+
+       /**
+        * Check whether this item has a potential conflict with the given item
+        *
+        * This checks whether the given item is in the list of conflicts of
+        * the current item, but makes no judgment about whether the conflict
+        * is currently at play (either one of the items may not be selected)
+        *
+        * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
+        * @return {boolean} This item has a conflict with the given item
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.existsInConflicts = function ( filterItem ) {
+               return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
+       };
+
        /**
         * Check whether there are any items selected
         *
        mw.rcfilters.dm.FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
                var selectedItems = this.getSelectedItems( filterItem );
 
-               return selectedItems.length > 0 && selectedItems.every( function ( selectedFilter ) {
-                       return selectedFilter.existsInConflicts( filterItem );
-               } );
+               return selectedItems.length > 0 &&
+                       (
+                               // The group as a whole is in conflict with this item
+                               this.existsInConflicts( filterItem ) ||
+                               // All selected items are in conflict individually
+                               selectedItems.every( function ( selectedFilter ) {
+                                       return selectedFilter.existsInConflicts( filterItem );
+                               } )
+                       );
        };
 
        /**
        mw.rcfilters.dm.FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
                var selectedItems = this.getSelectedItems( filterItem );
 
-               return selectedItems.length > 0 && selectedItems.some( function ( selectedFilter ) {
-                       return selectedFilter.existsInConflicts( filterItem );
-               } );
+               return selectedItems.length > 0 && (
+                       // The group as a whole is in conflict with this item
+                       this.existsInConflicts( filterItem ) ||
+                       // Any selected items are in conflict individually
+                       selectedItems.some( function ( selectedFilter ) {
+                               return selectedFilter.existsInConflicts( filterItem );
+                       } )
+               );
+       };
+
+       /**
+        * Get the parameter representation from this group
+        *
+        * @return {Object} Parameter representation
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.getParamRepresentation = function () {
+               var i, values,
+                       result = {},
+                       filterItems = this.getItems();
+
+               if ( this.getType() === 'send_unselected_if_any' ) {
+                       // First, check if any of the items are selected at all.
+                       // If none is selected, we're treating it as if they are
+                       // all false
+
+                       // Go over the items and define the correct values
+                       for ( i = 0; i < filterItems.length; i++ ) {
+                               result[ filterItems[ i ].getParamName() ] = this.areAnySelected() ?
+                                       Number( !filterItems[ i ].isSelected() ) : 0;
+                       }
+
+               } else if ( this.getType() === 'string_options' ) {
+                       values = [];
+                       for ( i = 0; i < filterItems.length; i++ ) {
+                               if ( filterItems[ i ].isSelected() ) {
+                                       values.push( filterItems[ i ].getParamName() );
+                               }
+                       }
+
+                       result[ this.getName() ] = ( values.length === filterItems.length ) ?
+                               'all' : values.join( this.getSeparator() );
+               }
+
+               return result;
        };
 
        /**
                return this.type;
        };
 
+       /**
+        * Get the prefix used for the filter names inside this group
+        *
+        * @return {string} Group prefix
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.getNamePrefix = function () {
+               return this.getName() + '__';
+       };
+
        /**
         * Get group's title
         *
index 0df34f8..852b810 100644 (file)
@@ -5,7 +5,7 @@
         * @mixins OO.EventEmitter
         *
         * @constructor
-        * @param {string} name Filter name
+        * @param {string} param Filter param name
         * @param {mw.rcfilters.dm.FilterGroup} groupModel Filter group model
         * @param {Object} config Configuration object
         * @cfg {string} [group] The group this item belongs to
         *  selected, makes inactive.
         * @cfg {boolean} [selected] The item is selected
         * @cfg {string[]} [subset] Defining the names of filters that are a subset of this filter
-        * @cfg {string[]} [conflictsWith] Defining the names of filters that conflict with this item
+        * @cfg {Object} [conflicts] Defines the conflicts for this filter
         * @cfg {string} [cssClass] The class identifying the results that match this filter
         */
-       mw.rcfilters.dm.FilterItem = function MwRcfiltersDmFilterItem( name, groupModel, config ) {
+       mw.rcfilters.dm.FilterItem = function MwRcfiltersDmFilterItem( param, groupModel, config ) {
                config = config || {};
 
                // Mixin constructor
                OO.EventEmitter.call( this );
 
-               this.name = name;
+               this.param = param;
                this.groupModel = groupModel;
+               this.name = this.groupModel.getNamePrefix() + param;
 
                this.label = config.label || this.name;
                this.description = config.description;
@@ -34,7 +35,7 @@
 
                // Interaction definitions
                this.subset = config.subset || [];
-               this.conflicts = config.conflicts || [];
+               this.conflicts = config.conflicts || {};
                this.superset = [];
 
                // Interaction states
                return this.name;
        };
 
+       /**
+        * Get the param name or value of this filter
+        *
+        * @return {string} Filter param name
+        */
+       mw.rcfilters.dm.FilterItem.prototype.getParamName = function () {
+               return this.param;
+       };
+
        /**
         * Get the model of the group this filter belongs to
         *
        /**
         * Get filter conflicts
         *
-        * @return {string[]} Filter conflicts
+        * Conflict object is set up by filter name keys and conflict
+        * definition. For example:
+        *              {
+        *                      filterName: {
+        *                              filter: filterName,
+        *                              group: group1
+        *                      }
+        *                      filterName2: {
+        *                              filter: filterName2,
+        *                              group: group2
+        *                      }
+        *              }
+        *
+        * @return {Object} Filter conflicts
         */
        mw.rcfilters.dm.FilterItem.prototype.getConflicts = function () {
-               return this.conflicts;
+               return $.extend( {}, this.conflicts, this.getGroupModel().getConflicts() );
        };
 
        /**
-        * Set filter conflicts
+        * Set conflicts for this filter. See #getConflicts for the expected
+        * structure of the definition.
         *
-        * @param {string[]} conflicts Filter conflicts
+        * @param {Object} conflicts Conflicts for this filter
         */
        mw.rcfilters.dm.FilterItem.prototype.setConflicts = function ( conflicts ) {
-               this.conflicts = conflicts || [];
+               this.conflicts = conflicts || {};
        };
 
        /**
                this.superset = superset || [];
        };
 
+       /**
+        * Set filter subset
+        *
+        * @param {string[]} subset Filter subset
+        */
+       mw.rcfilters.dm.FilterItem.prototype.setSubset = function ( subset ) {
+               this.subset = subset || [];
+       };
+
        /**
         * Check whether a filter exists in the subset list for this filter
         *
         * @return {boolean} This item has a conflict with the given item
         */
        mw.rcfilters.dm.FilterItem.prototype.existsInConflicts = function ( filterItem ) {
-               return this.conflicts.indexOf( filterItem.getName() ) > -1;
+               return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
        };
 
        /**
index 3bb7716..cf51424 100644 (file)
@@ -16,6 +16,7 @@
                this.defaultParams = {};
                this.defaultFiltersEmpty = null;
                this.highlightEnabled = false;
+               this.parameterMap = {};
 
                // Events
                this.aggregate( { update: 'filterItemUpdate' } );
         * @param {Array} filters Filter group definition
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) {
-               var i, filterItem, selectedFilterNames,
+               var i, filterItem, selectedFilterNames, filterConflictResult, groupConflictResult, subsetNames,
                        model = this,
                        items = [],
+                       supersetMap = {},
+                       groupConflictMap = {},
+                       filterConflictMap = {},
                        addArrayElementsUnique = function ( arr, elements ) {
                                elements = Array.isArray( elements ) ? elements : [ elements ];
 
 
                                return arr;
                        },
-                       conflictMap = {},
-                       supersetMap = {};
+                       expandConflictDefinitions = function ( obj ) {
+                               var result = {};
+
+                               $.each( obj, function ( key, conflicts ) {
+                                       var filterName,
+                                               adjustedConflicts = {};
+
+                                       conflicts.forEach( function ( conflict ) {
+                                               if ( conflict.filter ) {
+                                                       filterName = model.groups[ conflict.group ].getNamePrefix() + conflict.filter;
+
+                                                       // Rename
+                                                       adjustedConflicts[ filterName ] = $.extend(
+                                                               {},
+                                                               conflict,
+                                                               { filter: filterName }
+                                                       );
+                                               } else {
+                                                       // This conflict is for an entire group. Split it up to
+                                                       // represent each filter
+
+                                                       // Get the relevant group items
+                                                       model.groups[ conflict.group ].getItems().forEach( function ( groupItem ) {
+                                                               // Rebuild the conflict
+                                                               adjustedConflicts[ groupItem.getName() ] = $.extend(
+                                                                       {},
+                                                                       conflict,
+                                                                       { filter: groupItem.getName() }
+                                                               );
+                                                       } );
+                                               }
+                                       } );
+
+                                       result[ key ] = adjustedConflicts;
+                               } );
+
+                               return result;
+                       };
 
                // Reset
                this.clearItems();
                                } );
                        }
 
+                       if ( data.conflicts ) {
+                               groupConflictMap[ group ] = data.conflicts;
+                       }
+
                        selectedFilterNames = [];
                        for ( i = 0; i < data.filters.length; i++ ) {
                                data.filters[ i ].subset = data.filters[ i ].subset || [];
                                        group: group,
                                        label: mw.msg( data.filters[ i ].label ),
                                        description: mw.msg( data.filters[ i ].description ),
-                                       subset: data.filters[ i ].subset,
                                        cssClass: data.filters[ i ].cssClass
                                } );
 
-                               // For convenience, we should store each filter's "supersets" -- these are
-                               // the filters that have that item in their subset list. This will just
-                               // make it easier to go through whether the item has any other items
-                               // that affect it (and are selected) at any given time
                                if ( data.filters[ i ].subset ) {
+                                       subsetNames = [];
                                        data.filters[ i ].subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func
-                                               supersetMap[ subsetFilterName ] = supersetMap[ subsetFilterName ] || [];
+                                               var subsetName = model.groups[ group ].getNamePrefix() + subsetFilterName;
+                                               // For convenience, we should store each filter's "supersets" -- these are
+                                               // the filters that have that item in their subset list. This will just
+                                               // make it easier to go through whether the item has any other items
+                                               // that affect it (and are selected) at any given time
+                                               supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
                                                addArrayElementsUnique(
-                                                       supersetMap[ subsetFilterName ],
+                                                       supersetMap[ subsetName ],
                                                        filterItem.getName()
                                                );
+
+                                               // Translate subset param name to add the group name, so we
+                                               // get consistent naming. We know that subsets are only within
+                                               // the same group
+                                               subsetNames.push( subsetName );
                                        } );
+
+                                       // Set translated subset
+                                       filterItem.setSubset( subsetNames );
                                }
 
-                               // Conflicts are bi-directional, which means FilterA can define having
-                               // a conflict with FilterB, and this conflict should appear in **both**
-                               // filter definitions.
-                               // We need to remap all the 'conflicts' so they reflect the entire state
-                               // in either direction regardless of which filter defined the other as conflicting.
+                               // Store conflicts
                                if ( data.filters[ i ].conflicts ) {
-                                       conflictMap[ filterItem.getName() ] = conflictMap[ filterItem.getName() ] || [];
-                                       addArrayElementsUnique(
-                                               conflictMap[ filterItem.getName() ],
-                                               data.filters[ i ].conflicts
-                                       );
-
-                                       data.filters[ i ].conflicts.forEach( function ( conflictingFilterName ) { // eslint-disable-line no-loop-func
-                                               // Add this filter to the conflicts of each of the filters in its list
-                                               conflictMap[ conflictingFilterName ] = conflictMap[ conflictingFilterName ] || [];
-                                               addArrayElementsUnique(
-                                                       conflictMap[ conflictingFilterName ],
-                                                       filterItem.getName()
-                                               );
-                                       } );
+                                       filterConflictMap[ filterItem.getName() ] = data.filters[ i ].conflicts;
                                }
 
                                if ( data.type === 'send_unselected_if_any' ) {
                        }
                } );
 
-               items.forEach( function ( filterItem ) {
-                       // Apply conflict map to the items
-                       // Now that we mapped all items and conflicts bi-directionally
-                       // we need to apply the definition to each filter again
-                       filterItem.setConflicts( conflictMap[ filterItem.getName() ] );
+               // Expand conflicts
+               groupConflictResult = expandConflictDefinitions( groupConflictMap );
+               filterConflictResult = expandConflictDefinitions( filterConflictMap );
 
+               // Set conflicts for groups
+               $.each( groupConflictResult, function ( group, conflicts ) {
+                       model.groups[ group ].setConflicts( conflicts );
+               } );
+
+               items.forEach( function ( filterItem ) {
                        // Apply the superset map
                        filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
+
+                       // set conflicts for item
+                       if ( filterConflictResult[ filterItem.getName() ] ) {
+                               filterItem.setConflicts( filterConflictResult[ filterItem.getName() ] );
+                       }
+               } );
+
+               // Create a map between known parameters and their models
+               $.each( this.groups, function ( group, groupModel ) {
+                       if ( groupModel.getType() === 'send_unselected_if_any' ) {
+                               // Individual filters
+                               groupModel.getItems().forEach( function ( filterItem ) {
+                                       model.parameterMap[ filterItem.getParamName() ] = filterItem;
+                               } );
+                       } else if ( groupModel.getType() === 'string_options' ) {
+                               // Group
+                               model.parameterMap[ groupModel.getName() ] = groupModel;
+                       }
                } );
 
                // Add items to the model
         * @return {Object} Parameter state object
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.getParametersFromFilters = function ( filterGroups ) {
-               var i, filterItems, anySelected, values,
-                       result = {},
+               var result = {},
                        groupItems = filterGroups || this.getFilterGroups();
 
                $.each( groupItems, function ( group, model ) {
-                       filterItems = model.getItems();
-
-                       if ( model.getType() === 'send_unselected_if_any' ) {
-                               // First, check if any of the items are selected at all.
-                               // If none is selected, we're treating it as if they are
-                               // all false
-                               anySelected = filterItems.some( function ( filterItem ) {
-                                       return filterItem.isSelected();
-                               } );
-
-                               // Go over the items and define the correct values
-                               for ( i = 0; i < filterItems.length; i++ ) {
-                                       result[ filterItems[ i ].getName() ] = anySelected ?
-                                               Number( !filterItems[ i ].isSelected() ) : 0;
-                               }
-                       } else if ( model.getType() === 'string_options' ) {
-                               values = [];
-                               for ( i = 0; i < filterItems.length; i++ ) {
-                                       if ( filterItems[ i ].isSelected() ) {
-                                               values.push( filterItems[ i ].getName() );
-                                       }
-                               }
-
-                               if ( values.length === filterItems.length ) {
-                                       result[ group ] = 'all';
-                               } else {
-                                       result[ group ] = values.join( model.getSeparator() );
-                               }
-                       }
+                       $.extend( result, model.getParamRepresentation() );
                } );
 
                return result;
        mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function( groupName, valueArray ) {
                var result = [],
                        validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
-                               return filterItem.getName();
+                               return filterItem.getParamName();
                        } );
 
                if ( valueArray.indexOf( 'all' ) > -1 ) {
         * @return {Object} Filter state object
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
-               var i, filterItem,
+               var i,
                        groupMap = {},
                        model = this,
                        base = this.getDefaultParams(),
 
                params = $.extend( {}, base, params );
 
+               // Go over the given parameters
                $.each( params, function ( paramName, paramValue ) {
-                       // Find the filter item
-                       filterItem = model.getItemByName( paramName );
-                       // Ignore if no filter item exists
-                       if ( filterItem ) {
-                               groupMap[ filterItem.getGroupName() ] = groupMap[ filterItem.getGroupName() ] || {};
+                       var itemOrGroup = model.parameterMap[ paramName ];
 
+                       if ( itemOrGroup instanceof mw.rcfilters.dm.FilterItem ) {
                                // Mark the group if it has any items that are selected
-                               groupMap[ filterItem.getGroupName() ].hasSelected = (
-                                       groupMap[ filterItem.getGroupName() ].hasSelected ||
+                               groupMap[ itemOrGroup.getGroupName() ] = groupMap[ itemOrGroup.getGroupName() ] || {};
+                               groupMap[ itemOrGroup.getGroupName() ].hasSelected = (
+                                       groupMap[ itemOrGroup.getGroupName() ].hasSelected ||
                                        !!Number( paramValue )
                                );
 
-                               // Add the relevant filter into the group map
-                               groupMap[ filterItem.getGroupName() ].filters = groupMap[ filterItem.getGroupName() ].filters || [];
-                               groupMap[ filterItem.getGroupName() ].filters.push( filterItem );
-                       } else if ( model.groups.hasOwnProperty( paramName ) ) {
+                               // Add filters
+                               groupMap[ itemOrGroup.getGroupName() ].filters = groupMap[ itemOrGroup.getGroupName() ].filters || [];
+                               groupMap[ itemOrGroup.getGroupName() ].filters.push( itemOrGroup );
+                       } else if ( itemOrGroup instanceof mw.rcfilters.dm.FilterGroup ) {
+                               groupMap[ itemOrGroup.getName() ] = groupMap[ itemOrGroup.getName() ] || {};
                                // This parameter represents a group (values are the filters)
                                // this is equivalent to checking if the group is 'string_options'
-                               groupMap[ paramName ] = { filters: model.groups[ paramName ].getItems() };
+                               groupMap[ itemOrGroup.getName() ].filters = itemOrGroup.getItems();
                        }
                } );
 
                                for ( i = 0; i < allItemsInGroup.length; i++ ) {
                                        filterItem = allItemsInGroup[ i ];
 
-                                       result[ filterItem.getName() ] = data.hasSelected ?
+                                       result[ filterItem.getName() ] = groupMap[ filterItem.getGroupName() ].hasSelected ?
                                                // Flip the definition between the parameter
                                                // state and the filter state
                                                // This is what the 'toggleSelected' value of the filter is
-                                               !Number( params[ filterItem.getName() ] ) :
+                                               !Number( params[ filterItem.getParamName() ] ) :
                                                // Otherwise, there are no selected items in the
                                                // group, which means the state is false
                                                false;
                                }
                        } else if ( model.groups[ group ].getType() === 'string_options' ) {
-                               paramValues = model.sanitizeStringOptionGroup( group, params[ group ].split( model.groups[ group ].getSeparator() ) );
+                               paramValues = model.sanitizeStringOptionGroup(
+                                       group,
+                                       params[ group ].split(
+                                               model.groups[ group ].getSeparator()
+                                       )
+                               );
 
                                for ( i = 0; i < allItemsInGroup.length; i++ ) {
                                        filterItem = allItemsInGroup[ i ];
                                                // is the same as all filters set to false
                                                false :
                                                // Otherwise, the filter is selected only if it appears in the parameter values
-                                               paramValues.indexOf( filterItem.getName() ) > -1;
+                                               paramValues.indexOf( filterItem.getParamName() ) > -1;
                                }
                        }
                } );
+
                return result;
        };
 
         * @param {boolean} [isSelected] Filter selected state
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
-               this.getItemByName( name ).toggleSelected( isSelected );
+               var item = this.getItemByName( name );
+
+               if ( item ) {
+                       item.toggleSelected( isSelected );
+               }
        };
 
        /**
index b16e84c..9fe0ec6 100644 (file)
@@ -3,6 +3,7 @@
 .mw-rcfilters-ui-capsuleItemWidget {
        background-color: #fff;
        border-color: #979797;
+       margin: 0 0.6em 0 0;
        color: #222;
 
        // Background and color of the capsule widget need a bit
index a0ef293..b9dd3c1 100644 (file)
@@ -3,13 +3,16 @@
 
        &.oo-ui-widget-enabled .oo-ui-capsuleMultiselectWidget-handle {
                background-color: #f8f9fa;
-               border: 1px solid #a2a9b1;
-               min-height: 5.5em;
-               padding: 0.75em;
+               border-radius: 2px 2px 0 0;
+               padding: 0.3em 0.6em 0.6em 0.6em;
+               margin-top: 1.6em;
+       }
 
+       .mw-rcfilters-ui-table {
+               margin-top: 0.3em;
        }
 
-       &-content-title {
+       &-wrapper-content-title {
                font-weight: bold;
                color: #54595d;
        }
index 5111a17..ca19c22 100644 (file)
@@ -22,7 +22,7 @@
 
        &-search {
                max-width: none;
-               margin-top: -0.5em;
+               margin-top: -1px;
 
                input {
                        // We need to reiterate the directionality
index 3334d84..cb87989 100644 (file)
@@ -1,7 +1,7 @@
 .mw-rcfilters-ui-filtersListWidget {
        &-title {
                font-size: 1.2em;
-               padding: 0.75em;
+               padding: 0.75em 0.5em;
                // TODO: Unify colors with official design palette
                color: #54595d;
        }
                &-highlight {
                        width: 1em;
                        vertical-align: middle;
+                       // Using the same padding that the filter item
+                       // uses, so the button is aligned with the highlight
+                       // buttons for the filters
+                       padding-right: 0.5em;
                }
 
                &-title {
index 4ea284b..7e6d776 100644 (file)
                this.$element.toggleClass(
                        'mw-rcfilters-ui-filterItemWidget-muted',
                        this.model.isConflicted() ||
-                       this.model.isIncluded() ||
                        (
                                // Item is also muted when any of the items in its group is active
                                this.model.getGroupModel().isActive() &&
                                // But it isn't selected
-                               !this.model.isSelected()
+                               !this.model.isSelected() &&
+                               // And also not included
+                               !this.model.isIncluded()
                        )
                );
 
index d786025..d17ffff 100644 (file)
 
                // Replace the entire fieldset
                this.$element.empty().append( $fieldset.contents() );
+               // Make sure enhanced RC re-initializes correctly
+               mw.hook( 'wikipage.content' ).fire( this.$element );
 
                this.cleanUpFieldset();
 
         * Clean up the old-style show/hide that we have implemented in the filter list
         */
        mw.rcfilters.ui.FormWrapperWidget.prototype.cleanUpFieldset = function () {
+               var $namespaceSelect = this.$element.find( '#namespace' ),
+                       $namespaceCheckboxes = this.$element.find( '#nsassociated, #nsinvert' ),
+                       collapseCookieName = 'changeslist-state';
+
                this.$element.find( '.rcshowhideoption[data-feature-in-structured-ui=1]' ).each( function () {
                        // HACK: Remove the text node after the span.
                        // If there isn't one, we're at the end, so remove the text node before the span.
                        // Remove the span itself
                        this.parentNode.removeChild( this );
                } );
+
+               // Bind namespace select to change event
+               // see resources/src/mediawiki.special/mediawiki.special.recentchanges.js
+               $namespaceCheckboxes.prop( 'disabled', $namespaceSelect.val() === '' );
+               $namespaceSelect.on( 'change', function () {
+                       $namespaceCheckboxes.prop( 'disabled', $( this ).val() === '' );
+               } );
+
+               // Collapse legend
+               // see resources/src/mediawiki.special/mediawiki.special.changelist.legend.js
+               this.$element.find( '.mw-changeslist-legend' )
+                       .makeCollapsible( {
+                               collapsed: mw.cookie.get( collapseCookieName ) === 'collapsed'
+                       } )
+                       .on( 'beforeExpand.mw-collapsible', function () {
+                               mw.cookie.set( collapseCookieName, 'expanded' );
+                       } )
+                       .on( 'beforeCollapse.mw-collapsible', function () {
+                               mw.cookie.set( collapseCookieName, 'collapsed' );
+                       } );
+
        };
 }( mediaWiki ) );
index f712a2f..465a9d1 100644 (file)
@@ -51,4 +51,29 @@ class ChangesListFilterGroupTest extends MediaWikiTestCase {
                        )
                );
        }
+
+       // Get without warnings
+       public function testGetFilter() {
+               $group = new MockChangesListFilterGroup(
+                       [
+                               'type' => 'some_type',
+                               'name' => 'groupName',
+                               'isFullCoverage' => true,
+                               'priority' => 1,
+                               'filters' => [
+                                       [ 'name' => 'foo' ],
+                               ],
+                       ]
+               );
+
+               $this->assertEquals(
+                       'foo',
+                       $group->getFilter( 'foo' )->getName()
+               );
+
+               $this->assertEquals(
+                       null,
+                       $group->getFilter( 'bar' )
+               );
+       }
 }
index c212560..1d87aeb 100644 (file)
@@ -40,6 +40,30 @@ class ChangesListFilterTest extends MediaWikiTestCase {
                );
        }
 
+       // @codingStandardsIgnoreStart
+       /**
+        * @expectedException MWException
+        * @expectedExceptionMessage Two filters in a group cannot have the same name: 'somename'
+        */
+       // @codingStandardsIgnoreEnd
+       public function testDuplicateName() {
+               new MockChangesListFilter(
+                       [
+                               'group' => $this->group,
+                               'name' => 'somename',
+                               'priority' => 1,
+                       ]
+               );
+
+               new MockChangesListFilter(
+                       [
+                               'group' => $this->group,
+                               'name' => 'somename',
+                               'priority' => 2,
+                       ]
+               );
+       }
+
        /**
         * @expectedException MWException
         * @expectedExceptionMessage Supersets can only be defined for filters in the same group
index aeb82d1..a1acea6 100644 (file)
@@ -197,6 +197,62 @@ class ResourceLoaderImageModuleTest extends ResourceLoaderTestCase {
                ] ) );
                $this->assertInstanceOf( ResourceLoaderImage::class, $context->getImageObj() );
        }
+
+       public static function providerGetStyleDeclarations() {
+               return [
+                       [
+                               false,
+<<<TEXT
+background-image: url(rasterized.png);
+       background-image: linear-gradient(transparent, transparent), url(original.svg);
+       background-image: -o-linear-gradient(transparent, transparent), url(rasterized.png);
+TEXT
+                       ],
+                       [
+                               'data:image/svg+xml',
+<<<TEXT
+background-image: url(rasterized.png);
+       background-image: linear-gradient(transparent, transparent), url(data:image/svg+xml);
+       background-image: -o-linear-gradient(transparent, transparent), url(rasterized.png);
+TEXT
+                       ],
+
+               ];
+       }
+
+       /**
+        * @dataProvider providerGetStyleDeclarations
+        * @covers ResourceLoaderContext::getStyleDeclarations
+        */
+       public function testGetStyleDeclarations( $dataUriReturnValue, $expected ) {
+               $module = TestingAccessWrapper::newFromObject( new ResourceLoaderImageModule() );
+               $context = $this->getResourceLoaderContext();
+               $image = $this->getImageMock( $context, $dataUriReturnValue );
+
+               $styles = $module->getStyleDeclarations(
+                       $context,
+                       $image,
+                       'load.php'
+               );
+
+               $this->assertEquals( $expected, $styles );
+       }
+
+       private function getImageMock( ResourceLoaderContext $context, $dataUriReturnValue ) {
+               $image = $this->getMockBuilder( 'ResourceLoaderImage' )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $image->method( 'getDataUri' )
+                       ->will( $this->returnValue( $dataUriReturnValue ) );
+               $image->expects( $this->any() )
+                       ->method( 'getUrl' )
+                       ->will( $this->returnValueMap( [
+                               [ $context, 'load.php', null, 'original', 'original.svg' ],
+                               [ $context, 'load.php', null, 'rasterized', 'rasterized.png' ],
+                       ] ) );
+
+               return $image;
+       }
 }
 
 class ResourceLoaderImageModuleTestable extends ResourceLoaderImageModule {
index 25ea988..271648f 100644 (file)
@@ -1,14 +1,16 @@
+/* eslint-disable camelcase */
 ( function ( mw ) {
        QUnit.module( 'mediawiki.rcfilters - FilterItem' );
 
        QUnit.test( 'Initializing filter item', function ( assert ) {
                var item,
-                       group1 = new mw.rcfilters.dm.FilterGroup( 'group1' );
+                       group1 = new mw.rcfilters.dm.FilterGroup( 'group1' ),
+                       group2 = new mw.rcfilters.dm.FilterGroup( 'group2' );
 
                item = new mw.rcfilters.dm.FilterItem( 'filter1', group1 );
                assert.equal(
                        item.getName(),
-                       'filter1',
+                       'group1__filter1',
                        'Filter name is retained.'
                );
                assert.equal(
                        'filter1',
                        group1,
                        {
-                               conflicts: [ 'conflict1', 'conflict2', 'conflict3' ]
+                               conflicts: {
+                                       group2__conflict1: { group: 'group2', filter: 'group2__conflict1' },
+                                       group2__conflict2: { group: 'group2', filter: 'group2__conflict2' },
+                                       group2__conflict3: { group: 'group2', filter: 'group2__conflict3' }
+                               }
                        }
                );
                assert.deepEqual(
                        item.getConflicts(),
-                       [ 'conflict1', 'conflict2', 'conflict3' ],
+                       {
+                               group2__conflict1: { group: 'group2', filter: 'group2__conflict1' },
+                               group2__conflict2: { group: 'group2', filter: 'group2__conflict2' },
+                               group2__conflict3: { group: 'group2', filter: 'group2__conflict3' }
+                       },
                        'Conflict information is retained.'
                );
                assert.equal(
-                       // TODO: Consider allowing for either a FilterItem or a filter name
-                       // in this method, so it is consistent with the subset one
-                       item.existsInConflicts( new mw.rcfilters.dm.FilterItem( 'conflict1', group1 ) ),
+                       item.existsInConflicts( new mw.rcfilters.dm.FilterItem( 'conflict1', group2 ) ),
                        true,
                        'Specific item exists in conflicts.'
                );
index 52ba360..405fdcf 100644 (file)
@@ -1,3 +1,4 @@
+/* eslint-disable camelcase */
 ( function ( mw, $ ) {
        QUnit.module( 'mediawiki.rcfilters - FiltersViewModel', QUnit.newMwEnvironment( {
                messages: {
                                type: 'send_unselected_if_any',
                                filters: [
                                        {
-                                               name: 'group1filter1',
+                                               name: 'filter1',
                                                label: 'Group 1: Filter 1',
                                                description: 'Description of Filter 1 in Group 1'
                                        },
                                        {
-                                               name: 'group1filter2',
+                                               name: 'filter2',
                                                label: 'Group 1: Filter 2',
                                                description: 'Description of Filter 2 in Group 1'
                                        }
                                type: 'send_unselected_if_any',
                                filters: [
                                        {
-                                               name: 'group2filter1',
+                                               name: 'filter1',
                                                label: 'Group 2: Filter 1',
                                                description: 'Description of Filter 1 in Group 2'
                                        },
                                        {
-                                               name: 'group2filter2',
+                                               name: 'filter2',
                                                label: 'Group 2: Filter 2',
                                                description: 'Description of Filter 2 in Group 2'
                                        }
                                type: 'string_options',
                                filters: [
                                        {
-                                               name: 'group3filter1',
+                                               name: 'filter1',
                                                label: 'Group 3: Filter 1',
                                                description: 'Description of Filter 1 in Group 3'
                                        },
                                        {
-                                               name: 'group3filter2',
+                                               name: 'filter2',
                                                label: 'Group 3: Filter 2',
                                                description: 'Description of Filter 2 in Group 3'
                                        }
                model.initializeFilters( definition );
 
                assert.ok(
-                       model.getItemByName( 'group1filter1' ) instanceof mw.rcfilters.dm.FilterItem &&
-                       model.getItemByName( 'group1filter2' ) instanceof mw.rcfilters.dm.FilterItem &&
-                       model.getItemByName( 'group2filter1' ) instanceof mw.rcfilters.dm.FilterItem &&
-                       model.getItemByName( 'group2filter2' ) instanceof mw.rcfilters.dm.FilterItem &&
-                       model.getItemByName( 'group3filter1' ) instanceof mw.rcfilters.dm.FilterItem &&
-                       model.getItemByName( 'group3filter2' ) instanceof mw.rcfilters.dm.FilterItem,
+                       model.getItemByName( 'group1__filter1' ) instanceof mw.rcfilters.dm.FilterItem &&
+                       model.getItemByName( 'group1__filter2' ) instanceof mw.rcfilters.dm.FilterItem &&
+                       model.getItemByName( 'group2__filter1' ) instanceof mw.rcfilters.dm.FilterItem &&
+                       model.getItemByName( 'group2__filter2' ) instanceof mw.rcfilters.dm.FilterItem &&
+                       model.getItemByName( 'group3__filter1' ) instanceof mw.rcfilters.dm.FilterItem &&
+                       model.getItemByName( 'group3__filter2' ) instanceof mw.rcfilters.dm.FilterItem,
                        'Filters instantiated and stored correctly'
                );
 
                assert.deepEqual(
                        model.getSelectedState(),
                        {
-                               group1filter1: false,
-                               group1filter2: false,
-                               group2filter1: false,
-                               group2filter2: false,
-                               group3filter1: false,
-                               group3filter2: false
+                               group1__filter1: false,
+                               group1__filter2: false,
+                               group2__filter1: false,
+                               group2__filter2: false,
+                               group3__filter1: false,
+                               group3__filter2: false
                        },
                        'Initial state of filters'
                );
 
                model.toggleFiltersSelected( {
-                       group1filter1: true,
-                       group2filter2: true,
-                       group3filter1: true
+                       group1__filter1: true,
+                       group2__filter2: true,
+                       group3__filter1: true
                } );
                assert.deepEqual(
                        model.getSelectedState(),
                        {
-                               group1filter1: true,
-                               group1filter2: false,
-                               group2filter1: false,
-                               group2filter2: true,
-                               group3filter1: true,
-                               group3filter2: false
+                               group1__filter1: true,
+                               group1__filter2: false,
+                               group2__filter1: false,
+                               group2__filter2: true,
+                               group3__filter1: true,
+                               group3__filter2: false
                        },
                        'Updating filter states correctly'
                );
                                type: 'send_unselected_if_any',
                                filters: [
                                        {
-                                               name: 'group1filter1',
+                                               name: 'filter1',
                                                label: 'group1filter1-label',
                                                description: 'group1filter1-desc'
                                        },
                                        {
-                                               name: 'group1filter2',
+                                               name: 'filter2',
                                                label: 'group1filter2-label',
                                                description: 'group1filter2-desc'
                                        }
                                type: 'send_unselected_if_any',
                                filters: [
                                        {
-                                               name: 'group2filter1',
+                                               name: 'filter1',
                                                label: 'group2filter1-label',
                                                description: 'group2filter1-desc'
                                        },
                                        {
-                                               name: 'group2filter2',
+                                               name: 'filter2',
                                                label: 'group2filter2-label',
                                                description: 'group2filter2-desc'
                                        }
                                {
                                        query: 'group',
                                        expectedMatches: {
-                                               group1: [ 'group1filter1', 'group1filter2' ],
-                                               group2: [ 'group2filter1' ]
+                                               group1: [ 'group1__filter1', 'group1__filter2' ],
+                                               group2: [ 'group2__filter1' ]
                                        },
                                        reason: 'Finds filters starting with the query string'
                                },
                                {
                                        query: 'filter 2 in group',
                                        expectedMatches: {
-                                               group1: [ 'group1filter2' ],
-                                               group2: [ 'group2filter2' ]
+                                               group1: [ 'group1__filter2' ],
+                                               group2: [ 'group2__filter2' ]
                                        },
                                        reason: 'Finds filters containing the query string in their description'
                                },
                                {
                                        query: 'title',
                                        expectedMatches: {
-                                               group1: [ 'group1filter1', 'group1filter2' ],
-                                               group2: [ 'group2filter1', 'group2filter2' ]
+                                               group1: [ 'group1__filter1', 'group1__filter2' ],
+                                               group2: [ 'group2__filter1', 'group2__filter2' ]
                                        },
                                        reason: 'Finds filters containing the query string in their group title'
                                }
 
                // Select 1 filter
                model.toggleFiltersSelected( {
-                       hidefilter1: true,
-                       hidefilter2: false,
-                       hidefilter3: false,
-                       hidefilter4: false,
-                       hidefilter5: false,
-                       hidefilter6: false
+                       group1__hidefilter1: true,
+                       group1__hidefilter2: false,
+                       group1__hidefilter3: false,
+                       group2__hidefilter4: false,
+                       group2__hidefilter5: false,
+                       group2__hidefilter6: false
                } );
                // Only one filter in one group
                assert.deepEqual(
 
                // Select 2 filters
                model.toggleFiltersSelected( {
-                       hidefilter1: true,
-                       hidefilter2: true,
-                       hidefilter3: false,
-                       hidefilter4: false,
-                       hidefilter5: false,
-                       hidefilter6: false
+                       group1__hidefilter1: true,
+                       group1__hidefilter2: true,
+                       group1__hidefilter3: false,
+                       group2__hidefilter4: false,
+                       group2__hidefilter5: false,
+                       group2__hidefilter6: false
                } );
                // Two selected filters in one group
                assert.deepEqual(
 
                // Select 3 filters
                model.toggleFiltersSelected( {
-                       hidefilter1: true,
-                       hidefilter2: true,
-                       hidefilter3: true,
-                       hidefilter4: false,
-                       hidefilter5: false,
-                       hidefilter6: false
+                       group1__hidefilter1: true,
+                       group1__hidefilter2: true,
+                       group1__hidefilter3: true,
+                       group2__hidefilter4: false,
+                       group2__hidefilter5: false,
+                       group2__hidefilter6: false
                } );
                // All filters of the group are selected == this is the same as not selecting any
                assert.deepEqual(
 
                // Select 1 filter from string_options
                model.toggleFiltersSelected( {
-                       filter7: true,
-                       filter8: false,
-                       filter9: false
+                       group3__filter7: true,
+                       group3__filter8: false,
+                       group3__filter9: false
                } );
                // All filters of the group are selected == this is the same as not selecting any
                assert.deepEqual(
 
                // Select 2 filters from string_options
                model.toggleFiltersSelected( {
-                       filter7: true,
-                       filter8: true,
-                       filter9: false
+                       group3__filter7: true,
+                       group3__filter8: true,
+                       group3__filter9: false
                } );
                // All filters of the group are selected == this is the same as not selecting any
                assert.deepEqual(
 
                // Select 3 filters from string_options
                model.toggleFiltersSelected( {
-                       filter7: true,
-                       filter8: true,
-                       filter9: true
+                       group3__filter7: true,
+                       group3__filter8: true,
+                       group3__filter9: true
                } );
                // All filters of the group are selected == this is the same as not selecting any
                assert.deepEqual(
                        } ],
                        defaultFilterRepresentation = {
                                // Group 1 and 2, "send_unselected_if_any", the values of the filters are "flipped" from the values of the parameters
-                               hidefilter1: false,
-                               hidefilter2: true,
-                               hidefilter3: false,
-                               hidefilter4: true,
-                               hidefilter5: false,
-                               hidefilter6: true,
+                               group1__hidefilter1: false,
+                               group1__hidefilter2: true,
+                               group1__hidefilter3: false,
+                               group2__hidefilter4: true,
+                               group2__hidefilter5: false,
+                               group2__hidefilter6: true,
                                // Group 3, "string_options", default values correspond to parameters and filters
-                               filter7: false,
-                               filter8: true,
-                               filter9: false
+                               group3__filter7: false,
+                               group3__filter8: true,
+                               group3__filter9: false
                        },
                        model = new mw.rcfilters.dm.FiltersViewModel();
 
                                hidefilter2: '1'
                        } ),
                        $.extend( {}, defaultFilterRepresentation, {
-                               hidefilter1: false, // The text is "show filter 1"
-                               hidefilter2: false, // The text is "show filter 2"
-                               hidefilter3: false // The text is "show filter 3"
+                               group1__hidefilter1: false, // The text is "show filter 1"
+                               group1__hidefilter2: false, // The text is "show filter 2"
+                               group1__hidefilter3: false // The text is "show filter 3"
                        } ),
                        'One truthy parameter in a group whose other parameters are true by default makes the rest of the filters in the group false (unchecked)'
                );
                                hidefilter3: '1'
                        } ),
                        $.extend( {}, defaultFilterRepresentation, {
-                               hidefilter1: false, // The text is "show filter 1"
-                               hidefilter2: false, // The text is "show filter 2"
-                               hidefilter3: false // The text is "show filter 3"
+                               group1__hidefilter1: false, // The text is "show filter 1"
+                               group1__hidefilter2: false, // The text is "show filter 2"
+                               group1__hidefilter3: false // The text is "show filter 3"
                        } ),
                        'All paremeters in the same \'send_unselected_if_any\' group false is equivalent to none are truthy (checked) in the interface'
                );
                assert.deepEqual(
                        model.getSelectedState(),
                        $.extend( {}, defaultFilterRepresentation, {
-                               hidefilter5: false,
-                               hidefilter6: false
+                               group2__hidefilter5: false,
+                               group2__hidefilter6: false
                        } ),
                        'getFiltersFromParameters does not care about previous or existing state.'
                );
                assert.deepEqual(
                        model.getSelectedState(),
                        $.extend( {}, defaultFilterRepresentation, {
-                               filter7: true,
-                               filter8: false,
-                               filter9: false
+                               group3__filter7: true,
+                               group3__filter8: false,
+                               group3__filter9: false
                        } ),
                        'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
                );
                assert.deepEqual(
                        model.getSelectedState(),
                        $.extend( {}, defaultFilterRepresentation, {
-                               filter7: true,
-                               filter8: true,
-                               filter9: false
+                               group3__filter7: true,
+                               group3__filter8: true,
+                               group3__filter9: false
                        } ),
                        'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
                );
                assert.deepEqual(
                        model.getSelectedState(),
                        $.extend( {}, defaultFilterRepresentation, {
-                               filter7: false,
-                               filter8: false,
-                               filter9: false
+                               group3__filter7: false,
+                               group3__filter8: false,
+                               group3__filter9: false
                        } ),
                        'A \'string_options\' parameter containing all values, results in all filters of the group as unchecked.'
                );
                assert.deepEqual(
                        model.getSelectedState(),
                        $.extend( {}, defaultFilterRepresentation, {
-                               filter7: false,
-                               filter8: false,
-                               filter9: false
+                               group3__filter7: false,
+                               group3__filter8: false,
+                               group3__filter9: false
                        } ),
                        'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as unchecked.'
                );
                assert.deepEqual(
                        model.getSelectedState(),
                        $.extend( {}, defaultFilterRepresentation, {
-                               filter7: true,
-                               filter8: false,
-                               filter9: true
+                               group3__filter7: true,
+                               group3__filter8: false,
+                               group3__filter9: true
                        } ),
                        'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.'
                );
                        } ],
                        defaultFilterRepresentation = {
                                // Group 1 and 2, "send_unselected_if_any", the values of the filters are "flipped" from the values of the parameters
-                               hidefilter1: false,
-                               hidefilter2: true,
-                               hidefilter3: false,
-                               hidefilter4: true,
-                               hidefilter5: false,
-                               hidefilter6: true
+                               group1__hidefilter1: false,
+                               group1__hidefilter2: true,
+                               group1__hidefilter3: false,
+                               group2__hidefilter4: true,
+                               group2__hidefilter5: false,
+                               group2__hidefilter6: true
                        },
                        model = new mw.rcfilters.dm.FiltersViewModel();
 
                assert.deepEqual(
                        model.getSelectedState(),
                        {
-                               hidefilter1: false,
-                               hidefilter2: false,
-                               hidefilter3: false,
-                               hidefilter4: false,
-                               hidefilter5: false,
-                               hidefilter6: false
+                               group1__hidefilter1: false,
+                               group1__hidefilter2: false,
+                               group1__hidefilter3: false,
+                               group2__hidefilter4: false,
+                               group2__hidefilter5: false,
+                               group2__hidefilter6: false
                        },
                        'Initial state: default filters are not selected (controller selects defaults explicitly).'
                );
 
                model.toggleFiltersSelected( {
-                       hidefilter1: false,
-                       hidefilter3: false
+                       group1__hidefilter1: false,
+                       group1__hidefilter3: false
                } );
 
                model.setFiltersToDefaults();
                                ]
                        } ],
                        baseFullState = {
-                               filter1: { selected: false, conflicted: false, included: false },
-                               filter2: { selected: false, conflicted: false, included: false },
-                               filter3: { selected: false, conflicted: false, included: false }
+                               group1__filter1: { selected: false, conflicted: false, included: false },
+                               group1__filter2: { selected: false, conflicted: false, included: false },
+                               group1__filter3: { selected: false, conflicted: false, included: false }
                        },
                        model = new mw.rcfilters.dm.FiltersViewModel();
 
                model.initializeFilters( definition );
                // Select a filter that has subset with another filter
                model.toggleFiltersSelected( {
-                       filter1: true
+                       group1__filter1: true
                } );
 
-               model.reassessFilterInteractions( model.getItemByName( 'filter1' ) );
+               model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) );
                assert.deepEqual(
                        model.getFullState(),
                        $.extend( true, {}, baseFullState, {
-                               filter1: { selected: true },
-                               filter2: { included: true },
-                               filter3: { included: true }
+                               group1__filter1: { selected: true },
+                               group1__filter2: { included: true },
+                               group1__filter3: { included: true }
                        } ),
                        'Filters with subsets are represented in the model.'
                );
 
                // Select another filter that has a subset with the same previous filter
                model.toggleFiltersSelected( {
-                       filter2: true
+                       group1__filter2: true
                } );
                model.reassessFilterInteractions( model.getItemByName( 'filter2' ) );
                assert.deepEqual(
                        model.getFullState(),
                        $.extend( true, {}, baseFullState, {
-                               filter1: { selected: true },
-                               filter2: { selected: true, included: true },
-                               filter3: { included: true }
+                               group1__filter1: { selected: true },
+                               group1__filter2: { selected: true, included: true },
+                               group1__filter3: { included: true }
                        } ),
                        'Filters that have multiple subsets are represented.'
                );
 
-               // Remove one filter (but leave the other) that affects filter2
+               // Remove one filter (but leave the other) that affects filter3
                model.toggleFiltersSelected( {
-                       filter1: false
+                       group1__filter1: false
                } );
-               model.reassessFilterInteractions( model.getItemByName( 'filter1' ) );
+               model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) );
                assert.deepEqual(
                        model.getFullState(),
                        $.extend( true, {}, baseFullState, {
-                               filter2: { selected: true, included: false },
-                               filter3: { included: true }
+                               group1__filter2: { selected: true, included: false },
+                               group1__filter3: { included: true }
                        } ),
                        'Removing a filter only un-includes its subset if there is no other filter affecting.'
                );
 
                model.toggleFiltersSelected( {
-                       filter2: false
+                       group1__filter2: false
                } );
-               model.reassessFilterInteractions( model.getItemByName( 'filter2' ) );
+               model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) );
                assert.deepEqual(
                        model.getFullState(),
                        baseFullState,
                        },
                        getCurrentItemsMutedState = function () {
                                return {
-                                       filter1: isCapsuleItemMuted( 'filter1' ),
-                                       filter2: isCapsuleItemMuted( 'filter2' ),
-                                       filter3: isCapsuleItemMuted( 'filter3' ),
-                                       filter4: isCapsuleItemMuted( 'filter4' ),
-                                       filter5: isCapsuleItemMuted( 'filter5' ),
-                                       filter6: isCapsuleItemMuted( 'filter6' )
+                                       group1__filter1: isCapsuleItemMuted( 'group1__filter1' ),
+                                       group1__filter2: isCapsuleItemMuted( 'group1__filter2' ),
+                                       group1__filter3: isCapsuleItemMuted( 'group1__filter3' ),
+                                       group2__filter4: isCapsuleItemMuted( 'group2__filter4' ),
+                                       group2__filter5: isCapsuleItemMuted( 'group2__filter5' ),
+                                       group2__filter6: isCapsuleItemMuted( 'group2__filter6' )
                                };
                        },
                        baseMuteState = {
-                               filter1: false,
-                               filter2: false,
-                               filter3: false,
-                               filter4: false,
-                               filter5: false,
-                               filter6: false
+                               group1__filter1: false,
+                               group1__filter2: false,
+                               group1__filter3: false,
+                               group2__filter4: false,
+                               group2__filter5: false,
+                               group2__filter6: false
                        };
 
                model.initializeFilters( definition );
 
                // Select most (but not all) items in each group
                model.toggleFiltersSelected( {
-                       filter1: true,
-                       filter2: true,
-                       filter4: true,
-                       filter5: true
+                       group1__filter1: true,
+                       group1__filter2: true,
+                       group2__filter4: true,
+                       group2__filter5: true
                } );
 
                // Both groups have multiple (but not all) items selected, all items are non-muted
 
                // Select all items in 'fullCoverage' group (group2)
                model.toggleFiltersSelected( {
-                       filter6: true
+                       group2__filter6: true
                } );
 
                // Group2 (full coverage) has all items selected, all its items are muted
                assert.deepEqual(
                        getCurrentItemsMutedState(),
                        $.extend( {}, baseMuteState, {
-                               filter4: true,
-                               filter5: true,
-                               filter6: true
+                               group2__filter4: true,
+                               group2__filter5: true,
+                               group2__filter6: true
                        } ),
                        'All items in \'full coverage\' group are selected - all items in the group are muted'
                );
 
                // Select all items in non 'fullCoverage' group (group1)
                model.toggleFiltersSelected( {
-                       filter3: true
+                       group1__filter3: true
                } );
 
                // Group1 (full coverage) has all items selected, no items in it are muted (non full coverage)
                assert.deepEqual(
                        getCurrentItemsMutedState(),
                        $.extend( {}, baseMuteState, {
-                               filter4: true,
-                               filter5: true,
-                               filter6: true
+                               group2__filter4: true,
+                               group2__filter5: true,
+                               group2__filter6: true
                        } ),
                        'All items in a non \'full coverage\' group are selected - none of the items in the group are muted'
                );
 
                // Uncheck an item from each group
                model.toggleFiltersSelected( {
-                       filter3: false,
-                       filter5: false
+                       group1__filter3: false,
+                       group2__filter5: false
                } );
                assert.deepEqual(
                        getCurrentItemsMutedState(),
                                                name: 'filter1',
                                                label: '1',
                                                description: '1',
-                                               conflicts: [ 'filter2', 'filter4' ]
+                                               conflicts: [ { group: 'group2' } ]
                                        },
                                        {
                                                name: 'filter2',
                                                label: '2',
                                                description: '2',
-                                               conflicts: [ 'filter6' ]
+                                               conflicts: [ { group: 'group2', filter: 'filter6' } ]
                                        },
                                        {
                                                name: 'filter3',
                                name: 'group2',
                                title: 'Group 2',
                                type: 'send_unselected_if_any',
+                               conflicts: [ { group: 'group1', filter: 'filter1' } ],
                                filters: [
                                        {
                                                name: 'filter4',
                                        {
                                                name: 'filter5',
                                                label: '5',
-                                               description: '5',
-                                               conflicts: [ 'filter3' ]
+                                               description: '5'
                                        },
                                        {
                                                name: 'filter6',
                                                label: '6',
-                                               description: '6'
+                                               description: '6',
+                                               conflicts: [ { group: 'group1', filter: 'filter2' } ]
                                        }
                                ]
                        } ],
                        baseFullState = {
-                               filter1: { selected: false, conflicted: false, included: false },
-                               filter2: { selected: false, conflicted: false, included: false },
-                               filter3: { selected: false, conflicted: false, included: false },
-                               filter4: { selected: false, conflicted: false, included: false },
-                               filter5: { selected: false, conflicted: false, included: false },
-                               filter6: { selected: false, conflicted: false, included: false }
+                               group1__filter1: { selected: false, conflicted: false, included: false },
+                               group1__filter2: { selected: false, conflicted: false, included: false },
+                               group1__filter3: { selected: false, conflicted: false, included: false },
+                               group2__filter4: { selected: false, conflicted: false, included: false },
+                               group2__filter5: { selected: false, conflicted: false, included: false },
+                               group2__filter6: { selected: false, conflicted: false, included: false }
                        },
                        model = new mw.rcfilters.dm.FiltersViewModel();
 
                        'Initial state: no conflicts because no selections.'
                );
 
-               // Select a filter that has a conflict with another
+               // Select a filter that has a conflict with an entire group
                model.toggleFiltersSelected( {
-                       filter1: true // conflicts: filter2, filter4
+                       group1__filter1: true // conflicts: entire of group 2 ( filter4, filter5, filter6)
                } );
 
-               model.reassessFilterInteractions( model.getItemByName( 'filter1' ) );
+               model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) );
 
                assert.deepEqual(
                        model.getFullState(),
                        $.extend( true, {}, baseFullState, {
-                               filter1: { selected: true },
-                               filter2: { conflicted: true },
-                               filter4: { conflicted: true }
+                               group1__filter1: { selected: true },
+                               group2__filter4: { conflicted: true },
+                               group2__filter5: { conflicted: true },
+                               group2__filter6: { conflicted: true }
                        } ),
-                       'Selecting a filter set its conflicts list as "conflicted".'
+                       'Selecting a filter that conflicts with a group sets all the conflicted group items as "conflicted".'
                );
 
                // Select one of the conflicts (both filters are now conflicted and selected)
                model.toggleFiltersSelected( {
-                       filter4: true // conflicts: filter 1
+                       group2__filter4: true // conflicts: filter 1
+               } );
+               model.reassessFilterInteractions( model.getItemByName( 'group2__filter4' ) );
+
+               assert.deepEqual(
+                       model.getFullState(),
+                       $.extend( true, {}, baseFullState, {
+                               group1__filter1: { selected: true, conflicted: true },
+                               group2__filter4: { selected: true, conflicted: true },
+                               group2__filter5: { conflicted: true },
+                               group2__filter6: { conflicted: true }
+                       } ),
+                       'Selecting a conflicting filter inside a group, sets both sides to conflicted and selected.'
+               );
+
+               // Reset
+               model = new mw.rcfilters.dm.FiltersViewModel();
+               model.initializeFilters( definition );
+
+               // Select a filter that has a conflict with a specific filter
+               model.toggleFiltersSelected( {
+                       group1__filter2: true // conflicts: filter6
+               } );
+               model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) );
+
+               assert.deepEqual(
+                       model.getFullState(),
+                       $.extend( true, {}, baseFullState, {
+                               group1__filter2: { selected: true },
+                               group2__filter6: { conflicted: true }
+                       } ),
+                       'Selecting a filter that conflicts with another filter sets the other as "conflicted".'
+               );
+
+               // Select the conflicting filter
+               model.toggleFiltersSelected( {
+                       group2__filter6: true // conflicts: filter2
+               } );
+
+               model.reassessFilterInteractions( model.getItemByName( 'group2__filter6' ) );
+
+               assert.deepEqual(
+                       model.getFullState(),
+                       $.extend( true, {}, baseFullState, {
+                               group1__filter2: { selected: true, conflicted: true },
+                               group2__filter6: { selected: true, conflicted: true },
+                               // This is added to the conflicts because filter6 is part of group2,
+                               // who is in conflict with filter1; note that filter2 also conflicts
+                               // with filter6 which means that filter1 conflicts with filter6 (because it's in group2)
+                               // and also because its **own sibling** (filter2) is **also** in conflict with the
+                               // selected items in group2 (filter6)
+                               group1__filter1: { conflicted: true }
+                       } ),
+                       'Selecting a conflicting filter with an individual filter, sets both sides to conflicted and selected.'
+               );
+
+               // Now choose a non-conflicting filter from the group
+               model.toggleFiltersSelected( {
+                       group2__filter5: true
+               } );
+
+               model.reassessFilterInteractions( model.getItemByName( 'group2__filter5' ) );
+
+               assert.deepEqual(
+                       model.getFullState(),
+                       $.extend( true, {}, baseFullState, {
+                               group1__filter2: { selected: true },
+                               group2__filter6: { selected: true },
+                               group2__filter5: { selected: true }
+                               // Filter6 and filter1 are no longer in conflict because
+                               // filter5, while it is in conflict with filter1, it is
+                               // not in conflict with filter2 - and since filter2 is
+                               // selected, it removes the conflict bidirectionally
+                       } ),
+                       'Selecting a non-conflicting filter within the group of a conflicting filter removes the conflicts.'
+               );
+
+               // Followup on the previous test, unselect filter2 so filter1
+               // is now the only one selected in its own group, and since
+               // it is in conflict with the entire of group2, it means
+               // filter1 is once again conflicted
+               model.toggleFiltersSelected( {
+                       group1__filter2: false
+               } );
+
+               model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) );
+
+               assert.deepEqual(
+                       model.getFullState(),
+                       $.extend( true, {}, baseFullState, {
+                               group1__filter1: { conflicted: true },
+                               group2__filter6: { selected: true },
+                               group2__filter5: { selected: true }
+                       } ),
+                       'Unselecting an item that did not conflict returns the conflict state.'
+               );
+
+               // Followup #2: Now actually select filter1, and make everything conflicted
+               model.toggleFiltersSelected( {
+                       group1__filter1: true
+               } );
+
+               model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) );
+
+               assert.deepEqual(
+                       model.getFullState(),
+                       $.extend( true, {}, baseFullState, {
+                               group1__filter1: { selected: true, conflicted: true },
+                               group2__filter6: { selected: true, conflicted: true },
+                               group2__filter5: { selected: true, conflicted: true },
+                               group2__filter4: { conflicted: true } // Not selected but conflicted because it's in group2
+                       } ),
+                       'Selecting an item that conflicts with a whole group makes all selections in that group conflicted.'
+               );
+
+               /* Simple case */
+               // Reset
+               model = new mw.rcfilters.dm.FiltersViewModel();
+               model.initializeFilters( definition );
+
+               // Select a filter that has a conflict with a specific filter
+               model.toggleFiltersSelected( {
+                       group1__filter2: true // conflicts: filter6
                } );
-               model.reassessFilterInteractions( model.getItemByName( 'filter4' ) );
+
+               model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) );
 
                assert.deepEqual(
                        model.getFullState(),
                        $.extend( true, {}, baseFullState, {
-                               filter1: { selected: true, conflicted: true },
-                               filter2: { conflicted: true },
-                               filter4: { selected: true, conflicted: true }
+                               group1__filter2: { selected: true },
+                               group2__filter6: { conflicted: true }
                        } ),
-                       'Selecting a conflicting filter sets both sides to conflicted and selected.'
+                       'Simple case: Selecting a filter that conflicts with another filter sets the other as "conflicted".'
                );
 
-               // Select another filter from filter4 group, meaning:
-               // now filter1 no longer conflicts with filter4
                model.toggleFiltersSelected( {
-                       filter6: true // conflicts: filter2
+                       group1__filter3: true // conflicts: filter6
                } );
-               model.reassessFilterInteractions( model.getItemByName( 'filter6' ) );
+
+               model.reassessFilterInteractions( model.getItemByName( 'group1__filter3' ) );
 
                assert.deepEqual(
                        model.getFullState(),
                        $.extend( true, {}, baseFullState, {
-                               filter1: { selected: true, conflicted: false }, // No longer conflicts (filter4 is not the only in the group)
-                               filter2: { conflicted: true }, // While not selected, still in conflict with filter1, which is selected
-                               filter4: { selected: true, conflicted: false }, // No longer conflicts with filter1
-                               filter6: { selected: true, conflicted: false }
+                               group1__filter2: { selected: true },
+                               group1__filter3: { selected: true }
                        } ),
-                       'Selecting a non-conflicting filter from a conflicting group removes the conflict'
+                       'Simple case: Selecting a filter that is not in conflict removes the conflict.'
                );
+
        } );
 
        QUnit.test( 'Filter highlights', function ( assert ) {
                        'Highlight is enabled on toggle.'
                );
 
-               model.setHighlightColor( 'filter1', 'color1' );
-               model.setHighlightColor( 'filter2', 'color2' );
+               model.setHighlightColor( 'group1__filter1', 'color1' );
+               model.setHighlightColor( 'group1__filter2', 'color2' );
 
                assert.deepEqual(
                        model.getHighlightedItems().map( function ( item ) {
                                return item.getName();
                        } ),
                        [
-                               'filter1',
-                               'filter2'
+                               'group1__filter1',
+                               'group1__filter2'
                        ],
                        'Highlighted items are highlighted.'
                );
 
                assert.equal(
-                       model.getItemByName( 'filter1' ).getHighlightColor(),
+                       model.getItemByName( 'group1__filter1' ).getHighlightColor(),
                        'color1',
                        'Item highlight color is set.'
                );
 
-               model.setHighlightColor( 'filter1', 'color1changed' );
+               model.setHighlightColor( 'group1__filter1', 'color1changed' );
                assert.equal(
-                       model.getItemByName( 'filter1' ).getHighlightColor(),
+                       model.getItemByName( 'group1__filter1' ).getHighlightColor(),
                        'color1changed',
                        'Item highlight color is changed on setHighlightColor.'
                );
 
-               model.clearHighlightColor( 'filter1' );
+               model.clearHighlightColor( 'group1__filter1' );
                assert.deepEqual(
                        model.getHighlightedItems().map( function ( item ) {
                                return item.getName();
                        } ),
                        [
-                               'filter2'
+                               'group1__filter2'
                        ],
                        'Clear highlight from an item results in the item no longer being highlighted.'
                );
                model = new mw.rcfilters.dm.FiltersViewModel();
                model.initializeFilters( definition );
 
-               model.setHighlightColor( 'filter1', 'color1' );
-               model.setHighlightColor( 'filter2', 'color2' );
-               model.setHighlightColor( 'filter3', 'color3' );
+               model.setHighlightColor( 'group1__filter1', 'color1' );
+               model.setHighlightColor( 'group1__filter2', 'color2' );
+               model.setHighlightColor( 'group1__filter3', 'color3' );
 
                assert.deepEqual(
                        model.getHighlightedItems().map( function ( item ) {
                                return item.getName();
                        } ),
                        [
-                               'filter1',
-                               'filter2',
-                               'filter3'
+                               'group1__filter1',
+                               'group1__filter2',
+                               'group1__filter3'
                        ],
                        'Even if highlights are not enabled, the items remember their highlight state'
                        // NOTE: When actually displaying the highlights, the UI checks whether
                model = new mw.rcfilters.dm.FiltersViewModel();
                model.initializeFilters( definition );
 
-               model.setHighlightColor( 'filter1', 'color1' );
-               model.setHighlightColor( 'filter6', 'color6' );
+               model.setHighlightColor( 'group1__filter1', 'color1' );
+               model.setHighlightColor( 'group1__filter6', 'color6' );
 
                assert.deepEqual(
                        model.getHighlightedItems().map( function ( item ) {
                                return item.getName();
                        } ),
                        [
-                               'filter1'
+                               'group1__filter1'
                        ],
                        'Items without a specified class identifier are not highlighted.'
                );