Merge "wdio-mediawiki: fix @since versions"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 17 Sep 2019 19:06:31 +0000 (19:06 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 17 Sep 2019 19:06:31 +0000 (19:06 +0000)
70 files changed:
.travis.yml
INSTALL
RELEASE-NOTES-1.34
autoload.php
includes/DefaultSettings.php
includes/EditPage.php
includes/MergeHistory.php
includes/PHPVersionCheck.php
includes/Rest/EntryPoint.php
includes/Rest/LocalizedHttpException.php
includes/Rest/ResponseFactory.php
includes/Rest/Router.php
includes/ServiceWiring.php
includes/Setup.php
includes/Title.php
includes/actions/McrUndoAction.php
includes/api/ApiQuerySiteinfo.php
includes/api/i18n/ko.json
includes/exception/MWExceptionHandler.php
includes/profiler/SectionProfiler.php
includes/skins/BaseTemplate.php
includes/specials/SpecialBlock.php
includes/watcheditem/WatchedItemQueryService.php
languages/data/ZhConversion.php
languages/i18n/ar.json
languages/i18n/ban.json
languages/i18n/diq.json
languages/i18n/en.json
languages/i18n/exif/nds-nl.json
languages/i18n/fr.json
languages/i18n/jv.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/lzh.json
languages/i18n/my.json
languages/i18n/nap.json
languages/i18n/nl.json
languages/i18n/pl.json
languages/i18n/pt-br.json
languages/i18n/qqq.json
languages/i18n/roa-tara.json
languages/i18n/zh-hant.json
maintenance/cleanupRevActorPage.php [new file with mode: 0644]
maintenance/language/zhtable/simp2trad.manual
maintenance/language/zhtable/simp2trad_noconvert.manual
maintenance/language/zhtable/toCN.manual
maintenance/language/zhtable/toHK.manual
maintenance/language/zhtable/toSimp.manual
maintenance/language/zhtable/toTW.manual
maintenance/language/zhtable/toTrad.manual
maintenance/language/zhtable/trad2simp.manual
maintenance/language/zhtable/tradphrases.manual
maintenance/language/zhtable/tradphrases_exclude.manual
resources/Resources.php
resources/src/mediawiki.legacy/shared.css
resources/src/mediawiki.page.ready/.eslintrc.json [new file with mode: 0644]
resources/src/mediawiki.page.ready/checkboxShift.js
resources/src/mediawiki.page.ready/ready.js
tests/phpunit/includes/MediaWikiServicesTest.php
tests/phpunit/includes/Rest/BasicAccess/MWBasicRequestAuthorizerTest.php
tests/phpunit/includes/Rest/EntryPointTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/api/ApiBlockTest.php
tests/phpunit/includes/api/ApiQuerySiteinfoTest.php
tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php
tests/phpunit/unit/includes/Rest/Handler/HelloHandlerTest.php
tests/phpunit/unit/includes/Rest/ResponseFactoryTest.php
tests/phpunit/unit/includes/Rest/RouterTest.php
tests/selenium/wdio-mediawiki/Api.js
tests/selenium/wdio-mediawiki/README.md

index d5607f1..8dbc5f2 100644 (file)
@@ -26,8 +26,6 @@ matrix:
   include:
     - php: 7.3
     - php: 7.2
-    - php: 7.1
-    - php: 7
 
 services:
   - mysql
diff --git a/INSTALL b/INSTALL
index 07dd9c3..0359166 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -7,7 +7,7 @@ Starting with MediaWiki 1.2.0, it's possible to install and configure the wiki
 
 Required software as of MediaWiki 1.34.0:
 
-* Web server with PHP 7.0.13 or higher, plus the following extesnsions:
+* Web server with PHP 7.2.0 or higher, plus the following extesnsions:
 ** ctype
 ** dom
 ** fileinfo
index a99fd31..09f5346 100644 (file)
@@ -303,6 +303,8 @@ because of Phabricator reports.
 * mw.language.specialCharacters, deprecated in 1.33, has been removed.
   Use require( 'mediawiki.language.specialCharacters' ) instead.
 * The jquery.colorUtil module was removed. Use jquery.color instead.
+* The jquery.checkboxShiftClick module was removed. The functionality
+  is provided by mediawiki.page.ready instead (T232688).
 * EditPage::submit(), deprecated in 1.29, has been removed. Use $this->edit()
   directly.
 * HTMLForm::getErrors(), deprecated in 1.28, has been removed. Use
@@ -570,13 +572,16 @@ because of Phabricator reports.
 * StreamFile::send404Message() and StreamFile::parseRange() are now deprecated.
   Use HTTPFileStreamer::send404Message() and HTTPFileStreamer::parseRange()
   respectively instead.
+* Global variable $wgSysopEmailBans is deprecated; to allow sysops to ban
+  users from sending emails, use
+  $wgGroupPermissions['sysop']['blockemail'] = true;
 
 === Other changes in 1.34 ===
 * …
 
 == Compatibility ==
-MediaWiki 1.34 requires PHP 7.0.13 or later. Although HHVM 3.18.5 or later is
-supported, it is generally advised to use PHP 7.0.13 or later for long term
+MediaWiki 1.34 requires PHP 7.2.0 or later. Although HHVM 3.18.5 or later is
+supported, it is generally advised to use PHP 7.2.0 or later for long term
 support. It also requires the following PHP extensions:
 
 * ctype
index f95e001..48d5b30 100644 (file)
@@ -272,6 +272,7 @@ $wgAutoloadLocalClasses = [
        'CleanupInvalidDbKeys' => __DIR__ . '/maintenance/cleanupInvalidDbKeys.php',
        'CleanupPreferences' => __DIR__ . '/maintenance/cleanupPreferences.php',
        'CleanupRemovedModules' => __DIR__ . '/maintenance/cleanupRemovedModules.php',
+       'CleanupRevActorPage' => __DIR__ . '/maintenance/cleanupRevActorPage.php',
        'CleanupSpam' => __DIR__ . '/maintenance/cleanupSpam.php',
        'CleanupUploadStash' => __DIR__ . '/maintenance/cleanupUploadStash.php',
        'CleanupUsersWithNoId' => __DIR__ . '/maintenance/cleanupUsersWithNoId.php',
index 47fd073..c3a37f3 100644 (file)
@@ -2733,16 +2733,17 @@ $wgExtensionInfoMTime = false;
  * although they are sometimes still referred to as Squid settings for
  * historical reasons.
  *
- * Achieving a high hit ratio with an HTTP proxy requires special
- * configuration. See https://www.mediawiki.org/wiki/Manual:Squid_caching for
- * more details.
+ * Achieving a high hit ratio with an HTTP proxy requires special configuration.
+ * See https://www.mediawiki.org/wiki/Manual:Performance_tuning#Page_view_caching
+ * for more details.
  *
  * @{
  */
 
 /**
  * Enable/disable CDN.
- * See https://www.mediawiki.org/wiki/Manual:Squid_caching
+ *
+ * See https://www.mediawiki.org/wiki/Manual:Performance_tuning#Page_view_caching
  *
  * @since 1.34 Renamed from $wgUseSquid.
  */
@@ -4990,6 +4991,8 @@ $wgBlockAllowsUTEdit = true;
 
 /**
  * Allow sysops to ban users from accessing Emailuser
+ * @deprecated since 1.34; `$wgGroupPermissions['sysop']['blockemail'] = true;`
+ * should be used instead
  */
 $wgSysopEmailBans = true;
 
index 9d89785..d047f6e 100644 (file)
@@ -4045,11 +4045,11 @@ ERROR;
 
                if ( $this->isConflict ) {
                        $conflict = Html::rawElement(
-                               'h2', [ 'id' => 'mw-previewconflict' ],
+                               'div', [ 'id' => 'mw-previewconflict', 'class' => 'warningbox' ],
                                $this->context->msg( 'previewconflict' )->escaped()
                        );
                } else {
-                       $conflict = '<hr />';
+                       $conflict = '';
                }
 
                $previewhead = Html::rawElement(
@@ -4058,7 +4058,9 @@ ERROR;
                                'h2', [ 'id' => 'mw-previewheader' ],
                                $this->context->msg( 'preview' )->escaped()
                        ) .
-                       $out->parseAsInterface( $note ) . $conflict
+                       Html::rawElement( 'div', [ 'class' => 'warningbox' ],
+                               $out->parseAsInterface( $note )
+                       ) . $conflict
                );
 
                $pageViewLang = $this->mTitle->getPageViewLanguage();
index 4045a54..1a950c5 100644 (file)
@@ -273,6 +273,18 @@ class MergeHistory {
                        return $status;
                }
 
+               // Update denormalized revactor_page too
+               $this->dbw->update(
+                       'revision_actor_temp',
+                       [ 'revactor_page' => $this->dest->getArticleID() ],
+                       [
+                               'revactor_page' => $this->source->getArticleID(),
+                               // Slightly hacky, but should work given the values assigned in this class
+                               str_replace( 'rev_timestamp', 'revactor_timestamp', $this->timeWhere )
+                       ],
+                       __METHOD__
+               );
+
                // Make the source page a redirect if no revisions are left
                $haveRevisions = $this->dbw->lockForUpdate(
                        'revision',
index bf138c4..e726729 100644 (file)
@@ -108,8 +108,8 @@ class PHPVersionCheck {
                        'implementation' => 'PHP',
                        'version' => PHP_VERSION,
                        'vendor' => 'the PHP Group',
-                       'upstreamSupported' => '5.6.0',
-                       'minSupported' => '7.0.13',
+                       'upstreamSupported' => '7.1.0',
+                       'minSupported' => '7.2.0',
                        'upgradeURL' => 'https://www.php.net/downloads.php',
                );
        }
index ee3441e..4fdd1f8 100644 (file)
@@ -10,6 +10,7 @@ use MediaWiki\Rest\Validator\Validator;
 use RequestContext;
 use Title;
 use WebResponse;
+use Wikimedia\Message\ITextFormatter;
 
 class EntryPoint {
        /** @var RequestInterface */
@@ -49,6 +50,8 @@ class EntryPoint {
                        'cookiePrefix' => $conf->get( 'CookiePrefix' )
                ] );
 
+               $responseFactory = new ResponseFactory( self::getTextFormatters( $services ) );
+
                // @phan-suppress-next-line PhanAccessMethodInternal
                $authorizer = new MWBasicAuthorizer( $context->getUser(),
                        $services->getPermissionManager() );
@@ -62,7 +65,7 @@ class EntryPoint {
                        ExtensionRegistry::getInstance()->getAttribute( 'RestRoutes' ),
                        $conf->get( 'RestPath' ),
                        $services->getLocalServerObjectCache(),
-                       new ResponseFactory,
+                       $responseFactory,
                        $authorizer,
                        $objectFactory,
                        $restValidator
@@ -76,6 +79,25 @@ class EntryPoint {
                $entryPoint->execute();
        }
 
+       /**
+        * Get a TextFormatter array from MediaWikiServices
+        *
+        * @param MediaWikiServices $services
+        * @return ITextFormatter[]
+        */
+       public static function getTextFormatters( MediaWikiServices $services ) {
+               $langs = array_unique( [
+                       $services->getMainConfig()->get( 'ContLang' )->getCode(),
+                       'en'
+               ] );
+               $textFormatters = [];
+               $factory = $services->getMessageFormatterFactory();
+               foreach ( $langs as $lang ) {
+                       $textFormatters[] = $factory->getTextFormatter( $lang );
+               }
+               return $textFormatters;
+       }
+
        public function __construct( RequestContext $context, RequestInterface $request,
                WebResponse $webResponse, Router $router
        ) {
index 10d3a40..184fe16 100644 (file)
@@ -5,7 +5,14 @@ namespace MediaWiki\Rest;
 use Wikimedia\Message\MessageValue;
 
 class LocalizedHttpException extends HttpException {
-       public function __construct( MessageValue $message, $code = 500 ) {
-               parent::__construct( 'Localized exception with key ' . $message->getKey(), $code );
+       private $messageValue;
+
+       public function __construct( MessageValue $messageValue, $code = 500 ) {
+               parent::__construct( 'Localized exception with key ' . $messageValue->getKey(), $code );
+               $this->messageValue = $messageValue;
+       }
+
+       public function getMessageValue() {
+               return $this->messageValue;
        }
 }
index 5e5a198..fd0f3c7 100644 (file)
@@ -5,19 +5,31 @@ namespace MediaWiki\Rest;
 use Exception;
 use HttpStatus;
 use InvalidArgumentException;
+use LanguageCode;
 use MWExceptionHandler;
 use stdClass;
 use Throwable;
+use Wikimedia\Message\ITextFormatter;
+use Wikimedia\Message\MessageValue;
 
 /**
  * Generates standardized response objects.
  */
 class ResponseFactory {
-
        const CT_PLAIN = 'text/plain; charset=utf-8';
        const CT_HTML = 'text/html; charset=utf-8';
        const CT_JSON = 'application/json';
 
+       /** @var ITextFormatter[] */
+       private $textFormatters;
+
+       /**
+        * @param ITextFormatter[] $textFormatters
+        */
+       public function __construct( $textFormatters ) {
+               $this->textFormatters = $textFormatters;
+       }
+
        /**
         * Encode a stdClass object or array to a JSON string
         *
@@ -167,13 +179,23 @@ class ResponseFactory {
                return $response;
        }
 
+       /**
+        * Create an HTTP 4xx or 5xx response with error message localisation
+        */
+       public function createLocalizedHttpError( $errorCode, MessageValue $messageValue ) {
+               return $this->createHttpError( $errorCode, $this->formatMessage( $messageValue ) );
+       }
+
        /**
         * Turn an exception into a JSON error response.
         * @param Exception|Throwable $exception
         * @return Response
         */
        public function createFromException( $exception ) {
-               if ( $exception instanceof HttpException ) {
+               if ( $exception instanceof LocalizedHttpException ) {
+                       $response = $this->createLocalizedHttpError( $exception->getCode(),
+                               $exception->getMessageValue() );
+               } elseif ( $exception instanceof HttpException ) {
                        // FIXME can HttpException represent 2xx or 3xx responses?
                        $response = $this->createHttpError(
                                $exception->getCode(),
@@ -240,4 +262,18 @@ class ResponseFactory {
                return "<!doctype html><title>Redirect</title><a href=\"$url\">$url</a>";
        }
 
+       public function formatMessage( MessageValue $messageValue ) {
+               if ( !$this->textFormatters ) {
+                       // For unit tests
+                       return [];
+               }
+               $translations = [];
+               foreach ( $this->textFormatters as $formatter ) {
+                       $lang = LanguageCode::bcp47( $formatter->getLangCode() );
+                       $messageText = $formatter->format( $messageValue );
+                       $translations[$lang] = $messageText;
+               }
+               return [ 'messageTranslations' => $translations ];
+       }
+
 }
index a520130..6821d89 100644 (file)
@@ -4,6 +4,7 @@ namespace MediaWiki\Rest;
 
 use AppendIterator;
 use BagOStuff;
+use Wikimedia\Message\MessageValue;
 use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface;
 use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
 use MediaWiki\Rest\Validator\Validator;
@@ -226,18 +227,28 @@ class Router {
                $path = $request->getUri()->getPath();
                $relPath = $this->getRelativePath( $path );
                if ( $relPath === false ) {
-                       return $this->responseFactory->createHttpError( 404 );
+                       return $this->responseFactory->createLocalizedHttpError( 404,
+                               ( new MessageValue( 'rest-prefix-mismatch' ) )
+                                       ->plaintextParams( $path, $this->rootPath )
+                       );
                }
 
+               $requestMethod = $request->getMethod();
                $matchers = $this->getMatchers();
-               $matcher = $matchers[$request->getMethod()] ?? null;
+               $matcher = $matchers[$requestMethod] ?? null;
                $match = $matcher ? $matcher->match( $relPath ) : null;
 
+               // For a HEAD request, execute the GET handler instead if one exists.
+               // The webserver will discard the body.
+               if ( !$match && $requestMethod === 'HEAD' && isset( $matchers['GET'] ) ) {
+                       $match = $matchers['GET']->match( $relPath );
+               }
+
                if ( !$match ) {
                        // Check for 405 wrong method
                        $allowed = [];
                        foreach ( $matchers as $allowedMethod => $allowedMatcher ) {
-                               if ( $allowedMethod === $request->getMethod() ) {
+                               if ( $allowedMethod === $requestMethod ) {
                                        continue;
                                }
                                if ( $allowedMatcher->match( $relPath ) ) {
@@ -245,12 +256,20 @@ class Router {
                                }
                        }
                        if ( $allowed ) {
-                               $response = $this->responseFactory->createHttpError( 405 );
+                               $response = $this->responseFactory->createLocalizedHttpError( 405,
+                                       ( new MessageValue( 'rest-wrong-method' ) )
+                                               ->textParams( $requestMethod )
+                                               ->commaListParams( $allowed )
+                                               ->numParams( count( $allowed ) )
+                               );
                                $response->setHeader( 'Allow', $allowed );
                                return $response;
                        } else {
                                // Did not match with any other method, must be 404
-                               return $this->responseFactory->createHttpError( 404 );
+                               return $this->responseFactory->createLocalizedHttpError( 404,
+                                       ( new MessageValue( 'rest-no-match' ) )
+                                               ->plaintextParams( $relPath )
+                               );
                        }
                }
 
@@ -272,6 +291,7 @@ class Router {
 
        /**
         * Execute a fully-constructed handler
+        *
         * @param Handler $handler
         * @return ResponseInterface
         */
index 4cd3d85..8807d04 100644 (file)
@@ -784,7 +784,8 @@ return [
                        $services->getDBLoadBalancer(),
                        $services->getCommentStore(),
                        $services->getActorMigration(),
-                       $services->getWatchedItemStore()
+                       $services->getWatchedItemStore(),
+                       $services->getPermissionManager()
                );
        },
 
index 518531a..6838c37 100644 (file)
@@ -440,6 +440,13 @@ if ( $wgEnableEmail ) {
        $wgUsersNotifiedOnAllChanges = [];
 }
 
+// $wgSysopEmailBans deprecated in 1.34
+if ( isset( $wgSysopEmailBans ) && $wgSysopEmailBans === false ) {
+       foreach ( $wgGroupPermissions as $group => $_ ) {
+               unset( $wgGroupPermissions[$group]['blockemail'] );
+       }
+}
+
 if ( $wgMetaNamespace === false ) {
        $wgMetaNamespace = str_replace( ' ', '_', $wgSitename );
 }
index de418a7..0f5c384 100644 (file)
@@ -1561,14 +1561,32 @@ class Title implements LinkTarget, IDBAccessObject {
         * Get a Title object associated with the talk page of this article
         *
         * @deprecated since 1.34, use getTalkPageIfDefined() or NamespaceInfo::getTalkPage()
-        *             with NamespaceInfo::canHaveTalkPage().
+        *             with NamespaceInfo::canHaveTalkPage(). Note that the new method will
+        *             throw if asked for the talk page of a section-only link, or of an interwiki
+        *             link.
         * @return Title The object for the talk page
         * @throws MWException if $target doesn't have talk pages, e.g. because it's in NS_SPECIAL
         *         or because it's a relative link, or an interwiki link.
         */
        public function getTalkPage() {
-               return self::castFromLinkTarget(
-                       MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) );
+               // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles.
+               //       Instead of failing on invalid titles, let's just log the issue for now.
+               //       See the discussion on T227817.
+
+               // Is this the same title?
+               $talkNS = MediaWikiServices::getInstance()->getNamespaceInfo()->getTalk( $this->mNamespace );
+               if ( $this->mNamespace == $talkNS ) {
+                       return $this;
+               }
+
+               $title = self::makeTitle( $talkNS, $this->mDbkeyform );
+
+               $this->warnIfPageCannotExist( $title, __METHOD__ );
+
+               return $title;
+               // TODO: replace the above with the code below:
+               // return self::castFromLinkTarget(
+               // MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) );
        }
 
        /**
@@ -1596,8 +1614,51 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return Title The object for the subject page
         */
        public function getSubjectPage() {
-               return self::castFromLinkTarget(
-                       MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) );
+               // Is this the same title?
+               $subjectNS = MediaWikiServices::getInstance()->getNamespaceInfo()
+                       ->getSubject( $this->mNamespace );
+               if ( $this->mNamespace == $subjectNS ) {
+                       return $this;
+               }
+               // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles.
+               //       Instead of failing on invalid titles, let's just log the issue for now.
+               //       See the discussion on T227817.
+               $title = self::makeTitle( $subjectNS, $this->mDbkeyform );
+
+               $this->warnIfPageCannotExist( $title, __METHOD__ );
+
+               return $title;
+               // TODO: replace the above with the code below:
+               // return self::castFromLinkTarget(
+               // MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) );
+       }
+
+       /**
+        * @param Title $title
+        * @param string $method
+        *
+        * @return bool whether a warning was issued
+        */
+       private function warnIfPageCannotExist( Title $title, $method ) {
+               if ( $this->getText() == '' ) {
+                       wfLogWarning(
+                               $method . ': called on empty title ' . $this->getFullText() . ', returning '
+                               . $title->getFullText()
+                       );
+
+                       return true;
+               }
+
+               if ( $this->getInterwiki() !== '' ) {
+                       wfLogWarning(
+                               $method . ': called on interwiki title ' . $this->getFullText() . ', returning '
+                               . $title->getFullText()
+                       );
+
+                       return true;
+               }
+
+               return false;
        }
 
        /**
@@ -1610,8 +1671,23 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return Title
         */
        public function getOtherPage() {
-               return self::castFromLinkTarget(
-                       MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) );
+               // NOTE: Depend on the methods in this class instead of their equivalent in NamespaceInfo,
+               //       until their semantics has become exactly the same.
+               //       See the discussion on T227817.
+               if ( $this->isSpecialPage() ) {
+                       throw new MWException( 'Special pages cannot have other pages' );
+               }
+               if ( $this->isTalkPage() ) {
+                       return $this->getSubjectPage();
+               } else {
+                       if ( !$this->canHaveTalkPage() ) {
+                               throw new MWException( "{$this->getPrefixedText()} does not have an other page" );
+                       }
+                       return $this->getTalkPage();
+               }
+               // TODO: replace the above with the code below:
+               // return self::castFromLinkTarget(
+               // MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) );
        }
 
        /**
index 41cd24e..68c0ea1 100644 (file)
@@ -289,8 +289,9 @@ class McrUndoAction extends FormAction {
                                'h2', [ 'id' => 'mw-previewheader' ],
                                $this->context->msg( 'preview' )->text()
                        ) .
-                       $out->parseAsInterface( $note ) .
-                       "<hr />"
+                       Html::rawElement( 'div', [ 'class' => 'warningbox' ],
+                               $out->parseAsInterface( $note )
+                       )
                );
 
                $pageViewLang = $this->getTitle()->getPageViewLanguage();
index 7e4a891..47212b3 100644 (file)
@@ -279,6 +279,8 @@ class ApiQuerySiteinfo extends ApiQueryBase {
        }
 
        protected function appendNamespaces( $property ) {
+               $nsProtection = $this->getConfig()->get( 'NamespaceProtection' );
+
                $data = [
                        ApiResult::META_TYPE => 'assoc',
                ];
@@ -303,6 +305,17 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                        $data[$ns]['content'] = $nsInfo->isContent( $ns );
                        $data[$ns]['nonincludable'] = $nsInfo->isNonincludable( $ns );
 
+                       if ( isset( $nsProtection[$ns] ) ) {
+                               if ( is_array( $nsProtection[$ns] ) ) {
+                                       $specificNs = implode( "|", array_filter( $nsProtection[$ns] ) );
+                               } elseif ( $nsProtection[$ns] !== '' ) {
+                                       $specificNs = $nsProtection[$ns];
+                               }
+                               if ( isset( $specificNs ) && $specificNs !== '' ) {
+                                       $data[$ns]['namespaceprotection'] = $specificNs;
+                               }
+                       }
+
                        $contentmodel = $nsInfo->getNamespaceContentModel( $ns );
                        if ( $contentmodel ) {
                                $data[$ns]['defaultcontentmodel'] = $contentmodel;
index 6124066..6f390f9 100644 (file)
@@ -16,7 +16,8 @@
                        "Jonghaya",
                        "Jerrykim306",
                        "코코아",
-                       "Macofe"
+                       "Macofe",
+                       "렌즈"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|설명문서]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 메일링 리스트]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API 알림 사항]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 버그 및 요청]\n</div>\n<strong>상태:</strong> 이 페이지에 보이는 모든 기능은 정상적으로 작동하지만, API는 여전히 활발하게 개발되고 있으며, 언제든지 변경될 수 있습니다. 업데이트 공지를 받아보려면 [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 메일링 리스트]를 구독하십시오.\n\n<strong>잘못된 요청:</strong> API에 잘못된 요청이 전송되면 \"MediaWiki-API-Error\" 키가 포함된 HTTP 헤더가 전송되며 반환되는 헤더와 오류 코드의 값은 모두 동일한 값으로 설정됩니다. 자세한 정보에 대해서는 [[mw:Special:MyLanguage/API:Errors and warnings/ko|API:오류와 경고]]를 참조하십시오.\n\n<strong>테스트하기:</strong> API 요청 테스트를 용이하게 하려면, [[Special:ApiSandbox]]를 보십시오.",
        "apihelp-compare-paramvalue-prop-title": "'from'과 'to' 판의 문서 제목입니다.",
        "apihelp-compare-paramvalue-prop-user": "'from'과 'to' 판의 사용자 이름과 ID입니다.",
        "apihelp-compare-paramvalue-prop-comment": "'from'과 'to' 판의 설명입니다.",
-       "apihelp-compare-paramvalue-prop-parsedcomment": "'from'과 to' 판의 구문 분석된 설명입니다.",
+       "apihelp-compare-paramvalue-prop-parsedcomment": "'from'과 to' 판의 변환된 설명입니다.",
        "apihelp-compare-paramvalue-prop-size": "'from'과 'to' 판의 크기입니다.",
        "apihelp-compare-example-1": "판 1과 2의 차이를 생성합니다.",
        "apihelp-createaccount-summary": "새 사용자 계정을 만듭니다.",
        "apihelp-options-example-complex": "모든 환경 설정을 초기화하고 <kbd>skin</kbd>과 <kbd>nickname</kbd>을 설정합니다.",
        "apihelp-paraminfo-summary": "API 모듈의 정보를 가져옵니다.",
        "apihelp-paraminfo-param-helpformat": "도움말 문자열 포맷.",
-       "apihelp-parse-summary": "ë\82´ì\9a©ì\9d\98 êµ¬ë¬¸ì\9d\84 ë¶\84ì\84\9dí\95\98ê³  í\8c\8cì\84\9c 출력을 반환합니다.",
-       "apihelp-parse-param-summary": "구문 분석할 요약입니다.",
-       "apihelp-parse-paramvalue-prop-text": "위키텍스트의 구문 분석된 텍스트를 제공합니다.",
-       "apihelp-parse-paramvalue-prop-langlinks": "구문 분석된 위키텍스트의 언어 링크를 제공합니다.",
-       "apihelp-parse-paramvalue-prop-categories": "구문 분석된 위키텍스트의 분류를 제공합니다.",
+       "apihelp-parse-summary": "ë\82´ì\9a©ì\9d\84 ë³\80í\99\98í\95\98ê³  출력을 반환합니다.",
+       "apihelp-parse-param-summary": "변환할 요약입니다.",
+       "apihelp-parse-paramvalue-prop-text": "위키텍스트로 변환된 텍스트를 제공합니다.",
+       "apihelp-parse-paramvalue-prop-langlinks": "언어 링크를 위키텍스트로 변환하여 제공합니다.",
+       "apihelp-parse-paramvalue-prop-categories": "분류를 변환된 위키텍스트로 제공합니다.",
        "apihelp-parse-paramvalue-prop-categorieshtml": "분류의 HTML 버전을 제공합니다.",
-       "apihelp-parse-paramvalue-prop-links": "구문 분석된 위키텍스트의 내부 링크를 제공합니다.",
-       "apihelp-parse-paramvalue-prop-templates": "구문 분석된 위키텍스트의 틀을 제공합니다.",
-       "apihelp-parse-paramvalue-prop-images": "구문 ë¶\84ì\84\9dë\90\9c ì\9c\84í\82¤í\85\8dì\8a¤í\8a¸ì\9d\98 ê·¸ë¦¼ì\9d\84 제공합니다.",
-       "apihelp-parse-paramvalue-prop-externallinks": "구문 분석된 위키텍스트의 외부 링크를 제공합니다.",
-       "apihelp-parse-paramvalue-prop-sections": "구문 분석된 위키텍스트의 문단을 제공합니다.",
-       "apihelp-parse-paramvalue-prop-revid": "구문 분석된 페이지의 판 ID를 추가합니다.",
-       "apihelp-parse-paramvalue-prop-displaytitle": "구문 분석된 위키텍스트의 제목을 추가합니다.",
+       "apihelp-parse-paramvalue-prop-links": "내부 링크를 위키텍스트로 변환하여 제공합니다.",
+       "apihelp-parse-paramvalue-prop-templates": "틀을 변환된 위키텍스트로 제공합니다.",
+       "apihelp-parse-paramvalue-prop-images": "그림ì\9d\84 ì\9c\84í\82¤í\85\8dì\8a¤í\8a¸ë¡\9c ë³\80í\99\98í\95\98ì\97¬ 제공합니다.",
+       "apihelp-parse-paramvalue-prop-externallinks": "외부 링크를 위키텍스트로 변환하여 제공합니다.",
+       "apihelp-parse-paramvalue-prop-sections": "문단을 변환된 위키텍스트로 제공합니다.",
+       "apihelp-parse-paramvalue-prop-revid": "변환할 페이지의 판 ID를 추가합니다.",
+       "apihelp-parse-paramvalue-prop-displaytitle": "제목을 변환된 위키텍스트로 추가합니다.",
        "apihelp-parse-paramvalue-prop-headitems": "문서의 <code>&lt;head&gt;</code> 안에 넣을 항목을 제공합니다.",
-       "apihelp-parse-paramvalue-prop-headhtml": "문서의 구문 분석된 <code>&lt;head&gt;</code>를 제공합니다.",
+       "apihelp-parse-paramvalue-prop-headhtml": "문서의 파싱된 doctype, 여는 <code>&lt;html&gt;</code>, <code>&lt;head&gt;</code>, <code>&lt;body&gt;</code>를 제공합니다.",
        "apihelp-parse-paramvalue-prop-modules": "문서에 사용되는 ResourceLoader 모듈을 제공합니다. 불러오려면, <code>mw.loader.using()</code>을 사용하세요. <kbd>jsconfigvars</kbd> 또는 <kbd>encodedjsconfigvars</kbd>는 <kbd>modules</kbd>와 함께 요청해야 합니다.",
        "apihelp-parse-paramvalue-prop-jsconfigvars": "문서에 특화된 자바스크립트 구성 변수를 제공합니다. 적용하려면 <code>mw.config.set()</code>을 사용하세요.",
-       "apihelp-parse-paramvalue-prop-iwlinks": "구문 분석된 위키텍스트의 인터위키 링크를 제공합니다.",
-       "apihelp-parse-paramvalue-prop-wikitext": "구문 분석된 위키텍스트 원문을 제공합니다.",
-       "apihelp-parse-paramvalue-prop-properties": "구문 분석된 위키텍스트에 정의된 다양한 속성을 제공합니다.",
-       "apihelp-parse-param-pst": "구문 분석 이전에 입력에 대한 사전 저장 변환을 수행합니다. 텍스트로 사용할 때에만 유효합니다.",
+       "apihelp-parse-paramvalue-prop-iwlinks": "인터위키 링크를 위키텍스트로 변환하여 제공합니다.",
+       "apihelp-parse-paramvalue-prop-wikitext": "변환한 원문 위키텍스트를 제공합니다.",
+       "apihelp-parse-paramvalue-prop-properties": "정의된 다양한 속성을 변환된 위키텍스트로 제공합니다.",
+       "apihelp-parse-param-pst": "파싱에 앞서 입력에 대한 저장 직전의 변환을 수행합니다. 텍스트로 사용할 때에만 유효합니다.",
        "apihelp-parse-param-disablelimitreport": "파서 출력에서 제한 보고서(\"NewPP limit report\")를 제외합니다.",
        "apihelp-parse-param-disablepp": "<var>$1disablelimitreport</var>를 대신 사용합니다.",
        "apihelp-parse-param-disableeditsection": "파서 출력에서 문단 편집 링크를 제외합니다.",
        "apihelp-parse-param-disabletidy": "파서 출력에서 HTML 정리(예: tidy)를 수행하지 않습니다.",
-       "apihelp-parse-param-preview": "미리 보기 모드에서 구문 분석을 합니다.",
-       "apihelp-parse-param-sectionpreview": "문단 미리 보기 모드에서 구문 분석을 합니다. (미리 보기 모드도 활성화함)",
+       "apihelp-parse-param-preview": "미리 보기 모드에서 파싱합니다.",
+       "apihelp-parse-param-sectionpreview": "문단 미리 보기 모드에서 파싱합니다. (미리 보기 모드도 활성화함)",
        "apihelp-parse-param-disabletoc": "출력에서 목차를 제외합니다.",
        "apihelp-parse-param-useskin": "선택한 스킨을 파서 출력에 적용합니다. 다음의 속성에 영향을 줄 수 있습니다: <kbd>langlinks</kbd>, <kbd>headitems</kbd>, <kbd>modules</kbd>, <kbd>jsconfigvars</kbd>, <kbd>indicators</kbd>.",
        "apihelp-parse-param-contentformat": "입력 텍스트에 사용할 내용 직렬화 포맷입니다. $1text와 함께 사용할 때에만 유효합니다.",
-       "apihelp-parse-example-page": "페이지의 구문을 분석합니다.",
+       "apihelp-parse-example-page": "페이지를 파싱합니다.",
        "apihelp-parse-example-text": "위키텍스트의 구문을 분석합니다.",
-       "apihelp-parse-example-summary": "요약을 구문 분석합니다.",
+       "apihelp-parse-example-summary": "요약을 변환합니다.",
        "apihelp-patrol-summary": "문서나 판을 점검하기.",
        "apihelp-patrol-param-rcid": "점검할 최근 바뀜 ID입니다.",
        "apihelp-patrol-param-revid": "점검할 판 ID입니다.",
        "apihelp-query+imageinfo-summary": "파일 정보와 업로드 역사를 반환합니다.",
        "apihelp-query+imageinfo-param-prop": "가져올 파일 정보입니다:",
        "apihelp-query+imageinfo-paramvalue-prop-timestamp": "업로드된 판에 대한 타임스탬프를 추가합니다.",
-       "apihelp-query+imageinfo-paramvalue-prop-parsedcomment": "판의 설명을 구문 분석합니다.",
+       "apihelp-query+imageinfo-paramvalue-prop-parsedcomment": "판의 설명을 변환합니다.",
        "apihelp-query+imageinfo-paramvalue-prop-sha1": "파일에 대한 SHA-1 해시를 추가합니다.",
        "apihelp-query+imageinfo-paramvalue-prop-mediatype": "파일의 미디어 유형을 추가합니다.",
        "apihelp-query+imageinfo-param-urlheight": "$1urlwidth와 유사합니다.",
        "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "판의 콘텐츠 모델 ID.",
        "apihelp-query+revisions+base-paramvalue-prop-content": "판의 텍스트.",
        "apihelp-query+revisions+base-paramvalue-prop-tags": "판의 태그.",
-       "apihelp-query+revisions+base-param-parse": "<kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>를 ë\8c\80ì\8b  ì\82¬ì\9a©í\95©ë\8b\88ë\8b¤. í\8c\90 ë\82´ì\9a©ì\9d\98 êµ¬ë¬¸ì\9d\84 ë¶\84ì\84\9d합니다. ($1prop=content 필요) 성능 상의 이유로 이 옵션을 사용할 경우 $1limit은 1로 강제됩니다.",
+       "apihelp-query+revisions+base-param-parse": "<kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>를 ë\8c\80ì\8b  ì\82¬ì\9a©í\95\98ì\84¸ì\9a\94. í\8c\90 ë\82´ì\9a©ì\9d\84 í\8c\8cì\8b±합니다. ($1prop=content 필요) 성능 상의 이유로 이 옵션을 사용할 경우 $1limit은 1로 강제됩니다.",
        "apihelp-query+search-summary": "전문 검색을 수행합니다.",
        "apihelp-query+search-param-qiprofile": "쿼리 독립적인 프로파일 사용(순위 알고리즘에 영향있음)",
        "apihelp-query+search-paramvalue-prop-size": "바이트 단위로 문서의 크기를 추가합니다.",
        "apierror-cantsend": "로그인하지 않았거나 인증된 이메일 주소가 없거나 다른 사용자로 이메일을 보낼 권한이 없기 때문에 이메일을 보낼 수 없습니다.",
        "apierror-cantview-deleted-description": "삭제된 파일의 설명을 볼 권한이 없습니다.",
        "apierror-cantview-deleted-metadata": "삭제된 파일의 메타데이터를 볼 권한이 없습니다.",
+       "apierror-cantview-deleted-revision-content": "삭제된 판의 내용을 볼 권한이 없습니다.",
        "apierror-compare-maintextrequired": "<var>$1slots</var>에 <kbd>main</kbd>이 포함되어 있다면 <var>$1text-main</var> 변수는 필수입니다. (메인 슬롯을 삭제할 수 없습니다)",
        "apierror-compare-notext": "<var>$1</var> 변수는 <var>$2</var> 없이 사용할 수 없습니다.",
        "apierror-emptynewsection": "비어있는 새 문단을 만들 수 없습니다.",
index b4e483b..d7c5a85 100644 (file)
@@ -683,16 +683,21 @@ TXT;
         * This method must not assume the exception is an MWException,
         * it is also used to handle PHP exceptions or exceptions from other libraries.
         *
-        * @since 1.22
         * @param Exception|Throwable $e
         * @param string $catcher CAUGHT_BY_* class constant indicating what caught the error
+        * @param array $extraData (since 1.34) Additional data to log
+        * @since 1.22
         */
-       public static function logException( $e, $catcher = self::CAUGHT_BY_OTHER ) {
+       public static function logException( $e, $catcher = self::CAUGHT_BY_OTHER, $extraData = [] ) {
                if ( !( $e instanceof MWException ) || $e->isLoggable() ) {
                        $logger = LoggerFactory::getInstance( 'exception' );
+                       $context = self::getLogContext( $e, $catcher );
+                       if ( $extraData ) {
+                               $context['extraData'] = $extraData;
+                       }
                        $logger->error(
                                self::getLogNormalMessage( $e ),
-                               self::getLogContext( $e, $catcher )
+                               $context
                        );
 
                        $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher );
index c27ab4f..92e276d 100644 (file)
@@ -96,9 +96,15 @@ class SectionProfiler {
        public function getFunctionStats() {
                $this->collateData();
 
-               $totalCpu = max( $this->end['cpu'] - $this->start['cpu'], 0 );
-               $totalReal = max( $this->end['real'] - $this->start['real'], 0 );
-               $totalMem = max( $this->end['memory'] - $this->start['memory'], 0 );
+               if ( is_array( $this->start ) ) {
+                       $totalCpu = max( $this->end['cpu'] - $this->start['cpu'], 0 );
+                       $totalReal = max( $this->end['real'] - $this->start['real'], 0 );
+                       $totalMem = max( $this->end['memory'] - $this->start['memory'], 0 );
+               } else {
+                       $totalCpu = 0;
+                       $totalReal = 0;
+                       $totalMem = 0;
+               }
 
                $profile = [];
                foreach ( $this->collated as $fname => $data ) {
index cd63796..403f5b2 100644 (file)
@@ -48,7 +48,7 @@ abstract class BaseTemplate extends QuickTemplate {
         * @deprecated since 1.33 Use ->msg() or ->getMsg() instead.
         */
        function msgWiki( $str ) {
-               // TODO: Add wfDeprecated( __METHOD__, '1.33' ) after 1.33 got released
+               wfDeprecated( __METHOD__, '1.33' ); // Hard-deprecated in 1.34
                echo $this->getMsg( $str )->parseAsBlock();
        }
 
index 1b0db73..a03455b 100644 (file)
@@ -1124,9 +1124,9 @@ class SpecialBlock extends FormSpecialPage {
         * @return bool
         */
        public static function canBlockEmail( UserIdentity $user ) {
-               global $wgEnableUserEmail, $wgSysopEmailBans;
+               global $wgEnableUserEmail;
 
-               return ( $wgEnableUserEmail && $wgSysopEmailBans && MediaWikiServices::getInstance()
+               return ( $wgEnableUserEmail && MediaWikiServices::getInstance()
                                ->getPermissionManager()
                                ->userHasRight( $user, 'blockemail' ) );
        }
index 13f2b52..d8e4985 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\User\UserIdentity;
 use Wikimedia\Assert\Assert;
@@ -70,16 +71,21 @@ class WatchedItemQueryService {
        /** @var WatchedItemStoreInterface */
        private $watchedItemStore;
 
+       /** @var PermissionManager */
+       private $permissionManager;
+
        public function __construct(
                ILoadBalancer $loadBalancer,
                CommentStore $commentStore,
                ActorMigration $actorMigration,
-               WatchedItemStoreInterface $watchedItemStore
+               WatchedItemStoreInterface $watchedItemStore,
+               PermissionManager $permissionManager
        ) {
                $this->loadBalancer = $loadBalancer;
                $this->commentStore = $commentStore;
                $this->actorMigration = $actorMigration;
                $this->watchedItemStore = $watchedItemStore;
+               $this->permissionManager = $permissionManager;
        }
 
        /**
@@ -549,7 +555,7 @@ class WatchedItemQueryService {
                return $conds;
        }
 
-       private function getUserRelatedConds( IDatabase $db, User $user, array $options ) {
+       private function getUserRelatedConds( IDatabase $db, UserIdentity $user, array $options ) {
                if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
                        return [];
                }
@@ -566,9 +572,11 @@ class WatchedItemQueryService {
 
                // Avoid brute force searches (T19342)
                $bitmask = 0;
-               if ( !$user->isAllowed( 'deletedhistory' ) ) {
+               if ( !$this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
                        $bitmask = RevisionRecord::DELETED_USER;
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+               } elseif ( !$this->permissionManager
+                       ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+               ) {
                        $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
                }
                if ( $bitmask ) {
@@ -578,13 +586,15 @@ class WatchedItemQueryService {
                return $conds;
        }
 
-       private function getExtraDeletedPageLogEntryRelatedCond( IDatabase $db, User $user ) {
+       private function getExtraDeletedPageLogEntryRelatedCond( IDatabase $db, UserIdentity $user ) {
                // LogPage::DELETED_ACTION hides the affected page, too. So hide those
                // entirely from the watchlist, or someone could guess the title.
                $bitmask = 0;
-               if ( !$user->isAllowed( 'deletedhistory' ) ) {
+               if ( !$this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
                        $bitmask = LogPage::DELETED_ACTION;
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+               } elseif ( !$this->permissionManager
+                       ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+               ) {
                        $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
                }
                if ( $bitmask ) {
index 0a15530..8fe3be3 100644 (file)
@@ -2982,7 +2982,7 @@ public static $zh2Hant = [
 '𫄨' => '絺',
 '𫄷' => '繶',
 '𫄸' => '纁',
-'ð«\87­' => 'è\94¿',
+'ð«\87­' => 'è\92\8d',
 '𫌀' => '襀',
 '𫌨' => '覼',
 '𫍙' => '訑',
@@ -3707,6 +3707,8 @@ public static $zh2Hant = [
 '于少保' => '于少保',
 '于山国' => '于山國',
 '于山國' => '于山國',
+'于山岛' => '于山島',
+'于山島' => '于山島',
 '于帅' => '于帥',
 '于帥' => '于帥',
 '于幼军' => '于幼軍',
@@ -3724,7 +3726,6 @@ public static $zh2Hant = [
 '于志寧' => '于志寧',
 '于忠肃集' => '于忠肅集',
 '于忠肅集' => '于忠肅集',
-'于思' => '于思',
 '于慎行' => '于慎行',
 '于慧' => '于慧',
 '于成龍' => '于成龍',
@@ -3817,6 +3818,7 @@ public static $zh2Hant = [
 '于都县' => '于都縣',
 '于都縣' => '于都縣',
 '于里察' => '于里察',
+'于闐' => '于闐',
 '于阗' => '于闐',
 '于双戈' => '于雙戈',
 '于雙戈' => '于雙戈',
@@ -4114,6 +4116,7 @@ public static $zh2Hant = [
 '党進' => '党進',
 '党項' => '党項',
 '党项' => '党項',
+'入侵并' => '入侵並',
 '内脏' => '內臟',
 '内制' => '內製',
 '内面包' => '內面包',
@@ -4470,6 +4473,8 @@ public static $zh2Hant = [
 '叮叮当当' => '叮叮噹噹',
 '叮当' => '叮噹',
 '可紧可松' => '可緊可鬆',
+'可能干預' => '可能干預',
+'可能干预' => '可能干預',
 '可自制' => '可自制',
 '可鉴' => '可鑑',
 '台子女' => '台子女',
@@ -4802,7 +4807,7 @@ public static $zh2Hant = [
 '大型钟面' => '大型鐘面',
 '大多只' => '大多只',
 '大伙' => '大夥',
-'大干' => '大幹',
+'大干一' => '大幹一',
 '大批涌到' => '大批湧到',
 '大折儿' => '大摺兒',
 '大明历' => '大明曆',
@@ -5527,6 +5532,8 @@ public static $zh2Hant = [
 '恶直丑正' => '惡直醜正',
 '恶斗' => '惡鬥',
 '惴栗' => '惴慄',
+'意大利面临' => '意大利面臨',
+'意大利面臨' => '意大利面臨',
 '意大利面' => '意大利麵',
 '愛河里花子' => '愛河里花子',
 '爱河里花子' => '愛河里花子',
@@ -5991,6 +5998,8 @@ public static $zh2Hant = [
 '方向' => '方向',
 '方法里' => '方法裡',
 '于吉林' => '於吉林',
+'于格林' => '於格林',
+'于越南' => '於越南',
 '于震中' => '於震中',
 '于震前' => '於震前',
 '于震后' => '於震後',
@@ -6474,6 +6483,8 @@ public static $zh2Hant = [
 '浅淀' => '淺澱',
 '清心寡欲' => '清心寡欲',
 '渠冲' => '渠衝',
+'温岚' => '温嵐',
+'温嵐' => '温嵐',
 '测不准' => '測不準',
 '港制' => '港製',
 '游离' => '游離',
@@ -6699,6 +6710,7 @@ public static $zh2Hant = [
 '版本里' => '版本裡',
 '牙签' => '牙籤',
 '牛只' => '牛隻',
+'牢里' => '牢裡',
 '物欲' => '物慾',
 '抵牾' => '牴牾',
 '抵触' => '牴觸',
@@ -7028,7 +7040,6 @@ public static $zh2Hant = [
 '称赞' => '稱讚',
 '稻谷' => '稻穀',
 '稽征' => '稽徵',
-'谷人' => '穀人',
 '谷保家商' => '穀保家商',
 '谷仓' => '穀倉',
 '谷圭' => '穀圭',
@@ -7656,6 +7667,7 @@ public static $zh2Hant = [
 '药签' => '藥籤',
 '药面儿' => '藥麵兒',
 '苏昆' => '蘇崑',
+'𬞟' => '蘋',
 '苹婆' => '蘋婆',
 '苹果' => '蘋果',
 '苹果干' => '蘋果乾',
@@ -7751,6 +7763,7 @@ public static $zh2Hant = [
 '袋表' => '袋錶',
 '袖里' => '袖裡',
 '被废后' => '被廢後',
+'被卷回' => '被捲回',
 '被系上' => '被繫上',
 '被里' => '被裡',
 '被夸' => '被誇',
@@ -9319,6 +9332,7 @@ public static $zh2Hant = [
 '鹰雕' => '鹰鵰',
 '鹰鵰' => '鹰鵰',
 '咸、甜' => '鹹、甜',
+'咸吃' => '鹹吃',
 '咸味' => '鹹味',
 '咸嘴淡舌' => '鹹嘴淡舌',
 '咸土' => '鹹土',
@@ -11458,6 +11472,7 @@ public static $zh2Hans = [
 '葦' => '苇',
 '葯' => '药',
 '葷' => '荤',
+'蒍' => '𫇭',
 '蒓' => '莼',
 '蒔' => '莳',
 '蒞' => '莅',
@@ -13699,6 +13714,7 @@ public static $zh2Hans = [
 '孫乾' => '孙乾',
 '宏碁' => '宏碁',
 '官陞' => '官升',
+'尋陞' => '寻升',
 '將軍抽俥' => '将军抽俥',
 '將軍抽車' => '将军抽車',
 '爾冬陞' => '尔冬升',
@@ -14152,7 +14168,10 @@ public static $zh2TW = [
 '克罗地亚' => '克羅埃西亞',
 '克羅地亞' => '克羅埃西亞',
 '克里斯托弗' => '克里斯多福',
+'全角' => '全形',
 '万维网' => '全球資訊網',
+'全角度' => '全角度',
+'全角色' => '全角色',
 '八杆' => '八桿',
 '公共交通' => '公共運輸',
 '六杆' => '六桿',
@@ -14259,6 +14278,7 @@ public static $zh2TW = [
 '塞维利亚' => '塞維亞',
 '西維爾' => '塞維亞',
 '塞黑' => '塞蒙',
+'多美和普林西比' => '多美普林西比',
 '塔希提' => '大溪地',
 '共和联邦' => '大英國協',
 '英联邦' => '大英國協',
@@ -14330,6 +14350,7 @@ public static $zh2TW = [
 '希拉里' => '希拉蕊',
 '希特拉' => '希特勒',
 '残疾人奥林匹克' => '帕拉林匹克',
+'殘疾人奧林匹克' => '帕拉林匹克',
 '残奥会' => '帕運會',
 '殘奧會' => '帕運會',
 '巴尔米拉环礁' => '帕邁拉環礁',
@@ -14413,6 +14434,7 @@ public static $zh2TW = [
 '施罗德' => '施洛德',
 '旱烟' => '旱菸',
 '旱煙' => '旱菸',
+'比勒陀利' => '普利托利',
 '普利策' => '普利茲',
 '普利策奖' => '普立茲獎',
 '芯片' => '晶片',
@@ -14429,6 +14451,8 @@ public static $zh2TW = [
 '马恩岛' => '曼島',
 '木杆' => '木桿',
 '尾班車' => '末班車',
+'萨格勒布' => '札格瑞布',
+'薩格勒布' => '札格瑞布',
 '列奥纳多' => '李奧納多',
 '杜塞尔多夫' => '杜塞道夫',
 '杜塞爾多夫' => '杜塞道夫',
@@ -14496,6 +14520,8 @@ public static $zh2TW = [
 '海洛英' => '海洛因',
 '侯賽因' => '海珊',
 '侯赛因' => '海珊',
+'温得和克' => '溫荷克',
+'溫得和克' => '溫荷克',
 '鼠标' => '滑鼠',
 '汉诺威' => '漢諾瓦',
 '漢诺威' => '漢諾瓦',
@@ -14607,13 +14633,12 @@ public static $zh2TW = [
 '卢浮宫' => '羅浮宮',
 '樂行童軍' => '羅浮童軍',
 '意大利' => '義大利',
+'意大利面' => '義大利麵',
 '昂山素姬' => '翁山蘇姬',
 '昂山素季' => '翁山蘇姬',
 '圣基茨和尼维斯' => '聖克里斯多福及尼維斯',
 '聖吉斯納域斯' => '聖克里斯多福及尼維斯',
 '聖佐治' => '聖喬治',
-'圣多美和普林西比' => '聖多美普林西比',
-'聖多美和普林西比' => '聖多美普林西比',
 '圣文森特和格林纳丁斯' => '聖文森及格瑞那丁',
 '聖文森特和格林納丁斯' => '聖文森及格瑞那丁',
 '圣赫勒拿' => '聖赫倫那',
@@ -14836,6 +14861,8 @@ public static $zh2TW = [
 '亚拉巴马' => '阿拉巴馬',
 '阿联酋' => '阿聯',
 '阿聯酋' => '阿聯',
+'亚的斯亚贝巴' => '阿迪斯阿貝巴',
+'亞的斯亞貝巴' => '阿迪斯阿貝巴',
 '罗纳德·里根' => '隆納·雷根',
 '私隱' => '隱私',
 '耶加達' => '雅加達',
@@ -14958,6 +14985,7 @@ public static $zh2HK = [
 '因特网' => '互聯網',
 '網際網路' => '互聯網',
 '井里' => '井裏',
+'阿迪斯阿貝巴' => '亞的斯亞貝巴',
 '亮著' => '亮着',
 '亮著《' => '亮著《',
 '亮著作' => '亮著作',
@@ -15340,6 +15368,9 @@ public static $zh2HK = [
 '柯林頓' => '克林頓',
 '克羅埃西亞' => '克羅地亞',
 '奈洛比' => '內羅畢',
+'全角' => '全形',
+'全角度' => '全角度',
+'全角色' => '全角色',
 '公布' => '公佈',
 '公寓里' => '公寓裏',
 '冒著' => '冒着',
@@ -16406,7 +16437,7 @@ public static $zh2HK = [
 '格瑞那達' => '格林納達',
 '格莱美奖' => '格林美獎',
 '葛萊美獎' => '格林美獎',
-'格鲁吉亚' => '格魯吉亞',
+'喬治亞字母' => '格魯吉亞字母',
 '框里' => '框裏',
 '台式电脑' => '桌上型電腦',
 '台球' => '桌球',
@@ -16462,6 +16493,7 @@ public static $zh2HK = [
 '殺著錄' => '殺著錄',
 '壳里' => '殼裏',
 '殿里' => '殿裏',
+'普利托利亞' => '比勒陀利亞',
 '茅利塔尼亞' => '毛里塔尼亞',
 '模里西斯' => '毛里裘斯',
 '毛里求斯' => '毛里裘斯',
@@ -16587,6 +16619,7 @@ public static $zh2HK = [
 '溢著者' => '溢著者',
 '溢著述' => '溢著述',
 '溢著錄' => '溢著錄',
+'溫荷克' => '溫得和克',
 '演著' => '演着',
 '演著作' => '演著作',
 '演著名' => '演著名',
@@ -16657,6 +16690,7 @@ public static $zh2HK = [
 '版图里' => '版圖裏',
 '版本里' => '版本裏',
 '版权信息' => '版權資訊',
+'牢里' => '牢裏',
 '千里達及托巴哥' => '特立尼達和多巴哥',
 '牽著' => '牽着',
 '牽著作' => '牽著作',
@@ -17123,6 +17157,7 @@ public static $zh2HK = [
 '聖克里斯多福及尼維斯' => '聖吉斯納域斯',
 '聖多美普林西比' => '聖多美和普林西比',
 '聖文森及格瑞那丁' => '聖文森特和格林納丁斯',
+'聖文森國' => '聖文森特和格林納丁斯',
 '聖露西亞' => '聖盧西亞',
 '聖馬利諾' => '聖馬力諾',
 '聽不著' => '聽不着',
@@ -17243,6 +17278,7 @@ public static $zh2HK = [
 '肖邦' => '蕭邦',
 '薛丁格' => '薛定諤',
 '塞拉耶佛' => '薩拉熱窩',
+'札格瑞布' => '薩格勒布',
 '萨达姆' => '薩達姆',
 '藉著' => '藉着',
 '藏著' => '藏着',
@@ -18106,6 +18142,7 @@ public static $zh2CN = [
 '網際網絡' => '互联网',
 '網際網路' => '互联网',
 '亞歷山卓' => '亚历山大',
+'阿迪斯阿貝巴' => '亚的斯亚贝巴',
 '雅穆索戈' => '亚穆苏克罗',
 '交帳' => '交账',
 '亮著' => '亮着',
@@ -18636,6 +18673,7 @@ public static $zh2CN = [
 '聖吉斯納域斯' => '圣基茨和尼维斯',
 '聖多美普林西比' => '圣多美和普林西比',
 '聖文森及格瑞那丁' => '圣文森特和格林纳丁斯',
+'聖文森國' => '圣文森特和格林纳丁斯',
 '聖馬利諾' => '圣马力诺',
 '蓋亞那' => '圭亚那',
 '坐著' => '坐着',
@@ -19413,6 +19451,7 @@ public static $zh2CN = [
 '格瑞那達' => '格林纳达',
 '格林美獎' => '格莱美奖',
 '葛萊美獎' => '格莱美奖',
+'喬治亞字母' => '格鲁吉亚字母',
 '森巴舞' => '桑巴舞',
 '梅赫西迪' => '梅赛德斯',
 '夢著' => '梦着',
@@ -19437,6 +19476,7 @@ public static $zh2CN = [
 '帕運會' => '残奥会',
 '帕拉林匹克' => '残疾人奥林匹克',
 '庇里牛斯' => '比利牛斯',
+'普利托利亞' => '比勒陀利亚',
 '披索' => '比索',
 '畢卡索' => '毕加索',
 '茅利塔尼亞' => '毛里塔尼亚',
@@ -19524,6 +19564,7 @@ public static $zh2CN = [
 '混帳' => '混账',
 '清澈' => '清澈',
 '清帳' => '清账',
+'溫荷克' => '温得和克',
 '渴著' => '渴着',
 '渴著書' => '渴著书',
 '渴著作' => '渴著作',
@@ -20108,6 +20149,7 @@ public static $zh2CN = [
 '獲著述' => '获著述',
 '菁寮' => '菁寮',
 '塞拉耶佛' => '萨拉热窝',
+'札格瑞布' => '萨格勒布',
 '落著' => '落着',
 '落著書' => '落著书',
 '落著作' => '落著作',
index 7d93d5b..6d48938 100644 (file)
        "createaccountmail": "استخدم كلمة سر عشوائية مؤقتة وارسلها إلى عنوان البريد الإلكتروني المحدد أدناه",
        "createaccountmail-help": "يمكن استخدامه لإنشاء حساب لشخص آخر من دون معرفة كلمة المرور.",
        "createacct-realname": "الاسم الحقيقي (اختياري)",
-       "createacct-reason": "السبب",
+       "createacct-reason": "السبب (مسجل بشكل عام)",
        "createacct-reason-ph": "لماذا تقوم بإنشاء حساب آخر",
        "createacct-reason-help": "رسالة تظهر في سجل إنشاء الحسابات",
        "createacct-submit": "افتح الحساب",
index b142bb5..60fa0a1 100644 (file)
        "minoredit": "Puniki uahan alit",
        "watchthis": "Awasin kaca puniki",
        "savearticle": "Raksa kaca",
-       "publishpage": "Terbitang kaca",
-       "publishchanges": "Wedah uahan",
+       "publishpage": "Wedar kaca",
+       "publishchanges": "Wedar uahan",
        "savearticle-start": "Raksa kaca...",
-       "publishpage-start": "Terbitang kaca…",
-       "publishchanges-start": "Wedah uahan...",
+       "publishpage-start": "Wedar kaca…",
+       "publishchanges-start": "Wedar uahan...",
        "preview": "Pracingak",
-       "showpreview": "Édengang pracingak",
+       "showpreview": "Édéngang pracingak",
        "showdiff": "Cingak uahan",
        "anoneditwarning": "<strong>Pingetan:</strong> Ida dané nénten kacatet ngranjing. Alamat IP ida dané jagi kacatet ring sejarah (indik sané dumunan) ring lembar puniki. Yening ida dane <strong>[$1 log in]</strong> utawi <strong>[$2 create an account]</strong>, your edits will be attributed to your username, along with other benefits.",
        "blockedtext": "<strong>Peséngan penganggé utawi genah IP ragané kablokir .</strong>\n\nBlokir puniki kalaksanayang olih $1.\nKablokir krana <em>$2</em>.\n\n* Kablokir saking: $8\n* Blokir kadaluwarsa ring: $6\n* Tetujon ngablokir: $7\n\nRagané dados ngubungin $1 utawi [[{{MediaWiki:Grouppage-sysop}}|prajuru]] antuk mabligbagin.\nRagané nénten dados nganggén fitur puniki \"{{int:emailuser}}\" sajabaning ragané sampun ngranjingang email sané patut ring [[Special:Preferences|pustaka]] tur ragané nénten kablokir antuk nganggén\nGenah IP ragané sané pinih anyar inggih punika $3, tur ID pamblokiran inggih punika #$5.\nTulung genahang silih sinunggil utawi kalih pidarta ring pitakén sané kakaryanin.",
index 5b83833..e875f19 100644 (file)
@@ -78,7 +78,7 @@
        "tog-norollbackdiff": "Peyser ardışi ra dıme ferqi measne",
        "tog-useeditwarning": "Wexto ke mı yew pela nizami be vurnayışanê nêqeydbiyayeyan caverdê, hay be mı ser de",
        "tog-prefershttps": "Ronışten akerden de tım greyo itimadın bıkarne",
-       "tog-requireemail": "Parolay reset kerdışi rê email lazıma",
+       "tog-requireemail": "Parolapeysernayene rê email lazımo",
        "underline-always": "Tım",
        "underline-never": "Qet",
        "underline-default": "Cild ya zi cı geyrayoğo hesebiyaye",
        "nstab-mediawiki": "Mesac",
        "nstab-template": "Şablon",
        "nstab-help": "Perra pasti",
-       "nstab-category": "Kategori",
+       "nstab-category": "Kategoriye",
        "mainpage-nstab": "Pera seri",
        "nosuchaction": "Fealiyeto wınasi çıniyo",
        "nosuchactiontext": "URL ra kar qebul nêbı.\nŞıma belka URL şaş nuşt, ya zi gıreyi şaş ra ameyi.\nKeyepelê {{SITENAME}} eşkeno xeta aşkera bıkero.",
        "virus-scanfailed": "cıgerayiş tamam nêbı (kod $1)",
        "virus-unknownscanner": "antiviruso ke nêzanyeno:",
        "logouttext": "'''Henda şıma hesab ra veciyay.'''\n\nDiqat kerê ke tayê perri şenê hewna zey şıma kewtê ra cı bıasê, heta şıma ver-virê şanekerê (browserê) xo besterê.",
-       "logging-out-notify": "Şıma yê abıriyenê, reca kem bıpawê",
+       "logging-out-notify": "Şımayê cı ra veciyê, kerem kerê bıpawên.",
        "logout-failed": "Enewke ronıştışo nêracneyêno:$1",
        "cannotlogoutnow-title": "Enewke ronıştışo nêracneyêno",
        "cannotlogoutnow-text": "Gurenayışê $1i de veciyayış mımkın niyo.",
        "rcfilters-filter-showlinkedfrom-label": "Gıreyê pelan ra vurnayışan bıvêne",
        "rcfilters-target-page-placeholder": "Yew nameyê pele (ya zi kategoriye) cı kerê",
        "rcfilters-allcontents-label": "Zerrek pêro",
-       "rcfilters-alldiscussions-label": "Werênayışi pero",
+       "rcfilters-alldiscussions-label": "Werênayışi pêro",
        "rcnotefrom": "Cêr de <strong>$2</strong> ra nata {{PLURAL:$5|vurnayışiyê}} asenê (tewr vêşi <strong>$1</strong> asenê) <strong>$3, $4</strong>",
        "rclistfromreset": "Weçinayışê tarixi ragoze",
        "rclistfrom": "$3 sehat $2 ra tepiya vurnayışanê neweyan bımotne",
        "undelete-search-title": "Bıgeyre pelanê eserıtiyan",
        "undelete-search-box": "bıgêr pelê hewn a biyayeyani",
        "undelete-search-prefix": "pel ê ke pê ney destpêkenî, ramocın",
-       "undelete-search-full": "Zerreki ra serniya pele bımocne",
+       "undelete-search-full": "Zerrekê sernameyanê pele bımocne:",
        "undelete-search-submit": "Cı geyre",
        "undelete-no-results": "Zerre arşîvê esterayîşî de peleyan match nibiyê.",
        "undelete-filename-mismatch": "Vurnayîşê ke pê wextê puli ye $1î nieşkenî biyare: nameyê dosyayî match nibeno",
        "ipb-confirm": "Bloke kerdışi tesdik ke",
        "ipb-sitewide": "Site hemi de",
        "ipb-partial": "Qısmi",
-       "ipb-partial-help": "Peli ya zi namey cayanê spesifikan",
+       "ipb-partial-help": "Peli ya zi nameyê cayanê spesifikan.",
        "ipb-pages-label": "Peli",
        "ipb-namespaces-label": "Heruna nameyan",
        "badipaddress": "Adresê IPî raşt niyo",
        "ipb-blocklist": "Blokî ke hama estê ey bivîne",
        "ipb-blocklist-contribs": "İştirakê {{GENDER:$1|$1}}`i",
        "ipb-blocklist-duration-left": "$1 vet",
-       "block-actions": "Hereketê bloqey :",
+       "block-actions": "Hereketê kıliti:",
        "block-expiry": "Qedyayış:",
-       "block-options": "Weçinıtış dekerê:",
+       "block-options": "Weçinıtışê vêşi:",
        "block-prevent-edit": "Vurnayış",
        "block-reason": "Sebeb:",
        "block-target": "Nameyê karberi ya zi adresa eposteyi",
        "unblocked": "[[User:$1|$1]] blok biyo",
        "unblocked-range": "Blokey $1'i wederya",
        "unblocked-id": "Blokê $1î wedariyayo",
-       "unblocked-ip": "[[Special:Contributions/$1|$1]] bloqe wedarnayo.",
+       "unblocked-ip": "Kılitê [[Special:Contributions/$1|$1]] dariyo we.",
        "blocklist": "Karberê kılitbiyayey",
        "autoblocklist": "Blokeyê otomatiki",
        "autoblocklist-submit": "Cı geyre",
        "autoblocklist-legend": "Lista blokanê otomatikan",
        "autoblocklist-localblocks": "{{PLURAL:$1|otoblokoyo lokal|otoblokeyê lokali}}",
-       "autoblocklist-total-autoblocks": "Amardışê pêroyê autobloqey:$1",
+       "autoblocklist-total-autoblocks": "Amarê kılitkerdışê xoseri pêro piya: $1",
        "autoblocklist-otherblocks": "{{PLURAL:$1|otobloqeyo bin|otobloqeyê bini}}",
        "ipblocklist": "Karberê kılitbiyayey",
        "ipblocklist-legend": "Karberê kılit biyayey bıvin",
        "javascripttest-qunit-intro": "Mediawiki.org dı [dokumanê $1] bıvinê.",
        "tooltip-pt-userpage": "Pela {{GENDER:|şımaya karberi}}",
        "tooltip-pt-anonuserpage": "pelê karberê IPyi",
-       "tooltip-pt-mytalk": "Pera {{GENDER:|şıma}}y vateni",
+       "tooltip-pt-mytalk": "Pela {{GENDER:|toya}} werênayışi",
        "tooltip-pt-anontalk": "'''Ena adresa IP ra vurnayışa sero qal bıqerê'''",
        "tooltip-pt-preferences": "Tercihê {{GENDER:|şıma}}",
        "tooltip-pt-watchlist": "Listey peranê ke to gırotê seyr kerdış",
        "watchlistedit-clear-legend": "Lista serykerdışê pak kerê",
        "watchlistedit-clear-explain": "Listeya serykerdış da şıma dı sernamey pêro besteryay",
        "watchlistedit-clear-titles": "Sernamey:",
-       "watchlistedit-clear-done": "Lista seyrkerdişê şıma biya pak",
+       "watchlistedit-clear-done": "Lista seyrkerdişê şıma biya pak.",
        "watchlisttools-clear": "Lista serykerdışê xo pak kı",
        "watchlisttools-view": "Vurnayışanê elaqedaran bıvêne",
        "watchlisttools-edit": "Lista seyrkerdışi bıvêne û bıvurne",
        "permanentlink-submit": "Şo revizyoni",
        "newsection": "Leteyo newe",
        "newsection-page": "Etiketê pele",
-       "newsection-submit": "Ravêr pele",
+       "newsection-submit": "Şo be pele",
        "dberr-problems": "Mayê muxulêm! Ena sita dı newke xırabiya teknik esta.",
        "dberr-again": "Dı-rê deqiqeyi vınde û heni bar ke.",
        "dberr-info": "(Erzmelumati ra xızmetkari nêreseno: $1)",
index f5af2c5..69d4fe2 100644 (file)
        "mycustomjsredirectprotected": "You do not have permission to edit this JavaScript page because it is a redirect and it does not point inside your userspace.",
        "easydeflate-invaliddeflate": "Content provided is not properly deflated",
        "unprotected-js": "For security reasons JavaScript cannot be loaded from unprotected pages. Please only create javascript in the MediaWiki: namespace or as a User subpage",
-       "userlogout-continue": "Do you want to log out?"
+       "userlogout-continue": "Do you want to log out?",
+       "rest-prefix-mismatch": "The requested path ($1) was not inside the REST API root path ($2)",
+       "rest-wrong-method": "The request method ($1) was not {{PLURAL:$3|the allowed method for this path|one of the allowed methods for this path}} ($2)",
+       "rest-no-match": "The requested relative path ($1) did not match any known handler"
 }
index 23871cd..73b2b7b 100644 (file)
        "exif-urgency": "Urgensy",
        "exif-fixtureidentifier": "Grupsname",
        "exif-locationdest": "Weadergeaven lokaty",
-       "exif-locationdestcode": "Kode veur de weeregeven lokasie",
-       "exif-objectcycle": "Tied van de dag waor de media veur bedoeld is",
-       "exif-contact": "Kontaktgegevens",
-       "exif-writer": "Schriever",
-       "exif-languagecode": "Taal",
-       "exif-iimversion": "IIM-versie",
-       "exif-iimcategory": "Kategorie",
-       "exif-iimsupplementalcategory": "Anvullende kategorieën",
-       "exif-datetimeexpires": "Niet te gebruken nao",
-       "exif-datetimereleased": "Uutebröcht op",
-       "exif-originaltransmissionref": "Oorspronkelike taaklokasiekode",
+       "exif-locationdestcode": "Kode vöär de weadergeaven lokaty",
+       "exif-objectcycle": "Tyd van de dag wår de media vöär bedoold is",
+       "exif-contact": "Kontaktgegeavens",
+       "exif-writer": "Skryver",
+       "exif-languagecode": "Språke",
+       "exif-iimversion": "IIM-versy",
+       "exif-iimcategory": "Kategory",
+       "exif-iimsupplementalcategory": "Anvüllende kategoryen",
+       "exif-datetimeexpires": "Neet te bruken nå",
+       "exif-datetimereleased": "Uutbröcht up",
+       "exif-originaltransmissionref": "Oorsprungelike språklokatykode",
        "exif-identifier": "ID",
-       "exif-lens": "Lenze die gebruukt wörden",
-       "exif-serialnumber": "Serienummer van de camera",
-       "exif-cameraownername": "Eigenaar van camera",
+       "exif-lens": "Brukede lense",
+       "exif-serialnumber": "Serynummer van de kamera",
+       "exif-cameraownername": "Eigenaar van kamera",
        "exif-label": "Etiket",
-       "exif-datetimemetadata": "Daotum waorop de metadata veur t lest bie-ewörken bin",
-       "exif-nickname": "Informele naam van de aofbeelding",
-       "exif-rating": "Werdering (op n schaole van 5)",
-       "exif-rightscertificate": "Rechtenbeheercertificaot",
-       "exif-copyrighted": "Auteursrechtstaotus",
-       "exif-copyrightowner": "Auteursrechtenhouwer",
-       "exif-usageterms": "Gebruuksveurweerden",
-       "exif-webstatement": "Internetauteursrechverklaoring",
-       "exif-originaldocumentid": "Uniek ID van t originele dokument",
-       "exif-licenseurl": "Webadres veur auteursrechlisensie",
-       "exif-morepermissionsurl": "Alternatieve lisensiegegevens",
-       "exif-attributionurl": "Gebruuk de volgende verwiezing bie hergebruuk van dit wark",
-       "exif-preferredattributionname": "Gebruuk de volgende makersvermelding bie hergebruuk van dit wark",
+       "exif-datetimemetadata": "Dåtum wårup de metadata vöär et lätst bywarked is",
+       "exif-nickname": "Informele name van de afbealding",
+       "exif-rating": "Wardering (up en skale van 5)",
+       "exif-rightscertificate": "Rechtenbeheyrcertifikaat",
+       "exif-copyrighted": "Autöörsrechtenståtus",
+       "exif-copyrightowner": "Autöörsrechtenholder",
+       "exif-usageterms": "Bruuksbeding",
+       "exif-webstatement": "Binnennetse autöörsrechtenverklåring",
+       "exif-originaldocumentid": "Unik ID van et originele dokument",
+       "exif-licenseurl": "Webadres vöär autöörsrechtenlicensy",
+       "exif-morepermissionsurl": "Alternative licensygegeavens",
+       "exif-attributionurl": "Bruuk de volgende verwysing as dit wark herbruked wördt",
+       "exif-preferredattributionname": "Bruuk de volgende makersvermelding as dit wark herbruked wördt",
        "exif-pngfilecomment": "Opmarking bie PNG-bestaand",
        "exif-disclaimer": "Vöärbehold",
        "exif-contentwarning": "Waorschuwing over inhoud",
index 2cd71f0..6c5d8b5 100644 (file)
        "createaccountmail": "Utiliser un mot de passe aléatoire temporaire et l’envoyer à l’adresse de courriel spécifiée",
        "createaccountmail-help": "Peut être utilisé pour créer un compte pour une autre personne sans connaître le mot de passe.",
        "createacct-realname": "Nom réel (facultatif)",
-       "createacct-reason": "Motif",
+       "createacct-reason": "Motif (connecté publiquement)",
        "createacct-reason-ph": "Pourquoi créez-vous un autre compte",
        "createacct-reason-help": "Message affiché dans le journal de création de compte",
        "createacct-submit": "Créez votre compte",
index f31034c..a00e5fb 100644 (file)
        "accountcreated": "Akun wis kagawé",
        "accountcreatedtext": "Akun panganggo [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|rembug]]) wis digawé.",
        "createaccount-title": "Gawé akun kanggo {{SITENAME}}",
-       "createaccount-text": "Ana kang nggawé akun nganggo alamat layang-èlé panjenengan ing {{SITENAME}} ($4) kanthi aran \"$2\", mawa tembung wadi \"$3\".\nPanjenengan kudu mlebu log lan ngowahi tembung wadiné panjenengan saiki.\n\nPanjenengan kena nglirwakaké layang iki, manawa akun iki ginawé awit kaluputan.",
+       "createaccount-text": "Ana kang nggawé akun nganggo alamat layang èlèktronikmu ing {{SITENAME}} ($4) aran \"$2\", mawa tembung wadi \"$3\".\nKowé kudu mlebu log lan ngowahi tembung wadimu saiki.\n\nKowé kena nglirwakaké layang iki, manawa akun iki kagawé ora sengaja amarga luput.",
        "login-throttled": "Kowé wis njajal mlebu log ping akèh.\nEntènana $1 sadurung njajal manèh.",
        "login-abort-generic": "Kowé ora bisa mlebu log - Kawurungaké",
        "login-migrated-generic": "Akunmu wis ingalihan, lan jeneng panganggomu wis ora ana manèh ing wiki iki.",
        "loginlanguagelabel": "Basa: $1",
        "suspicious-userlogout": "Panjalukmu supaya metu log tinulak amarga panjaluké kakirim saka pangluru kang rusak utawa proksi telih.",
-       "createacct-another-realname-tip": "Jeneng asli ora kudu diisi.\nYèn diisi, jeneng asliné panjenengan bakal kanggo atribusi awit karyané panjenengan.",
+       "createacct-another-realname-tip": "Jeneng asli ora kudu kaisi.\nYèn koisi, jeneng asliné bakal kaanggo ing atribusi tumrap karyané.",
        "pt-login": "Mlebu log",
        "pt-login-button": "Mlebu log",
        "pt-login-continue-button": "Banjuraké mlebu log",
        "user-mail-no-addy": "Njajal ngirim layang èlèktronik tanpa alamat layang èlèktronik.",
        "user-mail-no-body": "Nyoba ngirim layang e-mail, tapi isine kosong.",
        "changepassword": "Ganti tembung wadi",
-       "resetpass_announce": "Saperlu ngrampungaké olèhé mlebu log, panjenengan kudu nggawé tembung wadi anyar.",
+       "resetpass_announce": "Saperlu ngrampungaké olèhé mlebu log, kowé kudu nggawé tembung wadi anyar.",
        "resetpass_text": "<!-- Tambahaké teks ing kéné -->",
        "resetpass_header": "Ganti tembung wadining akun",
        "oldpassword": "Tembung wadi lawas:",
        "newpassword": "Tembung wadi anyar:",
        "retypenew": "Isi manèh tembung wadi anyaré:",
        "resetpass_submit": "Setèl tembung wadi lan mlebu log",
-       "changepassword-success": "Tembung wadiné panjenengan kasil diowah!",
-       "changepassword-throttled": "Panjenengan wis kakèhan njajal mlebu log.\nTulung nunggu dhisik $1 sadurungé njajal manèh.",
+       "changepassword-success": "Tembung wadimu bisa ingowahan!",
+       "changepassword-throttled": "Kowé wis njajal mlebu log ping akèh.\nEntènana $1 sadurung njajal manèh.",
        "botpasswords": "Tembung wadi bot",
        "botpasswords-disabled": "Tembung wadiné bot dipatèni.",
-       "botpasswords-no-central-id": "Saperlu nganggo tembung wadiné bot, panjenengan kudu mlebu log menyang akun séntral.",
+       "botpasswords-no-central-id": "Saperlu nganggo tembung wadiné bot, kowé kudu mlebu log akun séntral.",
        "botpasswords-existing": "Tembung wadiné bot kang cumepak",
        "botpasswords-createnew": "Gawé anyar tembung wadiné bot",
        "botpasswords-editexisting": "Besut tembung wadiné bot kang anyar",
-       "botpasswords-label-needsreset": "(tembung wadi kudu panjenengan ambali setèl)",
+       "botpasswords-label-needsreset": "(tembung wadi kudu kasetèl ulang)",
        "botpasswords-label-appid": "Jeneng bot:",
        "botpasswords-label-create": "Gawé",
        "botpasswords-label-update": "Anyari",
        "botpasswords-bad-appid": "Jeneng bot \"$1\" ora trep.",
        "botpasswords-insert-failed": "Wurung nambah jeneng bot \"$1\". Apa wis tinambahaké sadurungé?",
        "resetpass_forbidden": "Tembung wadi ora bisa diganti",
-       "resetpass-no-info": "Panjenengan kudu mlebu log saperlu langsung ngaksès kaca iki.",
+       "resetpass-no-info": "Kowé kudu mlebu log saperlu ngaksès kaca iki langsung.",
        "resetpass-submit-loggedin": "Ganti tembung wadi",
        "resetpass-submit-cancel": "Wurung",
-       "resetpass-wrong-oldpass": "Tembung wadi saiki utawa sauntara ora trep.\nPanjengen bokmanawa wis ngganti tembung wadiné panjenengan utawa nyuwun tembung wadi sauntara kang anyar.",
+       "resetpass-wrong-oldpass": "Tembung wadi saiki utawa sadhéla ora trep.\nKowé bokmanawa wis ngganti tembung wadimu utawa njaluk tembung wadi sadhéla kang anyar.",
        "resetpass-temp-password": "Tembung wadi sauntara:",
        "resetpass-abort-generic": "Ngowahi tembung wadi kawurungaké déning èkstènsi.",
        "resetpass-expired": "Tembung wadimu wis kadaluwarsa. Setèl tembung wadi anyar saperlu mlebu log.",
        "userrights-reason": "Alesan:",
        "userrights-no-interwiki": "Panjenengan ora ana hak kanggo ngowahi hak panganggo ing wiki liyané.",
        "userrights-nodatabase": "Basis dhatah $1 ora ana utawa ora enggonan.",
-       "userrights-changeable-col": "Grup kang bisa panjenengan owahi",
-       "userrights-unchangeable-col": "Grup kang ora bisa diowahi panjenengan",
+       "userrights-changeable-col": "Golongan kang bisa koowahi",
+       "userrights-unchangeable-col": "Golongan kang ora bisa koowahi",
        "userrights-irreversible-marker": "$1*",
        "userrights-no-shorten-expiry-marker": "$1#",
        "userrights-expiry-current": "Kadaluwarsa $1",
        "userrights-expiry-options": "1 dina:1 dina,1 minggu:1 minggu,1 wulan:1 wulan,3 wulan:3 wulan,6 wulan:6 wulan,1 taun:1 taun",
        "userrights-invalid-expiry": "Wektu kadaluwarsa golongan \"$1\" ora trep.",
        "userrights-expiry-in-past": "Wektu kadaluwarsa golongan \"$1\" ana ing kala kawuri.",
-       "userrights-conflict": "Konflik pangowahan hak-hak panganggo! Tulung ditinjau maneh lan konfirmasi owah-owahané panjenengan.",
+       "userrights-conflict": "Cengkah owahan hak panganggo! Mangga priksa lan konfirmasi owahanmu.",
        "group": "Golongan:",
        "group-user": "Para panganggo",
        "group-autoconfirmed": "Panganggo kang otomatis kakonfirmasi",
        "right-editmyusercss": "Besut berkas CSS panganggomu",
        "right-editmyuserjson": "Besut berkas JSON panganggomu",
        "right-editmyuserjs": "Besut berkas JavaScript panganggomu",
-       "right-viewmywatchlist": "Deleng pawawangané panjenengan",
+       "right-viewmywatchlist": "Deleng pawawanganmu",
        "right-editmywatchlist": "Owahi pawawangané panjenengan. Cathetan: ana cara liyane kanggo nambahi kaca menyang pratélan, sanadyan ora duwe hak iki.",
        "right-viewmyprivateinfo": "Deleng dhata prianggané panjenengan dhéwé (kaya ta alamat layang-èl, jeneng asli)",
        "right-editmyprivateinfo": "Besut dhata prianggané panjenengan dhéwé (kaya ta alamat layang-èl, jeneng asli)",
-       "right-editmyoptions": "Owahi pilalané panjenengan",
+       "right-editmyoptions": "Besut pilalanmu dhéwé",
        "right-rollback": "Balèkaké kanthi gelis besutaning panganggo pungkasan kang mbesut kaca tinamtu",
        "right-markbotedits": "Tandhani besutan kang kawurungan yèn besutan bot",
        "right-noratelimit": "Ora kadayan watesing cacah besutan.",
        "right-sendemail": "Ngirim layang listrik (e-mail) menyang panganggo liya",
        "grant-group-page-interaction": "Srawungan karo kaca",
        "grant-group-file-interaction": "Srawungan karo médhia",
-       "grant-group-watchlist-interaction": "Srawungan karo pawawangané panjenengan",
+       "grant-group-watchlist-interaction": "Srawung karo pawawanganmu",
        "grant-group-email": "Kirim layang-èl",
        "grant-group-high-volume": "Ngayahi kagiyatan kang akih",
        "grant-group-customization": "Panglarasan lan pilalan",
        "grant-group-administration": "Ngayahi laku administratif",
-       "grant-group-private-information": "Ngaksès dhata pribadhi ngenani panjenengan",
+       "grant-group-private-information": "Aksès dhatah pribadi bab kowé",
        "grant-group-other": "Kagiyatan rena-rena",
        "grant-blockusers": "Blokir lan uculaké blokirané panganggo",
        "grant-createaccount": "Gawé akun",
        "grant-editinterface": "Besut jagad aran MediaWiki lan CSS/JavaScript panganggo",
        "grant-editmycssjs": "Besut CSS/JavaScript panganggomu",
        "grant-editmyoptions": "Besut préferènsi panganggomu",
-       "grant-editmywatchlist": "Besut pawawangané panjenengan",
+       "grant-editmywatchlist": "Besut pawawangmu",
        "grant-editpage": "Besut kaca kang ana",
        "grant-editprotected": "Besut kaca kang kareksa",
        "grant-highvolume": "Besutan gedhi",
        "grant-uploadfile": "Unggah berkas anyar",
        "grant-basic": "Hak pokok",
        "grant-viewdeleted": "Deleng berkas lan kaca kang kabusek",
-       "grant-viewmywatchlist": "Deleng pawawangané panjenengan",
+       "grant-viewmywatchlist": "Deleng pawawanganmu",
        "grant-viewrestrictedlogs": "Deleng isian log kang winates",
        "newuserlogpage": "Log panganggo anyar",
        "newuserlogpagetext": "Ing ngisor iki kapacak log pandaftaran panganggo anyar.",
        "action-import": "impor kaca saka wiki liyané",
        "action-importupload": "impor kaca saka unggahan berkas",
        "action-patrol": "nandhani besutan wong liya yèn wis kapriksa",
-       "action-autopatrol": "nandhani besutané panjenengan dhéwé yèn wis kapriksa",
+       "action-autopatrol": "nandhani besutanmu yèn wis kapriksa",
        "action-unwatchedpages": "deleng pratélan kaca kang ingawasan",
        "action-mergehistory": "nggabungaké sajarah kaca iki",
        "action-userrights": "besut kabèh hak panganggo",
        "action-userrights-interwiki": "besut hak aksès panganggo ing wiki liyané",
        "action-siteadmin": "nggembok utawa mbukak gembok basis dhatah",
        "action-sendemail": "kirim layang-èl",
-       "action-editmyoptions": "besut pilalané panjenengan",
-       "action-editmywatchlist": "owahi pawawangané panjenengan",
+       "action-editmyoptions": "besut pilalanmu",
+       "action-editmywatchlist": "besut pawawanganmu",
        "action-viewmywatchlist": "deleng pawawangané panjenengan",
        "action-viewmyprivateinfo": "deleng katerangan prianggané panjenengan",
        "action-editmyprivateinfo": "besut katerangan prianggané panjenengan",
        "rcfilters-filterlist-noresults": "Saringan ora katemu",
        "rcfilters-noresults-conflict": "Ora ana kasil amarga wewatoné kanggo nggolèk ana masalah",
        "rcfilters-filtergroup-authorship": "Pangripta besutan",
-       "rcfilters-filter-editsbyself-label": "Owah-owahané panjenengan",
+       "rcfilters-filter-editsbyself-label": "Owah-owahanmu",
        "rcfilters-filter-editsbyself-description": "Pasumbangmu dhéwé.",
        "rcfilters-filter-editsbyother-label": "Owah-owahané liyan",
-       "rcfilters-filter-editsbyother-description": "Kabèh owahan kajaba duwèké panjenengan.",
+       "rcfilters-filter-editsbyother-description": "Kabèh owahan kajaba duwému.",
        "rcfilters-filtergroup-user-experience-level": "Pandhaftaran lan pangalaman pangguna",
        "rcfilters-filter-user-experience-level-registered-label": "Kadhaftar",
        "rcfilters-filter-user-experience-level-registered-description": "Pambesut kang mlebu log.",
        "rcfilters-filter-major-description": "Besutan kang ora ditandhani minangka besutan cilik.",
        "rcfilters-filtergroup-watchlist": "Kaca ing pawawangan",
        "rcfilters-filter-watchlist-watched-label": "Ana ing Pawawangan",
-       "rcfilters-filter-watchlist-watched-description": "Owahané kaca-kaca ing Pawawangané panjenengan.",
+       "rcfilters-filter-watchlist-watched-description": "Owahaning kaca-kaca ing Pawawanganmu.",
        "rcfilters-filter-watchlist-watchednew-label": "Owah-owahané Pawawangan anyar",
        "rcfilters-filter-watchlist-watchednew-description": "Owah-owahan ngenani kaca-kaca ing Pawawangané panjenengan kang durung ditiliki.",
        "rcfilters-filter-watchlist-notwatched-label": "Ora ana ing Pawawangan",
        "filetype-unwanted-type": "<strong>\".$1\"</strong> iku jinis barkas kang ora kapéngini.\nAluwung {{PLURAL:$3|jinis barkasé}} $2.",
        "filetype-banned-type": "<strong>\".$1\"</strong> {{PLURAL:$4|dudu jinis barkas kang diidinaké}}.\n{{PLURAL:$3|Jinis barkas}} kang diidinaké $2.",
        "filetype-missing": "Barkas ini ora duwé ekstènsi (contoné \".jpg\").",
-       "empty-file": "Barkas kang panjenengan lebokaké kosong.",
-       "file-too-large": "Barkas kang panjenengan lebokaké kagedhèn.",
+       "empty-file": "Berkas kang kolebokaké kosong.",
+       "file-too-large": "Berkas kang kolebokaké kegedhèn.",
        "filename-tooshort": "Jeneng barkas kecendhèken.",
        "filetype-banned": "Barkas jinis iki dilarang.",
        "verification-error": "Barkas iki ora lulus vèrifikasi.",
-       "hookaborted": "Owahan kang panjenengan ayahi diwurungaké déning èkstènsi.",
+       "hookaborted": "Olèhmu ndandani diwurungaké èstènsi.",
        "illegal-filename": "Jeneng barkas ora diidinaké.",
        "overwrite": "Nibani barkas kang wis ana ora dililakaké.",
        "unknown-error": "Ana masalah kang ora dingertèni.",
        "tmp-write-error": "Ora bisa nulis barkas sauntara.",
        "large-file": "Ukuran barkas disaranaké supaya ora ngluwihi $1 bita; barkas iki ukurané $2 bita.",
        "largefileserver": "Barkas iki luwih gedhé tinimbang kang diidinaké ing paladèn.",
-       "emptyfile": "Barkas kang panjenengan unggahaké katoné kosong. Bokmanawa iki amarga anané salah ketik ing jeneng barkas. Mangga dipastèkaké apa panjenengan pancèn kersa ngunggahaké barkas iki.",
+       "emptyfile": "Berkas kang kounggah katon kosong.\nBokmanawa ana salah tik ing aran berkasé.\nMangga pesthèkaké yèn kowé pancèn péngin ngunggah berkas iki.",
        "windows-nonascii-filename": "Wiki iki ora nyengkuyung jeneng berkas mawa karakter kusus.",
        "fileexists": "Barkas mawa jeneng iku wis ana, mangga dipriksa <strong>[[:$1]]</strong> yèn panjenengan ora yakin sumedya ngowahiné.\n[[$1|thumb]]",
        "filepageexists": "Kaca dhèskripsi kanggo berkas iki wis digawé ing <strong>[[:$1]]</strong>, nanging saiki iki ora tinemu berkas mawa jeneng iku. Ringkesan kang panjenengan lebokaké ora bakal metu ing kaca dhèskripsi. Kanggo ngetokaké dhèskripsi iki, panjenengan kudu mbesut kanthi manual. [[$1|thumb]]",
index 769891d..2068d7a 100644 (file)
        "tog-useeditwarning": "바꾼 내용을 저장하지 않고 편집 페이지를 벗어날 때 내게 알리기",
        "tog-prefershttps": "로그인하는 동안 항상 보안 연결 사용",
        "tog-showrollbackconfirmation": "되돌리기 링크를 클릭할 때 확인창을 표시",
+       "tog-requireemail": "비밀번호 재설정을 위한 이메일 필요",
        "underline-always": "항상",
        "underline-never": "항상 긋지 않기",
        "underline-default": "스킨 또는 브라우저 기본값",
        "createaccountmail": "임의의 임시 비밀번호를 이메일로 보내기",
        "createaccountmail-help": "비밀번호를 기억하지 않고도 다른 사용자를 위한 계정을 만들 수 있습니다.",
        "createacct-realname": "실명 (선택 사항)",
-       "createacct-reason": "이유",
+       "createacct-reason": "이유 (기록은 공개됨)",
        "createacct-reason-ph": "다른 계정을 만들어야 하는 이유가 있나요",
        "createacct-reason-help": "계정 생성 로그에 표시되는 메시지",
        "createacct-submit": "계정 만들기",
index 643e52f..a5b82ce 100644 (file)
        "createaccountmail": "En temporäert zoufällegt Passwuert benotzen an et per E-Mail un déi spezifizéiert E-Mailadress schécken",
        "createaccountmail-help": "Ka benotzt gi fir e Benotzerkont fir eng aner Persoun unzeleeën ouni d'Passwuert gewuer ze ginn.",
        "createacct-realname": "Richtegen Numm (fakultativ)",
-       "createacct-reason": "Grond",
+       "createacct-reason": "Grond (ëffentlech geloggt)",
        "createacct-reason-ph": "Fir wat Dir een anere Benotzerkonnt uleet",
        "createacct-reason-help": "Message deen am Logbuch vun den ugeluechte Benotzerkonte gewise gëtt",
        "createacct-submit": "Äre Benotzerkont uleeën",
index 5924ed2..0ef842a 100644 (file)
        "cannotchangeemail": "郵址不可更於此wiki",
        "emaildisabled": "是站不可遣函也。",
        "accountcreated": "簿增矣",
-       "accountcreatedtext": "[[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|書]])簿增矣。",
+       "accountcreatedtext": "[[{{ns:User}}:$1|$1]]([[{{ns:User talk}}:$1|書]])簿增矣。",
        "createaccount-title": "於{{SITENAME}}增簿",
        "createaccount-text": "有人以汝電郵於{{SITENAME}}增簿。簿名為 \"$2\" ($4)。符節為 \"$3\" 。汝應登而更符節。\n\n如簿誤增,汝可略之。",
        "login-throttled": "汝嘗登簿甚矣。\n請候 $1 而試之。",
index fef0de8..b7e3c12 100644 (file)
        "rollbacklinkcount": "{{PLURAL:$1|တည်းဖြတ်မှု|တည်းဖြတ်မှုများ}} $1 ကို နောက်ပြန်ပြင်ရန်",
        "rollbacklinkcount-morethan": "$1 ထက်ပိုသော {{PLURAL:$1|တည်းဖြတ်မှု|တည်းဖြတ်မှုများ}}ကို နောက်ပြန်ပြင်ရန်",
        "rollbackfailed": "နောက်ပြန်ပြင်ခြင်း မအောင်မြင်ခဲ့ပါ။",
+       "cantrollback": "နောက်ပြန်မပြင်နိုင်ပါ၊ နောက်ဆုံးပံ့ပိုးသူမှာ ဤစာမျက်နှာ၏ တစ်ဦးတည်းသော စာရေးသူဖြစ်ပါသည်။",
        "editcomment": "တည်းဖြတ်မှု အကျဉ်းချုပ်မှာ: <em>$1</em>။",
        "revertpage": "[[Special:Contributions/$2|$2]] ([[User talk:$2|ဆွေးနွေး]]) ၏ ပြင်ဆင်မှုများကို [[User:$1|$1]] ၏ နောက်ဆုံးတည်းဖြတ်မူသို့ နောက်ပြန် ပြန်ပြင်ခဲ့သည်",
+       "rollback-success": "{{GENDER:$3|$1}} မှ နောက်ပြန်ပြင်ခဲ့သည်၊  {{GENDER:$4|$2}} ၏ နောက်ဆုံးမူသို့ ပြန်ပြောင်းခဲ့သည်။",
        "changecontentmodel": "စာမျက်နှာ၏ မာတိကာမော်ဒယ်ကို ပြောင်းလဲရန်",
        "changecontentmodel-legend": "မာတိကာမော်ဒယ်ကို ပြောင်းလဲရန်",
        "changecontentmodel-title-label": "စာမျက်နှာ ခေါင်းစဉ်",
index 3365b11..be1c459 100644 (file)
        "tool-link-userrights": "Càgna gruppe {{GENDER:$1|utente}}",
        "tool-link-userrights-readonly": "Vire gruppe {{GENDER:$1|utente}}",
        "tool-link-emailuser": "Manna na masciata email a st'{{GENDER:$1|utente}}",
-       "imagepage": "Vere a paggena d' 'o file",
-       "mediawikipage": "Vere 'a mmasciata",
-       "templatepage": "Vere 'o template",
-       "viewhelppage": "Vere 'a paggena 'e ajùto",
-       "categorypage": "Vere 'a categurìa",
+       "imagepage": "Vire 'a paggena d' 'o file",
+       "mediawikipage": "Vire 'a mmasciata",
+       "templatepage": "Vire 'o template",
+       "viewhelppage": "Vire 'a paggena 'e ajùto",
+       "categorypage": "Vire 'a categurìa",
        "viewtalkpage": "Vere 'a paggena 'e chiàcchierate",
        "otherlanguages": "Ate lengue",
        "redirectedfrom": "(Redirect 'a $1)",
        "youhavenewmessagesmulti": "Tiene nuove mmasciate $1",
        "editsection": "càgna",
        "editold": "càgna",
-       "viewsourceold": "vere sorgente",
+       "viewsourceold": "vire sorgente",
        "editlink": "càgna",
        "viewsourcelink": "Vire sorgente",
        "editsectionhint": "Modifica a sezzione $1",
        "perfcachedts": "'E ddate ca stanno ccà songhe asciute 'a na copia \"cache\" d' 'o database, 'o cuale tene l'úrdemo agghiurnamento 'o $1. Nu massimo 'e {{PLURAL:$4|unu risultato è|$4 risultate songhe}} a disposizione dint'a \"cache\".",
        "querypage-no-updates": "Ll'agghiurnamente pe' sta paggena songo sospese mmo'. 'E ddate cuntenute ccà nun s'agghiurnarranno.",
        "viewsource": "Vere sorgente",
-       "viewsource-title": "Vere surgente 'e $1",
+       "viewsource-title": "Vire surgente 'e $1",
        "actionthrottled": "Azione ritardata",
        "actionthrottledtext": "Comme mesùra anti-abuse, site lemmetato 'a ffà st'azione troppe vote dint'a nu curto spazio 'e tiempo, e mo stu lèmmeto l'avite superato.\nPe piacere pruvate n'ata vota dint'a quacche minuto.",
        "protectedpagetext": "Sta paggena s'è prutetta pe ne ntuppà 'o càgno o quacche ata azione.",
        "next-page": "paggena aroppo",
        "prevn-title": "{{PLURAL:$1|Risultato precediente|$1 risultate precedenti}}",
        "nextn-title": "{{PLURAL:$1|Risultato successivo|$1 risultate successive}}",
-       "shown-title": "Fa vere {{PLURAL:$1|'nu risultato|$1 risultate}} ppe paggena",
-       "viewprevnext": "Vere($1 {{int:pipe-separator}} $2) ($3).",
+       "shown-title": "Fa verè {{PLURAL:$1|nu risultato|$1 risultate}} pe paggena",
+       "viewprevnext": "Vire ($1 {{int:pipe-separator}} $2) ($3).",
        "searchmenu-exists": "'''Ncopp' 'o sito esiste na paggena c' 'o nomme \"[[:$1]]\"'''\n{{PLURAL:$2|0=|Vedite pure dint'a l'ati risultate 'e cerca.}}",
        "searchmenu-new": "<strong>'''Crèa 'a paggena \"[[:$1]]\" ncopp'a stu wiki!'''</strong> {{PLURAL:$2|0=|Vedite pure 'a paggena truvata c' 'a recerca vuosta|Vedite pure 'e risultate d\"a recerca}}",
        "searchprofile-articles": "Paggene 'e contenute",
        "tooltip-ca-talk": "Vide 'e chiacchere rilative a chesta paggena",
        "tooltip-ca-edit": "Cagna sta paggena",
        "tooltip-ca-addsection": "Cummincia 'na nova sezzione",
-       "tooltip-ca-viewsource": "Chista paggena è prutetta, ma puo vere 'o codice sorgente",
+       "tooltip-ca-viewsource": "Chista paggena è prutetta, ma può verè 'o codice sorgente",
        "tooltip-ca-history": "Vversione 'e primma 'e chesta paggena",
        "tooltip-ca-protect": "Prutegge chesta paggena",
        "tooltip-ca-unprotect": "Càgna 'a prutezzione 'e chesta paggena",
        "tooltip-t-specialpages": "Lista 'e tutte e paggene speciale",
        "tooltip-t-print": "Vversione pe stampà 'a chesta paggena",
        "tooltip-t-permalink": "Jonta permanente a chesta vversione dda paggena",
-       "tooltip-ca-nstab-main": "Vere a paggena e contenuto",
-       "tooltip-ca-nstab-user": "Vere a paggena utente",
+       "tooltip-ca-nstab-main": "Vire 'a paggena 'e contenuto",
+       "tooltip-ca-nstab-user": "Vire 'a paggena utente",
        "tooltip-ca-nstab-media": "Vide 'a pàggena d' 'e media",
        "tooltip-ca-nstab-special": "Chesta è 'na paggena speciale e nun può essere càgnata",
-       "tooltip-ca-nstab-project": "Vere a paggena 'e servizio",
+       "tooltip-ca-nstab-project": "Vire 'a paggena 'e servizio",
        "tooltip-ca-nstab-image": "Vere a paggena ddo file",
        "tooltip-ca-nstab-mediawiki": "Vide 'a mmasciata d' 'o sistema",
-       "tooltip-ca-nstab-template": "Vere 'o modello",
+       "tooltip-ca-nstab-template": "Vire 'o modello",
        "tooltip-ca-nstab-help": "Vide 'a paggena d'aiuto",
-       "tooltip-ca-nstab-category": "Vere a paggena d\"a categurìa",
+       "tooltip-ca-nstab-category": "Vire 'a paggena d' 'a categurìa",
        "tooltip-minoredit": "Rénne chìsto cagnamiénto cchiù ppiccirìllo.",
        "tooltip-save": "Sàrva 'e cagnamiénte.",
        "tooltip-publish": "Pubbreca 'e cagnamiente vuoste",
index 2607b72..e2a6854 100644 (file)
        "createaccountmail": "Gebruik een tijdelijk willekeurig wachtwoord en stuur het naar het opgegeven e-mailadres",
        "createaccountmail-help": "Kan worden gebruikt voor het aanmaken van een account voor een andere persoon zonder het wachtwoord te vernemen.",
        "createacct-realname": "Echte naam (optioneel)",
-       "createacct-reason": "Reden",
+       "createacct-reason": "Reden (door iedereen te zien)",
        "createacct-reason-ph": "Waarom u een ander account aanmaakt",
        "createacct-reason-help": "Weergegeven bericht in het logbestand van aangemaakte gebruikers",
        "createacct-submit": "Uw account aanmaken",
index 069fc5a..048e5f1 100644 (file)
        "createaccountmail": "Użyj tymczasowego hasła wygenerowanego losowo i wyślij je na podany adres e-mail",
        "createaccountmail-help": "Pozwala utworzyć konto dla innej osoby, nie znając jej hasła.",
        "createacct-realname": "Prawdziwe imię i nazwisko (opcjonalnie)",
-       "createacct-reason": "Powód",
+       "createacct-reason": "Powód (podawany publicznie)",
        "createacct-reason-ph": "Dlaczego zakładasz kolejne konto",
        "createacct-reason-help": "Komunikat wyświetlany w rejestrze tworzenia kont",
        "createacct-submit": "Utwórz konto",
index 622ab51..8732554 100644 (file)
        "createaccountmail": "Usar uma senha aleatória e temporária que será enviada ao endereço de e-mail especificado a seguir",
        "createaccountmail-help": "Pode ser utilizado para criar uma conta para outra pessoa sem saber a senha.",
        "createacct-realname": "Nome real (opcional)",
-       "createacct-reason": "Motivo",
+       "createacct-reason": "Motivo (entrado publicamente)",
        "createacct-reason-ph": "Por que você está criando outra conta",
        "createacct-reason-help": "Mensagem mostrada no registro de criação de conta",
        "createacct-submit": "Crie sua conta",
index a33608f..436c11b 100644 (file)
        "mycustomjsredirectprotected": "Error message shown when user tries to edit their own JS page that is a foreign redirect without the 'mycustomjsredirectprotected' right. See also {{msg-mw|mycustomjsprotected}}.",
        "easydeflate-invaliddeflate": "Error message if the content passed to easydeflate was not deflated (compressed) properly",
        "unprotected-js": "Error message shown when trying to load javascript via action=raw that is not protected",
-       "userlogout-continue": "Shown if user attempted to log out without a token specified. Probably the user clicked on an old link that hasn't been updated to use the new system. $1 - url that user should click on in order to log out."
+       "userlogout-continue": "Shown if user attempted to log out without a token specified. Probably the user clicked on an old link that hasn't been updated to use the new system. $1 - url that user should click on in order to log out.",
+       "rest-prefix-mismatch": "Error message for REST API debugging, shown if $wgRestPath is incorrect or otherwise not matched. Parameters:\n* $1: The requested path.\n* $2: The configured root path ($wgRestPath).",
+       "rest-wrong-method": "Error message for REST API debugging, shown if the HTTP method is incorrect. Parameters:\n* $1: The received request method.\n* $2: A comma-separated list of allowed methods for this path.\n* $3: The number of items in the list $2",
+       "rest-no-match": "Error message for REST API debugging, shown if the path has the correct prefix but did not match any registered handler. Parameters:\n* $1: The received request path, relative to $wgRestPath."
 }
index b7c17dc..7b56d69 100644 (file)
        "createaccountmail": "Ause 'na passuord temboranèe a uecchije e mannale a l'indirizze email specificate",
        "createaccountmail-help": "Pò essere ausate pe ccrejà 'n'utende pe 'n'otre crestiane senze ca adda canoscere 'a passuord.",
        "createacct-realname": "Nome vere (opzionale)",
-       "createacct-reason": "Mutive",
+       "createacct-reason": "Mutive (archiviate pubblecamende)",
        "createacct-reason-ph": "Purcé tu ste ccreje 'n'otre cunde utende?",
        "createacct-reason-help": "Messàgge 'ndrucate jndr'à l'archivije d'a ccrejazzione de le utinde",
        "createacct-submit": "Ccreje 'u cunde utende tune",
index f4154b6..42af915 100644 (file)
        "cannotchangeemail": "此 wiki 無法變更帳號的電子郵件地址。",
        "emaildisabled": "此網站不能傳送電子郵件。",
        "accountcreated": "已建立帳號",
-       "accountcreatedtext": "使用者帳號 [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|討論]]) 已建立。",
+       "accountcreatedtext": "使用者帳號[[{{ns:User}}:$1|$1]]([[{{ns:User talk}}:$1|討論]])已建立。",
        "createaccount-title": "{{SITENAME}} 的帳號建立",
        "createaccount-text": "不明人士使用您的電子郵件地址在 {{SITENAME}} ($4) 建立了一個帳號名稱為 \"$2\",密碼為 \"$3\"。\n您應該立即登入並更改密碼。\n\n如果該帳號是建立錯誤的話,您可以忽略此訊息。",
        "login-throttled": "您已經嘗試太多次的登入動作。\n請稍等 $1 後再試。",
        "upload-preferred": "建議的檔案類型:$1。",
        "upload-prohibited": "禁止的檔案類型:$1。",
        "uploadlogpage": "上傳日誌",
-       "uploadlogpagetext": "以下清單為最近上傳的檔案。\n請檢視 [[Special:NewFiles|最新檔案圖庫]] 以視覺化的方式檢視。",
+       "uploadlogpagetext": "以下清單為最近上傳的檔案。以視覺化的方式檢視請見[[Special:NewFiles|最新檔案圖庫]]。",
        "filename": "檔案名稱",
        "filedesc": "摘要",
        "fileuploadsummary": "摘要:",
diff --git a/maintenance/cleanupRevActorPage.php b/maintenance/cleanupRevActorPage.php
new file mode 100644 (file)
index 0000000..ac655fc
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script that cleans up cases where rev_page and revactor_page
+ * became desynced, e.g. from T232464.
+ *
+ * @ingroup Maintenance
+ * @since 1.34
+ */
+class CleanupRevActorPage extends LoggedUpdateMaintenance {
+
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription(
+                       'Resyncs revactor_page with rev_page when they differ, e.g. from T232464.'
+               );
+               $this->setBatchSize( 1000 );
+       }
+
+       protected function getUpdateKey() {
+               return __CLASS__;
+       }
+
+       protected function doDBUpdates() {
+               $dbw = $this->getDB( DB_MASTER );
+               $max = $dbw->selectField( 'revision', 'MAX(rev_id)', '', __METHOD__ );
+               $batchSize = $this->mBatchSize;
+
+               $this->output( "Resyncing revactor_page with rev_page...\n" );
+
+               $count = 0;
+               for ( $start = 1; $start <= $max; $start += $batchSize ) {
+                       $end = $start + $batchSize - 1;
+                       $this->output( "... rev_id $start - $end, $count changed\n" );
+
+                       // Fetch the rows needing update
+                       $res = $dbw->select(
+                               [ 'revision', 'revision_actor_temp' ],
+                               [ 'rev_id', 'rev_page' ],
+                               [
+                                       'rev_page != revactor_page',
+                                       "rev_id >= $start",
+                                       "rev_id <= $end",
+                               ],
+                               __METHOD__,
+                               [],
+                               [ 'revision_actor_temp' => [ 'JOIN', 'rev_id = revactor_rev' ] ]
+                       );
+
+                       if ( !$res->numRows() ) {
+                               continue;
+                       }
+
+                       // Update the existing rows
+                       foreach ( $res as $row ) {
+                               $dbw->update(
+                                       'revision_actor_temp',
+                                       [ 'revactor_page' => $row->rev_page ],
+                                       [ 'revactor_rev' => $row->rev_id ],
+                                       __METHOD__
+                               );
+                               $count += $dbw->affectedRows();
+                       }
+
+                       wfWaitForSlaves();
+               }
+
+               $this->output( "Completed resync, $count row(s) updated\n" );
+
+               return true;
+       }
+}
+
+$maintClass = CleanupRevActorPage::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
index 7a5e93e..7576781 100644 (file)
@@ -267,7 +267,7 @@ U+2B127𫄧|U+07D96綖|
 U+2B128𫄨|U+07D7A絺|
 U+2B137𫄷|U+07E76繶|
 U+2B138𫄸|U+07E81纁|
-U+2B1ED𫇭|U+0853F蔿|
+U+2B1ED𫇭|U+0848D蒍|U+0853F蔿|
 U+2B300𫌀|U+08940襀|
 U+2B363𫍣|U+08A77詷|
 U+2B36F𫍯|U+08AF4諴|
index e1016dc..43c7e61 100644 (file)
 聖吉斯納域斯     圣基茨和尼维斯
 聖克里斯多福及尼維斯 圣基茨和尼维斯
 聖文森及格瑞那丁       圣文森特和格林纳丁斯
+聖文森國   圣文森特和格林纳丁斯
 聖馬利諾   圣马力诺
 蓋亞那      圭亚那
 坦尚尼亞   坦桑尼亚
 提比里西   第比利斯
 巴斯拉      巴士拉
 杜拜 迪拜
+喬治亞字母        格鲁吉亚字母
 坚杜拜      坚杜拜
 堅杜拜      坚杜拜
 賽普勒斯   塞浦路斯
@@ -2728,3 +2730,7 @@ A型肝炎        甲型肝炎
 普立茲獎   普利策奖
 富比士      福布斯
 聖多美普林西比  圣多美和普林西比
+札格瑞布   萨格勒布
+溫荷克      温得和克
+普利托利亞        比勒陀利亚
+阿迪斯阿貝巴     亚的斯亚贝巴
\ No newline at end of file
index 915050b..1eaa387 100644 (file)
 镇里 鎮裏
 》里 》裏
 空里 空裏
+牢里 牢裏
 版本里      版本裏
 苑裡 苑裡
 霄裡 霄裡
 軟體動物   軟體動物
 軟體家具   軟體家具
 網路 網絡
+全角 全形
+全角度      全角度
+全角色      全角色
 人工智慧   人工智能
 航天飞机   穿梭機
 太空梭      穿梭機
 圣基茨和尼维斯  聖吉斯納域斯
 聖克里斯多福及尼維斯 聖吉斯納域斯
 聖文森及格瑞那丁       聖文森特和格林納丁斯
+聖文森國   聖文森特和格林納丁斯
 聖馬利諾   聖馬力諾
 蓋亞那      圭亞那
 坦尚尼亞   坦桑尼亞
 西臺人      赫梯人
 阿联酋      阿聯酋
 迪拜 杜拜
-格鲁吉亚   格魯吉亞
+喬治亞字母        格魯吉亞字母
 提比里西   第比利斯
 諾鲁 瑙魯
 玻里尼西亞        波利尼西亞
@@ -3092,3 +3097,7 @@ IP地址  IP位址
 普利策奖   普立茲獎
 聖多美普林西比  聖多美和普林西比
 塔希提      大溪地
+札格瑞布   薩格勒布
+溫荷克      溫得和克
+普利托利亞        比勒陀利亞
+阿迪斯阿貝巴     亞的斯亞貝巴
\ No newline at end of file
index 6329133..45fef1d 100644 (file)
 高陞 高升
 晉陞 晋升
 歷陞 历升
+尋陞 寻升
 官陞 官升
 榮陞 荣升
 又陞 又升
index 6b5ecdd..39fd1f3 100644 (file)
 三極管      三極體
 软件 軟體
 軟件 軟體
+全角 全形
+全角度      全角度
+全角色      全角色
 人工智能   人工智慧
 航天飞机   太空梭
 穿梭機      太空梭
 布隆迪      蒲隆地
 帕劳 帛琉
 意大利      義大利
+意大利面   義大利麵
 所罗门群岛        索羅門群島
 所羅門群島        索羅門群島
 文莱 汶萊
@@ -808,6 +812,7 @@ IP地址    IP位址
 残奥会      帕運會
 殘奧會      帕運會
 残疾人奥林匹克  帕拉林匹克
+殘疾人奧林匹克  帕拉林匹克
 不列颠哥伦比亚省       卑詩省
 登巴萨      丹帕沙
 登巴薩      丹帕沙
@@ -824,6 +829,12 @@ IP地址   IP位址
 格林納丁斯        格瑞那丁
 空中客车   空中巴士
 普利策奖   普立茲獎
-圣多美和普林西比       聖多美普林西比
-聖多美和普林西比       聖多美普林西比
+多美和普林西比  多美普林西比 #聖多美普林西比
 塔希提      大溪地
+萨格勒布   札格瑞布
+薩格勒布   札格瑞布
+温得和克   溫荷克
+溫得和克   溫荷克
+比勒陀利   普利托利 #普利托利亚
+亚的斯亚贝巴     阿迪斯阿貝巴
+亞的斯亞貝巴     阿迪斯阿貝巴
index 78b5a73..8bd2665 100644 (file)
@@ -6,6 +6,7 @@
 ’m   ’m
 ’t   ’t
 ’re  ’re
+𬞟   蘋
 手塚治虫   手塚治虫
 寇仇 寇讎
 往日无仇   往日無讎
@@ -78,6 +79,7 @@
 乾象曆      乾象曆
 乾象历      乾象曆
 不好干預   不好干預
+可能干預   可能干預
 范文瀾      范文瀾
 機械系      機械系
 頂多 頂多
@@ -97,6 +99,7 @@
 于帥 于帥
 于濤 于濤
 于贈 于贈
+于闐 于闐
 于會泳      于會泳
 于偉國      于偉國
 于光遠      于光遠
 于學忠      于學忠
 于小偉      于小偉
 于山國      于山國
+于山島      于山島
 于幼軍      于幼軍
 于廣洲      于廣洲
 于從濂      于從濂
 更钟情      更鍾情
 更钟爱      更鍾愛
 更钟意      更鍾意
+温嵐 温嵐
index 74064bb..5c30eb8 100644 (file)
@@ -586,6 +586,7 @@ U+08432萲|U+08431萱|
 U+08457著|U+08457著|U+07740着|
 U+08460葠|U+053C2参|
 U+0846F葯|U+0836F药|
+U+0848D蒍|U+2B1ED𫇭|
 U+08493蒓|U+083BC莼|
 U+084C6蓆|U+05E2D席|
 U+084E1蓡|U+053C2参|
index 2cf35ba..522ae31 100644 (file)
 併為一家
 併吞
 並吞下
+入侵並 #分詞用
 提摩太後書
 裏海
 不採
 捲葉蛾
 捲尾猴
 捲積雲
+被捲回
 夸父
 夸克
 夸特
 伊府麵
 藥麵兒
 意大利麵
+意大利面臨
 湯下麵
 茶麵
 麵團
 鹹豬
 甜鹹
 鹹甜
+鹹吃
 甜、鹹
 鹹、甜
 錦綉花園
 幹仗
 包幹
 幹過
+大幹一
 李連杰
 周杰
 杰倫
 不占算
 不好干涉
 不好干預
+可能干預
 不斗膽
 不每只
 不采聲
 詞裡
 》裡
 空裡
+牢裡
 版本裡
 裏白 #植物常用名
 烏蘇里 #分詞用
 于衡
 于贈
 于越
+於越南
 于靖
 于勒
 于格
+於格林
 鳳凰于飛
 于仁泰
 于會泳
 于小偉
 于小彤
 于山國
+于山島
 于幼軍
 于廣洲
 于康震
 關系科
 銹病
 嚐糞
+温嵐
index bcbe96d..c414c7b 100644 (file)
@@ -162,13 +162,6 @@ return [
                ],
                'targets' => [ 'mobile', 'desktop' ],
        ],
-       'jquery.checkboxShiftClick' => [
-               'deprecated' => 'Please use "mediawiki.page.ready" instead.',
-               'dependencies' => [
-                       'mediawiki.page.ready',
-               ],
-               'targets' => [ 'desktop', 'mobile' ],
-       ],
        'jquery.chosen' => [
                'scripts' => 'resources/lib/jquery.chosen/chosen.jquery.js',
                'styles' => 'resources/lib/jquery.chosen/chosen.css',
@@ -1666,9 +1659,11 @@ return [
                ]
        ],
        'mediawiki.page.ready' => [
-               'scripts' => [
-                       'resources/src/mediawiki.page.ready/checkboxShift.js',
-                       'resources/src/mediawiki.page.ready/ready.js',
+               'localBasePath' => "$IP/resources/src/mediawiki.page.ready",
+               'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.page.ready",
+               'packageFiles' => [
+                       'ready.js',
+                       'checkboxShift.js',
                ],
                'dependencies' => [
                        'mediawiki.util',
index 2c88bdc..75dae92 100644 (file)
@@ -436,15 +436,9 @@ a.new {
 
 /* Note on preview page */
 .previewnote {
-       color: #d33;
        margin-bottom: 1em;
 }
 
-.previewnote p {
-       text-indent: 3em;
-       margin: 0.8em 0;
-}
-
 .visualClear {
        clear: both;
 }
diff --git a/resources/src/mediawiki.page.ready/.eslintrc.json b/resources/src/mediawiki.page.ready/.eslintrc.json
new file mode 100644 (file)
index 0000000..ad8dbb3
--- /dev/null
@@ -0,0 +1,5 @@
+{
+       "parserOptions": {
+               "sourceType": "module"
+       }
+}
index 435e23f..86e0ec2 100644 (file)
@@ -1,43 +1,33 @@
 /**
- * @class jQuery.plugin.checkboxShiftClick
+ * @private
+ * @class mw.plugin.pageready
  */
-( function () {
-
-       /**
-        * Enable checkboxes to be checked or unchecked in a row by clicking one,
-        * holding shift and clicking another one.
-        *
-        * @return {jQuery}
-        * @chainable
-        */
-       $.fn.checkboxShiftClick = function () {
-               var prevCheckbox = null,
-                       $box = this;
-               // When our boxes are clicked..
-               $box.on( 'click', function ( e ) {
-                       // And one has been clicked before...
-                       if ( prevCheckbox !== null && e.shiftKey ) {
-                               // Check or uncheck this one and all in-between checkboxes,
-                               // except for disabled ones
-                               $box
-                                       .slice(
-                                               Math.min( $box.index( prevCheckbox ), $box.index( e.target ) ),
-                                               Math.max( $box.index( prevCheckbox ), $box.index( e.target ) ) + 1
-                                       )
-                                       .filter( function () {
-                                               return !this.disabled;
-                                       } )
-                                       .prop( 'checked', !!e.target.checked );
-                       }
-                       // Either way, update the prevCheckbox variable to the one clicked now
-                       prevCheckbox = e.target;
-               } );
-               return $box;
-       };
-
-       /**
-        * @class jQuery
-        * @mixins jQuery.plugin.checkboxShiftClick
-        */
-
-}() );
+/**
+ * Enable checkboxes to be checked or unchecked in a row by clicking one,
+ * holding shift and clicking another one.
+ *
+ * @method checkboxShift
+ * @param {jQuery} $box
+ */
+module.exports = function ( $box ) {
+       var prev;
+       // When our boxes are clicked..
+       $box.on( 'click', function ( e ) {
+               // And one has been clicked before...
+               if ( prev && e.shiftKey ) {
+                       // Check or uncheck this one and all in-between checkboxes,
+                       // except for disabled ones
+                       $box
+                               .slice(
+                                       Math.min( $box.index( prev ), $box.index( e.target ) ),
+                                       Math.max( $box.index( prev ), $box.index( e.target ) ) + 1
+                               )
+                               .filter( function () {
+                                       return !this.disabled;
+                               } )
+                               .prop( 'checked', e.target.checked );
+               }
+               // Either way, remember this as the last clicked one
+               prev = e.target;
+       } );
+};
index 0e59da6..48d605d 100644 (file)
@@ -1,82 +1,79 @@
-( function () {
-       mw.hook( 'wikipage.content' ).add( function ( $content ) {
-               var $sortable, $collapsible;
+var checkboxShift = require( './checkboxShift.js' );
+mw.hook( 'wikipage.content' ).add( function ( $content ) {
+       var $sortable, $collapsible;
 
-               $collapsible = $content.find( '.mw-collapsible' );
-               if ( $collapsible.length ) {
-                       // Preloaded by Skin::getDefaultModules()
-                       mw.loader.using( 'jquery.makeCollapsible', function () {
-                               $collapsible.makeCollapsible();
-                       } );
-               }
+       $collapsible = $content.find( '.mw-collapsible' );
+       if ( $collapsible.length ) {
+               // Preloaded by Skin::getDefaultModules()
+               mw.loader.using( 'jquery.makeCollapsible', function () {
+                       $collapsible.makeCollapsible();
+               } );
+       }
 
-               $sortable = $content.find( 'table.sortable' );
-               if ( $sortable.length ) {
-                       // Preloaded by Skin::getDefaultModules()
-                       mw.loader.using( 'jquery.tablesorter', function () {
-                               $sortable.tablesorter();
-                       } );
-               }
+       $sortable = $content.find( 'table.sortable' );
+       if ( $sortable.length ) {
+               // Preloaded by Skin::getDefaultModules()
+               mw.loader.using( 'jquery.tablesorter', function () {
+                       $sortable.tablesorter();
+               } );
+       }
 
-               // Run jquery.checkboxShiftClick
-               $content.find( 'input[type="checkbox"]:not(.noshiftselect)' ).checkboxShiftClick();
-       } );
+       checkboxShift( $content.find( 'input[type="checkbox"]:not(.noshiftselect)' ) );
+} );
 
-       // Things outside the wikipage content
-       $( function () {
-               var $nodes;
+// Handle elements outside the wikipage content
+$( function () {
+       var $nodes;
 
-               // Add accesskey hints to the tooltips
-               $( '[accesskey]' ).updateTooltipAccessKeys();
+       // Add accesskey hints to the tooltips
+       $( '[accesskey]' ).updateTooltipAccessKeys();
 
-               $nodes = $( '.catlinks[data-mw="interface"]' );
-               if ( $nodes.length ) {
-                       /**
-                        * Fired when categories are being added to the DOM
-                        *
-                        * It is encouraged to fire it before the main DOM is changed (when $content
-                        * is still detached).  However, this order is not defined either way, so you
-                        * should only rely on $content itself.
-                        *
-                        * This includes the ready event on a page load (including post-edit loads)
-                        * and when content has been previewed with LivePreview.
-                        *
-                        * @event wikipage_categories
-                        * @member mw.hook
-                        * @param {jQuery} $content The most appropriate element containing the content,
-                        *   such as .catlinks
-                        */
-                       mw.hook( 'wikipage.categories' ).fire( $nodes );
-               }
+       $nodes = $( '.catlinks[data-mw="interface"]' );
+       if ( $nodes.length ) {
+               /**
+                * Fired when categories are being added to the DOM
+                *
+                * It is encouraged to fire it before the main DOM is changed (when $content
+                * is still detached).  However, this order is not defined either way, so you
+                * should only rely on $content itself.
+                *
+                * This includes the ready event on a page load (including post-edit loads)
+                * and when content has been previewed with LivePreview.
+                *
+                * @event wikipage_categories
+                * @member mw.hook
+                * @param {jQuery} $content The most appropriate element containing the content,
+                *   such as .catlinks
+                */
+               mw.hook( 'wikipage.categories' ).fire( $nodes );
+       }
 
-               $( '#t-print a' ).on( 'click', function ( e ) {
-                       window.print();
-                       e.preventDefault();
-               } );
-
-               // Turn logout to a POST action
-               $( '#pt-logout a' ).on( 'click', function ( e ) {
-                       var api = new mw.Api(),
-                               returnUrl = $( '#pt-logout a' ).attr( 'href' );
-                       mw.notify(
-                               mw.message( 'logging-out-notify' ),
-                               { tag: 'logout', autoHide: false }
-                       );
-                       api.postWithToken( 'csrf', {
-                               action: 'logout'
-                       } ).then(
-                               function () {
-                                       location.href = returnUrl;
-                               },
-                               function ( e ) {
-                                       mw.notify(
-                                               mw.message( 'logout-failed', e ),
-                                               { type: 'error', tag: 'logout', autoHide: false }
-                                       );
-                               }
-                       );
-                       e.preventDefault();
-               } );
+       $( '#t-print a' ).on( 'click', function ( e ) {
+               window.print();
+               e.preventDefault();
        } );
 
-}() );
+       // Turn logout to a POST action
+       $( '#pt-logout a' ).on( 'click', function ( e ) {
+               var api = new mw.Api(),
+                       url = this.href;
+               mw.notify(
+                       mw.message( 'logging-out-notify' ),
+                       { tag: 'logout', autoHide: false }
+               );
+               api.postWithToken( 'csrf', {
+                       action: 'logout'
+               } ).then(
+                       function () {
+                               location.href = url;
+                       },
+                       function ( err ) {
+                               mw.notify(
+                                       mw.message( 'logout-failed', err ),
+                                       { type: 'error', tag: 'logout', autoHide: false }
+                               );
+                       }
+               );
+               e.preventDefault();
+       } );
+} );
index 8fa0cd6..d34ba0a 100644 (file)
@@ -308,7 +308,16 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
                                throw new MWException( 'All service callbacks must have a return type defined, ' .
                                        "none found for $name" );
                        }
-                       $ret[$name] = [ $name, $fun->getReturnType()->__toString() ];
+
+                       $returnType = $fun->getReturnType();
+
+                       // ReflectionType::__toString() generates deprecation notices in PHP 7.4 and above
+                       // TODO: T228342 - remove this check after MediaWiki only supports PHP 7.1+
+                       if ( is_callable( [ $returnType, 'getName' ] ) ) {
+                               $ret[$name] = [ $name, $returnType->getName() ];
+                       } else {
+                               $ret[$name] = [ $name, $fun->getReturnType()->__toString() ];
+                       }
                }
                return $ret;
        }
index 2d1fd98..a310242 100644 (file)
@@ -46,7 +46,7 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
                        [],
                        '/rest',
                        new \EmptyBagOStuff(),
-                       new ResponseFactory(),
+                       new ResponseFactory( [] ),
                        new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ),
                        $objectFactory,
                        new Validator( $objectFactory, $request, $user )
index b984895..1c9bc41 100644 (file)
@@ -38,7 +38,7 @@ class EntryPointTest extends \MediaWikiTestCase {
                        [],
                        '/rest',
                        new EmptyBagOStuff(),
-                       new ResponseFactory(),
+                       new ResponseFactory( [] ),
                        new StaticBasicAuthorizer(),
                        $objectFactory,
                        new Validator( $objectFactory, $request, new User )
index 82ccb9a..608d590 100644 (file)
@@ -834,8 +834,23 @@ class TitleTest extends MediaWikiTestCase {
                return [
                        [ Title::makeTitle( NS_SPECIAL, 'Test' ) ],
                        [ Title::makeTitle( NS_MEDIA, 'Test' ) ],
-                       [ Title::makeTitle( NS_MAIN, '', 'Kittens' ) ],
-                       [ Title::makeTitle( NS_MAIN, 'Kittens', '', 'acme' ) ],
+               ];
+       }
+
+       public static function provideGetTalkPage_broken() {
+               // These cases *should* be bad, but are not treated as bad, for backwards compatibility.
+               // See discussion on T227817.
+               return [
+                       [
+                               Title::makeTitle( NS_MAIN, '', 'Kittens' ),
+                               Title::makeTitle( NS_TALK, '' ), // Section is lost!
+                               false,
+                       ],
+                       [
+                               Title::makeTitle( NS_MAIN, 'Kittens', '', 'acme' ),
+                               Title::makeTitle( NS_TALK, 'Kittens', '' ), // Interwiki prefix is lost!
+                               true,
+                       ],
                ];
        }
 
@@ -895,6 +910,23 @@ class TitleTest extends MediaWikiTestCase {
                $title->getTalkPage();
        }
 
+       /**
+        * @dataProvider provideGetTalkPage_broken
+        * @covers Title::getTalkPageIfDefined
+        */
+       public function testGetTalkPage_broken( Title $title, Title $expected, $valid ) {
+               $errorLevel = error_reporting( E_ERROR );
+
+               // NOTE: Eventually we want to throw in this case. But while there is still code that
+               // calls this method without checking, we want to avoid fatal errors.
+               // See discussion on T227817.
+               $result = $title->getTalkPage();
+               $this->assertTrue( $expected->equals( $result ) );
+               $this->assertSame( $valid, $result->isValid() );
+
+               error_reporting( $errorLevel );
+       }
+
        /**
         * @dataProvider provideGetTalkPage_good
         * @covers Title::getTalkPageIfDefined
index 2d19d38..fe6fcfc 100644 (file)
@@ -182,7 +182,6 @@ class ApiBlockTest extends ApiTestCase {
                $this->setMwGlobals( [
                        'wgEnableEmail' => true,
                        'wgEnableUserEmail' => true,
-                       'wgSysopEmailBans' => true,
                ] );
 
                $res = $this->doBlock( [ 'noemail' => '' ] );
@@ -200,7 +199,6 @@ class ApiBlockTest extends ApiTestCase {
                $this->setMwGlobals( [
                        'wgEnableEmail' => true,
                        'wgEnableUserEmail' => true,
-                       'wgSysopEmailBans' => true,
                ] );
 
                $this->setExpectedException( ApiUsageException::class,
index 0a2558a..dce1a5f 100644 (file)
@@ -79,15 +79,46 @@ class ApiQuerySiteinfoTest extends ApiTestCase {
                $this->assertSame( 'Need more donations', $data['readonlyreason'] );
        }
 
-       public function testNamespaces() {
-               $this->setMwGlobals( 'wgExtraNamespaces', [ '138' => 'Testing' ] );
+       public function testNamespacesBasic() {
+               $this->assertSame(
+                       array_keys( MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces() ),
+                       array_keys( $this->doQuery( 'namespaces' ) )
+               );
+       }
 
+       public function testNamespacesExtraNS() {
+               $this->setMwGlobals( 'wgExtraNamespaces', [ '138' => 'Testing' ] );
                $this->assertSame(
                        array_keys( MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces() ),
                        array_keys( $this->doQuery( 'namespaces' ) )
                );
        }
 
+       public function testNamespacesProtection() {
+               $this->setMwGlobals(
+                       'wgNamespaceProtection',
+                       [
+                               '0' => '',
+                               '2' => [ '' ],
+                               '4' => 'editsemiprotected',
+                               '8' => [
+                                       'editinterface',
+                                       'noratelimit'
+                               ],
+                               '14' => [
+                                       'move-categorypages',
+                                       ''
+                               ]
+                       ]
+               );
+               $data = $this->doQuery( 'namespaces' );
+               $this->assertArrayNotHasKey( 'namespaceprotection', $data['0'] );
+               $this->assertArrayNotHasKey( 'namespaceprotection', $data['2'] );
+               $this->assertSame( 'editsemiprotected', $data['4']['namespaceprotection'] );
+               $this->assertSame( 'editinterface|noratelimit', $data['8']['namespaceprotection'] );
+               $this->assertSame( 'move-categorypages', $data['14']['namespaceprotection'] );
+       }
+
        public function testNamespaceAliases() {
                global $wgNamespaceAliases;
 
index 0b1d013..45fa143 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\User\UserIdentityValue;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\LoadBalancer;
@@ -66,14 +67,16 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
 
        /**
         * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
+        * @param PHPUnit_Framework_MockObject_MockObject|PermissionManager $mockPM
         * @return WatchedItemQueryService
         */
-       private function newService( $mockDb ) {
+       private function newService( $mockDb, $mockPM = null ) {
                return new WatchedItemQueryService(
                        $this->getMockLoadBalancer( $mockDb ),
                        $this->getMockCommentStore(),
                        $this->getMockActorMigration(),
-                       $this->getMockWatchedItemStore()
+                       $this->getMockWatchedItemStore(),
+                       $mockPM ?: $this->getMockPermissionManager()
                );
        }
 
@@ -153,6 +156,25 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
                return $mock;
        }
 
+       /**
+        * @param string $notAllowedAction
+        * @return PHPUnit_Framework_MockObject_MockObject|PermissionManager
+        */
+       private function getMockPermissionManager( $notAllowedAction = null ) {
+               $mock = $this->getMockBuilder( PermissionManager::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $mock->method( 'userHasRight' )
+                       ->will( $this->returnCallback( function ( $user, $action ) use ( $notAllowedAction ) {
+                               return $action !== $notAllowedAction;
+                       } ) );
+               $mock->method( 'userHasAnyRight' )
+                       ->will( $this->returnCallback( function ( $user, ...$actions ) use ( $notAllowedAction ) {
+                               return !in_array( $notAllowedAction, $actions );
+                       } ) );
+               return $mock;
+       }
+
        /**
         * @param int $id
         * @param string[] $extraMethods Extra methods that are expected might be called
@@ -177,9 +199,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
         */
        private function getMockUnrestrictedNonAnonUserWithId( $id, array $extraMethods = [] ) {
                $mock = $this->getMockNonAnonUserWithId( $id,
-                       array_merge( [ 'isAllowed', 'isAllowedAny', 'useRCPatrol' ], $extraMethods ) );
-               $mock->method( 'isAllowed' )->willReturn( true );
-               $mock->method( 'isAllowedAny' )->willReturn( true );
+                       array_merge( [ 'useRCPatrol' ], $extraMethods ) );
                $mock->method( 'useRCPatrol' )->willReturn( true );
                return $mock;
        }
@@ -189,18 +209,9 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
         * @param string $notAllowedAction
         * @return PHPUnit_Framework_MockObject_MockObject|User
         */
-       private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
+       private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id ) {
                $mock = $this->getMockNonAnonUserWithId( $id,
-                       [ 'isAllowed', 'isAllowedAny', 'useRCPatrol', 'useNPPatrol' ] );
-
-               $mock->method( 'isAllowed' )
-                       ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
-                               return $action !== $notAllowedAction;
-                       } ) );
-               $mock->method( 'isAllowedAny' )
-                       ->will( $this->returnCallback( function ( ...$actions ) use ( $notAllowedAction ) {
-                               return !in_array( $notAllowedAction, $actions );
-                       } ) );
+                       [ 'useRCPatrol', 'useNPPatrol' ] );
                $mock->method( 'useRCPatrol' )->willReturn( false );
                $mock->method( 'useNPPatrol' )->willReturn( false );
 
@@ -213,15 +224,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
         */
        private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
                $mock = $this->getMockNonAnonUserWithId( $id,
-                       [ 'isAllowed', 'isAllowedAny', 'useRCPatrol', 'useNPPatrol' ] );
-
-               $mock->expects( $this->any() )
-                       ->method( 'isAllowed' )
-                       ->will( $this->returnValue( true ) );
-               $mock->expects( $this->any() )
-                       ->method( 'isAllowedAny' )
-                       ->will( $this->returnValue( true ) );
-
+                       [ 'useRCPatrol', 'useNPPatrol' ] );
                $mock->expects( $this->any() )
                        ->method( 'useRCPatrol' )
                        ->will( $this->returnValue( false ) );
@@ -1132,9 +1135,10 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
                        )
                        ->will( $this->returnValue( [] ) );
 
-               $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
+               $permissionManager = $this->getMockPermissionManager( $notAllowedAction );
+               $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1 );
 
-               $queryService = $this->newService( $mockDb );
+               $queryService = $this->newService( $mockDb, $permissionManager );
                $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
 
                $this->assertEmpty( $items );
index 91652a2..7d682fd 100644 (file)
@@ -62,7 +62,7 @@ class HelloHandlerTest extends \MediaWikiUnitTestCase {
                        [],
                        '/rest',
                        new EmptyBagOStuff(),
-                       new ResponseFactory(),
+                       new ResponseFactory( [] ),
                        new StaticBasicAuthorizer(),
                        $objectFactory,
                        new Validator( $objectFactory, $request, new User )
index 04d54de..0a98686 100644 (file)
@@ -6,6 +6,8 @@ use ArrayIterator;
 use MediaWiki\Rest\HttpException;
 use MediaWiki\Rest\ResponseFactory;
 use MediaWikiUnitTestCase;
+use Wikimedia\Message\ITextFormatter;
+use Wikimedia\Message\MessageValue;
 
 /** @covers \MediaWiki\Rest\ResponseFactory */
 class ResponseFactoryTest extends MediaWikiUnitTestCase {
@@ -18,14 +20,27 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
                ];
        }
 
+       private function createResponseFactory() {
+               $fakeTextFormatter = new class implements ITextFormatter {
+                       function getLangCode() {
+                               return 'qqx';
+                       }
+
+                       function format( MessageValue $message ) {
+                               return $message->getKey();
+                       }
+               };
+               return new ResponseFactory( [ $fakeTextFormatter ] );
+       }
+
        /** @dataProvider provideEncodeJson */
        public function testEncodeJson( $input, $expected ) {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $this->assertSame( $expected, $rf->encodeJson( $input ) );
        }
 
        public function testCreateJson() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createJson( [] );
                $response->getBody()->rewind();
                $this->assertSame( 'application/json', $response->getHeaderLine( 'Content-Type' ) );
@@ -35,7 +50,7 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
        }
 
        public function testCreateNoContent() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createNoContent();
                $this->assertSame( [], $response->getHeader( 'Content-Type' ) );
                $this->assertSame( 0, $response->getBody()->getSize() );
@@ -43,35 +58,35 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
        }
 
        public function testCreatePermanentRedirect() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createPermanentRedirect( 'http://www.example.com/' );
                $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
                $this->assertSame( 301, $response->getStatusCode() );
        }
 
        public function testCreateLegacyTemporaryRedirect() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createLegacyTemporaryRedirect( 'http://www.example.com/' );
                $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
                $this->assertSame( 302, $response->getStatusCode() );
        }
 
        public function testCreateTemporaryRedirect() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createTemporaryRedirect( 'http://www.example.com/' );
                $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
                $this->assertSame( 307, $response->getStatusCode() );
        }
 
        public function testCreateSeeOther() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createSeeOther( 'http://www.example.com/' );
                $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
                $this->assertSame( 303, $response->getStatusCode() );
        }
 
        public function testCreateNotModified() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createNotModified();
                $this->assertSame( 0, $response->getBody()->getSize() );
                $this->assertSame( 304, $response->getStatusCode() );
@@ -79,12 +94,12 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
 
        /** @expectedException \InvalidArgumentException */
        public function testCreateHttpErrorInvalid() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $rf->createHttpError( 200 );
        }
 
        public function testCreateHttpError() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createHttpError( 415, [ 'message' => '...' ] );
                $this->assertSame( 415, $response->getStatusCode() );
                $body = $response->getBody();
@@ -95,7 +110,7 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
        }
 
        public function testCreateFromExceptionUnlogged() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createFromException( new HttpException( 'hello', 415 ) );
                $this->assertSame( 415, $response->getStatusCode() );
                $body = $response->getBody();
@@ -106,7 +121,7 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
        }
 
        public function testCreateFromExceptionLogged() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createFromException( new \Exception( "hello", 415 ) );
                $this->assertSame( 500, $response->getStatusCode() );
                $body = $response->getBody();
@@ -131,7 +146,7 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
 
        /** @dataProvider provideCreateFromReturnValue */
        public function testCreateFromReturnValue( $input, $expected ) {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createFromReturnValue( $input );
                $body = $response->getBody();
                $body->rewind();
@@ -140,7 +155,17 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
 
        /** @expectedException \InvalidArgumentException */
        public function testCreateFromReturnValueInvalid() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $rf->createFromReturnValue( new ArrayIterator );
        }
+
+       public function testCreateLocalizedHttpError() {
+               $rf = $this->createResponseFactory();
+               $response = $rf->createLocalizedHttpError( 404, new MessageValue( 'rftest' ) );
+               $body = $response->getBody();
+               $body->rewind();
+               $this->assertSame(
+                       '{"messageTranslations":{"qqx":"rftest"},"httpCode":404,"httpReason":"Not Found"}',
+                       $body->getContents() );
+       }
 }
index e16ea25..58039ea 100644 (file)
@@ -29,7 +29,7 @@ class RouterTest extends \MediaWikiUnitTestCase {
                        [],
                        '/rest',
                        new \EmptyBagOStuff(),
-                       new ResponseFactory(),
+                       new ResponseFactory( [] ),
                        new StaticBasicAuthorizer( $authError ),
                        $objectFactory,
                        new Validator( $objectFactory, $request, new User )
@@ -55,6 +55,16 @@ class RouterTest extends \MediaWikiUnitTestCase {
                $this->assertSame( 'GET', $response->getHeaderLine( 'Allow' ) );
        }
 
+       public function testHeadToGet() {
+               $request = new RequestData( [
+                       'uri' => new Uri( '/rest/user/joe/hello' ),
+                       'method' => 'HEAD'
+               ] );
+               $router = $this->createRouter( $request );
+               $response = $router->execute( $request );
+               $this->assertSame( 200, $response->getStatusCode() );
+       }
+
        public function testNoMatch() {
                $request = new RequestData( [ 'uri' => new Uri( '/rest/bogus' ) ] );
                $router = $this->createRouter( $request );
index 35bcbf7..8a28e4d 100644 (file)
@@ -3,6 +3,32 @@ const MWBot = require( 'mwbot' );
 // TODO: Once we require Node 7 or later, we can use async-await.
 
 module.exports = {
+       /**
+        * Get a logged-in instance of `MWBot` with edit token already set up.
+        * Default username, password and base URL is used unless specified.
+        *
+        * @since 0.5.0
+        * @param {string} username - Optional
+        * @param {string} password - Optional
+        * @param {string} baseUrl - Optional
+        * @return {Promise<MWBot>}
+        */
+       bot(
+               username = browser.options.username,
+               password = browser.options.password,
+               baseUrl = browser.options.baseUrl
+       ) {
+               const bot = new MWBot();
+
+               return bot.loginGetEditToken( {
+                       apiUrl: `${baseUrl}/api.php`,
+                       username: username,
+                       password: password
+               } ).then( function () {
+                       return bot;
+               } );
+       },
+
        /**
         * Shortcut for `MWBot#edit( .. )`.
         * Default username, password and base URL is used unless specified
@@ -22,15 +48,10 @@ module.exports = {
                password = browser.options.password,
                baseUrl = browser.options.baseUrl
        ) {
-               const bot = new MWBot();
-
-               return bot.loginGetEditToken( {
-                       apiUrl: `${baseUrl}/api.php`,
-                       username: username,
-                       password: password
-               } ).then( function () {
-                       return bot.edit( title, content, `Created or updated page with "${content}"` );
-               } );
+               return this.bot( username, password, baseUrl )
+                       .then( function ( bot ) {
+                               return bot.edit( title, content, `Created or updated page with "${content}"` );
+                       } );
        },
 
        /**
@@ -43,15 +64,10 @@ module.exports = {
         * @return {Object} Promise for API action=delete response data.
         */
        delete( title, reason ) {
-               const bot = new MWBot();
-
-               return bot.loginGetEditToken( {
-                       apiUrl: `${browser.options.baseUrl}/api.php`,
-                       username: browser.options.username,
-                       password: browser.options.password
-               } ).then( function () {
-                       return bot.delete( title, reason );
-               } );
+               return this.bot()
+                       .then( function ( bot ) {
+                               return bot.delete( title, reason );
+                       } );
        },
 
        /**
@@ -94,23 +110,17 @@ module.exports = {
         * @return {Object} Promise for API action=block response data.
         */
        blockUser( username, expiry ) {
-               const bot = new MWBot();
-
-               // Log in as admin
-               return bot.loginGetEditToken( {
-                       apiUrl: `${browser.options.baseUrl}/api.php`,
-                       username: browser.options.username,
-                       password: browser.options.password
-               } ).then( () => {
-                       // block user. default = admin
-                       return bot.request( {
-                               action: 'block',
-                               user: username || browser.options.username,
-                               reason: 'browser test',
-                               token: bot.editToken,
-                               expiry
+               return this.bot()
+                       .then( function ( bot ) {
+                               // block user. default = admin
+                               return bot.request( {
+                                       action: 'block',
+                                       user: username || browser.options.username,
+                                       reason: 'browser test',
+                                       token: bot.editToken,
+                                       expiry
+                               } );
                        } );
-               } );
        },
 
        /**
@@ -122,21 +132,15 @@ module.exports = {
         * @return {Object} Promise for API action=unblock response data.
         */
        unblockUser( username ) {
-               const bot = new MWBot();
-
-               // Log in as admin
-               return bot.loginGetEditToken( {
-                       apiUrl: `${browser.options.baseUrl}/api.php`,
-                       username: browser.options.username,
-                       password: browser.options.password
-               } ).then( () => {
-                       // unblock user. default = admin
-                       return bot.request( {
-                               action: 'unblock',
-                               user: username || browser.options.username,
-                               reason: 'browser test done',
-                               token: bot.editToken
+               return this.bot()
+                       .then( function ( bot ) {
+                               // unblock user. default = admin
+                               return bot.request( {
+                                       action: 'unblock',
+                                       user: username || browser.options.username,
+                                       reason: 'browser test done',
+                                       token: bot.editToken
+                               } );
                        } );
-               } );
        }
 };
index dc16e81..357fbd9 100644 (file)
@@ -22,11 +22,11 @@ Utilities to interact with the MediaWiki API. Uses the [mwbot](https://github.co
 Actions are performed logged-in using `browser.options.username` and `browser.options.password`,
 which typically come from `MEDIAWIKI_USER` and `MEDIAWIKI_PASSWORD` environment variables.
 
-* `edit(title, content [, string username [, string password [, string baseUrl ] ] ])`
-* `delete(title, reason)`
-* `createAccount(username, password)`
-* `blockUser(username, expiry)`
-* `unblockUser(username)`
+* `edit(string title, string content [, string username [, string password [, string baseUrl ] ] ])`
+* `delete(string title, string reason)`
+* `createAccount(string username, string password)`
+* `blockUser([ string username [, string expiry ] ])`
+* `unblockUser([ string username ])`
 
 ### RunJobs