Merge "Improve sorting on SpecialWanted*-Pages"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 9 Mar 2017 00:25:46 +0000 (00:25 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 9 Mar 2017 00:25:46 +0000 (00:25 +0000)
234 files changed:
CREDITS
RELEASE-NOTES-1.29
autoload.php
composer.json
docs/extension.schema.v2.json
includes/DefaultSettings.php
includes/EditPage.php
includes/Setup.php
includes/Title.php
includes/api/i18n/fr.json
includes/api/i18n/gl.json
includes/api/i18n/ko.json
includes/api/i18n/mk.json
includes/api/i18n/pl.json
includes/api/i18n/sv.json
includes/api/i18n/uk.json
includes/cache/FileCacheBase.php
includes/cache/MessageBlobStore.php
includes/cache/localisation/LocalisationCache.php
includes/collation/IcuCollation.php
includes/context/ContextSource.php
includes/exception/MWException.php
includes/export/XmlDumpWriter.php
includes/filerepo/ForeignAPIRepo.php
includes/filerepo/ForeignDBRepo.php
includes/filerepo/ForeignDBViaLBRepo.php
includes/filerepo/NullRepo.php
includes/filerepo/file/File.php
includes/filerepo/file/LocalFile.php
includes/htmlform/HTMLFormField.php
includes/htmlform/fields/HTMLCheckMatrix.php
includes/htmlform/fields/HTMLFormFieldCloner.php
includes/htmlform/fields/HTMLMultiSelectField.php
includes/installer/DatabaseUpdater.php
includes/installer/WebInstallerPage.php
includes/jobqueue/JobSpecification.php
includes/libs/filebackend/FileBackendMultiWrite.php
includes/libs/filebackend/FileBackendStore.php
includes/libs/filebackend/fileop/FileOp.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/WANObjectCache.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/resultwrapper/ResultWrapper.php
includes/libs/virtualrest/VirtualRESTService.php
includes/media/TransformationalImageHandler.php
includes/page/PageArchive.php [new file with mode: 0644]
includes/pager/IndexPager.php
includes/parser/MWTidy.php
includes/parser/Parser.php
includes/resourceloader/ResourceLoaderModule.php
includes/session/SessionProvider.php
includes/skins/BaseTemplate.php
includes/skins/Skin.php
includes/skins/SkinTemplate.php
includes/specialpage/QueryPage.php
includes/specialpage/RedirectSpecialPage.php
includes/specials/SpecialRecentchanges.php
includes/specials/SpecialUndelete.php
includes/tidy/RemexCompatFormatter.php [new file with mode: 0644]
includes/tidy/RemexCompatMunger.php [new file with mode: 0644]
includes/tidy/RemexDriver.php [new file with mode: 0644]
includes/tidy/RemexMungerData.php [new file with mode: 0644]
includes/tidy/TidyDriverBase.php
includes/title/NamespaceAwareForeignTitleFactory.php
languages/Language.php
languages/LanguageCode.php [new file with mode: 0644]
languages/LanguageConverter.php
languages/i18n/ar.json
languages/i18n/ast.json
languages/i18n/be-tarask.json
languages/i18n/bg.json
languages/i18n/bn.json
languages/i18n/br.json
languages/i18n/bs.json
languages/i18n/ca.json
languages/i18n/ckb.json
languages/i18n/cu.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/en-gb.json
languages/i18n/en.json
languages/i18n/es.json
languages/i18n/et.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/gl.json
languages/i18n/gsw.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/id.json
languages/i18n/it.json
languages/i18n/ka.json
languages/i18n/km.json
languages/i18n/ko.json
languages/i18n/mg.json
languages/i18n/mk.json
languages/i18n/nb.json
languages/i18n/nl.json
languages/i18n/pl.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/rue.json
languages/i18n/sh.json
languages/i18n/si.json
languages/i18n/sk.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/tr.json
languages/i18n/uk.json
languages/i18n/yue.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/messages/MessagesPnb.php
resources/Resources.php
resources/lib/oojs-ui/oojs-ui-apex.js
resources/lib/oojs-ui/oojs-ui-core-apex.css
resources/lib/oojs-ui/oojs-ui-core-mediawiki.css
resources/lib/oojs-ui/oojs-ui-core.js
resources/lib/oojs-ui/oojs-ui-mediawiki.js
resources/lib/oojs-ui/oojs-ui-toolbars-apex.css
resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css
resources/lib/oojs-ui/oojs-ui-toolbars.js
resources/lib/oojs-ui/oojs-ui-widgets-apex.css
resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css
resources/lib/oojs-ui/oojs-ui-widgets.js
resources/lib/oojs-ui/oojs-ui-windows-apex.css
resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css
resources/lib/oojs-ui/oojs-ui-windows.js
resources/lib/oojs-ui/themes/apex/icons-content.json
resources/lib/oojs-ui/themes/apex/icons-editing-styling.json
resources/lib/oojs-ui/themes/apex/icons-interactions.json
resources/lib/oojs-ui/themes/apex/icons-moderation.json
resources/lib/oojs-ui/themes/apex/icons.json
resources/lib/oojs-ui/themes/apex/images/icons/add-invert.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/add-invert.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/close.svg
resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/subtract.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/subtract.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/icons-editing-styling.json
resources/lib/oojs-ui/themes/mediawiki/icons-interactions.json
resources/lib/oojs-ui/themes/mediawiki/icons-moderation.json
resources/lib/oojs-ui/themes/mediawiki/icons.json
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.png [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.svg [deleted file]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/close.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.png
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.svg
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.png
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.svg
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.png
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.svg
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.png
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.svg
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.png
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.svg
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.png
resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.svg
resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.svg [new file with mode: 0644]
resources/src/mediawiki.action/mediawiki.action.edit.styles.css
resources/src/mediawiki.legacy/commonPrint.css
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
resources/src/mediawiki.skinning/content.externallinks.css
resources/src/mediawiki.skinning/elements.css
resources/src/mediawiki/mediawiki.inspect.js
resources/src/mediawiki/mediawiki.js
tests/parser/extraParserTests.txt
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/api/ApiTestCase.php
tests/phpunit/includes/filebackend/FileBackendTest.php
tests/phpunit/includes/filebackend/SwiftFileBackendTest.php
tests/phpunit/includes/tidy/RemexDriverTest.php [new file with mode: 0644]
tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php
tests/phpunit/languages/LanguageCodeTest.php [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.inspect.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.test.js

diff --git a/CREDITS b/CREDITS
index b37edf2..e8af23c 100644 (file)
--- a/CREDITS
+++ b/CREDITS
@@ -169,6 +169,7 @@ The following list can be found parsed under Special:Version/Credits -->
 * Ed Sanders
 * Edward Chernenko
 * Edward Z. Yang
+* Eddie Greiner-Petter
 * Elisabeth Bauer
 * Elliott Eggleston
 * Elvis Stansvik
index 9883474..5bc66fd 100644 (file)
@@ -63,6 +63,7 @@ production.
 * Updated cssjanus from v1.1.2 to 1.1.3.
 * Updated psr/log from v1.0.0 to v1.0.2.
 * Update Moment.js from v2.8.4 to v2.15.0.
+* Updated oyejorge/less.php from v1.7.0.10 to v1.7.0.13.
 
 ==== New external libraries ====
 
index 329ccb3..0f79323 100644 (file)
@@ -682,6 +682,7 @@ $wgAutoloadLocalClasses = [
        'LanguageBe_tarask' => __DIR__ . '/languages/classes/LanguageBe_tarask.php',
        'LanguageBg' => __DIR__ . '/languages/classes/LanguageBg.php',
        'LanguageBs' => __DIR__ . '/languages/classes/LanguageBs.php',
+       'LanguageCode' => __DIR__ . '/languages/LanguageCode.php',
        'LanguageConverter' => __DIR__ . '/languages/LanguageConverter.php',
        'LanguageCu' => __DIR__ . '/languages/classes/LanguageCu.php',
        'LanguageDsb' => __DIR__ . '/languages/classes/LanguageDsb.php',
@@ -918,6 +919,10 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Tidy\\RaggettInternalHHVM' => __DIR__ . '/includes/tidy/RaggettInternalHHVM.php',
        'MediaWiki\\Tidy\\RaggettInternalPHP' => __DIR__ . '/includes/tidy/RaggettInternalPHP.php',
        'MediaWiki\\Tidy\\RaggettWrapper' => __DIR__ . '/includes/tidy/RaggettWrapper.php',
+       'MediaWiki\\Tidy\\RemexCompatFormatter' => __DIR__ . '/includes/tidy/RemexCompatFormatter.php',
+       'MediaWiki\\Tidy\\RemexCompatMunger' => __DIR__ . '/includes/tidy/RemexCompatMunger.php',
+       'MediaWiki\\Tidy\\RemexDriver' => __DIR__ . '/includes/tidy/RemexDriver.php',
+       'MediaWiki\\Tidy\\RemexMungerData' => __DIR__ . '/includes/tidy/RemexMungerData.php',
        'MediaWiki\\Tidy\\TidyDriverBase' => __DIR__ . '/includes/tidy/TidyDriverBase.php',
        'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php',
        'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php',
@@ -1045,7 +1050,7 @@ $wgAutoloadLocalClasses = [
        'PackedImageGallery' => __DIR__ . '/includes/gallery/PackedImageGallery.php',
        'PackedOverlayImageGallery' => __DIR__ . '/includes/gallery/PackedOverlayImageGallery.php',
        'Page' => __DIR__ . '/includes/page/Page.php',
-       'PageArchive' => __DIR__ . '/includes/specials/SpecialUndelete.php',
+       'PageArchive' => __DIR__ . '/includes/page/PageArchive.php',
        'PageExists' => __DIR__ . '/maintenance/pageExists.php',
        'PageLangLogFormatter' => __DIR__ . '/includes/logging/PageLangLogFormatter.php',
        'PageProps' => __DIR__ . '/includes/PageProps.php',
index fe68a61..ce38914 100644 (file)
@@ -25,8 +25,8 @@
                "ext-xml": "*",
                "liuggio/statsd-php-client": "1.0.18",
                "mediawiki/at-ease": "1.1.0",
-               "oojs/oojs-ui": "0.19.4",
-               "oyejorge/less.php": "1.7.0.10",
+               "oojs/oojs-ui": "0.19.5",
+               "oyejorge/less.php": "1.7.0.13",
                "php": ">=5.5.9",
                "psr/log": "1.0.2",
                "wikimedia/assert": "0.2.2",
@@ -38,6 +38,7 @@
                "wikimedia/ip-set": "1.1.0",
                "wikimedia/php-session-serializer": "1.0.4",
                "wikimedia/relpath": "1.0.3",
+               "wikimedia/remex-html": "1.0.0",
                "wikimedia/running-stat": "1.1.0",
                "wikimedia/scoped-callback": "1.0.0",
                "wikimedia/utfnormal": "1.1.0",
index b7ee1a7..a2fdf65 100644 (file)
                                                "description": {
                                                        "type": ["string", "array"],
                                                        "description": "A description of the config setting, mostly for documentation/developers"
+                                               },
+                                               "decriptionmsg": {
+                                                       "type": "string",
+                                                       "description": "The message key which should be used as a description for this configuration option in a user interface. If empty, description will be used."
+                                               },
+                                               "public": {
+                                                       "type": "boolean",
+                                                       "default": false,
+                                                       "description": "Whether this configuration option and its value is allowed to be revealed in public or not."
                                                }
                                        }
                                }
index 3b08e07..dce6d3c 100644 (file)
@@ -2825,8 +2825,9 @@ $wgUsePrivateIPs = false;
  * MediaWiki out of the box. Not all languages listed there have translations,
  * see languages/messages/ for the list of languages with some localisation.
  *
- * Warning: Don't use language codes listed in $wgDummyLanguageCodes like "no"
- * for Norwegian (use "nb" instead), or things will break unexpectedly.
+ * Warning: Don't use any of MediaWiki's deprecated language codes listed in
+ * LanguageCode::getDeprecatedCodeMapping or $wgDummyLanguageCodes, like "no"
+ * for Norwegian (use "nb" instead). If you do, things will break unexpectedly.
  *
  * This defines the default interface language for all users, but users can
  * change it in their preferences.
@@ -2885,25 +2886,22 @@ $wgExtraInterlanguageLinkPrefixes = [];
 $wgExtraLanguageNames = [];
 
 /**
- * List of language codes that don't correspond to an actual language.
- * These codes are mostly left-offs from renames, or other legacy things.
- * This array makes them not appear as a selectable language on the installer,
- * and excludes them when running the transstat.php script.
- */
-$wgDummyLanguageCodes = [
-       'als' => 'gsw',
-       'bat-smg' => 'sgs',
-       'be-x-old' => 'be-tarask',
-       'bh' => 'bho',
-       'fiu-vro' => 'vro',
-       'no' => 'nb',
-       'qqq' => 'qqq', # Used for message documentation.
-       'qqx' => 'qqx', # Used for viewing message keys.
-       'roa-rup' => 'rup',
-       'simple' => 'en',
-       'zh-classical' => 'lzh',
-       'zh-min-nan' => 'nan',
-       'zh-yue' => 'yue',
+ * List of mappings from one language code to another.
+ * This array makes the codes not appear as a selectable language on the
+ * installer, and excludes them when running the transstat.php script.
+ *
+ * In Setup.php, the variable $wgDummyLanguageCodes is created by combining
+ * these codes with a list of "deprecated" codes, which are mostly leftovers
+ * from renames or other legacy things, and the internal codes 'qqq' and 'qqx'.
+ * If a mapping in $wgExtraLanguageCodes collide with a built-in mapping, the
+ * value in $wgExtraLanguageCodes will be used.
+ *
+ * @since 1.29
+ */
+$wgExtraLanguageCodes = [
+       'bh' => 'bho', // Bihari language family
+       'no' => 'nb', // Norwegian language family
+       'simple' => 'en', // Simple English
 ];
 
 /**
@@ -4240,7 +4238,8 @@ $wgAllowImageTag = false;
  *    - RaggettInternalPHP: Use the PECL extension
  *    - RaggettExternal: Shell out to an external binary (tidyBin)
  *    - Html5Depurate: Use external Depurate service
- *    - Html5Internal: Use the built-in HTML5 balancer
+ *    - Html5Internal: Use the Balancer library in PHP
+ *    - RemexHtml: Use the RemexHtml library in PHP
  *
  *  - tidyConfigFile: Path to configuration file for any of the Raggett drivers
  *  - debugComment: True to add a comment to the output with warning messages
@@ -8563,6 +8562,7 @@ $wgCSPFalsePositiveUrls = [
        'https://atpixel.alephd.com' => true,
        'https://rtb.metrigo.com' => true,
        'https://d5p.de17a.com' => true,
+       'https://ad.lkqd.net/vpaid/vpaid.js' => true,
 ];
 
 /**
index a02392e..c22125a 100644 (file)
@@ -363,8 +363,8 @@ class EditPage {
        /** @var bool */
        public $bot = true;
 
-       /** @var null|string */
-       public $contentModel = null;
+       /** @var string */
+       public $contentModel;
 
        /** @var null|string */
        public $contentFormat = null;
@@ -1255,11 +1255,7 @@ class EditPage {
                }
                $revision = $this->mArticle->getRevisionFetched();
                if ( $revision === null ) {
-                       if ( !$this->contentModel ) {
-                               throw new RuntimeException( 'EditPage contentModel was false' );
-                       }
                        $handler = ContentHandler::getForModelID( $this->contentModel );
-
                        return $handler->makeEmptyContent();
                }
                $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
@@ -1299,11 +1295,7 @@ class EditPage {
                $content = $rev ? $rev->getContent( Revision::RAW ) : null;
 
                if ( $content === false || $content === null ) {
-                       if ( !$this->contentModel ) {
-                               throw new RuntimeException( 'EditPage contentModel was false' );
-                       }
                        $handler = ContentHandler::getForModelID( $this->contentModel );
-
                        return $handler->makeEmptyContent();
                } elseif ( !$this->undidRev ) {
                        // Content models should always be the same since we error
index b61de73..e686cd8 100644 (file)
@@ -403,6 +403,14 @@ if ( is_array( $wgExtraNamespaces ) ) {
        $wgCanonicalNamespaceNames = $wgCanonicalNamespaceNames + $wgExtraNamespaces;
 }
 
+// Merge in the legacy language codes, unless overridden in the config
+if ( !isset( $wgDummyLanguageCodes ) ) {
+       $wgDummyLanguageCodes = [
+               'qqq' => 'qqq', // Used for message documentation
+               'qqx' => 'qqx', // Used for viewing message keys
+       ] + $wgExtraLanguageCodes + LanguageCode::getDeprecatedCodeMapping();
+}
+
 // These are now the same, always
 // To determine the user language, use $wgLang->getCode()
 $wgContLanguageCode = $wgLanguageCode;
index 13a6f56..3ed6b8b 100644 (file)
@@ -2549,6 +2549,29 @@ class Title implements LinkTarget {
         *   protection, or false if there's none.
         */
        public function getTitleProtection() {
+               $protection = $this->getTitleProtectionInternal();
+               if ( $protection ) {
+                       if ( $protection['permission'] == 'sysop' ) {
+                               $protection['permission'] = 'editprotected'; // B/C
+                       }
+                       if ( $protection['permission'] == 'autoconfirmed' ) {
+                               $protection['permission'] = 'editsemiprotected'; // B/C
+                       }
+               }
+               return $protection;
+       }
+
+       /**
+        * Fetch title protection settings
+        *
+        * To work correctly, $this->loadRestrictions() needs to have access to the
+        * actual protections in the database without munging 'sysop' =>
+        * 'editprotected' and 'autoconfirmed' => 'editsemiprotected'. Other
+        * callers probably want $this->getTitleProtection() instead.
+        *
+        * @return array|bool
+        */
+       protected function getTitleProtectionInternal() {
                // Can't protect pages in special namespaces
                if ( $this->getNamespace() < 0 ) {
                        return false;
@@ -2576,12 +2599,6 @@ class Title implements LinkTarget {
                        // fetchRow returns false if there are no rows.
                        $row = $dbr->fetchRow( $res );
                        if ( $row ) {
-                               if ( $row['permission'] == 'sysop' ) {
-                                       $row['permission'] = 'editprotected'; // B/C
-                               }
-                               if ( $row['permission'] == 'autoconfirmed' ) {
-                                       $row['permission'] = 'editsemiprotected'; // B/C
-                               }
                                $row['expiry'] = $dbr->decodeExpiry( $row['expiry'] );
                        }
                        $this->mTitleProtection = $row;
@@ -2979,7 +2996,7 @@ class Title implements LinkTarget {
 
                        $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
                } else {
-                       $title_protection = $this->getTitleProtection();
+                       $title_protection = $this->getTitleProtectionInternal();
 
                        if ( $title_protection ) {
                                $now = wfTimestampNow();
index da85e64..cfb1db8 100644 (file)
        "apihelp-query+langlinks-param-inlanguagecode": "Code de langue pour les noms de langue localisés.",
        "apihelp-query+langlinks-example-simple": "Obtenir les liens interlangue de la page <kbd>Main Page</kbd>.",
        "apihelp-query+links-description": "Renvoie tous les liens des pages fournies.",
-       "apihelp-query+links-param-namespace": "Afficher les liens uniquement dans ces espaces de nom.",
+       "apihelp-query+links-param-namespace": "Afficher les liens uniquement dans ces espaces de noms.",
        "apihelp-query+links-param-limit": "Combien de liens renvoyer.",
        "apihelp-query+links-param-titles": "Lister uniquement les liens vers ces titres. Utile pour vérifier si une certaine page a un lien vers un titre donné.",
        "apihelp-query+links-param-dir": "La direction dans laquelle lister.",
        "apihelp-query+links-example-simple": "Obtenir les liens de la page <kbd>Main Page</kbd>",
        "apihelp-query+links-example-generator": "Obtenir des informations sur tous les liens de page dans <kbd>Main Page</kbd>.",
-       "apihelp-query+links-example-namespaces": "Obtenir les liens de la page <kbd>Accueil</kbd> dans les espaces de nom {{ns:user}} et {{ns:template}}.",
+       "apihelp-query+links-example-namespaces": "Obtenir les liens de la page <kbd>Main Page</kbd> dans les espaces de nom {{ns:user}} et {{ns:template}}.",
        "apihelp-query+linkshere-description": "Trouver toutes les pages ayant un lien vers les pages données.",
        "apihelp-query+linkshere-param-prop": "Quelles propriétés obtenir :",
        "apihelp-query+linkshere-paramvalue-prop-pageid": "ID de chaque page.",
        "apihelp-query+linkshere-paramvalue-prop-title": "Titre de chaque page.",
        "apihelp-query+linkshere-paramvalue-prop-redirect": "Indique si la page est une redirection.",
-       "apihelp-query+linkshere-param-namespace": "Inclure uniquement les pages dans ces espaces de nom.",
+       "apihelp-query+linkshere-param-namespace": "Inclure uniquement les pages dans ces espaces de noms.",
        "apihelp-query+linkshere-param-limit": "Combien de résultats renvoyer.",
        "apihelp-query+linkshere-param-show": "Afficher uniquement les éléments qui correspondent à ces critères :\n;redirect:Afficher uniquement les redirections.\n;!redirect:Afficher uniquement les non-redirections.",
        "apihelp-query+linkshere-example-simple": "Obtenir une liste des pages liées à  [[Main Page]]",
        "apihelp-query+logevents-description": "Obtenir des événements des journaux.",
        "apihelp-query+logevents-param-prop": "Quelles propriétés obtenir :",
        "apihelp-query+logevents-paramvalue-prop-ids": "Ajoute l’ID de l’événement.",
-       "apihelp-query+logevents-paramvalue-prop-title": "Ajoute le titre de la page pour l’événement.",
-       "apihelp-query+logevents-paramvalue-prop-type": "Ajoute le type de l’événement.",
+       "apihelp-query+logevents-paramvalue-prop-title": "Ajoute le titre de la page pour l’événement enregistré.",
+       "apihelp-query+logevents-paramvalue-prop-type": "Ajoute le type de l’événement enregistré.",
        "apihelp-query+logevents-paramvalue-prop-user": "Ajoute l’utilisateur responsable de l’événement.",
        "apihelp-query+logevents-paramvalue-prop-userid": "Ajoute l’ID de l’utilisateur responsable de l’événement.",
        "apihelp-query+logevents-paramvalue-prop-timestamp": "Ajoute l’horodatage de l’événement.",
        "apihelp-query+logevents-paramvalue-prop-parsedcomment": "Ajoute le commentaire analysé de l’événement.",
        "apihelp-query+logevents-paramvalue-prop-details": "Liste les détails supplémentaires sur l’événement.",
        "apihelp-query+logevents-paramvalue-prop-tags": "Liste les balises de l’événement.",
-       "apihelp-query+logevents-param-type": "Filtrer les entrées du journal à ce seul type.",
-       "apihelp-query+logevents-param-action": "Filtrer les actions du journal à cette seule action. Écrase <var>$1type</var>. La présence d'une valeur avec un astérisque dans la liste, comme <var>$1type</var>, indique qu'une chaîne arbitraire peut être passée dans dans la requête à la place de l'astérisque.",
+       "apihelp-query+logevents-param-type": "Filtrer les entrées du journal sur ce seul type.",
+       "apihelp-query+logevents-param-action": "Filtrer les actions du journal sur cette seule action. Écrase <var>$1type</var>. Dans le liste des valeurs possibles, les valeurs suivies d'un astérisque, comme <kbd>action/*</kbd>, peuvent avoir différentes chaînes à la place du slash.",
        "apihelp-query+logevents-param-start": "L’horodatage auquel démarrer l’énumération.",
        "apihelp-query+logevents-param-end": "L’horodatage auquel arrêter l’énumération.",
        "apihelp-query+logevents-param-user": "Restreindre aux entrées générées par l’utilisateur spécifié.",
        "apihelp-query+logevents-param-title": "Restreindre aux entrées associées à une page donnée.",
-       "apihelp-query+logevents-param-namespace": "Restreindre aux entrées dans l’espace de nom spécifié.",
+       "apihelp-query+logevents-param-namespace": "Restreindre aux entrées dans l’espace de noms spécifié.",
        "apihelp-query+logevents-param-prefix": "Restreindre aux entrées commençant par ce préfixe.",
        "apihelp-query+logevents-param-tag": "Lister seulement les entrées ayant cette balise.",
        "apihelp-query+logevents-param-limit": "Combien d'entrées renvoyer au total.",
index f8e3f05..24cb77c 100644 (file)
        "apierror-invalidexpiry": "Hora de caducidade incorrecta \"$1\".",
        "apierror-invalid-file-key": "Non se corresponde cunha clave válida de ficheiro.",
        "apierror-invalidlang": "Código de lingua incorrecto para o parámetro <var>$1</var>.",
-       "apierror-invalidoldimage": "O parámetro oldimage ten un formato incorrecto.",
+       "apierror-invalidoldimage": "O parámetro <var>oldimage</var> ten un formato incorrecto.",
        "apierror-invalidparammix-cannotusewith": "O parámetro <kbd>$1</kbd> non pode usarse xunto con <kbd>$2</kbd>.",
        "apierror-invalidparammix-mustusewith": "O parámetro <kbd>$1</kbd> só pode usarse xunto con <kbd>$2</kbd>.",
        "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> non se pode combinar cos parámetros <var>oldid</var>, <var>pageid</var> e <var>page</var>. Por favor, utilice <var>title</var> e <var>text</var>.",
        "apierror-invalidparammix": "{{PLURAL:$2|Os parámetros}} $1 non poden usarse xuntos.",
-       "apierror-invalidsection": "O parámetro sección debe ser un ID de sección válido ou <kbd>new</kbd>.",
+       "apierror-invalidsection": "O parámetro <var>section</var> debe ser un ID de sección válido ou <kbd>new</kbd>.",
        "apierror-invalidsha1base36hash": "O código hash SHA1Base36 proporcionado non é correcto.",
        "apierror-invalidsha1hash": "O código hash SHA1 proporcionado non é correcto.",
        "apierror-invalidtitle": "Título incorrecto \"$1\".",
        "apiwarn-notfile": "\"$1\" non é un ficheiro.",
        "apiwarn-parse-nocontentmodel": "Non se proporcionou <var>title</var> nin <var>contentmodel</var>, asúmese $1.",
        "apiwarn-tokennotallowed": "A acción \"$1\" non está permitida para o usuario actual.",
-       "apiwarn-toomanyvalues": "Demasiados valores para o parámetro <var>$1</var>: o límite é $2.",
+       "apiwarn-toomanyvalues": "Demasiados valores para o parámetro <var>$1</var>. O límite é $2.",
        "apiwarn-truncatedresult": "Truncouse este resultado porque doutra maneira sobrepasaría o límite de $1 bytes.",
        "apiwarn-validationfailed-badpref": "non é unha preferencia válida.",
        "apiwarn-validationfailed-cannotset": "non pode ser establecido por este módulo.",
index 0be32a1..30dec54 100644 (file)
@@ -84,6 +84,8 @@
        "apihelp-createaccount-param-language": "사용자에게 기본으로 설정할 언어 코드. (선택 사항, 기본값으로는 본문의 언어입니다)",
        "apihelp-createaccount-example-pass": "사용자 <kbd>testuser</kbd>를 만들고 비밀번호를 <kbd>test123</kbd>으로 설정합니다.",
        "apihelp-createaccount-example-mail": "사용자 <kbd>testmailuser</kbd>를 만들고 자동 생성된 비밀번호를 이메일로 보냅니다.",
+       "apihelp-cspreport-description": "브라우저가 콘텐츠 보안 정책의 위반을 보고하기 위해 사용합니다. 이 모듈은 SCP를 준수하는 웹 브라우저에 의해 자동으로 사용될 때를 제외하고는 사용해서는 안 됩니다.",
+       "apihelp-cspreport-param-reportonly": "강제적 정책이 아닌, 모니터링 정책에서 나온 보고서인 것으로 표시합니다",
        "apihelp-delete-description": "문서 삭제",
        "apihelp-delete-param-title": "삭제할 문서의 제목. <var>$1pageid</var>과 함께 사용할 수 없습니다.",
        "apihelp-delete-param-pageid": "삭제할 문서의 ID. <var>$1title</var>과 함께 사용할 수 없습니다.",
        "apihelp-feedwatchlist-param-feedformat": "피드 포맷.",
        "apihelp-feedwatchlist-example-default": "주시문서 목록 피드를 보여줍니다.",
        "apihelp-filerevert-description": "파일을 이전 판으로 되돌립니다.",
+       "apihelp-filerevert-param-filename": "파일: 접두어가 없는 대상 파일 이름.",
        "apihelp-filerevert-param-comment": "업로드 댓글입니다.",
        "apihelp-filerevert-example-revert": "<kbd>Wiki.png</kbd>를 <kbd>2011-03-05T15:27:40Z</kbd> 판으로 되돌립니다.",
        "apihelp-help-description": "지정된 모듈의 도움말을 표시합니다.",
+       "apihelp-help-param-modules": "(<var>action</var>, <var>format</var> 변수의 값 또는 <kbd>main</kbd>)에 대한 도움말을 표시하는 모듈입니다. <kbd>+</kbd>로 하위 모듈을 지정할 수 있습니다.",
+       "apihelp-help-param-submodules": "명명된 모듈의 하위 모듈의 도움말을 포함합니다.",
+       "apihelp-help-param-recursivesubmodules": "하위 모듈의 도움말을 반복하여 포함합니다.",
        "apihelp-help-param-helpformat": "도움말 출력 포맷.",
+       "apihelp-help-param-wrap": "표준 API 응답 구조로 출력을 감쌉니다.",
+       "apihelp-help-param-toc": "HTML 출력에 목차를 포함합니다.",
        "apihelp-help-example-main": "메인 모듈의 도움말입니다.",
        "apihelp-help-example-recursive": "모든 도움말을 한 페이지로 모읍니다.",
+       "apihelp-help-example-help": "도움말 모듈 자체에 대한 도움말입니다.",
+       "apihelp-help-example-query": "2개의 쿼리 하위 모듈의 도움말입니다.",
        "apihelp-imagerotate-description": "하나 이상의 그림을 회전합니다.",
        "apihelp-imagerotate-param-rotation": "시계 방향으로 회전할 그림의 각도.",
        "apihelp-import-param-xml": "업로드한 XML 파일.",
        "apihelp-parse-example-page": "페이지의 구문을 분석합니다.",
        "apihelp-parse-example-text": "위키텍스트의 구문을 분석합니다.",
        "apihelp-parse-example-summary": "요약을 구문 분석합니다.",
+       "apihelp-patrol-param-rcid": "점검할 최근 바뀜 ID입니다.",
+       "apihelp-patrol-param-revid": "점검할 판 ID입니다.",
+       "apihelp-patrol-example-rcid": "최근의 변경사항을 점검합니다.",
        "apihelp-patrol-example-revid": "판을 점검합니다.",
        "apihelp-protect-description": "문서의 보호 수준을 변경합니다.",
        "apihelp-protect-param-reason": "보호 또는 보호 해제의 이유.",
        "apihelp-query+watchlist-paramvalue-prop-flags": "편집에 대한 플래그를 추가합니다.",
        "apihelp-query+watchlist-paramvalue-prop-loginfo": "적절한 곳에 로그 정보를 추가합니다.",
        "apihelp-removeauthenticationdata-description": "현재 사용자의 인증 데이터를 제거합니다.",
+       "apihelp-resetpassword-description": "비밀번호 재설정 이메일을 사용자에게 보냅니다.",
+       "apihelp-resetpassword-param-user": "재설정할 사용자입니다.",
+       "apihelp-resetpassword-param-email": "재설정할 사용자의 이메일 주소입니다.",
+       "apihelp-resetpassword-example-user": "사용자 <kbd>Example</kbd>에게 비밀번호 재설정 이메일을 보냅니다.",
+       "apihelp-resetpassword-example-email": "<kbd>user@example.com</kbd> 이메일 주소를 가진 모든 사용자에 대해 비밀번호 재설정 이메일을 보냅니다.",
        "apihelp-revisiondelete-description": "판을 삭제하거나 되살립니다.",
        "apihelp-revisiondelete-param-reason": "삭제 또는 복구 이유.",
        "apihelp-rollback-param-tags": "되돌리기를 적용하기 위해 태그합니다.",
        "apihelp-unblock-param-userid": "차단을 해제할 사용자 ID입니다. <var>$1id</var> 또는 <var>$1user</var>와(과) 함께 사용할 수 없습니다.",
        "apihelp-unblock-param-reason": "차단 해제 이유.",
        "apihelp-unblock-param-tags": "차단 기록의 항목에 적용할 태그를 변경합니다.",
+       "apihelp-upload-param-filename": "대상 파일 이름.",
        "apihelp-upload-param-ignorewarnings": "모든 경고를 무시합니다.",
        "apihelp-userrights-param-user": "사용자 이름.",
        "apihelp-userrights-param-userid": "사용자 ID.",
index 0d903ed..3045332 100644 (file)
@@ -79,7 +79,7 @@
        "apihelp-edit-param-tags": "Ознаки за измена што се однесуваат на преработката.",
        "apihelp-edit-param-minor": "Ситно уредување.",
        "apihelp-edit-param-notminor": "Неситно уредување.",
-       "apihelp-edit-param-bot": "Означи го уредувањево како ботско.",
+       "apihelp-edit-param-bot": "Означи го уредувањево како ботовско.",
        "apihelp-edit-param-basetimestamp": "Датум и време на преработката на базата, кои се користат за утврдување на спротиставености во уредувањето. Може да се добие преку [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].",
        "apihelp-edit-param-starttimestamp": "Датум и време кога сте почнало уредувањето, кои се користат за утврдување на спротиставености во уредувањата. Соодветната вредност се добива користејќи <var>[[Special:ApiHelp/main|curtimestamp]]</var> кога ќе почнете со уредување (на пр. кога ќе се вчита содржината што ќе ја уредувате).",
        "apihelp-edit-param-recreate": "Занемари ги грешките што се појавуваат во врска со страницата што е избришана во меѓувреме.",
index 86ac582..91e3e50 100644 (file)
        "apihelp-query+iwbacklinks-param-limit": "Łączna liczba stron do zwrócenia.",
        "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "Dodaje prefiks interwiki.",
        "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "Dodaje tytuł interwiki.",
+       "apihelp-query+iwlinks-description": "Wyświetla wszystkie liki interwiki z danych stron.",
        "apihelp-query+iwlinks-paramvalue-prop-url": "Dodaje pełny adres URL.",
        "apihelp-query+iwlinks-param-limit": "Łączna liczba linków interwiki do zwrócenia.",
        "apihelp-query+langbacklinks-param-limit": "Łączna liczba stron do zwrócenia.",
        "apihelp-query+langbacklinks-paramvalue-prop-lllang": "Dodaje kod języka linku językowego.",
        "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "Dodaje tytuł linku językowego.",
        "apihelp-query+langlinks-paramvalue-prop-url": "Dodaje pełny adres URL.",
+       "apihelp-query+links-description": "Zwraca wszystkie linki z danych stron.",
+       "apihelp-query+links-param-namespace": "Pokaż linki tylko w tych przestrzeniach nazw.",
        "apihelp-query+links-param-limit": "Liczba linków do zwrócenia.",
        "apihelp-query+linkshere-description": "Znajdź wszystkie strony, które linkują do danych stron.",
        "apihelp-query+linkshere-paramvalue-prop-title": "Nazwa każdej strony.",
        "apihelp-query+linkshere-param-limit": "Liczba do zwrócenia.",
        "apihelp-query+logevents-description": "Pobierz eventy z logu.",
        "apihelp-query+logevents-example-simple": "Lista ostatnich zarejestrowanych zdarzeń.",
+       "apihelp-query+pagepropnames-param-limit": "Maksymalna liczba zwracanych nazw.",
        "apihelp-query+pageswithprop-param-prop": "Jakie informacje dołączyć:",
        "apihelp-query+pageswithprop-paramvalue-prop-ids": "Doda ID strony.",
+       "apihelp-query+pageswithprop-paramvalue-prop-value": "Dodaje wartość właściwości strony.",
        "apihelp-query+pageswithprop-param-limit": "Maksymalna liczba zwracanych stron.",
        "apihelp-query+pageswithprop-param-dir": "W jakim kierunku sortować.",
        "apihelp-query+pageswithprop-example-generator": "Pobierz dodatkowe informacje o pierwszych 10 stronach wykorzystując <code>_&#95;NOTOC_&#95;</code>.",
        "apihelp-query+tags-paramvalue-prop-active": "Czy znacznik jest nadal stosowany.",
        "apihelp-query+tags-example-simple": "Lista dostęnych tagów.",
        "apihelp-query+templates-description": "Zwraca wszystkie strony osadzone w danych stronach.",
+       "apihelp-query+templates-param-namespace": "Pokaż szablony tylko w tych przestrzeniach nazw.",
        "apihelp-query+templates-param-limit": "Ile szablonów zwrócić?",
        "apihelp-query+transcludedin-paramvalue-prop-title": "Nazwa każdej strony.",
        "apihelp-query+transcludedin-paramvalue-prop-redirect": "Oznacz, jeśli strona jest przekierowaniem.",
        "apihelp-stashedit-param-title": "Tytuł edytowanej strony.",
        "apihelp-stashedit-param-sectiontitle": "Tytuł nowej sekcji.",
        "apihelp-stashedit-param-text": "Zawartość strony.",
+       "apihelp-stashedit-param-summary": "Opis zmian.",
        "apihelp-tag-param-reason": "Powód zmiany.",
        "apihelp-unblock-description": "Odblokuj użytkownika.",
        "apihelp-unblock-param-user": "Nazwa użytkownika, adres IP albo zakres adresów IP, które chcesz odblokować. Nie można używać jednocześnie z <var>$1id</var> lub <var>$1userid</var>.",
        "apierror-integeroutofrange-belowminimum": "Wartość <var>$1</var> nie może być mniejsza niż $2 (ustawiono $3).",
        "apierror-invalidcategory": "Wprowadzona nazwa kategorii jest nieprawidłowa.",
        "apierror-invalidlang": "Nieprawidłowy kod języka dla parametru <var>$1</var>.",
+       "apierror-invalidoldimage": "Parametr <var>oldimage</var> ma nieprawidłowy format.",
        "apierror-invalidparammix": "{{PLURAL:$2|Parametry}} $1 nie mogą być używane razem.",
        "apierror-invalidtitle": "Zły tytuł „$1”.",
        "apierror-invalidurlparam": "Nieprawidłowa wartość <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
index 5498449..1396458 100644 (file)
        "api-help-param-multi-separate": "Separera värden med <kbd>|</kbd> eller [[Special:ApiHelp/main#main/datatypes|alternativ]].",
        "apierror-articleexists": "Artikeln du försökte skapa har redan skapats.",
        "apierror-baddiff": "Diff kan inte hämtas. En eller båda sidversioner finns inte eller du har inte behörighet för att visa dem.",
+       "apierror-invalidsection": "Parametern <var>section</var> måste vara ett giltigt avsnitts-ID eller <kbd>new</kbd>.",
        "apierror-nosuchuserid": "Det finns ingen användare med ID $1.",
        "apierror-protect-invalidaction": "Ogiltig skyddstyp \"$1\".",
        "apierror-systemblocked": "Du har blockerats automatiskt av MediaWiki.",
index e3dba0b..996f26e 100644 (file)
        "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Видає інформацію щодо прав (ліцензії) вікі, якщо наявна.",
        "apihelp-query+siteinfo-paramvalue-prop-restrictions": "Видає інформацію про наявні типи обмежень (захисту).",
        "apihelp-query+siteinfo-paramvalue-prop-languages": "Видає список мов, які підтримує MediaWiki (за бажанням локалізовані через <var>$1inlanguagecode</var>).",
+       "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "Виводить список кодів мов, для яких увімкнено [[mw:LanguageConverter|LanguageConverter]], а також варіанти, підтримувані кожною з цих мов.",
        "apihelp-query+siteinfo-paramvalue-prop-skins": "Видає список усіх доступних тем оформлення (опціонально локалізовані з використанням <var>$1inlanguagecode</var>, в іншому разі — мовою вмісту).",
        "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "Видає список теґів розширення парсеру.",
        "apihelp-query+siteinfo-paramvalue-prop-functionhooks": "Видає список гуків парсерних функцій.",
        "apierror-invalidexpiry": "Недійсний час завершення «$1».",
        "apierror-invalid-file-key": "Недійсний ключ файлу.",
        "apierror-invalidlang": "Недійсний код мови для параметра <var>$1</var>.",
-       "apierror-invalidoldimage": "Параметр «oldimage» має недійсний формат.",
+       "apierror-invalidoldimage": "Параметр <var>oldimage</var> має недійсний формат.",
        "apierror-invalidparammix-cannotusewith": "Параметр <kbd>$1</kbd> не можна використовувати з <kbd>$2</kbd>.",
        "apierror-invalidparammix-mustusewith": "Параметр <kbd>$1</kbd> можна використовувати тільки з  <kbd>$2</kbd>.",
        "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> не можна поєднувати з параметрами <var>oldid</var>, <var>pageid</var> чи <var>page</var>. Будь ласка, використовуйте <var>title</var> і <var>text</var>.",
        "apierror-invalidparammix": "{{PLURAL:$2|Ці параметри}} $1 не можна використовувати водночас.",
-       "apierror-invalidsection": "Параметр «section» має бути дійсним ідентифікатором розділу або <kbd>new</kbd>.",
+       "apierror-invalidsection": "Параметр <var>section</var> має бути дійсним ідентифікатором розділу або <kbd>new</kbd>.",
        "apierror-invalidsha1base36hash": "Поданий хеш SHA1Base36 недійсний.",
        "apierror-invalidsha1hash": "Поданий хеш SHA1 недійсний.",
        "apierror-invalidtitle": "Погана назва «$1».",
        "apiwarn-redirectsandrevids": "Вирішення перенаправлень не може використовуватись разом з параметром <var>revids</var>. Усі перенаправлення, на які вказує <var>revids</var>, не було вирішено.",
        "apiwarn-tokennotallowed": "Дія «$1» недозволена для поточного користувача.",
        "apiwarn-tokens-origin": "Токени не можна отримати, поки не застосована політика одного походження.",
-       "apiwarn-toomanyvalues": "Надто багато значень задано для параметра <var>$1</var>: ліміт становить $2.",
+       "apiwarn-toomanyvalues": "Надто багато значень задано для параметра <var>$1</var>. Ліміт становить $2.",
        "apiwarn-truncatedresult": "Цей результат було скорочено, оскільки інакше він перевищив би ліміт у $1 байтів.",
        "apiwarn-unclearnowtimestamp": "Вказування «$2» для параметра мітки часу <var>$1</var> є застарілим. Якщо з якоїсь причини Вам треба чітко вказати поточний час без вираховування його з боку клієнта, використайте <kbd>now</kbd>.",
        "apiwarn-unrecognizedvalues": "{{PLURAL:$3|Нерозпізнане|Нерозпізнані}} значення для параметра <var>$1</var>: $2.",
index 6d5f8c3..0a302b6 100644 (file)
@@ -242,7 +242,7 @@ abstract class FileCacheBase {
                                : IP::sanitizeRange( "$ip/16" );
 
                        # Bail out if a request already came from this range...
-                       $key = wfMemcKey( get_class( $this ), 'attempt', $this->mType, $this->mKey, $ip );
+                       $key = wfMemcKey( static::class, 'attempt', $this->mType, $this->mKey, $ip );
                        if ( $cache->get( $key ) ) {
                                return; // possibly the same user
                        }
@@ -272,6 +272,6 @@ abstract class FileCacheBase {
         * @return string
         */
        protected function cacheMissKey() {
-               return wfMemcKey( get_class( $this ), 'misses', $this->mType, $this->mKey );
+               return wfMemcKey( static::class, 'misses', $this->mType, $this->mKey );
        }
 }
index 90ad241..5d48c03 100644 (file)
@@ -110,9 +110,6 @@ class MessageBlobStore implements LoggerAwareInterface {
                foreach ( $modules as $name => $module ) {
                        $key = $cacheKeys[$name];
                        if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) {
-                               $this->logger->info( 'Message blob cache-miss for {module}',
-                                       [ 'module' => $name, 'cacheKey' => $key ]
-                               );
                                $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang );
                        } else {
                                // Use unexpired cache
index 90b3de1..cbff113 100644 (file)
@@ -228,7 +228,7 @@ class LocalisationCache {
                        }
                }
 
-               wfDebugLog( 'caches', get_class( $this ) . ": using store $storeClass" );
+               wfDebugLog( 'caches', static::class . ": using store $storeClass" );
                if ( !empty( $conf['storeDirectory'] ) ) {
                        $storeConf['directory'] = $conf['storeDirectory'];
                }
index bf1fe74..e0eb1c1 100644 (file)
@@ -330,7 +330,7 @@ class IcuCollation extends Collation {
                        $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
                        $cacheKey = $cache->makeKey(
                                'first-letters',
-                               get_class( $this ),
+                               static::class,
                                $this->locale,
                                $this->digitTransformLanguage->getCode(),
                                self::getICUVersion(),
index 829dd73..ea5278f 100644 (file)
@@ -39,7 +39,7 @@ abstract class ContextSource implements IContextSource {
         */
        public function getContext() {
                if ( $this->context === null ) {
-                       $class = get_class( $this );
+                       $class = static::class;
                        wfDebug( __METHOD__ . " ($class): called and \$context is null. " .
                                "Using RequestContext::getMain() for sanity\n" );
                        $this->context = RequestContext::getMain();
index e958c94..4ff8636 100644 (file)
@@ -112,7 +112,7 @@ class MWException extends Exception {
                        "</p>\n";
                } else {
                        $logId = WebRequest::getRequestId();
-                       $type = get_class( $this );
+                       $type = static::class;
                        return "<div class=\"errorbox\">" .
                        '[' . $logId . '] ' .
                        gmdate( 'Y-m-d H:i:s' ) . ": " .
@@ -164,7 +164,7 @@ class MWException extends Exception {
                if ( $this->useOutputPage() ) {
                        $wgOut->prepareErrorPage( $this->getPageTitle() );
 
-                       $hookResult = $this->runHooks( get_class( $this ) );
+                       $hookResult = $this->runHooks( static::class );
                        if ( $hookResult ) {
                                $wgOut->addHTML( $hookResult );
                        } else {
@@ -183,7 +183,7 @@ class MWException extends Exception {
                                '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
                                "</head><body>\n";
 
-                       $hookResult = $this->runHooks( get_class( $this ) . 'Raw' );
+                       $hookResult = $this->runHooks( static::class . 'Raw' );
                        if ( $hookResult ) {
                                echo $hookResult;
                        } else {
@@ -203,7 +203,7 @@ class MWException extends Exception {
 
                if ( defined( 'MW_API' ) ) {
                        // Unhandled API exception, we can't be sure that format printer is alive
-                       self::header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $this ) );
+                       self::header( 'MediaWiki-API-Error: internal_api_error_' . static::class );
                        wfHttpError( 500, 'Internal Server Error', $this->getText() );
                } elseif ( self::isCommandLine() ) {
                        $message = $this->getText();
index 52bf0f0..5a1f92c 100644 (file)
@@ -433,6 +433,9 @@ class XmlDumpWriter {
                global $wgContLang;
                $prefix = $wgContLang->getFormattedNsText( $title->getNamespace() );
 
+               // @todo Emit some kind of warning to the user if $title->getNamespace() !==
+               // NS_MAIN and $prefix === '' (viz. pages in an unregistered namespace)
+
                if ( $prefix !== '' ) {
                        $prefix .= ':';
                }
index ca41718..43f1d21 100644 (file)
@@ -571,7 +571,7 @@ class ForeignAPIRepo extends FileRepo {
 
                $cache = ObjectCache::getMainWANInstance();
                return $cache->getWithSetCallback(
-                       $this->getLocalCacheKey( get_class( $this ), $target, md5( $url ) ),
+                       $this->getLocalCacheKey( static::class, $target, md5( $url ) ),
                        $cacheTTL,
                        function ( $curValue, &$ttl ) use ( $url, $cache ) {
                                $html = self::httpGet( $url, 'default', [], $mtime );
@@ -593,13 +593,13 @@ class ForeignAPIRepo extends FileRepo {
         * @throws MWException
         */
        function enumFiles( $callback ) {
-               throw new MWException( 'enumFiles is not supported by ' . get_class( $this ) );
+               throw new MWException( 'enumFiles is not supported by ' . static::class );
        }
 
        /**
         * @throws MWException
         */
        protected function assertWritableRepo() {
-               throw new MWException( get_class( $this ) . ': write operations are not supported.' );
+               throw new MWException( static::class . ': write operations are not supported.' );
        }
 }
index f49ef88..3e88508 100644 (file)
@@ -138,7 +138,7 @@ class ForeignDBRepo extends LocalRepo {
        }
 
        protected function assertWritableRepo() {
-               throw new MWException( get_class( $this ) . ': write operations are not supported.' );
+               throw new MWException( static::class . ': write operations are not supported.' );
        }
 
        /**
index a9cd030..f83fd1c 100644 (file)
@@ -100,7 +100,7 @@ class ForeignDBViaLBRepo extends LocalRepo {
        }
 
        protected function assertWritableRepo() {
-               throw new MWException( get_class( $this ) . ': write operations are not supported.' );
+               throw new MWException( static::class . ': write operations are not supported.' );
        }
 
        public function getInfo() {
index f2b7395..1c12e02 100644 (file)
@@ -33,6 +33,6 @@ class NullRepo extends FileRepo {
        }
 
        protected function assertWritableRepo() {
-               throw new MWException( get_class( $this ) . ': write operations are not supported.' );
+               throw new MWException( static::class . ': write operations are not supported.' );
        }
 }
index 3b873ea..f7e85a8 100644 (file)
@@ -1766,7 +1766,7 @@ abstract class File implements IDBAccessObject {
         * @throws MWException
         */
        function readOnlyError() {
-               throw new MWException( get_class( $this ) . ': write operations are not supported' );
+               throw new MWException( static::class . ': write operations are not supported' );
        }
 
        /**
index a1cb0a2..a633fd2 100644 (file)
@@ -391,7 +391,7 @@ class LocalFile extends File {
         * @param int $flags
         */
        function loadFromDB( $flags = 0 ) {
-               $fname = get_class( $this ) . '::' . __FUNCTION__;
+               $fname = static::class . '::' . __FUNCTION__;
 
                # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
                $this->dataLoaded = true;
@@ -416,7 +416,7 @@ class LocalFile extends File {
         * This covers fields that are sometimes not cached.
         */
        protected function loadExtraFromDB() {
-               $fname = get_class( $this ) . '::' . __FUNCTION__;
+               $fname = static::class . '::' . __FUNCTION__;
 
                # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
                $this->extraDataLoaded = true;
@@ -1100,7 +1100,7 @@ class LocalFile extends File {
         */
        public function nextHistoryLine() {
                # Polymorphic function name to distinguish foreign and local fetches
-               $fname = get_class( $this ) . '::' . __FUNCTION__;
+               $fname = static::class . '::' . __FUNCTION__;
 
                $dbr = $this->repo->getReplicaDB();
 
index 487d6f6..3a3146b 100644 (file)
@@ -475,7 +475,7 @@ abstract class HTMLFormField {
        public function getTableRow( $value ) {
                list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
                $inputHtml = $this->getInputHTML( $value );
-               $fieldType = get_class( $this );
+               $fieldType = static::class;
                $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
                $cellAttributes = [];
                $rowAttributes = [];
@@ -533,7 +533,7 @@ abstract class HTMLFormField {
        public function getDiv( $value ) {
                list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
                $inputHtml = $this->getInputHTML( $value );
-               $fieldType = get_class( $this );
+               $fieldType = static::class;
                $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
                $cellAttributes = [];
                $label = $this->getLabelHtml( $cellAttributes );
@@ -601,7 +601,7 @@ abstract class HTMLFormField {
                        $infusable = false;
                }
 
-               $fieldType = get_class( $this );
+               $fieldType = static::class;
                $help = $this->getHelpText();
                $errors = $this->getErrorsRaw( $value );
                foreach ( $errors as &$error ) {
index 46172a5..fa18a3c 100644 (file)
@@ -189,7 +189,7 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
        public function getTableRow( $value ) {
                list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
                $inputHtml = $this->getInputHTML( $value );
-               $fieldType = get_class( $this );
+               $fieldType = static::class;
                $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
                $cellAttributes = [ 'colspan' => 2 ];
 
index 8fb840a..dd9184b 100644 (file)
@@ -46,7 +46,7 @@ class HTMLFormFieldCloner extends HTMLFormField {
        protected $uniqueId;
 
        public function __construct( $params ) {
-               $this->uniqueId = get_class( $this ) . ++self::$counter . 'x';
+               $this->uniqueId = static::class . ++self::$counter . 'x';
                parent::__construct( $params );
 
                if ( empty( $this->mParams['fields'] ) || !is_array( $this->mParams['fields'] ) ) {
index 23044bd..2b6e066 100644 (file)
@@ -17,6 +17,11 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
        public function __construct( $params ) {
                parent::__construct( $params );
 
+               // If the disabled-options parameter is not provided, use an empty array
+               if ( isset( $this->mParams['disabled-options'] ) === false ) {
+                       $this->mParams['disabled-options'] = [];
+               }
+
                // For backwards compatibility, also handle the old way with 'cssclass' => 'mw-chosen'
                if ( isset( $params['dropdown'] ) || strpos( $this->mClass, 'mw-chosen' ) !== false ) {
                        $this->mClass .= ' mw-htmlform-dropdown';
@@ -75,6 +80,9 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
                                        'id' => "{$this->mID}-$info",
                                        'value' => $info,
                                ];
+                               if ( in_array( $info, $this->mParams['disabled-options'], true ) ) {
+                                       $thisAttribs['disabled'] = 'disabled';
+                               }
                                $checked = in_array( $info, $value, true );
 
                                $checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs, $label );
@@ -112,6 +120,18 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
                }
        }
 
+       /**
+        * Get options and make them into arrays suitable for OOUI.
+        * @return array Options for inclusion in a select or whatever.
+        */
+       public function getOptionsOOUI() {
+               $options = parent::getOptionsOOUI();
+               foreach ( $options as &$option ) {
+                       $option['disabled'] = in_array( $option['data'], $this->mParams['disabled-options'], true );
+               }
+               return $options;
+       }
+
        /**
         * Get the OOUI version of this field.
         *
index caaab46..f8ab1f2 100644 (file)
@@ -59,6 +59,11 @@ abstract class DatabaseUpdater {
         */
        protected $db;
 
+       /**
+        * @var Maintenance
+        */
+       protected $maintenance;
+
        protected $shared = false;
 
        /**
index 7a41ceb..3aad6f8 100644 (file)
@@ -133,7 +133,7 @@ abstract class WebInstallerPage {
         * @return string
         */
        public function getName() {
-               return str_replace( 'WebInstaller', '', get_class( $this ) );
+               return str_replace( 'WebInstaller', '', static::class );
        }
 
        /**
index d636dc6..d844795 100644 (file)
@@ -128,7 +128,7 @@ class JobSpecification implements IJobSpecification {
 
                $this->type = $type;
                $this->params = $params;
-               $this->title = $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . get_class( $this ) );
+               $this->title = $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . static::class );
                $this->opts = $opts;
        }
 
index 212e84f..53bce33 100644 (file)
@@ -167,7 +167,7 @@ class FileBackendMultiWrite extends FileBackend {
                // Do a consistency check to see if the backends are consistent...
                $syncStatus = $this->consistencyCheck( $relevantPaths );
                if ( !$syncStatus->isOK() ) {
-                       wfDebugLog( 'FileOperation', get_class( $this ) .
+                       wfDebugLog( 'FileOperation', static::class .
                                " failed sync check: " . FormatJson::encode( $relevantPaths ) );
                        // Try to resync the clone backends to the master on the spot...
                        if ( $this->autoResync === false
@@ -378,7 +378,7 @@ class FileBackendMultiWrite extends FileBackend {
                }
 
                if ( !$status->isOK() ) {
-                       wfDebugLog( 'FileOperation', get_class( $this ) .
+                       wfDebugLog( 'FileOperation', static::class .
                                " failed to resync: " . FormatJson::encode( $paths ) );
                }
 
index 5179477..7cb26c6 100644 (file)
@@ -360,7 +360,7 @@ abstract class FileBackendStore extends FileBackend {
                        $status->merge( $this->doConcatenate( $params ) );
                        $sec = microtime( true ) - $start_time;
                        if ( !$status->isOK() ) {
-                               $this->logger->error( get_class( $this ) . "-{$this->name}" .
+                               $this->logger->error( static::class . "-{$this->name}" .
                                        " failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" );
                        }
                }
@@ -1123,7 +1123,7 @@ abstract class FileBackendStore extends FileBackend {
                                $subStatus->success[$i] = false;
                                ++$subStatus->failCount;
                        }
-                       $this->logger->error( get_class( $this ) . "-{$this->name} " .
+                       $this->logger->error( static::class . "-{$this->name} " .
                                " stat failure; aborted operations: " . FormatJson::encode( $ops ) );
                }
 
index fab5a37..79af194 100644 (file)
@@ -461,7 +461,7 @@ abstract class FileOp {
                $params = $this->params;
                $params['failedAction'] = $action;
                try {
-                       $this->logger->error( get_class( $this ) .
+                       $this->logger->error( static::class .
                                " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
                } catch ( Exception $e ) {
                        // bad config? debug log error?
index d0b68bc..77c4259 100644 (file)
@@ -679,7 +679,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        protected function debug( $text ) {
                if ( $this->debugMode ) {
                        $this->logger->debug( "{class} debug: $text", [
-                               'class' => get_class( $this ),
+                               'class' => static::class,
                        ] );
                }
        }
index 75c79a9..f0a439a 100644 (file)
@@ -44,15 +44,20 @@ use Psr\Log\NullLogger;
  *
  * The simplest purge method is delete().
  *
- * There are two supported ways to handle broadcasted operations:
+ * There are three supported ways to handle broadcasted operations:
  *   - a) Configure the 'purge' EventRelayer to point to a valid PubSub endpoint
- *        that has subscribed listeners on the cache servers applying the cache updates.
+ *         that has subscribed listeners on the cache servers applying the cache updates.
  *   - b) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
- *        and set up mcrouter as the underlying cache backend, using one of the memcached
- *        BagOStuff classes as 'cache'. Use OperationSelectorRoute in the mcrouter settings
- *        to configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and
- *        configure other operations to go to the local DC via PoolRoute (for reference,
- *        see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles).
+ *         and set up mcrouter as the underlying cache backend, using one of the memcached
+ *         BagOStuff classes as 'cache'. Use OperationSelectorRoute in the mcrouter settings
+ *         to configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and
+ *         configure other operations to go to the local DC via PoolRoute (for reference,
+ *         see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles).
+ *   - c) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
+ *         and set up dynomite as cache middleware between the web servers and either
+ *         memcached or redis. This will also broadcast all key setting operations, not just purges,
+ *         which can be useful for cache warming. Writes are eventually consistent via the
+ *         Dynamo replication model (see https://github.com/Netflix/dynomite).
  *
  * Broadcasted operations like delete() and touchCheckKey() are done asynchronously
  * in all datacenters this way, though the local one should likely be near immediate.
index c5afe1e..e807bc8 100644 (file)
@@ -3412,7 +3412,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        public function __clone() {
                $this->connLogger->warning(
-                       "Cloning " . get_class( $this ) . " is not recomended; forking connection:\n" .
+                       "Cloning " . static::class . " is not recomended; forking connection:\n" .
                        ( new RuntimeException() )->getTraceAsString()
                );
 
index a76d66a..d658c96 100644 (file)
@@ -82,7 +82,7 @@ class ResultWrapper implements IResultWrapper {
         */
        private function getDB() {
                if ( !$this->db ) {
-                       throw new RuntimeException( get_class( $this ) . ' needs a DB handle for iteration.' );
+                       throw new RuntimeException( static::class . ' needs a DB handle for iteration.' );
                }
 
                return $this->db;
index ccb14db..2f16078 100644 (file)
@@ -51,8 +51,7 @@ abstract class VirtualRESTService {
         * @return string The name of the service behind this VRS object.
         */
        public function getName() {
-               return isset( $this->params['name'] ) ? $this->params['name'] :
-                       get_class( $this );
+               return isset( $this->params['name'] ) ? $this->params['name'] : static::class;
        }
 
        /**
index 60aec45..1ab0f36 100644 (file)
@@ -569,7 +569,7 @@ abstract class TransformationalImageHandler extends ImageHandler {
         */
        public function rotate( $file, $params ) {
                return new MediaTransformError( 'thumbnail_error', 0, 0,
-                       get_class( $this ) . ' rotation not implemented' );
+                       static::class . ' rotation not implemented' );
        }
 
        /**
diff --git a/includes/page/PageArchive.php b/includes/page/PageArchive.php
new file mode 100644 (file)
index 0000000..388e693
--- /dev/null
@@ -0,0 +1,743 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\ResultWrapper;
+
+/**
+ * Used to show archived pages and eventually restore them.
+ */
+class PageArchive {
+       /** @var Title */
+       protected $title;
+
+       /** @var Status */
+       protected $fileStatus;
+
+       /** @var Status */
+       protected $revisionStatus;
+
+       /** @var Config */
+       protected $config;
+
+       public function __construct( $title, Config $config = null ) {
+               if ( is_null( $title ) ) {
+                       throw new MWException( __METHOD__ . ' given a null title.' );
+               }
+               $this->title = $title;
+               if ( $config === null ) {
+                       wfDebug( __METHOD__ . ' did not have a Config object passed to it' );
+                       $config = MediaWikiServices::getInstance()->getMainConfig();
+               }
+               $this->config = $config;
+       }
+
+       public function doesWrites() {
+               return true;
+       }
+
+       /**
+        * List all deleted pages recorded in the archive table. Returns result
+        * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
+        * namespace/title.
+        *
+        * @return ResultWrapper
+        */
+       public static function listAllPages() {
+               $dbr = wfGetDB( DB_REPLICA );
+
+               return self::listPages( $dbr, '' );
+       }
+
+       /**
+        * List deleted pages recorded in the archive table matching the
+        * given title prefix.
+        * Returns result wrapper with (ar_namespace, ar_title, count) fields.
+        *
+        * @param string $prefix Title prefix
+        * @return ResultWrapper
+        */
+       public static function listPagesByPrefix( $prefix ) {
+               $dbr = wfGetDB( DB_REPLICA );
+
+               $title = Title::newFromText( $prefix );
+               if ( $title ) {
+                       $ns = $title->getNamespace();
+                       $prefix = $title->getDBkey();
+               } else {
+                       // Prolly won't work too good
+                       // @todo handle bare namespace names cleanly?
+                       $ns = 0;
+               }
+
+               $conds = [
+                       'ar_namespace' => $ns,
+                       'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
+               ];
+
+               return self::listPages( $dbr, $conds );
+       }
+
+       /**
+        * @param IDatabase $dbr
+        * @param string|array $condition
+        * @return bool|ResultWrapper
+        */
+       protected static function listPages( $dbr, $condition ) {
+               return $dbr->select(
+                       [ 'archive' ],
+                       [
+                               'ar_namespace',
+                               'ar_title',
+                               'count' => 'COUNT(*)'
+                       ],
+                       $condition,
+                       __METHOD__,
+                       [
+                               'GROUP BY' => [ 'ar_namespace', 'ar_title' ],
+                               'ORDER BY' => [ 'ar_namespace', 'ar_title' ],
+                               'LIMIT' => 100,
+                       ]
+               );
+       }
+
+       /**
+        * List the revisions of the given page. Returns result wrapper with
+        * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
+        *
+        * @return ResultWrapper
+        */
+       public function listRevisions() {
+               $dbr = wfGetDB( DB_REPLICA );
+
+               $tables = [ 'archive' ];
+
+               $fields = [
+                       'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
+                       'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
+               ];
+
+               if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
+                       $fields[] = 'ar_content_format';
+                       $fields[] = 'ar_content_model';
+               }
+
+               $conds = [ 'ar_namespace' => $this->title->getNamespace(),
+                       'ar_title' => $this->title->getDBkey() ];
+
+               $options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
+
+               $join_conds = [];
+
+               ChangeTags::modifyDisplayQuery(
+                       $tables,
+                       $fields,
+                       $conds,
+                       $join_conds,
+                       $options,
+                       ''
+               );
+
+               return $dbr->select( $tables,
+                       $fields,
+                       $conds,
+                       __METHOD__,
+                       $options,
+                       $join_conds
+               );
+       }
+
+       /**
+        * List the deleted file revisions for this page, if it's a file page.
+        * Returns a result wrapper with various filearchive fields, or null
+        * if not a file page.
+        *
+        * @return ResultWrapper
+        * @todo Does this belong in Image for fuller encapsulation?
+        */
+       public function listFiles() {
+               if ( $this->title->getNamespace() != NS_FILE ) {
+                       return null;
+               }
+
+               $dbr = wfGetDB( DB_REPLICA );
+               return $dbr->select(
+                       'filearchive',
+                       ArchivedFile::selectFields(),
+                       [ 'fa_name' => $this->title->getDBkey() ],
+                       __METHOD__,
+                       [ 'ORDER BY' => 'fa_timestamp DESC' ]
+               );
+       }
+
+       /**
+        * Return a Revision object containing data for the deleted revision.
+        * Note that the result *may* or *may not* have a null page ID.
+        *
+        * @param string $timestamp
+        * @return Revision|null
+        */
+       public function getRevision( $timestamp ) {
+               $dbr = wfGetDB( DB_REPLICA );
+
+               $fields = [
+                       'ar_rev_id',
+                       'ar_text',
+                       'ar_comment',
+                       'ar_user',
+                       'ar_user_text',
+                       'ar_timestamp',
+                       'ar_minor_edit',
+                       'ar_flags',
+                       'ar_text_id',
+                       'ar_deleted',
+                       'ar_len',
+                       'ar_sha1',
+               ];
+
+               if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
+                       $fields[] = 'ar_content_format';
+                       $fields[] = 'ar_content_model';
+               }
+
+               $row = $dbr->selectRow( 'archive',
+                       $fields,
+                       [ 'ar_namespace' => $this->title->getNamespace(),
+                               'ar_title' => $this->title->getDBkey(),
+                               'ar_timestamp' => $dbr->timestamp( $timestamp ) ],
+                       __METHOD__ );
+
+               if ( $row ) {
+                       return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] );
+               }
+
+               return null;
+       }
+
+       /**
+        * Return the most-previous revision, either live or deleted, against
+        * the deleted revision given by timestamp.
+        *
+        * May produce unexpected results in case of history merges or other
+        * unusual time issues.
+        *
+        * @param string $timestamp
+        * @return Revision|null Null when there is no previous revision
+        */
+       public function getPreviousRevision( $timestamp ) {
+               $dbr = wfGetDB( DB_REPLICA );
+
+               // Check the previous deleted revision...
+               $row = $dbr->selectRow( 'archive',
+                       'ar_timestamp',
+                       [ 'ar_namespace' => $this->title->getNamespace(),
+                               'ar_title' => $this->title->getDBkey(),
+                               'ar_timestamp < ' .
+                               $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
+                       __METHOD__,
+                       [
+                               'ORDER BY' => 'ar_timestamp DESC',
+                               'LIMIT' => 1 ] );
+               $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
+
+               $row = $dbr->selectRow( [ 'page', 'revision' ],
+                       [ 'rev_id', 'rev_timestamp' ],
+                       [
+                               'page_namespace' => $this->title->getNamespace(),
+                               'page_title' => $this->title->getDBkey(),
+                               'page_id = rev_page',
+                               'rev_timestamp < ' .
+                               $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
+                       __METHOD__,
+                       [
+                               'ORDER BY' => 'rev_timestamp DESC',
+                               'LIMIT' => 1 ] );
+               $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
+               $prevLiveId = $row ? intval( $row->rev_id ) : null;
+
+               if ( $prevLive && $prevLive > $prevDeleted ) {
+                       // Most prior revision was live
+                       return Revision::newFromId( $prevLiveId );
+               } elseif ( $prevDeleted ) {
+                       // Most prior revision was deleted
+                       return $this->getRevision( $prevDeleted );
+               }
+
+               // No prior revision on this page.
+               return null;
+       }
+
+       /**
+        * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
+        *
+        * @param object $row Database row
+        * @return string
+        */
+       public function getTextFromRow( $row ) {
+               if ( is_null( $row->ar_text_id ) ) {
+                       // An old row from MediaWiki 1.4 or previous.
+                       // Text is embedded in this row in classic compression format.
+                       return Revision::getRevisionText( $row, 'ar_' );
+               }
+
+               // New-style: keyed to the text storage backend.
+               $dbr = wfGetDB( DB_REPLICA );
+               $text = $dbr->selectRow( 'text',
+                       [ 'old_text', 'old_flags' ],
+                       [ 'old_id' => $row->ar_text_id ],
+                       __METHOD__ );
+
+               return Revision::getRevisionText( $text );
+       }
+
+       /**
+        * Fetch (and decompress if necessary) the stored text of the most
+        * recently edited deleted revision of the page.
+        *
+        * If there are no archived revisions for the page, returns NULL.
+        *
+        * @return string|null
+        */
+       public function getLastRevisionText() {
+               $dbr = wfGetDB( DB_REPLICA );
+               $row = $dbr->selectRow( 'archive',
+                       [ 'ar_text', 'ar_flags', 'ar_text_id' ],
+                       [ 'ar_namespace' => $this->title->getNamespace(),
+                               'ar_title' => $this->title->getDBkey() ],
+                       __METHOD__,
+                       [ 'ORDER BY' => 'ar_timestamp DESC' ] );
+
+               if ( $row ) {
+                       return $this->getTextFromRow( $row );
+               }
+
+               return null;
+       }
+
+       /**
+        * Quick check if any archived revisions are present for the page.
+        *
+        * @return bool
+        */
+       public function isDeleted() {
+               $dbr = wfGetDB( DB_REPLICA );
+               $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
+                       [ 'ar_namespace' => $this->title->getNamespace(),
+                               'ar_title' => $this->title->getDBkey() ],
+                       __METHOD__
+               );
+
+               return ( $n > 0 );
+       }
+
+       /**
+        * Restore the given (or all) text and file revisions for the page.
+        * Once restored, the items will be removed from the archive tables.
+        * The deletion log will be updated with an undeletion notice.
+        *
+        * This also sets Status objects, $this->fileStatus and $this->revisionStatus
+        * (depending what operations are attempted).
+        *
+        * @param array $timestamps Pass an empty array to restore all revisions,
+        *   otherwise list the ones to undelete.
+        * @param string $comment
+        * @param array $fileVersions
+        * @param bool $unsuppress
+        * @param User $user User performing the action, or null to use $wgUser
+        * @param string|string[] $tags Change tags to add to log entry
+        *   ($user should be able to add the specified tags before this is called)
+        * @return array|bool array(number of file revisions restored, number of image revisions
+        *   restored, log message) on success, false on failure.
+        */
+       public function undelete( $timestamps, $comment = '', $fileVersions = [],
+               $unsuppress = false, User $user = null, $tags = null
+       ) {
+               // If both the set of text revisions and file revisions are empty,
+               // restore everything. Otherwise, just restore the requested items.
+               $restoreAll = empty( $timestamps ) && empty( $fileVersions );
+
+               $restoreText = $restoreAll || !empty( $timestamps );
+               $restoreFiles = $restoreAll || !empty( $fileVersions );
+
+               if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
+                       $img = wfLocalFile( $this->title );
+                       $img->load( File::READ_LATEST );
+                       $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
+                       if ( !$this->fileStatus->isOK() ) {
+                               return false;
+                       }
+                       $filesRestored = $this->fileStatus->successCount;
+               } else {
+                       $filesRestored = 0;
+               }
+
+               if ( $restoreText ) {
+                       $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
+                       if ( !$this->revisionStatus->isOK() ) {
+                               return false;
+                       }
+
+                       $textRestored = $this->revisionStatus->getValue();
+               } else {
+                       $textRestored = 0;
+               }
+
+               // Touch the log!
+
+               if ( $textRestored && $filesRestored ) {
+                       $reason = wfMessage( 'undeletedrevisions-files' )
+                               ->numParams( $textRestored, $filesRestored )->inContentLanguage()->text();
+               } elseif ( $textRestored ) {
+                       $reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored )
+                               ->inContentLanguage()->text();
+               } elseif ( $filesRestored ) {
+                       $reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored )
+                               ->inContentLanguage()->text();
+               } else {
+                       wfDebug( "Undelete: nothing undeleted...\n" );
+
+                       return false;
+               }
+
+               if ( trim( $comment ) != '' ) {
+                       $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
+               }
+
+               if ( $user === null ) {
+                       global $wgUser;
+                       $user = $wgUser;
+               }
+
+               $logEntry = new ManualLogEntry( 'delete', 'restore' );
+               $logEntry->setPerformer( $user );
+               $logEntry->setTarget( $this->title );
+               $logEntry->setComment( $reason );
+               $logEntry->setTags( $tags );
+
+               Hooks::run( 'ArticleUndeleteLogEntry', [ $this, &$logEntry, $user ] );
+
+               $logid = $logEntry->insert();
+               $logEntry->publish( $logid );
+
+               return [ $textRestored, $filesRestored, $reason ];
+       }
+
+       /**
+        * This is the meaty bit -- It restores archived revisions of the given page
+        * to the revision table.
+        *
+        * @param array $timestamps Pass an empty array to restore all revisions,
+        *   otherwise list the ones to undelete.
+        * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs
+        * @param string $comment
+        * @throws ReadOnlyError
+        * @return Status Status object containing the number of revisions restored on success
+        */
+       private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
+               if ( wfReadOnly() ) {
+                       throw new ReadOnlyError();
+               }
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->startAtomic( __METHOD__ );
+
+               $restoreAll = empty( $timestamps );
+
+               # Does this page already exist? We'll have to update it...
+               $article = WikiPage::factory( $this->title );
+               # Load latest data for the current page (T33179)
+               $article->loadPageData( 'fromdbmaster' );
+               $oldcountable = $article->isCountable();
+
+               $page = $dbw->selectRow( 'page',
+                       [ 'page_id', 'page_latest' ],
+                       [ 'page_namespace' => $this->title->getNamespace(),
+                               'page_title' => $this->title->getDBkey() ],
+                       __METHOD__,
+                       [ 'FOR UPDATE' ] // lock page
+               );
+
+               if ( $page ) {
+                       $makepage = false;
+                       # Page already exists. Import the history, and if necessary
+                       # we'll update the latest revision field in the record.
+
+                       # Get the time span of this page
+                       $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
+                               [ 'rev_id' => $page->page_latest ],
+                               __METHOD__ );
+
+                       if ( $previousTimestamp === false ) {
+                               wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" );
+
+                               $status = Status::newGood( 0 );
+                               $status->warning( 'undeleterevision-missing' );
+                               $dbw->endAtomic( __METHOD__ );
+
+                               return $status;
+                       }
+               } else {
+                       # Have to create a new article...
+                       $makepage = true;
+                       $previousTimestamp = 0;
+               }
+
+               $oldWhere = [
+                       'ar_namespace' => $this->title->getNamespace(),
+                       'ar_title' => $this->title->getDBkey(),
+               ];
+               if ( !$restoreAll ) {
+                       $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
+               }
+
+               $fields = [
+                       'ar_id',
+                       'ar_rev_id',
+                       'rev_id',
+                       'ar_text',
+                       'ar_comment',
+                       'ar_user',
+                       'ar_user_text',
+                       'ar_timestamp',
+                       'ar_minor_edit',
+                       'ar_flags',
+                       'ar_text_id',
+                       'ar_deleted',
+                       'ar_page_id',
+                       'ar_len',
+                       'ar_sha1'
+               ];
+
+               if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
+                       $fields[] = 'ar_content_format';
+                       $fields[] = 'ar_content_model';
+               }
+
+               /**
+                * Select each archived revision...
+                */
+               $result = $dbw->select(
+                       [ 'archive', 'revision' ],
+                       $fields,
+                       $oldWhere,
+                       __METHOD__,
+                       /* options */
+                       [ 'ORDER BY' => 'ar_timestamp' ],
+                       [ 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ] ]
+               );
+
+               $rev_count = $result->numRows();
+               if ( !$rev_count ) {
+                       wfDebug( __METHOD__ . ": no revisions to restore\n" );
+
+                       $status = Status::newGood( 0 );
+                       $status->warning( "undelete-no-results" );
+                       $dbw->endAtomic( __METHOD__ );
+
+                       return $status;
+               }
+
+               // We use ar_id because there can be duplicate ar_rev_id even for the same
+               // page.  In this case, we may be able to restore the first one.
+               $restoreFailedArIds = [];
+
+               // Map rev_id to the ar_id that is allowed to use it.  When checking later,
+               // if it doesn't match, the current ar_id can not be restored.
+
+               // Value can be an ar_id or -1 (-1 means no ar_id can use it, since the
+               // rev_id is taken before we even start the restore).
+               $allowedRevIdToArIdMap = [];
+
+               $latestRestorableRow = null;
+
+               foreach ( $result as $row ) {
+                       if ( $row->ar_rev_id ) {
+                               // rev_id is taken even before we start restoring.
+                               if ( $row->ar_rev_id === $row->rev_id ) {
+                                       $restoreFailedArIds[] = $row->ar_id;
+                                       $allowedRevIdToArIdMap[$row->ar_rev_id] = -1;
+                               } else {
+                                       // rev_id is not taken yet in the DB, but it might be taken
+                                       // by a prior revision in the same restore operation. If
+                                       // not, we need to reserve it.
+                                       if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) {
+                                               $restoreFailedArIds[] = $row->ar_id;
+                                       } else {
+                                               $allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id;
+                                               $latestRestorableRow = $row;
+                                       }
+                               }
+                       } else {
+                               // If ar_rev_id is null, there can't be a collision, and a
+                               // rev_id will be chosen automatically.
+                               $latestRestorableRow = $row;
+                       }
+               }
+
+               $result->seek( 0 ); // move back
+
+               $oldPageId = 0;
+               if ( $latestRestorableRow !== null ) {
+                       $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
+
+                       // grab the content to check consistency with global state before restoring the page.
+                       $revision = Revision::newFromArchiveRow( $latestRestorableRow,
+                               [
+                                       'title' => $article->getTitle(), // used to derive default content model
+                               ]
+                       );
+                       $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
+                       $content = $revision->getContent( Revision::RAW );
+
+                       // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
+                       $status = $content->prepareSave( $article, 0, -1, $user );
+                       if ( !$status->isOK() ) {
+                               $dbw->endAtomic( __METHOD__ );
+
+                               return $status;
+                       }
+               }
+
+               $newid = false; // newly created page ID
+               $restored = 0; // number of revisions restored
+               /** @var Revision $revision */
+               $revision = null;
+
+               // If there are no restorable revisions, we can skip most of the steps.
+               if ( $latestRestorableRow === null ) {
+                       $failedRevisionCount = $rev_count;
+               } else {
+                       if ( $makepage ) {
+                               // Check the state of the newest to-be version...
+                               if ( !$unsuppress
+                                       && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
+                               ) {
+                                       $dbw->endAtomic( __METHOD__ );
+
+                                       return Status::newFatal( "undeleterevdel" );
+                               }
+                               // Safe to insert now...
+                               $newid = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id );
+                               if ( $newid === false ) {
+                                       // The old ID is reserved; let's pick another
+                                       $newid = $article->insertOn( $dbw );
+                               }
+                               $pageId = $newid;
+                       } else {
+                               // Check if a deleted revision will become the current revision...
+                               if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
+                                       // Check the state of the newest to-be version...
+                                       if ( !$unsuppress
+                                               && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
+                                       ) {
+                                               $dbw->endAtomic( __METHOD__ );
+
+                                               return Status::newFatal( "undeleterevdel" );
+                                       }
+                               }
+
+                               $newid = false;
+                               $pageId = $article->getId();
+                       }
+
+                       foreach ( $result as $row ) {
+                               // Check for key dupes due to needed archive integrity.
+                               if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) {
+                                       continue;
+                               }
+                               // Insert one revision at a time...maintaining deletion status
+                               // unless we are specifically removing all restrictions...
+                               $revision = Revision::newFromArchiveRow( $row,
+                                       [
+                                               'page' => $pageId,
+                                               'title' => $this->title,
+                                               'deleted' => $unsuppress ? 0 : $row->ar_deleted
+                                       ] );
+
+                               $revision->insertOn( $dbw );
+                               $restored++;
+
+                               Hooks::run( 'ArticleRevisionUndeleted',
+                                       [ &$this->title, $revision, $row->ar_page_id ] );
+                       }
+
+                       // Now that it's safely stored, take it out of the archive
+                       // Don't delete rows that we failed to restore
+                       $toDeleteConds = $oldWhere;
+                       $failedRevisionCount = count( $restoreFailedArIds );
+                       if ( $failedRevisionCount > 0 ) {
+                               $toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )';
+                       }
+
+                       $dbw->delete( 'archive',
+                               $toDeleteConds,
+                               __METHOD__ );
+               }
+
+               $status = Status::newGood( $restored );
+
+               if ( $failedRevisionCount > 0 ) {
+                       $status->warning(
+                               wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) );
+               }
+
+               // Was anything restored at all?
+               if ( $restored ) {
+                       $created = (bool)$newid;
+                       // Attach the latest revision to the page...
+                       $wasnew = $article->updateIfNewerOn( $dbw, $revision );
+                       if ( $created || $wasnew ) {
+                               // Update site stats, link tables, etc
+                               $article->doEditUpdates(
+                                       $revision,
+                                       User::newFromName( $revision->getUserText( Revision::RAW ), false ),
+                                       [
+                                               'created' => $created,
+                                               'oldcountable' => $oldcountable,
+                                               'restored' => true
+                                       ]
+                               );
+                       }
+
+                       Hooks::run( 'ArticleUndelete', [ &$this->title, $created, $comment, $oldPageId ] );
+                       if ( $this->title->getNamespace() == NS_FILE ) {
+                               DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) );
+                       }
+               }
+
+               $dbw->endAtomic( __METHOD__ );
+
+               return $status;
+       }
+
+       /**
+        * @return Status
+        */
+       public function getFileStatus() {
+               return $this->fileStatus;
+       }
+
+       /**
+        * @return Status
+        */
+       public function getRevisionStatus() {
+               return $this->revisionStatus;
+       }
+}
index dc302a2..4694890 100644 (file)
@@ -197,7 +197,7 @@ abstract class IndexPager extends ContextSource implements Pager {
         */
        public function doQuery() {
                # Use the child class name for profiling
-               $fname = __METHOD__ . ' (' . get_class( $this ) . ')';
+               $fname = __METHOD__ . ' (' . static::class . ')';
                $section = Profiler::instance()->scopedProfileIn( $fname );
 
                // @todo This should probably compare to DIR_DESCENDING and DIR_ASCENDING constants
@@ -348,7 +348,7 @@ abstract class IndexPager extends ContextSource implements Pager {
         * @return string
         */
        function getSqlComment() {
-               return get_class( $this );
+               return static::class;
        }
 
        /**
index 5e54615..01bf2d0 100644 (file)
@@ -138,6 +138,9 @@ class MWTidy {
                        case 'Html5Internal':
                                $instance = new MediaWiki\Tidy\Html5Internal( $config );
                                break;
+                       case 'RemexHtml':
+                               $instance = new MediaWiki\Tidy\RemexDriver( $config );
+                               break;
                        case 'disabled':
                                return false;
                        default:
index 9a9b9d8..8db1fe3 100644 (file)
@@ -89,13 +89,15 @@ class Parser {
        # Everything except bracket, space, or control characters
        # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
        # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
-       const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
+       # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM
+       # uses to replace invalid HTML characters.
+       const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
        # Simplified expression to match an IPv4 or IPv6 address, or
        # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
-       const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}])';
+       const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
        # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
        // @codingStandardsIgnoreStart Generic.Files.LineLength
-       const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
+       const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
                \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
        // @codingStandardsIgnoreEnd
 
@@ -264,7 +266,7 @@ class Parser {
                $this->mUrlProtocols = wfUrlProtocols();
                $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
                        self::EXT_LINK_ADDR .
-                       self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
+                       self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
                if ( isset( $conf['preprocessorClass'] ) ) {
                        $this->mPreprocessorClass = $conf['preprocessorClass'];
                } elseif ( defined( 'HPHP_VERSION' ) ) {
@@ -417,6 +419,8 @@ class Parser {
                        $text = strtr( $text, "\x7f", "?" );
                        $magicScopeVariable = $this->lock();
                }
+               // Strip U+0000 NULL (T159174)
+               $text = str_replace( "\000", '', $text );
 
                $this->startParse( $title, $options, self::OT_HTML, $clearState );
 
@@ -4463,6 +4467,9 @@ class Parser {
                $this->startParse( $title, $options, self::OT_WIKI, $clearState );
                $this->setUser( $user );
 
+               // Strip U+0000 NULL (T159174)
+               $text = str_replace( "\000", '', $text );
+
                // We still normalize line endings for backwards-compatibility
                // with other code that just calls PST, but this should already
                // be handled in TextContent subclasses
index d4dabe7..a2b4b1d 100644 (file)
@@ -843,7 +843,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
         */
        public function getDefinitionSummary( ResourceLoaderContext $context ) {
                return [
-                       '_class' => get_class( $this ),
+                       '_class' => static::class,
                        '_cacheEpoch' => $this->getConfig()->get( 'CacheEpoch' ),
                ];
        }
index e8d81cb..3cf69b7 100644 (file)
@@ -455,7 +455,7 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI
         * @return string
         */
        public function __toString() {
-               return get_class( $this );
+               return static::class;
        }
 
        /**
@@ -475,7 +475,7 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI
         */
        protected function describeMessage() {
                return wfMessage(
-                       'sessionprovider-' . str_replace( '\\', '-', strtolower( get_class( $this ) ) )
+                       'sessionprovider-' . str_replace( '\\', '-', strtolower( static::class ) )
                );
        }
 
index 61d34c6..e571c58 100644 (file)
@@ -648,7 +648,7 @@ abstract class BaseTemplate extends QuickTemplate {
         * @since 1.25
         */
        public function getIndicators() {
-               $out = "<div class=\"mw-indicators\">\n";
+               $out = "<div class=\"mw-indicators mw-body-content\">\n";
                foreach ( $this->data['indicators'] as $id => $content ) {
                        $out .= Html::rawElement(
                                'div',
index a7740a0..3ef646a 100644 (file)
@@ -1038,7 +1038,7 @@ abstract class Skin extends ContextSource {
                global $wgStylePath, $wgStyleVersion;
 
                if ( $this->stylename === null ) {
-                       $class = get_class( $this );
+                       $class = static::class;
                        throw new MWException( "$class::\$stylename must be set to use getSkinStylePath()" );
                }
 
index 780fac4..61dbf2b 100644 (file)
@@ -344,7 +344,7 @@ class SkinTemplate extends Skin {
                $tpl->set( 'charset', 'UTF-8' );
                $tpl->setRef( 'wgScript', $wgScript );
                $tpl->setRef( 'skinname', $this->skinname );
-               $tpl->set( 'skinclass', get_class( $this ) );
+               $tpl->set( 'skinclass', static::class );
                $tpl->setRef( 'skin', $this );
                $tpl->setRef( 'stylename', $this->stylename );
                $tpl->set( 'printable', $out->isPrintable() );
index c495f93..3b3ea26 100644 (file)
@@ -304,7 +304,7 @@ abstract class QueryPage extends SpecialPage {
                        return 0;
                }
 
-               $fname = get_class( $this ) . '::recache';
+               $fname = static::class . '::recache';
                $dbw = wfGetDB( DB_MASTER );
                if ( !$dbw ) {
                        return false;
@@ -389,7 +389,7 @@ abstract class QueryPage extends SpecialPage {
         * @since 1.18
         */
        public function reallyDoQuery( $limit, $offset = false ) {
-               $fname = get_class( $this ) . "::reallyDoQuery";
+               $fname = static::class . '::reallyDoQuery';
                $dbr = $this->getRecacheDB();
                $query = $this->getQueryInfo();
                $order = $this->getOrderFields();
@@ -488,7 +488,7 @@ abstract class QueryPage extends SpecialPage {
        public function getCachedTimestamp() {
                if ( is_null( $this->cachedTimestamp ) ) {
                        $dbr = wfGetDB( DB_REPLICA );
-                       $fname = get_class( $this ) . '::getCachedTimestamp';
+                       $fname = static::class . '::getCachedTimestamp';
                        $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
                                [ 'qci_type' => $this->getName() ], $fname );
                }
index ea7d783..b1ddacf 100644 (file)
@@ -52,7 +52,7 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage {
 
                        return $redirect;
                } else {
-                       $class = get_class( $this );
+                       $class = static::class;
                        throw new MWException( "RedirectSpecialPage $class doesn't redirect!" );
                }
        }
index cdad926..eb29907 100644 (file)
@@ -456,24 +456,31 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                $panel[] = $form;
                $panelString = implode( "\n", $panel );
 
+               $rcoptions = Xml::fieldset(
+                       $this->msg( 'recentchanges-legend' )->text(),
+                       $panelString,
+                       [ 'class' => 'rcoptions' ]
+               );
+
                // Insert a placeholder for RCFilters
                if ( $this->getUser()->getOption( 'rcenhancedfilters' ) ) {
+                       $rcfilterContainer = Html::element(
+                               'div',
+                               [ 'class' => 'rcfilters-container' ]
+                       );
+
+                       // Wrap both with rcfilters-head
                        $this->getOutput()->addHTML(
-                               Html::element(
+                               Html::rawElement(
                                        'div',
-                                       [ 'class' => 'rcfilters-container' ]
+                                       [ 'class' => 'rcfilters-head' ],
+                                       $rcfilterContainer . $rcoptions
                                )
                        );
+               } else {
+                       $this->getOutput()->addHTML( $rcoptions );
                }
 
-               $this->getOutput()->addHTML(
-                       Xml::fieldset(
-                               $this->msg( 'recentchanges-legend' )->text(),
-                               $panelString,
-                               [ 'class' => 'rcoptions' ]
-                       )
-               );
-
                $this->setBottomText( $opts );
        }
 
index 6746c3f..dc5f877 100644 (file)
  * @ingroup SpecialPage
  */
 
-use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\ResultWrapper;
 
-/**
- * Used to show archived pages and eventually restore them.
- */
-class PageArchive {
-       /** @var Title */
-       protected $title;
-
-       /** @var Status */
-       protected $fileStatus;
-
-       /** @var Status */
-       protected $revisionStatus;
-
-       /** @var Config */
-       protected $config;
-
-       public function __construct( $title, Config $config = null ) {
-               if ( is_null( $title ) ) {
-                       throw new MWException( __METHOD__ . ' given a null title.' );
-               }
-               $this->title = $title;
-               if ( $config === null ) {
-                       wfDebug( __METHOD__ . ' did not have a Config object passed to it' );
-                       $config = MediaWikiServices::getInstance()->getMainConfig();
-               }
-               $this->config = $config;
-       }
-
-       public function doesWrites() {
-               return true;
-       }
-
-       /**
-        * List all deleted pages recorded in the archive table. Returns result
-        * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
-        * namespace/title.
-        *
-        * @return ResultWrapper
-        */
-       public static function listAllPages() {
-               $dbr = wfGetDB( DB_REPLICA );
-
-               return self::listPages( $dbr, '' );
-       }
-
-       /**
-        * List deleted pages recorded in the archive table matching the
-        * given title prefix.
-        * Returns result wrapper with (ar_namespace, ar_title, count) fields.
-        *
-        * @param string $prefix Title prefix
-        * @return ResultWrapper
-        */
-       public static function listPagesByPrefix( $prefix ) {
-               $dbr = wfGetDB( DB_REPLICA );
-
-               $title = Title::newFromText( $prefix );
-               if ( $title ) {
-                       $ns = $title->getNamespace();
-                       $prefix = $title->getDBkey();
-               } else {
-                       // Prolly won't work too good
-                       // @todo handle bare namespace names cleanly?
-                       $ns = 0;
-               }
-
-               $conds = [
-                       'ar_namespace' => $ns,
-                       'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
-               ];
-
-               return self::listPages( $dbr, $conds );
-       }
-
-       /**
-        * @param IDatabase $dbr
-        * @param string|array $condition
-        * @return bool|ResultWrapper
-        */
-       protected static function listPages( $dbr, $condition ) {
-               return $dbr->select(
-                       [ 'archive' ],
-                       [
-                               'ar_namespace',
-                               'ar_title',
-                               'count' => 'COUNT(*)'
-                       ],
-                       $condition,
-                       __METHOD__,
-                       [
-                               'GROUP BY' => [ 'ar_namespace', 'ar_title' ],
-                               'ORDER BY' => [ 'ar_namespace', 'ar_title' ],
-                               'LIMIT' => 100,
-                       ]
-               );
-       }
-
-       /**
-        * List the revisions of the given page. Returns result wrapper with
-        * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
-        *
-        * @return ResultWrapper
-        */
-       public function listRevisions() {
-               $dbr = wfGetDB( DB_REPLICA );
-
-               $tables = [ 'archive' ];
-
-               $fields = [
-                       'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
-                       'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
-               ];
-
-               if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
-                       $fields[] = 'ar_content_format';
-                       $fields[] = 'ar_content_model';
-               }
-
-               $conds = [ 'ar_namespace' => $this->title->getNamespace(),
-                       'ar_title' => $this->title->getDBkey() ];
-
-               $options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
-
-               $join_conds = [];
-
-               ChangeTags::modifyDisplayQuery(
-                       $tables,
-                       $fields,
-                       $conds,
-                       $join_conds,
-                       $options,
-                       ''
-               );
-
-               return $dbr->select( $tables,
-                       $fields,
-                       $conds,
-                       __METHOD__,
-                       $options,
-                       $join_conds
-               );
-       }
-
-       /**
-        * List the deleted file revisions for this page, if it's a file page.
-        * Returns a result wrapper with various filearchive fields, or null
-        * if not a file page.
-        *
-        * @return ResultWrapper
-        * @todo Does this belong in Image for fuller encapsulation?
-        */
-       public function listFiles() {
-               if ( $this->title->getNamespace() != NS_FILE ) {
-                       return null;
-               }
-
-               $dbr = wfGetDB( DB_REPLICA );
-               return $dbr->select(
-                       'filearchive',
-                       ArchivedFile::selectFields(),
-                       [ 'fa_name' => $this->title->getDBkey() ],
-                       __METHOD__,
-                       [ 'ORDER BY' => 'fa_timestamp DESC' ]
-               );
-       }
-
-       /**
-        * Return a Revision object containing data for the deleted revision.
-        * Note that the result *may* or *may not* have a null page ID.
-        *
-        * @param string $timestamp
-        * @return Revision|null
-        */
-       public function getRevision( $timestamp ) {
-               $dbr = wfGetDB( DB_REPLICA );
-
-               $fields = [
-                       'ar_rev_id',
-                       'ar_text',
-                       'ar_comment',
-                       'ar_user',
-                       'ar_user_text',
-                       'ar_timestamp',
-                       'ar_minor_edit',
-                       'ar_flags',
-                       'ar_text_id',
-                       'ar_deleted',
-                       'ar_len',
-                       'ar_sha1',
-               ];
-
-               if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
-                       $fields[] = 'ar_content_format';
-                       $fields[] = 'ar_content_model';
-               }
-
-               $row = $dbr->selectRow( 'archive',
-                       $fields,
-                       [ 'ar_namespace' => $this->title->getNamespace(),
-                               'ar_title' => $this->title->getDBkey(),
-                               'ar_timestamp' => $dbr->timestamp( $timestamp ) ],
-                       __METHOD__ );
-
-               if ( $row ) {
-                       return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] );
-               }
-
-               return null;
-       }
-
-       /**
-        * Return the most-previous revision, either live or deleted, against
-        * the deleted revision given by timestamp.
-        *
-        * May produce unexpected results in case of history merges or other
-        * unusual time issues.
-        *
-        * @param string $timestamp
-        * @return Revision|null Null when there is no previous revision
-        */
-       public function getPreviousRevision( $timestamp ) {
-               $dbr = wfGetDB( DB_REPLICA );
-
-               // Check the previous deleted revision...
-               $row = $dbr->selectRow( 'archive',
-                       'ar_timestamp',
-                       [ 'ar_namespace' => $this->title->getNamespace(),
-                               'ar_title' => $this->title->getDBkey(),
-                               'ar_timestamp < ' .
-                               $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
-                       __METHOD__,
-                       [
-                               'ORDER BY' => 'ar_timestamp DESC',
-                               'LIMIT' => 1 ] );
-               $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
-
-               $row = $dbr->selectRow( [ 'page', 'revision' ],
-                       [ 'rev_id', 'rev_timestamp' ],
-                       [
-                               'page_namespace' => $this->title->getNamespace(),
-                               'page_title' => $this->title->getDBkey(),
-                               'page_id = rev_page',
-                               'rev_timestamp < ' .
-                               $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ],
-                       __METHOD__,
-                       [
-                               'ORDER BY' => 'rev_timestamp DESC',
-                               'LIMIT' => 1 ] );
-               $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
-               $prevLiveId = $row ? intval( $row->rev_id ) : null;
-
-               if ( $prevLive && $prevLive > $prevDeleted ) {
-                       // Most prior revision was live
-                       return Revision::newFromId( $prevLiveId );
-               } elseif ( $prevDeleted ) {
-                       // Most prior revision was deleted
-                       return $this->getRevision( $prevDeleted );
-               }
-
-               // No prior revision on this page.
-               return null;
-       }
-
-       /**
-        * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
-        *
-        * @param object $row Database row
-        * @return string
-        */
-       public function getTextFromRow( $row ) {
-               if ( is_null( $row->ar_text_id ) ) {
-                       // An old row from MediaWiki 1.4 or previous.
-                       // Text is embedded in this row in classic compression format.
-                       return Revision::getRevisionText( $row, 'ar_' );
-               }
-
-               // New-style: keyed to the text storage backend.
-               $dbr = wfGetDB( DB_REPLICA );
-               $text = $dbr->selectRow( 'text',
-                       [ 'old_text', 'old_flags' ],
-                       [ 'old_id' => $row->ar_text_id ],
-                       __METHOD__ );
-
-               return Revision::getRevisionText( $text );
-       }
-
-       /**
-        * Fetch (and decompress if necessary) the stored text of the most
-        * recently edited deleted revision of the page.
-        *
-        * If there are no archived revisions for the page, returns NULL.
-        *
-        * @return string|null
-        */
-       public function getLastRevisionText() {
-               $dbr = wfGetDB( DB_REPLICA );
-               $row = $dbr->selectRow( 'archive',
-                       [ 'ar_text', 'ar_flags', 'ar_text_id' ],
-                       [ 'ar_namespace' => $this->title->getNamespace(),
-                               'ar_title' => $this->title->getDBkey() ],
-                       __METHOD__,
-                       [ 'ORDER BY' => 'ar_timestamp DESC' ] );
-
-               if ( $row ) {
-                       return $this->getTextFromRow( $row );
-               }
-
-               return null;
-       }
-
-       /**
-        * Quick check if any archived revisions are present for the page.
-        *
-        * @return bool
-        */
-       public function isDeleted() {
-               $dbr = wfGetDB( DB_REPLICA );
-               $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
-                       [ 'ar_namespace' => $this->title->getNamespace(),
-                               'ar_title' => $this->title->getDBkey() ],
-                       __METHOD__
-               );
-
-               return ( $n > 0 );
-       }
-
-       /**
-        * Restore the given (or all) text and file revisions for the page.
-        * Once restored, the items will be removed from the archive tables.
-        * The deletion log will be updated with an undeletion notice.
-        *
-        * This also sets Status objects, $this->fileStatus and $this->revisionStatus
-        * (depending what operations are attempted).
-        *
-        * @param array $timestamps Pass an empty array to restore all revisions,
-        *   otherwise list the ones to undelete.
-        * @param string $comment
-        * @param array $fileVersions
-        * @param bool $unsuppress
-        * @param User $user User performing the action, or null to use $wgUser
-        * @param string|string[] $tags Change tags to add to log entry
-        *   ($user should be able to add the specified tags before this is called)
-        * @return array|bool array(number of file revisions restored, number of image revisions
-        *   restored, log message) on success, false on failure.
-        */
-       public function undelete( $timestamps, $comment = '', $fileVersions = [],
-               $unsuppress = false, User $user = null, $tags = null
-       ) {
-               // If both the set of text revisions and file revisions are empty,
-               // restore everything. Otherwise, just restore the requested items.
-               $restoreAll = empty( $timestamps ) && empty( $fileVersions );
-
-               $restoreText = $restoreAll || !empty( $timestamps );
-               $restoreFiles = $restoreAll || !empty( $fileVersions );
-
-               if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
-                       $img = wfLocalFile( $this->title );
-                       $img->load( File::READ_LATEST );
-                       $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
-                       if ( !$this->fileStatus->isOK() ) {
-                               return false;
-                       }
-                       $filesRestored = $this->fileStatus->successCount;
-               } else {
-                       $filesRestored = 0;
-               }
-
-               if ( $restoreText ) {
-                       $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
-                       if ( !$this->revisionStatus->isOK() ) {
-                               return false;
-                       }
-
-                       $textRestored = $this->revisionStatus->getValue();
-               } else {
-                       $textRestored = 0;
-               }
-
-               // Touch the log!
-
-               if ( $textRestored && $filesRestored ) {
-                       $reason = wfMessage( 'undeletedrevisions-files' )
-                               ->numParams( $textRestored, $filesRestored )->inContentLanguage()->text();
-               } elseif ( $textRestored ) {
-                       $reason = wfMessage( 'undeletedrevisions' )->numParams( $textRestored )
-                               ->inContentLanguage()->text();
-               } elseif ( $filesRestored ) {
-                       $reason = wfMessage( 'undeletedfiles' )->numParams( $filesRestored )
-                               ->inContentLanguage()->text();
-               } else {
-                       wfDebug( "Undelete: nothing undeleted...\n" );
-
-                       return false;
-               }
-
-               if ( trim( $comment ) != '' ) {
-                       $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
-               }
-
-               if ( $user === null ) {
-                       global $wgUser;
-                       $user = $wgUser;
-               }
-
-               $logEntry = new ManualLogEntry( 'delete', 'restore' );
-               $logEntry->setPerformer( $user );
-               $logEntry->setTarget( $this->title );
-               $logEntry->setComment( $reason );
-               $logEntry->setTags( $tags );
-
-               Hooks::run( 'ArticleUndeleteLogEntry', [ $this, &$logEntry, $user ] );
-
-               $logid = $logEntry->insert();
-               $logEntry->publish( $logid );
-
-               return [ $textRestored, $filesRestored, $reason ];
-       }
-
-       /**
-        * This is the meaty bit -- It restores archived revisions of the given page
-        * to the revision table.
-        *
-        * @param array $timestamps Pass an empty array to restore all revisions,
-        *   otherwise list the ones to undelete.
-        * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs
-        * @param string $comment
-        * @throws ReadOnlyError
-        * @return Status Status object containing the number of revisions restored on success
-        */
-       private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
-               if ( wfReadOnly() ) {
-                       throw new ReadOnlyError();
-               }
-
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->startAtomic( __METHOD__ );
-
-               $restoreAll = empty( $timestamps );
-
-               # Does this page already exist? We'll have to update it...
-               $article = WikiPage::factory( $this->title );
-               # Load latest data for the current page (T33179)
-               $article->loadPageData( 'fromdbmaster' );
-               $oldcountable = $article->isCountable();
-
-               $page = $dbw->selectRow( 'page',
-                       [ 'page_id', 'page_latest' ],
-                       [ 'page_namespace' => $this->title->getNamespace(),
-                               'page_title' => $this->title->getDBkey() ],
-                       __METHOD__,
-                       [ 'FOR UPDATE' ] // lock page
-               );
-
-               if ( $page ) {
-                       $makepage = false;
-                       # Page already exists. Import the history, and if necessary
-                       # we'll update the latest revision field in the record.
-
-                       # Get the time span of this page
-                       $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
-                               [ 'rev_id' => $page->page_latest ],
-                               __METHOD__ );
-
-                       if ( $previousTimestamp === false ) {
-                               wfDebug( __METHOD__ . ": existing page refers to a page_latest that does not exist\n" );
-
-                               $status = Status::newGood( 0 );
-                               $status->warning( 'undeleterevision-missing' );
-                               $dbw->endAtomic( __METHOD__ );
-
-                               return $status;
-                       }
-               } else {
-                       # Have to create a new article...
-                       $makepage = true;
-                       $previousTimestamp = 0;
-               }
-
-               $oldWhere = [
-                       'ar_namespace' => $this->title->getNamespace(),
-                       'ar_title' => $this->title->getDBkey(),
-               ];
-               if ( !$restoreAll ) {
-                       $oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
-               }
-
-               $fields = [
-                       'ar_id',
-                       'ar_rev_id',
-                       'rev_id',
-                       'ar_text',
-                       'ar_comment',
-                       'ar_user',
-                       'ar_user_text',
-                       'ar_timestamp',
-                       'ar_minor_edit',
-                       'ar_flags',
-                       'ar_text_id',
-                       'ar_deleted',
-                       'ar_page_id',
-                       'ar_len',
-                       'ar_sha1'
-               ];
-
-               if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
-                       $fields[] = 'ar_content_format';
-                       $fields[] = 'ar_content_model';
-               }
-
-               /**
-                * Select each archived revision...
-                */
-               $result = $dbw->select(
-                       [ 'archive', 'revision' ],
-                       $fields,
-                       $oldWhere,
-                       __METHOD__,
-                       /* options */
-                       [ 'ORDER BY' => 'ar_timestamp' ],
-                       [ 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ] ]
-               );
-
-               $rev_count = $result->numRows();
-               if ( !$rev_count ) {
-                       wfDebug( __METHOD__ . ": no revisions to restore\n" );
-
-                       $status = Status::newGood( 0 );
-                       $status->warning( "undelete-no-results" );
-                       $dbw->endAtomic( __METHOD__ );
-
-                       return $status;
-               }
-
-               // We use ar_id because there can be duplicate ar_rev_id even for the same
-               // page.  In this case, we may be able to restore the first one.
-               $restoreFailedArIds = [];
-
-               // Map rev_id to the ar_id that is allowed to use it.  When checking later,
-               // if it doesn't match, the current ar_id can not be restored.
-
-               // Value can be an ar_id or -1 (-1 means no ar_id can use it, since the
-               // rev_id is taken before we even start the restore).
-               $allowedRevIdToArIdMap = [];
-
-               $latestRestorableRow = null;
-
-               foreach ( $result as $row ) {
-                       if ( $row->ar_rev_id ) {
-                               // rev_id is taken even before we start restoring.
-                               if ( $row->ar_rev_id === $row->rev_id ) {
-                                       $restoreFailedArIds[] = $row->ar_id;
-                                       $allowedRevIdToArIdMap[$row->ar_rev_id] = -1;
-                               } else {
-                                       // rev_id is not taken yet in the DB, but it might be taken
-                                       // by a prior revision in the same restore operation. If
-                                       // not, we need to reserve it.
-                                       if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) {
-                                               $restoreFailedArIds[] = $row->ar_id;
-                                       } else {
-                                               $allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id;
-                                               $latestRestorableRow = $row;
-                                       }
-                               }
-                       } else {
-                               // If ar_rev_id is null, there can't be a collision, and a
-                               // rev_id will be chosen automatically.
-                               $latestRestorableRow = $row;
-                       }
-               }
-
-               $result->seek( 0 ); // move back
-
-               $oldPageId = 0;
-               if ( $latestRestorableRow !== null ) {
-                       $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
-
-                       // grab the content to check consistency with global state before restoring the page.
-                       $revision = Revision::newFromArchiveRow( $latestRestorableRow,
-                               [
-                                       'title' => $article->getTitle(), // used to derive default content model
-                               ]
-                       );
-                       $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
-                       $content = $revision->getContent( Revision::RAW );
-
-                       // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
-                       $status = $content->prepareSave( $article, 0, -1, $user );
-                       if ( !$status->isOK() ) {
-                               $dbw->endAtomic( __METHOD__ );
-
-                               return $status;
-                       }
-               }
-
-               $newid = false; // newly created page ID
-               $restored = 0; // number of revisions restored
-               /** @var Revision $revision */
-               $revision = null;
-
-               // If there are no restorable revisions, we can skip most of the steps.
-               if ( $latestRestorableRow === null ) {
-                       $failedRevisionCount = $rev_count;
-               } else {
-                       if ( $makepage ) {
-                               // Check the state of the newest to-be version...
-                               if ( !$unsuppress
-                                       && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
-                               ) {
-                                       $dbw->endAtomic( __METHOD__ );
-
-                                       return Status::newFatal( "undeleterevdel" );
-                               }
-                               // Safe to insert now...
-                               $newid = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id );
-                               if ( $newid === false ) {
-                                       // The old ID is reserved; let's pick another
-                                       $newid = $article->insertOn( $dbw );
-                               }
-                               $pageId = $newid;
-                       } else {
-                               // Check if a deleted revision will become the current revision...
-                               if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
-                                       // Check the state of the newest to-be version...
-                                       if ( !$unsuppress
-                                               && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
-                                       ) {
-                                               $dbw->endAtomic( __METHOD__ );
-
-                                               return Status::newFatal( "undeleterevdel" );
-                                       }
-                               }
-
-                               $newid = false;
-                               $pageId = $article->getId();
-                       }
-
-                       foreach ( $result as $row ) {
-                               // Check for key dupes due to needed archive integrity.
-                               if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) {
-                                       continue;
-                               }
-                               // Insert one revision at a time...maintaining deletion status
-                               // unless we are specifically removing all restrictions...
-                               $revision = Revision::newFromArchiveRow( $row,
-                                       [
-                                               'page' => $pageId,
-                                               'title' => $this->title,
-                                               'deleted' => $unsuppress ? 0 : $row->ar_deleted
-                                       ] );
-
-                               $revision->insertOn( $dbw );
-                               $restored++;
-
-                               Hooks::run( 'ArticleRevisionUndeleted',
-                                       [ &$this->title, $revision, $row->ar_page_id ] );
-                       }
-
-                       // Now that it's safely stored, take it out of the archive
-                       // Don't delete rows that we failed to restore
-                       $toDeleteConds = $oldWhere;
-                       $failedRevisionCount = count( $restoreFailedArIds );
-                       if ( $failedRevisionCount > 0 ) {
-                               $toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )';
-                       }
-
-                       $dbw->delete( 'archive',
-                               $toDeleteConds,
-                               __METHOD__ );
-               }
-
-               $status = Status::newGood( $restored );
-
-               if ( $failedRevisionCount > 0 ) {
-                       $status->warning(
-                               wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) );
-               }
-
-               // Was anything restored at all?
-               if ( $restored ) {
-                       $created = (bool)$newid;
-                       // Attach the latest revision to the page...
-                       $wasnew = $article->updateIfNewerOn( $dbw, $revision );
-                       if ( $created || $wasnew ) {
-                               // Update site stats, link tables, etc
-                               $article->doEditUpdates(
-                                       $revision,
-                                       User::newFromName( $revision->getUserText( Revision::RAW ), false ),
-                                       [
-                                               'created' => $created,
-                                               'oldcountable' => $oldcountable,
-                                               'restored' => true
-                                       ]
-                               );
-                       }
-
-                       Hooks::run( 'ArticleUndelete', [ &$this->title, $created, $comment, $oldPageId ] );
-                       if ( $this->title->getNamespace() == NS_FILE ) {
-                               DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) );
-                       }
-               }
-
-               $dbw->endAtomic( __METHOD__ );
-
-               return $status;
-       }
-
-       /**
-        * @return Status
-        */
-       public function getFileStatus() {
-               return $this->fileStatus;
-       }
-
-       /**
-        * @return Status
-        */
-       public function getRevisionStatus() {
-               return $this->revisionStatus;
-       }
-}
-
 /**
  * Special page allowing users with the appropriate permissions to view
  * and restore deleted content.
diff --git a/includes/tidy/RemexCompatFormatter.php b/includes/tidy/RemexCompatFormatter.php
new file mode 100644 (file)
index 0000000..3dc727b
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+
+namespace MediaWiki\Tidy;
+
+use RemexHtml\HTMLData;
+use RemexHtml\Serializer\HtmlFormatter;
+use RemexHtml\Serializer\SerializerNode;
+use RemexHtml\Tokenizer\PlainAttributes;
+
+/**
+ * @internal
+ */
+class RemexCompatFormatter extends HtmlFormatter {
+       private static $markedEmptyElements = [
+               'li' => true,
+               'p' => true,
+               'tr' => true,
+       ];
+
+       public function __construct( $options = [] ) {
+               parent::__construct( $options );
+               $this->attributeEscapes["\xc2\xa0"] = '&#160;';
+               unset( $this->attributeEscapes["&"] );
+               $this->textEscapes["\xc2\xa0"] = '&#160;';
+               unset( $this->textEscapes["&"] );
+       }
+
+       public function startDocument( $fragmentNamespace, $fragmentName ) {
+               return '';
+       }
+
+       public function element( SerializerNode $parent, SerializerNode $node, $contents ) {
+               $data = $node->snData;
+               if ( $data && $data->isPWrapper ) {
+                       if ( $data->nonblankNodeCount ) {
+                               return "<p>$contents</p>";
+                       } else {
+                               return $contents;
+                       }
+               }
+
+               $name = $node->name;
+               $attrs = $node->attrs;
+               if ( isset( self::$markedEmptyElements[$name] ) && $attrs->count() === 0 ) {
+                       if ( strspn( $contents, "\t\n\f\r " ) === strlen( $contents ) ) {
+                               return "<{$name} class=\"mw-empty-elt\">$contents</{$name}>";
+                       }
+               }
+
+               $s = "<$name";
+               foreach ( $attrs->getValues() as $attrName => $attrValue ) {
+                       $encValue = strtr( $attrValue, $this->attributeEscapes );
+                       $s .= " $attrName=\"$encValue\"";
+               }
+               if ( $node->namespace === HTMLData::NS_HTML && isset( $this->voidElements[$name] ) ) {
+                       $s .= ' />';
+                       return $s;
+               }
+
+               $s .= '>';
+               if ( $node->namespace === HTMLData::NS_HTML
+                       && isset( $contents[0] ) && $contents[0] === "\n"
+                       && isset( $this->prefixLfElements[$name] )
+               ) {
+                       $s .= "\n$contents</$name>";
+               } else {
+                       $s .= "$contents</$name>";
+               }
+               return $s;
+       }
+}
diff --git a/includes/tidy/RemexCompatMunger.php b/includes/tidy/RemexCompatMunger.php
new file mode 100644 (file)
index 0000000..d5f5c28
--- /dev/null
@@ -0,0 +1,468 @@
+<?php
+
+namespace MediaWiki\Tidy;
+
+use RemexHtml\HTMLData;
+use RemexHtml\Serializer\Serializer;
+use RemexHtml\Serializer\SerializerNode;
+use RemexHtml\Tokenizer\Attributes;
+use RemexHtml\Tokenizer\PlainAttributes;
+use RemexHtml\TreeBuilder\TreeBuilder;
+use RemexHtml\TreeBuilder\TreeHandler;
+use RemexHtml\TreeBuilder\Element;
+
+/**
+ * @internal
+ */
+class RemexCompatMunger implements TreeHandler {
+       private static $onlyInlineElements = [
+               "a" => true,
+               "abbr" => true,
+               "acronym" => true,
+               "applet" => true,
+               "b" => true,
+               "basefont" => true,
+               "bdo" => true,
+               "big" => true,
+               "br" => true,
+               "button" => true,
+               "cite" => true,
+               "code" => true,
+               "dfn" => true,
+               "em" => true,
+               "font" => true,
+               "i" => true,
+               "iframe" => true,
+               "img" => true,
+               "input" => true,
+               "kbd" => true,
+               "label" => true,
+               "legend" => true,
+               "map" => true,
+               "object" => true,
+               "param" => true,
+               "q" => true,
+               "rb" => true,
+               "rbc" => true,
+               "rp" => true,
+               "rt" => true,
+               "rtc" => true,
+               "ruby" => true,
+               "s" => true,
+               "samp" => true,
+               "select" => true,
+               "small" => true,
+               "span" => true,
+               "strike" => true,
+               "strong" => true,
+               "sub" => true,
+               "sup" => true,
+               "textarea" => true,
+               "tt" => true,
+               "u" => true,
+               "var" => true,
+       ];
+
+       private static $formattingElements = [
+               'a' => true,
+               'b' => true,
+               'big' => true,
+               'code' => true,
+               'em' => true,
+               'font' => true,
+               'i' => true,
+               'nobr' => true,
+               's' => true,
+               'small' => true,
+               'strike' => true,
+               'strong' => true,
+               'tt' => true,
+               'u' => true,
+       ];
+
+       /**
+        * Constructor
+        *
+        * @param Serializer $serializer
+        */
+       public function __construct( Serializer $serializer ) {
+               $this->serializer = $serializer;
+       }
+
+       public function startDocument( $fragmentNamespace, $fragmentName ) {
+               $this->serializer->startDocument( $fragmentNamespace, $fragmentName );
+               $root = $this->serializer->getRootNode();
+               $root->snData = new RemexMungerData;
+               $root->snData->needsPWrapping = true;
+       }
+
+       public function endDocument( $pos ) {
+               $this->serializer->endDocument( $pos );
+       }
+
+       private function getParentForInsert( $preposition, $refElement ) {
+               if ( $preposition === TreeBuilder::ROOT ) {
+                       return [ $this->serializer->getRootNode(), null ];
+               } elseif ( $preposition === TreeBuilder::BEFORE ) {
+                       $refNode = $refElement->userData;
+                       return [ $this->serializer->getParentNode( $refNode ), $refNode ];
+               } else {
+                       $refNode = $refElement->userData;
+                       $refData = $refNode->snData;
+                       if ( $refData->currentCloneElement ) {
+                               // Follow a chain of clone links if necessary
+                               $origRefData = $refData;
+                               while ( $refData->currentCloneElement ) {
+                                       $refElement = $refData->currentCloneElement;
+                                       $refNode = $refElement->userData;
+                                       $refData = $refNode->snData;
+                               }
+                               // Cache the end of the chain in the requested element
+                               $origRefData->currentCloneElement = $refElement;
+                       } elseif ( $refData->childPElement ) {
+                               $refElement = $refData->childPElement;
+                               $refNode = $refElement->userData;
+                       }
+                       return [ $refNode, $refNode ];
+               }
+       }
+
+       /**
+        * Insert a p-wrapper
+        *
+        * @param SerializerNode $parent
+        * @param integer $sourceStart
+        * @return SerializerNode
+        */
+       private function insertPWrapper( SerializerNode $parent, $sourceStart ) {
+               $pWrap = new Element( HTMLData::NS_HTML, 'mw:p-wrap', new PlainAttributes );
+               $this->serializer->insertElement( TreeBuilder::UNDER, $parent, $pWrap, false,
+                       $sourceStart, 0 );
+               $data = new RemexMungerData;
+               $data->isPWrapper = true;
+               $data->wrapBaseNode = $parent;
+               $pWrap->userData->snData = $data;
+               $parent->snData->childPElement = $pWrap;
+               return $pWrap->userData;
+       }
+
+       public function characters( $preposition, $refElement, $text, $start, $length,
+               $sourceStart, $sourceLength
+       ) {
+               $isBlank = strspn( $text, "\t\n\f\r ", $start, $length ) === $length;
+
+               list( $parent, $refNode ) = $this->getParentForInsert( $preposition, $refElement );
+               $parentData = $parent->snData;
+
+               if ( $preposition === TreeBuilder::UNDER ) {
+                       if ( $parentData->needsPWrapping && !$isBlank ) {
+                               // Add a p-wrapper for bare text under body/blockquote
+                               $refNode = $this->insertPWrapper( $refNode, $sourceStart );
+                               $parent = $refNode;
+                               $parentData = $parent->snData;
+                       } elseif ( $parentData->isSplittable && !$parentData->ancestorPNode ) {
+                               // The parent is splittable and in block mode, so split the tag stack
+                               $refNode = $this->splitTagStack( $refNode, true, $sourceStart );
+                               $parent = $refNode;
+                               $parentData = $parent->snData;
+                       }
+               }
+
+               if ( !$isBlank ) {
+                       // Non-whitespace characters detected
+                       $parentData->nonblankNodeCount++;
+               }
+               $this->serializer->characters( $preposition, $refNode, $text, $start,
+                       $length, $sourceStart, $sourceLength );
+       }
+
+       /**
+        * Insert or reparent an element. Create p-wrappers or split the tag stack
+        * as necessary.
+        *
+        * Consider the following insertion locations. The parent may be:
+        *
+        *   - A: A body or blockquote (!!needsPWrapping)
+        *   - B: A p-wrapper (!!isPWrapper)
+        *   - C: A descendant of a p-wrapper (!!ancestorPNode)
+        *     - CS: With splittable formatting elements in the stack region up to
+        *       the p-wrapper
+        *     - CU: With one or more unsplittable elements in the stack region up
+        *       to the p-wrapper
+        *   - D: Not a descendant of a p-wrapper (!ancestorNode)
+        *     - DS: With splittable formatting elements in the stack region up to
+        *       the body or blockquote
+        *     - DU: With one or more unsplittable elements in the stack region up
+        *       to the body or blockquote
+        *
+        * And consider that we may insert two types of element:
+        *   - b: block
+        *   - i: inline
+        *
+        * We handle the insertion as follows:
+        *
+        *   - A/i: Create a p-wrapper, insert under it
+        *   - A/b: Insert as normal
+        *   - B/i: Insert as normal
+        *   - B/b: Close the p-wrapper, insert under the body/blockquote (wrap
+        *     base) instead)
+        *   - C/i: Insert as normal
+        *   - CS/b: Split the tag stack, insert the block under cloned formatting
+        *     elements which have the wrap base (the parent of the p-wrap) as
+        *     their ultimate parent.
+        *   - CU/b: Disable the p-wrap, by reparenting the currently open child
+        *     of the p-wrap under the p-wrap's parent. Then insert the block as
+        *     normal.
+        *   - D/b: Insert as normal
+        *   - DS/i: Split the tag stack, creating a new p-wrapper as the ultimate
+        *     parent of the formatting elements thus cloned. The parent of the
+        *     p-wrapper is the body or blockquote.
+        *   - DU/i: Insert as normal
+        *
+        * FIXME: fostering ($preposition == BEFORE) is mostly done by inserting as
+        * normal, the full algorithm is not followed.
+        *
+        * @param integer $preposition
+        * @param Element|SerializerNode|null $refElement
+        * @param Element $element
+        * @param bool $void
+        * @param integer $sourceStart
+        * @param integer $sourceLength
+        */
+
+       public function insertElement( $preposition, $refElement, Element $element, $void,
+               $sourceStart, $sourceLength
+       ) {
+               list( $parent, $newRef ) = $this->getParentForInsert( $preposition, $refElement );
+               $parentData = $parent->snData;
+               $parentNs = $parent->namespace;
+               $parentName = $parent->name;
+               $elementName = $element->htmlName;
+
+               $inline = isset( self::$onlyInlineElements[$elementName] );
+               $under = $preposition === TreeBuilder::UNDER;
+
+               if ( $under && $parentData->isPWrapper && !$inline ) {
+                       // [B/b] The element is non-inline and the parent is a p-wrapper,
+                       // close the parent and insert into its parent instead
+                       $newParent = $this->serializer->getParentNode( $parent );
+                       $parent = $newParent;
+                       $parentData = $parent->snData;
+                       $parentData->childPElement = null;
+                       $newRef = $refElement->userData;
+                       // FIXME cannot call endTag() since we don't have an Element
+               } elseif ( $under && $parentData->isSplittable
+                       && (bool)$parentData->ancestorPNode !== $inline
+               ) {
+                       // [CS/b, DS/i] The parent is splittable and the current element is
+                       // inline in block context, or if the current element is a block
+                       // under a p-wrapper, split the tag stack.
+                       $newRef = $this->splitTagStack( $newRef, $inline, $sourceStart );
+                       $parent = $newRef;
+                       $parentData = $parent->snData;
+               } elseif ( $under && $parentData->needsPWrapping && $inline ) {
+                       // [A/i] If the element is inline and we are in body/blockquote,
+                       // we need to create a p-wrapper
+                       $newRef = $this->insertPWrapper( $newRef, $sourceStart );
+                       $parent = $newRef;
+                       $parentData = $parent->snData;
+               } elseif ( $parentData->ancestorPNode && !$inline ) {
+                       // [CU/b] If the element is non-inline and (despite attempting to
+                       // split above) there is still an ancestor p-wrap, disable that
+                       // p-wrap
+                       $this->disablePWrapper( $parent, $sourceStart );
+               }
+               // else [A/b, B/i, C/i, D/b, DU/i] insert as normal
+
+               // An element with element children is a non-blank element
+               $parentData->nonblankNodeCount++;
+
+               // Insert the element downstream and so initialise its userData
+               $this->serializer->insertElement( $preposition, $newRef,
+                       $element, $void, $sourceStart, $sourceLength );
+
+               // Initialise snData
+               if ( !$element->userData->snData ) {
+                       $elementData = $element->userData->snData = new RemexMungerData;
+               } else {
+                       $elementData = $element->userData->snData;
+               }
+               if ( ( $parentData->isPWrapper || $parentData->isSplittable )
+                       && isset( self::$formattingElements[$elementName] )
+               ) {
+                       $elementData->isSplittable = true;
+               }
+               if ( $parentData->isPWrapper ) {
+                       $elementData->ancestorPNode = $parent;
+               } elseif ( $parentData->ancestorPNode ) {
+                       $elementData->ancestorPNode = $parentData->ancestorPNode;
+               }
+               if ( $parentData->wrapBaseNode ) {
+                       $elementData->wrapBaseNode = $parentData->wrapBaseNode;
+               } elseif ( $parentData->needsPWrapping ) {
+                       $elementData->wrapBaseNode = $parent;
+               }
+               if ( $elementName === 'body'
+                       || $elementName === 'blockquote'
+                       || $elementName === 'html'
+               ) {
+                       $elementData->needsPWrapping = true;
+               }
+       }
+
+       /**
+        * Clone nodes in a stack range and return the new parent
+        *
+        * @param SerializerNode $parentNode
+        * @param bool $inline
+        * @param integer $pos The source position
+        * @return SerializerNode
+        */
+       private function splitTagStack( SerializerNode $parentNode, $inline, $pos ) {
+               $parentData = $parentNode->snData;
+               $wrapBase = $parentData->wrapBaseNode;
+               $pWrap = $parentData->ancestorPNode;
+               if ( !$pWrap ) {
+                       $cloneEnd = $wrapBase;
+               } else {
+                       $cloneEnd = $parentData->ancestorPNode;
+               }
+
+               $serializer = $this->serializer;
+               $node = $parentNode;
+               $root = $serializer->getRootNode();
+               $nodes = [];
+               $removableNodes = [];
+               $haveContent = false;
+               while ( $node !== $cloneEnd ) {
+                       $nextParent = $serializer->getParentNode( $node );
+                       if ( $nextParent === $root ) {
+                               throw new \Exception( 'Did not find end of clone range' );
+                       }
+                       $nodes[] = $node;
+                       if ( $node->snData->nonblankNodeCount === 0 ) {
+                               $removableNodes[] = $node;
+                               $nextParent->snData->nonblankNodeCount--;
+                       }
+                       $node = $nextParent;
+               }
+
+               if ( $inline ) {
+                       $pWrap = $this->insertPWrapper( $wrapBase, $pos );
+                       $node = $pWrap;
+               } else {
+                       if ( $pWrap ) {
+                               // End the p-wrap which was open, cancel the diversion
+                               $wrapBase->snData->childPElement = null;
+                       }
+                       $pWrap = null;
+                       $node = $wrapBase;
+               }
+
+               for ( $i = count( $nodes ) - 1; $i >= 0; $i-- ) {
+                       $oldNode = $nodes[$i];
+                       $oldData = $oldNode->snData;
+                       $nodeParent = $node;
+                       $element = new Element( $oldNode->namespace, $oldNode->name, $oldNode->attrs );
+                       $this->serializer->insertElement( TreeBuilder::UNDER, $nodeParent,
+                               $element, false, $pos, 0 );
+                       $oldData->currentCloneElement = $element;
+
+                       $newNode = $element->userData;
+                       $newData = $newNode->snData = new RemexMungerData;
+                       if ( $pWrap ) {
+                               $newData->ancestorPNode = $pWrap;
+                       }
+                       $newData->isSplittable = true;
+                       $newData->wrapBaseNode = $wrapBase;
+                       $newData->isPWrapper = $oldData->isPWrapper;
+
+                       $nodeParent->snData->nonblankNodeCount++;
+
+                       $node = $newNode;
+               }
+               foreach ( $removableNodes as $rNode ) {
+                       $fakeElement = new Element( $rNode->namespace, $rNode->name, $rNode->attrs );
+                       $fakeElement->userData = $rNode;
+                       $this->serializer->removeNode( $fakeElement, $pos );
+               }
+               return $node;
+       }
+
+       /**
+        * Find the ancestor of $node which is a child of a p-wrapper, and
+        * reparent that node so that it is placed after the end of the p-wrapper
+        */
+       private function disablePWrapper( SerializerNode $node, $sourceStart ) {
+               $nodeData = $node->snData;
+               $pWrapNode = $nodeData->ancestorPNode;
+               $newParent = $this->serializer->getParentNode( $pWrapNode );
+               if ( $pWrapNode !== $this->serializer->getLastChild( $newParent ) ) {
+                       // Fostering or something? Abort!
+                       return;
+               }
+
+               $nextParent = $node;
+               do {
+                       $victim = $nextParent;
+                       $victim->snData->ancestorPNode = null;
+                       $nextParent = $this->serializer->getParentNode( $victim );
+               } while ( $nextParent !== $pWrapNode );
+
+               // Make a fake Element to use in a reparenting operation
+               $victimElement = new Element( $victim->namespace, $victim->name, $victim->attrs );
+               $victimElement->userData = $victim;
+
+               // Reparent
+               $this->serializer->insertElement( TreeBuilder::UNDER, $newParent, $victimElement,
+                       false, $sourceStart, 0 );
+
+               // Decrement nonblank node count
+               $pWrapNode->snData->nonblankNodeCount--;
+
+               // Cancel the diversion so that no more elements are inserted under this p-wrap
+               $newParent->snData->childPElement = null;
+       }
+
+       public function endTag( Element $element, $sourceStart, $sourceLength ) {
+               $this->serializer->endTag( $element, $sourceStart, $sourceLength );
+       }
+
+       public function doctype( $name, $public, $system, $quirks, $sourceStart, $sourceLength ) {
+               $this->serializer->doctype( $name, $public,  $system, $quirks,
+                       $sourceStart, $sourceLength );
+       }
+
+       public function comment( $preposition, $refElement, $text, $sourceStart, $sourceLength ) {
+               list( $parent, $refNode ) = $this->getParentForInsert( $preposition, $refElement );
+               $this->serializer->comment( $preposition, $refNode, $text,
+                       $sourceStart, $sourceLength );
+       }
+
+       public function error( $text, $pos ) {
+               $this->serializer->error( $text, $pos );
+       }
+
+       public function mergeAttributes( Element $element, Attributes $attrs, $sourceStart ) {
+               $this->serializer->mergeAttributes( $element, $attrs, $sourceStart );
+       }
+
+       public function removeNode( Element $element, $sourceStart ) {
+               $this->serializer->removeNode( $element, $sourceStart );
+       }
+
+       public function reparentChildren( Element $element, Element $newParent, $sourceStart ) {
+               $self = $element->userData;
+               $children = $self->children;
+               $self->children = [];
+               $this->insertElement( TreeBuilder::UNDER, $element, $newParent, false, $sourceStart, 0 );
+               $newParentNode = $newParent->userData;
+               $newParentId = $newParentNode->id;
+               foreach ( $children as $child ) {
+                       if ( is_object( $child ) ) {
+                               $child->parentId = $newParentId;
+                       }
+               }
+               $newParentNode->children = $children;
+       }
+}
diff --git a/includes/tidy/RemexDriver.php b/includes/tidy/RemexDriver.php
new file mode 100644 (file)
index 0000000..e02af88
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+namespace MediaWiki\Tidy;
+
+use RemexHtml\Serializer\Serializer;
+use RemexHtml\Tokenizer\Tokenizer;
+use RemexHtml\TreeBuilder\Dispatcher;
+use RemexHtml\TreeBuilder\TreeBuilder;
+use RemexHtml\TreeBuilder\TreeMutationTracer;
+
+class RemexDriver extends TidyDriverBase {
+       private $trace;
+       private $pwrap;
+
+       public function __construct( array $config ) {
+               $config += [
+                       'treeMutationTrace' => false,
+                       'pwrap' => true
+               ];
+               $this->trace = $config['treeMutationTrace'];
+               $this->pwrap = $config['pwrap'];
+               parent::__construct( $config );
+       }
+
+       public function tidy( $text ) {
+               $formatter = new RemexCompatFormatter;
+               $serializer = new Serializer( $formatter );
+               if ( $this->pwrap ) {
+                       $munger = new RemexCompatMunger( $serializer );
+               } else {
+                       $munger = $serializer;
+               }
+               if ( $this->trace ) {
+                       $tracer = new TreeMutationTracer( $munger, function ( $msg ) {
+                               wfDebug( "RemexHtml: $msg" );
+                       } );
+               } else {
+                       $tracer = $munger;
+               }
+               $treeBuilder = new TreeBuilder( $tracer, [
+                       'ignoreErrors' => true,
+                       'ignoreNulls' => true,
+               ] );
+               $dispatcher = new Dispatcher( $treeBuilder );
+               $tokenizer = new Tokenizer( $dispatcher, $text, [
+                       'ignoreErrors' => true,
+                       'ignoreCharRefs' => true,
+                       'ignoreNulls' => true,
+                       'skipPreprocess' => true,
+               ] );
+               $tokenizer->execute( [
+                       'fragmentNamespace' => \RemexHtml\HTMLData::NS_HTML,
+                       'fragmentName' => 'body'
+               ] );
+               return $serializer->getResult();
+       }
+}
diff --git a/includes/tidy/RemexMungerData.php b/includes/tidy/RemexMungerData.php
new file mode 100644 (file)
index 0000000..d614a38
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+namespace MediaWiki\Tidy;
+
+/**
+ * @internal
+ */
+class RemexMungerData {
+       /**
+        * The Element for the mw:p-wrap which is a child of the current node. If
+        * this is set, inline insertions into this node will be diverted so that
+        * they insert into the p-wrap.
+        *
+        * @var \RemexHtml\TreeBuilder\Element|null
+        */
+       public $childPElement;
+
+       /**
+        * This tracks the mw:p-wrap node in the Serializer stack which is an
+        * ancestor of this node. If there is no mw:p-wrap ancestor, it is null.
+        *
+        * @var \RemexHtml\Serializer\SerializerNode|null
+        */
+       public $ancestorPNode;
+
+       /**
+        * The wrap base node is the body or blockquote node which is the parent
+        * of active p-wrappers. This is set if there is an ancestor p-wrapper,
+        * or if a p-wrapper was closed due to a block element being encountered
+        * inside it.
+        *
+        * @var \RemexHtml\Serializer\SerializerNode|null
+        */
+       public $wrapBaseNode;
+
+       /**
+        * Stack splitting (essentially our idea of AFE reconstruction) can clone
+        * formatting elements which are split over multiple paragraphs.
+        * TreeBuilder is not aware of the cloning, and continues to insert into
+        * the original element. This is set to the newer clone if this node was
+        * cloned, i.e. if there is an active diversion of the insertion location.
+        *
+        * @var \RemexHtml\TreeBuilder\Element|null
+        */
+       public $currentCloneElement;
+
+       /**
+        * Is the node a p-wrapper, with name mw:p-wrap?
+        *
+        * @var bool
+        */
+       public $isPWrapper = false;
+
+       /**
+        * Is the node splittable, i.e. a formatting element or a node with a
+        * formatting element ancestor which is under an active or deactivated
+        * p-wrapper.
+        *
+        * @var bool
+        */
+       public $isSplittable = false;
+
+       /**
+        * This is true if the node is a body or blockquote, which activates
+        * p-wrapping of child nodes.
+        */
+       public $needsPWrapping = false;
+
+       /**
+        * The number of child nodes, not counting whitespace-only text nodes or
+        * comments.
+        */
+       public $nonblankNodeCount = 0;
+
+       public function __set( $name, $value ) {
+               throw new \Exception( "Cannot set property \"$name\"" );
+       }
+}
index 96ee8c3..d3f9d48 100644 (file)
@@ -27,7 +27,7 @@ abstract class TidyDriverBase {
         * @return bool Whether the HTML is valid
         */
        public function validate( $text, &$errorStr ) {
-               throw new \MWException( get_class( $this ) . " does not support validate()" );
+               throw new \MWException( static::class . ' does not support validate()' );
        }
 
        /**
index 2d67a28..4d24cb8 100644 (file)
@@ -115,15 +115,23 @@ class NamespaceAwareForeignTitleFactory implements ForeignTitleFactory {
        protected function parseTitleWithNs( $title, $ns ) {
                $pieces = explode( ':', $title, 2 );
 
+               // Is $title of the form Namespace:Title (true), or just Title (false)?
+               $titleIncludesNamespace = ( $ns != '0' && count( $pieces ) === 2 );
+
                if ( isset( $this->foreignNamespaces[$ns] ) ) {
                        $namespaceName = $this->foreignNamespaces[$ns];
                } else {
-                       $namespaceName = $ns == '0' ? '' : $pieces[0];
+                       // If the foreign wiki is misconfigured, XML dumps can contain a page with
+                       // a non-zero namespace ID, but whose title doesn't contain a colon
+                       // (T114115). In those cases, output a made-up namespace name to avoid
+                       // collisions. The ImportTitleFactory might replace this with something
+                       // more appropriate.
+                       $namespaceName = $titleIncludesNamespace ? $pieces[0] : "Ns$ns";
                }
 
                // We assume that the portion of the page title before the colon is the
-               // namespace name, except in the case of namespace 0
-               if ( $ns != '0' ) {
+               // namespace name, except in the case of namespace 0.
+               if ( $titleIncludesNamespace ) {
                        $pageName = $pieces[1];
                } else {
                        $pageName = $title;
index 2ec2d54..0672315 100644 (file)
@@ -415,10 +415,10 @@ class Language {
        function __construct() {
                $this->mConverter = new FakeConverter( $this );
                // Set the code to the name of the descendant
-               if ( get_class( $this ) == 'Language' ) {
+               if ( static::class === 'Language' ) {
                        $this->mCode = 'en';
                } else {
-                       $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
+                       $this->mCode = str_replace( '_', '-', strtolower( substr( static::class, 8 ) ) );
                }
                self::getLocalisationCache();
        }
diff --git a/languages/LanguageCode.php b/languages/LanguageCode.php
new file mode 100644 (file)
index 0000000..7c9da77
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Language
+ */
+
+/**
+ * Methods for dealing with language codes.
+ * @todo Move some of the code-related static methods out of Language into this class
+ *
+ * @since 1.29
+ * @ingroup Language
+ */
+class LanguageCode {
+       /**
+        * Returns a mapping of deprecated language codes that were used in previous
+        * versions of MediaWiki to up-to-date, current language codes.
+        *
+        * This array is merged into $wgDummyLanguageCodes in Setup.php, along with
+        * the fake language codes 'qqq' and 'qqx', which are used internally by
+        * MediaWiki's localisation system.
+        *
+        * @return string[]
+        */
+       public static function getDeprecatedCodeMapping() {
+               return [
+                       // Note that als is actually a valid ISO 639 code (Tosk Albanian), but it
+                       // was previously used in MediaWiki for Alsatian, which comes under gsw
+                       'als' => 'gsw',
+                       'bat-smg' => 'sgs',
+                       'be-x-old' => 'be-tarask',
+                       'fiu-vro' => 'vro',
+                       'roa-rup' => 'rup',
+                       'zh-classical' => 'lzh',
+                       'zh-min-nan' => 'nan',
+                       'zh-yue' => 'yue',
+               ];
+       }
+}
index 361a9a7..7721015 100644 (file)
@@ -380,6 +380,7 @@ class LanguageConverter {
                $literalBlob = '';
 
                // Guard against delimiter nulls in the input
+               // (should never happen: see T159174)
                $text = str_replace( "\000", '', $text );
 
                $markupMatches = null;
@@ -844,9 +845,8 @@ class LanguageConverter {
         * @throws MWException
         */
        function loadDefaultTables() {
-               $name = get_class( $this );
-
-               throw new MWException( "Must implement loadDefaultTables() method in class $name" );
+               $class = static::class;
+               throw new MWException( "Must implement loadDefaultTables() method in class $class" );
        }
 
        /**
index 26b5857..03fabf7 100644 (file)
        "rcfilters-filterlist-feedbacklink": "تقديم مراجعات لمرشحات (بيتا) الجديدة",
        "rcfilters-highlightbutton-title": "التعليم على النتائج",
        "rcfilters-highlightmenu-title": "اختر لونًا",
+       "rcfilters-highlightmenu-help": "اختر لونا للتعليم على هذه الخاصية",
        "rcfilters-filterlist-noresults": "لم يتم العثور على مرشحات",
        "rcfilters-filtergroup-registration": "تسجيل المستخدم",
        "rcfilters-filter-registered-label": "مسجل",
index f3b72c6..deff232 100644 (file)
        "rcfilters-invalid-filter": "Filtru inválidu",
        "rcfilters-empty-filter": "Nun hai filtros activos. Amuésense toles contribuciones.",
        "rcfilters-filterlist-title": "Filtros",
+       "rcfilters-filterlist-feedbacklink": "Comentar sobro los nuevos filtros (beta)",
        "rcfilters-highlightbutton-title": "Resaltar resultaos",
        "rcfilters-highlightmenu-title": "Seleiciona un color",
        "rcfilters-filterlist-noresults": "Nun s'alcontraron filtros",
index ed4799a..b32e8dc 100644 (file)
        "rcfilters-filterlist-feedbacklink": "Пакінуць водгук пра новыя (бэта) фільтры",
        "rcfilters-highlightbutton-title": "Вылучыць вынікі",
        "rcfilters-highlightmenu-title": "Абярыце колер",
+       "rcfilters-highlightmenu-help": "Абярыце колер для вылучэньня гэтай уласьцівасьці",
        "rcfilters-filterlist-noresults": "Фільтры ня знойдзеныя",
        "rcfilters-filtergroup-registration": "Рэгістрацыя ўдзельнікаў",
        "rcfilters-filter-registered-label": "Зарэгістраваныя",
        "authmanager-create-no-primary": "Пададзеныя ўліковыя зьвесткі ня могуць быць выкарыстаныя для стварэньня рахунку.",
        "authmanager-link-no-primary": "Пададзеныя ўліковыя зьвесткі ня могуць быць выкарыстаныя для злучэньня рахункаў.",
        "authmanager-link-not-in-progress": "Злучэньне рахункаў не выконваецца або страчаныя зьвесткі сэсіі. Калі ласка, пачніце ізноў спачатку.",
+       "authmanager-authplugin-setpass-failed-title": "Памылка зьмены паролю",
+       "authmanager-authplugin-setpass-failed-message": "Дадатак аўтэнтыфікацыі адмовіў зьмену паролю.",
+       "authmanager-authplugin-create-fail": "Дадатак аўтэнтыфікацыі адмовіў у стварэньні рахунку.",
        "authmanager-realname-label": "Сапраўднае імя",
        "authmanager-provider-temporarypassword": "Часовы пароль",
        "changecredentials": "Зьмена ўліковых зьвестак",
index df6da1a..20fd158 100644 (file)
@@ -81,9 +81,9 @@
        "tog-watchlisthidepatrolled": "Скриване на патрулираните редакции от списъка за наблюдение",
        "tog-watchlisthidecategorization": "Скриване на категоризацията на статии",
        "tog-ccmeonemails": "Получаване на копия на писмата, които пращам на другите потребители",
-       "tog-diffonly": "Ð\91ез Ð¿Ð¾ÐºÐ°Ð·Ð²Ð°Ð½Ðµ Ð½а съдържанието на страницата при преглед на разлики",
+       "tog-diffonly": "Ð\94а Ð½Ðµ Ñ\81е Ð¿Ð¾ÐºÐ°Ð·Ð²а съдържанието на страницата при преглед на разлики",
        "tog-showhiddencats": "Показване на скритите категории",
-       "tog-norollbackdiff": "Ð\9dе Ð¿Ð¾ÐºÐ°Ð·Ð²Ð°Ð¹ разликата между редакциите след отмяна на редакции",
+       "tog-norollbackdiff": "Ð\94а Ð½Ðµ Ñ\81е Ð¿Ð¾ÐºÐ°Ð·Ð²Ð° разликата между редакциите след отмяна на редакции",
        "tog-useeditwarning": "Предупреждаване при опит за напускане на страница, отворена в режим на редактиране, без да са запазени промените",
        "tog-prefershttps": "Да се използва винаги защитена връзка при влизане",
        "underline-always": "Винаги",
        "throttled-mailpassword": "Функцията за напомняне на паролата е използвана през {{PLURAL:$1|последния един час|последните $1 часа}}.\nЗа предотвратяване на злоупотреби е разрешено да се изпраща не повече от едно напомняне в рамките на {{PLURAL:$1|един час|$1 часа}}.",
        "mailerror": "Грешка при изпращане на писмо: $1",
        "acct_creation_throttle_hit": "През последните $2, през този IP-адрес посетители на това уики са създали {{PLURAL:$1|1 сметка |$1 сметки}}, което е максималният допустим брой за този период.\nВ резултат, към момента не могат да създават повече потребителски сметки през този IP-адрес.",
-       "emailauthenticated": "Ð\90дÑ\80еÑ\81Ñ\8aÑ\82 Ð½Ð° ÐµÐ»ÐµÐºÑ\82Ñ\80оннаÑ\82а Ð²Ð¸ Ð¿Ð¾Ñ\89а Ð±ÐµÑ\88е потвърден на $2 в $3.",
+       "emailauthenticated": "Ð\90дÑ\80еÑ\81Ñ\8aÑ\82 Ð½Ð° ÐµÐ»ÐµÐºÑ\82Ñ\80оннаÑ\82а Ð¿Ð¾Ñ\89а e потвърден на $2 в $3.",
        "emailnotauthenticated": "Адресът на електронната ви поща все още не е потвърден.\nНяма да получавате писма за никоя от следните възможности.",
        "noemailprefs": "За да работят тези функционалности, трябва да посочите адрес на електронна поща в своите настройки.",
        "emailconfirmlink": "Потвърждаване на адреса за електронна поща",
        "editcomment": "Резюмето на редакцията беше: <em>$1</em>.",
        "revertpage": "Премахване на [[Special:Contributions/$2|редакции на $2]] ([[User talk:$2|беседа]]); възвръщане към последната версия на [[User:$1|$1]]",
        "revertpage-nouser": "Премахнати редакции на (скрито потребителско име) и връщане към последната версия на [[User:$1|$1]]",
-       "rollback-success": "Отменени редакции на $1; възвръщане към последната версия на $2.",
+       "rollback-success": "Отменени редакции на {{GENDER:$3|$1}};\nвъзвръщане към последната версия на {{GENDER:$4|$2}}.",
        "sessionfailure-title": "Прекъсната сесия",
        "sessionfailure": "Изглежда има проблем със сесията ви; действието беше отказано като предпазна мярка срещу крадене на сесията. Натиснете бутона за връщане на браузъра, презаредете страницата, от която сте дошли, и опитайте отново.",
        "changecontentmodel-title-label": "Заглавие на страницата",
        "api-error-emptypage": "Създаването на нови, празени страници, не е разрешено.",
        "api-error-publishfailed": "Вътрешна грешка: Сървърът не успя да съхрани временния файл.",
        "api-error-stashfailed": "Вътрешна грешка: Сървърът не успя да съхрани временния файл.",
-       "api-error-unknown-warning": "Непознато предупреждение: „$1“",
+       "api-error-unknown-warning": "Непознато предупреждение: „$1“.",
        "api-error-unknownerror": "Неизвестна грешка: „$1“.",
        "duration-seconds": "$1 {{PLURAL:$1|секунда|секунди}}",
        "duration-minutes": "$1 {{PLURAL:$1|минута|минути}}",
index 1525cb0..a25c258 100644 (file)
        "passwordremindertext": "কেউ একজন ($1 আইপি ঠিকানাটি থেকে সম্ভবত আপনি) অনুরোধ করেছেন যেন আমরা আপনাকে {{SITENAME}} ($4) এর জন্য একটি নতুন পাসওয়ার্ড পাঠাই।\n\"$2\" নামে অ্যাকাউন্ট খোলা হয়েছে এবং এর পাসওয়ার্ড \"$3\"। আপনি যদি এটাই চেয়ে থাকেন, তাহলে আপনাকে এখন অ্যাকাউন্টে প্রবেশ করতে হবে ও নতুন একটি পাসওয়ার্ড পছন্দ করতে হবে।\n{{PLURAL:$5|এক দিন|$5 দিন}} পরে আপনার এই অস্থায়ী পাসওয়ার্ডের মেয়াদ উত্তীর্ণ হয়ে যাবে।\n\nযদি আপনি ছাড়া অন্য কেউ এই অনুরোধ করে থাকে, কিংবা যদি আপনার পুরনো পাসওয়ার্ড মনে পড়ে গিয়ে থাকে ও সেটি আর বদলাবার ইচ্ছা না থাকে, তাহলে এই বার্তাটি উপেক্ষা করতে পারেন এবং পুরনো পাসওয়ার্ডটিই ব্যবহার করে যেতে পারেন।",
        "noemail": "\"$1\" ব্যবহারকারীর জন্য কোন ই-মেইল ঠিকানা সংরক্ষিত নেই।",
        "noemailcreate": "আপনাকে অবশ্যই একটি সঠিক ইমেইল ঠিকানা দিতে হবে",
-       "passwordsent": "একটি নতুন পাসওয়ার্ড \"$1\" ব্যবহারকারীর ই-মেইল ঠিকানায় পাঠানো হয়েছে। দয়াকরে তা পাওয়ার পর আবার লগ-ইন করুন।",
+       "passwordsent": "একটি নতুন পাসওয়ার্ড \"$1\" ব্যবহারকারীর ই-মেইল ঠিকানায় পাঠানো হয়েছে। দয়া করে তা পাওয়ার পর আবার প্রবেশ করুন।",
        "blocked-mailpassword": "আপনার আইপি ঠিকানাটি থেকে সম্পাদনা করতে বাধা আছে। অপব্যবহার রোধ করার জন্য, এই আইপি ঠিকানা থেকে পাসওয়ার্ড পুনরুদ্ধার করার অনুমতি দেয়া হয়নি।",
        "eauthentsent": "মনোনীত ই-মেইল ঠিকানায় একটি নিশ্চিতকরণ ই-মেইল পাঠানো হয়েছে।\nঐ অ্যাকাউন্টটে অন্য কোন ই-মেইল পাঠানোর আগে আপনাকে ই-মেইলের নির্দেশগুলি অনুসরণ করতে হবে, যাতে অ্যাকাউন্টটি যে আসলেই আপনার, তা নিশ্চিত হয়।",
        "throttled-mailpassword": "বিগত {{PLURAL:$1|ঘণ্টার|$1 ঘণ্টার}} মধ্যে ইতিমধ্যেই একবার পাসওয়ার্ড বদলের তথ্য পাঠানো হয়েছে। অপব্যবহার রোধে প্রতি {{PLURAL:$1|ঘণ্টায়|$1 ঘণ্টায়}} কেবল একবার পাসওয়ার্ড বদলের তথ্য পাঠানো যাবে।",
        "uploadbtn": "ফাইল আপলোড করুন",
        "reuploaddesc": "আপলোড বাতিল করো এবং আপলোড ফর্মে ফেরত যাও।",
        "upload-tryagain": "পরিবর্তিত ফাইল বর্ণনা জমা দিন",
-       "uploadnologin": "à¦\86পনি à¦²à¦\97-à¦\87ন à¦\95রà§\87ননি।",
+       "uploadnologin": "à¦\86পনি à¦ªà§\8dরবà§\87শ à¦\95রà§\87ননি",
        "uploadnologintext": "ফাইল আপলোড করতে হলে আপনাকে অবশ্যই $1 করতে হবে।",
        "upload_directory_missing": "আপলোড ডাইরেক্টরি ($1) পাওয়া যাচ্ছে না এবং ওয়েব সার্ভার কর্তৃক তৈরি করা যাচ্ছে না।",
        "upload_directory_read_only": "আপলোড ডিরেক্টরিটি ($1) ওয়েবসার্ভার কর্তৃক লিখনযোগ্য নয়।",
        "editcomment": "সম্পাদনা সারাংশ ছিল: \"''$1''\"।",
        "revertpage": "[[Special:Contributions/$2|$2]] ([[User talk:$2|আলাপ]]) এর সম্পাদিত সংস্করণ হতে [[User:$1|$1]] এর সম্পাদিত সর্বশেষ সংস্করণে ফেরত যাওয়া হয়েছে।",
        "revertpage-nouser": "একজন গোপন ব্যবহারকারী কর্তৃক সম্পাদিত সম্পাদনাটি বাতিলপূর্বক {{GENDER:$1|[[User:$1|$1]]}}-এর সর্বশেষ সম্পাদনায় ফেরত যাওয়া হয়েছে।",
-       "rollback-success": "$1-এর সম্পাদনাগুলি পূর্বাবস্থায় ফিরিয়ে নেওয়া হয়েছে; $2-এর করা শেষ সংস্করণে পাতাটি ফেরত নেওয়া হয়েছে।",
+       "rollback-success": "{{GENDER:$3|$1}}-এর সম্পাদনাগুলি পূর্বাবস্থায় ফিরিয়ে নেওয়া হয়েছে; {{GENDER:$4|$2}}-এর করা শেষ সংস্করণে পাতাটি ফেরত নেওয়া হয়েছে।",
        "rollback-success-notify": "$1-এর সম্পাদনাগুলি বাতিল করা হয়েছে; \n$2-এর করা শেষ সংস্করণে ফেরত নেওয়া হয়েছে। [$3 পরিবর্তন দেখুন]",
        "sessionfailure-title": "সেশন পরিত্যক্ত",
        "sessionfailure": "আপনার প্রবেশ সেশনে একটি সমস্যা হয়েছে বলে মনে হচ্ছে;\nসেশন হাইজ্যাক প্রতিরোধের উপায় হিসেবে এই কাজটি বাতিল করা হয়েছে।\nঅনুগ্রহ ব্রাউজারের \"পিছনে\" বোতাম চাপুন এবং যে পাতা থেকে এসেছিলেন, তা পুনঃলোড করুন এবং আবার চেষ্টা করুন।",
index 45e1f77..bca3c20 100644 (file)
        "rcfilters-filterlist-feedbacklink": "Reiñ ho soñj diwar-benn ar siloù (beta) nevez",
        "rcfilters-highlightbutton-title": "Lakaat an disoc'hoù war wel",
        "rcfilters-highlightmenu-title": "Dibabit ul liv",
+       "rcfilters-highlightmenu-help": "Diuzañ ul liv evit lakaat ar perzh-mañ war wel",
        "rcfilters-filterlist-noresults": "N'eus bet kavet sil ebet",
        "rcfilters-filtergroup-registration": "Enskrivadur an implijer",
        "rcfilters-filter-registered-label": "Marilhet",
        "listredirects": "Roll an adkasoù",
        "listduplicatedfiles": "Roll restroù gant doublennoù",
        "listduplicatedfiles-summary": "Hemañ zo ur roll restroù m'eo stumm diwezhañ ur restr un eilad eus stumm diwezhañ ur restr bennak all. Ne vez kemeret e kont nemet ar restroù lec'hel.",
+       "listduplicatedfiles-entry": "[[$3|{{PLURAL:$2|un doubl|$2 doubl}}]] en deus [[:File:$1|$1]].",
        "unusedtemplates": "Patromoù dizimplij",
        "unusedtemplatestext": "Rollet eo amañ an holl bajennoù zo en esaouenn anv \"{{ns:template}}\" ha n'int ket implijet war pajenn ebet. Ho pet soñj da wiriañ mat hag-eñ n'eus ket liammoù all war-du ar patromoù-se a-raok diverkañ anezho.",
        "unusedtemplateswlh": "liammoù all",
        "statistics-users-active-desc": "Implijerien o deus degaset da nebeutañ ur c'hemm {{PLURAL:$1|an deiz paseet|e-kerzh an $1 deiz diwezhañ}}",
        "pageswithprop": "Pajennoù gant ur perzh pajenn",
        "pageswithprop-legend": "Pajennoù gant ur perzh pajenn",
+       "pageswithprop-text": "Rollañ a ra ar bajenn-mañ ar pajennoù a ra gant ur perzh pajenn dibar",
        "pageswithprop-prop": "Anv ar perzh :",
        "pageswithprop-submit": "Mont",
        "pageswithprop-prophidden-long": "talvoud perzh testenn hir kuzhet ($1)",
        "uncategorizedcategories": "Rummadoù dirumm",
        "uncategorizedimages": "Restroù hep rummad",
        "uncategorizedtemplates": "Patromoù hep rummad",
+       "uncategorized-categories-exceptionlist": " # Ur roll rummadoù zo ennañ na zlefent ket bezañ meneget war Special:UncategorizedCategories. Unan dre linenn, en ur gregiñ gant \"*\". Ne vez ket kemeret e kont al linennoù a grog gant un arouezenn all (zoken gant esaouennoù c'houllo). Ober gant \"#\" evit ouzhpennañ evezhiadennoù.",
        "unusedcategories": "Rummadoù dizimplij",
        "unusedimages": "Skeudennoù en o-unan",
        "wantedcategories": "Rummadoù goulennet a vank",
        "wantedpages": "Pajennoù goulennet ar muiañ",
+       "wantedpages-summary": "Roll ar pajennoù a vank, gant ar muiañ a liammoù o kas war o zu, hep kontañ ar pajennoù n'eus nemet adkasoù o kas war o zu. Evit kaout roll ar pajennoù a vank gant adkasoù o kas war o zu, sellet ouzh [[{{#special:BrokenRedirects}}|roll an adkasoù a-dreuz]].",
        "wantedpages-badtitle": "Titl direizh er strollad disoc'hoù : $1",
        "wantedfiles": "Restroù a vank",
        "wantedfiletext-cat": "Ober a reer gant ar restroù da-heul koulskoude n'eus ket anezho. Gallout a reer rollañ kavlec'hioù diavaez ha pa vefe anezho. <del>Barrennet</del> e vo an holl falspozitivoù-se. Ouzhpenn-se emañ renablet an holl bajennoù zo enno restroù n'eus ket anezho e [[:$1]].",
        "deadendpagestext": "Ar pajennoù da-heul n'int ket liammet ouzh pajenn ebet all eus {{SITENAME}}.",
        "protectedpages": "Pajennoù gwarezet",
        "protectedpages-indef": "Gwarezoù da badout hepken",
+       "protectedpages-summary": "Rollañ a ra ar bajenn-mañ ar pajennoù zo gwarezet evit ar mare. Evit kaout roll ar pajennoù n'haller ket krouiñ, sellet ouzh [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]].",
        "protectedpages-cascade": "Gwarez dre skalierad hepken",
        "protectedpages-noredirect": "Kuzhat an adkasoù",
        "protectedpagesempty": "N'eus pajenn gwarezet ebet gant an arventennoù-mañ evit poent.",
        "protectedpages-unknown-timestamp": "Dianav",
        "protectedpages-unknown-performer": "Implijer dianav",
        "protectedtitles": "Titloù gwarezet",
+       "protectedtitles-summary": "Rollañ a ra ar bajenn-mañ an titloù n'haller ket implijout evit krouiñ pajennoù. Evit kaout roll ar pajennoù krouet gwarezet, sellet ouzh [[{{#special:ProtectedPages}}|{{int:protectedpages}}]].",
        "protectedtitlesempty": "N'eus bet gwarezet titl ebet dezhañ an arventennoù-se evit poent.",
        "protectedtitles-submit": "Diskwel an titloù",
        "listusers": "Roll an implijerien",
        "apisandbox": "Poull-traezh API",
        "apisandbox-jsonly": "Rekis eo JavaScript evit implijout poull-traezh an API.",
        "apisandbox-api-disabled": "Diweredekaet eo API war al lec'hienn-mañ.",
-       "apisandbox-intro": "Grit gant ar bajenn-mañ evit amprouiñ '''servij Web API MediaWiki'''.\nKit da deuler ur sell war [https://www.mediawiki.org/wiki/API:Main_page titouroù an API] evit gouzout hiroc'h war an doare da embreger API. Da skouer :\n[https://www.mediawiki.org/wiki/API#A_simple_example gwelet danvez ur bennbajenn]. Dibabit un oberiadenn bennak evit gwelet skouerioù all",
+       "apisandbox-intro": "Grit gant ar bajenn-mañ evit amprouiñ <strong>servij Web API MediaWiki</strong>.\nKit da deuler ur sell war [[mw:API:Main page|teuliadur API]] evit gouzout hiroc'h war an doare da embreger API. Da skouer :\n[https://www.mediawiki.org/wiki/API#A_simple_example gwelet danvez ur bajenn zegemer]. Dibabit un oberiadenn bennak evit gwelet skouerioù all.\n\nNotit mat : ha pa vefe ar bajenn-mañ ur poull-traezh, an oberiadennoù a rit amañ a c'hall kemmañ ar wiki.",
        "apisandbox-fullscreen": "Dispakañ ar banell",
+       "apisandbox-fullscreen-tooltip": "Brasaat panell ar poull-traezh evit leuniañ prenestr ar merdeer.",
        "apisandbox-unfullscreen": "Diskouez ar bajenn",
+       "apisandbox-unfullscreen-tooltip": "Bihanaat panell ar poull-traezh evit ma vefe hegerz liammoù merdeiñ MediaWiki.",
        "apisandbox-submit": "Sevel ar goulenn",
        "apisandbox-reset": "Riñsañ",
        "apisandbox-retry": "Klask en-dro",
+       "apisandbox-loading": "O kargañ titouroù modulenn \"$1\" an API...",
+       "apisandbox-load-error": "C'hoarvezet ez eus ur fazi en ur gargañ modulenn \"$1\" an API : $2",
+       "apisandbox-no-parameters": "N'eus arventenn ebet d'ar vodulenn API-mañ.",
        "apisandbox-helpurls": "Liammoù skoazell",
        "apisandbox-examples": "Skouerioù",
        "apisandbox-dynamic-parameters": "Arventenn ouzhpenn",
        "apisandbox-dynamic-parameters-add-label": "Ouzhpennañ un arventenn:",
        "apisandbox-dynamic-parameters-add-placeholder": "Anv an arventenn",
        "apisandbox-dynamic-error-exists": "Bez' ez eus c'hoazh eus un arventenn anvet \"$1\".",
+       "apisandbox-deprecated-parameters": "Arventennoù dispredet",
        "apisandbox-fetch-token": "Leuniañ emgefre ar jedouer",
        "apisandbox-submit-invalid-fields-title": "Direizh eo maeziennoù zo",
        "apisandbox-submit-invalid-fields-message": "Reizhit ar maeziennoù merket ha klaskit en-dro.",
        "apisandbox-results": "Disoc'hoù",
        "apisandbox-sending-request": "O kas ur reked API...",
        "apisandbox-loading-results": "O resev ur reked API...",
+       "apisandbox-results-error": "C'hoarvezet ez eus ur fazi en ur gargañ respont ar reked API : $1.",
+       "apisandbox-request-selectformat-label": "Diskouez roadennoù ar reked evel :",
+       "apisandbox-request-format-url-label": "Neudennad reked an URL",
        "apisandbox-request-url-label": "Goulenn URL :",
        "apisandbox-request-json-label": "Goulenn JSON :",
        "apisandbox-request-time": "Pad ar goulenn : {{PLURAL:$1|$1 ms}}",
+       "apisandbox-results-fixtoken": "Reizhañ ar jedouer ha kas en-dro",
+       "apisandbox-results-fixtoken-fail": "N'eus ket bet gallet adtapout ar jedouer \"$1\".",
        "apisandbox-alert-page": "Direizh eo maeziennoù ar bajenn-mañ.",
        "apisandbox-alert-field": "Talvoud ar vaezienn-mañ n'eo ket reizh.",
        "apisandbox-continue": "Kenderc'hel",
        "apisandbox-continue-clear": "Riñsañ",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} a [https://www.mediawiki.org/wiki/API:Query#Continuing_queries gendalc'ho] ar reked diwezhañ ; {{int:apisandbox-continue-clear}} a ziverko an arventennoù a denn d'ar c'hendalc'h.",
+       "apisandbox-param-limit": "Merkañ <kbd>max</kbd> evit ober gant ar vevenn vrasañ.",
        "apisandbox-multivalue-all-namespaces": "$1 (An holl esaouennoù anv)",
        "apisandbox-multivalue-all-values": "$1 (An holl dalvoudoù)",
        "booksources": "Oberennoù dave",
        "booksources-search": "Klask",
        "booksources-text": "Ur roll liammoù a gas da lec'hiennoù all ma werzher levrioù kozh ha nevez a gavot a-is; marteze e kavot eno titouroù pelloc'h war al levrioù a glaskit :",
        "booksources-invalid-isbn": "Evit doare n'eo ket reizh an ISBN merket; gwiriit ha n'oc'h ket faziet en ur eilañ adal ar vammenn orin.",
+       "magiclink-tracking-rfc": "Pajennoù a ra gant liammoù hud RFC",
+       "magiclink-tracking-rfc-desc": "Ober a ra ar bajenn-mañ gant liammoù hud RFC. Gwelet [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] evit an doare da dreuzlec'hiañ.",
+       "magiclink-tracking-pmid": "Pajennoù a ra gant liammoù hud PMID",
+       "magiclink-tracking-pmid-desc": "Ober a ra ar bajenn-mañ gant liammoù hud PMID. Gwelet [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] evit an doare da dreuzlec'hiañ.",
        "magiclink-tracking-isbn": "Pajennoù a ra gant liammoù burzhudus ISBN",
+       "magiclink-tracking-isbn-desc": "Ober a ra ar bajenn-mañ gant liammoù hud ISBN. Gwelet [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] evit an doare da dreuzlec'hiañ.",
        "specialloguserlabel": "Implijer :",
-       "speciallogtitlelabel": "Bukadenn (titl pe implijer) :",
+       "speciallogtitlelabel": "Pal (titl pe {{ns:user}}:anv implijer) :",
        "log": "Marilhoù",
        "logeventslist-submit": "Diskouez",
        "all-logs-page": "An holl varilhoù foran",
        "logempty": "Goullo eo istor ar bajenn-mañ.",
        "log-title-wildcard": "Klask an titloù a grog gant an destenn-mañ",
        "showhideselectedlogentries": "Diskouez/kuzhat penngerioù ar marilh bet diuzet",
+       "log-edit-tags": "Kemmañ tikedennoù an enmontoù marilh diuzet",
        "checkbox-select": "Diuzañ : $1",
        "checkbox-all": "An holl",
        "checkbox-none": "Hini ebet",
        "activeusers-intro": "Setu aze ur roll eus an implijerien zo bet oberiant mui pe vui e-pad an $1 {{PLURAL:$1|deiz|deiz}} diwezhañ.",
        "activeusers-count": "$1 {{PLURAL:$1|oberiadenn}} abaoe an {{PLURAL:$3|deiz|$3 deiz}} diwezhañ",
        "activeusers-from": "Diskouez an implijerien adal :",
+       "activeusers-groups": "Diskouez an implijerien zo ezel eus ar strolladoù :",
+       "activeusers-excludegroups": "Skarzhañ an implijerien ezel eus ar strolladoù :",
        "activeusers-noresult": "N'eus bet kavet implijer ebet.",
        "activeusers-submit": "Diskouez an implijerien oberiant",
        "listgrouprights": "Gwirioù ar strolladoù implijer",
        "listgrouprights-namespaceprotection-namespace": "Esaouenn anv",
        "listgrouprights-namespaceprotection-restrictedto": "Gwir(ioù) hag a aotre an implijer da aozañ",
        "listgrants": "Aotreoù",
+       "listgrants-summary": "Setu aze ur roll eus an aotreoù gant ar gwirioù moned stag ouzh ar gwirioù implijer. Gallout a ra an implijerien aotren an arloadoù da implijout o c'hont gant aotreoù strishaet diazezet war ar gwirioù bet grataet d'an arload gant an implijer. Un arload a ra en anv un implijer n'hall ket ober gant gwirioù n'int ket bet roet d'an implijer da gentañ. Gallout a ra bezañ tammoù [[{{MediaWiki:Listgrouprights-helppage}}|titouroù ouzhpenn]] diwar-benn ar gwirioù hiniennel.",
        "listgrants-grant": "Aotren",
        "listgrants-rights": "Gwirioù",
        "trackingcategories": "Rummadoù evezhiañ",
+       "trackingcategories-summary": "Rollañ a ra ar bajenn-mañ ar rummadoù heuliañ leuniet ent emgefre gant MediaWiki. Gallout a ra o anvioù bezañ cheñchet en ur gemmañ ar c'hemennadennoù reizhiad a glot ganto en esaouenn anv {{ns:8}}.",
        "trackingcategories-msg": "Rummad evezhiañ",
        "trackingcategories-name": "Anv ar gemennadenn",
        "trackingcategories-desc": "Dezverkoù evit degemer rummadoù",
+       "restricted-displaytitle-ignored": "Pajennoù gant titloù diskwel lezet a-gostez",
+       "restricted-displaytitle-ignored-desc": "Ar bajenn-mañ zo dezhi un <code><nowiki>{{DISPLAYTITLE}}</nowiki></code> zo bet laosket a-gostez peogwir n'eo ket kevatal d'an titl zo d'ar bajenn bremañ.",
+       "noindex-category-desc": "Ar bajenn-mañ n'eo ket menegeret gant ar robotoù rak ar ger hud <code><nowiki>__NOINDEX__</nowiki></code> zo enni hag emañ en un esaouenn anv m'eo aotreet ar merkañ.",
+       "index-category-desc": "Un <code><nowiki>__INDEX__</nowiki></code> zo er bajenn (hag emañ en un esaouenn anv m'eo aotreet ober gant ar merk-se). Abalamour da se e vo menegeret gant robotoù pa ne vije ket bet a-hend-all.",
        "broken-file-category-desc": "Er bajenn-mañ ez eus ul liamm restr torr (ul liamm da enframmañ ur restr pa n'eus ket eus ar restr-se).",
        "trackingcategories-nodesc": "N'eus deskrivadur ebet.",
        "trackingcategories-disabled": "Diweredekaet eo ar rummad",
        "rollbacklinkcount": "disteurel $1 {{PLURAL:$1|kemm}}",
        "rollbacklinkcount-morethan": "disteurel ouzhpenn $1 {{PLURAL:$1|kemm}}",
        "rollbackfailed": "C'hwitet eo bet an distaoladenn",
+       "rollback-missingparam": "Arventennoù rekis d'ar reked a vank.",
+       "rollback-missingrevision": "Dibosupl kargañ roadennoù ar stumm.",
        "cantrollback": "Dibosupl da zisteuler: an aozer diwezhañ eo an hini nemetañ da vezañ kemmet ar pennad-mañ",
        "alreadyrolled": "Dibosupl eo disteuler ar c'hemm diwezhañ graet d'ar bajenn [[:$1]] gant [[User:$2|$2]] ([[User talk:$2|Kaozeal]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]);\nkemmet pe distaolet eo bet c'hoazh gant unan bennak all.\n\nAr c'hemm diwezhañ d'ar bajenn-mañ a oa bet graet gant [[User:$3|$3]] ([[User talk:$3|Kaozeal]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "editcomment": "Diverradenn ar c'hemm a oa : <em>$1</em>.",
        "rollback-success": "Disteuler kemmoù $1; distreiñ da stumm diwezhañ $2.",
        "sessionfailure-title": "Fazi dalc'h",
        "sessionfailure": "Evit doare ez eus ur gudenn gant ho talc'h;\nNullet eo bet an ober-mañ a-benn en em wareziñ diouzh an tagadennoù preizhañ.\nKlikit war \"kent\" hag adkargit ar bajenn oc'h deuet drezi; goude klaskit en-dro.",
+       "changecontentmodel": "Cheñch patrom danvez ur bajenn",
+       "changecontentmodel-legend": "Cheñch ar patrom danvez",
        "changecontentmodel-title-label": "Anv ar bajenn",
        "changecontentmodel-model-label": "Patrom danvez nevez",
        "changecontentmodel-reason-label": "Abeg :",
        "changecontentmodel-submit": "Kemmañ",
        "changecontentmodel-success-title": "Cheñchet eo bet ar patrom danvez",
+       "changecontentmodel-success-text": "Kemmet eo bet patrom danvez [[:$1]].",
+       "changecontentmodel-cannot-convert": "N'hall ket danvez [[:$1]] bezañ troet en ur seurt $2.",
        "changecontentmodel-emptymodels-title": "N'eus patrom danvez hegerz ebet",
        "logentry-contentmodel-change-revertlink": "disteuler",
        "logentry-contentmodel-change-revert": "disteuler",
        "block-log-flags-hiddenname": "anv implijer kuzhet",
        "range_block_disabled": "Diweredekaet eo bet ar stankañ stuc'hadoù IP.",
        "ipb_expiry_invalid": "amzer termen direizh.",
+       "ipb_expiry_old": "Tremenet eo an termen echuiñ.",
        "ipb_expiry_temp": "Peurbadus e rank bezañ bloc'hadoù an implijerien guzh.",
        "ipb_hide_invalid": "Ne c'haller ket dilemel ar gont-mañ : Ouzhpenn {{PLURAL:$1|ur c'hemm|$1 kemm}} zo enni.",
        "ipb_already_blocked": "Stanket eo \"$1\" dija",
index 457fea9..fc83760 100644 (file)
        "viewcount": "Ovoj stranici je pristupljeno {{PLURAL:$1|$1 put|$1 puta}}.",
        "protectedpage": "Zaštićena stranica",
        "jumpto": "Idi na:",
-       "jumptonavigation": "navigacija",
-       "jumptosearch": "traži",
+       "jumptonavigation": "navigaciju",
+       "jumptosearch": "pretragu",
        "view-pool-error": "Žao nam je, serveri su trenutno preopterećeni.\nPreviše korisnika pokušava da pregleda ovu stranicu.\nMolimo pričekajte trenutak prije nego što ponovno pokušate pristupiti ovoj stranici.\n\n$1",
        "generic-pool-error": "Nažalost, serveri su trenutno preopterećeni.\nPreviše korisnika pokušava da vidi ovaj resurs.\nMolimo pričekajte trenutak prije nego što ponovo pokušate da mu pristupite.",
        "pool-timeout": "Zaustavi čekanje na zaključavanje",
        "headline_sample": "Naslov",
        "headline_tip": "Podnaslov",
        "nowiki_sample": "Dodaj neformatirani tekst ovdje",
-       "nowiki_tip": "Ignoriši viki formatiranje teksta",
+       "nowiki_tip": "Zanemari wikiformatiranje",
        "image_sample": "ime_slike.jpg",
        "image_tip": "Uklopljena slika",
        "media_sample": "ime_medija_fajla.ogg",
        "importcantopen": "Ne mogu otvoriti datoteku za uvoz",
        "importbadinterwiki": "Loš interwiki link",
        "importsuccess": "Uspješno ste uvezli stranicu!",
-       "importnosources": "Nije definisan međuwiki izvor za uvoz i direktna postavljanja historije su isključena.",
+       "importnosources": "Nije definiran međuwiki izvor za uvoz i direktna postavljanja historije su isključena.",
        "importnofile": "Uvozna datoteka nije postavljena.",
        "importuploaderrorsize": "Postavljanje uvozne datoteke nije uspjelo.\nDatoteka je veća nego što je dopušteno.",
        "importuploaderrorpartial": "Postavljanje uvozne datoteke nije uspjelo.\nDatoteka je samo djelimično postavljena.",
        "monthsall": "sve",
        "confirmemail": "Potvrdite adresu e-pošte",
        "confirmemail_noemail": "Niste unijeli tačnu e-mail adresu u Vaše [[Special:Preferences|korisničke postavke]].",
-       "confirmemail_text": "Ova viki zahtjeva da potvrdite adresu Vaše e-pošte prije nego što koristite mogućnosti e-pošte. Aktivirajte dugme ispod kako bi ste poslali poštu za potvrdu na Vašu adresu. Pošta uključuje link koji sadrži kod; učitajte link u Vaš preglednik da bi ste potvrdili da je adresa Vaše e-pošte validna.",
+       "confirmemail_text": "{{SITENAME}} zahtijeva da potvrdite adresu Vaše e-pošte prije nego što koristite mogućnosti e-pošte.\nAktivirajte dugme ispod kako biste poslali poštu za potvrdu na Vašu adresu.\nPošta uključuje link koji sadrži kod;\nučitajte link u Vaš preglednik da biste potvrdili da je adresa Vaše e-pošte validna.",
        "confirmemail_pending": "Potvrdni kod Vam je već poslan e-poštom;\nako ste nedavno otvorili račun, trebali biste pričekati nekoliko minuta da poslana pošta stigne, prije nego što ponovo zahtijevate novi kod.",
        "confirmemail_send": "Pošaljite kod za potvrdu",
        "confirmemail_sent": "E-pošta za potvrđivanje poslata.",
index fcfdd22..140bbf4 100644 (file)
@@ -55,7 +55,8 @@
                        "KRLS",
                        "Jaumeortola",
                        "Kippelboy",
-                       "Syum90"
+                       "Syum90",
+                       "Xð"
                ]
        },
        "tog-underline": "Subratlla els enllaços:",
        "unprotectthispage": "Desprotegeix aquesta pàgina",
        "newpage": "Pàgina nova",
        "talkpage": "Discussió",
-       "talkpagelinktext": "Discussió",
+       "talkpagelinktext": "discussió",
        "specialpage": "Pàgina especial",
        "personaltools": "Eines de l'usuari",
        "articlepage": "Mostra la pàgina",
index 4957e3c..f224ed7 100644 (file)
        "modifiedarticleprotection": "ئاستی پاراستنی «[[$1]]»ی گۆڕی",
        "unprotectedarticle": "پاراستنی لەسەر «[[$1]]» لابرد",
        "movedarticleprotection": "ڕێککارییەکانی پاراستن لە  «[[$2]]» گوازرایەوە بۆ «[[$1]]»",
+       "unprotectedarticle-comment": "{{GENDER:$2|پاراستنی}} لەسەر ''[[$1]]'' لابرد",
        "protect-title": "گۆڕینی ئاستی پاراستنی \"$1\"",
        "protect-title-notallowed": "دیتنی ئاستی پاراستنی «$1»",
        "prot_1movedto2": "[[$1]] گوازرایەوە بۆ [[$2]]",
index 9d39994..46f9cf4 100644 (file)
        "logentry-upload-upload": "$1 {{GENDER:$2|положишє}} $3",
        "revdelete-summary": "мѣнꙑ опьсаниѥ",
        "feedback-cancel": "отъмѣтаниѥ",
-       "searchsuggest-search": "исканиѥ",
+       "searchsuggest-search": "{{SITENAME}} : исканиѥ",
        "searchsuggest-containing": "сѥ дрьжащи···",
        "api-error-unknownerror": "нєвѣдома блаꙁна : ⁖ $1 ⁖",
        "duration-seconds": "$1 {{PLURAL:$1|дєѵтєролєпто|дєѵтєролєпта|дєѵтєролєптъ}}",
index 351ac32..e3291ce 100644 (file)
        "tog-newpageshidepatrolled": "Skjul patruljerede sider på listen over nye sider",
        "tog-hidecategorization": "Skjul kategorisering af sider",
        "tog-extendwatchlist": "Udvid overvågningslisten til at vise alle ændringer og ikke kun den nyeste",
-       "tog-usenewrc": "Gruppér ændringer per side i listen over seneste ændringer og i overvågningslisten",
+       "tog-usenewrc": "Gruppér ændringer efter side i listen over seneste ændringer og i overvågningslisten",
        "tog-numberheadings": "Automatisk nummerering af overskrifter",
        "tog-showtoolbar": "Vis værktøjslinje til redigering",
        "tog-editondblclick": "Redigér sider med dobbeltklik",
        "tog-editsectiononrightclick": "Redigér afsnit ved at højreklikke på deres titler",
-       "tog-watchcreations": "Tilføj sider, jeg opretter, og filer, jeg lægger op, til min overvågningsliste",
-       "tog-watchdefault": "Tilføj sider og filer, jeg redigerer, til min overvågningsliste",
-       "tog-watchmoves": "Tilføj sider og filer, jeg flytter, til min overvågningsliste",
-       "tog-watchdeletion": "Tilføj sider og filer, jeg sletter, til min overvågningsliste",
-       "tog-watchuploads": "Tilføj nye filer jeg uploader til min overvågningsliste",
-       "tog-watchrollback": "Tilføj sider, hvor jeg har udført en tilbagerulning til min overvågningsliste",
+       "tog-watchcreations": "Føj sider, jeg opretter, og filer, jeg lægger op, til min overvågningsliste",
+       "tog-watchdefault": "Føj sider og filer, jeg redigerer, til min overvågningsliste",
+       "tog-watchmoves": "Føj sider og filer, jeg flytter, til min overvågningsliste",
+       "tog-watchdeletion": "Føj sider og filer, jeg sletter, til min overvågningsliste",
+       "tog-watchuploads": "Føj nye filer, jeg uploader, til min overvågningsliste",
+       "tog-watchrollback": "Føj sider, hvor jeg har udført en tilbagerulning, til min overvågningsliste",
        "tog-minordefault": "Markér som standard alle redigeringer som mindre",
        "tog-previewontop": "Vis forhåndsvisning over redigeringsboksen",
        "tog-previewonfirst": "Vis forhåndsvisning ved første redigering",
        "tog-ccmeonemails": "Send mig kopier af e-mails som jeg sender til andre brugere",
        "tog-diffonly": "Vis ikke sideindhold neden under versionssammenligninger",
        "tog-showhiddencats": "Vis skjulte kategorier",
-       "tog-norollbackdiff": "Vis ikke forskel efter tilbagerulning",
+       "tog-norollbackdiff": "Vis ikke forskel efter udførelse af en tilbagerulning",
        "tog-useeditwarning": "Advar mig, hvis jeg forlader en redigeringsside med ændringer, der ikke er gemt.",
-       "tog-prefershttps": "Brug altid en sikker forbindelse, når du er logget ind",
+       "tog-prefershttps": "Brug altid en sikker forbindelse, når jeg er logget ind",
        "underline-always": "Altid",
        "underline-never": "Aldrig",
        "underline-default": "Brug browserens indstilling eller standarden for det valgte udseende",
        "pagecategories": "{{PLURAL:$1|Kategori|Kategorier}}",
        "category_header": "Sider i kategorien \"$1\"",
        "subcategories": "Underkategorier",
-       "category-media-header": "Medier i kategorien „$1“",
-       "category-empty": "''Denne kategori indeholder for øjeblikket hverken sider eller medie-filer.''",
+       "category-media-header": "Medier i kategorien \"$1\"",
+       "category-empty": "<em>Denne kategori indeholder for øjeblikket hverken sider eller medie-filer.</em>",
        "hidden-categories": "{{PLURAL:$1|Skjult kategori|Skjulte kategorier}}",
        "hidden-category-category": "Skjulte kategorier",
-       "category-subcat-count": "{{PLURAL:$2|Denne kategori har en underkategori.|Denne kategori indeholder nedenstående {{PLURAL:$1|underkategori|$1 underkategorier}}, af i alt $2.}}",
-       "category-subcat-count-limited": "Denne kategori indeholder {{PLURAL:$1|underkategori|$1 underkategorier}}.",
-       "category-article-count": "Denne kategori indeholder {{PLURAL:$2|kun den nedenstående side|{{PLURAL:$1|den nedenstående side|de nedenstående $1 sider}} af i alt $2.}}",
-       "category-article-count-limited": "Kategorien indeholder {{PLURAL:$1|den nedenstående side|de nedenstående $1 sider}}.",
-       "category-file-count": "Denne kategori indeholder {{PLURAL:$2|kun den nedenstående fil.|{{PLURAL:$1|den nedenstående fil|de nedenstående $1 filer}} af i alt $2.}}",
-       "category-file-count-limited": "Kategorien indeholder {{PLURAL:$1|den nedenstående fil|de nedenstående $1 filer}}.",
+       "category-subcat-count": "{{PLURAL:$2|Denne kategori har kun den nedenstående underkategori.|Denne kategori har nedenstående {{PLURAL:$1|underkategori|$1 underkategorier}}, af i alt $2.}}",
+       "category-subcat-count-limited": "Denne kategori har nedenstående {{PLURAL:$1|underkategori|$1 underkategorier}}.",
+       "category-article-count": "{{PLURAL:$2|Denne kategori indeholder kun den nedenstående side.|{{PLURAL:$1|Den nedenstående side|De nedenstående $1 sider}} er i denne kategori, ud af i alt $2.}}",
+       "category-article-count-limited": "{{PLURAL:$1|Den nedenstående side|De nedenstående $1 sider}} er i denne kategori.",
+       "category-file-count": "{{PLURAL:$2|Denne kategori indeholder kun den nedenstående fil.|{{PLURAL:$1|Den nedenstående fil|De nedenstående $1 filer}} er i denne kategori, ud af i alt $2.}}",
+       "category-file-count-limited": "{{PLURAL:$1|Den nedenstående fil|De nedenstående $1 filer}} er i denne kategori.",
        "listingcontinuesabbrev": "forts.",
        "index-category": "Indekserede sider",
        "noindex-category": "Ikke-indekserede sider",
        "qbfind": "Søg",
        "qbbrowse": "Gennemse",
        "qbedit": "Redigér",
-       "qbpageoptions": "Indstillinger for side",
-       "qbmyoptions": "Mine indstillinger",
+       "qbpageoptions": "Denne side",
+       "qbmyoptions": "Mine sider",
        "faq": "OSS",
        "faqpage": "Project:OSS",
        "actions": "Handlinger",
        "tagline": "Fra {{SITENAME}}",
        "help": "Hjælp",
        "search": "Søg",
-       "search-ignored-headings": " #<!-- lad denne linje være præcis som den er --> <pre>\n# Overskrifter, der vil blive ignoreret af søgning.\n# Ændringer til dette træder i kraft så snart siden med overskriften er indekseret.\n# Du kan tvinge siden til genindeksering ved at lave en nul redigering.\n# syntaksen er som følger:\n#   * Alt fra en tegnet \"#\" til slutningen af linjen er en kommentar\n#   * Hver ikke-tomme linje er den nøjagtige titel der skal ignoreres, der skelnes også mellem store og små bogstaver\nReferencer\nEksterne henvisninger\nSe også\nKilder og henvisninger\nEksterne kilder/henvisninger\nKilder\n #</pre> <!-- lad denne linje være præcis som den er -->",
+       "search-ignored-headings": " #<!-- leave this line exactly as it is --> <pre>\n# Overskrifter, der vil blive ignoreret af søgning.\n# Ændringer til dette træder i kraft så snart siden med overskriften er indekseret.\n# Du gennemtvinge genindeksering af siden ved at lave en tom redigering.\n# Syntaksen er som følger:\n#   * Alt fra et \"#\"-tegn til slutningen af linjen er en kommentar.\n#   * Hver ikke-tom linje er den nøjagtige titel der skal ignoreres; der skelnes mellem store og små bogstaver.\nReferencer\nEksterne henvisninger\nSe også\nKilder og henvisninger\nEksterne kilder/henvisninger\nKilder\n #<!-- leave this line exactly as it is --> <pre>",
        "searchbutton": "Søg",
        "go": "Gå til",
        "searcharticle": "Gå til",
        "delete": "Slet",
        "deletethispage": "Slet side",
        "undeletethispage": "Gendan denne side",
-       "undelete_short": "Fortryd sletning af {{PLURAL:$1|én version|$1 versioner}}",
-       "viewdeleted_short": "Vis {{PLURAL:$1|en slettet redigering|$1 slettede redigeringer}}",
+       "undelete_short": "Gendan {{PLURAL:$1|én redigering|$1 redigeringer}}",
+       "viewdeleted_short": "Vis {{PLURAL:$1|én slettet redigering|$1 slettede redigeringer}}",
        "protect": "Beskyt",
        "protect_change": "ændr",
        "protectthispage": "Beskyt side",
        "unprotect": "Ændr beskyttelse",
-       "unprotectthispage": "Ændre beskyttelsen af denne side",
+       "unprotectthispage": "Ændr beskyttelsen af denne side",
        "newpage": "Ny side",
        "talkpage": "Diskussion",
        "talkpagelinktext": "diskussion",
        "tool-link-userrights": "Ændre {{GENDER:$1|bruger}}grupper",
        "tool-link-userrights-readonly": "Se {{GENDER:$1|bruger}}grupper",
        "tool-link-emailuser": "Send e-mail til denne {{GENDER:$1|bruger}}",
-       "userpage": "Se brugersiden",
-       "projectpage": "Se projektsiden",
-       "imagepage": "Se filside",
-       "mediawikipage": "Vise indholdsside",
-       "templatepage": "Vise skabelonside",
-       "viewhelppage": "Vise hjælpeside",
-       "categorypage": "Vise kategoriside",
-       "viewtalkpage": "Se diskussion",
-       "otherlanguages": "Andre sprog",
+       "userpage": "Vis brugerside",
+       "projectpage": "Vis projektside",
+       "imagepage": "Vis filside",
+       "mediawikipage": "Vis indholdsside",
+       "templatepage": "Vis skabelonside",
+       "viewhelppage": "Vis hjælpeside",
+       "categorypage": "Vis kategoriside",
+       "viewtalkpage": "Vis diskussion",
+       "otherlanguages": "På andre sprog",
        "redirectedfrom": "(Omdirigeret fra $1)",
        "redirectpagesub": "Omdirigeringsside",
        "redirectto": "Omdiriger til:",
        "lastmodifiedat": "Denne side blev senest ændret $1 kl. $2.",
-       "viewcount": "Siden er vist {{PLURAL:$1|en gang|$1 gange}}.",
+       "viewcount": "Siden er vist {{PLURAL:$1|én gang|$1 gange}}.",
        "protectedpage": "Beskyttet side",
        "jumpto": "Skift til:",
        "jumptonavigation": "navigering",
        "jumptosearch": "søgning",
-       "view-pool-error": "Beklager, men serverne er i øjeblikket overbelastede.\nFor mange brugere prøver at se denne side.\nVent et øjeblik, før du prøver at besøge denne side igen.\n\n$1",
-       "generic-pool-error": "Beklager, men serverne er i øjeblikket overbelastede.\nFor mange brugere prøver at se denne side.\nVent et øjeblik før du prøver at besøge denne side igen.",
-       "pool-timeout": "Timeout mens man venter på låsningen",
-       "pool-queuefull": "Pool køen er fuld",
+       "view-pool-error": "Beklager, men serverne er i øjeblikket overbelastede.\nFor mange brugere prøver at se denne side.\nVent venligst lidt, før du igen prøver at besøge denne side.\n\n$1",
+       "generic-pool-error": "Beklager, men serverne er i øjeblikket overbelastede.\nFor mange brugere prøver at se denne side.\nVent venligst lidt, før du igen prøver at besøge denne side.",
+       "pool-timeout": "Timeout mens der blev ventet på låsen",
+       "pool-queuefull": "Puljekøen er fuld",
        "pool-errorunknown": "Ukendt fejl",
-       "pool-servererror": "Pool-counter servicen er ikke til rådighed ($1).",
+       "pool-servererror": "Puljetæller-tjenesten er ikke til rådighed ($1).",
        "poolcounter-usage-error": "Brugsfejl: $1",
        "aboutsite": "Om {{SITENAME}}",
        "aboutpage": "Project:Om",
        "privacy": "Behandling af personlige oplysninger",
        "privacypage": "Project:Behandling af personlige oplysninger",
        "badaccess": "Manglende rettigheder",
-       "badaccess-group0": "Du har ikke de nødvendige rettigheder til denne handling.",
-       "badaccess-groups": "Denne handling kan kun udføres af brugere som tilhører {{PLURAL:$2|gruppen|en af grupperne:}} $1.",
+       "badaccess-group0": "Du har ikke de nødvendige rettigheder til at udføre den handling, du har anmodet om.",
+       "badaccess-groups": "Denne handling kan kun udføres af brugere i {{PLURAL:$2|gruppen|disse grupper:}} $1.",
        "versionrequired": "Kræver version $1 af MediaWiki",
-       "versionrequiredtext": "Version $1 af MediaWiki er påkrævet, for at bruge denne side. Se [[Special:Version|Versionssiden]]",
+       "versionrequiredtext": "Version $1 af MediaWiki er nødvendig for at bruge denne side. Se [[Special:Version|Versionssiden]]",
        "ok": "OK",
        "retrievedfrom": "Hentet fra \"$1\"",
        "youhavenewmessages": "Du har $1 ($2).",
-       "youhavenewmessagesfromusers": "Du har $1 fra {{PLURAL:$3|en anden bruger| $3 brugere}} ($2).",
+       "youhavenewmessagesfromusers": "{{PLURAL:$4|Du har}} $1 fra {{PLURAL:$3|en anden bruger|$3 brugere}} ($2).",
        "youhavenewmessagesmanyusers": "Du har $1 fra mange brugere ($2).",
        "newmessageslinkplural": "{{PLURAL:$1|en ny besked|999=nye beskeder}}",
        "newmessagesdifflinkplural": "seneste {{PLURAL:$1|ændring|999=ændringer}}",
        "confirmable-confirm": "Er {{GENDER:$1|du}} sikker?",
        "confirmable-yes": "Ja",
        "confirmable-no": "Nej",
-       "thisisdeleted": "Se eller gendan $1?",
+       "thisisdeleted": "Vis eller gendan $1?",
        "viewdeleted": "Vis $1?",
-       "restorelink": "{{PLURAL:$1|en slettet ændring|$1 slettede ændringer}}",
+       "restorelink": "{{PLURAL:$1|én slettet redigering|$1 slettede redigeringer}}",
        "feedlinks": "Feed:",
        "feed-invalid": "Ugyldig abonnementstype.",
        "feed-unavailable": "Der er ingen syndikeringsfeeds tilgængelige",
        "site-atom-feed": "$1 Atom-feed",
        "page-rss-feed": "\"$1\" RSS-feed",
        "page-atom-feed": "\"$1\" Atom-feed",
-       "red-link-title": "$1 (siden er ikke skrevet endnu)",
+       "red-link-title": "$1 (siden findes ikke)",
        "sort-descending": "Sorter faldende",
        "sort-ascending": "Sorter stigende",
        "nstab-main": "Side",
        "nstab-category": "Kategori",
        "mainpage-nstab": "Forside",
        "nosuchaction": "Funktionen findes ikke",
-       "nosuchactiontext": "Handlingen som er angivet i URL'en er ugyldig.\nDu kan have skrevet URL'en forkert, eller fulgt en ukorrekt henvisning.\nDet kan også skyldes en fejl i programmellet som bruges af {{SITENAME}}.",
+       "nosuchactiontext": "Handlingen som er angivet i URL'en er ugyldig.\nDu har måske skrevet URL'en forkert eller fulgt en ukorrekt henvisning.\nDet kan også skyldes en fejl i programmellet som bruges af {{SITENAME}}.",
        "nosuchspecialpage": "En sådan specialside findes ikke",
-       "nospecialpagetext": "<strong>Du har bedt om en specialside, der ikke kan genkendes af MediaWiki-softwaren.</strong>\nEn liste over gyldige specialsider findes på [[Special:SpecialPages|{{int:specialpages}}]].",
+       "nospecialpagetext": "<strong>Du har bedt om en specialside, som ikke findes.</strong>\n\nEn liste over gyldige specialsider kan findes på [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "Fejl",
        "databaseerror": "Databasefejl",
-       "databaseerror-text": "Der opstod fejl i en forespørgsel til databasen.\nDette kan indikere en fejl i softwaren.",
-       "databaseerror-textcl": "Der opstod fejl i en forespørgsel til databasen.",
+       "databaseerror-text": "Der opstod en fejl i en forespørgsel til databasen.\nDette kan skyldes en programmeringsfejl i softwaren.",
+       "databaseerror-textcl": "Der opstod en fejl i en forespørgsel til databasen.",
        "databaseerror-query": "Forespørgsel: $1",
        "databaseerror-function": "Funktion: $1",
        "databaseerror-error": "Fejl: $1",
-       "transaction-duration-limit-exceeded": "For at undgå høje replikationsforsinkelser blev denne transaktion afbrudt fordi tiden det tog at lagre ($1) oversteg maksimumsgrænsen på $2 {{PLURAL:$2|sekund|sekunder}}.\nHvis du er ved at ændre mange emner på en gang, så prøv at udføre flere mindre operationer i stedet.",
-       "laggedslavemode": "'''Bemærk:''' Den viste side indeholder muligvis ikke de nyeste ændringer.",
-       "readonly": "Databasen er skrivebeskyttet",
-       "enterlockreason": "Skriv en begrundelse for skrivebeskyttelsen, med samt en vurdering af, hvornår skrivebeskyttelsen ophæves igen",
-       "readonlytext": "Databasen er i øjeblikket låst for nye poster og andre ændringer, formentlig pga. rutinemæssig databasevedligeholdelse, hvorefter den vil være tilbage til normalen.\n\nSystemadministratoren som låste den, gav denne forklaring: $1",
-       "missing-article": "Databasen burde indeholde siden \"$1\" $2, men det gør den ikke.\n\nDen sandsynlige årsag er at du har fulgt et forældet link til en forskel eller en gammel version af en side der er blevet slettet.\n\nHvis det ikke er tilfældet, har du muligvis fundet en software-fejl.\nGør venligst en [[Special:ListUsers/sysop|administrator]] opmærksom på det, og husk at fortælle hvilken URL du har fulgt.",
+       "transaction-duration-limit-exceeded": "For at undgå høj replikationsforsinkelse blev denne transaktion afbrudt, fordi tiden det tog at lagre ($1) oversteg grænsen på $2 {{PLURAL:$2|sekund|sekunder}}.\nHvis du er ved at ændre mange emner på en gang, så prøv at udføre flere mindre operationer i stedet.",
+       "laggedslavemode": "<strong>Advarsel:</strong> Siden indeholder muligvis ikke de nyeste ændringer.",
+       "readonly": "Databasen er låst",
+       "enterlockreason": "Skriv en begrundelse for låsningen, inklusive en vurdering af hvornår den vil blive ophævet igen",
+       "readonlytext": "Databasen er i øjeblikket låst for nye poster og andre ændringer, formentlig pga. rutinemæssig databasevedligeholdelse, hvorefter den vil være tilbage til normalen.\n\nDen systemadministrator, som låste den, gav denne forklaring: $1",
+       "missing-article": "Databasen burde indeholde siden \"$1\" $2, men det gør den ikke.\n\nDette skyldes normalt at du har fulgt et forældet link til en forskel eller til en gammel version af en side der er blevet slettet.\n\nHvis det ikke er tilfældet, har du muligvis fundet en programmeringsfejl i softwaren.\nGør venligst en [[Special:ListUsers/sysop|administrator]] opmærksom på det, og sørg for at fortælle vedkommende hvilken URL, du har fulgt.",
        "missingarticle-rev": "(versionsnummer: $1)",
-       "missingarticle-diff": "(Forskel: $1, $2)",
-       "readonly_lag": "Databasen er automatisk blevet låst mens slave database serverne synkronisere med master databasen",
-       "nonwrite-api-promise-error": "HTTP-headeren 'Promise-Non-Write-API-Action' blev sendt, men forespørgslen var til et API skrivemodul.",
+       "missingarticle-diff": "(forskel: $1, $2)",
+       "readonly_lag": "Databasen er automatisk blevet låst mens slavedatabaseserverne synkroniserer med masterdatabasen",
+       "nonwrite-api-promise-error": "HTTP-headeren 'Promise-Non-Write-API-Action' blev sendt, men forespørgslen var til et API-skrivemodul.",
        "internalerror": "Intern fejl",
        "internalerror_info": "Intern fejl: $1",
-       "internalerror-fatal-exception": "Alvorlig undtagelse af typen \"$1\"",
+       "internalerror-fatal-exception": "Fatal undtagelse af typen \"$1\"",
        "filecopyerror": "Kunne ikke kopiere filen \"$1\" til \"$2\".",
        "filerenameerror": "Kunne ikke omdøbe filen \"$1\" til \"$2\".",
        "filedeleteerror": "Kunne ikke slette filen \"$1\".",
        "directorycreateerror": "Kunne ikke oprette mappen \"$1\".",
-       "directoryreadonlyerror": "Folderen \"$1\" er skrivebeskyttet.",
-       "directorynotreadableerror": "Folderen \"$1\" er ikke læsbar.",
+       "directoryreadonlyerror": "Mappen \"$1\" er skrivebeskyttet.",
+       "directorynotreadableerror": "Mappen \"$1\" er ikke læsbar.",
        "filenotfound": "Kunne ikke finde filen \"$1\".",
        "unexpected": "Uventet værdi: \"$1\"=\"$2\".",
-       "formerror": "Fejl: Kunne ikke afsende formular",
+       "formerror": "Fejl: Kunne ikke afsende formular.",
        "badarticleerror": "Denne funktion kan ikke udføres på denne side.",
        "cannotdelete": "Kunne ikke slette siden eller filen \"$1\".\nDen kan være blevet slettet af en anden.",
        "cannotdelete-title": "Kan ikke slette siden \"$1\"",
-       "delete-hook-aborted": "Sletningen blev afbrudt af en programfunktion.\nDer var ikke nogen forklaring.",
+       "delete-hook-aborted": "Sletningen blev afbrudt af en programfunktion.\nDer blev ikke givet nogen forklaring.",
        "no-null-revision": "Kunne ikke oprette nye tom revision for side \"$1\"",
        "badtitle": "Ugyldig titel",
        "badtitletext": "Den ønskede sides titel var ikke tilladt, tom eller siden er forkert henvist fra {{SITENAME}} på et andet sprog.\nDen kan indeholde et eller flere tegn, som ikke må anvendes i titler.",
        "noname": "Du har ikke angivet et gyldigt brugernavn.",
        "loginsuccesstitle": "Logget ind",
        "loginsuccess": "'''Du er nu logget på {{SITENAME}} som \"$1\".'''",
-       "nosuchuser": "Der er ingen bruger med navnet \"$1\".\nDer skelnes mellem store og bogstaver i brugernavne.\nKontrollér stavemåden, eller [[Special:CreateAccount|opret en ny konto]].",
+       "nosuchuser": "Der er ingen bruger med navnet \"$1\".\nDer skelnes mellem store og små bogstaver i brugernavne.\nKontrollér stavningen, eller [[Special:CreateAccount|opret en ny konto]].",
        "nosuchusershort": "Der er ingen bruger ved navn \"$1\". Tjek din stavning.",
        "nouserspecified": "Angiv venligst et brugernavn.",
        "login-userblocked": "Denne bruger er blokeret. Det er ikke tilladt at logge på.",
        "noemail": "Der er ikke oplyst en e-mailadresse for bruger \"$1\".",
        "noemailcreate": "Du skal angive en gyldig e-mailadresse",
        "passwordsent": "En ny adgangskode er sendt til e-mailadressen, som er registreret for \"$1\".\nDu bør logge på og ændre din adgangskode straks efter du har modtaget e-mailen.",
-       "blocked-mailpassword": "Din IP-adresse er blokeret fra at redigere sider. For at forhindre misbrug, er det heller ikke muligt, at bestille en ny adgangskode.",
+       "blocked-mailpassword": "Din IP-adresse er blokeret fra at redigere sider. For at forhindre misbrug er det heller ikke muligt at bestille en ny adgangskode fra denne IP-adresse.",
        "eauthentsent": "En e-mailbekræftelse er sendt til den angivne e-mailadresse.\n\nFør flere e-mails bliver sendt til kontoen, skal du følge instruktionerne i e-mailen, for at bekræfte at kontoen rent faktisk er din.",
        "throttled-mailpassword": "Indenfor {{PLURAL:$1|den sidste time|de sidste $1 timer}} er der allerede sendt en ny adgangskode. For at forhindre misbrug af funktionen, kan der kun bestilles en ny adgangskode én gang for hver {{PLURAL:$1|time|$1 timer}}.",
        "mailerror": "Fejl ved afsendelse af e-mail: $1",
-       "acct_creation_throttle_hit": "Besøgende med samme IP-adresse som dig har oprettet {{PLURAL:$1|en konto|$1 kontoer}} det sidste døgn, og det er ikke tilladt at oprette flere.\nDerfor kan besøgende ikke oprette flere kontoer fra denne IP-adresse i øjeblikket.",
+       "acct_creation_throttle_hit": "Besøgende med samme IP-adresse som dig har oprettet {{PLURAL:$1|1 konto|$1 kontoer}} inden for den/det/de seneste $2, hvilket er det maksimalt tilladte inden for denne tidsperiode.\nDerfor kan besøgende, som bruger denne IP-adresse, ikke oprette flere kontoer fra denne IP-adresse for øjeblikket.",
        "emailauthenticated": "Din e-mailadresse blev bekræftet den $2 kl. $3.",
        "emailnotauthenticated": "Din e-mailadresse er endnu ikke bekræftet.\nIngen e-mail vil blive sendt for de følgende funktioner.",
        "noemailprefs": "Angiv en e-mailadresse, så følgende funktioner er til rådighed.",
        "createaccount-title": "Opret brugerkonto på {{SITENAME}}",
        "createaccount-text": "Nogen har oprettet en konto for din e-mailadresse på {{SITENAME}} ($4) med navnet \"$2\". Adgangskoden er \"$3\".\nDu opfordres til at logge på og ændre adgangskoden med det samme.\n\nDu kan ignorere denne besked hvis kontoen blev oprettet ved en fejl.",
        "login-throttled": "Du har forsøgt at logge på for mange gange.\nVent venligst $1, før du prøver igen.",
-       "login-abort-generic": "Det lykkedes dig ikke at logge på - afbrudt",
+       "login-abort-generic": "Det lykkedes ikke at logge dig på - Afbrudt",
        "login-migrated-generic": "Din konto er blevet globaliseret, og dit brugernavn eksisterer ikke længere på denne wiki.",
        "loginlanguagelabel": "Sprog: $1",
        "suspicious-userlogout": "Din anmodning om at logge af blev nægtet, fordi det ser ud som den blev sendt af en ødelagt browser eller caching proxy.",
        "newpassword": "Ny adgangskode:",
        "retypenew": "Gentag ny adgangskode:",
        "resetpass_submit": "Gem adgangskode og log på",
-       "changepassword-success": "Din adgangskode er nu ændret!",
+       "changepassword-success": "Din adgangskode er blevet ændret!",
        "changepassword-throttled": "Du har forsøgt at logge på for mange gange for nylig.\nVent venligst $1, før du prøver igen.",
        "botpasswords": "Bot adgangskoder",
        "botpasswords-summary": "<em>Bot adgangskoder</em> giver adgang til en brugerkonto via API'en, uden at bruge kontoens normale login-legitimationsoplysninger. Brugerrettighederne kan være begrænset, når du er logget på med et bot password,.\n\nHvis du ikke ved, hvorfor du måske ønsker at gøre dette, bør du nok ikke gøre det. Ingen bør nogensinde bede dig om at generere et af disse, og give det til dem.",
        "resetpass-no-info": "Du skal være logget på for at komme direkte til denne side.",
        "resetpass-submit-loggedin": "Skift adgangskode",
        "resetpass-submit-cancel": "Annuller",
-       "resetpass-wrong-oldpass": "Ugyldig midlertidig eller gældende adgangskode.\nDu har muligvis allerede skiftet din adgangskode eller anmodet om en ny midlertidig kode.",
+       "resetpass-wrong-oldpass": "Ugyldig midlertidig eller gældende adgangskode.\nDu har muligvis allerede ændret din adgangskode eller bedt om en ny midlertidig kode.",
        "resetpass-recycled": "Vær venlig at ændre din adgangskode til noget andet end din nuværende adgangskode.",
        "resetpass-temp-emailed": "Du loggede på med en midlertidig kode tilsendt på e-mail.\nFor at afslutte indlogning skal du angive en ny adgangskode her:",
        "resetpass-temp-password": "Midlertidig adgangskode",
        "selfredirect": "<strong>Advarsel:</strong> Du er ved at omdirigere denne side til sig selv.\nDu kan have angivet det forkerte mål for omdirigeringen, eller du kan være ved at redigere den forkerte side.\nHvis du klikker på \"{{int:savearticle}}\" igen, vil omdirigeringen blive oprettet uanset dette.",
        "missingcommenttext": "Skriv venligst en kommentar nedenfor.",
        "missingcommentheader": "<strong>Bemærk:</strong> Du har ikke angivet en overskrift/emne for denne kommentar. Hvis du trykker \"{{int:savearticle}}\" én gang til, gemmes dine ændringer uden overskrift/emne.",
-       "summary-preview": "Forhåndsvisning af beskrivelsen:",
+       "summary-preview": "Forhåndsvisning af sammenfatning:",
        "subject-preview": "Forhåndsvisning af emne/overskrift:",
        "previewerrortext": "Der opstod en fejl under forsøget på at lave en forhåndsvisning af dine ændringer.",
        "blockedtitle": "Du eller din IP-adresse er blokeret",
        "revdelete-unsuppress": "Ophæv begrænsninger for gendannede versioner",
        "revdelete-log": "Begrundelse:",
        "revdelete-submit": "Udfør på {{PLURAL:$1|den udvalgte version|de udvalgte versioner}}",
-       "revdelete-success": "'''Versionsvisningen er ændret.'''",
+       "revdelete-success": "Versionssynligheden blev opdateret.",
        "revdelete-failure": "'''Kunne ikke ændre versionssynligheden:'''\n$1",
-       "logdelete-success": "'''Synlighed ændret med success.'''",
+       "logdelete-success": "Log-synligheden blev ændret.",
        "logdelete-failure": "'''Kunne ikke ændre logsynligheden.'''\n$1",
        "revdel-restore": "ændre synlighed",
        "pagehist": "Sidehistorik",
        "modifiedarticleprotection": "ændrede beskyttelsen af \"[[$1]]\"",
        "unprotectedarticle": "fjernede beskyttelse af \"[[$1]]\"",
        "movedarticleprotection": "flyttede beskyttelsesindstillinger fra \"[[$2]]\" til \"[[$1]]\"",
+       "protectedarticle-comment": "{{GENDER:$2|beskyttede}} \"[[$1]]\"",
        "protect-title": "Ændre beskyttelse af \"$1\"",
        "protect-title-notallowed": "Få vist beskyttelsesniveauet af \"$1\"",
        "prot_1movedto2": "$1 flyttet til $2",
index c7131ae..76f0eea 100644 (file)
        "rcfilters-filterlist-feedbacklink": "Rückmeldung zu den neuen (Beta-)Filtern hinterlassen",
        "rcfilters-highlightbutton-title": "Ergebnisse hervorheben",
        "rcfilters-highlightmenu-title": "Eine Farbe auswählen",
+       "rcfilters-highlightmenu-help": "Eine Farbe auswählen, um diese Eigenschaft hervorzuheben.",
        "rcfilters-filterlist-noresults": "Keine Filter gefunden",
        "rcfilters-filtergroup-registration": "Benutzerregistrierung",
        "rcfilters-filter-registered-label": "Angemeldet",
index 7630b74..d208f4b 100644 (file)
        "action-patrol": "vurnayîşê karberanê binî nişan bike patrol biye",
        "action-autopatrol": "vurnayîşê xoye nişan bike ke belli biyo patrol biye",
        "action-unwatchedpages": "listeyê pelanê seyirnibiya bivîne",
-       "action-mergehistory": "tarixê ena pele yew ke",
+       "action-mergehistory": "tarixê ena perre yew ke",
        "action-userrights": "heqqa karberanê hemî bivurne",
        "action-userrights-interwiki": "heqqa karberanê ke wikiyê binî de hemî bivurne",
        "action-siteadmin": "database kilit bike ya zi a bike",
        "checkbox-select": "Weçinaye: $1",
        "checkbox-all": "Pêro",
        "checkbox-none": "Temam",
-       "checkbox-invert": "Dimlaşt ke (verdindayış)",
+       "checkbox-invert": "Verdindayış",
        "allpages": "Pêro peli",
        "nextpage": "Pela badê cû ($1)",
        "prevpage": "Pela verêne ($1)",
        "cant-move-user-page": "desturê şıma çino, şıma pelanê karberani bıkırışi (bê pelê cerıni).",
        "cant-move-to-user-page": "desturê şıma çino, şıma yew peli bıkırışi pelê yew karberi.",
        "newtitle": "Sernameyo newe:",
-       "move-watch": "Pela çıme u meqsedi seyr ke",
+       "move-watch": "Perra çımey u kırıştışi pıro bıdê",
        "movepagebtn": "Pele bere",
        "pagemovedsub": "Berdışi kerd temam",
        "movepage-moved": "'''\"$1\" berd \"$2\"'''",
        "tooltip-ca-delete": "Ena pele bestere",
        "tooltip-ca-undelete": "Perer fına zey verê esternayışi kerê",
        "tooltip-ca-move": "Ena pele bıkırışe",
-       "tooltip-ca-watch": "Ena pele lista xoya seyrkerdışi ke",
+       "tooltip-ca-watch": "Ena perre lista xoya seyrkerdışi ke",
        "tooltip-ca-unwatch": "Ena pele lista xoya seyrkerdışi ra vece",
        "tooltip-search": "{{SITENAME}} de cı geyre",
        "tooltip-search-go": "Ebe nê namey tami şo yew pela ke esta",
index 5909d4c..bf06e49 100644 (file)
@@ -28,7 +28,8 @@
                        "Usandaru555",
                        "Aefgh39622",
                        "Anomie",
-                       "Dr. Shikha Jaggi"
+                       "Dr. Shikha Jaggi",
+                       "Xð"
                ]
        },
        "tog-underline": "Link underlining:",
        "unprotectthispage": "Change protection of this page",
        "newpage": "New page",
        "talkpage": "Discuss this page",
-       "talkpagelinktext": "Talk",
+       "talkpagelinktext": "talk",
        "specialpage": "Special page",
        "personaltools": "Personal tools",
        "articlepage": "View content page",
index 098adb6..b7078dd 100644 (file)
        "rcfilters-filterlist-feedbacklink": "Provide feedback on the new (beta) filters",
        "rcfilters-highlightbutton-title": "Highlight results",
        "rcfilters-highlightmenu-title": "Select a color",
+       "rcfilters-highlightmenu-help": "Select a color to highlight this property",
        "rcfilters-filterlist-noresults": "No filters found",
        "rcfilters-filtergroup-registration": "User registration",
        "rcfilters-filter-registered-label": "Registered",
index 20ac8ae..5f26edc 100644 (file)
        "databaseerror-query": "Consulta: $1",
        "databaseerror-function": "Función: $1",
        "databaseerror-error": "Error: $1",
-       "transaction-duration-limit-exceeded": "Para evitar la creación de lentitud alta de respuesta, la transacción fue abortada porque la duración de escritura ($1) excedió el límite de $2 {{PLURAL:$2|segundo|segundos}}.\nSi estás cambiando muchos elementos a la vez, trata de hacer operaciones similares más pequeñas.",
+       "transaction-duration-limit-exceeded": "Con el fin de evitar un aumento excesivo del retardo de replicación, se anuló esta transacción porque la duración de escritura ($1) excedió el límite de $2 {{PLURAL:$2|segundo|segundos}}.\nSi estás cambiando muchos elementos a la vez, trata de hacer operaciones similares más pequeñas.",
        "laggedslavemode": "<strong>Advertencia:</strong> puede que falten las actualizaciones más recientes en esta página.",
        "readonly": "Base de datos bloqueada",
        "enterlockreason": "Explica el motivo del bloqueo, incluyendo una estimación de cuándo se producirá el desbloqueo",
        "rcfilters-filterlist-title": "Filtros",
        "rcfilters-highlightbutton-title": "Resaltar los resultados",
        "rcfilters-highlightmenu-title": "Selecciona un color",
+       "rcfilters-highlightmenu-help": "Selecciona un color para resaltar esta propiedad",
        "rcfilters-filterlist-noresults": "No se encontraron filtros",
        "rcfilters-filtergroup-registration": "Registro de usuario",
        "rcfilters-filter-registered-label": "Registrados",
        "reblock-logentry": "cambió el bloqueo para  [[$1]] con una caducidad de $2 $3",
        "blocklogtext": "Esto es un registro de acciones de bloqueo y desbloqueo de usuarios.\nLas direcciones IP bloqueadas automáticamente no aparecen aquí.\nConsulta la [[Special:BlockList|lista de bloqueos]] para ver la lista de bloqueos y prohibiciones de operar en vigor.",
        "unblocklogentry": "desbloqueó a $1",
-       "block-log-flags-anononly": "sólo anónimos",
+       "block-log-flags-anononly": "solo usuarios anónimos",
        "block-log-flags-nocreate": "desactivada la creación de cuentas",
        "block-log-flags-noautoblock": "bloqueo automático desactivado",
        "block-log-flags-noemail": "correo electrónico desactivado",
index eb89f59..61c6c6b 100644 (file)
        "editcomment": "Redaktsiooni resümee oli: <em>$1</em>.",
        "revertpage": "Tühistati kasutaja [[Special:Contributions/$2|$2]] ([[User talk:$2|arutelu]]) tehtud muudatused ja pöörduti tagasi viimasele muudatusele, mille tegi [[User:$1|$1]].",
        "revertpage-nouser": "Tühistati peidetud kasutaja muudatused ja pöörduti tagasi viimasele muudatusele, mille tegi [[User:$1|$1]].",
-       "rollback-success": "Tühistati muudatused, mille tegi $1;\npöörduti tagasi viimasele muudatusele, mille tegi $2.",
+       "rollback-success": "Tühistati muudatused, mille tegi {{GENDER:$3|$1}};\npöörduti tagasi viimasele muudatusele, mille tegi {{GENDER:$4|$2}}.",
        "rollback-success-notify": "Tühistatud kasutaja $1 tehtud muudatused;\npöördutud tagasi kasutaja $2 viimase redaktsiooni juurde. [$3 Näita muudatusi]",
        "sessionfailure-title": "Seansiviga",
        "sessionfailure": "Sinu sisselogimisseansiga näib probleem olevat.\nSee toiming on seansiärandamise vastase ettevaatusabinõuna tühistatud.\nMine tagasi eelmisele leheküljele ja taaslaadi see, seejärel proovi uuesti.",
        "htmlform-date-placeholder": "AAAA-KK-PP",
        "htmlform-time-placeholder": "TT:MM:SS",
        "htmlform-datetime-placeholder": "AAAA-KK-PP TT:MM:SS",
+       "htmlform-date-invalid": "Väärtus, mille ette andsid, pole äratuntav kuupäev. Proovi kasutada vormingut AAAA-KK-PP.",
+       "htmlform-time-invalid": "Väärtus, mille ette andsid, pole äratuntav kellaaeg. Proovi kasutada vormingut TT:MM:SS.",
+       "htmlform-datetime-invalid": "Väärtus, mille ette andsid, pole äratuntav kuupäev ja kellaaeg. Proovi kasutada vormingut AAAA-KK-PP TT:MM:SS.",
+       "htmlform-date-toolow": "Väärtus, mille ette andsid, on enne varaseimat lubatud kuupäeva $1.",
+       "htmlform-date-toohigh": "Väärtus, mille ette andsid, on pärast hiliseimat lubatud kuupäeva $1.",
+       "htmlform-time-toolow": "Väärtus, mille ette andsid, on enne varaseimat lubatud kellaaega $1.",
+       "htmlform-time-toohigh": "Väärtus, mille ette andsid, on pärast hiliseimat lubatud kellaaega $1.",
+       "htmlform-datetime-toolow": "Väärtus, mille ette andsid, on enne varaseimat lubatud kuupäeva ja kellaaega $1.",
+       "htmlform-datetime-toohigh": "Väärtus, mille ette andsid, on pärast hiliseimat lubatud kuupäeva ja kellaaega $1.",
        "htmlform-title-badnamespace": "[[:$1]] pole nimeruumis \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "Pealkirja \"$1\" all ei saa lehekülge alustada.",
        "htmlform-title-not-exists": "Lehekülge $1 pole olemas.",
        "logentry-tag-update-logentry": "$1 {{GENDER:$2|uuendas}} leheküljel \"$3\" logisissekande $5 märgiseid ({{PLURAL:$7|lisatud}} $6; {{PLURAL:$9|eemaldatud}} $8)",
        "rightsnone": "(puudub)",
        "revdelete-summary": "resümee",
+       "rightslogentry-temporary-group": "$1 (ajutine, tähtaeg $2)",
        "feedback-adding": "Tagasiside lisamine leheküljele...",
        "feedback-back": "Tagasi",
        "feedback-bugcheck": "Hästi! Kontrolli vaid, ega tegu pole juba [$1 teada oleva veaga].",
index 1e72167..aa1e090 100644 (file)
        "recentchanges-legend-plusminus": "(''±123'')",
        "recentchanges-submit": "Näytä",
        "rcfilters-activefilters": "Aktiiviset suodattimet",
+       "rcfilters-restore-default-filters": "Palauta oletussuodattimet",
        "rcfilters-clear-all-filters": "Tyhjennä kaikki suodattimet",
        "rcfilters-search-placeholder": "Suodattimen viimeaikaiset muutokset (selaa tai aloita kirjoittaa)",
        "rcfilters-invalid-filter": "Suodatin on epäkelpo",
        "rcfilters-empty-filter": "Ei aktiivisia suodattimia. Kaikki muutokset näytetään.",
        "rcfilters-filterlist-title": "Suodattimet",
+       "rcfilters-highlightmenu-title": "Valitse väri",
        "rcfilters-filterlist-noresults": "Ei löytynyt suodattimia",
+       "rcfilters-filtergroup-registration": "Käyttäjän rekisteröinti",
        "rcfilters-filter-registered-label": "Rekisteröitynyt",
        "rcfilters-filter-registered-description": "Kirjautuneet muokkaukset.",
+       "rcfilters-filter-unregistered-label": "Rekisteröimätön",
        "rcfilters-filter-unregistered-description": "Muokkaajat, jotka eivät ole kirjautuneet sisään.",
        "rcfilters-filter-editsbyself-label": "Omat muokkauksesi",
        "rcfilters-filter-editsbyself-description": "Muokkauksesi",
        "apisandbox-sending-request": "API-pyyntöä lähetetään...",
        "apisandbox-loading-results": "API-tuloksia vastaanotetaan...",
        "apisandbox-results-error": "Tapahtui virhe ladattaessa API-kyselyn vastausta: $1",
+       "apisandbox-request-format-url-label": "URL-kyselymerkkijono",
        "apisandbox-request-url-label": "Pyynnön URL",
        "apisandbox-request-time": "Pyyntöön kulunut aika: {{PLURAL:$1|$1 ms}}",
        "apisandbox-results-fixtoken": "Korjaa \"token\" ja lähetä uudelleen",
        "authmanager-realname-label": "Oikea nimi",
        "authmanager-realname-help": "Käyttäjän oikea nimi",
        "authmanager-provider-password": "Salasanapohjainen varmennus",
+       "authmanager-provider-password-domain": "Salasana- ja toimialue-pohjainen todennus",
        "authmanager-provider-temporarypassword": "Väliaikainen salasana",
        "authprovider-confirmlink-message": "Viimeisimpien kirjautumisyritystesi perusteella seuraavat tunnukset voidaan linkittää wikitunnuksellesi. Tunnusten linkittäminen mahdollistaa sisäänkirjautumisen niitä käyttämällä. Valitse tunnukset, jotka tulisi linkittää.",
        "authprovider-confirmlink-request-label": "Tunnukset, jotka tulisi yhdistää",
index 4b0fa74..29797b8 100644 (file)
        "rcfilters-filterlist-feedbacklink": "Fournir un commentaire sur les nouveaux filtres (en bêta)",
        "rcfilters-highlightbutton-title": "Mettre en valeur les résultats",
        "rcfilters-highlightmenu-title": "Choisir une couleur",
+       "rcfilters-highlightmenu-help": "Sélectionner une couleur pour mettre en évidence cette propriété",
        "rcfilters-filterlist-noresults": "Aucun filtre trouvé",
        "rcfilters-filtergroup-registration": "Inscription de l’utilisateur",
        "rcfilters-filter-registered-label": "Connectés",
index 7047334..ee93166 100644 (file)
        "rcfilters-invalid-filter": "Filtro no válido",
        "rcfilters-empty-filter": "Non hai filtros activos. Móstranse tódalas contribucións.",
        "rcfilters-filterlist-title": "Filtros",
+       "rcfilters-filterlist-feedbacklink": "Deixar comentarios sobre os novos filtros (en fase beta)",
        "rcfilters-highlightbutton-title": "Resaltar resultados",
        "rcfilters-highlightmenu-title": "Seleccione unha cor",
        "rcfilters-filterlist-noresults": "Non se atoparon filtros",
        "editcomment": "O resumo de edición foi: <em>$1</em>.",
        "revertpage": "Desfixéronse as edicións de [[Special:Contributions/$2|$2]] ([[User talk:$2|conversa]]); cambiado á última versión feita por [[User:$1|$1]]",
        "revertpage-nouser": "Desfixéronse as edicións dun usuario agochado; cambiado á última versión feita por {{GENDER:$1|[[User:$1|$1]]}}",
-       "rollback-success": "Desfixéronse as edicións de $1;\nvolveuse á última edición, feita por $2.",
+       "rollback-success": "Desfixéronse as edicións de {{GENDER:$3|$1}};\nvolveuse á última edición, feita por {{GENDER:$4|$2}}.",
        "rollback-success-notify": "Revertéronse as edicións de $1;\nrestaurouse a última revisión de $2. [$3 Mostrar os cambios]",
        "sessionfailure-title": "Erro de sesión",
        "sessionfailure": "Parece que hai un problema co rexistro da súa sesión;\nesta acción cancelouse como precaución fronte ao secuestro de sesións.\nPrema no botón \"atrás\", volva cargar a páxina da que proviña e inténteo de novo.",
index edb1b85..93c5007 100644 (file)
        "specialpages-group-changes": "D letschte Änderige un Logbüecher",
        "specialpages-group-media": "Medie",
        "specialpages-group-users": "Benutzer un Rächt",
-       "specialpages-group-highuse": "Syte wo oft bruucht werde",
+       "specialpages-group-highuse": "Syte wo hüüfig bruucht werde",
        "specialpages-group-pages": "Lischte vo Syte",
        "specialpages-group-pagetools": "Sytewerchzüüg",
        "specialpages-group-wiki": "Date un Wärchzyyg",
index 67e2b68..6a63089 100644 (file)
        "rcfilters-filterlist-feedbacklink": "שליחת משוב על המסננים החדשים (בטא)",
        "rcfilters-highlightbutton-title": "הבלטת התוצאות",
        "rcfilters-highlightmenu-title": "בחירת צבע",
+       "rcfilters-highlightmenu-help": "בחירת צבע להדגשת מאפיין זה",
        "rcfilters-filterlist-noresults": "לא נמצאו מסננים",
        "rcfilters-filtergroup-registration": "רישום העורכים",
        "rcfilters-filter-registered-label": "רשומים",
        "log-action-filter-upload-upload": "העלאות של קבצים חדשים",
        "log-action-filter-upload-overwrite": "דריסות של קבצים קיימים",
        "authmanager-authn-not-in-progress": "האימות נכשל או שנתוני הפעולה נאבדו. נא להתחיל את התהליך מחדש.",
-       "authmanager-authn-no-primary": "×\9c×\90 × ×\99ת×\9f ×\94×\99×\94 ×\9c×\90×\9eת ×\90ת ×\94×\90×\99ש×\95ר×\99×\9d שסופקו.",
-       "authmanager-authn-no-local-user": "×\94×\90×\99ש×\95ר×\99×\9d שסופקו אינם שייכים לשום משתמש באתר זה.",
+       "authmanager-authn-no-primary": "×\9c×\90 × ×\99ת×\9f ×\94×\99×\94 ×\9c×\90×\9eת ×\90ת × ×ª×\95× ×\99 ×\94×\94×\96×\93×\94×\95ת שסופקו.",
+       "authmanager-authn-no-local-user": "נת×\95× ×\99 ×\94×\94×\96×\93×\94×\95ת שסופקו אינם שייכים לשום משתמש באתר זה.",
        "authmanager-authn-no-local-user-link": "נתוני ההאמנה שניתנו תקינים, אבל אינם משויכים לשום משתמש בוויקי הזה. נא להיכנס לחשבון באופן שונה, או ליצור משתמש חדש ואז תהיה לך אפשרות לקשר את נתוני ההאמנה הקודמים שלך לחשבון ההוא.",
        "authmanager-authn-autocreate-failed": "יצירה אוטומטית של חשבון מקומי נכשלה: $1",
        "authmanager-change-not-supported": "לא ניתן לשנות את נתוני ההאמנה שניתנו, כי שום דבר לא ישתמש בהם.",
        "authmanager-create-disabled": "אפשרות יצירת החשבונות מבוטלת.",
        "authmanager-create-from-login": "כדי ליצור את החשבון, יש למלא את השדות.",
        "authmanager-create-not-in-progress": "יצירת החשבון נכשלה או שנתוני הפעולה נאבדו. נא להתחיל את התהליך מחדש.",
-       "authmanager-create-no-primary": "×\94×\90×\99ש×\95ר×\99×\9d שסופקו לא יכולים להיות בשימוש ביצירת חשבון.",
-       "authmanager-link-no-primary": "×\94×\90×\99ש×\95ר×\99×\9d ×©×¡×\95פק×\95 ×\9c×\90 ×\99×\9b×\95×\9c×\99×\9d ×\9c×\94×\99×\95ת ×\91ש×\99×\9e×\95ש ×\91קישור חשבונות.",
+       "authmanager-create-no-primary": "נת×\95× ×\99 ×\94×\94×\96×\93×\94×\95ת שסופקו לא יכולים להיות בשימוש ביצירת חשבון.",
+       "authmanager-link-no-primary": "נת×\95× ×\99 ×\94×\94×\96×\93×\94×\95ת ×©×¡×\95פק×\95 ×\9c×\90 ×\99×\9b×\95×\9c×\99×\9d ×\9c×\94×\99×\95ת ×\91ש×\99×\9e×\95ש ×\9cש×\9d קישור חשבונות.",
        "authmanager-link-not-in-progress": "קישור החשבונות נכשל או שנתוני הפעולה נאבדו. נא להתחיל את התהליך מחדש.",
        "authmanager-authplugin-setpass-failed-title": "שינוי הסיסמה נכשל",
        "authmanager-authplugin-setpass-failed-message": "תוסף האימות דחה את שינוי הסיסמה.",
        "authpage-cannot-link-continue": "לא ניתן להמשיך בתהליך קישור החשבונות. כנראה שזמן ההמתנה של הפעולה חלף.",
        "cannotauth-not-allowed-title": "הגישה נדחתה",
        "cannotauth-not-allowed": "אינך מורשה להשתמש בדף זה",
-       "changecredentials": "ש×\99× ×\95×\99 ×\94×\90×\99ש×\95ר×\99×\9d",
-       "changecredentials-submit": "ש×\99× ×\95×\99 ×\94×\90×\99ש×\95ר×\99×\9d",
+       "changecredentials": "ש×\99× ×\95×\99 × ×ª×\95× ×\99 ×\94×\94×\96×\93×\94×\95ת",
+       "changecredentials-submit": "ש×\99× ×\95×\99 × ×ª×\95× ×\99 ×\94×\94×\96×\93×\94×\95ת",
        "changecredentials-invalidsubpage": "$1 אינו סוג אישור תקין.",
-       "changecredentials-success": "×\94×\90×\99ש×\95ר×\99×\9d שלך שונו.",
-       "removecredentials": "×\94סרת ×\94×\90×\99ש×\95ר×\99×\9d",
-       "removecredentials-submit": "×\94סרת ×\94×\90×\99ש×\95ר×\99×\9d",
+       "changecredentials-success": "נת×\95× ×\99 ×\94×\94×\96×\93×\94×\95ת שלך שונו.",
+       "removecredentials": "×\94סרת × ×ª×\95× ×\99 ×\94×\94×\96×\93×\94×\95ת",
+       "removecredentials-submit": "×\94סרת × ×ª×\95× ×\99 ×\94×\94×\96×\93×\94×\95ת",
        "removecredentials-invalidsubpage": "$1 אינו סוג אישור תקין.",
-       "removecredentials-success": "×\94×\90×\99ש×\95ר×\99×\9d שלך הוסרו.",
-       "credentialsform-provider": "ס×\95×\92 ×\94×\90×\99ש×\95ר×\99×\9d:",
+       "removecredentials-success": "נת×\95× ×\99 ×\94×\94×\96×\93×\94×\95ת שלך הוסרו.",
+       "credentialsform-provider": "ס×\95×\92 × ×ª×\95× ×\99 ×\94×\94×\96×\93×\94×\95ת:",
        "credentialsform-account": "שם החשבון:",
        "cannotlink-no-provider-title": "אין חשבונות שניתן לקשר",
        "cannotlink-no-provider": "אין חשבונות שניתן לקשר.",
index 2e7ec25..bd4f303 100644 (file)
        "lastmodifiedatby": "Ovu je stranicu posljednji put {{GENDER:$4|mijenjao suradnik|mijenjala suradnica}} $3 dana $1 u $2.",
        "othercontribs": "Temelji se na doprinosu suradnika $1.",
        "others": "drugih",
-       "siteusers": "{{SITENAME}} {{PLURAL:$2|suradnik|suradnici}} $1",
+       "siteusers": "{{SITENAME}} {{PLURAL:$2|{{GENDER:$1|suradnik|suradnica}}|suradnici}} $1",
        "anonusers": "{{SITENAME}} {{PLURAL:$2|anonimni suradnik|anonimni suradnici}} $1",
        "creditspage": "Autori stranice",
        "nocredits": "Za ovu stranicu nema podataka o autorima.",
        "logentry-move-move-noredirect": "$1 je {{GENDER:$2|premjestio|premjestila}} stranicu $3 na $4 bez preusmjeravanja",
        "logentry-move-move_redir": "$1 je {{GENDER:$2|premjestio|premjestila}} stranicu $3 na $4 preko preusmjeravanja",
        "logentry-move-move_redir-noredirect": "$1 je {{GENDER:$2|premjestio|premjestila}} stranicu $3 na $4 preko preusmjeravanja bez ostavljanja preusmjeravanja",
-       "logentry-patrol-patrol": "$1 je {{GENDER:$2|označio|označila}} uređivanje $4 stranice $3 pregledanim",
+       "logentry-patrol-patrol": "$1 {{GENDER:$2|označio|označila}} je uređivanje $4 stranice $3 ophođenim",
        "logentry-patrol-patrol-auto": "$1 je automatski {{GENDER:$2|označio|označila}} uređivanje $4 stranice $3 pregledanim",
        "logentry-newusers-newusers": "$1 je {{GENDER:$2|otvorio|otvorila}} suradnički račun",
        "logentry-newusers-create": "$1 je {{GENDER:$2|stvorio|stvorila}} suradnički račun.",
index 412a133..c8c6fa0 100644 (file)
        "selfredirect": "<strong>Peringatan:</strong> Anda mengalihkan halaman ini kembali ke halaman semula.\nAnda bisa jadi telah memberikan tujuan pengalihan yang salah, atau telah menyunting halaman yang salah.\nJika Anda mengeklik \"{{int:savearticle}}\" sekali lagi, halaman pengalihan akan dibuat.",
        "missingcommenttext": "Harap masukkan komentar di bawah ini.",
        "missingcommentheader": "'''Peringatan:''' Anda belum memberikan subjek atau judul untuk komentar Anda. Jika Anda kembali menekan \"{{int:savearticle}}\", suntingan Anda akan disimpan tanpa komentar tersebut.",
-       "summary-preview": "Pratayang ringkasan:",
+       "summary-preview": "Pratayang ringkasan suntingan:",
        "subject-preview": "Pratayang subjek:",
        "previewerrortext": "Kesalahan terjadi saat mencoba memperlihatkan pratayang perubahan Anda.",
        "blockedtitle": "Pengguna diblokir",
        "search-interwiki-caption": "Proyek lain",
        "search-interwiki-default": "Hasil dari $1:",
        "search-interwiki-more": "(selanjutnya)",
+       "search-interwiki-more-results": "Hasil lainnya",
        "search-relatedarticle": "Berkaitan",
        "searchrelated": "berkaitan",
        "searchall": "semua",
        "editusergroup": "Muat kelompok pengguna",
        "editinguser": "Mengubah hak pengguna untuk {{GENDER:$1|pengguna}} <strong>[[User:$1|$1]]</strong> $2",
        "viewinguserrights": "Melihat hak pengguna dari {{GENDER:$1|pengguna}} <strong>[[User:$1|$1]]</strong> $2",
-       "userrights-editusergroup": "Sunting kelompok pengguna",
-       "userrights-viewusergroup": "Lihat kelompok pengguna",
+       "userrights-editusergroup": "Sunting kelompok {{GENDER:$1|pengguna}}",
+       "userrights-viewusergroup": "Lihat kelompok {{GENDER:$1|pengguna}}",
        "saveusergroups": "Simpan kelompok {{GENDER:$1|pengguna}}",
        "userrights-groupsmember": "Anggota dari:",
        "userrights-groupsmember-auto": "Anggota implisit dari:",
        "userrights-changeable-col": "Kelompok yang dapat Anda ubah",
        "userrights-unchangeable-col": "Kelompok yang tidak dapat Anda ubah",
        "userrights-irreversible-marker": "$1*",
+       "userrights-expiry-current": "Udang $1",
+       "userrights-expiry-none": "Tidak usang",
+       "userrights-expiry": "Usang:",
        "userrights-expiry-othertime": "Waktu lain:",
        "userrights-conflict": "Konflik perubahan hak pengguna! Silakan tinjau ulang dan konfirmasi perubahan Anda.",
        "group": "Kelompok:",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (lihat pula [[Special:NewPages|daftar halaman baru]])",
        "recentchanges-submit": "Tampilkan",
        "rcfilters-activefilters": "Filter aktif",
+       "rcfilters-restore-default-filters": "Kembalikan filter bawaan",
+       "rcfilters-clear-all-filters": "Hapus semua penyaringan",
+       "rcfilters-search-placeholder": "Filter perubahan terbaru (jelajahi atau masukan input)",
+       "rcfilters-invalid-filter": "Penyqringan tidak sah",
+       "rcfilters-empty-filter": "Tidak ada filter aktif. Semua kontribusi ditampilkan.",
+       "rcfilters-filterlist-title": "Penyaringan",
+       "rcfilters-filterlist-feedbacklink": "Berikan umpan balik untuk filter uji coba baru",
+       "rcfilters-highlightmenu-title": "Pilih warna",
+       "rcfilters-filterlist-noresults": "Tidak ada penyaring ditemukan",
+       "rcfilters-filtergroup-registration": "Pendaftaran pengguna",
        "rcfilters-filter-registered-label": "Terdaftar",
+       "rcfilters-filter-registered-description": "Penyunting masuk log",
        "rcfilters-filter-unregistered-label": "Tidak terdaftar",
+       "rcfilters-filter-unregistered-description": "Penyunting yang tidak masuk log",
+       "rcfilters-filter-editsbyself-label": "Suntingan Anda",
+       "rcfilters-filter-editsbyself-description": "Suntingan oleh Anda",
+       "rcfilters-filter-editsbyother-label": "Suntingan orang lain",
+       "rcfilters-filter-editsbyother-description": "Suntingan dibuat oleh orang lain (bukan Anda)",
        "rcfilters-filter-userExpLevel-newcomer-label": "Pendatang baru",
+       "rcfilters-filter-userExpLevel-newcomer-description": "Kurang dari 10 suntingan dan aktivitas selama 4 hari.",
+       "rcfilters-filter-userExpLevel-learner-label": "Pelajar",
+       "rcfilters-filter-userExpLevel-experienced-label": "Pengguna berpengalaman",
+       "rcfilters-filtergroup-automated": "Kontribusi otomatis",
        "rcfilters-filter-bots-label": "Bot",
+       "rcfilters-filter-bots-description": "Suntingan yang dibuat dengan perkakas terotomatisasi.",
+       "rcfilters-filter-humans-label": "Manusia (bukan bot)",
+       "rcfilters-filter-humans-description": "Suntingan yang dibuat oleh penyunting manusia.",
+       "rcfilters-filtergroup-significance": "Kepentingan",
        "rcfilters-filter-minor-label": "Suntingan kecil",
+       "rcfilters-filter-minor-description": "Suntingan yang ditandai penyunting sebagai suntingan kecil",
+       "rcfilters-filter-major-label": "Suntingan yang bukan suntingan kecil",
+       "rcfilters-filter-major-description": "Suntingan yang ditandai sebagai suntingan kecil",
+       "rcfilters-filtergroup-changetype": "Jenis perubahan",
+       "rcfilters-filter-pageedits-label": "Suntingan halaman",
+       "rcfilters-filter-pageedits-description": "Suntingan pada konten wiki, diskusi, deskripsi kategori....",
+       "rcfilters-filter-newpages-label": "Pembuatan halaman",
+       "rcfilters-filter-newpages-description": "Suntingan yang membuat halaman baru",
        "rcfilters-filter-categorization-label": "Perubahan kategori",
+       "rcfilters-filter-categorization-description": "Rekam jejak halaman yang telah ditambahkan atau dihapus dari kategori.",
+       "rcfilters-filter-logactions-label": "Tindakan tercatat",
+       "rcfilters-filter-logactions-description": "Tindakan administratif, pembuatan akun, penghapusan halaman, pengunggahan....",
        "rcnotefrom": "Di bawah ini adalah {{PLURAL:$5|perubahan}} sejak <strong>$3, $4</strong> (ditampilkan sampai <strong>$1</strong> perubahan).",
        "rclistfrom": "Perlihatkan perubahan terbaru sejak $3 $2",
        "rcshowhideminor": "$1 suntingan kecil",
        "apisandbox-sending-request": "Mengirim permintaan API...",
        "apisandbox-loading-results": "Menerima hasil API...",
        "apisandbox-results-error": "Sebuah galat terjadi ketika memuat permintaan respon API: $1.",
+       "apisandbox-request-selectformat-label": "Tampilkan permintaan data sebagai:",
        "apisandbox-request-url-label": "URL Permintaan:",
+       "apisandbox-request-json-label": "Meminta JSON:",
        "apisandbox-request-time": "Lama permintaan: {{PLURAL:$1|$1 ms}}",
        "apisandbox-results-fixtoken": "Perbaiki token dan kirim kembali",
        "apisandbox-results-fixtoken-fail": "Gagal mendapatkan token \"$1\".",
        "apisandbox-alert-field": "Nilai dalam kolom ini tidak valid.",
        "apisandbox-continue": "Lanjutkan",
        "apisandbox-continue-clear": "Kosongkan",
+       "apisandbox-param-limit": "Masukan <kbd>max</kbd> untuk menggunakan batas maksimum.",
+       "apisandbox-multivalue-all-namespaces": "$1 (Semua ruang nama)",
+       "apisandbox-multivalue-all-values": "$1 (Semua nilai)",
        "booksources": "Sumber buku",
        "booksources-search-legend": "Cari di sumber buku",
        "booksources-isbn": "ISBN:",
        "booksources-search": "Cari",
        "booksources-text": "Di bawah ini adalah daftar pranala ke situs lain yang menjual buku baru dan bekas, dan mungkin juga mempunyai informasi lebih lanjut mengenai buku yang sedang Anda cari:",
        "booksources-invalid-isbn": "ISBN yang diberikan tampaknya tidak valid; periksa kesalahan penyalinan dari sumber asli.",
+       "magiclink-tracking-rfc": "Halaman menggunakan pranala magis RFC",
+       "magiclink-tracking-pmid": "Halaman menggunakan pranala magis PMID",
+       "magiclink-tracking-isbn": "Halaman yang menggunakan pranala magis ISBN",
        "specialloguserlabel": "Pengguna:",
        "speciallogtitlelabel": "Target (judul atau{{ns:pengguna}}:nama pengguna untuk pengguna)",
        "log": "Catatan (Log)",
        "activeusers-intro": "Berikut adalah daftar pengguna yang memiliki suatu bentuk aktivitas selama paling tidak $1 {{PLURAL:$1|hari|hari}} terakhir.",
        "activeusers-count": "$1 {{PLURAL:$1|aktivitas|aktivitas}} dalam {{PLURAL:$3|1 hari|$3 hari}} terakhir",
        "activeusers-from": "Tampilkan pengguna mulai dari:",
+       "activeusers-groups": "Tampilkan pengguna yang termasuk kelompok:",
+       "activeusers-excludegroups": "Sembunyikan pengguna yang termasuk kelompok:",
        "activeusers-noresult": "Pengguna tidak ditemukan.",
        "activeusers-submit": "Tampilkan pengguna aktif",
        "listgrouprights": "Daftar hak kelompok",
        "rollbacklinkcount-morethan": "kembalikan lebih dari $1 {{PLURAL:$1|suntingan|suntingan}}",
        "rollbackfailed": "Pengembalian gagal dilakukan",
        "rollback-missingparam": "Parameter dibutuhkan ketika diminta tidak tersedia.",
+       "rollback-missingrevision": "Tidak mampu memuat data revisi.",
        "cantrollback": "Tidak dapat membatalkan suntingan;\nkontributor terakhir adalah satu-satunya penulis halaman ini.",
        "alreadyrolled": "Tidak dapat melakukan pengembalian ke revisi terakhir [[:$1]] oleh [[User:$2|$2]] ([[User talk:$2|bicara]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]);\npengguna lain telah menyunting atau melakukan pengembalian terhadap halaman ini.\n\nSuntingan terakhir dilakukan oleh [[User:$3|$3]] ([[User talk:$3|bicara]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "editcomment": "Komentar penyuntingan adalah: <em>$1</em>.",
        "revertpage": "←Suntingan [[Special:Contributions/$2|$2]] ([[User talk:$2|bicara]]) dibatalkan ke versi terakhir oleh [[User:$1|$1]]",
        "revertpage-nouser": "Mengembalikan suntingan oleh (nama pengguna dihapus) ke suntingan terakhir oleh [[User:$1|$1]]",
-       "rollback-success": "Pembatalan suntingan oleh $1; dibatalkan ke versi terakhir oleh $2.",
+       "rollback-success": "Pembatalan suntingan oleh {{GENDER:$3|$1}}; dibatalkan ke versi terakhir oleh {{GENDER:$4|$2}}.",
        "rollback-success-notify": "Mengembalikan suntingan oleh $1; rubah kembali untuk revisi terakhir oleh $2. [$3 Lihat perubahan]",
        "sessionfailure-title": "Kegagalan sesi",
        "sessionfailure": "Sepertinya ada masalah dengan sesi log Anda; log Anda telah dibatalkan untuk mencegah pembajakan. Silakan tekan tombol \"kembali\" dan muat kembali halaman sebelum Anda masuk, lalu coba lagi.",
        "sorbs": "DNSBL",
        "sorbsreason": "Alamat IP anda terdaftar sebagai proxy terbuka di DNSBL.",
        "sorbs_create_account_reason": "Alamat IP anda terdaftar sebagai proxy terbuka di DNSBL. Anda tidak dapat membuat akun.",
+       "softblockrangesreason": "Kontribusi anonim tidak diizinkan dari alamat IP Anda ($1). Silakan masuk log.",
        "xffblockreason": "Sebuah alamat IP terdapat di kepala X-Forwarded-For, entah milik Anda atau peladen ''proxy'' yang Anda gunakan, telah diblokir. Alasan pemblokirannya adalah: $1",
        "cant-see-hidden-user": "Pengguna yang Anda coba blokir telah diblokir dan disembunyikan. Selama Anda tidak memiliki hak sembunyikan pengguna, Anda tidak dapat melihat atau menyunting pemblokiran pengguna ini.",
        "ipbblocked": "Anda tidak dapat memblokir atau membuka blokir pengguna lain, karena Anda sendiri diblokir.",
        "cant-move-to-user-page": "Anda tidak memiliki hak akses untuk memindahkan halaman ke suatu halaman pengguna (kecuali ke subhalaman pengguna).",
        "cant-move-category-page": "Anda tidak memiliki izin untuk memindahkan halaman kategori.",
        "cant-move-to-category-page": "Anda tidak memiliki izin untuk memindahkan halaman ke halaman kategori.",
+       "cant-move-subpages": "Anda tidak memiliki izin untuk memindahkan subhalaman",
+       "namespace-nosubpages": "Ruang nama \"$1\" tidak mengizinkan subhalaman.",
        "newtitle": "Judul baru:",
        "move-watch": "Pantau halaman ini",
        "movepagebtn": "Pindahkan halaman",
        "newimages-showbots": "Tampilkan unggahan oleh bot",
        "newimages-hidepatrolled": "Sembunyikan unggahan yang telah dipatroli",
        "noimages": "Tidak ada yang dilihat.",
+       "gallery-slideshow-toggle": "Beralih  ''thumbnails''",
        "ilsubmit": "Cari",
        "bydate": "berdasarkan tanggal",
        "sp-newimages-showfrom": "Tampilkan berkas baru dimulai dari $2, $1",
        "tag-filter": "Filter [[Special:Tags|tag]]:",
        "tag-filter-submit": "Penyaring",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Tag}}]]: $2)",
+       "tag-mw-contentmodelchange": "Perubahan model konten",
+       "tag-mw-contentmodelchange-description": "Perubahan yang [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel mengubah model konten] suatu halaman",
        "tags-title": "Tanda",
        "tags-intro": "Halaman ini berisi daftar tag yang dapat ditandai oleh perangkat lunak terhadap suatu suntingan berikut artinya.",
        "tags-tag": "Nama tag",
        "tags-apply-not-allowed-multi": "{{PLURAL:$2|Tag|Tag}} berikut tidak diizinkan untuk diterapkan secara manual: $1",
        "tags-update-no-permission": "Anda tidak memiliki izin untuk menambah atau menghapus perubahan tag dari revisi atau entri log individu.",
        "tags-update-blocked": "Anda tidak dapat menambahkan atau menghapus perubahan tag ketika {{GENDER:$1|Anda}} sedang diblokir.",
+       "tags-update-add-not-allowed-one": "Tag \"$1\"tidak diizinkan untuk ditambahkan secara manual.",
+       "tags-update-add-not-allowed-multi": "{{PLURAL:$2|tag is|Tag ini}} tidak diizinkan untuk ditambahkan secara manual: $1",
+       "tags-update-remove-not-allowed-one": "Tag \"$1\" tidak diizinkan untuk dihapus.",
        "tags-edit-title": "Sunting tag",
        "tags-edit-manage-link": "Kelola tag",
        "tags-edit-revision-selected": "{{PLURAL:$1|Revisi terpilih|Revisi terpilih}} dari [[:$2]]:",
        "htmlform-time-placeholder": "JJ:MM:DD",
        "htmlform-datetime-placeholder": "TTTT-BB-HH JJ:MM:DD",
        "htmlform-date-invalid": "Nilai yang diberikan tidak dikenali sebagai tanggal. Coba lagi menggunakan format TTTT-BB-HH.",
+       "htmlform-time-invalid": "Nilai yang Anda tentukan bukan waktu yang dikenali. Cobalah menggunakan format HH:MM:SS",
        "htmlform-datetime-invalid": "Nilai yang Anda masukkan tidak dikenali sebagai tanggal dan waktu. Coba gunakan format YYYY-MM-DD HH:MM:SS",
        "htmlform-date-toolow": "Nilai yang Anda masukkan adalah sebelum tanggal paling dini yang dibolehkan $1",
        "htmlform-date-toohigh": "Nilai yang Anda masukkan adalah setelah tanggal paling akhir  yang dibolehkan $1",
        "htmlform-time-toolow": "Nilai yang Anda tentukan adalah sebelum waktu paling dini yang dibolehkan $1",
        "htmlform-time-toohigh": "Nilai yang Anda tentukan adalah setelah waktu paling baru yang dibolehkan $1",
+       "htmlform-datetime-toolow": "Nilai yang Anda tentukan berada sebelum tanggal dan waktu paling awal yang diperbolehkan $1",
+       "htmlform-datetime-toohigh": "Nilai yang Anda masukan telah terlewati setelah tanggal dan waktu terakhir yang diperbolehkan $1.",
        "htmlform-title-badnamespace": "[[:$1]] tidak berada dalam ruang nama \"{{ns:$2}}\".",
        "htmlform-title-not-creatable": "\"$1\" bukan merupakan judul halaman yang dapat dibuat",
        "htmlform-title-not-exists": "$1 tidak ada.",
        "logentry-tag-update-logentry": "$1 {{GENDER:$2|memperbarui}} tag pada entri log $5 dari halaman $3 ({{PLURAL:$7|menambahkan}} $6; {{PLURAL:$9|menghapus}} $8)",
        "rightsnone": "(tidak ada)",
        "revdelete-summary": "ringkasan",
+       "rightslogentry-temporary-group": "$1 (sementara, hingga $2)",
        "feedback-adding": "Menambahkan umpan balik ke halaman...",
        "feedback-back": "Kembali",
        "feedback-bugcheck": "Hebat! Hanya periksa bahwa itu bukan satu di antara [$1 bug yang telah dikenal].",
        "pagelang-language": "Bahasa",
        "pagelang-use-default": "Gunakan bahasa baku",
        "pagelang-select-lang": "Pilih bahasa",
+       "pagelang-reason": "Alasan",
        "pagelang-submit": "Kirim",
+       "pagelang-nonexistent-page": "Halaman $1 tidak tersedia",
+       "pagelang-unchanged-language": "Halaman $1 telah di atur ke bahasa $2",
+       "pagelang-unchanged-language-default": "Halaman $1 Telah diatur ke bahasa konten bawaan.",
+       "pagelang-db-failed": "Basis data gagal mengubah bahasa halaman",
        "right-pagelang": "Ubah bahasa halaman",
        "action-pagelang": "mengubah bahasa halaman",
        "log-name-pagelang": "Log perubahan bahasa",
        "mw-widgets-titleinput-description-new-page": "halaman belum ada",
        "mw-widgets-titleinput-description-redirect": "mengalihkan ke $1",
        "mw-widgets-categoryselector-add-category-placeholder": "Tambah sebuah kategori...",
+       "mw-widgets-usersmultiselect-placeholder": "Tambahkan lebih banyak...",
        "sessionmanager-tie": "Tidak dapat menggabungkan banyak jenis otentikasi permintaan: $1.",
        "sessionprovider-generic": "sesi $1",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "sesi berdasarkan kuki",
        "sessionprovider-nocookies": "Kuki mungkin dimatikan. Pastikan Anda telah mengaktifkan kuki dan coba mulai kembali.",
        "randomrootpage": "Halaman dasar sembarang",
        "log-action-filter-block": "Jenis pemblokiran:",
-       "log-action-filter-contentmodel": "Jenis modifikasi modelkonten:",
+       "log-action-filter-contentmodel": "Jenis perubahan modelkonten:",
        "log-action-filter-delete": "Jenis penghapusan:",
        "log-action-filter-import": "Jenis impor:",
        "log-action-filter-managetags": "Jenis tindakan manajemen tag:",
        "authmanager-link-no-primary": "Kredensial yang diberikan tidak dapat digunakan untuk menautkan akun.",
        "authmanager-link-not-in-progress": "Penautan akun tidak dilanjutkan atau data sesi telah hilang. Ulang kembali dari awal.",
        "authmanager-authplugin-setpass-failed-title": "Penggantian kata sandi gagal",
+       "authmanager-authplugin-setpass-failed-message": "Plugin autentikasi mencegah pengubahan pasword.",
+       "authmanager-authplugin-create-fail": "Plugin autentikasi mencegah pembuatan akun.",
+       "authmanager-authplugin-setpass-denied": "Plugin autentikasi tidak memperbolehkan mengubah kata kunci.",
        "authmanager-authplugin-setpass-bad-domain": "Domain tidak sah.",
        "authmanager-autocreate-noperm": "Pembuatan akun otomatis tidak diizinkan.",
        "authmanager-autocreate-exception": "Pembuatan akun otomatis dimatikan sementara karena galat sebelumnya.",
index d3590d9..57fefb1 100644 (file)
        "notloggedin": "Accesso non effettuato",
        "userlogin-noaccount": "Non hai ancora effettuato la registrazione?",
        "userlogin-joinproject": "Registrati su {{SITENAME}}",
-       "nologin": "Non hai ancora un accesso? $1.",
+       "nologin": "Non hai un'utenza? $1.",
        "nologinlink": "Registrati",
        "createaccount": "Registrati",
-       "gotaccount": "Hai già un accesso? $1.",
+       "gotaccount": "Hai già un'utenza? $1.",
        "gotaccountlink": "Entra",
        "userlogin-resetlink": "Hai dimenticato i tuoi dati di accesso?",
        "userlogin-resetpassword-link": "Hai dimenticato la password?",
        "rcfilters-empty-filter": "Nessun filtro attivo. Sono mostrati tutti i contributi.",
        "rcfilters-filterlist-title": "Filtri",
        "rcfilters-highlightmenu-title": "Seleziona un colore",
+       "rcfilters-highlightmenu-help": "Seleziona un colore per evidenziare questa proprietà",
        "rcfilters-filterlist-noresults": "Nessun filtro trovato",
        "rcfilters-filtergroup-registration": "Registrazione utente",
        "rcfilters-filter-registered-label": "Registrato",
index 9fde977..b37b468 100644 (file)
        "talk": "განხილვა",
        "views": "გადახედვა",
        "toolbox": "ხელსაწყოები",
+       "tool-link-userrights": "{{GENDER:$1|მომხმარებლის}} ჯგუფების შეცვლა",
+       "tool-link-userrights-readonly": "{{GENDER:$1|მომხმარებლის}} ჯგუფების ხილვა",
+       "tool-link-emailuser": "{{GENDER:$1|მომხმარებლისთვის}} ი-მეილის მიწერა",
        "userpage": "გადახედე მომხმარებლის გვერდს",
        "projectpage": "დაათვალიერე პროექტის გვერდი",
        "imagepage": "ფაილის გვერდის ნახვა",
        "contentmodelediterror": "არ შეგიძლიათ ამ ვერსიის რედაქტირება, რადგან მისი კონტენტის მოდელი არის <code>$1</code>, რაც განსხვავდება გვერდის მიმდინარე კონტენტის მოედლისაგან <code>$2</code>.",
        "recreate-moveddeleted-warn": "'''გაფრთხილება: თქვენ ხელახლა ქმნით გვერდს, რომელიც ადრე წაიშალა.'''\n\nგთხოვთ დაფიქრდეთ, მისაღები არის თუ არა ამ გვერდის რედაქტირების გაგრძელება.\nინფორმაციისთვის ქვემოთ მოყვანილია ამ გვერდის წაშლის ისტორია:",
        "moveddeleted-notice": "ეს გვერდი წაიშალა. ინფორმაციის მისაღებად ქვემოთ წარმოდგენილია შესაბამისი ჩანაწერები წაშლისა და გადარქმევის ჟურნალებიდან.",
-       "moveddeleted-notice-recent": "á\83\91á\83\9dá\83\93á\83\98á\83¨á\83\98, á\83\94á\83¡ á\83\92á\83\95á\83\94á\83 á\83\93á\83\98 á\83¬á\83\90á\83¨á\83\9aá\83\98á\83\9aá\83\98á\83\90 (á\83\91á\83\9dá\83\9aá\83\9d 24 á\83¡á\83\90á\83\90á\83\97á\83\98á\83¡ á\83\92á\83\90á\83\9cá\83\9bá\83\90á\83\95á\83\9aá\83\9dá\83\91á\83\90á\83¨á\83\98).\ná\83¬á\83\90á\83¨á\83\9aá\83\98á\83¡á\83\90 á\83\93á\83\90 á\83\92á\83\90á\83\93á\83\90á\83¢á\83\90á\83\9cá\83\98á\83¡ á\83\9fá\83£á\83 á\83\9cá\83\90á\83\9aá\83\98 á\83¥á\83\9bá\83\94á\83\9cოთ არის მოცემული.",
+       "moveddeleted-notice-recent": "á\83¡á\83\90á\83\9bá\83¬á\83£á\83®á\83\90á\83 á\83\9dá\83\93, á\83\94á\83¡ á\83\92á\83\95á\83\94á\83 á\83\93á\83\98 á\83¬á\83\90á\83¨á\83\9aá\83\98á\83\9aá\83\98á\83\90 (á\83\91á\83\9dá\83\9aá\83\9d 24 á\83¡á\83\90á\83\90á\83\97á\83\98á\83¡ á\83\92á\83\90á\83\9cá\83\9bá\83\90á\83\95á\83\9aá\83\9dá\83\91á\83\90á\83¨á\83\98).\ná\83¬á\83\90á\83¨á\83\9aá\83\98á\83¡á\83\90 á\83\93á\83\90 á\83\92á\83\90á\83\93á\83\90á\83¢á\83\90á\83\9cá\83\98á\83¡ á\83\9fá\83£á\83 á\83\9cá\83\90á\83\9aá\83\98 á\83¥á\83\95á\83\94á\83\9bოთ არის მოცემული.",
        "log-fulllog": "ყველა ჟურნალის ხილვა",
        "edit-hook-aborted": "შესწორება გაუქმებულია გადამჭერით.\nდამატებითი ახსნა არ ჩაწერილა.",
        "edit-gone-missing": "გვერდის განახლეა შეუძლებელია.\nშესაძლოა, იგი წაიშალა.",
        "userrights-user-editname": "შეიტანეთ მომხმარებლის სახელი:",
        "editusergroup": "{{GENDER:$1|მომხმარებელთა}} ჯგუფების რედაქტირება",
        "editinguser": "უფლებების შეცვლა {{GENDER:$1|მომხმარებლისთვის}} <strong>[[User:$1|$1]]</strong> $2",
+       "viewinguserrights": "{{GENDER:$1|მომხმარებლის}} უფლებების ხილვა <strong>[[User:$1|$1]]</strong> $2",
        "userrights-editusergroup": "დაარედაქტირეთ მომხმარებელთა ჯგუფები",
+       "userrights-viewusergroup": "{{GENDER:$1|მომხმარებლის}} ჯგუფების ხილვა",
        "saveusergroups": "{{GENDER:$1|მომხმარებელთა}} ჯგუფების შენახვა",
        "userrights-groupsmember": "ჯგუფის წევრი:",
        "userrights-groupsmember-auto": "ნაგულისხმევი წევრი:",
        "trackingcategories-disabled": "კატეგორია გამორთულია",
        "mailnologin": "გამგზავნი მისამართი არ არის მითითებული.",
        "mailnologintext": "თქვენ უნდა [[Special:UserLogin|წარადგენილი იყოთ სისტემისადმი]] და გქონდეთ წესიერი ელექტრონული ფოსტის მისამართი თქვენს [[Special:Preferences|კონფიგურაციაში]] იმისთვის, რომ გაუგზავნოთ წერილების სხვა მომხმარებლებს.",
-       "emailuser": "á\83\92á\83\90á\83£á\83\92á\83\96á\83\90á\83\95á\83\9cá\83\94á\83\97 á\83\98á\83\9bá\83\94á\83\98á\83\9aá\83\98 á\83\90á\83\9b á\83\9bá\83\9dá\83\9bá\83®á\83\9bá\83\90á\83 á\83\94á\83\91á\83\94á\83\9aá\83¡",
-       "emailuser-title-target": "ელ. ფოსტის მიწერა {{GENDER:$1|მომხმარებელთან}}",
+       "emailuser": "á\83\9bá\83\9dá\83\9bá\83®á\83\9bá\83\90á\83 á\83\94á\83\91á\83\9aá\83\98á\83¡á\83\97á\83\95á\83\98á\83¡ á\83\98\83\9bá\83\94á\83\98á\83\9aá\83\98á\83¡ á\83\9bá\83\98á\83¬á\83\94á\83 á\83\90",
+       "emailuser-title-target": "{{GENDER:$1|მომხმარებლისთვის}} ი-მეილის მიწერა",
        "emailuser-title-notarget": "ელ. ფოსტის გაგზავნა მომხმარებელთან",
        "emailpagetext": "თუ ამ მომხმარებელმა თავის პარამეტრებში ელ. ფოსტის მოქმედი მისამართი მიუთითა, ქვემოთ მოყვანილი ფორმის შევსებისას შეგიძლიათ {{GENDER:$1|მისთვის}} შეტყობინების გაგზავნა. ელ. ფოსტის მისამართი, რომელიც [[Special:Preferences|თქვენს პარამეტრებში]] მიუთითეთ, დაფიქსირდება „გამომგზავნის“ ველში, რათა ადრესატმა პასუხის გაცემა შეძლოს.",
        "defemailsubject": "ელ-ფოსტა საიტის {{SITENAME}} მომხმარებლისგან „$1“",
index 89564eb..2b3ef5a 100644 (file)
@@ -16,7 +16,8 @@
                        "វ័ណថារិទ្ធ",
                        "아라",
                        "Macofe",
-                       "Dcljr"
+                       "Dcljr",
+                       "Aefgh39622"
                ]
        },
        "tog-underline": "គូសបន្ទាត់ក្រោម​តំណភ្ជាប់៖",
@@ -43,7 +44,7 @@
        "tog-enotifminoredits": "ផ្ញើអ៊ីមែល​មកខ្ញុំពេលមានបន្លាស់ប្ដូរតិចតួច​លើទំព័រឬឯកសារផងដែរ​",
        "tog-enotifrevealaddr": "បង្ហាញ​អាសយដ្ឋានអ៊ីមែល​របស់ខ្ញុំ​ក្នុង​​អ៊ីមែល​ក្រើនរំលឹក​",
        "tog-shownumberswatching": "បង្ហាញ​ចំនួនអ្នកប្រើប្រាស់​ដែលតាមដាន​ទំព័រនេះ",
-       "tog-oldsig": "ហត្ថលេខាមានហើយ៖",
+       "tog-oldsig": "á\9e á\9e\8fá\9f\92á\9e\90á\9e\9bá\9f\81á\9e\81á\9e¶á\9e\8aá\9f\82á\9e\9bá\9e¢á\9f\92á\9e\93á\9e\80á\9e\98á\9e¶á\9e\93á\9e á\9e¾á\9e\99á\9f\96",
        "tog-fancysig": "ចុះហត្ថលេខា​ជា​អត្ថបទវិគី​ (ដោយ​គ្មានតំណភ្ជាប់​ស្វ័យប្រវត្តិ)",
        "tog-uselivepreview": "ប្រើប្រាស់​ការមើលមុនរហ័ស",
        "tog-forceeditsummary": "សូមរំលឹកខ្ញុំ​កាលបើខ្ញុំទុកប្រអប់ចំណារពន្យល់ឱ្យនៅទំនេរ",
@@ -59,7 +60,7 @@
        "tog-showhiddencats": "បង្ហាញចំណាត់ថ្នាក់ក្រុមដែលត្រូវបានលាក់",
        "tog-norollbackdiff": "បំភ្លេច​ភាព​ខុស​គ្នា​បន្ទាប់​ពី​អនុវត្តការ​ស្ដារវិញ",
        "tog-useeditwarning": "សូមព្រមាន​ខ្ញុំ​ ពេលដែលខ្ញុំ​ចាកចេញ​ពី​ទំព័រ​កែប្រែដោយមិន​បានរក្សា​ទុកបន្លាស់ប្ដូរ​នានា​",
-       "tog-prefershttps": "ប្រើប្រាស់ការតភ្ជាប់មានសុវត្ថិភាពជានិច្ចពេលកត់ឈ្មោះចូល",
+       "tog-prefershttps": "á\9e\94á\9f\92á\9e\9aá\9e¾á\9e\94á\9f\92á\9e\9aá\9e¶á\9e\9fá\9f\8bá\9e\80á\9e¶á\9e\9aá\9e\8fá\9e\97á\9f\92á\9e\87á\9e¶á\9e\94á\9f\8bá\9e\8aá\9f\82á\9e\9bá\9e\98á\9e¶á\9e\93á\9e\9fá\9e»á\9e\9cá\9e\8fá\9f\92á\9e\90á\9e·á\9e\97á\9e¶á\9e\96á\9e\87á\9e¶á\9e\93á\9e·á\9e\85á\9f\92á\9e\85á\9e\96á\9f\81á\9e\9bá\9e\80á\9e\8fá\9f\8bá\9e\88á\9f\92á\9e\98á\9f\84á\9f\87á\9e\85á\9e¼á\9e\9b",
        "underline-always": "ជានិច្ច",
        "underline-never": "កុំឲ្យសោះ",
        "underline-default": "តាមលំនាំដើមនៃ​កម្មវិធី​រុករក​",
        "morenotlisted": "បញ្ជីនេះមិនទាន់ពេញលេញទេ។",
        "mypage": "ទំព័រ​",
        "mytalk": "ការពិភាក្សា​",
-       "anontalk": "á\9e\91á\9f\86á\9e\96á\9f\90á\9e\9aá\9e\96á\9e·á\9e\97á\9e¶á\9e\80á\9f\92á\9e\9fá\9e¶á\9e\9fá\9f\86á\9e\9aá\9e¶á\9e\94á\9f\8b IP á\9e\93á\9f\81á\9f\87",
+       "anontalk": "á\9e\80á\9e¶á\9e\9aâ\80\8bá\9e\96á\9e·á\9e\97á\9e¶á\9e\80á\9f\92á\9e\9fá\9e",
        "navigation": "ការណែនាំ",
        "and": "&#32;និង",
        "qbfind": "ស្វែងរក",
        "nocookieslogin": "{{SITENAME}}ប្រើខូឃីដើម្បីកត់ឈ្មោះចូល។\n\nអ្នកបានជ្រើសមិនប្រើខូឃី។​\n\nសូមជ្រើសប្រើខូឃីវិញ រួចព្យាយាមម្តងទៀត។",
        "nocookiesforlogin": "{{int:nocookieslogin}}",
        "noname": "អ្នកមិនបានផ្ដល់អត្តនាមត្រឹមត្រូវទេ។",
-       "loginsuccesstitle": "á\9e\80á\9e\8fá\9f\8bá\9e\88á\9f\92á\9e\98á\9f\84á\9f\87á\9e\85á\9e¼á\9e\9bá\9e\94á\9e¶á\9e\93á\9e\9fá\9e\98á\9f\92á\9e\9aá\9f\81á\9e\85",
+       "loginsuccesstitle": "á\9e\94á\9e¶á\9e\93á\9e\80á\9e\8fá\9f\8bá\9e\88á\9f\92á\9e\98á\9f\84á\9f\87á\9e\85á\9e¼á\9e\9bá\9e á\9e¾á\9e\99",
        "loginsuccess": "'''ពេលនេះអ្នកបានកត់ឈ្មោះចូល{{SITENAME}}ដោយប្រើឈ្មោះ \"$1\"ហើយ។'''",
        "nosuchuser": "មិនមានអ្នកប្រើដែលមានឈ្មោះ \"$1\" ទេ។\n\nសូម​ពិនិត្យ​ក្រែង​លោ​មានកំហុស​អក្ខរាវិរុទ្ធឬ [[Special:CreateAccount|បង្កើត​គណនី​ថ្មី]]។",
        "nosuchusershort": "គ្មានអ្នកប្រើដែលមានឈ្មោះ $1\" ទេ។\n\nសូម​ពិនិត្យ​​អក្ខរាវិរុទ្ធ​របស់អ្នក ។",
        "newpassword": "ពាក្យសម្ងាត់ថ្មី៖",
        "retypenew": "សូមវាយពាក្យសម្ងាត់ថ្មី​ម្តងទៀត៖",
        "resetpass_submit": "ដាក់ប្រើពាក្យសម្ងាត់និង​កត់ឈ្មោះចូល",
-       "changepassword-success": "á\9e\96á\9e¶á\9e\80á\9f\92á\9e\99á\9e\9fá\9e\98á\9f\92á\9e\84á\9e¶á\9e\8fá\9f\8bá\9e\9aá\9e\94á\9e\9fá\9f\8bá\9e¢á\9f\92á\9e\93á\9e\80á\9e\8fá\9f\92á\9e\9aá\9e¼á\9e\9cá\9e\94á\9e¶á\9e\93á\9e\95á\9f\92á\9e\9bá\9e¶á\9e\9fá\9f\8bá\9e\94á\9f\92á\9e\8fá\9e¼á\9e\9aá\9e\94á\9e¶á\9e\93á\9e\9fá\9f\86á\9e\9aá\9f\81á\9e\85á\9e á\9e¾á\9e\99!",
+       "changepassword-success": "ពាក្យសម្ងាត់របស់អ្នកត្រូវបានផ្លាស់ប្តូរហើយ!",
        "changepassword-throttled": "អ្នកបានព្យាយាមកត់ឈ្មោះចូលជាប់ៗគ្នាច្រើនដងពេកហើយ។​\nសូមរង់ចាំរយៈពេល$1 មុនពេលសាកល្បងម្ដងទៀត។",
        "resetpass_forbidden": "ពាក្យសម្ងាត់មិនអាចផ្លាស់ប្តូរបានទេ",
        "resetpass-no-info": "អ្នក​ចាំបាច់​ត្រូវតែ​កត់ឈ្មោះចូល ដើម្បី​ចូលទៅកាន់​ទំព័រ​នេះ​ដោយផ្ទាល់​។",
        "passwordreset-emaildisabled": "មុខងារអ៊ីមែលត្រូវបានបិទមិនអោយប្រើនៅលើវិគីនេះ។",
        "passwordreset-username": "អត្តនាម៖",
        "passwordreset-domain": "ដូម៉ែន៖",
-       "passwordreset-capture": "មើលអ៊ីមែលលទ្ធផល?",
-       "passwordreset-capture-help": "ប្រសិនបើអ្នកគូសធីកប្រអប់នេះ អ៊ីមែល (ដែលមានពាក្យសម្ងាត់បណ្ដោះអាសន្ន) មិនត្រូវបានបង្ហាញដូចគ្នានឹងអ៊ីមែលដែលនឹងត្រូវផ្ញើទៅទៅកាន់អ្នកប្រើប្រាស់ដែរ។",
        "passwordreset-email": "អាសយដ្ឋានអ៊ីមែល៖",
        "passwordreset-emailtitle": "ព័ត៌មានលំអិតពីគណនីនៅលើ {{SITENAME}}",
        "passwordreset-emailtext-ip": "មាននរណាម្នាក់ (ប្រហែលជាខ្លួនអ្នកផ្ទាល់, មកពីអាស័យដ្ឋាន IP $1) បានស្នើសុំស្ដារពាក្យសម្ងាត់របស់អ្នកសម្រាប់ {{SITENAME}} ($4)។ {{PLURAL:$3|គណនី|គណនី}}អ្នកប្រើប្រាស់ដូចតទៅនេះ\nមានជាប់ទាក់ទិននឹងអាសយដ្ឋានអ៊ីមែលនេះ៖\n\n$2\n\n{{PLURAL:$3|ពាក្យសម្ងាត់បណ្ដោះអាសន្ននេះ|ពាក្យសម្ងាត់បណ្ដោះអាសន្នទាំងនេះ}} និងហួសសុពលភាពក្នុងរយៈពេល {{PLURAL:$5|មួយថ្ងៃ|$5 ថ្ងៃ}}។\nយកល្អអ្នកគួរតែកត់ឈ្មោះចូលរួចជ្រើសរើសពាក្យសម្ងាត់ថ្មីមួយ។ ប្រសិនបើមាននរណាម្នាក់ផ្សេងធ្វើការស្នើសុំនេះ \nឬប្រសិនបើអ្នកនឹកឃើញពាក្យសម្ងាត់ដើមរបស់អ្នក ហើយអ្នកមិនប្រាថ្នាផ្លាស់ប្ដូរវាទៀតទេនោះ អ្នកគ្រាន់តែ\nបំភ្លេចអំពីសារមួយនេះ ហើយបន្តប្រើប្រាស់ពាក្យសម្ងាត់ចាស់របស់អ្នកទៅបានហើយ។",
        "saveprefs": "រក្សាទុក",
        "restoreprefs": "ស្ដារ​ការកំណត់​ទាំងអស់​ទៅ​លំនាំដើម (គ្រប់ផ្នែកទាំងអស់)",
        "prefs-editing": "កំណែប្រែ",
-       "rows": "ជួរដេក៖",
-       "columns": "ជួរឈរ៖",
        "searchresultshead": "ស្វែងរក",
        "stub-threshold": "ទំហំអប្បបរមាសម្រាប់ដាក់ជាទម្រង់ទំព័រកំប៉ិចកំប៉ុក($1)៖",
        "stub-threshold-sample-link": "គំរូ",
        "userrights-reason": "មូលហេតុ៖",
        "userrights-no-interwiki": "អ្នកគ្មានការអនុញ្ញាតកែប្រែសិទ្ធិរបស់អ្នកប្រើប្រាស់លើវិគីផ្សេងទេ។",
        "userrights-nodatabase": "មូលដ្ឋានទិន្នន័យ $1 មិនមាន ឬ ស្ថិតនៅខាងក្រៅ។",
-       "userrights-nologin": "អ្នកត្រូវតែ [[Special:UserLogin|កត់ឈ្មោះចូល]]ដោយប្រើគណនីអ្នកអភិបាលដើម្បីផ្ដល់សិទ្ធិឱ្យអ្នកប្រើប្រាស់​។",
-       "userrights-notallowed": "លោកអ្នកគ្មានការអនុញ្ញាតដើម្បីបន្ថែមឬដកសិទ្ធិរបស់អ្នកប្រើប្រាស់ដទៃទេ។",
        "userrights-changeable-col": "ក្រុមនានាដែលអ្នកអាចផ្លាស់ប្ដូរបាន",
        "userrights-unchangeable-col": "ក្រុមនានាដែលអ្នកមិនអាចផ្លាស់ប្ដូរបាន",
        "userrights-conflict": "មានទំនាស់អំពីការកែប្រែសិទ្ធិអ្នកប្រើប្រាស់! សូមត្រួតពិនិត្យឡើងវិញរួចអះអាងពីការកែប្រែរបស់អ្នក។",
-       "userrights-removed-self": "អ្នកបានដកសិទ្ធិខ្លួនឯងបានសម្រេចហើយ។ ហេតុនេះ អ្នកមិនអាចចូលមើលទំព័រនេះតទៅទៀតទេ។",
        "group": "ក្រុម៖",
        "group-user": "អ្នកប្រើប្រាស់",
        "group-autoconfirmed": "អ្នកប្រើប្រាស់ទទួលស្គាល់ដោយស្វ័យប្រវត្តិ",
        "right-siteadmin": "ចាក់សោនិងបើកសោមូលដ្ឋានទិន្នន័យ",
        "right-override-export-depth": "នាំចេញទំព័ររួមទាំងទំព័រដែលមានភ្ជាប់តំណភ្ជាប់​រហូតដល់លំដាប់ទី៥",
        "right-sendemail": "ផ្ញើអ៊ីមែលទៅកាន់អ្នកប្រើដទៃ",
-       "right-passwordreset": "មើលអ៊ីមែលសំរាប់កំណត់ពាក្យសម្ងាត់ឡើងវិញ",
        "newuserlogpage": "កំណត់ហេតុនៃការបង្កើតគណនី",
        "newuserlogpagetext": "នេះជាកំណត់ហេតុនៃការបង្កើតអ្នកប្រើប្រាស់។",
        "rightslog": "កំណត់ហេតុនៃការប្តូរសិទ្ធិអ្នកប្រើប្រាស់",
        "rightslogtext": "នេះ​ជា​កំណត់ហេតុនៃបំលាស់ប្ដូរចំពោះកាប្ដូរក្រុមសមាជិកភាព​របស់​អ្នកប្រើប្រាស់។",
        "action-read": "អានទំព័រនេះ",
        "action-edit": "កែប្រែទំព័រនេះ",
-       "action-createpage": "á\9e\94á\9e\84á\9f\92á\9e\80á\9e¾á\9e\8fá\9e\91á\9f\86á\9e\96á\9f\90á\9e\9aá\9e\93á\9e¶á\9e\93á\9e",
-       "action-createtalk": "á\9e\94á\9e\84á\9f\92á\9e\80á\9e¾á\9e\8fá\9e\91á\9f\86á\9e\96á\9f\90á\9e\9aá\9e\96á\9e·á\9e\97á\9e¶á\9e\80á\9f\92á\9e\9fá\9e¶á\9e\93á\9e¶á\9e\93á\9e",
+       "action-createpage": "á\9e\94á\9e\84á\9f\92á\9e\80á\9e¾á\9e\8fá\9e\91á\9f\86á\9e\96á\9f\90á\9e\9aá\9e\93á\9f\81á\9f\87",
+       "action-createtalk": "á\9e\94á\9e\84á\9f\92á\9e\80á\9e¾á\9e\8fá\9e\91á\9f\86á\9e\96á\9f\90á\9e\9aá\9e\96á\9e·á\9e\97á\9e¶á\9e\80á\9f\92á\9e\9fá\9e¶á\9e\93á\9f\81á\9f\87",
        "action-createaccount": "បង្កើតគណនីអ្នកប្រើប្រាស់នេះ",
        "action-history": "មើលប្រវត្តិទំព័រនេះ",
        "action-minoredit": "ចំណាំកំណែប្រែនេះថាជាកំណែប្រែតិចតួច",
        "feedback-thanks-title": "សូមអរគុណ!",
        "searchsuggest-search": "ស្វែងរក​",
        "searchsuggest-containing": "ដែលមានពាក្យ...",
-       "api-error-badaccess-groups": "អ្នកគ្មានការអនុញ្ញាតអោយផ្ទុកឯកសារឡើងទៅក្នុងវិគីនេះទេ។",
-       "api-error-empty-file": "ឯកសារដែលអ្នកបានដាក់ស្នើគឺទទេ។",
        "api-error-emptypage": "ការអនុញ្ញាតអោយបង្កើតទំព័រថ្មីដែលគ្មានសរសេរអ្វីទេ",
-       "api-error-fileexists-forbidden": "ឯកសារដែលមានឈ្មោះ \"$1\" មានរួចហើយ ហើយមិនអាចសរសេរជាន់ពីលើបានទេ។",
-       "api-error-fileexists-shared-forbidden": "ឯកសារដែលមានឈ្មោះ \"$1\" មានរួចហើយនៅក្នុងថតឯកសាររួម ហើយមិនអាចសរសេរជាន់ពីលើបានទេ។",
-       "api-error-file-too-large": "ឯកសារដែលអ្នកបានដាក់ស្នើធំពេកហើយ។",
-       "api-error-filename-tooshort": "ឈ្មោះឯកសារខ្លីពេកហើយ។",
-       "api-error-filetype-banned": "ឯកសារប្រភេទនេះត្រូវបានហាមប្រាម។",
-       "api-error-filetype-banned-type": "$1 {{PLURAL:$4|មិនមែនជា​ប្រភេទ​ឯកសារ​ដែល​ត្រូវ​បាន​គេ​អនុញ្ញាត​ទេ|មិនមែនជា​ប្រភេទ​ឯកសារ​ដែល​ត្រូវ​បាន​គេ​អនុញ្ញាត​ទេ​}}។\n{{PLURAL:$3|ប្រភេទឯកសារ​|ប្រភេទឯកសារ​}}ដែល​ត្រូវ​បាន​គេ​អនុញ្ញាត​គឺ $2 ។",
-       "api-error-filetype-missing": "ឈ្មោះឯកសារបាត់កន្ទុយ។",
-       "api-error-http": "បញ្ហាខាងក្នុង៖ មិនអាចភ្ជាប់ទោកាន់ម៉ាស៊ីនបំរើការ។",
-       "api-error-illegal-filename": "មិនអនុញ្ញាតអោយប្រើឈ្មោះឯកសារនេះ។",
-       "api-error-internal-error": "បញ្ហាខាងក្នុង៖ មានបញ្ហាណាមួយកើតឡើងពេលកំពុងដំណើរការផ្ទុកឯកសារអ្នកឡើងទៅក្នុងវិគី។",
-       "api-error-missingresult": "បញ្ហាខាងក្នុង៖ មិនអាចកំណត់បានថាការថតចំលងបានសំរេចទេ។",
-       "api-error-mustbeloggedin": "អ្នកត្រូវតែកត់ឈ្មោះចូលដើម្បីផ្ទុកឯកសារឡើង។",
-       "api-error-ok-but-empty": "បញ្ហាខាងក្នុង៖ គ្មានចម្លើយពីម៉ាស៊ីនបម្រើការ។",
-       "api-error-overwrite": "មិនអនុញ្ញាតអោយសរសេរជាន់ពីលើឯកសារដែលមានស្រាប់ហើយ។",
-       "api-error-timeout": "ម៉ាស៊ីនបំរើការមិនបានឆ្លើយតបក្នុងរយៈពេលដែលយើងរំពឹងទុក។",
-       "api-error-unclassified": "បញ្ហាមិនស្គាល់មួយបានកើតឡើង។",
-       "api-error-unknown-code": "បញ្ហាមិនស្គាល់៖ \"$1\" ។",
-       "api-error-unknown-error": "បញ្ហាខាងក្នុង៖ មានបញ្ហាមិនស្រួលពេលកំពុងព្យាយាមផ្ទុកឯកសាររបស់អ្នកឡើង។",
        "api-error-unknown-warning": "ការព្រមានមិនស្គាល់៖ \"$1 ។",
        "api-error-unknownerror": "បញ្ហាមិនស្គាល់៖ \"$1\" ។",
-       "api-error-uploaddisabled": "ការផ្ទុកឡើងត្រូវបានបិទមិនអោយប្រើនៅលើវិគីនេះទេ។",
-       "api-error-verification-error": "ឯកសារនេះប្រហែលជាខូច ឯមានកន្ទុយមិនត្រឹមត្រូវ។",
        "duration-seconds": "$1 {{PLURAL:$1|វិនាទី|វិនាទី}}",
        "duration-minutes": "$1 {{PLURAL:$1|នាទី|នាទី}}",
        "duration-hours": "$1 {{PLURAL:$1|ម៉ោង|ម៉ោង}}",
index f959829..8660847 100644 (file)
        "rcfilters-filter-newpages-description": "새 문서를 만드는 편집.",
        "rcfilters-filter-categorization-label": "분류 차이",
        "rcfilters-filter-categorization-description": "분류에서 추가되거나 제거되는 페이지의 기록.",
+       "rcfilters-filter-logactions-description": "관리적 조치, 계정 만들기, 페이지 삭제, 업로드...",
        "rcnotefrom": "아래는 <strong>$3, $4</strong>부터 시작하는 {{PLURAL:$5|바뀜이 있습니다}}. (최대 <strong>$1</strong>개가 표시됨)",
        "rclistfrom": "$3 $2부터 시작하는 새로 바뀐 문서 보기",
        "rcshowhideminor": "사소한 편집 $1",
        "uncategorizedcategories": "분류되지 않은 분류 목록",
        "uncategorizedimages": "분류되지 않은 파일 목록",
        "uncategorizedtemplates": "분류되지 않은 틀 목록",
+       "uncategorized-categories-exceptionlist": "# \"특수:분류안된분류\"에 언급되지 않은 분류의 목록을 포함합니다. 한 줄에 하나씩 \"*\"로 시작합니다. 다른 문자(공백 포함)로 시작하는 줄들은 무시됩니다. 주석의 경우 \"#\"를 사용하십시오.",
        "unusedcategories": "사용하지 않는 분류 목록",
        "unusedimages": "사용하지 않는 파일 목록",
        "wantedcategories": "필요한 분류 목록",
        "apisandbox-alert-field": "이 필드의 값이 유효하지 않습니다.",
        "apisandbox-continue": "계속",
        "apisandbox-continue-clear": "지우기",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}}은 마지막 요청을 [https://www.mediawiki.org/wiki/API:Query#Continuing_queries 계속]합니다. {{int:apisandbox-continue-clear}}는 계속 관련 변수들을 삭제합니다.",
        "apisandbox-param-limit": "최대 한계치를 사용하려면 <kbd>max</kbd>를 입력하십시오.",
        "apisandbox-multivalue-all-namespaces": "$1 (모든 이름공간)",
        "apisandbox-multivalue-all-values": "$1 (모든 값)",
        "imagenocrossnamespace": "파일을 파일이 아닌 이름공간으로 이동할 수 없습니다.",
        "nonfile-cannot-move-to-file": "파일이 아닌 문서를 파일 이름공간으로 이동할 수 없습니다.",
        "imagetypemismatch": "새 파일의 확장자가 원래의 확장자와 일치하지 않습니다.",
-       "imageinvalidfilename": " 파일 이름이 잘못되었습니다.",
+       "imageinvalidfilename": "대상 파일 이름이 잘못되었습니다.",
        "fix-double-redirects": "원래 제목을 가리키는 넘겨주기를 새로 고침",
        "move-leave-redirect": "이동한 뒤 넘겨주기를 남기기",
        "protectedpagemovewarning": "<strong>경고:</strong> 이 문서는 관리자만 이동할 수 있도록 보호되어 있습니다.\n최근 기록을 참조를 위해 아래에 제공합니다:",
        "htmlform-date-invalid": "지정한 값은 인식할 수 있는 날짜가 아닙니다. YYYY-MM-DD 형식을 사용하세요.",
        "htmlform-time-invalid": "지정한 값은 인식할 수 있는 시간이 아닙니다. HH:MM:SS 형식을 사용하세요.",
        "htmlform-datetime-invalid": "지정한 값은 인식할 수 있는 날짜 및 시간이 아닙니다. YYYY-MM-DD HH:MM:SS 형식을 사용하세요.",
+       "htmlform-date-toolow": "지정된 값은 최소로 허용된 날짜 $1 보다 이전입니다.",
+       "htmlform-date-toohigh": "지정된 값은 최대로 허용된 날짜 $1 보다 이후입니다.",
+       "htmlform-time-toolow": "지정된 값은 최소로 허용된 시간 $1 보다 이전입니다.",
+       "htmlform-time-toohigh": "지정된 값은 최대로 허용된 시간 $1 보다 이후입니다.",
+       "htmlform-datetime-toolow": "지정된 값은 최소로 허용된 시간 $1 보다 이전입니다.",
+       "htmlform-datetime-toohigh": "지정된 값은 최대로 허용된 시간 $1 보다 이후입니다.",
        "htmlform-title-badnamespace": "[[:$1]] 문서는 \"{{ns:$2}}\" 이름공간에 없습니다.",
        "htmlform-title-not-creatable": "\"$1\"은 만들 수 없는 문서 제목입니다.",
        "htmlform-title-not-exists": "$1 문서는 존재하지 않습니다.",
index 5f512ff..27950f9 100644 (file)
        "botpasswords-no-provider": "Tsy afaka antsoina ny BotPasswordsSessionProvider.",
        "botpasswords-restriction-failed": "Manakana ity fidirana ity ny fepetra mifehy ny tenimiafina rôbô.",
        "resetpass_forbidden": "Tsy afaka ovaina ny tenimiafina",
+       "resetpass_forbidden-reason": "Tsy afaka ovaina ny tenimiafina: $1",
        "resetpass-no-info": "Tsy maintsy tafiditra ao amin'ny kaontinao ianao vao afaka mijery ity pejy ity.",
        "resetpass-submit-loggedin": "Ovay ny tenimiafina",
        "resetpass-submit-cancel": "Aoka ihany",
        "changeemail-password": "Tenimiafinao eo amin'i {{SITENAME}}:",
        "changeemail-submit": "Hanova ny adiresy imailaka",
        "changeemail-throttled": "Betsaka loatra ny andrana nidiranao.\nMiandrasa $1 alohan'ny manandrana indray.",
+       "changeemail-nochange": "Mampidira adiresy mailaka hafa vaovao.",
        "resettokens": "Hamerina ny token",
        "resettokens-text": "Azonao averina eto ny token izay hahafahana mitsidika ny fampahalalana tsy sarababem-bahoaka ao amin'ny kaontinao. Tokony ataonao izany raha voazara tsy fanahy iniana na raha nisy nangalatra ny kaontinao.",
        "resettokens-no-tokens": "Tsy misy token ho averina",
        "grant-highvolume": "Manova pejy ambongadiny",
        "grant-oversight": "Manafina mpikambana ary mamafa versiona",
        "grant-patrol": "Manamarika ireo fiovam-pejy ho voatsidika",
+       "grant-privateinfo": "Mijery fampahalalana tsy sarababembahoaka",
        "grant-protect": "Miaro ary manala ny fiavoram-pejy",
        "grant-rollback": "Mamafa fiovana amin'ny pejy",
        "grant-sendemail": "Mandefa mailaka amin'ny mpikambana hafa",
        "grant-basic": "Zo fototra",
        "grant-viewdeleted": "Mitsidika ireo rakitra ary pejy voafafa",
        "grant-viewmywatchlist": "Hijery ny lisitry ny pejy arahanao",
+       "grant-viewrestrictedlogs": "Mijery iditry ny laogy tsy sarababembahoaka",
        "newuserlogpage": "Laogim-panokafana kaontim-pikambana",
        "newuserlogpagetext": "Ity pejy ity dia maneho ny tantaran'asan'ny fampidirana mpikambana vaovao.",
        "rightslog": "Laogim-piovan'ny zom-pikambana",
        "rcfilters-invalid-filter": "Sivana tsy azo raisina",
        "rcfilters-empty-filter": "Tsy misy sivana miasa. Aseho avokoa ireo fandraisan'anjara.",
        "rcfilters-filterlist-title": "Sivana",
+       "rcfilters-filterlist-feedbacklink": "Mamoaka hevitra mikasika ny sivana vaovao (beta)",
        "rcfilters-highlightbutton-title": "Hampibaribary ny valiny",
        "rcfilters-highlightmenu-title": "Hisafidy loko",
+       "rcfilters-highlightmenu-help": "Misafidy loko hampanabaribariana ilay tondro",
        "rcfilters-filterlist-noresults": "Tsy nahitana sivana",
        "rcfilters-filtergroup-registration": "Fisoratan'ny mpikambana",
        "rcfilters-filter-registered-label": "Nisoratra anarana",
        "rcfilters-filter-registered-description": "Mpanova tafiditra.",
        "rcfilters-filter-unregistered-label": "Tsy nisoratra anarana",
        "rcfilters-filter-unregistered-description": "Mpikambana tsy niditra.",
+       "rcfilters-filtergroup-authorship": "Manova ny tompon-tsoratra",
        "rcfilters-filter-editsbyself-label": "Ny fiovanao",
        "rcfilters-filter-editsbyself-description": "Fiovana nataonao",
        "rcfilters-filter-editsbyother-label": "Fiovana nataon'ny hafa",
        "rcfilters-filter-major-description": "Fiovana tsy natao ho madinika.",
        "rcfilters-filtergroup-changetype": "Karazam-piovana",
        "rcfilters-filter-pageedits-label": "Fiovam-pejy",
+       "rcfilters-filter-pageedits-description": "Manova votoatin'ny wiki, resaka, visavisan-tsokajy...",
        "rcfilters-filter-newpages-label": "Famoronam-pejy",
        "rcfilters-filter-newpages-description": "Fiovana nahatonga famoronam-pejy.",
        "rcfilters-filter-categorization-label": "Fiovana amin'ny sokajy",
        "pageinfo-length": "Halavam-pejy (oktety)",
        "pageinfo-article-id": "Laharam-pejy",
        "pageinfo-language": "Tenin'ny votoatiny",
+       "pageinfo-language-change": "ovaina",
        "pageinfo-content-model": "Môdelim-botoatim-pejy",
+       "pageinfo-content-model-change": "ovaina",
        "pageinfo-robot-policy": "Fanondroana ataon'ny rôbô",
        "pageinfo-robot-index": "Azo atao",
        "pageinfo-robot-noindex": "Tsy azo tondroina",
        "pageinfo-watchers": "Isan'ny mpandray anjara manaraka",
+       "pageinfo-visiting-watchers": "Isan'ny mpijery pejy nijery ny fiovana vao haingan'ity pejy ity",
        "pageinfo-few-watchers": "Mpanaraka latsaky ny $1{{PLURAL:}}",
        "pageinfo-redirects-name": "Fihodinana manketo amin'ity pejy ity",
        "pageinfo-subpages-name": "Zana-pejin'ity pejy ity",
        "pageinfo-category-pages": "Isam-pejy",
        "pageinfo-category-subcats": "Isan'ny zana-tsokajy",
        "pageinfo-category-files": "Isan'ny rakitra",
+       "pageinfo-user-id": "ID mpikambana",
        "markaspatrolleddiff": "Marihana ho voamarina",
        "markaspatrolledtext": "Marihana ho hita sy voatsara",
        "markedaspatrolled": "Voamarina",
        "patrol-log-page": "Laogin'ny fanovana voamarina",
        "patrol-log-header": "Ity dia laogy mikasikan'ny fanovana voamarina.",
        "log-show-hide-patrol": "$1 ny laogy mikasikan'ny versiona voamarina",
+       "confirm-markpatrolled-button": "OK",
        "deletedrevision": "Fanovana an'i $1 taloha voafafa.",
        "filedeleteerror-short": "Tsi-fetezana teo am-pamafàna ilay rakitra : $1",
        "filedeleteerror-long": "Nisy tsi-fetezana nitranga teo am-pamafàna ilay rakitra :\n\n$1",
index 8c1cd1a..b932e1d 100644 (file)
@@ -19,7 +19,8 @@
                        "Macofe",
                        "Nemo bis",
                        "Matma Rex",
-                       "Kaldari"
+                       "Kaldari",
+                       "Xð"
                ]
        },
        "tog-underline": "Потцртување на врски:",
        "unprotectthispage": "Измена на заштитата на страницава",
        "newpage": "Нова страница",
        "talkpage": "Разговор",
-       "talkpagelinktext": "Разговор",
+       "talkpagelinktext": "разговор",
        "specialpage": "Службена страница",
        "personaltools": "Лични алатки",
        "articlepage": "Преглед на содржината",
        "rcfilters-filterlist-feedbacklink": "Дајте мислење за новите (бета) филтри",
        "rcfilters-highlightbutton-title": "Истакнување на исход",
        "rcfilters-highlightmenu-title": "Изберете боја",
+       "rcfilters-highlightmenu-help": "Изберете боја за да го истакнете ова својство",
        "rcfilters-filterlist-noresults": "Не пронајдов ниеден филтер",
        "rcfilters-filtergroup-registration": "Регистрација на корисници",
        "rcfilters-filter-registered-label": "Регистрирани",
index 15125c6..445a36e 100644 (file)
        "rcfilters-invalid-filter": "Ugyldig filter",
        "rcfilters-empty-filter": "Ingen aktive filtre. Alle bidrag vises.",
        "rcfilters-filterlist-title": "Filtre",
+       "rcfilters-filterlist-feedbacklink": "Gi tilbakemelding på de nye (beta)filtrene",
+       "rcfilters-highlightbutton-title": "Marker resultater",
+       "rcfilters-highlightmenu-title": "Velg en farge",
        "rcfilters-filterlist-noresults": "Ingen filtre funnet",
        "rcfilters-filtergroup-registration": "Brukerregistrering",
        "rcfilters-filter-registered-label": "Registrerte",
        "editcomment": "Redigeringskommentaren var: <em>$1</em>",
        "revertpage": "Tilbakestilte endringer av [[Special:Contributions/$2|$2]] ([[User talk:$2|brukerdiskusjon]]) til siste versjon av [[User:$1|$1]]",
        "revertpage-nouser": "Tilbakestilt endringer av skjult bruker til siste versjon av\n{{GENDER:$1|[[User:$1|$1]]}}",
-       "rollback-success": "Tilbakestilte endringer av $1; endret til siste versjon av $2.",
+       "rollback-success": "Tilbakestilte endringer av {{GENDER:$3|$1}}; endret til siste versjon av {{GENDER:$4|$2}}.",
        "rollback-success-notify": "Tilbakestilte endringer av $1;\nendret tilbake til siste revisjon av $2. [$3 Vis endringer]",
        "sessionfailure-title": "Sesjonsfeil",
        "sessionfailure": "Det ser ut til å være et problem med innloggingen din, og den ble avbrutt av sikkerhetshensyn. Trykk ''Tilbake'' i nettleseren din, oppdater siden og prøv igjen.",
index ea443c5..8cf03d5 100644 (file)
        "rcfilters-filterlist-feedbacklink": "Geef feedback op de nieuwe (beta) filters",
        "rcfilters-highlightbutton-title": "Resultaten markeren",
        "rcfilters-highlightmenu-title": "Kies een kleur",
+       "rcfilters-highlightmenu-help": "Selecteer een kleur om deze eigenschap uit te lichten",
        "rcfilters-filterlist-noresults": "Geen filters gevonden",
        "rcfilters-filtergroup-registration": "Gebruikers-registratie",
        "rcfilters-filter-registered-label": "Geregistreerd",
index 8df074c..227179c 100644 (file)
        "noarticletext": "Obecnie ta strona nie ma zawartości.\nMożesz [[Special:Search/{{PAGENAME}}|wyszukać ten tytuł na innych stronach]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} przeszukać rejestr] \nlub [{{fullurl:{{FULLPAGENAME}}|action=edit}} utworzyć tę stronę]</span>.",
        "noarticletext-nopermission": "Ta strona nie posiada jeszcze zawartości.\nMożesz [[Special:Search/{{PAGENAME}}|wyszukać ten tytuł]] w treści innych stron\nlub <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} przeszukać powiązane rejestry]</span>, ale nie masz uprawnień do utworzenia tej strony",
        "missing-revision": "Wersja #$1 strony \"{{FULLPAGENAME}}\" nie istnieje.\n\nZazwyczaj jest to spowodowane przestarzałym linkiem do usuniętej strony. Powód usunięcia znajduje się w [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} rejestrze].",
-       "userpage-userdoesnotexist": "Użytkownik „<nowiki>$1</nowiki>” nie jest zarejestrowany.\nUpewnij się, czy na pewno zamierza{{GENDER:|łeś|łaś|sz}} utworzyć lub zmodyfikować właśnie tę stronę.",
+       "userpage-userdoesnotexist": "Użytkownik „$1” nie jest zarejestrowany.\nUpewnij się, czy na pewno zamierzał{{GENDER:|eś|aś|eś/aś}} utworzyć lub zmodyfikować właśnie tę stronę.",
        "userpage-userdoesnotexist-view": "Konto użytkownika „$1” nie jest zarejestrowane.",
        "blocked-notice-logextract": "{{GENDER:$1|Ten użytkownik|Ta użytkowniczka}} jest obecnie {{GENDER:$1|zablokowany|zablokowana}}.\nOstatni wpis rejestru blokad jest pokazany poniżej.",
        "clearyourcache": "<strong>Uwaga:</strong> aby zobaczyć zmiany po zapisaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.\n* <strong>Firefox / Safari:</strong> Przytrzymaj <em>Shift</em> podczas klikania <em>Odśwież bieżącą stronę</em>, lub naciśnij klawisze <em>Ctrl+F5</em> lub <em>Ctrl+R</em> (<em>⌘-R</em> na komputerze Mac)\n* <strong>Google Chrome:</strong> Naciśnij <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> na komputerze Mac)\n* <strong>Internet Explorer:</strong> Przytrzymaj <em>Ctrl</em>, jednocześnie klikając <em>Odśwież</em>, lub naciśnij klawisze <em>Ctrl+F5</em>\n* <strong>Opera:</strong> Przejdź do <em>Menu → Ustawienia</em> (<em>Opera → Preferencje</em> w Mac), a następnie <em>Prywatność i bezpieczeństwo → Wyczyść dane przeglądania → Opróżnij pamięć podręczną</em>.",
        "rcfilters-invalid-filter": "Nieprawidłowy filtr",
        "rcfilters-empty-filter": "Brak aktywnych filtrów. Wyświetlane są wszystkie zmiany.",
        "rcfilters-filterlist-title": "Filtry",
+       "rcfilters-filterlist-feedbacklink": "Podziel się swoją opinią na temat tych nowych (beta) filtrów",
        "rcfilters-highlightbutton-title": "Podświetl wyniki",
        "rcfilters-highlightmenu-title": "Wybierz kolor",
+       "rcfilters-highlightmenu-help": "Wybierz kolor, aby podświetlić tę właściwość",
        "rcfilters-filterlist-noresults": "Nie znaleziono filtrów",
        "rcfilters-filtergroup-registration": "Rejestracja użytkownika",
-       "rcfilters-filter-registered-label": "Zarejestrowany",
+       "rcfilters-filter-registered-label": "Zarejestrowani",
        "rcfilters-filter-registered-description": "Zalogowani edytorzy.",
-       "rcfilters-filter-unregistered-label": "Niezarejestrowany",
+       "rcfilters-filter-unregistered-label": "Niezarejestrowani",
        "rcfilters-filter-unregistered-description": "Niezalogowani",
        "rcfilters-filtergroup-authorship": "Autorstwo edycji",
        "rcfilters-filter-editsbyself-label": "Moje edycje",
        "rcfilters-filter-pageedits-label": "Edycje strony",
        "rcfilters-filter-pageedits-description": "Edycje treści, stron dyskusji, opisów kategorii...",
        "rcfilters-filter-newpages-label": "Tworzenie stron",
-       "rcfilters-filter-newpages-description": "Zmiany, prowadzące do utworzenia nowych stron.",
+       "rcfilters-filter-newpages-description": "Zmiany prowadzące do utworzenia nowych stron.",
        "rcfilters-filter-categorization-label": "Zmiany kategorii",
        "rcfilters-filter-categorization-description": "Dodanie lub usunięcie strony z kategorii",
        "rcfilters-filter-logactions-label": "Działania rejestrowane",
index ea280ab..6b33b27 100644 (file)
                        "Chaduvari",
                        "MarcoAurelio",
                        "Joao Xavier",
-                       "Winstonyin"
+                       "Winstonyin",
+                       "Jhertel"
                ]
        },
        "sidebar": "{{notranslate}}",
        "search": "{{doc-special|Search}}\nNoun. Text of menu section shown on every page of the wiki above the search form.\n\nSee also:\n* {{msg-mw|Search}}\n* {{msg-mw|Accesskey-search}}\n* {{msg-mw|Tooltip-search}}\n{{Identical|Search}}",
        "search-ignored-headings": "Headings that will be ignored by search. You can translate the text, including \"Leave this line exactly as it is\". Some lines of this messages have one (1) leading space.",
        "searchbutton": "The button you can see in the sidebar, below the search input box. The \"Go\" button is {{msg-mw|Searcharticle}}.\n{{Identical|Search}}",
-       "go": "See also:\n* {{msg-mw|Go}}\n* {{msg-mw|Accesskey-search-go}}\n* {{msg-mw|Tooltip-search-go}}\n{{Identical|Go}}",
+       "go": "Appears next to the search button. Goes directly to the page with that name, if it exists.\n\nSee also:\n* {{msg-mw|Go}}\n* {{msg-mw|Accesskey-search-go}}\n* {{msg-mw|Tooltip-search-go}}\n{{Identical|Go}}",
        "searcharticle": "Button description in the search menu displayed on every page. The \"Search\" button is {{msg-mw|Searchbutton}}.\n{{Identical|Go}}",
        "history": "{{Identical|Page history}}",
        "history_short": "Text used on the history tab.\n\n{{Identical|History}}",
        "databaseerror-query": "Identifies, in the list of technical details, the [[wikipedia:SQL|SQL]] statement that failed.\nParameters:\n* $1 - SQL statement (shown within a box)\n{{Identical|Query}}",
        "databaseerror-function": "Identifies, in the list of technical details, the function that tried to execute the database query.\nParameters:\n* $1 - Name of function\n{{Identical|Function}}",
        "databaseerror-error": "Identifies, in the list of technical details, the error message the database server returned.\nParameters:\n* $1 - Error message from the database server, probably in English\n{{Identical|Error}}",
-       "transaction-duration-limit-exceeded": "Plain text error shown when DB updates take too long. Parameters:\n* $1 - time spent in database updates\n* $2 - maximum time allowed in database updates",
+       "transaction-duration-limit-exceeded": "Plain text error shown when DB updates take too long. Parameters:\n* $1 - time spent in database updates\n* $2 - maximum number of seconds allowed in database updates",
        "laggedslavemode": "Used as warning when getting the timestamp of the latest version, if in LaggedSlaveMode.",
        "readonly": "Used as title of error message when database is locked.",
        "enterlockreason": "For developers when locking the database",
        "userlogin-yourpassword": "In login & create account forms, label for password field.\n\nSee examples: [[Special:UserLogin]] and [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]\n{{Identical|Password}}",
        "userlogin-yourpassword-ph": "Placeholder text in login form for password field.\n\nSee examples: [[Special:UserLogin]] and [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]\n{{Identical|Enter password}}",
        "createacct-yourpassword-ph": "Placeholder text in create account form for password field.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]\n{{Identical|Enter password}}",
-       "yourpasswordagain": "Since 1.22 no longer used in core, but may be used by some extensions. DEPRECATED",
+       "yourpasswordagain": "Label for field to re-enter password.",
        "createacct-yourpasswordagain": "In create account form, label for field to re-enter password\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]\n{{Identical|Confirm password}}",
        "createacct-yourpasswordagain-ph": "Placeholder text in create account form for re-enter password field.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]",
        "userlogin-remembermypassword": "The text for a check box in [[Special:UserLogin]].",
        "rcfilters-filterlist-feedbacklink": "Caption for the link to the feedback page about the filters beta feature.",
        "rcfilters-highlightbutton-title": "Title for the highlight button used to toggle the highlight feature on and off.",
        "rcfilters-highlightmenu-title": "Title for the highlight menu used to select the highlight color for an individual filter.",
+       "rcfilters-highlightmenu-help": "Tooltip for the highlight menu for individual filters.",
        "rcfilters-filterlist-noresults": "Message showing no results found for searching a filter.",
        "rcfilters-filtergroup-registration": "Title for the filter group for editor registration type.",
        "rcfilters-filter-registered-label": "Label for the filter for showing edits made by logged-in users.\n{{Identical|Registered}}",
index 32725d3..ac07205 100644 (file)
        "missing-revision": "Версия $1 страницы «{{FULLPAGENAME}}» не существует.\n\nЭто обычно бывает, если последовать по устаревшей ссылке на страницу, которая была удалена.\nПодробности могут быть в [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале удалений].",
        "userpage-userdoesnotexist": "Учётной записи «<nowiki>$1</nowiki>» не существует. Убедитесь, что вы действительно желаете создать или изменить эту страницу.",
        "userpage-userdoesnotexist-view": "Не зарегистрировано учётной записи «$1».",
-       "blocked-notice-logextract": "Этот участник в данный момент заблокирован.\nНиже приведена последняя запись из журнала блокировок:",
+       "blocked-notice-logextract": "{{GENDER:$1|Этот участник|Эта участница}} в данный момент {{GENDER:$1|заблокирован|заблокирована}}.\nНиже приведена последняя запись из журнала блокировок:",
        "clearyourcache": "<strong>Замечание.</strong> Возможно, после сохранения вам придётся очистить кэш своего браузера, чтобы увидеть изменения.\n* <strong>Firefox / Safari:</strong> Удерживая клавишу <em>Shift</em>, нажмите на панели инструментов <em>Обновить</em> либо нажмите <em>Ctrl-F5</em> или <em>Ctrl-R</em> (<em>⌘-R</em> на Mac)\n* <strong>Google Chrome:</strong> Нажмите <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> на Mac)\n* <strong>Internet Explorer:</strong> Удерживая <em>Ctrl</em>, нажмите <em>Обновить</em> либо нажмите <em>Ctrl-F5</em>\n* <strong>Opera:</strong> Перейдите в <em>Menu → Настройки</em> (<em>Opera → Настройки</em> на Mac), а затем <em>Безопасность → Очистить историю посещений → Кэшированные изображения и файлы</em>",
        "usercssyoucanpreview": "'''Подсказка.''' Нажмите кнопку «{{int:showpreview}}», чтобы проверить свой новый CSS-файл перед сохранением.",
        "userjsyoucanpreview": "'''Подсказка.''' Нажмите кнопку «{{int:showpreview}}», чтобы проверить свой новый JS-файл перед сохранением.",
        "saveusergroups": "Сохранить группы {{GENDER:$1|участника|участницы}}",
        "userrights-groupsmember": "Состоит в группах:",
        "userrights-groupsmember-auto": "Неявно состоит в группах:",
-       "userrights-groups-help": "Вы можете изменить группы, в которые входит этот участник.\n* Если около названия группы стоит отметка — участник входит в эту группу.\n* Если отметка не стоит — участник не входит в эту группу.\n* Символ * указывает на то, что вы не сможете удалить участника из группы, если добавите его в неё (или наоборот).\n* Символ # указывает на то, что вы можете только отложить время истечения этой группы, вы не можете перенести его на более ранний срок.",
+       "userrights-groups-help": "Вы можете изменить группы, в которые входит {{GENDER:$1|этот участник|эта участница}}.\n* Если около названия группы стоит отметка — {{GENDER:$1|участник|участница}} входит в эту группу.\n* Если отметка не стоит — {{GENDER:$1|участник|участница}} не входит в эту группу.\n* Символ * указывает на то, что вы не сможете удалить {{GENDER:$1|участника|участницу}} из группы, если добавите {{GENDER:$1|его|её}} в неё (или наоборот).\n* Символ # указывает на то, что вы можете только отложить время истечения этой группы, вы не можете перенести его на более ранний срок.",
        "userrights-reason": "Причина:",
        "userrights-no-interwiki": "У вас нет разрешения изменять права участников в других вики.",
        "userrights-nodatabase": "База данных $1 не существует или расположена не локально.",
        "rcfilters-filterlist-feedbacklink": "Оставить отзыв о новых (бета) фильтрах",
        "rcfilters-highlightbutton-title": "Выделить результаты",
        "rcfilters-highlightmenu-title": "Выберите цвет",
+       "rcfilters-highlightmenu-help": "Выберите цвет, чтобы подсветить это свойство",
        "rcfilters-filterlist-noresults": "Фильтры не найдены",
        "rcfilters-filtergroup-registration": "Регистрация участников",
        "rcfilters-filter-registered-label": "Зарегистрированные",
index 24f7622..7c1f625 100644 (file)
        "feedback-subject": "Предмет:",
        "feedback-submit": "Одослати",
        "feedback-thanks": "Дякуєме! Ваш коментарь быв приданый на сторінку „[$2 $1]“.",
-       "searchsuggest-search": "Глядати",
+       "searchsuggest-search": "Глядати {{SITENAME}}",
        "searchsuggest-containing": "обсягуючій...",
        "api-error-badtoken": "Внутрїшня хыба: планый знак.",
        "api-error-emptypage": "Створїня новых, порожнїх сторінк неслободно.",
index 72acae2..eea4a6a 100644 (file)
@@ -12,7 +12,8 @@
                        "KWiki",
                        "Matma Rex",
                        "Srdjan m",
-                       "Conquistador"
+                       "Conquistador",
+                       "Xð"
                ]
        },
        "tog-underline": "Podvuci linkove:",
        "unprotectthispage": "Promijeni zaštitu za ovu stranicu",
        "newpage": "Nova stranica / Нова страница",
        "talkpage": "Razgovaraj o ovoj stranici - Разговарај о овој страници",
-       "talkpagelinktext": "Razgovor",
+       "talkpagelinktext": "razgovor",
        "specialpage": "Posebna stranica",
        "personaltools": "Lični alati",
        "articlepage": "Vidi stranicu sadržaja",
index c54d38e..13fa595 100644 (file)
        "sp-contributions-search": "දායකත්ව පිළිබඳ ගවේෂණය කරන්න",
        "sp-contributions-username": "පරිශීලක නාමය හෝ IP ලිපිනය:",
        "sp-contributions-toponly": "නවතම අනුවාදයන් වන සංස්කරණයන් පමණක් පෙන්වයි",
+       "sp-contributions-newonly": "පිටු නිර්මාණය කිරීම් වන සංස්කරණ පමණක් පෙන්වන්න",
        "sp-contributions-submit": "සොයන්න",
        "whatlinkshere": "මෙතනට සබැඳෙන්නේ කුමක්ද",
        "whatlinkshere-title": "\"$1\" වෙත සබැ‍ඳෙන පිටු",
index c8103b6..e16e746 100644 (file)
@@ -34,7 +34,8 @@
                        "Mikulas1",
                        "Hromoslav",
                        "Matma Rex",
-                       "LacoR"
+                       "LacoR",
+                       "Xð"
                ]
        },
        "tog-underline": "Podčiarkovať odkazy:",
        "unprotectthispage": "Zmeniť stav ochrany tejto stránky",
        "newpage": "Nová stránka",
        "talkpage": "Diskusia k stránke",
-       "talkpagelinktext": "Diskusia",
+       "talkpagelinktext": "diskusia",
        "specialpage": "Špeciálna stránka",
        "personaltools": "Osobné nástroje",
        "articlepage": "Zobraziť stránku",
        "saveprefs": "Uložiť nastavenia",
        "restoreprefs": "Obnoviť všetky predvolené nastavenia (vo všetkých sekciách)",
        "prefs-editing": "Úpravy",
-       "rows": "Riadky:",
-       "columns": "Stĺpce:",
        "searchresultshead": "Vyhľadávanie",
        "stub-threshold": "Prah formátovania odkazu ako výhonok ($1):",
        "stub-threshold-sample-link": "príklad",
        "feedback-useragent": "Prehliadač:",
        "searchsuggest-search": "Hľadať",
        "searchsuggest-containing": "obsahuje...",
-       "api-error-autoblocked": "Vaše IP adresa bola automaticky zablokovaná, pretože ju používal zablokovaný používateľ.",
-       "api-error-badaccess-groups": "Nemáte oprávnenie nahrávať súbory na tejto wiki.",
        "api-error-badtoken": "Vnútorná chyba: Zlý token.",
-       "api-error-blocked": "Možnosť editovať vám bola zablokovaná.",
-       "api-error-copyuploaddisabled": "Nahrávanie z URL je na tomto serveri zakázané.",
-       "api-error-duplicate": "{{PLURAL:$1|ďalší súbor|ďalšie súbory}} s rovnakým obsahom už na tejto wiki existujú",
-       "api-error-duplicate-archive": "{{PLURAL:$1|ďalší súbor|ďalšie súbory}} s rovnakým obsahom už na tejto wiki existoval, ale {{PLURAL:$1|bol zmazaný|boli zmazané}}.",
-       "api-error-empty-file": "Súbor, ktorý ste poslali bol prázdny.",
        "api-error-emptypage": "Vytváranie nových, prázdnych stránok nie je dovolené.",
-       "api-error-fetchfileerror": "Vnútorná chyba: Niečo pokazilo počas sťahovania súboru.",
-       "api-error-fileexists-forbidden": "Súbor s názvom „$1“ už existuje a nie je možné prepísať ho.",
-       "api-error-fileexists-shared-forbidden": "Súbor s názvom „$1“ už neexistuje v repozitári zdieľaných súborov a nie je možné prepísať ho.",
-       "api-error-file-too-large": "Súbor, ktorý ste poslali bol príliš veľký.",
-       "api-error-filename-tooshort": "Názov súboru je príliš krátky.",
-       "api-error-filetype-banned": "Tento typ súboru je zakázaný.",
-       "api-error-filetype-banned-type": "$1 {{PLURAL:$4|nie je povolený typ súboru|nie sú povolené typy súboru}}. {{PLURAL:$3|Povolený typ súborov je|Povolené typy súborov sú}} $2.",
-       "api-error-filetype-missing": "Súboru chýba prípona.",
-       "api-error-hookaborted": "Zmena, ktorú ste sa pokúsili vykonať bola zrušená prípojným bodom rozšírenia.",
-       "api-error-http": "Vnútorná chyba: Nepodarilo sa pripojiť k serveru.",
-       "api-error-illegal-filename": "Názov súboru nie je povolený.",
-       "api-error-internal-error": "Vnútorná chyba: Niečo pokazilo pri spracovaní súboru, ktorý ste nahrávali na wiki.",
-       "api-error-invalid-file-key": "Vnútorná chyba: Súbor sa nenašiel v dočasnom úložisku.",
-       "api-error-missingparam": "Vnútorná chyba: Chýbajú parametre požiadavky.",
-       "api-error-missingresult": "Vnútorná chyba: Nepodarilo sa určiť, či kopírovanie prebehlo úspešne.",
-       "api-error-mustbeloggedin": "Aby ste mohli nahrávať súbory, musíte sa prihlásiť",
-       "api-error-mustbeposted": "Vnútorná chyba: Požiadavka vyžaduje HTTP POST.",
-       "api-error-noimageinfo": "Nahranie sa podarilo, ale server nám nedal žiadne informácie o súbore.",
-       "api-error-nomodule": "Vnútorná chyba: Nebol nastavený žiaden nahrávací modul.",
-       "api-error-ok-but-empty": "Vnútorná chyba: Žiadna odpoveď zo servera.",
-       "api-error-overwrite": "Prepísanie existujúceho súboru nie je povolené.",
-       "api-error-stashfailed": "Vnútorná chyba: Serveru sa nepodarilo uložiť dočasný súbor.",
        "api-error-publishfailed": "Vnútorná chyba: Serveru sa nepodarilo publikovať dočasný súbor.",
-       "api-error-timeout": "Server neodpovedal v očakávanom čase.",
-       "api-error-unclassified": "Vyskytla sa neznáma chyba.",
-       "api-error-unknown-code": "Neznáma chyba: „$1“",
-       "api-error-unknown-error": "Vnútorná chyba: Niečo pokazilo pri pokuse nahrať váš súbor.",
+       "api-error-stashfailed": "Vnútorná chyba: Serveru sa nepodarilo uložiť dočasný súbor.",
        "api-error-unknown-warning": "Neznáme upozornenie: $1",
        "api-error-unknownerror": "Neznáma chyba: „$1“",
-       "api-error-uploaddisabled": "Nahrávanie je na tejto wiki zakázané.",
-       "api-error-verification-error": "Tento súbor môže byť poškodený, alebo má zlú príponu.",
        "duration-seconds": "$1 {{PLURAL:$1|sekunda|sekundy|sekúnd}}",
        "duration-minutes": "$1 {{PLURAL:$1|minúta|minúty|minút}}",
        "duration-hours": "$1 {{PLURAL:$1|hodina|hodiny|hodín}}",
index ee45af3..7ddeba7 100644 (file)
@@ -13,7 +13,8 @@
                        "Skalcaa",
                        "Janezdrilc",
                        "Matma Rex",
-                       "NegativeTwelveDollars"
+                       "NegativeTwelveDollars",
+                       "Xð"
                ]
        },
        "tog-underline": "Podčrtavanje povezav:",
        "unprotectthispage": "Spremeni zaščito strani",
        "newpage": "Nova stran",
        "talkpage": "Pogovorite se o strani",
-       "talkpagelinktext": "Pogovor",
+       "talkpagelinktext": "pogovor",
        "specialpage": "Posebna stran",
        "personaltools": "Osebna orodja",
        "articlepage": "Prikaže članek",
        "rcfilters-filterlist-feedbacklink": "Podajte povratne informacije o novih (preizkusnih) filtrih",
        "rcfilters-highlightbutton-title": "Označi rezultate",
        "rcfilters-highlightmenu-title": "Izberite barvo",
+       "rcfilters-highlightmenu-help": "Izberite barvo za označitev te lastnosti",
        "rcfilters-filterlist-noresults": "Nismo našli nobenega filtra",
        "rcfilters-filtergroup-registration": "Registracija uporabnika",
        "rcfilters-filter-registered-label": "Registriran",
index 2d815cd..9078ecf 100644 (file)
        "htmlform-user-not-exists": "<strong>$1</strong> не постоји.",
        "htmlform-user-not-valid": "<strong>$1</strong> није исправно корисничко име.",
        "logentry-delete-delete": "$1 је {{GENDER:$2|обрисао|обрисала}} страницу $3",
-       "logentry-delete-delete_redir": "$1 је {{GENDER:$2|обрисао|обрисала}} преусмерење $3 преснимавањем",
+       "logentry-delete-delete_redir": "$1 је {{GENDER:$2|обрисао|обрисала}} преусмерење $3 преписивањем",
        "logentry-delete-restore": "$1 је {{GENDER:$2|вратио|вратила}} страницу $3",
        "logentry-delete-event": "$1 је {{GENDER:$2|променио|променила}} видљивост {{PLURAL:$5|1=догађаја|$5 догађаја}} у дневнику $3: $4",
        "logentry-delete-revision": "$1 је {{GENDER:$2|променио|променила}} видљивост {{PLURAL:$5|1=једне измене|$5 измене|$5 измена}} на страници $3: $4",
        "log-action-filter-managetags-activate": "активирање ознаке",
        "log-action-filter-managetags-deactivate": "деактивирање ознаке",
        "log-action-filter-move-move": "премештање без преснимавања преусмерења",
-       "log-action-filter-move-move_redir": "пÑ\80емеÑ\88Ñ\82аÑ\9aе Ñ\81а Ð¿Ñ\80еÑ\81нимавањем преусмерења",
+       "log-action-filter-move-move_redir": "Ð\9fÑ\80емеÑ\88Ñ\82аÑ\9aе Ñ\81а Ð¿Ñ\80епиÑ\81ивањем преусмерења",
        "log-action-filter-newusers-create": "отворио анониман корисник",
        "log-action-filter-newusers-create2": "отворио регистрован корисник",
        "log-action-filter-newusers-autocreate": "аутоматски отворен",
index 1647da8..d1a3a20 100644 (file)
        "htmlform-user-not-exists": "<strong>$1</strong> ne postoji.",
        "htmlform-user-not-valid": "<strong>$1</strong> nije ispravno korisničko ime.",
        "logentry-delete-delete": "$1 je {{GENDER:$2|obrisao|obrisala}} stranicu $3",
+       "logentry-delete-delete_redir": "$1 je {{GENDER:$2|obrisao|obrisala}} preusmerenje $3 prepisivanjem",
        "logentry-delete-restore": "$1 je {{GENDER:$2|vratio|vratila}} stranicu $3",
        "logentry-delete-event": "$1 je {{GENDER:$2|promenio|promenila}} vidljivost {{PLURAL:$5|1=događaja|$5 događaja}} u dnevniku $3: $4",
        "logentry-delete-revision": "$1 je {{GENDER:$2|promenio|promenila}} vidljivost {{PLURAL:$5|1=jedne izmene|$5 izmene|$5 izmena}} na stranici $3: $4",
        "mw-widgets-titleinput-description-redirect": "preusmerava na $1",
        "randomrootpage": "Slučajna korenska stranica",
        "log-action-filter-all": "Sve",
+       "log-action-filter-move-move_redir": "Premeštanje sa prepisivanjem preusmerenja",
        "log-action-filter-upload-upload": "Novo otpremanje",
        "authmanager-email-label": "Imejl",
        "authmanager-email-help": "Imejl adresa",
index 1b2cb44..a9a2760 100644 (file)
        "feedback-thanks": "Teşekkürler! Görüşleriniz \"[$2 $1]\" sayfasında paylaşılmıştır.",
        "feedback-thanks-title": "Teşekkürler!",
        "feedback-useragent": "Kullanıcı temsilcisi",
-       "searchsuggest-search": "Ara",
+       "searchsuggest-search": "{{SITENAME}} üzerinde ara",
        "searchsuggest-containing": "içeren...",
        "api-error-badtoken": "İç hata: Bozuk anahtar.",
        "api-error-emptypage": "Yeni, boş bir sayfa oluşturmaya izin verilmez.",
index 391162b..34c9087 100644 (file)
        "createacct-benefit-body1": "{{PLURAL:$1|редагування|редагування|редагувань}}",
        "createacct-benefit-body2": "{{PLURAL:$1|сторінка|сторінки|сторінок}}",
        "createacct-benefit-body3": "{{PLURAL:$1|дописувач|дописувачі|дописувачів}} цього місяця",
-       "badretype": "Введені паролі не співпадають.",
+       "badretype": "Введені паролі не збігаються.",
        "usernameinprogress": "Створення облікового запису для цього імені користувача уже виконується.\nБудь ласка, зачекайте.",
        "userexists": "Введене ім'я користувача вже існує.\nБудь ласка оберіть інше ім'я.",
        "loginerror": "Помилка при вході до системи",
        "selfredirect": "<strong>Попередження:</strong> Ви створюєте перенаправлення на цю ж сторінку.\nВи могли вказати невірну цільову сторінку, або ж редагуєте хибну сторінку.\nЯкщо Ви натиснете \"{{int:savearticle}}\" ще раз, перенаправлення буде створено.",
        "missingcommenttext": "Будь ласка, введіть нижче ваше повідомлення.",
        "missingcommentheader": "<strong>Нагадування</strong>: Ви не вказали тему для цього коментаря.\nНатиснувши кнопку «{{int:savearticle}}» ще раз, Ви збережете редагування без заголовка.",
-       "summary-preview": "Ð\9eпиÑ\81 Ð±Ñ\83де:",
-       "subject-preview": "Тема Ð±Ñ\83де:",
+       "summary-preview": "Ð\9fопеÑ\80еднÑ\96й Ð¿ÐµÑ\80еглÑ\8fд Ð¾Ð¿Ð¸Ñ\81Ñ\83 Ñ\80едагÑ\83ваннÑ\8f:",
+       "subject-preview": "Ð\9fопеÑ\80еднÑ\96й Ð¿ÐµÑ\80еглÑ\8fд Ñ\82еми:",
        "previewerrortext": "Сталася помилка при спробі попереднього перегляду Ваших змін.",
        "blockedtitle": "Користувача заблоковано",
        "blockedtext": "<strong>Ваш обліковий запис або IP-адреса заблоковані.</strong>\n\nБлокування виконане адміністратором $1.\nПричина блокування: <em>$2</em>.\n\n* Початок блокування: $8\n* Закінчення блокування: $6\n* Діапазон блокування: $7\n\nВи можете надіслати листа користувачеві $1 або будь-якому іншому [[{{MediaWiki:Grouppage-sysop}}|адміністратору]], щоб обговорити блокування.\n\nЗверніть увагу, що ви не зможете надіслати листа адміністратору, якщо ви не зареєстровані або не підтвердили свою електронну адресу в [[Special:Preferences|особистих налаштуваннях]], а також якщо вам було заборонено надсилати листи при блокуванні.\n\nВаша поточна IP-адреса — $3, ідентифікатор блокування — #$5. Будь ласка, зазначайте ці дані у своїх запитах.",
        "missing-revision": "Версія #$1 сторінки «{{FULLPAGENAME}}» не існує.\n\nІмовірно, Ви перейшли за застарілим посиланням на вилучену сторінку.\nПодробиці можна дізнатися з [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журналу вилучень].",
        "userpage-userdoesnotexist": "Користувач під назвою \"<nowiki>$1</nowiki>\" не зареєстрований. Переконайтеся, що ви хочете створити/редагувати цю сторінку.",
        "userpage-userdoesnotexist-view": "Обліковий запис користувача «$1» не зареєстровано.",
-       "blocked-notice-logextract": "Цей користувач наразі заблокований.\nОстанній запис у журналі блокувань такий:",
+       "blocked-notice-logextract": "{{GENDER:$1|Цей користувач|Ця користувачка}} наразі {{GENDER:$1|заблокований|заблокована}}.\nОстанній запис у журналі блокувань такий:",
        "clearyourcache": "<strong>Увага:</strong> Після збереження слід очистити кеш оглядача, щоб побачити зміни.\n* <strong>Firefox / Safari:</strong> тримайте <em>Shift</em>, коли натискаєте <em>Оновити</em>, або натисніть <em>Ctrl-F5</em> чи <em>Ctrl-Shift-R</em> (<em>⌘-R</em> на Apple Mac)\n* <strong>Google Chrome:</strong> натисніть <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> на Apple Mac)\n* <strong>Internet Explorer:</strong> тримайте <em>Ctrl</em>, коли натискаєте <em>Оновити</em>, або натисніть <em>Ctrl-F5</em>\n* <strong>Opera:</strong> очистіть кеш за допомогою <em>Інструменти → Налаштування</em> (<em>Opera → Побажання</em> на Apple Mac) та перейдіть на <em>Приватність & безпека → очистити дані браузера → кеш</em>",
        "usercssyoucanpreview": "'''Підказка:''' використовуйте кнопку «{{int:showpreview}}», щоб протестувати ваш новий css-файл перед збереженням.",
        "userjsyoucanpreview": "'''Підказка:''' використовуйте кнопку «{{int:showpreview}}», щоб протестувати ваш новий код JavaScript перед збереженням.",
        "saveusergroups": "Зберегти групи {{GENDER:$1|користувачів}}",
        "userrights-groupsmember": "Член груп:",
        "userrights-groupsmember-auto": "Неявний член:",
-       "userrights-groups-help": "Ви можете змінити групи, до яких належить цей користувач:\n* Якщо біля назви групи стоїть позначка, то користувач належить до цієї групи.\n* Якщо позначка не стоїть — користувач не належить до відповідної групи.\n* Зірочка «*» означає, що ви не можете вилучити користувача з групи, якщо додасте його до неї, і навпаки.\n* Ґратка «#» означає, що ви можете зменшити строк членства в групі, але не збільшити.",
+       "userrights-groups-help": "Ви можете змінити групи, до яких належить {{GENDER:$1|цей користувач|ця користувачка}}:\n* Якщо біля назви групи стоїть позначка, то {{GENDER:$1|користувач|користувачка}} належить до цієї групи.\n* Якщо позначка не стоїть — {{GENDER:$1|користувач|користувачка}} не належить до відповідної групи.\n* Зірочка «*» означає, що Ви не можете вилучити {{GENDER:$1|користувача|користувачку}} з групи, якщо додасте {{GENDER:$1|його|її}} до неї, і навпаки.\n* Решітка «#» означає, що Ви можете зменшити строк членства в групі, але не збільшити.",
        "userrights-reason": "Причина:",
        "userrights-no-interwiki": "У вас нема дозволу змінювати права користувачів на інших вікі.",
        "userrights-nodatabase": "База даних $1 не існує або не є локальною.",
        "rcfilters-invalid-filter": "Недійсний фільтр",
        "rcfilters-empty-filter": "Без фільтрів. Показано всі зміни.",
        "rcfilters-filterlist-title": "Фільтри",
+       "rcfilters-filterlist-feedbacklink": "Надайте відгук про нові (бета) фільтри",
+       "rcfilters-highlightbutton-title": "Виділити результати",
+       "rcfilters-highlightmenu-title": "Вибрати колір",
+       "rcfilters-highlightmenu-help": "Вибрати колір, щоб виділити цю властивість",
        "rcfilters-filterlist-noresults": "Фільтри не знайдено",
        "rcfilters-filtergroup-registration": "Реєстрація користувача",
        "rcfilters-filter-registered-label": "Зареєстровані",
        "editcomment": "Пояснення редагування було: «<em>$1</em>.».",
        "revertpage": "Відкинуто редагування [[Special:Contributions/$2|$2]] ([[User talk:$2|обговорення]]) до зробленого [[User:$1|$1]]",
        "revertpage-nouser": "Відкинуто редагування прихованого користувача до останньої версії, зробленої {{GENDER:$1|[[User:$1|$1]]}}",
-       "rollback-success": "Відкинуті редагування користувача $1; повернення до версії користувача $2.",
+       "rollback-success": "Відкинуті редагування {{GENDER:$3|користувача|користувачки}} $1; повернення до версії {{GENDER:$4|користувача|користувачки}} $2.",
        "rollback-success-notify": "Відкинуті редагування користувача $1; \nповернено до останньої версії користувача $2. [$3 Показати зміни]",
        "sessionfailure-title": "Помилка сеансу",
        "sessionfailure": "Здається, виникли проблеми з поточним сеансом роботи;\nця дія була скасована з метою попередити «захоплення сеансу».\nБудь ласка, натисніть кнопку «Назад» і перезавантажте сторінку, з якої ви прийшли.",
        "delete_and_move_text": "Сторінка з назвою [[:$1|«$1»]] вже існує.\nБажаєте вилучити її для можливості перейменування?",
        "delete_and_move_confirm": "Так, вилучити для перейменування",
        "delete_and_move_reason": "Вилучена для можливості перейменування сторінки «[[$1]]»",
-       "selfmove": "Неможливо перейменувати сторінку: поточна й нова назви сторінки співпадають.",
+       "selfmove": "Неможливо перейменувати сторінку: поточна й нова назви сторінки збігаються.",
        "immobile-source-namespace": "Не можна перейменовувати сторінки з простору назв «$1»",
        "immobile-target-namespace": "Не можна перейменовувати сторінки до простору назв «$1»",
        "immobile-target-namespace-iw": "Інтервікі-посилання не підходить для перейменування сторінки.",
index baf1f10..19f211a 100644 (file)
        "rightslogtext": "呢個係用戶權力嘅修改日誌。",
        "action-read": "讀呢版",
        "action-edit": "編輯呢版",
-       "action-createpage": "開版",
+       "action-createpage": "建立呢頁",
        "action-createtalk": "開討論版",
        "action-createaccount": "開呢個用戶戶口",
        "action-history": "睇呢頁嘅歷史",
        "action-suppressionlog": "睇呢個私有日誌",
        "action-block": "封鎖呢位用戶嘅編輯",
        "action-protect": "改呢版嘅保護等級",
+       "action-rollback": "速還原某版最後修改人之修改",
        "action-import": "由其它wiki度倒入版",
        "action-importupload": "由檔案上載度倒入版",
        "action-patrol": "標示其它嘅編輯做已巡查嘅",
        "allpagesbadtitle": "提供嘅頁面名無效,又或者有一個跨語言或跨wiki嘅字頭。佢可能包括一個或多個字係唔可以用響標題度嘅。",
        "allpages-bad-ns": "{{SITENAME}}係無一個空間名叫做\"$1\"。",
        "allpages-hide-redirects": "收埋跳轉",
+       "cachedspecial-viewing-cached-ttl": "以下資料,來自暫存,上次更新係$1之前嘅事。",
        "cachedspecial-refresh-now": "睇最新。",
        "categories": "類",
        "categoriespagetext": "下面嘅{{PLURAL:$1|類}}有版或媒體。\n[[Special:UnusedCategories|未用類]]唔會響呢度列示。\n請同時參閱[[Special:WantedCategories|需要嘅分類]]。",
index 0dfae47..b1227d2 100644 (file)
        "resettokens": "重置密钥",
        "resettokens-text": "您可以在这里重置允许访问与您的账户有关的特定私人数据的密钥。\n\n如果您意外将它们分享给他人,或是您的账户已经被入侵,您应该重置它们。",
        "resettokens-no-tokens": "没有可以重置的密钥。",
-       "resettokens-tokens": "密钥:",
+       "resettokens-tokens": "令牌:",
        "resettokens-token-label": "$1(当前值:$2)",
        "resettokens-watchlist-token": "[[Special:Watchlist|对你的监视列表中的页面的更改]]的网页feed(Atom/RSS)的密钥",
        "resettokens-done": "密钥已重置。",
        "prefs-advancedwatchlist": "高级选项",
        "prefs-displayrc": "显示",
        "prefs-displaywatchlist": "显示",
-       "prefs-tokenwatchlist": "密钥",
+       "prefs-tokenwatchlist": "令牌",
        "prefs-diffs": "差异对比",
        "prefs-help-prefershttps": "该设置将在下次登录时生效。",
        "prefswarning-warning": "您对您的参数设置的更改尚未保存。如果您不点击“$1”就离开,您的设置就不会更新。",
        "rcfilters-filterlist-feedbacklink": "在新(测试版)过滤器中提供反馈",
        "rcfilters-highlightbutton-title": "高亮结果",
        "rcfilters-highlightmenu-title": "选择颜色",
+       "rcfilters-highlightmenu-help": "选择颜色来高亮该属性",
        "rcfilters-filterlist-noresults": "找不到过滤器",
        "rcfilters-filtergroup-registration": "用户注册",
        "rcfilters-filter-registered-label": "已注册",
index a511365..ac9b60e 100644 (file)
@@ -81,7 +81,8 @@
                        "Knch903",
                        "Winstonyin",
                        "Wmr",
-                       "烈羽"
+                       "烈羽",
+                       "和平奮鬥救地球"
                ]
        },
        "tog-underline": "底線標示連結:",
        "rcfilters-search-placeholder": "過濾最近變更(瀏覽或開始輸入)",
        "rcfilters-invalid-filter": "過濾規則無效",
        "rcfilters-filterlist-title": "篩選器",
+       "rcfilters-filterlist-feedbacklink": "在新(測試版)過濾器中提供反饋",
        "rcfilters-highlightmenu-title": "選擇顏色",
        "rcfilters-filterlist-noresults": "找不到過濾規則",
+       "rcfilters-filter-registered-label": "已註冊",
+       "rcfilters-filter-unregistered-label": "未註冊",
        "rcfilters-filtergroup-authorship": "編輯者",
        "rcfilters-filter-editsbyself-label": "您自己的編輯",
        "rcfilters-filter-editsbyself-description": "您的編輯。",
index fcbdac4..65ce3d8 100644 (file)
@@ -37,3 +37,11 @@ $namespaceNames = [
        NS_CATEGORY         => 'گٹھ',
        NS_CATEGORY_TALK    => 'گٹھ_گل_بات',
 ];
+
+$namespaceAliases = [
+       'تصویر' => NS_FILE,
+];
+
+$magicWords = [
+       'redirect' => [ '0', '#مڑجوڑ', '#REDIRECT' ],
+];
index ea97337..0c3d27d 100644 (file)
@@ -1360,9 +1360,10 @@ return [
        'mediawiki.user' => [
                'scripts' => 'resources/src/mediawiki/mediawiki.user.js',
                'dependencies' => [
-                       'mediawiki.cookie',
                        'mediawiki.api',
                        'mediawiki.api.user',
+                       'mediawiki.cookie',
+                       'mediawiki.storage',
                        'user.options',
                        'user.tokens',
                ],
@@ -1433,7 +1434,7 @@ return [
                'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css',
                'dependencies' => [
                        'jquery.makeCollapsible',
-                       'mediawiki.cookie',
+                       'mediawiki.storage',
                        'mediawiki.icon',
                ],
        ],
@@ -1786,7 +1787,6 @@ return [
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.variables.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterGroupWidget.less',
@@ -1845,6 +1845,7 @@ return [
                        'rcfilters-filter-logactions-description',
                        'rcfilters-highlightbutton-title',
                        'rcfilters-highlightmenu-title',
+                       'rcfilters-highlightmenu-help',
                        'recentchanges-noresult',
                ],
                'dependencies' => [
@@ -1852,6 +1853,7 @@ return [
                        'mediawiki.rcfilters.filters.dm',
                        'oojs-ui.styles.icons-moderation',
                        'oojs-ui.styles.icons-editing-core',
+                       'oojs-ui.styles.icons-editing-styling',
                        'oojs-ui.styles.icons-interactions',
                ],
        ],
@@ -1929,7 +1931,10 @@ return [
        'mediawiki.special.block' => [
                'scripts' => 'resources/src/mediawiki.special/mediawiki.special.block.js',
                'styles' => 'resources/src/mediawiki.special/mediawiki.special.block.css',
-               'dependencies' => 'mediawiki.util',
+               'dependencies' => [
+                       'mediawiki.util',
+                       'mediawiki.htmlform',
+               ],
        ],
        'mediawiki.special.changeslist' => [
                'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.css',
index 541462f..53ce966 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:40Z
+ * Date: 2017-03-07T22:57:01Z
  */
 ( function ( OO ) {
 
index 0818782..369cf09 100644 (file)
@@ -1,18 +1,15 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:44Z
+ * Date: 2017-03-07T22:57:06Z
  */
-/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */
-/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */
 .oo-ui-element-hidden {
   display: none !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .oo-ui-buttonElement {
   display: inline-block;
@@ -667,9 +664,49 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
 .oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
   display: block;
   position: absolute;
-  /* `top` property is to be set in theme's selector due to specific `@size-anchor` values */
   background-repeat: no-repeat;
 }
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
+  content: '';
+  position: absolute;
+  width: 0;
+  height: 0;
+  border-style: solid;
+  border-color: transparent;
+}
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor {
+  left: 0;
+  /* `top` property is to be set in theme's selector due to specific `@size-anchor` values */
+}
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:after {
+  border-top: 0;
+}
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor {
+  left: 0;
+  /* `bottom` property is to be set in theme's selector due to specific `@size-anchor` values */
+}
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:after {
+  border-bottom: 0;
+}
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor {
+  top: 0;
+  /* `left` property is to be set in theme's selector due to specific `@size-anchor` values */
+}
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:after {
+  border-left: 0;
+}
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor {
+  top: 0;
+  /* `right` property is to be set in theme's selector due to specific `@size-anchor` values */
+}
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:after {
+  border-right: 0;
+}
 .oo-ui-popupWidget-head {
   -webkit-touch-callout: none;
   -webkit-user-select: none;
@@ -696,34 +733,78 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
   border-radius: 0.25em;
   box-shadow: 0 0.15em 0.5em 0 rgba(0, 0, 0, 0.2);
 }
-.oo-ui-popupWidget-anchored {
+.oo-ui-popupWidget-anchored-top {
   margin-top: 6px;
 }
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor {
   top: -6px;
 }
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before,
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
-  content: '';
-  position: absolute;
-  width: 0;
-  height: 0;
-  border-style: solid;
-  border-color: transparent;
-  border-top: 0;
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before {
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:before {
   bottom: -7px;
   left: -6px;
   border-bottom-color: #aaa;
   border-width: 7px;
 }
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:after {
   bottom: -7px;
   left: -5px;
   border-bottom-color: #fff;
   border-width: 6px;
 }
+.oo-ui-popupWidget-anchored-bottom {
+  margin-bottom: 6px;
+}
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor {
+  bottom: -6px;
+}
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:before {
+  top: -7px;
+  left: -6px;
+  border-top-color: #aaa;
+  border-width: 7px;
+}
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:after {
+  top: -7px;
+  left: -5px;
+  border-top-color: #fff;
+  border-width: 6px;
+}
+.oo-ui-popupWidget-anchored-start {
+  margin-left: 6px;
+}
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor {
+  left: -6px;
+}
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:before {
+  right: -7px;
+  top: -6px;
+  border-right-color: #aaa;
+  border-width: 7px;
+}
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:after {
+  right: -7px;
+  top: -5px;
+  border-right-color: #fff;
+  border-width: 6px;
+}
+.oo-ui-popupWidget-anchored-end {
+  margin-right: 6px;
+}
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor {
+  right: -6px;
+}
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:before {
+  left: -7px;
+  top: -6px;
+  border-left-color: #aaa;
+  border-width: 7px;
+}
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:after {
+  left: -7px;
+  top: -5px;
+  border-left-color: #fff;
+  border-width: 6px;
+}
 .oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup {
   -webkit-transition: width 100ms ease, height 100ms ease, left 100ms ease;
      -moz-transition: width 100ms ease, height 100ms ease, left 100ms ease;
@@ -750,10 +831,12 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
 .oo-ui-popupButtonWidget .oo-ui-popupWidget {
   cursor: auto;
 }
-.oo-ui-popupWidget.oo-ui-popupButtonWidget-frameless-popup {
+.oo-ui-popupButtonWidget-frameless-popup.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor,
+.oo-ui-popupButtonWidget-frameless-popup.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor {
   margin-left: 0.9375em;
 }
-.oo-ui-popupWidget.oo-ui-popupButtonWidget-framed-popup {
+.oo-ui-popupButtonWidget-framed-popup.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor,
+.oo-ui-popupButtonWidget-framed-popup.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor {
   margin-left: 1.2375em;
 }
 .oo-ui-inputWidget {
@@ -931,8 +1014,6 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
   -webkit-transition: border-color 250ms ease, box-shadow 250ms ease;
      -moz-transition: border-color 250ms ease, box-shadow 250ms ease;
           transition: border-color 250ms ease, box-shadow 250ms ease;
-  /* stylelint-disable indentation */
-  /* stylelint-enable indentation */
 }
 .oo-ui-textInputWidget input.oo-ui-pendingElement-pending,
 .oo-ui-textInputWidget textarea.oo-ui-pendingElement-pending {
index f468d17..b041ef4 100644 (file)
@@ -1,18 +1,15 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:44Z
+ * Date: 2017-03-07T22:57:06Z
  */
-/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */
-/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */
 .oo-ui-element-hidden {
   display: none !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .oo-ui-buttonElement {
   display: inline-block;
@@ -826,9 +823,49 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
 .oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
   display: block;
   position: absolute;
-  /* `top` property is to be set in theme's selector due to specific `@size-anchor` values */
   background-repeat: no-repeat;
 }
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
+  content: '';
+  position: absolute;
+  width: 0;
+  height: 0;
+  border-style: solid;
+  border-color: transparent;
+}
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor {
+  left: 0;
+  /* `top` property is to be set in theme's selector due to specific `@size-anchor` values */
+}
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:after {
+  border-top: 0;
+}
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor {
+  left: 0;
+  /* `bottom` property is to be set in theme's selector due to specific `@size-anchor` values */
+}
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:after {
+  border-bottom: 0;
+}
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor {
+  top: 0;
+  /* `left` property is to be set in theme's selector due to specific `@size-anchor` values */
+}
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:after {
+  border-left: 0;
+}
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor {
+  top: 0;
+  /* `right` property is to be set in theme's selector due to specific `@size-anchor` values */
+}
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:before,
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:after {
+  border-right: 0;
+}
 .oo-ui-popupWidget-head {
   -webkit-touch-callout: none;
   -webkit-user-select: none;
@@ -855,34 +892,78 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
   border-radius: 2px;
   box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.25);
 }
-.oo-ui-popupWidget-anchored {
+.oo-ui-popupWidget-anchored-top {
   margin-top: 9px;
 }
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor {
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor {
   top: -9px;
 }
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before,
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
-  content: '';
-  position: absolute;
-  width: 0;
-  height: 0;
-  border-style: solid;
-  border-color: transparent;
-  border-top: 0;
-}
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before {
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:before {
   bottom: -10px;
   left: -9px;
   border-bottom-color: #a2a9b1;
   border-width: 10px;
 }
-.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after {
+.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:after {
   bottom: -10px;
   left: -8px;
   border-bottom-color: #fff;
   border-width: 9px;
 }
+.oo-ui-popupWidget-anchored-bottom {
+  margin-bottom: 9px;
+}
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor {
+  bottom: -9px;
+}
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:before {
+  top: -10px;
+  left: -9px;
+  border-top-color: #a2a9b1;
+  border-width: 10px;
+}
+.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:after {
+  top: -10px;
+  left: -8px;
+  border-top-color: #fff;
+  border-width: 9px;
+}
+.oo-ui-popupWidget-anchored-start {
+  margin-left: 9px;
+}
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor {
+  left: -9px;
+}
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:before {
+  right: -10px;
+  top: -9px;
+  border-right-color: #a2a9b1;
+  border-width: 10px;
+}
+.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:after {
+  right: -10px;
+  top: -8px;
+  border-right-color: #fff;
+  border-width: 9px;
+}
+.oo-ui-popupWidget-anchored-end {
+  margin-right: 9px;
+}
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor {
+  right: -9px;
+}
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:before {
+  left: -10px;
+  top: -9px;
+  border-left-color: #a2a9b1;
+  border-width: 10px;
+}
+.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:after {
+  left: -10px;
+  top: -8px;
+  border-left-color: #fff;
+  border-width: 9px;
+}
 .oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup {
   -webkit-transition: width 100ms, height 100ms, left 100ms;
      -moz-transition: width 100ms, height 100ms, left 100ms;
@@ -909,10 +990,12 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
 .oo-ui-popupButtonWidget .oo-ui-popupWidget {
   cursor: auto;
 }
-.oo-ui-popupWidget.oo-ui-popupButtonWidget-frameless-popup {
+.oo-ui-popupButtonWidget-frameless-popup.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor,
+.oo-ui-popupButtonWidget-frameless-popup.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor {
   margin-left: 0.9375em;
 }
-.oo-ui-popupWidget.oo-ui-popupButtonWidget-framed-popup {
+.oo-ui-popupButtonWidget-framed-popup.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor,
+.oo-ui-popupButtonWidget-framed-popup.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor {
   margin-left: 1.5em;
 }
 .oo-ui-inputWidget {
@@ -1343,8 +1426,6 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout {
   -webkit-transition: border-color 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1);
      -moz-transition: border-color 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1);
           transition: border-color 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1);
-  /* stylelint-disable indentation */
-  /* stylelint-enable indentation */
 }
 .oo-ui-textInputWidget.oo-ui-widget-enabled input:hover,
 .oo-ui-textInputWidget.oo-ui-widget-enabled textarea:hover {
index 9eb8716..f10bdfa 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:40Z
+ * Date: 2017-03-07T22:57:01Z
  */
 ( function ( OO ) {
 
@@ -4079,6 +4079,8 @@ OO.ui.mixin.PendingElement.prototype.popPending = function () {
  *  'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
  *  'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
  *  'center': Horizontally align the center with $floatableContainer's center
+ * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
+ *  is out of view
  */
 OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
        // Configuration initialization
@@ -4097,6 +4099,7 @@ OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
        this.setFloatableElement( config.$floatable || this.$element );
        this.setVerticalPosition( config.verticalPosition || 'below' );
        this.setHorizontalPosition( config.horizontalPosition || 'start' );
+       this.hideWhenOutOfView = config.hideWhenOutOfView === undefined ? true : !!config.hideWhenOutOfView;
 };
 
 /* Methods */
@@ -4141,9 +4144,11 @@ OO.ui.mixin.FloatableElement.prototype.setVerticalPosition = function ( position
        if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position ) === -1 ) {
                throw new Error( 'Invalid value for vertical position: ' + position );
        }
-       this.verticalPosition = position;
-       if ( this.$floatable ) {
-               this.position();
+       if ( this.verticalPosition !== position ) {
+               this.verticalPosition = position;
+               if ( this.$floatable ) {
+                       this.position();
+               }
        }
 };
 
@@ -4156,9 +4161,11 @@ OO.ui.mixin.FloatableElement.prototype.setHorizontalPosition = function ( positi
        if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position ) === -1 ) {
                throw new Error( 'Invalid value for horizontal position: ' + position );
        }
-       this.horizontalPosition = position;
-       if ( this.$floatable ) {
-               this.position();
+       if ( this.horizontalPosition !== position ) {
+               this.horizontalPosition = position;
+               if ( this.$floatable ) {
+                       this.position();
+               }
        }
 };
 
@@ -4290,27 +4297,49 @@ OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element
  * @chainable
  */
 OO.ui.mixin.FloatableElement.prototype.position = function () {
-       var containerPos, direction, $offsetParent, isBody, scrollableX, scrollableY,
-               horizScrollbarHeight, vertScrollbarWidth, scrollTop, scrollLeft,
-               newPos = { top: '', left: '', bottom: '', right: '' };
-
        if ( !this.positioning ) {
                return this;
        }
 
-       if ( !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
+       if ( this.hideWhenOutOfView && !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
                this.$floatable.addClass( 'oo-ui-element-hidden' );
-               return;
+               return this;
        } else {
                this.$floatable.removeClass( 'oo-ui-element-hidden' );
        }
 
        if ( !this.needsCustomPosition ) {
-               return;
+               return this;
        }
 
-       direction = this.$floatableContainer.css( 'direction' );
-       $offsetParent = this.$floatable.offsetParent();
+       this.$floatable.css( this.computePosition() );
+
+       // We updated the position, so re-evaluate the clipping state.
+       // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
+       // will not notice the need to update itself.)
+       // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
+       // it not listen to the right events in the right places?
+       if ( this.clip ) {
+               this.clip();
+       }
+
+       return this;
+};
+
+/**
+ * Compute how #$floatable should be positioned based on the position of #$floatableContainer
+ * and the positioning settings. This is a helper for #position that shouldn't be called directly,
+ * but may be overridden by subclasses if they want to change or add to the positioning logic.
+ *
+ * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
+ */
+OO.ui.mixin.FloatableElement.prototype.computePosition = function () {
+       var isBody, scrollableX, scrollableY, containerPos,
+               horizScrollbarHeight, vertScrollbarWidth, scrollTop, scrollLeft,
+               newPos = { top: '', left: '', bottom: '', right: '' },
+               direction = this.$floatableContainer.css( 'direction' ),
+               $offsetParent = this.$floatable.offsetParent();
+
        if ( $offsetParent.is( 'html' ) ) {
                // The innerHeight/Width and clientHeight/Width calculations don't work well on the
                // <html> element, but they do work on the <body>
@@ -4408,18 +4437,7 @@ OO.ui.mixin.FloatableElement.prototype.position = function () {
                }
        }
 
-       this.$floatable.css( newPos );
-
-       // We updated the position, so re-evaluate the clipping state.
-       // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
-       // will not notice the need to update itself.)
-       // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
-       // it not listen to the right events in the right places?
-       if ( this.clip ) {
-               this.clip();
-       }
-
-       return this;
+       return newPos;
 };
 
 /**
@@ -4745,13 +4763,24 @@ OO.ui.mixin.ClippableElement.prototype.clip = function () {
  * @cfg {number} [width=320] Width of popup in pixels
  * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
  * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
- * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
- *  If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
- *  popup is leaning towards the right of the screen.
- *  Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
- *  in the given language, which means it will flip to the correct positioning in right-to-left languages.
- *  Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
- *  sentence in the given language.
+ * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
+ *  'above': Put popup above $floatableContainer; anchor points down to the start edge of $floatableContainer
+ *  'below': Put popup below $floatableContainer; anchor points up to the start edge of $floatableContainer
+ *  'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
+ *            endwards (right/left) to the vertical center of $floatableContainer
+ *  'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
+ *            startwards (left/right) to the vertical center of $floatableContainer
+ * @cfg {string} [align='center'] How to align the popup to $floatableContainer
+ *  'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
+ *              as possible while still keeping the anchor within the popup;
+ *              if position is before/after, move the popup as far downwards as possible.
+ *  'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
+ *               as possible while still keeping the anchor within the popup;
+ *               if position in before/after, move the popup as far upwards as possible.
+ *  'center': Horizontally (if position is above/below) or vertically (before/after) align the center
+ *            of the popup with the center of $floatableContainer.
+ * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
+ * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
  * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
  *  See the [OOjs UI docs on MediaWiki][3] for an example.
  *  [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
@@ -4794,15 +4823,16 @@ OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
        this.autoClose = !!config.autoClose;
        this.$autoCloseIgnore = config.$autoCloseIgnore;
        this.transitionTimeout = null;
-       this.anchor = null;
+       this.anchored = false;
        this.width = config.width !== undefined ? config.width : 320;
        this.height = config.height !== undefined ? config.height : null;
-       this.setAlignment( config.align );
        this.onMouseDownHandler = this.onMouseDown.bind( this );
        this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
 
        // Initialization
        this.toggleAnchor( config.anchor === undefined || config.anchor );
+       this.setAlignment( config.align || 'center' );
+       this.setPosition( config.position || 'below' );
        this.$body.addClass( 'oo-ui-popupWidget-body' );
        this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
        this.$popup
@@ -4950,6 +4980,21 @@ OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
                this.anchored = show;
        }
 };
+/**
+ * Change which edge the anchor appears on.
+ *
+ * @param {string} edge 'top', 'bottom', 'start' or 'end'
+ */
+OO.ui.PopupWidget.prototype.setAnchorEdge = function ( edge ) {
+       if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge ) === -1 ) {
+               throw new Error( 'Invalid value for edge: ' + edge );
+       }
+       if ( this.anchorEdge !== null ) {
+               this.$element.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge );
+       }
+       this.anchorEdge = edge;
+       this.$element.addClass( 'oo-ui-popupWidget-anchored-' + edge );
+};
 
 /**
  * Check if the anchor is visible.
@@ -4957,7 +5002,7 @@ OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
  * @return {boolean} Anchor is visible
  */
 OO.ui.PopupWidget.prototype.hasAnchor = function () {
-       return this.anchor;
+       return this.anchored;
 };
 
 /**
@@ -4981,6 +5026,10 @@ OO.ui.PopupWidget.prototype.toggle = function ( show ) {
                OO.ui.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
                this.warnedUnattached = true;
        }
+       if ( show && !this.$floatableContainer && this.isElementAttached() ) {
+               // Fall back to the parent node if the floatableContainer is not set
+               this.setFloatableContainer( this.$element.parent() );
+       }
 
        // Parent method
        OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
@@ -5035,9 +5084,37 @@ OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
  * @chainable
  */
 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
-       var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
-               popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth, direction,
-               dirFactor, align,
+       var widget = this;
+
+       // Prevent transition from being interrupted
+       clearTimeout( this.transitionTimeout );
+       if ( transition ) {
+               // Enable transition
+               this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
+       }
+
+       this.position();
+
+       if ( transition ) {
+               // Prevent transitioning after transition is complete
+               this.transitionTimeout = setTimeout( function () {
+                       widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
+               }, 200 );
+       } else {
+               // Prevent transitioning immediately
+               this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
+       }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.PopupWidget.prototype.computePosition = function () {
+       var direction, align, vertical, start, end, near, far, sizeProp, popupSize, anchorSize, anchorPos,
+               anchorOffset, anchorMargin, parentPosition, positionProp, positionAdjustment, floatablePos,
+               offsetParentPos, containerPos,
+               popupPos = {},
+               anchorCss = { left: '', right: '', top: '', bottom: '' },
                alignMap = {
                        ltr: {
                                'force-left': 'backwards',
@@ -5048,77 +5125,119 @@ OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
                                'force-right': 'backwards'
                        }
                },
-               widget = this;
+               anchorEdgeMap = {
+                       above: 'bottom',
+                       below: 'top',
+                       before: 'end',
+                       after: 'start'
+               },
+               hPosMap = {
+                       forwards: 'start',
+                       center: 'center',
+                       backwards: 'before'
+               },
+               vPosMap = {
+                       forwards: 'top',
+                       center: 'center',
+                       backwards: 'bottom'
+               };
 
        if ( !this.$container ) {
                // Lazy-initialize $container if not specified in constructor
                this.$container = $( this.getClosestScrollableElementContainer() );
        }
        direction = this.$container.css( 'direction' );
-       dirFactor = direction === 'rtl' ? -1 : 1;
-       align = alignMap[ direction ][ this.align ] || this.align;
 
-       // Set height and width before measuring things, since it might cause our measurements
-       // to change (e.g. due to scrollbars appearing or disappearing)
+       // Set height and width before we do anything else, since it might cause our measurements
+       // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
        this.$popup.css( {
                width: this.width,
                height: this.height !== null ? this.height : 'auto'
        } );
 
-       // Compute initial popupOffset based on alignment
-       popupOffset = this.width * ( { backwards: -1, center: -0.5, forwards: 0 } )[ align ];
-
-       // Figure out if this will cause the popup to go beyond the edge of the container
-       originOffset = this.$element.offset().left;
-       containerLeft = this.$container.offset().left;
-       containerWidth = this.$container.innerWidth();
-       containerRight = containerLeft + containerWidth;
-       popupLeft = dirFactor * popupOffset - this.containerPadding;
-       popupRight = dirFactor * popupOffset + this.containerPadding + this.width + this.containerPadding;
-       overlapLeft = ( originOffset + popupLeft ) - containerLeft;
-       overlapRight = containerRight - ( originOffset + popupRight );
-
-       // Adjust offset to make the popup not go beyond the edge, if needed
-       if ( overlapRight < 0 ) {
-               popupOffset += dirFactor * overlapRight;
-       } else if ( overlapLeft < 0 ) {
-               popupOffset -= dirFactor * overlapLeft;
-       }
+       align = alignMap[ direction ][ this.align ] || this.align;
+       // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
+       vertical = this.popupPosition === 'before' || this.popupPosition === 'after';
+       start = vertical ? 'top' : ( direction === 'rtl' ? 'right' : 'left' );
+       end = vertical ? 'bottom' : ( direction === 'rtl' ? 'left' : 'right' );
+       near = vertical ? 'top' : 'left';
+       far = vertical ? 'bottom' : 'right';
+       sizeProp = vertical ? 'Height' : 'Width';
+       popupSize = vertical ? ( this.height || this.$popup.height() ) : this.width;
+
+       this.setAnchorEdge( anchorEdgeMap[ this.popupPosition ] );
+       this.horizontalPosition = vertical ? this.popupPosition : hPosMap[ align ];
+       this.verticalPosition = vertical ? vPosMap[ align ] : this.popupPosition;
 
-       // Adjust offset to avoid anchor being rendered too close to the edge
-       // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
-       // TODO: Find a measurement that works for CSS anchors and image anchors
-       anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
-       if ( popupOffset + this.width < anchorWidth ) {
-               popupOffset = anchorWidth - this.width;
-       } else if ( -popupOffset < anchorWidth ) {
-               popupOffset = -anchorWidth;
+       // Parent method
+       parentPosition = OO.ui.mixin.FloatableElement.prototype.computePosition.call( this );
+       // Find out which property FloatableElement used for positioning, and adjust that value
+       positionProp = vertical ?
+               ( parentPosition.top !== '' ? 'top' : 'bottom' ) :
+               ( parentPosition.left !== '' ? 'left' : 'right' );
+
+       // Figure out where the near and far edges of the popup and $floatableContainer are
+       floatablePos = this.$floatableContainer.offset();
+       floatablePos[ far ] = floatablePos[ near ] + this.$floatableContainer[ 'outer' + sizeProp ]();
+       // Measure where the offsetParent is and compute our position based on that and parentPosition
+       offsetParentPos = this.$element.offsetParent().offset();
+
+       if ( positionProp === near ) {
+               popupPos[ near ] = offsetParentPos[ near ] + parentPosition[ near ];
+               popupPos[ far ] = popupPos[ near ] + popupSize;
+       } else {
+               popupPos[ far ] = offsetParentPos[ near ] +
+                       this.$element.offsetParent()[ 'inner' + sizeProp ]() - parentPosition[ far ];
+               popupPos[ near ] = popupPos[ far ] - popupSize;
+       }
+
+       // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
+       // For popups above/below, we point to the start edge; for popups before/after, we point to the center
+       anchorPos = vertical ? ( floatablePos[ start ] + floatablePos[ end ] ) / 2 : floatablePos[ start ];
+       anchorOffset = ( start === far ? -1 : 1 ) * ( anchorPos - popupPos[ start ] );
+
+       // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
+       // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
+       anchorSize = this.$anchor[ 0 ][ 'scroll' + sizeProp ];
+       anchorMargin = parseFloat( this.$anchor.css( 'margin-' + start ) );
+       if ( anchorOffset + anchorMargin < 2 * anchorSize ) {
+               // Not enough space for the anchor on the start side; pull the popup startwards
+               positionAdjustment = ( positionProp === start ? -1 : 1 ) *
+                       ( 2 * anchorSize - ( anchorOffset + anchorMargin ) );
+       } else if ( anchorOffset + anchorMargin > popupSize - 2 * anchorSize ) {
+               // Not enough space for the anchor on the end side; pull the popup endwards
+               positionAdjustment = ( positionProp === end ? -1 : 1 ) *
+                       ( anchorOffset + anchorMargin - ( popupSize - 2 * anchorSize ) );
+       } else {
+               positionAdjustment = 0;
        }
 
-       // Prevent transition from being interrupted
-       clearTimeout( this.transitionTimeout );
-       if ( transition ) {
-               // Enable transition
-               this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
+       // Check if the popup will go beyond the edge of this.$container
+       containerPos = this.$container.offset();
+       containerPos[ far ] = containerPos[ near ] + this.$container[ 'inner' + sizeProp ]();
+       // Take into account how much the popup will move because of the adjustments we're going to make
+       popupPos[ near ] += ( positionProp === near ? 1 : -1 ) * positionAdjustment;
+       popupPos[ far ] += ( positionProp === near ? 1 : -1 ) * positionAdjustment;
+       if ( containerPos[ near ] + this.containerPadding > popupPos[ near ] ) {
+               // Popup goes beyond the near (left/top) edge, move it to the right/bottom
+               positionAdjustment += ( positionProp === near ? 1 : -1 ) *
+                       ( containerPos[ near ] + this.containerPadding - popupPos[ near ] );
+       } else if ( containerPos[ far ] - this.containerPadding < popupPos[ far ] ) {
+               // Popup goes beyond the far (right/bottom) edge, move it to the left/top
+               positionAdjustment += ( positionProp === far ? 1 : -1 ) *
+                       ( popupPos[ far ] - ( containerPos[ far ] - this.containerPadding ) );
        }
 
-       // Position body relative to anchor
-       this.$popup.css( direction === 'rtl' ? 'margin-right' : 'margin-left', popupOffset );
-
-       if ( transition ) {
-               // Prevent transitioning after transition is complete
-               this.transitionTimeout = setTimeout( function () {
-                       widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
-               }, 200 );
-       } else {
-               // Prevent transitioning immediately
-               this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
-       }
+       // Adjust anchorOffset for positionAdjustment
+       anchorOffset += ( positionProp === start ? -1 : 1 ) * positionAdjustment;
 
-       // Reevaluate clipping state since we've relocated and resized the popup
-       this.clip();
+       // Position the anchor
+       anchorCss[ start ] = anchorOffset;
+       this.$anchor.css( anchorCss );
+       // Move the popup if needed
+       parentPosition[ positionProp ] += positionAdjustment;
 
-       return this;
+       return parentPosition;
 };
 
 /**
@@ -5140,18 +5259,41 @@ OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
        } else {
                this.align = 'center';
        }
+       this.position();
 };
 
 /**
  * Get popup alignment
  *
- * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
+ * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
  *  `backwards` or `forwards`.
  */
 OO.ui.PopupWidget.prototype.getAlignment = function () {
        return this.align;
 };
 
+/**
+ * Change the positioning of the popup.
+ *
+ * @param {string} position 'above', 'below', 'before' or 'after'
+ */
+OO.ui.PopupWidget.prototype.setPosition = function ( position ) {
+       if ( [ 'above', 'below', 'before', 'after' ].indexOf( position ) === -1 ) {
+               position = 'below';
+       }
+       this.popupPosition = position;
+       this.position();
+};
+
+/**
+ * Get popup positioning.
+ *
+ * @return {string} 'above', 'below', 'before' or 'after'
+ */
+OO.ui.PopupWidget.prototype.getPosition = function () {
+       return this.popupPosition;
+};
+
 /**
  * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
  * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
@@ -5172,9 +5314,14 @@ OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
 
        // Properties
        this.popup = new OO.ui.PopupWidget( $.extend(
-               { autoClose: true },
+               {
+                       autoClose: true,
+                       $floatableContainer: this.$element
+               },
                config.popup,
-               { $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore ) }
+               {
+                       $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore )
+               }
        ) );
 };
 
@@ -5222,11 +5369,7 @@ OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
        OO.ui.PopupButtonWidget.parent.call( this, config );
 
        // Mixin constructors
-       OO.ui.mixin.PopupElement.call( this, $.extend( true, {}, config, {
-               popup: {
-                       $floatableContainer: this.$element
-               }
-       } ) );
+       OO.ui.mixin.PopupElement.call( this, config );
 
        // Properties
        this.$overlay = config.$overlay || this.$element;
@@ -6736,20 +6879,34 @@ OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
  * @protected
  */
 OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
-       var i, item, visible,
+       var i, item, visible, section, sectionEmpty,
                anyVisible = false,
                len = this.items.length,
                showAll = !this.isVisible(),
                filter = showAll ? null : this.getItemMatcher( this.$input.val() );
 
+       // Hide non-matching options, and also hide section headers if all options
+       // in their section are hidden.
        for ( i = 0; i < len; i++ ) {
                item = this.items[ i ];
-               if ( item instanceof OO.ui.OptionWidget ) {
+               if ( item instanceof OO.ui.MenuSectionOptionWidget ) {
+                       if ( section ) {
+                               // If the previous section was empty, hide its header
+                               section.toggle( showAll || !sectionEmpty );
+                       }
+                       section = item;
+                       sectionEmpty = true;
+               } else if ( item instanceof OO.ui.OptionWidget ) {
                        visible = showAll || filter( item );
                        anyVisible = anyVisible || visible;
+                       sectionEmpty = sectionEmpty && !visible;
                        item.toggle( visible );
                }
        }
+       // Process the final section
+       if ( section ) {
+               section.toggle( showAll || !sectionEmpty );
+       }
 
        this.$element.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible );
 
@@ -9125,7 +9282,7 @@ OO.ui.CheckboxMultiselectInputWidget.prototype.setOptions = function ( options )
  *  specifies minimum number of rows to display.
  * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
  *  Use the #maxRows config to specify a maximum number of displayed rows.
- * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
+ * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
  *  Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
  * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
  *  the value or placeholder text: `'before'` or `'after'`
@@ -10081,6 +10238,10 @@ OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) {
  * - by choosing a value from the menu. The value of the chosen option will then appear in the text
  *   input field.
  *
+ * After the user chooses an option, its `data` will be used as a new value for the widget.
+ * A `label` also can be specified for each option: if given, it will be shown instead of the
+ * `data` in the dropdown menu.
+ *
  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
  *
  * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
@@ -10088,32 +10249,33 @@ OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) {
  *     @example
  *     // Example: A ComboBoxInputWidget.
  *     var comboBox = new OO.ui.ComboBoxInputWidget( {
- *         label: 'ComboBoxInputWidget',
  *         value: 'Option 1',
- *         menu: {
- *             items: [
- *                 new OO.ui.MenuOptionWidget( {
- *                     data: 'Option 1',
- *                     label: 'Option One'
- *                 } ),
- *                 new OO.ui.MenuOptionWidget( {
- *                     data: 'Option 2',
- *                     label: 'Option Two'
- *                 } ),
- *                 new OO.ui.MenuOptionWidget( {
- *                     data: 'Option 3',
- *                     label: 'Option Three'
- *                 } ),
- *                 new OO.ui.MenuOptionWidget( {
- *                     data: 'Option 4',
- *                     label: 'Option Four'
- *                 } ),
- *                 new OO.ui.MenuOptionWidget( {
- *                     data: 'Option 5',
- *                     label: 'Option Five'
- *                 } )
- *             ]
- *         }
+ *         options: [
+ *             { data: 'Option 1' },
+ *             { data: 'Option 2' },
+ *             { data: 'Option 3' }
+ *         ]
+ *     } );
+ *     $( 'body' ).append( comboBox.$element );
+ *
+ *     @example
+ *     // Example: A ComboBoxInputWidget with additional option labels.
+ *     var comboBox = new OO.ui.ComboBoxInputWidget( {
+ *         value: 'Option 1',
+ *         options: [
+ *             {
+ *                 data: 'Option 1',
+ *                 label: 'Option One'
+ *             },
+ *             {
+ *                 data: 'Option 2',
+ *                 label: 'Option Two'
+ *             },
+ *             {
+ *                 data: 'Option 3',
+ *                 label: 'Option Three'
+ *             }
+ *         ]
  *     } );
  *     $( 'body' ).append( comboBox.$element );
  *
index b39010c..7b1c099 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:40Z
+ * Date: 2017-03-07T22:57:01Z
  */
 ( function ( OO ) {
 
index 4d7f9d7..7604589 100644 (file)
@@ -1,21 +1,19 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:44Z
+ * Date: 2017-03-07T22:57:06Z
  */
-/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */
-/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */
 .oo-ui-popupTool .oo-ui-popupWidget-popup,
 .oo-ui-popupTool .oo-ui-popupWidget-anchor {
   z-index: 4;
 }
-.oo-ui-popupTool .oo-ui-popupWidget {
-  /* @noflip */
+.oo-ui-popupTool .oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor,
+.oo-ui-popupTool .oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor {
   margin-left: 1.25em;
 }
 .oo-ui-toolGroupTool > .oo-ui-popupToolGroup {
index 59de29b..ad1746d 100644 (file)
@@ -1,15 +1,13 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:44Z
+ * Date: 2017-03-07T22:57:06Z
  */
-/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */
-/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */
 .oo-ui-tool.oo-ui-widget-enabled {
   -webkit-transition: background-color 100ms;
      -moz-transition: background-color 100ms;
@@ -24,8 +22,8 @@
 .oo-ui-popupTool .oo-ui-popupWidget-anchor {
   z-index: 4;
 }
-.oo-ui-popupTool .oo-ui-popupWidget {
-  /* @noflip */
+.oo-ui-popupTool .oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor,
+.oo-ui-popupTool .oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor {
   margin-left: 1.25em;
 }
 .oo-ui-toolGroupTool > .oo-ui-toolGroup {
index 777debf..1574f6c 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:40Z
+ * Date: 2017-03-07T22:57:01Z
  */
 ( function ( OO ) {
 
@@ -1469,6 +1469,7 @@ OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
        OO.ui.mixin.PopupElement.call( this, config );
 
        // Initialization
+       this.popup.setPosition( toolGroup.getToolbar().position === 'bottom' ? 'above' : 'below' );
        this.$element
                .addClass( 'oo-ui-popupTool' )
                .append( this.popup.$element );
@@ -2194,6 +2195,9 @@ OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
        for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
                this.collapsibleTools[ i ].toggle( this.expanded );
        }
+
+       // Re-evaluate clipping, because our height has changed
+       this.clip();
 };
 
 /**
index a1064bb..4ec3d1c 100644 (file)
@@ -1,15 +1,13 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:44Z
+ * Date: 2017-03-07T22:57:06Z
  */
-/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */
-/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */
 .oo-ui-draggableElement-handle,
 .oo-ui-draggableElement-handle.oo-ui-widget {
   cursor: move;
@@ -81,8 +79,6 @@
   left: 0;
   right: 0;
   bottom: 0;
-  /* stylelint-disable declaration-no-important */
-  /* stylelint-enable declaration-no-important */
 }
 .oo-ui-menuLayout-menu,
 .oo-ui-menuLayout-content {
 .oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
   opacity: 0.5;
 }
-.oo-ui-outlineOptionWidget-level-0 {
-  padding-left: 3.5em;
-}
-.oo-ui-outlineOptionWidget-level-0 .oo-ui-iconElement-icon {
-  left: 1em;
+.oo-ui-outlineOptionWidget-level-0.oo-ui-iconElement {
+  padding-left: 2.5em;
 }
 .oo-ui-outlineOptionWidget-level-1 {
-  padding-left: 5em;
+  padding-left: 2.5em;
+}
+.oo-ui-outlineOptionWidget-level-1.oo-ui-iconElement {
+  padding-left: 4.5em;
 }
-.oo-ui-outlineOptionWidget-level-1 .oo-ui-iconElement-icon {
+.oo-ui-outlineOptionWidget-level-1.oo-ui-iconElement .oo-ui-iconElement-icon {
   left: 2.5em;
 }
 .oo-ui-outlineOptionWidget-level-2 {
-  padding-left: 6.5em;
+  padding-left: 5em;
+}
+.oo-ui-outlineOptionWidget-level-2.oo-ui-iconElement {
+  padding-left: 7em;
 }
-.oo-ui-outlineOptionWidget-level-2 .oo-ui-iconElement-icon {
-  left: 4em;
+.oo-ui-outlineOptionWidget-level-2.oo-ui-iconElement .oo-ui-iconElement-icon {
+  left: 5em;
 }
 .oo-ui-selectWidget-depressed .oo-ui-outlineOptionWidget.oo-ui-optionWidget-selected {
   background-color: #a7dcff;
   background-color: transparent;
   color: #000;
   vertical-align: middle;
-  /* stylelint-disable indentation */
-  /* stylelint-enable indentation */
 }
 .oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content > input::-webkit-input-placeholder {
   color: #72777d;
index 495cbfe..f64a619 100644 (file)
@@ -1,15 +1,13 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:44Z
+ * Date: 2017-03-07T22:57:06Z
  */
-/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */
-/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */
 .oo-ui-draggableElement-handle,
 .oo-ui-draggableElement-handle.oo-ui-widget {
   cursor: move;
@@ -92,8 +90,6 @@
   left: 0;
   right: 0;
   bottom: 0;
-  /* stylelint-disable declaration-no-important */
-  /* stylelint-enable declaration-no-important */
 }
 .oo-ui-menuLayout-menu,
 .oo-ui-menuLayout-content {
 .oo-ui-widget-disabled .oo-ui-selectFileWidget-dropLabel {
   display: none;
 }
+.oo-ui-outlineSelectWidget {
+  height: 100%;
+}
+.oo-ui-outlineSelectWidget:focus {
+  box-shadow: inset 0 0 0 2px #36c;
+}
 .oo-ui-outlineOptionWidget {
   -webkit-touch-callout: none;
   -webkit-user-select: none;
 .oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
   opacity: 0.5;
 }
-.oo-ui-outlineOptionWidget-level-0 {
-  padding-left: 3.5em;
-}
-.oo-ui-outlineOptionWidget-level-0 .oo-ui-iconElement-icon {
-  left: 1em;
+.oo-ui-outlineOptionWidget-level-0.oo-ui-iconElement {
+  padding-left: 2.571em;
 }
 .oo-ui-outlineOptionWidget-level-1 {
-  padding-left: 5em;
+  padding-left: 2.571em;
+}
+.oo-ui-outlineOptionWidget-level-1.oo-ui-iconElement {
+  padding-left: 4.429em;
 }
-.oo-ui-outlineOptionWidget-level-1 .oo-ui-iconElement-icon {
-  left: 2.5em;
+.oo-ui-outlineOptionWidget-level-1.oo-ui-iconElement .oo-ui-iconElement-icon {
+  left: 2.571em;
 }
 .oo-ui-outlineOptionWidget-level-2 {
-  padding-left: 6.5em;
+  padding-left: 5.142em;
+}
+.oo-ui-outlineOptionWidget-level-2.oo-ui-iconElement {
+  padding-left: 6.857em;
 }
-.oo-ui-outlineOptionWidget-level-2 .oo-ui-iconElement-icon {
-  left: 4em;
+.oo-ui-outlineOptionWidget-level-2.oo-ui-iconElement .oo-ui-iconElement-icon {
+  left: 4.429em;
 }
 .oo-ui-outlineControlsWidget {
   height: 3em;
   background-color: transparent;
   color: #000;
   vertical-align: middle;
-  /* stylelint-disable indentation */
-  /* stylelint-enable indentation */
 }
 .oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content > input::-webkit-input-placeholder {
   color: #72777d;
index b89262d..d1fbe0d 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:40Z
+ * Date: 2017-03-07T22:57:01Z
  */
 ( function ( OO ) {
 
@@ -3643,11 +3643,7 @@ OO.ui.CapsuleMultiselectWidget = function OoUiCapsuleMultiselectWidget( config )
                        align: 'forwards',
                        anchor: false
                } );
-               OO.ui.mixin.PopupElement.call( this, $.extend( true, {}, config, {
-                       popup: {
-                               $floatableContainer: this.$element
-                       }
-               } ) );
+               OO.ui.mixin.PopupElement.call( this, config );
                $tabFocus = $( '<span>' );
                OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: $tabFocus } ) );
        } else {
@@ -4273,6 +4269,9 @@ OO.ui.CapsuleMultiselectWidget.prototype.updateIfHeightChanged = function () {
        if ( height !== this.height ) {
                this.height = height;
                this.menu.position();
+               if ( this.popup ) {
+                       this.popup.updateDimensions();
+               }
                this.emit( 'resize' );
        }
 };
index b546dd1..4353b43 100644 (file)
@@ -1,15 +1,13 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:44Z
+ * Date: 2017-03-07T22:57:06Z
  */
-/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */
-/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */
 .oo-ui-actionWidget.oo-ui-pendingElement-pending {
   background-image: /* @embed */ url(themes/apex/images/textures/pending.gif);
 }
index 701c058..823b1a1 100644 (file)
@@ -1,15 +1,13 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:44Z
+ * Date: 2017-03-07T22:57:06Z
  */
-/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */
-/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */
 .oo-ui-window {
   background: transparent;
 }
index ad3c226..586efb0 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.19.4
+ * OOjs UI v0.19.5
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-02-28T23:19:40Z
+ * Date: 2017-03-07T22:57:01Z
  */
 ( function ( OO ) {
 
index 394ec85..0cd901f 100644 (file)
                        "ltr": "images/icons/articleRedirect-ltr.svg",
                        "rtl": "images/icons/articleRedirect-rtl.svg"
                } },
+               "journal": { "file": {
+                       "ltr": "images/icons/journal-ltr.svg",
+                       "rtl": "images/icons/journal-rtl.svg"
+               } },
                "upload": { "file": {
                        "ltr": "images/icons/upload-ltr.svg",
                        "rtl": "images/icons/upload-rtl.svg"
index 5b35e74..718f9ff 100644 (file)
                                "os": "images/icons/bold-cyrl-be.svg"
                        }
                } },
+               "highlight": { "file": {
+                       "ltr": "images/icons/highlight-ltr.svg",
+                       "rtl": "images/icons/highlight-rtl.svg"
+               } },
                "italic": { "file": {
                        "default": "images/icons/italic-a.svg",
                        "lang": {
index d68e70c..ec18f29 100644 (file)
@@ -8,8 +8,9 @@
                }
        },
        "images": {
-               "beta": { "file": "images/icons/beta.svg" },
-               "betaLaunch": { "file": "images/icons/logo-wikimediaDiscovery.svg" },
+               "add": { "file": "images/icons/add.svg" },
+               "beta": { "file": "images/icons/beta.svg", "deprecated": "Deprecated since v0.18.3, don't use." },
+               "betaLaunch": { "file": "images/icons/logo-wikimediaDiscovery.svg", "deprecated": "Moved since v0.18.3, use 'logoWikimediaDiscovery' from the 'Wikimedia' pack instead." },
                "bookmark": { "file": {
                        "ltr": "images/icons/bookmark-ltr.svg",
                        "rtl": "images/icons/bookmark-rtl.svg"
@@ -49,7 +50,8 @@
                        "ltr": "images/icons/printer-ltr.svg",
                        "rtl": "images/icons/printer-rtl.svg"
                } },
-               "ribbonPrize": { "file": "images/icons/ribbonPrize.svg" },
+               "ribbonPrize": { "file": "images/icons/ribbonPrize.svg", "deprecated": "Deprecated since v0.18.3, don't use." },
+               "subtract": { "file": "images/icons/subtract.svg" },
                "sun": { "file": {
                        "ltr": "images/icons/sun-ltr.svg",
                        "rtl": "images/icons/sun-rtl.svg"
index 15c0251..c8385e4 100644 (file)
@@ -6,7 +6,7 @@
                "blockUndo": { "file": {
                        "ltr": "images/icons/unBlock-ltr.svg",
                        "rtl": "images/icons/unBlock-rtl.svg"
-               } },
+               }, "deprecated": "Renamed since v0.18.3, use 'unBlock' instead." },
                "unBlock": { "file": {
                        "ltr": "images/icons/unBlock-ltr.svg",
                        "rtl": "images/icons/unBlock-rtl.svg"
@@ -18,7 +18,7 @@
                "flagUndo": { "file": {
                        "ltr": "images/icons/unFlag-ltr.svg",
                        "rtl": "images/icons/unFlag-rtl.svg"
-               } },
+               }, "deprecated": "Renamed since v0.18.3, use 'unFlag' instead." },
                "unFlag": { "file": {
                        "ltr": "images/icons/unFlag-ltr.svg",
                        "rtl": "images/icons/unFlag-rtl.svg"
@@ -42,7 +42,7 @@
                "trashUndo": { "file": {
                        "ltr": "images/icons/unTrash-ltr.svg",
                        "rtl": "images/icons/unTrash-rtl.svg"
-               } },
+               }, "deprecated": "Renamed since v0.18.3, use 'unTrash' instead." },
                "ongoingConversation": {
                        "file": {
                                "ltr": "images/icons/ongoingConversation-ltr.svg",
index b5fbbed..f4a2dc9 100644 (file)
@@ -2,7 +2,7 @@
        "prefix": "oo-ui-icon",
        "intro": "@import '../../../../src/styles/common';",
        "images": {
-               "add": { "file": "images/icons/add.svg" },
+               "add": { "file": "images/icons/add.svg", "deprecated": "Moved since v0.19.5, use from the 'interactive' pack instead." },
                "advanced": { "file": "images/icons/advanced.svg" },
                "alert": { "file": "images/icons/alert.svg" },
                "cancel": { "file": "images/icons/cancel.svg" },
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/add-invert.png b/resources/lib/oojs-ui/themes/apex/images/icons/add-invert.png
new file mode 100644 (file)
index 0000000..8deeddf
Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/add-invert.png differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/add-invert.svg b/resources/lib/oojs-ui/themes/apex/images/icons/add-invert.svg
new file mode 100644 (file)
index 0000000..a2d49a1
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#FFFFFF">
+    <g id="add">
+        <path id="plus" d="M13 6h-2v5H6v2h5v5h2v-5h5v-2h-5z"/>
+    </g>
+</g></svg>
index d058d65..b379e18 100644 (file)
@@ -1,6 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <g id="close">
-        <path id="cross" d="M17.717 7.697l-1.414-1.414L12 10.586 7.697 6.283 6.283 7.697 10.586 12l-4.303 4.303 1.414 1.414L12 13.414l4.303 4.303 1.414-1.414L13.414 12z"/>
-    </g>
+  <path d="M17.717 7.697l-1.414-1.414L12 10.586 7.697 6.283 6.283 7.697 10.586 12l-4.303 4.303 1.414 1.414L12 13.414l4.303 4.303 1.414-1.414L13.414 12z"/>
 </svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.png b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.png
new file mode 100644 (file)
index 0000000..73dd6b9
Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.png differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.svg b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.svg
new file mode 100644 (file)
index 0000000..eb42923
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24">
+    <path d="M5.066 18.236l.14-.244c.976-1.69 1.341-4.587.815-6.469l-.14-.507.2-.365L11.074 2l9.011 5.203-4.994 8.65-.204.354-.522.134c-1.893.485-4.22 2.252-5.195 3.94l-.14.244-.721-.416-1.041 1.89H3.914l1.893-3.336z" fill-rule="evenodd"/>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.png b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.png
new file mode 100644 (file)
index 0000000..2ea2983
Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.png differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.svg b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.svg
new file mode 100644 (file)
index 0000000..9b1940e
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24">
+    <path d="M18.934 18.236l-.14-.244c-.976-1.69-1.341-4.587-.815-6.469l.14-.507-.2-.365L12.926 2 3.914 7.203l4.994 8.65.204.354.522.134c1.893.485 4.22 2.252 5.195 3.94l.14.244.721-.416 1.041 1.89h3.355l-1.893-3.336z" fill-rule="evenodd"/>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.png b/resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.png
new file mode 100644 (file)
index 0000000..2af4eb0
Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.png differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.svg b/resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.svg
new file mode 100644 (file)
index 0000000..8a83e41
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+  <path d="M16 8V7h-6v1h6zm-2 2V9h-4v1h4zM6 4h1v16H6V4zm2 0h10v13c0 1.7-1.3 3-3 3H8V4z"/>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.png b/resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.png
new file mode 100644 (file)
index 0000000..033ede1
Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.png differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.svg b/resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.svg
new file mode 100644 (file)
index 0000000..2a07a44
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+  <path d="M8 8V7h6v1H8zm2 2V9h4v1h-4zm8-6h-1v16h1V4zm-2 0H6v13c0 1.7 1.3 3 3 3h7V4z"/>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.png b/resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.png
new file mode 100644 (file)
index 0000000..4e58b70
Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.png differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.svg b/resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.svg
new file mode 100644 (file)
index 0000000..9595ac6
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#FFFFFF">
+       <path d="M18 13H6v-2h12"/>
+</g></svg>
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/subtract.png b/resources/lib/oojs-ui/themes/apex/images/icons/subtract.png
new file mode 100644 (file)
index 0000000..bd719cb
Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/subtract.png differ
diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/subtract.svg b/resources/lib/oojs-ui/themes/apex/images/icons/subtract.svg
new file mode 100644 (file)
index 0000000..a79f0b5
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+       <path d="M18 13H6v-2h12"/>
+</svg>
index c97d770..85e47ee 100644 (file)
                                "os": "images/icons/bold-cyrl-be.svg"
                        }
                } },
+               "highlight": { "file": {
+                       "ltr": "images/icons/highlight-ltr.svg",
+                       "rtl": "images/icons/highlight-rtl.svg"
+               } },
                "italic": { "file": {
                        "default": "images/icons/italic-a.svg",
                        "lang": {
index 3436446..827a72e 100644 (file)
@@ -22,8 +22,9 @@
                }
        },
        "images": {
-               "beta": { "file": "images/icons/beta.svg" },
-               "betaLaunch": { "file": "images/icons/logo-wikimediaDiscovery.svg" },
+               "add": { "file": "images/icons/add.svg", "variants": [ "constructive", "progressive" ] },
+               "beta": { "file": "images/icons/beta.svg", "deprecated": "Deprecated since v0.18.3, don't use." },
+               "betaLaunch": { "file": "images/icons/logo-wikimediaDiscovery.svg", "deprecated": "Moved since v0.18.3, use 'logoWikimediaDiscovery' from the 'Wikimedia' pack instead." },
                "bookmark": { "file": {
                        "ltr": "images/icons/bookmark-ltr.svg",
                        "rtl": "images/icons/bookmark-rtl.svg"
@@ -66,7 +67,8 @@
                        "ltr": "images/icons/printer-ltr.svg",
                        "rtl": "images/icons/printer-rtl.svg"
                } },
-               "ribbonPrize": { "file": "images/icons/ribbonPrize.svg" },
+               "ribbonPrize": { "file": "images/icons/ribbonPrize.svg", "deprecated": "Deprecated since v0.18.3, don't use." },
+               "subtract": { "file": "images/icons/subtract.svg" },
                "sun": { "file": {
                        "ltr": "images/icons/sun-ltr.svg",
                        "rtl": "images/icons/sun-rtl.svg"
index 6a47267..b4acff1 100644 (file)
@@ -26,7 +26,7 @@
                "blockUndo": { "file": {
                        "ltr": "images/icons/unBlock-ltr.svg",
                        "rtl": "images/icons/unBlock-rtl.svg"
-               } },
+               }, "deprecated": "Renamed since v0.18.3, use 'unBlock' instead." },
                "unBlock": { "file": {
                        "ltr": "images/icons/unBlock-ltr.svg",
                        "rtl": "images/icons/unBlock-rtl.svg"
@@ -42,7 +42,7 @@
                "flagUndo": { "file": {
                        "ltr": "images/icons/unFlag-ltr.svg",
                        "rtl": "images/icons/unFlag-rtl.svg"
-               } },
+               }, "deprecated": "Renamed since v0.18.3, use 'unFlag' instead." },
                "lock": { "file": {
                        "ltr": "images/icons/lock-ltr.svg",
                        "rtl": "images/icons/lock-rtl.svg"
@@ -65,7 +65,7 @@
                "trashUndo": { "file": {
                        "ltr": "images/icons/unTrash-ltr.svg",
                        "rtl": "images/icons/unTrash-rtl.svg"
-               } },
+               }, "deprecated": "Renamed since v0.18.3, use 'unTrash' instead." },
                "ongoingConversation": {
                        "file": {
                                "ltr": "images/icons/ongoingConversation-ltr.svg",
index 4666fd1..60c05f3 100644 (file)
                }
        },
        "images": {
-               "add": { "file": "images/icons/add.svg", "variants": [ "constructive", "progressive" ] },
+               "add": { "file": "images/icons/add.svg", "variants": [ "constructive", "progressive" ], "deprecated": "Moved since v0.19.5, use from the 'interactive' pack instead." },
                "advanced": { "file": "images/icons/advanced.svg" },
                "alert": { "file": "images/icons/alert.svg", "variants": [ "warning" ] },
                "cancel": { "file": "images/icons/cancel.svg", "variants": [ "destructive" ] },
                "check": { "file": "images/icons/check.svg", "variants": [ "constructive", "progressive", "destructive" ] },
                "circle": { "file": "images/icons/circle.svg", "variants": [ "constructive", "progressive" ] },
-               "close": { "file": {
-                       "ltr": "images/icons/close-ltr.svg",
-                       "rtl": "images/icons/close-rtl.svg"
-               } },
+               "close": { "file": "images/icons/close.svg" },
                "code": { "file": "images/icons/code.svg" },
                "collapse": { "file": "images/icons/collapse.svg" },
                "comment": { "file": "images/icons/comment.svg" },
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.png
new file mode 100644 (file)
index 0000000..92af7b6
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.svg
new file mode 100644 (file)
index 0000000..8027fff
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#fff">
+  <path d="M18 7.6l-1.4-1.4-4.6 4.6-4.6-4.6L6 7.6l4.6 4.6L6 16.8l1.4 1.4 4.6-4.6 4.6 4.6 1.4-1.4-4.6-4.6z"/>
+</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.png
deleted file mode 100644 (file)
index a35e9d1..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.svg
deleted file mode 100644 (file)
index 6de3e30..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#fff">
-    <g id="close">
-        <path id="cross" d="M17.4 8.1c.8-.8.8-2 0-2.8L12 10.8 7.4 6.2 6 7.6l4.6 4.6-4 4c-.8.8-.8 2 0 2.8l5.4-5.4 4.6 4.6 1.4-1.4-4.6-4.6z"/>
-    </g>
-</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.png
deleted file mode 100644 (file)
index 69f0787..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.svg
deleted file mode 100644 (file)
index 122340e..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#36c">
-    <g id="close">
-        <path id="cross" d="M17.4 8.1c.8-.8.8-2 0-2.8L12 10.8 7.4 6.2 6 7.6l4.6 4.6-4 4c-.8.8-.8 2 0 2.8l5.4-5.4 4.6 4.6 1.4-1.4-4.6-4.6z"/>
-    </g>
-</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.png
deleted file mode 100644 (file)
index b6a42b8..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.svg
deleted file mode 100644 (file)
index c269316..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <g id="close">
-        <path id="cross" d="M17.4 8.1c.8-.8.8-2 0-2.8L12 10.8 7.4 6.2 6 7.6l4.6 4.6-4 4c-.8.8-.8 2 0 2.8l5.4-5.4 4.6 4.6 1.4-1.4-4.6-4.6z"/>
-    </g>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.png
new file mode 100644 (file)
index 0000000..06fcd5d
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.svg
new file mode 100644 (file)
index 0000000..37c960e
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#36c">
+  <path d="M18 7.6l-1.4-1.4-4.6 4.6-4.6-4.6L6 7.6l4.6 4.6L6 16.8l1.4 1.4 4.6-4.6 4.6 4.6 1.4-1.4-4.6-4.6z"/>
+</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.png
deleted file mode 100644 (file)
index 16462a2..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.svg
deleted file mode 100644 (file)
index 67dc06c..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#fff">
-    <g id="close">
-        <path id="cross" d="M6.6 8.1c-.8-.8-.8-2 0-2.8l5.4 5.5 4.6-4.6L18 7.6l-4.6 4.6 4 4c.8.8.8 2 0 2.8L12 13.6l-4.6 4.6L6 16.8l4.6-4.6z"/>
-    </g>
-</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.png
deleted file mode 100644 (file)
index 2c88596..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.svg
deleted file mode 100644 (file)
index c6494de..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#36c">
-    <g id="close">
-        <path id="cross" d="M6.6 8.1c-.8-.8-.8-2 0-2.8l5.4 5.5 4.6-4.6L18 7.6l-4.6 4.6 4 4c.8.8.8 2 0 2.8L12 13.6l-4.6 4.6L6 16.8l4.6-4.6z"/>
-    </g>
-</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.png
deleted file mode 100644 (file)
index e3e2417..0000000
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.png and /dev/null differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.svg
deleted file mode 100644 (file)
index 36e58ec..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <g id="close">
-        <path id="cross" d="M6.6 8.1c-.8-.8-.8-2 0-2.8l5.4 5.5 4.6-4.6L18 7.6l-4.6 4.6 4 4c.8.8.8 2 0 2.8L12 13.6l-4.6 4.6L6 16.8l4.6-4.6z"/>
-    </g>
-</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close.png
new file mode 100644 (file)
index 0000000..cdb037a
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close.svg
new file mode 100644 (file)
index 0000000..88ffb56
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+  <path d="M18 7.6l-1.4-1.4-4.6 4.6-4.6-4.6L6 7.6l4.6 4.6L6 16.8l1.4 1.4 4.6-4.6 4.6 4.6 1.4-1.4-4.6-4.6z"/>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.png
new file mode 100644 (file)
index 0000000..314d7ac
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.svg
new file mode 100644 (file)
index 0000000..e301dea
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><g fill="#fff">
+    <path d="M5.066 18.236l.14-.244c.976-1.69 1.341-4.587.815-6.469l-.14-.507.2-.365L11.074 2l9.011 5.203-4.994 8.65-.204.354-.522.134c-1.893.485-4.22 2.252-5.195 3.94l-.14.244-.721-.416-1.041 1.89H3.914l1.893-3.336z" fill-rule="evenodd"/>
+</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.png
new file mode 100644 (file)
index 0000000..14bd7be
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.svg
new file mode 100644 (file)
index 0000000..4cc90b5
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><g fill="#36c">
+    <path d="M5.066 18.236l.14-.244c.976-1.69 1.341-4.587.815-6.469l-.14-.507.2-.365L11.074 2l9.011 5.203-4.994 8.65-.204.354-.522.134c-1.893.485-4.22 2.252-5.195 3.94l-.14.244-.721-.416-1.041 1.89H3.914l1.893-3.336z" fill-rule="evenodd"/>
+</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.png
new file mode 100644 (file)
index 0000000..73dd6b9
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.svg
new file mode 100644 (file)
index 0000000..eb42923
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24">
+    <path d="M5.066 18.236l.14-.244c.976-1.69 1.341-4.587.815-6.469l-.14-.507.2-.365L11.074 2l9.011 5.203-4.994 8.65-.204.354-.522.134c-1.893.485-4.22 2.252-5.195 3.94l-.14.244-.721-.416-1.041 1.89H3.914l1.893-3.336z" fill-rule="evenodd"/>
+</svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.png
new file mode 100644 (file)
index 0000000..024595e
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.svg
new file mode 100644 (file)
index 0000000..46d61c2
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><g fill="#fff">
+    <path d="M18.934 18.236l-.14-.244c-.976-1.69-1.341-4.587-.815-6.469l.14-.507-.2-.365L12.926 2 3.914 7.203l4.994 8.65.204.354.522.134c1.893.485 4.22 2.252 5.195 3.94l.14.244.721-.416 1.041 1.89h3.355l-1.893-3.336z" fill-rule="evenodd"/>
+</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.png
new file mode 100644 (file)
index 0000000..039ccbe
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.svg
new file mode 100644 (file)
index 0000000..4f3997a
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24"><g fill="#36c">
+    <path d="M18.934 18.236l-.14-.244c-.976-1.69-1.341-4.587-.815-6.469l.14-.507-.2-.365L12.926 2 3.914 7.203l4.994 8.65.204.354.522.134c1.893.485 4.22 2.252 5.195 3.94l.14.244.721-.416 1.041 1.89h3.355l-1.893-3.336z" fill-rule="evenodd"/>
+</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.png
new file mode 100644 (file)
index 0000000..2ea2983
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.svg
new file mode 100644 (file)
index 0000000..9b1940e
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24">
+    <path d="M18.934 18.236l-.14-.244c-.976-1.69-1.341-4.587-.815-6.469l.14-.507-.2-.365L12.926 2 3.914 7.203l4.994 8.65.204.354.522.134c1.893.485 4.22 2.252 5.195 3.94l.14.244.721-.416 1.041 1.89h3.355l-1.893-3.336z" fill-rule="evenodd"/>
+</svg>
index e7dc90e..bc2e9cc 100644 (file)
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.png differ
index e04818f..370d0a3 100644 (file)
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#fff">
-    <path d="M16 9V8h-6v1h6zm-2 2v-1h-4v1h4zM6 5h1v16H6V5zm2 0h10v13c0 1.7-1.3 3-3 3H8V5z"/>
+  <path d="M16 8V7h-6v1h6zm-2 2V9h-4v1h4zM6 4h1v16H6V4zm2 0h10v13c0 1.7-1.3 3-3 3H8V4z"/>
 </g></svg>
index 444d48b..19f17bd 100644 (file)
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.png differ
index 0232e22..baf8a4a 100644 (file)
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#36c">
-    <path d="M16 9V8h-6v1h6zm-2 2v-1h-4v1h4zM6 5h1v16H6V5zm2 0h10v13c0 1.7-1.3 3-3 3H8V5z"/>
+  <path d="M16 8V7h-6v1h6zm-2 2V9h-4v1h4zM6 4h1v16H6V4zm2 0h10v13c0 1.7-1.3 3-3 3H8V4z"/>
 </g></svg>
index f30b5ff..2af4eb0 100644 (file)
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.png differ
index c9fa553..8a83e41 100644 (file)
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <path d="M16 9V8h-6v1h6zm-2 2v-1h-4v1h4zM6 5h1v16H6V5zm2 0h10v13c0 1.7-1.3 3-3 3H8V5z"/>
+  <path d="M16 8V7h-6v1h6zm-2 2V9h-4v1h4zM6 4h1v16H6V4zm2 0h10v13c0 1.7-1.3 3-3 3H8V4z"/>
 </svg>
index deb4f76..60b5f47 100644 (file)
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.png differ
index 3487511..4660285 100644 (file)
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#fff">
-    <path d="M8 9V8h6v1H8zm2 2v-1h4v1h-4zm8-6h-1v16h1V5zm-2 0H6v13c0 1.7 1.3 3 3 3h7V5z"/>
+  <path d="M8 8V7h6v1H8zm2 2V9h4v1h-4zm8-6h-1v16h1V4zm-2 0H6v13c0 1.7 1.3 3 3 3h7V4z"/>
 </g></svg>
index e5a4fa1..b16badc 100644 (file)
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.png differ
index f2eb554..17ccb82 100644 (file)
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#36c">
-    <path d="M8 9V8h6v1H8zm2 2v-1h4v1h-4zm8-6h-1v16h1V5zm-2 0H6v13c0 1.7 1.3 3 3 3h7V5z"/>
+  <path d="M8 8V7h6v1H8zm2 2V9h4v1h-4zm8-6h-1v16h1V4zm-2 0H6v13c0 1.7 1.3 3 3 3h7V4z"/>
 </g></svg>
index 7826fa8..033ede1 100644 (file)
Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.png differ
index 84da9fa..2a07a44 100644 (file)
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <path d="M8 9V8h6v1H8zm2 2v-1h4v1h-4zm8-6h-1v16h1V5zm-2 0H6v13c0 1.7 1.3 3 3 3h7V5z"/>
+  <path d="M8 8V7h6v1H8zm2 2V9h4v1h-4zm8-6h-1v16h1V4zm-2 0H6v13c0 1.7 1.3 3 3 3h7V4z"/>
 </svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.png
new file mode 100644 (file)
index 0000000..4e58b70
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.svg
new file mode 100644 (file)
index 0000000..d76eb3c
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#fff">
+       <path d="M18 13H6v-2h12"/>
+</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.png
new file mode 100644 (file)
index 0000000..1732972
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.svg
new file mode 100644 (file)
index 0000000..24795ac
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#36c">
+       <path d="M18 13H6v-2h12"/>
+</g></svg>
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.png
new file mode 100644 (file)
index 0000000..bd719cb
Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.png differ
diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.svg
new file mode 100644 (file)
index 0000000..a79f0b5
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+       <path d="M18 13H6v-2h12"/>
+</svg>
index b63f87d..d228236 100644 (file)
@@ -6,6 +6,10 @@
 #wpTextbox1 {
        margin: 0;
        display: block;
+       /* Ensure the textarea is not higher than browser's viewport on small screens */
+       max-height: 100vh;
+       /* But don't let it collapse into nothingness on really tiny screens */
+       min-height: 5em;
 }
 
 /* Adjustments to edit form elements */
index b047f62..e3e80d8 100644 (file)
@@ -169,16 +169,16 @@ a {
 }
 
 /* Expand URLs for printing */
-.mw-body a.external.text:after,
-.mw-body a.external.autonumber:after {
+.mw-body-content a.external.text:after,
+.mw-body-content a.external.autonumber:after {
        content: ' (' attr( href ) ')';
        word-break: break-all;
        word-wrap: break-word;
 }
 
 /* Expand protocol-relative URLs for printing */
-.mw-body a.external.text[href^='//']:after,
-.mw-body a.external.autonumber[href^='//']:after {
+.mw-body-content a.external.text[href^='//']:after,
+.mw-body-content a.external.autonumber[href^='//']:after {
        content: ' (https:' attr( href ) ')';
 }
 
index 18f1299..0df34f8 100644 (file)
        mw.rcfilters.dm.FilterItem.prototype.isHighlightSupported = function () {
                return !!this.getCssClass();
        };
+
+       /**
+        * Check if the filter is currently highlighted
+        *
+        * @return {boolean}
+        */
+       mw.rcfilters.dm.FilterItem.prototype.isHighlighted = function () {
+               return this.isHighlightEnabled() && !!this.getHighlightColor();
+       };
 }( mediaWiki ) );
index 2afe286..5be3656 100644 (file)
        mw.rcfilters.dm.FiltersViewModel.prototype.setFiltersToDefaults = function () {
                var defaultFilterStates = this.getFiltersFromParameters( this.getDefaultParams() );
 
-               this.updateFilters( defaultFilterStates );
+               this.toggleFiltersSelected( defaultFilterStates );
        };
 
        /**
         *                  are the selected highlight colors.
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightParameters = function () {
-               var result = { highlight: this.isHighlightEnabled() };
+               var result = { highlight: Number( this.isHighlightEnabled() ) };
 
                this.getItems().forEach( function ( filterItem ) {
                        result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
         * @return {boolean} Current filters are all empty
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.areCurrentFiltersEmpty = function () {
-               var model = this;
-
                // Check if there are either any selected items or any items
                // that have highlight enabled
                return !this.getItems().some( function ( filterItem ) {
-                       return (
-                               filterItem.isSelected() ||
-                               ( model.isHighlightEnabled() && filterItem.getHighlightColor() )
-                       );
+                       return filterItem.isSelected() || filterItem.isHighlighted();
                } );
        };
 
         * This is equivalent to display all.
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.emptyAllFilters = function () {
-               var filters = {};
-
                this.getItems().forEach( function ( filterItem ) {
-                       filters[ filterItem.getName() ] = false;
-               } );
+                       this.toggleFilterSelected( filterItem.getName(), false );
+               }.bind( this ) );
+       };
 
-               // Update filters
-               this.updateFilters( filters );
+       /**
+        * Toggle selected state of one item
+        *
+        * @param {string} name Name of the filter item
+        * @param {boolean} [isSelected] Filter selected state
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
+               this.getItemByName( name ).toggleSelected( isSelected );
        };
 
        /**
         *
         * @param {Object} filterDef Filter definitions
         */
-       mw.rcfilters.dm.FiltersViewModel.prototype.updateFilters = function ( filterDef ) {
-               var name, filterItem;
-
-               for ( name in filterDef ) {
-                       filterItem = this.getItemByName( name );
-                       filterItem.toggleSelected( filterDef[ name ] );
-               }
+       mw.rcfilters.dm.FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
+               Object.keys( filterDef ).forEach( function ( name ) {
+                       this.toggleFilterSelected( name, filterDef[ name ] );
+               }.bind( this ) );
        };
 
        /**
         * @return {boolean}
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.isHighlightEnabled = function () {
-               return this.highlightEnabled;
+               return !!this.highlightEnabled;
        };
 
        /**
index 98eaa59..e562057 100644 (file)
         * @param {Object} filterStructure Filter definition and structure for the model
         */
        mw.rcfilters.Controller.prototype.initialize = function ( filterStructure ) {
-               var uri = new mw.Uri();
-
                // Initialize the model
                this.filtersModel.initializeFilters( filterStructure );
+               this.updateStateBasedOnUrl();
+       };
+
+       /**
+        * Update filter state (selection and highlighting) based
+        * on current URL and default values.
+        */
+       mw.rcfilters.Controller.prototype.updateStateBasedOnUrl = function () {
+               var uri = new mw.Uri();
 
                // Set filter states based on defaults and URL params
-               this.filtersModel.updateFilters(
+               this.filtersModel.toggleFiltersSelected(
                        this.filtersModel.getFiltersFromParameters(
                                // Merge defaults with URL params for initialization
                                $.extend(
                this.filtersModel.toggleHighlight( !!uri.query.highlight );
                this.filtersModel.getItems().forEach( function ( filterItem ) {
                        var color = uri.query[ filterItem.getName() + '_color' ];
-                       if ( !color ) {
-                               return;
+                       if ( color ) {
+                               filterItem.setHighlightColor( color );
+                       } else {
+                               filterItem.clearHighlightColor();
                        }
-
-                       filterItem.setHighlightColor( color );
                } );
 
                // Check all filter interactions
@@ -59,6 +66,7 @@
         */
        mw.rcfilters.Controller.prototype.resetToDefaults = function () {
                this.filtersModel.setFiltersToDefaults();
+               this.filtersModel.clearAllHighlightColors();
                // Check all filter interactions
                this.filtersModel.reassessFilterInteractions();
 
         * @param {boolean} [isSelected] Filter selected state
         */
        mw.rcfilters.Controller.prototype.toggleFilterSelect = function ( filterName, isSelected ) {
-               var obj = {},
-                       filterItem = this.filtersModel.getItemByName( filterName );
+               var filterItem = this.filtersModel.getItemByName( filterName );
 
                isSelected = isSelected === undefined ? !filterItem.isSelected() : isSelected;
 
                if ( filterItem.isSelected() !== isSelected ) {
-                       obj[ filterName ] = isSelected;
-                       this.filtersModel.updateFilters( obj );
+                       this.filtersModel.toggleFilterSelected( filterName, isSelected );
 
                        this.updateChangesList();
 
                        // Check filter interactions
-                       this.filtersModel.reassessFilterInteractions( this.filtersModel.getItemByName( filterName ) );
+                       this.filtersModel.reassessFilterInteractions( filterItem );
                }
        };
 
         * @param {Object} [params] Extra parameters to add to the API call
         */
        mw.rcfilters.Controller.prototype.updateURL = function ( params ) {
-               var uri;
+               var updatedUri,
+                       notEquivalent = function ( obj1, obj2 ) {
+                               var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
+                               return keys.some( function ( key ) {
+                                       return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
+                               } );
+                       };
 
                params = params || {};
 
-               uri = this.getUpdatedUri();
-               uri.extend( params );
+               updatedUri = this.getUpdatedUri();
+               updatedUri.extend( params );
 
-               window.history.pushState( { tag: 'rcfilters' }, document.title, uri.toString() );
+               if ( notEquivalent( updatedUri.query, new mw.Uri().query ) ) {
+                       window.history.pushState( { tag: 'rcfilters' }, document.title, updatedUri.toString() );
+               }
        };
 
        /**
                                function ( pieces ) {
                                        var $changesListContent = pieces.changes,
                                                $fieldset = pieces.fieldset;
-
                                        this.changesListModel.update( $changesListContent, $fieldset );
                                }.bind( this )
                                // Do nothing for failure
                this.filtersModel.clearHighlightColor( filterName );
                this.updateURL();
        };
+
+       /**
+        * Clear both highlight and selection of a filter
+        *
+        * @param {string} filterName Name of the filter item
+        */
+       mw.rcfilters.Controller.prototype.clearFilter = function ( filterName ) {
+               var filterItem = this.filtersModel.getItemByName( filterName );
+
+               if ( filterItem.isSelected() || filterItem.isHighlighted() ) {
+                       this.filtersModel.clearHighlightColor( filterName );
+                       this.filtersModel.toggleFilterSelected( filterName, false );
+                       this.updateChangesList();
+                       this.filtersModel.reassessFilterInteractions( filterItem );
+               }
+       };
+
+       /**
+        * Synchronize the URL with the current state of the filters
+        * without adding an history entry.
+        */
+       mw.rcfilters.Controller.prototype.replaceUrl = function () {
+               window.history.replaceState(
+                       { tag: 'rcfilters' },
+                       document.title,
+                       this.getUpdatedUri().toString()
+               );
+       };
 }( mediaWiki, jQuery ) );
index 746907b..a0b785d 100644 (file)
                                filtersWidget = new mw.rcfilters.ui.FilterWrapperWidget(
                                        controller, filtersModel, { $overlay: $overlay } );
 
+                       // TODO: The changesListWrapperWidget should be able to initialize
+                       // after the model is ready.
                        // eslint-disable-next-line no-new
                        new mw.rcfilters.ui.ChangesListWrapperWidget(
                                filtersModel, changesListModel, $( '.mw-changeslist, .mw-changeslist-empty' ) );
 
-                       // eslint-disable-next-line no-new
-                       new mw.rcfilters.ui.FormWrapperWidget(
-                               changesListModel, controller, $( 'fieldset.rcoptions' ) );
-
                        controller.initialize( {
                                registration: {
                                        title: mw.msg( 'rcfilters-filtergroup-registration' ),
                                }
                        } );
 
+                       // eslint-disable-next-line no-new
+                       new mw.rcfilters.ui.FormWrapperWidget(
+                               filtersModel, changesListModel, controller, $( 'fieldset.rcoptions' ) );
+
                        $( '.rcfilters-container' ).append( filtersWidget.$element );
                        $( 'body' ).append( $overlay );
 
-                       // HACK: Remove old-style filter links for filters handled by the widget
-                       // Ideally the widget would handle all filters and we'd just remove .rcshowhide entirely
-                       $( '.rcshowhide' ).children().each( function () {
-                               // HACK: Interpret the class name to get the filter name
-                               // This should really be set as a data attribute
-                               var i,
-                                       name = null,
-                                       // Some of the older browsers we support don't have .classList,
-                                       // so we have to interpret the class attribute manually.
-                                       classes = this.getAttribute( 'class' ).split( ' ' );
-                               for ( i = 0; i < classes.length; i++ ) {
-                                       if ( classes[ i ].substr( 0, 'rcshow'.length ) === 'rcshow' ) {
-                                               name = classes[ i ].substr( 'rcshow'.length );
-                                               break;
-                                       }
-                               }
-                               if ( name === null ) {
-                                       return;
-                               }
-                               if ( name === 'hidemine' ) {
-                                       // HACK: the span for hidemyself is called hidemine
-                                       name = 'hidemyself';
-                               }
-                               // This span corresponds to a filter that's in our model, so remove it
-                               if ( filtersModel.getItemByName( name ) ) {
-                                       // 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.
-                                       // This would be unnecessary if we added separators with CSS.
-                                       if ( this.nextSibling && this.nextSibling.nodeType === Node.TEXT_NODE ) {
-                                               this.parentNode.removeChild( this.nextSibling );
-                                       } else if ( this.previousSibling && this.previousSibling.nodeType === Node.TEXT_NODE ) {
-                                               this.parentNode.removeChild( this.previousSibling );
-                                       }
-                                       // Remove the span itself
-                                       this.parentNode.removeChild( this );
-                               }
-                       } );
+                       // Set as ready
+                       $( '.rcfilters-head' ).addClass( 'mw-rcfilters-ui-ready' );
 
                        window.addEventListener( 'popstate', function () {
+                               controller.updateStateBasedOnUrl();
                                controller.updateChangesList();
                        } );
 
                                'href',
                                'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:New_filters_for_edit_review'
                        );
+
+                       controller.replaceUrl();
                }
        };
 
index 897a9e8..d47346c 100644 (file)
@@ -7,7 +7,10 @@
                legend {
                        display: none;
                }
+       }
 
+       .rcfilters-head {
+               min-height: 270px;
                &:not( .mw-rcfilters-ui-ready ) {
                        /* @embed */
                        background-image: url( ../images/pending.gif );
        .rcfilters-container {
                min-height: 100px;
                margin: 0;
-
-               &:not( .mw-rcfilters-ui-ready ) {
-                       /* @embed */
-                       background-image: url( ../images/pending.gif );
-               }
        }
 }
 
index 8173150..b16e84c 100644 (file)
                }
 
                &[data-color='c1'] {
-                       .mw-rcfilters-mixin-circle( @highlight-c1, 0.7em, ~'0 0.5em 0 0' );
+                       .mw-rcfilters-mixin-circle( @highlight-c1, 10px, ~'0 0.5em 0 0' );
                }
                &[data-color='c2'] {
-                       .mw-rcfilters-mixin-circle( @highlight-c2, 0.7em, ~'0 0.5em 0 0' );
+                       .mw-rcfilters-mixin-circle( @highlight-c2, 10px, ~'0 0.5em 0 0' );
                }
                &[data-color='c3'] {
-                       .mw-rcfilters-mixin-circle( @highlight-c3, 0.7em, ~'0 0.5em 0 0' );
+                       .mw-rcfilters-mixin-circle( @highlight-c3, 10px, ~'0 0.5em 0 0' );
                }
                &[data-color='c4'] {
-                       .mw-rcfilters-mixin-circle( @highlight-c4, 0.7em, ~'0 0.5em 0 0' );
+                       .mw-rcfilters-mixin-circle( @highlight-c4, 10px, ~'0 0.5em 0 0' );
                }
                &[data-color='c5'] {
-                       .mw-rcfilters-mixin-circle( @highlight-c5, 0.7em, ~'0 0.5em 0 0' );
+                       .mw-rcfilters-mixin-circle( @highlight-c5, 10px, ~'0 0.5em 0 0' );
                }
        }
 }
index fcd5f67..c18fe5e 100644 (file)
@@ -7,6 +7,10 @@
                        // Each li's margin-left should be the width of the highlights
                        // element + the margin
                        margin-left: ~'calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 + @{result-circle-general-margin} )';
+
+                       li {
+                               list-style: none;
+                       }
                }
        }
 
index 3f70125..198f599 100644 (file)
@@ -1,7 +1,6 @@
 @import 'mw.rcfilters.mixins';
 
 .mw-rcfilters-ui-filterItemHighlightButton {
-
        .oo-ui-buttonWidget.oo-ui-popupButtonWidget .oo-ui-buttonElement-button > &-circle {
                display: inline-block;
                vertical-align: middle;
index 1c3caa0..0e38942 100644 (file)
@@ -9,7 +9,7 @@
        }
 
        &:hover {
-               background-color: #f8f9fa; // Base90 AAA
+               background-color: #fbfbfb;
        }
 
        .mw-rcfilters-ui-table {
index 05f2f66..f28523a 100644 (file)
@@ -38,6 +38,7 @@
                        popup: {
                                padded: false,
                                align: 'center',
+                               position: 'above',
                                $content: $popupContent
                                        .append( descLabelWidget.$element ),
                                $floatableContainer: this.$element,
                }
 
                // Respond to user removing the filter
-               this.controller.toggleFilterSelect( this.model.getName(), false );
-               this.controller.clearHighlightColor( this.model.getName() );
+               this.controller.clearFilter( this.model.getName() );
        };
 
        mw.rcfilters.ui.CapsuleItemWidget.prototype.setHighlightColor = function () {
        mw.rcfilters.ui.CapsuleItemWidget.prototype.onMouseEnter = function () {
                if ( this.model.getDescription() ) {
                        if ( !this.positioned ) {
-                               // Recalculate position to be center of the capsule item
-                               this.popup.$element.css( 'margin-left', ( this.$element.width() / 2 ) );
+                               // Recalculate anchor position to be center of the capsule item
+                               this.popup.$anchor.css( 'margin-left', ( this.$element.width() / 2 ) );
                                this.positioned = true;
                        }
 
index 34fa82e..889ba08 100644 (file)
 
                // Parent
                mw.rcfilters.ui.FilterItemHighlightButton.parent.call( this, $.extend( {}, config, {
-                       icon: 'edit',
+                       icon: 'highlight',
                        indicator: 'down',
                        popup: {
-                               anchor: false,
+                               // TODO: There is a bug in non-anchored popups in
+                               // OOUI, so we set this popup to "anchored" until
+                               // the bug is fixed.
+                               // See: https://phabricator.wikimedia.org/T159906
+                               anchor: true,
                                padded: true,
                                align: 'backwards',
                                horizontalPosition: 'end',
index 81b856d..4ea284b 100644 (file)
@@ -45,7 +45,8 @@
                        this.controller,
                        this.model,
                        {
-                               $overlay: config.$overlay || this.$element
+                               $overlay: config.$overlay || this.$element,
+                               title: mw.msg( 'rcfilters-highlightmenu-help' )
                        }
                );
                this.highlightButton.toggle( this.model.isHighlightEnabled() );
index 7da97a1..e64a4c0 100644 (file)
@@ -56,7 +56,8 @@
                                $content: this.filterPopup.$element,
                                $footer: $footer,
                                classes: [ 'mw-rcfilters-ui-filterWrapperWidget-popup' ],
-                               width: 650
+                               width: 650,
+                               hideWhenOutOfView: false
                        }
                } );
 
@@ -75,7 +76,6 @@
                // Initialize
                this.$element
                        .addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
-                       .addClass( 'mw-rcfilters-ui-ready' )
                        .append( this.capsule.$element, this.textInput.$element );
        };
 
         */
        mw.rcfilters.ui.FilterWrapperWidget.prototype.scrollToTop = function ( $element, marginFromTop ) {
                var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
-                       pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) );
+                       pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
+                       containerScrollTop = $( container ).is( 'body, html' ) ? 0 : $( container ).scrollTop();
 
                // Scroll to item
                $( container ).animate( {
-                       scrollTop: $( container ).scrollTop() + pos.top + ( marginFromTop || 0 )
+                       scrollTop: containerScrollTop + pos.top - ( marginFromTop || 0 )
                } );
        };
 }( mediaWiki ) );
index cefe749..4011e6d 100644 (file)
@@ -29,7 +29,8 @@
                this.groups = {};
                this.selected = null;
 
-               this.highlightButton = new OO.ui.ButtonWidget( {
+               this.highlightButton = new OO.ui.ToggleButtonWidget( {
+                       icon: 'highlight',
                        label: mw.message( 'rcfilters-highlightbutton-title' ).text(),
                        classes: [ 'mw-rcfilters-ui-filtersListWidget-hightlightButton' ]
                } );
@@ -43,7 +44,7 @@
                this.highlightButton.connect( this, { click: 'onHighlightButtonClick' } );
                this.model.connect( this, {
                        initialize: 'onModelInitialize',
-                       highlightChange: 'onHighlightChange'
+                       highlightChange: 'onModelHighlightChange'
                } );
 
                // Initialize
                );
        };
 
-       mw.rcfilters.ui.FiltersListWidget.prototype.onHighlightChange = function ( highlightEnabled ) {
+       /**
+        * Respond to model highlight change event
+        *
+        * @param {boolean} highlightEnabled Highlight is enabled
+        */
+       mw.rcfilters.ui.FiltersListWidget.prototype.onModelHighlightChange = function ( highlightEnabled ) {
                this.highlightButton.setActive( highlightEnabled );
        };
 
index 3c81ff1..e914bbe 100644 (file)
@@ -1,16 +1,18 @@
 ( function ( mw ) {
        /**
         * Wrapper for the RC form with hide/show links
+        * Must be constructed after the model is initialized.
         *
         * @extends OO.ui.Widget
         *
         * @constructor
-        * @param {mw.rcfilters.dm.ChangesListViewModel} model Changes list view model
+        * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Changes list view model
+        * @param {mw.rcfilters.dm.ChangesListViewModel} changeListModel Changes list view model
         * @param {mw.rcfilters.Controller} controller RCfilters controller
         * @param {jQuery} $formRoot Root element of the form to attach to
         * @param {Object} config Configuration object
         */
-       mw.rcfilters.ui.FormWrapperWidget = function MwRcfiltersUiFormWrapperWidget( model, controller, $formRoot, config ) {
+       mw.rcfilters.ui.FormWrapperWidget = function MwRcfiltersUiFormWrapperWidget( filtersModel, changeListModel, controller, $formRoot, config ) {
                config = config || {};
 
                // Parent
@@ -20,7 +22,8 @@
                // Mixin constructors
                OO.ui.mixin.PendingElement.call( this, config );
 
-               this.model = model;
+               this.changeListModel = changeListModel;
+               this.filtersModel = filtersModel;
                this.controller = controller;
                this.$submitButton = this.$element.find( 'form input[type=submit]' );
 
                        .on( 'submit', 'form', this.onFormSubmit.bind( this ) );
 
                // Events
-               this.model.connect( this, {
-                       invalidate: 'onModelInvalidate',
-                       update: 'onModelUpdate'
+               this.changeListModel.connect( this, {
+                       invalidate: 'onChangesModelInvalidate',
+                       update: 'onChangesModelUpdate'
                } );
 
                // Initialize
-               this.cleanupForm();
+               this.cleanUpFieldset();
                this.$element
-                       .addClass( 'mw-rcfilters-ui-FormWrapperWidget' )
-                       .addClass( 'mw-rcfilters-ui-ready' );
+                       .addClass( 'mw-rcfilters-ui-FormWrapperWidget' );
        };
 
        /* Initialization */
        OO.inheritClass( mw.rcfilters.ui.FormWrapperWidget, OO.ui.Widget );
        OO.mixinClass( mw.rcfilters.ui.FormWrapperWidget, OO.ui.mixin.PendingElement );
 
-       /**
-        * Clean up the base form we're getting from the back-end.
-        * Remove <strong> tags and replace those with classes, so
-        * we can toggle those on click.
-        */
-       mw.rcfilters.ui.FormWrapperWidget.prototype.cleanupForm = function () {
-               this.$element.find( '[data-keys] strong' ).each( function () {
-                       $( this )
-                               .parent().addClass( 'mw-rcfilters-staticfilters-selected' );
-
-                       $( this )
-                               .replaceWith( $( this ).contents() );
-               } );
-       };
-
        /**
         * Respond to link click
         *
         * @return {boolean} false
         */
        mw.rcfilters.ui.FormWrapperWidget.prototype.onLinkClick = function ( e ) {
-               var $element = $( e.target ),
-                       data = $element.data( 'params' ),
-                       keys = $element.data( 'keys' ),
-                       $similarElements = $element.parent().find( '[data-keys="' + keys + '"]' );
-
-               // Only highlight choice if this link isn't a show/hide link
-               if ( !$element.parents( '.rcshowhideoption' ).length ) {
-                       // Remove the class from similar elements
-                       $similarElements.removeClass( 'mw-rcfilters-staticfilters-selected' );
-                       // Add the class to this element
-                       $element.addClass( 'mw-rcfilters-staticfilters-selected' );
-               }
-
-               e.stopPropagation();
-
-               this.controller.updateChangesList( data );
+               this.controller.updateChangesList( $( e.target ).data( 'params' ) );
                return false;
        };
 
        /**
         * Respond to model invalidate
         */
-       mw.rcfilters.ui.FormWrapperWidget.prototype.onModelInvalidate = function () {
+       mw.rcfilters.ui.FormWrapperWidget.prototype.onChangesModelInvalidate = function () {
                this.pushPending();
                this.$submitButton.prop( 'disabled', true );
        };
         * @param {jQuery|string} $changesList Updated changes list
         * @param {jQuery} $fieldset Updated fieldset
         */
-       mw.rcfilters.ui.FormWrapperWidget.prototype.onModelUpdate = function ( $changesList, $fieldset ) {
+       mw.rcfilters.ui.FormWrapperWidget.prototype.onChangesModelUpdate = function ( $changesList, $fieldset ) {
                this.$submitButton.prop( 'disabled', false );
 
-               // Replace the links we have in the content
-               // We don't want to replace the entire thing, because there is a big difference between
-               // the links in the backend and the links we have initialized, since we are removing
-               // the ones that are implemented in the new system
+               // Replace the entire fieldset
+               this.$element.empty().append( $fieldset.contents() );
+
+               this.cleanUpFieldset();
+
+               this.popPending();
+       };
+
+       /**
+        * Clean up the old-style show/hide that we have implemented in the filter list
+        */
+       mw.rcfilters.ui.FormWrapperWidget.prototype.cleanUpFieldset = function () {
+               var widget = this;
+
+               // HACK: Remove old-style filter links for filters handled by the widget
+               // Ideally the widget would handle all filters and we'd just remove .rcshowhide entirely
                this.$element.find( '.rcshowhide' ).children().each( function () {
-                       // Go over existing links and replace only them
-                       var classes = $( this ).attr( 'class' ).split( ' ' ),
-                               // Look for that item in the fieldset from the server
-                               $remoteItem = $fieldset.find( '.' + classes.join( '.' ) );
+                       // HACK: Interpret the class name to get the filter name
+                       // This should really be set as a data attribute
+                       var i,
+                               name = null,
+                               // Some of the older browsers we support don't have .classList,
+                               // so we have to interpret the class attribute manually.
+                               classes = this.getAttribute( 'class' ).split( ' ' );
+                       for ( i = 0; i < classes.length; i++ ) {
+                               if ( classes[ i ].substr( 0, 'rcshow'.length ) === 'rcshow' ) {
+                                       name = classes[ i ].substr( 'rcshow'.length );
+                                       break;
+                               }
+                       }
+                       if ( name === null ) {
+                               return;
+                       }
+                       if ( name === 'hidemine' ) {
+                               // HACK: the span for hidemyself is called hidemine
+                               name = 'hidemyself';
+                       }
 
-                       if ( $remoteItem ) {
-                               $( this ).replaceWith( $remoteItem );
+                       // This span corresponds to a filter that's in our model, so remove it
+                       if ( widget.filtersModel.getItemByName( name ) ) {
+                               // 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.
+                               // This would be unnecessary if we added separators with CSS.
+                               if ( this.nextSibling && this.nextSibling.nodeType === Node.TEXT_NODE ) {
+                                       this.parentNode.removeChild( this.nextSibling );
+                               } else if ( this.previousSibling && this.previousSibling.nodeType === Node.TEXT_NODE ) {
+                                       this.parentNode.removeChild( this.previousSibling );
+                               }
+                               // Remove the span itself
+                               this.parentNode.removeChild( this );
                        }
                } );
-
-               this.popPending();
        };
 }( mediaWiki ) );
index b392259..cd674ef 100644 (file)
@@ -9,7 +9,7 @@
  * compatibility ( browsers able to understand gradient syntax support also SVG ).
  * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique */
 
-.mw-body a.external,
+.mw-body-content a.external,
 .link-https {
        background: url( images/external-ltr.png ) center right no-repeat;
        /* @embed */
@@ -19,7 +19,7 @@
        padding-right: 15px;
 }
 
-.mw-body a.external[href^='mailto:'],
+.mw-body-content a.external[href^='mailto:'],
 .link-mailto {
        background: url( images/mail.png ) center right no-repeat;
        /* @embed */
@@ -27,7 +27,7 @@
        padding-right: 15px;
 }
 
-.mw-body a.external[href^='ftp://'],
+.mw-body-content a.external[href^='ftp://'],
 .link-ftp {
        background: url( images/ftp-ltr.png ) center right no-repeat;
        /* @embed */
@@ -35,8 +35,8 @@
        padding-right: 15px;
 }
 
-.mw-body a.external[href^='irc://'],
-.mw-body a.external[href^='ircs://'],
+.mw-body-content a.external[href^='irc://'],
+.mw-body-content a.external[href^='ircs://'],
 .link-irc {
        background: url( images/chat-ltr.png ) center right no-repeat;
        /* @embed */
        padding-right: 15px;
 }
 
-.mw-body a.external[href$='.ogg'],
-.mw-body a.external[href$='.OGG'],
-.mw-body a.external[href$='.mid'],
-.mw-body a.external[href$='.MID'],
-.mw-body a.external[href$='.midi'],
-.mw-body a.external[href$='.MIDI'],
-.mw-body a.external[href$='.mp3'],
-.mw-body a.external[href$='.MP3'],
-.mw-body a.external[href$='.wav'],
-.mw-body a.external[href$='.WAV'],
-.mw-body a.external[href$='.wma'],
-.mw-body a.external[href$='.WMA'],
+.mw-body-content a.external[href$='.ogg'],
+.mw-body-content a.external[href$='.OGG'],
+.mw-body-content a.external[href$='.mid'],
+.mw-body-content a.external[href$='.MID'],
+.mw-body-content a.external[href$='.midi'],
+.mw-body-content a.external[href$='.MIDI'],
+.mw-body-content a.external[href$='.mp3'],
+.mw-body-content a.external[href$='.MP3'],
+.mw-body-content a.external[href$='.wav'],
+.mw-body-content a.external[href$='.WAV'],
+.mw-body-content a.external[href$='.wma'],
+.mw-body-content a.external[href$='.WMA'],
 .link-audio {
        background: url( images/audio-ltr.png ) center right no-repeat;
        /* @embed */
        padding-right: 15px;
 }
 
-.mw-body a.external[href$='.ogm'],
-.mw-body a.external[href$='.OGM'],
-.mw-body a.external[href$='.avi'],
-.mw-body a.external[href$='.AVI'],
-.mw-body a.external[href$='.mpeg'],
-.mw-body a.external[href$='.MPEG'],
-.mw-body a.external[href$='.mpg'],
-.mw-body a.external[href$='.MPG'],
+.mw-body-content a.external[href$='.ogm'],
+.mw-body-content a.external[href$='.OGM'],
+.mw-body-content a.external[href$='.avi'],
+.mw-body-content a.external[href$='.AVI'],
+.mw-body-content a.external[href$='.mpeg'],
+.mw-body-content a.external[href$='.MPEG'],
+.mw-body-content a.external[href$='.mpg'],
+.mw-body-content a.external[href$='.MPG'],
 .link-video {
        background: url( images/video.png ) center right no-repeat;
        /* @embed */
        padding-right: 15px;
 }
 
-.mw-body a.external[href$='.pdf'],
-.mw-body a.external[href$='.PDF'],
-.mw-body a.external[href*='.pdf#'],
-.mw-body a.external[href*='.PDF#'],
-.mw-body a.external[href*='.pdf?'],
-.mw-body a.external[href*='.PDF?'],
+.mw-body-content a.external[href$='.pdf'],
+.mw-body-content a.external[href$='.PDF'],
+.mw-body-content a.external[href*='.pdf#'],
+.mw-body-content a.external[href*='.PDF#'],
+.mw-body-content a.external[href*='.pdf?'],
+.mw-body-content a.external[href*='.PDF?'],
 .link-document {
        background: url( images/document-ltr.png ) center right no-repeat;
        /* @embed */
 }
 
 /* Interwiki styling */
-.mw-body a.extiw,
-.mw-body a.extiw:active {
+.mw-body-content a.extiw,
+.mw-body-content a.extiw:active {
        color: #36b;
 }
 
 /* External link color */
-.mw-body a.external {
+.mw-body-content a.external {
        color: #36b;
 }
index 46de7b5..d204d5d 100644 (file)
@@ -53,33 +53,33 @@ a.new:visited,
 }
 
 /* Interwiki Styling */
-.mw-body a.extiw,
-.mw-body a.extiw:active {
+.mw-body-content a.extiw,
+.mw-body-content a.extiw:active {
        color: #36b;
 }
 
-.mw-body a.extiw:visited {
+.mw-body-content a.extiw:visited {
        color: #636;
 }
 
-.mw-body a.extiw:active {
+.mw-body-content a.extiw:active {
        color: #b63;
 }
 
 /* External links */
-.mw-body a.external {
+.mw-body-content a.external {
        color: #36b;
 }
 
-.mw-body a.external:visited {
+.mw-body-content a.external:visited {
        color: #636; /* T5112 */
 }
 
-.mw-body a.external:active {
+.mw-body-content a.external:active {
        color: #b63;
 }
 
-.mw-body a.external.free {
+.mw-body-content a.external.free {
        word-wrap: break-word;
 }
 
index 3ca9537..5c2f83f 100644 (file)
@@ -73,9 +73,9 @@
                 */
                getModuleSize: function ( moduleName ) {
                        var module = mw.loader.moduleRegistry[ moduleName ],
-                               args, i;
+                               args, i, size;
 
-                       if ( mw.loader.getState( moduleName ) !== 'ready' ) {
+                       if ( module.state !== 'ready' ) {
                                return null;
                        }
 
                                return 0;
                        }
 
-                       // Reverse-engineer the load.php response for this module.
-                       // For example: `mw.loader.implement("example", function(){}, );`
+                       function getFunctionBody( func ) {
+                               return String( func )
+                                       // To ensure a deterministic result, replace the start of the function
+                                       // declaration with a fixed string. For example, in Chrome 55, it seems
+                                       // V8 seemingly-at-random decides to sometimes put a line break between
+                                       // the opening brace and first statement of the function body. T159751.
+                                       .replace( /^\s*function\s*\([^)]*\)\s*{\s*/, 'function(){' )
+                                       .replace( /\s*}\s*$/, '}' );
+                       }
+
+                       // Based on the load.php response for this module.
+                       // For example: `mw.loader.implement("example", function(){}, {"css":[".x{color:red}"]});`
                        // @see mw.loader.store.set().
                        args = [
-                               JSON.stringify( moduleName ),
-                               // function, array of urls, or eval string
-                               typeof module.script === 'function' ?
-                                               String( module.script ) :
-                                               JSON.stringify( module.script ),
-                               JSON.stringify( module.style ),
-                               JSON.stringify( module.messages ),
-                               JSON.stringify( module.templates )
+                               moduleName,
+                               module.script,
+                               module.style,
+                               module.messages,
+                               module.templates
                        ];
                        // Trim trailing null or empty object, as load.php would have done.
                        // @see ResourceLoader::makeLoaderImplementScript and ResourceLoader::trimArray.
                        i = args.length;
                        while ( i-- ) {
-                               if ( args[ i ] === '{}' || args[ i ] === 'null' ) {
+                               if ( args[ i ] === null || ( $.isPlainObject( args[ i ] ) && $.isEmptyObject( args[ i ] ) ) ) {
                                        args.splice( i, 1 );
                                } else {
                                        break;
                                }
                        }
 
-                       return $.byteLength( args.join( ',' ) );
+                       size = 0;
+                       for ( i = 0; i < args.length; i++ ) {
+                               if ( typeof args[ i ] === 'function' ) {
+                                       size += $.byteLength( getFunctionBody( args[ i ] ) );
+                               } else {
+                                       size += $.byteLength( JSON.stringify( args[ i ] ) );
+                               }
+                       }
+
+                       return size;
                },
 
                /**
index 7872818..33f146b 100644 (file)
@@ -52,6 +52,7 @@
                /* eslint-enable no-bitwise */
        }
 
+       // <https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Set>
        StringSet = window.Set || ( function () {
                /**
                 * @private
@@ -64,7 +65,7 @@
                        this.set[ value ] = true;
                };
                StringSet.prototype.has = function ( value ) {
-                       return this.set.hasOwnProperty( value );
+                       return hasOwn.call( this.set, value );
                };
                return StringSet;
        }() );
@@ -82,9 +83,8 @@
         *  copied in one direction only. Changes to globals do not reflect in the map.
         */
        function Map( global ) {
-               this.internalValues = {};
+               this.values = {};
                if ( global === true ) {
-
                        // Override #set to also set the global variable
                        this.set = function ( selection, value ) {
                                var s;
                                return false;
                        };
                }
-
-               // Deprecated since MediaWiki 1.28
-               log.deprecate(
-                       this,
-                       'values',
-                       this.internalValues,
-                       'mw.Map#values is deprecated. Use mw.Map#get() instead.',
-                       'Map-values'
-               );
        }
 
        /**
         * @param {Mixed} value
         */
        function setGlobalMapValue( map, key, value ) {
-               map.internalValues[ key ] = value;
+               map.values[ key ] = value;
                log.deprecate(
                                window,
                                key,
                 *
                 * @param {string|Array} [selection] Key or array of keys to retrieve values for.
                 * @param {Mixed} [fallback=null] Value for keys that don't exist.
-                * @return {Mixed|Object| null} If selection was a string, returns the value,
+                * @return {Mixed|Object|null} If selection was a string, returns the value,
                 *  If selection was an array, returns an object of key/values.
-                *  If no selection is passed, the internal container is returned. (Beware that,
-                *  as is the default in JavaScript, the object is returned by reference.)
+                *  If no selection is passed, a new object with all key/values is returned.
                 */
                get: function ( selection, fallback ) {
                        var results, i;
-                       // If we only do this in the `return` block, it'll fail for the
-                       // call to get() from the mutli-selection block.
                        fallback = arguments.length > 1 ? fallback : null;
 
                        if ( $.isArray( selection ) ) {
-                               selection = slice.call( selection );
                                results = {};
                                for ( i = 0; i < selection.length; i++ ) {
-                                       results[ selection[ i ] ] = this.get( selection[ i ], fallback );
+                                       if ( typeof selection[ i ] === 'string' ) {
+                                               results[ selection[ i ] ] = hasOwn.call( this.values, selection[ i ] ) ?
+                                                       this.values[ selection[ i ] ] :
+                                                       fallback;
+                                       }
                                }
                                return results;
                        }
 
                        if ( typeof selection === 'string' ) {
-                               if ( !hasOwn.call( this.internalValues, selection ) ) {
-                                       return fallback;
-                               }
-                               return this.internalValues[ selection ];
+                               return hasOwn.call( this.values, selection ) ?
+                                       this.values[ selection ] :
+                                       fallback;
                        }
 
                        if ( selection === undefined ) {
-                               return this.internalValues;
+                               results = {};
+                               for ( i in this.values ) {
+                                       results[ i ] = this.values[ i ];
+                               }
+                               return results;
                        }
 
                        // Invalid selection key
-                       return null;
+                       return fallback;
                },
 
                /**
 
                        if ( $.isPlainObject( selection ) ) {
                                for ( s in selection ) {
-                                       this.internalValues[ s ] = selection[ s ];
+                                       this.values[ s ] = selection[ s ];
                                }
                                return true;
                        }
                        if ( typeof selection === 'string' && arguments.length > 1 ) {
-                               this.internalValues[ selection ] = value;
+                               this.values[ selection ] = value;
                                return true;
                        }
                        return false;
                 * @return {boolean} True if the key(s) exist
                 */
                exists: function ( selection ) {
-                       var s;
-
+                       var i;
                        if ( $.isArray( selection ) ) {
-                               for ( s = 0; s < selection.length; s++ ) {
-                                       if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.internalValues, selection[ s ] ) ) {
+                               for ( i = 0; i < selection.length; i++ ) {
+                                       if ( typeof selection[ i ] !== 'string' || !hasOwn.call( this.values, selection[ i ] ) ) {
                                                return false;
                                        }
                                }
                                return true;
                        }
-                       return typeof selection === 'string' && hasOwn.call( this.internalValues, selection );
+                       return typeof selection === 'string' && hasOwn.call( this.values, selection );
                }
        };
 
index a48087e..50d1bc9 100644 (file)
Binary files a/tests/parser/extraParserTests.txt and b/tests/parser/extraParserTests.txt differ
index 37f76ff..419ff00 100644 (file)
@@ -439,7 +439,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         * @return string Absolute name of the temporary file
         */
        protected function getNewTempFile() {
-               $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' );
+               $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . static::class . '_' );
                $this->tmpFiles[] = $fileName;
 
                return $fileName;
@@ -1304,8 +1304,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                if ( isset( $compatibility[$func] ) ) {
                        return call_user_func_array( [ $this, $compatibility[$func] ], $args );
                } else {
-                       throw new MWException( "Called non-existent $func method on "
-                               . get_class( $this ) );
+                       throw new MWException( "Called non-existent $func method on " . static::class );
                }
        }
 
index 6b299c9..abef1c9 100644 (file)
@@ -219,7 +219,7 @@ abstract class ApiTestCase extends MediaWikiLangTestCase {
        }
 
        public function testApiTestGroup() {
-               $groups = PHPUnit_Util_Test::getGroups( get_class( $this ) );
+               $groups = PHPUnit_Util_Test::getGroups( static::class );
                $constraint = PHPUnit_Framework_Assert::logicalOr(
                        $this->contains( 'medium' ),
                        $this->contains( 'large' )
index c3d31d1..f777206 100644 (file)
@@ -4,6 +4,44 @@
  * @group FileRepo
  * @group FileBackend
  * @group medium
+ *
+ * @covers FileBackend
+ *
+ * @covers CopyFileOp
+ * @covers CreateFileOp
+ * @covers DeleteFileOp
+ * @covers DescribeFileOp
+ * @covers FSFile
+ * @covers FSFileBackend
+ * @covers FSFileBackendDirList
+ * @covers FSFileBackendFileList
+ * @covers FSFileBackendList
+ * @covers FSFileOpHandle
+ * @covers FileBackendDBRepoWrapper
+ * @covers FileBackendError
+ * @covers FileBackendGroup
+ * @covers FileBackendMultiWrite
+ * @covers FileBackendStore
+ * @covers FileBackendStoreOpHandle
+ * @covers FileBackendStoreShardDirIterator
+ * @covers FileBackendStoreShardFileIterator
+ * @covers FileBackendStoreShardListIterator
+ * @covers FileJournal
+ * @covers FileOp
+ * @covers FileOpBatch
+ * @covers HTTPFileStreamer
+ * @covers LockManagerGroup
+ * @covers MemoryFileBackend
+ * @covers MoveFileOp
+ * @covers MySqlLockManager
+ * @covers NullFileJournal
+ * @covers NullFileOp
+ * @covers StoreFileOp
+ * @covers TempFSFile
+ *
+ * @covers FSLockManager
+ * @covers LockManager
+ * @covers NullLockManager
  */
 class FileBackendTest extends MediaWikiTestCase {
 
@@ -89,7 +127,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testIsStoragePath
-        * @covers FileBackend::isStoragePath
         */
        public function testIsStoragePath( $path, $isStorePath ) {
                $this->assertEquals( $isStorePath, FileBackend::isStoragePath( $path ),
@@ -114,7 +151,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testSplitStoragePath
-        * @covers FileBackend::splitStoragePath
         */
        public function testSplitStoragePath( $path, $res ) {
                $this->assertEquals( $res, FileBackend::splitStoragePath( $path ),
@@ -139,7 +175,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_normalizeStoragePath
-        * @covers FileBackend::normalizeStoragePath
         */
        public function testNormalizeStoragePath( $path, $res ) {
                $this->assertEquals( $res, FileBackend::normalizeStoragePath( $path ),
@@ -169,7 +204,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testParentStoragePath
-        * @covers FileBackend::parentStoragePath
         */
        public function testParentStoragePath( $path, $res ) {
                $this->assertEquals( $res, FileBackend::parentStoragePath( $path ),
@@ -191,7 +225,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testExtensionFromPath
-        * @covers FileBackend::extensionFromPath
         */
        public function testExtensionFromPath( $path, $res ) {
                $this->assertEquals( $res, FileBackend::extensionFromPath( $path ),
@@ -224,9 +257,6 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->tearDownFiles();
        }
 
-       /**
-        * @covers FileBackend::doOperation
-        */
        private function doTestStore( $op ) {
                $backendName = $this->backendClass();
 
@@ -286,7 +316,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testCopy
-        * @covers FileBackend::doOperation
         */
        public function testCopy( $op ) {
                $this->backend = $this->singleBackend;
@@ -407,7 +436,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testMove
-        * @covers FileBackend::doOperation
         */
        public function testMove( $op ) {
                $this->backend = $this->singleBackend;
@@ -529,7 +557,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testDelete
-        * @covers FileBackend::doOperation
         */
        public function testDelete( $op, $withSource, $okStatus ) {
                $this->backend = $this->singleBackend;
@@ -621,7 +648,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testDescribe
-        * @covers FileBackend::doOperation
         */
        public function testDescribe( $op, $withSource, $okStatus ) {
                $this->backend = $this->singleBackend;
@@ -722,7 +748,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testCreate
-        * @covers FileBackend::doOperation
         */
        public function testCreate( $op, $alreadyExists, $okStatus, $newSize ) {
                $this->backend = $this->singleBackend;
@@ -843,9 +868,6 @@ class FileBackendTest extends MediaWikiTestCase {
                return $cases;
        }
 
-       /**
-        * @covers FileBackend::doQuickOperations
-        */
        public function testDoQuickOperations() {
                $this->backend = $this->singleBackend;
                $this->doTestDoQuickOperations();
@@ -1056,7 +1078,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testGetFileStat
-        * @covers FileBackend::getFileStat
         */
        public function testGetFileStat( $path, $content, $alreadyExists ) {
                $this->backend = $this->singleBackend;
@@ -1132,7 +1153,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testGetFileStat
-        * @covers FileBackend::streamFile
         */
        public function testStreamFile( $path, $content, $alreadyExists ) {
                $this->backend = $this->singleBackend;
@@ -1231,8 +1251,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testGetFileContents
-        * @covers FileBackend::getFileContents
-        * @covers FileBackend::getFileContentsMulti
         */
        public function testGetFileContents( $source, $content ) {
                $this->backend = $this->singleBackend;
@@ -1304,7 +1322,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testGetLocalCopy
-        * @covers FileBackend::getLocalCopy
         */
        public function testGetLocalCopy( $source, $content ) {
                $this->backend = $this->singleBackend;
@@ -1390,7 +1407,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testGetLocalReference
-        * @covers FileBackend::getLocalReference
         */
        public function testGetLocalReference( $source, $content ) {
                $this->backend = $this->singleBackend;
@@ -1467,10 +1483,6 @@ class FileBackendTest extends MediaWikiTestCase {
                return $cases;
        }
 
-       /**
-        * @covers FileBackend::getLocalCopy
-        * @covers FileBackend::getLocalReference
-        */
        public function testGetLocalCopyAndReference404() {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
@@ -1499,7 +1511,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testGetFileHttpUrl
-        * @covers FileBackend::getFileHttpUrl
         */
        public function testGetFileHttpUrl( $source, $content ) {
                $this->backend = $this->singleBackend;
@@ -1544,8 +1555,6 @@ class FileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testPrepareAndClean
-        * @covers FileBackend::prepare
-        * @covers FileBackend::clean
         */
        public function testPrepareAndClean( $path, $isOK ) {
                $this->backend = $this->singleBackend;
@@ -1626,9 +1635,6 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->tearDownFiles();
        }
 
-       /**
-        * @covers FileBackend::clean
-        */
        private function doTestRecursiveClean() {
                $backendName = $this->backendClass();
 
@@ -1673,9 +1679,6 @@ class FileBackendTest extends MediaWikiTestCase {
                }
        }
 
-       /**
-        * @covers FileBackend::doOperations
-        */
        public function testDoOperations() {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
@@ -1763,9 +1766,6 @@ class FileBackendTest extends MediaWikiTestCase {
                        "Correct file SHA-1 of $fileC" );
        }
 
-       /**
-        * @covers FileBackend::doOperations
-        */
        public function testDoOperationsPipeline() {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
@@ -1862,9 +1862,6 @@ class FileBackendTest extends MediaWikiTestCase {
                        "Correct file SHA-1 of $fileC" );
        }
 
-       /**
-        * @covers FileBackend::doOperations
-        */
        public function testDoOperationsFailing() {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
@@ -1939,9 +1936,6 @@ class FileBackendTest extends MediaWikiTestCase {
                        "Correct file SHA-1 of $fileA" );
        }
 
-       /**
-        * @covers FileBackend::getFileList
-        */
        public function testGetFileList() {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
@@ -2117,10 +2111,6 @@ class FileBackendTest extends MediaWikiTestCase {
                }
        }
 
-       /**
-        * @covers FileBackend::getTopDirectoryList
-        * @covers FileBackend::getDirectoryList
-        */
        public function testGetDirectoryList() {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
@@ -2334,10 +2324,6 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->assertEquals( [], $items, "Directory listing is empty." );
        }
 
-       /**
-        * @covers FileBackend::lockFiles
-        * @covers FileBackend::unlockFiles
-        */
        public function testLockCalls() {
                $this->backend = $this->singleBackend;
                $this->doTestLockCalls();
index 95ffb70..6acc943 100644 (file)
@@ -4,6 +4,11 @@
  * @group FileRepo
  * @group FileBackend
  * @group medium
+ *
+ * @covers SwiftFileBackend
+ * @covers SwiftFileBackendDirList
+ * @covers SwiftFileBackendFileList
+ * @covers SwiftFileBackendList
  */
 class SwiftFileBackendTest extends MediaWikiTestCase {
        /** @var TestingAccessWrapper Proxy to SwiftFileBackend */
@@ -28,8 +33,6 @@ class SwiftFileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testSanitizeHdrs
-        * @covers SwiftFileBackend::sanitizeHdrs
-        * @covers SwiftFileBackend::getCustomHeaders
         */
        public function testSanitizeHdrs( $raw, $sanitized ) {
                $hdrs = $this->backend->sanitizeHdrs( [ 'headers' => $raw ] );
@@ -92,7 +95,6 @@ class SwiftFileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testGetMetadataHeaders
-        * @covers SwiftFileBackend::getMetadataHeaders
         */
        public function testGetMetadataHeaders( $raw, $sanitized ) {
                $hdrs = $this->backend->getMetadataHeaders( $raw );
@@ -120,7 +122,6 @@ class SwiftFileBackendTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provider_testGetMetadata
-        * @covers SwiftFileBackend::getMetadata
         */
        public function testGetMetadata( $raw, $sanitized ) {
                $hdrs = $this->backend->getMetadata( $raw );
diff --git a/tests/phpunit/includes/tidy/RemexDriverTest.php b/tests/phpunit/includes/tidy/RemexDriverTest.php
new file mode 100644 (file)
index 0000000..6b16cbf
--- /dev/null
@@ -0,0 +1,297 @@
+<?php
+
+class RemexDriverTest extends MediaWikiTestCase {
+       static private $remexTidyTestData = [
+               // Tests from Html5Depurate
+               [
+                       'Empty string',
+                       "",
+                       ""
+               ],
+               [
+                       'Simple p-wrap',
+                       "x",
+                       "<p>x</p>"
+               ],
+               [
+                       'No p-wrap of blank node',
+                       " ",
+                       " "
+               ],
+               [
+                       'p-wrap terminated by div',
+                       "x<div></div>",
+                       "<p>x</p><div></div>"
+               ],
+               [
+                       'p-wrap not terminated by span',
+                       "x<span></span>",
+                       "<p>x<span></span></p>"
+               ],
+               [
+                       'An element is non-blank and so gets p-wrapped',
+                       "<span></span>",
+                       "<p><span></span></p>"
+               ],
+               [
+                       'The blank flag is set after a block-level element',
+                       "<div></div> ",
+                       "<div></div> "
+               ],
+               [
+                       'Blank detection between two block-level elements',
+                       "<div></div> <div></div>",
+                       "<div></div> <div></div>"
+               ],
+               [
+                       'But p-wrapping of non-blank content works after an element',
+                       "<div></div>x",
+                       "<div></div><p>x</p>"
+               ],
+               [
+                       'p-wrapping between two block-level elements',
+                       "<div></div>x<div></div>",
+                       "<div></div><p>x</p><div></div>"
+               ],
+               [
+                       'p-wrap inside blockquote',
+                       "<blockquote>x</blockquote>",
+                       "<blockquote><p>x</p></blockquote>"
+               ],
+               [
+                       'A comment is blank for p-wrapping purposes',
+                       "<!-- x -->",
+                       "<!-- x -->"
+               ],
+               [
+                       'A comment is blank even when a p-wrap was opened by a text node',
+                       " <!-- x -->",
+                       " <!-- x -->"
+               ],
+               [
+                       'A comment does not open a p-wrap',
+                       "<!-- x -->x",
+                       "<!-- x --><p>x</p>"
+               ],
+               [
+                       'A comment does not close a p-wrap',
+                       "x<!-- x -->",
+                       "<p>x<!-- x --></p>"
+               ],
+               [
+                       'Empty li',
+                       "<ul><li></li></ul>",
+                       "<ul><li class=\"mw-empty-elt\"></li></ul>"
+               ],
+               [
+                       'li with element',
+                       "<ul><li><span></span></li></ul>",
+                       "<ul><li><span></span></li></ul>"
+               ],
+               [
+                       'li with text',
+                       "<ul><li>x</li></ul>",
+                       "<ul><li>x</li></ul>"
+               ],
+               [
+                       'Empty tr',
+                       "<table><tbody><tr></tr></tbody></table>",
+                       "<table><tbody><tr class=\"mw-empty-elt\"></tr></tbody></table>"
+               ],
+               [
+                       'Empty p',
+                       "<p>\n</p>",
+                       "<p class=\"mw-empty-elt\">\n</p>"
+               ],
+               [
+                       'No p-wrapping of an inline element which contains a block element (T150317)',
+                       "<small><div>x</div></small>",
+                       "<small><div>x</div></small>"
+               ],
+               [
+                       'p-wrapping of an inline element which contains an inline element',
+                       "<small><b>x</b></small>",
+                       "<p><small><b>x</b></small></p>"
+               ],
+               [
+                       'p-wrapping is enabled in a blockquote in an inline element',
+                       "<small><blockquote>x</blockquote></small>",
+                       "<small><blockquote><p>x</p></blockquote></small>"
+               ],
+               [
+                       'All bare text should be p-wrapped even when surrounded by block tags',
+                       "<small><blockquote>x</blockquote></small>y<div></div>z",
+                       "<small><blockquote><p>x</p></blockquote></small><p>y</p><div></div><p>z</p>"
+               ],
+               [
+                       'Split tag stack 1',
+                       "<small>x<div>y</div>z</small>",
+                       "<p><small>x</small></p><small><div>y</div></small><p><small>z</small></p>"
+               ],
+               [
+                       'Split tag stack 2',
+                       "<small><div>y</div>z</small>",
+                       "<small><div>y</div></small><p><small>z</small></p>"
+               ],
+               [
+                       'Split tag stack 3',
+                       "<small>x<div>y</div></small>",
+                       "<p><small>x</small></p><small><div>y</div></small>"
+               ],
+               [
+                       'Split tag stack 4 (modified to use splittable tag)',
+                       "a<code>b<i>c<div>d</div></i>e</code>",
+                       "<p>a<code>b<i>c</i></code></p><code><i><div>d</div></i></code><p><code>e</code></p>"
+               ],
+               [
+                       "Split tag stack regression check 1",
+                       "x<span><div>y</div></span>",
+                       "<p>x</p><span><div>y</div></span>"
+               ],
+               [
+                       "Split tag stack regression check 2 (modified to use splittable tag)",
+                       "a<code><i><div>d</div></i>e</code>",
+                       "<p>a</p><code><i><div>d</div></i></code><p><code>e</code></p>"
+               ],
+               // Simple tests from pwrap.js
+               [
+                       'Simple pwrap test 1',
+                       'a',
+                       '<p>a</p>'
+               ],
+               [
+                       '<span> is not a splittable tag, but gets p-wrapped in simple wrapping scenarios',
+                       '<span>a</span>',
+                       '<p><span>a</span></p>'
+               ],
+               [
+                       'Simple pwrap test 3',
+                       'x <div>a</div> <div>b</div> y',
+                       '<p>x </p><div>a</div> <div>b</div><p> y</p>'
+               ],
+               [
+                       'Simple pwrap test 4',
+                       'x<!--c--> <div>a</div> <div>b</div> <!--c-->y',
+                       '<p>x<!--c--> </p><div>a</div> <div>b</div> <!--c--><p>y</p>'
+               ],
+               // Complex tests from pwrap.js
+               [
+                       'Complex pwrap test 1',
+                       '<i>x<div>a</div>y</i>',
+                       '<p><i>x</i></p><i><div>a</div></i><p><i>y</i></p>'
+               ],
+               [
+                       'Complex pwrap test 2',
+                       'a<small>b</small><i>c<div>d</div>e</i>f',
+                       '<p>a<small>b</small><i>c</i></p><i><div>d</div></i><p><i>e</i>f</p>'
+               ],
+               [
+                       'Complex pwrap test 3',
+                       'a<small>b<i>c<div>d</div></i>e</small>',
+                       '<p>a<small>b<i>c</i></small></p><small><i><div>d</div></i></small><p><small>e</small></p>'
+               ],
+               [
+                       'Complex pwrap test 4',
+                       'x<small><div>y</div></small>',
+                       '<p>x</p><small><div>y</div></small>'
+               ],
+               [
+                       'Complex pwrap test 5',
+                       'a<small><i><div>d</div></i>e</small>',
+                       '<p>a</p><small><i><div>d</div></i></small><p><small>e</small></p>'
+               ],
+               [
+                       'Complex pwrap test 6',
+                       '<i>a<div>b</div>c<b>d<div>e</div>f</b>g</i>',
+                       // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
+                       // PHP 5 does not allow concatenation in initialisation of a class static variable
+                       '<p><i>a</i></p><i><div>b</div></i><p><i>c<b>d</b></i></p><i><b><div>e</div></b></i><p><i><b>f</b>g</i></p>'
+                       // @codingStandardsIgnoreEnd
+               ],
+               /* FIXME the second <b> causes a stack split which clones the <i> even
+                * though no <p> is actually generated
+               [
+                       'Complex pwrap test 7',
+                       '<i><b><font><div>x</div></font></b><div>y</div><b><font><div>z</div></font></b></i>',
+                       '<i><b><font><div>x</div></font></b><div>y</div><b><font><div>z</div></font></b></i>'
+               ],
+                */
+               // New local tests
+               [
+                       'Blank text node after block end',
+                       '<small>x<div>y</div> <b>z</b></small>',
+                       '<p><small>x</small></p><small><div>y</div></small><p><small> <b>z</b></small></p>'
+               ],
+               [
+                       'Text node fostering (FIXME: wrap missing)',
+                       '<table>x</table>',
+                       'x<table></table>'
+               ],
+               [
+                       'Blockquote fostering',
+                       '<table><blockquote>x</blockquote></table>',
+                       '<blockquote><p>x</p></blockquote><table></table>'
+               ],
+               [
+                       'Block element fostering',
+                       '<table><div>x',
+                       '<div>x</div><table></table>'
+               ],
+               [
+                       'Formatting element fostering (FIXME: wrap missing)',
+                       '<table><b>x',
+                       '<b>x</b><table></table>'
+               ],
+               [
+                       'AAA clone of p-wrapped element (FIXME: empty b)',
+                       '<b>x<p>y</b>z</p>',
+                       '<p><b>x</b></p><b></b><p><b>y</b>z</p>',
+               ],
+               [
+                       'AAA with fostering (FIXME: wrap missing)',
+                       '<table><b>1<p>2</b>3</p>',
+                       '<b>1</b><p><b>2</b>3</p><table></table>'
+               ],
+       ];
+
+       public function provider() {
+               return self::$remexTidyTestData;
+       }
+
+       /**
+        * @dataProvider provider
+        * @covers MediaWiki\Tidy\RemexCompatFormatter
+        * @covers MediaWiki\Tidy\RemexCompatMunger
+        * @covers MediaWiki\Tidy\RemexDriver
+        * @covers MediaWiki\Tidy\RemexMungerData
+        */
+       public function testTidy( $desc, $input, $expected ) {
+               $r = new MediaWiki\Tidy\RemexDriver( [] );
+               $result = $r->tidy( $input );
+               $this->assertEquals( $expected, $result, $desc );
+       }
+
+       public function html5libProvider() {
+               $files = json_decode( file_get_contents( __DIR__ . '/html5lib-tests.json' ), true );
+               $tests = [];
+               foreach ( $files as $file => $fileTests ) {
+                       foreach ( $fileTests as $i => $test ) {
+                               $tests[] = [ "$file:$i", $test['data'] ];
+                       }
+               }
+               return $tests;
+       }
+
+       /**
+        * This is a quick and dirty test to make sure none of the html5lib tests
+        * generate exceptions. We don't really know what the expected output is.
+        *
+        * @dataProvider html5libProvider
+        * @coversNothing
+        */
+       public function testHtml5Lib( $desc, $input ) {
+               $r = new MediaWiki\Tidy\RemexDriver( [] );
+               $result = $r->tidy( $input );
+               $this->assertTrue( true, $desc );
+       }
+}
index 76cedc6..520108a 100644 (file)
@@ -36,10 +36,18 @@ class NamespaceAwareForeignTitleFactoryTest extends MediaWikiTestCase {
                                'MainNamespaceArticle', null,
                                new ForeignTitle( 0, '', 'MainNamespaceArticle' ),
                        ],
+                       [
+                               'Magic:_The_Gathering', 0,
+                               new ForeignTitle( 0, '', 'Magic:_The_Gathering' ),
+                       ],
                        [
                                'Talk:Nice_talk', 1,
                                new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
                        ],
+                       [
+                               'Talk:Magic:_The_Gathering', 1,
+                               new ForeignTitle( 1, 'Talk', 'Magic:_The_Gathering' ),
+                       ],
                        [
                                'Bogus:Nice_talk', 0,
                                new ForeignTitle( 0, '', 'Bogus:Nice_talk' ),
@@ -56,6 +64,11 @@ class NamespaceAwareForeignTitleFactoryTest extends MediaWikiTestCase {
                                'Bogus:Nice_talk', 1,
                                new ForeignTitle( 1, 'Talk', 'Nice_talk' ),
                        ],
+                       // Misconfigured wiki with unregistered namespace (T114115)
+                       [
+                               'Nice_talk', 1234,
+                               new ForeignTitle( 1234, 'Ns1234', 'Nice_talk' ),
+                       ],
                ];
        }
 
diff --git a/tests/phpunit/languages/LanguageCodeTest.php b/tests/phpunit/languages/LanguageCodeTest.php
new file mode 100644 (file)
index 0000000..b33360c
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @covers LanguageCode
+ *
+ * @group Language
+ *
+ * @license GPL-2.0+
+ * @author Thiemo Mättig
+ */
+class LanguageCodeTest extends PHPUnit_Framework_TestCase {
+
+       public function testConstructor() {
+               $instance = new LanguageCode();
+
+               $this->assertInstanceOf( LanguageCode::class, $instance );
+       }
+
+       public function testGetDeprecatedCodeMapping() {
+               $map = LanguageCode::getDeprecatedCodeMapping();
+
+               $this->assertInternalType( 'array', $map );
+               $this->assertContainsOnly( 'string', array_keys( $map ) );
+               $this->assertArrayNotHasKey( '', $map );
+               $this->assertContainsOnly( 'string', $map );
+               $this->assertNotContains( '', $map );
+
+               // Codes special to MediaWiki should never appear in a map of "deprecated" codes
+               $this->assertArrayNotHasKey( 'qqq', $map, 'documentation' );
+               $this->assertNotContains( 'qqq', $map, 'documentation' );
+               $this->assertArrayNotHasKey( 'qqx', $map, 'debug code' );
+               $this->assertNotContains( 'qqx', $map, 'debug code' );
+
+               // Valid language codes that are currently not "deprecated"
+               $this->assertArrayNotHasKey( 'bh', $map, 'family of Bihari languages' );
+               $this->assertArrayNotHasKey( 'no', $map, 'family of Norwegian languages' );
+               $this->assertArrayNotHasKey( 'simple', $map );
+       }
+
+}
index 3a940d0..ad0ed54 100644 (file)
@@ -79,7 +79,7 @@
                        'Initial state of filters'
                );
 
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        group1filter1: true,
                        group2filter2: true,
                        group3filter1: true
                );
 
                // Select 1 filter
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        hidefilter1: true,
                        hidefilter2: false,
                        hidefilter3: false,
                );
 
                // Select 2 filters
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        hidefilter1: true,
                        hidefilter2: true,
                        hidefilter3: false,
                );
 
                // Select 3 filters
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        hidefilter1: true,
                        hidefilter2: true,
                        hidefilter3: true,
                );
 
                // Select 1 filter from string_options
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter7: true,
                        filter8: false,
                        filter9: false
                );
 
                // Select 2 filters from string_options
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter7: true,
                        filter8: true,
                        filter9: false
                );
 
                // Select 3 filters from string_options
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter7: true,
                        filter8: true,
                        filter9: true
                // This test is demonstrating wrong usage of the method;
                // We should be aware that getFiltersFromParameters is stateless,
                // so each call gives us a filter state that only reflects the query given.
-               // This means that the two calls to updateFilters() below collide.
+               // This means that the two calls to toggleFiltersSelected() below collide.
                // The result of the first is overridden by the result of the second,
                // since both get a full state object from getFiltersFromParameters that **only** relates
                // to the input it receives.
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                hidefilter1: '1'
                        } )
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                hidefilter6: '1'
                        } )
                );
 
-               // The result here is ignoring the first updateFilters call
+               // The result here is ignoring the first toggleFiltersSelected call
                // We should receive default values + hidefilter6 as false
                assert.deepEqual(
                        model.getSelectedState(),
                model = new mw.rcfilters.dm.FiltersViewModel();
                model.initializeFilters( definition );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                hidefilter1: '0'
                        } )
                );
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                hidefilter1: '1'
                        } )
                        'After checking and then unchecking a \'send_unselected_if_any\' filter (without touching other filters in that group), results are default'
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                group3: 'filter7'
                        } )
                        'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                group3: 'filter7,filter8'
                        } )
                        'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                group3: 'filter7,filter8,filter9'
                        } )
                        'A \'string_options\' parameter containing all values, results in all filters of the group as unchecked.'
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                group3: 'filter7,all,filter9'
                        } )
                        'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as unchecked.'
                );
 
-               model.updateFilters(
+               model.toggleFiltersSelected(
                        model.getFiltersFromParameters( {
                                group3: 'filter7,foo,filter9'
                        } )
                        'Initial state: default filters are not selected (controller selects defaults explicitly).'
                );
 
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        hidefilter1: false,
                        hidefilter3: false
                } );
 
                model.initializeFilters( definition );
                // Select a filter that has subset with another filter
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter1: true
                } );
 
                );
 
                // Select another filter that has a subset with the same previous filter
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter4: true
                } );
                model.reassessFilterInteractions( model.getItemByName( 'filter4' ) );
                );
 
                // Remove one filter (but leave the other) that affects filter2
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter1: false
                } );
                model.reassessFilterInteractions( model.getItemByName( 'filter1' ) );
                        'Removing a filter only un-includes its subset if there is no other filter affecting.'
                );
 
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter4: false
                } );
                model.reassessFilterInteractions( model.getItemByName( 'filter4' ) );
                );
 
                // Select most (but not all) items in each group
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter1: true,
                        filter2: true,
                        filter4: true,
                );
 
                // Select all items in 'fullCoverage' group (group2)
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter6: true
                } );
 
                );
 
                // Select all items in non 'fullCoverage' group (group1)
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter3: true
                } );
 
                );
 
                // Uncheck an item from each group
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter3: false,
                        filter5: false
                } );
                );
 
                // Select a filter that has a conflict with another
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter1: true // conflicts: filter2, filter4
                } );
 
                );
 
                // Select one of the conflicts (both filters are now conflicted and selected)
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter4: true // conflicts: filter 1
                } );
                model.reassessFilterInteractions( model.getItemByName( 'filter4' ) );
 
                // Select another filter from filter4 group, meaning:
                // now filter1 no longer conflicts with filter4
-               model.updateFilters( {
+               model.toggleFiltersSelected( {
                        filter6: true // conflicts: filter2
                } );
                model.reassessFilterInteractions( model.getItemByName( 'filter6' ) );
index 3bd15c4..1f7a5ec 100644 (file)
@@ -1,7 +1,4 @@
 ( function ( mw ) {
-       // Whitespace and serialisation of function bodies
-       // different in browsers.
-       var functionSize = String( function () {} ).length;
 
        QUnit.module( 'mediawiki.inspect' );
 
@@ -13,9 +10,9 @@
 
                return mw.loader.using( 'test.inspect.script' ).then( function () {
                        assert.equal(
-                               mw.inspect.getModuleSize( 'test.inspect.script' ) - functionSize,
+                               mw.inspect.getModuleSize( 'test.inspect.script' ),
                                // name, script function
-                               32,
+                               43,
                                'test.inspect.script'
                        );
                } );
@@ -30,9 +27,9 @@
 
                return mw.loader.using( 'test.inspect.both' ).then( function () {
                        assert.equal(
-                               mw.inspect.getModuleSize( 'test.inspect.both' ) - functionSize,
+                               mw.inspect.getModuleSize( 'test.inspect.both' ),
                                // name, script function, styles object
-                               54,
+                               64,
                                'test.inspect.both'
                        );
                } );
@@ -48,9 +45,9 @@
 
                return mw.loader.using( 'test.inspect.scriptmsg' ).then( function () {
                        assert.equal(
-                               mw.inspect.getModuleSize( 'test.inspect.scriptmsg' ) - functionSize,
+                               mw.inspect.getModuleSize( 'test.inspect.scriptmsg' ),
                                // name, script function, empty styles object, messages object
-                               65,
+                               74,
                                'test.inspect.scriptmsg'
                        );
                } );
@@ -67,9 +64,9 @@
 
                return mw.loader.using( 'test.inspect.all' ).then( function () {
                        assert.equal(
-                               mw.inspect.getModuleSize( 'test.inspect.all' ) - functionSize,
+                               mw.inspect.getModuleSize( 'test.inspect.all' ),
                                // name, script function, styles object, messages object, templates object
-                               118,
+                               126,
                                'test.inspect.all'
                        );
                } );
index d3e73ae..119222a 100644 (file)
                assert.strictEqual( conf.set( arry, 'Arry' ), false, 'Map.set returns boolean false if key was invalid (Array)' );
                assert.strictEqual( conf.set( nummy, 'Nummy' ), false, 'Map.set returns boolean false if key was invalid (Number)' );
 
-               assert.strictEqual( conf.get( funky ), null, 'Map.get ruturns null if selection was invalid (Function)' );
-               assert.strictEqual( conf.get( nummy ), null, 'Map.get ruturns null if selection was invalid (Number)' );
-
                conf.set( String( nummy ), 'I used to be a number' );
 
+               assert.strictEqual( conf.get( funky ), null, 'Map.get returns null if selection was invalid (Function)' );
+               assert.strictEqual( conf.get( nummy ), null, 'Map.get returns null if selection was invalid (Number)' );
+               assert.propEqual( conf.get( [ nummy ] ), {}, 'Map.get returns null if selection was invalid (multiple)' );
+               assert.strictEqual( conf.get( nummy, false ), false, 'Map.get returns custom fallback for invalid selection' );
+
                assert.strictEqual( conf.exists( 'doesNotExist' ), false, 'Map.exists where property does not exist' );
                assert.strictEqual( conf.exists( 'undef' ), true, 'Map.exists where value is `undefined`' );
-               assert.strictEqual( conf.exists( nummy ), false, 'Map.exists where key is invalid but looks like an existing key' );
+               assert.strictEqual( conf.exists( [ 'undef', 'example' ] ), true, 'Map.exists with multiple keys (all existing)' );
+               assert.strictEqual( conf.exists( [ 'example', 'doesNotExist' ] ), false, 'Map.exists with multiple keys (some non-existing)' );
+               assert.strictEqual( conf.exists( [] ), true, 'Map.exists with no keys' );
+               assert.strictEqual( conf.exists( nummy ), false, 'Map.exists with invalid key that looks like an existing key' );
+               assert.strictEqual( conf.exists( [ nummy ] ), false, 'Map.exists with invalid key that looks like an existing key' );
 
                // Multiple values at once
+               conf = new mw.Map();
                someValues = {
                        foo: 'bar',
                        lorem: 'ipsum',
                        notExist: null
                }, 'Map.get return includes keys that were not found as null values' );
 
-               // Interacting with globals and accessing the values object
-               this.suppressWarnings();
-               assert.strictEqual( conf.get(), conf.values, 'Map.get returns the entire values object by reference (if called without arguments)' );
-               this.restoreWarnings();
+               assert.propEqual( conf.values, someValues, 'Map.values is an internal object with all values (exposed for convenience)' );
+               assert.propEqual( conf.get(), someValues, 'Map.get() returns an object with all values' );
 
+               // Interacting with globals
                conf.set( 'globalMapChecker', 'Hi' );
 
                assert.ok( ( 'globalMapChecker' in window ) === false, 'Map does not its store values in the window object by default' );