Merge "docs: Factor out MWDoxygenFilter from mwdoc-filter.php with tests"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 6 Sep 2019 18:05:39 +0000 (18:05 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 6 Sep 2019 18:05:39 +0000 (18:05 +0000)
119 files changed:
RELEASE-NOTES-1.34
api.php
includes/DefaultSettings.php
includes/Message/TextFormatter.php
includes/PathRouter.php
includes/Rest/EntryPoint.php
includes/Rest/Handler.php
includes/Rest/Handler/HelloHandler.php
includes/Rest/HttpException.php
includes/Rest/ResponseFactory.php
includes/Rest/Router.php
includes/Rest/SimpleHandler.php
includes/Rest/Validator/BodyValidator.php [new file with mode: 0644]
includes/Rest/Validator/NullBodyValidator.php [new file with mode: 0644]
includes/Rest/Validator/ParamValidatorCallbacks.php [new file with mode: 0644]
includes/Rest/Validator/Validator.php [new file with mode: 0644]
includes/Setup.php
includes/Title.php
includes/WebRequest.php
includes/api/ApiBase.php
includes/cache/HTMLFileCache.php
includes/debug/MWDebug.php
includes/diff/DifferenceEngine.php
includes/htmlform/HTMLFormField.php
includes/htmlform/fields/HTMLCheckMatrix.php
includes/installer/SqliteInstaller.php
includes/installer/i18n/ar.json
includes/installer/i18n/fr.json
includes/installer/i18n/qqq.json
includes/installer/i18n/sh.json
includes/libs/Message/ITextFormatter.php
includes/libs/Message/ListParam.php
includes/libs/Message/ListType.php
includes/libs/Message/MessageParam.php
includes/libs/Message/MessageValue.php
includes/libs/Message/ParamType.php
includes/libs/Message/README.md [new file with mode: 0644]
includes/libs/Message/ScalarParam.php [new file with mode: 0644]
includes/libs/Message/TextParam.php [deleted file]
includes/libs/http/MultiHttpClient.php
includes/libs/objectcache/wancache/WANObjectCache.php
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMysqli.php
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/lbfactory/ILBFactory.php
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/lbfactory/LBFactoryMulti.php
includes/libs/rdbms/lbfactory/LBFactorySimple.php
includes/libs/rdbms/lbfactory/LBFactorySingle.php
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/page/Article.php
includes/pager/IndexPager.php
includes/registration/ExtensionRegistry.php
includes/resourceloader/ResourceLoaderFileModule.php
languages/i18n/az.json
languages/i18n/co.json
languages/i18n/diq.json
languages/i18n/eu.json
languages/i18n/exif/diq.json
languages/i18n/exif/fr.json
languages/i18n/exif/ia.json
languages/i18n/exif/mk.json
languages/i18n/exif/nds-nl.json
languages/i18n/exif/szl.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fit.json
languages/i18n/fr.json
languages/i18n/gl.json
languages/i18n/gom-deva.json
languages/i18n/gom-latn.json
languages/i18n/he.json
languages/i18n/hu.json
languages/i18n/it.json
languages/i18n/ko.json
languages/i18n/luz.json
languages/i18n/nds-nl.json
languages/i18n/nqo.json
languages/i18n/pt-br.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/sd.json
languages/i18n/szl.json
languages/i18n/uk.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
maintenance/Doxyfile
maintenance/convertLinks.php
maintenance/importImages.php
maintenance/mwdocgen.php
maintenance/rebuildFileCache.php
phpunit.xml.dist
resources/Resources.php
resources/src/jquery/jquery.accessKeyLabel.js [deleted file]
resources/src/mediawiki.htmlform.checker.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.monobook.less [deleted file]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.vector.less [deleted file]
resources/src/mediawiki.rcfilters/ui/MainWrapperWidget.js
resources/src/mediawiki.util.js [deleted file]
resources/src/mediawiki.util/jquery.accessKeyLabel.js [new file with mode: 0644]
resources/src/mediawiki.util/util.js [new file with mode: 0644]
tests/phpunit/MediaWikiIntegrationTestCase.php
tests/phpunit/MediaWikiUnitTestCase.php
tests/phpunit/includes/Message/TextFormatterTest.php
tests/phpunit/includes/Rest/BasicAccess/MWBasicRequestAuthorizerTest.php
tests/phpunit/includes/Rest/EntryPointTest.php
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/libs/Message/MessageValueTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php
tests/phpunit/includes/registration/ExtensionRegistryTest.php
tests/phpunit/unit/includes/Rest/Handler/HelloHandlerTest.php
tests/phpunit/unit/includes/Rest/RouterTest.php
tests/phpunit/unit/includes/installer/SqliteInstallerTest.php [new file with mode: 0644]
tests/qunit/QUnitTestResources.php
thumb.php

index 5cd1bbc..229fda4 100644 (file)
@@ -26,6 +26,13 @@ For notes on 1.33.x and older releases, see HISTORY.
 
 === Configuration changes for system administrators in 1.34 ===
 
+In an effort to enforce best practices for passwords, MediaWiki will now warn
+users, and suggest that they change their password, if it is in the list of
+100,000 commonly used passwords that are considered bad passwords. If you want
+to disable this for your users, please add the following to your local settings:
+
+$wgPasswordPolicy['policies']['default']['PasswordNotInLargeBlacklist'] = false;
+
 ==== New configuration ====
 * $wgAllowExternalReqID (T201409) - This configuration setting controls whether
   Mediawiki accepts the request ID set by the incoming request via the
@@ -66,6 +73,7 @@ For notes on 1.33.x and older releases, see HISTORY.
   which was deprecated in 1.30, no longer works. Instead, $wgProxyList should be
   an array with IP addresses as the values, or a string path to a file
   containing one IP address per line.
+* $wgCookieSetOnAutoblock and $wgCookieSetOnIpBlock are now enabled by default.
 * …
 
 ==== Removed configuration ====
@@ -361,6 +369,8 @@ because of Phabricator reports.
   initialized after calling SearchResult::initFromTitle().
 * The UserIsBlockedFrom hook is only called if a block is found first, and
   should only be used to unblock a blocked user.
+* Parameters for index.php from PATH_INFO, such as the title, are no longer
+  written to $_GET.
 * …
 
 === Deprecations in 1.34 ===
@@ -417,6 +427,8 @@ because of Phabricator reports.
 * ResourceLoaderContext::getConfig and ResourceLoaderContext::getLogger have
   been deprecated. Inside ResourceLoaderModule subclasses, use the local methods
   instead. Elsewhere, use the methods from the ResourceLoader class.
+* The 'jquery.accessKeyLabel' module has been deprecated. This jQuery
+  plugin is now ships as part of the 'mediawiki.util' module bundle.
 * The Profiler::setTemplated and Profiler::getTemplated methods have been
   deprecated. Use Profiler::setAllowOutput and Profiler::getAllowOutput
   instead.
diff --git a/api.php b/api.php
index 0fb674b..fe13263 100644 (file)
--- a/api.php
+++ b/api.php
@@ -44,7 +44,7 @@ if ( !$wgRequest->checkUrlExtension() ) {
 // PATH_INFO can be used for stupid things. We don't support it for api.php at
 // all, so error out if it's present.
 if ( isset( $_SERVER['PATH_INFO'] ) && $_SERVER['PATH_INFO'] != '' ) {
-       $correctUrl = wfAppendQuery( wfScript( 'api' ), $wgRequest->getQueryValues() );
+       $correctUrl = wfAppendQuery( wfScript( 'api' ), $wgRequest->getQueryValuesOnly() );
        $correctUrl = wfExpandUrl( $correctUrl, PROTO_CANONICAL );
        header( "Location: $correctUrl", true, 301 );
        echo 'This endpoint does not support "path info", i.e. extra text between "api.php"'
index 81de1a0..5d3fba7 100644 (file)
@@ -4463,7 +4463,7 @@ $wgCentralIdLookupProvider = 'local';
  *             Deprecated since 1.33. Use PasswordNotInLargeBlacklist instead.
  *     - PasswordNotInLargeBlacklist - Password not in best practices list of
  *             100,000 commonly used passwords. Due to the size of the list this
- *      is a probabilistic test.
+ *             is a probabilistic test.
  *
  * If you add custom checks, for Special:PasswordPolicies to display them correctly,
  * every check should have a corresponding passwordpolicies-policy-<check> message,
@@ -4481,28 +4481,25 @@ $wgPasswordPolicy = [
                'bureaucrat' => [
                        'MinimalPasswordLength' => 10,
                        'MinimumPasswordLengthToLogin' => 1,
-                       'PasswordNotInLargeBlacklist' => true,
                ],
                'sysop' => [
                        'MinimalPasswordLength' => 10,
                        'MinimumPasswordLengthToLogin' => 1,
-                       'PasswordNotInLargeBlacklist' => true,
                ],
                'interface-admin' => [
                        'MinimalPasswordLength' => 10,
                        'MinimumPasswordLengthToLogin' => 1,
-                       'PasswordNotInLargeBlacklist' => true,
                ],
                'bot' => [
                        'MinimalPasswordLength' => 10,
                        'MinimumPasswordLengthToLogin' => 1,
-                       'PasswordNotInLargeBlacklist' => true,
                ],
                'default' => [
                        'MinimalPasswordLength' => [ 'value' => 1, 'suggestChangeOnLogin' => true ],
                        'PasswordCannotMatchUsername' => [ 'value' => true, 'suggestChangeOnLogin' => true ],
                        'PasswordCannotMatchBlacklist' => [ 'value' => true, 'suggestChangeOnLogin' => true ],
                        'MaximalPasswordLength' => [ 'value' => 4096, 'suggestChangeOnLogin' => true ],
+                       'PasswordNotInLargeBlacklist' => [ 'value' => true, 'suggestChangeOnLogin' => true ],
                ],
        ],
        'checks' => [
@@ -6071,7 +6068,7 @@ $wgSessionName = false;
  * which case there is a possibility of an attacker discovering the names of revdeleted users, so
  * it is best to use this in conjunction with $wgSecretKey being set).
  */
-$wgCookieSetOnAutoblock = false;
+$wgCookieSetOnAutoblock = true;
 
 /**
  * Whether to set a cookie when a logged-out user is blocked. Doing so means that a blocked user,
@@ -6080,7 +6077,7 @@ $wgCookieSetOnAutoblock = false;
  * case there is a possibility of an attacker discovering the names of revdeleted users, so it
  * is best to use this in conjunction with $wgSecretKey being set).
  */
-$wgCookieSetOnIpBlock = false;
+$wgCookieSetOnIpBlock = true;
 
 /** @} */ # end of cookie settings }
 
index f5eeb16..783dd43 100644 (file)
@@ -45,18 +45,27 @@ class TextFormatter implements ITextFormatter {
                return $this->langCode;
        }
 
-       private static function convertParam( MessageParam $param ) {
+       private function convertParam( MessageParam $param ) {
                if ( $param instanceof ListParam ) {
                        $convertedElements = [];
                        foreach ( $param->getValue() as $element ) {
-                               $convertedElements[] = self::convertParam( $element );
+                               $convertedElements[] = $this->convertParam( $element );
                        }
                        return Message::listParam( $convertedElements, $param->getListType() );
                } elseif ( $param instanceof MessageParam ) {
+                       $value = $param->getValue();
+                       if ( $value instanceof MessageValue ) {
+                               $mv = $value;
+                               $value = $this->createMessage( $mv->getKey() );
+                               foreach ( $mv->getParams() as $mvParam ) {
+                                       $value->params( $this->convertParam( $mvParam ) );
+                               }
+                       }
+
                        if ( $param->getType() === ParamType::TEXT ) {
-                               return $param->getValue();
+                               return $value;
                        } else {
-                               return [ $param->getType() => $param->getValue() ];
+                               return [ $param->getType() => $value ];
                        }
                } else {
                        throw new \InvalidArgumentException( 'Invalid message parameter type' );
@@ -66,7 +75,7 @@ class TextFormatter implements ITextFormatter {
        public function format( MessageValue $mv ) {
                $message = $this->createMessage( $mv->getKey() );
                foreach ( $mv->getParams() as $param ) {
-                       $message->params( self::convertParam( $param ) );
+                       $message->params( $this->convertParam( $param ) );
                }
                $message->inLanguage( $this->langCode );
                return $message->text();
index 2882e66..4d7bd38 100644 (file)
@@ -401,4 +401,22 @@ class PathRouter {
 
                return $value;
        }
+
+       /**
+        * @internal For use by Title and WebRequest only.
+        * @param array $actionPaths
+        * @param string $articlePath
+        * @return string[]|false
+        */
+       public static function getActionPaths( array $actionPaths, $articlePath ) {
+               if ( !$actionPaths ) {
+                       return false;
+               }
+               // Processing of urls for this feature requires that 'view' is set.
+               // By default, set it to the pretty article path.
+               if ( !isset( $actionPaths['view'] ) ) {
+                       $actionPaths['view'] = $articlePath;
+               }
+               return $actionPaths;
+       }
 }
index f28b4ea..ee3441e 100644 (file)
@@ -6,6 +6,7 @@ use ExtensionRegistry;
 use MediaWiki;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Rest\BasicAccess\MWBasicAuthorizer;
+use MediaWiki\Rest\Validator\Validator;
 use RequestContext;
 use Title;
 use WebResponse;
@@ -36,6 +37,7 @@ class EntryPoint {
 
                $services = MediaWikiServices::getInstance();
                $conf = $services->getMainConfig();
+               $objectFactory = $services->getObjectFactory();
 
                if ( !$conf->get( 'EnableRestAPI' ) ) {
                        wfHttpError( 403, 'Access Denied',
@@ -51,6 +53,9 @@ class EntryPoint {
                $authorizer = new MWBasicAuthorizer( $context->getUser(),
                        $services->getPermissionManager() );
 
+               // @phan-suppress-next-line PhanAccessMethodInternal
+               $restValidator = new Validator( $objectFactory, $request, RequestContext::getMain()->getUser() );
+
                global $IP;
                $router = new Router(
                        [ "$IP/includes/Rest/coreRoutes.json" ],
@@ -58,7 +63,9 @@ class EntryPoint {
                        $conf->get( 'RestPath' ),
                        $services->getLocalServerObjectCache(),
                        new ResponseFactory,
-                       $authorizer
+                       $authorizer,
+                       $objectFactory,
+                       $restValidator
                );
 
                $entryPoint = new self(
index c05d8e7..efe2b7e 100644 (file)
@@ -2,7 +2,18 @@
 
 namespace MediaWiki\Rest;
 
+use MediaWiki\Rest\Validator\BodyValidator;
+use MediaWiki\Rest\Validator\NullBodyValidator;
+use MediaWiki\Rest\Validator\Validator;
+
 abstract class Handler {
+
+       /**
+        * (string) ParamValidator constant to specify the source of the parameter.
+        * Value must be 'path', 'query', or 'post'.
+        */
+       const PARAM_SOURCE = 'rest-param-source';
+
        /** @var Router */
        private $router;
 
@@ -15,6 +26,12 @@ abstract class Handler {
        /** @var ResponseFactory */
        private $responseFactory;
 
+       /** @var array|null */
+       private $validatedParams;
+
+       /** @var mixed */
+       private $validatedBody;
+
        /**
         * Initialise with dependencies from the Router. This is called after construction.
         * @internal
@@ -68,6 +85,62 @@ abstract class Handler {
                return $this->responseFactory;
        }
 
+       /**
+        * Validate the request parameters/attributes and body. If there is a validation
+        * failure, a response with an error message should be returned or an
+        * HttpException should be thrown.
+        *
+        * @param Validator $restValidator
+        * @throws HttpException On validation failure.
+        */
+       public function validate( Validator $restValidator ) {
+               $validatedParams = $restValidator->validateParams( $this->getParamSettings() );
+               $validatedBody = $restValidator->validateBody( $this->request, $this );
+               $this->validatedParams = $validatedParams;
+               $this->validatedBody = $validatedBody;
+       }
+
+       /**
+        * Fetch ParamValidator settings for parameters
+        *
+        * Every setting must include self::PARAM_SOURCE to specify which part of
+        * the request is to contain the parameter.
+        *
+        * @return array[] Associative array mapping parameter names to
+        *  ParamValidator settings arrays
+        */
+       public function getParamSettings() {
+               return [];
+       }
+
+       /**
+        * Fetch the BodyValidator
+        * @param string $contentType Content type of the request.
+        * @return BodyValidator
+        */
+       public function getBodyValidator( $contentType ) {
+               return new NullBodyValidator();
+       }
+
+       /**
+        * Fetch the validated parameters
+        *
+        * @return array|null Array mapping parameter names to validated values,
+        *  or null if validateParams() was not called yet or validation failed.
+        */
+       public function getValidatedParams() {
+               return $this->validatedParams;
+       }
+
+       /**
+        * Fetch the validated body
+        * @return mixed Value returned by the body validator, or null if validateParams() was
+        *  not called yet, validation failed, there was no body, or the body was form data.
+        */
+       public function getValidatedBody() {
+               return $this->validatedBody;
+       }
+
        /**
         * The subclass should override this to provide the maximum last modified
         * timestamp for the current request. This is called before execute() in
index 34faee2..495b101 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace MediaWiki\Rest\Handler;
 
+use Wikimedia\ParamValidator\ParamValidator;
 use MediaWiki\Rest\SimpleHandler;
 
 /**
@@ -16,4 +17,14 @@ class HelloHandler extends SimpleHandler {
        public function needsWriteAccess() {
                return false;
        }
+
+       public function getParamSettings() {
+               return [
+                       'name' => [
+                               self::PARAM_SOURCE => 'path',
+                               ParamValidator::PARAM_TYPE => 'string',
+                               ParamValidator::PARAM_REQUIRED => true,
+                       ],
+               ];
+       }
 }
index ae6dde2..bcc414f 100644 (file)
@@ -8,7 +8,19 @@ namespace MediaWiki\Rest;
  * error response.
  */
 class HttpException extends \Exception {
-       public function __construct( $message, $code = 500 ) {
+
+       /** @var array|null */
+       private $errorData = null;
+
+       public function __construct( $message, $code = 500, $errorData = null ) {
                parent::__construct( $message, $code );
+               $this->errorData = $errorData;
+       }
+
+       /**
+        * @return array|null
+        */
+       public function getErrorData() {
+               return $this->errorData;
        }
 }
index d18cdb5..5e5a198 100644 (file)
@@ -175,8 +175,13 @@ class ResponseFactory {
        public function createFromException( $exception ) {
                if ( $exception instanceof HttpException ) {
                        // FIXME can HttpException represent 2xx or 3xx responses?
-                       $response = $this->createHttpError( $exception->getCode(),
-                               [ 'message' => $exception->getMessage() ] );
+                       $response = $this->createHttpError(
+                               $exception->getCode(),
+                               array_merge(
+                                       [ 'message' => $exception->getMessage() ],
+                                       (array)$exception->getErrorData()
+                               )
+                       );
                } else {
                        $response = $this->createHttpError( 500, [
                                'message' => 'Error: exception of type ' . get_class( $exception ),
index 961da01..a520130 100644 (file)
@@ -6,6 +6,7 @@ use AppendIterator;
 use BagOStuff;
 use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface;
 use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
+use MediaWiki\Rest\Validator\Validator;
 use Wikimedia\ObjectFactory;
 
 /**
@@ -44,6 +45,12 @@ class Router {
        /** @var BasicAuthorizerInterface */
        private $basicAuth;
 
+       /** @var ObjectFactory */
+       private $objectFactory;
+
+       /** @var Validator */
+       private $restValidator;
+
        /**
         * @param string[] $routeFiles List of names of JSON files containing routes
         * @param array $extraRoutes Extension route array
@@ -51,10 +58,13 @@ class Router {
         * @param BagOStuff $cacheBag A cache in which to store the matcher trees
         * @param ResponseFactory $responseFactory
         * @param BasicAuthorizerInterface $basicAuth
+        * @param ObjectFactory $objectFactory
+        * @param Validator $restValidator
         */
        public function __construct( $routeFiles, $extraRoutes, $rootPath,
                BagOStuff $cacheBag, ResponseFactory $responseFactory,
-               BasicAuthorizerInterface $basicAuth
+               BasicAuthorizerInterface $basicAuth, ObjectFactory $objectFactory,
+               Validator $restValidator
        ) {
                $this->routeFiles = $routeFiles;
                $this->extraRoutes = $extraRoutes;
@@ -62,6 +72,8 @@ class Router {
                $this->cacheBag = $cacheBag;
                $this->responseFactory = $responseFactory;
                $this->basicAuth = $basicAuth;
+               $this->objectFactory = $objectFactory;
+               $this->restValidator = $restValidator;
        }
 
        /**
@@ -245,9 +257,10 @@ class Router {
                $request->setPathParams( array_map( 'rawurldecode', $match['params'] ) );
                $spec = $match['userData'];
                $objectFactorySpec = array_intersect_key( $spec,
+                       // @todo ObjectFactory supports more keys than this.
                        [ 'factory' => true, 'class' => true, 'args' => true ] );
                /** @var $handler Handler (annotation for PHPStorm) */
-               $handler = ObjectFactory::getObjectFromSpec( $objectFactorySpec );
+               $handler = $this->objectFactory->createObject( $objectFactorySpec );
                $handler->init( $this, $request, $spec, $this->responseFactory );
 
                try {
@@ -268,6 +281,9 @@ class Router {
                if ( $authResult ) {
                        return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
                }
+
+               $handler->validate( $this->restValidator );
+
                $response = $handler->execute();
                if ( !( $response instanceof ResponseInterface ) ) {
                        $response = $this->responseFactory->createFromReturnValue( $response );
index 3718d66..3c19e48 100644 (file)
@@ -14,7 +14,26 @@ namespace MediaWiki\Rest;
  */
 class SimpleHandler extends Handler {
        public function execute() {
-               $params = array_values( $this->getRequest()->getPathParams() );
+               $paramSettings = $this->getParamSettings();
+               $validatedParams = $this->getValidatedParams();
+               $unvalidatedParams = [];
+               $params = [];
+               foreach ( $this->getRequest()->getPathParams() as $name => $value ) {
+                       $source = $paramSettings[$name][self::PARAM_SOURCE] ?? 'unknown';
+                       if ( $source !== 'path' ) {
+                               $unvalidatedParams[] = $name;
+                               $params[] = $value;
+                       } else {
+                               $params[] = $validatedParams[$name];
+                       }
+               }
+
+               if ( $unvalidatedParams ) {
+                       throw new \LogicException(
+                               'Path parameters were not validated: ' . implode( ', ', $unvalidatedParams )
+                       );
+               }
+
                // @phan-suppress-next-line PhanUndeclaredMethod
                return $this->run( ...$params );
        }
diff --git a/includes/Rest/Validator/BodyValidator.php b/includes/Rest/Validator/BodyValidator.php
new file mode 100644 (file)
index 0000000..0147fa8
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+namespace MediaWiki\Rest\Validator;
+
+use MediaWiki\Rest\HttpException;
+use MediaWiki\Rest\RequestInterface;
+
+/**
+ * Interface for validating a request body
+ */
+interface BodyValidator {
+
+       /**
+        * Validate the body of a request.
+        *
+        * This may return a data structure representing the parsed body. When used
+        * in the context of Handler::validateParams(), the returned value will be
+        * available to the handler via Handler::getValidatedBody().
+        *
+        * @param RequestInterface $request
+        * @return mixed
+        * @throws HttpException on validation failure
+        */
+       public function validateBody( RequestInterface $request );
+
+}
diff --git a/includes/Rest/Validator/NullBodyValidator.php b/includes/Rest/Validator/NullBodyValidator.php
new file mode 100644 (file)
index 0000000..4fba5fb
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+namespace MediaWiki\Rest\Validator;
+
+use MediaWiki\Rest\RequestInterface;
+
+/**
+ * Do-nothing body validator
+ */
+class NullBodyValidator implements BodyValidator {
+
+       public function validateBody( RequestInterface $request ) {
+               return null;
+       }
+
+}
diff --git a/includes/Rest/Validator/ParamValidatorCallbacks.php b/includes/Rest/Validator/ParamValidatorCallbacks.php
new file mode 100644 (file)
index 0000000..6c54a50
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+namespace MediaWiki\Rest\Validator;
+
+use InvalidArgumentException;
+use MediaWiki\Rest\RequestInterface;
+use Psr\Http\Message\UploadedFileInterface;
+use User;
+use Wikimedia\ParamValidator\Callbacks;
+use Wikimedia\ParamValidator\ValidationException;
+
+class ParamValidatorCallbacks implements Callbacks {
+
+       /** @var RequestInterface */
+       private $request;
+
+       /** @var User */
+       private $user;
+
+       public function __construct( RequestInterface $request, User $user ) {
+               $this->request = $request;
+               $this->user = $user;
+       }
+
+       /**
+        * Get the raw parameters from a source in the request
+        * @param string $source 'path', 'query', or 'post'
+        * @return array
+        */
+       private function getParamsFromSource( $source ) {
+               switch ( $source ) {
+                       case 'path':
+                               return $this->request->getPathParams();
+
+                       case 'query':
+                               return $this->request->getQueryParams();
+
+                       case 'post':
+                               return $this->request->getPostParams();
+
+                       default:
+                               throw new InvalidArgumentException( __METHOD__ . ": Invalid source '$source'" );
+               }
+       }
+
+       public function hasParam( $name, array $options ) {
+               $params = $this->getParamsFromSource( $options['source'] );
+               return isset( $params[$name] );
+       }
+
+       public function getValue( $name, $default, array $options ) {
+               $params = $this->getParamsFromSource( $options['source'] );
+               return $params[$name] ?? $default;
+               // @todo Should normalization to NFC UTF-8 be done here (much like in the
+               // action API and the rest of MW), or should it be left to handlers to
+               // do whatever normalization they need?
+       }
+
+       public function hasUpload( $name, array $options ) {
+               if ( $options['source'] !== 'post' ) {
+                       return false;
+               }
+               return $this->getUploadedFile( $name, $options ) !== null;
+       }
+
+       public function getUploadedFile( $name, array $options ) {
+               if ( $options['source'] !== 'post' ) {
+                       return null;
+               }
+               $upload = $this->request->getUploadedFiles()[$name] ?? null;
+               return $upload instanceof UploadedFileInterface ? $upload : null;
+       }
+
+       public function recordCondition( ValidationException $condition, array $options ) {
+               // @todo Figure out how to handle warnings
+       }
+
+       public function useHighLimits( array $options ) {
+               return $this->user->isAllowed( 'apihighlimits' );
+       }
+
+}
diff --git a/includes/Rest/Validator/Validator.php b/includes/Rest/Validator/Validator.php
new file mode 100644 (file)
index 0000000..cee1cdb
--- /dev/null
@@ -0,0 +1,163 @@
+<?php
+
+namespace MediaWiki\Rest\Validator;
+
+use MediaWiki\Rest\Handler;
+use MediaWiki\Rest\HttpException;
+use MediaWiki\Rest\RequestInterface;
+use User;
+use Wikimedia\ObjectFactory;
+use Wikimedia\ParamValidator\ParamValidator;
+use Wikimedia\ParamValidator\TypeDef\BooleanDef;
+use Wikimedia\ParamValidator\TypeDef\EnumDef;
+use Wikimedia\ParamValidator\TypeDef\FloatDef;
+use Wikimedia\ParamValidator\TypeDef\IntegerDef;
+use Wikimedia\ParamValidator\TypeDef\PasswordDef;
+use Wikimedia\ParamValidator\TypeDef\StringDef;
+use Wikimedia\ParamValidator\TypeDef\TimestampDef;
+use Wikimedia\ParamValidator\TypeDef\UploadDef;
+use Wikimedia\ParamValidator\ValidationException;
+
+/**
+ * Wrapper for ParamValidator
+ *
+ * It's intended to be used in the REST API classes by composition.
+ *
+ * @since 1.34
+ */
+class Validator {
+
+       /** @var array Type defs for ParamValidator */
+       private static $typeDefs = [
+               'boolean' => [ 'class' => BooleanDef::class ],
+               'enum' => [ 'class' => EnumDef::class ],
+               'integer' => [ 'class' => IntegerDef::class ],
+               'float' => [ 'class' => FloatDef::class ],
+               'double' => [ 'class' => FloatDef::class ],
+               'NULL' => [
+                       'class' => StringDef::class,
+                       'args' => [ [
+                               'allowEmptyWhenRequired' => true,
+                       ] ],
+               ],
+               'password' => [ 'class' => PasswordDef::class ],
+               'string' => [ 'class' => StringDef::class ],
+               'timestamp' => [ 'class' => TimestampDef::class ],
+               'upload' => [ 'class' => UploadDef::class ],
+       ];
+
+       /** @var string[] HTTP request methods that we expect never to have a payload */
+       private static $noBodyMethods = [ 'GET', 'HEAD', 'DELETE' ];
+
+       /** @var string[] HTTP request methods that we expect always to have a payload */
+       private static $bodyMethods = [ 'POST', 'PUT' ];
+
+       /** @var string[] Content types handled via $_POST */
+       private static $formDataContentTypes = [
+               'application/x-www-form-urlencoded',
+               'multipart/form-data',
+       ];
+
+       /** @var ParamValidator */
+       private $paramValidator;
+
+       /**
+        * @internal
+        * @param ObjectFactory $objectFactory
+        * @param RequestInterface $request
+        * @param User $user
+        */
+       public function __construct(
+               ObjectFactory $objectFactory, RequestInterface $request, User $user
+       ) {
+               $this->paramValidator = new ParamValidator(
+                       new ParamValidatorCallbacks( $request, $user ),
+                       $objectFactory,
+                       [
+                               'typeDefs' => self::$typeDefs,
+                       ]
+               );
+       }
+
+       /**
+        * Validate parameters
+        * @param array[] $paramSettings Parameter settings
+        * @return array Validated parameters
+        * @throws HttpException on validaton failure
+        */
+       public function validateParams( array $paramSettings ) {
+               $validatedParams = [];
+               foreach ( $paramSettings as $name => $settings ) {
+                       try {
+                               $validatedParams[$name] = $this->paramValidator->getValue( $name, $settings, [
+                                       'source' => $settings[Handler::PARAM_SOURCE] ?? 'unspecified',
+                               ] );
+                       } catch ( ValidationException $e ) {
+                               throw new HttpException( 'Parameter validation failed', 400, [
+                                       'error' => 'parameter-validation-failed',
+                                       'name' => $e->getParamName(),
+                                       'value' => $e->getParamValue(),
+                                       'failureCode' => $e->getFailureCode(),
+                                       'failureData' => $e->getFailureData(),
+                               ] );
+                       }
+               }
+               return $validatedParams;
+       }
+
+       /**
+        * Validate the body of a request.
+        *
+        * This may return a data structure representing the parsed body. When used
+        * in the context of Handler::validateParams(), the returned value will be
+        * available to the handler via Handler::getValidatedBody().
+        *
+        * @param RequestInterface $request
+        * @param Handler $handler Used to call getBodyValidator()
+        * @return mixed May be null
+        * @throws HttpException on validation failure
+        */
+       public function validateBody( RequestInterface $request, Handler $handler ) {
+               $method = strtoupper( trim( $request->getMethod() ) );
+
+               // If the method should never have a body, don't bother validating.
+               if ( in_array( $method, self::$noBodyMethods, true ) ) {
+                       return null;
+               }
+
+               // Get the content type
+               list( $ct ) = explode( ';', $request->getHeaderLine( 'Content-Type' ), 2 );
+               $ct = strtolower( trim( $ct ) );
+               if ( $ct === '' ) {
+                       // No Content-Type was supplied. RFC 7231 § 3.1.1.5 allows this, but since it's probably a
+                       // client error let's return a 415. But don't 415 for unknown methods and an empty body.
+                       if ( !in_array( $method, self::$bodyMethods, true ) ) {
+                               $body = $request->getBody();
+                               $size = $body->getSize();
+                               if ( $size === null ) {
+                                       // No size available. Try reading 1 byte.
+                                       if ( $body->isSeekable() ) {
+                                               $body->rewind();
+                                       }
+                                       $size = $body->read( 1 ) === '' ? 0 : 1;
+                               }
+                               if ( $size === 0 ) {
+                                       return null;
+                               }
+                       }
+                       throw new HttpException( "A Content-Type header must be supplied with a request payload.", 415, [
+                               'error' => 'no-content-type',
+                       ] );
+               }
+
+               // Form data is parsed into $_POST and $_FILES by PHP and from there is accessed as parameters,
+               // don't bother trying to handle these via BodyValidator too.
+               if ( in_array( $ct, self::$formDataContentTypes, true ) ) {
+                       return null;
+               }
+
+               // Validate the body. BodyValidator throws an HttpException on failure.
+               return $handler->getBodyValidator( $ct )->validateBody( $request );
+       }
+
+}
index d629021..cfb2ac1 100644 (file)
@@ -156,12 +156,6 @@ if ( $wgArticlePath === false ) {
        }
 }
 
-if ( !empty( $wgActionPaths ) && !isset( $wgActionPaths['view'] ) ) {
-       // 'view' is assumed the default action path everywhere in the code
-       // but is rarely filled in $wgActionPaths
-       $wgActionPaths['view'] = $wgArticlePath;
-}
-
 if ( $wgResourceBasePath === null ) {
        $wgResourceBasePath = $wgScriptPath;
 }
@@ -537,12 +531,6 @@ if ( isset( $wgSquidMaxage ) ) {
        $wgSquidMaxage = $wgCdnMaxAge;
 }
 
-// Easy to forget to falsify $wgDebugToolbar for static caches.
-// If file cache or CDN cache is on, just disable this (DWIMD).
-if ( $wgUseFileCache || $wgUseCdn ) {
-       $wgDebugToolbar = false;
-}
-
 // Blacklisted file extensions shouldn't appear on the "allowed" list
 $wgFileExtensions = array_values( array_diff( $wgFileExtensions, $wgFileBlacklist ) );
 
@@ -611,12 +599,7 @@ if ( defined( 'MW_NO_SESSION' ) ) {
        $wgPHPSessionHandling = MW_NO_SESSION === 'warn' ? 'warn' : 'disable';
 }
 
-// Disable MWDebug for command line mode, this prevents MWDebug from eating up
-// all the memory from logging SQL queries on maintenance scripts
-global $wgCommandLineMode;
-if ( $wgDebugToolbar && !$wgCommandLineMode ) {
-       MWDebug::init();
-}
+MWDebug::setup();
 
 // Reset the global service locator, so any services that have already been created will be
 // re-created while taking into account any custom settings and extensions.
index 547b28c..1e93c44 100644 (file)
@@ -51,10 +51,11 @@ class Title implements LinkTarget, IDBAccessObject {
        const CACHE_MAX = 1000;
 
        /**
-        * Used to be GAID_FOR_UPDATE define. Used with getArticleID() and friends
-        * to use the master DB
+        * Used to be GAID_FOR_UPDATE define(). Used with getArticleID() and friends
+        * to use the master DB and inject it into link cache.
+        * @deprecated since 1.34, use Title::READ_LATEST instead.
         */
-       const GAID_FOR_UPDATE = 1;
+       const GAID_FOR_UPDATE = 512;
 
        /**
         * Flag for use with factory methods like newFromLinkTarget() that have
@@ -74,25 +75,18 @@ class Title implements LinkTarget, IDBAccessObject {
 
        /** @var string Text form (spaces not underscores) of the main part */
        public $mTextform = '';
-
        /** @var string URL-encoded form of the main part */
        public $mUrlform = '';
-
        /** @var string Main part with underscores */
        public $mDbkeyform = '';
-
        /** @var string Database key with the initial letter in the case specified by the user */
        protected $mUserCaseDBKey;
-
        /** @var int Namespace index, i.e. one of the NS_xxxx constants */
        public $mNamespace = NS_MAIN;
-
        /** @var string Interwiki prefix */
        public $mInterwiki = '';
-
        /** @var bool Was this Title created from a string with a local interwiki prefix? */
        private $mLocalInterwiki = false;
-
        /** @var string Title fragment (i.e. the bit after the #) */
        public $mFragment = '';
 
@@ -467,16 +461,18 @@ class Title implements LinkTarget, IDBAccessObject {
         * Create a new Title from an article ID
         *
         * @param int $id The page_id corresponding to the Title to create
-        * @param int $flags Use Title::GAID_FOR_UPDATE to use master
+        * @param int $flags Bitfield of class READ_* constants
         * @return Title|null The new object, or null on an error
         */
        public static function newFromID( $id, $flags = 0 ) {
-               $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
-               $row = $db->selectRow(
+               $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c
+               list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
+               $row = wfGetDB( $index )->selectRow(
                        'page',
                        self::getSelectFields(),
                        [ 'page_id' => $id ],
-                       __METHOD__
+                       __METHOD__,
+                       $options
                );
                if ( $row !== false ) {
                        $title = self::newFromRow( $row );
@@ -545,10 +541,10 @@ class Title implements LinkTarget, IDBAccessObject {
                        if ( isset( $row->page_latest ) ) {
                                $this->mLatestID = (int)$row->page_latest;
                        }
-                       if ( !$this->mForcedContentModel && isset( $row->page_content_model ) ) {
-                               $this->mContentModel = (string)$row->page_content_model;
-                       } elseif ( !$this->mForcedContentModel ) {
-                               $this->mContentModel = false; # initialized lazily in getContentModel()
+                       if ( isset( $row->page_content_model ) ) {
+                               $this->lazyFillContentModel( $row->page_content_model );
+                       } else {
+                               $this->lazyFillContentModel( false ); // lazily-load getContentModel()
                        }
                        if ( isset( $row->page_lang ) ) {
                                $this->mDbPageLanguage = (string)$row->page_lang;
@@ -561,9 +557,7 @@ class Title implements LinkTarget, IDBAccessObject {
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
-                       if ( !$this->mForcedContentModel ) {
-                               $this->mContentModel = false; # initialized lazily in getContentModel()
-                       }
+                       $this->lazyFillContentModel( false ); // lazily-load getContentModel()
                }
        }
 
@@ -598,7 +592,6 @@ class Title implements LinkTarget, IDBAccessObject {
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = strtr( $title, '_', ' ' );
-               $t->mContentModel = false; # initialized lazily in getContentModel()
                return $t;
        }
 
@@ -676,7 +669,7 @@ class Title implements LinkTarget, IDBAccessObject {
         * Get the prefixed DB key associated with an ID
         *
         * @param int $id The page_id of the article
-        * @return Title|null An object representing the article, or null if no such article was found
+        * @return string|null An object representing the article, or null if no such article was found
         */
        public static function nameOf( $id ) {
                $dbr = wfGetDB( DB_REPLICA );
@@ -691,8 +684,7 @@ class Title implements LinkTarget, IDBAccessObject {
                        return null;
                }
 
-               $n = self::makeName( $s->page_namespace, $s->page_title );
-               return $n;
+               return self::makeName( $s->page_namespace, $s->page_title );
        }
 
        /**
@@ -1051,21 +1043,31 @@ class Title implements LinkTarget, IDBAccessObject {
         *
         * @todo Deprecate this in favor of SlotRecord::getModel()
         *
-        * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
+        * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return string Content model id
         */
        public function getContentModel( $flags = 0 ) {
-               if ( !$this->mForcedContentModel
-                       && ( !$this->mContentModel || $flags === self::GAID_FOR_UPDATE )
-                       && $this->getArticleID( $flags )
+               if ( $this->mForcedContentModel ) {
+                       if ( !$this->mContentModel ) {
+                               throw new RuntimeException( 'Got out of sync; an empty model is being forced' );
+                       }
+                       // Content model is locked to the currently loaded one
+                       return $this->mContentModel;
+               }
+
+               if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
+                       $this->lazyFillContentModel( $this->loadFieldFromDB( 'page_content_model', $flags ) );
+               } elseif (
+                       ( !$this->mContentModel || $flags & self::GAID_FOR_UPDATE ) &&
+                       $this->getArticleId( $flags )
                ) {
                        $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                        $linkCache->addLinkObj( $this ); # in case we already had an article ID
-                       $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
+                       $this->lazyFillContentModel( $linkCache->getGoodLinkFieldObj( $this, 'model' ) );
                }
 
                if ( !$this->mContentModel ) {
-                       $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
+                       $this->lazyFillContentModel( ContentHandler::getDefaultModelFor( $this ) );
                }
 
                return $this->mContentModel;
@@ -1082,21 +1084,38 @@ class Title implements LinkTarget, IDBAccessObject {
        }
 
        /**
-        * Set a proposed content model for the page for permissions
-        * checking. This does not actually change the content model
-        * of a title!
+        * Set a proposed content model for the page for permissions checking
+        *
+        * This does not actually change the content model of a title in the DB.
+        * It only affects this particular Title instance. The content model is
+        * forced to remain this value until another setContentModel() call.
         *
-        * Additionally, you should make sure you've checked
-        * ContentHandler::canBeUsedOn() first.
+        * ContentHandler::canBeUsedOn() should be checked before calling this
+        * if there is any doubt regarding the applicability of the content model
         *
         * @since 1.28
         * @param string $model CONTENT_MODEL_XXX constant
         */
        public function setContentModel( $model ) {
+               if ( (string)$model === '' ) {
+                       throw new InvalidArgumentException( "Missing CONTENT_MODEL_* constant" );
+               }
+
                $this->mContentModel = $model;
                $this->mForcedContentModel = true;
        }
 
+       /**
+        * If the content model field is not frozen then update it with a retreived value
+        *
+        * @param string|bool $model CONTENT_MODEL_XXX constant or false
+        */
+       private function lazyFillContentModel( $model ) {
+               if ( !$this->mForcedContentModel ) {
+                       $this->mContentModel = ( $model === false ) ? false : (string)$model;
+               }
+       }
+
        /**
         * Get the namespace text
         *
@@ -2068,16 +2087,18 @@ class Title implements LinkTarget, IDBAccessObject {
                                $url = false;
                                $matches = [];
 
-                               if ( !empty( $wgActionPaths )
+                               $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath );
+
+                               if ( $articlePaths
                                        && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
                                ) {
                                        $action = urldecode( $matches[2] );
-                                       if ( isset( $wgActionPaths[$action] ) ) {
+                                       if ( isset( $articlePaths[$action] ) ) {
                                                $query = $matches[1];
                                                if ( isset( $matches[4] ) ) {
                                                        $query .= $matches[4];
                                                }
-                                               $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
+                                               $url = str_replace( '$1', $dbkey, $articlePaths[$action] );
                                                if ( $query != '' ) {
                                                        $url = wfAppendQuery( $url, $query );
                                                }
@@ -2796,10 +2817,7 @@ class Title implements LinkTarget, IDBAccessObject {
                        return;
                }
 
-               // TODO: should probably pass $flags into getArticleID, but it seems hacky
-               // to mix READ_LATEST and GAID_FOR_UPDATE, even if they have the same value.
-               // Maybe deprecate GAID_FOR_UPDATE now that we implement IDBAccessObject?
-               $id = $this->getArticleID();
+               $id = $this->getArticleID( $flags );
                if ( $id ) {
                        $fname = __METHOD__;
                        $loadRestrictionsFromDb = function ( IDatabase $dbr ) use ( $fname, $id ) {
@@ -3023,24 +3041,28 @@ class Title implements LinkTarget, IDBAccessObject {
         * Get the article ID for this Title from the link cache,
         * adding it if necessary
         *
-        * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select
-        *  for update
+        * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return int The ID
         */
        public function getArticleID( $flags = 0 ) {
                if ( $this->mNamespace < 0 ) {
                        $this->mArticleID = 0;
+
                        return $this->mArticleID;
                }
+
                $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                if ( $flags & self::GAID_FOR_UPDATE ) {
                        $oldUpdate = $linkCache->forUpdate( true );
                        $linkCache->clearLink( $this );
                        $this->mArticleID = $linkCache->addLinkObj( $this );
                        $linkCache->forUpdate( $oldUpdate );
+               } elseif ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
+                       $this->mArticleID = (int)$this->loadFieldFromDB( 'page_id', $flags );
                } elseif ( $this->mArticleID == -1 ) {
                        $this->mArticleID = $linkCache->addLinkObj( $this );
                }
+
                return $this->mArticleID;
        }
 
@@ -3048,33 +3070,27 @@ class Title implements LinkTarget, IDBAccessObject {
         * Is this an article that is a redirect page?
         * Uses link cache, adding it if necessary
         *
-        * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
+        * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return bool
         */
        public function isRedirect( $flags = 0 ) {
-               if ( !is_null( $this->mRedirect ) ) {
-                       return $this->mRedirect;
-               }
-               if ( !$this->getArticleID( $flags ) ) {
-                       $this->mRedirect = false;
-                       return $this->mRedirect;
-               }
+               if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
+                       $this->mRedirect = (bool)$this->loadFieldFromDB( 'page_is_redirect', $flags );
+               } else {
+                       if ( $this->mRedirect !== null ) {
+                               return $this->mRedirect;
+                       } elseif ( !$this->getArticleID( $flags ) ) {
+                               $this->mRedirect = false;
 
-               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
-               $linkCache->addLinkObj( $this ); # in case we already had an article ID
-               $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
-               if ( $cached === null ) {
-                       # Trust LinkCache's state over our own
-                       # LinkCache is telling us that the page doesn't exist, despite there being cached
-                       # data relating to an existing page in $this->mArticleID. Updaters should clear
-                       # LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
-                       # set, then LinkCache will definitely be up to date here, since getArticleID() forces
-                       # LinkCache to refresh its data from the master.
-                       $this->mRedirect = false;
-                       return $this->mRedirect;
-               }
+                               return $this->mRedirect;
+                       }
 
-               $this->mRedirect = (bool)$cached;
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
+                       $linkCache->addLinkObj( $this ); // in case we already had an article ID
+                       // Note that LinkCache returns null if it thinks the page does not exist;
+                       // always trust the state of LinkCache over that of this Title instance.
+                       $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+               }
 
                return $this->mRedirect;
        }
@@ -3083,27 +3099,26 @@ class Title implements LinkTarget, IDBAccessObject {
         * What is the length of this page?
         * Uses link cache, adding it if necessary
         *
-        * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
+        * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return int
         */
        public function getLength( $flags = 0 ) {
-               if ( $this->mLength != -1 ) {
-                       return $this->mLength;
-               }
-               if ( !$this->getArticleID( $flags ) ) {
-                       $this->mLength = 0;
-                       return $this->mLength;
-               }
-               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
-               $linkCache->addLinkObj( $this ); # in case we already had an article ID
-               $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
-               if ( $cached === null ) {
-                       # Trust LinkCache's state over our own, as for isRedirect()
-                       $this->mLength = 0;
-                       return $this->mLength;
-               }
+               if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
+                       $this->mLength = (int)$this->loadFieldFromDB( 'page_len', $flags );
+               } else {
+                       if ( $this->mLength != -1 ) {
+                               return $this->mLength;
+                       } elseif ( !$this->getArticleID( $flags ) ) {
+                               $this->mLength = 0;
+                               return $this->mLength;
+                       }
 
-               $this->mLength = intval( $cached );
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
+                       $linkCache->addLinkObj( $this ); // in case we already had an article ID
+                       // Note that LinkCache returns null if it thinks the page does not exist;
+                       // always trust the state of LinkCache over that of this Title instance.
+                       $this->mLength = (int)$linkCache->getGoodLinkFieldObj( $this, 'length' );
+               }
 
                return $this->mLength;
        }
@@ -3111,49 +3126,46 @@ class Title implements LinkTarget, IDBAccessObject {
        /**
         * What is the page_latest field for this page?
         *
-        * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
+        * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return int Int or 0 if the page doesn't exist
         */
        public function getLatestRevID( $flags = 0 ) {
-               if ( !( $flags & self::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
-                       return intval( $this->mLatestID );
-               }
-               if ( !$this->getArticleID( $flags ) ) {
-                       $this->mLatestID = 0;
-                       return $this->mLatestID;
-               }
-               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
-               $linkCache->addLinkObj( $this ); # in case we already had an article ID
-               $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
-               if ( $cached === null ) {
-                       # Trust LinkCache's state over our own, as for isRedirect()
-                       $this->mLatestID = 0;
-                       return $this->mLatestID;
-               }
+               if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
+                       $this->mLatestID = (int)$this->loadFieldFromDB( 'page_latest', $flags );
+               } else {
+                       if ( $this->mLatestID !== false ) {
+                               return (int)$this->mLatestID;
+                       } elseif ( !$this->getArticleID( $flags ) ) {
+                               $this->mLatestID = 0;
+
+                               return $this->mLatestID;
+                       }
 
-               $this->mLatestID = intval( $cached );
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
+                       $linkCache->addLinkObj( $this ); // in case we already had an article ID
+                       // Note that LinkCache returns null if it thinks the page does not exist;
+                       // always trust the state of LinkCache over that of this Title instance.
+                       $this->mLatestID = (int)$linkCache->getGoodLinkFieldObj( $this, 'revision' );
+               }
 
                return $this->mLatestID;
        }
 
        /**
-        * This clears some fields in this object, and clears any associated
-        * keys in the "bad links" section of the link cache.
+        * Inject a page ID, reset DB-loaded fields, and clear the link cache for this title
+        *
+        * This can be called on page insertion to allow loading of the new page_id without
+        * having to create a new Title instance. Likewise with deletion.
         *
-        * - This is called from WikiPage::doEditContent() and WikiPage::insertOn() to allow
-        * loading of the new page_id. It's also called from
-        * WikiPage::doDeleteArticleReal()
+        * @note This overrides Title::setContentModel()
         *
-        * @param int $newid The new Article ID
+        * @param int|bool $id Page ID, 0 for non-existant, or false for "unknown" (lazy-load)
         */
-       public function resetArticleID( $newid ) {
-               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
-               $linkCache->clearLink( $this );
-
-               if ( $newid === false ) {
+       public function resetArticleID( $id ) {
+               if ( $id === false ) {
                        $this->mArticleID = -1;
                } else {
-                       $this->mArticleID = intval( $newid );
+                       $this->mArticleID = (int)$id;
                }
                $this->mRestrictionsLoaded = false;
                $this->mRestrictions = [];
@@ -3162,10 +3174,13 @@ class Title implements LinkTarget, IDBAccessObject {
                $this->mLength = -1;
                $this->mLatestID = false;
                $this->mContentModel = false;
+               $this->mForcedContentModel = false;
                $this->mEstimateRevisions = null;
                $this->mPageLanguage = null;
                $this->mDbPageLanguage = false;
                $this->mIsBigDeletion = null;
+
+               MediaWikiServices::getInstance()->getLinkCache()->clearLink( $this );
        }
 
        public static function clearCaches() {
@@ -3499,6 +3514,7 @@ class Title implements LinkTarget, IDBAccessObject {
 
                $mp = MediaWikiServices::getInstance()->getMovePageFactory()->newMovePage( $this, $nt );
                $method = $auth ? 'moveIfAllowed' : 'move';
+               /** @var Status $status */
                $status = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags );
                if ( $status->isOK() ) {
                        return true;
@@ -3531,6 +3547,7 @@ class Title implements LinkTarget, IDBAccessObject {
 
                $mp = new MovePage( $this, $nt );
                $method = $auth ? 'moveSubpagesIfAllowed' : 'moveSubpages';
+               /** @var Status $result */
                $result = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags );
 
                if ( !$result->isOK() ) {
@@ -3539,6 +3556,7 @@ class Title implements LinkTarget, IDBAccessObject {
 
                $retval = [];
                foreach ( $result->getValue() as $key => $status ) {
+                       /** @var Status $status */
                        if ( $status->isOK() ) {
                                $retval[$key] = $status->getValue();
                        } else {
@@ -3549,8 +3567,9 @@ class Title implements LinkTarget, IDBAccessObject {
        }
 
        /**
-        * Checks if this page is just a one-rev redirect.
-        * Adds lock, so don't use just for light purposes.
+        * Locks the page row and check if this page is single revision redirect
+        *
+        * This updates the cached fields of this instance via Title::loadFromRow()
         *
         * @return bool
         */
@@ -3730,24 +3749,22 @@ class Title implements LinkTarget, IDBAccessObject {
        /**
         * Get next/previous revision ID relative to another revision ID
         * @param int $revId Revision ID. Get the revision that was before this one.
-        * @param int $flags Title::GAID_FOR_UPDATE
+        * @param int $flags Bitfield of class READ_* constants
         * @param string $dir 'next' or 'prev'
         * @return int|bool New revision ID, or false if none exists
         */
        private function getRelativeRevisionID( $revId, $flags, $dir ) {
                $rl = MediaWikiServices::getInstance()->getRevisionLookup();
-               $rlFlags = $flags === self::GAID_FOR_UPDATE ? IDBAccessObject::READ_LATEST : 0;
-               $rev = $rl->getRevisionById( $revId, $rlFlags );
+               $rev = $rl->getRevisionById( $revId, $flags );
                if ( !$rev ) {
                        return false;
                }
-               $oldRev = $dir === 'next'
-                       ? $rl->getNextRevision( $rev, $rlFlags )
-                       : $rl->getPreviousRevision( $rev, $rlFlags );
-               if ( !$oldRev ) {
-                       return false;
-               }
-               return $oldRev->getId();
+
+               $oldRev = ( $dir === 'next' )
+                       ? $rl->getNextRevision( $rev, $flags )
+                       : $rl->getPreviousRevision( $rev, $flags );
+
+               return $oldRev ? $oldRev->getId() : false;
        }
 
        /**
@@ -3755,7 +3772,7 @@ class Title implements LinkTarget, IDBAccessObject {
         *
         * @deprecated since 1.34, use RevisionLookup::getPreviousRevision
         * @param int $revId Revision ID. Get the revision that was before this one.
-        * @param int $flags Title::GAID_FOR_UPDATE
+        * @param int $flags Bitfield of class READ_* constants
         * @return int|bool Old revision ID, or false if none exists
         */
        public function getPreviousRevisionID( $revId, $flags = 0 ) {
@@ -3767,7 +3784,7 @@ class Title implements LinkTarget, IDBAccessObject {
         *
         * @deprecated since 1.34, use RevisionLookup::getNextRevision
         * @param int $revId Revision ID. Get the revision that was after this one.
-        * @param int $flags Title::GAID_FOR_UPDATE
+        * @param int $flags Bitfield of class READ_* constants
         * @return int|bool Next revision ID, or false if none exists
         */
        public function getNextRevisionID( $revId, $flags = 0 ) {
@@ -3777,21 +3794,26 @@ class Title implements LinkTarget, IDBAccessObject {
        /**
         * Get the first revision of the page
         *
-        * @param int $flags Title::GAID_FOR_UPDATE
+        * @param int $flags Bitfield of class READ_* constants
         * @return Revision|null If page doesn't exist
         */
        public function getFirstRevision( $flags = 0 ) {
                $pageId = $this->getArticleID( $flags );
                if ( $pageId ) {
-                       $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
+                       $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c
+                       list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
                        $revQuery = Revision::getQueryInfo();
-                       $row = $db->selectRow( $revQuery['tables'], $revQuery['fields'],
+                       $row = wfGetDB( $index )->selectRow(
+                               $revQuery['tables'], $revQuery['fields'],
                                [ 'rev_page' => $pageId ],
                                __METHOD__,
-                               [
-                                       'ORDER BY' => 'rev_timestamp ASC, rev_id ASC',
-                                       'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
-                               ],
+                               array_merge(
+                                       [
+                                               'ORDER BY' => 'rev_timestamp ASC, rev_id ASC',
+                                               'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
+                                       ],
+                                       $options
+                               ),
                                $revQuery['joins']
                        );
                        if ( $row ) {
@@ -3804,7 +3826,7 @@ class Title implements LinkTarget, IDBAccessObject {
        /**
         * Get the oldest revision timestamp of this page
         *
-        * @param int $flags Title::GAID_FOR_UPDATE
+        * @param int $flags Bitfield of class READ_* constants
         * @return string|null MW timestamp
         */
        public function getEarliestRevTime( $flags = 0 ) {
@@ -4035,8 +4057,7 @@ class Title implements LinkTarget, IDBAccessObject {
         * If you want to know if a title can be meaningfully viewed, you should
         * probably call the isKnown() method instead.
         *
-        * @param int $flags An optional bit field; may be Title::GAID_FOR_UPDATE to check
-        *   from master/for update
+        * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return bool
         */
        public function exists( $flags = 0 ) {
@@ -4632,6 +4653,27 @@ class Title implements LinkTarget, IDBAccessObject {
                return $notices;
        }
 
+       /**
+        * @param int $flags Bitfield of class READ_* constants
+        * @return string|bool
+        */
+       private function loadFieldFromDB( $field, $flags ) {
+               if ( !in_array( $field, self::getSelectFields(), true ) ) {
+                       return false; // field does not exist
+               }
+
+               $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c
+               list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
+
+               return wfGetDB( $index )->selectField(
+                       'page',
+                       $field,
+                       $this->pageCond(),
+                       __METHOD__,
+                       $options
+               );
+       }
+
        /**
         * @return array
         */
index 9b8f5a6..a48d032 100644 (file)
@@ -40,9 +40,28 @@ use Wikimedia\AtEase\AtEase;
  * @ingroup HTTP
  */
 class WebRequest {
-       /** @var array */
+       /**
+        * The parameters from $_GET, $_POST and the path router
+        * @var array
+        */
        protected $data;
-       /** @var array */
+
+       /**
+        * The parameters from $_GET. The parameters from the path router are
+        * added by interpolateTitle() during Setup.php.
+        * @var array
+        */
+       protected $queryAndPathParams;
+
+       /**
+        * The parameters from $_GET only.
+        */
+       protected $queryParams;
+
+       /**
+        * Lazy-initialized request headers indexed by upper-case header name
+        * @var array
+        */
        protected $headers = [];
 
        /**
@@ -100,6 +119,8 @@ class WebRequest {
                // POST overrides GET data
                // We don't use $_REQUEST here to avoid interference from cookies...
                $this->data = $_POST + $_GET;
+
+               $this->queryAndPathParams = $this->queryParams = $_GET;
        }
 
        /**
@@ -162,8 +183,9 @@ class WebRequest {
                        }
 
                        global $wgActionPaths;
-                       if ( $wgActionPaths ) {
-                               $router->add( $wgActionPaths, [ 'action' => '$key' ] );
+                       $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath );
+                       if ( $articlePaths ) {
+                               $router->add( $articlePaths, [ 'action' => '$key' ] );
                        }
 
                        global $wgVariantArticlePath;
@@ -335,7 +357,7 @@ class WebRequest {
 
                $matches = self::getPathInfo( 'title' );
                foreach ( $matches as $key => $val ) {
-                       $this->data[$key] = $_GET[$key] = $_REQUEST[$key] = $val;
+                       $this->data[$key] = $this->queryAndPathParams[$key] = $val;
                }
        }
 
@@ -667,14 +689,27 @@ class WebRequest {
        }
 
        /**
-        * Get the values passed in the query string.
+        * Get the values passed in the query string and the path router parameters.
         * No transformation is performed on the values.
         *
         * @codeCoverageIgnore
         * @return array
         */
        public function getQueryValues() {
-               return $_GET;
+               return $this->queryAndPathParams;
+       }
+
+       /**
+        * Get the values passed in the query string only, not including the path
+        * router parameters. This is less suitable for self-links to index.php but
+        * useful for other entry points. No transformation is performed on the
+        * values.
+        *
+        * @since 1.34
+        * @return array
+        */
+       public function getQueryValuesOnly() {
+               return $this->queryParams;
        }
 
        /**
index 0cd9806..056d10c 100644 (file)
@@ -992,7 +992,7 @@ abstract class ApiBase extends ContextSource {
                        return;
                }
 
-               $queryValues = $this->getRequest()->getQueryValues();
+               $queryValues = $this->getRequest()->getQueryValuesOnly();
                $badParams = [];
                foreach ( $params as $param ) {
                        if ( $prefix !== 'noprefix' ) {
index a0d61b2..ab78ee4 100644 (file)
@@ -94,10 +94,6 @@ class HTMLFileCache extends FileCacheBase {
                $config = MediaWikiServices::getInstance()->getMainConfig();
 
                if ( !$config->get( 'UseFileCache' ) && $mode !== self::MODE_REBUILD ) {
-                       return false;
-               } elseif ( $config->get( 'DebugToolbar' ) ) {
-                       wfDebug( "HTML file cache skipped. \$wgDebugToolbar on\n" );
-
                        return false;
                }
 
index e877836..6bcb0e6 100644 (file)
@@ -67,6 +67,30 @@ class MWDebug {
         */
        protected static $deprecationWarnings = [];
 
+       /**
+        * @internal For use by Setup.php only.
+        */
+       public static function setup() {
+               global $wgDebugToolbar,
+                       $wgUseCdn, $wgUseFileCache, $wgCommandLineMode;
+
+               if (
+                       // Easy to forget to falsify $wgDebugToolbar for static caches.
+                       // If file cache or CDN cache is on, just disable this (DWIMD).
+                       $wgUseCdn ||
+                       $wgUseFileCache ||
+                       // Keep MWDebug off on CLI. This prevents MWDebug from eating up
+                       // all the memory for logging SQL queries in maintenance scripts.
+                       $wgCommandLineMode
+               ) {
+                       return;
+               }
+
+               if ( $wgDebugToolbar ) {
+                       self::init();
+               }
+       }
+
        /**
         * Enabled the debugger and load resource module.
         * This is called by Setup.php when $wgDebugToolbar is true.
index b8697e5..7fcda4c 100644 (file)
@@ -1511,10 +1511,6 @@ class DifferenceEngine extends ContextSource {
        private function userCanEdit( Revision $rev ) {
                $user = $this->getUser();
 
-               if ( !$rev->getContentHandler()->supportsDirectEditing() ) {
-                       return false;
-               }
-
                if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
                        return false;
                }
index 91c6e6a..048abbb 100644 (file)
@@ -41,7 +41,7 @@ abstract class HTMLFormField {
         * the input object itself.  It should not implement the surrounding
         * table cells/rows, or labels/help messages.
         *
-        * @param string $value The value to set the input to; eg a default
+        * @param mixed $value The value to set the input to; eg a default
         *     text for a text input.
         *
         * @return string Valid HTML.
index 595b71e..8e51858 100644 (file)
@@ -77,7 +77,6 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
         * mParams['columns'] is an array with column labels as keys and column tags as values.
         *
         * @param array $value Array of the options that should be checked
-        * @suppress PhanParamSignatureMismatch
         *
         * @return string
         */
index 01bb30e..b21a177 100644 (file)
@@ -71,16 +71,19 @@ class SqliteInstaller extends DatabaseInstaller {
        }
 
        public function getGlobalDefaults() {
+               global $IP;
                $defaults = parent::getGlobalDefaults();
-               if ( isset( $_SERVER['DOCUMENT_ROOT'] ) ) {
-                       $path = str_replace(
-                               [ '/', '\\' ],
-                               DIRECTORY_SEPARATOR,
-                               dirname( $_SERVER['DOCUMENT_ROOT'] ) . '/data'
-                       );
-
-                       $defaults['wgSQLiteDataDir'] = $path;
+               if ( !empty( $_SERVER['DOCUMENT_ROOT'] ) ) {
+                       $path = dirname( $_SERVER['DOCUMENT_ROOT'] );
+               } else {
+                       // We use $IP when unable to get $_SERVER['DOCUMENT_ROOT']
+                       $path = $IP;
                }
+               $defaults['wgSQLiteDataDir'] = str_replace(
+                       [ '/', '\\' ],
+                       DIRECTORY_SEPARATOR,
+                       $path . '/data'
+               );
                return $defaults;
        }
 
@@ -122,7 +125,7 @@ class SqliteInstaller extends DatabaseInstaller {
 
                # Try realpath() if the directory already exists
                $dir = self::realpath( $this->getVar( 'wgSQLiteDataDir' ) );
-               $result = self::dataDirOKmaybeCreate( $dir, true /* create? */ );
+               $result = self::checkDataDir( $dir );
                if ( $result->isOK() ) {
                        # Try expanding again in case we've just created it
                        $dir = self::realpath( $dir );
@@ -135,12 +138,17 @@ class SqliteInstaller extends DatabaseInstaller {
        }
 
        /**
-        * @param string $dir
-        * @param bool $create
-        * @return Status
+        * Check if the data directory is writable or can be created
+        * @param string $dir Path to the data directory
+        * @return Status Return fatal Status if $dir un-writable or no permission to create a directory
         */
-       private static function dataDirOKmaybeCreate( $dir, $create = false ) {
-               if ( !is_dir( $dir ) ) {
+       private static function checkDataDir( $dir ) : Status {
+               if ( is_dir( $dir ) ) {
+                       if ( !is_readable( $dir ) ) {
+                               return Status::newFatal( 'config-sqlite-dir-unwritable', $dir );
+                       }
+               } else {
+                       // Check the parent directory if $dir not exists
                        if ( !is_writable( dirname( $dir ) ) ) {
                                $webserverGroup = Installer::maybeGetWebserverPrimaryGroup();
                                if ( $webserverGroup !== null ) {
@@ -156,25 +164,25 @@ class SqliteInstaller extends DatabaseInstaller {
                                        );
                                }
                        }
+               }
+               return Status::newGood();
+       }
 
-                       # Called early on in the installer, later we just want to sanity check
-                       # if it's still writable
-                       if ( $create ) {
-                               Wikimedia\suppressWarnings();
-                               $ok = wfMkdirParents( $dir, 0700, __METHOD__ );
-                               Wikimedia\restoreWarnings();
-                               if ( !$ok ) {
-                                       return Status::newFatal( 'config-sqlite-mkdir-error', $dir );
-                               }
-                               # Put a .htaccess file in in case the user didn't take our advice
-                               file_put_contents( "$dir/.htaccess", "Deny from all\n" );
+       /**
+        * @param string $dir Path to the data directory
+        * @return Status Return good Status if without error
+        */
+       private static function createDataDir( $dir ) : Status {
+               if ( !is_dir( $dir ) ) {
+                       Wikimedia\suppressWarnings();
+                       $ok = wfMkdirParents( $dir, 0700, __METHOD__ );
+                       Wikimedia\restoreWarnings();
+                       if ( !$ok ) {
+                               return Status::newFatal( 'config-sqlite-mkdir-error', $dir );
                        }
                }
-               if ( !is_writable( $dir ) ) {
-                       return Status::newFatal( 'config-sqlite-dir-unwritable', $dir );
-               }
-
-               # We haven't blown up yet, fall through
+               # Put a .htaccess file in in case the user didn't take our advice
+               file_put_contents( "$dir/.htaccess", "Deny from all\n" );
                return Status::newGood();
        }
 
@@ -217,10 +225,15 @@ class SqliteInstaller extends DatabaseInstaller {
        public function setupDatabase() {
                $dir = $this->getVar( 'wgSQLiteDataDir' );
 
-               # Sanity check. We checked this before but maybe someone deleted the
-               # data dir between then and now
-               $dir_status = self::dataDirOKmaybeCreate( $dir, false /* create? */ );
-               if ( !$dir_status->isOK() ) {
+               # Sanity check (Only available in web installation). We checked this before but maybe someone
+               # deleted the data dir between then and now
+               $dir_status = self::checkDataDir( $dir );
+               if ( $dir_status->isGood() ) {
+                       $res = self::createDataDir( $dir );
+                       if ( !$res->isGood() ) {
+                               return $res;
+                       }
+               } else {
                        return $dir_status;
                }
 
index 834c129..ccb68b9 100644 (file)
@@ -89,7 +89,7 @@
        "config-uploads-not-safe": "<strong>تحذير:</strong>  الدليل الافتراضي للمرفوعات <code>$1</code> عرضة لتنفيذ سكريبتات عشوائية،\nعلى الرغم من أن ميدياويكي يتحقق من كل الملفات المرفوعة للتهديدات الأمنية، فمن المستحسن بشدة [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security إغلاق هذه الثغرة الأمنية] قبل تمكين المرفوعات.",
        "config-no-cli-uploads-check": "<strong>تحذير:</strong> لم يتم تحديد الدليل الافتراضي للمرفوعات (<code>$1</code>) للقابلية للتأثر\nلتنفيذ برنامج تعسفي أثناء تثبيت CLI.",
        "config-brokenlibxml": "يحتوي نظامك على مجموعة من إصدارات PHP وlibxml2 ويمكن أن تسبب فسادا للبيانات في ميدياويكي وتطبيقات الويب الأخرى;\nقم بالترقية إلى libxml2 2.7.3 أو أحدث ([https://bugs.php.net/bug.php؟id=45996 تم تدقيم العلة مع PHP]); \nتم إحباط التثبيت.",
-       "config-suhosin-max-value-length": "تم تثبيت Suhosin وتقييد وسيط GET <code>length</code> إلى $1 بايت،\nسيعمل مكون ResourceLoader في ميدياويكي حول هذا الحد، لكن ذلك سيؤدي إلى انخفاض مستوى الأداء، \nإذا كان ذلك ممكنا، فيجب تعيين <code>suhosin.get.max_value_length</code> على 1024 أو أعلى في <code>php.ini</code>، وتعيين <code>$wgResourceLoaderMaxQueryLength</code> لنفس القيمة في <code>LocalSettings.php</code>.",
+       "config-suhosin-max-value-length": "تم تثبيت Suhosin وتقييد وسيط GET <code>length</code> إلى $1 بايت،\nيتطلب ميدياويكي أن يكون <code>suhosin.get.max_value_length</code> $2 على الأقل، قم بتعطيل هذا الإعداد  أو بزيادة هذه القيمة إلى $3 في <code>php.ini</code>.",
        "config-using-32bit": "<strong>تحذير:</strong> يبدو أن نظامك يعمل مع الأعداد الصحيحة 32 بت، هذا [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit لا يُنصَح به].",
        "config-db-type": "نوع قاعدة البيانات:",
        "config-db-host": "مضيف قاعدة البيانات:",
index fddb6a2..e7e653b 100644 (file)
        "config-welcome": "=== Vérifications liées à l’environnement ===\nDes vérifications de base vont maintenant être effectuées pour voir si cet environnement est adapté à l’installation de MediaWiki.\nRappelez-vous d’inclure ces informations si vous recherchez de l’aide sur la manière de terminer l’installation.",
        "config-welcome-section-copyright": "=== Droit d’auteur et conditions ===\n\n$1\n\nCe programme est un logiciel libre : vous pouvez le redistribuer ou le modifier selon les termes de la Licence Publique Générale GNU telle que publiée par la Free Software Foundation (version 2 de la Licence, ou, à votre choix, toute version ultérieure).\n\nCe programme est distribué dans l’espoir qu’il sera utile, mais '''sans aucune garantie''' : sans même les garanties implicites de '''commercialisabilité''' ou d’'''adéquation à un usage particulier'''.\nVoir la Licence Publique Générale GNU pour plus de détails.\n\nVous devriez avoir reçu [$2 une copie de la Licence Publique Générale GNU] avec ce programme ; dans le cas contraire, écrivez à la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ou [https://www.gnu.org/copyleft/gpl.html lisez-la en ligne].",
        "config-sidebar": "* [https://www.mediawiki.org Accueil MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guide de l’utilisateur]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guide de l’administrateur]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]",
-       "config-sidebar-readme": "Me lire",
+       "config-sidebar-readme": "Lisez-moi",
        "config-sidebar-relnotes": "Notes de version",
-       "config-sidebar-license": "Copie",
+       "config-sidebar-license": "Droit de copie",
        "config-sidebar-upgrade": "Mise à jour",
        "config-env-good": "L’environnement a été vérifié.\nVous pouvez installer MediaWiki.",
        "config-env-bad": "L’environnement a été vérifié.\nVous ne pouvez pas installer MediaWiki.",
        "config-env-php": "PHP $1 est installé.",
        "config-env-hhvm": "HHVM $1 est installé.",
-       "config-unicode-using-intl": "Utilisation de [https://php.net/manual/en/book.intl.php extension intl de PHP] pour la normalisation Unicode.",
-       "config-unicode-pure-php-warning": "<strong>Attention :</strong> L’[https://php.net/manual/en/book.intl.php extension intl de PHP] n’est pas disponible pour la normalisation d’Unicode, retour à la version lente implémentée en PHP seulement.\nSi votre site web sera très fréquenté, vous devriez lire ceci : [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (en anglais).",
+       "config-unicode-using-intl": "Utilisation de l’[https://php.net/manual/en/book.intl.php extension intl de PHP] pour la normalisation Unicode.",
+       "config-unicode-pure-php-warning": "<strong>Attention :</strong> l’[https://php.net/manual/en/book.intl.php extension intl de PHP] n’est pas disponible pour la normalisation d’Unicode, retour à la version lente implémentée en PHP seulement.\nSi votre site web sera très fréquenté, vous devriez lire ceci : [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations ''Unicode normalization''] (en anglais).",
        "config-unicode-update-warning": "<strong>Attention :</strong> la version installée du normalisateur Unicode utilise une ancienne version de la bibliothèque logicielle du [http://site.icu-project.org/ ''Projet ICU''].\nVous devriez faire une [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations mise à jour] si vous êtes concerné par l’usage d’Unicode.",
        "config-no-db": "Impossible de trouver un pilote de base de données approprié ! Vous devez installer un pilote de base de données pour PHP. {{PLURAL:$2|Le type suivant|Les types suivants}} de bases de données {{PLURAL:$2|est reconnu|sont reconnus}} : $1.\n\nSi vous avez compilé PHP vous-même, reconfigurez-le avec un client de base de données activé, par exemple en utilisant <code>./configure --with-mysqli</code>.  \nSi vous avez installé PHP depuis un paquet Debian ou Ubuntu, alors vous devrez aussi installer, par exemple, le paquet <code>php-mysql</code>.",
-       "config-outdated-sqlite": "<strong>Attention :</strong> vous avez SQLite $2, qui est inférieur à la version minimale requise $1. SQLite sera indisponible.",
-       "config-no-fts3": "<strong>Attention :</strong> SQLite est compilé sans le [//sqlite.org/fts3.html module FTS3] ; les fonctions de recherche ne seront pas disponibles sur ce moteur.",
+       "config-outdated-sqlite": "<strong>Attention:</strong> vous avez SQLite $2, qui est inférieur à la version minimale requise $1. SQLite sera indisponible.",
+       "config-no-fts3": "<strong>Attention:</strong> SQLite est compilé sans le [//sqlite.org/fts3.html module FTS3] ; les fonctions de recherche ne seront pas disponibles sur ce moteur.",
        "config-pcre-old": "<strong>Erreur fatale :</strong> PCRE $1 ou ultérieur est nécessaire.\nVotre binaire PHP est lié avec PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/Plus d’information sur PCRE].",
        "config-pcre-no-utf8": "<strong>Erreur fatale :</strong> le module PCRE de PHP semble être compilé sans la prise en charge de PCRE_UTF8.\nMediaWiki a besoin de la gestion d’UTF-8 pour fonctionner correctement.",
        "config-memory-raised": "Le paramètre <code>memory_limit</code> de PHP était à $1, porté à $2.",
        "config-apc": "[https://www.php.net/apc APC] est installé",
        "config-apcu": "[https://www.php.net/apcu APCu] est installé",
        "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] est installé",
-       "config-no-cache-apcu": "<strong>Attention :</strong> impossible de trouver [https://www.php.net/apcu APCu] ou [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nLa mise en cache des objets n’est pas activée.",
-       "config-mod-security": "<strong>Attention :</strong> votre serveur web a activé [https://modsecurity.org/ mod_security]/mod_security2 . Dans plusieurs configurations communes cela pose des problèmes à MediaWiki ou à d’autres applications qui permettent aux utilisateurs de publier un contenu quelconque. \nSi possible, ceci devrait être désactivé. Sinon, reportez-vous à [https://modsecurity.org/documentation/ la documentation de mod_security] ou contactez l’assistance de votre hébergeur si vous rencontrez des erreurs aléatoires.",
+       "config-no-cache-apcu": "<strong>Attention:</strong> impossible de trouver [https://www.php.net/apcu APCu] ou [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nLa mise en cache des objets n’est pas activée.",
+       "config-mod-security": "<strong>Attention :</strong> votre serveur web a activé [https://modsecurity.org/ mod_security] ou mod_security2. Dans plusieurs configurations communes cela pose des problèmes à MediaWiki ou à d’autres applications qui permettent aux utilisateurs de publier un contenu quelconque. \nSi possible, ceci devrait être désactivé. Sinon, reportez-vous à la [https://modsecurity.org/documentation/  documentation de mod_security] ou contactez l’assistance de votre hébergeur si vous rencontrez des erreurs aléatoires.",
        "config-diff3-bad": "L’utilitaire de comparaison de texte GNU diff3 est introuvable. Vous pouvez l’ignorer pour le moment, mais cela peut provoquer des conflits de modification plus souvent.",
        "config-git": "Logiciel de contrôle de version Git trouvé : <code>$1</code>.",
        "config-git-bad": "Logiciel de contrôle de version Git non trouvé. Vous pouvez l’ignorer pour le moment. Notez que Special:Version n’affichera pas les hachages de validation.",
        "config-imagemagick": "ImageMagick trouvé : <code>$1</code>.\nLa génération de vignettes d’images sera activée si vous activez les téléversements.",
-       "config-gd": "La bibliothèque graphique GD intégrée a été trouvée.\nLa miniaturisation d'images sera activée si vous activez le téléversement de fichiers.",
+       "config-gd": "La bibliothèque graphique GD intégrée a été trouvée.\nLa miniaturisation dimages sera activée si vous activez le téléversement de fichiers.",
        "config-no-scaling": "Impossible de trouver la bibliothèque GD ou ImageMagick.\nLa miniaturisation d’images sera désactivée.",
        "config-no-uri": "<strong>Erreur :</strong> impossible de déterminer l’URI du script actuel.\nInstallation interrompue.",
-       "config-no-cli-uri": "<strong>Attention :</strong> Aucun <code>--scriptpath</code> n’a été spécifié ; <code>$1</code> sera utilisé par défaut.",
+       "config-no-cli-uri": "<strong>Attention :</strong> aucun <code>--scriptpath</code> n’a été spécifié ; <code>$1</code> sera utilisé par défaut.",
        "config-using-server": "Utilisation du nom de serveur « <nowiki>$1</nowiki> ».",
-       "config-using-uri": "Utilisation de l'URL de serveur \"<nowiki>$1$2</nowiki>\".",
-       "config-uploads-not-safe": "<strong>Attention :</strong> Votre répertoire par défaut pour les téléversements, <code>$1</code>, est vulnérable, car il peut exécuter n’importe quel script.\nBien que MediaWiki vérifie tous les fichiers téléversés, il est fortement recommandé de [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security fermer cette faille de sécurité] (texte en anglais) avant d’activer les téléversements.",
-       "config-no-cli-uploads-check": "'''Attention:''' Votre répertoire par défaut pour les imports(<code>$1</code>) n'est pas contrôlé concernant la vulnérabilité d'exécution de scripts arbitraires lors de l'installation CLI.",
-       "config-brokenlibxml": "Votre système utilise une combinaison de versions de PHP et libxml2 qui est boguée et peut engendrer des corruptions cachées de données dans MediaWiki et d’autres applications web.\nVeuillez mettre à jour votre système vers libxml2 2.7.3 ou plus récent ([https://bugs.php.net/bug.php?id=45996 bogue déposé auprès de PHP]).\nInstallation interrompue.",
-       "config-suhosin-max-value-length": "Suhosin est installé et limite la <code>longueur</code> du paramètre GET à $1 octets.\nLe composant ResourceLoader de MediaWiki va répondre en respectant cette limite, mais ses performances seront dégradées. Si possible, vous devriez définir <code>suhosin.get.max_value_length</code> à 1024 ou plus dans le fichier <code>php.ini</code>, et fixer <code>$wgResourceLoaderMaxQueryLength</code> à la même valeur dans <code>LocalSettings.php</code>.",
-       "config-using-32bit": "<strong>Attention:</strong> votre système semble utiliser les entiers sur 32 bits. Ceci n'est [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit pas recommandé].",
-       "config-db-type": "Type de base de données :",
-       "config-db-host": "Nom d’hôte de la base de données :",
+       "config-using-uri": "Utilisation de l’URL de serveur « <nowiki>$1$2</nowiki> ».",
+       "config-uploads-not-safe": "<strong>Attention :</strong> votre répertoire par défaut pour les téléversements, <code>$1</code>, est vulnérable, car il peut exécuter n’importe quel script.\nBien que MediaWiki vérifie tous les fichiers téléversés, il est fortement recommandé de [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security fermer cette faille de sécurité] (texte en anglais) avant d’activer les téléversements.",
+       "config-no-cli-uploads-check": "'''Attention :''' votre répertoire par défaut pour les imports (<code>$1</code>) n’est pas contrôlé concernant la vulnérabilité d’exécution de scripts arbitraires lors de l’installation CLI.",
+       "config-brokenlibxml": "Votre système utilise une combinaison de versions de PHP et libxml2 qui est boguée et peut engendrer des corruptions cachées de données dans MediaWiki et d’autres applications web.\nVeuillez mettre à jour votre système vers libxml2 2.7.3 ou plus récent ([https://bugs.php.net/bug.php?id=45996 anomalie signalée auprès de PHP]).\nInstallation interrompue.",
+       "config-suhosin-max-value-length": "Suhosin est installé et limite la <code>longueur</code> de paramètre GET à $1 octets.\nLe composant ResourceLoader de MediaWiki va répondre en respectant cette limite, mais ses performances seront dégradées. Si possible, vous devriez définir <code>suhosin.get.max_value_length</code> à 1024 ou plus dans le fichier <code>php.ini</code>, et fixer <code>$wgResourceLoaderMaxQueryLength</code> à la même valeur dans <code>LocalSettings.php</code>.",
+       "config-using-32bit": "<strong>Attention :</strong> votre système semble utiliser les entiers sur 32 bits. Ceci n’est [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit pas recommandé].",
+       "config-db-type": "Type de base de données:",
+       "config-db-host": "Nom d’hôte de la base de données:",
        "config-db-host-help": "Si votre serveur de base de données est sur un serveur différent, saisissez ici son nom d’hôte ou son adresse IP.\n\nSi vous utilisez un hébergement mutualisé, votre hébergeur doit vous avoir fourni le nom d’hôte correct dans sa documentation.\n\nSi vous utilisez MySQL, « localhost » peut ne pas fonctionner comme nom de serveur. S’il ne fonctionne pas, essayez « 127.0.0.1 » comme adresse IP locale.\n\nSi vous utilisez PostgreSQL, laissez ce champ vide pour vous connecter via un socket Unix.",
        "config-db-wiki-settings": "Identifier ce wiki",
-       "config-db-name": "Nom de la base de données (sans tirets):",
-       "config-db-name-help": "Choisissez un nom qui identifie votre wiki.\nIl ne doit pas contenir d'espaces.\n\nSi vous utilisez un hébergement web partagé, votre hébergeur vous fournira un nom spécifique de base de données à utiliser, ou bien vous permet de créer des bases de données via un panneau de contrôle.",
-       "config-db-install-account": "Compte d'utilisateur pour l'installation",
-       "config-db-username": "Nom d’utilisateur de la base de données :",
-       "config-db-password": "Mot de passe de la base de données :",
-       "config-db-install-username": "Entrez le nom d’utilisateur qui sera utilisé pour se connecter à la base de données pendant le processus d'installation. Il ne s’agit pas du nom d’utilisateur du compte MediaWiki, mais du nom d’utilisateur pour votre base de données.",
-       "config-db-install-password": "Entrez le mot de passe qui sera utilisé pour se connecter à la base de données pendant le processus d'installation. Il ne s’agit pas du mot de passe du compte MediaWiki, mais du mot de passe pour votre base de données.",
-       "config-db-install-help": "Entrez le nom d'utilisateur et le mot de passe qui seront utilisés pour se connecter à la base de données pendant le processus d'installation.",
-       "config-db-account-lock": "Utiliser le même nom d'utilisateur et le même mot de passe pendant le fonctionnement habituel",
-       "config-db-wiki-account": "Compte d'utilisateur pour le fonctionnement habituel",
-       "config-db-wiki-help": "Entrez le nom d'utilisateur et le mot de passe qui seront utilisés pour se connecter à la base de données pendant le fonctionnement habituel du wiki.\nSi le compte n'existe pas, et le compte d'installation dispose de privilèges suffisants, ce compte d'utilisateur sera créé avec les privilèges minimum requis pour faire fonctionner le wiki.",
-       "config-db-prefix": "Préfixe des tables de la base de données (sans tirets) :",
-       "config-db-prefix-help": "Si vous avez besoin de partager une base de données entre plusieurs wikis, ou entre MediaWiki et une autre application Web, vous pouvez choisir d'ajouter un préfixe à tous les noms de table pour éviter les conflits.\nNe pas utiliser d'espaces.\n\nCe champ est généralement laissé vide.",
+       "config-db-name": "Nom de la base de données (sans tirets):",
+       "config-db-name-help": "Choisissez un nom qui identifie votre wiki.\nIl ne doit pas contenir despaces.\n\nSi vous utilisez un hébergement web partagé, votre hébergeur vous fournira un nom spécifique de base de données à utiliser, ou bien vous permet de créer des bases de données via un panneau de contrôle.",
+       "config-db-install-account": "Compte d’utilisateur pour l’installation",
+       "config-db-username": "Nom d’utilisateur de la base de données:",
+       "config-db-password": "Mot de passe de la base de données:",
+       "config-db-install-username": "Entrez le nom d’utilisateur qui sera utilisé pour se connecter à la base de données pendant le processus d’installation. Il ne s’agit pas du nom d’utilisateur du compte MediaWiki (serveur web, PHP) sur le système, mais du nom d’utilisateur dans votre base de données SQL.",
+       "config-db-install-password": "Entrez le mot de passe qui sera utilisé pour se connecter à la base de données pendant le processus d’installation. Il ne s’agit pas du mot de passe du compte MediaWiki (serveur web, PHP) sur le système, mais du mot de passe dans votre base de données SQL.",
+       "config-db-install-help": "Entrez le nom d’utilisateur et le mot de passe qui seront utilisés pour se connecter à la base de données pendant le processus d’installation.",
+       "config-db-account-lock": "Utiliser le même nom d’utilisateur et le même mot de passe pour les opérations communes",
+       "config-db-wiki-account": "Compte d’utilisateur pour les opérations communes",
+       "config-db-wiki-help": "Entrez le nom d’utilisateur et le mot de passe qui seront utilisés pour se connecter à la base de données pendant le fonctionnement habituel du wiki.\nSi le compte n’existe pas, et le compte d’installation dispose de privilèges suffisants, ce compte d’utilisateur sera créé avec les privilèges minimum requis pour faire fonctionner le wiki.",
+       "config-db-prefix": "Préfixe des tables de la base de données (sans tirets):",
+       "config-db-prefix-help": "Si vous avez besoin de partager une base de données entre plusieurs wikis, ou entre MediaWiki et une autre application Web, vous pouvez choisir d’ajouter un préfixe à tous les noms de table pour éviter les conflits.\nNe pas utiliser d’espaces.\n\nCe champ est généralement laissé vide.",
        "config-mysql-old": "MySQL $1 ou version ultérieure est requis. Vous avez $2.",
-       "config-db-port": "Port de la base de données :",
-       "config-db-schema": "Schéma pour MediaWiki (sans tirets) :",
-       "config-db-schema-help": "Ce schéma est généralement correct.\nNe le changez que si vous êtes sûr que c'est nécessaire.",
-       "config-pg-test-error": "Impossible de se connecter à la base de données '''$1''' : $2",
-       "config-sqlite-dir": "Dossier des données SQLite :",
-       "config-sqlite-dir-help": "SQLite stocke toutes les données dans un fichier unique.\n\nLe répertoire que vous fournissez doit être accessible en écriture par le serveur lors de l'installation.\n\nIl '''ne faut pas''' qu'il soit accessible via le web, c'est pourquoi il n'est pas à l'endroit où sont vos fichiers PHP.\n\nL'installateur écrira un fichier <code>.htaccess</code> en même temps, mais s'il y a échec, quelqu'un peut accéder à votre base de données.\nCela comprend les données des utilisateurs (adresses de courriel, mots de passe hachés) ainsi que des révisions supprimées et d'autres données confidentielles du wiki.\n\nEnvisagez de placer la base de données ailleurs, par exemple dans <code>/var/lib/mediawiki/yourwiki</code>.",
+       "config-db-port": "Port de la base de données:",
+       "config-db-schema": "Schéma pour MediaWiki (sans tirets):",
+       "config-db-schema-help": "Ce schéma est généralement correct.\nNe le changez que si vous êtes sûr que cest nécessaire.",
+       "config-pg-test-error": "Impossible de se connecter à la base de données '''$1''': $2",
+       "config-sqlite-dir": "Dossier des données SQLite:",
+       "config-sqlite-dir-help": "SQLite stocke toutes les données dans un fichier unique.\n\nLe répertoire que vous fournissez doit être accessible en écriture par le serveur lors de l’installation.\n\nIl '''ne faut pas''' qu’il soit accessible via le web, c’est pourquoi il n’est pas à l’endroit où sont vos fichiers PHP.\n\nL’installateur écrira un fichier <code>.htaccess</code> en même temps, mais s’il y a échec, quelqu’un peut accéder à votre base de données.\nCela comprend les données des utilisateurs (adresses de courriel, mots de passe hachés) ainsi que des révisions supprimées et d’autres données confidentielles du wiki.\n\nEnvisagez de placer la base de données ailleurs, par exemple dans <code>/var/lib/mediawiki/yourwiki</code>.",
        "config-type-mysql": "MariaDB, MySQL , ou compatible",
        "config-type-postgres": "PostgreSQL",
        "config-type-sqlite": "SQLite",
index 1f8a3c6..6c58c43 100644 (file)
        "config-uploads-not-safe": "Used as a part of environment check result. Parameters:\n* $1 - name of directory for images: <code>$IP/images/</code>",
        "config-no-cli-uploads-check": "CLI = [[w:Command-line interface|command-line interface]] (i.e. the installer runs as a command-line script, not using HTML interface via an internet browser)",
        "config-brokenlibxml": "Status message in the MediaWiki installer environment checks.",
-       "config-suhosin-max-value-length": "{{doc-important|Do not translate \"length\", \"suhosin.get.max_value_length\", and \"php.ini\".}}\nThis error message is shown when PHP configuration <code>suhosin.get.max_value_length</code> is not high enough.\n\n* $1 - The current value\n* $2 - The minimum required value\n* $3 - The recommended value\n",
+       "config-suhosin-max-value-length": "{{doc-important|Do not translate \"length\", \"suhosin.get.max_value_length\", and \"php.ini\".}}\nThis error message is shown when PHP configuration <code>suhosin.get.max_value_length</code> is not high enough.\n\n* $1 - The current value\n* $2 - The minimum required value\n* $3 - The recommended value",
        "config-using-32bit": "Warning message shown when installing on a 32-bit system.",
        "config-db-type": "Field label in the MediaWiki installer followed by possible database types.",
        "config-db-host": "Used as label.\n\nAlso used in {{msg-mw|Config-missing-db-host}}.",
index 3ea2df6..b83cf18 100644 (file)
        "config-mysql-innodb": "InnoDB (preporučeno)",
        "config-site-name": "Ime wikija:",
        "config-site-name-help": "Ovo će se pojaviti u naslovnoj traci pregledača i na raznim drugim mestima.",
+       "config-site-name-blank": "Upišite ime sajta.",
        "config-project-namespace": "Projektni imenski prostor:",
        "config-ns-generic": "Projekat",
        "config-ns-site-name": "Isto ime kao wikija: $1",
        "config-admin-password-blank": "Upišite lozinku za administratorski račun",
        "config-admin-password-mismatch": "Lozinke što ste upisali se ne poklapaju.",
        "config-admin-email": "E-mail adresa:",
+       "config-admin-error-bademail": "Upisali ste neispravnu adresu e-pošte",
+       "config-pingback": "Dijeli podatke o instalaciji s programerima MediaWikija.",
+       "config-almost-done": "Skoro ste gotovi!\nSada možete preskočiti preostala postavljivanja i odmah instalirati wiki.",
+       "config-optional-continue": "Postavi mi više pitanja.",
+       "config-optional-skip": "Već mi je dosadilo, daj samo instaliraj wiki.",
        "config-profile": "Profil korisničkih prava:",
        "config-profile-wiki": "Otvoren wiki",
        "config-profile-no-anon": "Neophodno otvaranje računa",
        "config-profile-private": "Privatan wiki",
        "config-license": "Autorska prava i licenca:",
        "config-license-none": "Bez podnožja za licencu",
+       "config-license-cc-choose": "Odaberite drugu licencu Creative Commonsa na vaš izbor",
        "config-email-settings": "Podešavanja e-pošte",
        "config-enable-email": "Omogući odlaznu e-poštu",
        "config-email-user": "Omogući slanje e-poruka među korisnicima",
        "config-email-usertalk-help": "Omogući korisnicima da primaju obaveštenja o promenama u njihovim korisničkim razgovornim stranicama ako su ih omogućili u podešavanjima.",
        "config-email-watchlist": "Omogući obaveštenja o spisku praćenja",
        "config-email-watchlist-help": "Omogući korisnicima da primaju obaveštenja o svojim nadgledanim stranicama ako su ih omogućili u podešavanjima.",
+       "config-email-auth": "Omogući potvrdu identiteta putem e-pošte",
+       "config-email-sender": "Povratna adresa e-pošte:",
        "config-upload-settings": "Otpremanja slika i datoteka",
        "config-upload-enable": "Omogući postavljanje datoteka",
        "config-upload-deleted": "Folder za obrisane datoteke:",
        "config-memcached-servers": "Memcached-serveri:",
        "config-memcached-help": "Lista IP adresa za uporabu u Memcached.\nTreba da se navede jednu u svaki red, kao i port što će se koristiti. Na primer:\n 127.0.0.1:11211\n 192.168.1.25:1234",
        "config-memcache-needservers": "Odabrali ste Memcached kao vaš tip međuspremnika (keša), ali niste naveli nijedan server.",
+       "config-extensions": "Dodaci",
+       "config-skins": "Skinovi",
+       "config-skins-use-as-default": "Koristi kao predodređenu",
+       "config-skins-missing": "Nisam pronašao nijednu temu. MediaWiki će koristiti rezervnu temu dok ne instalirate druge.",
+       "config-skins-must-enable-some": "Će treba izabrati barem jednu temu.",
+       "config-skins-must-enable-default": "Tema koju ste izabrali za predodređenu mora se omogućiti.",
        "config-install-step-done": "gotovo",
        "config-install-step-failed": "nije uspjelo",
        "config-install-extensions": "Uključujem dodatke",
        "config-help": "pomoć",
        "config-help-tooltip": "kliknite da rasklopite",
        "config-nofile": "Datoteka \"$1\" nije pronađena. Da nije obrisana?",
+       "config-extension-link": "Jeste li znali da vaš wiki podržava [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions dodatke]?\n\nMožete ih pregledati [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category po kategoriji]",
        "config-skins-screenshots": "$1 (ekr. snimci: $2)",
        "config-extensions-requires": "$1 (zahtjeva $2)",
        "config-screenshot": "ekranski snimak",
index 00f6e99..c433e47 100644 (file)
@@ -3,16 +3,7 @@
 namespace Wikimedia\Message;
 
 /**
- * ITextFormatter is a simplified interface to the Message class. It converts
- * MessageValue message specifiers to localized text in a certain language.
- *
- * MessageValue supports message keys, and parameters with a wide variety of
- * types. It does not expose any details of how messages are retrieved from
- * storage or what format they are stored in.
- *
- * Thus, TextFormatter supports single message keys, but not the concept of
- * presence or absence of a key from storage. So it does not support
- * fallback sequences of multiple keys.
+ * Converts MessageValue message specifiers to localized plain text in a certain language.
  *
  * The caller cannot modify the details of message translation, such as which
  * of multiple sources the message is taken from. Any such flags may be injected
index c6a9c65..970ef6b 100644 (file)
@@ -3,19 +3,17 @@
 namespace Wikimedia\Message;
 
 /**
- * The class for list parameters
+ * Value object representing a message parameter that consists of a list of values.
+ *
+ * Message parameter classes are pure value objects and are safely newable.
  */
 class ListParam extends MessageParam {
        private $listType;
 
        /**
-        * @param string $listType One of the ListType constants:
-        *   - ListType::COMMA: A comma-separated list
-        *   - ListType::SEMICOLON: A semicolon-separated list
-        *   - ListType::PIPE: A pipe-separated list
-        *   - ListType::TEXT: A natural language list, separated by commas and
-        *     the word "and".
-        * @param (MessageParam|string)[] $elements An array of parameters
+        * @param string $listType One of the ListType constants.
+        * @param (MessageParam|MessageValue|string|int|float)[] $elements Values in the list.
+        *  Values that are not instances of MessageParam are wrapped using ParamType::TEXT.
         */
        public function __construct( $listType, array $elements ) {
                $this->type = ParamType::LIST;
@@ -24,8 +22,8 @@ class ListParam extends MessageParam {
                foreach ( $elements as $element ) {
                        if ( $element instanceof MessageParam ) {
                                $this->value[] = $element;
-                       } elseif ( is_scalar( $element ) ) {
-                               $this->value[] = new TextParam( ParamType::TEXT, $element );
+                       } elseif ( is_scalar( $element ) || $element instanceof MessageValue ) {
+                               $this->value[] = new ScalarParam( ParamType::TEXT, $element );
                        } else {
                                throw new \InvalidArgumentException(
                                        'ListParam elements must be MessageParam or scalar' );
index 60f3a82..f846464 100644 (file)
@@ -4,8 +4,7 @@ namespace Wikimedia\Message;
 
 /**
  * The constants used to specify list types. The values of the constants are an
- * unstable implementation detail and correspond to the names of the list types
- * in the Message class.
+ * unstable implementation detail.
  */
 class ListType {
        /** A comma-separated list */
index 8162212..b6475a7 100644 (file)
@@ -3,7 +3,9 @@
 namespace Wikimedia\Message;
 
 /**
- * The base class for message parameters.
+ * Value object representing a message parameter that consists of a list of values.
+ *
+ * Message parameter classes are pure value objects and are safely newable.
  */
 abstract class MessageParam {
        protected $type;
@@ -21,7 +23,7 @@ abstract class MessageParam {
        /**
         * Get the input value of the parameter
         *
-        * @return int|float|string|array
+        * @return mixed
         */
        public function getValue() {
                return $this->value;
index 13b97f2..1d80d60 100644 (file)
@@ -3,7 +3,13 @@
 namespace Wikimedia\Message;
 
 /**
- * A MessageValue holds a key and an array of parameters
+ * Value object representing a message for i18n.
+ *
+ * A MessageValue holds a key and an array of parameters. It can be converted
+ * to a string in a particular language using formatters obtained from an
+ * IMessageFormatterFactory.
+ *
+ * MessageValues are pure value objects and are safely newable.
  */
 class MessageValue {
        /** @var string */
@@ -14,9 +20,8 @@ class MessageValue {
 
        /**
         * @param string $key
-        * @param array $params Each element of the parameter array
-        *   may be either a MessageParam or a scalar. If it is a scalar, it is
-        *   converted to a parameter of type TEXT.
+        * @param (MessageParam|MessageValue|string|int|float)[] $params Values that are not instances
+        *  of MessageParam are wrapped using ParamType::TEXT.
         */
        public function __construct( $key, $params = [] ) {
                $this->key = $key;
@@ -45,7 +50,7 @@ class MessageValue {
        /**
         * Chainable mutator which adds text parameters and MessageParam parameters
         *
-        * @param mixed ...$values Scalar or MessageParam values
+        * @param MessageParam|MessageValue|string|int|float ...$values
         * @return MessageValue
         */
        public function params( ...$values ) {
@@ -53,7 +58,7 @@ class MessageValue {
                        if ( $value instanceof MessageParam ) {
                                $this->params[] = $value;
                        } else {
-                               $this->params[] = new TextParam( ParamType::TEXT, $value );
+                               $this->params[] = new ScalarParam( ParamType::TEXT, $value );
                        }
                }
                return $this;
@@ -63,12 +68,12 @@ class MessageValue {
         * Chainable mutator which adds text parameters with a common type
         *
         * @param string $type One of the ParamType constants
-        * @param mixed ...$values Scalar values
+        * @param MessageValue|string|int|float ...$values Scalar values
         * @return MessageValue
         */
        public function textParamsOfType( $type, ...$values ) {
                foreach ( $values as $value ) {
-                       $this->params[] = new TextParam( $type, $value );
+                       $this->params[] = new ScalarParam( $type, $value );
                }
                return $this;
        }
@@ -77,7 +82,8 @@ class MessageValue {
         * Chainable mutator which adds list parameters with a common type
         *
         * @param string $listType One of the ListType constants
-        * @param array ...$values Each value should be an array of list items.
+        * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
+        *  is an array of items suitable to pass as $params to ListParam::__construct()
         * @return MessageValue
         */
        public function listParamsOfType( $listType, ...$values ) {
@@ -88,9 +94,9 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters of type text.
+        * Chainable mutator which adds parameters of type text (ParamType::TEXT).
         *
-        * @param string ...$values
+        * @param MessageValue|string|int|float ...$values
         * @return MessageValue
         */
        public function textParams( ...$values ) {
@@ -98,9 +104,9 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds numeric parameters
+        * Chainable mutator which adds numeric parameters (ParamType::NUM).
         *
-        * @param mixed ...$values
+        * @param int|float ...$values
         * @return MessageValue
         */
        public function numParams( ...$values ) {
@@ -109,8 +115,10 @@ class MessageValue {
 
        /**
         * Chainable mutator which adds parameters which are a duration specified
-        * in seconds. This is similar to timePeriodParams() except that the result
-        * will be more verbose.
+        * in seconds (ParamType::DURATION_LONG).
+        *
+        * This is similar to shorDurationParams() except that the result will be
+        * more verbose.
         *
         * @param int|float ...$values
         * @return MessageValue
@@ -120,8 +128,10 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters which are a time period in seconds.
-        * This is similar to durationParams() except that the result will be more
+        * Chainable mutator which adds parameters which are a duration specified
+        * in seconds (ParamType::DURATION_SHORT).
+        *
+        * This is similar to longDurationParams() except that the result will be more
         * compact.
         *
         * @param int|float ...$values
@@ -132,10 +142,10 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters which are an expiry timestamp
-        * as used in the MediaWiki database schema.
+        * Chainable mutator which adds parameters which are an expiry timestamp (ParamType::EXPIRY).
         *
-        * @param string ...$values
+        * @param string ...$values Timestamp as accepted by the Wikimedia\Timestamp library,
+        *  or "infinity"
         * @return MessageValue
         */
        public function expiryParams( ...$values ) {
@@ -143,7 +153,7 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters which are a number of bytes.
+        * Chainable mutator which adds parameters which are a number of bytes (ParamType::SIZE).
         *
         * @param int ...$values
         * @return MessageValue
@@ -154,7 +164,7 @@ class MessageValue {
 
        /**
         * Chainable mutator which adds parameters which are a number of bits per
-        * second.
+        * second (ParamType::BITRATE).
         *
         * @param int|float ...$values
         * @return MessageValue
@@ -164,9 +174,13 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters of type "raw".
+        * Chainable mutator which adds "raw" parameters (ParamType::RAW).
         *
-        * @param mixed ...$values
+        * Raw parameters are substituted after formatter processing. The caller is responsible
+        * for ensuring that the value will be safe for the intended output format, and
+        * documenting what that intended output format is.
+        *
+        * @param string ...$values
         * @return MessageValue
         */
        public function rawParams( ...$values ) {
@@ -174,21 +188,27 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters of type "plaintext".
+        * Chainable mutator which adds plaintext parameters (ParamType::PLAINTEXT).
+        *
+        * Plaintext parameters are substituted after formatter processing. The value
+        * will be escaped by the formatter as appropriate for the target output format
+        * so as to be represented as plain text rather than as any sort of markup.
+        *
+        * @param string ...$values
+        * @return MessageValue
         */
        public function plaintextParams( ...$values ) {
                return $this->textParamsOfType( ParamType::PLAINTEXT, ...$values );
        }
 
        /**
-        * Chainable mutator which adds comma lists. Each comma list is an array of
-        * list elements, and each list element is either a MessageParam or a
-        * string. String parameters are converted to parameters of type "text".
+        * Chainable mutator which adds comma lists (ListType::COMMA).
         *
         * The list parameters thus created are formatted as a comma-separated list,
         * or some local equivalent.
         *
-        * @param (MessageParam|string)[] ...$values
+        * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
+        *  is an array of items suitable to pass as $params to ListParam::__construct()
         * @return MessageValue
         */
        public function commaListParams( ...$values ) {
@@ -196,15 +216,13 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds semicolon lists. Each semicolon list is an
-        * array of list elements, and each list element is either a MessageParam
-        * or a string. String parameters are converted to parameters of type
-        * "text".
+        * Chainable mutator which adds semicolon lists (ListType::SEMICOLON).
         *
         * The list parameters thus created are formatted as a semicolon-separated
         * list, or some local equivalent.
         *
-        * @param (MessageParam|string)[] ...$values
+        * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
+        *  is an array of items suitable to pass as $params to ListParam::__construct()
         * @return MessageValue
         */
        public function semicolonListParams( ...$values ) {
@@ -212,14 +230,13 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds pipe lists. Each pipe list is an array of
-        * list elements, and each list element is either a MessageParam or a
-        * string. String parameters are converted to parameters of type "text".
+        * Chainable mutator which adds pipe lists (ListType::PIPE).
         *
         * The list parameters thus created are formatted as a pipe ("|") -separated
         * list, or some local equivalent.
         *
-        * @param (MessageParam|string)[] ...$values
+        * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
+        *  is an array of items suitable to pass as $params to ListParam::__construct()
         * @return MessageValue
         */
        public function pipeListParams( ...$values ) {
@@ -227,9 +244,7 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds text lists. Each text list is an array of
-        * list elements, and each list element is either a MessageParam or a
-        * string. String parameters are converted to parameters of type "text".
+        * Chainable mutator which adds natural-language lists (ListType::AND).
         *
         * The list parameters thus created, when formatted, are joined as in natural
         * language. In English, this means a comma-separated list, with the last
index 890ef38..4db7112 100644 (file)
@@ -4,44 +4,62 @@ namespace Wikimedia\Message;
 
 /**
  * The constants used to specify parameter types. The values of the constants
- * are an unstable implementation detail, and correspond to the names of the
- * parameter types in the Message class.
+ * are an unstable implementation detail.
+ *
+ * Unless otherwise noted, these should be used with an instance of ScalarParam.
  */
 class ParamType {
-       /** A simple text parameter */
+       /** A simple text string or another MessageValue, not otherwise formatted. */
        const TEXT = 'text';
 
        /** A number, to be formatted using local digits and separators */
        const NUM = 'num';
 
-       /** A number of seconds, to be formatted as natural language text. */
+       /**
+        * A number of seconds, to be formatted as natural language text.
+        * The value will be output exactly.
+        */
        const DURATION_LONG = 'duration';
 
-       /** A number of seconds, to be formatted in an abbreviated way. */
+       /**
+        * A number of seconds, to be formatted as natural language text in an abbreviated way.
+        * The output will be rounded to an appropriate magnitude.
+        */
        const DURATION_SHORT = 'timeperiod';
 
        /**
-        * An expiry time for a block. The input is either a timestamp in one
-        * of the formats accepted by the Wikimedia\Timestamp library, or
-        * "infinity" for an infinite block.
+        * An expiry time.
+        *
+        * The input is either a timestamp in one of the formats accepted by the
+        * Wikimedia\Timestamp library, or "infinity" if the thing doesn't expire.
+        *
+        * The output is a date and time in local format, or a string representing
+        * an "infinite" expiry.
         */
        const EXPIRY = 'expiry';
 
-       /** A number of bytes. */
+       /** A number of bytes. The output will be rounded to an appropriate magnitude. */
        const SIZE = 'size';
 
-       /** A number of bits per second. */
+       /** A number of bits per second. The output will be rounded to an appropriate magnitude. */
        const BITRATE = 'bitrate';
 
-       /** The list type (ListParam) */
+       /** A list of values. Must be used with ListParam. */
        const LIST = 'list';
 
        /**
-        * A text parameter which is substituted after preprocessing, and so is
-        * not available to the preprocessor and cannot be modified by it.
+        * A text parameter which is substituted after formatter processing.
+        *
+        * The creator of the parameter and message is responsible for ensuring
+        * that the value will be safe for the intended output format, and
+        * documenting what that intended output format is.
         */
        const RAW = 'raw';
 
-       /** Reserved for future use. */
+       /**
+        * A text parameter which is substituted after formatter processing.
+        * The output will be escaped as appropriate for the output format so
+        * as to represent plain text rather than any sort of markup.
+        */
        const PLAINTEXT = 'plaintext';
 }
diff --git a/includes/libs/Message/README.md b/includes/libs/Message/README.md
new file mode 100644 (file)
index 0000000..9f6255a
--- /dev/null
@@ -0,0 +1,291 @@
+Wikimedia Internationalization Library
+======================================
+
+This library provides interfaces and value objects for internationalization (i18n)
+of applications in PHP.
+
+It is based on the i18n code used in MediaWiki, and is also intended to be
+compatible with [jQuery.i18n], a JavaScript i18n library.
+
+Concepts
+--------
+
+Any text string that is needed in an application is a **message**. This might
+be something like a button label, a sentence, or a longer text. Each message is
+assigned a **message key**, which is used as the identifier in code.
+
+Each message is translated into various languages, each represented by a
+**language code**. The message's text (as translated into each language) can
+contain **placeholders**, which represents a place in the message where a
+**parameter** is to be inserted, and **formatting commands**. It might be plain
+text other than these placeholders and formatting commands, or it might be in a
+**markup language** such as wikitext or Markdown.
+
+A **formatter** is used to convert the message key and parameters into a text
+representation in a particular language and **output format**.
+
+The library itself imposes few restrictions on all of these concepts; this
+document contains recommendations to help various implementations operate in
+compatible ways.
+
+Usage
+-----
+
+<pre lang="php">
+use Wikimedia\Message\MessageValue;
+use Wikimedia\Message\MessageParam;
+use Wikimedia\Message\ParamType;
+
+// Constructor interface
+$message = new MessageValue( 'message-key', [
+    'parameter',
+    new MessageValue( 'another-message' ),
+    new MessageParam( ParamType::NUM, 12345 ),
+] );
+
+// Fluent interface
+$message = ( new MessageValue( 'message-key' ) )
+    ->params( 'parameter', new MessageValue( 'another-message' ) )
+    ->numParams( 12345 );
+
+// Formatting
+$messageFormatter = $serviceContainter->get( 'MessageFormatterFactory' )->getTextFormatter( 'de' );
+$output = $messageFormatter->format( $message );
+</pre>
+
+Class Overview
+--------------
+
+### Messages
+
+Messages and their parameters are represented by newable value objects.
+
+**MessageValue** represents an instance of a message, holding the key and any
+parameters. It is mutable in that parameters can be added to the object after
+creation.
+
+**MessageParam** is an abstract value class representing a parameter to a message.
+It has a type (using constants defined in the **ParamType** class) and a value. It
+has two implementations:
+
+- **ScalarParam** represents a single-valued parameter, such as a text string, a
+  number, or another message.
+- **ListParam** represents a list of values, which will be joined together with
+  appropriate separators. It has a "list type" (using constants defined in the
+  **ListType** class) defining the desired separators.
+
+### Formatters
+
+A formatter for a particular language is obtained from an implementation of
+**IMessageFormatterFactory**. No implementation of this interface is provided by
+this library. If an environment needs its formatters to vary behavior on things
+other than the language code, for example selecting among multiple sources of
+messages or markup language used for processing message texts, it should define
+a MessageFormatterFactoryFactory of some sort to provide appropriate
+IMessageFormatterFactory implementations.
+
+There is no one base interface for all formatters; the intent is that type
+hinting will ensure that the formatter being used will produce output in the
+expected output format. The defined output formats are:
+
+- **ITextFormatter** produces plain text output.
+
+No implementation of these interfaces are provided by this library.
+
+Formatter implementations are expected to perform the following procedure to
+generate the output string:
+
+1. Fetch the message's translation in the formatter's language. Details of this
+   fetching are unspecified here.
+   - If no translation is found in the formatter's language, it should attempt
+     to fall back to appropriate other languages. Details of the fallback are
+     unspecified here.
+   - If no translation can be found in any fallback language, a string should
+        be returned that indicates at minimum the message key that was unable to
+        be found.
+2. Replace placeholders with parameter values.
+   - Note that placeholders must not be replaced recursively. That is, if a
+     parameter's value contains text that looks like a placeholder, it must not
+     be replaced as if it really were a placeholder.
+   - Certain types of parameters are not substituted directly at this stage.
+     Instead their placeholders must be replaced with an opaque representation
+     that will not be misinterpreted during later stages.
+     - Parameters of type RAW or PLAINTEXT
+     - TEXT parameters with a MessageValue as the value
+     - LIST parameters with any late-substituted value as one of their values.
+3. Process any formatting commands.
+4. Process the source markup language to produce a string in the desired output
+   format. This may be a no-op, and may be combined with the previous step if
+   the markup language implements compatible formatting commands.
+5. Replace any opaque representations from step 2 with the actual values of
+   the corresponding parameters.
+
+Guidelines for Interoperability
+-------------------------------
+
+Besides allowing for libraries to safely supply their own translations for
+every app using them, and apps to easily use libraries' translations instead of
+having to retranslate everything, following these guidelines will also help
+open source projects use [translatewiki.net] for crowdsourced volunteer
+translation into many languages.
+
+### Language codes
+
+[BCP 47] language tags should be used for language codes. If a supplied
+language tag is not recognized, at minimum the corresponding tag with all
+optional subtags stripped should be tried as a fallback.
+
+All messages must have a translation in English (code "en"). All languages
+should fall back to English as a last resort.
+
+The English translations should use `{{PLURAL:...}}` and `{{GENDER:...}}` even
+when English doesn't make a grammatical distinction, to signal to translators
+that plural/gender support is available.
+
+Language code "qqq" is reserved for documenting messages. Documentation should
+describe the context in which the message is used and the values of all
+parameters used with the message. Generally this is written in English.
+Attempting to obtain a message formatter for "qqq" should return one for "en"
+instead.
+
+Language code "qqx" is reserved for debugging. Rather than retrieving
+translations from some underlying storage, every key should act as if it were
+translated as something `(key-name: $1, $2, $3)` with the number of
+placeholders depending on how many parameters are included in the
+MessageValue.
+
+### Message keys
+
+Message keys intended for use with external implementations should follow
+certain guidelines for interoperability:
+
+- Keys should be restricted to the regular expression `/^[a-z][a-z0-9-]*$/`.
+  That is, it should consist of lowercase ASCII letters, numbers, and hyphen
+  only, and should begin with a letter.
+- Keys should be prefixed to help avoid collisions. For example, a library
+  named "ApplePicker" should prefix its message keys with "applepicker-".
+- Common values needing translation, such as names of months and weekdays,
+  should not be prefixed by each library. Libraries needing these should use
+  keys from the [Common Locale Data Repository][CLDR] and document this
+  requirement, and environments should provide these messages.
+
+### Message format
+
+Placeholders are represented by `$1`, `$2`, `$3`, and so on. Text like `$100`
+is interpreted as a placeholder for parameter 100 if 100 or more parameters
+were supplied, as a placeholder for parameter 10 followed by text "0" if
+between ten and 99 parameters were supplied, and as a placeholder for parameter
+1 followed by text "00" if between one and nine parameters were supplied.
+
+All formatting commands look like `{{NAME:$value1|$value2|$value3|...}}`. Braces
+are to be balanced, e.g. `{{NAME:foo|{{bar|baz}}}}` has $value1 as "foo" and
+$value2 as "{{bar|baz}}". The name is always case-insensitive.
+
+Anything syntactically resembling a placeholder or formatting command that does
+not correspond to an actual paramter or known command should be left unchanged
+for processing by the markup language processor.
+
+Libraries providing messages for use by externally-defined formatters should
+generally assume no markup language will be applied, and should avoid
+constructs used by common markup languages unless they also make sense when
+read as plain text.
+
+### Formatting commands
+
+The following formatting commands should be supported.
+
+#### PLURAL
+
+`{{PLURAL:$count|$formA|$formB|...}}` is used to produce plurals.
+
+$count is a number, which may have been formatted with ParamType::NUM.
+
+The number of forms and which count corresponds to which form depend on the
+language, for example English uses `{{PLURAL:$1|one|other}}` while Arabic uses
+`{{PLURAL:$1|zero|one|two|few|many|other}}`. Details are defined in
+[CLDR][CLDR plurals].
+
+It is not possible to "skip" positions while still suppling later ones. If too
+few values are supplied, the final form is repeated for subsequent positions.
+
+If there is an explicit plural form to be given for a specific number, it may
+be specified with syntax like `{{PLURAL:$1|one egg|$1 eggs|12=a dozen eggs}}`.
+
+#### GENDER
+
+`{{GENDER:$name|$masculine|$feminine|$unspecified}}` is used to handle
+grammatical gender, typically when messages refer to user accounts.
+
+This supports three grammatical genders: "male", "female", and a third option
+for cases where the gender is unspecified, unknown, or neither male nor female.
+It does not attempt to handle animate-inanimate or [T-V] distinctions.
+
+$name is a user account name or other similar identifier. If the name given
+does not correspond to any known user account, it should probably use the
+$unspecified gender.
+
+If $feminine and/or $unspecified is not specified, the value of $masculine
+is normally used in its place.
+
+#### GRAMMAR
+
+`{{GRAMMAR:$form|$term}}` converts a term to an appropriate grammatical form.
+
+If no mapping for $term to $form exists, $term should be returned unchanged.
+
+See [jQuery.i18n § Grammar][jQuery.i18n grammar] for details.
+
+#### BIDI
+
+`{{BIDI:$text}}` applies directional isolation to the wrapped text, to attempt
+to avoid errors where directionally-neutral characters are wrongly displayed
+when between LTR and RTL content.
+
+This should output U+202A (left-to-right embedding) or U+202B (right-to-left
+embedding) before the text, depending on the directionality of the first
+strongly-directional character in $text, and U+202C (pop directional
+formatting) after, or do something equivalent for the target output format.
+
+### Supplying translations
+
+Code intending its messages to be used by externally-defined formatters should
+supply the translations as described by
+[jQuery.i18n § Message File Format][jQuery.i18n file format].
+
+In brief, the base directory of the library should contain a directory named
+"i18n". This directory should contain JSON files named by code such as
+"en.json", "de.json", "qqq.json", each with contents like:
+
+```json
+{
+    "@metadata": {
+        "authors": [
+            "Alice",
+            "Bob",
+            "Carol",
+            "David"
+        ],
+        "last-updated": "2012-09-21"
+    },
+    "appname-title": "Example Application",
+    "appname-sub-title": "An example application",
+    "appname-header-introduction": "Introduction",
+    "appname-about": "About this application",
+    "appname-footer": "Footer text"
+}
+```
+
+Formatter implementations should be able to consume message data supplied in
+this format, either directly via registration of i18n directories to check or
+by providing tooling to incorporate it during a build step.
+
+
+---
+[jQuery.i18n]: https://github.com/wikimedia/jquery.i18n
+[BCP 47]: https://tools.ietf.org/rfc/bcp/bcp47.txt
+[CLDR]: http://cldr.unicode.org/
+[CLDR plurals]: https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
+[jQuery.i18n grammar]: https://github.com/wikimedia/jquery.i18n#grammar
+[jQuery.i18n file format]: https://github.com/wikimedia/jquery.i18n#message-file-format
+[translatewiki.net]: https://translatewiki.net/wiki/Translating:New_project
+[T-V]: https://en.wikipedia.org/wiki/T%E2%80%93V_distinction
diff --git a/includes/libs/Message/ScalarParam.php b/includes/libs/Message/ScalarParam.php
new file mode 100644 (file)
index 0000000..c17bc7f
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace Wikimedia\Message;
+
+/**
+ * Value object representing a message parameter holding a single value.
+ *
+ * Message parameter classes are pure value objects and are safely newable.
+ */
+class ScalarParam extends MessageParam {
+       /**
+        * Construct a text parameter
+        *
+        * @param string $type One of the ParamType constants.
+        * @param string|int|float|MessageValue $value
+        */
+       public function __construct( $type, $value ) {
+               $this->type = $type;
+               $this->value = $value;
+       }
+
+       public function dump() {
+               if ( $this->value instanceof MessageValue ) {
+                       $contents = $this->value->dump();
+               } else {
+                       $contents = htmlspecialchars( $this->value );
+               }
+               return "<{$this->type}>" . $contents . "</{$this->type}>";
+       }
+}
diff --git a/includes/libs/Message/TextParam.php b/includes/libs/Message/TextParam.php
deleted file mode 100644 (file)
index c1a1f08..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-namespace Wikimedia\Message;
-
-class TextParam extends MessageParam {
-       /**
-        * Construct a text parameter
-        *
-        * @param string $type May be one of:
-        *   - ParamType::TEXT: A simple text parameter
-        *   - ParamType::NUM: A number, to be formatted using local digits and
-        *     separators
-        *   - ParamType::DURATION_LONG: A number of seconds, to be formatted as natural
-        *     language text.
-        *   - ParamType::DURATION_SHORT: A number of seconds, to be formatted in an
-        *     abbreviated way.
-        *   - ParamType::EXPIRY: An expiry time for a block. The input is either
-        *     a timestamp in one of the formats accepted by the Wikimedia\Timestamp
-        *     library, or "infinity" for an infinite block.
-        *   - ParamType::SIZE: A number of bytes.
-        *   - ParamType::BITRATE: A number of bits per second.
-        *   - ParamType::RAW: A text parameter which is substituted after
-        *     preprocessing, and so is not available to the preprocessor and cannot
-        *     be modified by it.
-        *   - ParamType::PLAINTEXT: Reserved for future use.
-        *
-        * @param string|int|float $value
-        */
-       public function __construct( $type, $value ) {
-               $this->type = $type;
-               $this->value = $value;
-       }
-
-       public function dump() {
-               return "<{$this->type}>" . htmlspecialchars( $this->value ) . "</{$this->type}>";
-       }
-}
index 2e3aa70..b195a08 100644 (file)
@@ -161,6 +161,8 @@ class MultiHttpClient implements LoggerAwareInterface {
         */
        public function runMulti( array $reqs, array $opts = [] ) {
                $this->normalizeRequests( $reqs );
+               $opts += [ 'connTimeout' => $this->connTimeout, 'reqTimeout' => $this->reqTimeout ];
+
                if ( $this->isCurlEnabled() ) {
                        return $this->runMultiCurl( $reqs, $opts );
                } else {
@@ -195,7 +197,7 @@ class MultiHttpClient implements LoggerAwareInterface {
         * @throws Exception
         * @suppress PhanTypeInvalidDimOffset
         */
-       private function runMultiCurl( array $reqs, array $opts = [] ) {
+       private function runMultiCurl( array $reqs, array $opts ) {
                $chm = $this->getCurlMulti();
 
                $selectTimeout = $this->getSelectTimeout( $opts );
@@ -292,21 +294,21 @@ class MultiHttpClient implements LoggerAwareInterface {
 
        /**
         * @param array &$req HTTP request map
+        * @codingStandardsIgnoreStart
+        * @phan-param array{url:string,proxy?:?string,query:mixed,method:string,body:string|resource,headers:string[],stream?:resource,flags:array} $req
+        * @codingStandardsIgnoreEnd
         * @param array $opts
-        *   - connTimeout    : default connection timeout
-        *   - reqTimeout     : default request timeout
+        *   - connTimeout : default connection timeout
+        *   - reqTimeout : default request timeout
         * @return resource
         * @throws Exception
-        * @suppress PhanTypeMismatchArgumentInternal
         */
-       protected function getCurlHandle( array &$req, array $opts = [] ) {
+       protected function getCurlHandle( array &$req, array $opts ) {
                $ch = curl_init();
 
-               curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT_MS,
-                       ( $opts['connTimeout'] ?? $this->connTimeout ) * 1000 );
                curl_setopt( $ch, CURLOPT_PROXY, $req['proxy'] ?? $this->proxy );
-               curl_setopt( $ch, CURLOPT_TIMEOUT_MS,
-                       ( $opts['reqTimeout'] ?? $this->reqTimeout ) * 1000 );
+               curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT_MS, intval( $opts['connTimeout'] * 1e3 ) );
+               curl_setopt( $ch, CURLOPT_TIMEOUT_MS, intval( $opts['reqTimeout'] * 1e3 ) );
                curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
                curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 );
                curl_setopt( $ch, CURLOPT_HEADER, 0 );
@@ -322,11 +324,8 @@ class MultiHttpClient implements LoggerAwareInterface {
                        $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
                }
                curl_setopt( $ch, CURLOPT_URL, $url );
-
                curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req['method'] );
-               if ( $req['method'] === 'HEAD' ) {
-                       curl_setopt( $ch, CURLOPT_NOBODY, 1 );
-               }
+               curl_setopt( $ch, CURLOPT_NOBODY, ( $req['method'] === 'HEAD' ) );
 
                if ( $req['method'] === 'PUT' ) {
                        curl_setopt( $ch, CURLOPT_PUT, 1 );
@@ -358,10 +357,6 @@ class MultiHttpClient implements LoggerAwareInterface {
                        );
                } elseif ( $req['method'] === 'POST' ) {
                        curl_setopt( $ch, CURLOPT_POST, 1 );
-                       // Don't interpret POST parameters starting with '@' as file uploads, because this
-                       // makes it impossible to POST plain values starting with '@' (and causes security
-                       // issues potentially exposing the contents of local files).
-                       curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true );
                        curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
                } else {
                        if ( is_resource( $req['body'] ) || $req['body'] !== '' ) {
@@ -410,23 +405,20 @@ class MultiHttpClient implements LoggerAwareInterface {
                        }
                );
 
-               if ( isset( $req['stream'] ) ) {
-                       // Don't just use CURLOPT_FILE as that might give:
-                       // curl_setopt(): cannot represent a stream of type Output as a STDIO FILE*
-                       // The callback here handles both normal files and php://temp handles.
-                       curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
-                               function ( $ch, $data ) use ( &$req ) {
+               // This works with both file and php://temp handles (unlike CURLOPT_FILE)
+               $hasOutputStream = isset( $req['stream'] );
+               curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
+                       function ( $ch, $data ) use ( &$req, $hasOutputStream ) {
+                               if ( $hasOutputStream ) {
                                        return fwrite( $req['stream'], $data );
-                               }
-                       );
-               } else {
-                       curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
-                               function ( $ch, $data ) use ( &$req ) {
+                               } else {
+                                       // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
                                        $req['response']['body'] .= $data;
+
                                        return strlen( $data );
                                }
-                       );
-               }
+                       }
+               );
 
                return $ch;
        }
index a090e16..629d2cd 100644 (file)
@@ -125,7 +125,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
        /** @var callable|null Function that takes a WAN cache callback and runs it later */
        protected $asyncHandler;
 
-       /** @bar bool Whether to use mcrouter key prefixing for routing */
+       /** @var bool Whether to use mcrouter key prefixing for routing */
        protected $mcrouterAware;
        /** @var string Physical region for mcrouter use */
        protected $region;
index f0b135f..7870f69 100644 (file)
@@ -280,7 +280,7 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function close() {
+       public function close( $fname = __METHOD__, $owner = null ) {
                throw new DBUnexpectedError( $this->conn, 'Cannot close shared connection.' );
        }
 
index be41ee0..51596da 100644 (file)
@@ -174,6 +174,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var float Query rount trip time estimate */
        private $lastRoundTripEstimate = 0.0;
 
+       /** @var int|null Integer ID of the managing LBFactory instance or null if none */
+       private $ownerId;
+
        /** @var string Lock granularity is on the level of the entire database */
        const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
        /** @var string The SCHEMA keyword refers to a grouping of tables in a database */
@@ -268,6 +271,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $params['schema'] != '' ? $params['schema'] : null,
                        $params['tablePrefix']
                );
+
+               $this->ownerId = $params['ownerId'] ?? null;
        }
 
        /**
@@ -355,7 +360,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         *   - cliMode: Whether to consider the execution context that of a CLI script.
         *   - agent: Optional name used to identify the end-user in query profiling/logging.
         *   - srvCache: Optional BagOStuff instance to an APC-style cache.
-        *   - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT emulation.
+        *   - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT.
+        *   - ownerId: Optional integer ID of a LoadBalancer instance that manages this instance.
         * @param int $connect One of the class constants (NEW_CONNECTED, NEW_UNCONNECTED) [optional]
         * @return Database|null If the database driver or extension cannot be found
         * @throws InvalidArgumentException If the database driver or extension cannot be found
@@ -375,7 +381,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                'flags' => 0,
                                'variables' => [],
                                'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
-                               'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname()
+                               'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname(),
+                               'ownerId' => null
                        ];
 
                        $normalizedParams = [
@@ -866,8 +873,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                );
        }
 
-       final public function close() {
-               $exception = null; // error to throw after disconnecting
+       final public function close( $fname = __METHOD__, $owner = null ) {
+               $error = null; // error to throw after disconnecting
 
                $wasOpen = (bool)$this->conn;
                // This should mostly do nothing if the connection is already closed
@@ -877,34 +884,22 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                if ( $this->trxAtomicLevels ) {
                                        // Cannot let incomplete atomic sections be committed
                                        $levels = $this->flatAtomicSectionList();
-                                       $exception = new DBUnexpectedError(
-                                               $this,
-                                               __METHOD__ . ": atomic sections $levels are still open"
-                                       );
+                                       $error = "$fname: atomic sections $levels are still open";
                                } elseif ( $this->trxAutomatic ) {
                                        // Only the connection manager can commit non-empty DBO_TRX transactions
                                        // (empty ones we can silently roll back)
                                        if ( $this->writesOrCallbacksPending() ) {
-                                               $exception = new DBUnexpectedError(
-                                                       $this,
-                                                       __METHOD__ .
-                                                       ": mass commit/rollback of peer transaction required (DBO_TRX set)"
-                                               );
+                                               $error = "$fname: " .
+                                                       "expected mass rollback of all peer transactions (DBO_TRX set)";
                                        }
                                } else {
                                        // Manual transactions should have been committed or rolled
                                        // back, even if empty.
-                                       $exception = new DBUnexpectedError(
-                                               $this,
-                                               __METHOD__ . ": transaction is still open (from {$this->trxFname})"
-                                       );
+                                       $error = "$fname: transaction is still open (from {$this->trxFname})";
                                }
 
-                               if ( $this->trxEndCallbacksSuppressed ) {
-                                       $exception = $exception ?: new DBUnexpectedError(
-                                               $this,
-                                               __METHOD__ . ': callbacks are suppressed; cannot properly commit'
-                                       );
+                               if ( $this->trxEndCallbacksSuppressed && $error === null ) {
+                                       $error = "$fname: callbacks are suppressed; cannot properly commit";
                                }
 
                                // Rollback the changes and run any callbacks as needed
@@ -919,9 +914,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
                $this->conn = null;
 
-               // Throw any unexpected errors after having disconnected
-               if ( $exception instanceof Exception ) {
-                       throw $exception;
+               // Log or throw any unexpected errors after having disconnected
+               if ( $error !== null ) {
+                       // T217819, T231443: if this is probably just LoadBalancer trying to recover from
+                       // errors and shutdown, then log any problems and move on since the request has to
+                       // end one way or another. Throwing errors is not very useful at some point.
+                       if ( $this->ownerId !== null && $owner === $this->ownerId ) {
+                               $this->queryLogger->error( $error );
+                       } else {
+                               throw new DBUnexpectedError( $this, $error );
+                       }
                }
 
                // Note that various subclasses call close() at the start of open(), which itself is
@@ -3981,17 +3983,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                if ( $this->trxLevel() ) {
                        if ( $this->trxAtomicLevels ) {
                                $levels = $this->flatAtomicSectionList();
-                               $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open";
+                               $msg = "$fname: got explicit BEGIN while atomic section(s) $levels are open";
                                throw new DBUnexpectedError( $this, $msg );
                        } elseif ( !$this->trxAutomatic ) {
-                               $msg = "$fname: Explicit transaction already active (from {$this->trxFname})";
+                               $msg = "$fname: explicit transaction already active (from {$this->trxFname})";
                                throw new DBUnexpectedError( $this, $msg );
                        } else {
-                               $msg = "$fname: Implicit transaction already active (from {$this->trxFname})";
+                               $msg = "$fname: implicit transaction already active (from {$this->trxFname})";
                                throw new DBUnexpectedError( $this, $msg );
                        }
                } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
-                       $msg = "$fname: Implicit transaction expected (DBO_TRX set)";
+                       $msg = "$fname: implicit transaction expected (DBO_TRX set)";
                        throw new DBUnexpectedError( $this, $msg );
                }
 
@@ -4045,7 +4047,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $levels = $this->flatAtomicSectionList();
                        throw new DBUnexpectedError(
                                $this,
-                               "$fname: Got COMMIT while atomic sections $levels are still open"
+                               "$fname: got COMMIT while atomic sections $levels are still open"
                        );
                }
 
@@ -4055,17 +4057,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        } elseif ( !$this->trxAutomatic ) {
                                throw new DBUnexpectedError(
                                        $this,
-                                       "$fname: Flushing an explicit transaction, getting out of sync"
+                                       "$fname: flushing an explicit transaction, getting out of sync"
                                );
                        }
                } elseif ( !$this->trxLevel() ) {
                        $this->queryLogger->error(
-                               "$fname: No transaction to commit, something got out of sync" );
+                               "$fname: no transaction to commit, something got out of sync" );
                        return; // nothing to do
                } elseif ( $this->trxAutomatic ) {
                        throw new DBUnexpectedError(
                                $this,
-                               "$fname: Expected mass commit of all peer transactions (DBO_TRX set)"
+                               "$fname: expected mass commit of all peer transactions (DBO_TRX set)"
                        );
                }
 
index 106772b..b9a1af6 100644 (file)
@@ -26,6 +26,7 @@ use mysqli;
 use mysqli_result;
 use IP;
 use stdClass;
+use Wikimedia\AtEase\AtEase;
 
 /**
  * Database abstraction object for PHP extension mysqli.
@@ -41,7 +42,11 @@ class DatabaseMysqli extends DatabaseMysqlBase {
         * @return mysqli_result|bool
         */
        protected function doQuery( $sql ) {
-               return $this->getBindingHandle()->query( $sql );
+               AtEase::suppressWarnings();
+               $res = $this->getBindingHandle()->query( $sql );
+               AtEase::restoreWarnings();
+
+               return $res;
        }
 
        /**
index 41f7cb6..befc80c 100644 (file)
@@ -460,10 +460,12 @@ interface IDatabase {
         * aside from read-only automatic transactions (assuming no callbacks are registered).
         * If a transaction is still open anyway, it will be rolled back.
         *
+        * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         * @return bool Success
         * @throws DBError
         */
-       public function close();
+       public function close( $fname = __METHOD__, $owner = null );
 
        /**
         * Run an SQL query and return the result
index 6e9591b..23232f4 100644 (file)
@@ -109,9 +109,10 @@ interface ILBFactory {
         * but still use DBO_TRX transaction rounds on other tables.
         *
         * @param bool|string $domain Domain ID, or false for the current domain
+        * @param int|null $owner Owner ID of the new instance (e.g. this LBFactory ID)
         * @return ILoadBalancer
         */
-       public function newMainLB( $domain = false );
+       public function newMainLB( $domain = false, $owner = null );
 
        /**
         * Get a cached (tracked) load balancer object.
@@ -131,9 +132,10 @@ interface ILBFactory {
         * (DBO_TRX off) but still use DBO_TRX transaction rounds on other tables.
         *
         * @param string $cluster External storage cluster name
+        * @param int|null $owner Owner ID of the new instance (e.g. this LBFactory ID)
         * @return ILoadBalancer
         */
-       public function newExternalLB( $cluster );
+       public function newExternalLB( $cluster, $owner = null );
 
        /**
         * Get a cached (tracked) load balancer for external storage
index 36e961a..07a5fe6 100644 (file)
@@ -155,15 +155,16 @@ abstract class LBFactory implements ILBFactory {
                $this->defaultGroup = $conf['defaultGroup'] ?? null;
                $this->secret = $conf['secret'] ?? '';
 
-               $this->id = mt_rand();
-               $this->ticket = mt_rand();
+               static $nextId, $nextTicket;
+               $this->id = $nextId = ( is_int( $nextId ) ? $nextId++ : mt_rand() );
+               $this->ticket = $nextTicket = ( is_int( $nextTicket ) ? $nextTicket++ : mt_rand() );
        }
 
        public function destroy() {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $scope = ScopedCallback::newScopedIgnoreUserAbort();
 
-               $this->forEachLBCallMethod( 'disable' );
+               $this->forEachLBCallMethod( 'disable', [ __METHOD__, $this->id ] );
        }
 
        public function getLocalDomainID() {
@@ -195,34 +196,6 @@ abstract class LBFactory implements ILBFactory {
                $this->commitMasterChanges( __METHOD__ ); // sanity
        }
 
-       /**
-        * @see ILBFactory::newMainLB()
-        * @param bool $domain
-        * @return ILoadBalancer
-        */
-       abstract public function newMainLB( $domain = false );
-
-       /**
-        * @see ILBFactory::getMainLB()
-        * @param bool $domain
-        * @return ILoadBalancer
-        */
-       abstract public function getMainLB( $domain = false );
-
-       /**
-        * @see ILBFactory::newExternalLB()
-        * @param string $cluster
-        * @return ILoadBalancer
-        */
-       abstract public function newExternalLB( $cluster );
-
-       /**
-        * @see ILBFactory::getExternalLB()
-        * @param string $cluster
-        * @return ILoadBalancer
-        */
-       abstract public function getExternalLB( $cluster );
-
        /**
         * Call a method of each tracked load balancer
         *
@@ -245,13 +218,13 @@ abstract class LBFactory implements ILBFactory {
                                [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
                        );
                }
-               $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] );
+               $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname, $this->id ] );
        }
 
        final public function commitAll( $fname = __METHOD__, array $options = [] ) {
                $this->commitMasterChanges( $fname, $options );
-               $this->forEachLBCallMethod( 'flushMasterSnapshots', [ $fname ] );
-               $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] );
+               $this->forEachLBCallMethod( 'flushMasterSnapshots', [ $fname, $this->id ] );
+               $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname, $this->id ] );
        }
 
        final public function beginMasterChanges( $fname = __METHOD__ ) {
@@ -604,10 +577,12 @@ abstract class LBFactory implements ILBFactory {
        }
 
        /**
-        * Base parameters to ILoadBalancer::__construct()
+        * Get parameters to ILoadBalancer::__construct()
+        *
+        * @param int|null $owner Use getOwnershipId() if this is for getMainLB()/getExternalLB()
         * @return array
         */
-       final protected function baseLoadBalancerParams() {
+       final protected function baseLoadBalancerParams( $owner ) {
                if ( $this->trxRoundStage === self::ROUND_COMMIT_CALLBACKS ) {
                        $initStage = ILoadBalancer::STAGE_POSTCOMMIT_CALLBACKS;
                } elseif ( $this->trxRoundStage === self::ROUND_ROLLBACK_CALLBACKS ) {
@@ -639,7 +614,7 @@ abstract class LBFactory implements ILBFactory {
                                $this->getChronologyProtector()->applySessionReplicationPosition( $lb );
                        },
                        'roundStage' => $initStage,
-                       'ownerId' => $this->id
+                       'ownerId' => $owner
                ];
        }
 
@@ -689,7 +664,7 @@ abstract class LBFactory implements ILBFactory {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $scope = ScopedCallback::newScopedIgnoreUserAbort();
 
-               $this->forEachLBCallMethod( 'closeAll' );
+               $this->forEachLBCallMethod( 'closeAll', [ __METHOD__, $this->id ] );
        }
 
        public function setAgentName( $agent ) {
@@ -757,6 +732,14 @@ abstract class LBFactory implements ILBFactory {
                $this->requestInfo = $info + $this->requestInfo;
        }
 
+       /**
+        * @return int Internal instance ID used to assert ownership of ILoadBalancer instances
+        * @since 1.34
+        */
+       final protected function getOwnershipId() {
+               return $this->id;
+       }
+
        /**
         * @param string $stage
         */
index ef1f0a6..77b029f 100644 (file)
@@ -157,7 +157,7 @@ class LBFactoryMulti extends LBFactory {
                $this->loadMonitorClass = $conf['loadMonitorClass'] ?? LoadMonitor::class;
        }
 
-       public function newMainLB( $domain = false ) {
+       public function newMainLB( $domain = false, $owner = null ) {
                $section = $this->getSectionForDomain( $domain );
                if ( !isset( $this->groupLoadsBySection[$section][ILoadBalancer::GROUP_GENERIC] ) ) {
                        throw new UnexpectedValueException( "Section '$section' has no hosts defined." );
@@ -175,7 +175,8 @@ class LBFactoryMulti extends LBFactory {
                        // Use the LB-specific read-only reason if everything isn't already read-only
                        is_string( $this->readOnlyReason )
                                ? $this->readOnlyReason
-                               : ( $this->readOnlyBySection[$section] ?? false )
+                               : ( $this->readOnlyBySection[$section] ?? false ),
+                       $owner
                );
        }
 
@@ -183,13 +184,13 @@ class LBFactoryMulti extends LBFactory {
                $section = $this->getSectionForDomain( $domain );
 
                if ( !isset( $this->mainLBs[$section] ) ) {
-                       $this->mainLBs[$section] = $this->newMainLB( $domain );
+                       $this->mainLBs[$section] = $this->newMainLB( $domain, $this->getOwnershipId() );
                }
 
                return $this->mainLBs[$section];
        }
 
-       public function newExternalLB( $cluster ) {
+       public function newExternalLB( $cluster, $owner = null ) {
                if ( !isset( $this->externalLoads[$cluster] ) ) {
                        throw new InvalidArgumentException( "Unknown cluster '$cluster'" );
                }
@@ -201,13 +202,15 @@ class LBFactoryMulti extends LBFactory {
                                $this->templateOverridesByCluster[$cluster] ?? []
                        ),
                        [ ILoadBalancer::GROUP_GENERIC => $this->externalLoads[$cluster] ],
-                       $this->readOnlyReason
+                       $this->readOnlyReason,
+                       $owner
                );
        }
 
        public function getExternalLB( $cluster ) {
                if ( !isset( $this->externalLBs[$cluster] ) ) {
-                       $this->externalLBs[$cluster] = $this->newExternalLB( $cluster );
+                       $this->externalLBs[$cluster] =
+                               $this->newExternalLB( $cluster, $this->getOwnershipId() );
                }
 
                return $this->externalLBs[$cluster];
@@ -265,11 +268,12 @@ class LBFactoryMulti extends LBFactory {
         * @param array $serverTemplate Server config map
         * @param int[][] $groupLoads Map of (group => host => load)
         * @param string|bool $readOnlyReason
+        * @param int|null $owner
         * @return LoadBalancer
         */
-       private function newLoadBalancer( $serverTemplate, $groupLoads, $readOnlyReason ) {
+       private function newLoadBalancer( $serverTemplate, $groupLoads, $readOnlyReason, $owner ) {
                $lb = new LoadBalancer( array_merge(
-                       $this->baseLoadBalancerParams(),
+                       $this->baseLoadBalancerParams( $owner ),
                        [
                                'servers' => $this->makeServerArray( $serverTemplate, $groupLoads ),
                                'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
index 7e73e5b..cc79f99 100644 (file)
@@ -75,29 +75,29 @@ class LBFactorySimple extends LBFactory {
                $this->loadMonitorClass = $conf['loadMonitorClass'] ?? LoadMonitor::class;
        }
 
-       public function newMainLB( $domain = false ) {
-               return $this->newLoadBalancer( $this->mainServers );
+       public function newMainLB( $domain = false, $owner = null ) {
+               return $this->newLoadBalancer( $this->mainServers, $owner );
        }
 
        public function getMainLB( $domain = false ) {
                if ( $this->mainLB === null ) {
-                       $this->mainLB = $this->newMainLB( $domain );
+                       $this->mainLB = $this->newMainLB( $domain, $this->getOwnershipId() );
                }
 
                return $this->mainLB;
        }
 
-       public function newExternalLB( $cluster ) {
+       public function newExternalLB( $cluster, $owner = null ) {
                if ( !isset( $this->externalServersByCluster[$cluster] ) ) {
                        throw new InvalidArgumentException( "Unknown cluster '$cluster'." );
                }
 
-               return $this->newLoadBalancer( $this->externalServersByCluster[$cluster] );
+               return $this->newLoadBalancer( $this->externalServersByCluster[$cluster], $owner );
        }
 
        public function getExternalLB( $cluster ) {
                if ( !isset( $this->externalLBs[$cluster] ) ) {
-                       $this->externalLBs[$cluster] = $this->newExternalLB( $cluster );
+                       $this->externalLBs[$cluster] = $this->newExternalLB( $cluster, $this->getOwnershipId() );
                }
 
                return $this->externalLBs[$cluster];
@@ -116,9 +116,9 @@ class LBFactorySimple extends LBFactory {
                return $lbs;
        }
 
-       private function newLoadBalancer( array $servers ) {
+       private function newLoadBalancer( array $servers, $owner ) {
                $lb = new LoadBalancer( array_merge(
-                       $this->baseLoadBalancerParams(),
+                       $this->baseLoadBalancerParams( $owner ),
                        [
                                'servers' => $servers,
                                'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
index 60044ba..97daf10 100644 (file)
@@ -44,8 +44,12 @@ class LBFactorySingle extends LBFactory {
                        throw new InvalidArgumentException( "Missing 'connection' argument." );
                }
 
-               $lb = new LoadBalancerSingle( array_merge( $this->baseLoadBalancerParams(), $conf ) );
+               $lb = new LoadBalancerSingle( array_merge(
+                       $this->baseLoadBalancerParams( $this->getOwnershipId() ),
+                       $conf
+               ) );
                $this->initLoadBalancer( $lb );
+
                $this->lb = $lb;
        }
 
@@ -63,23 +67,15 @@ class LBFactorySingle extends LBFactory {
                ) );
        }
 
-       /**
-        * @param bool|string $domain (unused)
-        * @return LoadBalancerSingle
-        */
-       public function newMainLB( $domain = false ) {
-               return $this->lb;
+       public function newMainLB( $domain = false, $owner = null ) {
+               throw new BadMethodCallException( "Method is not supported." );
        }
 
-       /**
-        * @param bool|string $domain (unused)
-        * @return LoadBalancerSingle
-        */
        public function getMainLB( $domain = false ) {
                return $this->lb;
        }
 
-       public function newExternalLB( $cluster ) {
+       public function newExternalLB( $cluster, $owner = null ) {
                throw new BadMethodCallException( "Method is not supported." );
        }
 
@@ -87,24 +83,14 @@ class LBFactorySingle extends LBFactory {
                throw new BadMethodCallException( "Method is not supported." );
        }
 
-       /**
-        * @return LoadBalancerSingle[] Map of (cluster name => LoadBalancer)
-        */
        public function getAllMainLBs() {
                return [ 'DEFAULT' => $this->lb ];
        }
 
-       /**
-        * @return LoadBalancerSingle[] Map of (cluster name => LoadBalancer)
-        */
        public function getAllExternalLBs() {
                return [];
        }
 
-       /**
-        * @param string|callable $callback
-        * @param array $params
-        */
        public function forEachLB( $callback, array $params = [] ) {
                if ( isset( $this->lb ) ) { // may not be set during _destruct()
                        $callback( $this->lb, ...$params );
index 160d501..4ca4250 100644 (file)
@@ -493,15 +493,22 @@ interface ILoadBalancer {
        public function getReplicaResumePos();
 
        /**
-        * Disable this load balancer. All connections are closed, and any attempt to
-        * open a new connection will result in a DBAccessError.
+        * Close all connections and disable this load balancer
+        *
+        * Any attempt to open a new connection will result in a DBAccessError.
+        *
+        * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         */
-       public function disable();
+       public function disable( $fname = __METHOD__, $owner = null );
 
        /**
         * Close all open connections
+        *
+        * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         */
-       public function closeAll();
+       public function closeAll( $fname = __METHOD__, $owner = null );
 
        /**
         * Close a connection
@@ -598,8 +605,9 @@ interface ILoadBalancer {
         * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshots
         *
         * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         */
-       public function flushReplicaSnapshots( $fname = __METHOD__ );
+       public function flushReplicaSnapshots( $fname = __METHOD__, $owner = null );
 
        /**
         * Commit all master DB transactions so as to flush any REPEATABLE-READ or SSI snapshots
@@ -607,8 +615,9 @@ interface ILoadBalancer {
         * An error will be thrown if a connection has pending writes or callbacks
         *
         * @param string $fname Caller name
+        * @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
         */
-       public function flushMasterSnapshots( $fname = __METHOD__ );
+       public function flushMasterSnapshots( $fname = __METHOD__, $owner = null );
 
        /**
         * @return bool Whether a master connection is already open
index f60e8db..39d1bd6 100644 (file)
@@ -106,6 +106,10 @@ class LoadBalancer implements ILoadBalancer {
        /** @var bool[] Map of (domain => whether to use "temp tables only" mode) */
        private $tempTablesOnlyMode = [];
 
+       /** @var string|bool Explicit DBO_TRX transaction round active or false if none */
+       private $trxRoundId = false;
+       /** @var string Stage of the current transaction round in the transaction round life-cycle */
+       private $trxRoundStage = self::ROUND_CURSORY;
        /** @var Database Connection handle that caused a problem */
        private $errorConnection;
        /** @var int[] The group replica server indexes keyed by group */
@@ -125,12 +129,10 @@ class LoadBalancer implements ILoadBalancer {
        /** @var bool Whether any connection has been attempted yet */
        private $connectionAttempted = false;
 
+       /** var int An identifier for this class instance */
+       private $id;
        /** @var int|null Integer ID of the managing LBFactory instance or null if none */
        private $ownerId;
-       /** @var string|bool Explicit DBO_TRX transaction round active or false if none */
-       private $trxRoundId = false;
-       /** @var string Stage of the current transaction round in the transaction round life-cycle */
-       private $trxRoundStage = self::ROUND_CURSORY;
 
        /** @var int Warn when this many connection are held */
        const CONN_HELD_WARN_THRESHOLD = 10;
@@ -218,7 +220,6 @@ class LoadBalancer implements ILoadBalancer {
                $this->deprecationLogger = $params['deprecationLogger'] ?? function ( $msg ) {
                        trigger_error( $msg, E_USER_DEPRECATED );
                };
-
                foreach ( [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ] as $key ) {
                        $this->$key = $params[$key] ?? new NullLogger();
                }
@@ -242,6 +243,8 @@ class LoadBalancer implements ILoadBalancer {
                $group = $params['defaultGroup'] ?? self::GROUP_GENERIC;
                $this->defaultGroup = isset( $this->groupLoads[$group] ) ? $group : self::GROUP_GENERIC;
 
+               static $nextId;
+               $this->id = $nextId = ( is_int( $nextId ) ? $nextId++ : mt_rand() );
                $this->ownerId = $params['ownerId'] ?? null;
        }
 
@@ -1301,6 +1304,7 @@ class LoadBalancer implements ILoadBalancer {
                // Use DBO_DEFAULT flags by default for LoadBalancer managed databases. Assume that the
                // application calls LoadBalancer::commitMasterChanges() before the PHP script completes.
                $server['flags'] = $server['flags'] ?? IDatabase::DBO_DEFAULT;
+               $server['ownerId'] = $this->id;
 
                // Create a live connection object
                $conn = Database::factory( $server['type'], $server, Database::NEW_UNCONNECTED );
@@ -1498,23 +1502,22 @@ class LoadBalancer implements ILoadBalancer {
                return $highestPos;
        }
 
-       public function disable() {
-               $this->closeAll();
+       public function disable( $fname = __METHOD__, $owner = null ) {
+               $this->assertOwnership( $fname, $owner );
+               $this->closeAll( $fname, $owner );
                $this->disabled = true;
        }
 
-       public function closeAll() {
+       public function closeAll( $fname = __METHOD__, $owner = null ) {
+               $this->assertOwnership( $fname, $owner );
                if ( $this->ownerId === null ) {
                        /** @noinspection PhpUnusedLocalVariableInspection */
                        $scope = ScopedCallback::newScopedIgnoreUserAbort();
                }
-
-               $fname = __METHOD__;
                $this->forEachOpenConnection( function ( IDatabase $conn ) use ( $fname ) {
                        $host = $conn->getServer();
-                       $this->connLogger->debug(
-                               $fname . ": closing connection to database '$host'." );
-                       $conn->close();
+                       $this->connLogger->debug( "$fname: closing connection to database '$host'." );
+                       $conn->close( $fname, $this->id );
                } );
 
                $this->conns = self::newTrackedConnectionsArray();
@@ -1543,13 +1546,13 @@ class LoadBalancer implements ILoadBalancer {
                        }
                }
 
-               $conn->close();
+               $conn->close( __METHOD__ );
        }
 
        public function commitAll( $fname = __METHOD__, $owner = null ) {
                $this->commitMasterChanges( $fname, $owner );
-               $this->flushMasterSnapshots( $fname );
-               $this->flushReplicaSnapshots( $fname );
+               $this->flushMasterSnapshots( $fname, $owner );
+               $this->flushReplicaSnapshots( $fname, $owner );
        }
 
        public function finalizeMasterChanges( $fname = __METHOD__, $owner = null ) {
@@ -1634,7 +1637,7 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                // Clear any empty transactions (no writes/callbacks) from the implicit round
-               $this->flushMasterSnapshots( $fname );
+               $this->flushMasterSnapshots( $fname, $owner );
 
                $this->trxRoundId = $fname;
                $this->trxRoundStage = self::ROUND_ERROR; // "failed" until proven otherwise
@@ -1738,7 +1741,7 @@ class LoadBalancer implements ILoadBalancer {
                                        $this->queryLogger->warning( $fname . ": found writes pending." );
                                        $fnames = implode( ', ', $conn->pendingWriteAndCallbackCallers() );
                                        $this->queryLogger->warning(
-                                               $fname . ": found writes pending ($fnames).",
+                                               "$fname: found writes pending ($fnames).",
                                                [
                                                        'db_server' => $conn->getServer(),
                                                        'db_name' => $conn->getDBname()
@@ -1747,7 +1750,7 @@ class LoadBalancer implements ILoadBalancer {
                                } elseif ( $conn->trxLevel() ) {
                                        // A callback from another handle read from this one and DBO_TRX is set,
                                        // which can easily happen if there is only one DB (no replicas)
-                                       $this->queryLogger->debug( $fname . ": found empty transaction." );
+                                       $this->queryLogger->debug( "$fname: found empty transaction." );
                                }
                                try {
                                        $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
@@ -1838,15 +1841,22 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        /**
+        * Assure that if this instance is owned, the caller is either the owner or is internal
+        *
+        * If an LBFactory owns the LoadBalancer, then certain methods should only called through
+        * that LBFactory to avoid broken contracts. Otherwise, those methods can publically be
+        * called by anything. In any case, internal methods from the LoadBalancer itself should
+        * always be allowed.
+        *
         * @param string $fname
         * @param int|null $owner Owner ID of the caller
         * @throws DBTransactionError
         */
        private function assertOwnership( $fname, $owner ) {
-               if ( $this->ownerId !== null && $owner !== $this->ownerId ) {
+               if ( $this->ownerId !== null && $owner !== $this->ownerId && $owner !== $this->id ) {
                        throw new DBTransactionError(
                                null,
-                               "$fname: LoadBalancer is owned by LBFactory #{$this->ownerId} (got '$owner')."
+                               "$fname: LoadBalancer is owned by ID '{$this->ownerId}' (got '$owner')."
                        );
                }
        }
@@ -1893,13 +1903,15 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
-       public function flushReplicaSnapshots( $fname = __METHOD__ ) {
+       public function flushReplicaSnapshots( $fname = __METHOD__, $owner = null ) {
+               $this->assertOwnership( $fname, $owner );
                $this->forEachOpenReplicaConnection( function ( IDatabase $conn ) use ( $fname ) {
                        $conn->flushSnapshot( $fname );
                } );
        }
 
-       public function flushMasterSnapshots( $fname = __METHOD__ ) {
+       public function flushMasterSnapshots( $fname = __METHOD__, $owner = null ) {
+               $this->assertOwnership( $fname, $owner );
                $this->forEachOpenMasterConnection( function ( IDatabase $conn ) use ( $fname ) {
                        $conn->flushSnapshot( $fname );
                } );
@@ -2317,7 +2329,7 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function redefineLocalDomain( $domain ) {
-               $this->closeAll();
+               $this->closeAll( __METHOD__, $this->id );
 
                $this->setLocalDomain( DatabaseDomain::newFromId( $domain ) );
        }
@@ -2379,7 +2391,7 @@ class LoadBalancer implements ILoadBalancer {
 
        function __destruct() {
                // Avoid connection leaks for sanity
-               $this->disable();
+               $this->disable( __METHOD__, $this->ownerId );
        }
 }
 
index 0149171..1c2e782 100644 (file)
@@ -587,7 +587,7 @@ class Article implements Page {
         * page of the given title.
         */
        public function view() {
-               global $wgUseFileCache, $wgDebugToolbar;
+               global $wgUseFileCache;
 
                # Get variables from query string
                # As side effect this will load the revision and update the title
@@ -643,7 +643,7 @@ class Article implements Page {
                }
 
                # Try client and file cache
-               if ( !$wgDebugToolbar && $oldid === 0 && $this->mPage->checkTouched() ) {
+               if ( $oldid === 0 && $this->mPage->checkTouched() ) {
                        # Try to stream the output from file cache
                        if ( $wgUseFileCache && $this->tryFileCache() ) {
                                wfDebug( __METHOD__ . ": done file cache\n" );
index 472bcdd..6af60a7 100644 (file)
@@ -22,7 +22,6 @@
  */
 
 use MediaWiki\Linker\LinkRenderer;
-use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Navigation\PrevNextNavigationRenderer;
 use Wikimedia\Rdbms\IDatabase;
@@ -796,14 +795,14 @@ abstract class IndexPager extends ContextSource implements Pager {
        /**
         * Generate (prev x| next x) (20|50|100...) type links for paging
         *
-        * @param LinkTarget $title
+        * @param Title $title
         * @param int $offset
         * @param int $limit
         * @param array $query Optional URL query parameter string
         * @param bool $atend Optional param for specified if this is the last page
         * @return string
         */
-       protected function buildPrevNextNavigation( LinkTarget $title, $offset, $limit,
+       protected function buildPrevNextNavigation( Title $title, $offset, $limit,
                                                                                                array $query = [], $atend = false
        ) {
                $prevNext = new PrevNextNavigationRenderer( $this );
index 07fe318..c5d4b4a 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 use Composer\Semver\Semver;
+use Wikimedia\AtEase\AtEase;
 use Wikimedia\ScopedCallback;
 use MediaWiki\Shell\Shell;
 use MediaWiki\ShellDisabledError;
@@ -126,15 +127,13 @@ class ExtensionRegistry {
 
                $mtime = $wgExtensionInfoMTime;
                if ( $mtime === false ) {
-                       if ( file_exists( $path ) ) {
-                               $mtime = filemtime( $path );
-                       } else {
-                               throw new Exception( "$path does not exist!" );
-                       }
+                       AtEase::suppressWarnings();
+                       $mtime = filemtime( $path );
+                       AtEase::restoreWarnings();
                        // @codeCoverageIgnoreStart
                        if ( $mtime === false ) {
                                $err = error_get_last();
-                               throw new Exception( "Couldn't stat $path: {$err['message']}" );
+                               throw new Exception( "Unable to open file $path: {$err['message']}" );
                                // @codeCoverageIgnoreEnd
                        }
                }
index d308d50..f2d0856 100644 (file)
@@ -798,9 +798,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
        /**
         * Get a list of file paths for all styles in this module, in order of proper inclusion.
         *
-        * This is considered a private method. Exposed for internal use by WebInstallerOutput.
-        *
-        * @private
+        * @internal Exposed only for use by WebInstallerOutput.
         * @param ResourceLoaderContext $context
         * @return array List of file paths
         */
index 810c304..402019e 100644 (file)
        "privacypage": "Project:Gizlilik prinsipi",
        "badaccess": "İcazə xətası",
        "badaccess-group0": "Bu fəaliyyəti icra etmək səlahiyyətiniz yoxdur.",
-       "badaccess-groups": "Bu fəaliyyəti, yalnız $1 {{PLURAL:$2|qrupundakı|qruplarından birindəki}} istifadəçilər icra edə bilərlər.",
+       "badaccess-groups": "Bu fəaliyyəti yalnız \"$1\" {{PLURAL:$2|qrupundakı|qruplarından birindəki}} istifadəçilər icra edə bilərlər.",
        "versionrequired": "MediaViki $1 versiyası lazımdır",
        "versionrequiredtext": "Bu səhifəni istifadə etmək üçün MediaVikinin $1 versiyası tələb olunur.\nBax: [[Special:Version|Versiyalar]].",
        "ok": "OK",
index 77374a6..2559468 100644 (file)
@@ -75,7 +75,7 @@
        "june-date": "{{PLURAL:$1|1°|$1}} ghjugnu",
        "july-date": "{{PLURAL:$1|1°|$1}} lugliu",
        "august-date": "{{PLURAL:$1|1°|$1}} aostu",
-       "september-date": "{{PLURAL:$1|1°|$1}} sittembre",
+       "september-date": "{{PLURAL:$1|1°|$1}} settembre",
        "october-date": "$1 uttobre",
        "november-date": "{{PLURAL:$1|1°|$1}} nuvembre",
        "december-date": "$1 dicembre",
index cda1db8..6ba40ff 100644 (file)
        "htmlform-date-placeholder": "SSSS-AA-RR",
        "htmlform-time-placeholder": "SS:DD:SS",
        "htmlform-datetime-placeholder": "SSSS-AA-RR SS:DD:SS",
+       "htmlform-title-not-creatable": "\"$1\" yew nameyê pela vıraziyiye niyo.",
        "htmlform-title-not-exists": "$1 çıni ya.",
        "htmlform-user-not-exists": "<strong>$1</strong> çıni ya.",
        "htmlform-user-not-valid": "<strong>$1</strong> hewl namey karberi niyo.",
        "log-action-filter-upload-upload": "Barkerdışo newe",
        "log-action-filter-upload-overwrite": "Anciya bar kerê",
        "log-action-filter-upload-revert": "Wegeyrayış",
+       "authmanager-retype-help": "Parola reyna raşt ke.",
        "authmanager-email-label": "E-poste",
        "authmanager-email-help": "Adresa e-posteyi",
        "authmanager-realname-label": "Nameyo raştıkên",
        "authmanager-realname-help": "Nameyê karberiyo raştıkên",
+       "authmanager-provider-temporarypassword": "Parolaya demkiye",
        "authprovider-resetpass-skip-label": "Ravêre",
        "authprovider-resetpass-skip-help": "Peysereştışê parola ra bıvêre.",
        "authform-notoken": "Tokeno kemi",
        "unlinkaccounts": "Hesabo bêgıre",
        "edit-error-short": "Xeta: $1",
        "edit-error-long": "Xeteyi:\n\n$1",
+       "specialmute": "Bêveng",
+       "specialmute-submit": "Tesdiq ke",
+       "specialmute-label-mute-email": "Nê karberi ra emailê bêvengi",
+       "mute-preferences": "Tercihê bêvengi",
        "revid": "Revizyonê $1",
        "pageid": "IDyê pela $1",
        "gotointerwiki": "{{SITENAME}} ra abırriyeno",
index 1293bb1..305043e 100644 (file)
        "sessionfailure": "Badirudi saioarekin arazoren bat dagoela; ekintza hau ezeztatua izan da, saio bahiketa saihesteko neurri bezala. Mesedez, nabigatzaileko \"atzera\" botoian klik egin, hona ekarri zaituen orrialde hori berriz kargatu, eta saiatu berriz.",
        "changecontentmodel": "Aldatu orri bateko eduki eredua",
        "changecontentmodel-legend": "Aldatu eduki eredua",
-       "changecontentmodel-title-label": "Orriaren izenburua",
-       "changecontentmodel-model-label": "Eduki eredu berria",
+       "changecontentmodel-title-label": "Orriaren izenburua:",
+       "changecontentmodel-model-label": "Eduki eredu berria:",
        "changecontentmodel-reason-label": "Arrazoia:",
        "changecontentmodel-submit": "Aldatu",
        "changecontentmodel-success-title": "Eduki eredua aldatu egin da",
index f09c093..87417b3 100644 (file)
        "exif-scenetype-1": "ca de fotoğraf ker",
        "exif-customrendered-0": "Prosesê normali",
        "exif-customrendered-1": "proseso xususi",
+       "exif-customrendered-2": "HDR (oricinal nêşevekiyayo)",
+       "exif-customrendered-3": "HDR (oricinal şevekiyayo)",
+       "exif-customrendered-4": "Oricinal (seba HDRyi ra)",
+       "exif-customrendered-6": "Panorama",
+       "exif-customrendered-7": "Portre HDR",
+       "exif-customrendered-8": "Portre",
        "exif-exposuremode-0": "pozkerdışê otomatiki",
        "exif-exposuremode-1": "pozkerdışê manueli",
        "exif-exposuremode-2": "Auto bracket",
index af28ea6..8915e6c 100644 (file)
        "exif-scenetype-1": "Image photographiée directement",
        "exif-customrendered-0": "Procédé normal",
        "exif-customrendered-1": "Procédé personnalisé",
-       "exif-customrendered-2": "HDR (pas d’original enregistré)",
+       "exif-customrendered-2": "HDR (aucun original enregistré)",
        "exif-customrendered-3": "HDR (original enregistré)",
        "exif-customrendered-4": "Original (pour HDR)",
        "exif-customrendered-6": "Panorama",
index 37d33f2..df7984e 100644 (file)
        "exif-orientation": "Orientation",
        "exif-samplesperpixel": "Numero de componentes",
        "exif-planarconfiguration": "Arrangiamento del datos",
-       "exif-ycbcrsubsampling": "Ration de reduction de Y a C",
+       "exif-ycbcrsubsampling": "Ration de submonstrage de Y a C",
        "exif-ycbcrpositioning": "Positionamento Y e C",
        "exif-xresolution": "Resolution horizontal",
        "exif-yresolution": "Resolution vertical",
-       "exif-stripoffsets": "Location del datos del imagine",
+       "exif-stripoffsets": "Position del datos del imagine",
        "exif-rowsperstrip": "Numero de lineas per banda",
        "exif-stripbytecounts": "Bytes per banda comprimite",
        "exif-jpeginterchangeformat": "Position de JPEG SOI",
        "exif-webstatement": "Declaration in linea de copyright",
        "exif-originaldocumentid": "ID unic del documento original",
        "exif-licenseurl": "URL pro licentia de copyright",
-       "exif-morepermissionsurl": "Information alternative de licentia",
+       "exif-morepermissionsurl": "Information sur licentias alternative",
        "exif-attributionurl": "Si tu re-usa iste obra, per favor insere un ligamine a",
        "exif-preferredattributionname": "Si tu re-usa iste obra, per favor da recognoscentia a",
        "exif-pngfilecomment": "Commento del file PNG",
        "exif-scenetype-1": "Un imagine directemente photographiate",
        "exif-customrendered-0": "Processo normal",
        "exif-customrendered-1": "Processo personalisate",
+       "exif-customrendered-2": "HDR (original non salveguardate)",
+       "exif-customrendered-3": "HDR (original salveguardate)",
+       "exif-customrendered-4": "Original (pro HDR)",
+       "exif-customrendered-6": "Panorama",
+       "exif-customrendered-7": "Portrait HDR",
+       "exif-customrendered-8": "Portrait",
        "exif-exposuremode-0": "Exposition automatic",
        "exif-exposuremode-1": "Exposition manual",
        "exif-exposuremode-2": "Bracketing automatic",
index 44c3330..4da4ec6 100644 (file)
@@ -91,9 +91,9 @@
        "exif-imageuniqueid": "Назнака на сликата",
        "exif-gpsversionid": "Верзија на ознака за GPS податоци",
        "exif-gpslatituderef": "Северна или јужна ГШ",
-       "exif-gpslatitude": "Геог. ширина",
+       "exif-gpslatitude": "Гео. ширина",
        "exif-gpslongituderef": "Источна или западна ГД",
-       "exif-gpslongitude": "Геог. должина",
+       "exif-gpslongitude": "Гео. должина",
        "exif-gpsaltituderef": "Упатна точка за висната",
        "exif-gpsaltitude": "Височина",
        "exif-gpstimestamp": "GPS-време (атомски часовник)",
index 856cd17..3eea40f 100644 (file)
        "exif-attributionurl": "Gebruuk de volgende verwiezing bie hergebruuk van dit wark",
        "exif-preferredattributionname": "Gebruuk de volgende makersvermelding bie hergebruuk van dit wark",
        "exif-pngfilecomment": "Opmarking bie PNG-bestaand",
-       "exif-disclaimer": "Veurbehoud",
+       "exif-disclaimer": "Vöärbehold",
        "exif-contentwarning": "Waorschuwing over inhoud",
        "exif-giffilecomment": "Opmarking bie GIF-bestaand",
        "exif-intellectualgenre": "Soort onderwarp",
index 473a03a..bef4432 100644 (file)
@@ -7,7 +7,7 @@
                        "Uostofchuodnego"
                ]
        },
-       "exif-imagewidth": "Šyrokość",
+       "exif-imagewidth": "Szyrokość",
        "exif-imagelength": "Wysokość",
        "exif-bitspersample": "Bitůw na průbka",
        "exif-compression": "Metoda kompresyji",
        "exif-sharpness-0": "Normalno",
        "exif-sharpness-1": "Licho",
        "exif-sharpness-2": "Srogo",
-       "exif-subjectdistancerange-0": "ńyznano",
+       "exif-subjectdistancerange-0": "niyznōmŏ",
        "exif-subjectdistancerange-1": "Makro",
        "exif-subjectdistancerange-2": "widok z bliska",
        "exif-subjectdistancerange-3": "widok z daleka",
index 18c0da7..0b0f7ce 100644 (file)
@@ -74,7 +74,8 @@
                        "Amjad Khan",
                        "Ahmad252",
                        "FarsiNevis",
-                       "Moyogo"
+                       "Moyogo",
+                       "MohammadtheEditor"
                ]
        },
        "tog-underline": "خط کشیدن زیر پیوندها:",
        "revdelete-unsuppress": "حذف محدودیت‌ها در بازبینی‌های ترمیم‌شده",
        "revdelete-log": "دلیل:",
        "revdelete-submit": "اعمال بر {{PLURAL:$1|نسخهٔ|نسخه‌های}} انتخاب شده",
-       "revdelete-success": "Ù¾Û\8cداÛ\8cÛ\8c Ù\86سخÙ\87 Ø±Ù\88زآÙ\85د شد.",
+       "revdelete-success": "Ù¾Û\8cداÛ\8cÛ\8c Ù\86سخÙ\87 Ø¨Ø±Ù\88ز شد.",
        "revdelete-failure": "'''پیدایی نسخه‌ها قابل به روز کردن نیست:'''\n$1",
        "logdelete-success": "تغییر پیدایی مورد انجام شد.",
        "logdelete-failure": "'''پیدایی سیاهه‌ها قابل تنظیم نیست:'''\n$1",
index 33cddd8..994b1e1 100644 (file)
        "passwordreset-ignored": "Salasanan palauttamista ei käsitelty. Ehkä tarjoajaa ei ollut määritetty?",
        "passwordreset-invalidemail": "Virheellinen sähköpostiosoite",
        "passwordreset-nodata": "Käyttäjätunnusta ja salasanaa ei annettu",
-       "changeemail": "Muuta tai poista E-posti atressi",
+       "changeemail": "Muuta tai poista sähköpostiosoite",
        "changeemail-header": "Täydennä tämä lomake, jolla voit muuttaa sähköpostiosoitettasi. Jos haluat poistaa sähköpostiosoitteesi kokonaan tunnuksesi yhteydestä, älä kirjoita uudeksi osoitteeksi mitään vaan jätä se tyhjäksi.",
        "changeemail-no-info": "Tämän sivun käyttö edellyttää sisäänkirjautumista.",
        "changeemail-oldemail": "Nykyinen sähköpostiosoite:",
        "updated": "(Päivitetty)",
        "note": "'''Huomautus:'''",
        "previewnote": "<strong>Tämä on vasta sivun esikatselu.</strong>\nTekemiäsi muutoksia ei ole vielä tallennettu.",
-       "continue-editing": "Siiry mookkauskenttään",
+       "continue-editing": "Siirry muokkauskenttään",
        "previewconflict": "Tämä esikatselu näyttää miltä muokkausalueella oleva teksti näyttää tallennettuna.",
        "session_fail_preview": "Muokkaustasi ei voitu tallentaa, koska istuntosi tiedot ovat kadonneet.\n\nSaatat olla kirjautunut ulos. '''Varmista, että olet edelleen kirjautunut sisään ja yritä uudelleen'''. Jos ongelma ei katoa, yritä [[Special:UserLogout|kirjautua ulos]] ja takaisin sisään, ja varmista, että selaimesi sallii evästeet tältä sivustolta.",
        "session_fail_preview_html": "Valitettavasti muokkaustasi ei voitu käsitellä istunnon tietojen katoamisen vuoksi.\n\n<em>Koska {{GRAMMAR:inessive|{{SITENAME}}}} on käytössä suodattamaton HTML-koodi, esikatselu on piilotettu JavaScript-hyökkäyksien torjumiseksi</em>\n\n<strong>Jos tämä on oikea muokkausyritys, yritä uudelleen.</strong> Jos ongelma ei katoa, yritä [[Special:UserLogout|kirjautua ulos]] ja takaisin sisään. Tarkista myös, että selaimesi sallii evästeet tältä sivustolta.",
        "prefs-watchlist-managetokens": "Hallitse avaimia",
        "prefs-misc": "Muut",
        "prefs-resetpass": "Muuta salasana",
-       "prefs-changeemail": "Muuta tai poista E-posti atressi",
+       "prefs-changeemail": "Muuta tai poista sähköpostiosoite",
        "prefs-setemail": "Aseta sähköpostiosoite",
        "prefs-email": "Sähköpostiasetukset",
        "prefs-rendering": "Ulkoasu",
index 71e9e88..5ce9fd5 100644 (file)
        "botpasswords-label-delete": "Ota poies",
        "resetpass-submit-cancel": "Lopeta",
        "passwordreset-email": "E-postin atressi:",
+       "changeemail": "Muuta tai poista E-postin atressi",
        "changeemail-newemail": "Uusi E-postin atressi:",
        "bold_sample": "Lihava teksti",
        "bold_tip": "Lihava teksti",
        "linkstoimage": "{{PLURAL:$1|Seuraava sivu|Seuraavat $1 sivua}} käytthävät tätä fiilhiä:",
        "nolinkstoimage": "Ei ole yhtään sivua joka käyttää tätä fiilhiä.",
        "sharedupload-desc-here": "Tämä fiili on jaettu kohtheesta $1 ja muut prujektit saattavat käyttää sitä.\nTiot [$2 fiilin kuvvaussivulta] näkyvät tässä alla.",
+       "shared-repo-name-wikimediacommons": "Wikimeetia Commons",
        "filedelete": "Ota poies $1",
        "filedelete-legend": "Ota poies fiili",
        "filedelete-submit": "Ota poies",
        "istemplate": "sisäletty mallina",
        "isimage": "linkki fiilhiin",
        "whatlinkshere-prev": "← {{PLURAL:$1|eelinen sivu|$1 eelistä sivua}}",
-       "whatlinkshere-next": "{{PLURAL:$1|seuraava sivu|$1 seuraava sivu}} →",
+       "whatlinkshere-next": "{{PLURAL:$1|seuraava sivu|$1 seuraavaa sivua}} →",
        "whatlinkshere-links": "linkit",
        "whatlinkshere-hideredirs": "$1 ohjaukset",
        "whatlinkshere-hidetrans": "$1 mallin inklyteerinkiä",
index 73a7bc4..f2fbb2d 100644 (file)
        "nocreate-loggedin": "Vous n'avez pas la permission de créer de nouvelles pages.",
        "sectioneditnotsupported-title": "Modification de section non prise en charge",
        "sectioneditnotsupported-text": "La modification d’une section n’est pas prise en charge pour cette page.",
-       "modeleditnotsupported-title": "Modification non supportée",
-       "modeleditnotsupported-text": "La modification n’est pas supportée pour le modèle de contenu $1.",
+       "modeleditnotsupported-title": "Modification non prise en charge",
+       "modeleditnotsupported-text": "La modification n’est pas prise en charge pour le modèle de contenu $1.",
        "permissionserrors": "Erreur de permissions",
        "permissionserrorstext": "Vous n'avez pas la permission d'effectuer l'opération demandée pour {{PLURAL:$1|la raison suivante|les raisons suivantes}} :",
        "permissionserrorstext-withaction": "Vous ne pouvez pas $2, pour {{PLURAL:$1|la raison suivante|les raisons suivantes}} :",
        "content-model-json": "JSON",
        "content-json-empty-object": "Objet vide",
        "content-json-empty-array": "Tableau vide",
-       "unsupported-content-model": "<strong>Attention :</strong> Le modèle de contenu $1 n’est pas supporté sur ce wiki.",
+       "unsupported-content-model": "<strong>Attention :</strong> le modèle de contenu $1 n’est pas pris en charge sur ce wiki.",
        "unsupported-content-diff": "Les diffs ne sont pas supportés pour le modèle de contenu $1.",
        "unsupported-content-diff2": "Les diffs entre les modèles de contenu $1 et $2 ne sont pas supportés sur ce wiki.",
        "deprecated-self-close-category": "Pages utilisant des balises HTML auto-fermantes non valides",
        "sessionfailure": "Votre session de connexion semble avoir des problèmes ;\ncette action a été annulée en prévention d'un piratage de session.\nVeuillez soumettre le formulaire de nouveau.",
        "changecontentmodel": "Modifier le modèle de contenu d’une page",
        "changecontentmodel-legend": "Modifier le modèle de contenu",
-       "changecontentmodel-title-label": "Titre de la page :",
+       "changecontentmodel-title-label": "Titre de la page:",
        "changecontentmodel-current-label": "Modèle de contenu actuel :",
-       "changecontentmodel-model-label": "Nouveau modèle de contenu :",
+       "changecontentmodel-model-label": "Nouveau modèle de contenu:",
        "changecontentmodel-reason-label": "Motif :",
        "changecontentmodel-submit": "Modifier",
        "changecontentmodel-success-title": "Le modèle de contenu a été modifié",
        "delete_and_move_reason": "Page supprimée pour permettre le renommage depuis « [[$1]] »",
        "selfmove": "Le titre est le même ;\nimpossible de renommer une page sur elle-même.",
        "immobile-source-namespace": "Vous ne pouvez pas renommer les pages dans l'espace de noms « $1 »",
-       "immobile-source-namespace-iw": "Il n'est pas possible de déplacer les pages depuis ce wiki vers les autres wikis.",
+       "immobile-source-namespace-iw": "Il nest pas possible de déplacer les pages depuis ce wiki vers les autres wikis.",
        "immobile-target-namespace": "Vous ne pouvez pas renommer des pages vers l’espace de noms « $1 ».",
        "immobile-target-namespace-iw": "Un lien interwiki n’est pas une cible valide pour un renommage de page.",
        "immobile-source-page": "Cette page n'est pas renommable.",
index b4deae8..d8dffa5 100644 (file)
@@ -28,7 +28,8 @@
                        "Athena in Wonderland",
                        "Navhy",
                        "PokéDex Nacional",
-                       "Maria zaos"
+                       "Maria zaos",
+                       "Iváns"
                ]
        },
        "tog-underline": "Subliñar as ligazóns:",
index 853ef7b..9c5a899 100644 (file)
        "createacct-reason": "कारण",
        "createacct-reason-ph": "तूं दुसरें खातें कित्याक उगडटात",
        "createacct-submit": "तुमचे खातें रोचात",
-       "createacct-another-submit": "दà¥\81सरà¥\87à¤\82 à¤\96ातà¥\87à¤\82 à¤¤à¤¯à¤¾à¤° à¤\95र",
+       "createacct-another-submit": "à¤\96ातà¥\87à¤\82 à¤°à¥\8bà¤\9aात",
        "createacct-benefit-heading": "{{SITENAME}} तुमच्या सारख्या लोकांनी केल्लो",
        "createacct-benefit-body1": "{{PLURAL:$1|संपादन|संपादना}}",
        "createacct-benefit-body2": "{{PLURAL:$1|पान|पानां}}",
        "sig_tip": "वेळ-छाप सयत तुमची निशाणी",
        "hr_tip": "आडवी वळ (उणो वापरचो)",
        "summary": "आपरोस:",
-       "subject": "विशय/माथाळो",
+       "subject": "विशय:",
        "minoredit": "हें दाकटें संपादन",
        "watchthis": "हें पानार नदर दवरात",
        "savearticle": "पान सांभाळ",
        "license-header": "परवांगी",
        "listfiles-delete": "काडून उडयात",
        "imgfile": "फायल",
+       "listfiles": "Faylichi volleri‎",
        "listfiles_date": "तारीख",
        "listfiles_name": "नांव",
        "listfiles_user": "वापरपी",
        "dellogpage": "काडून उडयिल्ल्यांची वळेरी",
        "rollbacklink": "फाटीं घेयात",
        "rollbacklinkcount": "$1 {{PLURAL:$1|संपादन}} फाटीं घेयात",
-       "changecontentmodel-title-label": "पानाचो माथाळो",
+       "changecontentmodel-title-label": "पानाचो माथाळो:",
        "changecontentmodel-reason-label": "कारण:",
        "protectlogpage": "सुरक्षितेचें सोत्र",
        "protectedarticle": "राखिल्ले\"[[$1]]\"",
        "tooltip-t-recentchangeslinked": "ह्या पानावेल्यान दुवे दिल्ल्या पानांतले हालींचे बदल",
        "tooltip-feed-atom": "ह्या पाना खातीर ऍटम पूर्वण",
        "tooltip-t-contributions": "ह्या वापरप्याची योगदानाची वळेरी",
-       "tooltip-t-emailuser": "ह्या उपेगकर्त्याक इ-मेल धाडात",
+       "tooltip-t-emailuser": "{{GENDER:$1|ह्या उपेगकर्त्याक}} इ-मेल धाडात",
        "tooltip-t-upload": "फायली अपलोड करात",
        "tooltip-t-specialpages": "सगळ्या विशेश पानांची वळेरी",
        "tooltip-t-print": "ह्या पानाची छापपायोग्य आवृत्ती",
        "tag-filter": "[[Special:Tags|कुर्वेचीट]] गाळणो:",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|कुरवेचीट|कुरवेचीटी}}]]: $2",
        "tags-active-yes": "हय",
+       "tags-active-no": "ना",
        "htmlform-title-not-exists": "$1 अस्तित्वांत ना.",
        "logentry-delete-delete": "$1 {{GENDER:$2|काडून उडयल्ले पान}} $3",
        "logentry-move-move": "$1 हाणें $3 पानाक $4 {{GENDER:$2|हालयला}}",
index 83a633a..51842f5 100644 (file)
        "rcfilters-activefilters": "Kriaxil challnneo",
        "rcfilters-activefilters-hide": "Lipoi",
        "rcfilters-activefilters-show": "Dakhoi",
+       "rcfilters-activefilters-hide-tooltip": "Kriaxil challnnecho kxetr lipoi",
+       "rcfilters-activefilters-show-tooltip": "Kriaxil challnneache kxetr dakhoi.",
        "rcfilters-advancedfilters": "Sudarit challnneo",
        "rcfilters-limit-title": "Dakhovpache porinnam",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|bodol}}, $2",
+       "rcfilters-date-popup-title": "Soda khatir vellacho kall",
        "rcfilters-days-title": "Halinche dis",
        "rcfilters-hours-title": "Halinchim voram",
        "rcfilters-days-show-days": "$1 {{PLURAL:$1|dis}}",
        "rcfilters-days-show-hours": "$1 {{PLURAL:$1|vor|voram}}",
        "rcfilters-quickfilters": "Samball’lleleo challnneo",
+       "rcfilters-quickfilters-placeholder-title": "Ozun poriant khoinchich challnni samballunk na",
        "rcfilters-savedqueries-defaultlabel": "Samball’lleleo challnneo",
        "rcfilters-savedqueries-rename": "Nanv bodol",
        "rcfilters-savedqueries-setdefault": "Default koxem bosoi",
        "rcfilters-savedqueries-new-name-label": "Nanv",
        "rcfilters-savedqueries-apply-label": "Challnni roch",
        "rcfilters-savedqueries-cancel-label": "Rod'd kor",
+       "rcfilters-restore-default-filters": "Default challnneo porot hadd",
+       "rcfilters-clear-all-filters": "Soglleo challnneo nivoll kor",
        "rcfilters-show-new-changes": "$1 savn noveo bodol polloi",
        "rcfilters-search-placeholder-mobile": "Challnneo",
        "rcfilters-invalid-filter": "Ovoid challnni",
        "rcfilters-filter-editsbyother-label": "Dusreanim kel'le bodol",
        "rcfilters-filter-editsbyother-description": "Tuje khas bhairavn, soglle bodol",
        "rcfilters-filtergroup-user-experience-level": "Vaporpeachi nondnni ani onnbhov",
+       "rcfilters-filter-user-experience-level-learner-label": "Xikpi",
        "rcfilters-filtergroup-automated": "Apoap zalolem iogdan",
        "rcfilters-filter-bots-label": "Robot",
        "rcfilters-filter-bots-description": "Apoap avtamni kelolem sompadon",
        "rcfilters-filter-humans-label": "Monxan kelolem (nhoi robotan)",
        "rcfilters-filter-humans-description": "Monxani kelolem sompadon",
        "rcfilters-filter-reviewstatus-unpatrolled-description": "Paro kela mhonn khunnavnk naslolem sompadon.",
+       "rcfilters-filter-reviewstatus-unpatrolled-label": "Paro korunk naslolem",
        "rcfilters-filtergroup-significance": "Mhotv",
        "rcfilters-filter-minor-label": "Dhakte bodol",
        "rcfilters-filter-minor-description": "Borovpean dhaktem mhonn khunne chitt kelolem sompadon",
        "rcfilters-filter-watchlistactivity-seen-description": "Bodol ghoddlea uprant tuvem bhett dilolea tea panache bodol.",
        "rcfilters-filtergroup-changetype": "Bodolacho prokar",
        "rcfilters-filter-pageedits-label": "Panacheo sompadonam",
+       "rcfilters-filter-newpages-label": "Panam rochop",
+       "rcfilters-filter-newpages-description": "Sompadon jim novim panam rochtat",
        "rcfilters-filter-categorization-label": "Vorgache bodol",
        "rcfilters-filter-categorization-description": "Vorgant savn pana zoddloleachi vo kaddloleachi nond",
        "rcfilters-filter-logactions-label": "Sotran nond zal’leo kario",
        "rcfilters-filtergroup-lastrevision": "Akherchim uzollnnim",
        "rcfilters-filter-lastrevision-label": "Sogleanvon novi uzollnni",
        "rcfilters-filter-lastrevision-description": "Ek panak fokot nimannem bodol",
+       "rcfilters-filter-previousrevision-label": "Halinchi uzollnni nhoi",
        "rcfilters-filter-previousrevision-description": "Soglle bodol je \"halinchi uzollnni\" nant.",
        "rcfilters-tag-prefix-namespace-inverted": "$1 <strong>:nhoi</strong>",
        "rcfilters-view-tags": "Khunnechittichem sompadon",
+       "rcfilters-view-tags-tooltip": "Sompadonacheo khunne chitti vaprun porinnam chall",
        "rcfilters-view-tags-help-icon-tooltip": "Khunnechittichem sompadona babtint odik xikun ghe",
+       "rcfilters-watchlist-markseen-button": "Soglle bodol polleleat mhonn khunnai.",
+       "rcfilters-watchlist-showupdated": "Bodol zal'leak savn je panank tuvem bhett dinvk na, te bodol <strong>datt</strong> okxoramni, ani ghott khunnamni dileat.",
+       "rcfilters-filter-showlinkedto-label": "Panak zoddtat tea panache bodol dakhoi",
        "rcfilters-target-page-placeholder": "Ek panache nanv ( vo vorg) ghal",
        "rcnotefrom": "Sokoil <strong>$3, $4<strong> savn {{PLURAL:$5|zalelem bodol dilam|zalelem bodol dileant}} (<strong>$1<strong> meren {{PLURAL:$5|dakhoilam|dakhoileant}}).",
        "rclistfrom": "$3 $2 savn suru zatelim nove bodol dakhoi",
        "watchlist-options": "Sadurvollericheo poryay",
        "watching": "Disht dovortanv...",
        "unwatching": "Disht kaddthanv...",
-       "enotif_reset": "Panam bhett dilolim mhunn khunnai",
+       "enotif_reset": "Sogllim panam bhett dilolim mhunn khunnai",
        "delete-legend": "Kadun udoi",
        "actioncomplete": "Karvai sompurnn",
        "actionfailed": "Karvai oiesiesvi",
index e4efdb8..6480a93 100644 (file)
        "backend-fail-batchsize": "למאגר אחסון הקבצים הפנימי הועבר אוסף של {{PLURAL:$1|פעולת קובץ אחת|$1 פעולות קובץ}}; המגבלה היא {{PLURAL:$2|פעולה אחת|$2 פעולות}}.",
        "backend-fail-usable": "קריאת או כתיבת הקובץ \"$1\" לא הצליחה כיוון שההרשאות אינן מספיקות או כיוון שהספריות/המכלים חסרים.",
        "backend-fail-stat": "לא היה אפשר לקרוא את המצב של הקובץ \"$1\".",
+       "backend-fail-hash": "לא היה אפשר להחליט מהו גיבוב ההצפנה של הקובץ \"$1\".",
        "filejournal-fail-dbconnect": "לא ניתן היה להתחבר לבסיס הנתונים של היומן עבור מאגר אחסון הקבצים הפנימי \"$1\".",
        "filejournal-fail-dbquery": "לא ניתן היה לעדכן את בסיס הנתונים של היומן עבור מאגר אחסון הקבצים הפנימי \"$1\".",
        "lockmanager-notlocked": "פתיחת הנעילה של \"$1\" לא הצליחה; הוא לא נעול.",
index ba8806b..e9ae01b 100644 (file)
        "nocreate-loggedin": "Nincs jogosultságod új lapokat létrehozni.",
        "sectioneditnotsupported-title": "A szakaszszerkesztés nem támogatott",
        "sectioneditnotsupported-text": "Ezen a lapon nem támogatott a szakaszok szerkesztése",
+       "modeleditnotsupported-title": "A szerkesztés nem támogatott",
+       "modeleditnotsupported-text": "A következő tartalommodell szerkesztése nem támogatott: $1.",
        "permissionserrors": "Engedélyezési hiba",
        "permissionserrorstext": "A művelet elvégzése nem engedélyezett a számodra, a következő {{PLURAL:$1|ok|okok}} miatt:",
        "permissionserrorstext-withaction": "Nincs jogosultságod a következő művelet elvégzéséhez: $2, a következő {{PLURAL:$1|ok|okok}} miatt:",
        "content-model-css": "CSS",
        "content-json-empty-object": "Üres objektum",
        "content-json-empty-array": "Üres tömb",
+       "unsupported-content-model": "<strong>Figyelem:</strong> A következő tartalommodell nem támogatott ezen a wikin: $1.",
        "deprecated-self-close-category": "Érvénytelen önzáró HTML-címkéket használó lapok",
        "deprecated-self-close-category-desc": "A lap érvénytelen önzáró HTML-címkéket használ (pl. <code>&lt;b/></code> vagy <code>&lt;span/></code>). Ezeknek a működése hamarosan meg fog változni a HTML5 szabvánnyal összhangban lévőre, ezért a wikiszövegben való használatuk elavult.",
        "duplicate-args-warning": "<strong>Figyelmeztetés:</strong> A(z) [[:$1]] lap dupla értékkel hívja meg a(z) [[:$2]] sablont („$3” paraméter). Csak az utolsó érték lesz felhasználva.",
        "sessionfailure": "Úgy látszik, hogy probléma van a bejelentkezési munkameneteddel;\nez a művelet a munkamenet eltérítése miatti óvatosságból megszakadt.\nKérjük, küldd el újra az űrlapot.",
        "changecontentmodel": "A lap tartalommodelljének megváltoztatása",
        "changecontentmodel-legend": "Tartalommodell megváltoztatása",
-       "changecontentmodel-title-label": "Lapcím",
+       "changecontentmodel-title-label": "Lapcím:",
        "changecontentmodel-current-label": "Jelenlegi tartalommodell:",
-       "changecontentmodel-model-label": "Új tartalommodell",
+       "changecontentmodel-model-label": "Új tartalommodell:",
        "changecontentmodel-reason-label": "Indoklás:",
        "changecontentmodel-submit": "Változtatás",
        "changecontentmodel-success-title": "A tartalommodell megváltozott",
        "move-subpages": "Allapok átnevezése (maximum $1)",
        "move-talk-subpages": "A vitalap allapjainak átnevezése (maximum $1)",
        "movepage-page-exists": "A(z) „$1” nevű lap már létezik, és nem írható felül automatikusan.",
+       "movepage-source-doesnt-exist": "A(z) „$1” oldal nem létezik, ezért nem lehet átnevezni.",
        "movepage-page-moved": "A(z) „$1” nevű lap át lett nevezve „$2” névre.",
        "movepage-page-unmoved": "A(z) „$1” nevű lap nem nevezhető át „$2” névre.",
        "movepage-max-pages": "{{PLURAL:$1|Egy|$1}} lapnál több nem nevezhető át automatikusan, így a további lapok a helyükön maradnak.",
        "immobile-target-namespace-iw": "Wikiközi hivatkozás nem lehet a lap új neve.",
        "immobile-source-page": "Ez a lap nem nevezhető át.",
        "immobile-target-page": "A lap nem helyezhető át a megadott címre.",
+       "movepage-invalid-target-title": "A célnév érvénytelen.",
        "bad-target-model": "A kívánt célhely eltérő tartalom modellt használ. Nem lehet $1 modellről $2 modellre konvertálni.",
        "imagenocrossnamespace": "A fájlok nem helyezhetőek át más névtérbe",
        "nonfile-cannot-move-to-file": "Nem fájlok nem nevezhetők át fájlnévtérbe",
index ad0b23b..ba8f515 100644 (file)
        "nocreate-loggedin": "Non si dispone dei permessi necessari a creare nuove pagine.",
        "sectioneditnotsupported-title": "Modifica delle sezioni non supportata",
        "sectioneditnotsupported-text": "La modifica delle sezioni non è supportata in questa pagina.",
+       "modeleditnotsupported-title": "Modifica non supportata",
+       "modeleditnotsupported-text": "La modifica per il modello di contenuto $1 non è supportata.",
        "permissionserrors": "Permessi non sufficienti",
        "permissionserrorstext": "Non si dispone dei permessi necessari ad eseguire l'azione richiesta, per {{PLURAL:$1|il seguente motivo|i seguenti motivi}}:",
        "permissionserrorstext-withaction": "Non si dispone dei permessi necessari per $2, per {{PLURAL:$1|il seguente motivo|i seguenti motivi}}:",
        "content-model-css": "CSS",
        "content-json-empty-object": "Oggetto vuoto",
        "content-json-empty-array": "Array vuoto",
+       "unsupported-content-model": "<strong>Attenzione:</strong> il modello di contenuto $1 non è supportato in questo wiki.",
+       "unsupported-content-diff": "Le differenze per il modello di contenuto $1 non sono supportate.",
+       "unsupported-content-diff2": "Le differenze fra i modelli di contenuto $1 e $2 non sono supportate su questo wiki.",
        "deprecated-self-close-category": "Pagine che utilizzano tag HTML auto-chiusi non validi",
        "deprecated-self-close-category-desc": "La pagina contiene tag HTML auto-chiusi non validi, come <code>&lt;b/></code> o <code>&lt;span/></code>. Il comportamento di questi presto cambierà per essere coerente con le specifiche HTML5, per questo il loro uso nel wikitesto è deprecato.",
        "duplicate-args-warning": "<strong>Avvertenza:</strong> [[:$1]] chiama [[:$2]] con più di un valore per il parametro \"$3\". Verrà utilizzato solo l'ultimo valore fornito.",
        "action-blockemail": "impedire a un utente di inviare email",
        "action-bot": "essere trattato come processo automatizzato",
        "action-editprotected": "modificare pagine protette con \"{{int:protect-level-sysop}}\"",
+       "action-editsemiprotected": "modificare pagine protette con \"{{int:protect-level-autoconfirmed}}\"",
        "action-editinterface": "modificare l'interfaccia utente",
        "action-editusercss": "modificare i file CSS di altri utenti",
        "action-edituserjson": "modificare i file JSON di altri utenti",
        "action-editmyusercss": "modificare i propri file CSS",
        "action-editmyuserjson": "modificare i propri file JSON",
        "action-editmyuserjs": "modificare i propri file JavaScript",
+       "action-editmyuserjsredirect": "modificare i propri file JavaScript che sono reindirizzamenti",
        "action-viewsuppressed": "vedere versioni nascoste a qualsiasi utente",
        "action-hideuser": "bloccare un nome utente, nascondendolo al pubblico",
        "action-ipblock-exempt": "ignorare i blocchi IP, blocchi automatici e blocchi ad intervalli",
+       "action-unblockself": "sbloccare sé stessi",
        "action-noratelimit": "non essere soggetto a limiti di intervallo",
        "action-reupload-own": "sovrascrivere file esistenti caricati da qualcuno",
+       "action-nominornewtalk": "evitare che le modifiche minori a pagine di discussione facciano scattare l'avviso di nuovo messaggio",
+       "action-markbotedits": "segnare le modifiche soggette a rollback come effettuate da bot",
        "action-override-export-depth": "esportare pagine che includono pagine collegate fino ad una profondità di 5",
        "action-suppressredirect": "non creare reindirizzamenti da pagine sorgente quando si spostano le pagine",
        "nchanges": "$1 {{PLURAL:$1|modifica|modifiche}}",
        "blocklist-addressblocks": "Nascondi i blocchi di un solo IP",
        "blocklist-type": "Tipo:",
        "blocklist-type-opt-all": "Tutto",
+       "blocklist-type-opt-sitewide": "Completo",
        "blocklist-type-opt-partial": "Parziale",
        "blocklist-rangeblocks": "Nascondi i blocchi di range",
        "blocklist-timestamp": "Data e ora",
        "move-subpages": "Sposta le sottopagine (sino a $1)",
        "move-talk-subpages": "Sposta le sottopagine di discussione (fino a $1)",
        "movepage-page-exists": "La pagina $1 esiste già e non può essere automaticamente sovrascritta.",
+       "movepage-source-doesnt-exist": "La pagina $1 non esiste e non può essere spostata.",
        "movepage-page-moved": "La pagina $1 è stata spostata a $2.",
        "movepage-page-unmoved": "La pagina $1 non può essere spostata a $2.",
        "movepage-max-pages": "È stato spostato il numero massimo di $1 {{PLURAL:$1|pagina|pagine}} e non potranno essere spostate ulteriori pagine automaticamente.",
        "delete_and_move_reason": "Cancellata per rendere possibile lo spostamento da \"[[$1]]\"",
        "selfmove": "Il titolo è lo stesso, non è possibile spostare una pagina su sé stessa.",
        "immobile-source-namespace": "Non è possibile spostare pagine del namespace \"$1\"",
+       "immobile-source-namespace-iw": "Non è possibile spostare pagine da questo wiki verso altri wiki.",
        "immobile-target-namespace": "Non è possibile spostare pagine nel namespace \"$1\"",
        "immobile-target-namespace-iw": "Un collegamento interwiki non è una destinazione valida per spostare la pagina.",
        "immobile-source-page": "Questa pagina non può essere spostata.",
        "immobile-target-page": "Non è possibile spostare sul titolo indicato.",
+       "movepage-invalid-target-title": "Il titolo richiesto non è valido.",
        "bad-target-model": "La destinazione desiderata utilizza un modello di contenuti diverso. Non è possibile convertire da $1 a $2.",
        "imagenocrossnamespace": "Non è possibile spostare un file fuori dal relativo namespace.",
        "nonfile-cannot-move-to-file": "Non è possibile spostare un file fuori dal relativo namespace.",
        "permanentlink": "Link permanente",
        "permanentlink-revid": "ID versione",
        "permanentlink-submit": "Vai alla versione",
+       "newsection": "Nuova sezione",
+       "newsection-page": "Pagina di destinazione",
+       "newsection-submit": "Vai alla pagina",
        "dberr-problems": "Questo sito sta avendo dei problemi tecnici.",
        "dberr-again": "Prova ad attendere qualche minuto e ricaricare.",
        "dberr-info": "(Impossibile accedere al server del database: $1)",
        "mw-widgets-abandonedit-keep": "Continuare a modificare",
        "mw-widgets-abandonedit-title": "Sei sicuro?",
        "mw-widgets-copytextlayout-copy": "Copia",
+       "mw-widgets-copytextlayout-copy-fail": "Copia negli appunti non riuscita.",
+       "mw-widgets-copytextlayout-copy-success": "Copiato negli appunti.",
        "mw-widgets-dateinput-no-date": "Nessuna data selezionata",
        "mw-widgets-dateinput-placeholder-day": "AAAA-MM-GG",
        "mw-widgets-dateinput-placeholder-month": "AAAA-MM",
        "gotointerwiki-external": "Stai per lasciare {{SITENAME}} per visitare [[$2]], che è un sito web diverso.\n\n'''[$1 Continua su $1]'''",
        "undelete-cantedit": "Non puoi ripristinare questa pagina poiché non hai sufficienti permessi per modificarla.",
        "undelete-cantcreate": "Non puoi ripristinare questa pagina poiché la pagina con questo nome non è ancora inesistente e non hai sufficienti permessi per crearla.",
+       "pagedata-title": "Dati della pagina",
        "pagedata-not-acceptable": "Nessun formato corrispondente trovato. Tipi MIME supportati: $1",
        "pagedata-bad-title": "Titolo non valido: $1.",
        "unregistered-user-config": "Per motivi di sicurezza, non è possibile caricare sottopagine utente JavaScript, CSS e JSON per utenti non registrati.",
+       "passwordpolicies": "Politiche sulle password",
        "passwordpolicies-summary": "Questo è un elenco delle politiche sulle password efficaci per i gruppi di utenti definiti in questo wiki.",
        "passwordpolicies-group": "Gruppo",
        "passwordpolicies-policies": "Politiche",
        "passwordpolicies-policy-maximalpasswordlength": "La password deve essere lunga meno di $1 {{PLURAL:$1|carattere|caratteri}}",
        "passwordpolicies-policy-passwordcannotbepopular": "La password non può essere {{PLURAL:$1|la password più popolare|nell'elenco delle $1 password più popolari}}",
        "passwordpolicies-policy-passwordnotinlargeblacklist": "La password non può essere nell'elenco delle 100 000 password utilizzate più comunemente.",
+       "passwordpolicies-policyflag-forcechange": "(deve essere modificata all'accesso)",
+       "passwordpolicies-policyflag-suggestchangeonlogin": "(suggerita modifica all'accesso)",
+       "mycustomjsredirectprotected": "Non si dispone dei permessi necessari a modificare questa pagina JavaScript perché si tratta di un redirect che non punta al proprio spazio utente.",
        "easydeflate-invaliddeflate": "Il contenuto fornito non è compresso correttamente",
        "unprotected-js": "Per motivi di sicurezza, non è possibile caricare JavaScript da pagine non protette. Crea javascript solo nel namespace MediaWiki o come sottopagina Utente",
        "userlogout-continue": "Vuoi davvero uscire?"
index 52a713f..7b8b114 100644 (file)
@@ -77,7 +77,8 @@
                        "Comjun04",
                        "Son77391",
                        "Jango",
-                       "D6283"
+                       "D6283",
+                       "Ktrst"
                ]
        },
        "tog-underline": "링크에 밑줄 긋기:",
        "systemblockedtext": "당신의 사용자 이름 또는 IP 주소가 자동으로 미디어위키에 의해 차단되었습니다.\n이유는 다음과 같습니다:\n\n:<em>$2</em>\n\n* 차단 시작: $8\n* 차단 만료: $6\n* 차단 대상: $7\n\n당신의 현재 IP 주소는 $3입니다.\n문의에 대해 상기의 상세 설명을 모두 포함해 주십시오.",
        "blockednoreason": "이유를 입력하지 않음",
        "blockedtext-composite": "<strong>당신의 사용자 이름 또는 IP 주소가 미디어위키에 의해 차단되었습니다.\n\n이유는 다음과 같습니다:\n\n:<em>$2</em>\n\n* 차단 시작: $8\n* 차단 만료: $6\n\n* $5\n\n당신의 현재 IP 주소는 $3입니다.\n문의에 대해 상기의 상세 설명을 모두 포함해 주십시오.",
+       "blockedtext-composite-ids": "관련 블록 ID: $1 (IP 주소는 블랙리스트에 추가될 수도 있습니다)",
+       "blockedtext-composite-no-ids": "IP 주소가 여러 블랙리스트에 나타납니다",
+       "blockedtext-composite-reason": "당신의 계정 또는 IP 주소가 여러 번 차단되었습니다",
        "whitelistedittext": "문서를 편집하기 전에 $1해야 합니다.",
        "confirmedittext": "문서를 고치려면 이메일 인증 절차가 필요합니다.\n[[Special:Preferences|사용자 환경 설정]]에서 이메일 주소를 입력하고 이메일 주소 인증을 해주시기 바랍니다.",
        "nosuchsectiontitle": "문단을 찾을 수 없음",
        "sectioneditnotsupported-title": "부분 편집이 지원되지 않음",
        "sectioneditnotsupported-text": "이 문서에서는 문단 편집을 지원하지 않습니다.",
        "modeleditnotsupported-title": "편집이 지원되지 않습니다",
+       "modeleditnotsupported-text": "편집은 콘텐츠 모델 $1에서 지원되지 않습니다.",
        "permissionserrors": "권한 오류",
        "permissionserrorstext": "해당 명령을 수행할 권한이 없습니다. 다음 {{PLURAL:$1|이유}}를 확인해보세요:",
        "permissionserrorstext-withaction": "$2 권한이 없습니다. 다음 {{PLURAL:$1|이유}}를 확인해주세요:",
        "content-model-css": "CSS",
        "content-json-empty-object": "빈 오브젝트",
        "content-json-empty-array": "빈 배열",
+       "unsupported-content-model": "<strong>경고:</strong> $1 콘텐츠 모델은 이 위키에서 지원되지 않습니다.",
+       "unsupported-content-diff": "차이 비교는 콘텐츠 모델 $1에서 지원되지 않습니다.",
+       "unsupported-content-diff2": "콘텐츠 모델 $1, $2 간의 차이 비교는 이 위키에서 지원되지 않습니다.",
        "deprecated-self-close-category": "유효하지 않은, 스스로 닫는 HTML 태그를 사용하고 있는 문서",
        "deprecated-self-close-category-desc": "이 문서는 <code>&lt;b/></code>나 <code>&lt;span/></code>와 같은 유효하지 않은, 스스로 닫는 HTML 태그를 포함하고 있습니다. 이 태그들의 동작은 곧 HTML5 사양과 일관되도록 변경될 예정이므로 위키텍스트에서 이것들을 사용하는 것은 권장되지 않습니다.",
        "duplicate-args-warning": "<strong>경고:</strong> [[:$1]] 문서는 [[:$2]]에 \"$3\" 변수를 하나보다 더 많이 입력했습니다. 마지막으로 주어진 값만이 유효합니다.",
        "right-editmyusercss": "자신의 사용자 CSS 파일 편집하기",
        "right-editmyuserjson": "자신의 사용자 JSON 파일 편집하기",
        "right-editmyuserjs": "자신의 사용자 자바스크립트 파일 편집하기",
+       "right-editmyuserjsredirect": "넘겨주기인 자신의 사용자 자바스크립트 파일 편집하기",
        "right-viewmywatchlist": "자신의 주시문서 목록 보기",
        "right-editmywatchlist": "자신의 주시문서 목록을 편집합니다. 이 권한이 없어도 문서를 추가할 수 있는 권한이 이외에도 있음을 참고하세요.",
        "right-viewmyprivateinfo": "자신의 개인정보 보기 (이메일 주소, 실명 등)",
        "action-editmyusercss": "자신의 사용자 CSS 파일 편집하기",
        "action-editmyuserjson": "자신의 사용자 JSON 파일 편집하기",
        "action-editmyuserjs": "자신의 사용자 자바스크립트 파일 편집하기",
+       "action-editmyuserjsredirect": "넘겨주기인 자신의 사용자 자바스크립트 파일 편집하기",
        "action-viewsuppressed": "어떤 사용자도 보지 못하도록 감춰진 판 보기",
        "action-hideuser": "사용자 이름을 차단하고 감춤",
        "action-ipblock-exempt": "IP 차단, 자동 차단, 광역 차단을 무시",
        "backend-fail-batchsize": "저장 백엔드에서 파일 {{PLURAL:$1|작업}} $1개가 쌓였습니다. 한계는 {{PLURAL:$2|작업}} $2개입니다.",
        "backend-fail-usable": "파일 읽기/쓰기 권한이 없거나 저장 위치가 빠졌기 때문에 \"$1\" 파일을 읽거나 쓸 수 없습니다.",
        "backend-fail-stat": "\"$1\" 파일의 상태를 읽지 못했습니다.",
-       "backend-fail-hash": "\"$1\" 파일의 암호화 해시를 결정하지 못했습니다.",
+       "backend-fail-hash": "\"$1\" 파일의 암호화 해시를 결정하지 못했습니다",
        "filejournal-fail-dbconnect": "저장소 백엔드 \"$1\"에 대한 저널 데이터베이스에 연결할 수 없습니다.",
        "filejournal-fail-dbquery": "저장소 백엔드 \"$1\"에 대한 저널 데이터베이스에서 새로 고칠 수 없습니다.",
        "lockmanager-notlocked": "\"$1\" 경로의 잠금을 풀 수 없습니다. 해당 경로는 잠겨 있지 않습니다.",
        "move-page-legend": "문서 이동",
        "movepagetext": "아래 양식을 채워 문서의 이름을 바꾸고 모든 역사를 새 이름으로 된 문서로 이동할 수 있습니다.\n원래의 문서는 새 문서로 넘겨주는 링크로만 남게 되고,\n원래 이름을 가리키는 넘겨주기는 자동으로 갱신됩니다.\n만약 이 설정을 선택하지 않았다면 [[Special:DoubleRedirects|이중 넘겨주기]]와 [[Special:BrokenRedirects|끊긴 넘겨주기]]를 확인해주세요.\n당신은 링크와 가리키는 대상이 서로 일치하도록 해야 할 책임이 있습니다.\n\n만약 이미 있는 문서의 이름을 새 이름으로 입력했을 때는 그 문서가 넘겨주기 문서이고 문서 역사가 없어야만 이동이 됩니다. 그렇지 않을 경우에는 이동되지 <strong>않습니다</strong>.\n이것은 실수로 이동한 문서를 되돌릴 수는 있지만, 이미 존재하는 문서 위에 덮어씌울 수는 없다는 것을 의미합니다.\n\n<strong>주의!</strong>\n자주 사용하는 문서를 이동하면 해결하기 어려운 문제를 일으킬 수도 있습니다.\n이동하기 전에 반드시 이 문서를 이동해도 문제가 없는지 확인해주세요.",
        "movepagetext-noredirectfixer": "아래 양식을 채워 문서의 이름을 바꾸고 모든 역사를 새 이름으로 된 문서로 이동할 수 있습니다.\n원래의 문서는 새 문서로 넘겨주는 링크로만 남게 됩니다.\n[[Special:DoubleRedirects|이중 넘겨주기]]와 [[Special:BrokenRedirects|끊긴 넘겨주기]]를 확인해주세요.\n당신은 링크와 가리키는 대상이 서로 일치하도록 해야 할 책임이 있습니다.\n\n만약 이미 있는 문서의 이름을 새 이름으로 입력했을 때는 그 문서가 넘겨주기 문서이고 문서 역사가 없어야만 이동이 됩니다. 그렇지 않을 경우에는 이동되지 <strong>않습니다</strong>.\n이것은 실수로 이동한 문서를 되돌릴 수는 있지만, 이미 존재하는 문서 위에 덮어씌울 수는 없다는 것을 의미합니다.\n\n<strong>주의!</strong>\n자주 사용하는 문서를 이동하면 해결하기 어려운 문제를 일으킬 수도 있습니다.\n이동하기 전에 반드시 이 문서를 이동해도 문제가 없는지 확인해주세요.",
-       "movepagetext-noredirectsupport": "아래 양식을 채워 문서의 이름을 바꾸고 모든 역사를 새 이름으로 된 문서로 이동할 수 있습니다.\n당신은 링크와 가리키는 대상이 서로 일치하도록 해야 할 책임이 있습니다.\n\n만약 이미 있는 문서의 제목을 새 제목으로 입력했을 때는 그 문서가 이동되지 <strong>않습니다</strong>.\n이것은 실수로 이동한 문서를 되돌릴 수는 있지만, 이미 존재하는 문서 위에 덮어씌울 수는 없다는 것을 의미합니다.\n\n<strong>주의:</strong>\n자주 사용하는 문서를 이동하면 해결하기 어려운 문제를 일으킬 수도 있습니다.\n이동하기 전에 반드시 이 문서를 이동해도 문제가 없는지 확인해주세요.",
+       "movepagetext-noredirectsupport": "아래 양식을 채워 문서의 이름을 바꾸고 모든 역사를 새 이름으로 된 문서로 이동할 수 있습니다.\n당신은 링크와 가리키는 대상이 서로 일치하도록 해야 할 책임이 있습니다.\n\n만약 이미 있는 문서의 제목을 새 제목으로 입력했을 때는 그 문서가 이동되지 <strong>않습니다</strong>.\n이는 실수로 이동한 문서를 되돌릴 수는 있지만, 이미 존재하는 문서 위에 덮어씌울 수는 없다는 것을 의미합니다.\n\n<strong>참고:</strong>\n자주 사용하는 문서를 이동하면 해결하기 어려운 문제를 일으킬 수도 있습니다.\n이동하기 전에 반드시 이 문서를 이동해도 문제가 없는지 확인해 주세요.",
        "movepagetalktext": "이 칸에 체크하면, 딸린 토론 문서가 자동으로 이동됩니다. 다만 비어있지 않은 토론 문서가 있다면 이동되지 않습니다.\n\n이러한 경우에는 수동으로 이동하거나 합쳐야 합니다.",
        "moveuserpage-warning": "<strong>경고:</strong> 사용자 문서를 이동하려고 하고 있습니다. 사용자 문서만 이동되며 사용자 이름이 바뀌지 <strong>않는다</strong>는 점을 참고하세요.",
        "movecategorypage-warning": "<strong>경고:</strong> 분류 문서를 이동하려고 합니다. 해당 문서만 이동되고 옛 분류에 있는 문서는 새 분류 안에 다시 분류되지 <em>않음</em>을 참고하세요.",
        "logentry-block-block": "$1님이 {{GENDER:$4|$3}}님을 $5 {{GENDER:$2|차단했습니다}} $6",
        "logentry-block-unblock": "$1님이 {{GENDER:$4|$3}}님의 {{GENDER:$2|차단을 해제했습니다}}",
        "logentry-block-reblock": "$1 님이 {{GENDER:$4|$3}} 님의 차단 기간을 $5(으)로 {{GENDER:$2|바꾸었습니다}} $6",
+       "logentry-partialblock-block-page": "{{PLURAL:$1|문서}} $2",
+       "logentry-partialblock-block-ns": "{{PLURAL:$1|이름공간}} $2",
        "logentry-partialblock-block": "$1님이 {{GENDER:$4|$3}}님을 $7 편집하지 못하도록 $5 {{GENDER:$2|차단}}했습니다. $6",
        "logentry-suppress-block": "$1님이 {{GENDER:$4|$3}} 사용자를 $5 {{GENDER:$2|차단했습니다}} $6",
        "logentry-suppress-reblock": "$1 님이 {{GENDER:$4|$3}} 님의 차단 기간을 $5(으)로 {{GENDER:$2|바꾸었습니다}} $6",
        "linkaccounts": "계정 연결",
        "linkaccounts-success-text": "계정이 연결되었습니다.",
        "linkaccounts-submit": "계정 연결",
+       "cannotunlink-no-provider-title": "연결을 해제할 계정이 없습니다",
+       "cannotunlink-no-provider": "연결을 해제할 계정이 없습니다.",
        "unlinkaccounts": "계정 연결 해제",
        "unlinkaccounts-success": "계정의 연결이 해제되었습니다.",
        "authenticationdatachange-ignored": "인증 데이터 변경을 처리하지 못했습니다. 제공자를 설정하지 않으셨습니까?",
        "passwordpolicies-policy-maximalpasswordlength": "비밀번호는 적어도 $1 {{PLURAL:$1|자}} 미만이어야 합니다",
        "passwordpolicies-policy-passwordcannotbepopular": "비밀번호는 {{PLURAL:$1|저명한 비밀번호가 될|$1개의 저명한 비밀번호에 속할}} 수 없습니다",
        "passwordpolicies-policy-passwordnotinlargeblacklist": "비밀번호는 가장 흔히 쓰이는 비밀번호 100,000개 목록에 속할 수 없습니다.",
+       "passwordpolicies-policyflag-forcechange": "로그인 시 변경 필요",
        "passwordpolicies-policyflag-suggestchangeonlogin": "로그인할 때 변경 제안",
        "easydeflate-invaliddeflate": "주어진 컨텐츠가 적절히 압축되지 않았습니다",
        "unprotected-js": "보안 상의 이유로 자바스크립트는 보호되지 않은 문서로부터 불러올 수 없습니다. 미디어위키: 이름공간이나 사용자의 하위 문서에서만 자바스크립트를 만들어 주십시오.",
index 373ba26..54cc55c 100644 (file)
        "tuesday": "سەشمە",
        "wednesday": "چار شأمأھ",
        "thursday": "پأشأمە",
-       "friday": "جÙ\88Ù\99Ù\85Ù\8e",
-       "saturday": "شأمە",
-       "sun": "یە شأمأھ",
-       "mon": "دوٙشأمأھ",
-       "tue": "سەشأمە",
-       "wed": "چارشأمأھ",
-       "thu": "پأشأمە",
-       "fri": "جÙ\88Ù\85Ù\8e",
+       "friday": "جÙ\85Ù\87",
+       "saturday": "شمبە",
+       "sun": "یە شمبد",
+       "mon": "دوشمبد",
+       "tue": "سەشمبد",
+       "wed": "چارشمبد",
+       "thu": "پیشمبد",
+       "fri": "جÙ\85Ù\8eÙ\87",
        "sat": "شأمأھ",
        "january": "أڤأل قأھارھ",
        "february": "لیریشگوٙن",
        "sep": "شینیاروٙن",
        "oct": "مالبارکوٙنوٙن",
        "nov": "ئا سأردکوٙنوٙن",
-       "dec": "ئا Ø±Û\8cجکÙ\86Ù\88Ù\99Ù\86",
+       "dec": "دساÙ\85بر",
        "january-date": "ژانویه $1",
        "february-date": "فوریه $1",
        "march-date": "مارس $1",
        "category-file-count-limited": "دومن الذکر {{PLURAL:$1|فایل هس|$1 فایلل هسن}} د او دسه جریانی.",
        "listingcontinuesabbrev": "دۉنبالە",
        "index-category": "بلگه یل ایندکس وابیده",
-       "noindex-category": "بلگه یل ایندکس نوابیده",
+       "noindex-category": "ولاگئل ایندکس نوابیده",
        "broken-file-category": "بلگه یل وا فایلل لینک اشکسه",
        "about": "درباره",
        "article": "بلگه محتوا",
        "protect_change": "تغییر بی",
        "unprotect": "تغییر دائن حالت حفاظت",
        "newpage": "بألگە نۉ",
-       "talkpagelinktext": "گأپ",
+       "talkpagelinktext": "گپ",
        "specialpage": "بلگه مخصوص",
        "personaltools": "ئوزارگل سی خۉتی",
        "talk": "قسە",
        "redirectedfrom": "(تصحیح مجدد زھ $1)",
        "redirectpagesub": "بلگه تصحیح و هدایت زه مجدد",
        "redirectto": "تأغییر دائن مأسیر ڤە:",
-       "lastmodifiedat": "ئÛ\8c Ø¨Ø£Ù\84Ú¯Û\95 Ø§Ø®Û\8cرا ØªØ£ØºÛ\8cÛ\8cر Ú¤ Ø¦Û\8cصÙ\84اح Ú¤Ø§Ø¨Û\8cÛ\95 Ù\85أئÙ\86Û\95 $1, Ù\85أئÙ\86Û\95 $2.",
+       "lastmodifiedat": "اÛ\8cÙ\86 Ù\88Ù\84اگ Ø¢Ø®Ø±Û\8cÙ\86â\80\8cبار Ù\85Ù\90Ù\86 $1 Ø³Ø§Ø¹Øª $2 Ø§ØµÙ\84اح Ù\88ابÛ\8cدÙ\87.",
        "viewcount": "ای بلگه قابل دسترسی وابیه {{PLURAL:$1|یه بار|$1 مدتل}}.",
        "protectedpage": "بلگه حفاظت وابیه",
        "jumpto": "پریدن ڤھ:",
        "pool-queuefull": "صف استخر پر هسی",
        "pool-errorunknown": "خطا ناشناخته",
        "pool-servererror": "شمارنده سرویس استخر ور تیه نی ($1).",
-       "aboutsite": "پۉرۉجھ : دأربارھ{{SITENAME}}",
+       "aboutsite": "پروژه : دربارهٔ‌{{SITENAME}}",
        "aboutpage": "Project:دأربارھ",
        "copyright": "مطلب دومن $ 1 هس نکه خلاف هونو ذکر وابی.",
        "copyrightpage": "{{ns:project}}:کۉپی رایت",
        "disclaimers": "ئینکار کنندھ یل",
        "disclaimerpage": "Project:ئینکار کارۉأران",
        "edithelp": "هوٙمیاری سی ئیصلاح",
-       "mainpage": "بألگە أصلی",
+       "mainpage": "ولاگ اصلی",
        "mainpage-description": "بألگە أصلی",
        "policy-url": "Project:خط مشی",
        "portal": "دأرگاھ کارڤأرل",
        "newmessageslinkplural": "{{PLURAL:$1|یه پیوم نو|999=پیومل نو}}",
        "newmessagesdifflinkplural": "آخر {{PLURAL:$1|تغییر|999=تغییرل}}",
        "youhavenewmessagesmulti": "ایشا پیوم نو داریت مئنه\n$1",
-       "editsection": "ئÛ\8cصلاح",
+       "editsection": "اصلاح",
        "editold": "ئیصلاح",
        "viewsourceold": "دیئن منبع",
        "editlink": "ئیصلاح",
        "viewsourcelink": "دیئن سأرچیشمە",
-       "editsectionhint": "ئÛ\8cصÙ\84اح Ø¦Û\8c Ø¨Ø£خش: $1",
+       "editsectionhint": "اصÙ\84اح Ø§Û\8c Ø¨خش: $1",
        "toc": "مۉحتڤا یل",
        "showtoc": "نمایش",
        "hidetoc": "قائم",
        "site-atom-feed": "خأھ ڤأر خۉ Atom سی $1",
        "page-rss-feed": "خبرخو RSS سی «$1»",
        "page-atom-feed": "خیڤأر Atom سی «$1»",
-       "red-link-title": "(بألگە ۉجوٙد نارھ) $1",
+       "red-link-title": "$1 (ولاگه نیسی)",
        "sort-descending": "مرتب سازی وا صعودی",
        "sort-ascending": "مرتب سازی وا صعودی",
        "nstab-main": "بألگە",
        "nstab-template": "ئۉلگوٙ",
        "nstab-help": "بلگه هومیاری",
        "nstab-category": "دسە",
-       "mainpage-nstab": "بألگە أصلی",
+       "mainpage-nstab": "ولاگ اصلی",
        "nosuchaction": "چنی دستوری موجود نی",
        "nosuchspecialpage": "چنو بلگه مخصوصی نی",
        "error": "خطا",
        "anoneditwarning": "<strong>هۉشدار:</strong> ئیشا نأڤایتە مئنە سیستم. ئای پی ئیشا سی عۉموٙم قابل رۉیأت هی أر ئیصلاحی بۉکۉنیت. أر ئیشا <strong>[$1 وروٙد]</strong> یا <strong>[$2 راس کیردأن یە حیسآۉ]</strong>, ئیصلاحل ئیشا بە حیسآۉ کارڤأری ئیشا ھشتە ئیڤان ڤا مۉنفأعیل حیسآڤل دیە..",
        "loginreqlink": "ئوٙییدن ڤە سیستم",
        "newarticletext": "ایشا یه لینک ۉھ یه بلگنه که هنی ۉجود نارنه دنبال کردیته.\nسی راس کردن ای بلگه،نوشتنه مئنه جعبه زیر شرۉع کنیت(بینیتۉ [$1 help page] سی اصلاعات اضافی).\nار ایشا ۉا خطا ایچه هیسیت، ری <strong>back</strong> button مرۉرگر ایشا کلیژ کیت.",
-       "noarticletext": "Ø£Ù\84اÙ\86 Ù\85أتÙ\86Û\8c Ù\85ئÙ\86Û\95 Ø¦Û\8c Ø¨Ø£Ù\84Ú¯Û\95 Ù\86Û\8c.\nئÛ\8cشا Ø¦Û\8cتأرÛ\8cد [[Special:Search/{{PAGENAME}}|جÛ\89ستأÙ\86 Ø³Û\8c Ø¹Û\89Ù\86ڤاÙ\86 Ø¦Û\8c Ø¨Ø£Ù\84Ú¯Û\95]] Ù\85ئÙ\86Û\95 Ø¨Ø£Ù\84Ú¯Û\95 Û\8cÙ\84Û\95 Ø¯Û\8cÛ\95.\n<span class=\"plainlinks\">[{{fullurl:{{#Ù\85أخصÙ\88Ù\99ص:Ù\86Û\8cÙ\85اÛ\8cÙ\84}}|بأÙ\84Ú¯Û\95={{FULLPAGENAMEE}}}} Ø¬Û\89ستأÙ\86 Ø³Û\8c Ù\86Û\8cÙ\85اÛ\8cÙ\84 Ù\85أربÙ\88Ù\99Ø·], Û\8cا [{{fullurl:{{FULLPAGENAME}}|ئÛ\8cÙ\82داÙ\85=ئÛ\8cصÙ\84اح}} Ø¦Û\8cصÙ\84اح Ú©Û\89 Ø¦Û\8c Ø¨Ø£Ù\84Ú¯Ù\86Û\95]</span>.",
+       "noarticletext": "اÛ\8c ØµÙ\81Ø­Ù\87 Ø§Û\8cسÙ\88 Ø¯Ø§Ø±Ø§Û\8c Ù\87Û\8cÚ\86 Ù\85تÙ\86Û\8c Ù\86Û\8cسÛ\8c.\nاÛ\8cشا Ø§Û\8cترÛ\8cد Ù\85Ù\90Ù\86 ØµÙ\81حئÙ\84 Ø¯Ù\8e [[Special:Search/{{PAGENAME}}| Ø¹Ù\86Ù\88اÙ\86 Ø§Û\8c ØµÙ\81Ø­Ù\87 Ù\86Ù\8eÙ\87 Ø¨Ø¬Ù\88رÛ\8cد]]Ø\8c\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} Ø³Û\8cاÙ\87Ù\87â\80\8cÛ\8cÙ\84Ù\87 Ù\85رتبطÙ\87 Ø¨Ø¬Ù\88رÛ\8cÛ\8cد]Ø\8c\nÛ\8cا [{{fullurl:{{FULLPAGENAME}}|action=edit}} Ø§Û\8c ØµÙ\81Ø­Ù\87 Ù\86Ù\87 Ø¨Ø¬Ù\88رÛ\8cÛ\8cد]</span>.",
        "noarticletext-nopermission": "د حال جاری مأتنی مئنە ئی بألگە نیسس.\nئیشا ئیتأرید [[Special:Search/{{PAGENAME}}|search for this page title]] مئنە بألگل دیە، یا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs]</span>، ڤألی ئیشا نیتأرید ئی بألگنە راس بۉکۉنیت.",
        "editing": "درحال اصلاح $1",
        "creating": "راس کردن $1",
        "currentrevisionlink": "آخرین ۉرژن",
        "cur": "فیعلی",
        "last": "قأبلی",
+       "histfirst": "قدیمی‌ترین",
+       "histlast": "جدیدترین",
        "rev-delundel": "قابلیأت تأغییر دائن",
        "history-title": "تاریخچه اصلاحل $1",
        "difference-title": "فرخ ۉا بین تجدید نطرل \"$1\"",
        "searchprofile-everything-tooltip": "جۉستأن سی مۉحتڤا(شامل بألگل گأپ)",
        "searchprofile-advanced-tooltip": "جۉستأن مأئنە هۉمدیرأنگل سفارشی",
        "search-result-size": "$1 ({{PLURAL:$2|1 کألمە|$2 کألمل}})",
-       "search-redirect": "(تأغÛ\8cÛ\8cر Ù\85أسÛ\8cر $1)",
+       "search-redirect": "(تغÛ\8cÛ\8cر Ø±Ù\87 Ù\88 $1)",
        "search-section": "(قیسمأت $1)",
        "search-suggest": "آیا منطۉر ایشا ای بی:$1",
        "searchall": "ھأمە",
        "rcshowhidemine": "$1 ئیصلاحل مۉ",
        "rcshowhidemine-show": "نشۉ دائن",
        "rcshowhidemine-hide": "قائم کیردأن",
-       "rclinks": "نیشۉ دائن ئاخأرین $1 تأغییر مئن $2 روٙز أخیر؛ $3",
+       "rclinks": "نشون داۮن آخرین $1 تغییرئل مِن $2 روز اخیر",
        "diff": "فأرخ",
        "hist": "گۉزاریش",
        "hide": "قائم کیردأن",
        "recentchangeslinked": "تأغییرل مأربوٙط",
        "recentchangeslinked-toolbox": "تأغییرل مأربوٙط",
        "recentchangeslinked-title": "تأغییرل مۉرتأبیط ڤا $1",
-       "recentchangeslinked-summary": "ئی بألگە خاص تأغییرل اخیر مأئنە بألگل لینک ڤابیدھ ڤە ئی بألگنە نیشۉ ادھ.\nبألگلی کە مأئنە [[Special:Watchlist|لیست پیگیری یل]] ئیشا هیسن بە شکل '''سیاھ''' نیشۉ دادھ ابۉن.",
+       "recentchangeslinked-summary": "نووم یه صفحه نَه وارۮ کنیت تا تغییرئل صفحئلی که و وو پیوند زده وابۮه  یا و وو پیوند گروتنه نَه بویینید. (سی سیل کردن اعضای یه رده، ورودی نَه و صورت {{ns:category}}:نووم رده وارد کنیت). تغییرئل من صفحئلی که من  [[Special:Watchlist|فهرست پی‌گیرییل ایشا]] هِسِن <strong>ضخیم</strong> نما ایجورِن.",
        "recentchangeslinked-page": "نۉم بألگە:",
        "recentchangeslinked-to": "نیشۉ دائن تأغییرل بألگلی کە ڤە بألگە دادھ بیە لینک دادھ شۉدنە بە جای",
        "upload": "بلم گیر کردن فایل",
        "filehist-comment": "توٙضیح",
        "imagelinks": "ئیستفادھ د فایل",
        "linkstoimage": "{{PLURAL:$1|صفحهٔ|صفحَلِ}} زِر و ای عکس پیوند دارہ :",
-       "nolinkstoimage": "بأÙ\84Ú¯Û\95 Û\8cÙ\84Û\8c Ú©Û\95 Ú¤Û\95 Ø¦Û\8c Ù\81اÛ\8cÙ\84 Ù\84Û\8cÙ\86Ú© Ø¯Ø§Ø¦Ù\86Û\95 Ù\86Û\8c.",
+       "nolinkstoimage": "اÛ\8c Ù¾Ø±Ù\88Ù\86دÙ\87 Ù\85Ù\90Ù\86 Ù\87Û\8cÚ\86 ØµÙ\81Ø­Ù\87â\80\8cاÛ\8c Ù\88 Ú©Ø§Ø± Ù\86رتÙ\87.",
        "sharedupload-desc-here": "ئی فایل ز $1 ئوٙمائە ڤ شاید د پۉرۉجە یل دیە مورد ئیستفادھ ڤابین.\nتوٙضیحتل ری [$2 بألگە تۉضیح فایل] دوٙمین نیشۉ ڤابیە .",
        "upload-disallowed-here": "ئیشا نیتأریت ئی فایلنە بینڤیسیت",
        "randompage": "بألگە بأختە کی",
        "mycontris": "سأھمیل",
        "month": "مئنھ ای ماھ (ۉ قبل زھ ھۉ):",
        "year": "مئنھ ای سال (ۉ قبل زھ ھۉ):",
+       "sp-contributions-submit": "جُستن",
        "whatlinkshere": "لینکل ئی بألگە",
        "whatlinkshere-title": "بألگل کە لینک دائنە ڤە \"$1\"",
        "whatlinkshere-page": "بألگە:",
        "whatlinkshere-prev": "{{PLURAL:$1|قأبلی |مۉرید قأبلی$1}}",
        "whatlinkshere-next": "{{PLURAL:$1|بأعدی |مۉرید بأعدی $1}}",
        "whatlinkshere-links": "← لینکل",
-       "whatlinkshere-hideredirs": "$1 ØªØ£ØºÛ\8cÛ\8cرÙ\84 Ù\85Ø£سیر",
-       "whatlinkshere-hidetrans": "$1 ØªØ£Ø±Ø§Ú¯Û\89Ù\86جاÛ\8cÛ\8cØ´",
-       "whatlinkshere-hidelinks": "$1 لینکل",
+       "whatlinkshere-hideredirs": "$1 ØªØºÛ\8cÛ\8cر Ù\85سیر",
+       "whatlinkshere-hidetrans": "$1 Ø§Ø³ØªÙ\81ادÙ\87 Ù\88ابÛ\8cدÙ\87 Ù\85Ù\86 Ù\88Ù\84اگ",
+       "whatlinkshere-hidelinks": "$1 لینکئل",
        "whatlinkshere-filters": "فیلتیرل",
        "blocklink": "بسە بۉھ",
        "contribslink": "شۉراکأتل",
        "movelogpage": "نمایه جابجایی",
        "export": "بألگل صادرھ",
        "thumbnail-more": "گأپ کردن",
-       "tooltip-pt-userpage": "حیسآۉ کارڤأری ئیشا",
-       "tooltip-pt-mytalk": "بألگە گأپ ئیشا",
-       "tooltip-pt-preferences": "ئÛ\89Ù\84Ø£Ú¤Û\8cأتÙ\84 Ù\85Û\89",
+       "tooltip-pt-userpage": "ولاگ {{GENDER:|کاربری ایشا}}",
+       "tooltip-pt-mytalk": "ولاگ گپ {{GENDER:|ایشا}}",
+       "tooltip-pt-preferences": "ترجÛ\8cحئÙ\84 {{GENDER:|اÛ\8cشا}}",
        "tooltip-pt-watchlist": "لیست بألگلی کە ئیشا تأغییرل هۉنۉنە  دۉنبال ئیکۉنین",
        "tooltip-pt-mycontris": "لیست سأھمیل ئیشا",
        "tooltip-pt-login": "توٙصیە ڤابوٙھ کە ڤە سیستم داخل بوٙین. أما ئیجباری نیسس",
        "tooltip-t-whatlinkshere": "فهرست همە بألگە یل ڤیکی کە ئیچوٙ لینک دارن",
        "tooltip-t-recentchangeslinked": "تأغییرل آخر مئن بألگە کە لینک دانە ڤە ئی بألگە",
        "tooltip-feed-atom": "تأغذیە کچک تأرین جۉزء  ئی بألگە",
-       "tooltip-t-contributions": "یە لیست ز مۉشاریکأت کۉنأندھ یل ڤ مأقالە دهأندھ یل ئی بألگە",
+       "tooltip-t-contributions": "لیست مشارکتئل توسط {{GENDER:$1|این کاربر}}",
        "tooltip-t-upload": "بلم گیر کردن فایلل",
        "tooltip-t-specialpages": "بألگە یل ڤیجە",
        "tooltip-t-print": "ویرژن سی چاپ ئی بألگە",
        "tooltip-t-permalink": "لینکل دائمی ڤە ئی ۉیرژن ئی بألگە",
        "tooltip-ca-nstab-main": "دیئن بألگە مۉحتڤا",
        "tooltip-ca-nstab-user": "دیئن بألگە کارڤأر",
-       "tooltip-ca-nstab-special": "ئی بألگە ھا ڤیجە ڤ ئیشا نیتأرین خۉد ئی بألگنە ئیصلاح کنیت",
+       "tooltip-ca-nstab-special": "یو یه صفحِیْ ویژه ییَ، و قابل اصلاح نیسی",
        "tooltip-ca-nstab-project": "دیئن بلگه پرۉجه",
        "tooltip-ca-nstab-image": "دیئن بألگە فایل",
        "tooltip-ca-nstab-template": "دیئن قالیب",
        "tooltip-rollback": "\"اعادە\" ۉرگأردوٙندأن بە ڤأضع أڤألیە سی ئی بألگە کە سی مۉشارکأت  ئاخر ئیصلاح ڤابیدھ ڤا یە کلیک",
        "tooltip-undo": "\"لأغڤ\" ڤۉرگأشت ئی ئیصلاح ڤ ڤا ڤیدن فۉرم ئیصلاح مئنە پیش نیمایش.ئیجازھ ئیدھ کە یە دألیل ڤە خۉلاصە ئیضافە بۉکۉنی.",
        "tooltip-summary": "یە خۉلاصە کچکی بینڤیسیت",
-       "simpleantispam-label": "ئÛ\8cÙ\86تÛ\8cخاب Ø¦Ø§Ù\86تÛ\8c-ئÛ\8cسپÛ\8cÙ\85\nÙ¾Û\89ر <strong>Ù\86Ø£Ú©Û\89Ù\86Û\8cت</strong> Ø¦Û\8cÙ\86Û\95 Ù\85ئÙ\86!",
+       "simpleantispam-label": "بررسÛ\8c Ø¶Ø¯ Ù\87رزÙ\86گارÛ\8c.\nاÛ\8c Ù\82سÙ\85تÙ\87 Ù¾Ø± <strong>Ù\86Ú©Ù\86Û\8cت</strong>!",
        "pageinfo-toolboxlink": "اطلاعات بألگە",
+       "pageinfo-contentpage-yes": "ها",
        "previousdiff": "← اصلاح قدیمی",
        "nextdiff": "اصلاح نۉتر→",
        "file-info-size": "$1 × $2 پیکسل, اندازھ فایل: $3, MIME نۉع: $4",
        "logentry-move-move": "$1 {{GENDER:$2|انتقال دادھ بیه}} بلگه $3 ۉھ $4",
        "logentry-newusers-create": "حسآۉ کارڤأر $1 ڤابیە {{GENDER:$2|راس ڤیدھ }}",
        "logentry-upload-upload": "$1 {{GENDER:$2|بلم گیر کردھ ۉابی}} $3",
-       "searchsuggest-search": "جۉستأن",
+       "searchsuggest-search": "جُستَنْ",
        "specialmute": "بی‌صدا",
        "userlogout-continue": "ایخیت برِیِتو وَدَر"
 }
index c56b4df..dec927a 100644 (file)
        "editundo": "weaderümmedraien",
        "diff-empty": "(Gien verschil)",
        "diff-multi-sameuser": "({{PLURAL:$1|n Tussenliggende versie|$1 tussenliggende versies}} deur de zelfde gebruker is verbörgen)",
+       "diff-multi-otherusers": "({{PLURAL:$1|Eyn tüskenliggende versy|$1 tüskenliggende versys}} döär {{PLURAL:$2|eyn andere bruker|$2 brukers}} neet weadergeaven)",
        "diff-multi-manyusers": "($1 tussenliggende {{PLURAL:$1|versie|versies}} deur meer as $2 {{PLURAL:$2|gebruker|gebrukers}} niet weeregeven)",
        "difference-missing-revision": "{{PLURAL:$2|Eén versie|$2 versies}} van disse verschillen ($1) {{PLURAL:$2|is|bin}} niet evunnen.\n\nDit kömp meestentieds deur t volgen van n verouwerde verwiezing naor n zied die vortedaon is.\nWaorschienlik ku'j der meer gegevens over vienen in t [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} vortdologboek].",
        "searchresults": "Söökresultaten",
        "search-result-category-size": "{{PLURAL:$1|1 kategorielid|$1 kategorielejen}} ({{PLURAL:$2|1 onderkategorie|$2 onderkategorieën}}, {{PLURAL:$3|1 bestaand|$3 bestaanden}})",
        "search-redirect": "(deurverwiezing vanaof $1)",
        "search-section": "(onderwarp $1)",
+       "search-file-match": "(kümt oavereyne mid de inhold van et bestand)",
        "search-suggest": "Bedoelden je: $1",
        "search-interwiki-caption": "Zusterprojekten",
        "search-interwiki-default": "Resultaoten van $1:",
        "filehist-comment": "Kommentaar",
        "imagelinks": "Bestandsbruuk",
        "linkstoimage": "Dit bestand wördt up de volgende {{PLURAL:$1|syde|$1 syden}} bruked:",
-       "linkstoimage-more": "Der {{PLURAL:$2|is|bin}} meer as $1 {{PLURAL:$1|verwiezing|verwiezingen}} naor dit bestaand.\nDe volgende lieste gif allinnig de eerste {{PLURAL:$1|verwiezing|$1 verwiezingen}} naor dit bestaand weer.\nDe [[Special:WhatLinksHere/$2|hele lieste]] is oek beschikbaor.",
+       "linkstoimage-more": "Meyr as $1 {{PLURAL:$1|syde bruukt|syden bruken}} dit bestand.\nDe volgende lyste givt allinnig de {{PLURAL:$1|eyrste syde|eyrste $1 syden}} weader dee dit bestand bruukt.\nDe [[Special:WhatLinksHere/$2|heyle lyste]] is ouk beskikbår.",
        "nolinkstoimage": "Geen enkelde syde gebrüükt disse holder.",
        "morelinkstoimage": "[[Special:WhatLinksHere/$1|Meer verwiezingen]] naor dit bestaand bekieken.",
        "linkstoimage-redirect": "$1 (bestaandsdeurverwiezing) $2",
        "unwatchthispage": "Niet volgen",
        "notanarticle": "Gien artikel",
        "notvisiblerev": "Bewarking is vortedaon",
-       "watchlist-details": "Der {{PLURAL:$1|steet één zied|staon $1 ziejen}} op joew volglieste, zonder de overlegziejen mee-erekend.",
+       "watchlist-details": "Der {{PLURAL:$1|steyt eyn syde|stån $1 syden}} up juw volglyste (plus oaverlegsyden).",
        "wlheader-enotif": "Je kriegen bericht per netpost",
        "wlheader-showupdated": "Ziejen die sinds joew leste bezeuk bie-ewörken bin staon '''vet'''.",
-       "wlnote": "Hieronder {{PLURAL:$1|steet de leste wieziging|staon de leste $1 wiezigingen}} in {{PLURAL:$2|t aofgeleupen ure|de leste $2 uren}} vanaof $3 um $4.",
+       "wlnote": "Hyrunder {{PLURAL:$1|steyt de lätste wysiging|stån de lätste <strong>$1</strong> wysigingen}} in {{PLURAL:$2|et vöärbye ure|de vöärbye $2 uren}} vanaf $3 üm $4.",
        "watchlist-submit": "Bekiek",
        "wlshowhideminor": "kleine bewarkingen",
        "watchlist-options": "Opsies veur de volglieste",
        "version-entrypoints-header-entrypoint": "Ingang",
        "version-entrypoints-header-url": "Webadres",
        "redirect": "Deurverwiezen op bestaandsnaam, gebrukers-, zied-, versie- of logboekregelnummer",
-       "redirect-summary": "Disse spesiale zied verwis deur naor n bestaand (as de bestaandsnaam op-egeven wördt), n zied (as n zied- of versienummer op-egeven wördt) of n gebrukerszied (as t gebrukersnummer op-egeven wördt). Gebruuk: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], of [[{{#Special:Redirect}}/user/101]].",
+       "redirect-summary": "Disse speciale syde verwist döär nå een bestand (as de bestandsname upgeaven wördt), een syde (as een syd- of versynummer upgeaven wördt), een brukerssyde (as et brukersnummer upgeaven wördt) of een logbookinskryving (as een logbooknummer upgeaven wördt). Gebruuk: [[{{#Special:Redirect}}/file/Vöärbeald.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]] of [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Zeuk",
        "redirect-lookup": "Opzeuken:",
        "redirect-value": "Weerde:",
index 8b9debd..75e75f4 100644 (file)
        "invalidtitle-unknownnamespace": "ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߓߊߟߌ ߞߊ߬ ߓߍ߲߬ ߕߐ߯ߛߓߍ ߞߣߍ߫ ߡߊߟߐ߲ߓߊߟߌ ߝߙߍߕߍ ߡߊ߬ $1 ߊ߬ ߣߌ߫ ߛߓߍߟߌ  \"$2\"",
        "exception-nologin": "ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߕߍ߫",
        "exception-nologin-text": "ߌ ߜߊ߲߬ߞߎ߲߫ ߖߊ߰ߣߌ߲߬߸ ߛߴߌ ߘߌ߫ ߛߋ߫ ߞߐߜߍ ߣߌ߲߬ ߡߊߛߐ߬ߘߐ߲߬ ߠߊ߫ ߥߟߊ߫ ߝߏ߲߬ߝߏ߲.",
+       "virus-scanfailed": "ߕߎ߬ߡߊ߬ߢߐ߲߰ߦߊ ߓߘߊ߫ ߗߌߙߏ߲߫ (ߘߏߝߙߍߕߍ $1)",
        "virus-unknownscanner": "ߢߐߛߌߙߋ߲ߞߟߊ߬ ߡߊߟߐ߲ߓߊߟߌ",
        "logouttext": "<strong>ߌ ߜߊ߲߬ߞߎ߲߬ߓߐ߬ߣߍ߲߬ ߕߍ߫.</strong>\n\nߞߐߜߍ ߘߏ߫ ߟߎ߫ ߕߘߍ߬ ߘߌ߫ ߞߍ߫ ߓߊ߯ߙߊ߫ ߟߊ߫ ߞߵߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲ ߕߏ߫߸ ߝߏ߫ ߣߴߌ ߞߵߌ ߟߊ߫ ߛߏ߲߯ߓߊߟߊ߲ ߢߡߊߘߏ߲߰ߣߍ߲ ߠߎ߬ ߖߏ߬ߛߌ߬.",
        "logging-out-notify": "ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲ ߓߐ ߦߴߌ ߘߐ߫߸ ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬.",
        "noemailcreate": "ߌ ߞߊߞߊ߲߫ ߞߊ߬ ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ߫ ߞߟߊ߬ߟߊ߬ߡߊ ߘߏ߫ ߡߊߛߐ߫.",
        "passwordsent": "ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߡߍ߲ ߦߋ߫ \"$1\" ߟߊ߫߸ ߕߊ߬ߡߌ߲߬ߞߊ߲߬ ߞߎߘߊ߫ ߓߘߊ߫ ߗߋ߫ ߏ߬ ߡߊ߬. ߖߊ߰ߣߌ߲߬ ߣߴߌ ߞߵߊ߬ ߡߊߛߐ߬ߘߐ߲߬ ߌ ߦߴߌ ߜߊ߲߬ߞߎ߲߬ ߕߎ߲߯.",
        "blocked-mailpassword": "ߌ ߟߊ߫ IP ߓߘߊ߫ ߓߊ߬ߟߊ߲߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߏ ߘߐ߫. ߖߐ߲߬ߛߊ߫ ߞߊ߬ ߘߊ߲߬ߠߊ߬ߕߊߡߌ߲ ߢߍߓߍ߲߬߸ \nߕߊ߬ߡߌ߲߬ߞߊ߲߬ ߡߊ߬ߛߐߘߐ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߍ߫ IP ߛߊ߲߬ߓߊ߬ߕߐ߮ ߣߌ߲߬ ߠߊ߫.",
+       "eauthentsent": "ߟߊ߬ߛߙߋ߬ߦߊ߬ߟߌ߬ ߗߋߛߓߍ ߓߘߊ߫ ߗߋ߫ ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ߫ ߛߊ߲߬ߓߊ߬ߕߐ߰ ߞߙߍߞߙߍߣߍ߲ ߡߊ߬.\nߦߊ߲߬ߣߌ߫ ߗߋߛߓߍ߫ ߜߘߍ ߛߎ߯_ߎ߯_ߛߎ߫ ߗߋ߫ ߕߍ߫ ߖߊ߬ߕߋ߬ߘߊ ߡߊ߬߸ ߌ ߞߊߞߊ߲߫ ߞߊ߬  ߢߍߡߌߘߊߟߌ ߟߊߓߊ߬ߕߏ߬ ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߘߐ߫߸ ߞߵߊ߬ ߟߊߛߙߋߦߊ߫ ߞߏ߫ ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߦߴߌ ߕߊ ߟߋ߬ ߘߌ߫.",
+       "throttled-mailpassword": "ߕߊ߬ߡߌ߲߬ߞߊ߲ ߡߊߝߊ߬ߟߋ߲߬ ߗߋߛߓߍ ߓߘߊ߫ ߓߊ߲߫ ߗߋ߫ ߟߊ߫ {{PLURAL:$1|ߕߎ߬ߡߊ߬ߙߋ߲|ߕߎ߬ߡߊ߬ߙߋ߲ $1 ߠߎ߬}} ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߬ ߞߘߐ߫.\nߞߵߊ߬ ߟߊߥߟߌ߬ ߘߊ߲߬ߠߊ߬ߕߊߡߌ߲ ߢߍߓߍ߲߭ ߡߊ߬߸ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߡߊߝߊ߬ߟߋ߲߬ߠߌ߲߬ ߞߋߟߋ߲߫ ߠߋ߬ ߗߋ߫ ߟߊ߫ {{PLURAL:$1|ߕߎ߬ߡߊ߬ߙߋ߲|ߕߎ߬ߡߊ߬ߙߋ߲ $1 ߠߎ߬}} ߞߘߐ߫.",
        "mailerror": "ߢߎߡߍߙߋ߲ ߗߋߟߌ ߝߎ߬ߕߎ߲߬ߕߌ:$1",
        "acct_creation_throttle_hit": "ߥߞߌ ߣߌ߲߬ ߓߐߒߡߟߊ ߟߎ߬ ߦߴߌ ߟߊ߫ IP ߛߊ߲߬ߓߊ߬ߕߐ߮ ߟߊߓߊ߯ߙߊ߫ ߟߊ߫߸ ߊ߬ߟߎ߬ ߓߘߊ߫ ߖߊ߬ߕߋ߬ߘߊ߬ {{PLURAL:$1|ߖߊ߬ߕߋ߬ߘߊ߬ ߁|$1ߖߊ߬ߕߋ߬ߘߊ ߟߎ߬}} ߛߌ߲ߘߌ߫߸ ߞߐ߯ߟߕߊ $2,ߟߋ߬ ߦߴߊ߬ ߞߐߘߊ߲߫ ߠߊߘߤߊ߬ߣߍ߲ ߘߌ߫ ߥߊ߯ߕߌ ߣߌ߲߬ ߠߊ. ߞߐߖߋߓߌ ߘߐ߫߸ ߓߐߒߡߟߊ ߟߎ߬ ߦߋ߫ IP ߛߊ߲߬ߓߊ߬ߕߐ߮ ߣߌ߲߬ ߠߋ ߟߊߓߊ߯ߙߊ߫ ߟߊ߫ ߕߊ߲߬߸ ߏ߬ ߘߏ߲߬ ߕߴߛߋ߫ ߖߊ߬ߕߋ߬ߘߊ߬ ߜߘߍ߫ ߛߌ߲ߘߌ߫ ߟߊ߫ ߥߊ߯ߕߌ ߣߌ߲߬ ߠߴߏ߬ ߞߐ߫.",
        "emailauthenticated": "ߌ ߟߊ߫ ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߟߊߛߙߋߦߊ߫ ߘߊ߫ $3 $2 ߟߊ߫",
        "emailnotauthenticated": "ߌ ߟߊ߫ ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߡߊ߫ ߟߊߛߙߋߦߊ߫ ߡߎߣߎ߲߬.\nߢߎߡߍߙߋ߲߫ ߕߍ߫ ߛߋ߫ ߗߋ߫ ߟߴߌ ߡߊ߬ ߘߊߞߎ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬.",
+       "noemailprefs": "ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߘߏ߫ ߡߊߕߍ߰ ߌ ߟߊ߫ ߦߟߌߡߊߛߙߋ ߘߐ߫ ߛߊ߫ ߓߊ߯ߙߢߊ ߣߌ߲߬ ߘߌ߫ ߓߊ߯ߙߊ߫.",
        "emailconfirmlink": "ߌ ߟߊ߫ ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߟߊߛߙߋߦߊ߫.",
        "invalidemailaddress": "ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߣߌ߲߬ ߕߍߣߊ߬ ߛߋ߫ ߟߊ߫ ߟߊߡߌ߬ߣߊ߬ ߟߊ߫߸ ߓߴߊ߬ ߦߋ߫ ߣߍ߲߫ ߦߋ߫ ߖߙߎߡߎ߲߫ ߓߍ߲߬ߓߊߟߌ ߟߋ߬ ߘߌ߫.\nߖߊ߰ߣߌ߲߬ ߌ ߦߋ߫ ߛߊ߲߬ߓߊ߬ߕߐ߮ ߖߙߎߡߎ߲߫ ߓߍ߲߬ߣߍ߲ ߘߏ߫ ߟߊߘߏ߲߬߸ ߥߟߊ߫ ߘߐ߬ߞߏߟߏ߲ ߡߍ߲ ߗߌߙߏ߲߫ ߣߍ߲߫.",
        "cannotchangeemail": "ߖߊ߬ߕߋ߬ߘߊ ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߕߴߛߋ߫ ߡߊߝߊ߬ߟߋ߲߬ ߠߊ߫ ߥߞߌ ߣߌ߲߬ ߘߐ߫.",
        "emaildisabled": "ߞߍߦߙߐ ߣߌ߲߬ ߕߍߣߊ߬ ߛߋ߫ ߟߊ߫ ߢߎߡߍߙߋ߲߫ ߗߋ߫ ߟߊ߫.",
        "accountcreated": "ߖߊ߬ߕߋ߬ߘߊ ߓߘߊ߫ ߛߌ߲ߘߌ߫",
+       "accountcreatedtext": "[[{{ns:User}}:$1|$1]] ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ ([[{{ns:User talk}}:$1|talk]]) ߓߘߊ߫ ߓߊ߲߫ ߛߌ߲ߘߌ߫ ߟߊ߫.",
        "createaccount-title": "ߖߊ߬ߕߋ߬ߘߊ ߓߘߊ߫ ߟߊߞߊ߬  {{SITENAME}} ߢߍ߫.",
        "createaccount-text": "ߡߐ߱ ߘߏ߫ ߓߘߊ߫ ߖߊ߬ߕߋ߬ߘߊ ߘߏ߫ ߛߌ߲ߘߴߌ ߟߊ߫ ߢߎߡߍߙߋ߲߫ ߞߏ߲ߘߏ ߣߌ߲߬ ߡߊ߬ ($4) ߡߍ߲ ߕߐ߯ߟߊ߫ ߣߍ߲߫ ߞߏ߫ \"$2\"߸ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߣߌ߲߬ ߠߊ߫  \"$3\". ߌ ߦߴߌ ߜߊ߲߬ߞߎ߲߬ ߞߵߌ ߟߊ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߡߊߝߊ߬ߟߋ߲߬ ߌߞߘߐ߫߹\n\nߌ ߦߋ߫ ߗߋߛߓߍߊ ߡߊߓߌ߬ߟߊ߬߸ ߣߌ߫ ߖߊ߬ߕߋ߬ߘߊ ߟߊߞߊ߬ߣߍ߲߬ ߞߍ߫ ߘߊ߫ ߝߎ߬ߕߎ߲߬ߕߌ߬ ߓߟߏߡߊ߬",
+       "login-throttled": "ߌ ߓߘߴߌ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߡߊߛߊ߬ߦߌ߫ ߛߋ߲߬ߧߊ߬ ߛߌߦߊߡߊ߲߫ ߞߏߖߎ߰.\nߌ ߘߐߟߐ߬ $1 ߞߘߐ߫ ߦߊ߬ߣߴߌ ߦߴߊ߬ ߡߊߝߍߣߍ߲߫ ߕߎ߲߯.",
        "login-abort-generic": "ߌ ߟߊ߫ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫ - ߕߌߢߍ߫",
        "login-migrated-generic": "ߌ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ ߓߘߊ߫ ߝߎ߲ߘߌ߫߸ ߊ߬ ߣߴߌ ߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮ ߕߍ߫ ߣߊ߬ ߥߊ߯ߕߌߖߊ߲߫ ߞߍ߫ ߟߊ߫ ߥߞߌ ߣߌ߲߬ ߠߊ߫ ߦߊ߲߬ ߏ߬ ߞߐ߫.",
        "loginlanguagelabel": "ߞߊ߲ $1",
+       "suspicious-userlogout": "ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲ ߓߐ߫ ߞߏ ߡߊ߬ߢߌ߬ߣߌ߲߬ߠߌ߲ ߟߊߕߐ߲ߣߍ߲߫ ߠߋ߬ ߓߊߏ߬ ߊ߬ ߞߍߣߍ߲߫ ߦߏ߫ ߊ߬ ߗߋ߫ ߣߍ߲߫ ߦߋ߫ ߛߏ߲߯ߓߊߟߊ߲߫ ߖߐ߲ߝߐ߲ߣߍ߲ ߠߋ߬ ߓߟߏ߫ ߥߟߊ߫ ߟߐ߲߬ߞߋ߬ߟߊ ߡߊߛߐߟߊ߫ (ߔߑߙߏ߬ߞߛߌ) ߢߡߊߘߏ߲߰ߣߍ߲.",
        "pt-login": "ߌ ߜߊ߲߬ߞߎ߲߬",
        "pt-login-button": "ߌ ߜߊ߲߬ߞߎ߲߬",
        "pt-login-continue-button": "ߌ ߟߊ߫ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߘߊߓߊ߲߫",
        "action-minoredit": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߌ߲߬ ߣߐ߬ߣߐ߬ ߢߟߋߢߟߋ ߘߌ߫",
        "action-move": "ߞߐߜߍ ߣߌ߲߬ ߛߋ߲߬ߓߐ߫",
        "action-move-subpages": "ߞߐߜߍ ߣߌ߲߬ ߛߋ߲߬ߓߐ߫߸ ߊ߬ ߣߴߊ߬ ߞߐߜߍߙߋ߲ ߠߎ߬",
+       "action-move-rootuserpages": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ߫ ߟߌ߯ߟߌ߲ߖߌ߰ߣߍ߲ ߞߐߜߍ ߛߋ߲߬ߓߐ߫",
        "action-move-categorypages": "ߦߌߟߡߊ߫ ߞߐߜߍ ߟߎ߬ ߛߋ߲߬ߓߐ߫",
        "action-movefile": "ߞߐߕߐ߮ ߣߌ߲߬ ߛߋ߲߬ߓߐ߫",
        "action-upload": "ߞߐߕߐ߮ ߣߌ߲߬ ߠߊߦߟߍ߬",
        "action-reupload": "ߞߐߕߐ߯ ߓߍߓߊ߮ ߣߌ߲߬ ߥߦߊ߬",
+       "action-reupload-shared": "ߞߐߕߐ߮ ߣߌ߲߬ ߖߏ߰ߛߌ߫ ߟߊߖߍ߲߬ߛߍ߲߬ߣߍ߲ ߠߎ߬ ߟߊߡߊ߲߬ߘߌ߬ ߦߙߐ.",
        "action-upload_by_url": "ߞߐߕߐ߮ ߣߌ߲߬ ߠߊߦߟߍ߬ ߞߊ߬ ߓߐ߫ URL ߘߐ߫",
        "action-writeapi": "ߛߓߍߟߌ API ߟߊߓߊ߯ߙߊ߫",
        "action-delete": "ߞߐߜߍ ߣߌ߲߬ ߖߏ߰ߛߌ߬",
        "action-deleterevision": "ߟߢߊ߬ߟߌ ߟߎ߬ ߖߏ߬ߛߌ߬",
        "action-deletedhistory": "ߞߐߜߍ ߟߎ߬ ߖߏ߰ߛߌ߬ߟߌ ߘߐ߬ߝߐ ߦߋ߫",
+       "action-deletedtext": "ߟߢߊ߬ߟߌ ߖߏ߰ߛߌ߬ߣߍ߲ ߠߎ߬ ߛߓߍߟߌ ߦߋ߫.",
        "action-browsearchive": "ߞߐߜߍ߬ ߖߏ߰ߛߌ߬ߣߍ߲ ߠߎ߬ ߢߌߣߌ߲߫",
        "action-undelete": "ߞߐߜߍ ߖߏ߰ߛߌ߬ߓߊߟߌ ߟߎ߬",
        "action-suppressionlog": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߘߎ߲ߛߓߍ ߣߌ߲߬ ߦߋ߫",
        "upload-description": "ߞߐߕߐ߮ ߞߊ߲߬ߛߓߍߟߌ",
        "upload-options": "ߟߊ߬ߦߟߍ߬ߟߌ ߢߣߊߕߊߟߌ",
        "watchthisupload": "ߞߐߕߐ߮ ߣߌ߲߬ ߘߐߜߍ߫",
+       "upload-proto-error": "ߡߛߍ߬ߞߍ߬ߡߛߍߞߍ ߓߍ߲߬ߓߊߟߌ",
        "upload-file-error": "ߞߣߐߟߊߘߐ߫ ߝߎߕߎ߲ߕߌ",
        "upload-misc-error": "ߟߊ߬ߦߟߍ߬ߟߌ ߝߎ߬ߕߎ߲߬ߕߌ߬ ߡߊߟߐ߲ߓߊߟߌ",
        "upload-misc-error-text": "ߝߎ߬ߕߎ߲߬ߕߌ߬ ߡߊߟߐ߲ߓߊߟߌ ߘߏ߫ ߓߘߊ߫ ߓߌ߬ߟߵߊ߬ ߘߐ߫ ߟߊ߬ߦߟߍ߬ߟߌ ߞߎ߲߬ߕߊ߮ ߞߘߐ߫. ߊ߬ ߝߛߍ߬ߝߛߍ߬ߟߌ ߞߍ߫ ߞߵߊ߬ ߟߐ߲߫ ߣߌ߫ URL ߓߍ߲߬ߣߍ߲߬ ߊ߬ ߣߴߊ߬ ߡߊߛߐ߬ߘߐ߲߬ߕߊ ߦߋ߫ ߞߣߊ߬ ߕߴߊ߬ ߡߊߝߍߣߍ߲߫ ߣߴߏ߬ ߞߐ߫.\nߣߌ߫ ߝߙߋߞߋ ߘߏ߲߬ ߠߊߝߛߊ߬ ߘߊ߫߸ ߓߌ߬ߟߊ߬ߢߐ߲߰ߡߊ ߢߌߣߌ߲߫ [[Special:ListUsers/sysop|administrator]] ߝߍ߬.",
        "editcomment": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߟߊ߬ߘߛߏ߬ߟߌ ߕߘߍ߬ ߦߋ߫: <em>$1</em>",
        "changecontentmodel": "ߞߐߜߍ ߣߌ߲߬ ߞߣߐߘߐ ߛߎ߮ߦߊ ߡߊߝߊ߬ߟߋ߲߬",
        "changecontentmodel-legend": "ߞߣߐߘߐ ߛߎ߯ߦߊ ߡߊߝߊ߬ߟߋ߲߬",
-       "changecontentmodel-title-label": "ߞߐߜߍ ߞߎ߲߬ߕߐ߮",
+       "changecontentmodel-title-label": "ߞߐߜߍ ߞߎ߲߬ߕߐ߮ ߟߎ߬:",
        "changecontentmodel-current-label": "ߕߋ߲߭ߕߋ߲߭ ߞߣߐߘߐ ߛߎ߯ߦߊ:",
-       "changecontentmodel-model-label": "ߞߣߐߘߐ߫ ߛߎ߯ߦߊ߫ ߞߎߘߊ",
+       "changecontentmodel-model-label": "ߞߣߐߘߐ߫ ߛߎ߯ߦߊ߫ ߞߎߘߊ:",
        "changecontentmodel-reason-label": "ߊ߬ ߛߊߓߎ:",
        "changecontentmodel-submit": "ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
        "changecontentmodel-success-title": "ߞߣߐߘߐ ߛߎ߯ߦߊ ߓߘߊ߫ ߡߊߦߟߍ߬ߡߊ߲߫",
        "immobile-target-namespace": "ߞߐߜߍ ߕߍ߫ ߛߐ߲߬ ߟߊߕߊ߯ ߟߊ߫ ߕߐ߯ߛߓߍ ߞߣߍ \"$1\" ߘߐ߫.",
        "immobile-target-namespace-iw": "ߥߞߌߣߌߢߐ߲߯ߕߍ ߛߘߌ߬ߜߋ߲ ߕߍ߫ ߞߏ߲߰ ߓߍ߲߬ߣߍ߲߬ ߘߌ߫ ߞߊ߬ ߞߐߜߍ ߟߎ߬ ߛߋ߲߬ߓߐ߫.",
        "immobile-source-page": "ߞߐߜߍ ߕߍ߫ ߛߋ߲߬ߓߐ߬ߕߊ߫ ߘߌ߫.",
+       "immobile-target-page": "ߊ߬ ߕߴߛߋ߫ ߟߊߕߊ߯ ߟߊ߫ ߦߋ߲߬.",
+       "movepage-invalid-target-title": "ߕߐ߯ ߡߊߢߌߣߌ߲ߣߍ߲ ߓߍ߲߬ߣߍ߲߬ ߕߍ߫.",
        "imagenocrossnamespace": "ߞߐߕߐ߮ ߕߴߛߋ߫ ߟߥߊ߫ ߟߊ߫ ߕߐ߯ߛߓߍ ߞߣߍ ߕߍ߫ ߞߐߜߍ ߡߍ߲ ߠߎ߬ ߟߊ߫ ߘߐ߫.",
+       "nonfile-cannot-move-to-file": "ߞߐߕߐ߯ߦߊߓߊߟߌ ߕߴߛߋ߫ ߟߥߊ߫ ߟߊ߫ ߞߐߕߐ߮ ߟߎ߬ ߟߊ߫ ߕߐ߯ߛߓߍ߫ ߞߣߍ ߘߐ߫.",
+       "imagetypemismatch": "ߞߐߕߐ߯ ߞߎߘߊ ߘߐߥߙߊ߬ߟߌ ߡߊ߫ ߛߐ߲߬ ߟߊߓߏ߬ߙߌ߬ ߟߴߊ߬ ߛߎ߯ߦߊ ߞߊ߲߬.",
        "imageinvalidfilename": "ߞߐߕߐ߮ ߕߐ߮ ߞߏ߲߭ ߓߍ߲߬ߣߍ߲߬ ߕߍ߫.",
+       "fix-double-redirects": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߛߎ_ߎ߯_ߛߎ߫ ߞߎ߲߬ߛߌ߲߬ߣߍ߲߬ ߦߴߊ߬ ߞߎ߲߬ߕߐ߮ ߓߐߛߎ߲ ߡߊ߬߸ ߏ߬ ߟߎ߬ ߓߍ߯ ߟߏ߲ߘߐߦߊ߫",
+       "move-leave-redirect": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߟߊߝߟߌ߬ ߌ ߞߘߐ߫",
        "export": "ߞߐߜߍ ߟߎ߬ ߟߊߝߏ߬ߦߌ߬",
        "exportall": "ߞߐߜߍ ߓߍ߯ ߟߊߝߏ߬ߦߌ߬",
        "export-submit": "ߟߊ߬ߝߏ߬ߦߌ߬ߟߌ",
index bf58c17..150fa14 100644 (file)
        "backend-fail-contenttype": "Não foi possível determinar o tipo de conteúdo do arquivo para armazenar em \"$1\".",
        "backend-fail-batchsize": "O servidor de armazenamento retornou um conjunto de $1 {{PLURAL:$1|operação|operações}} de arquivo, enquanto seu limite é de $2 {{PLURAL:$1|operação|operações}}.",
        "backend-fail-usable": "Não foi possível ler ou salvar o arquivo $1 devido a permissões insuficientes a diretórios, ou a repositórios/diretórios inexistentes.",
+       "backend-fail-stat": "Não foi possível ler o estado do arquivo \"$1\".",
+       "backend-fail-hash": "Não foi possível determinar o resumo criptográfico do arquivo \"$1\".",
        "filejournal-fail-dbconnect": "Não foi possível se conectar ao banco de dados de registros do sistema de armazenamento \"$1\".",
        "filejournal-fail-dbquery": "Não foi possível atualizar o banco de dados de registros do sistema de armazenamento \"$1\".",
        "lockmanager-notlocked": "Não foi possível desbloquear \"$1\" por ele não se encontrar bloqueado.",
index e550e4a..c728421 100644 (file)
        "nocreate-loggedin": "Não tem permissão para criar páginas novas.",
        "sectioneditnotsupported-title": "Edição de secções não suportada",
        "sectioneditnotsupported-text": "A edição de secções não é suportada nesta página.",
+       "modeleditnotsupported-title": "Não é suportada a edição",
+       "modeleditnotsupported-text": "Não é suportada a edição do modelo de conteúdo $1.",
        "permissionserrors": "Erro de permissão",
        "permissionserrorstext": "Não tem permissão para fazer isso, {{PLURAL:$1|pelo seguinte motivo|pelos seguintes motivos}}:",
        "permissionserrorstext-withaction": "Não tem permissão para $2, {{PLURAL:$1|pelo seguinte motivo|pelos seguintes motivos}}:",
        "content-model-json": "JSON",
        "content-json-empty-object": "Objeto vazio",
        "content-json-empty-array": "Matriz vazia",
+       "unsupported-content-model": "<strong>Aviso:</strong> O modelo de conteúdo $1 não é suportado nesta wiki.",
+       "unsupported-content-diff": "As diferenças entre edições não são suportadas para o modelo de conteúdo $1.",
+       "unsupported-content-diff2": "As diferenças entre edições dos modelos de conteúdo $1 e $2 não são suportadas nesta wiki.",
        "deprecated-self-close-category": "Páginas com etiquetas HTML autofechadas inválidas",
        "deprecated-self-close-category-desc": "Esta página contém etiquetas HTML autofechadas, que são inválidas, tais como <code>&lt;b/></code> e <code>&lt;span/></code>. O comportamento destas etiquetas será alterado em breve, para ser consistente com a especificação HTML5, pelo que o seu uso no texto wiki foi descontinuado.",
        "duplicate-args-warning": "<strong>Aviso:</strong> [[:$1]] chama [[:$2]] com mais de um valor para o parâmetro \"$3\". Somente o último valor fornecido será utilizado.",
        "backend-fail-contenttype": "Não foi possível determinar o tipo de conteúdo do ficheiro para armazenar em \"$1\".",
        "backend-fail-batchsize": "Foi fornecido um bloco de $1 {{PLURAL:$1|operação|operações}} sobre ficheiros ao servidor de armazenamento; o limite é de $2 {{PLURAL:$2|operação|operações}}.",
        "backend-fail-usable": "Não foi possível ler ou gravar o ficheiro \"$1\" devido a permissões insuficientes ou a diretórios/repositórios inexistentes.",
+       "backend-fail-stat": "Não foi possível ler o estado do ficheiro \"$1\".",
+       "backend-fail-hash": "Não foi possível determinar o resumo criptográfico do ficheiro \"$1\".",
        "filejournal-fail-dbconnect": "Não foi possível estabelecer ligação à base de dados de registos no servidor de armazenamento \"$1\".",
        "filejournal-fail-dbquery": "Não foi possível atualizar a base de dados de registos do servidor de armazenamento \"$1\".",
        "lockmanager-notlocked": "Não foi possível desbloquear \"$1\" porque não se encontra bloqueado.",
        "sessionfailure": "Foram detetados problemas com a sua sessão;\na operação foi cancelada como medida de proteção contra a intercetação de sessões.\nReenvie o formulário, por favor.",
        "changecontentmodel": "Alterar modelo de conteúdo de uma página",
        "changecontentmodel-legend": "Editar modelo de contéudo",
-       "changecontentmodel-title-label": "Título da página",
+       "changecontentmodel-title-label": "Título da página:",
        "changecontentmodel-current-label": "Modelo de conteúdo atual:",
-       "changecontentmodel-model-label": "Novo modelo de conteúdo",
+       "changecontentmodel-model-label": "Novo modelo de conteúdo:",
        "changecontentmodel-reason-label": "Motivo:",
        "changecontentmodel-submit": "Alterar",
        "changecontentmodel-success-title": "O modelo de conteúdo foi alterado",
index 50b0658..5f0b966 100644 (file)
                        "Woytecr",
                        "PiefPafPier",
                        "Bagas Chrisara",
-                       "Waldyrious"
+                       "Waldyrious",
+                       "Ktrst"
                ]
        },
        "sidebar": "{{notranslate}}",
        "blockednoreason": "Substituted with <code>$2</code> in the following message if the reason is not given:\n* {{msg-mw|cantcreateaccount-text}}.\n{{Identical|No reason given}}",
        "blockedtext-composite": "Text displayed to requests blocked by more than one block.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - (Unused) A dummy user attributed as the blocker, possibly as a link to a user page.\n* $2 - the reason for the block\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the dummy blocking user's username (plain text, without the link).\n* $5 - details of the individual blocks that this block is made from\n* $6 - the expiry of the block with the longest duration\n* $7 - (Unused) the intended target of the block\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Systemblockedtext|notext=1}}",
        "blockedtext-composite-ids": "Text displayed when a user is blocked by multiple blocks, if at least one block comes from the database.\n\nParameters:\n* $1 - IDs of the blocks from the database",
-       "blockedtext-composite-no-ids": "Text displayed when a user is blocked by multiple blocks, if all the blocks are due to the IP being blacklisted.",
+       "blockedtext-composite-no-ids": "IP가 블랙리스트에 올라간 경우 여러 차단으로 사용자를 차단할 때 표시되는 텍스트 입니다.",
        "blockedtext-composite-reason": "Reason given to blocked users who are affected by more than one block.\n\nSee also:\n* {{msg-mw|blockedtext-composite}}",
        "whitelistedittext": "Used as error message. Parameters:\n* $1 - a link to [[Special:UserLogin]] with {{msg-mw|loginreqlink}} as link description\n* $2 - an URL to the same\n\nSee also:\n* {{msg-mw|Nocreatetext}}\n* {{msg-mw|Uploadnologintext}}\n* {{msg-mw|Loginreqpagetext}}",
        "confirmedittext": "Used as error message.",
index 3bcb1fd..d6094cd 100644 (file)
        "rcfilters-liveupdates-button-title-off": "Показывать новые изменения сразу после их появления",
        "rcfilters-watchlist-markseen-button": "Отметить все изменения как просмотренные",
        "rcfilters-watchlist-edit-watchlist-button": "Редактировать ваш список наблюдения",
-       "rcfilters-watchlist-showupdated": "Ð\98зменениÑ\8f Ñ\81Ñ\82Ñ\80аниÑ\86, ÐºÐ¾Ñ\82оÑ\80Ñ\8bе Ð²Ñ\8b Ð½Ðµ Ð¿Ð¾Ñ\81еÑ\89али Ñ\81 Ñ\82ого Ð¼Ð¾Ð¼ÐµÐ½Ñ\82а, ÐºÐ°Ðº Ð¾Ð½Ð¸ Ð¸Ð·Ð¼ÐµÐ½Ð¸Ð»Ð¸Ñ\81Ñ\8c, Ð²Ñ\8bделенÑ\8b <strong>жиÑ\80нÑ\8bм</strong> Ð¸ Ð¾Ñ\82меÑ\87енÑ\8b Ð¿Ð¾Ð»ным маркером.",
+       "rcfilters-watchlist-showupdated": "Ð\98зменениÑ\8f Ñ\81Ñ\82Ñ\80аниÑ\86, ÐºÐ¾Ñ\82оÑ\80Ñ\8bе Ð²Ñ\8b Ð½Ðµ Ð¿Ð¾Ñ\81еÑ\89али Ñ\81 Ñ\82ого Ð¼Ð¾Ð¼ÐµÐ½Ñ\82а, ÐºÐ°Ðº Ð¾Ð½Ð¸ Ð¸Ð·Ð¼ÐµÐ½Ð¸Ð»Ð¸Ñ\81Ñ\8c, Ð²Ñ\8bделенÑ\8b <strong>полÑ\83жиÑ\80нÑ\8bм</strong> Ñ\88Ñ\80иÑ\84Ñ\82ом Ð¸ Ð¾Ñ\82меÑ\87енÑ\8b Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½ным маркером.",
        "rcfilters-preference-label": "Использовать не JavaScript интерфейс",
        "rcfilters-preference-help": "Загружает свежие правки без поиска по фильтрам или возможностей подсветки.",
        "rcfilters-watchlist-preference-label": "Использовать интерфейс без JavaScript",
        "backend-fail-batchsize": "Хранилище получило блок из $1 {{PLURAL:$1|файловой операции|файловых операций}}, ограничение составляет $2 {{PLURAL:$1|файловую операцию|файловых операций|файловых операции}}.",
        "backend-fail-usable": "Не удалось прочитать или записать файл «$1» из-за нехватки прав или отсутствия нужных папок.",
        "backend-fail-stat": "Не удалось прочитать статус файла «$1».",
-       "backend-fail-hash": "Ð\9cожеÑ\82 определить криптографический хеш файла «$1».",
+       "backend-fail-hash": "Ð\9dевозможно определить криптографический хеш файла «$1».",
        "filejournal-fail-dbconnect": "Не удалось подключиться к базе данных журнала для хранилища «$1».",
        "filejournal-fail-dbquery": "Не удалось обновить базу данных журнала для хранилища «$1».",
        "lockmanager-notlocked": "Не удалось разблокировать «$1»; он не заблокирован.",
index 45da11d..d5a801c 100644 (file)
        "rollbacklink": "واپس ورايو",
        "rollbacklinkcount": "$1 {{PLURAL:$1|سنوار|سنوارون}} واپس-ورايو",
        "revertpage": "[[Special:Contributions/$2|$2]] ([[User talk:$2|بحث]]) پاران سنوارون واپس [[User:$1|$1]] جي آخري مسودي ڏانھن ڪيون ويون",
-       "changecontentmodel-title-label": "صفحي جو عنوان",
+       "changecontentmodel-title-label": "صفحي جو عنوان:",
        "changecontentmodel-reason-label": "سبب:",
        "changecontentmodel-submit": "بدلايو",
        "logentry-contentmodel-change-revertlink": "واپس ورايو",
        "mycontris": "ڀاڱيداريون",
        "anoncontribs": "ڀاڱيداريون",
        "contribsub2": "{{GENDER:$3|$1}} ($2) لاءِ",
+       "contributions-subtitle": "{{GENDER:$3|$1}} لاءِ",
        "contributions-userdoesnotexist": "واپرائيندڙ کاتو \"$1\" درج ٿيل نہ آهي.",
        "nocontribs": "هن معيار سان ملندڙ ڪي بہ تبديليون نہ لڌيون ويون.",
        "uctop": "هاڻوڪو",
        "month": "مھيني کان (۽ اڳوڻيون):",
        "year": "سال کان (۽ اڳوڻيون):",
+       "date": "تاريخ کان (۽ اڳ):",
        "sp-contributions-blocklog": "بندش لاگ",
        "sp-contributions-suppresslog": "{{GENDER:$1|واپرائيندڙ}} جو دٻايل ڀاڱيداريون",
        "sp-contributions-deleted": "{{GENDER:$1|واپرائيندڙ}} جون ڊاٿل ڀاڱيداريون",
        "import-upload-filename": "فائيل نانءُ:",
        "import-comment": "تاثر:",
        "importstart": "صفحا درآمد ٿين پيا...",
+       "import-revision-count": "$1 {{PLURAL:$1|ورجاءُ|ورجاءَ}}",
        "importlogpage": "درآمد لاگ",
        "tooltip-pt-userpage": "{{GENDER:|توھانجو}} صفحو",
        "tooltip-pt-mytalk": "{{GENDER:|توھانجو}} بحث صفحو",
        "ilsubmit": "ڳوليو",
        "bydate": "تاريخوار",
        "days-abbrev": "$1 ڏ",
+       "seconds": "{{PLURAL:$1|$1 سيڪنڊ|$1 سيڪنڊَ}}",
+       "minutes": "{{PLURAL:$1|$1 منٽ|$1 منٽَ}}",
        "hours": "{{PLURAL:$1|$1 ڪلاڪ|$1 ڪلاڪَ}}",
        "days": "{{PLURAL:$1|$1 ڏينهن|$1 ڏينهَن}}",
        "weeks": "{{PLURAL:$1|$1 هفتو|$1 هفتا}}",
        "htmlform-yes": "ها",
        "htmlform-cloner-create": "ٻيا بہ شامل ڪريو",
        "htmlform-cloner-delete": "هٽايو",
+       "htmlform-date-placeholder": "سسسس-مم-ڏڏ",
        "htmlform-title-not-exists": "$1 وجود نٿو رکي.",
        "logentry-delete-delete": "$1 {{GENDER:$2|ڊاٿو}} صفحو $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|بحاليو}} صفحو $3 ($4)",
        "searchsuggest-search": "ڳوليو {{SITENAME}}",
        "api-error-unknown-warning": "اڻڄاتل چتاءُ: \"$1\".",
        "api-error-unknownerror": "اڻڄاتل چُڪَ: \"$1\".",
+       "duration-seconds": "$1 {{PLURAL:$1|سيڪنڊُ|سيڪنڊَ}}",
+       "duration-minutes": "$1 {{PLURAL:$1|منٽُ|منٽَ}}",
+       "duration-hours": "$1 {{PLURAL:$1|ڪلاڪ}}",
        "duration-days": "$1 {{PLURAL:$1|ڏينھُن|ڏينھَن}}",
+       "duration-weeks": "$1 {{PLURAL:$1|هفتو|هفتا}}",
        "expand_templates_output": "نتيجو",
        "expand_templates_ok": "ٺيڪ",
        "expand_templates_remove_comments": "تاثرات مِٽايو",
index d80d061..8a7289f 100644 (file)
@@ -29,7 +29,7 @@
        "tog-hideminor": "Schŏw drobne pōmiany we niydŏwno pōmiynianych",
        "tog-hidepatrolled": "Schŏw przichwŏlōne pōmiany we niydŏwno pōmiynianych",
        "tog-newpageshidepatrolled": "Schŏw przichwŏlōne zajty na wykŏzie nowych zajtōw",
-       "tog-extendwatchlist": "Pokŏż na mojij pozōrliście wszyjske, a niy jyno niydŏwne pōmiany",
+       "tog-extendwatchlist": "We ôbserwowanych pokazuj wszyjske zmiany, a niy ino ôstatnie",
        "tog-usenewrc": "Potajluj pōmiany podug zajtōw we niydŏwnych pōmianach i pozōrliście",
        "tog-numberheadings": "Automatycznŏ nōmeracyjŏ titlōw",
        "tog-editondblclick": "Edycyjõ napoczynajōm dwa klikniyncia (JavaScript)",
@@ -46,8 +46,8 @@
        "tog-enotifminoredits": "Wyślij e-brifa tyż, kej by szło uo drobne pomjyńańa",
        "tog-enotifrevealaddr": "Ńy chow mojigo e-brifa we powjadomjyńach",
        "tog-shownumberswatching": "Pokoż, wjela sprowjorzy dowo pozůr",
-       "tog-oldsig": "Teroźni wyglůnd Twojygo szrajbowańo",
-       "tog-fancysig": "Szrajbńij ze kodůma wiki (bez autůmatycznygo linka)",
+       "tog-oldsig": "Twōj terŏźny podpis:",
+       "tog-fancysig": "Traktuj podpis jako wikitext (bez autōmatycznego linka)",
        "tog-uselivepreview": "Używej dynamiczne uobźyrańy (JavaScript) (ekszperymentalny)",
        "tog-forceeditsummary": "Pedź, kejbych ńic ńy naszkryfloł we uopiśe pomjyńań",
        "tog-watchlisthideown": "Schow moje pomjyńańa we artiklach, na kere dowom pozůr",
@@ -57,7 +57,7 @@
        "tog-watchlisthideanons": "Schow sprowjyńa anůńimowych sprowjoczy na liśće artikli, na kere dowom pozůr",
        "tog-watchlisthidepatrolled": "Schowej sprowdzůne sprowjyńa na pozorliśće",
        "tog-ccmeonemails": "Przesyłej mi kopje e-brifůw co żech je posłoł inkszym sprowjaczom",
-       "tog-diffonly": "Ńy pokozuj treśći zajtůw půniżyj porůwnańo pomjyńań",
+       "tog-diffonly": "Niy pokazuj treści strōn pod porōwaniami zmian",
        "tog-showhiddencats": "Pokoż schowane kategoryje",
        "tog-norollbackdiff": "Uomiń pokozywańy pomjyńań po użyću funkcyje „cofej”",
        "tog-useeditwarning": "Uostrzegej mje, kej uopuszczom zajta edycyji bez spamjyntańo půmjań",
        "disclaimers": "Prawne informacyje",
        "disclaimerpage": "Project:Prawne informacyje",
        "edithelp": "Pōmoc we edycyji",
+       "helppage-top-gethelp": "Pōmoc",
        "mainpage": "Przodniŏ zajta",
        "mainpage-description": "Przodniŏ strōna",
        "policy-url": "Project:Prawidła",
        "hidetoc": "schrůń",
        "collapsible-collapse": "Skryj",
        "collapsible-expand": "Pokŏż",
-       "thisisdeleted": "Pokoż/wćepej nazod $1",
+       "thisisdeleted": "Pokŏzać abo stworzić zaś $1?",
        "viewdeleted": "Uobejrzij $1",
        "restorelink": "{{PLURAL:$1|jedna wyćepano wersyjo|$1 wyćepane wersyje|$1 wyćepanych wersyjůw}}",
        "feedlinks": "Kanały:",
        "nav-login-createaccount": "Logowańy / Tworzyńy kůnta",
        "logout": "Wyloguj",
        "userlogout": "Uodloguj śe",
-       "notloggedin": "Ńy jeżeś zalogowany",
+       "notloggedin": "Niy je żeś wlogowany(ŏ)",
        "userlogin-noaccount": "Niy mŏsz kōnta?",
        "userlogin-joinproject": "Dołōncz do {{GRAMMAR:D.lp|{{SITENAME}}}}",
        "createaccount": "Twōrz nowe kōnto",
        "userlogin-resetpassword-link": "Niy pamiyntŏsz hasła?",
        "userlogin-helplink2": "Pōmoc przi logowaniu",
        "userlogin-loggedin": "Zalogowano kej {{GENDER:$1|$1}}. Użyj formulara půńiżyj, coby zalogować śe kej inkszy używocz.",
-       "userlogin-createanother": "Twůrz inksze kůnto",
-       "createacct-emailrequired": "E-brif",
+       "userlogin-createanother": "Stwōrz inksze kōnto",
+       "createacct-emailrequired": "Adresa e‐mail",
        "createacct-emailoptional": "Adresa e-mail (niymusowo)",
        "createacct-email-ph": "Wkludź swojã adresã e-mail",
-       "createacct-another-email-ph": "Nastow e-brif",
-       "createaccountmail": "Użyj chwilowygo hasła losowo genyrowanygo a wyślij je na wrychtowany adres e-brifa.",
-       "createacct-realname": "Prawdźiwe imje a nazwisko (uopcjůnalńe)",
-       "createacct-reason": "Powůd:",
-       "createacct-reason-ph": "Pojakymu tworzisz nowe kůnta",
+       "createacct-another-email-ph": "Wkludź adresã e-mail",
+       "createaccountmail": "Użyj tymczasowego losowego hasła i wyślij je na wkludzōnõ adresã e-mail",
+       "createacct-realname": "Prŏwdziwe miano i nazwisko (niymusowo)",
+       "createacct-reason": "PowÅ\8dd",
+       "createacct-reason-ph": "Czymu tworzisz nowe kōnto",
        "createacct-submit": "Stwōrz kōnto",
        "createacct-another-submit": "Twůrz inksze kůnto",
        "createacct-benefit-heading": "{{grammar:B.lp|{{SITENAME}}}} tworzōm ludzie jak Ty.",
        "userexists": "Mjano użytkowńika, kere żeś wybroł, je zajynte. Wybjer, prosza, inksze mjano.",
        "loginerror": "Feler przi logowańu",
        "createacct-error": "Feler tworzyńo kůnta",
-       "createaccounterror": "Ńy możno stworzić konta $1",
+       "createaccounterror": "Niy idzie stworzić kōnta $1",
        "nocookiesnew": "Kůnto użytkowńika zostoło utworzůne, nale ńy jeżeś zalůgowany. {{SITENAME}} używo ćosteczek do logůwańo. Mosz wyłůnczone ćosteczka. Coby śe zalůgować, uodymknij ćosteczka a podej mjano a hasło swojigo kůnta.",
        "nocookieslogin": "{{SITENAME}} używo ćosteczek do logowańo użytkowńikůw. Mosz zablokowano jejich uobsłůga. Sprůbuj zaś kej załůnczysz uobsłůga ćosteczek.",
        "nocookiesfornew": "Konto sprowjorza ńy uostoło stworzone. Sprawdź, co mosz uodymkńynto uobsługa cookies.",
        "wrongpasswordempty": "Hasło kere żeś podou je uostawjůne blank. Naszkryflej je jeszcze roz.",
        "passwordtooshort": "Hasło kere żeś podoł je felerne abo za krůtke.\nHasło muśi mjeć przinojmńij {{PLURAL:$1|1 buchsztaba|$1 buchsztabůw}} a być inksze uod mjana użytkowńika.",
        "password-name-match": "Hasło mo być inksze atoli mjano używocza.",
-       "password-login-forbidden": "Mogebność wyboru tygo mjana używocza abo hasła je zawarte.",
+       "password-login-forbidden": "Używanie tego miana ôd używŏcza i hasła było zakŏzane.",
        "mailmypassword": "Wyczyść hasło",
        "passwordremindertitle": "Nowe tymczasowe hasło lo {{SITENAME}}",
        "passwordremindertext": "Ftoś (cheba Ty, ze IP $1)\npado, aże chce nowe hasło do {{SITENAME}} ($4).\nLo użytkowńika \"$2\" wygenyrowano nowe hasło a je ńim \"$3\".\nJak chćołżeś gynał to zrobjyć, to zalůgůj śe terozki a podej swoje hasło.\nHasło wygaśnie za {{PLURAL:$5|1 dzień|$5 dni}}.\n\nJak ftoś inkszy chćoł nowe hasło abo jak Ci śe przipůmńouo stare a ńy chcesz nowygo, to zignoruj to a używej starygo hasła.",
        "retypenew": "Naszkryflej jeszcze roz nowe hasło:",
        "resetpass_submit": "Nasztaluj hasło a zaloguj",
        "changepassword-success": "Twoje hasło zostoło půmyślńy půmjyńone!",
+       "botpasswords": "Hasła bota",
        "resetpass_forbidden": "Ńy idźe sam půmjyńyć hasłůw.",
        "resetpass-no-info": "Muśisz być zalogowany, coby uzyskoć bezpostrzedńi dostymp do tyj zajty.",
        "resetpass-submit-loggedin": "Zmjyń hasło",
        "passwordreset-disabled": "No tyj wiki zamkńynto resytowańy hasył.",
        "passwordreset-username": "Miano ôd używŏcza:",
        "passwordreset-domain": "Domyna:",
-       "passwordreset-email": "E-brif:",
+       "passwordreset-email": "Adresa e‐mail:",
        "passwordreset-emailtitle": "Kůnto na {{GRAMMAR:MS.lp|{{SITENAME}}}}",
        "passwordreset-emailtext-ip": "Ftoś (cheba Ty, s IP $1)\npado, aże chce informacyji lo konta do {{GRAMMAR:MS.lp{{SITENAME}}}} ($4).\nZe tym ausdrukym sům powjůnzane kůnta:\n$2\n\n{{PLURAL:$3|Tymczasowygo hasła|Tymczasowych hasył}} możno użyć we {{PLURAL:$5|jedyn dźyń|$5 dńi}}.\n\nJak chćołżeś gynał to zrobjyć, to zaloguj śe terozki a podej swoje hasło.\n\nJak ftoś inkszy chćoł nowe hasło abo jak Ci śe przipůmńoło stare a ńy chcysz nowygo, to zignoruj to a używej starygo hasła.",
        "passwordreset-emailelement": "Mjano sprowjorza: \n$1\n\nTymczasowe hasło: \n$2",
        "changeemail-header": "Pomjyno ausduku e-mail",
        "changeemail-no-info": "Muśisz być zalogowany, coby uzyskać bezpostrzedńi dostymp do tyj zajty.",
        "changeemail-oldemail": "Uobecny ausdruk:",
-       "changeemail-newemail": "Nowy adresu e-brif",
+       "changeemail-newemail": "Nowŏ e-mailowŏ adresa:",
        "changeemail-none": "podstawowo",
        "changeemail-submit": "Spamjyntej nowy",
        "resettokens": "Resetuj tokeny",
        "minoredit": "To je małŏ zmiana",
        "watchthis": "Ôbserwuj tã strōnã",
        "savearticle": "Spamiyntej",
+       "publishpage": "Ôpublikuj strōnã",
+       "publishpage-start": "Ôpublikuj strōnã...",
        "preview": "Podglōnd",
        "showpreview": "Pokŏż podglōnd",
        "showdiff": "Pokŏż zmiany",
        "anoneditwarning": "<strong>Pozōr:</strong> Niy je żeś wlogowany(ŏ). Jak zrobisz jakeś zmiany, to Twoja adresa IP bydzie publicznie widać. Jeźli <strong>[$1 sie wlogujesz]</strong> abo <strong>[$2 stworzisz kōnto]</strong>, to Twoje zmiany bydōm przipisane do kōnta społym ze inkszymi profitami.",
        "anonpreviewwarning": "Ńy jeżeś zalogowany. Twój IP ausdruk uostańy spamjyntany, eli ty bydźesz sprowjać zajte.",
        "missingsummary": "'''Pozůr:''' Ńy wprowadźůł żeś uopisu pomjyńań. Kej go ńy chcesz wprowadzać, naćiś knefel Spamjyntej jeszcze roz.",
-       "missingcommenttext": "Wćepej kůmyntorz půńiżyj.",
+       "missingcommenttext": "Wkludź kōmyntŏrz niżyj.",
        "missingcommentheader": "'''Dej pozůr:''' Treść nagłůwka je blank - uzupełńij go! Jeli tego ńy zrobisz, Twůj kůmyntorz bydźe naszkryflony bez nagłůwka.",
        "summary-preview": "Podglůnd uopisu:",
        "subject-preview": "Podglůnd tyjmy/nagłůwka:",
        "yourtext": "Twůj tekst",
        "storedversion": "Naszkryflano wersyjo",
        "editingold": "'''Dej pozůr: Sprowjosz inkszo wersyjo zajty kej bjeżůnco. Jeli jům naszkryflosz, wszyjske půźńyjsze pomjyńańa bydům wyćepane.'''",
-       "yourdiff": "RůżÅ\84ice",
+       "yourdiff": "RÅ\8dżnice",
        "copyrightwarning": "Pamjyntej uo tym, aże cołki wkłod do {{SITENAME}} udostympńůmy wedle zasad $2 (dokładńij we $1). Jak ńy chcesz, coby kożdy můg go půmjyńać a dalij rozpowszychńoć, ńy wćepuj uůnygo sam. Szkryflajůnc sam tukej pośwjadczosz tyż, co te pisańy je twoje własne, abo żeś go wźůn(a) ze materjołůw kere sům na ''public domain'', abo kůmpatybilne.<br />\n'''PROSZA ŃY WĆEPYWAĆ SAM MATYRJOŁŮW KERE SŮM CHRŮŃONE AUTORSKIM PRAWYM BEZ DOZWOLEŃO WŁAŚĆIĆELA!'''",
        "copyrightwarning2": "Pamjyntej uo tym, aże cołki wkłod do {{GRAMMAR:MS.lp|{{SITENAME}}}} może być sprowjany, pomjyńany abo wyćepany bez inkszych użytkownikůw. Jak ńy chcysz, coby kożdy můg uůnygo zmjyńać a dalij rozpowszychńoć bez uograniczyń, ńy wćepuj go sam.<br />\nSzkryflajůnc sam tukej pośwjadczosz tyż, co te pisańy je twoje własne, abo żeś go wźůn(a) ze matyrjołůw kere sům na public domain, abo kůmpatybilne (kuknij tyż: $1).\n'''PROSZA ŃY WĆEPYWAĆ SAM MATYRJOŁŮW KERE SŮM CHRŮŃONE PRAWYM AUTORSKIM BEZ DOZWOLEŃO WŁAŚĆIĆELA!'''",
        "longpageerror": "''Feler: Tekst kery żeś sam wćepywoł mo {{PLURAL:$1|jedyn kilobajt|$1 kilobajtůw}}. Maksymalno dugość tekstu ńy może być srogszo kej {{PLURAL:$2|jedyn kilobajt|$2 kilobajtůw}}. Twůj tekst ńy bydźe sam naszkryflany.'''",
        "revisiondelete": "Wyćep/wćep nazod wersyje",
        "revdelete-nooldid-title": "Ńy wybrano wersyji",
        "revdelete-nooldid-text": "Ńy wybrano wersyji na kerych mo zostać wykůnano ta uoperacyjo.",
-       "revdelete-no-file": "Ńy mo tygo plika.",
+       "revdelete-no-file": "Tyn zbiōr niy istniyje.",
        "revdelete-show-file-confirm": "Jeżeś echt pewny co chcesz uobejzdrzeć wyćepano wersyjo plika „<nowiki>$1</nowiki>” s $2 $3?",
        "revdelete-show-file-submit": "Ja",
        "logdelete-selected": "{{PLURAL:$1|Wybrane zdarzyńy ze rejeru|Wybrane zdarzyńa ze rejeru}}:",
        "search-nonefound": "Żŏdne wyniki niy ôdpowiadajōm tymu zapytaniu.",
        "powersearch-legend": "Sznupańy zaawansowane",
        "powersearch-ns": "Sznupej we przestrzyńach mjan:",
-       "powersearch-togglelabel": "Uoznocz:",
+       "powersearch-togglelabel": "Ôznŏcz:",
        "powersearch-toggleall": "Wszyjsko",
-       "powersearch-togglenone": "żodno",
+       "powersearch-togglenone": "Nic",
        "search-external": "Zewnyntrzne sznupańy",
        "searchdisabled": "Sznupańy we {{GRAMMAR:MS.lp|{{SITENAME}}}} uostoło zawarte. Ńim go uotworzům nazod, moges sprůbować sznupańo bez Google. Ino zauważ, co informacyje uo treśći {{GRAMMAR:MS.lp|{{SITENAME}}}} můgům być we Google ńyaktuelne.",
        "search-error": "Wystůmpjůł feler przi sznupańu: $1",
        "preferences": "Preferyncyje",
        "mypreferences": "Preferyncyje",
-       "prefs-edits": "Liczba sprowjyń:",
+       "prefs-edits": "Liczba edycyji:",
        "prefs-skin": "Skůrka",
        "skin-preview": "podglůnd",
        "datedefault": "Důmyślny",
        "prefs-watchlist-token": "ID pozůrlisty:",
        "prefs-misc": "Roztůmajte",
        "prefs-resetpass": "Zmjyń hasło",
-       "prefs-changeemail": "Pomjyno ausdruka e-brif",
-       "prefs-setemail": "Nastow e-brif",
-       "prefs-email": "Uopcyje e-brifa",
-       "prefs-rendering": "Wyglůnd",
+       "prefs-changeemail": "Zmiyń abo skasuj adresã e-mail",
+       "prefs-setemail": "Nasztaluj adresã e-mail",
+       "prefs-email": "Ôpcyje e-maila",
+       "prefs-rendering": "WyglÅ\8dnd",
        "saveprefs": "Spamjyntej",
-       "restoreprefs": "Wćep wszyjskie důmyślne preferencyje",
+       "restoreprefs": "Prziwrōć wszyjske wychodne preferyncyje (we wszyjskich sekcyjach)",
        "prefs-editing": "Sprowjańy",
        "searchresultshead": "Sznupańy",
        "stub-threshold": "Maksymalny rozmjar artikla uoznaczanygo kej <a href=\"#\" class=\"stub\">stub (kůnsek)</a>",
        "prefs-namespaces": "Raumy mjan",
        "default": "důmyślńy",
        "prefs-files": "Pliki",
-       "youremail": "E-brif:",
+       "youremail": "E-mail:",
        "username": "{{GENDER:$1|Mjano używocza}}:",
        "prefs-memberingroups": "Należy do {{PLURAL:$1|grupy|grup:}}",
-       "prefs-registration": "Czas twůrzyńa kůnta:",
+       "prefs-registration": "Data registracyje:",
        "yourrealname": "Prawdźiwe mjano",
        "yourlanguage": "Godka interfejsu",
-       "yournick": "Twoja szrajbka:",
+       "yournick": "Nowy podpis:",
        "badsig": "Felerny podpis, wejzdrzij na znaczniki HTML.",
        "badsiglength": "Twojo szrajbka je za dugo. Ji maksymalno dugość to $1 {{PLURAL:$1|buchsztaby|buchsztabůw}}",
        "yourgender": "Płeć:",
        "gender-unknown": "ńyznano",
        "gender-male": "chop",
        "gender-female": "baba",
-       "email": "E-brif",
+       "email": "E‐mail",
        "prefs-help-realname": "* Mjano a nazwisko (uopcjůnalńy): jak żeś zdecydowoł aże je podosz, bydům użyte, coby Twoja robota mjoła atrybucyjo.",
        "prefs-help-email": "Ukozańy e-brifowygo adresu ńy je powinne, nale nutne, coby resetować ausdruk, eli zapomńisz.",
        "prefs-help-email-others": "Mogesz tyż doć mogebność inkszym używoczům posłać ci e-brif bez twojo zajta używocza abo zajta dyskusyje. Twůj e-brifowy adres śe ńy ukoże.",
        "prefs-help-email-required": "Wymogany je adres e-brifa.",
        "prefs-diffs": "Diffy",
-       "userrights": "Zarzůndzańy prowami użytkowńikůw",
+       "userrights": "Prawa ôd używŏczōw",
        "userrights-lookup-user": "Zarzůndzej prowami użytkownika",
        "userrights-user-editname": "Wkludź sam mjano użytkowńika:",
        "editusergroup": "Sprowjej grupy użytkowńika",
        "right-suppressredirect": "Ńy twůrz przekerowańo ze starygo mjana jak przećepujesz zajta",
        "right-upload": "Wćepane pliki",
        "right-reupload": "Nadpisuj pliki kere sam już sům wćepane",
-       "right-reupload-own": "Nadpisuj plik wćepany sam bez tygo samygo użytkowńika",
+       "right-reupload-own": "Nadpisowanie przōdzij przisłanych zbiorōw przed siebie",
        "right-reupload-shared": "Nadpisuj pliki umjeszczůne we repozytorjům dźelůnych plikůw na lokalnyj kopje",
        "right-upload_by_url": "Wćepńij sam plik ze adresa URL",
        "right-purge": "Wyczyść pamjyńć podrynczno do zajty za wyjůntkym zajty potwjerdzyńo",
        "right-deleterevision": "Wyćepywańy a wćepywańy nazod wskazanych sprowjyń zajtůw",
        "right-deletedhistory": "Pokazuj historyjo usuńyntych sprowjyń, bez tekstu uopisu",
        "right-browsearchive": "Sznupej za wyćepanymi zajtůma",
-       "right-undelete": "Wćepej nazod wyćepano zajta",
+       "right-undelete": "Prziwrŏcanie skasowanych strōn",
        "right-suppressrevision": "Přyglůndańy i uodtwařańy sprowjyń schrůńůnych před admińistratorami",
        "right-suppressionlog": "Pokoż prywatne lůgi",
        "right-block": "Zawjyrańy sprowjorzům mogebnośći edytowańo",
        "action-move": "přećepańe tyj zajty",
        "action-move-subpages": "přećepańo tyj zajty uoroz s jeij podzajtůma",
        "action-move-rootuserpages": "Překludzańy zajtůw uod užytkowńikůw (nale bes jeich podzajtůw)",
-       "action-movefile": "przećepańe tygo plika",
-       "action-upload": "wćepńyńćo tygo plika",
-       "action-reupload": "nadpisańo tygo plika",
-       "action-reupload-shared": "nadpisańo tygo plika we wspůlnym repozytorjům",
-       "action-upload_by_url": "wćepańo tygo plika s adresa URL",
+       "action-movefile": "pōnkniyńcie tego zbioru",
+       "action-upload": "przisłanie tego zbioru",
+       "action-reupload": "nadpisanie tego zbioru",
+       "action-reupload-shared": "nadpisanie tego zbioru we spōlnym repozytorium",
+       "action-upload_by_url": "zaladowanie tego zbioru ze adresy URL",
        "action-writeapi": "naškryflańo bez interfejs API",
        "action-delete": "wyćepańo tyj zajty",
        "action-deleterevision": "wyćepańo tyj wersyje",
        "action-undelete": "wćepańo nazod tyj zajty",
        "action-suppressrevision": "podglůndu a wćepańo nazod tyj wersyje schrůńůnyj",
        "action-suppressionlog": "podglůndu rejera schrůńańo",
-       "action-block": "zawarća uod sprowjyń tygo spowjořa",
+       "action-block": "zablokowanie edytowaniŏ tymu używŏczowi",
        "action-protect": "zmiany poziōmōw zabezpieczyń na tyj strōnie",
        "action-import": "importu tyj zajty s inkšyj wiki",
        "action-importupload": "importu tyj zajty bez wćepańe plika",
        "recentchanges-label-plusminus": "Strōna zmiyniyła srogość ô tela bajtōw",
        "recentchanges-legend-heading": "<strong>Legynda:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (ôbejzdrzij tyż [[Special:NewPages|listã nowych strōn]])",
+       "rcfilters-legend-heading": "<strong>Wykŏz skrōtōw:</strong>",
        "rcfilters-other-review-tools": "Inksze nŏrzyńdzia kōntrole",
        "rcfilters-filter-humans-label": "Czowiek (niy bot)",
        "rcfilters-liveupdates-button": "Aktualizacyje na żywo",
        "uploaddisabled": "Wćepywanie sam plikůw je zawarte",
        "uploaddisabledtext": "Wćepywańe plikůw je zawarte.",
        "uploadscripted": "Tyn plik zawjyro kod HTML abo skrypt kery može zostać felerńe zinterpretowany bez přyglůndarka internetowo.",
-       "uploadvirus": "W tym pliku je wirus! Ščygůuy: $1",
+       "uploadvirus": "W zbiorze je wirus!\nInformacyje: $1",
        "upload-source": "Plik zdrzůdłowy",
        "sourcefilename": "Mjano oryginalne:",
        "destfilename": "Mjano docylowe:",
        "linkstoimage": "{{PLURAL:$1|Ta strōna używŏ|Te strōny używajōm}} tego zbioru:",
        "linkstoimage-more": "Tyn zbiōr {{PLURAL:$1|używŏ wiyncyj niż jedna strōna|używajōm wiyncyj niż $1 strōny|używŏ wiyncyj niż $1 strōn}}.\nTa lista pokazuje ino {{PLURAL:$1|piyrszõ|piyrsze $1}}.\n[[Special:WhatLinksHere/$2|Połnŏ lista]] je tyż dostympnŏ.",
        "nolinkstoimage": "Żŏdnŏ strōna niy używŏ tego zbioru.",
-       "morelinkstoimage": "Pokož [[Special:WhatLinksHere/$1|wjyncy uodnośnikůw]] do tygo plika.",
+       "morelinkstoimage": "Pokŏż [[Special:WhatLinksHere/$1|wiyncyj linkōw]] do tego zbioru.",
        "linkstoimage-redirect": "$1 (przekerowanie do zbioru) $2",
        "duplicatesoffile": "{{PLURAL:$1|Nastympujůncy plik je kopjům|Nastympujůnce pliki sům kopjůma}} tygo plika:",
        "sharedupload": "Tyn plik je wćepńynty na $1 a inksze projekty tyż go mogům używać.",
        "sharedupload-desc-here": "Tyn zbiōr je ze $1 i może być używany we inkszych projektach.\nÔpis na jego [$2 strōnie ôpisu zbioru] je pokŏzany niżyj.",
        "filepage-nofile": "Niy ma zbioru ze tym mianym.",
-       "uploadnewversion-linktext": "Wćepńij nowšo wersyjo tygo plika",
+       "uploadnewversion-linktext": "Zaladuj nowszõ wersyjõ tego zbioru",
        "upload-disallowed-here": "Niy możesz podmiynić tego zbioru.",
        "filerevert": "Přiwracańy $1",
        "filerevert-legend": "Přiwracańy poprzedńy wersje plika",
        "mimetype": "Typ MIME:",
        "download": "pobier",
        "unwatchedpages": "Zajty na kere ńy je dowany pozůr",
-       "listredirects": "Lista překerowań",
-       "unusedtemplates": "Ńyužywane šablôny",
+       "listredirects": "Lista przekerowań",
+       "unusedtemplates": "Niyużywane mustry",
        "unusedtemplatestext": "Půńižej znojdowo śe lista wšyjstkich zajtůw s přestřyńi mjan {{ns:template}}, kere ńy sům užywane bez inkše zajty. Sprowdź inkše adresowańa ku šablůnům, ńim wyćepńeš ta zajta.",
        "unusedtemplateswlh": "ku adresatu",
        "randompage": "Losowŏ strōna",
        "statistics-users": "Zarejerowanych użytkowńikůw",
        "statistics-users-active": "Aktywnych użytkowńikůw",
        "statistics-users-active-desc": "Użytkowńiki, kere bůły aktywne bez {{PLURAL:$1|uostatńi dźyń|uostatńich $1 dńi}}",
-       "doubleredirects": "Podwůjne překierowańa",
+       "pageswithprop": "Strōny ze włŏsnościami",
+       "pageswithprop-legend": "Strōny ze włŏsnościami",
+       "doubleredirects": "Tuplowane przekerowania",
        "doubleredirectstext": "Na tyi liśće mogům znojdować śe překerowańo pozorne. Uoznača to, aže půńižej pjyrwšej lińii artikla, zawjerajůncyj \"#REDIRECT ...\", može znojdować śe dodotkowy tekst. Koždy wjerš listy zawjero uodwouańo do pjyrwšygo i drůgygo překerowańo a pjyrwšom lińjům tekstu drůgygo překerowańo. Uůmožliwjo to na ogůu uodnaleźyńy wuaśćiwygo artikla, do kerygo powinno śe překerowywać.",
        "double-redirect-fixed-move": "zajta [[$1]] zostoła zastůmpjůno bez przekerowańy, skiż jeij przekludzyńo ku [[$2]]",
        "double-redirect-fixer": "Korektōr przekerowań",
-       "brokenredirects": "Zuomane překerowańa",
+       "brokenredirects": "Złōmane przekerowania",
        "brokenredirectstext": "Překerowańo půńižej wskazujům na artikle kerych sam ńy ma.",
        "brokenredirects-edit": "sprowjéj",
        "brokenredirects-delete": "wyćep",
-       "withoutinterwiki": "Artikle bez interwiki",
+       "withoutinterwiki": "Artykuły bez interwiki",
        "withoutinterwiki-summary": "Zajty půńižej ńy majům uodwouań do wersjůw w inkšych godkach.",
        "withoutinterwiki-legend": "Prefiks",
        "withoutinterwiki-submit": "Pokož",
-       "fewestrevisions": "Zajty z nojmńijšom ilośćům wersyji",
+       "fewestrevisions": "Strōny, co majōm nojmynij wersyji",
        "nbytes": "$1 {{PLURAL:$1|bajt|bajty|bajtōw}}",
        "ncategories": "$1 {{PLURAL:$1|kategoryja|kategorje|kategorjůw}}",
        "nlinks": "$1 {{PLURAL:$1|link|linki|linkůw}}",
        "nmembers": "$1 {{PLURAL:$1|elymynt|elymynta|elymyntōw}}",
        "nrevisions": "$1 {{PLURAL:$1|wersja|wersje|wersjůw}}",
        "specialpage-empty": "Ta zajta je pusto.",
-       "lonelypages": "Poćepńynte zajty",
+       "lonelypages": "Sierocie strōny",
        "lonelypagestext": "Do zajtůw půńiżyj ńy adresuje żodno inkszo zajta we {{SITENAME}}.",
-       "uncategorizedpages": "Zajty bez kategoryje",
-       "uncategorizedcategories": "Kategoryje bez kategoriůw",
-       "uncategorizedimages": "Pliki bez kategoryjůw",
-       "uncategorizedtemplates": "Mustry bez kategorii",
-       "unusedcategories": "Ńyużywane kategoryje",
-       "unusedimages": "Ńyużywane pliki",
-       "wantedcategories": "Potrzebne katygoryje",
-       "wantedpages": "Nojpotrzebńijsze zajty",
-       "wantedfiles": "Potrzebne pliki",
-       "wantedtemplates": "Potrzebne szablůny",
+       "uncategorizedpages": "Niyskategoryzowane strōny",
+       "uncategorizedcategories": "Kategoryje bez kategoryji",
+       "uncategorizedimages": "Niyskategoryzowane zbiory",
+       "uncategorizedtemplates": "Niyskategoryzowane mustry",
+       "unusedcategories": "Niyużywane kategoryje",
+       "unusedimages": "Niyużywane zbiory",
+       "wantedcategories": "Potrzebne kategoryje",
+       "wantedpages": "Potrzebne strōny",
+       "wantedfiles": "Potrzebne zbiory",
+       "wantedtemplates": "Potrzebne mustry",
        "mostlinked": "Nojczyńśćij adresowane",
        "mostlinkedcategories": "Kategoryje we kerych je nojwjyncyj artikli",
        "mostlinkedtemplates": "Nojczyńśćij adresowane mustry",
        "mostimages": "Nojczyńśćij adresowane pliki",
        "mostrevisions": "Nojczyńśćij sprowjane artikle",
        "prefixindex": "Wszyjske strōny ze prefiksym",
-       "shortpages": "Nojkrůtsze zajty",
-       "longpages": "Duge artikle",
-       "deadendpages": "Artikle bez linkůw",
+       "shortpages": "NojkrÅ\8dtsze strÅ\8dny",
+       "longpages": "Duge strōny",
+       "deadendpages": "Ślepe strōny",
        "deadendpagestext": "Zajty wymjyńůne půńiżyj ńy majům uodnośńikůw do żodnych inkszych zajtůw kere sům na tyj wiki.",
-       "protectedpages": "Zawarte zajty",
+       "protectedpages": "Zastawiōne strōny",
        "protectedpages-indef": "Ino zabezpjeczyńo ńyuokreślůne",
        "protectedpages-cascade": "Yno zajty zabezpjeczůne rekursywńy",
        "protectedpagesempty": "Żodno zajta ńy je terozki zawarto ze podanymi parametrami.",
-       "protectedtitles": "Zawarte mjana artikli",
+       "protectedtitles": "Zastawiōne tytuły",
        "protectedtitlesempty": "Do tych štalowań utwořyńy artikla uo dowolnym mjańy ńy je zawarte",
        "listusers": "Lista używŏczōw",
        "listusers-editsonly": "Pokoż yno użytkowńikůw kere majům sprowjyńa",
        "usercreated": "{{GENDER:$3|Utworzono}} $1 uo $2",
        "newpages": "Nowe strōny",
        "newpages-username": "Miano ôd używŏcza:",
-       "ancientpages": "Nojstarše artikle",
+       "ancientpages": "Nojstarsze strōny",
        "move": "Przeniyś",
        "movethispage": "Přećepej ta zajta",
        "unusedimagestext": "Pamjyntej, proša, aže inkše witryny, np. projekty Wikimedja w inkšych godkach, můgům adresować do tych plikůw užywajůnc bezpośredńo URL. Bez tůž ńykere ze plikůw můgům sam być na tej liśće pokozane mimo, aže žodna zajta ńy adresuje do ńich.",
        "all-logs-page": "Wszyjske óperacyje",
        "alllogstext": "Spōlne pokŏzanie wszyjskich dostympnych regestōw {{SITENAME}}.\nMożesz uakuratnić widok bez ôbranie zorty regestu, miana ôd używŏcza, abo tykanyj strōny (dŏwŏ pozōr na małe i sroge litery).",
        "logempty": "Niy ma we regeście zgodliwych elymyntōw.",
-       "log-title-wildcard": "Šnupej za titlami kere začynojům śe uod tygo tekstu",
+       "log-title-wildcard": "Szukej tytułōw, co sie zaczynajōm ôd tego tekstu",
        "allpages": "Wszyjske strōny",
-       "nextpage": "Nostympno zajta ($1)",
-       "prevpage": "Popředńo zajta ($1)",
+       "nextpage": "Nastympnŏ strōna ($1)",
+       "prevpage": "Poprzedniŏ strōna ($1)",
        "allpagesfrom": "Strōny, co sie zaczynajōm ôd:",
-       "allpagesto": "Zajty uo titlach kere na zadku majům:",
+       "allpagesto": "Pokŏż strōny do:",
        "allarticles": "Wszyjske strōny",
        "allinnamespace": "Wszyjstke zajty (we przestrzyńi mjan $1)",
        "allpagessubmit": "Idź",
        "listusersfrom": "Pokaž užytkowńikůw začynojůnc uod:",
        "listusers-submit": "Uobejrzij",
        "listusers-noresult": "Ńy znejdźůno žodnygo užytkowńika.",
+       "activeusers": "Lista aktywnych używŏczōw",
        "activeusers-noresult": "Niy szło znŏjść żŏdnych używŏczōw",
        "listgrouprights": "Uprawniynia grup używŏczōw",
        "listgrouprights-summary": "Niżyj widać wykŏz grup używŏczōw zdefiniowanych na tyj wiki, społym ze jejich prawami dostympu.\n[[{{MediaWiki:Listgrouprights-helppage}}|Ekstra informacyje]] ô uprawniyniach.",
        "listgrouprights-key": "* <span class=\"listgrouprights-granted\">Dane uprawńyńy</span>\n* <span class=\"listgrouprights-revoked\">Uodebrane uprawńyńy</span>",
        "listgrouprights-group": "Grupa",
        "listgrouprights-rights": "Uprawniynia",
-       "listgrouprights-helppage": "Help:Uprawńyńo grup użytkowńikůw",
+       "listgrouprights-helppage": "Help:Prawa grup używŏczōw",
        "listgrouprights-members": "(lista czōnkōw grupy)",
        "listgrouprights-addgroup": "Idźe dodać do {{PLURAL:$2|grupy|grup}}: $1",
        "listgrouprights-removegroup": "Idźe wyćepać s {{PLURAL:$2|grupy|grup}}: $1",
        "listgrouprights-addgroup-all": "Idźe dodać do kożdyj grupy",
        "listgrouprights-removegroup-all": "Idźe wyćepać s wszyjstkich grup",
        "listgrouprights-addgroup-self": "Je mogebny dać swe konto do {{PLURAL:$2|grupy|grup:}} $1",
+       "listgrants": "Prawa",
+       "trackingcategories": "Kategoryje, co śledzōm",
        "mailnologin": "Brak adresu",
        "mailnologintext": "Muśyš śe [[Special:UserLogin|zalůgować]] i mjeć wpisany aktualny adres e-brif w swojich [[Special:Preferences|preferyncyjach]], coby můc wysuać e-brif do inkšygo užytkowńika.",
        "emailuser": "Poślij tymu używŏczowi e-mail",
        "emailpagetext": "Możesz użyć půńiższygo formularza, coby wysłać wjadůmość e-brif do tygo użytkowńika.\nAdres e-brifa, kery zostoł bez Ćebje wkludzůny we [[Special:Preferences|Twojich sztalowańach]], pojawi śe we polu „Uod”, bez cůż uodbjorca bydźe můg Ći uodpedźeć.",
        "defemailsubject": "{{SITENAME}} - e-mail ôd używŏcza \"$1\"",
        "usermaildisabled": "E-mail ôd używŏcza je zastŏwiōny",
-       "noemailtitle": "Brak adresu e-brif",
+       "noemailtitle": "Brak adresy e-mail",
        "noemailtext": "Tyn używŏcz niy podoł nŏleżnyj adresy email.",
        "nowikiemailtext": "Tyn używŏcz niy chce dostŏwać emaili ôd inkszych.",
        "emailtarget": "Podej adresata",
        "email-legend": "Wyślij e-brif ku inkszymu użytkowńikowi {{GRAMMAR:MS.lp|{{SITENAME}}}}",
        "emailfrom": "Uod:",
        "emailto": "Ku:",
-       "emailsubject": "Tyjma:",
+       "emailsubject": "Tymat:",
        "emailmessage": "Nowina:",
        "emailsend": "Wyślij",
        "emailccme": "Wyślij mi kopja moiy wjadomości.",
        "confirm": "Potwjyrdź",
        "excontent": "zawartość zajty „$1”",
        "excontentauthor": "treść: „$1” (jedyny aůtor: [[Special:Contributions/$2|$2]])",
-       "exbeforeblank": "popředńo zawartość uobecńy pustej zajty: „$1”",
+       "exbeforeblank": "zawartość przed ôprōzniyniym: „$1”",
        "delete-confirm": "Wyćep „$1”",
        "delete-legend": "Wyćep",
        "historywarning": "Pozor! Ta zajta kerům chceš wyćepnůńć mo historjo:",
        "dellogpage": "Regest kasowań",
        "dellogpagetext": "To je lista uostatńo wykůnanych wyćepań.",
        "deletionlog": "rejer wyćepań",
-       "reverted": "Přiwrůcůno popředńo wersyja",
+       "reverted": "Prziwrōcōnŏ poprzedniõ wersyjõ",
        "deletecomment": "Čymu:",
        "deleteotherreason": "Inkšy powůd:",
        "deletereasonotherlist": "Inkszy powůd",
        "editcomment": "Sprowjyńe uopisano: <em>$1</em>.",
        "revertpage": "Wycofano sprowjyńe użytkowńika [[Special:Contributions/$2|$2]] ([[User talk:$2|godka]]). Autor prziwrůcůnej wersyji to [[User:$1|$1]].",
        "rollback-success": "Wycofano sprowjyńa użytkowńika $1.\nPrziwrůcůno uostatńo wersyja autorstwa  $2.",
-       "sessionfailure": "Feler weryfikacyji zalůgowańo.\nPolecyńy zostoło anulowane, coby ůńiknůńć przechwycyńo sesyji.\n\nNaćiś knefel „cofej”, przeładuj zajta, a potym zaś wydej polecyńy",
+       "sessionfailure": "Wyglōndŏ na to, że je problym ze twojōm sesyjōm logowaniŏ;\nta ôperacyjŏ była zastawiōnŏ jako zabezpieczynie przed przejyńciym sesyje.\nPrziślij formular zaś.",
        "protectlogpage": "Regest zawarć",
        "protectlogtext": "Půńižej znojdowo śe lista zawarć i uodymkńjyńć pojydynčych zajtůw.\nCoby přejřeć lista uobecńy zawartych zajtůw, přeńdź na zajta wykazu [[Special:ProtectedPages|zawartych zajtůw]].",
        "protectedarticle": "zawar „[[$1]]”",
        "undeletedpage": "'''Wćepano nazod zajta $1.'''\n\nUobejřij [[Special:Log/delete|rejer wyćepań]], kejbyś chćou přeglůndnůnć uostatnie uoperacyje wyćepywańo i wćepywańo nazod zajtůw.",
        "undelete-header": "Uobejřij [[Special:Log/delete|rejer wyćepań]] coby sprawdźić uostatńo wyćepane zajty.",
        "undelete-search-box": "Šnupej za wyćepńjyntymi zajtami",
-       "undelete-search-prefix": "Zajty začynajůnce śe uod:",
+       "undelete-search-prefix": "Strōny, co sie zaczynajōm ôd:",
        "undelete-search-submit": "Šnupej",
        "undelete-no-results": "Ńy znejdźono wskazanych zajtůw we archiwum wyćepanych.",
        "undelete-filename-mismatch": "Ńy idźe wćepać nazod wersyji plika z datům $1: ńyzgodność mjana plika",
        "ipbcreateaccount": "Ńy dozwůl utwožyć kůnta",
        "ipbemailban": "Zawrzij mogebność wysůłańo e-brifůw",
        "ipbenableautoblock": "Zawřij uostatńi adres IP tygo užytkowńika i autůmatyčńy wšyjstke kolejne, s kerych bydźe průbowou sprowjać zajty",
-       "ipbsubmit": "Zawřij uod sprowjyń tygo užytkowńika",
+       "ipbsubmit": "Zablokuj tego używŏcza",
        "ipbother": "Ikszy czas",
        "ipboptions": "2 godziny:2 hours,1 dziyń:1 day,3 dni:3 days,1 tydziyń:1 week,2 tydnie:2 weeks,1 miesiōnc:1 month,3 miesiōnce:3 months,6 miesiyncy:6 months,1 rok:1 year,na dycki:infinite",
        "ipbhidename": "Schrůń mjano użytkowńika/adres IP w rejerze zawarć, na liśće aktywnych zawarć i liśće użytkowńikůw",
-       "ipbwatchuser": "Dowej pozůr na zajta uosobisto i zajta godki tygo užytkowńika",
+       "ipbwatchuser": "Ôbserwuj włŏsnõ strōnã i strōnã dyskusyje ôd tego używŏcza",
        "ipb-change-block": "Zmjyń sztalowańa zawarća uod sprowjyń",
        "badipaddress": "Felerny adres IP",
        "blockipsuccesssub": "Zawarće uod sprowjyń udane",
        "ipusubmit": "Uodymkńij sprowjyńo užytkowńikowi",
        "unblocked": "[[User:$1|$1]] zostou uodymkńynty.",
        "unblocked-id": "Zawarće $1 zostouo zdjynte",
+       "blocklist": "Zablokowani używocze",
+       "autoblocklist": "Autōmatyczne blokady",
        "ipblocklist": "Zawarte używocze",
        "ipblocklist-legend": "Znejdź zawartygo uod sprawjyń užytkowńika",
        "ipblocklist-submit": "Šnupej",
        "anononlyblock": "ino ńyzalůgowańy",
        "noautoblockblock": "autůmatyčne zawjyrańy uod sprowjyń wůuůnčůne",
        "createaccountblock": "zawarto twořyńe kont",
-       "emailblock": "zawarty e-brif",
+       "emailblock": "e-mail zablokowany",
        "blocklist-nousertalk": "ńy mogům sprowjać własnych zajtůw godki",
        "ipblocklist-empty": "Lista zawarć je pusto.",
        "ipblocklist-no-results": "Podany adres IP abo užytkowńik ńy je zawarty uod sprowjyń.",
        "block-log-flags-anononly": "ino anůnimowi",
        "block-log-flags-nocreate": "tworzynie kōnta je zastawiōne",
        "block-log-flags-noautoblock": "autůmatyczne zawjerańy uod sprawjyń wyłůnczůne",
-       "block-log-flags-noemail": "e-brif zawarty",
+       "block-log-flags-noemail": "e-mail zablokowany",
        "block-log-flags-nousertalk": "ńy może sprowjać włosnyj zajty godki",
        "block-log-flags-angry-autoblock": "rozszerzůne automatyczne zawjyrańe załůnczůne",
        "range_block_disabled": "Možliwość zawjerańo zakresu adresůw IP zostoua wůuůnčůno.",
        "import-noarticle": "Ńy ma zajtůw do zaimportowańo!",
        "import-nonewrevisions": "Wšyjstke wersyje zostouy juž wčeśńij zaimportowane.",
        "xml-error-string": "$1 lińa $2, kolůmna $3 (bajt $4): $5",
-       "import-upload": "Wćepej dane XML",
+       "import-upload": "Prziślij dane XML",
        "import-token-mismatch": "Straćiły śe dane ze sesyje. Prosza spróbować zaś.",
        "import-invalid-interwiki": "Ńy idźe importować s podanyj wiki.",
        "importlogpage": "Regest importōw",
        "tooltip-diff": "Pokŏż zmiany zrobiōne we tekście.",
        "tooltip-compareselectedversions": "Ôbejzdrzij rōżnice miyndzy dwōma ôbranymi wersyjami tyj strōny",
        "tooltip-watch": "Przidej tyn artykuł do ôbserwowanych",
-       "tooltip-recreate": "Wćepej nazod zajta mimo aže bůua wčeśńij wyćepano.",
+       "tooltip-recreate": "Stwōrz strōnã zaś, chociŏż była skasowanŏ",
        "tooltip-upload": "Rozpočyńće wćepywańa",
        "tooltip-rollback": "\"Cŏfej\" jednym klikniyńciym cŏfie wszyjske zmiany ôd ôstatnigo używŏcza.",
        "tooltip-undo": "\"Cŏfnij\" cŏfie tã edycyjõ i ôtwiyrŏ ôkno edycyje we trybie podglōndu.\nDozwolŏ na wkludzynie powodu we ôpisie.",
        "bad_image_list": "Dane trza wćepać we formaće:\n\nJyno tajle listy (lińije, kere śe napoczynajům uod *) absztychujemy.\nPjyrszy link we lińiji muśi być linkym do zakozanygo pliku.\nDolsze linki we lińiji sům uwożane za wyjimki  – sům to mjana zajtůw, na kerych idzie użyć plik ze zakozanym mjanym.",
        "metadata": "Metadane",
        "metadata-help": "We tym zbiorze sōm ekstra informacyje pewnikym przidane ôd fotoaparatu abo skanera użytego do zrobiyniŏ abo zdigitalizowaniŏ go.\nJeźli zbiōr bōł modyfikowany, niykere informacyje mogōm niy cołkym ôdpowiadać zmodyfikowanymu zbiorowi.",
-       "metadata-expand": "Pokož ščygůuy",
-       "metadata-collapse": "Schowej ščygůuy",
+       "metadata-expand": "Pokŏż rozszyrzōne informacyje",
+       "metadata-collapse": "Skryj rozszyrzōne informacyje",
        "metadata-fields": "Metadane ôbrazōw wymianowane we tyj wiadōmości bydōm pokazowane na strōnie grafiki po zwiniyńciu tabule metadanych.\nInksze pola bydōm wychodnie skryte.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "namespacesall": "wszyjske",
        "monthsall": "wszyjske",
        "scarytranscludetoolong": "[za dugo adresa URL]",
        "deletedwhileediting": "'''Pozůr''': Ta zajta zostoła wyćepano po tym, jak żeś rozpoczůł jej sprowjańy!",
        "confirmrecreate": "Užytkowńik [[User:$1|$1]] ([[User talk:$1|godka]]) wyćepnůu tyn artikel po tym jak žeś rozpočůu(eua) jygo sprowjańe, podajůnc kej powůd wyćepańo:\n: ''$2''\nPotwjerdź chęć wćepańo nazod tygo artikla.",
-       "recreate": "Wćepej nazod",
+       "recreate": "Stwōrz zaś",
        "confirm_purge_button": "OK",
        "confirm-purge-top": "Wyčyśćić pamjyńć podrynčnům do tyi zajty?",
        "confirm-purge-bottom": "Uodśwjyżeńy zajty wyczyśći pamjyńć podrynczno a wymuśi pokozańy jeij aktualnyj wersyji.",
-       "imgmultipageprev": "← popředńo zajta",
+       "imgmultipageprev": "← poprzedniŏ strōna",
        "imgmultipagenext": "nastympnŏ strōna →",
        "imgmultigo": "Idź!",
        "imgmultigoto": "Idź do strōny $1",
-       "table_pager_next": "Nostympno zajta",
-       "table_pager_prev": "Popředńo zajta",
+       "table_pager_next": "Nastympnŏ strōna",
+       "table_pager_prev": "Poprzedniŏ strōna",
        "table_pager_first": "Pjyrwšo zajta",
        "table_pager_last": "Uostatńo zajta",
        "table_pager_limit": "Pokož $1 pozycyji na zajće",
        "fileduplicatesearch-result-n": "We {{GRAMMAR:MS.lp|{{SITENAME}}}} {{PLURAL:$2|je dodatkowo kopia|sům $2 dodatkowe kopje|je $2 dodatkowych kopii}} plika „$1”.",
        "specialpages": "Ekstra strōny",
        "specialpages-note-restricted": "* Ekstra zajty uogůlńy dostympne.\n* <strong class=\"mw-specialpagerestricted\">Ekstra zajty do kerych dostymp je uograńiczůny.</strong>",
-       "specialpages-group-maintenance": "Raporty kůnserwacyjne",
+       "specialpages-group-maintenance": "Reporty kōnserwacyjne",
        "specialpages-group-other": "Inkše ekstra zajty",
-       "specialpages-group-login": "Logowańy / regisztrowańy",
+       "specialpages-group-login": "Logowanie / registracyjŏ",
        "specialpages-group-changes": "Pomjyńane na uostatku a rejery",
        "specialpages-group-media": "Pliki",
-       "specialpages-group-users": "Użytkowńiki i uprawńyńa",
+       "specialpages-group-users": "Używŏcze i uprawniynia",
        "specialpages-group-highuse": "Zajty čynsto užywane",
-       "specialpages-group-pages": "Listy zajt",
+       "specialpages-group-pages": "Listy strōn",
        "specialpages-group-pagetools": "Nořyńdźa zajtůw",
        "specialpages-group-wiki": "Informacyje a werkcojgi wiki",
        "specialpages-group-redirects": "Ekstra zajty, kere kerujům",
        "searchsuggest-search": "Szukej we {{SITENAME}}",
        "duration-days": "$1 {{PLURAL:$1|dziyń|dni}}",
        "expand_templates_ok": "OK",
-       "randomrootpage": "Losowŏ strōna (bez podstrōn)"
+       "randomrootpage": "Losowŏ strōna (bez podstrōn)",
+       "changecredentials": "Zmiyń poświadczynia",
+       "changecredentials-submit": "Zmiyń poświadczynia"
 }
index df3af16..519a355 100644 (file)
        "nocreate-loggedin": "У вас нема дозволу створювати нові сторінки.",
        "sectioneditnotsupported-title": "Редагування окремих розділів не підтримується",
        "sectioneditnotsupported-text": "На цій сторінці не підтримується редагування окремих розділів",
+       "modeleditnotsupported-title": "Редагування не підтримується",
+       "modeleditnotsupported-text": "Редагування не підтримується для моделі вмісту $1.",
        "permissionserrors": "Помилка доступу",
        "permissionserrorstext": "У вас нема прав на виконання цієї операції з {{PLURAL:$1|1=наступної причини|наступних причин}}:",
        "permissionserrorstext-withaction": "У Вас нема дозволу на $2 з {{PLURAL:$1|1=такої причини|таких причин}}:",
-       "contentmodelediterror": "Ð\92и Ð½Ðµ Ð¼Ð¾Ð¶ÐµÑ\82е Ñ\80едагÑ\83ваÑ\82и Ñ\86Ñ\8e Ð²ÐµÑ\80Ñ\81Ñ\96Ñ\8e, Ð¾Ñ\81кÑ\96лÑ\8cки Ð¼Ð¾Ð´ÐµÐ»Ñ\8c Ð¹Ð¾Ð³Ð¾ Ð·Ð¼Ñ\96Ñ\81Ñ\82Ñ\83 â\80\94  <code>$1</code>, Ð²Ñ\96дÑ\80Ñ\96знÑ\8fÑ\94Ñ\82Ñ\8cÑ\81Ñ\8f Ð²Ñ\96д Ñ\82епеÑ\80Ñ\96Ñ\88нÑ\8cоÑ\97 Ð¼Ð¾Ð´ÐµÐ»Ñ\96 Ð·місту сторінки — <code>$2</code>.",
+       "contentmodelediterror": "Ð\92и Ð½Ðµ Ð¼Ð¾Ð¶ÐµÑ\82е Ñ\80едагÑ\83ваÑ\82и Ñ\86Ñ\8e Ð²ÐµÑ\80Ñ\81Ñ\96Ñ\8e, Ð¾Ñ\81кÑ\96лÑ\8cки Ð¼Ð¾Ð´ÐµÐ»Ñ\8c Ð¹Ð¾Ð³Ð¾ Ð²Ð¼Ñ\96Ñ\81Ñ\82Ñ\83 â\80\94  <code>$1</code>, Ð²Ñ\96дÑ\80Ñ\96знÑ\8fÑ\94Ñ\82Ñ\8cÑ\81Ñ\8f Ð²Ñ\96д Ñ\82епеÑ\80Ñ\96Ñ\88нÑ\8cоÑ\97 Ð¼Ð¾Ð´ÐµÐ»Ñ\96 Ð²місту сторінки — <code>$2</code>.",
        "recreate-moveddeleted-warn": "'''Попередження: Ви намагаєтеся створити сторінку, яка раніше вже була вилучена.'''\n\nПеревірте, чи Вам справді потрібно створювати цю сторінку.\nНижче, для зручності, наведений журнал вилучень і перейменувань:",
        "moveddeleted-notice": "Цю сторінку було вилучено.\nДля довідки нижче наведені відповідні записи з журналів вилучень, захисту й перейменувань цієї сторінки.",
        "moveddeleted-notice-recent": "На жаль, ця сторінка нещодавно була вилучена (протягом останніх 24 годин). Для довідки нижче наведені відповідні записи з журналів вилучень, захисту й перейменувань цієї сторінки.",
        "invalid-content-data": "Неприпустимі дані",
        "content-not-allowed-here": "Вміст «$1» недопустимий на сторінці [[:$2]] у слоті «$3»",
        "editwarning-warning": "Перехід на іншу сторінку призведе до втрати ваших змін.\nЯкщо ви ввійшли до системи, то ви можете відключити це попередження в розділі \"{{int:prefs-editing}}\" ваших налаштувань.",
-       "editpage-invalidcontentmodel-title": "Ð\9aонÑ\82енÑ\82на Ð¼Ð¾Ð´ÐµÐ»Ñ\8c не підтримується",
+       "editpage-invalidcontentmodel-title": "Ð\9cоделÑ\8c Ð²Ð¼Ñ\96Ñ\81Ñ\82Ñ\83 не підтримується",
        "editpage-invalidcontentmodel-text": "Контентна модель «$1» не підтримується.",
        "editpage-notsupportedcontentformat-title": "Формат вмісту не підтримується",
        "editpage-notsupportedcontentformat-text": "Формат вмісту $1 не підтримується моделлю вмісту $2.",
        "content-model-css": "CSS",
        "content-json-empty-object": "Порожній об'єкт",
        "content-json-empty-array": "Порожній масив",
+       "unsupported-content-model": "<strong>Увага:</strong> Модель вмісту $1 не підтримується у цій вікі.",
+       "unsupported-content-diff": "Різниця між версіями не підтримується моделлю вмісту $1.",
+       "unsupported-content-diff2": "Зміни між моделями вмісту $1 та $2 не підтримуються у цій вікі.",
        "deprecated-self-close-category": "Сторінки, що використовують недійсні самозакривні теги HTML",
        "deprecated-self-close-category-desc": "Сторінка містить недійсні самозакривні теги HTML, наприклад, <code>&lt;b/></code> чи <code>&lt;span/></code>.  Їхню поведінку невдовзі буде змінено у відповідності зі специфікацією HTML5, тому їх використання у вікітексті є застарілим.",
        "duplicate-args-warning": "<strong>Увага:</strong> [[:$1]] викликає [[:$2]] з більш ніж одним значенням параметра «$3». Буде використано лише останнє вказане значення.",
        "backend-fail-contenttype": "Не вдалося визначити тип вмісту файла, щоб зберегти його в \"$1\".",
        "backend-fail-batchsize": "Серверна частина отримала блок із $1 {{PLURAL:$1|1=файлової операції|файлових операцій}}; обмеження складає $2 {{PLURAL:$2|файлову операцію|файлові операції|файлових операцій}}.",
        "backend-fail-usable": "Файл «$1» не може бути прочитано чи записано через недостатні повноваження або відсутність каталогів (контейнерів).",
+       "backend-fail-stat": "Не вдалось прочитати статус файлу «$1».",
+       "backend-fail-hash": "Неможливо визначити криптографічний хеш файлу «$1».",
        "filejournal-fail-dbconnect": "Не вдалося підключитися до бази даних журналу для сховища «$1».",
        "filejournal-fail-dbquery": "Не вдалося оновити базу даних журналу для сховища «$1».",
        "lockmanager-notlocked": "Не вдалося розблокувати \"$1\"; він не заблокований.",
        "changecontentmodel-emptymodels-title": "Немає доступних моделей коментарів",
        "changecontentmodel-emptymodels-text": "Вміст сторінки [[:$1]] не може бути перетворений до будь якого типу.",
        "log-name-contentmodel": "Журнал змін моделі вмісту",
-       "log-description-contentmodel": "Ð\9dа Ñ\86Ñ\96й Ñ\81Ñ\82оÑ\80Ñ\96нÑ\86Ñ\96 Ð¿ÐµÑ\80елÑ\96Ñ\87енÑ\96 Ð·Ð¼Ñ\96ни Ð´Ð¾ ÐºÐ¾Ð½Ñ\82енÑ\82ниÑ\85 Ð¼Ð¾Ð´ÐµÐ»ÐµÐ¹ Ñ\81Ñ\82оÑ\80Ñ\96нок, Ð° Ñ\82акож Ñ\81Ñ\82оÑ\80Ñ\96нки, Ñ\81Ñ\82воÑ\80енÑ\96 Ð· Ð½ÐµÑ\81Ñ\82андаÑ\80Ñ\82ноÑ\8e ÐºÐ¾Ð½Ñ\82енÑ\82ноÑ\8e Ð¼Ð¾Ð´ÐµÐ»Ð»Ñ\8e.",
+       "log-description-contentmodel": "Ð\9dа Ñ\86Ñ\96й Ñ\81Ñ\82оÑ\80Ñ\96нÑ\86Ñ\96 Ð¿ÐµÑ\80елÑ\96Ñ\87енÑ\96 Ð·Ð¼Ñ\96ни Ð´Ð¾ Ð¼Ð¾Ð´ÐµÐ»ÐµÐ¹ Ð²Ð¼Ñ\96Ñ\81Ñ\82Ñ\83 Ñ\81Ñ\82оÑ\80Ñ\96нок, Ð° Ñ\82акож Ñ\81Ñ\82оÑ\80Ñ\96нки, Ñ\81Ñ\82воÑ\80енÑ\96 Ð· Ð½ÐµÑ\81Ñ\82андаÑ\80Ñ\82ноÑ\8e  Ð¼Ð¾Ð´ÐµÐ»Ð»Ñ\8e Ð²Ð¼Ñ\96Ñ\81Ñ\82Ñ\83.",
        "logentry-contentmodel-new": "$1 {{GENDER:$2|створив|створила}} сторінку $3, використовуючи нестандартну модель вмісту «$5»",
        "logentry-contentmodel-change": "$1 {{GENDER:$2|змінив|змінила}} модель вмісту сторінки $3 з «$4» на «$5»",
        "logentry-contentmodel-change-revertlink": "відкинути",
        "immobile-source-page": "Цю сторінку не можна перейменувати.",
        "immobile-target-page": "Не можна присвоїти сторінці цю назву.",
        "movepage-invalid-target-title": "Запитуване ім'я недопустиме.",
-       "bad-target-model": "Ð\9dеможливо Ð¿ÐµÑ\80еÑ\82воÑ\80иÑ\82и $1 Ð½Ð° $2: Ð½ÐµÑ\81Ñ\83мÑ\96Ñ\81нÑ\96 Ð¼Ð¾Ð´ÐµÐ»Ñ\96 Ð´Ð°Ð½Ð¸Ñ\85.",
+       "bad-target-model": "Ð\9dеможливо Ð¿ÐµÑ\80еÑ\82воÑ\80иÑ\82и $1 Ð½Ð° $2: Ð½ÐµÑ\81Ñ\83мÑ\96Ñ\81нÑ\96 Ð¼Ð¾Ð´ÐµÐ»Ñ\96 Ð²Ð¼Ñ\96Ñ\81Ñ\82Ñ\83.",
        "imagenocrossnamespace": "Неможливо дати файлові назву з іншого простору назв",
        "nonfile-cannot-move-to-file": "Не можна перейменовувати сторінки з інших просторів назв на файли",
        "imagetypemismatch": "Нове розширення файлу не співпадає з його типом",
        "import-error-interwiki": "Сторінку «$1» не було імпортовано, оскільки її назва зарезервована для зовнішніх посилань (interwiki).",
        "import-error-special": "Сторінку «$1» не було імпортовано, оскільки вона належить до особливого простору назв, що не дозволяє створення сторінок.",
        "import-error-invalid": "Сторінку «$1» не було імпортовано, оскільки назва, у яку вона імпортується, неприпустима у цій вікі.",
-       "import-error-unserialize": "Версія $2 сторінки «$1» не може бути деструктурованою (десеріалізованою). Отримано повідомлення, що у цій версії використано модель $3 сериалізована як $4.",
+       "import-error-unserialize": "Версія $2 сторінки «$1» не може бути деструктурованою (десеріалізованою). Отримано повідомлення, що у цій версії використано модель вмісту $3 сериалізовану як $4.",
        "import-error-bad-location": "Версія $2, що використовує модель вмісту $3, не може бути збережена у «$1» цієї вікі, тому що ця модель не підтримується на цій сторінці.",
        "import-options-wrong": "{{PLURAL:$2|1=Неправильна опція|Неправильні опції}}: <nowiki>$1</nowiki>",
        "import-rootpage-invalid": "Вказана некоректна назва кореневої сторінки",
        "sessionprovider-nocookies": "Куки можуть бути відключені. Переконайтеся, що у Вас включені cookies і почніть знову.",
        "randomrootpage": "Випадкова коренева сторінка",
        "log-action-filter-block": "Тип блокування:",
-       "log-action-filter-contentmodel": "Тип Ð·Ð¼Ñ\96ни ÐºÐ¾Ð½Ñ\82енÑ\82ноÑ\97 Ð¼Ð¾Ð´ÐµÐ»Ñ\96:",
+       "log-action-filter-contentmodel": "Тип Ð·Ð¼Ñ\96ни Ð¼Ð¾Ð´ÐµÐ»Ñ\96 Ð²Ð¼Ñ\96Ñ\81Ñ\82Ñ\83:",
        "log-action-filter-delete": "Тип вилучення:",
        "log-action-filter-import": "Тип імпорту:",
        "log-action-filter-managetags": "Тип дії з управління тегами:",
        "log-action-filter-block-block": "Блокування",
        "log-action-filter-block-reblock": "Зміна блокування",
        "log-action-filter-block-unblock": "Розблокування",
-       "log-action-filter-contentmodel-change": "Ð\97мÑ\96на ÐºÐ¾Ð½Ñ\82енÑ\82ноÑ\97 Ð¼Ð¾Ð´ÐµÐ»Ñ\96",
-       "log-action-filter-contentmodel-new": "СÑ\82воÑ\80еннÑ\8f Ñ\81Ñ\82оÑ\80Ñ\96нки Ð· Ð½ÐµÑ\81Ñ\82андаÑ\80Ñ\82ноÑ\8e ÐºÐ¾Ð½Ñ\82енÑ\82ноÑ\8e Ð¼Ð¾Ð´ÐµÐ»Ð»Ñ\8e",
+       "log-action-filter-contentmodel-change": "Ð\97мÑ\96на Ð¼Ð¾Ð´ÐµÐ»Ñ\96 Ð²Ð¼Ñ\96Ñ\81Ñ\82Ñ\83",
+       "log-action-filter-contentmodel-new": "СÑ\82воÑ\80еннÑ\8f Ñ\81Ñ\82оÑ\80Ñ\96нки Ð· Ð½ÐµÑ\81Ñ\82андаÑ\80Ñ\82ноÑ\8e Ð¼Ð¾Ð´ÐµÐ»Ð»Ñ\8e Ð²Ð¼Ñ\96Ñ\81Ñ\82Ñ\83",
        "log-action-filter-delete-delete": "Видалення сторінки",
        "log-action-filter-delete-delete_redir": "Перезапис перенаправлення",
        "log-action-filter-delete-restore": "Відновлення сторінки",
index 93db9ca..a65b321 100644 (file)
        "permanentlink": "固定链接",
        "permanentlink-revid": "修订版本ID",
        "permanentlink-submit": "前往修订版本",
-       "newsection": "新会话",
+       "newsection": "新章节",
        "newsection-page": "目标页面",
-       "newsection-submit": "跳转页",
+       "newsection-submit": "前往页面",
        "dberr-problems": "抱歉!本网站出现了一些技术问题。",
        "dberr-again": "请等待几分钟后重试。",
        "dberr-info": "(无法访问数据库:$1)",
index dd764d1..208449b 100644 (file)
        "logentry-partialblock-block-ns": "{{PLURAL:$1|命名空間|命名空間}}$2",
        "logentry-partialblock-block": "$1{{GENDER:$2|已封鎖}}{{GENDER:$4|$3}}對於$7的編輯為期限至 $5 $6",
        "logentry-partialblock-reblock": "$1{{GENDER:$2|已變更}}在$7的{{GENDER:$4|$3}}禁止編輯封鎖設定為期限至 $5 $6",
-       "logentry-non-editing-block-block": "$1{{GENDER:$2|已封鎖}}{{GENDER:$4|$3}}的指定編輯操作至期限$5 $6",
+       "logentry-non-editing-block-block": "$1{{GENDER:$2|已封鎖}}{{GENDER:$4|$3}}的指定非編輯操作至期限$5$6",
        "logentry-non-editing-block-reblock": "$1{{GENDER:$2|已變更}}{{GENDER:$4|$3}}的指定非編輯操作之封鎖設定,到期時間為$5 $6",
        "logentry-suppress-block": "$1 {{GENDER:$2|已封鎖}} {{GENDER:$4|$3}} 期限為 $5 $6",
        "logentry-suppress-reblock": "$1 {{GENDER:$2|已變更}} {{GENDER:$4|$3}} 的封鎖設定期限為 $5 $6",
index e5a8d98..1a5daca 100644 (file)
@@ -55,13 +55,17 @@ ALIASES                = "type{1}=<b> \1 </b>:" \
                          "protected=\access protected" \
                          "copyright=\note" \
                          "license=\note" \
+                         "inheritDoc=\inheritdoc" \
                          "codeCoverageIgnore=" \
+                         "codingStandardsIgnoreEnd=" \
                          "codingStandardsIgnoreStart=" \
-                         "group=" \
                          "covers=" \
                          "dataProvider=" \
                          "expectedException=" \
-                         "expectedExceptionMessage="
+                         "expectedExceptionMessage=" \
+                         "group=" \
+                         "phan=" \
+                         "suppress="
 TCL_SUBST              =
 OPTIMIZE_OUTPUT_FOR_C  = NO
 OPTIMIZE_OUTPUT_JAVA   = NO
@@ -232,63 +236,6 @@ SEARCHDATA_FILE        = searchdata.xml
 EXTERNAL_SEARCH_ID     =
 EXTRA_SEARCH_MAPPINGS  =
 #---------------------------------------------------------------------------
-# Configuration options related to the LaTeX output
-#---------------------------------------------------------------------------
-GENERATE_LATEX         = NO
-LATEX_OUTPUT           = latex
-LATEX_CMD_NAME         = latex
-MAKEINDEX_CMD_NAME     = makeindex
-COMPACT_LATEX          = NO
-PAPER_TYPE             = a4wide
-EXTRA_PACKAGES         =
-LATEX_HEADER           =
-LATEX_FOOTER           =
-LATEX_EXTRA_FILES      =
-PDF_HYPERLINKS         = YES
-USE_PDFLATEX           = YES
-LATEX_BATCHMODE        = NO
-LATEX_HIDE_INDICES     = NO
-LATEX_SOURCE_CODE      = NO
-LATEX_BIB_STYLE        = plain
-#---------------------------------------------------------------------------
-# Configuration options related to the RTF output
-#---------------------------------------------------------------------------
-GENERATE_RTF           = NO
-RTF_OUTPUT             = rtf
-COMPACT_RTF            = NO
-RTF_HYPERLINKS         = NO
-RTF_STYLESHEET_FILE    =
-RTF_EXTENSIONS_FILE    =
-#---------------------------------------------------------------------------
-# Configuration options related to the man page output
-#---------------------------------------------------------------------------
-GENERATE_MAN           = {{GENERATE_MAN}}
-MAN_OUTPUT             = man
-MAN_EXTENSION          = .3
-MAN_LINKS              = NO
-#---------------------------------------------------------------------------
-# Configuration options related to the XML output
-#---------------------------------------------------------------------------
-GENERATE_XML           = NO
-XML_OUTPUT             = xml
-XML_PROGRAMLISTING     = YES
-#---------------------------------------------------------------------------
-# Configuration options related to the DOCBOOK output
-#---------------------------------------------------------------------------
-GENERATE_DOCBOOK       = NO
-DOCBOOK_OUTPUT         = docbook
-#---------------------------------------------------------------------------
-# Configuration options for the AutoGen Definitions output
-#---------------------------------------------------------------------------
-GENERATE_AUTOGEN_DEF   = NO
-#---------------------------------------------------------------------------
-# Configuration options related to the Perl module output
-#---------------------------------------------------------------------------
-GENERATE_PERLMOD       = NO
-PERLMOD_LATEX          = NO
-PERLMOD_PRETTY         = YES
-PERLMOD_MAKEVAR_PREFIX =
-#---------------------------------------------------------------------------
 # Configuration options related to the preprocessor
 #---------------------------------------------------------------------------
 ENABLE_PREPROCESSING   = YES
index 23c46bc..26f8182 100644 (file)
@@ -100,10 +100,10 @@ class ConvertLinks extends Maintenance {
                # not used yet; highest row number from links table to process
                # $finalRowOffset = 0;
 
+               $this->logPerformance = $this->hasOption( 'logperformance' );
+               $perfLogFilename = $this->getArg( 1, "convLinksPerf.txt" );
                $overwriteLinksTable = !$this->hasOption( 'keep-links-table' );
                $noKeys = $this->hasOption( 'noKeys' );
-               $this->logPerformance = $this->hasOption( 'logperformance' );
-               $perfLogFilename = $this->getArg( 'perfLogFilename', "convLinksPerf.txt" );
 
                # --------------------------------------------------------------------
 
index 4065978..954f36d 100644 (file)
  * @author Mij <mij@bitchx.it>
  */
 
-use MediaWiki\MediaWikiServices;
-
 require_once __DIR__ . '/Maintenance.php';
 
+use MediaWiki\MediaWikiServices;
+
 class ImportImages extends Maintenance {
 
        public function __construct() {
@@ -127,6 +127,8 @@ class ImportImages extends Maintenance {
        public function execute() {
                global $wgFileExtensions, $wgUser, $wgRestrictionLevels;
 
+               $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+
                $processed = $added = $ignored = $skipped = $overwritten = $failed = 0;
 
                $this->output( "Importing Files\n\n" );
@@ -198,7 +200,8 @@ class ImportImages extends Maintenance {
                                $title = Title::makeTitleSafe( NS_FILE, $base );
                                if ( !is_object( $title ) ) {
                                        $this->output(
-                                               "{$base} could not be imported; a valid title cannot be produced\n" );
+                                               "{$base} could not be imported; a valid title cannot be produced\n"
+                                       );
                                        continue;
                                }
 
@@ -213,10 +216,12 @@ class ImportImages extends Maintenance {
 
                                if ( $checkUserBlock && ( ( $processed % $checkUserBlock ) == 0 ) ) {
                                        $user->clearInstanceCache( 'name' ); // reload from DB!
-                                       // @TODO Use PermissionManager::isBlockedFrom() instead.
-                                       if ( $user->getBlock() ) {
-                                               $this->output( $user->getName() . " was blocked! Aborting.\n" );
-                                               break;
+                                       if ( $permissionManager->isBlockedFrom( $user, $title ) ) {
+                                               $this->output(
+                                                       "{$user->getName()} is blocked from {$title->getPrefixedText()}! skipping.\n"
+                                               );
+                                               $skipped++;
+                                               continue;
                                        }
                                }
 
@@ -242,7 +247,8 @@ class ImportImages extends Maintenance {
 
                                                if ( $dupes ) {
                                                        $this->output(
-                                                               "{$base} already exists as {$dupes[0]->getName()}, skipping\n" );
+                                                               "{$base} already exists as {$dupes[0]->getName()}, skipping\n"
+                                                       );
                                                        $skipped++;
                                                        continue;
                                                }
@@ -270,7 +276,8 @@ class ImportImages extends Maintenance {
                                                if ( $wgUser === false ) {
                                                        # user does not exist in target wiki
                                                        $this->output(
-                                                               "failed: user '$real_user' does not exist in target wiki." );
+                                                               "failed: user '$real_user' does not exist in target wiki."
+                                                       );
                                                        continue;
                                                }
                                        }
@@ -287,7 +294,8 @@ class ImportImages extends Maintenance {
                                                        $commentText = file_get_contents( $f );
                                                        if ( !$commentText ) {
                                                                $this->output(
-                                                                       " Failed to load comment file {$f}, using default comment. " );
+                                                                       " Failed to load comment file {$f}, using default comment. "
+                                                               );
                                                        }
                                                }
                                        }
index 791b360..4a50cc5 100644 (file)
@@ -56,8 +56,6 @@ class MWDocGen extends Maintenance {
                $this->addOption( 'version',
                        'Pass a MediaWiki version',
                        false, true );
-               $this->addOption( 'generate-man',
-                       'Whether to generate man files' );
                $this->addOption( 'file',
                        "Only process given file or directory. Multiple values " .
                        "accepted with comma separation. Path relative to \$IP.",
@@ -99,6 +97,7 @@ class MWDocGen extends Maintenance {
                $this->excludes = [
                        'vendor',
                        'node_modules',
+                       'resources/lib',
                        'images',
                        'static',
                ];
@@ -108,7 +107,6 @@ class MWDocGen extends Maintenance {
                }
 
                $this->doDot = shell_exec( 'which dot' );
-               $this->doMan = $this->hasOption( 'generate-man' );
        }
 
        public function execute() {
@@ -133,7 +131,6 @@ class MWDocGen extends Maintenance {
                                '{{EXCLUDE}}' => $exclude,
                                '{{EXCLUDE_PATTERNS}}' => $excludePatterns,
                                '{{HAVE_DOT}}' => $this->doDot ? 'YES' : 'NO',
-                               '{{GENERATE_MAN}}' => $this->doMan ? 'YES' : 'NO',
                                '{{INPUT_FILTER}}' => $this->inputFilter,
                        ]
                );
index 2cdf418..d484392 100644 (file)
@@ -43,18 +43,18 @@ class RebuildFileCache extends Maintenance {
        }
 
        public function finalSetup() {
-               global $wgDebugToolbar, $wgUseFileCache;
+               global $wgUseFileCache;
 
                $this->enabled = $wgUseFileCache;
                // Script will handle capturing output and saving it itself
                $wgUseFileCache = false;
-               // Debug toolbar makes content uncacheable so we disable it.
-               // Has to be done before Setup.php initialize MWDebug
-               $wgDebugToolbar = false;
                //  Avoid DB writes (like enotif/counters)
                MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode()
                        ->setReason( 'Building cache' );
 
+               // Ensure no debug-specific logic ends up in the cache (must be after Setup.php)
+               MWDebug::deinit();
+
                parent::finalSetup();
        }
 
index ff29ebf..1a05e81 100644 (file)
                </testsuite>
                <testsuite name="extensions:unit">
                        <directory>extensions/**/tests/phpunit/unit</directory>
+                       <directory>extensions/**/tests/phpunit/Unit</directory>
                </testsuite>
                <testsuite name="skins:unit">
                        <directory>skins/**/tests/phpunit/unit</directory>
+                       <directory>skins/**/tests/phpunit/Unit</directory>
                </testsuite>
                <testsuite name="core:integration">
                        <directory>tests/phpunit/integration</directory>
index 8234e89..cfcfc19 100644 (file)
@@ -156,12 +156,10 @@ return [
        /* jQuery Plugins */
 
        'jquery.accessKeyLabel' => [
-               'scripts' => 'resources/src/jquery/jquery.accessKeyLabel.js',
+               'deprecated' => 'Please use "mediawiki.util" instead.',
                'dependencies' => [
-                       'jquery.client',
-                       'mediawiki.RegExp',
+                       'mediawiki.util',
                ],
-               'messages' => [ 'brackets', 'word-separator' ],
                'targets' => [ 'mobile', 'desktop' ],
        ],
        'jquery.checkboxShiftClick' => [
@@ -942,9 +940,6 @@ return [
                'scripts' => [
                        'resources/src/mediawiki.htmlform.checker.js',
                ],
-               'dependencies' => [
-                       'jquery.throttle-debounce',
-               ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.htmlform.ooui' => [
@@ -1254,19 +1249,21 @@ return [
                ]
        ],
        'mediawiki.util' => [
-               'localBasePath' => "$IP/resources/src",
-               'remoteBasePath' => "$wgResourceBasePath/resources/src",
+               'localBasePath' => "$IP/resources/src/mediawiki.util/",
+               'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.util/",
                'packageFiles' => [
-                       'mediawiki.util.js',
+                       'util.js',
+                       'jquery.accessKeyLabel.js',
                        [ 'name' => 'config.json', 'config' => [
                                'FragmentMode',
                                'LoadScript',
                        ] ],
                ],
                'dependencies' => [
-                       'jquery.accessKeyLabel',
+                       'jquery.client',
                        'mediawiki.RegExp',
                ],
+               'messages' => [ 'brackets', 'word-separator' ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.viewport' => [
@@ -1663,8 +1660,8 @@ return [
        'mediawiki.page.ready' => [
                'scripts' => 'resources/src/mediawiki.page.ready.js',
                'dependencies' => [
-                       'jquery.accessKeyLabel',
                        'jquery.checkboxShiftClick',
+                       'mediawiki.util',
                        'mediawiki.notify',
                        'mediawiki.api'
                ],
@@ -1702,7 +1699,6 @@ return [
                        'mediawiki.util',
                        'mediawiki.Title',
                        'mediawiki.jqueryMsg',
-                       'jquery.accessKeyLabel',
                        'mediawiki.RegExp',
                ],
                'messages' => [
@@ -1860,11 +1856,7 @@ return [
                        'styles/mw.rcfilters.ui.FilterTagMultiselectWidgetMobile.less'
                ],
                'skinStyles' => [
-                       'vector' => [
-                               'styles/mw.rcfilters.ui.Overlay.vector.less',
-                       ],
                        'monobook' => [
-                               'styles/mw.rcfilters.ui.Overlay.monobook.less',
                                'styles/mw.rcfilters.ui.CapsuleItemWidget.monobook.less',
                                'styles/mw.rcfilters.ui.FilterMenuOptionWidget.monobook.less',
                        ],
diff --git a/resources/src/jquery/jquery.accessKeyLabel.js b/resources/src/jquery/jquery.accessKeyLabel.js
deleted file mode 100644 (file)
index cdc5808..0000000
+++ /dev/null
@@ -1,239 +0,0 @@
-/**
- * jQuery plugin to update the tooltip to show the correct access key
- *
- * @class jQuery.plugin.accessKeyLabel
- */
-( function () {
-
-       // Cached access key modifiers for used browser
-       var cachedAccessKeyModifiers,
-
-               // Whether to use 'test-' instead of correct prefix (used for testing)
-               useTestPrefix = false,
-
-               // tag names which can have a label tag
-               // https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Form-associated_content
-               labelable = 'button, input, textarea, keygen, meter, output, progress, select';
-
-       /**
-        * Find the modifier keys that need to be pressed together with the accesskey to trigger the input.
-        *
-        * The result is dependant on the ua paramater or the current platform.
-        * For browsers that support accessKeyLabel, #getAccessKeyLabel never calls here.
-        * Valid key values that are returned can be: ctrl, alt, option, shift, esc
-        *
-        * @private
-        * @param {Object} [ua] An object with a 'userAgent' and 'platform' property.
-        * @return {Array} Array with 1 or more of the string values, in this order: ctrl, option, alt, shift, esc
-        */
-       function getAccessKeyModifiers( ua ) {
-               var profile, accessKeyModifiers;
-
-               // use cached prefix if possible
-               if ( !ua && cachedAccessKeyModifiers ) {
-                       return cachedAccessKeyModifiers;
-               }
-
-               profile = $.client.profile( ua );
-
-               switch ( profile.name ) {
-                       case 'chrome':
-                       case 'opera':
-                               if ( profile.name === 'opera' && profile.versionNumber < 15 ) {
-                                       accessKeyModifiers = [ 'shift', 'esc' ];
-                               } else if ( profile.platform === 'mac' ) {
-                                       accessKeyModifiers = [ 'ctrl', 'option' ];
-                               } else {
-                                       // Chrome/Opera on Windows or Linux
-                                       // (both alt- and alt-shift work, but alt with E, D, F etc does not
-                                       // work since they are browser shortcuts)
-                                       accessKeyModifiers = [ 'alt', 'shift' ];
-                               }
-                               break;
-                       case 'firefox':
-                       case 'iceweasel':
-                               if ( profile.versionBase < 2 ) {
-                                       // Before v2, Firefox used alt, though it was rebindable in about:config
-                                       accessKeyModifiers = [ 'alt' ];
-                               } else {
-                                       if ( profile.platform === 'mac' ) {
-                                               if ( profile.versionNumber < 14 ) {
-                                                       accessKeyModifiers = [ 'ctrl' ];
-                                               } else {
-                                                       accessKeyModifiers = [ 'ctrl', 'option' ];
-                                               }
-                                       } else {
-                                               accessKeyModifiers = [ 'alt', 'shift' ];
-                                       }
-                               }
-                               break;
-                       case 'safari':
-                       case 'konqueror':
-                               if ( profile.platform === 'win' ) {
-                                       accessKeyModifiers = [ 'alt' ];
-                               } else {
-                                       if ( profile.layoutVersion > 526 ) {
-                                               // Non-Windows Safari with webkit_version > 526
-                                               accessKeyModifiers = [ 'ctrl', profile.platform === 'mac' ? 'option' : 'alt' ];
-                                       } else {
-                                               accessKeyModifiers = [ 'ctrl' ];
-                                       }
-                               }
-                               break;
-                       case 'msie':
-                       case 'edge':
-                               accessKeyModifiers = [ 'alt' ];
-                               break;
-                       default:
-                               accessKeyModifiers = profile.platform === 'mac' ? [ 'ctrl' ] : [ 'alt' ];
-                               break;
-               }
-
-               // cache modifiers
-               if ( !ua ) {
-                       cachedAccessKeyModifiers = accessKeyModifiers;
-               }
-               return accessKeyModifiers;
-       }
-
-       /**
-        * Get the access key label for an element.
-        *
-        * Will use native accessKeyLabel if available (currently only in Firefox 8+),
-        * falls back to #getAccessKeyModifiers.
-        *
-        * @private
-        * @param {HTMLElement} element Element to get the label for
-        * @return {string} Access key label
-        */
-       function getAccessKeyLabel( element ) {
-               // abort early if no access key
-               if ( !element.accessKey ) {
-                       return '';
-               }
-               // use accessKeyLabel if possible
-               // https://html.spec.whatwg.org/multipage/interaction.html#dom-accesskeylabel
-               if ( !useTestPrefix && element.accessKeyLabel ) {
-                       return element.accessKeyLabel;
-               }
-               return ( useTestPrefix ? 'test' : getAccessKeyModifiers().join( '-' ) ) + '-' + element.accessKey;
-       }
-
-       /**
-        * Update the title for an element (on the element with the access key or it's label) to show
-        * the correct access key label.
-        *
-        * @private
-        * @param {HTMLElement} element Element with the accesskey
-        * @param {HTMLElement} titleElement Element with the title to update (may be the same as `element`)
-        */
-       function updateTooltipOnElement( element, titleElement ) {
-               var oldTitle, parts, regexp, newTitle, accessKeyLabel,
-                       separatorMsg = mw.message( 'word-separator' ).plain();
-
-               oldTitle = titleElement.title;
-               if ( !oldTitle ) {
-                       // don't add a title if the element didn't have one before
-                       return;
-               }
-
-               parts = ( separatorMsg + mw.message( 'brackets' ).plain() ).split( '$1' );
-               regexp = new RegExp( parts.map( mw.RegExp.escape ).join( '.*?' ) + '$' );
-               newTitle = oldTitle.replace( regexp, '' );
-               accessKeyLabel = getAccessKeyLabel( element );
-
-               if ( accessKeyLabel ) {
-                       // Should be build the same as in Linker::titleAttrib
-                       newTitle += separatorMsg + mw.message( 'brackets', accessKeyLabel ).plain();
-               }
-               if ( oldTitle !== newTitle ) {
-                       titleElement.title = newTitle;
-               }
-       }
-
-       /**
-        * Update the title for an element to show the correct access key label.
-        *
-        * @private
-        * @param {HTMLElement} element Element with the accesskey
-        */
-       function updateTooltip( element ) {
-               var id, $element, $label, $labelParent;
-               updateTooltipOnElement( element, element );
-
-               // update associated label if there is one
-               $element = $( element );
-               if ( $element.is( labelable ) ) {
-                       // Search it using 'for' attribute
-                       id = element.id.replace( /"/g, '\\"' );
-                       if ( id ) {
-                               $label = $( 'label[for="' + id + '"]' );
-                               if ( $label.length === 1 ) {
-                                       updateTooltipOnElement( element, $label[ 0 ] );
-                               }
-                       }
-
-                       // Search it as parent, because the form control can also be inside the label element itself
-                       $labelParent = $element.parents( 'label' );
-                       if ( $labelParent.length === 1 ) {
-                               updateTooltipOnElement( element, $labelParent[ 0 ] );
-                       }
-               }
-       }
-
-       /**
-        * Update the titles for all elements in a jQuery selection.
-        *
-        * @return {jQuery}
-        * @chainable
-        */
-       $.fn.updateTooltipAccessKeys = function () {
-               return this.each( function () {
-                       updateTooltip( this );
-               } );
-       };
-
-       /**
-        * getAccessKeyModifiers
-        *
-        * @method updateTooltipAccessKeys_getAccessKeyModifiers
-        * @inheritdoc #getAccessKeyModifiers
-        */
-       $.fn.updateTooltipAccessKeys.getAccessKeyModifiers = getAccessKeyModifiers;
-
-       /**
-        * getAccessKeyLabel
-        *
-        * @method updateTooltipAccessKeys_getAccessKeyLabel
-        * @inheritdoc #getAccessKeyLabel
-        */
-       $.fn.updateTooltipAccessKeys.getAccessKeyLabel = getAccessKeyLabel;
-
-       /**
-        * getAccessKeyPrefix
-        *
-        * @method updateTooltipAccessKeys_getAccessKeyPrefix
-        * @deprecated since 1.27 Use #getAccessKeyModifiers
-        * @param {Object} [ua] An object with a 'userAgent' and 'platform' property.
-        * @return {string}
-        */
-       $.fn.updateTooltipAccessKeys.getAccessKeyPrefix = function ( ua ) {
-               return getAccessKeyModifiers( ua ).join( '-' ) + '-';
-       };
-
-       /**
-        * Switch test mode on and off.
-        *
-        * @method updateTooltipAccessKeys_setTestMode
-        * @param {boolean} mode New mode
-        */
-       $.fn.updateTooltipAccessKeys.setTestMode = function ( mode ) {
-               useTestPrefix = mode;
-       };
-
-       /**
-        * @class jQuery
-        * @mixins jQuery.plugin.accessKeyLabel
-        */
-
-}() );
index 674584b..33207d4 100644 (file)
@@ -3,6 +3,14 @@
        // FIXME: mw.htmlform.Element also sets this to empty object
        mw.htmlform = {};
 
+       function debounce( delay, callback ) {
+               var timeout;
+               return function () {
+                       clearTimeout( timeout );
+                       timeout = setTimeout( Function.prototype.apply.bind( callback, this, arguments ), delay );
+               };
+       }
+
        /**
         * @class mw.htmlform.Checker
         */
@@ -52,7 +60,7 @@
                if ( $extraElements ) {
                        $e = $e.add( $extraElements );
                }
-               $e.on( events, $.debounce( 1000, this.validate.bind( this ) ) );
+               $e.on( events, debounce( 1000, this.validate.bind( this ) ) );
 
                return this;
        };
index 06840da..c883309 100644 (file)
@@ -1,8 +1,6 @@
 .mw-rcfilters-ui-overlay {
-       font-size: 0.875em;
        position: absolute;
        top: 0;
        right: 0;
        left: 0;
-       z-index: 1;
 }
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.monobook.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.monobook.less
deleted file mode 100644 (file)
index fae2b32..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-.mw-rcfilters-ui-overlay {
-       font-size: 1.28em; /* 0.8em / x-small */
-}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.vector.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.vector.less
deleted file mode 100644 (file)
index 528707b..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-// Fix z-index for the overlay in Vector, see T183442
-.mw-rcfilters-ui-overlay {
-       z-index: 101;
-}
index 31edb77..85794cd 100644 (file)
@@ -42,7 +42,7 @@ MainWrapperWidget = function MwRcfiltersUiMainWrapperWidget(
        this.$filtersContainer = config.$filtersContainer;
        this.$changesListContainer = config.$changesListContainer;
        this.$formContainer = config.$formContainer;
-       this.$overlay = $( '<div>' ).addClass( 'mw-rcfilters-ui-overlay' );
+       this.$overlay = $( '<div>' ).addClass( 'mw-rcfilters-ui-overlay oo-ui-defaultOverlay' );
        this.$wrapper = config.$wrapper || this.$element;
 
        this.savedLinksListWidget = new SavedLinksListWidget(
diff --git a/resources/src/mediawiki.util.js b/resources/src/mediawiki.util.js
deleted file mode 100644 (file)
index 36a0195..0000000
+++ /dev/null
@@ -1,565 +0,0 @@
-( function () {
-       'use strict';
-
-       var util,
-               config = require( './config.json' );
-
-       /**
-        * Encode the string like PHP's rawurlencode
-        * @ignore
-        *
-        * @param {string} str String to be encoded.
-        * @return {string} Encoded string
-        */
-       function rawurlencode( str ) {
-               str = String( str );
-               return encodeURIComponent( str )
-                       .replace( /!/g, '%21' ).replace( /'/g, '%27' ).replace( /\(/g, '%28' )
-                       .replace( /\)/g, '%29' ).replace( /\*/g, '%2A' ).replace( /~/g, '%7E' );
-       }
-
-       /**
-        * Private helper function used by util.escapeId*()
-        * @ignore
-        *
-        * @param {string} str String to be encoded
-        * @param {string} mode Encoding mode, see documentation for $wgFragmentMode
-        *     in DefaultSettings.php
-        * @return {string} Encoded string
-        */
-       function escapeIdInternal( str, mode ) {
-               str = String( str );
-
-               switch ( mode ) {
-                       case 'html5':
-                               return str.replace( / /g, '_' );
-                       case 'legacy':
-                               return rawurlencode( str.replace( / /g, '_' ) )
-                                       .replace( /%3A/g, ':' )
-                                       .replace( /%/g, '.' );
-                       default:
-                               throw new Error( 'Unrecognized ID escaping mode ' + mode );
-               }
-       }
-
-       /**
-        * Utility library
-        * @class mw.util
-        * @singleton
-        */
-       util = {
-
-               /**
-                * Encode the string like PHP's rawurlencode
-                *
-                * @param {string} str String to be encoded.
-                * @return {string} Encoded string
-                */
-               rawurlencode: rawurlencode,
-
-               /**
-                * Encode string into HTML id compatible form suitable for use in HTML
-                * Analog to PHP Sanitizer::escapeIdForAttribute()
-                *
-                * @since 1.30
-                *
-                * @param {string} str String to encode
-                * @return {string} Encoded string
-                */
-               escapeIdForAttribute: function ( str ) {
-                       var mode = config.FragmentMode[ 0 ];
-
-                       return escapeIdInternal( str, mode );
-               },
-
-               /**
-                * Encode string into HTML id compatible form suitable for use in links
-                * Analog to PHP Sanitizer::escapeIdForLink()
-                *
-                * @since 1.30
-                *
-                * @param {string} str String to encode
-                * @return {string} Encoded string
-                */
-               escapeIdForLink: function ( str ) {
-                       var mode = config.FragmentMode[ 0 ];
-
-                       return escapeIdInternal( str, mode );
-               },
-
-               /**
-                * Encode page titles for use in a URL
-                *
-                * We want / and : to be included as literal characters in our title URLs
-                * as they otherwise fatally break the title.
-                *
-                * The others are decoded because we can, it's prettier and matches behaviour
-                * of `wfUrlencode` in PHP.
-                *
-                * @param {string} str String to be encoded.
-                * @return {string} Encoded string
-                */
-               wikiUrlencode: function ( str ) {
-                       return util.rawurlencode( str )
-                               .replace( /%20/g, '_' )
-                               // wfUrlencode replacements
-                               .replace( /%3B/g, ';' )
-                               .replace( /%40/g, '@' )
-                               .replace( /%24/g, '$' )
-                               .replace( /%21/g, '!' )
-                               .replace( /%2A/g, '*' )
-                               .replace( /%28/g, '(' )
-                               .replace( /%29/g, ')' )
-                               .replace( /%2C/g, ',' )
-                               .replace( /%2F/g, '/' )
-                               .replace( /%7E/g, '~' )
-                               .replace( /%3A/g, ':' );
-               },
-
-               /**
-                * Get the link to a page name (relative to `wgServer`),
-                *
-                * @param {string|null} [pageName=wgPageName] Page name
-                * @param {Object} [params] A mapping of query parameter names to values,
-                *  e.g. `{ action: 'edit' }`
-                * @return {string} Url of the page with name of `pageName`
-                */
-               getUrl: function ( pageName, params ) {
-                       var titleFragmentStart, url, query,
-                               fragment = '',
-                               title = typeof pageName === 'string' ? pageName : mw.config.get( 'wgPageName' );
-
-                       // Find any fragment
-                       titleFragmentStart = title.indexOf( '#' );
-                       if ( titleFragmentStart !== -1 ) {
-                               fragment = title.slice( titleFragmentStart + 1 );
-                               // Exclude the fragment from the page name
-                               title = title.slice( 0, titleFragmentStart );
-                       }
-
-                       // Produce query string
-                       if ( params ) {
-                               query = $.param( params );
-                       }
-                       if ( query ) {
-                               url = title ?
-                                       util.wikiScript() + '?title=' + util.wikiUrlencode( title ) + '&' + query :
-                                       util.wikiScript() + '?' + query;
-                       } else {
-                               url = mw.config.get( 'wgArticlePath' )
-                                       .replace( '$1', util.wikiUrlencode( title ).replace( /\$/g, '$$$$' ) );
-                       }
-
-                       // Append the encoded fragment
-                       if ( fragment.length ) {
-                               url += '#' + util.escapeIdForLink( fragment );
-                       }
-
-                       return url;
-               },
-
-               /**
-                * Get address to a script in the wiki root.
-                * For index.php use `mw.config.get( 'wgScript' )`.
-                *
-                * @since 1.18
-                * @param {string} str Name of script (e.g. 'api'), defaults to 'index'
-                * @return {string} Address to script (e.g. '/w/api.php' )
-                */
-               wikiScript: function ( str ) {
-                       str = str || 'index';
-                       if ( str === 'index' ) {
-                               return mw.config.get( 'wgScript' );
-                       } else if ( str === 'load' ) {
-                               return config.LoadScript;
-                       } else {
-                               return mw.config.get( 'wgScriptPath' ) + '/' + str + '.php';
-                       }
-               },
-
-               /**
-                * Append a new style block to the head and return the CSSStyleSheet object.
-                * Use .ownerNode to access the `<style>` element, or use mw.loader#addStyleTag.
-                * This function returns the styleSheet object for convience (due to cross-browsers
-                * difference as to where it is located).
-                *
-                *     var sheet = util.addCSS( '.foobar { display: none; }' );
-                *     $( foo ).click( function () {
-                *         // Toggle the sheet on and off
-                *         sheet.disabled = !sheet.disabled;
-                *     } );
-                *
-                * @param {string} text CSS to be appended
-                * @return {CSSStyleSheet} Use .ownerNode to get to the `<style>` element.
-                */
-               addCSS: function ( text ) {
-                       var s = mw.loader.addStyleTag( text );
-                       return s.sheet || s.styleSheet || s;
-               },
-
-               /**
-                * Grab the URL parameter value for the given parameter.
-                * Returns null if not found.
-                *
-                * @param {string} param The parameter name.
-                * @param {string} [url=location.href] URL to search through, defaulting to the current browsing location.
-                * @return {Mixed} Parameter value or null.
-                */
-               getParamValue: function ( param, url ) {
-                       // Get last match, stop at hash
-                       var re = new RegExp( '^[^#]*[&?]' + mw.RegExp.escape( param ) + '=([^&#]*)' ),
-                               m = re.exec( url !== undefined ? url : location.href );
-
-                       if ( m ) {
-                               // Beware that decodeURIComponent is not required to understand '+'
-                               // by spec, as encodeURIComponent does not produce it.
-                               return decodeURIComponent( m[ 1 ].replace( /\+/g, '%20' ) );
-                       }
-                       return null;
-               },
-
-               /**
-                * The content wrapper of the skin (e.g. `.mw-body`).
-                *
-                * Populated on document ready. To use this property,
-                * wait for `$.ready` and be sure to have a module dependency on
-                * `mediawiki.util` which will ensure
-                * your document ready handler fires after initialization.
-                *
-                * Because of the lazy-initialised nature of this property,
-                * you're discouraged from using it.
-                *
-                * If you need just the wikipage content (not any of the
-                * extra elements output by the skin), use `$( '#mw-content-text' )`
-                * instead. Or listen to mw.hook#wikipage_content which will
-                * allow your code to re-run when the page changes (e.g. live preview
-                * or re-render after ajax save).
-                *
-                * @property {jQuery}
-                */
-               $content: null,
-
-               /**
-                * Add a link to a portlet menu on the page, such as:
-                *
-                * p-cactions (Content actions), p-personal (Personal tools),
-                * p-navigation (Navigation), p-tb (Toolbox)
-                *
-                * The first three parameters are required, the others are optional and
-                * may be null. Though providing an id and tooltip is recommended.
-                *
-                * By default the new link will be added to the end of the list. To
-                * add the link before a given existing item, pass the DOM node
-                * (e.g. `document.getElementById( 'foobar' )`) or a jQuery-selector
-                * (e.g. `'#foobar'`) for that item.
-                *
-                *     util.addPortletLink(
-                *         'p-tb', 'https://www.mediawiki.org/',
-                *         'mediawiki.org', 't-mworg', 'Go to mediawiki.org', 'm', '#t-print'
-                *     );
-                *
-                *     var node = util.addPortletLink(
-                *         'p-tb',
-                *         new mw.Title( 'Special:Example' ).getUrl(),
-                *         'Example'
-                *     );
-                *     $( node ).on( 'click', function ( e ) {
-                *         console.log( 'Example' );
-                *         e.preventDefault();
-                *     } );
-                *
-                * @param {string} portletId ID of the target portlet (e.g. 'p-cactions' or 'p-personal')
-                * @param {string} href Link URL
-                * @param {string} text Link text
-                * @param {string} [id] ID of the list item, should be unique and preferably have
-                *  the appropriate prefix ('ca-', 'pt-', 'n-' or 't-')
-                * @param {string} [tooltip] Text to show when hovering over the link, without accesskey suffix
-                * @param {string} [accesskey] Access key to activate this link. One character only,
-                *  avoid conflicts with other links. Use `$( '[accesskey=x]' )` in the console to
-                *  see if 'x' is already used.
-                * @param {HTMLElement|jQuery|string} [nextnode] Element that the new item should be added before.
-                *  Must be another item in the same list, it will be ignored otherwise.
-                *  Can be specified as DOM reference, as jQuery object, or as CSS selector string.
-                * @return {HTMLElement|null} The added list item, or null if no element was added.
-                */
-               addPortletLink: function ( portletId, href, text, id, tooltip, accesskey, nextnode ) {
-                       var item, link, $portlet, portlet, portletDiv, ul, next;
-
-                       if ( !portletId ) {
-                               // Avoid confusing id="undefined" lookup
-                               return null;
-                       }
-
-                       portlet = document.getElementById( portletId );
-                       if ( !portlet ) {
-                               // Invalid portlet ID
-                               return null;
-                       }
-
-                       // Setup the anchor tag and set any the properties
-                       link = document.createElement( 'a' );
-                       link.href = href;
-                       link.textContent = text;
-                       if ( tooltip ) {
-                               link.title = tooltip;
-                       }
-                       if ( accesskey ) {
-                               link.accessKey = accesskey;
-                       }
-
-                       // Unhide portlet if it was hidden before
-                       $portlet = $( portlet );
-                       $portlet.removeClass( 'emptyPortlet' );
-
-                       // Setup the list item (and a span if $portlet is a Vector tab)
-                       // eslint-disable-next-line no-jquery/no-class-state
-                       if ( $portlet.hasClass( 'vectorTabs' ) ) {
-                               item = $( '<li>' ).append( $( '<span>' ).append( link )[ 0 ] )[ 0 ];
-                       } else {
-                               item = $( '<li>' ).append( link )[ 0 ];
-                       }
-                       if ( id ) {
-                               item.id = id;
-                       }
-
-                       // Select the first (most likely only) unordered list inside the portlet
-                       ul = portlet.querySelector( 'ul' );
-                       if ( !ul ) {
-                               // If it didn't have an unordered list yet, create one
-                               ul = document.createElement( 'ul' );
-                               portletDiv = portlet.querySelector( 'div' );
-                               if ( portletDiv ) {
-                                       // Support: Legacy skins have a div (such as div.body or div.pBody).
-                                       // Append the <ul> to that.
-                                       portletDiv.appendChild( ul );
-                               } else {
-                                       // Append it to the portlet directly
-                                       portlet.appendChild( ul );
-                               }
-                       }
-
-                       if ( nextnode && ( typeof nextnode === 'string' || nextnode.nodeType || nextnode.jquery ) ) {
-                               nextnode = $( ul ).find( nextnode );
-                               if ( nextnode.length === 1 && nextnode[ 0 ].parentNode === ul ) {
-                                       // Insertion point: Before nextnode
-                                       nextnode.before( item );
-                                       next = true;
-                               }
-                               // Else: Invalid nextnode value (no match, more than one match, or not a direct child)
-                               // Else: Invalid nextnode type
-                       }
-
-                       if ( !next ) {
-                               // Insertion point: End of list (default)
-                               ul.appendChild( item );
-                       }
-
-                       // Update tooltip for the access key after inserting into DOM
-                       // to get a localized access key label (T69946).
-                       if ( accesskey ) {
-                               $( link ).updateTooltipAccessKeys();
-                       }
-
-                       return item;
-               },
-
-               /**
-                * Validate a string as representing a valid e-mail address
-                * according to HTML5 specification. Please note the specification
-                * does not validate a domain with one character.
-                *
-                * FIXME: should be moved to or replaced by a validation module.
-                *
-                * @param {string} mailtxt E-mail address to be validated.
-                * @return {boolean|null} Null if `mailtxt` was an empty string, otherwise true/false
-                * as determined by validation.
-                */
-               validateEmail: function ( mailtxt ) {
-                       var rfc5322Atext, rfc1034LdhStr, html5EmailRegexp;
-
-                       if ( mailtxt === '' ) {
-                               return null;
-                       }
-
-                       // HTML5 defines a string as valid e-mail address if it matches
-                       // the ABNF:
-                       //     1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
-                       // With:
-                       // - atext   : defined in RFC 5322 section 3.2.3
-                       // - ldh-str : defined in RFC 1034 section 3.5
-                       //
-                       // (see STD 68 / RFC 5234 https://tools.ietf.org/html/std68)
-                       // First, define the RFC 5322 'atext' which is pretty easy:
-                       // atext = ALPHA / DIGIT / ; Printable US-ASCII
-                       //     "!" / "#" /    ; characters not including
-                       //     "$" / "%" /    ; specials. Used for atoms.
-                       //     "&" / "'" /
-                       //     "*" / "+" /
-                       //     "-" / "/" /
-                       //     "=" / "?" /
-                       //     "^" / "_" /
-                       //     "`" / "{" /
-                       //     "|" / "}" /
-                       //     "~"
-                       rfc5322Atext = 'a-z0-9!#$%&\'*+\\-/=?^_`{|}~';
-
-                       // Next define the RFC 1034 'ldh-str'
-                       //     <domain> ::= <subdomain> | " "
-                       //     <subdomain> ::= <label> | <subdomain> "." <label>
-                       //     <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
-                       //     <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
-                       //     <let-dig-hyp> ::= <let-dig> | "-"
-                       //     <let-dig> ::= <letter> | <digit>
-                       rfc1034LdhStr = 'a-z0-9\\-';
-
-                       html5EmailRegexp = new RegExp(
-                               // start of string
-                               '^' +
-                               // User part which is liberal :p
-                               '[' + rfc5322Atext + '\\.]+' +
-                               // 'at'
-                               '@' +
-                               // Domain first part
-                               '[' + rfc1034LdhStr + ']+' +
-                               // Optional second part and following are separated by a dot
-                               '(?:\\.[' + rfc1034LdhStr + ']+)*' +
-                               // End of string
-                               '$',
-                               // RegExp is case insensitive
-                               'i'
-                       );
-                       return ( mailtxt.match( html5EmailRegexp ) !== null );
-               },
-
-               /**
-                * Note: borrows from IP::isIPv4
-                *
-                * @param {string} address
-                * @param {boolean} [allowBlock=false]
-                * @return {boolean}
-                */
-               isIPv4Address: function ( address, allowBlock ) {
-                       var block, RE_IP_BYTE, RE_IP_ADD;
-
-                       if ( typeof address !== 'string' ) {
-                               return false;
-                       }
-
-                       block = allowBlock ? '(?:\\/(?:3[0-2]|[12]?\\d))?' : '';
-                       RE_IP_BYTE = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])';
-                       RE_IP_ADD = '(?:' + RE_IP_BYTE + '\\.){3}' + RE_IP_BYTE;
-
-                       return ( new RegExp( '^' + RE_IP_ADD + block + '$' ).test( address ) );
-               },
-
-               /**
-                * Note: borrows from IP::isIPv6
-                *
-                * @param {string} address
-                * @param {boolean} [allowBlock=false]
-                * @return {boolean}
-                */
-               isIPv6Address: function ( address, allowBlock ) {
-                       var block, RE_IPV6_ADD;
-
-                       if ( typeof address !== 'string' ) {
-                               return false;
-                       }
-
-                       block = allowBlock ? '(?:\\/(?:12[0-8]|1[01][0-9]|[1-9]?\\d))?' : '';
-                       RE_IPV6_ADD =
-                               '(?:' + // starts with "::" (including "::")
-                                       ':(?::|(?::' +
-                                               '[0-9A-Fa-f]{1,4}' +
-                                       '){1,7})' +
-                                       '|' + // ends with "::" (except "::")
-                                       '[0-9A-Fa-f]{1,4}' +
-                                       '(?::' +
-                                               '[0-9A-Fa-f]{1,4}' +
-                                       '){0,6}::' +
-                                       '|' + // contains no "::"
-                                       '[0-9A-Fa-f]{1,4}' +
-                                       '(?::' +
-                                               '[0-9A-Fa-f]{1,4}' +
-                                       '){7}' +
-                               ')';
-
-                       if ( new RegExp( '^' + RE_IPV6_ADD + block + '$' ).test( address ) ) {
-                               return true;
-                       }
-
-                       // contains one "::" in the middle (single '::' check below)
-                       RE_IPV6_ADD =
-                               '[0-9A-Fa-f]{1,4}' +
-                               '(?:::?' +
-                                       '[0-9A-Fa-f]{1,4}' +
-                               '){1,6}';
-
-                       return (
-                               new RegExp( '^' + RE_IPV6_ADD + block + '$' ).test( address ) &&
-                               /::/.test( address ) &&
-                               !/::.*::/.test( address )
-                       );
-               },
-
-               /**
-                * Check whether a string is an IP address
-                *
-                * @since 1.25
-                * @param {string} address String to check
-                * @param {boolean} [allowBlock=false] If a block of IPs should be allowed
-                * @return {boolean}
-                */
-               isIPAddress: function ( address, allowBlock ) {
-                       return util.isIPv4Address( address, allowBlock ) ||
-                               util.isIPv6Address( address, allowBlock );
-               }
-       };
-
-       // Not allowed outside unit tests
-       if ( window.QUnit ) {
-               util.setOptionsForTest = function ( opts ) {
-                       var oldConfig = config;
-                       config = $.extend( {}, config, opts );
-                       return oldConfig;
-               };
-       }
-
-       /**
-        * Initialisation of mw.util.$content
-        */
-       function init() {
-               util.$content = ( function () {
-                       var i, l, $node, selectors;
-
-                       selectors = [
-                               // The preferred standard is class "mw-body".
-                               // You may also use class "mw-body mw-body-primary" if you use
-                               // mw-body in multiple locations. Or class "mw-body-primary" if
-                               // you use mw-body deeper in the DOM.
-                               '.mw-body-primary',
-                               '.mw-body',
-
-                               // If the skin has no such class, fall back to the parser output
-                               '#mw-content-text'
-                       ];
-
-                       for ( i = 0, l = selectors.length; i < l; i++ ) {
-                               $node = $( selectors[ i ] );
-                               if ( $node.length ) {
-                                       return $node.first();
-                               }
-                       }
-
-                       // Should never happen... well, it could if someone is not finished writing a
-                       // skin and has not yet inserted bodytext yet.
-                       return $( 'body' );
-               }() );
-       }
-
-       $( init );
-
-       mw.util = util;
-       module.exports = util;
-
-}() );
diff --git a/resources/src/mediawiki.util/jquery.accessKeyLabel.js b/resources/src/mediawiki.util/jquery.accessKeyLabel.js
new file mode 100644 (file)
index 0000000..cdc5808
--- /dev/null
@@ -0,0 +1,239 @@
+/**
+ * jQuery plugin to update the tooltip to show the correct access key
+ *
+ * @class jQuery.plugin.accessKeyLabel
+ */
+( function () {
+
+       // Cached access key modifiers for used browser
+       var cachedAccessKeyModifiers,
+
+               // Whether to use 'test-' instead of correct prefix (used for testing)
+               useTestPrefix = false,
+
+               // tag names which can have a label tag
+               // https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Form-associated_content
+               labelable = 'button, input, textarea, keygen, meter, output, progress, select';
+
+       /**
+        * Find the modifier keys that need to be pressed together with the accesskey to trigger the input.
+        *
+        * The result is dependant on the ua paramater or the current platform.
+        * For browsers that support accessKeyLabel, #getAccessKeyLabel never calls here.
+        * Valid key values that are returned can be: ctrl, alt, option, shift, esc
+        *
+        * @private
+        * @param {Object} [ua] An object with a 'userAgent' and 'platform' property.
+        * @return {Array} Array with 1 or more of the string values, in this order: ctrl, option, alt, shift, esc
+        */
+       function getAccessKeyModifiers( ua ) {
+               var profile, accessKeyModifiers;
+
+               // use cached prefix if possible
+               if ( !ua && cachedAccessKeyModifiers ) {
+                       return cachedAccessKeyModifiers;
+               }
+
+               profile = $.client.profile( ua );
+
+               switch ( profile.name ) {
+                       case 'chrome':
+                       case 'opera':
+                               if ( profile.name === 'opera' && profile.versionNumber < 15 ) {
+                                       accessKeyModifiers = [ 'shift', 'esc' ];
+                               } else if ( profile.platform === 'mac' ) {
+                                       accessKeyModifiers = [ 'ctrl', 'option' ];
+                               } else {
+                                       // Chrome/Opera on Windows or Linux
+                                       // (both alt- and alt-shift work, but alt with E, D, F etc does not
+                                       // work since they are browser shortcuts)
+                                       accessKeyModifiers = [ 'alt', 'shift' ];
+                               }
+                               break;
+                       case 'firefox':
+                       case 'iceweasel':
+                               if ( profile.versionBase < 2 ) {
+                                       // Before v2, Firefox used alt, though it was rebindable in about:config
+                                       accessKeyModifiers = [ 'alt' ];
+                               } else {
+                                       if ( profile.platform === 'mac' ) {
+                                               if ( profile.versionNumber < 14 ) {
+                                                       accessKeyModifiers = [ 'ctrl' ];
+                                               } else {
+                                                       accessKeyModifiers = [ 'ctrl', 'option' ];
+                                               }
+                                       } else {
+                                               accessKeyModifiers = [ 'alt', 'shift' ];
+                                       }
+                               }
+                               break;
+                       case 'safari':
+                       case 'konqueror':
+                               if ( profile.platform === 'win' ) {
+                                       accessKeyModifiers = [ 'alt' ];
+                               } else {
+                                       if ( profile.layoutVersion > 526 ) {
+                                               // Non-Windows Safari with webkit_version > 526
+                                               accessKeyModifiers = [ 'ctrl', profile.platform === 'mac' ? 'option' : 'alt' ];
+                                       } else {
+                                               accessKeyModifiers = [ 'ctrl' ];
+                                       }
+                               }
+                               break;
+                       case 'msie':
+                       case 'edge':
+                               accessKeyModifiers = [ 'alt' ];
+                               break;
+                       default:
+                               accessKeyModifiers = profile.platform === 'mac' ? [ 'ctrl' ] : [ 'alt' ];
+                               break;
+               }
+
+               // cache modifiers
+               if ( !ua ) {
+                       cachedAccessKeyModifiers = accessKeyModifiers;
+               }
+               return accessKeyModifiers;
+       }
+
+       /**
+        * Get the access key label for an element.
+        *
+        * Will use native accessKeyLabel if available (currently only in Firefox 8+),
+        * falls back to #getAccessKeyModifiers.
+        *
+        * @private
+        * @param {HTMLElement} element Element to get the label for
+        * @return {string} Access key label
+        */
+       function getAccessKeyLabel( element ) {
+               // abort early if no access key
+               if ( !element.accessKey ) {
+                       return '';
+               }
+               // use accessKeyLabel if possible
+               // https://html.spec.whatwg.org/multipage/interaction.html#dom-accesskeylabel
+               if ( !useTestPrefix && element.accessKeyLabel ) {
+                       return element.accessKeyLabel;
+               }
+               return ( useTestPrefix ? 'test' : getAccessKeyModifiers().join( '-' ) ) + '-' + element.accessKey;
+       }
+
+       /**
+        * Update the title for an element (on the element with the access key or it's label) to show
+        * the correct access key label.
+        *
+        * @private
+        * @param {HTMLElement} element Element with the accesskey
+        * @param {HTMLElement} titleElement Element with the title to update (may be the same as `element`)
+        */
+       function updateTooltipOnElement( element, titleElement ) {
+               var oldTitle, parts, regexp, newTitle, accessKeyLabel,
+                       separatorMsg = mw.message( 'word-separator' ).plain();
+
+               oldTitle = titleElement.title;
+               if ( !oldTitle ) {
+                       // don't add a title if the element didn't have one before
+                       return;
+               }
+
+               parts = ( separatorMsg + mw.message( 'brackets' ).plain() ).split( '$1' );
+               regexp = new RegExp( parts.map( mw.RegExp.escape ).join( '.*?' ) + '$' );
+               newTitle = oldTitle.replace( regexp, '' );
+               accessKeyLabel = getAccessKeyLabel( element );
+
+               if ( accessKeyLabel ) {
+                       // Should be build the same as in Linker::titleAttrib
+                       newTitle += separatorMsg + mw.message( 'brackets', accessKeyLabel ).plain();
+               }
+               if ( oldTitle !== newTitle ) {
+                       titleElement.title = newTitle;
+               }
+       }
+
+       /**
+        * Update the title for an element to show the correct access key label.
+        *
+        * @private
+        * @param {HTMLElement} element Element with the accesskey
+        */
+       function updateTooltip( element ) {
+               var id, $element, $label, $labelParent;
+               updateTooltipOnElement( element, element );
+
+               // update associated label if there is one
+               $element = $( element );
+               if ( $element.is( labelable ) ) {
+                       // Search it using 'for' attribute
+                       id = element.id.replace( /"/g, '\\"' );
+                       if ( id ) {
+                               $label = $( 'label[for="' + id + '"]' );
+                               if ( $label.length === 1 ) {
+                                       updateTooltipOnElement( element, $label[ 0 ] );
+                               }
+                       }
+
+                       // Search it as parent, because the form control can also be inside the label element itself
+                       $labelParent = $element.parents( 'label' );
+                       if ( $labelParent.length === 1 ) {
+                               updateTooltipOnElement( element, $labelParent[ 0 ] );
+                       }
+               }
+       }
+
+       /**
+        * Update the titles for all elements in a jQuery selection.
+        *
+        * @return {jQuery}
+        * @chainable
+        */
+       $.fn.updateTooltipAccessKeys = function () {
+               return this.each( function () {
+                       updateTooltip( this );
+               } );
+       };
+
+       /**
+        * getAccessKeyModifiers
+        *
+        * @method updateTooltipAccessKeys_getAccessKeyModifiers
+        * @inheritdoc #getAccessKeyModifiers
+        */
+       $.fn.updateTooltipAccessKeys.getAccessKeyModifiers = getAccessKeyModifiers;
+
+       /**
+        * getAccessKeyLabel
+        *
+        * @method updateTooltipAccessKeys_getAccessKeyLabel
+        * @inheritdoc #getAccessKeyLabel
+        */
+       $.fn.updateTooltipAccessKeys.getAccessKeyLabel = getAccessKeyLabel;
+
+       /**
+        * getAccessKeyPrefix
+        *
+        * @method updateTooltipAccessKeys_getAccessKeyPrefix
+        * @deprecated since 1.27 Use #getAccessKeyModifiers
+        * @param {Object} [ua] An object with a 'userAgent' and 'platform' property.
+        * @return {string}
+        */
+       $.fn.updateTooltipAccessKeys.getAccessKeyPrefix = function ( ua ) {
+               return getAccessKeyModifiers( ua ).join( '-' ) + '-';
+       };
+
+       /**
+        * Switch test mode on and off.
+        *
+        * @method updateTooltipAccessKeys_setTestMode
+        * @param {boolean} mode New mode
+        */
+       $.fn.updateTooltipAccessKeys.setTestMode = function ( mode ) {
+               useTestPrefix = mode;
+       };
+
+       /**
+        * @class jQuery
+        * @mixins jQuery.plugin.accessKeyLabel
+        */
+
+}() );
diff --git a/resources/src/mediawiki.util/util.js b/resources/src/mediawiki.util/util.js
new file mode 100644 (file)
index 0000000..7e0722f
--- /dev/null
@@ -0,0 +1,567 @@
+( function () {
+       'use strict';
+
+       var util,
+               config = require( './config.json' );
+
+       require( './jquery.accessKeyLabel.js' );
+
+       /**
+        * Encode the string like PHP's rawurlencode
+        * @ignore
+        *
+        * @param {string} str String to be encoded.
+        * @return {string} Encoded string
+        */
+       function rawurlencode( str ) {
+               str = String( str );
+               return encodeURIComponent( str )
+                       .replace( /!/g, '%21' ).replace( /'/g, '%27' ).replace( /\(/g, '%28' )
+                       .replace( /\)/g, '%29' ).replace( /\*/g, '%2A' ).replace( /~/g, '%7E' );
+       }
+
+       /**
+        * Private helper function used by util.escapeId*()
+        * @ignore
+        *
+        * @param {string} str String to be encoded
+        * @param {string} mode Encoding mode, see documentation for $wgFragmentMode
+        *     in DefaultSettings.php
+        * @return {string} Encoded string
+        */
+       function escapeIdInternal( str, mode ) {
+               str = String( str );
+
+               switch ( mode ) {
+                       case 'html5':
+                               return str.replace( / /g, '_' );
+                       case 'legacy':
+                               return rawurlencode( str.replace( / /g, '_' ) )
+                                       .replace( /%3A/g, ':' )
+                                       .replace( /%/g, '.' );
+                       default:
+                               throw new Error( 'Unrecognized ID escaping mode ' + mode );
+               }
+       }
+
+       /**
+        * Utility library
+        * @class mw.util
+        * @singleton
+        */
+       util = {
+
+               /**
+                * Encode the string like PHP's rawurlencode
+                *
+                * @param {string} str String to be encoded.
+                * @return {string} Encoded string
+                */
+               rawurlencode: rawurlencode,
+
+               /**
+                * Encode string into HTML id compatible form suitable for use in HTML
+                * Analog to PHP Sanitizer::escapeIdForAttribute()
+                *
+                * @since 1.30
+                *
+                * @param {string} str String to encode
+                * @return {string} Encoded string
+                */
+               escapeIdForAttribute: function ( str ) {
+                       var mode = config.FragmentMode[ 0 ];
+
+                       return escapeIdInternal( str, mode );
+               },
+
+               /**
+                * Encode string into HTML id compatible form suitable for use in links
+                * Analog to PHP Sanitizer::escapeIdForLink()
+                *
+                * @since 1.30
+                *
+                * @param {string} str String to encode
+                * @return {string} Encoded string
+                */
+               escapeIdForLink: function ( str ) {
+                       var mode = config.FragmentMode[ 0 ];
+
+                       return escapeIdInternal( str, mode );
+               },
+
+               /**
+                * Encode page titles for use in a URL
+                *
+                * We want / and : to be included as literal characters in our title URLs
+                * as they otherwise fatally break the title.
+                *
+                * The others are decoded because we can, it's prettier and matches behaviour
+                * of `wfUrlencode` in PHP.
+                *
+                * @param {string} str String to be encoded.
+                * @return {string} Encoded string
+                */
+               wikiUrlencode: function ( str ) {
+                       return util.rawurlencode( str )
+                               .replace( /%20/g, '_' )
+                               // wfUrlencode replacements
+                               .replace( /%3B/g, ';' )
+                               .replace( /%40/g, '@' )
+                               .replace( /%24/g, '$' )
+                               .replace( /%21/g, '!' )
+                               .replace( /%2A/g, '*' )
+                               .replace( /%28/g, '(' )
+                               .replace( /%29/g, ')' )
+                               .replace( /%2C/g, ',' )
+                               .replace( /%2F/g, '/' )
+                               .replace( /%7E/g, '~' )
+                               .replace( /%3A/g, ':' );
+               },
+
+               /**
+                * Get the link to a page name (relative to `wgServer`),
+                *
+                * @param {string|null} [pageName=wgPageName] Page name
+                * @param {Object} [params] A mapping of query parameter names to values,
+                *  e.g. `{ action: 'edit' }`
+                * @return {string} Url of the page with name of `pageName`
+                */
+               getUrl: function ( pageName, params ) {
+                       var titleFragmentStart, url, query,
+                               fragment = '',
+                               title = typeof pageName === 'string' ? pageName : mw.config.get( 'wgPageName' );
+
+                       // Find any fragment
+                       titleFragmentStart = title.indexOf( '#' );
+                       if ( titleFragmentStart !== -1 ) {
+                               fragment = title.slice( titleFragmentStart + 1 );
+                               // Exclude the fragment from the page name
+                               title = title.slice( 0, titleFragmentStart );
+                       }
+
+                       // Produce query string
+                       if ( params ) {
+                               query = $.param( params );
+                       }
+                       if ( query ) {
+                               url = title ?
+                                       util.wikiScript() + '?title=' + util.wikiUrlencode( title ) + '&' + query :
+                                       util.wikiScript() + '?' + query;
+                       } else {
+                               url = mw.config.get( 'wgArticlePath' )
+                                       .replace( '$1', util.wikiUrlencode( title ).replace( /\$/g, '$$$$' ) );
+                       }
+
+                       // Append the encoded fragment
+                       if ( fragment.length ) {
+                               url += '#' + util.escapeIdForLink( fragment );
+                       }
+
+                       return url;
+               },
+
+               /**
+                * Get address to a script in the wiki root.
+                * For index.php use `mw.config.get( 'wgScript' )`.
+                *
+                * @since 1.18
+                * @param {string} str Name of script (e.g. 'api'), defaults to 'index'
+                * @return {string} Address to script (e.g. '/w/api.php' )
+                */
+               wikiScript: function ( str ) {
+                       str = str || 'index';
+                       if ( str === 'index' ) {
+                               return mw.config.get( 'wgScript' );
+                       } else if ( str === 'load' ) {
+                               return config.LoadScript;
+                       } else {
+                               return mw.config.get( 'wgScriptPath' ) + '/' + str + '.php';
+                       }
+               },
+
+               /**
+                * Append a new style block to the head and return the CSSStyleSheet object.
+                * Use .ownerNode to access the `<style>` element, or use mw.loader#addStyleTag.
+                * This function returns the styleSheet object for convience (due to cross-browsers
+                * difference as to where it is located).
+                *
+                *     var sheet = util.addCSS( '.foobar { display: none; }' );
+                *     $( foo ).click( function () {
+                *         // Toggle the sheet on and off
+                *         sheet.disabled = !sheet.disabled;
+                *     } );
+                *
+                * @param {string} text CSS to be appended
+                * @return {CSSStyleSheet} Use .ownerNode to get to the `<style>` element.
+                */
+               addCSS: function ( text ) {
+                       var s = mw.loader.addStyleTag( text );
+                       return s.sheet || s.styleSheet || s;
+               },
+
+               /**
+                * Grab the URL parameter value for the given parameter.
+                * Returns null if not found.
+                *
+                * @param {string} param The parameter name.
+                * @param {string} [url=location.href] URL to search through, defaulting to the current browsing location.
+                * @return {Mixed} Parameter value or null.
+                */
+               getParamValue: function ( param, url ) {
+                       // Get last match, stop at hash
+                       var re = new RegExp( '^[^#]*[&?]' + mw.RegExp.escape( param ) + '=([^&#]*)' ),
+                               m = re.exec( url !== undefined ? url : location.href );
+
+                       if ( m ) {
+                               // Beware that decodeURIComponent is not required to understand '+'
+                               // by spec, as encodeURIComponent does not produce it.
+                               return decodeURIComponent( m[ 1 ].replace( /\+/g, '%20' ) );
+                       }
+                       return null;
+               },
+
+               /**
+                * The content wrapper of the skin (e.g. `.mw-body`).
+                *
+                * Populated on document ready. To use this property,
+                * wait for `$.ready` and be sure to have a module dependency on
+                * `mediawiki.util` which will ensure
+                * your document ready handler fires after initialization.
+                *
+                * Because of the lazy-initialised nature of this property,
+                * you're discouraged from using it.
+                *
+                * If you need just the wikipage content (not any of the
+                * extra elements output by the skin), use `$( '#mw-content-text' )`
+                * instead. Or listen to mw.hook#wikipage_content which will
+                * allow your code to re-run when the page changes (e.g. live preview
+                * or re-render after ajax save).
+                *
+                * @property {jQuery}
+                */
+               $content: null,
+
+               /**
+                * Add a link to a portlet menu on the page, such as:
+                *
+                * p-cactions (Content actions), p-personal (Personal tools),
+                * p-navigation (Navigation), p-tb (Toolbox)
+                *
+                * The first three parameters are required, the others are optional and
+                * may be null. Though providing an id and tooltip is recommended.
+                *
+                * By default the new link will be added to the end of the list. To
+                * add the link before a given existing item, pass the DOM node
+                * (e.g. `document.getElementById( 'foobar' )`) or a jQuery-selector
+                * (e.g. `'#foobar'`) for that item.
+                *
+                *     util.addPortletLink(
+                *         'p-tb', 'https://www.mediawiki.org/',
+                *         'mediawiki.org', 't-mworg', 'Go to mediawiki.org', 'm', '#t-print'
+                *     );
+                *
+                *     var node = util.addPortletLink(
+                *         'p-tb',
+                *         new mw.Title( 'Special:Example' ).getUrl(),
+                *         'Example'
+                *     );
+                *     $( node ).on( 'click', function ( e ) {
+                *         console.log( 'Example' );
+                *         e.preventDefault();
+                *     } );
+                *
+                * @param {string} portletId ID of the target portlet (e.g. 'p-cactions' or 'p-personal')
+                * @param {string} href Link URL
+                * @param {string} text Link text
+                * @param {string} [id] ID of the list item, should be unique and preferably have
+                *  the appropriate prefix ('ca-', 'pt-', 'n-' or 't-')
+                * @param {string} [tooltip] Text to show when hovering over the link, without accesskey suffix
+                * @param {string} [accesskey] Access key to activate this link. One character only,
+                *  avoid conflicts with other links. Use `$( '[accesskey=x]' )` in the console to
+                *  see if 'x' is already used.
+                * @param {HTMLElement|jQuery|string} [nextnode] Element that the new item should be added before.
+                *  Must be another item in the same list, it will be ignored otherwise.
+                *  Can be specified as DOM reference, as jQuery object, or as CSS selector string.
+                * @return {HTMLElement|null} The added list item, or null if no element was added.
+                */
+               addPortletLink: function ( portletId, href, text, id, tooltip, accesskey, nextnode ) {
+                       var item, link, $portlet, portlet, portletDiv, ul, next;
+
+                       if ( !portletId ) {
+                               // Avoid confusing id="undefined" lookup
+                               return null;
+                       }
+
+                       portlet = document.getElementById( portletId );
+                       if ( !portlet ) {
+                               // Invalid portlet ID
+                               return null;
+                       }
+
+                       // Setup the anchor tag and set any the properties
+                       link = document.createElement( 'a' );
+                       link.href = href;
+                       link.textContent = text;
+                       if ( tooltip ) {
+                               link.title = tooltip;
+                       }
+                       if ( accesskey ) {
+                               link.accessKey = accesskey;
+                       }
+
+                       // Unhide portlet if it was hidden before
+                       $portlet = $( portlet );
+                       $portlet.removeClass( 'emptyPortlet' );
+
+                       // Setup the list item (and a span if $portlet is a Vector tab)
+                       // eslint-disable-next-line no-jquery/no-class-state
+                       if ( $portlet.hasClass( 'vectorTabs' ) ) {
+                               item = $( '<li>' ).append( $( '<span>' ).append( link )[ 0 ] )[ 0 ];
+                       } else {
+                               item = $( '<li>' ).append( link )[ 0 ];
+                       }
+                       if ( id ) {
+                               item.id = id;
+                       }
+
+                       // Select the first (most likely only) unordered list inside the portlet
+                       ul = portlet.querySelector( 'ul' );
+                       if ( !ul ) {
+                               // If it didn't have an unordered list yet, create one
+                               ul = document.createElement( 'ul' );
+                               portletDiv = portlet.querySelector( 'div' );
+                               if ( portletDiv ) {
+                                       // Support: Legacy skins have a div (such as div.body or div.pBody).
+                                       // Append the <ul> to that.
+                                       portletDiv.appendChild( ul );
+                               } else {
+                                       // Append it to the portlet directly
+                                       portlet.appendChild( ul );
+                               }
+                       }
+
+                       if ( nextnode && ( typeof nextnode === 'string' || nextnode.nodeType || nextnode.jquery ) ) {
+                               nextnode = $( ul ).find( nextnode );
+                               if ( nextnode.length === 1 && nextnode[ 0 ].parentNode === ul ) {
+                                       // Insertion point: Before nextnode
+                                       nextnode.before( item );
+                                       next = true;
+                               }
+                               // Else: Invalid nextnode value (no match, more than one match, or not a direct child)
+                               // Else: Invalid nextnode type
+                       }
+
+                       if ( !next ) {
+                               // Insertion point: End of list (default)
+                               ul.appendChild( item );
+                       }
+
+                       // Update tooltip for the access key after inserting into DOM
+                       // to get a localized access key label (T69946).
+                       if ( accesskey ) {
+                               $( link ).updateTooltipAccessKeys();
+                       }
+
+                       return item;
+               },
+
+               /**
+                * Validate a string as representing a valid e-mail address
+                * according to HTML5 specification. Please note the specification
+                * does not validate a domain with one character.
+                *
+                * FIXME: should be moved to or replaced by a validation module.
+                *
+                * @param {string} mailtxt E-mail address to be validated.
+                * @return {boolean|null} Null if `mailtxt` was an empty string, otherwise true/false
+                * as determined by validation.
+                */
+               validateEmail: function ( mailtxt ) {
+                       var rfc5322Atext, rfc1034LdhStr, html5EmailRegexp;
+
+                       if ( mailtxt === '' ) {
+                               return null;
+                       }
+
+                       // HTML5 defines a string as valid e-mail address if it matches
+                       // the ABNF:
+                       //     1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
+                       // With:
+                       // - atext   : defined in RFC 5322 section 3.2.3
+                       // - ldh-str : defined in RFC 1034 section 3.5
+                       //
+                       // (see STD 68 / RFC 5234 https://tools.ietf.org/html/std68)
+                       // First, define the RFC 5322 'atext' which is pretty easy:
+                       // atext = ALPHA / DIGIT / ; Printable US-ASCII
+                       //     "!" / "#" /    ; characters not including
+                       //     "$" / "%" /    ; specials. Used for atoms.
+                       //     "&" / "'" /
+                       //     "*" / "+" /
+                       //     "-" / "/" /
+                       //     "=" / "?" /
+                       //     "^" / "_" /
+                       //     "`" / "{" /
+                       //     "|" / "}" /
+                       //     "~"
+                       rfc5322Atext = 'a-z0-9!#$%&\'*+\\-/=?^_`{|}~';
+
+                       // Next define the RFC 1034 'ldh-str'
+                       //     <domain> ::= <subdomain> | " "
+                       //     <subdomain> ::= <label> | <subdomain> "." <label>
+                       //     <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
+                       //     <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
+                       //     <let-dig-hyp> ::= <let-dig> | "-"
+                       //     <let-dig> ::= <letter> | <digit>
+                       rfc1034LdhStr = 'a-z0-9\\-';
+
+                       html5EmailRegexp = new RegExp(
+                               // start of string
+                               '^' +
+                               // User part which is liberal :p
+                               '[' + rfc5322Atext + '\\.]+' +
+                               // 'at'
+                               '@' +
+                               // Domain first part
+                               '[' + rfc1034LdhStr + ']+' +
+                               // Optional second part and following are separated by a dot
+                               '(?:\\.[' + rfc1034LdhStr + ']+)*' +
+                               // End of string
+                               '$',
+                               // RegExp is case insensitive
+                               'i'
+                       );
+                       return ( mailtxt.match( html5EmailRegexp ) !== null );
+               },
+
+               /**
+                * Note: borrows from IP::isIPv4
+                *
+                * @param {string} address
+                * @param {boolean} [allowBlock=false]
+                * @return {boolean}
+                */
+               isIPv4Address: function ( address, allowBlock ) {
+                       var block, RE_IP_BYTE, RE_IP_ADD;
+
+                       if ( typeof address !== 'string' ) {
+                               return false;
+                       }
+
+                       block = allowBlock ? '(?:\\/(?:3[0-2]|[12]?\\d))?' : '';
+                       RE_IP_BYTE = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])';
+                       RE_IP_ADD = '(?:' + RE_IP_BYTE + '\\.){3}' + RE_IP_BYTE;
+
+                       return ( new RegExp( '^' + RE_IP_ADD + block + '$' ).test( address ) );
+               },
+
+               /**
+                * Note: borrows from IP::isIPv6
+                *
+                * @param {string} address
+                * @param {boolean} [allowBlock=false]
+                * @return {boolean}
+                */
+               isIPv6Address: function ( address, allowBlock ) {
+                       var block, RE_IPV6_ADD;
+
+                       if ( typeof address !== 'string' ) {
+                               return false;
+                       }
+
+                       block = allowBlock ? '(?:\\/(?:12[0-8]|1[01][0-9]|[1-9]?\\d))?' : '';
+                       RE_IPV6_ADD =
+                               '(?:' + // starts with "::" (including "::")
+                                       ':(?::|(?::' +
+                                               '[0-9A-Fa-f]{1,4}' +
+                                       '){1,7})' +
+                                       '|' + // ends with "::" (except "::")
+                                       '[0-9A-Fa-f]{1,4}' +
+                                       '(?::' +
+                                               '[0-9A-Fa-f]{1,4}' +
+                                       '){0,6}::' +
+                                       '|' + // contains no "::"
+                                       '[0-9A-Fa-f]{1,4}' +
+                                       '(?::' +
+                                               '[0-9A-Fa-f]{1,4}' +
+                                       '){7}' +
+                               ')';
+
+                       if ( new RegExp( '^' + RE_IPV6_ADD + block + '$' ).test( address ) ) {
+                               return true;
+                       }
+
+                       // contains one "::" in the middle (single '::' check below)
+                       RE_IPV6_ADD =
+                               '[0-9A-Fa-f]{1,4}' +
+                               '(?:::?' +
+                                       '[0-9A-Fa-f]{1,4}' +
+                               '){1,6}';
+
+                       return (
+                               new RegExp( '^' + RE_IPV6_ADD + block + '$' ).test( address ) &&
+                               /::/.test( address ) &&
+                               !/::.*::/.test( address )
+                       );
+               },
+
+               /**
+                * Check whether a string is an IP address
+                *
+                * @since 1.25
+                * @param {string} address String to check
+                * @param {boolean} [allowBlock=false] If a block of IPs should be allowed
+                * @return {boolean}
+                */
+               isIPAddress: function ( address, allowBlock ) {
+                       return util.isIPv4Address( address, allowBlock ) ||
+                               util.isIPv6Address( address, allowBlock );
+               }
+       };
+
+       // Not allowed outside unit tests
+       if ( window.QUnit ) {
+               util.setOptionsForTest = function ( opts ) {
+                       var oldConfig = config;
+                       config = $.extend( {}, config, opts );
+                       return oldConfig;
+               };
+       }
+
+       /**
+        * Initialisation of mw.util.$content
+        */
+       function init() {
+               util.$content = ( function () {
+                       var i, l, $node, selectors;
+
+                       selectors = [
+                               // The preferred standard is class "mw-body".
+                               // You may also use class "mw-body mw-body-primary" if you use
+                               // mw-body in multiple locations. Or class "mw-body-primary" if
+                               // you use mw-body deeper in the DOM.
+                               '.mw-body-primary',
+                               '.mw-body',
+
+                               // If the skin has no such class, fall back to the parser output
+                               '#mw-content-text'
+                       ];
+
+                       for ( i = 0, l = selectors.length; i < l; i++ ) {
+                               $node = $( selectors[ i ] );
+                               if ( $node.length ) {
+                                       return $node.first();
+                               }
+                       }
+
+                       // Should never happen... well, it could if someone is not finished writing a
+                       // skin and has not yet inserted bodytext yet.
+                       return $( 'body' );
+               }() );
+       }
+
+       $( init );
+
+       mw.util = util;
+       module.exports = util;
+
+}() );
index 41c65b2..b738312 100644 (file)
@@ -1830,6 +1830,7 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
                        $pageTables = [
                                'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment', 'archive',
                                'revision_actor_temp', 'slots', 'content', 'content_models', 'slot_roles',
+                               'change_tag',
                        ];
                        $coreDBDataTables = array_merge( $userTables, $pageTables );
 
index 3f876ae..fda986c 100644 (file)
@@ -20,6 +20,7 @@
  */
 
 use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\Exception;
 
 /**
  * Base class for unit tests.
@@ -37,6 +38,25 @@ abstract class MediaWikiUnitTestCase extends TestCase {
        private static $originalGlobals;
        private static $unitGlobals;
 
+       /**
+        * Whitelist of globals to allow in MediaWikiUnitTestCase.
+        *
+        * Please, keep this list to the bare minimum.
+        *
+        * @return string[]
+        */
+       private static function getGlobalsWhitelist() {
+               return [
+                       // The autoloader may change between bootstrap and the first test,
+                       // so (lazily) capture these here instead.
+                       'wgAutoloadClasses',
+                       'wgAutoloadLocalClasses',
+                       // Need for LoggerFactory. Default is NullSpi.
+                       'wgMWLoggerDefaultSpi',
+                       'wgAutoloadAttemptLowercase'
+               ];
+       }
+
        public static function setUpBeforeClass() {
                parent::setUpBeforeClass();
 
@@ -56,12 +76,10 @@ abstract class MediaWikiUnitTestCase extends TestCase {
                }
 
                self::$unitGlobals =& TestSetup::$bootstrapGlobals;
-               // The autoloader may change between bootstrap and the first test,
-               // so (lazily) capture these here instead.
-               self::$unitGlobals['wgAutoloadClasses'] =& $GLOBALS['wgAutoloadClasses'];
-               self::$unitGlobals['wgAutoloadLocalClasses'] =& $GLOBALS['wgAutoloadLocalClasses'];
-               // This value should always be true.
-               self::$unitGlobals['wgAutoloadAttemptLowercase'] = true;
+
+               foreach ( self::getGlobalsWhitelist() as $global ) {
+                       self::$unitGlobals[ $global ] =& $GLOBALS[ $global ];
+               }
 
                // Would be nice if we coud simply replace $GLOBALS as a whole,
                // but unsetting or re-assigning that breaks the reference of this magic
@@ -84,6 +102,22 @@ abstract class MediaWikiUnitTestCase extends TestCase {
                }
        }
 
+       /**
+        * @inheritDoc
+        */
+       protected function runTest() {
+               try {
+                       return parent::runTest();
+               } catch ( ConfigException $exception ) {
+                       throw new Exception(
+                               'Config variables must be mocked, they cannot be accessed directly in tests which extend '
+                               . self::class,
+                               $exception->getCode(),
+                               $exception
+                       );
+               }
+       }
+
        protected function tearDown() {
                if ( !defined( 'HHVM_VERSION' ) ) {
                        // Quick reset between tests
index 233810f..53539ea 100644 (file)
@@ -7,13 +7,13 @@ use MediaWikiTestCase;
 use Message;
 use Wikimedia\Message\MessageValue;
 use Wikimedia\Message\ParamType;
-use Wikimedia\Message\TextParam;
+use Wikimedia\Message\ScalarParam;
 
 /**
  * @covers \MediaWiki\Message\TextFormatter
  * @covers \Wikimedia\Message\MessageValue
  * @covers \Wikimedia\Message\ListParam
- * @covers \Wikimedia\Message\TextParam
+ * @covers \Wikimedia\Message\ScalarParam
  * @covers \Wikimedia\Message\MessageParam
  */
 class TextFormatterTest extends MediaWikiTestCase {
@@ -45,11 +45,24 @@ class TextFormatterTest extends MediaWikiTestCase {
                $formatter = $this->createTextFormatter( 'en' );
                $mv = ( new MessageValue( 'test' ) )->commaListParams( [
                        'a',
-                       new TextParam( ParamType::BITRATE, 100 ),
+                       new ScalarParam( ParamType::BITRATE, 100 ),
                ] );
                $result = $formatter->format( $mv );
                $this->assertSame( 'test a, 100 bps $2', $result );
        }
+
+       public function testFormatMessage() {
+               $formatter = $this->createTextFormatter( 'en' );
+               $mv = ( new MessageValue( 'test' ) )
+                       ->params( new MessageValue( 'test2', [ 'a', 'b' ] ) )
+                       ->commaListParams( [
+                               'x',
+                               new ScalarParam( ParamType::BITRATE, 100 ),
+                               new MessageValue( 'test3', [ 'c', new MessageValue( 'test4', [ 'd', 'e' ] ) ] )
+                       ] );
+               $result = $formatter->format( $mv );
+               $this->assertSame( 'test test2 a b x, 100 bps, test3 c test4 d e', $result );
+       }
 }
 
 class FakeMessage extends Message {
index 3c6573a..2d1fd98 100644 (file)
@@ -9,8 +9,11 @@ use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\RequestData;
 use MediaWiki\Rest\ResponseFactory;
 use MediaWiki\Rest\Router;
+use MediaWiki\Rest\Validator\Validator;
 use MediaWikiTestCase;
+use Psr\Container\ContainerInterface;
 use User;
+use Wikimedia\ObjectFactory;
 
 /**
  * @group Database
@@ -21,7 +24,7 @@ use User;
  * @covers \MediaWiki\Rest\BasicAccess\MWBasicRequestAuthorizer
  */
 class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
-       private function createRouter( $userRights ) {
+       private function createRouter( $userRights, $request ) {
                $user = User::newFromName( 'Test user' );
                // Don't allow the rights to everybody so that user rights kick in.
                $this->mergeMwGlobalArrayValue( 'wgGroupPermissions', [ '*' => $userRights ] );
@@ -34,18 +37,25 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
 
                global $IP;
 
+               $objectFactory = new ObjectFactory(
+                       $this->getMockForAbstractClass( ContainerInterface::class )
+               );
+
                return new Router(
                        [ "$IP/tests/phpunit/unit/includes/Rest/testRoutes.json" ],
                        [],
                        '/rest',
                        new \EmptyBagOStuff(),
                        new ResponseFactory(),
-                       new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ) );
+                       new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ),
+                       $objectFactory,
+                       new Validator( $objectFactory, $request, $user )
+               );
        }
 
        public function testReadDenied() {
-               $router = $this->createRouter( [ 'read' => false ] );
                $request = new RequestData( [ 'uri' => new Uri( '/rest/user/joe/hello' ) ] );
+               $router = $this->createRouter( [ 'read' => false ], $request );
                $response = $router->execute( $request );
                $this->assertSame( 403, $response->getStatusCode() );
 
@@ -56,8 +66,8 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
        }
 
        public function testReadAllowed() {
-               $router = $this->createRouter( [ 'read' => true ] );
                $request = new RequestData( [ 'uri' => new Uri( '/rest/user/joe/hello' ) ] );
+               $router = $this->createRouter( [ 'read' => true ], $request );
                $response = $router->execute( $request );
                $this->assertSame( 200, $response->getStatusCode() );
        }
@@ -75,10 +85,10 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
        }
 
        public function testWriteDenied() {
-               $router = $this->createRouter( [ 'read' => true, 'writeapi' => false ] );
                $request = new RequestData( [
                        'uri' => new Uri( '/rest/mock/MWBasicRequestAuthorizerTest/write' )
                ] );
+               $router = $this->createRouter( [ 'read' => true, 'writeapi' => false ], $request );
                $response = $router->execute( $request );
                $this->assertSame( 403, $response->getStatusCode() );
 
@@ -89,10 +99,10 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
        }
 
        public function testWriteAllowed() {
-               $router = $this->createRouter( [ 'read' => true, 'writeapi' => true ] );
                $request = new RequestData( [
                        'uri' => new Uri( '/rest/mock/MWBasicRequestAuthorizerTest/write' )
                ] );
+               $router = $this->createRouter( [ 'read' => true, 'writeapi' => true ], $request );
                $response = $router->execute( $request );
 
                $this->assertSame( 200, $response->getStatusCode() );
index b599e9d..b984895 100644 (file)
@@ -9,10 +9,15 @@ use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer;
 use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\EntryPoint;
 use MediaWiki\Rest\RequestData;
+use MediaWiki\Rest\RequestInterface;
 use MediaWiki\Rest\ResponseFactory;
 use MediaWiki\Rest\Router;
+use MediaWiki\Rest\Validator\Validator;
+use Psr\Container\ContainerInterface;
 use RequestContext;
 use WebResponse;
+use Wikimedia\ObjectFactory;
+use User;
 
 /**
  * @covers \MediaWiki\Rest\EntryPoint
@@ -21,16 +26,23 @@ use WebResponse;
 class EntryPointTest extends \MediaWikiTestCase {
        private static $mockHandler;
 
-       private function createRouter() {
+       private function createRouter( RequestInterface $request ) {
                global $IP;
 
+               $objectFactory = new ObjectFactory(
+                       $this->getMockForAbstractClass( ContainerInterface::class )
+               );
+
                return new Router(
                        [ "$IP/tests/phpunit/unit/includes/Rest/testRoutes.json" ],
                        [],
                        '/rest',
                        new EmptyBagOStuff(),
                        new ResponseFactory(),
-                       new StaticBasicAuthorizer() );
+                       new StaticBasicAuthorizer(),
+                       $objectFactory,
+                       new Validator( $objectFactory, $request, new User )
+               );
        }
 
        private function createWebResponse() {
@@ -58,11 +70,12 @@ class EntryPointTest extends \MediaWikiTestCase {
                                [ 'Foo: Bar', true, null ]
                        );
 
+               $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/header' ) ] );
                $entryPoint = new EntryPoint(
                        RequestContext::getMain(),
-                       new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/header' ) ] ),
+                       $request,
                        $webResponse,
-                       $this->createRouter() );
+                       $this->createRouter( $request ) );
                $entryPoint->execute();
                $this->assertTrue( true );
        }
@@ -83,11 +96,12 @@ class EntryPointTest extends \MediaWikiTestCase {
         * Make sure EntryPoint rewinds a seekable body stream before reading.
         */
        public function testBodyRewind() {
+               $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/bodyRewind' ) ] );
                $entryPoint = new EntryPoint(
                        RequestContext::getMain(),
-                       new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/bodyRewind' ) ] ),
+                       $request,
                        $this->createWebResponse(),
-                       $this->createRouter() );
+                       $this->createRouter( $request ) );
                ob_start();
                $entryPoint->execute();
                $this->assertSame( 'hello', ob_get_clean() );
index 1016f28..c789e83 100644 (file)
@@ -32,6 +32,7 @@ use Wikimedia\Rdbms\LoadBalancer;
 use Wikimedia\Rdbms\ChronologyProtector;
 use Wikimedia\Rdbms\MySQLMasterPos;
 use Wikimedia\Rdbms\DatabaseDomain;
+use Wikimedia\Rdbms\LoadMonitorNull;
 
 /**
  * @group Database
@@ -110,7 +111,6 @@ class LBFactoryTest extends MediaWikiTestCase {
                $this->assertSame( $factory->getLocalDomainID(), $factory->resolveDomainID( false ) );
 
                $factory->shutdown();
-               $lb->closeAll();
        }
 
        public function testLBFactorySimpleServers() {
@@ -160,7 +160,6 @@ class LBFactoryTest extends MediaWikiTestCase {
                        'cluster master set' );
 
                $factory->shutdown();
-               $lb->closeAll();
        }
 
        public function testLBFactoryMultiConns() {
index 04dfa4e..f0205b8 100644 (file)
@@ -5,13 +5,13 @@ namespace Wikimedia\Tests\Message;
 use Wikimedia\Message\ListType;
 use Wikimedia\Message\MessageValue;
 use Wikimedia\Message\ParamType;
-use Wikimedia\Message\TextParam;
+use Wikimedia\Message\ScalarParam;
 use MediaWikiTestCase;
 
 /**
  * @covers \Wikimedia\Message\MessageValue
  * @covers \Wikimedia\Message\ListParam
- * @covers \Wikimedia\Message\TextParam
+ * @covers \Wikimedia\Message\ScalarParam
  * @covers \Wikimedia\Message\MessageParam
  */
 class MessageValueTest extends MediaWikiTestCase {
@@ -26,7 +26,7 @@ class MessageValueTest extends MediaWikiTestCase {
                                '<message key="key"><text>a</text></message>'
                        ],
                        [
-                               [ new TextParam( ParamType::BITRATE, 100 ) ],
+                               [ new ScalarParam( ParamType::BITRATE, 100 ) ],
                                '<message key="key"><bitrate>100</bitrate></message>'
                        ],
                ];
@@ -46,7 +46,7 @@ class MessageValueTest extends MediaWikiTestCase {
        public function testParams() {
                $mv = new MessageValue( 'key' );
                $mv->params( 1, 'x' );
-               $mv2 = $mv->params( new TextParam( ParamType::BITRATE, 100 ) );
+               $mv2 = $mv->params( new ScalarParam( ParamType::BITRATE, 100 ) );
                $this->assertSame(
                        '<message key="key"><text>1</text><text>x</text><bitrate>100</bitrate></message>',
                        $mv->dump() );
@@ -76,10 +76,11 @@ class MessageValueTest extends MediaWikiTestCase {
 
        public function testTextParams() {
                $mv = new MessageValue( 'key' );
-               $mv2 = $mv->textParams( 'a', 'b' );
+               $mv2 = $mv->textParams( 'a', 'b', new MessageValue( 'key2' ) );
                $this->assertSame( '<message key="key">' .
                        '<text>a</text>' .
                        '<text>b</text>' .
+                       '<text><message key="key2"></message></text>' .
                        '</message>',
                        $mv->dump() );
                $this->assertSame( $mv, $mv2 );
index 58f6c05..4419533 100644 (file)
@@ -2239,7 +2239,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                } catch ( DBUnexpectedError $ex ) {
                        $this->assertSame(
                                'Wikimedia\Rdbms\Database::close: ' .
-                               'mass commit/rollback of peer transaction required (DBO_TRX set)',
+                               'expected mass rollback of all peer transactions (DBO_TRX set)',
                                $ex->getMessage()
                        );
                }
index 5de1b0c..106cca3 100644 (file)
@@ -19,7 +19,7 @@ class ExtensionRegistryTest extends MediaWikiTestCase {
                $path = __DIR__ . '/doesnotexist.json';
                $this->setExpectedException(
                        Exception::class,
-                       "$path does not exist!"
+                       "file $path"
                );
                $registry->queue( $path );
        }
index 188629f..91652a2 100644 (file)
@@ -8,6 +8,10 @@ use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer;
 use MediaWiki\Rest\RequestData;
 use MediaWiki\Rest\ResponseFactory;
 use MediaWiki\Rest\Router;
+use MediaWiki\Rest\Validator\Validator;
+use Psr\Container\ContainerInterface;
+use Wikimedia\ObjectFactory;
+use User;
 
 /**
  * @covers \MediaWiki\Rest\Handler\HelloHandler
@@ -48,14 +52,21 @@ class HelloHandlerTest extends \MediaWikiUnitTestCase {
 
        /** @dataProvider provideTestViaRouter */
        public function testViaRouter( $requestInfo, $responseInfo ) {
+               $objectFactory = new ObjectFactory(
+                       $this->getMockForAbstractClass( ContainerInterface::class )
+               );
+
+               $request = new RequestData( $requestInfo );
                $router = new Router(
                        [ __DIR__ . '/../testRoutes.json' ],
                        [],
                        '/rest',
                        new EmptyBagOStuff(),
                        new ResponseFactory(),
-                       new StaticBasicAuthorizer() );
-               $request = new RequestData( $requestInfo );
+                       new StaticBasicAuthorizer(),
+                       $objectFactory,
+                       new Validator( $objectFactory, $request, new User )
+               );
                $response = $router->execute( $request );
                if ( isset( $responseInfo['statusCode'] ) ) {
                        $this->assertSame( $responseInfo['statusCode'], $response->getStatusCode() );
index cacccb9..e16ea25 100644 (file)
@@ -7,37 +7,48 @@ use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer;
 use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\HttpException;
 use MediaWiki\Rest\RequestData;
+use MediaWiki\Rest\RequestInterface;
 use MediaWiki\Rest\ResponseFactory;
 use MediaWiki\Rest\Router;
+use MediaWiki\Rest\Validator\Validator;
+use Psr\Container\ContainerInterface;
+use Wikimedia\ObjectFactory;
+use User;
 
 /**
  * @covers \MediaWiki\Rest\Router
  */
 class RouterTest extends \MediaWikiUnitTestCase {
        /** @return Router */
-       private function createRouter( $authError = null ) {
+       private function createRouter( RequestInterface $request, $authError = null ) {
+               $objectFactory = new ObjectFactory(
+                       $this->getMockForAbstractClass( ContainerInterface::class )
+               );
                return new Router(
                        [ __DIR__ . '/testRoutes.json' ],
                        [],
                        '/rest',
                        new \EmptyBagOStuff(),
                        new ResponseFactory(),
-                       new StaticBasicAuthorizer( $authError ) );
+                       new StaticBasicAuthorizer( $authError ),
+                       $objectFactory,
+                       new Validator( $objectFactory, $request, new User )
+               );
        }
 
        public function testPrefixMismatch() {
-               $router = $this->createRouter();
                $request = new RequestData( [ 'uri' => new Uri( '/bogus' ) ] );
+               $router = $this->createRouter( $request );
                $response = $router->execute( $request );
                $this->assertSame( 404, $response->getStatusCode() );
        }
 
        public function testWrongMethod() {
-               $router = $this->createRouter();
                $request = new RequestData( [
                        'uri' => new Uri( '/rest/user/joe/hello' ),
                        'method' => 'OPTIONS'
                ] );
+               $router = $this->createRouter( $request );
                $response = $router->execute( $request );
                $this->assertSame( 405, $response->getStatusCode() );
                $this->assertSame( 'Method Not Allowed', $response->getReasonPhrase() );
@@ -45,8 +56,8 @@ class RouterTest extends \MediaWikiUnitTestCase {
        }
 
        public function testNoMatch() {
-               $router = $this->createRouter();
                $request = new RequestData( [ 'uri' => new Uri( '/rest/bogus' ) ] );
+               $router = $this->createRouter( $request );
                $response = $router->execute( $request );
                $this->assertSame( 404, $response->getStatusCode() );
                // TODO: add more information to the response body and test for its presence here
@@ -61,8 +72,8 @@ class RouterTest extends \MediaWikiUnitTestCase {
        }
 
        public function testException() {
-               $router = $this->createRouter();
                $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/RouterTest/throw' ) ] );
+               $router = $this->createRouter( $request );
                $response = $router->execute( $request );
                $this->assertSame( 555, $response->getStatusCode() );
                $body = $response->getBody();
@@ -72,9 +83,9 @@ class RouterTest extends \MediaWikiUnitTestCase {
        }
 
        public function testBasicAccess() {
-               $router = $this->createRouter( 'test-error' );
                // Using the throwing handler is a way to assert that the handler is not executed
                $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/RouterTest/throw' ) ] );
+               $router = $this->createRouter( $request, 'test-error' );
                $response = $router->execute( $request );
                $this->assertSame( 403, $response->getStatusCode() );
                $body = $response->getBody();
diff --git a/tests/phpunit/unit/includes/installer/SqliteInstallerTest.php b/tests/phpunit/unit/includes/installer/SqliteInstallerTest.php
new file mode 100644 (file)
index 0000000..19a2973
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @group sqlite
+ * @group Database
+ * @group medium
+ */
+class SqliteInstallerTest extends \MediaWikiUnitTestCase {
+       /**
+        * @covers SqliteInstaller::checkDataDir
+        */
+       public function testCheckDataDir() {
+               $method = new ReflectionMethod( SqliteInstaller::class, 'checkDataDir' );
+               $method->setAccessible( true );
+
+               # Test 1: Should return fatal Status if $dir exist and it un-writable
+               if ( ( isset( $_SERVER['USER'] ) && $_SERVER['USER'] !== 'root' ) && !wfIsWindows() ) {
+                       // We can't simulate this environment under Windows or login as root
+                       $dir = sys_get_temp_dir() . '/' . uniqid( 'MediaWikiTest' );
+                       mkdir( $dir, 0000 );
+                       /** @var Status $status */
+                       $status = $method->invoke( null, $dir );
+                       $this->assertFalse( $status->isGood() );
+                       $this->assertSame( 'config-sqlite-dir-unwritable', $status->getErrors()[0]['message'] );
+                       rmdir( $dir );
+               }
+
+               # Test 2: Should return fatal Status if $dir not exist and it parent also not exist
+               $dir = sys_get_temp_dir() . '/' . uniqid( 'MediaWikiTest' ) . '/' . uniqid( 'MediaWikiTest' );
+               $status = $method->invoke( null, $dir );
+               $this->assertFalse( $status->isGood() );
+
+               # Test 3: Should return good Status if $dir not exist and it parent writable
+               $dir = sys_get_temp_dir() . '/' . uniqid( 'MediaWikiTest' );
+               /** @var Status $status */
+               $status = $method->invoke( null, $dir );
+               $this->assertTrue( $status->isGood() );
+       }
+
+       /**
+        * @covers SqliteInstaller::createDataDir
+        */
+       public function testCreateDataDir() {
+               $method = new ReflectionMethod( SqliteInstaller::class, 'createDataDir' );
+               $method->setAccessible( true );
+
+               # Test 1: Should return fatal Status if $dir not exist and it parent un-writable
+               if ( ( isset( $_SERVER['USER'] ) && $_SERVER['USER'] !== 'root' ) && !wfIsWindows() ) {
+                       // We can't simulate this environment under Windows or login as root
+                       $random = uniqid( 'MediaWikiTest' );
+                       $dir = sys_get_temp_dir() . '/' . $random . '/' . uniqid( 'MediaWikiTest' );
+                       mkdir( sys_get_temp_dir() . "/$random", 0000 );
+                       /** @var Status $status */
+                       $status = $method->invoke( null, $dir );
+                       $this->assertFalse( $status->isGood() );
+                       $this->assertSame( 'config-sqlite-mkdir-error', $status->getErrors()[0]['message'] );
+                       rmdir( sys_get_temp_dir() . "/$random" );
+               }
+
+               # Test 2: Test .htaccess content after created successfully
+               $dir = sys_get_temp_dir() . '/' . uniqid( 'MediaWikiTest' );
+               $status = $method->invoke( null, $dir );
+               $this->assertTrue( $status->isGood() );
+               $this->assertSame( "Deny from all\n", file_get_contents( "$dir/.htaccess" ) );
+               unlink( "$dir/.htaccess" );
+               rmdir( $dir );
+       }
+}
index d55b603..76f4f82 100644 (file)
@@ -102,7 +102,6 @@ return [
                        'tests/qunit/suites/resources/mediawiki/mediawiki.visibleTimeout.test.js',
                ],
                'dependencies' => [
-                       'jquery.accessKeyLabel',
                        'jquery.color',
                        'jquery.colorUtil',
                        'jquery.getAttrs',
index 13dbc0e..f425d87 100644 (file)
--- a/thumb.php
+++ b/thumb.php
@@ -35,7 +35,7 @@ if ( defined( 'THUMB_HANDLER' ) ) {
        wfThumbHandle404();
 } else {
        // Called directly, use $_GET params
-       wfStreamThumb( $wgRequest->getQueryValues() );
+       wfStreamThumb( $wgRequest->getQueryValuesOnly() );
 }
 
 $mediawiki = new MediaWiki();