Merge "Make Status extend StatusValue and start FileBackend update"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sat, 17 Sep 2016 23:34:33 +0000 (23:34 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sat, 17 Sep 2016 23:34:33 +0000 (23:34 +0000)
30 files changed:
autoload.php
docs/extension.schema.json
docs/extension.schema.v1.json
includes/ServiceWiring.php
includes/Setup.php
includes/db/loadbalancer/LBFactoryMW.php
includes/db/loadbalancer/LBFactoryMulti.php
includes/db/loadbalancer/LBFactorySimple.php [deleted file]
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseDomain.php [new file with mode: 0644]
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/lbfactory/LBFactorySimple.php [new file with mode: 0644]
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/registration/ExtensionProcessor.php
includes/specialpage/LoginSignupSpecialPage.php
languages/i18n/ar.json
languages/i18n/be-tarask.json
languages/i18n/bn.json
languages/i18n/diq.json
languages/i18n/gu.json
languages/i18n/nb.json
languages/i18n/pl.json
languages/i18n/pt.json
languages/i18n/ro.json
languages/i18n/sa.json
languages/i18n/ur.json
tests/phpunit/includes/MediaWikiServicesTest.php
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php [new file with mode: 0644]

index 716e56d..52c929a 100644 (file)
@@ -318,6 +318,7 @@ $wgAutoloadLocalClasses = [
        'DataUpdate' => __DIR__ . '/includes/deferred/DataUpdate.php',
        'Database' => __DIR__ . '/includes/libs/rdbms/database/Database.php',
        'DatabaseBase' => __DIR__ . '/includes/libs/rdbms/database/DatabaseBase.php',
+       'DatabaseDomain' => __DIR__ . '/includes/libs/rdbms/database/DatabaseDomain.php',
        'DatabaseInstaller' => __DIR__ . '/includes/installer/DatabaseInstaller.php',
        'DatabaseLag' => __DIR__ . '/maintenance/lag.php',
        'DatabaseLogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
@@ -659,7 +660,7 @@ $wgAutoloadLocalClasses = [
        'LBFactory' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactory.php',
        'LBFactoryMW' => __DIR__ . '/includes/db/loadbalancer/LBFactoryMW.php',
        'LBFactoryMulti' => __DIR__ . '/includes/db/loadbalancer/LBFactoryMulti.php',
-       'LBFactorySimple' => __DIR__ . '/includes/db/loadbalancer/LBFactorySimple.php',
+       'LBFactorySimple' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactorySimple.php',
        'LBFactorySingle' => __DIR__ . '/includes/db/loadbalancer/LBFactorySingle.php',
        'LCStore' => __DIR__ . '/includes/cache/localisation/LCStore.php',
        'LCStoreCDB' => __DIR__ . '/includes/cache/localisation/LCStoreCDB.php',
index c010014..384bfb4 100644 (file)
                        "type": "array",
                        "description": "Parser test suite files to be run by parserTests.php when no specific filename is passed to it"
                },
+               "ServiceWiringFiles": {
+                       "type": "array",
+                       "description": "List of service wiring files to be loaded by the default instance of MediaWikiServices"
+               },
                "load_composer_autoloader": {
                        "type": "boolean",
                        "description": "Load the composer autoloader for this extension, if one is present"
index d707864..c4a1a8d 100644 (file)
                        "type": "array",
                        "description": "Parser test suite files to be run by parserTests.php when no specific filename is passed to it"
                },
+               "ServiceWiringFiles": {
+                       "type": "array",
+                       "description": "List of service wiring files to be loaded by the default instance of MediaWikiServices"
+               },
                "load_composer_autoloader": {
                        "type": "boolean",
                        "description": "Load the composer autoloader for this extension, if one is present"
index 4ab412e..7a34b3a 100644 (file)
@@ -43,15 +43,53 @@ use MediaWiki\MediaWikiServices;
 
 return [
        'DBLoadBalancerFactory' => function( MediaWikiServices $services ) {
-               $config = $services->getMainConfig()->get( 'LBFactoryConf' );
+               $mainConfig = $services->getMainConfig();
 
-               $class = LBFactoryMW::getLBFactoryClass( $config );
-               if ( !isset( $config['readOnlyReason'] ) ) {
+               $lbConf = $mainConfig->get( 'LBFactoryConf' );
+               $lbConf += [
+                       'localDomain' => new DatabaseDomain(
+                               $mainConfig->get( 'DBname' ), null, $mainConfig->get( 'DBprefix' ) ),
                        // TODO: replace the global wfConfiguredReadOnlyReason() with a service.
-                       $config['readOnlyReason'] = wfConfiguredReadOnlyReason();
+                       'readOnlyReason' => wfConfiguredReadOnlyReason(),
+               ];
+
+               $class = LBFactoryMW::getLBFactoryClass( $lbConf );
+               if ( $class === 'LBFactorySimple' ) {
+                       if ( is_array( $mainConfig->get( 'DBservers' ) ) ) {
+                               foreach ( $mainConfig->get( 'DBservers' ) as $i => $server ) {
+                                       $lbConf['servers'][$i] = $server + [
+                                               'schema' => $mainConfig->get( 'DBmwschema' ),
+                                               'tablePrefix' => $mainConfig->get( 'DBprefix' ),
+                                               'flags' => DBO_DEFAULT,
+                                               'sqlMode' => $mainConfig->get( 'SQLMode' ),
+                                               'utf8Mode' => $mainConfig->get( 'DBmysql5' )
+                                       ];
+                               }
+                       } else {
+                               $flags = DBO_DEFAULT;
+                               $flags |= $mainConfig->get( 'DebugDumpSql' ) ? DBO_DEBUG : 0;
+                               $flags |= $mainConfig->get( 'DBssl' ) ? DBO_SSL : 0;
+                               $flags |= $mainConfig->get( 'DBcompress' ) ? DBO_COMPRESS : 0;
+                               $lbConf['servers'] = [
+                                       [
+                                               'host' => $mainConfig->get( 'DBserver' ),
+                                               'user' => $mainConfig->get( 'DBuser' ),
+                                               'password' => $mainConfig->get( 'DBpassword' ),
+                                               'dbname' => $mainConfig->get( 'DBname' ),
+                                               'schema' => $mainConfig->get( 'DBmwschema' ),
+                                               'tablePrefix' => $mainConfig->get( 'DBprefix' ),
+                                               'type' => $mainConfig->get( 'DBtype' ),
+                                               'load' => 1,
+                                               'flags' => $flags,
+                                               'sqlMode' => $mainConfig->get( 'SQLMode' ),
+                                               'utf8Mode' => $mainConfig->get( 'DBmysql5' )
+                                       ]
+                               ];
+                       }
+                       $lbConf['externalServers'] = $mainConfig->get( 'ExternalServers' );
                }
 
-               return new $class( $config );
+               return new $class( LBFactoryMW::applyDefaultConfig( $lbConf ) );
        },
 
        'DBLoadBalancer' => function( MediaWikiServices $services ) {
index ddf5b89..7cda14c 100644 (file)
@@ -504,8 +504,9 @@ if ( !class_exists( 'AutoLoader' ) ) {
 // 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.
 MediaWikiServices::resetGlobalInstance( new GlobalVarConfig(), 'quick' );
-// Apply $wgSharedDB table aliases for the local LB (all non-foreign DB connections)
+
 if ( $wgSharedDB && $wgSharedTables ) {
+       // Apply $wgSharedDB table aliases for the local LB (all non-foreign DB connections)
        MediaWikiServices::getInstance()->getDBLoadBalancer()->setTableAliases(
                array_fill_keys(
                        $wgSharedTables,
@@ -661,6 +662,12 @@ if ( !$wgDBerrorLogTZ ) {
 
 // initialize the request object in $wgRequest
 $wgRequest = RequestContext::getMain()->getRequest(); // BackCompat
+// Set user IP/agent information for causal consistency purposes
+MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->setRequestInfo( [
+       'IPAddress' => $wgRequest->getIP(),
+       'UserAgent' => $wgRequest->getHeader( 'User-Agent' ),
+       'ChronologyProtection' => $wgRequest->getHeader( 'ChronologyProtection' )
+] );
 
 // Useful debug output
 if ( $wgCommandLineMode ) {
index 49d0624..5db7736 100644 (file)
  * @ingroup Database
  */
 
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Services\DestructibleService;
 use MediaWiki\Logger\LoggerFactory;
 
 /**
  * Legacy MediaWiki-specific class for generating database load balancers
  * @ingroup Database
  */
-abstract class LBFactoryMW extends LBFactory implements DestructibleService {
+abstract class LBFactoryMW extends LBFactory {
        /** @noinspection PhpMissingParentConstructorInspection */
        /**
         * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
@@ -37,17 +35,29 @@ abstract class LBFactoryMW extends LBFactory implements DestructibleService {
         * @TODO: inject objects via dependency framework
         */
        public function __construct( array $conf ) {
-               global $wgCommandLineMode, $wgSQLMode, $wgDBmysql5;
+               parent::__construct( self::applyDefaultConfig( $conf ) );
+       }
+
+       /**
+        * @param array $conf
+        * @return array
+        * @TODO: inject objects via dependency framework
+        */
+       public static function applyDefaultConfig( array $conf ) {
+               global $wgCommandLineMode, $wgSQLMode, $wgDBmysql5, $wgDBname, $wgDBprefix;
 
                $defaults = [
-                       'localDomain' => wfWikiID(),
+                       'localDomain' => new DatabaseDomain( $wgDBname, null, $wgDBprefix ),
                        'hostname' => wfHostname(),
+                       'profiler' => Profiler::instance(),
                        'trxProfiler' => Profiler::instance()->getTransactionProfiler(),
                        'replLogger' => LoggerFactory::getInstance( 'DBReplication' ),
                        'queryLogger' => LoggerFactory::getInstance( 'wfLogDBError' ),
                        'connLogger' => LoggerFactory::getInstance( 'wfLogDBError' ),
                        'perfLogger' => LoggerFactory::getInstance( 'DBPerformance' ),
-                       'errorLogger' => [ MWExceptionHandler::class, 'logException' ]
+                       'errorLogger' => [ MWExceptionHandler::class, 'logException' ],
+                       'cliMode' => $wgCommandLineMode,
+                       'agent' => ''
                ];
                // Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804)
                $sCache = ObjectCache::getLocalServerInstance();
@@ -63,15 +73,12 @@ abstract class LBFactoryMW extends LBFactory implements DestructibleService {
                        $defaults['wanCache'] = $wCache;
                }
 
-               $this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
-               $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : $wgCommandLineMode;
-
                if ( isset( $conf['serverTemplate'] ) ) { // LBFactoryMulti
                        $conf['serverTemplate']['sqlMode'] = $wgSQLMode;
                        $conf['serverTemplate']['utf8Mode'] = $wgDBmysql5;
                }
 
-               parent::__construct( $conf + $defaults );
+               return $conf + $defaults;
        }
 
        /**
@@ -103,57 +110,4 @@ abstract class LBFactoryMW extends LBFactory implements DestructibleService {
 
                return $class;
        }
-
-       /**
-        * @return bool
-        * @since 1.27
-        * @deprecated Since 1.28; use laggedReplicaUsed()
-        */
-       public function laggedSlaveUsed() {
-               return $this->laggedReplicaUsed();
-       }
-
-       protected function newChronologyProtector() {
-               $request = RequestContext::getMain()->getRequest();
-               $chronProt = new ChronologyProtector(
-                       ObjectCache::getMainStashInstance(),
-                       [
-                               'ip' => $request->getIP(),
-                               'agent' => $request->getHeader( 'User-Agent' ),
-                       ],
-                       $request->getFloat( 'cpPosTime', $request->getCookie( 'cpPosTime', '' ) )
-               );
-               if ( PHP_SAPI === 'cli' ) {
-                       $chronProt->setEnabled( false );
-               } elseif ( $request->getHeader( 'ChronologyProtection' ) === 'false' ) {
-                       // Request opted out of using position wait logic. This is useful for requests
-                       // done by the job queue or background ETL that do not have a meaningful session.
-                       $chronProt->setWaitEnabled( false );
-               }
-
-               return $chronProt;
-       }
-
-       /**
-        * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed
-        *
-        * Note that unlike cookies, this works accross domains
-        *
-        * @param string $url
-        * @param float $time UNIX timestamp just before shutdown() was called
-        * @return string
-        * @since 1.28
-        */
-       public function appendPreShutdownTimeAsQuery( $url, $time ) {
-               $usedCluster = 0;
-               $this->forEachLB( function ( LoadBalancer $lb ) use ( &$usedCluster ) {
-                       $usedCluster |= ( $lb->getServerCount() > 1 );
-               } );
-
-               if ( !$usedCluster ) {
-                       return $url; // no master/replica clusters touched
-               }
-
-               return wfAppendQuery( $url, [ 'cpPosTime' => $time ] );
-       }
 }
index 95bc8f4..1f7f528 100644 (file)
@@ -254,7 +254,7 @@ class LBFactoryMulti extends LBFactoryMW {
                $section = $this->getSectionForWiki( $wiki );
                if ( !isset( $this->mainLBs[$section] ) ) {
                        $lb = $this->newMainLB( $wiki );
-                       $this->chronProt->initLB( $lb );
+                       $this->getChronologyProtector()->initLB( $lb );
                        $this->mainLBs[$section] = $lb;
                }
 
@@ -295,7 +295,7 @@ class LBFactoryMulti extends LBFactoryMW {
        public function getExternalLB( $cluster, $wiki = false ) {
                if ( !isset( $this->extLBs[$cluster] ) ) {
                        $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
-                       $this->chronProt->initLB( $this->extLBs[$cluster] );
+                       $this->getChronologyProtector()->initLB( $this->extLBs[$cluster] );
                }
 
                return $this->extLBs[$cluster];
diff --git a/includes/db/loadbalancer/LBFactorySimple.php b/includes/db/loadbalancer/LBFactorySimple.php
deleted file mode 100644 (file)
index d937ddb..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-<?php
-/**
- * Generator of database load balancing objects.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Database
- */
-
-/**
- * A simple single-master LBFactory that gets its configuration from the b/c globals
- */
-class LBFactorySimple extends LBFactoryMW {
-       /** @var LoadBalancer */
-       private $mainLB;
-       /** @var LoadBalancer[] */
-       private $extLBs = [];
-
-       /** @var string */
-       private $loadMonitorClass;
-
-       public function __construct( array $conf ) {
-               parent::__construct( $conf );
-
-               $this->loadMonitorClass = isset( $conf['loadMonitorClass'] )
-                       ? $conf['loadMonitorClass']
-                       : null;
-       }
-
-       /**
-        * @param bool|string $wiki
-        * @return LoadBalancer
-        */
-       public function newMainLB( $wiki = false ) {
-               global $wgDBservers, $wgDBprefix, $wgDBmwschema, $wgSQLMode, $wgDBmysql5;
-
-               if ( is_array( $wgDBservers ) ) {
-                       $servers = $wgDBservers;
-                       foreach ( $servers as $i => &$server ) {
-                               if ( $i == 0 ) {
-                                       $server['master'] = true;
-                               } else {
-                                       $server['replica'] = true;
-                               }
-                               $server += [
-                                       'schema' => $wgDBmwschema,
-                                       'tablePrefix' => $wgDBprefix,
-                                       'flags' => DBO_DEFAULT,
-                                       'sqlMode' => $wgSQLMode,
-                                       'utf8Mode' => $wgDBmysql5
-                               ];
-                       }
-               } else {
-                       global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
-                       global $wgDBssl, $wgDBcompress;
-
-                       $flags = DBO_DEFAULT;
-                       if ( $wgDebugDumpSql ) {
-                               $flags |= DBO_DEBUG;
-                       }
-                       if ( $wgDBssl ) {
-                               $flags |= DBO_SSL;
-                       }
-                       if ( $wgDBcompress ) {
-                               $flags |= DBO_COMPRESS;
-                       }
-
-                       $servers = [ [
-                               'host' => $wgDBserver,
-                               'user' => $wgDBuser,
-                               'password' => $wgDBpassword,
-                               'dbname' => $wgDBname,
-                               'schema' => $wgDBmwschema,
-                               'tablePrefix' => $wgDBprefix,
-                               'type' => $wgDBtype,
-                               'load' => 1,
-                               'flags' => $flags,
-                               'master' => true,
-                               'sqlMode' => $wgSQLMode,
-                               'utf8Mode' => $wgDBmysql5
-                       ] ];
-               }
-
-               return $this->newLoadBalancer( $servers );
-       }
-
-       /**
-        * @param bool|string $wiki
-        * @return LoadBalancer
-        */
-       public function getMainLB( $wiki = false ) {
-               if ( !isset( $this->mainLB ) ) {
-                       $this->mainLB = $this->newMainLB( $wiki );
-                       $this->chronProt->initLB( $this->mainLB );
-               }
-
-               return $this->mainLB;
-       }
-
-       /**
-        * @param string $cluster
-        * @param bool|string $wiki
-        * @return LoadBalancer
-        * @throws InvalidArgumentException
-        */
-       protected function newExternalLB( $cluster, $wiki = false ) {
-               global $wgExternalServers;
-               if ( !isset( $wgExternalServers[$cluster] ) ) {
-                       throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
-               }
-
-               return $this->newLoadBalancer( $wgExternalServers[$cluster] );
-       }
-
-       /**
-        * @param string $cluster
-        * @param bool|string $wiki
-        * @return array
-        */
-       public function getExternalLB( $cluster, $wiki = false ) {
-               if ( !isset( $this->extLBs[$cluster] ) ) {
-                       $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
-                       $this->chronProt->initLB( $this->extLBs[$cluster] );
-               }
-
-               return $this->extLBs[$cluster];
-       }
-
-       private function newLoadBalancer( array $servers ) {
-               $lb = new LoadBalancer( array_merge(
-                       $this->baseLoadBalancerParams(),
-                       [
-                               'servers' => $servers,
-                               'loadMonitor' => $this->loadMonitorClass,
-                       ]
-               ) );
-               $this->initLoadBalancer( $lb );
-
-               return $lb;
-       }
-
-       /**
-        * Execute a function for each tracked load balancer
-        * The callback is called with the load balancer as the first parameter,
-        * and $params passed as the subsequent parameters.
-        *
-        * @param callable $callback
-        * @param array $params
-        */
-       public function forEachLB( $callback, array $params = [] ) {
-               if ( isset( $this->mainLB ) ) {
-                       call_user_func_array( $callback, array_merge( [ $this->mainLB ], $params ) );
-               }
-               foreach ( $this->extLBs as $lb ) {
-                       call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
-               }
-       }
-}
index 876ee30..0d9b692 100644 (file)
@@ -14,22 +14,22 @@ class DBConnRef implements IDatabase {
        /** @var IDatabase|null Live connection handle */
        private $conn;
 
-       /** @var array|null */
+       /** @var array|null N-tuple of (server index, group, DatabaseDomain|string) */
        private $params;
 
        const FLD_INDEX = 0;
        const FLD_GROUP = 1;
-       const FLD_WIKI = 2;
+       const FLD_DOMAIN = 2;
 
        /**
         * @param ILoadBalancer $lb
-        * @param IDatabase|array $conn Connection or (server index, group, wiki ID)
+        * @param IDatabase|array $conn Connection or (server index, group, DatabaseDomain|string)
         */
        public function __construct( ILoadBalancer $lb, $conn ) {
                $this->lb = $lb;
                if ( $conn instanceof IDatabase ) {
                        $this->conn = $conn; // live handle
-               } elseif ( count( $conn ) >= 3 && $conn[self::FLD_WIKI] !== false ) {
+               } elseif ( count( $conn ) >= 3 && $conn[self::FLD_DOMAIN] !== false ) {
                        $this->params = $conn;
                } else {
                        throw new InvalidArgumentException( "Missing lazy connection arguments." );
@@ -147,8 +147,9 @@ class DBConnRef implements IDatabase {
 
        public function getDomainID() {
                if ( $this->conn === null ) {
-                       // Avoid triggering a connection
-                       return $this->params[self::FLD_WIKI];
+                       $domain = $this->params[self::FLD_DOMAIN];
+                       // Avoid triggering a database connection
+                       return $domain instanceof DatabaseDomain ? $domain->getId() : $domain;
                }
 
                return $this->__call( __FUNCTION__, func_get_args() );
index 9a63b7f..3672f4a 100644 (file)
@@ -112,6 +112,8 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
        protected $htmlErrors;
        /** @var string */
        protected $delimiter = ';';
+       /** @var DatabaseDomain */
+       protected $currentDomain;
 
        /**
         * Either 1 if a transaction is active or 0 otherwise.
@@ -223,7 +225,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
        /** @var int[] Prior mFlags values */
        private $priorFlags = [];
 
-       /** @var Profiler */
+       /** @var object|string Class name or object With profileIn/profileOut methods */
        protected $profiler;
        /** @var TransactionProfiler */
        protected $trxProfiler;
@@ -272,9 +274,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                        ? $params['srvCache']
                        : new HashBagOStuff();
 
-               $this->profiler = isset( $params['profiler'] )
-                       ? $params['profiler']
-                       : Profiler::instance(); // @TODO: remove global state
+               $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
                $this->trxProfiler = isset( $params['trxProfiler'] )
                        ? $params['trxProfiler']
                        : new TransactionProfiler();
@@ -288,6 +288,10 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                if ( $user ) {
                        $this->open( $server, $user, $password, $dbName );
                }
+
+               $this->currentDomain = ( $this->mDBname != '' )
+                       ? new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix )
+                       : DatabaseDomain::newUnspecified();
        }
 
        /**
@@ -442,6 +446,9 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                $old = $this->mTablePrefix;
                if ( $prefix !== null ) {
                        $this->mTablePrefix = $prefix;
+                       $this->currentDomain = ( $this->mDBname != '' )
+                               ? new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix )
+                               : DatabaseDomain::newUnspecified();
                }
 
                return $old;
@@ -498,14 +505,6 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                return $this->lazyMasterHandle;
        }
 
-       /**
-        * @param TransactionProfiler $profiler
-        * @since 1.27
-        */
-       public function setTransactionProfiler( TransactionProfiler $profiler ) {
-               $this->trxProfiler = $profiler;
-       }
-
        public function implicitGroupby() {
                return true;
        }
@@ -621,11 +620,7 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
        }
 
        public function getDomainID() {
-               if ( $this->mTablePrefix != '' ) {
-                       return "{$this->mDBname}-{$this->mTablePrefix}";
-               } else {
-                       return $this->mDBname;
-               }
+               return $this->currentDomain->getId();
        }
 
        final public function getWikiID() {
@@ -892,9 +887,13 @@ abstract class Database implements IDatabase, LoggerAwareInterface {
                $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
 
                $startTime = microtime( true );
-               $this->profiler->profileIn( $queryProf );
+               if ( $this->profiler ) {
+                       call_user_func( [ $this->profiler, 'profileIn' ], $queryProf );
+               }
                $ret = $this->doQuery( $commentedSql );
-               $this->profiler->profileOut( $queryProf );
+               if ( $this->profiler ) {
+                       call_user_func( [ $this->profiler, 'profileOut' ], $queryProf );
+               }
                $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
 
                unset( $queryProfSection ); // profile out (if set)
diff --git a/includes/libs/rdbms/database/DatabaseDomain.php b/includes/libs/rdbms/database/DatabaseDomain.php
new file mode 100644 (file)
index 0000000..01b6b21
--- /dev/null
@@ -0,0 +1,203 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
+/**
+ * Class to handle database/prefix specification for IDatabase domains
+ */
+class DatabaseDomain {
+       /** @var string|null */
+       private $database;
+       /** @var string|null */
+       private $schema;
+       /** @var string */
+       private $prefix;
+
+       /** @var string Cache of convertToString() */
+       private $equivalentString;
+
+       /**
+        * @param string|null $database Database name
+        * @param string|null $schema Schema name
+        * @param string $prefix Table prefix
+        */
+       public function __construct( $database, $schema, $prefix ) {
+               if ( $database !== null && ( !is_string( $database ) || !strlen( $database ) ) ) {
+                       throw new InvalidArgumentException( "Database must be null or a non-empty string." );
+               }
+               $this->database = $database;
+               if ( $schema !== null && ( !is_string( $schema ) || !strlen( $schema ) ) ) {
+                       throw new InvalidArgumentException( "Schema must be null or a non-empty string." );
+               }
+               $this->schema = $schema;
+               if ( !is_string( $prefix ) ) {
+                       throw new InvalidArgumentException( "Prefix must be a string." );
+               }
+               $this->prefix = $prefix;
+               $this->equivalentString = $this->convertToString();
+       }
+
+       /**
+        * @param DatabaseDomain|string $domain Result of DatabaseDomain::toString()
+        * @return DatabaseDomain
+        */
+       public static function newFromId( $domain ) {
+               if ( $domain instanceof self ) {
+                       return $domain;
+               }
+
+               $parts = array_map( [ __CLASS__, 'decode' ], explode( '-', $domain ) );
+
+               $schema = null;
+               $prefix = '';
+
+               if ( count( $parts ) == 1 ) {
+                       $database = $parts[0];
+               } elseif ( count( $parts ) == 2 ) {
+                       list( $database, $prefix ) = $parts;
+               } elseif ( count( $parts ) == 3 ) {
+                       list( $database, $schema, $prefix ) = $parts;
+               } else {
+                       throw new InvalidArgumentException( "Domain has too few or too many parts." );
+               }
+
+               if ( $database === '' ) {
+                       $database = null;
+               }
+
+               return new self( $database, $schema, $prefix );
+       }
+
+       /**
+        * @return DatabaseDomain
+        */
+       public static function newUnspecified() {
+               return new self( null, null, '' );
+       }
+
+       /**
+        * @param DatabaseDomain|string $other
+        * @return bool
+        */
+       public function equals( $other ) {
+               if ( $other instanceof DatabaseDomain ) {
+                       return (
+                               $this->database === $other->database &&
+                               $this->schema === $other->schema &&
+                               $this->prefix === $other->prefix
+                       );
+               }
+
+               return ( $this->equivalentString === $other );
+       }
+
+       /**
+        * @return string|null Database name
+        */
+       public function getDatabase() {
+               return $this->database;
+       }
+
+       /**
+        * @return string|null Database schema
+        */
+       public function getSchema() {
+               return $this->schema;
+       }
+
+       /**
+        * @return string Table prefix
+        */
+       public function getTablePrefix() {
+               return $this->prefix;
+       }
+
+       /**
+        * @return string
+        */
+       public function getId() {
+               return $this->equivalentString;
+       }
+
+       /**
+        * @return string
+        */
+       private function convertToString() {
+               $parts = [ $this->database ];
+               if ( $this->schema !== null ) {
+                       $parts[] = $this->schema;
+               }
+               if ( $this->prefix != '' ) {
+                       $parts[] = $this->prefix;
+               }
+
+               return implode( '-', array_map( [ __CLASS__, 'encode' ], $parts ) );
+       }
+
+       private static function encode( $decoded ) {
+               $encoded = '';
+
+               $length = strlen( $decoded );
+               for ( $i = 0; $i < $length; ++$i ) {
+                       $char = $decoded[$i];
+                       if ( $char === '-' ) {
+                               $encoded .= '?h';
+                       } elseif ( $char === '?' ) {
+                               $encoded .= '??';
+                       } else {
+                               $encoded .= $char;
+                       }
+               }
+
+               return $encoded;
+       }
+
+       private static function decode( $encoded ) {
+               $decoded = '';
+
+               $length = strlen( $encoded );
+               for ( $i = 0; $i < $length; ++$i ) {
+                       $char = $encoded[$i];
+                       if ( $char === '?' ) {
+                               $nextChar = isset( $encoded[$i + 1] ) ? $encoded[$i + 1] : null;
+                               if ( $nextChar === 'h' ) {
+                                       $decoded .= '-';
+                                       ++$i;
+                               } elseif ( $nextChar === '?' ) {
+                                       $decoded .= '?';
+                                       ++$i;
+                               } else {
+                                       $decoded .= $char;
+                               }
+                       } else {
+                               $decoded .= $char;
+                       }
+               }
+
+               return $decoded;
+       }
+
+       /**
+        * @return string
+        */
+       function __toString() {
+               return $this->getId();
+       }
+}
index 49fac6a..00474fe 100644 (file)
@@ -30,6 +30,8 @@ use Psr\Log\LoggerInterface;
 abstract class LBFactory {
        /** @var ChronologyProtector */
        protected $chronProt;
+       /** @var object|string Class name or object With profileIn/profileOut methods */
+       protected $profiler;
        /** @var TransactionProfiler */
        protected $trxProfiler;
        /** @var LoggerInterface */
@@ -49,10 +51,13 @@ abstract class LBFactory {
        /** @var WANObjectCache */
        protected $wanCache;
 
-       /** @var string Local domain */
+       /** @var DatabaseDomain Local domain */
        protected $localDomain;
        /** @var string Local hostname of the app server */
        protected $hostname;
+       /** @var array Web request information about the client */
+       protected $requestInfo;
+
        /** @var mixed */
        protected $ticket;
        /** @var string|bool String if a requested DBO_TRX transaction round is active */
@@ -79,7 +84,9 @@ abstract class LBFactory {
         * @param array $conf
         */
        public function __construct( array $conf ) {
-               $this->localDomain = isset( $conf['localDomain'] ) ? $conf['localDomain'] : '';
+               $this->localDomain = isset( $conf['localDomain'] )
+                       ? DatabaseDomain::newFromId( $conf['localDomain'] )
+                       : DatabaseDomain::newUnspecified();
 
                if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
                        $this->readOnlyReason = $conf['readOnlyReason'];
@@ -99,20 +106,25 @@ abstract class LBFactory {
                        : function ( Exception $e ) {
                                trigger_error( E_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
                        };
-               $this->hostname = isset( $conf['hostname'] )
-                       ? $conf['hostname']
-                       : gethostname();
 
-               $this->chronProt = isset( $conf['chronProt'] )
-                       ? $conf['chronProt']
-                       : $this->newChronologyProtector();
+               $this->chronProt = isset( $conf['chronProt'] ) ? $conf['chronProt'] : null;
+
+               $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
                $this->trxProfiler = isset( $conf['trxProfiler'] )
                        ? $conf['trxProfiler']
                        : new TransactionProfiler();
 
-               $this->ticket = mt_rand();
+               $this->requestInfo = [
+                       'IPAddress' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
+                       'UserAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
+                       'ChronologyProtection' => 'true'
+               ];
+
                $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli';
+               $this->hostname = isset( $conf['hostname'] ) ? $conf['hostname'] : gethostname();
                $this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
+
+               $this->ticket = mt_rand();
        }
 
        /**
@@ -180,10 +192,11 @@ abstract class LBFactory {
        public function shutdown(
                $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
        ) {
+               $chronProt = $this->getChronologyProtector();
                if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
-                       $this->shutdownChronologyProtector( $this->chronProt, $workCallback, 'sync' );
+                       $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
                } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
-                       $this->shutdownChronologyProtector( $this->chronProt, null, 'async' );
+                       $this->shutdownChronologyProtector( $chronProt, null, 'async' );
                }
 
                $this->commitMasterChanges( __METHOD__ ); // sanity
@@ -534,7 +547,7 @@ abstract class LBFactory {
         * @since 1.28
         */
        public function getChronologyProtectorTouched( $dbName ) {
-               return $this->chronProt->getTouched( $dbName );
+               return $this->getChronologyProtector()->getTouched( $dbName );
        }
 
        /**
@@ -545,27 +558,39 @@ abstract class LBFactory {
         * @since 1.27
         */
        public function disableChronologyProtection() {
-               $this->chronProt->setEnabled( false );
+               $this->getChronologyProtector()->setEnabled( false );
        }
 
        /**
         * @return ChronologyProtector
         */
-       protected function newChronologyProtector() {
-               $chronProt = new ChronologyProtector(
+       protected function getChronologyProtector() {
+               if ( $this->chronProt ) {
+                       return $this->chronProt;
+               }
+
+               $this->chronProt = new ChronologyProtector(
                        $this->memCache,
                        [
-                               'ip' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
-                               'agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : ''
+                               'ip' => $this->requestInfo['IPAddress'],
+                               'agent' => $this->requestInfo['UserAgent'],
                        ],
                        isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null
                );
-               $chronProt->setLogger( $this->replLogger );
+               $this->chronProt->setLogger( $this->replLogger );
+
                if ( $this->cliMode ) {
-                       $chronProt->setEnabled( false );
+                       $this->chronProt->setEnabled( false );
+               } elseif ( $this->requestInfo['ChronologyProtection'] === 'false' ) {
+                       // Request opted out of using position wait logic. This is useful for requests
+                       // done by the job queue or background ETL that do not have a meaningful session.
+                       $this->chronProt->setWaitEnabled( false );
                }
 
-               return $chronProt;
+               $this->replLogger->debug( __METHOD__ . ': using request info ' .
+                       json_encode( $this->requestInfo, JSON_PRETTY_PRINT ) );
+
+               return $this->chronProt;
        }
 
        /**
@@ -611,6 +636,7 @@ abstract class LBFactory {
                        'readOnlyReason' => $this->readOnlyReason,
                        'srvCache' => $this->srvCache,
                        'wanCache' => $this->wanCache,
+                       'profiler' => $this->profiler,
                        'trxProfiler' => $this->trxProfiler,
                        'queryLogger' => $this->queryLogger,
                        'connLogger' => $this->connLogger,
@@ -638,8 +664,11 @@ abstract class LBFactory {
         * @since 1.28
         */
        public function setDomainPrefix( $prefix ) {
-               list( $dbName, ) = explode( '-', $this->localDomain, 2 );
-               $this->localDomain = "{$dbName}-{$prefix}";
+               $this->localDomain = new DatabaseDomain(
+                       $this->localDomain->getDatabase(),
+                       null,
+                       $prefix
+               );
 
                $this->forEachLB( function( LoadBalancer $lb ) use ( $prefix ) {
                        $lb->setDomainPrefix( $prefix );
@@ -661,4 +690,42 @@ abstract class LBFactory {
        public function setAgentName( $agent ) {
                $this->agent = $agent;
        }
+
+       /**
+        * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed
+        *
+        * Note that unlike cookies, this works accross domains
+        *
+        * @param string $url
+        * @param float $time UNIX timestamp just before shutdown() was called
+        * @return string
+        * @since 1.28
+        */
+       public function appendPreShutdownTimeAsQuery( $url, $time ) {
+               $usedCluster = 0;
+               $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$usedCluster ) {
+                       $usedCluster |= ( $lb->getServerCount() > 1 );
+               } );
+
+               if ( !$usedCluster ) {
+                       return $url; // no master/replica clusters touched
+               }
+
+               return strpos( $url, '?' ) === false ? "$url?cpPosTime=$time" : "$url&cpPosTime=$time";
+       }
+
+       /**
+        * @param array $info Map of fields, including:
+        *   - IPAddress : IP address
+        *   - UserAgent : User-Agent HTTP header
+        *   - ChronologyProtection : cookie/header value specifying ChronologyProtector usage
+        * @since 1.28
+        */
+       public function setRequestInfo( array $info ) {
+               $this->requestInfo = $info + $this->requestInfo;
+       }
+
+       function __destruct() {
+               $this->destroy();
+       }
 }
diff --git a/includes/libs/rdbms/lbfactory/LBFactorySimple.php b/includes/libs/rdbms/lbfactory/LBFactorySimple.php
new file mode 100644 (file)
index 0000000..0476cf2
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Generator of database load balancing objects.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
+/**
+ * A simple single-master LBFactory that gets its configuration from the b/c globals
+ */
+class LBFactorySimple extends LBFactory {
+       /** @var LoadBalancer */
+       private $mainLB;
+       /** @var LoadBalancer[] */
+       private $extLBs = [];
+
+       /** @var array[] Map of (server index => server config) */
+       private $servers = [];
+       /** @var array[] Map of (cluster => (server index => server config)) */
+       private $externalClusters = [];
+
+       /** @var string */
+       private $loadMonitorClass;
+
+       public function __construct( array $conf ) {
+               parent::__construct( $conf );
+
+               $this->servers = isset( $conf['servers'] ) ? $conf['servers'] : [];
+               foreach ( $this->servers as $i => $server ) {
+                       if ( $i == 0 ) {
+                               $this->servers[$i]['master'] = true;
+                       } else {
+                               $this->servers[$i]['replica'] = true;
+                       }
+               }
+
+               $this->externalClusters = isset( $conf['externalClusters'] )
+                       ? $conf['externalClusters']
+                       : [];
+               $this->loadMonitorClass = isset( $conf['loadMonitorClass'] )
+                       ? $conf['loadMonitorClass']
+                       : null;
+       }
+
+       /**
+        * @param bool|string $domain
+        * @return LoadBalancer
+        */
+       public function newMainLB( $domain = false ) {
+               return $this->newLoadBalancer( $this->servers );
+       }
+
+       /**
+        * @param bool|string $domain
+        * @return LoadBalancer
+        */
+       public function getMainLB( $domain = false ) {
+               if ( !isset( $this->mainLB ) ) {
+                       $this->mainLB = $this->newMainLB( $domain );
+                       $this->getChronologyProtector()->initLB( $this->mainLB );
+               }
+
+               return $this->mainLB;
+       }
+
+       /**
+        * @param string $cluster
+        * @param bool|string $domain
+        * @return LoadBalancer
+        * @throws InvalidArgumentException
+        */
+       protected function newExternalLB( $cluster, $domain = false ) {
+               if ( !isset( $this->externalClusters[$cluster] ) ) {
+                       throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
+               }
+
+               return $this->newLoadBalancer( $this->externalClusters[$cluster] );
+       }
+
+       /**
+        * @param string $cluster
+        * @param bool|string $domain
+        * @return array
+        */
+       public function getExternalLB( $cluster, $domain = false ) {
+               if ( !isset( $this->extLBs[$cluster] ) ) {
+                       $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $domain );
+                       $this->getChronologyProtector()->initLB( $this->extLBs[$cluster] );
+               }
+
+               return $this->extLBs[$cluster];
+       }
+
+       private function newLoadBalancer( array $servers ) {
+               $lb = new LoadBalancer( array_merge(
+                       $this->baseLoadBalancerParams(),
+                       [
+                               'servers' => $servers,
+                               'loadMonitor' => $this->loadMonitorClass,
+                       ]
+               ) );
+               $this->initLoadBalancer( $lb );
+
+               return $lb;
+       }
+
+       /**
+        * Execute a function for each tracked load balancer
+        * The callback is called with the load balancer as the first parameter,
+        * and $params passed as the subsequent parameters.
+        *
+        * @param callable $callback
+        * @param array $params
+        */
+       public function forEachLB( $callback, array $params = [] ) {
+               if ( isset( $this->mainLB ) ) {
+                       call_user_func_array( $callback, array_merge( [ $this->mainLB ], $params ) );
+               }
+               foreach ( $this->extLBs as $lb ) {
+                       call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
+               }
+       }
+}
index 57c905f..75ecd27 100644 (file)
@@ -53,6 +53,8 @@ class LoadBalancer implements ILoadBalancer {
        private $memCache;
        /** @var WANObjectCache */
        private $wanCache;
+       /** @var object|string Class name or object With profileIn/profileOut methods */
+       protected $profiler;
        /** @var TransactionProfiler */
        protected $trxProfiler;
        /** @var LoggerInterface */
@@ -84,8 +86,10 @@ class LoadBalancer implements ILoadBalancer {
        private $trxRoundId = false;
        /** @var array[] Map of (name => callable) */
        private $trxRecurringCallbacks = [];
-       /** @var string Local Domain ID and default for selectDB() calls */
+       /** @var DatabaseDomain Local Domain ID and default for selectDB() calls */
        private $localDomain;
+       /** @var string Alternate ID string for the domain instead of DatabaseDomain::getId() */
+       private $localDomainIdAlias;
        /** @var string Current server name */
        private $host;
        /** @var bool Whether this PHP instance is for a CLI script */
@@ -113,10 +117,22 @@ class LoadBalancer implements ILoadBalancer {
                        throw new InvalidArgumentException( __CLASS__ . ': missing servers parameter' );
                }
                $this->mServers = $params['servers'];
+
+               $this->localDomain = isset( $params['localDomain'] )
+                       ? DatabaseDomain::newFromId( $params['localDomain'] )
+                       : DatabaseDomain::newUnspecified();
+               // In case a caller assumes that the domain ID is simply <db>-<prefix>, which is almost
+               // always true, gracefully handle the case when they fail to account for escaping.
+               if ( $this->localDomain->getTablePrefix() != '' ) {
+                       $this->localDomainIdAlias =
+                               $this->localDomain->getDatabase() . '-' . $this->localDomain->getTablePrefix();
+               } else {
+                       $this->localDomainIdAlias = $this->localDomain->getDatabase();
+               }
+
                $this->mWaitTimeout = isset( $params['waitTimeout'] )
                        ? $params['waitTimeout']
                        : self::POS_WAIT_TIMEOUT;
-               $this->localDomain = isset( $params['localDomain'] ) ? $params['localDomain'] : '';
 
                $this->mReadIndex = -1;
                $this->mConns = [
@@ -170,6 +186,7 @@ class LoadBalancer implements ILoadBalancer {
                } else {
                        $this->wanCache = WANObjectCache::newEmpty();
                }
+               $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
                if ( isset( $params['trxProfiler'] ) ) {
                        $this->trxProfiler = $params['trxProfiler'];
                } else {
@@ -514,7 +531,7 @@ class LoadBalancer implements ILoadBalancer {
                                ' with invalid server index' );
                }
 
-               if ( $domain === $this->localDomain ) {
+               if ( $this->localDomain->equals( $domain ) || $domain === $this->localDomainIdAlias ) {
                        $domain = false; // local connection requested
                }
 
@@ -652,7 +669,7 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function openConnection( $i, $domain = false ) {
-               if ( $domain === $this->localDomain ) {
+               if ( $this->localDomain->equals( $domain ) || $domain === $this->localDomainIdAlias ) {
                        $domain = false; // local connection requested
                }
 
@@ -708,7 +725,9 @@ class LoadBalancer implements ILoadBalancer {
         * @return IDatabase
         */
        private function openForeignConnection( $i, $domain ) {
-               list( $dbName, $prefix ) = explode( '-', $domain, 2 ) + [ '', '' ];
+               $domainInstance = DatabaseDomain::newFromId( $domain );
+               $dbName = $domainInstance->getDatabase();
+               $prefix = $domainInstance->getTablePrefix();
 
                if ( isset( $this->mConns['foreignUsed'][$i][$domain] ) ) {
                        // Reuse an already-used connection
@@ -821,6 +840,7 @@ class LoadBalancer implements ILoadBalancer {
                // Set loggers
                $server['connLogger'] = $this->connLogger;
                $server['queryLogger'] = $this->queryLogger;
+               $server['profiler'] = $this->profiler;
                $server['trxProfiler'] = $this->trxProfiler;
                $server['cliMode'] = $this->cliMode;
                $server['errorLogger'] = $this->errorLogger;
@@ -1612,8 +1632,11 @@ class LoadBalancer implements ILoadBalancer {
         * @since 1.28
         */
        public function setDomainPrefix( $prefix ) {
-               list( $dbName, ) = explode( '-', $this->localDomain, 2 );
-               $this->localDomain = "{$dbName}-{$prefix}";
+               $this->localDomain = new DatabaseDomain(
+                       $this->localDomain->getDatabase(),
+                       null,
+                       $prefix
+               );
 
                $this->forEachOpenConnection( function ( IDatabase $db ) use ( $prefix ) {
                        $db->tablePrefix( $prefix );
index 5555e8b..745c233 100644 (file)
@@ -110,6 +110,7 @@ class ExtensionProcessor implements Processor {
                'type',
                'config',
                'config_prefix',
+               'ServiceWiringFiles',
                'ParserTestFiles',
                'AutoloadClasses',
                'manifest_version',
@@ -174,6 +175,7 @@ class ExtensionProcessor implements Processor {
                $this->extractMessagesDirs( $dir, $info );
                $this->extractNamespaces( $info );
                $this->extractResourceLoaderModules( $dir, $info );
+               $this->extractServiceWiringFiles( $dir, $info );
                $this->extractParserTestFiles( $dir, $info );
                if ( isset( $info['callback'] ) ) {
                        $this->callbacks[] = $info['callback'];
@@ -406,6 +408,14 @@ class ExtensionProcessor implements Processor {
                }
        }
 
+       protected function extractServiceWiringFiles( $dir, array $info ) {
+               if ( isset( $info['ServiceWiringFiles'] ) ) {
+                       foreach ( $info['ServiceWiringFiles'] as $path ) {
+                               $this->globals['wgServiceWiringFiles'][] = "$dir/$path";
+                       }
+               }
+       }
+
        protected function extractParserTestFiles( $dir, array $info ) {
                if ( isset( $info['ParserTestFiles'] ) ) {
                        foreach ( $info['ParserTestFiles'] as $path ) {
index 2b802ba..bf83e7b 100644 (file)
@@ -499,12 +499,20 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
 
                $form = $this->getAuthForm( $requests, $this->authAction, $msg, $msgtype );
                $form->prepareForm();
+
                $submitStatus = Status::newGood();
                if ( $msg && $msgtype === 'warning' ) {
                        $submitStatus->warning( $msg );
                } elseif ( $msg && $msgtype === 'error' ) {
                        $submitStatus->fatal( $msg );
                }
+
+               // warning header for non-standard workflows (e.g. security reauthentication)
+               if ( !$this->isSignup() && $this->getUser()->isLoggedIn() ) {
+                       $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
+                       $submitStatus->warning( $reauthMessage, $this->getUser()->getName() );
+               }
+
                $formHtml = $form->getHTML( $submitStatus );
 
                $out->addHTML( $this->getPageHtml( $formHtml ) );
@@ -627,13 +635,6 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                        $form->setId( 'userlogin2' );
                }
 
-               // warning header for non-standard workflows (e.g. security reauthentication)
-               if ( !$this->isSignup() && $this->getUser()->isLoggedIn() ) {
-                       $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
-                       $form->addHeaderText( Html::rawElement( 'div', [ 'class' => 'warningbox' ],
-                               $this->msg( $reauthMessage )->params( $this->getUser()->getName() )->parse() ) );
-               }
-
                $form->suppressDefaultSubmit();
 
                $this->authForm = $form;
index 1492094..95c7a5d 100644 (file)
        "tog-watchlisthideliu": "أخف تعديلات المستخدمين المسجلين في قائمة المراقبة",
        "tog-watchlistreloadautomatically": "أعد تحميل قائمة المراقبة بصفة آلية حينما يتغير مرشح ما (يتطلب جافاسكربت)",
        "tog-watchlisthideanons": "أخف تعديلات المستخدمين المجهولين في قائمة المراقبة",
-       "tog-watchlisthidepatrolled": " أخف التعديلات المراجعة في قائمة المراقبة",
+       "tog-watchlisthidepatrolled": "أخف التعديلات المراجعة في قائمة المراقبة",
        "tog-watchlisthidecategorization": "أخف تصنيف الصفحات",
        "tog-ccmeonemails": "أرسل إلي نسخا من الرسائل الإلكترونية التي أرسلها إلى المستخدمين الآخرين",
        "tog-diffonly": "لا تعرض محتوى الصفحة أسفل الفرق",
        "exif-lens": "العدسة المستخدمة",
        "exif-serialnumber": "الرقم التسلسلي للكاميرا",
        "exif-cameraownername": "مالك الكاميرا",
-       "exif-label": "عÙ\84اÙ\85ة",
+       "exif-label": "اÙ\84تسÙ\85Ù\8aة",
        "exif-datetimemetadata": "آخر تعديل للبيانات التعريفية",
        "exif-nickname": "الاسم غير الرسمي للصورة",
        "exif-rating": "التقييم (من 5)",
index 32ddf70..451c046 100644 (file)
        "newwindow": "(адкрываецца ў новым акне)",
        "cancel": "Скасаваць",
        "moredotdotdot": "Далей…",
-       "morenotlisted": "Гэта ня поўны сьпіс.",
+       "morenotlisted": "Гэты сьпіс можа быць няпоўным.",
        "mypage": "Старонка",
        "mytalk": "Гутаркі",
        "anontalk": "Гутаркі",
        "tag-filter-submit": "Фільтар",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|1=Метка|Меткі}}]]: $2)",
        "tag-mw-contentmodelchange": "зьмена мадэлі зьместу",
+       "tag-mw-contentmodelchange-description": "Рэдагаваньні, якія [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel зьмяняюць мадэль зьместу] старонкі",
        "tags-title": "Меткі",
        "tags-intro": "На гэтай старонцы знаходзіцца сьпіс метак, якімі праграмнае забесьпячэньне можа пазначыць рэдагаваньне, і іх значэньне.",
        "tags-tag": "Назва меткі",
index 65e1021..e2fec0d 100644 (file)
@@ -58,7 +58,7 @@
        "tog-enotifminoredits": "পাতা এবং ফাইলগুলোতে অনুল্লেখ্য সম্পাদনার জন্যও আমাকে ই-মেইল করা হোক",
        "tog-enotifrevealaddr": "বিজ্ঞপ্তি মেইলে আমার ই-মেইল ঠিকানা প্রকাশ করা হোক",
        "tog-shownumberswatching": "নজরদারী করছে, এমন ব্যবহারকারীর সংখ্যা দেখানো হোক",
-       "tog-oldsig": "বর্তমান স্বাক্ষর:",
+       "tog-oldsig": "à¦\86পনার à¦¬à¦°à§\8dতমান à¦¸à§\8dবাà¦\95à§\8dষর:",
        "tog-fancysig": "স্বাক্ষরকে উইকিটেক্সট হিসেবে মনে করুন (কোন সয়ংক্রিয় লিঙ্ক ছাড়া)",
        "tog-uselivepreview": "তাৎক্ষণিক প্রাকদর্শন ব্যবহার করো",
        "tog-forceeditsummary": "খালি সম্পাদনা সারাংশ প্রবেশ করানোর সময় আমাকে জানানো হোক",
@@ -75,7 +75,7 @@
        "tog-showhiddencats": "লুকায়িত বিষয়শ্রেণীসমূহ দেখাও",
        "tog-norollbackdiff": "রোলব্যাকের পরে পার্থক্য দেখিও না",
        "tog-useeditwarning": "অসংরক্ষিত পরিবর্তনসহ কোনো পাতা ত্যাগের সময় সাবধান করো",
-       "tog-prefershttps": "যà¦\96নà¦\87 à¦ªà§\8dরবà§\87শ à¦\95রবà§\87ন সবসময় নিরাপদ সংযোগ ব্যবহার করুন",
+       "tog-prefershttps": "পà§\8dরবà§\87শ à¦\95রার à¦¸à¦®à¦¯à¦¼ সবসময় নিরাপদ সংযোগ ব্যবহার করুন",
        "underline-always": "সব সময়",
        "underline-never": "কখনো নয়",
        "underline-default": "স্কিন অথবা ব্রাউজারে যেমনভাবে নির্দিষ্ট করা আছে",
        "newwindow": "(নতুন উইন্ডোতে খুলবে)",
        "cancel": "বাতিল",
        "moredotdotdot": "আরও...",
-       "morenotlisted": "à¦\8fà¦\9fি à¦\8fà¦\95à¦\9fি à¦\85সমà§\8dপà§\82রà§\8dণ à¦¤à¦¾à¦²à¦¿à¦\95া।",
+       "morenotlisted": "à¦\8fà¦\87 à¦¤à¦¾à¦²à¦¿à¦\95াà¦\9fি à¦\85সমà§\8dপà§\82রà§\8dণ à¦¹à¦¤à§\87 à¦ªà¦¾à¦°à§\87।",
        "mypage": " পাতা",
        "mytalk": "আলোচনা",
        "anontalk": "আলাপ",
index fa0653f..9e89037 100644 (file)
        "category-empty": "''Ena kategoriye de hewna qet nuştey ya zi medya çıniyê.''",
        "hidden-categories": "{{PLURAL:$1|Kategoriya nımıtiye|Kategoriyê nımıtey}}",
        "hidden-category-category": "Kategoriyê nımıtey",
-       "category-subcat-count": "{| border=\"1\" cellpadding=\"2\" cellspacing=\"0\" align=\"left\" style=\"margin-left:1em; background:khaki; border: 1px #aaa solid; border-collapse: collapse; font-size: 250%;\"\n| align=\"center\" |{{PLURAL:$2|Na kategori de $1 bınkategoriy est ê.|$2 kategoriyan ra $1 kategoriyê bınêni asenê.}} \n|-\n| align=\"center\" |(K) Kategoriye (D) Dosya (P) Peli (M)  Medya\n|}",
+       "category-subcat-count": "{{PLURAL:$2|Na kategoriye de tenya na bınkategoriye esta.|Na kategoriye de, $2 ra pêro piya, {{PLURAL:$1|bınkategoriye esta|$1 bınkategoriy estê}}.}}",
        "category-subcat-count-limited": "Na kategoriye de {{PLURAL:$1|na kategoriya bınêne esta|nê $1 kategoriyê bınêni estê}}.",
-       "category-article-count": "{| border=\"1\" cellpadding=\"2\" cellspacing=\"0\" align=\"left\" style=\"margin-left:1em; background:khaki; border: 1px #aaa solid; border-collapse: collapse; font-size: 250%;\"\n| align=\"center\" |{{PLURAL:$2|Na kategori de teyna ena perr esta.|pêro piya $2 ra  {{PLURAL:$1|ena perra na kategori de ya|$1 perri na kategori de yê.}}}}\n|}",
+       "category-article-count": "{{PLURAL:$2|Na kategoriye de teyna ena pele esta.|Ebe $2 ra pêro piya {{PLURAL:$1|ena pele na kategoriye dera|$1 enê peli na kategoriye derê}}.}}",
        "category-article-count-limited": "{{PLURAL:$1|Pela cêrêne|$1 Pelê cêrêni}} na kategoriye derê.",
        "category-file-count": "{{PLURAL:$2|Na kategori tenya dosya ya cêri muhtewa kena.|Na kategori de $2 ra pêro piya {{PLURAL:$1|1 dosya est a|$1 dosyey est ê}}.}}",
        "category-file-count-limited": "{{PLURAL:$1|Dosya cêrêne|$1 Dosyê cêrêni}} na kategoriye derê.",
        "about": "Heqa cı de",
        "article": "Pela zerreki",
        "newwindow": "(pençereyê newey de beno a)",
-       "cancel": "Bıtexelne",
+       "cancel": "İbtal kı",
        "moredotdotdot": "Vêşi...",
        "morenotlisted": "Vêşi lista nêbi...",
        "mypage": "Pele",
        "specialpage": "Pela xısusiye",
        "personaltools": "Hacetê şexsiy",
        "articlepage": "Pera zerreki bıvin",
-       "talk": "Werênayış",
+       "talk": "Vatene",
        "views": "Asayışi",
        "toolbox": "Haceti",
        "userpage": "Pela karberi bıvêne",
        "emailsubject": "Mewzu:",
        "emailmessage": "Mesac:",
        "emailsend": "Bırışe",
-       "emailccme": "kopyayekê mesaji mı re bıerşaw",
+       "emailccme": "Ju kopya ya mesaci bırş mı rê?",
        "emailccsubject": "$2 kopyaya mesaj a ke şıma erşawıto/a $1:",
        "emailsent": "E-poste rışna",
        "emailsenttext": "e-mailê şıma erşawiya/ruşiya",
        "usermessage-summary": "Mesacê sistemi caverde.",
        "usermessage-editor": "Xeberdarê sistemi",
        "usermessage-template": "MediaWiki:UserMessage",
-       "watchlist": "Lista seyrkerdışi",
-       "mywatchlist": "Lista seyrkerdışi",
+       "watchlist": "Listey pawıteyan",
+       "mywatchlist": "Listey pawıteyan",
        "watchlistfor2": "Qandê $1 ($2)",
        "nowatchlist": "listeya temaşa kerdıişê şıma de yew madde zi çina.",
        "watchlistanontext": "qey vurnayişê maddeya listeya temaşakerdiş ronıştış akerê",
        "tooltip-pt-anonuserpage": "pelê karberê IPyi",
        "tooltip-pt-mytalk": "Pela {{GENDER:|toya}} werênayışi",
        "tooltip-pt-anontalk": "vurnayiş ê ke no Ipadresi ra biyo muneqeşa bıker",
-       "tooltip-pt-preferences": "Tercihê {{GENDER:|şıma}}",
+       "tooltip-pt-preferences": "Tercihê {{GENDER:|to}}",
        "tooltip-pt-watchlist": "Lista pelanê ke to gırewtê seyrkerdış",
        "tooltip-pt-mycontris": "Yew lista iştırakanê {{GENDER:|şıma}}",
        "tooltip-pt-login": "Mayê şıma ronıştış akerdışi rê dawet keme; labelê ronıştış mecburi niyo",
index 8241917..7e0e0fd 100644 (file)
        "minoredit": "આ એક નાનો સુધારો છે",
        "watchthis": "આ પાનાને ધ્યાનમાં રાખો",
        "savearticle": "પાનું સાચવો",
+       "savechanges": "પરિવર્તન સાચવો",
        "publishpage": "પાનું પ્રકાશિત કરો",
        "publishchanges": "ફેરફારો પ્રકાશિત કરો",
        "preview": "પૂર્વાવલોકન",
index e5f3e01..9fce778 100644 (file)
        "newwindow": "(åpnes i et nytt vindu)",
        "cancel": "Avbryt",
        "moredotdotdot": "Mer …",
-       "morenotlisted": "Denne lista er ufullstendig.",
+       "morenotlisted": "Denne lista er muligens ufullstendig.",
        "mypage": "Min brukerside",
        "mytalk": "Diskusjon",
        "anontalk": "Brukerdiskusjon",
        "createacct-yourpasswordagain-ph": "Gjenta passordet",
        "userlogin-remembermypassword": "Hold meg innlogget",
        "userlogin-signwithsecure": "Logg inn med sikker tjener",
+       "cannotlogin-title": "Kan ikke logge inn",
+       "cannotlogin-text": "Innlogging er ikke mulig.",
        "cannotloginnow-title": "Kan ikke logge inn nå",
        "cannotloginnow-text": "Å logge inn er ikke mulig ved bruk av $1.",
+       "cannotcreateaccount-title": "Kan ikke opprette kontoer",
+       "cannotcreateaccount-text": "Direkte kontooppretting er ikke slått på på denne wikien.",
        "yourdomainname": "Ditt domene",
        "password-change-forbidden": "Du kan ikke endre passord på denne wikien.",
        "externaldberror": "Det var en ekstern autentifiseringsfeil, eller du kan ikke oppdatere din eksterne konto.",
        "botpasswords-updated-body": "Robotpassordet for boten «$1» til brukeren «$2» ble oppdatert.",
        "botpasswords-deleted-title": "Robotpassord slettet",
        "botpasswords-deleted-body": "Robotpassordet for boten «$1» til brukeren «$2» ble slettet.",
-       "botpasswords-newpassword": "Det nye passordet for å logge inn med <strong>$1</strong> er <strong>$2</strong>. <em>Vennligst lagre dette for fremtidig referanse.</em>",
+       "botpasswords-newpassword": "Det nye passordet for å logge inn med <strong>$1</strong> er <strong>$2</strong>. <em>Vennligst lagre dette for fremtidig referanse.</em> <br /> (For gamle roboter som trenger samme innloggingsnavn og brukernavn kan du også bruke <strong>$3</strong> som brukernavn og <strong>$4</strong> som passord.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider er ikke tilgjengelig.",
        "botpasswords-restriction-failed": "Begrensninger for robotpassord tillater ikke denne innloggingen.",
        "botpasswords-invalid-name": "Det angitte brukernavnet inneholder ikke separasjonstegnet for robotpassord (\"$1\").",
        "invalid-content-data": "Ugyldig innhold",
        "content-not-allowed-here": "Innholdsmodellen «$1» er ikke tillatt på siden [[$2]]",
        "editwarning-warning": "Ved å forlate siden kan du miste alle endringer du har gjort.\nHvis du er innlogget, kan du slå av denne advarselen under \"{{int:prefs-editing}}\"-avsnittet i dine innstillinger.",
+       "editpage-invalidcontentmodel-title": "Innholdsmodellen støttes ikke",
+       "editpage-invalidcontentmodel-text": "Innholdsmodellen «$1» støttes ikke.",
        "editpage-notsupportedcontentformat-title": "Innholdsformatet er ikke støttet",
        "editpage-notsupportedcontentformat-text": "Innholdsformatet $1 er ikke støttet av innholdsmodellen $2.",
        "content-model-wikitext": "wikitekst",
        "listgrouprights-namespaceprotection-header": "Navneromsbegrensinger",
        "listgrouprights-namespaceprotection-namespace": "Navnerom",
        "listgrouprights-namespaceprotection-restrictedto": "Rettighet(er) som tillater at brukeren redigerer",
+       "listgrants": "Tildelinger",
        "listgrants-summary": "Følgende er en liste over tildelinger samt hvilke brukerrettigheter de gir tilgang til. Brukere kan autorisere applikasjoner til å bruke kontoen deres, med rettigheter begrenset til de gitt av tildelingene brukeren har godkjent. En applikasjon som handler på vegne av en bruker kan imidlertid aldri benytte seg av rettigheter brukeren ikke selv har.\nDet kan finnes [[{{MediaWiki:Listgrouprights-helppage}}|ytterligere informasjon]] om de ulike rettighetene.",
+       "listgrants-grant": "Tildeling",
        "listgrants-rights": "Rettigheter",
        "trackingcategories": "Sporingskategori",
        "trackingcategories-summary": "Denne siden lister sporingskategorier som er automatisk befolket av Mediawiki-programvaren. Navnene deres kan endres ved å redigere de tilhørende systembeskjedene i {{ns:8}}-navnerommet.",
        "trackingcategories-name": "Beskjednavn",
        "trackingcategories-desc": "Kategori-inklusjonskriterium",
        "restricted-displaytitle-ignored": "Sider med ignorerte visningstitler",
+       "restricted-displaytitle-ignored-desc": "Denne sidens <code><nowiki>{{DISPLAYTITLE}}</nowiki></code> er ignorert fordi den ikke tilsvarer sidens faktiske tittel.",
        "noindex-category-desc": "Denne siden indekseres ikke av roboter fordi den er merket med det magiske ordet <code><nowiki>__NOINDEX__</nowiki></code> og er i navnerom der dette flagget tillates.",
        "index-category-desc": "Denne siden er påført det magiske ordet <code><nowiki>__INDEX__</nowiki></code> (og er i et navnerom hvor flagget er tillatt), og vil derfor bli indeksert av roboter selv når det normalt ikke ville skjedd.",
        "post-expand-template-inclusion-category-desc": "Sidestørrelsen er større enn <code>$wgMaxArticleSize</code> etter at alle maler er utvidet, så noen maler ble ikke utvidet.",
        "watchnologin": "Ikke logget inn",
        "addwatch": "Legg til i overvåkningslisten",
        "addedwatchtext": "«[[:$1]]» og den tilhørende diskusjonssiden er lagt til i [[Special:Watchlist|overvåkningslisten]] din.",
+       "addedwatchtext-talk": "«[[:$1]]» og dens tilhørende side har blitt lagt til i [[Special:Watchlist|overvåkningslista di]].",
        "addedwatchtext-short": "Siden «$1» har blitt lagt til i overvåkningslisten din.",
        "removewatch": "Fjern fra overvåkningslisten",
        "removedwatchtext": "«[[:$1]]» og den tilhørende diskusjonssiden har blitt fjernet fra [[Special:Watchlist|overvåkningslisten din]].",
+       "removedwatchtext-talk": "«[[:$1]]» og dens tilhørende side har blitt fjernet fra [[Special:Watchlist|overvåkningslista di]].",
        "removedwatchtext-short": "Siden «$1» har blitt fjernet fra overvåkningslisten din.",
        "watch": "Overvåk",
        "watchthispage": "Overvåk denne siden",
        "rollbacklinkcount": "tilbakestill {{PLURAL:$1|én endring|$1 endringer}}",
        "rollbacklinkcount-morethan": "tilbakestill mer enn $1 {{PLURAL:$1|endring|endringer}}",
        "rollbackfailed": "Kunne ikke tilbakestille",
+       "rollback-missingparam": "Påkrevde parametere i forespørselen mangler.",
+       "rollback-missingrevision": "Kunne ikke laste revisjonsdata.",
        "cantrollback": "Kan ikke fjerne redigering; den siste brukeren er den eneste forfatteren.",
        "alreadyrolled": "Kan ikke fjerne den siste redigeringen på [[$1]] av [[User:$2|$2]] ([[User talk:$2|diskusjon]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]); en annen har allerede redigert siden eller fjernet redigeringen.\n\nDen siste redigeringen ble foretatt av [[User:$3|$3]] ([[User talk:$3|diskusjon]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "editcomment": "Redigeringskommentaren var: <em>$1</em>",
        "revertpage": "Tilbakestilte endringer av [[Special:Contributions/$2|$2]] ([[User talk:$2|brukerdiskusjon]]) til siste versjon av [[User:$1|$1]]",
        "revertpage-nouser": "Tilbakestilt endringer av skjult bruker til siste versjon av\n{{GENDER:$1|[[User:$1|$1]]}}",
        "rollback-success": "Tilbakestilte endringer av $1; endret til siste versjon av $2.",
+       "rollback-success-notify": "Tilbakestilte endringer av $1;\nendret tilbake til siste revisjon av $2. [$3 Vis endringer]",
        "sessionfailure-title": "Sesjonsfeil",
        "sessionfailure": "Det ser ut til å være et problem med innloggingen din, og den ble avbrutt av sikkerhetshensyn. Trykk ''Tilbake'' i nettleseren din, oppdater siden og prøv igjen.",
        "changecontentmodel": "Endre innholdsmodell for en side",
        "changecontentmodel-title-label": "Sidetittel",
        "changecontentmodel-model-label": "Ny innholdsmodell",
        "changecontentmodel-reason-label": "Begrunnelse:",
+       "changecontentmodel-submit": "Endre",
        "changecontentmodel-success-title": "Innholdsmodellen ble endret",
        "changecontentmodel-success-text": "Innholdstypen for [[:$1]] har blitt endret.",
        "changecontentmodel-cannot-convert": "Innholdet på [[:$1]] kan ikke konverteres til en type av $2.",
        "changecontentmodel-nodirectediting": "Innholdsmodellen $1 støtter ikke direkte redigering",
+       "changecontentmodel-emptymodels-title": "Ingen innholdsmodeller er tilgjengelige",
+       "changecontentmodel-emptymodels-text": "Innholdet på [[:$1]] kan ikke konverteres til noen type.",
        "log-name-contentmodel": "Logg over endringer i endringsloggen",
        "log-description-contentmodel": "Hendelseslogg relatert til innholdsmodellen for en side",
+       "logentry-contentmodel-new": "$1 {{GENDER:$2|opprettet}} siden $3 med den ikke-standard innholdsmodellen «$5»",
        "logentry-contentmodel-change": "$1 {{GENDER:$2|endret}} innholdsmodellen for siden $3 fra «$4» til «$5»",
        "logentry-contentmodel-change-revertlink": "tilbakestill",
        "logentry-contentmodel-change-revert": "tilbakestill",
        "undeletehistorynoadmin": "Denne artikkelen har blitt slettet. Grunnen for slettingen vises i oppsummeringen nedenfor, sammen med detaljer om brukerne som redigerte siden før den ble slettet. Teksten i disse slettede revisjonene er kun tilgjengelig for administratorer.",
        "undelete-revision": "Slettet revisjon av $1 (per $4 $5) av $3:",
        "undeleterevision-missing": "Ugyldig eller manglende revisjon. Du kan ha en ødelagt lenke, eller revisjonen har blitt fjernet fra arkivet.",
+       "undeleterevision-duplicate-revid": "{{PLURAL:$1|Én revisjon|$1 revisjoner}} kunne ikke gjenopprettes, fordi {{PLURAL:$1|dens|deres}} <code>rev_id</code> allerede er i bruk.",
        "undelete-nodiff": "Ingen tidligere revisjoner funnet.",
        "undeletebtn": "Gjenopprett",
        "undeletelink": "vis/gjenopprett",
        "sp-contributions-newbies-title": "Bidrag av nye kontoer",
        "sp-contributions-blocklog": "blokkeringslogg",
        "sp-contributions-suppresslog": "undertrykte brukerbidrag",
-       "sp-contributions-deleted": "slettede brukerbidrag",
+       "sp-contributions-deleted": "slettede {{GENDER:$1|brukerbidrag}}",
        "sp-contributions-uploads": "opplastinger",
        "sp-contributions-logs": "logger",
        "sp-contributions-talk": "diskusjon",
        "sp-contributions-username": "IP-adresse eller brukernavn:",
        "sp-contributions-toponly": "Vis kun endringer som er gjeldende revisjoner",
        "sp-contributions-newonly": "Bare vis bidrag som er sideopprettinger",
+       "sp-contributions-hideminor": "Skjul mindre endringer",
        "sp-contributions-submit": "Søk",
        "whatlinkshere": "Hva lenker hit",
        "whatlinkshere-title": "Sider som lenker til «$1»",
        "unblock": "Fjern blokkering av bruker",
        "blockip": "Blokker {{GENDER:$1|bruker}}",
        "blockip-legend": "Blokker bruker",
-       "blockiptext": "Bruk skjemaet under for å blokkere en IP-adresses tilgang til å redigere artikler. Dette må kun gjøres for å forhindre hærverk, og i overensstemmelse med [[{{MediaWiki:Policy-url}}|retningslinjene]]. Fyll ut en spesiell begrunnelse under.",
+       "blockiptext": "Bruk skjemaet under for å blokkere skrivetilgangen til en spesifikk IP-adresse eller et brukernavn.\nDette bør kun gjøres for å forhindre vandalisme, og i samsvar med [[{{MediaWiki:Policy-url}}|retningslinjene]].\nSkriv inn en spesifikk grunn nedenfor (for eksempel ved å angi hvilke sider som ble vandalisert).\nDu kan blokkere IP-intervaller med [https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR]-syntaks; det største tillatte intervallet er /$1 for IPv4 og /$2 for IPv6.",
        "ipaddressorusername": "IP-adresse eller brukernavn",
        "ipbexpiry": "Varighet:",
        "ipbreason": "Årsak:",
        "ipb-unblock": "Opphev blokkering av et brukernavn eller en IP-adresse",
        "ipb-blocklist": "Vis gjeldende blokkeringer",
        "ipb-blocklist-contribs": "Bidrag fra {{GENDER:$1|$1}}",
+       "ipb-blocklist-duration-left": "$1 igjen",
        "unblockip": "Opphev blokkering",
        "unblockiptext": "Bruk skjemaet under for å gjenopprette skriveadgangen for en tidligere blokkert adresse eller bruker.",
        "ipusubmit": "Opphev blokkering",
        "block-log-flags-hiddenname": "brukernavn skjult",
        "range_block_disabled": "Muligheten til å blokkere flere IP-adresser om gangen er slått av.",
        "ipb_expiry_invalid": "Ugyldig utløpstid.",
+       "ipb_expiry_old": "Utløpstiden har allerede vært.",
        "ipb_expiry_temp": "For å skjule brukernavnet må blokkeringen være permanent.",
        "ipb_hide_invalid": "Denne kontoen kan ikke skjules; den har mer enn {{PLURAL:$1|én redigering|$1 redigeringer}}.",
        "ipb_already_blocked": "«$1» er allerede blokkert",
        "lockdbsuccesstext": "Databasen er låst.<br />Husk å [[Special:UnlockDB|låse den opp]] når du er ferdig med vedlikeholdet.",
        "unlockdbsuccesstext": "Databasen er låst opp.",
        "lockfilenotwritable": "Kan ikke skrive til databasen. For å låse eller åpne databasen, må denne kunne skrives til av tjeneren.",
+       "databaselocked": "Databasen er allerede låst.",
        "databasenotlocked": "Databasen er ikke låst.",
        "lockedbyandtime": "(av $1 den $2, kl $3)",
        "move-page": "Flytt $1",
        "move-page-legend": "Flytt side",
-       "movepagetext": "Når du bruker skjemaet nedenfor døper du om en side og flytter hele historikken til det nye navnet.\nDen gamle tittelen blir en omdirigeringsside til den nye tittelen.\nDu kan oppdatere omdirigeringer som peker til den opprinnelige tittelen automatisk.\nOm du velger å ikke gjøre det, sjekk at flyttingen ikke fører til [[Special:DoubleRedirects|doble]] eller [[Special:BrokenRedirects|ødelagte omdirigeringer]].\nDu er ansvarlig for at lenker fortsetter å peke til de sidene de er ment å peke til.\n\nLegg merke til at siden '''ikke''' kan flyttes hvis det allerede finnes en side med den nye tittelen, med mindre sistnevnte er tom eller er en omdirigeringsside uten historikk.\nDet betyr at du kan flytte en side tilbake dit den kom fra hvis du gjør en feil, og du kan ikke overskrive eksisterende sider ved et uhell.\n\n'''Advarsel!'''\nDette kan være en drastisk og uventet endring for en populær side;\nvær sikker på at du forstår konsekvensene av dette før du fortsetter.",
-       "movepagetext-noredirectfixer": "Skjemaet nedenfor vil gi en side ny tittel og flytte historikken dens til det nye navnet.\nDen gamle tittelen vil bli en omdirigering til den nye.\nSjekk om det blir [[Special:DoubleRedirects|doble]] eller [[Special:BrokenRedirects|ødelagte omdirigeringer]].\nDu er ansvarlig for å sjekke at lenker fortsetter å gå dit de skal.\n\nMerk at sider '''ikke''' blir flyttet om det allerede finnes en side med den tittelen, med mindre siden er tom eller en omdirigering og ikke har noen redigeringshistorikk.\nDette betyr at du kan endre tittelen til en tittel siden hadde tidligere, og at du ikke kan skrive over en eksisterende side.\n\n'''Advarsel!'''\nDette kan være en drastisk og uventet endring for en populær side;\nvær sikker på at du forstår konsekvensene av dette før du fortsetter.",
-       "movepagetalktext": "Den tilhørende diskusjonssiden vil automatisk bli flyttet sammen med siden '''med mindre:'''\n*Det allerede finnes en diskusjonsside som ikke er tom under det nye navnet, eller\n*Du fjerner markeringen i boksen nedenfor.\n\nI disse tilfellene er du nødt til å flytte eller flette siden manuelt, om ønskelig.",
+       "movepagetext": "Når du bruker skjemaet nedenfor døper du om en side og flytter hele historikken til det nye navnet.\nDen gamle tittelen blir en omdirigeringsside til den nye tittelen.\nDu kan oppdatere omdirigeringer som peker til den opprinnelige tittelen automatisk.\nOm du velger å ikke gjøre det, sjekk at flyttingen ikke fører til [[Special:DoubleRedirects|doble]] eller [[Special:BrokenRedirects|ødelagte omdirigeringer]].\nDu er ansvarlig for at lenker fortsetter å peke til de sidene de er ment å peke til.\n\nLegg merke til at siden <strong>ikke</strong> kan flyttes hvis det allerede finnes en side med den nye tittelen, med mindre sistnevnte er tom eller er en omdirigeringsside uten historikk.\nDet betyr at du kan flytte en side tilbake dit den kom fra hvis du gjør en feil, og du kan ikke overskrive eksisterende sider ved et uhell.\n\n<strong>Merk:</strong>\nDette kan være en drastisk og uventet endring for en populær side;\nvær sikker på at du forstår konsekvensene av dette før du fortsetter.",
+       "movepagetext-noredirectfixer": "Skjemaet nedenfor vil gi en side ny tittel og flytte historikken dens til det nye navnet.\nDen gamle tittelen vil bli en omdirigering til den nye.\nSjekk om det blir [[Special:DoubleRedirects|doble]] eller [[Special:BrokenRedirects|ødelagte omdirigeringer]].\nDu er ansvarlig for å sjekke at lenker fortsetter å gå dit de skal.\n\nMerk at sider <strong>ikke</strong> blir flyttet om det allerede finnes en side med den tittelen, med mindre siden er en omdirigering og ikke har noen redigeringshistorikk.\nDette betyr at du kan endre tittelen til en tittel siden hadde tidligere, og at du ikke kan skrive over en eksisterende side.\n\n<strong>Merk:</strong>\nDette kan være en drastisk og uventet endring for en populær side;\nvær sikker på at du forstår konsekvensene av dette før du fortsetter.",
+       "movepagetalktext": "Om du merker av denne boksen vil den tilhørende diskusjonssiden også flyttes til den nye tittelen, med mindre en ikke-tom diskusjonsside allerede finnes der.\n\nOm det er tilfelle må du flytte eller flette siden manuelt om det er ønskelig.",
        "moveuserpage-warning": "'''Advarsel:''' Du er i ferd med å flytte en brukerside. Merk at kun siden vil bli flyttet; brukernavnet vil ''ikke'' bli endret.",
        "movecategorypage-warning": "<strong>Advarsel:</strong> Du er i ferd med å flytte en kategoriside. Merk at kun siden blir flyttet, og at sider i det gamle kategorinavnet <em>ikke</em> blir omkategorisert til det nye navnet.",
        "movenologintext": "Du må være registrert bruker og være [[Special:UserLogin|logget på]] for å flytte en side.",
        "movenosubpage": "Denne siden har ingen undersider.",
        "movereason": "Årsak:",
        "revertmove": "tilbakestill",
-       "delete_and_move_text": "==Sletting nødvendig==\nMålsiden «[[:$1]]» finnes allerede. Vil du slette den så denne siden kan flyttes dit?",
+       "delete_and_move_text": "Målsiden «[[:$1]]» finnes fra før.\nØnsker du å slette den for å muliggjøre flyttingen?",
        "delete_and_move_confirm": "Ja, slett siden",
        "delete_and_move_reason": "Slettet for å muliggjøre flytting fra \"[[$1]]\"",
        "selfmove": "Kilde- og destinasjonstittel er den samme; kan ikke flytte siden.",
        "move-leave-redirect": "La det være igjen en omdirigering",
        "protectedpagemovewarning": "'''Advarsel:''' Denne siden har blitt låst slik at kun brukere med administratorrettigheter kan flytte den.\nDet siste loggelementet er oppgitt under som referanse:",
        "semiprotectedpagemovewarning": "'''Merk:''' Denne siden har blitt låst slik at kun registrerte brukere kan flytte den.\nDet siste loggelementet er oppgitt under som referanse:",
-       "move-over-sharedrepo": "== Filen finnes ==\n[[:$1]] finnes på en delt kilde. Dersom du flytter en fil til dette navnet, vil du overstyre den delte filen.",
+       "move-over-sharedrepo": "[[:$1]] finnes på et delt fillager. Flytting av filen til denne tittelen vil overstyre den delte filen.",
        "file-exists-sharedrepo": "Det valgte filnavnet er allerede i bruk på en delt kilde.\nVennligst velg et annet navn.",
        "export": "Eksporter sider",
        "exporttext": "Du kan eksportere teksten og redigeringshistorikken for en bestemt side eller en gruppe sider i XML.\nDette kan senere importeres til en annen wiki som bruker MediaWiki ved hjelp av [[Special:Import|importsiden]].\n\nFor å eksportere sider, skriv inn titler i tekstboksen under, én tittel per linje, og velg om du vil ha kun nåværende versjon, eller alle versjoner i historikken.\n\nDersom du bare vil ha nåværende versjon, kan du også bruke en lenke, for eksempel [[{{#Special:Export}}/{{MediaWiki:Mainpage}}]] for siden «[[{{MediaWiki:Mainpage}}]]».",
        "export-download": "Lagre som fil",
        "export-templates": "Ta med maler",
        "export-pagelinks": "Inkluder lenkede sider med en dybde på:",
+       "export-manual": "Legg til sider manuelt:",
        "allmessages": "Systemmeldinger",
        "allmessagesname": "Navn",
        "allmessagesdefault": "Standardtekst",
        "import-nonewrevisions": "Ingen revisjoner ble importert: De var enten allerede på plass, eller hoppet over pga. feil.",
        "xml-error-string": "$1 på linje $2, kolonne $3 (byte: $4): $5",
        "import-upload": "Last opp XML-data",
-       "import-token-mismatch": "Sesjonsdata mistet. Venligst prøv igjen.",
+       "import-token-mismatch": "Sesjonsdata mistet.\n\nDu kan ha blitt logget ut. <strong>Sjekk at du fortsatt er logget inn og prøv igjen.</strong>\nOm det fortsatt ikke fungerer, prøv å [[Special:UserLogout|logge ut]] og logge inn igjen, og sjekk om netteleseren din tillater informasjonskapsler fra denne siden.",
        "import-invalid-interwiki": "Kan ikke importere fra angitt wiki.",
        "import-error-edit": "Siden «$1» ble ikke importert fordi du ikke har tillatelse til å redigere den.",
        "import-error-create": "Siden «$1» ble ikke importert fordi du ikke har tillatelse til å opprette den.",
        "tooltip-feed-rss": "RSS-mating for denne siden",
        "tooltip-feed-atom": "Atom-mating for denne siden",
        "tooltip-t-contributions": "En liste over bidrag fra {{GENDER:$1|denne brukeren}}",
-       "tooltip-t-emailuser": "Send en e-post til denne brukeren",
+       "tooltip-t-emailuser": "Send en e-post til {{GENDER:$1|denne brukeren}}",
        "tooltip-t-info": "Mer informasjon om denne siden",
        "tooltip-t-upload": "Last opp filer",
        "tooltip-t-specialpages": "Liste over alle spesialsider",
        "tooltip-ca-nstab-category": "Vis kategorisiden",
        "tooltip-minoredit": "Merk dette som en mindre endring",
        "tooltip-save": "Lagre endringene dine",
+       "tooltip-publish": "Publiser endringene dine",
        "tooltip-preview": "Forhåndsvis endringene dine, vennligst gjør dette før du lagrer!",
        "tooltip-diff": "Vis hvilke endringer du har gjort på teksten",
        "tooltip-compareselectedversions": "Se forskjellen mellom de to valgte revisjonene av denne siden",
        "pageinfo-article-id": "Side-ID",
        "pageinfo-language": "Språk for sideinnholdet",
        "pageinfo-content-model": "Modell for sideinnhold",
+       "pageinfo-content-model-change": "endre",
        "pageinfo-robot-policy": "Bot-indeksering",
        "pageinfo-robot-index": "Tillatt",
        "pageinfo-robot-noindex": "Ikke tillatt",
        "pageinfo-category-files": "Antall filer",
        "markaspatrolleddiff": "Merk som patruljert",
        "markaspatrolledtext": "Merk denne siden som patruljert",
+       "markaspatrolledtext-file": "Merk denne filversjonen som patruljert",
        "markedaspatrolled": "Merket som patruljert",
        "markedaspatrolledtext": "Den valgte revisjonen av [[:$1]] har blitt merket som patruljert.",
        "rcpatroldisabled": "Siste endringer-patruljering er slått av",
        "newimages-legend": "Filnavn",
        "newimages-label": "Filnavn (helt eller delvis):",
        "newimages-showbots": "Vis opplastinger av botter",
+       "newimages-hidepatrolled": "Skjul patruljerte opplastinger",
        "noimages": "Ingenting å se.",
        "ilsubmit": "Søk",
        "bydate": "etter dato",
        "confirmemail_body_set": "Noen med IP-adresse $1, mest sannsynlig deg, har satt e-postadressen for kontoen «$2» til denne adressen på {{SITENAME}}.\n\nFor å bekrefte at denne kontoen faktisk tilhører deg og for å slå på e-post-tjenestene fra {{SITENAME}}, må du åpne denne lenken i nettleseren din:\n\n$3\n\nOm kontoen *ikke* tilhører deg, følg denne lenken for å avbryte e-post-bekreftelsen:\n\n$5\n\nDenne bekreftelseskoden utløper $4.",
        "confirmemail_invalidated": "Bekreftelse av e-postadresse avbrutt",
        "invalidateemail": "Avbryt bekreftelse av e-postadresse",
+       "notificationemail_subject_changed": "Registrert epostadresse på {{SITENAME}} har blitt endret",
+       "notificationemail_subject_removed": "Registrert epostadresse på {{SITENAME}} har blitt fjernet",
+       "notificationemail_body_changed": "Noen, antageligvis du (fra IP-adressen $1), har endret epostadressen til kontoen «$2» til «$3» på {{SITENAME}}.\n\nOm det ikke var du som gjorde det, kontakt en sideadministrator umiddelbart.",
+       "notificationemail_body_removed": "Noen, antageligvis deg (fra IP-adressen $1), har fjernet epostadressen til kontoen «$2» på {{SITENAME}}.\n\nOm det ikke var du som gjorde det, kontakt en sideadministrator umiddelbart.",
        "scarytranscludedisabled": "[Interwiki-transkludering er slått av]",
        "scarytranscludefailed": "[Malen kunne ikke hentes for $1]",
        "scarytranscludefailed-httpstatus": "[Henting av mal for $1 feilet: HTTP $2]",
        "confirm-watch-top": "Legg denne siden til overvåkningslisten din?",
        "confirm-unwatch-button": "OK",
        "confirm-unwatch-top": "Fjern denne siden fra overvåkningslisten din?",
+       "confirm-rollback-button": "OK",
+       "confirm-rollback-top": "Tilbakestill redigeringer på denne siden?",
        "quotation-marks": "«$1»",
        "imgmultipageprev": "← forrige side",
        "imgmultipagenext": "neste side &rarr;",
        "hebrew-calendar-m11-gen": "Ab",
        "hebrew-calendar-m12-gen": "Elúl",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|diskusjon]])",
+       "timezone-local": "Lokal",
        "duplicate-defaultsort": "Advarsel: Standardsorteringen «$2» tar over for den tidligere sorteringen «$1».",
        "duplicate-displaytitle": "<strong>Advarsel:</strong> Visningstittel \"$2\" erstatter tidligere visningstittel \"$1\".",
+       "restricted-displaytitle": "<strong>Advarsel:</strong> Visningstittelen «$1» ble ignorert siden den ikke tilsvarer sidens faktiske tittel.",
        "invalid-indicator-name": "<p>Feil:</strong> Sidestatus-indikatornes <code>navn</code>-attributt kan ikke være tomt.",
        "version": "Versjon",
        "version-extensions": "Installerte utvidelser",
        "version-libraries-license": "Lisens",
        "version-libraries-description": "Beskrivelse",
        "version-libraries-authors": "Forfattere",
-       "redirect": "Omdiriger via filnavn, bruker eller versjonsid",
-       "redirect-summary": "Denne spesialsiden omdirigerer til en fil (hvis et filnavn angis), en side (hvis et redigeringsnummer angis) eller en brukerside (hvis en numerisk brukeridentifikator angis).\nEksempler:[[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/revision/328429]], or [[{{#Special:Redirect}}/user/101]].",
+       "redirect": "Omdiriger via filnavn, bruker-, side-, revisjons- eller logg-ID.",
+       "redirect-summary": "Denne spesialsiden omdirigerer til en fil (hvis et filnavn angis), en side (om revisjons- eller side-ID angis), en brukerside (om bruker-ID angis), eller en loggoppføring (om logg-ID angis). Bruk: [[{{#Special:Redirect}}/file/Eksempel.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]] eller [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Gå",
        "redirect-lookup": "Oppslag:",
        "redirect-value": "Verdi:",
        "redirect-page": "Side-ID",
        "redirect-revision": "Sideversjon",
        "redirect-file": "Filnavn",
+       "redirect-logid": "Logg-ID",
        "redirect-not-exists": "Verdi er ikke funnet",
        "fileduplicatesearch": "Søk etter duplikatfiler",
        "fileduplicatesearch-summary": "Søk etter duplikatfiler basert på dets hash-verdi.",
        "tag-filter": "Filter for [[Special:Tags|tagger]]:",
        "tag-filter-submit": "Filtrer",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Tagg|Tagger}}]]: $2)",
+       "tag-mw-contentmodelchange": "innholdsmodellendring",
+       "tag-mw-contentmodelchange-description": "Redigeringer som [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel endrer innholdsmodellen] til en side",
        "tags-title": "Tagger",
        "tags-intro": "Denne siden lister opp taggene programvaren kan merke en endring med, og hva de betyr.",
        "tags-tag": "Taggnavn",
        "tags-actions-header": "Handlinger",
        "tags-active-yes": "Ja",
        "tags-active-no": "Nei",
-       "tags-source-extension": "Definert av en utvidelse",
+       "tags-source-extension": "Definert av programvaren",
        "tags-source-manual": "Brukes manuelt av brukere og roboter",
        "tags-source-none": "Brukes ikke lenger",
        "tags-edit": "rediger",
        "tags-deactivate": "deaktiver",
        "tags-hitcount": "{{PLURAL:$1|én endring|$1 endringer}}",
        "tags-manage-no-permission": "Du har ikke tillatelse til å behandle tagger.",
+       "tags-manage-blocked": "Du kan ikke behandle endringstagger mens du er blokkert.",
        "tags-create-heading": "Opprett ny tagg",
        "tags-create-explanation": "Som standard vil nyopprettede tagger være tilgjengelige for brukere og roboter.",
        "tags-create-tag-name": "Taggnavn:",
        "tags-delete-not-found": "Taggen «$1» finnes ikke.",
        "tags-delete-too-many-uses": "Taggen «$1» brukes på mer enn $2 {{PLURAL:$2|revisjon|revisjoner}}, hvilket betyr at den ikke kan slettes.",
        "tags-delete-warnings-after-delete": "Taggen «$1» ble slettet, men følgende {{PLURAL:$2|advarsel|advarsler}} dukket opp:",
+       "tags-delete-no-permission": "Du har ikke tillatelse til å slette endringstagger.",
        "tags-activate-title": "Aktiver taggen",
        "tags-activate-question": "Du er i ferd med å aktivere taggen «$1».",
        "tags-activate-reason": "Årsak:",
        "tags-apply-not-allowed-one": "Merket «$1» kan ikke legges til manuelt.",
        "tags-apply-not-allowed-multi": "{{PLURAL:$2|Det følgende merket|De følgende merkene}} kan ikke legges til manuelt: $1",
        "tags-update-no-permission": "Du har ikke tilgang til å legge til eller fjerne merker fra individuelle revisjoner eller loggposter.",
+       "tags-update-blocked": "Du kan ikke legge til eller fjerne endringstagger mens du er blokkert.",
        "tags-update-add-not-allowed-one": "Merket «$1» kan ikke legges til manuelt.",
        "tags-update-add-not-allowed-multi": "{{PLURAL:$2|Det følgende merket|De følgende merkene}} kan ikke legges til manuelt: $1",
        "tags-update-remove-not-allowed-one": "Merket «$1» kan ikke fjernes.",
        "feedback-useragent": "Brukeragent",
        "searchsuggest-search": "Søk",
        "searchsuggest-containing": "inneholder …",
+       "api-error-autoblocked": "Din IP-adresse har blitt blokkert automatisk fordi den ble brukt av en blokkert bruker.",
        "api-error-badaccess-groups": "Du har ikke tillatelse til å laste opp filer til denne wikien.",
        "api-error-badtoken": "Intern feil: Ugyldig nøkkel.",
+       "api-error-blocked": "Du har blitt blokkert fra å redigere.",
        "api-error-copyuploaddisabled": "Opplasting ved URL er deaktivert på denne tjeneren.",
        "api-error-duplicate": "Det er allerede {{PLURAL:$1|en annen fil|flere andre filer}} på denne siden med samme innhold.",
        "api-error-duplicate-archive": "Det fantes {{PLURAL:$1|en annen fil|noen andre filer}} på siden som hadde samme innhold, men {{PLURAL:$1|den|de}} ble slettet.",
        "api-error-nomodule": "Intern feil: ingen opplastningsmodul har blitt valgt.",
        "api-error-ok-but-empty": "Intern feil: ingen svar fra server.",
        "api-error-overwrite": "Det er ikke tillatt å overskrive eksisterende filer.",
+       "api-error-ratelimited": "Du prøver å laste opp flere filer enn wikien tillater i et kort tidsrom.\nPrøv igjen om noen minutter.",
        "api-error-stashfailed": "Internal error: tjeneren greide ikke å lagre midlertidig fil.",
        "api-error-publishfailed": "Intern feil: Tjeneren greide ikke å publisere midlertidig fil.",
        "api-error-stasherror": "Det oppstod en feil mens filen ble lastet opp til stash.",
        "api-error-unknownerror": "Ukjent feil: «$1».",
        "api-error-uploaddisabled": "Opplastning har blitt deaktivert på denne wikien.",
        "api-error-verification-error": "Filen kan være korrupt, eller ha feil filendelse.",
+       "api-error-was-deleted": "En fil med dette navnet har tidligere blitt lastet opp og senere slettet.",
        "duration-seconds": "$1 {{PLURAL:$1|sekund|sekunder}}",
        "duration-minutes": "$1 {{PLURAL:$1|minutt|minutter}}",
        "duration-hours": "$1 {{PLURAL:$1|time|timer}}",
        "expand_templates_preview": "Forhåndsvisning",
        "expand_templates_preview_fail_html": "<em>Fordi {{SITENAME}} har slått på rå HTML og sesjonsdata ble tapt er forhåndsvisningen skjult for å beskytte mot JavaScript-angrep.</em>\n\n<strong>Om dette er et legitimt forsøk på å forhåndsvise, prøv på nytt.</strong> Om det fortsatt ikke fungerer, prøv å [[Special:UserLogout|logge ut]] og logge inn igjen, og sjekk at nettleseren din godtar nettkapsler fra dette nettstedet.",
        "expand_templates_preview_fail_html_anon": "<em>Fordi {{SITENAME}} har slått på rå HTML og du ikke er logget inn er forhåndsvisningen skjult for å beskytte mot JavaScript-angrep.</em>\n\n<strong>Om dette er et legitimt forsøk på å forhåndsvise, [[Special:UserLogin|logg inn]] og prøv igjen.</strong>",
+       "expand_templates_input_missing": "Du må angi noe inndata.",
        "pagelanguage": "Endre sidespråk",
        "pagelang-name": "Side",
        "pagelang-language": "Språk",
        "log-name-pagelang": "Logg for språkendringer",
        "log-description-pagelang": "Dette er en logg som viser endringer i sidespråk",
        "logentry-pagelang-pagelang": "$1 {{GENDER:$2|endret}} språk for $3 fra $4 til $5.",
-       "default-skin-not-found": "Ops! Standarddrakten for wikien din, definert i <code dir=\"ltr\">$wgDefaultSkin</code> som <code>$1</code>, er ikke tilgjengelig.\n\nInstallasjonen din ser ut til å inneholde følgende {{PLURAL:$4|drakt|drakter}}. Se [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Skin configuration] for informasjon om hvordan du kan slå {{PLURAL:$4|denne på|disse på og velge en standarddrakt}}.\n\n$2\n\n; Om du nettopp har installert MediaWiki:\n: Du har trolig installert fra git, eller direkte fra kildekoden med en annen metode. Dette er forventet. Prøv å installere noen drakter fra [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org sin draktbase] ved å\n:* laste ned [https://www.mediawiki.org/wiki/Download tarball-installereren], som kommer med flere drakter og utvidelser. Du kan kopiere og lime inn <code>skins/</code>-mappen fra denne.\n:* laste ned individuelle drakter fra [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* klone en av <code>mediawiki/skins/*</code>-lagrene via git inn i <code>skins/</code> -mappen av din MediaWiki-installasjon.\n: Å gjøre dette skal ikke forstyrre git-mappen din om du er en MediaWiki-utvikler.\n\n; Om du nettopp har oppgradert MediaWiki:\n: MediaWiki 1.24 og nyere slår ikke lenger på automatisk installerte drakter (se [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Manual: Skin autodiscovery]). Du kan lime inn følgende {{PLURAL:$5|linje|linjer}} i <code>LocalSettings.php</code> for å slå på {{PLURAL:$5|den|alle}} nåværende installerte {{PLURAL:$5|drakten|drakter}}:\n\n<pre dir=\"ltr\">$3</pre>\n\n; Om du nettopp har endret <code>LocalSettings.php</code>:\n: Dobbelsjekk draktnavnene for skrivefeil.",
-       "default-skin-not-found-no-skins": "Ops! Standarddrakten for wikien din, definert i <code>$wgDefaultSkin</code> som <code>$1</code>, er ikke tilgjengelig.\n\nDu har ingen installerte drakter.\n\n;Om du nettopp har installert eller oppgradert MediaWiki:\n: Du installerte trolig fra git, eller direkte fra kildekoden med en annen metode. Dette er forventet. MediaWiki 1.24 og nyere inkluderer ingen drakter i hovedarkivet. Prøv å installere noen drakter fra [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.orgs draktmappe], ved å:\n:* laste ned [https://www.mediawiki.org/wiki/Download tarball-installereren], som kommer med mange drakter og tillegg. Du kan kopiere og lime inn <code>skins/</code>-mappen fra denne.\n:* laste ned individuelle drakt-tarballer fra [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* klone en av <code>mediawiki/skins/*</code>-arkivene via git til <code dir=\"ltr\">skins/</code>-mappa i din MediaWiki-installasjon.\n: Å gjøre dette vil ikke forstyrre ditt git-arkiv om du er en MediaWiki-utvikler. Se [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual:Skin configuration] for informasjon om hvordan du slår på drakter og velger en standarddrakt.",
+       "default-skin-not-found": "Ops! Standarddrakten for wikien din, definert i <code dir=\"ltr\">$wgDefaultSkin</code> som <code>$1</code>, er ikke tilgjengelig.\n\nInstallasjonen din ser ut til å inneholde følgende {{PLURAL:$4|drakt|drakter}}. Se [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Skin configuration] for informasjon om hvordan du kan slå {{PLURAL:$4|denne på|disse på og velge en standarddrakt}}.\n\n$2\n\n; Om du nettopp har installert MediaWiki:\n: Du har trolig installert fra git, eller direkte fra kildekoden med en annen metode. Dette er forventet. Prøv å installere noen drakter fra [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org sin draktbase] ved å\n:* laste ned [https://www.mediawiki.org/wiki/Download tarball-installereren], som kommer med flere drakter og utvidelser. Du kan kopiere og lime inn <code>skins/</code>-mappen fra denne.\n:* laste ned individuelle drakter fra [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Bruk Git for å laste ned drakter].\n: Å gjøre dette skal ikke forstyrre git-mappen din om du er en MediaWiki-utvikler.\n\n; Om du nettopp har oppgradert MediaWiki:\n: MediaWiki 1.24 og nyere slår ikke lenger på automatisk installerte drakter (se [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Manual: Skin autodiscovery]). Du kan lime inn følgende {{PLURAL:$5|linje|linjer}} i <code>LocalSettings.php</code> for å slå på {{PLURAL:$5|den|alle}} installerte {{PLURAL:$5|drakten|drakter}}:\n\n<pre dir=\"ltr\">$3</pre>\n\n; Om du nettopp har endret <code>LocalSettings.php</code>:\n: Dobbelsjekk draktnavnene for skrivefeil.",
+       "default-skin-not-found-no-skins": "Ops! Standarddrakten for wikien din, definert i <code>$wgDefaultSkin</code> som <code>$1</code>, er ikke tilgjengelig.\n\nDu har ingen installerte drakter.\n\n;Om du nettopp har installert eller oppgradert MediaWiki:\n: Du installerte trolig fra git, eller direkte fra kildekoden med en annen metode. Dette er forventet. MediaWiki 1.24 og nyere inkluderer ingen drakter i hovedarkivet. Prøv å installere noen drakter fra [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.orgs draktmappe], ved å:\n:* laste ned [https://www.mediawiki.org/wiki/Download tarball-installereren], som kommer med mange drakter og tillegg. Du kan kopiere og lime inn <code>skins/</code>-mappen fra denne.\n:* laste ned individuelle drakt-tarballer fra [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Bruk Git for å laste ned drakter].\n: Å gjøre dette vil ikke forstyrre ditt git-arkiv om du er en MediaWiki-utvikler. Se [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual:Skin configuration] for informasjon om hvordan du slår på drakter og velger en standarddrakt.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (slått på)",
        "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>slått av</strong>)",
        "mediastatistics": "Mediestatistikk",
        "special-characters-group-ipa": "IPA",
        "special-characters-group-symbols": "Symboler",
        "special-characters-group-greek": "Gresk",
+       "special-characters-group-greekextended": "Utvidet gresk",
        "special-characters-group-cyrillic": "Kyrillisk",
        "special-characters-group-arabic": "Arabisk",
        "special-characters-group-arabicextended": "Utvidet arabisk",
        "mw-widgets-dateinput-placeholder-month": "ÅÅÅÅ-MM",
        "mw-widgets-titleinput-description-new-page": "siden eksisterer ikke ennå",
        "mw-widgets-titleinput-description-redirect": "omdiriger til $1",
+       "sessionmanager-tie": "Kan ikke kombinere flere forespørselsautentiseringstyper: $1",
        "sessionprovider-generic": "$1 sesjoner",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "informasjons&shy;kapsel-baserte sesjoner",
-       "randomrootpage": "Tilfeldig rotside"
+       "sessionprovider-nocookies": "Informasjonskapsler er kanskje slått av. Sjekk at du har slått på informasjonskapsler og prøv igjen.",
+       "randomrootpage": "Tilfeldig rotside",
+       "log-action-filter-block": "Type blokkering:",
+       "log-action-filter-contentmodel": "Type innholdsmodellendring:",
+       "log-action-filter-delete": "Type sletting:",
+       "log-action-filter-import": "Type import:",
+       "log-action-filter-managetags": "Type tagghåndteringshandling:",
+       "log-action-filter-move": "Type flytting:",
+       "log-action-filter-newusers": "Type kontooppretting:",
+       "log-action-filter-patrol": "Type patruljering:",
+       "log-action-filter-protect": "Type beskyttelse:",
+       "log-action-filter-rights": "Type rettighetsendring:",
+       "log-action-filter-upload": "Type opplasting:",
+       "log-action-filter-all": "Alle",
+       "log-action-filter-block-block": "Blokkering",
+       "log-action-filter-block-reblock": "Blokkeringsendring",
+       "log-action-filter-block-unblock": "Avblokkering",
+       "log-action-filter-contentmodel-change": "Endring av innholdsmodell",
+       "log-action-filter-contentmodel-new": "Oppretting av side med ikke-standard innholdsmodell",
+       "log-action-filter-delete-delete": "Sidesletting",
+       "log-action-filter-delete-restore": "Sidegjenoppretting",
+       "log-action-filter-delete-event": "Loggsletting",
+       "log-action-filter-delete-revision": "Revisjonssletting",
+       "log-action-filter-import-interwiki": "Transwiki-importering",
+       "log-action-filter-import-upload": "XML-opplastingsimportering",
+       "log-action-filter-managetags-create": "Taggopprettelse",
+       "log-action-filter-managetags-delete": "Taggsletting",
+       "log-action-filter-managetags-activate": "Taggaktivering",
+       "log-action-filter-managetags-deactivate": "Taggdeaktivering",
+       "log-action-filter-move-move": "Flytting uten overskriving av omdirigeringer",
+       "log-action-filter-move-move_redir": "Flytting med overskriving av omdirigeringer",
+       "log-action-filter-newusers-create": "Opprettelse av anonym bruker",
+       "log-action-filter-newusers-create2": "Opprettelse av registrert bruker",
+       "log-action-filter-newusers-autocreate": "Automatisk opprettelse",
+       "log-action-filter-newusers-byemail": "Opprettelse med passord sendt på epost",
+       "log-action-filter-patrol-patrol": "Manuell patruljering",
+       "log-action-filter-patrol-autopatrol": "Automatisk patruljering",
+       "log-action-filter-protect-protect": "Beskyttelse",
+       "log-action-filter-protect-modify": "Beskyttelsesendring",
+       "log-action-filter-protect-unprotect": "Avbeskyttelse",
+       "log-action-filter-protect-move_prot": "Flyttingsbeskyttelse",
+       "log-action-filter-rights-rights": "Manuell endring",
+       "log-action-filter-rights-autopromote": "Automatisk endring",
+       "log-action-filter-upload-upload": "Ny opplasting",
+       "log-action-filter-upload-overwrite": "Gjenopplasting",
+       "authmanager-authn-not-in-progress": "Autentisering foregår ikke eller sesjonsdata er tapt. Start igjen fra begynnelsen.",
+       "authmanager-authn-no-primary": "De oppgitte akkreditivene kunne ikke autentiseres.",
+       "authmanager-authn-no-local-user": "De oppgitte akkreditivene tilhører ingen bruker på denne wikien.",
+       "authmanager-authn-no-local-user-link": "De oppgitte akkreditivene er gyldige men tilhører ingen brukere på denne wikien. Logg inn på en annen måte eller opprett en ny bruker, og du vil ha mulighet til å lenke dine tidligere akkreditiver med den kontoen.",
+       "authmanager-authn-autocreate-failed": "Autooprrettelse av lokal konto mislyktes: $1",
+       "authmanager-change-not-supported": "De oppgitte akkreditivene kan ikke endres, siden ingenting ville bruke dem.",
+       "authmanager-create-disabled": "Kontoopprettelse er deaktivert.",
+       "authmanager-create-from-login": "For å opprette kontoen din, fyll inn feltene nedenfor.",
+       "authmanager-create-not-in-progress": "Kontoopprettelse foregår ikke eller sesjonsdata er tapt. Start igjen fra begynnelsen.",
+       "authmanager-create-no-primary": "De oppgitte akkreditivene kunne ikke brukes for kontooppretting.",
+       "authmanager-link-no-primary": "De oppgitte akkreditivene kunne ikke brukes for kontolenking.",
+       "authmanager-link-not-in-progress": "Kontolenking foregår ikke eller sesjonsdata er tapt. Start igjen fra begynnelsen.",
+       "authmanager-authplugin-setpass-failed-title": "Passordendring mislyktes",
+       "authmanager-authplugin-setpass-failed-message": "Autentiseringspluginen avviste passordendringen.",
+       "authmanager-authplugin-create-fail": "Autentiseringspluginen avviste kontoopprettelsen.",
+       "authmanager-authplugin-setpass-denied": "Autentiseringspluginen tillater ikke endring av passord.",
+       "authmanager-authplugin-setpass-bad-domain": "Ugyldig domene.",
+       "authmanager-autocreate-noperm": "Automatisk kontoopprettelse tillates ikke.",
+       "authmanager-autocreate-exception": "Automatisk kontoopprettelse er midlertidig deaktivert på grunn av tidligere feil.",
+       "authmanager-userdoesnotexist": "Brukerkontoen «$1» er ikke registrert.",
+       "authmanager-userlogin-remembermypassword-help": "Hvorvidt passordet skal huskes lenger enn sesjonslengden.",
+       "authmanager-username-help": "Brukernavn for autentisering.",
+       "authmanager-password-help": "Passord for autentisering.",
+       "authmanager-domain-help": "Domene for ekstern autentisering.",
+       "authmanager-retype-help": "Passord igjen for å bekrefte.",
+       "authmanager-email-label": "Epost",
+       "authmanager-email-help": "Epostadresse",
+       "authmanager-realname-label": "Virkelig navn",
+       "authmanager-realname-help": "Brukerens virkelige navn",
+       "authmanager-provider-password": "Passordbasert autentisering",
+       "authmanager-provider-password-domain": "Passord- og domenebasert autentisering",
+       "authmanager-provider-temporarypassword": "Midlertidig passord",
+       "authprovider-confirmlink-message": "Basert på dine nylige innloggingsforsøk kan følgende kontoer lenkes til den wikikonto. Å lenke dem muliggjør innlogging via de kontoene. Vennligst velg hvilke som skal lenkes.",
+       "authprovider-confirmlink-request-label": "Kontoer som skal lenkes",
+       "authprovider-confirmlink-success-line": "$1: Lenking gjennomført.",
+       "authprovider-confirmlink-failed": "Konto kunne ikke lenkes fullstendig: $1",
+       "authprovider-confirmlink-ok-help": "Fortsett etter at feilmeldinger om lenking har blitt vist.",
+       "authprovider-resetpass-skip-label": "Hopp over",
+       "authprovider-resetpass-skip-help": "Hopp over nullstilling av passordet.",
+       "authform-nosession-login": "Autentiseringen lyktes, men nettleseren din kan ikke «huske» å være innlogget.\n\n$1",
+       "authform-nosession-signup": "Kontoen ble opprettet, men nettleseren kan ikke «huske» å være innlogget.\n\n$1",
+       "authform-newtoken": "Manglende nøkkel. $1",
+       "authform-notoken": "Mangler nøkkel",
+       "authform-wrongtoken": "Feil nøkkel",
+       "specialpage-securitylevel-not-allowed-title": "Ikke tillatt",
+       "specialpage-securitylevel-not-allowed": "Beklager, du har ikke tillatelse til å bruke denne siden fordi identiteten din ikke kunne bekreftes.",
+       "authpage-cannot-login": "Kunne ikke starte innlogging.",
+       "authpage-cannot-login-continue": "Kunne ikke fortsette innlogging. Sesjonen din har trolig fått et tidsavbrudd.",
+       "authpage-cannot-create": "Kunne ikke starte kontoopprettelse.",
+       "authpage-cannot-create-continue": "Kunne ikke fortsette kontoopprettelse. Sesjonen din har trolig fått et tidsavbrudd.",
+       "authpage-cannot-link": "Kunne ikke starte kontolenking.",
+       "authpage-cannot-link-continue": "Kunne ikke fortsette kontolenking. Sesjonen din har trolig fått et tidsavbrudd.",
+       "cannotauth-not-allowed-title": "Ingen tilgang",
+       "cannotauth-not-allowed": "Du har ikke tillatelse til å bruke denne siden",
+       "changecredentials": "Endre akkreditiver",
+       "changecredentials-submit": "Endre akkreditiver",
+       "changecredentials-invalidsubpage": "$1 er ikke en gyldig akkreditivtype.",
+       "changecredentials-success": "Akkreditivene dine har blitt endret.",
+       "removecredentials": "Fjern akkreditiver",
+       "removecredentials-submit": "Fjern akkreditiver",
+       "removecredentials-invalidsubpage": "$1 er ikke en gyldig akkreditivtype.",
+       "removecredentials-success": "Akkreditivene dine har blitt fjernet.",
+       "credentialsform-provider": "Akkreditivtype:",
+       "credentialsform-account": "Kontonavn:",
+       "cannotlink-no-provider-title": "Det er ingen kontoer som kan lenkes",
+       "cannotlink-no-provider": "Det er ingen kontoer som kan lenkes.",
+       "linkaccounts": "Lenk kontoer",
+       "linkaccounts-success-text": "Kontoen ble lenket.",
+       "linkaccounts-submit": "Lenk kontoer",
+       "unlinkaccounts": "Fjern lenking av kontoer",
+       "unlinkaccounts-success": "Kontoens lenking ble fjernet.",
+       "userjsispublic": "Merk: JavaScript-undersidene bør ikke inneholde konfidensielle data, siden de kan ses av andre brukere.",
+       "usercssispublic": "Merk: CSS-undersidene bør ikke inneholde konfidensielle data siden de kan ses av andre brukere."
 }
index c66b261..b2fcdde 100644 (file)
        "newwindow": "(otwiera się w nowym oknie)",
        "cancel": "Anuluj",
        "moredotdotdot": "Więcej...",
-       "morenotlisted": "Nie jest to kompletna lista.",
+       "morenotlisted": "Ta lista może być niekompletna.",
        "mypage": "Strona",
        "mytalk": "Dyskusja",
        "anontalk": "Dyskusja",
        "invalid-content-data": "Zawartość strony zawiera nieprawidłowe dane",
        "content-not-allowed-here": "Zawartość tego typu ($1) nie jest dozwolona na stronie [[$2]]",
        "editwarning-warning": "Opuszczenie tej strony może spowodować utratę wprowadzonych przez Ciebie zmian.\nJeśli jesteś zalogowany, możesz wyłączyć wyświetlanie tego ostrzeżenia w zakładce „{{int:prefs-editing}}” w swoich preferencjach.",
+       "editpage-invalidcontentmodel-title": "Model zawartości nie jest obsługiwany",
+       "editpage-invalidcontentmodel-text": "Model zawartości „$1” nie jest obsługiwany.",
        "editpage-notsupportedcontentformat-title": "Nieobsługiwany format zawartości",
        "editpage-notsupportedcontentformat-text": "Format zawartości $1 nie jest obsługiwany modelem treści $2.",
        "content-model-wikitext": "wikitekst",
        "tags-actions-header": "Działania",
        "tags-active-yes": "Tak",
        "tags-active-no": "Nie",
-       "tags-source-extension": "Określony przez rozszerzenie",
+       "tags-source-extension": "Określony przez oprogramowanie",
        "tags-source-manual": "Ręcznie wprowadzany przez użytkowników i boty",
        "tags-source-none": "Nieużywany",
        "tags-edit": "edytuj",
index c68b6f8..16d9f85 100644 (file)
        "file-thumbnail-no": "O nome do ficheiro começa por <strong>$1</strong>.\nParece ser uma imagem de tamanho reduzido (uma ''miniatura'' ou ''thumbnail)''.\nSe tiver a imagem original de maior dimensão, envie-a em vez desta. Se não, altere o nome do ficheiro, por favor.",
        "fileexists-forbidden": "Já existe um ficheiro com este nome, e não pode ser reescrito.\nSe ainda pretende carregar o seu ficheiro volte atrás e use outro nome, por favor. [[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "Já existe um ficheiro com este nome no repositório de ficheiros partilhados.\nCaso deseje, mesmo assim, carregar o seu ficheiro, volte atrás e envie-o com um novo nome. [[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "O ficheiro carregado é um duplicado exato da versão atual de <strong>[[:$1]]</strong>.",
+       "fileexists-duplicate-version": "O ficheiro carregado é um duplicado exato {{PLURAL:$2|de uma versão anterior|de uma das versões anteriores}} de <strong>[[:$1]]</strong>.",
        "file-exists-duplicate": "Este ficheiro é um duplicado {{PLURAL:$1|do seguinte|dos seguintes}}:",
        "file-deleted-duplicate": "Um ficheiro idêntico a este ([[:$1]]) foi eliminado anteriormente.\nVerifique o motivo da eliminação do ficheiro antes de prosseguir com o re-envio.",
        "file-deleted-duplicate-notitle": "Um ficheiro idêntico já foi eliminado e o seu título suprimido. Devia pedir a alguém capaz de ver os dados dos ficheiros eliminados para verificar a situação antes de carregá-lo novamente.",
index cc77f46..f591aba 100644 (file)
        "perfcachedts": "Informațiile de mai jos provin din cache, ultima actualizare efectuându-se la $1. Un maxim de {{PLURAL:$4|un rezultat este disponibil|$4 rezultate sunt disponibile}} în cache.",
        "querypage-no-updates": "Actualizările acestei pagini sunt momentan dezactivate. Informațiile de aici nu sunt împrospătate.",
        "viewsource": "Sursă pagină",
-       "viewsource-title": "Vizualizare sursă pentru $1",
+       "viewsource-title": "Vizualizare sursă pentru „$1”",
        "actionthrottled": "Acțiune limitată",
        "actionthrottledtext": "Ca o măsură anti-spam, aveți permisiuni limitate în a efectua această acțiune de prea multe ori într-o perioadă scurtă de timp, iar dumneavoastră tocmai ați depășit această limită.\nVă rugăm să încercați din nou în câteva minute.",
        "protectedpagetext": "Această pagină este protejată împotriva modificărilor sau a altor acțiuni.",
index 8f664fc..018652b 100644 (file)
        "minoredit": "इदं लघु सम्पादनम्",
        "watchthis": "इदं पृष्ठं निरीक्षताम्",
        "savearticle": "पृष्ठं रक्ष्यताम्",
+       "savechanges": "परिवर्तनानि रक्ष्यन्ताम्",
        "publishpage": "पृष्ठं प्रकाश्यताम्",
        "publishchanges": "परिवर्तनानि प्रकाश्यन्ताम्",
        "preview": "प्राग्दृश्यम्",
index 95d8031..3a16bb5 100644 (file)
        "continue-editing": "خانہ ترمیم میں جائیں",
        "previewconflict": "اس نمائش میں خانہ ترمیم کے اوپر موجود متن جس انداز میں ظاہر ہو رہا ہے، محفوظ کرنے کے بعد اسی طرح نظر آئے گا۔",
        "session_fail_preview": "معاف کیجئے! نشست کے مواد میں خامی کی وجہ سے آپکی  ترمیم پر عمل نہیں کیا جاسکا.\nبرائے مہربانی دوبارہ کوشش کیجئے.\nاگر آپکو پھر بھی مشکل پیش آرہی ہے تو [[Special:UserLogout|خارجِ نوشتہ]] ہوکر واپس داخلِ نوشتہ ہوجایئے.",
+       "edit_form_incomplete": "<strong>خانہ ترمیم سے کچھ حصے سرور تک نہیں پہنچ سکے ہیں؛ براہ کرم اپنی ترامیم کو دوبارہ جانچ لیں کہ آیا وہ برقرار ہیں یا نہیں اور دوبارہ کوشش کریں۔</strong>",
        "editing": "آپ \"$1\" میں ترمیم کر رہے ہیں۔",
        "creating": "زیر تخلیق $1",
        "editingsection": "$1 کے قطعہ کی تدوین",
        "editingold": "'''انتباہ: آپ اس صفحے کا ایک پرانا مسودہ مرتب کررہے ہیں۔ اگر آپ اسے محفوظ کرتے ہیں تو اس صفحے کے اس پرانے مسودے سے اب تک کی جانے والی تمام تدوین ضائع ہو جاۓ گی۔'''",
        "yourdiff": "تضادات",
        "copyrightwarning": "یہ یادآوری کرلیجیۓ کہ {{SITENAME}} میں تمام تحریری شراکت جی این یو آزاد مسوداتی اجازہ ($2)کے تحت تصور کی جاتی ہے (مزید تفصیل کیلیۓ $1 دیکھیۓ)۔ اگر آپ اس بات سے متفق نہیں کہ آپکی تحریر میں ترمیمات کری جائیں اور اسے آزادانہ (جیسے ضرورت ہو) استعمال کیا جاۓ تو براۓ کرم اپنی تصانیف یہاں داخل نہ کیجیۓ۔ اگر آپ یہاں اپنی تحریر جمع کراتے ہیں تو آپ اس بات کا بھی اقرار کر رہے ہیں کہ، اسے آپ نے خود تصنیف کیا ہے یا دائرہ ءعام (پبلک ڈومین) سے حاصل کیا ہے یا اس جیسے کسی اور آذاد وسیلہ سے۔'''بلااجازت ایسا کام داخل نہ کیجیۓ جسکا حق ِطبع و نشر محفوظ ہو!'''",
+       "copyrightwarning2": "براہ کرم اس بات کا خیال رکھیں کہ {{SITENAME}} میں آپ کی جانب سے کی جانے والی تمام ترمیموں میں دیگر صارفین بھی حذف و اضافہ کر سکتے ہیں۔\nاگر آپ اپنی تحریر کے ساتھ اس قسم کے سلوک کے روادار نہیں تو براہ کرم اسے یہاں شائع نہ کریں۔<br />\nنیز اس تحریر کو شائع کرتے وقت آپ ہم سے یہ وعدہ بھی کر رہے ہیں کہ اسے آپ نے خود لکھا ہے یا اسے دائرہ عام یا کسی آزاد ماخذ سے یہاں نقل کر رہے ہیں (تفصیلات کے لیے $1 ملاحظہ فرمائیں)۔\n<strong>براہ کرم اجازت کے بغیر کسی کاپی رائٹ شدہ مواد کو یہاں شائع نہ کریں۔</strong>",
+       "editpage-cannot-use-custom-model": "اس صفحہ کے مواد کے ماڈل کو تبدیل نہیں کیا جا سکتا۔",
+       "readonlywarning": "<strong>انتباہ: انتظامی نگہداشت کی خاطر ڈیٹابیس کو مقفل کر دیا گیا ہے، لہذا اس وقت آپ اپنی ترامیم کو محفوظ نہیں کر سکتے۔</strong>\nآپ اپنی تحریر کو کسی ٹیکسٹ فائل میں محفوظ کر سکتے ہیں تاکہ وہ ضائع نہ ہو اور آئندہ اسے استعمال کیا جا سکے۔\n\nانتظامیہ کی جانب سے مقفل کرنے کی حسب ذیل وجہ بیان کی گئی ہے:\n\n$1",
        "protectedpagewarning": "<strong>انتباہ: اس صفحہ میں ترمیم کاری کو مقفل کر دیا گیا ہے اور محض انتظامی اختیارات کے حامل صارفین ہی اس میں ترمیم کر سکتے ہیں۔</strong>\nحوالہ کے لیے ذیل میں نوشتہ جاتی اندراج فراہم کیا گیا ہے:",
        "semiprotectedpagewarning": "<strong>اطلاع:</strong> اس صفحہ کو یوں مقفل کیا جاچکا ہے کہ اس میں صرف اندراج شدہ صارفین ہی ترمیم کرسکتے ہیں۔\nحوالہ کے لیے ذیل میں تازہ ترین نوشتہ جاتی اندراج دیا گیا ہے:",
        "cascadeprotectedwarning": "<strong>انتباہ:</strong> اس صفحہ میں ترمیم کاری کو مقفل کر دیا گیا ہے اور محض انتظامی اختیارات کے حامل صارفین ہی اس میں ترمیم کر سکتے ہیں۔ اسے مقفل کرنے کی وجہ یہ ہے کہ پیش نظر صفحہ درج ذیل محفوظ {{PLURAL:$1|صفحہ|صفحات}} کی آبشاری حفاظت میں شامل ہے:",
+       "titleprotectedwarning": "<strong>انتباہ: اس صفحہ کو محفوظ کر دیا گیا ہے، چنانچہ اسے تخلیق کرنے کے لیے [[Special:ListGroupRights|خصوصی اختیارات]] درکار ہونگے۔</strong>\nحوالہ کے لیے ذیل میں نوشتہ کا تازہ ترین اندراج موجود ہے:",
        "templatesused": "اِس صفحہ پر مستعمل {{PLURAL:$1|سانچہ|سانچے}}:",
        "templatesusedpreview": "اِس پیش منظر میں مستعمل {{PLURAL:$1|سانچہ|سانچے}}:",
        "templatesusedsection": "اِس قطعہ میں مستعمل {{PLURAL:$1|سانچہ|سانچے}}:",
        "template-protected": "(محفوظ شدہ)",
        "template-semiprotected": "(نیم محفوظ)",
        "hiddencategories": "یہ صفحہ {{PLURAL:$1|1 چُھپے زمرے|$1 چُھپے زمرہ جات}} میں شامل ہے:",
+       "nocreatetext": "{{SITENAME}} نے نئے صفحات تخلیق کرنے پر پابندی لگا رکھی ہے۔\nتاہم آپ پہلے سے موجود صفحات میں ترمیم کر سکتے ہیں یا [[Special:UserLogin|اپنے کھاتے ميں داخل ہوں یا کھاتہ بنائیں]]۔",
        "nocreate-loggedin": "آپ کو نئے صفحات تخلیق کرنے کی اجازت نہیں ہے.",
        "sectioneditnotsupported-title": "قطعہ کی تدوین حمایت شدہ نہیں ہے",
        "sectioneditnotsupported-text": "اِس صفحہ میں قطعہ کی تدوین حمایت شدہ نہیں ہے.",
        "permissionserrors": "خطائے اجازت",
        "permissionserrorstext": "درج ذیل {{PLURAL:$1|وجہ|وجوہات}} کی بناء پر آپ کو ایسا کرنے کی اجازت نہیں ہے:",
        "permissionserrorstext-withaction": "درج ذیل {{PLURAL:$1|وجہ|وجوہات}} کی بناء پر آپ کو $2 کرنے کی اجازت نہیں ہے:",
+       "contentmodelediterror": "آپ اس نسخے میں ترمیم نہیں کر سکتے کیونکہ اس کے مواد کا ماڈل ‌‌<code>$1</code> ہے جو اس صفحہ کے مواد کے موجودہ ماڈل <code>$2</code> سے مختلف ہے۔",
        "recreate-moveddeleted-warn": "''' انتباہ: آپ ایک گزشتہ حذف شدہ صفحہ دوبارہ تخلیق کررہے ہیں. '''\n\nآپ کو اِس بات پر غور کرنا چاہئے کہ آیا اِس صفحہ کی تدوین جاری رکھنا موزوں ہے یا نہیں.\nصفحہ کا نوشتۂ حذف شدگی و منتقلی یہاں سہولت کی خاطر مہیّا کیا جارہا ہے:",
        "moveddeleted-notice": "یہ ایک حذف شدہ صفحہ ہے.\nصفحہ کا نوشتۂ حذف شدگی و منتقلی ذیل میں بطورِ حوالہ دیا جارہا ہے.",
+       "moveddeleted-notice-recent": "معذرت، اس صفحہ کو حال ہی میں حذف کیا گیا ہے (گزشتہ چوبیس گھنٹوں میں)۔\nحوالہ کے لیے ذیل میں اس صفحہ کا نوشتہ حذف اور نوشتہ منتقلی موجود ہے۔",
        "log-fulllog": "پورا نوشتہ دیکھئے",
        "edit-gone-missing": "صفحہ تجدید نہیں کیا جاسکتا.\nلگتا ہے یہ حذف ہوچکا ہے.",
        "edit-conflict": "تنازعۂ تدوین.",
        "postedit-confirmation-saved": "آپ کی ترمیم محفوظ ہوگئی۔",
        "edit-already-exists": "نیا صفحہ تخلیق نہیں کیا جاسکتا.\nیہ پہلے سے موجود ہے.",
        "defaultmessagetext": "طے شدہ پیغام کا متن",
+       "content-failed-to-parse": "ماڈل $1 کے $2 مواد کے تجزیہ میں ناکامی: $3",
        "invalid-content-data": "نادرست ڈیٹا مندرجات",
+       "content-not-allowed-here": "صفحہ [[$2]] پر \"$1\" مواد کی اجازت نہیں",
+       "editwarning-warning": "اس صفحہ کو چھوڑنے پر ممکن ہے جو تبدیلیاں آپ نے کی ہیں وہ سب ضائع ہو جائیں۔\nاگر آپ داخل ہیں تو اپنی ترجیحات کے خانہ «{{int:prefs-editing}}» سے اس انتباہ کو غیر فعال کر سکتے ہیں۔",
+       "editpage-invalidcontentmodel-title": "مواد کا ماڈل معاونت یافتہ نہیں",
+       "editpage-invalidcontentmodel-text": "مواد کا ماڈل \"$1\" معاونت یافتہ نہیں ہے۔",
+       "editpage-notsupportedcontentformat-title": "مواد کا فارمیٹ معاونت یافتہ نہیں",
+       "editpage-notsupportedcontentformat-text": "مواد کے ماڈل $2 کی جانب سے مواد کا فارمیٹ $1 معاونت یافتہ نہیں۔",
        "content-model-wikitext": "ویکی متن",
        "content-model-text": "سادہ متن",
        "content-model-javascript": "جاوا اسکرپٹ",
        "content-json-empty-object": "خالی آبجیکٹ",
        "content-json-empty-array": "خالی ایرے",
+       "deprecated-self-close-category": "صفحات مع نادرست ایچ ٹی ایم ایل ٹیگ",
+       "deprecated-self-close-category-desc": "اس صفحہ میں ایچ ٹی ایم ایل کے نادرست ٹیگ مثلاً <code>&lt;b/></code> or <code>&lt;span/></code> استعمال کیے گئے ہیں۔ چونکہ ایچ ٹی ایم ایل 5 میں ان ٹیگوں کا رویہ تبدیل ہو جائے گا، لہذا ویکی متن میں ان کا استعمال متروک ہو چکا ہے۔",
+       "undo-nochange": "معلوم ہوتا ہے کہ اس ترمیم کو پہلے ہی واپس پھیر دیا گیا ہے۔",
        "undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|تبادلہ خیال]]) کی جانب سے کی گئی ترمیم $1 رد کردی گئی ہے۔",
+       "undo-summary-username-hidden": "پوشیدہ صارف کے نسخہ $1 کو واپس پھیریں",
+       "cantcreateaccount-text": "[[User:$3|$3]] نے اس آئی پی پتہ (<strong>$1</strong>) کی کھاتہ سازی پر پابندی لگا رکھی ہے۔\n\n$3 نے «<em>$2</em>» وجہ بیان کی ہے",
+       "cantcreateaccount-range-text": "[[User:$3|$3]] نے <strong>$1</strong> رینج کے آئی پی پتوں پر جس میں آپ کا آئی پی پتہ (<strong>$4</strong>) بھی موجود ہے پر پابندی لگا دی ہے۔\n\n$3 نے «<em>$2</em>» وجہ بیان کی ہے",
        "viewpagelogs": "اس صفحہ کیلیے نوشتہ جات دیکھیے",
        "nohistory": "اِس صفحہ کیلئے کوئی تدوینی تاریخچہ موجود نہیں ہے.",
        "currentrev": "حـالیـہ تـجدید",
        "rev-deleted-comment": "(تبصرہ حذف کی گيا ہے)",
        "rev-deleted-user": "(صارف نام حذف کیا گيا ہے)",
        "rev-deleted-event": "(نوشتہ کی تفصیلات ہٹا دی گئیں)",
+       "rev-deleted-user-contribs": "[صارف نام یا آئی پی پتہ ہٹا دیا گیا - شراکتوں سے ترمیم پوشیدہ ہو گئی]",
+       "rev-deleted-text-permission": "پیش نظر صفحہ کی یہ ترمیم <strong>حذف کر دی گئی ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-suppressed-text-permission": "پیش نظر صفحہ کی یہ ترمیم <strong>پوشیدہ کر دی گئی ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} نوشتہ پوشیدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-deleted-text-unhide": "پیش نظر صفحہ کی یہ ترمیم <strong>حذف کر دی گئی ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔\nتاہم اگر آپ چاہیں تو [$1 اس نسخے کو ابھی بھی دیکھ سکتے ہیں]۔",
+       "rev-suppressed-text-unhide": "پیش نظر صفحہ کی یہ ترمیم <strong>پوشیدہ کر دی گئی ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔\nتاہم اگر آپ چاہیں تو [$1 اس نسخے کو ابھی بھی دیکھ سکتے ہیں]۔",
+       "rev-deleted-text-view": "پیش نظر صفحہ کی یہ ترمیم <strong>حذف کر دی گئی ہے</strong>۔\nتاہم اسے آپ دیکھ سکتے ہیں، مزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-suppressed-text-view": "پیش نظر صفحہ کی یہ ترمیم <strong>پوشیدہ کر دی گئی ہے</strong>۔\nتاہم اسے آپ دیکھ سکتے ہیں، مزید تفصیلات [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-deleted-no-diff": "آپ اس فرق کو نہیں دیکھ سکتے کیونکہ دونوں میں سے کسی ایک ترمیم کو <strong>حذف کر دیا گیا ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-suppressed-no-diff": "آپ اس فرق کو نہیں دیکھ سکتے کیونکہ دونوں میں سے کسی ایک ترمیم کو <strong>حذف کر دیا گیا ہے</strong>۔",
+       "rev-deleted-unhide-diff": "اس فرق کی کسی ایک ترمیم کو <strong>حذف کر دیا گیا ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔\nتاہم اگر آپ چاہیں تو [$1 اس فرق کو ابھی بھی دیکھ سکتے ہیں]۔",
+       "rev-suppressed-unhide-diff": "اس فرق کی کسی ایک ترمیم کو <strong>پوشیدہ کر دیا گیا ہے</strong>۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔\nتاہم اگر آپ چاہیں تو [$1 اس فرق کو ابھی بھی دیکھ سکتے ہیں]۔",
+       "rev-deleted-diff-view": "اس فرق کی کسی ایک ترمیم کو <strong>حذف کر دیا گیا ہے</strong>۔\nآپ اس فرق کو دیکھ سکتے ہیں؛ مزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
+       "rev-suppressed-diff-view": "اس فرق کی کسی ایک ترمیم کو <strong>پوشیدہ کر دیا گیا ہے</strong>۔\nآپ اس فرق کو دیکھ سکتے ہیں؛ مزید تفصیلات [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} نوشتہ پوشیدگی] میں دیکھی جا سکتی ہیں۔",
        "rev-delundel": "دکھاؤ/چھپاؤ",
        "rev-showdeleted": "دکھاؤ",
        "revisiondelete": "نظرثانی حذف کریں/واپس لائیں",
        "revdelete-nooldid-title": "ناقص مقصود نظرثانی",
+       "revdelete-nooldid-text": "اس فنکشن کو جس نسخے پر انجام دینا ہے اسے آپ نے منتخب نہیں کیا، یا منتخب کردہ نسخہ موجود نہیں، یا آپ موجودہ نسخہ کو پوشیدہ کرنے کی کوشش کر رہے ہیں۔",
        "revdelete-no-file": "درج کردہ فائل موجود نہیں ہے۔",
        "revdelete-show-file-submit": "ہاں",
+       "revdelete-selected-text": "[[:$2]] {{PLURAL:$1|کا منتخب نسخہ|کے منتخب نسخے}}:",
+       "revdelete-selected-file": "[[:$2]] {{PLURAL:$1|کا منتخب فائل نسخہ|کے منتخب فائل نسخے}}:",
        "logdelete-selected": "{{PLURAL:$1|منتخب واقعۂ نوشتہ|منتخب واقعاتِ نوشتہ}}:",
+       "revdelete-text-text": "صفحہ کے تاریخچے میں حذف شدہ نسخے نظر آئی گے لیکن ان کا مواد عام صارفین کے لیے ناقابل رسائی ہوگا۔",
+       "revdelete-text-file": "فائل کے تاریخچے میں حذف شدہ نسخے نظر آئی گے لیکن ان کا مواد عام صارفین کے لیے ناقابل رسائی ہوگا۔",
+       "logdelete-text": "نوشتہ کے حذف شدہ اندراجات نوشتوں میں ظاہر ہوتے رہیں گے لیکن ان کا مواد عام صارفین کے لیے ناقابل رسائی ہوگا۔",
+       "revdelete-text-others": "جب تک اضافی پابندیاں نہیں لگائی جاتیں دیگر منتظمین کو اس پوشیدہ مواد تک رسائی اور اسے بحال کرنے کا اختیار حاصل ہوگا۔",
        "revdelete-confirm": "برائے مہربانی! یقین دِہانی کرلیجئے کہ آپ واقعی ایسا کرنا چاہتے ہیں، آپ اِس کے نتائج سے باخبر ہیں، اور آپ یہ [[{{MediaWiki:Policy-url}}|پالیسی]] کے مطابق کررہے ہیں.",
        "revdelete-legend": "رویتی پابندیاں لگائیں",
        "revdelete-hide-text": "نظرثانی متن چھپاؤ",
        "revdelete-hide-name": "ہدف اور پیرامیٹرز کو چھپائیں",
        "revdelete-hide-comment": "ترمیمی تبصرہ چھپاؤ",
        "revdelete-hide-user": "ترمیم کار کا اسمِ صارف / آئی.پی پتہ چُھپاؤ",
+       "revdelete-hide-restricted": "منتظمین اور دیگر صارفین سے معلومات کو پوشیدہ کریں",
        "revdelete-radio-same": "(تبدیل مت کرو)",
        "revdelete-radio-set": "پوشیدہ",
        "revdelete-radio-unset": "ظاہر",
+       "revdelete-suppress": "منتظمین اور دیگر صارفین سے معلومات کو پوشیدہ کریں",
        "revdelete-unsuppress": "بحال شدہ نظرثانیوں پر پابندیاں ہٹاؤ",
        "revdelete-log": "وجہ",
        "revdelete-success": "'''رؤیتِ نظرثانی کی تجدید کامیابی سے ہوئی.'''",
        "revdel-restore": "ظاہریت تبدیل کرو",
        "pagehist": "تاریخچۂ صفحہ",
        "deletedhist": "حذف شدہ تاریخچہ",
+       "revdelete-reason-dropdown": "* عمومی وجوہات حذف شدگی\n** کاپی رائٹ کی خلاف ورزی\n** نامناسب تبصرہ یا ذاتی معلومات\n** نامناسب صارف نام\n** ممکنہ طور پر افترا آمیر معلومات",
        "revdelete-otherreason": "دوسری/اضافی وجہ:",
        "revdelete-reasonotherlist": "کوئی اَور وجہ",
        "revdelete-edit-reasonlist": "تحذیفی وجوہات کی تدوین",
        "revdelete-offender": "نظرثانی مصنف:",
+       "suppressionlog": "نوشتہ پوشیدگی",
        "mergehistory": "تواریخِ صفحہ کا انضمام",
        "mergehistory-box": "دو صفحات کی نظرثانیوں کا انضمام:",
        "mergehistory-from": "مآخذ صفحہ:",
        "prefs-editwatchlist-raw": "زیر نظر خام فہرست میں ترمیم کریں",
        "prefs-editwatchlist-clear": "اپنی زیر نظر فہرست صاف کریں",
        "prefs-watchlist-days": "زیر نظر فہرست میں نظر آنے والے ایام:",
-       "prefs-watchlist-days-max": "زیادہ سے زیادہ $1 دن",
+       "prefs-watchlist-days-max": "زیادہ سے زیادہ $1 {{PLURAL:$1|دن}}",
        "prefs-watchlist-edits": "توسیع شدہ زیر نظر فہرست میں نظر آنے والی تبدیلیوں کی زیادہ سے زیادہ تعداد:",
        "prefs-watchlist-edits-max": "زیادہ سے زیادہ تعداد: 1000",
        "prefs-watchlist-token": "زیر نظر فہرست کی کلید:",
        "stub-threshold-sample-link": "نمونہ",
        "stub-threshold-disabled": "غیر فعال",
        "recentchangesdays": "حالیہ تبدیلیوں میں دکھائے جانے والے ایّام:",
-       "recentchangesdays-max": "زیادہ سے زیادہ $1 دن",
+       "recentchangesdays-max": "زیادہ سے زیادہ $1 {{PLURAL:$1|دن}}",
        "recentchangescount": "دکھائی جانے والی ترامیم کی تعداد:",
        "prefs-help-recentchangescount": "اِس میں حالیہ تبدیلیاں، تاریخچے اور نوشتہ جات شامل ہیں۔",
        "prefs-help-watchlist-token2": "یہ آپ کی زیر نظر فہرست کے ویب فیڈ کی خفیہ کلید ہے۔\nاسے خفیہ رکھیں، تاکہ کوئی دوسرا شخص آپ کی زیر نظر فہرست نہ دیکھ سکے۔\nاگر آپ کو کلید تبدیل کرنی ہو تو [[Special:ResetTokens|یہاں کلک کریں]]۔",
        "savedprefs": "آپ کی ترجیحات محفوظ ہوگئیں۔",
+       "savedrights": "{{GENDER:$1|$1}} کے اختیارات محفوظ ہو گئے۔",
        "timezonelegend": "منطقۂ وقت:",
        "localtime": "مقامی وقت:",
        "timezoneuseserverdefault": "ویکی کا طے شدہ استعمال کریں ($1)",
        "prefs-custom-css": "شخصی سی ایس ایس",
        "prefs-custom-js": "شخصی جاوا اسکرپٹ",
        "prefs-common-css-js": "جملہ پوشاکوں کے لیے مشترکہ سی ایس ایس/جاوا اسکرپٹ:",
+       "prefs-reset-intro": "آپ اس صفحہ کے ذریعہ اپنی موجودہ ترجیحات کو سائٹ کی ابتدائی ترتیبات کے مطابق ڈھال سکتے ہیں۔\nلیکن اسے واپس نہیں پھیرا جا سکتا۔",
        "prefs-emailconfirm-label": "برقی خط کی تصدیق:",
        "youremail": "برقی خط:",
        "username": "صارف:",
        "yourrealname": "* اصلی نام",
        "yourlanguage": "زبان:",
        "yourvariant": "متغیّر:",
+       "prefs-help-variant": "اس ویکی کے صفحات دکھانے کے لیے آپ کا پسندیدہ لہجہ یا املا۔",
        "yournick": "شخصی دستخط:",
        "prefs-help-signature": "تبادلۂ خیال صفحات پر تبصرہ تحریر کرنے کے بعد یہ \"<nowiki>~~~~</nowiki>\" علامتیں درج کرنی چاہئیں، یہ علامتیں از خود آپ کے دستخط اور وقت میں تبدیل ہو جائیں گی۔",
        "badsig": "ناقص خام دستخط.\nHTML tags جانچئے.",
        "prefs-displaywatchlist": "نمائش کے اختیارات",
        "prefs-tokenwatchlist": "ٹوکن",
        "prefs-diffs": "فرق",
+       "prefs-help-prefershttps": "یہ ترجیح آپ کے اگلے لاگ ان پر اثر انداز ہوگی۔",
        "userrights": "حقوقِ صارف کی نظامت",
        "userrights-lookup-user": "گروہائے صارف کا انتظام",
        "userrights-user-editname": "کوئی اسم‌صارف داخل کیجئے:",
        "grouppage-bot": "{{ns:project}}:روبہ جات",
        "grouppage-sysop": "{{ns:project}}:منتظمین",
        "grouppage-bureaucrat": "{{ns:project}}:مامورین اداری",
+       "right-read": "مطالعہ صفحات",
+       "right-edit": "ترمیم صفحات",
+       "right-createpage": "تخلیق صفحات (تبادلہ خیال صفحات نہیں)",
+       "right-createtalk": "تخلیق تبادلہ خیال صفحات",
+       "right-createaccount": "کھاتہ سازی",
+       "right-autocreateaccount": "بیرونی صارف کھاتے کے ذریعہ خودکار لاگ ان",
+       "right-minoredit": "ترامیم کی بطور معمولی ترمیم نشان زدگی",
+       "right-move": "منتقلی صفحات",
+       "right-move-subpages": "منتقلی صفحات مع ذیلی صفحات",
        "right-upload": "ملفات زبراثقال (اپ لوڈ) کریں",
        "right-writeapi": "اے پی آئی لکھائی کا استعمال",
        "right-delete": "صفحات حذف کریں",
index 8c2b143..41f516a 100644 (file)
@@ -147,9 +147,6 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
                        ->disableOriginalConstructor()
                        ->getMock();
 
-               $lbFactory->expects( $this->once() )
-                       ->method( 'destroy' );
-
                $newServices->redefineService(
                        'DBLoadBalancerFactory',
                        function() use ( $lbFactory ) {
@@ -164,12 +161,11 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
 
                try {
                        MediaWikiServices::getInstance()->getService( 'DBLoadBalancerFactory' );
-                       $this->fail( 'DBLoadBalancerFactory shoudl have been disabled' );
+                       $this->fail( 'DBLoadBalancerFactory should have been disabled' );
                }
                catch ( ServiceDisabledException $ex ) {
                        // ok, as expected
-               }
-               catch ( Throwable $ex ) {
+               } catch ( Throwable $ex ) {
                        $this->fail( 'ServiceDisabledException expected, caught ' . get_class( $ex ) );
                }
 
index 5affa9c..adf8a40 100644 (file)
@@ -58,9 +58,21 @@ class LBFactoryTest extends MediaWikiTestCase {
        }
 
        public function testLBFactorySimpleServer() {
-               $this->setMwGlobals( 'wgDBservers', false );
+               global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype;
 
-               $factory = new LBFactorySimple( [] );
+               $servers = [
+                       [
+                               'host'      => $wgDBserver,
+                               'dbname'    => $wgDBname,
+                               'user'      => $wgDBuser,
+                               'password'  => $wgDBpassword,
+                               'type'      => $wgDBtype,
+                               'load'      => 0,
+                               'flags'     => DBO_TRX // REPEATABLE-READ for consistency
+                       ],
+               ];
+
+               $factory = new LBFactorySimple( [ 'servers' => $servers ] );
                $lb = $factory->getMainLB();
 
                $dbw = $lb->getConnection( DB_MASTER );
@@ -76,28 +88,31 @@ class LBFactoryTest extends MediaWikiTestCase {
        public function testLBFactorySimpleServers() {
                global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype;
 
-               $this->setMwGlobals( 'wgDBservers', [
+               $servers = [
                        [ // master
-                               'host'          => $wgDBserver,
-                               'dbname'    => $wgDBname,
-                               'user'          => $wgDBuser,
-                               'password'      => $wgDBpassword,
-                               'type'          => $wgDBtype,
-                               'load'      => 0,
-                               'flags'     => DBO_TRX // REPEATABLE-READ for consistency
+                               'host'     => $wgDBserver,
+                               'dbname'   => $wgDBname,
+                               'user'     => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type'     => $wgDBtype,
+                               'load'     => 0,
+                               'flags'    => DBO_TRX // REPEATABLE-READ for consistency
                        ],
                        [ // emulated slave
-                               'host'          => $wgDBserver,
-                               'dbname'    => $wgDBname,
-                               'user'          => $wgDBuser,
-                               'password'      => $wgDBpassword,
-                               'type'          => $wgDBtype,
-                               'load'      => 100,
-                               'flags'     => DBO_TRX // REPEATABLE-READ for consistency
+                               'host'     => $wgDBserver,
+                               'dbname'   => $wgDBname,
+                               'user'     => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type'     => $wgDBtype,
+                               'load'     => 100,
+                               'flags'    => DBO_TRX // REPEATABLE-READ for consistency
                        ]
-               ] );
+               ];
 
-               $factory = new LBFactorySimple( [ 'loadMonitorClass' => 'LoadMonitorNull' ] );
+               $factory = new LBFactorySimple( [
+                       'servers' => $servers,
+                       'loadMonitorClass' => 'LoadMonitorNull'
+               ] );
                $lb = $factory->getMainLB();
 
                $dbw = $lb->getConnection( DB_MASTER );
@@ -216,4 +231,146 @@ class LBFactoryTest extends MediaWikiTestCase {
                $cp->shutdownLB( $lb );
                $cp->shutdown();
        }
+
+       private function newLBFactoryMulti( array $baseOverride = [], array $serverOverride = [] ) {
+               global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype;
+
+               return new LBFactoryMulti( $baseOverride + [
+                       'sectionsByDB' => [],
+                       'sectionLoads' => [
+                               'DEFAULT' => [
+                                       'test-db1' => 1,
+                               ],
+                       ],
+                       'serverTemplate' => $serverOverride + [
+                               'dbname' => $wgDBname,
+                               'user' => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type' => $wgDBtype,
+                               'flags' => DBO_DEFAULT
+                       ],
+                       'hostsByName' => [
+                               'test-db1' => $wgDBserver,
+                       ],
+                       'loadMonitorClass' => 'LoadMonitorNull',
+                       'localDomain' => wfWikiID()
+               ] );
+       }
+
+       public function testNiceDomains() {
+               global $wgDBname;
+
+               $factory = $this->newLBFactoryMulti();
+               $lb = $factory->getMainLB();
+
+               $db = $lb->getConnectionRef( DB_MASTER );
+               $this->assertEquals(
+                       $wgDBname,
+                       $db->getDomainID()
+               );
+               unset( $db );
+
+               /** @var DatabaseBase $db */
+               $db = $lb->getConnection( DB_MASTER, [], '' );
+
+               $this->assertEquals(
+                       '',
+                       $db->getDomainID()
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'page' ),
+                       "Correct full table name"
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( $wgDBname ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( "$wgDBname.page" ),
+                       "Correct full table name"
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'nice_db' ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'nice_db.page' ),
+                       "Correct full table name"
+               );
+
+               $factory->setDomainPrefix( 'my_' );
+               $this->assertEquals(
+                       '',
+                       $db->getDomainID()
+               );
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'my_page' ),
+                       $db->tableName( 'page' ),
+                       "Correct full table name"
+               );
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'other_nice_db' ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'other_nice_db.page' ),
+                       "Correct full table name"
+               );
+
+               $factory->closeAll();
+               $factory->destroy();
+       }
+
+       public function testTrickyDomain() {
+               $dbname = 'unittest-domain';
+               $factory = $this->newLBFactoryMulti(
+                       [ 'localDomain' => $dbname ], [ 'dbname' => $dbname ] );
+               $lb = $factory->getMainLB();
+               /** @var DatabaseBase $db */
+               $db = $lb->getConnection( DB_MASTER, [], '' );
+
+               $this->assertEquals(
+                       '',
+                       $db->getDomainID()
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'page' ),
+                       "Correct full table name"
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( $dbname ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( "$dbname.page" ),
+                       "Correct full table name"
+               );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'nice_db' ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'nice_db.page' ),
+                       "Correct full table name"
+               );
+
+               $factory->setDomainPrefix( 'my_' );
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'my_page' ),
+                       $db->tableName( 'page' ),
+                       "Correct full table name"
+               );
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'other_nice_db' ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'other_nice_db.page' ),
+                       "Correct full table name"
+               );
+
+               \MediaWiki\suppressWarnings();
+               $this->assertFalse( $db->selectDB( 'garbage-db' ) );
+               \MediaWiki\restoreWarnings();
+
+               $this->assertEquals(
+                       $db->addIdentifierQuotes( 'garbage-db' ) . '.' . $db->addIdentifierQuotes( 'page' ),
+                       $db->tableName( 'garbage-db.page' ),
+                       "Correct full table name"
+               );
+
+               $factory->closeAll();
+               $factory->destroy();
+       }
 }
diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseDomainTest.php
new file mode 100644 (file)
index 0000000..d13fbf9
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @covers DatabaseDomain
+ */
+class DatabaseDomainTest extends PHPUnit_Framework_TestCase {
+       public static function provideConstruct() {
+               return [
+                       // All strings
+                       [ 'foo', 'bar', 'baz', 'foo-bar-baz' ],
+                       // Nothing
+                       [ null, null, '', '' ],
+                       // Invalid $database
+                       [ 0, 'bar', '', '', true ],
+                       // - in one of the fields
+                       [ 'foo-bar', 'baz', 'baa', 'foo?hbar-baz-baa' ],
+                       // ? in one of the fields
+                       [ 'foo?bar', 'baz', 'baa', 'foo??bar-baz-baa' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstruct
+        */
+       public function testConstruct( $db, $schema, $prefix, $id, $exception = false ) {
+               if ( $exception ) {
+                       $this->setExpectedException( InvalidArgumentException::class );
+               }
+
+               $domain = new DatabaseDomain( $db, $schema, $prefix );
+               $this->assertInstanceOf( DatabaseDomain::class, $domain );
+               $this->assertEquals( $db, $domain->getDatabase() );
+               $this->assertEquals( $schema, $domain->getSchema() );
+               $this->assertEquals( $prefix, $domain->getTablePrefix() );
+               $this->assertEquals( $id, $domain->getId() );
+       }
+
+       public static function provideNewFromId() {
+               return [
+                       // basic
+                       [ 'foo', 'foo', null, '' ],
+                       // <database>-<prefix>
+                       [ 'foo-bar', 'foo', null, 'bar' ],
+                       [ 'foo-bar-baz', 'foo', 'bar', 'baz' ],
+                       // ?h -> -
+                       [ 'foo?hbar-baz-baa', 'foo-bar', 'baz', 'baa' ],
+                       // ?? -> ?
+                       [ 'foo??bar-baz-baa', 'foo?bar', 'baz', 'baa' ],
+                       // ? is left alone
+                       [ 'foo?bar-baz-baa', 'foo?bar', 'baz', 'baa' ],
+                       // too many parts
+                       [ 'foo-bar-baz-baa', '', '', '', true ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideNewFromId
+        */
+       public function testNewFromId( $id, $db, $schema, $prefix, $exception = false ) {
+               if ( $exception ) {
+                       $this->setExpectedException( InvalidArgumentException::class );
+               }
+               $domain = DatabaseDomain::newFromId( $id );
+               $this->assertInstanceOf( DatabaseDomain::class, $domain );
+               $this->assertEquals( $db, $domain->getDatabase() );
+               $this->assertEquals( $schema, $domain->getSchema() );
+               $this->assertEquals( $prefix, $domain->getTablePrefix() );
+       }
+}