From: jenkins-bot Date: Mon, 1 Jul 2019 01:37:41 +0000 (+0000) Subject: Merge "Rest API: urldecode path parameters" X-Git-Tag: 1.34.0-rc.0~1229 X-Git-Url: http://git.cyclocoop.org//%27http:/jquery.khurshid.com/ifixpng.php/%27?a=commitdiff_plain;h=a7c7cfb33404f5bc314a17b95544d90017d8917d;hp=697423977880fdfbd8618b83237b53bafeb3a5e9;p=lhc%2Fweb%2Fwiklou.git Merge "Rest API: urldecode path parameters" --- diff --git a/.gitattributes b/.gitattributes index 81b7a33173..145caeb3b8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,4 +10,4 @@ package.json export-ignore README.mediawiki export-ignore Gemfile* export-ignore vendor/pear/net_smtp/README.rst export-ignore - +phpunit.xml.dist export-ignore diff --git a/.gitignore b/.gitignore index 8cacb1ee30..a76270e184 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ npm-debug.log node_modules/ /resources/lib/.foreign /tests/phpunit/phpunit.phar +phpunit.xml /tests/selenium/log .eslintcache diff --git a/.phpcs.xml b/.phpcs.xml index 9ccf5657b7..76234a2b56 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -180,6 +180,7 @@ */tests/phpunit/includes/GlobalFunctions/*\.php */tests/phpunit/maintenance/*\.php + */tests/phpunit/integration/includes/GlobalFunctions/*\.php diff --git a/RELEASE-NOTES-1.34 b/RELEASE-NOTES-1.34 index f3baa52a95..acd82d63be 100644 --- a/RELEASE-NOTES-1.34 +++ b/RELEASE-NOTES-1.34 @@ -33,6 +33,9 @@ For notes on 1.33.x and older releases, see HISTORY. the code as the request identificator. Otherwise, the sent header will be ignored and the request ID will either be taken from Apache's mod_unique module or will be generated by Mediawiki itself (depending on the set-up). +* $wgEnableSpecialMute (T218265) - This configuration controls whether + Special:Mute is available and whether to include a link to it on emails + originating from Special:Email. ==== Changed configuration ==== * $wgUseCdn, $wgCdnServers, $wgCdnServersNoPurge, and $wgCdnMaxAge – These four @@ -57,7 +60,8 @@ For notes on 1.33.x and older releases, see HISTORY. wikidiff2.moved_paragraph_detection_cutoff. === New user-facing features in 1.34 === -* … +* Special:Mute has been added as a quick way for users to block unwanted emails + from other users originating from Special:EmailUser. === New developer features in 1.34 === * Language::formatTimePeriod now supports the new 'avoidhours' option to output @@ -70,7 +74,7 @@ For notes on 1.33.x and older releases, see HISTORY. ==== Changed external libraries ==== * Updated Mustache from 1.0.0 to v3.0.1. -* Updated OOUI from v0.31.3 to v0.32.1. +* Updated OOUI from v0.31.3 to v0.33.0. * Updated composer/semver from 1.4.2 to 1.5.0. * Updated composer/spdx-licenses from 1.4.0 to 1.5.1 (dev-only). * Updated mediawiki/codesniffer from 25.0.0 to 26.0.0 (dev-only). @@ -229,6 +233,25 @@ because of Phabricator reports. \Maintenance::countDown() method instead. * OutputPage::wrapWikiMsg() no longer accepts an options parameter. This was deprecated since 1.20. +* Skin::outputPage() no longer accepts a context. This was deprecated in 1.20. +* Linker::link() no longer accepts a string for the query array, as was + deprecated in 1.20. +* PrefixSearch::titleSearch(), deprecated in 1.23, has been removed. Use the + SearchEngine::defaultPrefixSearch or ::completionSearch() methods instead. +* The UserRights hook, deprecated in 1.26, has been removed. Instead, use the + UserGroupsChanged hook. +* Skin::getDefaultInstance(), deprecated in 1.27, has been removed. Get the + instance from MediaWikiServices instead. +* The UserLoadFromSession hook, deprecated in 1.27, has been removed. +* The wfResetSessionID global function, deprecated in 1.27, has been removed. + Use MediaWiki\Session\SessionManager instead. +* The wfGetLBFactory global function, deprecated in 1.27, has been removed. + Use MediaWikiServices::getInstance()->getDBLoadBalancerFactory(). +* The internal method OutputPage->addScriptFile() will no longer silently drop + calls that use an invalid path (i.e., something other than an absolute path, + protocol-relative URL, or full scheme URL), and will instead pass them to the + client where they will likely 404. This usage was deprecated in 1.24. +* Database::reportConnectionError, deprecated in 1.32, has been removed. * … === Deprecations in 1.34 === @@ -290,6 +313,22 @@ because of Phabricator reports. instead. * Sanitizer::attributeWhitelist() and Sanitizer::setupAttributeWhitelist() have been deprecated; they will be made private in the future. +* SearchResult::termMatches() method is deprecated. It was unreliable because + only populated by few search engine implementations. Use + SqlSearchResult::getTermMatches() if really needed. +* SearchResult::getTextSnippet( $terms ) the $terms param is being deprecated + and should no longer be passed. Search engine implemenations should be + responsible for carrying relevant information needed for highlighting with + their own SearchResultSet/SearchResult sub-classes. +* SearchEngine::$searchTerms protected field is deprecated. Moved to + SearchDatabase. +* The use of the $terms param in the ShowSearchHit and ShowSearchHitTitle + hooks is highly discouraged as it's only populated by SearchDatabase search + engines. +* Skin::escapeSearchLink() is deprecated. Use Skin::getSearchLink() or the skin + template option 'searchaction' instead. +* LoadBalancer::haveIndex() and LoadBalancer::isNonZeroLoad() have + been deprecated. === Other changes in 1.34 === * … diff --git a/autoload.php b/autoload.php index 698dbf2fbd..6457747c84 100644 --- a/autoload.php +++ b/autoload.php @@ -481,10 +481,13 @@ $wgAutoloadLocalClasses = [ 'ExtensionProcessor' => __DIR__ . '/includes/registration/ExtensionProcessor.php', 'ExtensionRegistry' => __DIR__ . '/includes/registration/ExtensionRegistry.php', 'ExternalStore' => __DIR__ . '/includes/externalstore/ExternalStore.php', + 'ExternalStoreAccess' => __DIR__ . '/includes/externalstore/ExternalStoreAccess.php', 'ExternalStoreDB' => __DIR__ . '/includes/externalstore/ExternalStoreDB.php', + 'ExternalStoreException' => __DIR__ . '/includes/externalstore/ExternalStoreException.php', 'ExternalStoreFactory' => __DIR__ . '/includes/externalstore/ExternalStoreFactory.php', 'ExternalStoreHttp' => __DIR__ . '/includes/externalstore/ExternalStoreHttp.php', 'ExternalStoreMedium' => __DIR__ . '/includes/externalstore/ExternalStoreMedium.php', + 'ExternalStoreMemory' => __DIR__ . '/includes/externalstore/ExternalStoreMemory.php', 'ExternalStoreMwstore' => __DIR__ . '/includes/externalstore/ExternalStoreMwstore.php', 'ExternalUserNames' => __DIR__ . '/includes/user/ExternalUserNames.php', 'FSFile' => __DIR__ . '/includes/libs/filebackend/fsfile/FSFile.php', @@ -1388,6 +1391,7 @@ $wgAutoloadLocalClasses = [ 'SpecialLockdb' => __DIR__ . '/includes/specials/SpecialLockdb.php', 'SpecialLog' => __DIR__ . '/includes/specials/SpecialLog.php', 'SpecialMergeHistory' => __DIR__ . '/includes/specials/SpecialMergeHistory.php', + 'SpecialMute' => __DIR__ . '/includes/specials/SpecialMute.php', 'SpecialMyLanguage' => __DIR__ . '/includes/specials/SpecialMyLanguage.php', 'SpecialMycontributions' => __DIR__ . '/includes/specials/redirects/SpecialMycontributions.php', 'SpecialMypage' => __DIR__ . '/includes/specials/redirects/SpecialMypage.php', @@ -1437,6 +1441,7 @@ $wgAutoloadLocalClasses = [ 'SpecialWatchlist' => __DIR__ . '/includes/specials/SpecialWatchlist.php', 'SpecialWhatLinksHere' => __DIR__ . '/includes/specials/SpecialWhatLinksHere.php', 'SqlBagOStuff' => __DIR__ . '/includes/objectcache/SqlBagOStuff.php', + 'SqlSearchResult' => __DIR__ . '/includes/search/SqlSearchResult.php', 'SqlSearchResultSet' => __DIR__ . '/includes/search/SqlSearchResultSet.php', 'Sqlite' => __DIR__ . '/maintenance/sqlite.inc', 'SqliteInstaller' => __DIR__ . '/includes/installer/SqliteInstaller.php', diff --git a/composer.json b/composer.json index 59873ef843..df516215e6 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "ext-xml": "*", "guzzlehttp/guzzle": "6.3.3", "liuggio/statsd-php-client": "1.0.18", - "oojs/oojs-ui": "0.32.1", + "oojs/oojs-ui": "0.33.0", "pear/mail": "1.4.1", "pear/mail_mime": "1.10.2", "pear/net_smtp": "1.8.1", @@ -116,7 +116,11 @@ "test": [ "composer lint", "composer phpcs" - ] + ], + "phpunit": "vendor/bin/phpunit", + "phpunit:unit": "vendor/bin/phpunit --colors=always --testsuite=unit", + "phpunit:integration": "vendor/bin/phpunit --colors=always --testsuite=integration", + "phpunit:coverage": "php -d zend_extensions=xdebug.so vendor/bin/phpunit --testsuite=unit --exclude-group Dump,Broken,ParserFuzz,Stub" }, "config": { "optimize-autoloader": true, diff --git a/docs/export-0.11.xsd b/docs/export-0.11.xsd new file mode 100644 index 0000000000..6dbc63b789 --- /dev/null +++ b/docs/export-0.11.xsd @@ -0,0 +1,335 @@ + + + + + + + MediaWiki's page export format + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/hooks.txt b/docs/hooks.txt index b275adc29c..47505607f6 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -2986,7 +2986,7 @@ $article: The article object corresponding to the page 'ShowSearchHit': Customize display of search hit. $searchPage: The SpecialSearch instance. $result: The SearchResult to show -$terms: Search terms, for highlighting +$terms: Search terms, for highlighting (unreliable as search engine dependent). &$link: HTML of link to the matching page. May be modified. &$redirect: HTML of redirect info. May be modified. &$section: HTML of matching section. May be modified. @@ -3744,14 +3744,6 @@ $name: user name $user: user object &$s: database query object -'UserLoadFromSession': DEPRECATED since 1.27! Create a -MediaWiki\Session\SessionProvider instead. -Called to authenticate users on external/environmental means; occurs before -session is loaded. -$user: user object being loaded -&$result: set this to a boolean value to abort the normal authentication - process - 'UserLoadOptions': When user options/preferences are being loaded from the database. $user: User object @@ -3832,12 +3824,6 @@ message(s). &$user: user retrieving new talks messages &$talks: array of new talks page(s) -'UserRights': DEPRECATED since 1.26! Use UserGroupsChanged instead. -After a user's group memberships are changed. -&$user: User object that was changed -$add: Array of strings corresponding to groups added -$remove: Array of strings corresponding to groups removed - 'UserSaveOptions': Called just before saving user preferences. Hook handlers can either add or manipulate options, or reset one back to it's default to block changing it. Hook handlers are also allowed to abort the process by returning @@ -3999,8 +3985,9 @@ $title: The title of the page. add extra metadata. &$obj: The XmlDumpWriter object. &$out: The text being output. -$row: The database row for the revision. -$text: The revision text. +$row: The database row for the revision being dumped. DEPRECATED, use $rev instead. +$text: The revision text to be dumped. DEPRECATED, use $rev instead. +$rev: The RevisionRecord that is being dumped to XML More hooks might be available but undocumented, you can execute "php maintenance/findHooks.php" to find hidden ones. diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 57e434102c..b893bc9e14 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -143,6 +143,7 @@ class AutoLoader { 'MediaWiki\\Sparql\\' => __DIR__ . '/sparql/', 'MediaWiki\\Storage\\' => __DIR__ . '/Storage/', 'MediaWiki\\Tidy\\' => __DIR__ . '/tidy/', + 'Wikimedia\\ParamValidator\\' => __DIR__ . '/libs/ParamValidator/', 'Wikimedia\\Services\\' => __DIR__ . '/libs/services/', ]; } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 2075432b2d..a32af365d0 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1689,6 +1689,16 @@ $wgEnableEmail = true; */ $wgEnableUserEmail = true; +/** + * Set to true to enable the Special Mute page. This allows users + * to mute unwanted communications from other users, and is linked + * to from emails originating from Special:Email. + * + * @since 1.34 + * @deprecated 1.34 + */ +$wgEnableSpecialMute = false; + /** * Set to true to enable user-to-user e-mail blacklist. * @@ -7415,7 +7425,7 @@ $wgSpecialPages = []; /** * Array mapping class names to filenames, for autoloading. */ -$wgAutoloadClasses = []; +$wgAutoloadClasses = $wgAutoloadClasses ?? []; /** * Switch controlling legacy case-insensitive classloading. diff --git a/includes/Defines.php b/includes/Defines.php index e5cd5ed64d..648e493b91 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -322,4 +322,5 @@ define( 'MIGRATION_NEW', 0x30000000 | SCHEMA_COMPAT_NEW ); * were already unsupported at the time these constants were introduced. */ define( 'XML_DUMP_SCHEMA_VERSION_10', '0.10' ); +define( 'XML_DUMP_SCHEMA_VERSION_11', '0.11' ); /**@}*/ diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index c3829be981..05c4655b87 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -30,7 +30,6 @@ use MediaWiki\MediaWikiServices; use MediaWiki\ProcOpenError; use MediaWiki\Session\SessionManager; use MediaWiki\Shell\Shell; -use Wikimedia\ScopedCallback; use Wikimedia\WrappedString; use Wikimedia\AtEase\AtEase; @@ -1882,10 +1881,9 @@ function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) { /** * Convenience function; returns MediaWiki timestamp for the present time. * - * @return string + * @return string TS_MW timestamp */ function wfTimestampNow() { - # return NOW return MWTimestamp::now( TS_MW ); } @@ -2431,28 +2429,6 @@ function wfRelativePath( $path, $from ) { return implode( DIRECTORY_SEPARATOR, $pieces ); } -/** - * Reset the session id - * - * @deprecated since 1.27, use MediaWiki\Session\SessionManager instead - * @since 1.22 - */ -function wfResetSessionID() { - wfDeprecated( __FUNCTION__, '1.27' ); - $session = SessionManager::getGlobalSession(); - $delay = $session->delaySave(); - - $session->resetId(); - - // Make sure a session is started, since that's what the old - // wfResetSessionID() did. - if ( session_id() !== $session->getId() ) { - wfSetupSession( $session->getId() ); - } - - ScopedCallback::consume( $delay ); -} - /** * Initialise php session * @@ -2601,19 +2577,6 @@ function wfGetLB( $wiki = false ) { } } -/** - * Get the load balancer factory object - * - * @deprecated since 1.27, use MediaWikiServices::getInstance()->getDBLoadBalancerFactory() instead. - * TODO: Remove in MediaWiki 1.35 - * - * @return \Wikimedia\Rdbms\LBFactory - */ -function wfGetLBFactory() { - wfDeprecated( __METHOD__, '1.27' ); - return MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); -} - /** * Find a file. * @deprecated since 1.34, use MediaWikiServices diff --git a/includes/Linker.php b/includes/Linker.php index 39f43946d2..01f695a31f 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -89,12 +89,6 @@ class Linker { return "$html"; } - if ( is_string( $query ) ) { - // some functions withing core using this still hand over query strings - wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' ); - $query = wfCgiToArray( $query ); - } - $services = MediaWikiServices::getInstance(); $options = (array)$options; if ( $options ) { @@ -694,8 +688,8 @@ class Linker { $label = $title->getPrefixedText(); } $encLabel = htmlspecialchars( $label ); - $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title ); - $currentExists = $time ? ( $file != false ) : false; + $currentExists = $time + && MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title ) !== false; if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads ) && !$currentExists diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index 689477b545..a37e32e675 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -571,6 +571,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'EventRelayerGroup' ); } + /** + * @since 1.34 + * @return \ExternalStoreAccess + */ + public function getExternalStoreAccess() { + return $this->getService( 'ExternalStoreAccess' ); + } + /** * @since 1.31 * @return \ExternalStoreFactory diff --git a/includes/OutputPage.php b/includes/OutputPage.php index b8cbff1da2..28e0a31352 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -29,9 +29,9 @@ use Wikimedia\WrappedString; use Wikimedia\WrappedStringList; /** - * This class should be covered by a general architecture document which does - * not exist as of January 2011. This is one of the Core classes and should - * be read at least once by any new developers. + * This is one of the Core classes and should + * be read at least once by any new developers. Also documented at + * https://www.mediawiki.org/wiki/Manual:Architectural_modules/OutputPage * * This class is used to prepare the final rendering. A skin is then * applied to the output parameters (links, javascript, html, categories ...). @@ -459,13 +459,6 @@ class OutputPage extends ContextSource { * @param string|null $unused Previously used to change the cache-busting query parameter */ public function addScriptFile( $file, $unused = null ) { - if ( substr( $file, 0, 1 ) !== '/' && !preg_match( '#^[a-z]*://#i', $file ) ) { - // This is not an absolute path, protocol-relative url, or full scheme url, - // presumed to be an old call intended to include a file from /w/skins/common, - // which doesn't exist anymore as of MediaWiki 1.24 per T71277. Ignore. - wfDeprecated( __METHOD__, '1.24' ); - return; - } $this->addScript( Html::linkedScript( $file, $this->getCSPNonce() ) ); } @@ -2278,7 +2271,7 @@ class OutputPage extends ContextSource { } /** - * T23672: Add Accept-Language to Vary and Key headers if there's no 'variant' parameter in GET. + * T23672: Add Accept-Language to Vary header if there's no 'variant' parameter in GET. * * For example: * /w/index.php?title=Main_page will vary based on Accept-Language; but @@ -4040,7 +4033,6 @@ class OutputPage extends ContextSource { $this->addModuleStyles( [ 'oojs-ui-core.styles', 'oojs-ui.styles.indicators', - 'oojs-ui.styles.textures', 'mediawiki.widgets.styles', 'oojs-ui-core.icons', ] ); diff --git a/includes/Permissions/PermissionManager.php b/includes/Permissions/PermissionManager.php index 202014f072..defcb656de 100644 --- a/includes/Permissions/PermissionManager.php +++ b/includes/Permissions/PermissionManager.php @@ -21,12 +21,12 @@ namespace MediaWiki\Permissions; use Action; use Exception; -use FatalError; use Hooks; use MediaWiki\Linker\LinkTarget; +use MediaWiki\Session\SessionManager; use MediaWiki\Special\SpecialPageFactory; +use MediaWiki\User\UserIdentity; use MessageSpecifier; -use MWException; use NamespaceInfo; use RequestContext; use SpecialPage; @@ -69,12 +69,121 @@ class PermissionManager { /** @var NamespaceInfo */ private $nsInfo; + /** @var string[][] Access rights for groups and users in these groups */ + private $groupPermissions; + + /** @var string[][] Permission keys revoked from users in each group */ + private $revokePermissions; + + /** @var string[] A list of available rights, in addition to the ones defined by the core */ + private $availableRights; + + /** @var string[] Cached results of getAllRights() */ + private $allRights = false; + + /** @var string[][] Cached user rights */ + private $usersRights = null; + + /** @var string[] Cached rights for isEveryoneAllowed */ + private $cachedRights = []; + + /** + * Array of Strings Core rights. + * Each of these should have a corresponding message of the form + * "right-$right". + * @showinitializer + */ + private $coreRights = [ + 'apihighlimits', + 'applychangetags', + 'autoconfirmed', + 'autocreateaccount', + 'autopatrol', + 'bigdelete', + 'block', + 'blockemail', + 'bot', + 'browsearchive', + 'changetags', + 'createaccount', + 'createpage', + 'createtalk', + 'delete', + 'deletechangetags', + 'deletedhistory', + 'deletedtext', + 'deletelogentry', + 'deleterevision', + 'edit', + 'editcontentmodel', + 'editinterface', + 'editprotected', + 'editmyoptions', + 'editmyprivateinfo', + 'editmyusercss', + 'editmyuserjson', + 'editmyuserjs', + 'editmywatchlist', + 'editsemiprotected', + 'editsitecss', + 'editsitejson', + 'editsitejs', + 'editusercss', + 'edituserjson', + 'edituserjs', + 'hideuser', + 'import', + 'importupload', + 'ipblock-exempt', + 'managechangetags', + 'markbotedits', + 'mergehistory', + 'minoredit', + 'move', + 'movefile', + 'move-categorypages', + 'move-rootuserpages', + 'move-subpages', + 'nominornewtalk', + 'noratelimit', + 'override-export-depth', + 'pagelang', + 'patrol', + 'patrolmarks', + 'protect', + 'purge', + 'read', + 'reupload', + 'reupload-own', + 'reupload-shared', + 'rollback', + 'sendemail', + 'siteadmin', + 'suppressionlog', + 'suppressredirect', + 'suppressrevision', + 'unblockself', + 'undelete', + 'unwatchedpages', + 'upload', + 'upload_by_url', + 'userrights', + 'userrights-interwiki', + 'viewmyprivateinfo', + 'viewmywatchlist', + 'viewsuppressed', + 'writeapi', + ]; + /** * @param SpecialPageFactory $specialPageFactory * @param string[] $whitelistRead * @param string[] $whitelistReadRegexp * @param bool $emailConfirmToEdit * @param bool $blockDisablesLogin + * @param string[][] $groupPermissions + * @param string[][] $revokePermissions + * @param string[] $availableRights * @param NamespaceInfo $nsInfo */ public function __construct( @@ -83,6 +192,9 @@ class PermissionManager { $whitelistReadRegexp, $emailConfirmToEdit, $blockDisablesLogin, + $groupPermissions, + $revokePermissions, + $availableRights, NamespaceInfo $nsInfo ) { $this->specialPageFactory = $specialPageFactory; @@ -90,6 +202,9 @@ class PermissionManager { $this->whitelistReadRegexp = $whitelistReadRegexp; $this->emailConfirmToEdit = $emailConfirmToEdit; $this->blockDisablesLogin = $blockDisablesLogin; + $this->groupPermissions = $groupPermissions; + $this->revokePermissions = $revokePermissions; + $this->availableRights = $availableRights; $this->nsInfo = $nsInfo; } @@ -111,7 +226,6 @@ class PermissionManager { * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed * * @return bool - * @throws Exception */ public function userCan( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE ) { return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) ); @@ -133,7 +247,6 @@ class PermissionManager { * whose corresponding errors may be ignored. * * @return array Array of arrays of the arguments to wfMessage to explain permissions problems. - * @throws Exception */ public function getPermissionErrors( $action, @@ -167,8 +280,6 @@ class PermissionManager { * @param bool $fromReplica Whether to check the replica DB instead of the master * * @return bool - * @throws FatalError - * @throws MWException */ public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) { $blocked = $user->isHidden(); @@ -286,8 +397,6 @@ class PermissionManager { * @param LinkTarget $page * * @return array List of errors - * @throws FatalError - * @throws MWException */ private function checkPermissionHooks( $action, @@ -363,8 +472,6 @@ class PermissionManager { * @param LinkTarget $page * * @return array List of errors - * @throws FatalError - * @throws MWException */ private function checkReadPermissions( $action, @@ -497,7 +604,6 @@ class PermissionManager { * @param LinkTarget $page * * @return array List of errors - * @throws MWException */ private function checkUserBlock( $action, @@ -583,8 +689,6 @@ class PermissionManager { * @param LinkTarget $page * * @return array List of errors - * @throws FatalError - * @throws MWException */ private function checkQuickPermissions( $action, @@ -762,6 +866,7 @@ class PermissionManager { } if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) { $wikiPages = ''; + /** @var Title $wikiPage */ foreach ( $cascadingSources as $wikiPage ) { $wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n"; } @@ -789,7 +894,6 @@ class PermissionManager { * @param LinkTarget $page * * @return array List of errors - * @throws Exception */ private function checkActionPermissions( $action, @@ -1052,4 +1156,256 @@ class PermissionManager { return $errors; } + /** + * Testing a permission + * + * @since 1.34 + * + * @param UserIdentity $user + * @param string $action + * + * @return bool + */ + public function userHasRight( UserIdentity $user, $action = '' ) { + if ( $action === '' ) { + return true; // In the spirit of DWIM + } + // Use strict parameter to avoid matching numeric 0 accidentally inserted + // by misconfiguration: 0 == 'foo' + return in_array( $action, $this->getUserPermissions( $user ), true ); + } + + /** + * Get the permissions this user has. + * + * @since 1.34 + * + * @param UserIdentity $user + * + * @return string[] permission names + */ + public function getUserPermissions( UserIdentity $user ) { + $user = User::newFromIdentity( $user ); + if ( !isset( $this->usersRights[ $user->getId() ] ) ) { + $this->usersRights[ $user->getId() ] = $this->getGroupPermissions( + $user->getEffectiveGroups() + ); + Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $user->getId() ] ] ); + + // Deny any rights denied by the user's session, unless this + // endpoint has no sessions. + if ( !defined( 'MW_NO_SESSION' ) ) { + // FIXME: $user->getRequest().. need to be replaced with something else + $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights(); + if ( $allowedRights !== null ) { + $this->usersRights[ $user->getId() ] = array_intersect( + $this->usersRights[ $user->getId() ], + $allowedRights + ); + } + } + + Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $user->getId() ] ] ); + // Force reindexation of rights when a hook has unset one of them + $this->usersRights[ $user->getId() ] = array_values( + array_unique( $this->usersRights[ $user->getId() ] ) + ); + + if ( + $user->isLoggedIn() && + $this->blockDisablesLogin && + $user->getBlock() + ) { + $anon = new User; + $this->usersRights[ $user->getId() ] = array_intersect( + $this->usersRights[ $user->getId() ], + $this->getUserPermissions( $anon ) + ); + } + } + return $this->usersRights[ $user->getId() ]; + } + + /** + * Clears users permissions cache, if specific user is provided it tries to clear + * permissions cache only for provided user. + * + * @since 1.34 + * + * @param User|null $user + */ + public function invalidateUsersRightsCache( $user = null ) { + if ( $user !== null ) { + if ( isset( $this->usersRights[ $user->getId() ] ) ) { + unset( $this->usersRights[$user->getId()] ); + } + } else { + $this->usersRights = null; + } + } + + /** + * Check, if the given group has the given permission + * + * If you're wanting to check whether all users have a permission, use + * PermissionManager::isEveryoneAllowed() instead. That properly checks if it's revoked + * from anyone. + * + * @since 1.34 + * + * @param string $group Group to check + * @param string $role Role to check + * + * @return bool + */ + public function groupHasPermission( $group, $role ) { + return isset( $this->groupPermissions[$group][$role] ) && + $this->groupPermissions[$group][$role] && + !( isset( $this->revokePermissions[$group][$role] ) && + $this->revokePermissions[$group][$role] ); + } + + /** + * Get the permissions associated with a given list of groups + * + * @since 1.34 + * + * @param array $groups Array of Strings List of internal group names + * @return array Array of Strings List of permission key names for given groups combined + */ + public function getGroupPermissions( $groups ) { + $rights = []; + // grant every granted permission first + foreach ( $groups as $group ) { + if ( isset( $this->groupPermissions[$group] ) ) { + $rights = array_merge( $rights, + // array_filter removes empty items + array_keys( array_filter( $this->groupPermissions[$group] ) ) ); + } + } + // now revoke the revoked permissions + foreach ( $groups as $group ) { + if ( isset( $this->revokePermissions[$group] ) ) { + $rights = array_diff( $rights, + array_keys( array_filter( $this->revokePermissions[$group] ) ) ); + } + } + return array_unique( $rights ); + } + + /** + * Get all the groups who have a given permission + * + * @since 1.34 + * + * @param string $role Role to check + * @return array Array of Strings List of internal group names with the given permission + */ + public function getGroupsWithPermission( $role ) { + $allowedGroups = []; + foreach ( array_keys( $this->groupPermissions ) as $group ) { + if ( $this->groupHasPermission( $group, $role ) ) { + $allowedGroups[] = $group; + } + } + return $allowedGroups; + } + + /** + * Check if all users may be assumed to have the given permission + * + * We generally assume so if the right is granted to '*' and isn't revoked + * on any group. It doesn't attempt to take grants or other extension + * limitations on rights into account in the general case, though, as that + * would require it to always return false and defeat the purpose. + * Specifically, session-based rights restrictions (such as OAuth or bot + * passwords) are applied based on the current session. + * + * @param string $right Right to check + * + * @return bool + * @since 1.34 + */ + public function isEveryoneAllowed( $right ) { + // Use the cached results, except in unit tests which rely on + // being able change the permission mid-request + if ( isset( $this->cachedRights[$right] ) ) { + return $this->cachedRights[$right]; + } + + if ( !isset( $this->groupPermissions['*'][$right] ) + || !$this->groupPermissions['*'][$right] ) { + $this->cachedRights[$right] = false; + return false; + } + + // If it's revoked anywhere, then everyone doesn't have it + foreach ( $this->revokePermissions as $rights ) { + if ( isset( $rights[$right] ) && $rights[$right] ) { + $this->cachedRights[$right] = false; + return false; + } + } + + // Remove any rights that aren't allowed to the global-session user, + // unless there are no sessions for this endpoint. + if ( !defined( 'MW_NO_SESSION' ) ) { + + // XXX: think what could be done with the below + $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights(); + if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) { + $this->cachedRights[$right] = false; + return false; + } + } + + // Allow extensions to say false + if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) { + $this->cachedRights[$right] = false; + return false; + } + + $this->cachedRights[$right] = true; + return true; + } + + /** + * Get a list of all available permissions. + * + * @since 1.34 + * + * @return string[] Array of permission names + */ + public function getAllPermissions() { + if ( $this->allRights === false ) { + if ( count( $this->availableRights ) ) { + $this->allRights = array_unique( array_merge( + $this->coreRights, + $this->availableRights + ) ); + } else { + $this->allRights = $this->coreRights; + } + Hooks::run( 'UserGetAllRights', [ &$this->allRights ] ); + } + return $this->allRights; + } + + /** + * Overrides user permissions cache + * + * @since 1.34 + * + * @param User $user + * @param string[]|string $rights + * + * @throws Exception + */ + public function overrideUserRightsForTesting( $user, $rights = [] ) { + if ( !defined( 'MW_PHPUNIT_TEST' ) ) { + throw new Exception( __METHOD__ . ' can not be called outside of tests' ); + } + $this->usersRights[ $user->getId() ] = is_array( $rights ) ? $rights : [ $rights ]; + } + } diff --git a/includes/Rest/Handler.php b/includes/Rest/Handler.php index 472e1cc367..cee403fa2f 100644 --- a/includes/Rest/Handler.php +++ b/includes/Rest/Handler.php @@ -3,6 +3,9 @@ namespace MediaWiki\Rest; abstract class Handler { + /** @var Router */ + private $router; + /** @var RequestInterface */ private $request; @@ -14,15 +17,25 @@ abstract class Handler { /** * Initialise with dependencies from the Router. This is called after construction. + * @internal */ - public function init( RequestInterface $request, array $config, + public function init( Router $router, RequestInterface $request, array $config, ResponseFactory $responseFactory ) { + $this->router = $router; $this->request = $request; $this->config = $config; $this->responseFactory = $responseFactory; } + /** + * Get the Router. The return type declaration causes it to raise + * a fatal error if init() has not yet been called. + */ + protected function getRouter(): Router { + return $this->router; + } + /** * Get the current request. The return type declaration causes it to raise * a fatal error if init() has not yet been called. diff --git a/includes/Rest/ResponseFactory.php b/includes/Rest/ResponseFactory.php index 7ccb612748..d18cdb5d6b 100644 --- a/includes/Rest/ResponseFactory.php +++ b/includes/Rest/ResponseFactory.php @@ -78,7 +78,7 @@ class ResponseFactory { * the new URL in the future. 301 redirects tend to get cached and are hard to undo. * Client behavior for methods other than GET/HEAD is not well-defined and this type * of response should be avoided in such cases. - * @param string $target Redirect URL (can be relative) + * @param string $target Redirect target (an absolute URL) * @return Response */ public function createPermanentRedirect( $target ) { @@ -87,12 +87,28 @@ class ResponseFactory { return $response; } + /** + * Creates a temporary (302) redirect. + * HTTP 302 was underspecified and has been superseded by 303 (when the redirected request + * should be a GET, regardless of what the current request is) and 307 (when the method should + * not be changed), but might still be needed for HTTP 1.0 clients or to match legacy behavior. + * @param string $target Redirect target (an absolute URL) + * @return Response + * @see self::createTemporaryRedirect() + * @see self::createSeeOther() + */ + public function createLegacyTemporaryRedirect( $target ) { + $response = $this->createRedirectBase( $target ); + $response->setStatus( 302 ); + return $response; + } + /** * Creates a temporary (307) redirect. * This indicates that the operation the client was trying to perform can temporarily * be achieved by using a different URL. Clients will preserve the request method when * retrying the request with the new URL. - * @param string $target Redirect URL (can be relative) + * @param string $target Redirect target (an absolute URL) * @return Response */ public function createTemporaryRedirect( $target ) { @@ -106,7 +122,7 @@ class ResponseFactory { * This indicates that the target resource might be of interest to the client, without * necessarily implying that it is the same resource. The client will always use GET * (or HEAD) when following the redirection. Useful for GET-after-POST. - * @param string $target Redirect URL (can be relative) + * @param string $target Redirect target (an absolute URL) * @return Response */ public function createSeeOther( $target ) { diff --git a/includes/Rest/Router.php b/includes/Rest/Router.php index 5dccb4f7b1..5ba3d08c5c 100644 --- a/includes/Rest/Router.php +++ b/includes/Rest/Router.php @@ -237,8 +237,9 @@ class Router { $spec = $match['userData']; $objectFactorySpec = array_intersect_key( $spec, [ 'factory' => true, 'class' => true, 'args' => true ] ); + /** @var $handler Handler (annotation for PHPStorm) */ $handler = ObjectFactory::getObjectFromSpec( $objectFactorySpec ); - $handler->init( $request, $spec, $this->responseFactory ); + $handler->init( $this, $request, $spec, $this->responseFactory ); try { return $this->executeHandler( $handler ); diff --git a/includes/Revision/MutableRevisionRecord.php b/includes/Revision/MutableRevisionRecord.php index f287c05db7..e9136cbb5d 100644 --- a/includes/Revision/MutableRevisionRecord.php +++ b/includes/Revision/MutableRevisionRecord.php @@ -70,15 +70,14 @@ class MutableRevisionRecord extends RevisionRecord { * in RevisionStore instead. * * @param Title $title The title of the page this Revision is associated with. - * @param bool|string $wikiId the wiki ID of the site this Revision belongs to, - * or false for the local site. + * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one. * * @throws MWException */ - function __construct( Title $title, $wikiId = false ) { + function __construct( Title $title, $dbDomain = false ) { $slots = new MutableRevisionSlots(); - parent::__construct( $title, $slots, $wikiId ); + parent::__construct( $title, $slots, $dbDomain ); $this->mSlots = $slots; // redundant, but nice for static analysis } diff --git a/includes/Revision/RenderedRevision.php b/includes/Revision/RenderedRevision.php index c4a00545c4..4acb9c0a64 100644 --- a/includes/Revision/RenderedRevision.php +++ b/includes/Revision/RenderedRevision.php @@ -291,19 +291,28 @@ class RenderedRevision implements SlotRenderingProvider { $this->setRevisionInternal( $rev ); - $this->pruneRevisionSensitiveOutput( $this->revision->getId() ); + $this->pruneRevisionSensitiveOutput( + $this->revision->getId(), + $this->revision->getTimestamp() + ); } /** * Prune any output that depends on the revision ID. * - * @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID + * @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID * against, or false to not purge on vary-revision-id, or true to purge on * vary-revision-id unconditionally. + * @param string|bool $actualRevTimestamp The actual rev timestamp, to check against the + * parser output revision timestamp, or false to not purge on vary-revision-timestamp */ - private function pruneRevisionSensitiveOutput( $actualRevId ) { + private function pruneRevisionSensitiveOutput( $actualRevId, $actualRevTimestamp ) { if ( $this->revisionOutput ) { - if ( $this->outputVariesOnRevisionMetaData( $this->revisionOutput, $actualRevId ) ) { + if ( $this->outputVariesOnRevisionMetaData( + $this->revisionOutput, + $actualRevId, + $actualRevTimestamp + ) ) { $this->revisionOutput = null; } } else { @@ -311,7 +320,11 @@ class RenderedRevision implements SlotRenderingProvider { } foreach ( $this->slotsOutput as $role => $output ) { - if ( $this->outputVariesOnRevisionMetaData( $output, $actualRevId ) ) { + if ( $this->outputVariesOnRevisionMetaData( + $output, + $actualRevId, + $actualRevTimestamp + ) ) { unset( $this->slotsOutput[$role] ); } } @@ -372,19 +385,24 @@ class RenderedRevision implements SlotRenderingProvider { /** * @param ParserOutput $out * @param int|bool $actualRevId The actual rev id, to check the used speculative rev ID - * against, or false to not purge on vary-revision-id, or true to purge on + * against, false to not purge on vary-revision-id, or true to purge on * vary-revision-id unconditionally. + * @param string|bool $actualRevTimestamp The actual rev timestamp, to check against the + * parser output revision timestamp, false to not purge on vary-revision-timestamp, + * or true to purge on vary-revision-timestamp unconditionally. * @return bool */ - private function outputVariesOnRevisionMetaData( ParserOutput $out, $actualRevId ) { + private function outputVariesOnRevisionMetaData( + ParserOutput $out, + $actualRevId, + $actualRevTimestamp + ) { $method = __METHOD__; if ( $out->getFlag( 'vary-revision' ) ) { - // If {{PAGEID}} resolved to 0 or {{REVISIONTIMESTAMP}} used the current - // timestamp rather than that of an actual revision, then those words need - // to resolve to the actual page ID or revision timestamp, respectively. + // If {{PAGEID}} resolved to 0, then that word need to resolve to the actual page ID $this->saveParseLogger->info( - "$method: Prepared output has vary-revision...\n" + "$method: Prepared output has vary-revision..." ); return true; } elseif ( $out->getFlag( 'vary-revision-id' ) @@ -392,7 +410,16 @@ class RenderedRevision implements SlotRenderingProvider { && ( $actualRevId === true || $out->getSpeculativeRevIdUsed() !== $actualRevId ) ) { $this->saveParseLogger->info( - "$method: Prepared output has vary-revision-id with wrong ID...\n" + "$method: Prepared output has vary-revision-id with wrong ID..." + ); + return true; + } elseif ( $out->getFlag( 'vary-revision-timestamp' ) + && $actualRevTimestamp !== false + && ( $actualRevTimestamp === true || + $out->getRevisionTimestampUsed() !== $actualRevTimestamp ) + ) { + $this->saveParseLogger->info( + "$method: Prepared output has vary-revision-timestamp with wrong timestamp..." ); return true; } elseif ( $out->getFlag( 'vary-revision-exists' ) ) { @@ -400,7 +427,7 @@ class RenderedRevision implements SlotRenderingProvider { // Note that edit stashing always uses '-', which can be used for both // edit filter checks and canonical parser cache. $this->saveParseLogger->info( - "$method: Prepared output has vary-revision-exists...\n" + "$method: Prepared output has vary-revision-exists..." ); return true; } else { @@ -412,7 +439,7 @@ class RenderedRevision implements SlotRenderingProvider { // constructs the ParserOptions: For a null-edit, setCurrentRevisionCallback is called // with the old, existing revision. - wfDebug( "$method: Keeping prepared output...\n" ); + $this->saveParseLogger->debug( "$method: Keeping prepared output..." ); return false; } } diff --git a/includes/Revision/RevisionArchiveRecord.php b/includes/Revision/RevisionArchiveRecord.php index 67dc9b26d9..6e8db7fa87 100644 --- a/includes/Revision/RevisionArchiveRecord.php +++ b/includes/Revision/RevisionArchiveRecord.php @@ -54,8 +54,7 @@ class RevisionArchiveRecord extends RevisionRecord { * @param object $row An archive table row. Use RevisionStore::getArchiveQueryInfo() to build * a query that yields the required fields. * @param RevisionSlots $slots The slots of this revision. - * @param bool|string $wikiId the wiki ID of the site this Revision belongs to, - * or false for the local site. + * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one. */ function __construct( Title $title, @@ -63,9 +62,9 @@ class RevisionArchiveRecord extends RevisionRecord { CommentStoreComment $comment, $row, RevisionSlots $slots, - $wikiId = false + $dbDomain = false ) { - parent::__construct( $title, $slots, $wikiId ); + parent::__construct( $title, $slots, $dbDomain ); Assert::parameterType( 'object', $row, '$row' ); $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp ); diff --git a/includes/Revision/RevisionRecord.php b/includes/Revision/RevisionRecord.php index 70a891cfed..0dcc35c2e5 100644 --- a/includes/Revision/RevisionRecord.php +++ b/includes/Revision/RevisionRecord.php @@ -94,17 +94,16 @@ abstract class RevisionRecord { * * @param Title $title The title of the page this Revision is associated with. * @param RevisionSlots $slots The slots of this revision. - * @param bool|string $wikiId the wiki ID of the site this Revision belongs to, - * or false for the local site. + * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one. * * @throws MWException */ - function __construct( Title $title, RevisionSlots $slots, $wikiId = false ) { - Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' ); + function __construct( Title $title, RevisionSlots $slots, $dbDomain = false ) { + Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' ); $this->mTitle = $title; $this->mSlots = $slots; - $this->mWiki = $wikiId; + $this->mWiki = $dbDomain; // XXX: this is a sensible default, but we may not have a Title object here in the future. $this->mPageId = $title->getArticleID(); @@ -515,10 +514,19 @@ abstract class RevisionRecord { } else { $permissions = [ 'deletedhistory' ]; } + + // XXX: How can we avoid global scope here? + // Perhaps the audience check should be done in a callback. + $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); $permissionlist = implode( ', ', $permissions ); if ( $title === null ) { wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" ); - return $user->isAllowedAny( ...$permissions ); + foreach ( $permissions as $perm ) { + if ( $permissionManager->userHasRight( $user, $perm ) ) { + return true; + } + } + return false; } else { $text = $title->getPrefixedText(); wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" ); diff --git a/includes/Revision/RevisionRenderer.php b/includes/Revision/RevisionRenderer.php index f97390ad49..99150c1368 100644 --- a/includes/Revision/RevisionRenderer.php +++ b/includes/Revision/RevisionRenderer.php @@ -54,22 +54,21 @@ class RevisionRenderer { private $roleRegistery; /** @var string|bool */ - private $wikiId; + private $dbDomain; /** * @param ILoadBalancer $loadBalancer * @param SlotRoleRegistry $roleRegistry - * @param bool|string $wikiId + * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one */ public function __construct( ILoadBalancer $loadBalancer, SlotRoleRegistry $roleRegistry, - $wikiId = false + $dbDomain = false ) { $this->loadBalancer = $loadBalancer; $this->roleRegistery = $roleRegistry; - $this->wikiId = $wikiId; - + $this->dbDomain = $dbDomain; $this->saveParseLogger = new NullLogger(); } @@ -105,7 +104,7 @@ class RevisionRenderer { User $forUser = null, array $hints = [] ) { - if ( $rev->getWikiId() !== $this->wikiId ) { + if ( $rev->getWikiId() !== $this->dbDomain ) { throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() ); } @@ -132,6 +131,13 @@ class RevisionRenderer { return $this->getSpeculativeRevId( $dbIndex ); } ); + if ( !$rev->getId() && $rev->getTimestamp() ) { + // This is an unsaved revision with an already determined timestamp. + // Make the "current" time used during parsing match that of the revision. + // Any REVISION* parser variables will match up if the revision is saved. + $options->setTimestamp( $rev->getTimestamp() ); + } + $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() ); $renderedRevision = new RenderedRevision( @@ -162,7 +168,7 @@ class RevisionRenderer { $flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA ? 0 : ILoadBalancer::CONN_TRX_AUTOCOMMIT; - $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->wikiId, $flags ); + $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags ); return 1 + (int)$db->selectField( 'revision', @@ -209,7 +215,7 @@ class RevisionRenderer { $slotOutput[$role] = $out; // XXX: should the SlotRoleHandler be able to intervene here? - $combinedOutput->mergeInternalMetaDataFrom( $out, $role ); + $combinedOutput->mergeInternalMetaDataFrom( $out ); $combinedOutput->mergeTrackingMetaDataFrom( $out ); } diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php index faa162a1d6..f269afe639 100644 --- a/includes/Revision/RevisionStore.php +++ b/includes/Revision/RevisionStore.php @@ -63,6 +63,7 @@ use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\DBConnRef; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\ILoadBalancer; +use Wikimedia\Rdbms\ResultWrapper; /** * Service for looking up page revisions. @@ -86,7 +87,7 @@ class RevisionStore /** * @var bool|string */ - private $wikiId; + private $dbDomain; /** * @var boolean @@ -141,7 +142,7 @@ class RevisionStore * @param ILoadBalancer $loadBalancer * @param SqlBlobStore $blobStore * @param WANObjectCache $cache A cache for caching revision rows. This can be the local - * wiki's default instance even if $wikiId refers to a different wiki, since + * wiki's default instance even if $dbDomain refers to a different wiki, since * makeGlobalKey() is used to constructed a key that allows cached revision rows from * the same database to be re-used between wikis. For example, enwiki and frwiki will * use the same cache keys for revision rows from the wikidatawiki database, regardless @@ -152,8 +153,7 @@ class RevisionStore * @param SlotRoleRegistry $slotRoleRegistry * @param int $mcrMigrationStage An appropriate combination of SCHEMA_COMPAT_XXX flags * @param ActorMigration $actorMigration - * @param bool|string $wikiId - * + * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one */ public function __construct( ILoadBalancer $loadBalancer, @@ -165,9 +165,9 @@ class RevisionStore SlotRoleRegistry $slotRoleRegistry, $mcrMigrationStage, ActorMigration $actorMigration, - $wikiId = false + $dbDomain = false ) { - Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' ); + Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' ); Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' ); Assert::parameter( ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== SCHEMA_COMPAT_READ_BOTH, @@ -206,7 +206,7 @@ class RevisionStore $this->slotRoleRegistry = $slotRoleRegistry; $this->mcrMigrationStage = $mcrMigrationStage; $this->actorMigration = $actorMigration; - $this->wikiId = $wikiId; + $this->dbDomain = $dbDomain; $this->logger = new NullLogger(); } @@ -226,7 +226,7 @@ class RevisionStore * @throws RevisionAccessException */ private function assertCrossWikiContentLoadingIsSafe() { - if ( $this->wikiId !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) { + if ( $this->dbDomain !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) { throw new RevisionAccessException( "Cross-wiki content loading is not supported by the pre-MCR schema" ); @@ -284,7 +284,7 @@ class RevisionStore */ private function getDBConnection( $mode, $groups = [] ) { $lb = $this->getDBLoadBalancer(); - return $lb->getConnection( $mode, $groups, $this->wikiId ); + return $lb->getConnection( $mode, $groups, $this->dbDomain ); } /** @@ -312,7 +312,7 @@ class RevisionStore */ private function getDBConnectionRef( $mode ) { $lb = $this->getDBLoadBalancer(); - return $lb->getConnectionRef( $mode, [], $this->wikiId ); + return $lb->getConnectionRef( $mode, [], $this->dbDomain ); } /** @@ -340,7 +340,7 @@ class RevisionStore $queryFlags = self::READ_NORMAL; } - $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false ); + $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false ); list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags ); $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 ); @@ -630,7 +630,7 @@ class RevisionStore $comment, (object)$revisionRow, new RevisionSlots( $newSlots ), - $this->wikiId + $this->dbDomain ); return $rev; @@ -812,9 +812,11 @@ class RevisionStore throw new MWException( 'Failed to get database lock for T202032' ); } $fname = __METHOD__; - $dbw->onTransactionResolution( function ( $trigger, $dbw ) use ( $fname ) { - $dbw->unlock( 'fix-for-T202032', $fname ); - } ); + $dbw->onTransactionResolution( + function ( $trigger, IDatabase $dbw ) use ( $fname ) { + $dbw->unlock( 'fix-for-T202032', $fname ); + } + ); $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ ); @@ -1606,10 +1608,11 @@ class RevisionStore /** * @param int $revId The revision to load slots for. * @param int $queryFlags + * @param Title $title * * @return SlotRecord[] */ - private function loadSlotRecords( $revId, $queryFlags ) { + private function loadSlotRecords( $revId, $queryFlags, Title $title ) { $revQuery = self::getSlotsQueryInfo( [ 'content' ] ); list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags ); @@ -1626,12 +1629,45 @@ class RevisionStore $revQuery['joins'] ); + $slots = $this->constructSlotRecords( $revId, $res, $queryFlags, $title ); + + return $slots; + } + + /** + * Factory method for SlotRecords based on known slot rows. + * + * @param int $revId The revision to load slots for. + * @param object[]|ResultWrapper $slotRows + * @param int $queryFlags + * @param Title $title + * + * @return SlotRecord[] + */ + private function constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title ) { $slots = []; - foreach ( $res as $row ) { - // resolve role names and model names from in-memory cache, instead of joining. - $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id ); - $row->model_name = $this->contentModelStore->getName( (int)$row->content_model ); + foreach ( $slotRows as $row ) { + // Resolve role names and model names from in-memory cache, if they were not joined in. + if ( !isset( $row->role_name ) ) { + $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id ); + } + + if ( !isset( $row->model_name ) ) { + if ( isset( $row->content_model ) ) { + $row->model_name = $this->contentModelStore->getName( (int)$row->content_model ); + } else { + // We may get here if $row->model_name is set but null, perhaps because it + // came from rev_content_model, which is NULL for the default model. + $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name ); + $row->model_name = $slotRoleHandler->getDefaultModel( $title ); + } + } + + if ( !isset( $row->content_id ) && isset( $row->rev_text_id ) ) { + $row->slot_content_id + = $this->emulateContentId( intval( $row->rev_text_id ) ); + } $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags ) { return $this->loadSlotContent( $slot, null, null, null, $queryFlags ); @@ -1650,13 +1686,14 @@ class RevisionStore } /** - * Factory method for RevisionSlots. + * Factory method for RevisionSlots based on a revision ID. * * @note If other code has a need to construct RevisionSlots objects, this should be made * public, since RevisionSlots instances should not be constructed directly. * * @param int $revId * @param object $revisionRow + * @param object[]|null $slotRows * @param int $queryFlags * @param Title $title * @@ -1666,10 +1703,15 @@ class RevisionStore private function newRevisionSlots( $revId, $revisionRow, + $slotRows, $queryFlags, Title $title ) { - if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) { + if ( $slotRows ) { + $slots = new RevisionSlots( + $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $title ) + ); + } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) { $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title ); // @phan-suppress-next-line PhanTypeInvalidCallableArraySize false positive $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] ); @@ -1677,8 +1719,8 @@ class RevisionStore // XXX: do we need the same kind of caching here // that getKnownCurrentRevision uses (if $revId == page_latest?) - $slots = new RevisionSlots( function () use( $revId, $queryFlags ) { - return $this->loadSlotRecords( $revId, $queryFlags ); + $slots = new RevisionSlots( function () use( $revId, $queryFlags, $title ) { + return $this->loadSlotRecords( $revId, $queryFlags, $title ); } ); } @@ -1741,7 +1783,7 @@ class RevisionStore $row->ar_user ?? null, $row->ar_user_text ?? null, $row->ar_actor ?? null, - $this->wikiId + $this->dbDomain ); } catch ( InvalidArgumentException $ex ) { wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() ); @@ -1752,9 +1794,9 @@ class RevisionStore // Legacy because $row may have come from self::selectFields() $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true ); - $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title ); + $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, null, $queryFlags, $title ); - return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId ); + return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->dbDomain ); } /** @@ -1762,7 +1804,7 @@ class RevisionStore * * MCR migration note: this replaces Revision::newFromRow * - * @param object $row + * @param object $row A database row generated from a query based on getQueryInfo() * @param int $queryFlags * @param Title|null $title * @param bool $fromCache if true, the returned RevisionRecord will ensure that no stale @@ -1774,6 +1816,32 @@ class RevisionStore $queryFlags = 0, Title $title = null, $fromCache = false + ) { + return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $title, $fromCache ); + } + + /** + * @param object $row A database row generated from a query based on getQueryInfo() + * @param null|object[] $slotRows Database rows generated from a query based on + * getSlotsQueryInfo with the 'content' flag set. + * @param int $queryFlags + * @param Title|null $title + * @param bool $fromCache if true, the returned RevisionRecord will ensure that no stale + * data is returned from getters, by querying the database as needed + * + * @return RevisionRecord + * @throws MWException + * @see RevisionFactory::newRevisionFromRow + * + * MCR migration note: this replaces Revision::newFromRow + * + */ + public function newRevisionFromRowAndSlots( + $row, + $slotRows, + $queryFlags = 0, + Title $title = null, + $fromCache = false ) { Assert::parameterType( 'object', $row, '$row' ); @@ -1796,7 +1864,7 @@ class RevisionStore $row->rev_user ?? null, $row->rev_user_text ?? null, $row->rev_actor ?? null, - $this->wikiId + $this->dbDomain ); } catch ( InvalidArgumentException $ex ) { wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() ); @@ -1807,7 +1875,7 @@ class RevisionStore // Legacy because $row may have come from self::selectFields() $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true ); - $slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title ); + $slots = $this->newRevisionSlots( $row->rev_id, $row, $slotRows, $queryFlags, $title ); // If this is a cached row, instantiate a cache-aware revision class to avoid stale data. if ( $fromCache ) { @@ -1819,11 +1887,11 @@ class RevisionStore [ 'rev_id' => intval( $revId ) ] ); }, - $title, $user, $comment, $row, $slots, $this->wikiId + $title, $user, $comment, $row, $slots, $this->dbDomain ); } else { $rev = new RevisionStoreRecord( - $title, $user, $comment, $row, $slots, $this->wikiId ); + $title, $user, $comment, $row, $slots, $this->dbDomain ); } return $rev; } @@ -1908,7 +1976,7 @@ class RevisionStore } } - $revision = new MutableRevisionRecord( $title, $this->wikiId ); + $revision = new MutableRevisionRecord( $title, $this->dbDomain ); $this->initializeMutableRevisionFromArray( $revision, $fields ); if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) { @@ -1939,7 +2007,7 @@ class RevisionStore // remote wiki with unsuppressed ids, due to issues described in T222212. if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) && - ( $this->wikiId === false || + ( $this->dbDomain === false || ( !$fields['user']->getId() && !$fields['user']->getActorId() ) ) ) { $user = $fields['user']; @@ -1949,7 +2017,7 @@ class RevisionStore $fields['user'] ?? null, $fields['user_text'] ?? null, $fields['actor'] ?? null, - $this->wikiId + $this->dbDomain ); } catch ( InvalidArgumentException $ex ) { $user = null; @@ -2180,7 +2248,7 @@ class RevisionStore * @throws MWException */ private function checkDatabaseWikiId( IDatabase $db ) { - $storeWiki = $this->wikiId; + $storeWiki = $this->dbDomain; $dbWiki = $db->getDomainID(); if ( $dbWiki === $storeWiki ) { @@ -2405,21 +2473,24 @@ class RevisionStore if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) { $db = $this->getDBConnectionRef( DB_REPLICA ); - $ret['tables']['slots'] = 'revision'; + $ret['tables'][] = 'revision'; - $ret['fields']['slot_revision_id'] = 'slots.rev_id'; + $ret['fields']['slot_revision_id'] = 'rev_id'; $ret['fields']['slot_content_id'] = 'NULL'; - $ret['fields']['slot_origin'] = 'slots.rev_id'; + $ret['fields']['slot_origin'] = 'rev_id'; $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN ); if ( in_array( 'content', $options, true ) ) { - $ret['fields']['content_size'] = 'slots.rev_len'; - $ret['fields']['content_sha1'] = 'slots.rev_sha1'; + $ret['fields']['content_size'] = 'rev_len'; + $ret['fields']['content_sha1'] = 'rev_sha1'; $ret['fields']['content_address'] - = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ); + = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'rev_text_id' ] ); + + // Allow the content_id field to be emulated later + $ret['fields']['rev_text_id'] = 'rev_text_id'; if ( $this->contentHandlerUseDB ) { - $ret['fields']['model_name'] = 'slots.rev_content_model'; + $ret['fields']['model_name'] = 'rev_content_model'; } else { $ret['fields']['model_name'] = 'NULL'; } diff --git a/includes/Revision/RevisionStoreCacheRecord.php b/includes/Revision/RevisionStoreCacheRecord.php index ef5f10e312..0420d34d89 100644 --- a/includes/Revision/RevisionStoreCacheRecord.php +++ b/includes/Revision/RevisionStoreCacheRecord.php @@ -53,8 +53,7 @@ class RevisionStoreCacheRecord extends RevisionStoreRecord { * @param object $row A row from the revision table. Use RevisionStore::getQueryInfo() to build * a query that yields the required fields. * @param RevisionSlots $slots The slots of this revision. - * @param bool|string $wikiId the wiki ID of the site this Revision belongs to, - * or false for the local site. + * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one. */ function __construct( $callback, @@ -63,9 +62,9 @@ class RevisionStoreCacheRecord extends RevisionStoreRecord { CommentStoreComment $comment, $row, RevisionSlots $slots, - $wikiId = false + $dbDomain = false ) { - parent::__construct( $title, $user, $comment, $row, $slots, $wikiId ); + parent::__construct( $title, $user, $comment, $row, $slots, $dbDomain ); $this->mCallback = $callback; } diff --git a/includes/Revision/RevisionStoreFactory.php b/includes/Revision/RevisionStoreFactory.php index 6b3117fc78..0475557387 100644 --- a/includes/Revision/RevisionStoreFactory.php +++ b/includes/Revision/RevisionStoreFactory.php @@ -116,24 +116,24 @@ class RevisionStoreFactory { /** * @since 1.32 * - * @param bool|string $wikiId false for the current domain / wikid + * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one * * @return RevisionStore for the given wikiId with all necessary services and a logger */ - public function getRevisionStore( $wikiId = false ) { - Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' ); + public function getRevisionStore( $dbDomain = false ) { + Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' ); $store = new RevisionStore( - $this->dbLoadBalancerFactory->getMainLB( $wikiId ), - $this->blobStoreFactory->newSqlBlobStore( $wikiId ), + $this->dbLoadBalancerFactory->getMainLB( $dbDomain ), + $this->blobStoreFactory->newSqlBlobStore( $dbDomain ), $this->cache, // Pass local cache instance; Leave cache sharing to RevisionStore. $this->commentStore, - $this->nameTables->getContentModels( $wikiId ), - $this->nameTables->getSlotRoles( $wikiId ), + $this->nameTables->getContentModels( $dbDomain ), + $this->nameTables->getSlotRoles( $dbDomain ), $this->slotRoleRegistry, $this->mcrMigrationStage, $this->actorMigration, - $wikiId + $dbDomain ); $store->setLogger( $this->loggerProvider->getLogger( 'RevisionStore' ) ); diff --git a/includes/Revision/RevisionStoreRecord.php b/includes/Revision/RevisionStoreRecord.php index 955cc82de6..469e494a3d 100644 --- a/includes/Revision/RevisionStoreRecord.php +++ b/includes/Revision/RevisionStoreRecord.php @@ -51,8 +51,7 @@ class RevisionStoreRecord extends RevisionRecord { * @param object $row A row from the revision table. Use RevisionStore::getQueryInfo() to build * a query that yields the required fields. * @param RevisionSlots $slots The slots of this revision. - * @param bool|string $wikiId the wiki ID of the site this Revision belongs to, - * or false for the local site. + * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one. */ function __construct( Title $title, @@ -60,9 +59,9 @@ class RevisionStoreRecord extends RevisionRecord { CommentStoreComment $comment, $row, RevisionSlots $slots, - $wikiId = false + $dbDomain = false ) { - parent::__construct( $title, $slots, $wikiId ); + parent::__construct( $title, $slots, $dbDomain ); Assert::parameterType( 'object', $row, '$row' ); $this->mId = intval( $row->rev_id ); diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index e371b5a5c8..96baf1469e 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -82,6 +82,7 @@ return [ 'BlobStoreFactory' => function ( MediaWikiServices $services ) : BlobStoreFactory { return new BlobStoreFactory( $services->getDBLoadBalancerFactory(), + $services->getExternalStoreAccess(), $services->getMainWANObjectCache(), new ServiceOptions( BlobStoreFactory::$constructorOptions, $services->getMainConfig() ), @@ -201,11 +202,22 @@ return [ return new EventRelayerGroup( $services->getMainConfig()->get( 'EventRelayerConfig' ) ); }, + 'ExternalStoreAccess' => function ( MediaWikiServices $services ) : ExternalStoreAccess { + return new ExternalStoreAccess( + $services->getExternalStoreFactory(), + LoggerFactory::getInstance( 'ExternalStore' ) + ); + }, + 'ExternalStoreFactory' => function ( MediaWikiServices $services ) : ExternalStoreFactory { $config = $services->getMainConfig(); + $writeStores = $config->get( 'DefaultExternalStore' ); return new ExternalStoreFactory( - $config->get( 'ExternalStores' ) + $config->get( 'ExternalStores' ), + ( $writeStores !== false ) ? (array)$writeStores : [], + $services->getDBLoadBalancer()->getLocalDomainID(), + LoggerFactory::getInstance( 'ExternalStore' ) ); }, @@ -464,6 +476,9 @@ return [ $config->get( 'WhitelistReadRegexp' ), $config->get( 'EmailConfirmToEdit' ), $config->get( 'BlockDisablesLogin' ), + $config->get( 'GroupPermissions' ), + $config->get( 'RevokePermissions' ), + $config->get( 'AvailableRights' ), $services->getNamespaceInfo() ); }, diff --git a/includes/Setup.php b/includes/Setup.php index 54e6795414..641f1f9030 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -807,7 +807,9 @@ if ( $wgRequest->getCookie( 'UseDC', '' ) === 'master' ) { // Useful debug output if ( $wgCommandLineMode ) { - wfDebug( "\n\nStart command line script $self\n" ); + if ( isset( $self ) ) { + wfDebug( "\n\nStart command line script $self\n" ); + } } else { $debug = "\n\nStart request {$wgRequest->getMethod()} {$wgRequest->getRequestURL()}\n"; diff --git a/includes/Storage/BlobStoreFactory.php b/includes/Storage/BlobStoreFactory.php index 368ca485fb..b59c68d175 100644 --- a/includes/Storage/BlobStoreFactory.php +++ b/includes/Storage/BlobStoreFactory.php @@ -24,6 +24,7 @@ use Language; use MediaWiki\Config\ServiceOptions; use WANObjectCache; use Wikimedia\Rdbms\ILBFactory; +use ExternalStoreAccess; /** * Service for instantiating BlobStores @@ -39,6 +40,11 @@ class BlobStoreFactory { */ private $lbFactory; + /** + * @var ExternalStoreAccess + */ + private $extStoreAccess; + /** * @var WANObjectCache */ @@ -69,6 +75,7 @@ class BlobStoreFactory { public function __construct( ILBFactory $lbFactory, + ExternalStoreAccess $extStoreAccess, WANObjectCache $cache, ServiceOptions $options, Language $contLang @@ -76,6 +83,7 @@ class BlobStoreFactory { $options->assertRequiredOptions( self::$constructorOptions ); $this->lbFactory = $lbFactory; + $this->extStoreAccess = $extStoreAccess; $this->cache = $cache; $this->options = $options; $this->contLang = $contLang; @@ -84,27 +92,28 @@ class BlobStoreFactory { /** * @since 1.31 * - * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki. + * @param bool|string $dbDomain The ID of the target wiki database. Use false for the local wiki. * * @return BlobStore */ - public function newBlobStore( $wikiId = false ) { - return $this->newSqlBlobStore( $wikiId ); + public function newBlobStore( $dbDomain = false ) { + return $this->newSqlBlobStore( $dbDomain ); } /** * @internal Please call newBlobStore and use the BlobStore interface. * - * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki. + * @param bool|string $dbDomain The ID of the target wiki database. Use false for the local wiki. * * @return SqlBlobStore */ - public function newSqlBlobStore( $wikiId = false ) { - $lb = $this->lbFactory->getMainLB( $wikiId ); + public function newSqlBlobStore( $dbDomain = false ) { + $lb = $this->lbFactory->getMainLB( $dbDomain ); $store = new SqlBlobStore( $lb, + $this->extStoreAccess, $this->cache, - $wikiId + $dbDomain ); $store->setCompressBlobs( $this->options->get( 'CompressRevisions' ) ); diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php index 0008ef7055..b4d6f052f6 100644 --- a/includes/Storage/DerivedPageDataUpdater.php +++ b/includes/Storage/DerivedPageDataUpdater.php @@ -52,6 +52,9 @@ use MWCallableUpdate; use ParserCache; use ParserOptions; use ParserOutput; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use RecentChangesUpdateJob; use ResourceLoaderWikiModule; use Revision; @@ -94,7 +97,7 @@ use WikiPage; * @since 1.32 * @ingroup Page */ -class DerivedPageDataUpdater implements IDBAccessObject { +class DerivedPageDataUpdater implements IDBAccessObject, LoggerAwareInterface { /** * @var UserIdentity|null @@ -136,6 +139,11 @@ class DerivedPageDataUpdater implements IDBAccessObject { */ private $loadbalancerFactory; + /** + * @var LoggerInterface + */ + private $logger; + /** * @var string see $wgArticleCountMethod */ @@ -293,6 +301,11 @@ class DerivedPageDataUpdater implements IDBAccessObject { // XXX only needed for waiting for replicas to catch up; there should be a narrower // interface for that. $this->loadbalancerFactory = $loadbalancerFactory; + $this->logger = new NullLogger(); + } + + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; } /** @@ -850,11 +863,12 @@ class DerivedPageDataUpdater implements IDBAccessObject { if ( $stashedEdit ) { /** @var ParserOutput $output */ $output = $stashedEdit->output; - // TODO: this should happen when stashing the ParserOutput, not now! $output->setCacheTime( $stashedEdit->timestamp ); $renderHints['known-revision-output'] = $output; + + $this->logger->debug( __METHOD__ . ': using stashed edit output...' ); } // NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions diff --git a/includes/Storage/NameTableStore.php b/includes/Storage/NameTableStore.php index a14e339020..5ef03042dc 100644 --- a/includes/Storage/NameTableStore.php +++ b/includes/Storage/NameTableStore.php @@ -66,7 +66,7 @@ class NameTableStore { /** * @param ILoadBalancer $dbLoadBalancer A load balancer for acquiring database connections * @param WANObjectCache $cache A cache manager for caching data. This can be the local - * wiki's default instance even if $wikiId refers to a different wiki, since + * wiki's default instance even if $dbDomain refers to a different wiki, since * makeGlobalKey() is used to constructed a key that allows cached names from * the same database to be re-used between wikis. For example, enwiki and frwiki will * use the same cache keys for names from the wikidatawiki database, regardless diff --git a/includes/Storage/PageEditStash.php b/includes/Storage/PageEditStash.php index 9c2b3e7de4..2285f4a953 100644 --- a/includes/Storage/PageEditStash.php +++ b/includes/Storage/PageEditStash.php @@ -280,6 +280,12 @@ class PageEditStash { "Cache for key '{key}' has vary_revision_id; post-insertion parse possible.", $context ); + } elseif ( $editInfo->output->getFlag( 'vary-revision-timestamp' ) ) { + // Similar to the above if we didn't guess the timestamp correctly. + $logger->debug( + "Cache for key '{key}' has vary_revision_timestamp; post-insertion parse possible.", + $context + ); } return $editInfo; diff --git a/includes/Storage/SqlBlobStore.php b/includes/Storage/SqlBlobStore.php index e0e14b0cd6..5260754f5f 100644 --- a/includes/Storage/SqlBlobStore.php +++ b/includes/Storage/SqlBlobStore.php @@ -27,13 +27,13 @@ namespace MediaWiki\Storage; use DBAccessObjectUtils; -use ExternalStore; use IDBAccessObject; use IExpiringStore; use InvalidArgumentException; use Language; use MWException; use WANObjectCache; +use ExternalStoreAccess; use Wikimedia\Assert\Assert; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\ILoadBalancer; @@ -56,15 +56,20 @@ class SqlBlobStore implements IDBAccessObject, BlobStore { */ private $dbLoadBalancer; + /** + * @var ExternalStoreAccess + */ + private $extStoreAccess; + /** * @var WANObjectCache */ private $cache; /** - * @var bool|string Wiki ID + * @var string|bool DB domain ID of a wiki or false for the local one */ - private $wikiId; + private $dbDomain; /** * @var int @@ -93,22 +98,25 @@ class SqlBlobStore implements IDBAccessObject, BlobStore { /** * @param ILoadBalancer $dbLoadBalancer A load balancer for acquiring database connections + * @param ExternalStoreAccess $extStoreAccess Access layer for external storage * @param WANObjectCache $cache A cache manager for caching blobs. This can be the local - * wiki's default instance even if $wikiId refers to a different wiki, since + * wiki's default instance even if $dbDomain refers to a different wiki, since * makeGlobalKey() is used to constructed a key that allows cached blobs from the * same database to be re-used between wikis. For example, enwiki and frwiki will * use the same cache keys for blobs from the wikidatawiki database, regardless of * the cache's default key space. - * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki. + * @param bool|string $dbDomain The ID of the target wiki database. Use false for the local wiki. */ public function __construct( ILoadBalancer $dbLoadBalancer, + ExternalStoreAccess $extStoreAccess, WANObjectCache $cache, - $wikiId = false + $dbDomain = false ) { $this->dbLoadBalancer = $dbLoadBalancer; + $this->extStoreAccess = $extStoreAccess; $this->cache = $cache; - $this->wikiId = $wikiId; + $this->dbDomain = $dbDomain; } /** @@ -199,7 +207,7 @@ class SqlBlobStore implements IDBAccessObject, BlobStore { */ private function getDBConnection( $index ) { $lb = $this->getDBLoadBalancer(); - return $lb->getConnection( $index, [], $this->wikiId ); + return $lb->getConnection( $index, [], $this->dbDomain ); } /** @@ -219,7 +227,10 @@ class SqlBlobStore implements IDBAccessObject, BlobStore { # Write to external storage if required if ( $this->useExternalStore ) { // Store and get the URL - $data = ExternalStore::insertToDefault( $data ); + $data = $this->extStoreAccess->insert( $data, [ 'domain' => $this->dbDomain ] ); + if ( !$data ) { + throw new BlobAccessException( "Failed to store text to external storage" ); + } if ( $flags ) { $flags .= ','; } @@ -368,7 +379,7 @@ class SqlBlobStore implements IDBAccessObject, BlobStore { return $this->cache->makeGlobalKey( 'BlobStore', 'address', - $this->dbLoadBalancer->resolveDomainID( $this->wikiId ), + $this->dbLoadBalancer->resolveDomainID( $this->dbDomain ), $blobAddress ); } @@ -412,14 +423,15 @@ class SqlBlobStore implements IDBAccessObject, BlobStore { $this->getCacheTTL(), function () use ( $url, $flags ) { // Ignore $setOpts; blobs are immutable and negatives are not cached - $blob = ExternalStore::fetchFromURL( $url, [ 'wiki' => $this->wikiId ] ); + $blob = $this->extStoreAccess + ->fetchFromURL( $url, [ 'domain' => $this->dbDomain ] ); return $blob === false ? false : $this->decompressData( $blob, $flags ); }, [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => WANObjectCache::TTL_PROC_LONG ] ); } else { - $blob = ExternalStore::fetchFromURL( $url, [ 'wiki' => $this->wikiId ] ); + $blob = $this->extStoreAccess->fetchFromURL( $url, [ 'domain' => $this->dbDomain ] ); return $blob === false ? false : $this->decompressData( $blob, $flags ); } } else { @@ -623,7 +635,7 @@ class SqlBlobStore implements IDBAccessObject, BlobStore { } public function isReadOnly() { - if ( $this->useExternalStore && ExternalStore::defaultStoresAreReadOnly() ) { + if ( $this->useExternalStore && $this->extStoreAccess->isReadOnly() ) { return true; } diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index eeb0cf7399..bdb0dc22aa 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -441,6 +441,7 @@ class ApiQuery extends ApiBase { $exporter = new WikiExporter( $this->getDB() ); $sink = new DumpStringOutput; $exporter->setOutputSink( $sink ); + $exporter->setSchemaVersion( $this->mParams['exportschema'] ); $exporter->openStream(); foreach ( $exportTitles as $title ) { $exporter->pageByTitle( $title ); @@ -479,6 +480,10 @@ class ApiQuery extends ApiBase { 'indexpageids' => false, 'export' => false, 'exportnowrap' => false, + 'exportschema' => [ + ApiBase::PARAM_DFLT => WikiExporter::schemaVersion(), + ApiBase::PARAM_TYPE => XmlDumpWriter::$supportedSchemas, + ], 'iwurl' => false, 'continue' => [ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index e123a2ac46..0791426f77 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -757,58 +757,6 @@ class ApiQueryImageInfo extends ApiQueryBase { ); } - /** - * Returns array key value pairs of properties and their descriptions - * - * @deprecated since 1.25 - * @param string $modulePrefix - * @return array - */ - private static function getProperties( $modulePrefix = '' ) { - return [ - 'timestamp' => ' timestamp - Adds timestamp for the uploaded version', - 'user' => ' user - Adds the user who uploaded the image version', - 'userid' => ' userid - Add the user ID that uploaded the image version', - 'comment' => ' comment - Comment on the version', - 'parsedcomment' => ' parsedcomment - Parse the comment on the version', - 'canonicaltitle' => ' canonicaltitle - Adds the canonical title of the image file', - 'url' => ' url - Gives URL to the image and the description page', - 'size' => ' size - Adds the size of the image in bytes, ' . - 'its height and its width. Page count and duration are added if applicable', - 'dimensions' => ' dimensions - Alias for size', // B/C with Allimages - 'sha1' => ' sha1 - Adds SHA-1 hash for the image', - 'mime' => ' mime - Adds MIME type of the image', - 'thumbmime' => ' thumbmime - Adds MIME type of the image thumbnail' . - ' (requires url and param ' . $modulePrefix . 'urlwidth)', - 'mediatype' => ' mediatype - Adds the media type of the image', - 'metadata' => ' metadata - Lists Exif metadata for the version of the image', - 'commonmetadata' => ' commonmetadata - Lists file format generic metadata ' . - 'for the version of the image', - 'extmetadata' => ' extmetadata - Lists formatted metadata combined ' . - 'from multiple sources. Results are HTML formatted.', - 'archivename' => ' archivename - Adds the file name of the archive ' . - 'version for non-latest versions', - 'bitdepth' => ' bitdepth - Adds the bit depth of the version', - 'uploadwarning' => ' uploadwarning - Used by the Special:Upload page to ' . - 'get information about an existing file. Not intended for use outside MediaWiki core', - ]; - } - - /** - * Returns the descriptions for the properties provided by getPropertyNames() - * - * @deprecated since 1.25 - * @param array $filter List of properties to filter out - * @param string $modulePrefix - * @return array - */ - public static function getPropertyDescriptions( $filter = [], $modulePrefix = '' ) { - return array_merge( - [ 'What image information to get:' ], - array_values( array_diff_key( static::getProperties( $modulePrefix ), array_flip( $filter ) ) ) - ); - } - protected function getExamplesMessages() { return [ 'action=query&titles=File:Albert%20Einstein%20Head.jpg&prop=imageinfo' diff --git a/includes/api/ApiQueryLanguageinfo.php b/includes/api/ApiQueryLanguageinfo.php index 72b59b006b..1c2d490a26 100644 --- a/includes/api/ApiQueryLanguageinfo.php +++ b/includes/api/ApiQueryLanguageinfo.php @@ -233,7 +233,7 @@ class ApiQueryLanguageinfo extends ApiQueryBase { return [ "$pathUrl" => "apihelp-$pathMsg-example-simple", - "$pathUrl&{$prefix}prop=autonym|name&lang=de" + "$pathUrl&{$prefix}prop=autonym|name&uselang=de" => "apihelp-$pathMsg-example-autonym-name-de", "$pathUrl&{$prefix}prop=fallbacks|variants&{$prefix}code=oc" => "apihelp-$pathMsg-example-fallbacks-variants-oc", diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 98c65516c9..23f702cc96 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -20,8 +20,6 @@ * @file */ -use MediaWiki\MediaWikiServices; - /** * Query module to perform full text search within wiki titles and content * @@ -145,9 +143,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } } - // Add the search results to the result - $terms = MediaWikiServices::getInstance()->getContentLanguage()-> - convertForSearchResult( $matches->termMatches() ); $titles = []; $count = 0; @@ -163,7 +158,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } if ( $resultPageSet === null ) { - $vals = $this->getSearchResultData( $result, $prop, $terms ); + $vals = $this->getSearchResultData( $result, $prop ); if ( $vals ) { // Add item to results and see whether it fits $fit = $apiResult->addValue( [ 'query', $this->getModuleName() ], null, $vals ); @@ -184,13 +179,13 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { // Interwiki results inside main result set $canAddInterwiki = (bool)$params['enablerewrites'] && ( $resultPageSet === null ); if ( $canAddInterwiki ) { - $this->addInterwikiResults( $matches, $apiResult, $prop, $terms, 'additional', + $this->addInterwikiResults( $matches, $apiResult, $prop, 'additional', SearchResultSet::INLINE_RESULTS ); } // Interwiki results outside main result set if ( $interwiki && $resultPageSet === null ) { - $this->addInterwikiResults( $matches, $apiResult, $prop, $terms, 'interwiki', + $this->addInterwikiResults( $matches, $apiResult, $prop, 'interwiki', SearchResultSet::SECONDARY_RESULTS ); } @@ -217,10 +212,9 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { * Assemble search result data. * @param SearchResult $result Search result * @param array $prop Props to extract (as keys) - * @param array $terms Terms list * @return array|null Result data or null if result is broken in some way. */ - private function getSearchResultData( SearchResult $result, $prop, $terms ) { + private function getSearchResultData( SearchResult $result, $prop ) { // Silently skip broken and missing titles if ( $result->isBrokenTitle() || $result->isMissingRevision() ) { return null; @@ -239,7 +233,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $vals['wordcount'] = $result->getWordCount(); } if ( isset( $prop['snippet'] ) ) { - $vals['snippet'] = $result->getTextSnippet( $terms ); + $vals['snippet'] = $result->getTextSnippet(); } if ( isset( $prop['timestamp'] ) ) { $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $result->getTimestamp() ); @@ -287,14 +281,13 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { * @param SearchResultSet $matches * @param ApiResult $apiResult * @param array $prop Props to extract (as keys) - * @param array $terms Terms list * @param string $section Section name where results would go * @param int $type Interwiki result type * @return int|null Number of total hits in the data or null if none was produced */ private function addInterwikiResults( SearchResultSet $matches, ApiResult $apiResult, $prop, - $terms, $section, $type + $section, $type ) { $totalhits = null; if ( $matches->hasInterwikiResults( $type ) ) { @@ -304,7 +297,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { foreach ( $interwikiMatches as $result ) { $title = $result->getTitle(); - $vals = $this->getSearchResultData( $result, $prop, $terms ); + $vals = $this->getSearchResultData( $result, $prop ); $vals['namespace'] = $result->getInterwikiNamespaceText(); $vals['title'] = $title->getText(); diff --git a/includes/api/i18n/ar.json b/includes/api/i18n/ar.json index eadf3f7ab7..70515eb61a 100644 --- a/includes/api/i18n/ar.json +++ b/includes/api/i18n/ar.json @@ -412,6 +412,7 @@ "apihelp-query-param-indexpageids": "تضمين قسم إضافي لمعرفات الصفحات يسرد جميع معرفات الصفحات التي تم إرجاعها.", "apihelp-query-param-export": "تصدير المراجعات الحالية لجميع الصفحات المعينة أو المولدة.", "apihelp-query-param-exportnowrap": "إعادة تصدير XML دون التفاف عليه في نتيجة XML (نفس شكل [[Special:Export|خاص:تصدير]]). يمكن استخدامها فقط مع $1export.", + "apihelp-query-param-exportschema": "استهداف الإصدار المحدد من تنسيق تفريغ XML عند التصدير، يمكن استخدامه مع $1export فقط.", "apihelp-query-param-iwurl": "ما إذا كنت تريد الحصول على المسار الكامل إذا كان العنوان رابط إنترويكي.", "apihelp-query-param-rawcontinue": "إرجاع query-continue بيانات خام للاستمرار.", "apihelp-query-example-revisions": "جلب [[Special:ApiHelp/query+siteinfo|معلومات الموقع]] و[[Special:ApiHelp/query+revisions|مراجعات]] Main Page.", @@ -1515,7 +1516,7 @@ "api-help-param-templated-var-first": "يجب استبدال {$1} في اسم الوسيط بقيم $2", "api-help-param-templated-var": "{$1} بقيم $2", "api-help-datatypes-header": "أنواع البيانات", - "api-help-datatypes": "يجب أن يكون الإدخال إلى ميدياويكي هو UTF-8 المعيَّن لـNFC، قد يحاول ميدياويكي تحويل مدخلات أخرى، ولكن قد يتسبب ذلك في فشل بعض العمليات (مثل [[Special:ApiHelp/edit|التعديلات]] مع عمليات فحص MD5).\n\nتحتاج بعض أنواع الوسائط في طلبات API إلى مزيد من الشرح:\n;منطقية\n:تعمل الوسائط المنطقية مثل صناديق اختيار HTML: إذا تم تحديد الوسيط، بغض النظر عن القيمة، فيتم اعتباره صحيحا، للحصول على قيمة خاطئة; احذف الوسيط تماما.\n;الطابع الزمني\n:قد يتم تحديد الطوابع الزمنية بتنسيقات متعددة، يُوصَى بـISO 8601 التاريخ والوقت، جميع الأوقات بالتوقيت العالمي المنسق، يتم تجاهل أية منطقة زمنية مضمنة.\n:* تاريخ ووقت ISO 8601، 2001-01-15T14:56:00Z (علامات الترقيم وZ اختيارية)\n:* تاريخ ووقت ISO 8601مع الثواني المجزأة (متجاهلة)، 2001-01-15T14:56:00.00001Z (الشرطات، والنقطتان الرأسيتان اختيارية وZ)\n:* تنسيق ميدياويكي، 20010115145600\n:* تنسيق رقمي عام، (توقيت اختياري، أو يتم تجاهل) 2001-01-15 14:56:00 (منطقة زمنية اختيارية لـGMT، +##، أو يتم تجاهل -##)\n:* تنسيق EXIF، 2001:01:15 14:56:00\n:*تنسيق RFC 2822 (قد يتم حذف المنطقة الزمنية)، Mon, 15 Jan 2001 14:56:00\n:*تنسيق RFC 850 format (قد يتم حذف المنطقة الزمنية)، Monday, 15-Jan-2001 14:56:00\n:* تنسيق C ctime format, Mon Jan 15 14:56:00 2001\n:* الثواني منذ 1970-01-01T00:00:00Z كعدد صحيح يتراوح بين 1 و13 (باستثناء 0)\n:* السلسلة now\n;فاصل بديل متعدد القيم\n:يتم عادةً إرسال الوسائط التي تأخذ قيم متعددة مع القيم المفصولة باستخدام حرف الأنبوب، على سبيل المثال param=value1|value2 or param=value1%7Cvalue2 إذا كانت القيمة يجب أن تحتوي على حرف الأنبوب، فاستخدم U+001F (فاصل الوحدة) مثل الفاصل ''و'' بادئة القيمة بـU+001F، على سبيل المثال param=%1Fvalue1%1Fvalue2.", + "api-help-datatypes": "يجب أن يكون الإدخال إلى ميدياويكي هو UTF-8 المعيَّن لـNFC، قد يحاول ميدياويكي تحويل مدخلات أخرى، ولكن قد يتسبب ذلك في فشل بعض العمليات (مثل [[Special:ApiHelp/edit|التعديلات]] مع عمليات فحص MD5).\n\nتحتاج بعض أنواع الوسائط في طلبات API إلى مزيد من الشرح:\n;منطقية\n:تعمل الوسائط المنطقية مثل صناديق اختيار HTML: إذا تم تحديد الوسيط، بغض النظر عن القيمة، فيتم اعتباره صحيحا، للحصول على قيمة خاطئة; احذف الوسيط تماما.\n;الطابع الزمني\n:يمكن تحديد الطوابع الزمنية بتنسيقات متعددة، راجع [[mw:Special:MyLanguage/Timestamp|تنسيقات إدخال مكتبة الطابع الزمني الموثقة على mediawiki.org]] للتفاصيل، يُوصَى بتاريخ ISO 8601 ووقت: 2001-01-15T14:56:00Z، بالإضافة إلى ذلك، يمكن استخدام السلسلة now لتحديد الطابع الزمني الحالي.\n;فاصل بديل متعدد القيم\n:يتم عادةً إرسال الوسائط التي تأخذ قيم متعددة مع القيم المفصولة باستخدام حرف الأنبوب، على سبيل المثال param=value1|value2 or param=value1%7Cvalue2 إذا كانت القيمة يجب أن تحتوي على حرف الأنبوب، فاستخدم U+001F (فاصل الوحدة) مثل الفاصل ''و'' بادئة القيمة بـU+001F، على سبيل المثال param=%1Fvalue1%1Fvalue2.", "api-help-templatedparams-header": "وسائط القالب", "api-help-templatedparams": "تدعم وسائط القوالب الحالات التي تحتاج فيها API إلى قيمة لكل قيمة من وسيط آخر، على سبيل المثال، إذا كانت هناك وحدة API لطلب الفاكهة، فإنه قد يكون لديك وسيط fruits لتحديد أي الفواكه تم طلبها ووسيط قالب {fruit}-quantityلتحديد عدد الفواكه لكل طلب، يمكن لعميل API الذي يريد 1 تفاحة، 5 موز، 20 فراولة ثم تقديم طلب مثل fruits=apples|bananas|strawberries&apples-quantity=1&bananas-quantity=5&strawberries-quantity=20.", "api-help-param-type-limit": "النوع: عدد صحيح أو max", diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 9843af42e8..cae7687b69 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -440,6 +440,7 @@ "apihelp-query-param-indexpageids": "Include an additional pageids section listing all returned page IDs.", "apihelp-query-param-export": "Export the current revisions of all given or generated pages.", "apihelp-query-param-exportnowrap": "Return the export XML without wrapping it in an XML result (same format as [[Special:Export]]). Can only be used with $1export.", + "apihelp-query-param-exportschema": "Target the given version of the XML dump format when exporting. Can only be used with $1export.", "apihelp-query-param-iwurl": "Whether to get the full URL if the title is an interwiki link.", "apihelp-query-param-rawcontinue": "Return raw query-continue data for continuation.", "apihelp-query-example-revisions": "Fetch [[Special:ApiHelp/query+siteinfo|site info]] and [[Special:ApiHelp/query+revisions|revisions]] of Main Page.", diff --git a/includes/api/i18n/es.json b/includes/api/i18n/es.json index af345d5548..6473c35852 100644 --- a/includes/api/i18n/es.json +++ b/includes/api/i18n/es.json @@ -886,6 +886,7 @@ "apihelp-query+languageinfo-summary": "Devolver información sobre los idiomas disponibles.", "apihelp-query+languageinfo-paramvalue-prop-code": "El código lingüístico (es específico de MediaWiki, pero existen coincidencias con otras normas.)", "apihelp-query+languageinfo-paramvalue-prop-dir": "La dirección de escritura del idioma (bien ltr o bien rtl).", + "apihelp-query+languageinfo-example-simple": "Obtener los códigos lingüísticos de todos los idiomas admitidos.", "apihelp-query+languageinfo-example-autonym-name-de": "Obtener los endónimos y los nombres alemanes de todos los idiomas compatibles.", "apihelp-query+languageinfo-example-fallbacks-variants-oc": "Obtener los idiomas de reserva y las variantes del occitano.", "apihelp-query+languageinfo-example-bcp47-dir": "Obtener el código lingüístico BCP-47 y la dirección de todos los idiomas compatibles.", @@ -1421,7 +1422,7 @@ "api-help-param-deprecated": "En desuso.", "api-help-param-required": "Este parámetro es obligatorio.", "api-help-datatypes-header": "Tipos de datos", - "api-help-datatypes": "Las entradas en MediaWiki deberían estar en UTF-8 según la norma NFC. MediaWiki puede tratar de convertir otros formatos, pero esto puede provocar errores en algunas operaciones (tales como las [[Special:ApiHelp/edit|ediciones]] con controles MD5).\n\nAlgunos tipos de parámetros en las solicitudes de API requieren de una explicación más detallada:\n;boolean\n:Los parámetros booleanos trabajo como cajas de verificación de HTML: si el parámetro está definido, independientemente de su valor, se considera verdadero. Para un valor falso, se debe omitir el parámetro por completo.\n;marca de tiempo\n:Las marcas de tiempo se pueden definir en varios formatos. Se recomienda seguir la norma ISO 8601 de fecha y hora. Todas las horas están en UTC, ignorándose cualquier indicación de zona horaria.\n:* Fecha y hora en ISO 8601, 2001-01-15T14:56:00Z (los signos de puntuación y la Z son opcionales)\n:* Fecha y hora en ISO 8601 con fracciones de segundo (que se omiten), 2001-01-15T14:56:00.00001Z (los guiones, los dos puntos y la Z son opcionales)\n:* Formato MediaWiki, 20010115145600\n:* Formato genérico de número, 2001-01-15 14:56:00 (la zona horaria opcional, sea GMT, +## o -## se omite)\n:* Formato EXIF, 2001:01:15 14:56:00\n:*Formato RFC 2822 (la zona horaria es opcional), lun, 15 ene 2001 14:56:00\n:* Formato RFC 850 (la zona horaria es opcional), lunes, 15-ene-2001 14:56:00\n:* Formato ctime de C, Mon Jan 15 14:56:00 2001\n:* Número de segundos desde 1970-01-01T00:00:00Z en forma de número entero de entre 1 y 13 cifras (sin 0)\n:* La cadena now\n\n;separador alternativo de valores múltiples\n:Los parámetros que toman valores múltiples se envían normalmente utilizando la barra vertical para separar los valores, p. ej., param=valor1|valor2 o param=valor1%7Cvalor2. Si un valor tiene que contener el carácter de barra vertical, utiliza U+001F (separador de unidades) como separador ''y'' prefija el valor con, p. ej. param=%1Fvalor1%1Fvalor2.", + "api-help-datatypes": "El formato de entrada de MediaWiki debe ser UTF-8 normalizado por NFC. MediaWiki puede intentar convertir otros formatos, pero ello podría causar que algunas operaciones, como las [[Special:ApiHelp/edit|ediciones]] con comprobaciones MD5, fallen.\n\nAlgunos tipos de parámetros para las solicitudes de API requieren una explicación más a fondo:\n;boolean\n:Los parámetros booleanos funcionan como las casillas de verificación en HTML: si se especifica el parámetro, sin importar su valor, se considera verdadero. Para un valor falso, omítase el parámetro completamente.\n;timestamp\n:Los cronomarcadores pueden especificarse en varios formatos; para obtener detalles, consúltense [[mw:Special:MyLanguage/Timestamp|los formatos de entrada de la biblioteca Timestamp documentados en mediawiki.org]]. Se recomienda el formato ISO 8601: 2001-01-15T14:56:00Z. Además, es posible utilizar la cadena now para especificar la fecha y la hora actuales.\n;separador alternativo para valores múltiples\n:Normalmente, los parámetros que reciben varios valores se envían con estos separados por una pleca; p. ej., parámetro=valor1|valor2 o parámetro=valor1%7Cvalor2. Si un valor debe contener una pleca, utilícese U+001F (separador de unidades) como el separador ''y'' prefíjese el valor con U+001F; p. ej., parámetro=%1Fvalor1%1Fvalor2.", "api-help-param-type-limit": "Tipo: entero o max", "api-help-param-type-integer": "Tipo: {{PLURAL:$1|1=entero|2=lista de enteros}}", "api-help-param-type-boolean": "Tipo: booleano/lógico ([[Special:ApiHelp/main#main/datatypes|detalles]])", diff --git a/includes/api/i18n/fr.json b/includes/api/i18n/fr.json index acd1d786a5..844221382f 100644 --- a/includes/api/i18n/fr.json +++ b/includes/api/i18n/fr.json @@ -933,7 +933,7 @@ "apihelp-query+languageinfo-paramvalue-prop-code": "Le code de langue (ce code est spécifique à MédiaWiki, bien qu’il y ait des recouvrements avec d’autres standards).", "apihelp-query+languageinfo-paramvalue-prop-bcp47": "Le code de langue BCP-47.", "apihelp-query+languageinfo-paramvalue-prop-dir": "La direction d’écriture de la langue (ltr ou rtl).", - "apihelp-query+languageinfo-paramvalue-prop-autonym": "L’autonyme d&une langue, c’est-à-dire son nom dans cette langue.", + "apihelp-query+languageinfo-paramvalue-prop-autonym": "L’autonyme d'une langue, c’est-à-dire son nom dans cette langue.", "apihelp-query+languageinfo-paramvalue-prop-name": "Le nom de la langue dans la langue spécifiée par le paramètre lilang, avec application des langues de secours si besoin.", "apihelp-query+languageinfo-paramvalue-prop-fallbacks": "Les codes de langue des langues de secours configurées pour cette langue. Le secours implicite final en 'en' n’est pas inclus (mais certaines langues peuvent avoir 'en' en secours explicitement).", "apihelp-query+languageinfo-paramvalue-prop-variants": "Les codes de langue des variantes supportées par cette langue.", diff --git a/includes/api/i18n/hu.json b/includes/api/i18n/hu.json index 530b7dd789..7c98c7ae55 100644 --- a/includes/api/i18n/hu.json +++ b/includes/api/i18n/hu.json @@ -1153,6 +1153,7 @@ "api-help-param-integer-max": "Az {{PLURAL:$1|1=érték nem lehet nagyobb|2=értékek nem lehetnek nagyobbak}} mint $3.", "api-help-param-integer-minmax": "{{PLURAL:$1|1=Az értéknek $2 és $3 között kell lennie.|2=Az értékeknek $2 és $3 között kell lenniük.}}", "api-help-param-default": "Alapértelmezett: $1", + "api-help-param-default-empty": "Alapértelmezett: (üres)", "api-help-examples": "{{PLURAL:$1|Példa|Példák}}:", "apierror-timeout": "A kiszolgáló nem adott választ a várt időn belül." } diff --git a/includes/api/i18n/ja.json b/includes/api/i18n/ja.json index 70fa9da518..a7ff703908 100644 --- a/includes/api/i18n/ja.json +++ b/includes/api/i18n/ja.json @@ -14,7 +14,8 @@ "ネイ", "Omotecho", "Yusuke1109", - "Suyama" + "Suyama", + "Yuukin0248" ] }, "apihelp-main-extended-description": "
\n* [[mw:Special:MyLanguage/API:Main_page|説明文書]]\n* [[mw:Special:MyLanguage/API:FAQ|よくある質問]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api メーリングリスト]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API 告知]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R バグの報告とリクエスト]\n
\n状態: MediaWiki APIは、積極的にサポートされ、改善された成熟した安定したインターフェースです。避けようとはしていますが、時には壊れた変更が加えられるかもしれません。アップデートの通知を受け取るには、[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ the mediawiki-api-announce メーリングリスト]に参加してください。\n\n誤ったリクエスト: 誤ったリクエストが API に送られた場合、\"MediaWiki-API-Error\" HTTP ヘッダーが送信され、そのヘッダーの値と送り返されるエラーコードは同じ値にセットされます。より詳しい情報は [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Errors and warnings]] を参照してください。\n\n

テスト: API のリクエストのテストは、[[Special:ApiSandbox]]で簡単に行えます。

", @@ -45,6 +46,8 @@ "apihelp-block-param-watchuser": "その利用者またはIPアドレスの利用者ページとトークページをウォッチします。", "apihelp-block-param-tags": "ブロック記録の項目に適用する変更タグ。", "apihelp-block-param-partial": "サイト全体ではなく特定のページまたは名前空間での編集をブロックします。", + "apihelp-block-param-pagerestrictions": "利用者が編集できないようにするページのタイトルのリスト。partial に true が設定されている場合のみ適用します。", + "apihelp-block-param-namespacerestrictions": "利用者が編集できないようにする名前空間のID。partial に true が設定されている場合のみ適用します。", "apihelp-block-example-ip-simple": "IPアドレス 192.0.2.5 を First strike という理由で3日ブロックする", "apihelp-block-example-user-complex": "利用者 Vandal を Vandalism という理由で無期限ブロックし、新たなアカウント作成とメールの送信を禁止する。", "apihelp-changeauthenticationdata-summary": "現在の利用者の認証データを変更します。", @@ -110,7 +113,7 @@ "apihelp-edit-param-text": "ページの本文。", "apihelp-edit-param-summary": "編集の要約。$1section=new で $1sectiontitle が設定されていない場合は節名としても利用されます。", "apihelp-edit-param-tags": "この版に適用する変更タグ。", - "apihelp-edit-param-minor": "細部の編集", + "apihelp-edit-param-minor": "この編集に細部の変更の印を付ける", "apihelp-edit-param-notminor": "細部の編集ではない。", "apihelp-edit-param-bot": "この編集をボットの編集としてマークする。", "apihelp-edit-param-basetimestamp": "編集前の版のタイムスタンプ。編集競合を検出するために使用されます。\n[[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]] で取得できます。", @@ -849,25 +852,25 @@ "apihelp-query+tags-paramvalue-prop-description": "タグの説明を追加します。", "apihelp-query+tags-paramvalue-prop-hitcount": "版の記録項目の数と、このタグを持っている記録項目の数を、追加します。", "apihelp-query+tags-example-simple": "利用可能なタグを一覧表示する。", - "apihelp-query+templates-summary": "与えられたページでトランスクルードされているすべてのページを返します。", + "apihelp-query+templates-summary": "与えられたページで参照読み込みされているすべてのページを返します。", "apihelp-query+templates-param-namespace": "この名前空間のテンプレートのみ表示する。", "apihelp-query+templates-param-limit": "返すテンプレートの数。", "apihelp-query+templates-param-dir": "昇順・降順の別。", "apihelp-query+templates-example-simple": "Main Page で使用されているテンプレートを取得する。", "apihelp-query+templates-example-generator": "Main Page で使用されているテンプレートに関する情報を取得する。", - "apihelp-query+templates-example-namespaces": "Main Page でトランスクルードされている {{ns:user}} および {{ns:template}} 名前空間のページを取得する。", + "apihelp-query+templates-example-namespaces": "Main Page で参照読み込みされている {{ns:user}} および {{ns:template}} 名前空間のページを取得する。", "apihelp-query+tokens-summary": "データ変更操作用のトークンを取得します。", "apihelp-query+tokens-param-type": "リクエストするトークンの種類。", "apihelp-query+tokens-example-simple": "csrfトークンを取得する (既定)。", "apihelp-query+tokens-example-types": "ウォッチトークンおよび巡回トークンを取得する。", - "apihelp-query+transcludedin-summary": "与えられたページをトランスクルードしているすべてのページを検索します。", + "apihelp-query+transcludedin-summary": "与えられたページを参照読み込みしているすべてのページを検索します。", "apihelp-query+transcludedin-param-prop": "取得するプロパティ:", "apihelp-query+transcludedin-paramvalue-prop-pageid": "各ページのページID。", "apihelp-query+transcludedin-paramvalue-prop-title": "各ページのページ名。", "apihelp-query+transcludedin-paramvalue-prop-redirect": "ページがリダイレクトである場合マークします。", "apihelp-query+transcludedin-param-namespace": "この名前空間に含まれるページのみを一覧表示します。", "apihelp-query+transcludedin-param-limit": "返す数。", - "apihelp-query+transcludedin-example-simple": "Main Page をトランスクルードしているページの一覧を取得する。", + "apihelp-query+transcludedin-example-simple": "Main Page を参照読み込みしているページの一覧を取得する。", "apihelp-query+transcludedin-example-generator": "Main Page を参照読み込みしているページに関する情報を取得する。", "apihelp-query+usercontribs-summary": "利用者によるすべての編集を取得します。", "apihelp-query+usercontribs-param-limit": "返す投稿記録の最大数。", diff --git a/includes/api/i18n/mk.json b/includes/api/i18n/mk.json index fa4110e0f4..26c7f10f17 100644 --- a/includes/api/i18n/mk.json +++ b/includes/api/i18n/mk.json @@ -17,9 +17,9 @@ "apihelp-main-param-servedby": "Вклучи го домаќинското име што го услужило барањето во исходот.", "apihelp-main-param-curtimestamp": "Вклучи тековно време и време и датум во исходот.", "apihelp-main-param-origin": "Кога му пристапувате на Пирлогот користејќи повеќедоменско AJAX-барање (CORS), задајте му го на ова изворниот домен. Ова мора да се вклучи во секое подготвително барање и затоа мора да биде дел од URI на барањето (не главната содржина во POST). Ова мора точно да се совпаѓа со еден од изворниците на заглавието Origin:, така што мора да е зададен на нешто како https://mk.wikipedia.org or https://meta.wikimedia.org. Ако овој параметар не се совпаѓа со заглавието Origin:, ќе се појави одговор 403. Ако се совпаѓа, а изворникот е на бел список (на допуштени), тогаш ќе се зададе заглавието Access-Control-Allow-Origin.", - "apihelp-main-param-uselang": "Јазик за преведување на пораките. Список на јазични кодови ќе најдете на [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] со siprop=languages или укажете user за да го користите тековно зададениот јазик корисникот, или пак укажете content за да го користите јазикот на содржината на ова вики.", + "apihelp-main-param-uselang": "Јазик за преведување на пораките. [[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]] со siprop=languages дава список на јазични кодови, или укажете user за да го користите тековно зададениот јазик корисникот, или пак укажете content за да го користите јазикот на содржината на ова вики.", "apihelp-block-summary": "Блокирај корисник.", - "apihelp-block-param-user": "Корисничко име, IP-адреса или IP-опсег ако сакате да блокирате.", + "apihelp-block-param-user": "Корисничко име, IP-адреса или IP-опсег ако сакате да блокирате. Не може да се користи заедно со $1userid", "apihelp-block-param-expiry": "Време на истек. Може да биде релативно (на пр. 5 months или „2 недели“) или пак апсолутно (на пр. 2014-09-18T12:34:56Z). Ако го зададете infinite, indefinite или never, блокот ќе трае засекогаш.", "apihelp-block-param-reason": "Причина за блокирање.", "apihelp-block-param-anononly": "Блокирај само анонимни корисници (т.е. оневозможи анонимно уредување од оваа IP-адреса).", @@ -30,6 +30,7 @@ "apihelp-block-param-allowusertalk": "Овозможи му на корисникот да ја уредува неговата разговорна страница (зависи од [[mw:Special:MyLanguage/Manual:$wgBlockAllowsUTEdit|$wgBlockAllowsUTEdit]]).", "apihelp-block-param-reblock": "Ако корисникот е веќе блокиран, наметни врз постоечкиот блок.", "apihelp-block-param-watchuser": "Набљудувај ја корисничката страница и разговорна страница на овој корисник или IP-адреса", + "apihelp-block-param-tags": "Ознаки за примена врз ставката во дневникот на блокирања.", "apihelp-block-example-ip-simple": "Блокирај ја IP-адресата 192.0.2.5 три дена со причината Прва опомена.", "apihelp-block-example-user-complex": "Блокирај го корисникот Vandal (Вандал) бесконечно со причината Vandal (Вандализам) и оневозможи создавање на нови сметки и праќање е-пошта.", "apihelp-checktoken-summary": "Проверка на полноважноста на шифрата од [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", @@ -187,6 +188,7 @@ "apihelp-login-example-login": "Најава", "apihelp-logout-summary": "Одјави се и исчисти ги податоците на седницата.", "apihelp-logout-example-logout": "Одјави го тековниот корисник", + "apihelp-mergehistory-summary": "Спојување на истории на страници.", "apihelp-move-summary": "Премести страница.", "apihelp-move-param-from": "Наслов на страницата што треба да се премести. Не може да се користи заедно со $1fromid.", "apihelp-move-param-fromid": "Назнака на страницата што треба да се премести. Не може да се користи заедно со $1from.", @@ -252,10 +254,19 @@ "apihelp-query+allcategories-param-from": "Од која категорија да почне набројувањето.", "apihelp-query+allcategories-param-to": "На која категорија да запре набројувањето.", "apihelp-query+allcategories-param-dir": "Насока на подредувањето.", + "apihelp-query+allcategories-param-prop": "Кои својства да се дадат:", "apihelp-query+alldeletedrevisions-param-from": "Почни го исписот од овој наслов.", "apihelp-query+alldeletedrevisions-param-to": "Запри го исписот на овој наслов.", "apihelp-query+alldeletedrevisions-example-user": "Список на последните 50 избришани придонеси на корисникот Example.", "apihelp-query+alldeletedrevisions-example-ns-main": "Список на последните 50 избришани преработки во главниот именски простор.", + "apihelp-query+allfileusages-param-prop": "Кои информации да се вклучат:", + "apihelp-query+allfileusages-paramvalue-prop-title": "Го додава насловот на податотеката.", + "apihelp-query+allfileusages-param-limit": "Колку вкупно ставки да се дадат.", + "apihelp-query+allfileusages-param-dir": "Насока на исписот.", + "apihelp-query+allfileusages-example-unique": "Испиши единствени наслови на податотеки.", + "apihelp-query+allfileusages-example-unique-generator": "Ги дава сите наслови на податотеки, означувајќи ги отсутните.", + "apihelp-query+allfileusages-example-generator": "Дава страници што ги содржат податотеките.", + "apihelp-query+allimages-param-dir": "Насока на исписот.", "apihelp-query+allimages-example-b": "Прикажи список на податотеки што почнуваат со буквата B.", "apihelp-query+allimages-example-recent": "Прикажи список на неодамна подигнати податотеки сличен на [[Special:NewFiles]]", "apihelp-query+allimages-example-generator": "Прикажи информации за околу 4 податотеки што почнуваат со буквата T.", @@ -294,6 +305,30 @@ "apihelp-query+allpages-param-minsize": "Ограничи на страници со барем олку бајти.", "apihelp-query+allpages-param-maxsize": "Ограничи на страници со највеќе олку бајти.", "apihelp-query+allpages-param-prtype": "Ограничи на само заштитени страници.", + "apihelp-query+allpages-param-limit": "Колку вкупно страници да се дадат.", + "apihelp-query+allpages-param-dir": "Насока на исписот.", + "apihelp-query+allredirects-param-prop": "Кои информации да се вклучат:", + "apihelp-query+allredirects-paramvalue-prop-title": "Го додава насловот на пренасочувањето.", + "apihelp-query+allredirects-param-namespace": "Именскиот простор што се набројува.", + "apihelp-query+allredirects-param-limit": "Колку вкупно ставки да се дадат.", + "apihelp-query+allredirects-param-dir": "Насока на исписот.", + "apihelp-query+allrevisions-param-start": "Од кој датум и време да почне набројувањето.", + "apihelp-query+allrevisions-param-end": "На кој датум и време да запре набројувањето.", + "apihelp-query+alltransclusions-param-prop": "Кои информации да се вклучат:", + "apihelp-query+alltransclusions-param-namespace": "Именскиот простор што се набројува.", + "apihelp-query+alltransclusions-param-limit": "Колку вкупно ставки да се дадат.", + "apihelp-query+alltransclusions-param-dir": "Насока на исписот.", + "apihelp-query+allusers-param-from": "Од кое корисничко име да почне набројувањето.", + "apihelp-query+allusers-param-to": "На кое корисничко име да престане набројувањето.", + "apihelp-query+allusers-param-prefix": "Пребарај ги сите корисници што почнуваат со оваа вредност.", + "apihelp-query+allusers-param-dir": "Насока на подредувањето.", + "apihelp-query+allusers-param-group": "Вклучи ги корисниците само од дадените групи.", + "apihelp-query+allusers-param-excludegroup": "Исклучи ги корисниците од дадените групи.", + "apihelp-query+allusers-param-prop": "Кои информации да се вклучат:", + "apihelp-query+allusers-paramvalue-prop-blockinfo": "Ги додава информациите за тековното блокирање на корисникот.", + "apihelp-query+allusers-param-limit": "Колку вкупно кориснички имиња да се дадат.", + "apihelp-query+backlinks-param-namespace": "Именскиот простор што се набројува.", + "apihelp-query+backlinks-param-dir": "Насока на исписот.", "apihelp-query+backlinks-example-simple": "Прикажи врски до Main page.", "apihelp-query+backlinks-example-generator": "Дава информации за страниците што водат до Main page.", "apihelp-query+blocks-summary": "Список на сите блокирани корисници и IP-адреси", @@ -301,17 +336,98 @@ "apihelp-query+blocks-param-end": "На кој датум и време да запре набројувањето.", "apihelp-query+blocks-param-ids": "Список на назнаки на блоковите за испис (незадолжително)", "apihelp-query+blocks-param-users": "Список на корисници што ќе се пребаруваат (незадолжително)", + "apihelp-query+blocks-param-prop": "Кои својства да се дадат:", + "apihelp-query+categories-param-dir": "Насока на исписот.", + "apihelp-query+categorymembers-param-prop": "Кои информации да се вклучат:", + "apihelp-query+categorymembers-param-limit": "Највеќе страници за прикажување.", "apihelp-query+deletedrevs-paraminfo-modes": "{{PLURAL:$1|Режим|Режими}}: $2", + "apihelp-query+deletedrevs-param-start": "Од кој датум и време да почне набројувањето.", + "apihelp-query+deletedrevs-param-end": "На кој датум и време да запре набројувањето.", + "apihelp-query+deletedrevs-param-from": "Почни го исписот од овој наслов.", + "apihelp-query+deletedrevs-param-to": "Запри го исписот на овој наслов.", + "apihelp-query+deletedrevs-param-prefix": "Пребарај ги сите наслови на страници што почнуваат со оваа вредност.", + "apihelp-query+disabled-summary": "Овој модул за барања е оневозможен.", + "apihelp-query+duplicatefiles-param-dir": "Насока на исписот.", + "apihelp-query+embeddedin-param-namespace": "Именскиот простор што се набројува.", + "apihelp-query+embeddedin-param-dir": "Насока на исписот.", + "apihelp-query+embeddedin-param-filterredir": "Како да се филтрираат пренасочувањата.", + "apihelp-query+embeddedin-param-limit": "Колку вкупно страници да се дадат.", + "apihelp-query+extlinks-param-limit": "Колку врски да се дадат.", + "apihelp-query+exturlusage-param-prop": "Кои информации да се вклучат:", + "apihelp-query+exturlusage-param-limit": "Колку страници да се дадат.", + "apihelp-query+filearchive-param-from": "Наслов на сликата од која ќе почне набројувањето.", + "apihelp-query+filearchive-param-to": "Наслов на сликата на која ќе запре набројувањето.", + "apihelp-query+filearchive-param-prefix": "Пребарај ги сите наслови на слики што почнуваат со оваа вредност.", + "apihelp-query+filearchive-param-limit": "Колку вкупно слики да се дадат.", + "apihelp-query+filearchive-param-dir": "Насока на исписот.", + "apihelp-query+filearchive-param-prop": "Кои информации за слики да се дадат:", + "apihelp-query+filearchive-example-simple": "Прикажи список на сите избришани податотеки.", + "apihelp-query+fileusage-param-prop": "Кои својства да се дадат:", + "apihelp-query+imageinfo-param-prop": "Кои информации за податотеки да се дадат:", "apihelp-query+imageinfo-param-urlheight": "Слично на $1urlwidth.", + "apihelp-query+images-param-limit": "Колку податотеки да се дадат.", + "apihelp-query+images-param-dir": "Насока на исписот.", + "apihelp-query+imageusage-param-namespace": "Именскиот простор што се набројува.", + "apihelp-query+imageusage-param-dir": "Насока на исписот.", + "apihelp-query+iwbacklinks-param-limit": "Колку вкупно страници да се дадат.", + "apihelp-query+iwbacklinks-param-prop": "Кои својства да се дадат:", + "apihelp-query+iwbacklinks-param-dir": "Насока на исписот.", + "apihelp-query+iwlinks-param-dir": "Насока на исписот.", + "apihelp-query+langbacklinks-param-limit": "Колку вкупно страници да се дадат.", + "apihelp-query+langbacklinks-param-prop": "Кои својства да се дадат:", + "apihelp-query+langbacklinks-param-dir": "Насока на исписот.", + "apihelp-query+langlinks-param-dir": "Насока на исписот.", + "apihelp-query+links-param-limit": "Колку врски да се дадат.", + "apihelp-query+links-param-dir": "Насока на исписот.", + "apihelp-query+linkshere-param-prop": "Кои својства да се дадат:", + "apihelp-query+logevents-param-prop": "Кои својства да се дадат:", + "apihelp-query+logevents-param-start": "Од кој датум и време да почне набројувањето.", + "apihelp-query+logevents-param-end": "На кој датум и време да запре набројувањето.", + "apihelp-query+pageswithprop-param-prop": "Кои информации да се вклучат:", + "apihelp-query+pageswithprop-param-limit": "Највеќе страници за прикажување.", + "apihelp-query+prefixsearch-param-search": "Низа за пребарување.", + "apihelp-query+prefixsearch-param-limit": "Највеќе ставки во исходот за прикажување.", + "apihelp-query+protectedtitles-param-limit": "Колку вкупно страници да се дадат.", + "apihelp-query+protectedtitles-param-prop": "Кои својства да се дадат:", + "apihelp-query+random-param-filterredir": "Како да се филтрираат пренасочувањата.", + "apihelp-query+recentchanges-param-start": "Од кој датум и време да почне набројувањето.", + "apihelp-query+recentchanges-param-end": "На кој датум и време да запре набројувањето.", + "apihelp-query+recentchanges-param-limit": "Колку вкупно промени да се дадат.", + "apihelp-query+redirects-param-prop": "Кои својства да се дадат:", + "apihelp-query+redirects-param-limit": "Колку пренасочувања да се дадат.", + "apihelp-query+redirects-example-simple": "Дај список на пренасочувања до [[Main Page|Главната страница]].", "apihelp-query+revisions-example-last5": "Дај ги последните 5 преработки на Главна страница.", "apihelp-query+revisions-example-first5": "Дај ги првите 5 преработки на Главна страница.", "apihelp-query+revisions-example-first5-after": "Дај ги првите 5 преработки на Главна страница направени по 2006-05-01 (1 мај 2006 г.)", "apihelp-query+revisions-example-first5-not-localhost": "Дај ги првите 5 преработки на Главна страница кои не се направени од анонимниот корисник „127.0.0.1“", "apihelp-query+revisions-example-first5-user": "Дај ги првите 5 преработки на Главна страница кои се направени од корисникот „зададен од МедијаВики“ (MediaWiki default)", + "apihelp-query+search-param-namespace": "Пребарување само во овие именски простори.", + "apihelp-query+search-param-info": "Кои метаподатоци да се дадат.", + "apihelp-query+search-param-prop": "Кои својства да се дадат:", + "apihelp-query+search-paramvalue-prop-score": "Занемарено.", + "apihelp-query+search-paramvalue-prop-hasrelated": "Занемарено.", + "apihelp-query+search-param-limit": "Колку вкупно страници да се дадат.", "apihelp-query+search-example-simple": "Побарај meaning.", "apihelp-query+search-example-text": "Побарај го meaning по текстовите.", "apihelp-query+search-example-generator": "Дај информации за страниците што излегуваат во исходот од пребарувањето на meaning.", "apihelp-query+siteinfo-summary": "Дај општи информации за мрежното место.", + "apihelp-query+siteinfo-param-prop": "Кои информации да се дадат:", + "apihelp-query+tags-param-limit": "Најголемиот број на ознаки за наведување во списокот.", + "apihelp-query+tags-param-prop": "Кои својства да се дадат:", + "apihelp-query+templates-param-limit": "Колку шаблони да се дадат.", + "apihelp-query+templates-param-dir": "Насока на исписот.", + "apihelp-query+transcludedin-param-prop": "Кои својства да се дадат:", + "apihelp-query+usercontribs-paramvalue-prop-patrolled": "Ги означува проверените уредувања.", + "apihelp-query+usercontribs-paramvalue-prop-autopatrolled": "Ги означува самопроверените уредувања.", + "apihelp-query+userinfo-param-prop": "Кои информации да се вклучат:", + "apihelp-query+users-param-prop": "Кои информации да се вклучат:", + "apihelp-query+watchlist-param-start": "Од кој датум и време да почне набројувањето.", + "apihelp-query+watchlist-param-end": "На кој датум и време да запре набројувањето.", + "apihelp-query+watchlist-paramvalue-type-new": "Создавања на страници.", + "apihelp-query+watchlist-paramvalue-type-log": "Дневнички записи.", + "apihelp-query+watchlistraw-param-dir": "Насока на исписот.", + "apihelp-revisiondelete-param-suppress": "Дали се притајуваат податоци од администраторите на ист начин како и за останатите.", + "apihelp-revisiondelete-param-tags": "Ознаки за примена врз ставката во дневникот на бришења.", "apihelp-upload-param-filename": "Целно име на податотеката.", "apihelp-upload-param-comment": "Коментар при подигање. Се користи и како првичен текст на страницата за нови податотеки ако не е укажано $1text.", "apihelp-upload-param-text": "Првичен текст на страницата за нови податотеки.", diff --git a/includes/api/i18n/pt-br.json b/includes/api/i18n/pt-br.json index 1e0d508f6c..ef03a3dde5 100644 --- a/includes/api/i18n/pt-br.json +++ b/includes/api/i18n/pt-br.json @@ -415,6 +415,7 @@ "apihelp-query-param-indexpageids": "Inclua uma seção adicional de pageids listando todas as IDs de página retornadas.", "apihelp-query-param-export": "Exporte as revisões atuais de todas as páginas dadas ou geradas.", "apihelp-query-param-exportnowrap": "Retorna o XML de exportação sem envolvê-lo em um resultado XML (mesmo formato que [[Special:Export]]). Só pode ser usado com $1export.", + "apihelp-query-param-exportschema": "Segmente a versão fornecida do formato de dump XML ao exportar. Só pode ser usado com $1export.", "apihelp-query-param-iwurl": "Obter o URL completo se o título for um link interwiki.", "apihelp-query-param-rawcontinue": "Retorne os dados de query-continue para continuar.", "apihelp-query-example-revisions": "Obter [[Special:ApiHelp/query+siteinfo|site info]] e [[Special:ApiHelp/query+revisions|revisions]] da Main Page.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index 279c0c18db..d5de23f9bf 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -416,6 +416,7 @@ "apihelp-query-param-indexpageids": "{{doc-apihelp-param|query|indexpageids}}", "apihelp-query-param-export": "{{doc-apihelp-param|query|export}}", "apihelp-query-param-exportnowrap": "{{doc-apihelp-param|query|exportnowrap}}", + "apihelp-query-param-exportschema": "{{doc-apihelp-param|query|exportschema}}", "apihelp-query-param-iwurl": "{{doc-apihelp-param|query|iwurl}}", "apihelp-query-param-rawcontinue": "{{doc-apihelp-param|query|rawcontinue}}", "apihelp-query-example-revisions": "{{doc-apihelp-example|query}}", diff --git a/includes/api/i18n/zh-hant.json b/includes/api/i18n/zh-hant.json index a1d7cb9be8..9647675dae 100644 --- a/includes/api/i18n/zh-hant.json +++ b/includes/api/i18n/zh-hant.json @@ -912,6 +912,10 @@ "apihelp-query+langlinks-param-dir": "列出時所採用的方向。", "apihelp-query+langlinks-param-inlanguagecode": "用於本地化語言名稱的語言代碼。", "apihelp-query+langlinks-example-simple": "從頁面 Main Page 取得跨語言連結。", + "apihelp-query+languageinfo-paramvalue-prop-bcp47": "BCP-47 語言代碼。", + "apihelp-query+languageinfo-paramvalue-prop-autonym": "語言的本語稱呼,也就是該語言用自己語言本身寫出的名稱。", + "apihelp-query+languageinfo-example-simple": "取得所有支援語言的語言代碼。", + "apihelp-query+languageinfo-example-autonym-name-de": "取得所有支援語言的本語稱呼和德語名稱。", "apihelp-query+links-summary": "回傳指定頁面的所有連結。", "apihelp-query+links-param-namespace": "僅顯示在這些命名空間的連結。", "apihelp-query+links-param-limit": "要回傳的連結數量。", diff --git a/includes/block/AbstractBlock.php b/includes/block/AbstractBlock.php index c7ba68daa2..0357f8d29d 100644 --- a/includes/block/AbstractBlock.php +++ b/includes/block/AbstractBlock.php @@ -84,7 +84,7 @@ abstract class AbstractBlock { * timestamp string The time at which the block comes into effect * byText string Username of the blocker (for foreign users) */ - function __construct( $options = [] ) { + public function __construct( array $options = [] ) { $defaults = [ 'address' => '', 'by' => null, diff --git a/includes/block/BlockManager.php b/includes/block/BlockManager.php index abd2db24af..dc07364e15 100644 --- a/includes/block/BlockManager.php +++ b/includes/block/BlockManager.php @@ -76,23 +76,23 @@ class BlockManager { * @param bool $applyIpBlocksToXff * @param bool $cookieSetOnAutoblock * @param bool $cookieSetOnIpBlock - * @param array $dnsBlacklistUrls + * @param string[] $dnsBlacklistUrls * @param bool $enableDnsBlacklist - * @param array $proxyList - * @param array $proxyWhitelist + * @param string[] $proxyList + * @param string[] $proxyWhitelist * @param string $secretKey * @param array $softBlockRanges */ public function __construct( - $currentUser, - $currentRequest, + User $currentUser, + WebRequest $currentRequest, $applyIpBlocksToXff, $cookieSetOnAutoblock, $cookieSetOnIpBlock, - $dnsBlacklistUrls, + array $dnsBlacklistUrls, $enableDnsBlacklist, - $proxyList, - $proxyWhitelist, + array $proxyList, + array $proxyWhitelist, $secretKey, $softBlockRanges ) { @@ -232,7 +232,7 @@ class BlockManager { * @param AbstractBlock[] $blocks * @return AbstractBlock[] */ - private function getUniqueBlocks( $blocks ) { + private function getUniqueBlocks( array $blocks ) { $systemBlocks = []; $databaseBlocks = []; diff --git a/includes/block/BlockRestrictionStore.php b/includes/block/BlockRestrictionStore.php index 88c0951b03..df09eadc8f 100644 --- a/includes/block/BlockRestrictionStore.php +++ b/includes/block/BlockRestrictionStore.php @@ -26,6 +26,7 @@ use MediaWiki\Block\Restriction\NamespaceRestriction; use MediaWiki\Block\Restriction\PageRestriction; use MediaWiki\Block\Restriction\Restriction; use MWException; +use stdClass; use Wikimedia\Rdbms\IResultWrapper; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\ILoadBalancer; @@ -45,8 +46,8 @@ class BlockRestrictionStore { */ private $loadBalancer; - /* - * @param LoadBalancer $loadBalancer load balancer for acquiring database connections + /** + * @param ILoadBalancer $loadBalancer load balancer for acquiring database connections */ public function __construct( ILoadBalancer $loadBalancer ) { $this->loadBalancer = $loadBalancer; @@ -224,7 +225,7 @@ class BlockRestrictionStore { * Delete the restrictions. * * @since 1.33 - * @param Restriction[]|null $restrictions + * @param Restriction[] $restrictions * @throws MWException * @return bool */ @@ -435,10 +436,10 @@ class BlockRestrictionStore { /** * Convert a result row from the database into a restriction object. * - * @param \stdClass $row + * @param stdClass $row * @return Restriction|null */ - private function rowToRestriction( \stdClass $row ) { + private function rowToRestriction( stdClass $row ) { if ( array_key_exists( (int)$row->ir_type, $this->types ) ) { $class = $this->types[ (int)$row->ir_type ]; return call_user_func( [ $class, 'newFromRow' ], $row ); diff --git a/includes/block/CompositeBlock.php b/includes/block/CompositeBlock.php index 8efd7de956..e0f69dda8a 100644 --- a/includes/block/CompositeBlock.php +++ b/includes/block/CompositeBlock.php @@ -43,7 +43,7 @@ class CompositeBlock extends AbstractBlock { * @param array $options Parameters of the block: * originalBlocks Block[] Blocks that this block is composed from */ - function __construct( $options = [] ) { + public function __construct( array $options = [] ) { parent::__construct( $options ); $defaults = [ diff --git a/includes/block/DatabaseBlock.php b/includes/block/DatabaseBlock.php index 0f193240a0..fbf9a073ca 100644 --- a/includes/block/DatabaseBlock.php +++ b/includes/block/DatabaseBlock.php @@ -265,7 +265,7 @@ class DatabaseBlock extends AbstractBlock { * Load blocks from the database which target the specific target exactly, or which cover the * vague target. * - * @param User|String|null $specificTarget + * @param User|string|null $specificTarget * @param int|null $specificType * @param bool $fromMaster * @param User|string|null $vagueTarget Also search for blocks affecting this target. Doesn't @@ -369,7 +369,7 @@ class DatabaseBlock extends AbstractBlock { * @param DatabaseBlock[] $blocks These should not include autoblocks or ID blocks * @return DatabaseBlock|null The block with the most specific target */ - protected static function chooseMostSpecificBlock( $blocks ) { + protected static function chooseMostSpecificBlock( array $blocks ) { if ( count( $blocks ) === 1 ) { return $blocks[0]; } @@ -535,7 +535,7 @@ class DatabaseBlock extends AbstractBlock { * @return bool|array False on failure, assoc array on success: * ('id' => block ID, 'autoIds' => array of autoblock IDs) */ - public function insert( $dbw = null ) { + public function insert( IDatabase $dbw = null ) { global $wgBlockDisablesLogin; if ( !$this->getBlocker() || $this->getBlocker()->getName() === '' ) { diff --git a/includes/block/SystemBlock.php b/includes/block/SystemBlock.php index 2a8c663b8b..029e0b8741 100644 --- a/includes/block/SystemBlock.php +++ b/includes/block/SystemBlock.php @@ -45,7 +45,7 @@ class SystemBlock extends AbstractBlock { * in the database. Value is a string to return * from self::getSystemBlockType(). */ - function __construct( $options = [] ) { + public function __construct( array $options = [] ) { parent::__construct( $options ); $defaults = [ diff --git a/includes/cache/LinkBatch.php b/includes/cache/LinkBatch.php index 2573f8ac49..3edfe1b9e9 100644 --- a/includes/cache/LinkBatch.php +++ b/includes/cache/LinkBatch.php @@ -146,7 +146,7 @@ class LinkBatch { } /** - * Add a ResultWrapper containing IDs and titles to a LinkCache object. + * Add a result wrapper containing IDs and titles to a LinkCache object. * As normal, titles will go into the static Title cache field. * This function *also* stores extra fields of the title used for link * parsing to avoid extra DB queries. @@ -187,7 +187,7 @@ class LinkBatch { } /** - * Perform the existence test query, return a ResultWrapper with page_id fields + * Perform the existence test query, return a result wrapper with page_id fields * @return bool|IResultWrapper */ public function doQuery() { diff --git a/includes/changes/ChangesFeed.php b/includes/changes/ChangesFeed.php index bb9114aa62..69c709c258 100644 --- a/includes/changes/ChangesFeed.php +++ b/includes/changes/ChangesFeed.php @@ -93,7 +93,7 @@ class ChangesFeed { $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); foreach ( $sorted as $obj ) { $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title ); - $talkpage = $nsInfo->hasTalkNamespace( $obj->rc_namespace ) + $talkpage = $nsInfo->hasTalkNamespace( $obj->rc_namespace ) && $title->isValid() ? $title->getTalkPage()->getFullURL() : ''; diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 5df7aef620..c716e4d047 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -21,6 +21,7 @@ * @ingroup Database */ +use Wikimedia\AtEase\AtEase; use Wikimedia\Timestamp\ConvertibleTimestamp; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\DatabaseDomain; @@ -68,10 +69,10 @@ class DatabaseOracle extends Database { } function __destruct() { - if ( $this->opened ) { - Wikimedia\suppressWarnings(); + if ( $this->conn ) { + AtEase::suppressWarnings(); $this->close(); - Wikimedia\restoreWarnings(); + AtEase::restoreWarnings(); } } @@ -159,8 +160,6 @@ class DatabaseOracle extends Database { throw new DBConnectionError( $this, $this->lastError() ); } - $this->opened = true; - # removed putenv calls because they interfere with the system globaly $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' ); $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' ); @@ -179,7 +178,7 @@ class DatabaseOracle extends Database { } function execFlags() { - return $this->trxLevel ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS; + return $this->trxLevel() ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS; } /** @@ -549,7 +548,7 @@ class DatabaseOracle extends Database { } } - if ( !$this->trxLevel ) { + if ( !$this->trxLevel() ) { oci_commit( $this->conn ); } @@ -943,26 +942,24 @@ class DatabaseOracle extends Database { } protected function doBegin( $fname = __METHOD__ ) { - $this->trxLevel = 1; - $this->doQuery( 'SET CONSTRAINTS ALL DEFERRED' ); + $this->query( 'SET CONSTRAINTS ALL DEFERRED' ); } protected function doCommit( $fname = __METHOD__ ) { - if ( $this->trxLevel ) { + if ( $this->trxLevel() ) { $ret = oci_commit( $this->conn ); if ( !$ret ) { throw new DBUnexpectedError( $this, $this->lastError() ); } - $this->trxLevel = 0; - $this->doQuery( 'SET CONSTRAINTS ALL IMMEDIATE' ); + $this->query( 'SET CONSTRAINTS ALL IMMEDIATE' ); } } protected function doRollback( $fname = __METHOD__ ) { - if ( $this->trxLevel ) { + if ( $this->trxLevel() ) { oci_rollback( $this->conn ); - $this->trxLevel = 0; - $this->doQuery( 'SET CONSTRAINTS ALL IMMEDIATE' ); + $ignoreErrors = true; + $this->query( 'SET CONSTRAINTS ALL IMMEDIATE', $fname, $ignoreErrors ); } } @@ -1339,7 +1336,7 @@ class DatabaseOracle extends Database { } } - if ( !$this->trxLevel ) { + if ( !$this->trxLevel() ) { oci_commit( $this->conn ); } diff --git a/includes/export/WikiExporter.php b/includes/export/WikiExporter.php index 8b42be1315..f834fb1e5e 100644 --- a/includes/export/WikiExporter.php +++ b/includes/export/WikiExporter.php @@ -27,7 +27,8 @@ * @defgroup Dump Dump */ -use Wikimedia\Rdbms\ResultWrapper; +use MediaWiki\MediaWikiServices as MediaWikiServicesAlias; +use Wikimedia\Rdbms\IResultWrapper; use Wikimedia\Rdbms\IDatabase; /** @@ -52,8 +53,8 @@ class WikiExporter { const LOGS = 8; const RANGE = 16; - const TEXT = 0; - const STUB = 1; + const TEXT = XmlDumpWriter::WRITE_CONTENT; + const STUB = XmlDumpWriter::WRITE_STUB; const BATCH_SIZE = 50000; @@ -339,18 +340,28 @@ class WikiExporter { ); } - $revOpts = [ 'page' ]; - - $revQuery = Revision::getQueryInfo( $revOpts ); + $revQuery = MediaWikiServicesAlias::getInstance()->getRevisionStore()->getQueryInfo( + [ 'page' ] + ); + $slotQuery = MediaWikiServicesAlias::getInstance()->getRevisionStore()->getSlotsQueryInfo( + [ 'content' ] + ); - // We want page primary rather than revision + // We want page primary rather than revision. + // We also want to join in the slots and content tables. + // NOTE: This means we may get multiple rows per revision, and more rows + // than the batch size! Should be ok, since the max number of slots is + // fixed and low (dozens at worst). $tables = array_merge( [ 'page' ], array_diff( $revQuery['tables'], [ 'page' ] ) ); + $tables = array_merge( $tables, array_diff( $slotQuery['tables'], $tables ) ); $join = $revQuery['joins'] + [ - 'revision' => $revQuery['joins']['page'] + 'revision' => $revQuery['joins']['page'], + 'slots' => [ 'JOIN', [ 'slot_revision_id = rev_id' ] ], + 'content' => [ 'JOIN', [ 'content_id = slot_content_id' ] ], ]; unset( $join['page'] ); - $fields = $revQuery['fields']; + $fields = array_merge( $revQuery['fields'], $slotQuery['fields'] ); $fields[] = 'page_restrictions'; if ( $this->text != self::STUB ) { @@ -387,7 +398,6 @@ class WikiExporter { # Full history dumps... # query optimization for history stub dumps if ( $this->text == self::STUB ) { - $tables = $revQuery['tables']; $opts[] = 'STRAIGHT_JOIN'; $opts['USE INDEX']['revision'] = 'rev_page_id'; unset( $join['revision'] ); @@ -464,24 +474,36 @@ class WikiExporter { } /** - * Runs through a query result set dumping page and revision records. - * The result set should be sorted/grouped by page to avoid duplicate - * page records in the output. + * Runs through a query result set dumping page, revision, and slot records. + * The result set should join the page, revision, slots, and content tables, + * and be sorted/grouped by page and revision to avoid duplicate page records in the output. * - * @param ResultWrapper $results + * @param IResultWrapper $results * @param object $lastRow the last row output from the previous call (or null if none) * @return object the last row processed */ protected function outputPageStreamBatch( $results, $lastRow ) { - foreach ( $results as $row ) { + $rowCarry = null; + while ( true ) { + $slotRows = $this->getSlotRowBatch( $results, $rowCarry ); + + if ( !$slotRows ) { + break; + } + + // All revision info is present in all slot rows. + // Use the first slot row as the revision row. + $revRow = $slotRows[0]; + if ( $this->limitNamespaces && - !in_array( $row->page_namespace, $this->limitNamespaces ) ) { - $lastRow = $row; + !in_array( $revRow->page_namespace, $this->limitNamespaces ) ) { + $lastRow = $revRow; continue; } + if ( $lastRow === null || - $lastRow->page_namespace !== $row->page_namespace || - $lastRow->page_title !== $row->page_title ) { + $lastRow->page_namespace !== $revRow->page_namespace || + $lastRow->page_title !== $revRow->page_title ) { if ( $lastRow !== null ) { $output = ''; if ( $this->dumpUploads ) { @@ -490,17 +512,52 @@ class WikiExporter { $output .= $this->writer->closePage(); $this->sink->writeClosePage( $output ); } - $output = $this->writer->openPage( $row ); - $this->sink->writeOpenPage( $row, $output ); + $output = $this->writer->openPage( $revRow ); + $this->sink->writeOpenPage( $revRow, $output ); } - $output = $this->writer->writeRevision( $row ); - $this->sink->writeRevision( $row, $output ); - $lastRow = $row; + $output = $this->writer->writeRevision( $revRow, $slotRows ); + $this->sink->writeRevision( $revRow, $output ); + $lastRow = $revRow; + } + + if ( $rowCarry ) { + throw new LogicException( 'Error while processing a stream of slot rows' ); } return $lastRow; } + /** + * Returns all slot rows for a revision. + * Takes and returns a carry row from the last batch; + * + * @param IResultWrapper|array $results + * @param null|object &$carry A row carried over from the last call to getSlotRowBatch() + * + * @return object[] + */ + protected function getSlotRowBatch( $results, &$carry = null ) { + $slotRows = []; + $prev = null; + + if ( $carry ) { + $slotRows[] = $carry; + $prev = $carry; + $carry = null; + } + + while ( $row = $results->fetchObject() ) { + if ( $prev && $prev->rev_id !== $row->rev_id ) { + $carry = $row; + break; + } + $slotRows[] = $row; + $prev = $row; + } + + return $slotRows; + } + /** * Final page stream output, after all batches are complete * @@ -517,7 +574,7 @@ class WikiExporter { } /** - * @param ResultWrapper $resultset + * @param IResultWrapper $resultset * @return int the log_id value of the last item output, or null if none */ protected function outputLogStream( $resultset ) { diff --git a/includes/export/XmlDumpWriter.php b/includes/export/XmlDumpWriter.php index 0659ec18cb..bedfe133c7 100644 --- a/includes/export/XmlDumpWriter.php +++ b/includes/export/XmlDumpWriter.php @@ -23,21 +23,46 @@ * @file */ use MediaWiki\MediaWikiServices; +use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\RevisionStore; +use MediaWiki\Revision\SlotRecord; +use MediaWiki\Revision\SuppressedDataException; use MediaWiki\Storage\SqlBlobStore; +use Wikimedia\Assert\Assert; /** * @ingroup Dump */ class XmlDumpWriter { + + /** Output serialized revision content. */ + const WRITE_CONTENT = 0; + + /** Only output subs for revision content. */ + const WRITE_STUB = 1; + + /** + * Only output subs for revision content, indicating that the content has been + * deleted/suppressed. For internal use only. + */ + const WRITE_STUB_DELETED = 2; + /** * @var string[] the schema versions supported for output * @final */ public static $supportedSchemas = [ XML_DUMP_SCHEMA_VERSION_10, + XML_DUMP_SCHEMA_VERSION_11 ]; + /** + * @var string which schema version the generated XML should comply to. + * One of the values from self::$supportedSchemas, using the SCHEMA_VERSION_XX + * constants. + */ + private $schemaVersion; + /** * Title of the currently processed page * @@ -45,6 +70,40 @@ class XmlDumpWriter { */ private $currentTitle = null; + /** + * @var int Whether to output revision content or just stubs. WRITE_CONTENT or WRITE_STUB. + */ + private $contentMode; + + /** + * XmlDumpWriter constructor. + * + * @param int $contentMode WRITE_CONTENT or WRITE_STUB. + * @param string $schemaVersion which schema version the generated XML should comply to. + * One of the values from self::$supportedSchemas, using the XML_DUMP_SCHEMA_VERSION_XX + * constants. + */ + public function __construct( + $contentMode = self::WRITE_CONTENT, + $schemaVersion = XML_DUMP_SCHEMA_VERSION_11 + ) { + Assert::parameter( + in_array( $contentMode, [ self::WRITE_CONTENT, self::WRITE_STUB ] ), + '$contentMode', + 'must be one of the following constants: WRITE_CONTENT or WRITE_STUB.' + ); + + Assert::parameter( + in_array( $schemaVersion, self::$supportedSchemas ), + '$schemaVersion', + 'must be one of the following schema versions: ' + . implode( ',', self::$supportedSchemas ) + ); + + $this->contentMode = $contentMode; + $this->schemaVersion = $schemaVersion; + } + /** * Opens the XML output stream's root "" element. * This does not include an xml directive, so is safe to include @@ -56,7 +115,7 @@ class XmlDumpWriter { * @return string */ function openStream() { - $ver = WikiExporter::schemaVersion(); + $ver = $this->schemaVersion; return Xml::element( 'mediawiki', [ 'xmlns' => "http://www.mediawiki.org/xml/export-$ver/", 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance", @@ -177,7 +236,7 @@ class XmlDumpWriter { */ public function openPage( $row ) { $out = " \n"; - $this->currentTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); + $this->currentTitle = Title::newFromRow( $row ); $canonicalTitle = self::canonicalTitle( $this->currentTitle ); $out .= ' ' . Xml::elementClean( 'title', [], $canonicalTitle ) . "\n"; $out .= ' ' . Xml::element( 'ns', [], strval( $row->page_namespace ) ) . "\n"; @@ -237,144 +296,204 @@ class XmlDumpWriter { * data filled in from the given database row. * * @param object $row + * @param null|object[] $slotRows + * * @return string + * @throws FatalError + * @throws MWException * @private */ - function writeRevision( $row ) { + function writeRevision( $row, $slotRows = null ) { + $rev = $this->getRevisionStore()->newRevisionFromRowAndSlots( + $row, + $slotRows, + 0, + $this->currentTitle + ); + $out = " \n"; - $out .= " " . Xml::element( 'id', null, strval( $row->rev_id ) ) . "\n"; - if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) { - $out .= " " . Xml::element( 'parentid', null, strval( $row->rev_parent_id ) ) . "\n"; + $out .= " " . Xml::element( 'id', null, strval( $rev->getId() ) ) . "\n"; + + if ( $rev->getParentId() ) { + $out .= " " . Xml::element( 'parentid', null, strval( $rev->getParentId() ) ) . "\n"; } - $out .= $this->writeTimestamp( $row->rev_timestamp ); + $out .= $this->writeTimestamp( $rev->getTimestamp() ); - if ( isset( $row->rev_deleted ) && ( $row->rev_deleted & Revision::DELETED_USER ) ) { + if ( $rev->isDeleted( Revision::DELETED_USER ) ) { $out .= " " . Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n"; } else { // empty values get written out as uid 0, see T224221 - $out .= $this->writeContributor( $row->rev_user ?: 0, $row->rev_user_text ); + $user = $rev->getUser(); + $out .= $this->writeContributor( + $user ? $user->getId() : 0, + $user ? $user->getName() : '' + ); } - if ( isset( $row->rev_minor_edit ) && $row->rev_minor_edit ) { + if ( $rev->isMinor() ) { $out .= " \n"; } - if ( isset( $row->rev_deleted ) && ( $row->rev_deleted & Revision::DELETED_COMMENT ) ) { + if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) { $out .= " " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n"; } else { - $comment = CommentStore::getStore()->getComment( 'rev_comment', $row )->text; - if ( $comment != '' ) { - $out .= " " . Xml::elementClean( 'comment', [], strval( $comment ) ) . "\n"; - } + $out .= " " + . Xml::elementClean( 'comment', [], strval( $rev->getComment()->text ) ) + . "\n"; + } + + $contentMode = $rev->isDeleted( Revision::DELETED_TEXT ) ? self::WRITE_STUB_DELETED + : $this->contentMode; + + foreach ( $rev->getSlots()->getSlots() as $slot ) { + $out .= $this->writeSlot( $slot, $contentMode ); } - // TODO: rev_content_model no longer exists with MCR, see T174031 - if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model ) ) { - $content_model = strval( $row->rev_content_model ); + if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $out .= " \n"; } else { - // probably using $wgContentHandlerUseDB = false; - $content_model = ContentHandler::getDefaultModelFor( $this->currentTitle ); + $out .= " " . Xml::element( 'sha1', null, strval( $rev->getSha1() ) ) . "\n"; } - $content_handler = ContentHandler::getForModelID( $content_model ); + // Avoid PHP 7.1 warning from passing $this by reference + $writer = $this; + $text = $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW ); + Hooks::run( 'XmlDumpWriterWriteRevision', [ &$writer, &$out, $row, $text, $rev ] ); - // TODO: rev_content_format no longer exists with MCR, see T174031 - if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) { - $content_format = strval( $row->rev_content_format ); - } else { - // probably using $wgContentHandlerUseDB = false; - $content_format = $content_handler->getDefaultFormat(); + $out .= " \n"; + + return $out; + } + + /** + * @param SlotRecord $slot + * @param int $contentMode see the WRITE_XXX constants + * + * @return string + */ + private function writeSlot( SlotRecord $slot, $contentMode ) { + $isMain = $slot->getRole() === SlotRecord::MAIN; + $isV11 = $this->schemaVersion >= XML_DUMP_SCHEMA_VERSION_11; + + if ( !$isV11 && !$isMain ) { + // ignore extra slots + return ''; } - $out .= " " . Xml::element( 'model', null, strval( $content_model ) ) . "\n"; - $out .= " " . Xml::element( 'format', null, strval( $content_format ) ) . "\n"; + $out = ''; + $indent = ' '; - $text = ''; - if ( isset( $row->rev_deleted ) && ( $row->rev_deleted & Revision::DELETED_TEXT ) ) { - $out .= " " . Xml::element( 'text', [ 'deleted' => 'deleted' ] ) . "\n"; - } elseif ( isset( $row->old_text ) ) { - // Raw text from the database may have invalid chars - $text = strval( Revision::getRevisionText( $row ) ); - try { - $text = $content_handler->exportTransform( $text, $content_format ); - } - catch ( Exception $ex ) { - if ( $ex instanceof MWException || $ex instanceof RuntimeException ) { - // leave text as is; that's the way it goes - wfLogWarning( 'exportTransform failed on text for revid ' . $row->rev_id . "\n" ); - } else { - throw $ex; - } - } - $out .= " " . Xml::elementClean( 'text', - [ 'xml:space' => 'preserve', 'bytes' => intval( $row->rev_len ) ], - strval( $text ) ) . "\n"; - } elseif ( isset( $row->_load_content ) ) { - // TODO: make this fully MCR aware, see T174031 - $rev = $this->getRevisionStore()->newRevisionFromRow( $row, 0, $this->currentTitle ); - $slot = $rev->getSlot( 'main' ); - try { - $content = $slot->getContent(); + if ( !$isMain ) { + // non-main slots are wrapped into an additional element. + $out .= ' ' . Xml::openElement( 'content' ) . "\n"; + $indent .= ' '; + $out .= $indent . Xml::element( 'role', null, strval( $slot->getRole() ) ) . "\n"; + } - if ( $content instanceof TextContent ) { - // HACK: For text based models, bypass the serialization step. - // This allows extensions (like Flow)that use incompatible combinations - // of serialization format and content model. - $text = $content->getNativeData(); - } else { - $text = $content->serialize( $content_format ); - } - $text = $content_handler->exportTransform( $text, $content_format ); - $out .= " " . Xml::elementClean( 'text', - [ 'xml:space' => 'preserve', 'bytes' => intval( $slot->getSize() ) ], - strval( $text ) ) . "\n"; + if ( $isV11 ) { + $out .= $indent . Xml::element( 'origin', null, strval( $slot->getOrigin() ) ) . "\n"; + } + + $contentModel = $slot->getModel(); + $contentHandler = ContentHandler::getForModelID( $contentModel ); + $contentFormat = $contentHandler->getDefaultFormat(); + + // XXX: The content format is only relevant when actually outputting serialized content. + // It should probably be an attribute on the text tag. + $out .= $indent . Xml::element( 'model', null, strval( $contentModel ) ) . "\n"; + $out .= $indent . Xml::element( 'format', null, strval( $contentFormat ) ) . "\n"; + + $textAttributes = [ + 'xml:space' => 'preserve', + 'bytes' => $slot->getSize(), + ]; + + if ( $isV11 ) { + $textAttributes['sha1'] = $slot->getSha1(); + } + + if ( $contentMode === self::WRITE_CONTENT ) { + try { + // write tag + $out .= $this->writeText( $slot->getContent(), $textAttributes, $indent ); + } catch ( SuppressedDataException $ex ) { + // NOTE: this shouldn't happen, since the caller is supposed to have checked + // for suppressed content! + // write placeholder tag + $textAttributes['deleted'] = 'deleted'; + $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n"; } catch ( Exception $ex ) { if ( $ex instanceof MWException || $ex instanceof RuntimeException ) { - // there's no provsion in the schema for an attribute that will let + // there's no provision in the schema for an attribute that will let // the user know this element was unavailable due to error; an empty // tag is the best we can do - $out .= " " . Xml::element( 'text' ) . "\n"; - wfLogWarning( 'failed to load content for revid ' . $row->rev_id . "\n" ); + $out .= $indent . Xml::element( 'text' ) . "\n"; + wfLogWarning( + 'failed to load content slot ' . $slot->getRole() . ' for revision ' + . $slot->getRevision() . "\n" + ); } else { throw $ex; } } - } elseif ( isset( $row->rev_text_id ) ) { - // Stub output for pre-MCR schema - // TODO: MCR: rev_text_id only exists in the pre-MCR schema. Remove this when - // we drop support for the old schema. - $out .= " " . Xml::element( 'text', - [ 'id' => $row->rev_text_id, 'bytes' => intval( $row->rev_len ) ], - "" ) . "\n"; + } elseif ( $contentMode === self::WRITE_STUB_DELETED ) { + // write placeholder tag + $textAttributes['deleted'] = 'deleted'; + $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n"; } else { - // Backwards-compatible stub output for MCR aware schema - // TODO: MCR: emit content addresses instead of text ids, see T174031, T199121 - $rev = $this->getRevisionStore()->newRevisionFromRow( $row, 0, $this->currentTitle ); - $slot = $rev->getSlot( 'main' ); + // write stub tag + if ( $isV11 ) { + $textAttributes['location'] = $slot->getAddress(); + } + // Output the numerical text ID if possible, for backwards compatibility. // Note that this is currently the ONLY reason we have a BlobStore here at all. // When removing this line, check whether the BlobStore has become unused. $textId = $this->getBlobStore()->getTextIdFromAddress( $slot->getAddress() ); - $out .= " " . Xml::element( 'text', - [ 'id' => $textId, 'bytes' => intval( $slot->getSize() ) ], - "" ) . "\n"; + if ( $textId ) { + $textAttributes['id'] = $textId; + } elseif ( !$isV11 ) { + throw new InvalidArgumentException( + 'Cannot produce stubs for non-text-table content blobs with schema version ' + . $this->schemaVersion + ); + } + + $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n"; } - if ( isset( $row->rev_sha1 ) - && $row->rev_sha1 - && !( $row->rev_deleted & Revision::DELETED_TEXT ) - ) { - $out .= " " . Xml::element( 'sha1', null, strval( $row->rev_sha1 ) ) . "\n"; - } else { - $out .= " \n"; + if ( !$isMain ) { + $out .= ' ' . Xml::closeElement( 'content' ) . "\n"; } - // Avoid PHP 7.1 warning from passing $this by reference - $writer = $this; - Hooks::run( 'XmlDumpWriterWriteRevision', [ &$writer, &$out, $row, $text ] ); + return $out; + } - $out .= " \n"; + /** + * @param Content $content + * @param string[] $textAttributes + * @param string $indent + * + * @return string + */ + private function writeText( Content $content, $textAttributes, $indent ) { + $out = ''; + + $contentHandler = $content->getContentHandler(); + $contentFormat = $contentHandler->getDefaultFormat(); + + if ( $content instanceof TextContent ) { + // HACK: For text based models, bypass the serialization step. This allows extensions (like Flow) + // that use incompatible combinations of serialization format and content model. + $data = $content->getNativeData(); + } else { + $data = $content->serialize( $contentFormat ); + } + + $data = $contentHandler->exportTransform( $data, $contentFormat ); + $textAttributes['bytes'] = $size = strlen( $data ); // make sure to use the actual size + $out .= $indent . Xml::elementClean( 'text', $textAttributes, strval( $data ) ) . "\n"; return $out; } diff --git a/includes/externalstore/ExternalStore.php b/includes/externalstore/ExternalStore.php index 76f20f0ed3..7c90b35075 100644 --- a/includes/externalstore/ExternalStore.php +++ b/includes/externalstore/ExternalStore.php @@ -44,6 +44,7 @@ use MediaWiki\MediaWikiServices; * as the possibility to have any storage format (i.e. for archives). * * @ingroup ExternalStorage + * @deprecated 1.34 Use ExternalStoreFactory directly instead */ class ExternalStore { /** @@ -52,11 +53,16 @@ class ExternalStore { * @param string $proto Type of external storage, should be a value in $wgExternalStores * @param array $params Associative array of ExternalStoreMedium parameters * @return ExternalStoreMedium|bool The store class or false on error + * @deprecated 1.34 */ public static function getStoreObject( $proto, array $params = [] ) { - return MediaWikiServices::getInstance() - ->getExternalStoreFactory() - ->getStoreObject( $proto, $params ); + try { + return MediaWikiServices::getInstance() + ->getExternalStoreFactory() + ->getStore( $proto, $params ); + } catch ( ExternalStoreException $e ) { + return false; + } } /** @@ -66,59 +72,16 @@ class ExternalStore { * @param array $params Associative array of ExternalStoreMedium parameters * @return string|bool The text stored or false on error * @throws MWException + * @deprecated 1.34 */ public static function fetchFromURL( $url, array $params = [] ) { - $parts = explode( '://', $url, 2 ); - if ( count( $parts ) != 2 ) { - return false; // invalid URL - } - - list( $proto, $path ) = $parts; - if ( $path == '' ) { // bad URL - return false; - } - - $store = self::getStoreObject( $proto, $params ); - if ( $store === false ) { + try { + return MediaWikiServices::getInstance() + ->getExternalStoreAccess() + ->fetchFromURL( $url, $params ); + } catch ( ExternalStoreException $e ) { return false; } - - return $store->fetchFromURL( $url ); - } - - /** - * Fetch data from multiple URLs with a minimum of round trips - * - * @param array $urls The URLs of the text to get - * @return array Map from url to its data. Data is either string when found - * or false on failure. - * @throws MWException - */ - public static function batchFetchFromURLs( array $urls ) { - $batches = []; - foreach ( $urls as $url ) { - $scheme = parse_url( $url, PHP_URL_SCHEME ); - if ( $scheme ) { - $batches[$scheme][] = $url; - } - } - $retval = []; - foreach ( $batches as $proto => $batchedUrls ) { - $store = self::getStoreObject( $proto ); - if ( $store === false ) { - continue; - } - $retval += $store->batchFetchFromURLs( $batchedUrls ); - } - // invalid, not found, db dead, etc. - $missing = array_diff( $urls, array_keys( $retval ) ); - if ( $missing ) { - foreach ( $missing as $url ) { - $retval[$url] = false; - } - } - - return $retval; } /** @@ -131,24 +94,30 @@ class ExternalStore { * @param array $params Associative array of ExternalStoreMedium parameters * @return string|bool The URL of the stored data item, or false on error * @throws MWException + * @deprecated 1.34 */ public static function insert( $url, $data, array $params = [] ) { - $parts = explode( '://', $url, 2 ); - if ( count( $parts ) != 2 ) { - return false; // invalid URL - } + try { + $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory(); + $location = $esFactory->getStoreLocationFromUrl( $url ); - list( $proto, $path ) = $parts; - if ( $path == '' ) { // bad URL + return $esFactory->getStoreForUrl( $url, $params )->store( $location, $data ); + } catch ( ExternalStoreException $e ) { return false; } + } - $store = self::getStoreObject( $proto, $params ); - if ( $store === false ) { - return false; - } else { - return $store->store( $path, $data ); - } + /** + * Fetch data from multiple URLs with a minimum of round trips + * + * @param array $urls The URLs of the text to get + * @return array Map from url to its data. Data is either string when found + * or false on failure. + * @throws MWException + * @deprecated 1.34 + */ + public static function batchFetchFromURLs( array $urls ) { + return MediaWikiServices::getInstance()->getExternalStoreAccess()->fetchFromURLs( $urls ); } /** @@ -161,11 +130,10 @@ class ExternalStore { * @param array $params Map of ExternalStoreMedium::__construct context parameters * @return string The URL of the stored data item * @throws MWException + * @deprecated 1.34 */ public static function insertToDefault( $data, array $params = [] ) { - global $wgDefaultExternalStore; - - return self::insertWithFallback( (array)$wgDefaultExternalStore, $data, $params ); + return MediaWikiServices::getInstance()->getExternalStoreAccess()->insert( $data, $params ); } /** @@ -179,67 +147,12 @@ class ExternalStore { * @param array $params Map of ExternalStoreMedium::__construct context parameters * @return string The URL of the stored data item * @throws MWException + * @deprecated 1.34 */ public static function insertWithFallback( array $tryStores, $data, array $params = [] ) { - $error = false; - while ( count( $tryStores ) > 0 ) { - $index = mt_rand( 0, count( $tryStores ) - 1 ); - $storeUrl = $tryStores[$index]; - wfDebug( __METHOD__ . ": trying $storeUrl\n" ); - list( $proto, $path ) = explode( '://', $storeUrl, 2 ); - $store = self::getStoreObject( $proto, $params ); - if ( $store === false ) { - throw new MWException( "Invalid external storage protocol - $storeUrl" ); - } - - try { - if ( $store->isReadOnly( $path ) ) { - $msg = 'read only'; - } else { - $url = $store->store( $path, $data ); - if ( $url !== false ) { - return $url; // a store accepted the write; done! - } - $msg = 'operation failed'; - } - } catch ( Exception $error ) { - $msg = 'caught exception'; - } - - unset( $tryStores[$index] ); // Don't try this one again! - $tryStores = array_values( $tryStores ); // Must have consecutive keys - wfDebugLog( 'ExternalStorage', - "Unable to store text to external storage $storeUrl ($msg)" ); - } - // All stores failed - if ( $error ) { - throw $error; // rethrow the last error - } else { - throw new MWException( "Unable to store text to external storage" ); - } - } - - /** - * @return bool Whether all the default insertion stores are marked as read-only - * @since 1.31 - */ - public static function defaultStoresAreReadOnly() { - global $wgDefaultExternalStore; - - $tryStores = (array)$wgDefaultExternalStore; - if ( !$tryStores ) { - return false; // no stores exists which can be "read only" - } - - foreach ( $tryStores as $storeUrl ) { - list( $proto, $path ) = explode( '://', $storeUrl, 2 ); - $store = self::getStoreObject( $proto, [] ); - if ( !$store->isReadOnly( $path ) ) { - return false; // at least one store is not read-only - } - } - - return true; // all stores are read-only + return MediaWikiServices::getInstance() + ->getExternalStoreAccess() + ->insert( $data, $params, $tryStores ); } /** @@ -247,8 +160,11 @@ class ExternalStore { * @param string $wiki * @return string The URL of the stored data item * @throws MWException + * @deprecated 1.34 Use insertToDefault() with 'wiki' set */ public static function insertToForeignDefault( $data, $wiki ) { - return self::insertToDefault( $data, [ 'wiki' => $wiki ] ); + return MediaWikiServices::getInstance() + ->getExternalStoreAccess() + ->insert( $data, [ 'domain' => $wiki ] ); } } diff --git a/includes/externalstore/ExternalStoreAccess.php b/includes/externalstore/ExternalStoreAccess.php new file mode 100644 index 0000000000..8603cc2697 --- /dev/null +++ b/includes/externalstore/ExternalStoreAccess.php @@ -0,0 +1,164 @@ +:///". Each type of storage + * medium has an associated protocol. Insertions will randomly pick mediums and locations from + * the provided list of writable medium-qualified locations. Insertions will also fail-over to + * other writable locations or mediums if one or more are not available. + * + * @ingroup ExternalStorage + * @since 1.34 + */ +class ExternalStoreAccess implements LoggerAwareInterface { + /** @var ExternalStoreFactory */ + private $storeFactory; + /** @var LoggerInterface */ + private $logger; + + /** + * @param ExternalStoreFactory $factory + * @param LoggerInterface|null $logger + */ + public function __construct( ExternalStoreFactory $factory, LoggerInterface $logger = null ) { + $this->storeFactory = $factory; + $this->logger = $logger ?: new NullLogger(); + } + + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } + + /** + * Fetch data from given URL + * + * @see ExternalStoreFactory::getStore() + * + * @param string $url The URL of the text to get + * @param array $params Map of context parameters; same as ExternalStoreFactory::getStore() + * @return string|bool The text stored or false on error + * @throws ExternalStoreException + */ + public function fetchFromURL( $url, array $params = [] ) { + return $this->storeFactory->getStoreForUrl( $url, $params )->fetchFromURL( $url ); + } + + /** + * Fetch data from multiple URLs with a minimum of round trips + * + * @see ExternalStoreFactory::getStore() + * + * @param array $urls The URLs of the text to get + * @param array $params Map of context parameters; same as ExternalStoreFactory::getStore() + * @return array Map of (url => string or false if not found) + * @throws ExternalStoreException + */ + public function fetchFromURLs( array $urls, array $params = [] ) { + $batches = $this->storeFactory->getUrlsByProtocol( $urls ); + $retval = []; + foreach ( $batches as $proto => $batchedUrls ) { + $store = $this->storeFactory->getStore( $proto, $params ); + $retval += $store->batchFetchFromURLs( $batchedUrls ); + } + // invalid, not found, db dead, etc. + $missing = array_diff( $urls, array_keys( $retval ) ); + foreach ( $missing as $url ) { + $retval[$url] = false; + } + + return $retval; + } + + /** + * Insert data into storage and return the assigned URL + * + * This will randomly pick one of the available write storage locations to put the data. + * It will keep failing-over to any untried storage locations whenever one location is + * not usable. + * + * @see ExternalStoreFactory::getStore() + * + * @param string $data + * @param array $params Map of context parameters; same as ExternalStoreFactory::getStore() + * @param string[]|null $tryStores Refer to $wgDefaultExternalStore + * @return string|bool The URL of the stored data item, or false on error + * @throws ExternalStoreException + */ + public function insert( $data, array $params = [], array $tryStores = null ) { + $tryStores = $tryStores ?? $this->storeFactory->getWriteBaseUrls(); + if ( !$tryStores ) { + throw new ExternalStoreException( "List of external stores provided is empty." ); + } + + $error = false; + while ( count( $tryStores ) > 0 ) { + $index = mt_rand( 0, count( $tryStores ) - 1 ); + $storeUrl = $tryStores[$index]; + + $this->logger->debug( __METHOD__ . ": trying $storeUrl\n" ); + + $store = $this->storeFactory->getStoreForUrl( $storeUrl, $params ); + if ( $store === false ) { + throw new ExternalStoreException( "Invalid external storage protocol - $storeUrl" ); + } + + $location = $this->storeFactory->getStoreLocationFromUrl( $storeUrl ); + try { + if ( $store->isReadOnly( $location ) ) { + $msg = 'read only'; + } else { + $url = $store->store( $location, $data ); + if ( strlen( $url ) ) { + return $url; // a store accepted the write; done! + } + $msg = 'operation failed'; + } + } catch ( Exception $error ) { + $msg = 'caught ' . get_class( $error ) . ' exception: ' . $error->getMessage(); + } + + unset( $tryStores[$index] ); // Don't try this one again! + $tryStores = array_values( $tryStores ); // Must have consecutive keys + $this->logger->error( + "Unable to store text to external storage {store_path} ({failure})", + [ 'store_path' => $storeUrl, 'failure' => $msg ] + ); + } + // All stores failed + if ( $error ) { + throw $error; // rethrow the last error + } else { + throw new ExternalStoreException( "Unable to store text to external storage" ); + } + } + + /** + * @return bool Whether all the default insertion stores are marked as read-only + * @throws ExternalStoreException + */ + public function isReadOnly() { + $writableStores = $this->storeFactory->getWriteBaseUrls(); + if ( !$writableStores ) { + return false; // no stores exists which can be "read only" + } + + foreach ( $writableStores as $storeUrl ) { + $store = $this->storeFactory->getStoreForUrl( $storeUrl ); + $location = $this->storeFactory->getStoreLocationFromUrl( $storeUrl ); + if ( $store !== false && !$store->isReadOnly( $location ) ) { + return false; // at least one store is not read-only + } + } + + return true; // all stores are read-only + } +} diff --git a/includes/externalstore/ExternalStoreDB.php b/includes/externalstore/ExternalStoreDB.php index 15bc3e0eb7..feb0614d1e 100644 --- a/includes/externalstore/ExternalStoreDB.php +++ b/includes/externalstore/ExternalStoreDB.php @@ -20,7 +20,7 @@ * @file */ -use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\LBFactory; use Wikimedia\Rdbms\ILoadBalancer; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\DBConnRef; @@ -36,6 +36,22 @@ use Wikimedia\Rdbms\DatabaseDomain; * @ingroup ExternalStorage */ class ExternalStoreDB extends ExternalStoreMedium { + /** @var LBFactory */ + private $lbFactory; + + /** + * @see ExternalStoreMedium::__construct() + * @param array $params Additional parameters include: + * - lbFactory: an LBFactory instance + */ + public function __construct( array $params ) { + parent::__construct( $params ); + if ( !isset( $params['lbFactory'] ) || !( $params['lbFactory'] instanceof LBFactory ) ) { + throw new InvalidArgumentException( "LBFactory required in 'lbFactory' field." ); + } + $this->lbFactory = $params['lbFactory']; + } + /** * The provided URL is in the form of DB://cluster/id * or DB://cluster/id/itemid for concatened storage. @@ -97,9 +113,7 @@ class ExternalStoreDB extends ExternalStoreMedium { */ public function store( $location, $data ) { $dbw = $this->getMaster( $location ); - $dbw->insert( $this->getTable( $dbw ), - [ 'blob_text' => $data ], - __METHOD__ ); + $dbw->insert( $this->getTable( $dbw ), [ 'blob_text' => $data ], __METHOD__ ); $id = $dbw->insertId(); if ( !$id ) { throw new MWException( __METHOD__ . ': no insert ID' ); @@ -112,8 +126,13 @@ class ExternalStoreDB extends ExternalStoreMedium { * @inheritDoc */ public function isReadOnly( $location ) { + if ( parent::isReadOnly( $location ) ) { + return true; + } + $lb = $this->getLoadBalancer( $location ); $domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) ); + return ( $lb->getReadOnlyReason( $domainId ) !== false ); } @@ -124,8 +143,7 @@ class ExternalStoreDB extends ExternalStoreMedium { * @return ILoadBalancer */ private function getLoadBalancer( $cluster ) { - $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); - return $lbFactory->getExternalLB( $cluster ); + return $this->lbFactory->getExternalLB( $cluster ); } /** @@ -135,16 +153,14 @@ class ExternalStoreDB extends ExternalStoreMedium { * @return DBConnRef */ public function getSlave( $cluster ) { - global $wgDefaultExternalStore; - $lb = $this->getLoadBalancer( $cluster ); $domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) ); - if ( !in_array( "DB://" . $cluster, (array)$wgDefaultExternalStore ) ) { - wfDebug( "read only external store\n" ); + if ( !in_array( $cluster, $this->writableLocations, true ) ) { + $this->logger->debug( "read only external store\n" ); $lb->allowLagged( true ); } else { - wfDebug( "writable external store\n" ); + $this->logger->debug( "writable external store\n" ); } $db = $lb->getConnectionRef( DB_REPLICA, [], $domainId ); @@ -174,8 +190,8 @@ class ExternalStoreDB extends ExternalStoreMedium { * @return string|bool Database domain ID or false */ private function getDomainId( array $server ) { - if ( isset( $this->params['wiki'] ) && $this->params['wiki'] !== false ) { - return $this->params['wiki']; // explicit domain + if ( $this->isDbDomainExplicit ) { + return $this->dbDomain; // explicit foreign domain } if ( isset( $server['dbname'] ) ) { @@ -230,33 +246,27 @@ class ExternalStoreDB extends ExternalStoreMedium { static $externalBlobCache = []; $cacheID = ( $itemID === false ) ? "$cluster/$id" : "$cluster/$id/"; - - $wiki = $this->params['wiki'] ?? false; - $cacheID = ( $wiki === false ) ? $cacheID : "$cacheID@$wiki"; + $cacheID = "$cacheID@{$this->dbDomain}"; if ( isset( $externalBlobCache[$cacheID] ) ) { - wfDebugLog( 'ExternalStoreDB-cache', - "ExternalStoreDB::fetchBlob cache hit on $cacheID" ); + $this->logger->debug( "ExternalStoreDB::fetchBlob cache hit on $cacheID" ); return $externalBlobCache[$cacheID]; } - wfDebugLog( 'ExternalStoreDB-cache', - "ExternalStoreDB::fetchBlob cache miss on $cacheID" ); + $this->logger->debug( "ExternalStoreDB::fetchBlob cache miss on $cacheID" ); $dbr = $this->getSlave( $cluster ); $ret = $dbr->selectField( $this->getTable( $dbr ), 'blob_text', [ 'blob_id' => $id ], __METHOD__ ); if ( $ret === false ) { - wfDebugLog( 'ExternalStoreDB', - "ExternalStoreDB::fetchBlob master fallback on $cacheID" ); + $this->logger->info( "ExternalStoreDB::fetchBlob master fallback on $cacheID" ); // Try the master $dbw = $this->getMaster( $cluster ); $ret = $dbw->selectField( $this->getTable( $dbw ), 'blob_text', [ 'blob_id' => $id ], __METHOD__ ); if ( $ret === false ) { - wfDebugLog( 'ExternalStoreDB', - "ExternalStoreDB::fetchBlob master failed to find $cacheID" ); + $this->logger->error( "ExternalStoreDB::fetchBlob master failed to find $cacheID" ); } } if ( $itemID !== false && $ret !== false ) { @@ -279,16 +289,22 @@ class ExternalStoreDB extends ExternalStoreMedium { */ private function batchFetchBlobs( $cluster, array $ids ) { $dbr = $this->getSlave( $cluster ); - $res = $dbr->select( $this->getTable( $dbr ), - [ 'blob_id', 'blob_text' ], [ 'blob_id' => array_keys( $ids ) ], __METHOD__ ); + $res = $dbr->select( + $this->getTable( $dbr ), + [ 'blob_id', 'blob_text' ], + [ 'blob_id' => array_keys( $ids ) ], + __METHOD__ + ); + $ret = []; if ( $res !== false ) { $this->mergeBatchResult( $ret, $ids, $res ); } if ( $ids ) { - wfDebugLog( __CLASS__, __METHOD__ . - " master fallback on '$cluster' for: " . - implode( ',', array_keys( $ids ) ) ); + $this->logger->info( + __METHOD__ . ": master fallback on '$cluster' for: " . + implode( ',', array_keys( $ids ) ) + ); // Try the master $dbw = $this->getMaster( $cluster ); $res = $dbw->select( $this->getTable( $dbr ), @@ -296,15 +312,16 @@ class ExternalStoreDB extends ExternalStoreMedium { [ 'blob_id' => array_keys( $ids ) ], __METHOD__ ); if ( $res === false ) { - wfDebugLog( __CLASS__, __METHOD__ . " master failed on '$cluster'" ); + $this->logger->error( __METHOD__ . ": master failed on '$cluster'" ); } else { $this->mergeBatchResult( $ret, $ids, $res ); } } if ( $ids ) { - wfDebugLog( __CLASS__, __METHOD__ . - " master on '$cluster' failed locating items: " . - implode( ',', array_keys( $ids ) ) ); + $this->logger->error( + __METHOD__ . ": master on '$cluster' failed locating items: " . + implode( ',', array_keys( $ids ) ) + ); } return $ret; diff --git a/includes/externalstore/ExternalStoreException.php b/includes/externalstore/ExternalStoreException.php new file mode 100644 index 0000000000..a2ef27de72 --- /dev/null +++ b/includes/externalstore/ExternalStoreException.php @@ -0,0 +1,5 @@ +protocols = array_map( 'strtolower', $externalStores ); + $this->writeBaseUrls = $defaultStores; + $this->localDomainId = $localDomainId; + $this->logger = $logger ?: new NullLogger(); + } + + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } /** - * @param array $externalStores See $wgExternalStores + * @return string[] List of active store types/protocols (lowercased), e.g. [ "db" ] + * @since 1.34 */ - public function __construct( array $externalStores ) { - $this->externalStores = array_map( 'strtolower', $externalStores ); + public function getProtocols() { + return $this->protocols; + } + + /** + * @return string[] List of base URLs for writes, e.g. [ "DB://cluster1" ] + * @since 1.34 + */ + public function getWriteBaseUrls() { + return $this->writeBaseUrls; } /** * Get an external store object of the given type, with the given parameters * + * The 'domain' field in $params will be set to the local DB domain if it is unset + * or false. A special 'isDomainImplicit' flag is set when this happens, which should + * only be used to handle legacy DB domain configuration concerns (e.g. T200471). + * * @param string $proto Type of external storage, should be a value in $wgExternalStores - * @param array $params Associative array of ExternalStoreMedium parameters - * @return ExternalStoreMedium|bool The store class or false on error + * @param array $params Map of ExternalStoreMedium::__construct context parameters. + * @return ExternalStoreMedium The store class or false on error + * @throws ExternalStoreException When $proto is not recognized */ - public function getStoreObject( $proto, array $params = [] ) { - if ( !$this->externalStores || !in_array( strtolower( $proto ), $this->externalStores ) ) { - // Protocol not enabled - return false; + public function getStore( $proto, array $params = [] ) { + $protoLowercase = strtolower( $proto ); // normalize + if ( !$this->protocols || !in_array( $protoLowercase, $this->protocols ) ) { + throw new ExternalStoreException( "Protocol '$proto' is not enabled." ); } $class = 'ExternalStore' . ucfirst( $proto ); + if ( isset( $params['wiki'] ) ) { + $params += [ 'domain' => $params['wiki'] ]; // b/c + } + if ( !isset( $params['domain'] ) || $params['domain'] === false ) { + $params['domain'] = $this->localDomainId; // default + $params['isDomainImplicit'] = true; // b/c for ExternalStoreDB + } + $params['writableLocations'] = []; + // Determine the locations for this protocol/store still receiving writes + foreach ( $this->writeBaseUrls as $storeUrl ) { + list( $storeProto, $storePath ) = self::splitStorageUrl( $storeUrl ); + if ( $protoLowercase === strtolower( $storeProto ) ) { + $params['writableLocations'][] = $storePath; + } + } + // @TODO: ideally, this class should not hardcode what classes need what backend factory + // objects. For now, inject the factory instances into __construct() for those that do. + if ( $protoLowercase === 'db' ) { + $params['lbFactory'] = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + } elseif ( $protoLowercase === 'mwstore' ) { + $params['fbGroup'] = FileBackendGroup::singleton(); + } + $params['logger'] = $this->logger; + + if ( !class_exists( $class ) ) { + throw new ExternalStoreException( "Class '$class' is not defined." ); + } // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading - return class_exists( $class ) ? new $class( $params ) : false; + return new $class( $params ); + } + + /** + * Get the ExternalStoreMedium for a given URL + * + * $url is either of the form: + * - a) ":///", for retrieval, or + * - b) "://", for storage + * + * @param string $url + * @param array $params Map of ExternalStoreMedium::__construct context parameters + * @return ExternalStoreMedium + * @throws ExternalStoreException When the protocol is missing or not recognized + * @since 1.34 + */ + public function getStoreForUrl( $url, array $params = [] ) { + list( $proto, $path ) = self::splitStorageUrl( $url ); + if ( $path == '' ) { // bad URL + throw new ExternalStoreException( "Invalid URL '$url'" ); + } + + return $this->getStore( $proto, $params ); } + /** + * Get the location within the appropriate store for a given a URL + * + * @param string $url + * @return string + * @throws ExternalStoreException + * @since 1.34 + */ + public function getStoreLocationFromUrl( $url ) { + list( , $location ) = self::splitStorageUrl( $url ); + if ( $location == '' ) { // bad URL + throw new ExternalStoreException( "Invalid URL '$url'" ); + } + + return $location; + } + + /** + * @param string[] $urls + * @return array[] Map of (protocol => list of URLs) + * @throws ExternalStoreException + * @since 1.34 + */ + public function getUrlsByProtocol( array $urls ) { + $urlsByProtocol = []; + foreach ( $urls as $url ) { + list( $proto, ) = self::splitStorageUrl( $url ); + $urlsByProtocol[$proto][] = $url; + } + + return $urlsByProtocol; + } + + /** + * @param string $storeUrl + * @return string[] (protocol, store location or location-qualified path) + * @throws ExternalStoreException + */ + private static function splitStorageUrl( $storeUrl ) { + $parts = explode( '://', $storeUrl ); + if ( count( $parts ) != 2 || $parts[0] === '' || $parts[1] === '' ) { + throw new ExternalStoreException( "Invalid storage URL '$storeUrl'" ); + } + + return $parts; + } } diff --git a/includes/externalstore/ExternalStoreMedium.php b/includes/externalstore/ExternalStoreMedium.php index da7752b745..0cdcf53ee7 100644 --- a/includes/externalstore/ExternalStoreMedium.php +++ b/includes/externalstore/ExternalStoreMedium.php @@ -21,22 +21,55 @@ * @ingroup ExternalStorage */ +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + /** - * Accessable external objects in a particular storage medium + * Key/value blob storage for a particular storage medium type (e.g. RDBMs, files) + * + * There can be multiple "locations" for a storage medium type (e.g. DB clusters, filesystems). + * Blobs are stored under URLs of the form ":///". Each type of storage + * medium has an associated protocol. * * @ingroup ExternalStorage * @since 1.21 */ -abstract class ExternalStoreMedium { - /** @var array */ +abstract class ExternalStoreMedium implements LoggerAwareInterface { + /** @var array Usage context options for this instance */ protected $params = []; + /** @var string Default database domain to store content under */ + protected $dbDomain; + /** @var bool Whether this was factoried with an explicit DB domain */ + protected $isDbDomainExplicit; + /** @var string[] Writable locations */ + protected $writableLocations = []; + + /** @var LoggerInterface */ + protected $logger; /** - * @param array $params Usage context options: - * - wiki: the domain ID of the wiki this is being used for [optional] + * @param array $params Usage context options for this instance: + * - domain: the DB domain ID of the wiki the content is for [required] + * - writableLocations: locations that are writable [required] + * - logger: LoggerInterface instance [optional] + * - isDomainImplicit: whether this was factoried without an explicit DB domain [optional] */ - public function __construct( array $params = [] ) { + public function __construct( array $params ) { $this->params = $params; + if ( isset( $params['domain'] ) ) { + $this->dbDomain = $params['domain']; + $this->isDbDomainExplicit = empty( $params['isDomainImplicit'] ); + } else { + throw new InvalidArgumentException( 'Missing DB "domain" parameter.' ); + } + + $this->logger = $params['logger'] ?? new NullLogger(); + $this->writableLocations = $params['writableLocations'] ?? []; + } + + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; } /** @@ -52,14 +85,13 @@ abstract class ExternalStoreMedium { * Fetch data from given external store URLs. * * @param array $urls A list of external store URLs - * @return array Map from the url to the text stored. Unfound data is not represented + * @return string[] Map of (url => text) for the URLs where data was actually found */ public function batchFetchFromURLs( array $urls ) { $retval = []; foreach ( $urls as $url ) { $data = $this->fetchFromURL( $url ); - // Dont return when false to allow for simpler implementations. - // errored urls are handled in ExternalStore::batchFetchFromURLs + // Dont return when false to allow for simpler implementations if ( $data !== false ) { $retval[$url] = $data; } @@ -86,6 +118,6 @@ abstract class ExternalStoreMedium { * @since 1.31 */ public function isReadOnly( $location ) { - return false; + return !in_array( $location, $this->writableLocations, true ); } } diff --git a/includes/externalstore/ExternalStoreMemory.php b/includes/externalstore/ExternalStoreMemory.php new file mode 100644 index 0000000000..daad75c485 --- /dev/null +++ b/includes/externalstore/ExternalStoreMemory.php @@ -0,0 +1,102 @@ + DB domain => id => value) */ + private static $data = []; + /** @var int */ + private static $nextId = 0; + + public function __construct( array $params ) { + parent::__construct( $params ); + } + + public function fetchFromURL( $url ) { + list( $location, $id ) = self::getURLComponents( $url ); + if ( $id === null ) { + throw new UnexpectedValueException( "Missing ID in URL component." ); + } + + return self::$data[$location][$this->dbDomain][$id] ?? false; + } + + public function batchFetchFromURLs( array $urls ) { + $blobs = []; + foreach ( $urls as $url ) { + $blob = $this->fetchFromURL( $url ); + if ( $blob !== false ) { + $blobs[$url] = $blob; + } + } + + return $blobs; + } + + public function store( $location, $data ) { + $index = ++self::$nextId; + self::$data[$location][$this->dbDomain][$index] = $data; + + return "memory://$location/$index"; + } + + /** + * Remove all data from memory for this domain + */ + public function clear() { + foreach ( self::$data as &$dataForLocation ) { + unset( $dataForLocation[$this->dbDomain] ); + } + unset( $dataForLocation ); + self::$data = array_filter( self::$data, 'count' ); + self::$nextId = 0; + } + + /** + * @param string $url + * @return array (location, ID or null) + */ + private function getURLComponents( $url ) { + list( $proto, $path ) = explode( '://', $url, 2 ) + [ null, null ]; + if ( $proto !== 'memory' ) { + throw new UnexpectedValueException( "Got URL of protocol '$proto', not 'memory'." ); + } elseif ( $path === null ) { + throw new UnexpectedValueException( "URL is missing path component." ); + } + + $parts = explode( '/', $path ); + if ( count( $parts ) > 2 ) { + throw new UnexpectedValueException( "Too components in URL '$path'." ); + } + + return [ $parts[0], $parts[1] ?? null ]; + } +} diff --git a/includes/externalstore/ExternalStoreMwstore.php b/includes/externalstore/ExternalStoreMwstore.php index 7414f23e9e..77c23ee395 100644 --- a/includes/externalstore/ExternalStoreMwstore.php +++ b/includes/externalstore/ExternalStoreMwstore.php @@ -31,6 +31,22 @@ * @since 1.21 */ class ExternalStoreMwstore extends ExternalStoreMedium { + /** @var FileBackendGroup */ + private $fbGroup; + + /** + * @see ExternalStoreMedium::__construct() + * @param array $params Additional parameters include: + * - fbGroup: a FileBackendGroup instance + */ + public function __construct( array $params ) { + parent::__construct( $params ); + if ( !isset( $params['fbGroup'] ) || !( $params['fbGroup'] instanceof FileBackendGroup ) ) { + throw new InvalidArgumentException( "FileBackendGroup required in 'fbGroup' field." ); + } + $this->fbGroup = $params['fbGroup']; + } + /** * The URL returned is of the form of the form mwstore://backend/container/wiki/id * @@ -39,7 +55,7 @@ class ExternalStoreMwstore extends ExternalStoreMedium { * @return bool */ public function fetchFromURL( $url ) { - $be = FileBackendGroup::singleton()->backendFromPath( $url ); + $be = $this->fbGroup->backendFromPath( $url ); if ( $be instanceof FileBackend ) { // We don't need "latest" since objects are immutable and // backends should at least have "read-after-create" consistency. @@ -59,14 +75,14 @@ class ExternalStoreMwstore extends ExternalStoreMedium { public function batchFetchFromURLs( array $urls ) { $pathsByBackend = []; foreach ( $urls as $url ) { - $be = FileBackendGroup::singleton()->backendFromPath( $url ); + $be = $this->fbGroup->backendFromPath( $url ); if ( $be instanceof FileBackend ) { $pathsByBackend[$be->getName()][] = $url; } } $blobs = []; foreach ( $pathsByBackend as $backendName => $paths ) { - $be = FileBackendGroup::singleton()->get( $backendName ); + $be = $this->fbGroup->get( $backendName ); $blobs += $be->getFileContentsMulti( [ 'srcs' => $paths ] ); } @@ -77,16 +93,18 @@ class ExternalStoreMwstore extends ExternalStoreMedium { * @inheritDoc */ public function store( $backend, $data ) { - $be = FileBackendGroup::singleton()->get( $backend ); + $be = $this->fbGroup->get( $backend ); // Get three random base 36 characters to act as shard directories $rand = Wikimedia\base_convert( mt_rand( 0, 46655 ), 10, 36, 3 ); // Make sure ID is roughly lexicographically increasing for performance $id = str_pad( UIDGenerator::newTimestampedUID128( 32 ), 26, '0', STR_PAD_LEFT ); - // Segregate items by wiki ID for the sake of bookkeeping - // @FIXME: this does not include the domain for b/c but it ideally should - $wiki = $this->params['wiki'] ?? wfWikiID(); - - $url = $be->getContainerStoragePath( 'data' ) . '/' . rawurlencode( $wiki ); + // Segregate items by DB domain ID for the sake of bookkeeping + $domain = $this->isDbDomainExplicit + ? $this->dbDomain + // @FIXME: this does not include the schema for b/c but it ideally should + : WikiMap::getWikiIdFromDbDomain( $this->dbDomain ); + $url = $be->getContainerStoragePath( 'data' ) . '/' . rawurlencode( $domain ); + // Use directory/container sharding $url .= ( $be instanceof FSFileBackend ) ? "/{$rand[0]}/{$rand[1]}/{$rand[2]}/{$id}" // keep directories small : "/{$rand[0]}/{$rand[1]}/{$id}"; // container sharding is only 2-levels @@ -96,13 +114,17 @@ class ExternalStoreMwstore extends ExternalStoreMedium { if ( $status->isOK() ) { return $url; - } else { - throw new MWException( __METHOD__ . ": operation failed: $status" ); } + + throw new MWException( __METHOD__ . ": operation failed: $status" ); } public function isReadOnly( $backend ) { - $be = FileBackendGroup::singleton()->get( $backend ); + if ( parent::isReadOnly( $backend ) ) { + return true; + } + + $be = $this->fbGroup->get( $backend ); return $be ? $be->isReadOnly() : false; } diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 1e1bde38be..d7d6bf78d8 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -1906,7 +1906,7 @@ class LocalFile extends File { * @return Status */ function move( $target ) { - $localRepo = MediaWikiServices::getInstance()->getRepoGroup(); + $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo(); if ( $this->getRepo()->getReadOnlyReason() !== false ) { return $this->readOnlyFatalStatus(); } @@ -1923,8 +1923,8 @@ class LocalFile extends File { wfDebugLog( 'imagemove', "Finished moving {$this->name}" ); // Purge the source and target files... - $oldTitleFile = $localRepo->findFile( $this->title ); - $newTitleFile = $localRepo->findFile( $target ); + $oldTitleFile = $localRepo->newFile( $this->title ); + $newTitleFile = $localRepo->newFile( $target ); // To avoid slow purges in the transaction, move them outside... DeferredUpdates::addUpdate( new AutoCommitUpdate( diff --git a/includes/historyblob/HistoryBlobStub.php b/includes/historyblob/HistoryBlobStub.php index 4995d3b3f0..9a4df1f81d 100644 --- a/includes/historyblob/HistoryBlobStub.php +++ b/includes/historyblob/HistoryBlobStub.php @@ -20,6 +20,8 @@ * @file */ +use MediaWiki\MediaWikiServices; + /** * Pointer object for an item within a CGZ blob stored in the text table. */ @@ -99,8 +101,9 @@ class HistoryBlobStub { if ( !isset( $parts[1] ) || $parts[1] == '' ) { return false; } - $row->old_text = ExternalStore::fetchFromURL( $url ); - + $row->old_text = MediaWikiServices::getInstance() + ->getExternalStoreAccess() + ->fetchFromURL( $url ); } if ( !in_array( 'object', $flags ) ) { diff --git a/includes/htmlform/HTMLForm.php b/includes/htmlform/HTMLForm.php index d071478440..99e387a24b 100644 --- a/includes/htmlform/HTMLForm.php +++ b/includes/htmlform/HTMLForm.php @@ -180,9 +180,8 @@ class HTMLForm extends ContextSource { protected $mMessagePrefix; /** @var HTMLFormField[] */ - protected $mFlatFields; - - protected $mFieldTree; + protected $mFlatFields = []; + protected $mFieldTree = []; protected $mShowReset = false; protected $mShowSubmit = true; protected $mSubmitFlags = [ 'primary', 'progressive' ]; @@ -315,7 +314,8 @@ class HTMLForm extends ContextSource { /** * Build a new HTMLForm from an array of field attributes * - * @param array $descriptor Array of Field constructs, as described above + * @param array $descriptor Array of Field constructs, as described + * in the class documentation * @param IContextSource|null $context Available since 1.18, will become compulsory in 1.18. * Obviates the need to call $form->setTitle() * @param string $messagePrefix A prefix to go in front of default messages @@ -343,11 +343,23 @@ class HTMLForm extends ContextSource { $this->displayFormat = 'div'; } - // Expand out into a tree. + $this->addFields( $descriptor ); + } + + /** + * Add fields to the form + * + * @since 1.34 + * + * @param array $descriptor Array of Field constructs, as described + * in the class documentation + * @return HTMLForm + */ + public function addFields( $descriptor ) { $loadedDescriptor = []; - $this->mFlatFields = []; foreach ( $descriptor as $fieldname => $info ) { + $section = $info['section'] ?? ''; if ( isset( $info['type'] ) && $info['type'] === 'file' ) { @@ -371,7 +383,9 @@ class HTMLForm extends ContextSource { $this->mFlatFields[$fieldname] = $field; } - $this->mFieldTree = $loadedDescriptor; + $this->mFieldTree = array_merge( $this->mFieldTree, $loadedDescriptor ); + + return $this; } /** @@ -454,7 +468,8 @@ class HTMLForm extends ContextSource { * @since 1.23 * * @param string $fieldname Name of the field - * @param array &$descriptor Input Descriptor, as described above + * @param array &$descriptor Input Descriptor, as described + * in the class documentation * * @throws MWException * @return string Name of a HTMLFormField subclass @@ -481,7 +496,8 @@ class HTMLForm extends ContextSource { * Initialise a new Object for the field * * @param string $fieldname Name of the field - * @param array $descriptor Input Descriptor, as described above + * @param array $descriptor Input Descriptor, as described + * in the class documentation * @param HTMLForm|null $parent Parent instance of HTMLForm * * @throws MWException diff --git a/includes/installer/PostgresInstaller.php b/includes/installer/PostgresInstaller.php index a1a80612ab..6592c51420 100644 --- a/includes/installer/PostgresInstaller.php +++ b/includes/installer/PostgresInstaller.php @@ -505,6 +505,7 @@ class PostgresInstaller extends DatabaseInstaller { if ( !$status->isOK() ) { return $status; } + /** @var DatabasePostgres $conn */ $conn = $status->value; // Create the schema if necessary diff --git a/includes/installer/i18n/ca.json b/includes/installer/i18n/ca.json index c9abc94cf3..44cd45e0d3 100644 --- a/includes/installer/i18n/ca.json +++ b/includes/installer/i18n/ca.json @@ -57,18 +57,20 @@ "config-env-bad": "S'ha comprovat l'entorn.\nNo podeu instal·lar el MediaWiki.", "config-env-php": "El PHP $1 està instal·lat.", "config-env-hhvm": "L’HHVM $1 és instal·lat.", - "config-unicode-using-intl": "S'utilitza l'[https://pecl.php.net/intl extensió intl PECL] per a la normalització de l'Unicode.", - "config-unicode-pure-php-warning": "Avís: L'[https://pecl.php.net/intl extensió intl PECL] no és disponible per gestionar la normalització d'Unicode. Es reverteix a una implementació més lenta en PHP pur.\nSi administreu un lloc web amb molt de trànsit, hauríeu de consultar la guia de [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalització d'Unicode].", + "config-unicode-using-intl": "S'utilitza l'[https://php.net/manual/en/book.intl.php extensió intl de PHP] per a la normalització de l'Unicode.", + "config-unicode-pure-php-warning": "Avís: L'[https://php.net/manual/en/book.intl.php extensió intl de PHP] no és disponible per gestionar la normalització d'Unicode. Es reverteix a una implementació més lenta en PHP pur.\nSi administreu un lloc web amb molt de trànsit, hauríeu de consultar la guia de [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalització d'Unicode].", "config-unicode-update-warning": "Avís: La versió instal·lada del contenidor de normalització d'Unicode utilitza una versió antiga de la biblioteca [http://site.icu-project.org/ del projecte ICU].\nHauríeu [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations d'actualitzar-la] si us importa poder utilitzar Unicode.", "config-no-db": "No s'ha pogut trobar un controlador adequat per a la base de dades. Instal·leu-ne un per al PHP.\nHi ha suport per {{PLURAL:$2|al tipus de base de dades següent|als tipus de base de dades següents}}: $1\n\nSi heu compilat el PHP manualment, torneu a configurar-lo amb un client de base de dades habilitat, per exemple fent servir ./configure --with-mysqli.\nSi heu instal·lat el PHP d'un paquet de Debian o Ubuntu, també cal que instal·leu, per exemple, el paquet php-mysql.", - "config-outdated-sqlite": "Avís: teniu el SQLite $1, que és menor que la versió mínima necessària $2. SQLite no estarà disponible.", + "config-outdated-sqlite": "Avís: teniu el SQLite $2, que és menor que la versió mínima necessària $1. SQLite no estarà disponible.", "config-no-fts3": "Avís: SQLite està compilat sense el [//sqlite.org/fts3.html mòdul FTS3], per tant les funcionalitats de cerca no estaran disponibles en aquesta instal·lació.", "config-pcre-old": "Error fatal: Cal el PCRE $1 o superior.\nEl binari PHP que utilitzeu està enllaçat amb el PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Més informació].", + "config-pcre-no-utf8": "Fatal: El mòdul PCRE de PHP sembla que no va compilar-se per funcionar amb PCRE_UTF8.\nMediaWiki necessita que UTF-8 funcioni correctament.", "config-memory-raised": "El memory_limit del PHP és $1 i s'ha aixecat a $2.", "config-memory-bad": "Avís: El memory_limit del PHP és $1.\nAixò és probablement massa baix.\nLa instal·lació pot fallar!", "config-apc": "L'[https://www.php.net/apc APC] està instal·lat", "config-apcu": "L'[https://www.php.net/apcu APCu] està instal·lat", "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] està instal·lat", + "config-no-cache-apcu": "Avís: no s'ha pogut trobar [https://www.php.net/apcu APCu] o [https://www.iis.net/downloads/microsoft/wincache-extension WinCache].\nNo s'habilitarà la memòria cau d'objectes.", "config-diff3-bad": "No s'ha trobat el GNU diff3. Podeu ignorar-ho per ara, però us podeu trobar amb conflictes d'edició més habitualment.", "config-git": "S'ha trobat el programari de control de versions Git: $1.", "config-git-bad": "No s'ha trobat el programari de control de versions Git. Podeu ignorar-ho per ara, però la pàgina Especial:Versió no mostrarà els resums de publicacions.", @@ -184,6 +186,7 @@ "config-admin-error-password": "S'ha produït un error intern en definir una contrasenya per a l'administrador «$1»:
$2
", "config-admin-error-bademail": "Heu introduït una adreça electrònica no vàlida.", "config-subscribe": "Subscriu a la [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce llista de correu d'anunci de noves versions].", + "config-subscribe-noemail": "Us heu provat de subscriure a la llista de correu d'anuncis de noves versions sense proporcionar-hi una adreça electrònica.\nProporcioneu-ne una si voleu subscriure-us a la llista de correu electrònic.", "config-pingback": "Comparteix dades d'aquesta instal·lació amb els desenvolupadors de MediaWiki.", "config-almost-done": "Gairebé ja heu acabat!\nPodeu ometre el que queda de la configuració i procedir amb la instal·lació del wiki.", "config-optional-continue": "Fes-me més preguntes.", @@ -214,12 +217,14 @@ "config-email-auth": "Habilita l'autenticació per correu electrònic", "config-email-auth-help": "Si s'habilita l'opció, els usuaris hauran de confirmar llur adreça electrònica utilitzant un enllaç que els enviarem quan la defineixin o la canviïn.\nNomés les adreces electròniques autenticades poden rebre correus d'altres usuaris o canviar les notificacions de correu.\nDefinir aquesta opció és recomanat per a wikis públics per tal d'evitar els possibles abusos de l'ús del correu.", "config-email-sender": "Adreça electrònica de retorn:", + "config-email-sender-help": "Introduïu una adreça electrònica per utilitzar-la com a adreça de retorn dels missatges electrònics de sortida.\nAquí és on s'enviaran els missatges que no arribin a lloc.\nMolts servidors de correu electrònic necessiten com a mínim que la part del nom de domini sigui vàlida.", "config-upload-settings": "Imatges i càrregues de fitxers", "config-upload-enable": "Habilita la càrrega de fitxers", "config-upload-help": "Les càrregues de fitxers potencialment exposen el vostre servidor a riscos de seguretat.\nPer a més informació, llegiu la [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security secció de seguretat] del manual.\n\nPer habilitar les càrregues de fitxer, canvieu el mode del subdirectori images del directori arrel de MediaWiki per tal que el servidor web pugui escriure-hi.\nA continuació, habiliteu-ne l'opció.", "config-upload-deleted": "Directori pels arxius suprimits:", "config-upload-deleted-help": "Trieu un directori per a arxivar els fitxers suprimits.\nIdealment no hauria de ser accessible des del web.", "config-logo": "URL del logo:", + "config-logo-help": "L'aparença per defecte de MediaWiki inclou un espai per a un logotip de 135x160 píxels sobre el menú de la barra lateral.\nCarregueu una imatge de la mida apropiada i introduïu un URL aquí.\n\nPodeu utilitzar $wgStylePath o $wgScriptPath si el vostre logotip es relatiu a aquests camins.\n\nSi no voleu cap logotip, deixeu el quadre en blanc.", "config-instantcommons": "Habilita Instant Commons", "config-instantcommons-help": "[https://www.mediawiki.org/wiki/InstantCommons Instant Commons] és una característica que permet que els wikis utilitzin imatges, sons i altres fitxers multimèdies que es troben al lloc web de [https://commons.wikimedia.org/ Wikimedia Commons].\nPer a això, cal que el MediaWiki tingui accés a Internet.\n\nPer a més informació d'aquesta característica, amb instuccions de com definir altres wikis apart de Wikimedia Commons, consulteu [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos el manual].", "config-cc-error": "El selector de llicència Creative Commons no ha donat cap resultat.\nIntroduïu la llicència manualment.", @@ -240,11 +245,11 @@ "config-extensions": "Extensions", "config-extensions-help": "Les extensions que es llisten a dalt s'han detecta en el directori ./extensions.\n\nPoden necessitar configuració addicional, però ja podeu habilitar-les.", "config-skins": "Aparences", - "config-skins-help": "S'han detectat els temes llistats a dalt en el directori ./skins. Heu d'habilitar-ne com a mínim un i trieu-ne el predeterminat.", - "config-skins-use-as-default": "Utilitza aquest tema per defecte", - "config-skins-missing": "No s'ha trobat cap tema; MediaWiki utilitzarà el tema per defecte fins que hi instal·leu alguns adequats.", + "config-skins-help": "S'han detectat les aparences llistades a dalt en el directori ./skins. Heu d'habilitar-ne com a mínim un i trieu-ne el predeterminat.", + "config-skins-use-as-default": "Utilitza aquesta aparença per defecte", + "config-skins-missing": "No s'ha trobat cap aparença; MediaWiki utilitzarà l'aparença per defecte fins que hi instal·leu algunes adequades.", "config-skins-must-enable-some": "Heu de triar com a mínim un tema per habilitar.", - "config-skins-must-enable-default": "Cal habilitar el tema triat per defecte.", + "config-skins-must-enable-default": "Cal habilitar l'aparença triada per defecte.", "config-install-alreadydone": "Avís: Sembla que ja havíeu instal·lat MediaWiki i esteu provant d'instal·lar-lo de nou.\nProcediu a la pàgina següent.", "config-install-begin": "En fer clic a «{{int:config-continue}}» s’iniciarà la instal·lació del MediaWiki. Si encara voleu fer canvis, feu clic a «{{int:config-back}}».", "config-install-step-done": "fet", diff --git a/includes/installer/i18n/es.json b/includes/installer/i18n/es.json index feef3357b6..6e1d5a2d58 100644 --- a/includes/installer/i18n/es.json +++ b/includes/installer/i18n/es.json @@ -39,7 +39,8 @@ "Adjen", "Dschultz", "Carlosmg.dg", - "Harvest" + "Harvest", + "Anarhistička Maca" ] }, "config-desc": "El instalador de MediaWiki", @@ -85,8 +86,8 @@ "config-env-bad": "El entorno ha sido comprobado.\nNo puedes instalar MediaWiki.", "config-env-php": "PHP $1 está instalado.", "config-env-hhvm": "HHVM $1 está instalado.", - "config-unicode-using-intl": "Se utiliza la [https://pecl.php.net/intl extensión «intl» de PECL] para la normalización Unicode.", - "config-unicode-pure-php-warning": "Advertencia: la [https://pecl.php.net/intl extensión intl] no está disponible para efectuar la normalización Unicode. Se utilizará la implementación más lenta en PHP puro.\nSi tu web tiene mucho tráfico, te recomendamos leer acerca de la [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].", + "config-unicode-using-intl": "Se utiliza la [https://php.net/manual/en/book.intl.php PHP extensión «intl» de PECL] para la normalización Unicode.", + "config-unicode-pure-php-warning": "Advertencia: la [https://php.net/manual/en/book.intl.php PHP extensión intl] no está disponible para efectuar la normalización Unicode. Se utilizará la implementación más lenta en PHP puro.\nSi tu web tiene mucho tráfico, te recomendamos leer acerca [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalización Unicode].", "config-unicode-update-warning": "Atención: la versión instalada del contenedor de normalización de Unicode utiliza una versión anticuada de la biblioteca del [http://site.icu-project.org/ proyecto ICU].\nDeberías [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations modernizarla] si te interesa utilizar Unicode.", "config-no-db": "No se encontró un controlador adecuado para la base de datos. Necesitas instalar un controlador de base de datos para PHP.\n{{PLURAL:$2|Se admite el siguiente gestor de bases de datos|Se admiten los siguientes gestores de bases de datos}}: $1.\n\nSi compilaste PHP por tu cuenta, debes reconfigurarlo activando un cliente de base de datos, por ejemplo, mediante ./configure --with-mysqli.\nSi instalaste PHP desde un paquete de Debian o Ubuntu, también debes instalar, por ejemplo, el paquete php-mysql.", "config-outdated-sqlite": "Advertencia: tienes SQLite $2, que es inferior a la mínima versión requerida: $1. SQLite no estará disponible.", diff --git a/includes/installer/i18n/fa.json b/includes/installer/i18n/fa.json index b17b0936c5..daa4de984b 100644 --- a/includes/installer/i18n/fa.json +++ b/includes/installer/i18n/fa.json @@ -304,7 +304,7 @@ "config-install-stats": "شروع آمار", "config-install-keys": "تولید کلیدهای مخفی", "config-install-updates": "جلوگیری از به روز رسانی‌های غیر ضروری در حال اجرا", - "config-install-updates-failed": "خطا: قراردادن کلیدهای به روز رسانی به داخل جداول با خطای روبرو مواجه شد: $1", + "config-install-updates-failed": "خطا: قرار دادن کلیدهای روزآمدسازی در جدول‌ها با شکست و این خطا مواجه شد: $1", "config-install-sysop": "ایجاد حساب کاربری مدیر", "config-install-subscribe-fail": "قادر تصدیق اعلام مدیاویکی نیست:$1", "config-install-subscribe-notpossible": "سی‌یوآر‌ال نصب نشده‌است و allow_url_fopen در دسترس نیست.", diff --git a/includes/installer/i18n/ia.json b/includes/installer/i18n/ia.json index 67f769f990..2b85baa7d4 100644 --- a/includes/installer/i18n/ia.json +++ b/includes/installer/i18n/ia.json @@ -51,8 +51,8 @@ "config-env-bad": "Le ambiente ha essite verificate.\nTu non pote installar MediaWiki.", "config-env-php": "PHP $1 es installate.", "config-env-hhvm": "HHVM $1 es installate.", - "config-unicode-using-intl": "Le [https://pecl.php.net/intl extension PECL intl] es usate pro le normalisation Unicode.", - "config-unicode-pure-php-warning": "'''Aviso''': Le [https://pecl.php.net/intl extension PECL intl] non es disponibile pro exequer le normalisation Unicode; le systema recurre al implementation lente in PHP pur.\nSi tu sito ha un alte volumine de traffico, tu deberea informar te un poco super le [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisation Unicode].", + "config-unicode-using-intl": "Le [https://php.net/manual/en/book.intl.php extension PHP intl] es usate pro le normalisation Unicode.", + "config-unicode-pure-php-warning": "'''Aviso''': Le [https://php.net/manual/en/book.intl.php extension PHP intl] non es disponibile pro exequer le normalisation Unicode; le systema recurre al implementation lente in PHP pur.\nSi tu sito ha un alte volumine de traffico, tu deberea informar te super le [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisation Unicode].", "config-unicode-update-warning": "'''Aviso''': Le version installate del bibliotheca inveloppante pro normalisation Unicode usa un version ancian del bibliotheca del [http://site.icu-project.org/ projecto ICU].\nTu deberea [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations actualisar lo] si le uso de Unicode importa a te.", "config-no-db": "Non poteva trovar un driver appropriate pro le base de datos! Es necessari installar un driver de base de datos pro PHP.\nLe sequente {{PLURAL:$2|typo|typos}} de base de datos es supportate: $1.\n\nSi tu compilava PHP tu mesme, reconfigura lo con un cliente de base de datos activate, per exemplo, usante ./configure --with-mysqli.\nSi tu installava PHP ex un pacchetto Debian o Ubuntu, tu debe etiam installar, per exemplo, le modulo php-mysql.", "config-outdated-sqlite": "Attention: tu ha SQLite $2, que es inferior al minime version requirite, $1. SQLite essera indisponibile.", diff --git a/includes/installer/i18n/io.json b/includes/installer/i18n/io.json index eb3042bd21..f1e441bb4d 100644 --- a/includes/installer/i18n/io.json +++ b/includes/installer/i18n/io.json @@ -10,6 +10,8 @@ "config-title": "Instalo di MediaWiki $1", "config-information": "Informo", "config-localsettings-upgrade": "L'arkivo LocalSettings.php trovesis.\nPor plubonigar l'instaluro, voluntez informar la valoro dil $wgUpgradeKey en l'infra buxo.\nVu trovos ol en LocalSettings.php.", + "config-session-error": "Eroro dum komenco di seciono: $1", + "config-session-expired": "Vua sesiono probable finis.\nSesioni programesis por durar $1\nVu povas augmentar to per modifiko di session.gc_maxlifetime en php.ini.\nRikomencez l'instalo-procedo.", "config-your-language": "Vua idiomo:", "config-your-language-help": "Selektez l'idiomo por uzar dum l'instalo-procedo.", "config-wiki-language": "Wiki linguo:", @@ -39,11 +41,38 @@ "config-env-php": "PHP $1 instalesis.", "config-env-hhvm": "HHVM $1 instalesis.", "config-unicode-pure-php-warning": "Atencez: La [https://php.net/manual/en/book.intl.php prolonguro PHP intl] ne esas disponebla por traktar skribo-normaligo \"Unicode\". Vice, uzesas la plu lenta laborado en pura PHP.\nSe vu administras pagini multe vizitata, vu mustas lektar la [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations skribo-normaligo Unicode].", + "config-memory-raised": "Parametro memory_limit esas $1, modifikata a $2.", + "config-memory-bad": "Atences: la limito por PHP memory_limit esas $1.\nTo probable esas nesuficanta.\nL'instalo-procedo povas faliar!", "config-apc": "[https://www.php.net/apc APC] instalesis", "config-apcu": "[https://www.php.net/apcu APCu] instalesis", + "config-wincache": "[https://www.iis.net/downloads/microsoft/wincache-extension WinCache] instalesis", + "config-using-uri": "Ret-adreso (URL) dil servero \"$1$2\".", + "config-db-wiki-settings": "Identifikez ca wiki", + "config-db-name": "Nomo dil datumaro (sen strekteti):", + "config-db-install-account": "Konto dil uzero por instalo", + "config-db-username": "Uzero-nomo dil datumaro:", + "config-db-password": "Pasovorto dil datumaro:", + "config-type-mssql": "Microsoft SQL Server", + "config-header-oracle": "Ajusti por Oracle-sistemo:", + "config-header-mssql": "Ajusti por Microsoft SQL Server", + "config-invalid-db-type": "Nevalida tipo di datumaro.", + "config-mysql-myisam": "MyISAM", + "config-ns-generic": "Projeto", + "config-ns-site-name": "Sama kam la wiki-nomo: $1", + "config-ns-other": "Altra (definez precise)", + "config-ns-other-default": "MyWiki", + "config-admin-name": "Vua uzero-nomo:", + "config-admin-password": "Pasovorto:", + "config-admin-password-confirm": "Riskribez la pasovorto:", + "config-admin-email": "E-postal adreso:", + "config-profile-wiki": "Aperta wiki", + "config-profile-no-anon": "Bezonas krear konto", + "config-profile-fishbowl": "Nur permisata redakteri", "config-profile-private": "Privata wiki", + "config-profile-help": "Wikis work best when you let as many people edit them as possible.\nIn MediaWiki, it is easy to review the recent changes, and to revert any damage that is done by naive or malicious users.\n\nHowever, many have found MediaWiki to be useful in a wide variety of roles, and sometimes it is not easy to convince everyone of the benefits of the wiki way.\nSo you have the choice.\n\nThe {{int:config-profile-wiki}} model allows anyone to edit, without even logging in.\nA wiki with {{int:config-profile-no-anon}} provides extra accountability, but may deter casual contributors.\n\nThe {{int:config-profile-fishbowl}} scenario allows approved users to edit, but the public can view the pages, including history.\nA {{int:config-profile-private}} only allows approved users to view pages, with the same group allowed to edit.\n\nMore complex user rights configurations are available after installation, see the [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights relevant manual entry].", "config-license": "Autoroyuro e permiso:", "config-license-cc-0": "Creative Commons Zero (Publika domeno)", + "config-license-pd": "Publika domeno", "config-install-step-done": "Facita", "config-install-step-failed": "faliis", "config-install-extensions": "Komplementi inkluzita", diff --git a/includes/installer/i18n/ja.json b/includes/installer/i18n/ja.json index 934c7c641d..7f7c801e8b 100644 --- a/includes/installer/i18n/ja.json +++ b/includes/installer/i18n/ja.json @@ -75,7 +75,7 @@ "config-unicode-pure-php-warning": "警告: Unicode 正規化の処理に[https://php.net/manual/en/book.intl.php PHP intl 拡張機能]を利用できないため、処理が遅いピュア PHP の実装を代わりに使用しています。\n高トラフィックのサイトを運営する場合、[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode 正規化]は必ず読むよう推奨されます。", "config-unicode-update-warning": "警告: インストールされているバージョンの Unicode 正規化ラッパーは、[http://site.icu-project.org/ ICU プロジェクト]のライブラリの古いバージョンを使用しています。\nUnicode を少しでも利用する可能性がある場合は、[https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations アップグレード]してください。", "config-no-db": "適切なデータベース ドライバーが見つかりませんでした! PHP にデータベース ドライバーをインストールする必要があります。\n以下の種類のデータベース{{PLURAL:$2|のタイプ}}に対応しています: $1\n\nPHP を自分でコンパイルした場合は、例えば ./configure --with-mysqli を実行して、データベース クライアントを使用できるように再設定してください。\nDebian または Ubuntu のパッケージから PHP をインストールした場合は、モジュール (例: php-mysql) もインストールする必要があります。", - "config-outdated-sqlite": "警告: あなたは SQLite $1 を使用していますが、最低限必要なバージョン $2 より古いバージョンです。SQLite は利用できません。", + "config-outdated-sqlite": "警告: ご利用の SQLite $2 は容認されている最古の版 $1 よりも古い版です。SQLite が対応しません。", "config-no-fts3": "警告: SQLite は [//sqlite.org/fts3.html FTS3] モジュールなしでコンパイルされており、このバックエンドでは検索機能は利用できなくなります。", "config-pcre-old": "致命的エラー: PCRE $1 以降が必要です。\nご使用中の PHP のバイナリは PCRE $2 とリンクされています。\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE 詳細情報]", "config-pcre-no-utf8": "致命的エラー: PHP の PCRE が PCRE_UTF8 対応なしでコンパイルされているようです。\nMediaWiki を正しく動作させるには、UTF-8 対応が必要です。", @@ -154,7 +154,7 @@ "config-invalid-db-server-oracle": "「$1」は無効なデータベース TNS です。\n「TNS 名」「Easy Connect」文字列のいずれかを使用してください ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle ネーミング メソッド])。", "config-invalid-db-name": "「$1」は無効なデータベース名です。\n半角の英数字 (a-z、A-Z、0-9)、アンダースコア (_)、ハイフン (-) のみを使用してください。", "config-invalid-db-prefix": "「$1」は無効なデータベース接頭辞です。\n半角の英数字 (a-z、A-Z、0-9)、アンダースコア (_)、ハイフン (-) のみを使用してください。", - "config-connection-error": "$1。\n\n以下のホスト名、ユーザー名、パスワードを確認してから再度試してください。", + "config-connection-error": "$1。\n\n以下のホスト名、ユーザー名、パスワードを確認してから再度試してください。データベースホストとして「localhost」を使用している場合は、代わりに 「127.0.0.1」を使用してください(またはその逆)。", "config-invalid-schema": "「$1」は MediaWiki のスキーマとして無効です。\n半角の英数字 (a-z、A-Z、0-9)、アンダースコア (_) のみを使用してください。", "config-db-sys-create-oracle": "インストーラーは、新規アカウント作成にはSYSDBAアカウントの利用のみをサポートしています。", "config-db-sys-user-exists-oracle": "利用者アカウント「$1」は既に存在します。SYSDBA は新しいアカウントの作成のみに使用できます!", @@ -243,7 +243,7 @@ "config-license-help": "多くの公開ウィキでは、すべての寄稿物が[https://freedomdefined.org/Definition フリーライセンス]のもとに置かれています。\nこうすることにより、コミュニティによる共有の感覚が生まれ、長期的な寄稿が促されます。\n私的ウィキや企業のウィキでは、通常、フリーライセンスにする必要はありません。\n\nウィキペディアにあるテキストをあなたのウィキで利用し、逆にあなたのウィキにあるテキストをウィキペディアに複製することを許可したい場合には、{{int:config-license-cc-by-sa}}を選択するべきです。\n\nウィキペディアは以前、GNUフリー文書利用許諾契約書(GFDL)を使用していました。\nGFDLは有効なライセンスですが、内容を理解するのは困難です。\nまた、GFDLのもとに置かれているコンテンツの再利用も困難です。", "config-email-settings": "メールの設定", "config-enable-email": "メール送信を有効にする", - "config-enable-email-help": "メールを使用したい場合は、[Config-dbsupport-oracle/manual/en/mail.configuration.php PHP のメール設定]が正しく設定されている必要があります。\nメールの機能を使用しない場合は、ここで無効にすることができます。", + "config-enable-email-help": "メールを使用したい場合は、[https://www.php.net/manual/en/mail.configuration.php PHP のメール設定]が正しく設定されている必要があります。\nメールの機能を使用しない場合は、ここで無効にすることができます。", "config-email-user": "利用者間のメールを有効にする", "config-email-user-help": "設定で有効になっている場合、すべてのユーザーがお互いにメールのやりとりを行うことを許可する。", "config-email-usertalk": "利用者のトークページでの通知を有効にする", @@ -326,6 +326,7 @@ "config-install-done": "おめでとうございます!\nMediaWikiのインストールに成功しました。\n\nLocalSettings.phpファイルが生成されました。\nこのファイルはすべての設定を含んでいます。\n\nこれをダウンロードして、ウィキをインストールした基準ディレクトリ (index.phpと同じディレクトリ) に設置する必要があります。ダウンロードは自動的に開始されるはずです。\n\nダウンロードが開始されていない場合、またはダウンロードをキャンセルした場合は、下記のリンクをクリックしてダウンロードを再開できます:\n\n$3\n\n注意: この生成された設定ファイルをダウンロードせずにインストールを終了すると、このファイルは利用できなくなります。\n\n上記の作業が完了すると、[$2 ウィキに入る]ことができます。", "config-install-done-path": "おめでとうございます!\nMediaWikiのインストールに成功しました。\n\nLocalSettings.phpファイルが生成されました。\nこのファイルはすべての設定を含んでいます。\n\nこれをダウンロードして、$4 に設置する必要があります。ダウンロードは自動的に開始されるはずです。\n\nダウンロードが開始されていない場合、またはダウンロードをキャンセルした場合は、下記のリンクをクリックしてダウンロードを再開できます:\n\n$3\n\n注意: この生成された設定ファイルをダウンロードせずにインストールを終了すると、このファイルは利用できなくなります。\n\n上記の作業が完了すると、[$2 ウィキに入る]ことができます。", "config-install-success": "MediaWikiが正常にインストールされました。\n今すぐ<$1$2>にアクセスしてあなたのwikiを表示できます。\nご質問がある場合は、よくある質問リストをご覧ください:\nまたは\nそのページにリンクされているサポートフォーラム", + "config-install-db-success": "データベースは正常にセットアップされました", "config-download-localsettings": "LocalSettings.php をダウンロード", "config-help": "ヘルプ", "config-help-tooltip": "クリックで展開", diff --git a/includes/installer/i18n/sr-ec.json b/includes/installer/i18n/sr-ec.json index 39d803c297..12b5620500 100644 --- a/includes/installer/i18n/sr-ec.json +++ b/includes/installer/i18n/sr-ec.json @@ -277,6 +277,7 @@ "config-install-done": "Честитамо!\nИнсталирали сте MediaWiki.\n\nИнсталациони програм је генерисао датотеку LocalSettings.php.\nОна садржи сву вашу конфигурацију.\n\nМораћете да је преузмете и ставите у базу ваше вики инсталације (исти директоријум као index.php). Преузимање би аутоматски требало почети.\n\nАко преузимање није понуђено, или ако га откажете, можете поново покренути преузимање тако што ћете кликнути на доленаведену везу:\n\n$3\n\nНапомена: Ако то одмах не урадите, ова генерисана конфигурациона датотека неће вам бити доступна касније ако изађете из инсталације без преузимања.\n\nКада је то учињено, можете да [$2 посетите свој вики].", "config-install-done-path": "Честитамо!\nИнсталирали сте MediaWiki.\n\nИнсталациони програм је генерисао датотеку LocalSettings.php.\nОна садржи сву вашу конфигурацију.\n\nМораћете да је преузмете и ставите у $4. Преузимање би аутоматски требало почети.\n\nАко преузимање није понуђено, или ако га откажете, можете поново покренути преузимање тако што ћете кликнути на доленаведену везу:\n\n$3\n\nНапомена: Ако то одмах не урадите, ова генерисана конфигурациона датотека неће вам бити доступна касније ако изађете из инсталације без преузимања.\n\nКада је то учињено, можете да [$2 посетите свој вики].", "config-install-success": "MediaWiki је успешно инсталиран. Сада можете посетити <$1$2> да бисте видели свој вики.\nАко имате питања, погледајте нашу листу често постављаних питања: или користите један од форума за подршку који су повезани на тој страници.", + "config-install-db-success": "База података је успешно подешена", "config-download-localsettings": "Преузми датотеку LocalSettings.php", "config-help": "помоћ", "config-help-tooltip": "кликните да бисте проширили", diff --git a/includes/installer/i18n/vi.json b/includes/installer/i18n/vi.json index cedcee1489..4f083c60df 100644 --- a/includes/installer/i18n/vi.json +++ b/includes/installer/i18n/vi.json @@ -55,7 +55,7 @@ "config-env-php": "PHP $1 đã được cài đặt.", "config-env-hhvm": "HHVM $1 được cài đặt.", "config-unicode-using-intl": "Sẽ sử dụng [https://pecl.php.net/intl phần mở rộng PECL intl] để chuẩn hóa Unicode.", - "config-unicode-pure-php-warning": "Cảnh báo: [https://pecl.php.net/intl intl PECL extension] không được phép xử lý Unicode chuẩn hóa, trả lại thực thi PHP-gốc chậm.\nNếu bạn chạy một site lưu lượng lớn, bạn phải để ý qua một chút trên [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].", + "config-unicode-pure-php-warning": "Cảnh báo: [https://pecl.php.net/intl PECL intl extension] không được phép xử lý Unicode chuẩn hóa, trả lại thực thi PHP-gốc chậm.\nNếu bạn chạy một site lưu lượng lớn, bạn nên đọc qua [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations chuẩn hóa Unicode].", "config-unicode-update-warning": "Cảnh báo: Phiên bản cài đặt của gói Unicode chuẩn hóa sử dụng một phiên bản cũ của thư viện [http://site.icu-project.org/ the ICU project].\nBạn phải [https://www.mediawiki.org/wiki/Special:MyLanguage/nâng cấp Unicode_normalization_considerations] nếu bạn quan tâm đến việc sử dụng Unicode.", "config-no-db": "Không tìm thấy một trình điều khiển cơ sở dữ liệu phù hợp! Bạn cần phải cài một trình điều khiển cơ sở dữ liệu cho PHP.\n{{PLURAL:$2|Loại|Các loại}} cơ sở dữ liệu sau đây được hỗ trợ: $1.\n\nNếu bạn đã biên dịch PHP lấy, cấu hình lại nó mà kích hoạt một trình khách cơ sở dữ liệu, ví dụ bằng lệnh ./configure --with-mysqli.\nNếu bạn đã cài PHP từ một gói Debian hoặc Ubuntu, thì bạn cũng cần phải cài ví dụ gói php-mysql.", "config-outdated-sqlite": "Chú ý: Bạn có SQLite $1, phiên bản này thấp hơn phiên bản yêu câu tối thiểu $2. SQLite sẽ không có tác dụng.", diff --git a/includes/libs/ParamValidator/Callbacks.php b/includes/libs/ParamValidator/Callbacks.php new file mode 100644 index 0000000000..d94a81fbd5 --- /dev/null +++ b/includes/libs/ParamValidator/Callbacks.php @@ -0,0 +1,78 @@ + [ 'class' => TypeDef\BooleanDef::class ], + 'checkbox' => [ 'class' => TypeDef\PresenceBooleanDef::class ], + 'integer' => [ 'class' => TypeDef\IntegerDef::class ], + 'limit' => [ 'class' => TypeDef\LimitDef::class ], + 'float' => [ 'class' => TypeDef\FloatDef::class ], + 'double' => [ 'class' => TypeDef\FloatDef::class ], + 'string' => [ 'class' => TypeDef\StringDef::class ], + 'password' => [ 'class' => TypeDef\PasswordDef::class ], + 'NULL' => [ + 'class' => TypeDef\StringDef::class, + 'args' => [ [ + 'allowEmptyWhenRequired' => true, + ] ], + ], + 'timestamp' => [ 'class' => TypeDef\TimestampDef::class ], + 'upload' => [ 'class' => TypeDef\UploadDef::class ], + 'enum' => [ 'class' => TypeDef\EnumDef::class ], + ]; + + /** @var Callbacks */ + private $callbacks; + + /** @var ObjectFactory */ + private $objectFactory; + + /** @var (TypeDef|array)[] Map parameter type names to TypeDef objects or ObjectFactory specs */ + private $typeDefs = []; + + /** @var int Default values for PARAM_ISMULTI_LIMIT1 */ + private $ismultiLimit1; + + /** @var int Default values for PARAM_ISMULTI_LIMIT2 */ + private $ismultiLimit2; + + /** + * @param Callbacks $callbacks + * @param ObjectFactory $objectFactory To turn specs into TypeDef objects + * @param array $options Associative array of additional settings + * - 'typeDefs': (array) As for addTypeDefs(). If omitted, self::$STANDARD_TYPES will be used. + * Pass an empty array if you want to start with no registered types. + * - 'ismultiLimits': (int[]) Two ints, being the default values for PARAM_ISMULTI_LIMIT1 and + * PARAM_ISMULTI_LIMIT2. If not given, defaults to `[ 50, 500 ]`. + */ + public function __construct( + Callbacks $callbacks, + ObjectFactory $objectFactory, + array $options = [] + ) { + $this->callbacks = $callbacks; + $this->objectFactory = $objectFactory; + + $this->addTypeDefs( $options['typeDefs'] ?? self::$STANDARD_TYPES ); + $this->ismultiLimit1 = $options['ismultiLimits'][0] ?? 50; + $this->ismultiLimit2 = $options['ismultiLimits'][1] ?? 500; + } + + /** + * List known type names + * @return string[] + */ + public function knownTypes() { + return array_keys( $this->typeDefs ); + } + + /** + * Register multiple type handlers + * + * @see addTypeDef() + * @param array $typeDefs Associative array mapping `$name` to `$typeDef`. + */ + public function addTypeDefs( array $typeDefs ) { + foreach ( $typeDefs as $name => $def ) { + $this->addTypeDef( $name, $def ); + } + } + + /** + * Register a type handler + * + * To allow code to omit PARAM_TYPE in settings arrays to derive the type + * from PARAM_DEFAULT, it is strongly recommended that the following types be + * registered: "boolean", "integer", "double", "string", "NULL", and "enum". + * + * When using ObjectFactory specs, the following extra arguments are passed: + * - The Callbacks object for this ParamValidator instance. + * + * @param string $name Type name + * @param TypeDef|array $typeDef Type handler or ObjectFactory spec to create one. + */ + public function addTypeDef( $name, $typeDef ) { + Assert::parameterType( + implode( '|', [ TypeDef::class, 'array' ] ), + $typeDef, + '$typeDef' + ); + + if ( isset( $this->typeDefs[$name] ) ) { + throw new InvalidArgumentException( "Type '$name' is already registered" ); + } + $this->typeDefs[$name] = $typeDef; + } + + /** + * Register a type handler, overriding any existing handler + * @see addTypeDef + * @param string $name Type name + * @param TypeDef|array|null $typeDef As for addTypeDef, or null to unregister a type. + */ + public function overrideTypeDef( $name, $typeDef ) { + Assert::parameterType( + implode( '|', [ TypeDef::class, 'array', 'null' ] ), + $typeDef, + '$typeDef' + ); + + if ( $typeDef === null ) { + unset( $this->typeDefs[$name] ); + } else { + $this->typeDefs[$name] = $typeDef; + } + } + + /** + * Test if a type is registered + * @param string $name Type name + * @return bool + */ + public function hasTypeDef( $name ) { + return isset( $this->typeDefs[$name] ); + } + + /** + * Get the TypeDef for a type + * @param string|array $type Any array is considered equivalent to the string "enum". + * @return TypeDef|null + */ + public function getTypeDef( $type ) { + if ( is_array( $type ) ) { + $type = 'enum'; + } + + if ( !isset( $this->typeDefs[$type] ) ) { + return null; + } + + $def = $this->typeDefs[$type]; + if ( !$def instanceof TypeDef ) { + $def = $this->objectFactory->createObject( $def, [ + 'extraArgs' => [ $this->callbacks ], + 'assertClass' => TypeDef::class, + ] ); + $this->typeDefs[$type] = $def; + } + + return $def; + } + + /** + * Normalize a parameter settings array + * @param array|mixed $settings Default value or an array of settings + * using PARAM_* constants. + * @return array + */ + public function normalizeSettings( $settings ) { + // Shorthand + if ( !is_array( $settings ) ) { + $settings = [ + self::PARAM_DEFAULT => $settings, + ]; + } + + // When type is not given, determine it from the type of the PARAM_DEFAULT + if ( !isset( $settings[self::PARAM_TYPE] ) ) { + $settings[self::PARAM_TYPE] = gettype( $settings[self::PARAM_DEFAULT] ?? null ); + } + + $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); + if ( $typeDef ) { + $settings = $typeDef->normalizeSettings( $settings ); + } + + return $settings; + } + + /** + * Fetch and valiate a parameter value using a settings array + * + * @param string $name Parameter name + * @param array|mixed $settings Default value or an array of settings + * using PARAM_* constants. + * @param array $options Options array, passed through to the TypeDef and Callbacks. + * @return mixed Validated parameter value + * @throws ValidationException if the value is invalid + */ + public function getValue( $name, $settings, array $options = [] ) { + $settings = $this->normalizeSettings( $settings ); + + $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); + if ( !$typeDef ) { + throw new DomainException( + "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}" + ); + } + + $value = $typeDef->getValue( $name, $settings, $options ); + + if ( $value !== null ) { + if ( !empty( $settings[self::PARAM_SENSITIVE] ) ) { + $this->callbacks->recordCondition( + new ValidationException( $name, $value, $settings, 'param-sensitive', [] ), + $options + ); + } + + // Set a warning if a deprecated parameter has been passed + if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) { + $this->callbacks->recordCondition( + new ValidationException( $name, $value, $settings, 'param-deprecated', [] ), + $options + ); + } + } elseif ( isset( $settings[self::PARAM_DEFAULT] ) ) { + $value = $settings[self::PARAM_DEFAULT]; + } + + return $this->validateValue( $name, $value, $settings, $options ); + } + + /** + * Valiate a parameter value using a settings array + * + * @param string $name Parameter name + * @param null|mixed $value Parameter value + * @param array|mixed $settings Default value or an array of settings + * using PARAM_* constants. + * @param array $options Options array, passed through to the TypeDef and Callbacks. + * - An additional option, 'values-list', will be set when processing the + * values of a multi-valued parameter. + * @return mixed Validated parameter value(s) + * @throws ValidationException if the value is invalid + */ + public function validateValue( $name, $value, $settings, array $options = [] ) { + $settings = $this->normalizeSettings( $settings ); + + $typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] ); + if ( !$typeDef ) { + throw new DomainException( + "Param $name's type is unknown - {$settings[self::PARAM_TYPE]}" + ); + } + + if ( $value === null ) { + if ( !empty( $settings[self::PARAM_REQUIRED] ) ) { + throw new ValidationException( $name, $value, $settings, 'missingparam', [] ); + } + return null; + } + + // Non-multi + if ( empty( $settings[self::PARAM_ISMULTI] ) ) { + return $typeDef->validate( $name, $value, $settings, $options ); + } + + // Split the multi-value and validate each parameter + $limit1 = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1; + $limit2 = $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2; + $valuesList = is_array( $value ) ? $value : self::explodeMultiValue( $value, $limit2 + 1 ); + + // Handle PARAM_ALL + $enumValues = $typeDef->getEnumValues( $name, $settings, $options ); + if ( is_array( $enumValues ) && isset( $settings[self::PARAM_ALL] ) && + count( $valuesList ) === 1 + ) { + $allValue = is_string( $settings[self::PARAM_ALL] ) + ? $settings[self::PARAM_ALL] + : self::ALL_DEFAULT_STRING; + if ( $valuesList[0] === $allValue ) { + return $enumValues; + } + } + + // Avoid checking useHighLimits() unless it's actually necessary + $sizeLimit = count( $valuesList ) > $limit1 && $this->callbacks->useHighLimits( $options ) + ? $limit2 + : $limit1; + if ( count( $valuesList ) > $sizeLimit ) { + throw new ValidationException( $name, $valuesList, $settings, 'toomanyvalues', [ + 'limit' => $sizeLimit + ] ); + } + + $options['values-list'] = $valuesList; + $validValues = []; + $invalidValues = []; + foreach ( $valuesList as $v ) { + try { + $validValues[] = $typeDef->validate( $name, $v, $settings, $options ); + } catch ( ValidationException $ex ) { + if ( empty( $settings[self::PARAM_IGNORE_INVALID_VALUES] ) ) { + throw $ex; + } + $invalidValues[] = $v; + } + } + if ( $invalidValues ) { + $this->callbacks->recordCondition( + new ValidationException( $name, $value, $settings, 'unrecognizedvalues', [ + 'values' => $invalidValues, + ] ), + $options + ); + } + + // Throw out duplicates if requested + if ( empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) { + $validValues = array_values( array_unique( $validValues ) ); + } + + return $validValues; + } + + /** + * Split a multi-valued parameter string, like explode() + * + * Note that, unlike explode(), this will return an empty array when given + * an empty string. + * + * @param string $value + * @param int $limit + * @return string[] + */ + public static function explodeMultiValue( $value, $limit ) { + if ( $value === '' || $value === "\x1f" ) { + return []; + } + + if ( substr( $value, 0, 1 ) === "\x1f" ) { + $sep = "\x1f"; + $value = substr( $value, 1 ); + } else { + $sep = '|'; + } + + return explode( $sep, $value, $limit ); + } + +} diff --git a/includes/libs/ParamValidator/README.md b/includes/libs/ParamValidator/README.md new file mode 100644 index 0000000000..dd992a408a --- /dev/null +++ b/includes/libs/ParamValidator/README.md @@ -0,0 +1,58 @@ +Wikimedia API Parameter Validator +================================= + +This library implements a system for processing and validating parameters to an +API from data like that in PHP's `$_GET`, `$_POST`, and `$_FILES` arrays, based +on a declarative definition of available parameters. + +Usage +----- + +
+use Wikimedia\ParamValidator\ParamValidator;
+use Wikimedia\ParamValidator\TypeDef\IntegerDef;
+use Wikimedia\ParamValidator\SimpleCallbacks as ParamValidatorCallbacks;
+use Wikimedia\ParamValidator\ValidationException;
+
+$validator = new ParamValidator(
+	new ParamValidatorCallbacks( $_POST + $_GET, $_FILES ),
+	$serviceContainer->getObjectFactory()
+);
+
+try {
+	$intValue = $validator->getValue( 'intParam', [
+			ParamValidator::PARAM_TYPE => 'integer',
+			ParamValidator::PARAM_DEFAULT => 0,
+			IntegerDef::PARAM_MIN => 0,
+			IntegerDef::PARAM_MAX => 5,
+	] );
+} catch ( ValidationException $ex ) {
+	$error = lookupI18nMessage( 'param-validator-error-' . $ex->getFailureCode() );
+	echo "Validation error: $error\n";
+}
+
+ +I18n +---- + +This library is designed to generate output in a manner suited to use with an +i18n system. To that end, errors and such are indicated by means of "codes" +consisting of ASCII lowercase letters, digits, and hyphen (and always beginning +with a letter). + +Additional details about each error, such as the allowed range for an integer +value, are similarly returned by means of associative arrays with keys being +similar "code" strings and values being strings, integers, or arrays of strings +that are intended to be formatted as a list (e.g. joined with commas). The +details for any particular "message" will also always have the same keys in the +same order to facilitate use with i18n systems using positional rather than +named parameters. + +For possible codes and their parameters, see the documentation of the relevant +`PARAM_*` constants and TypeDef classes. + +Running tests +------------- + + composer install --prefer-dist + composer test diff --git a/includes/libs/ParamValidator/SimpleCallbacks.php b/includes/libs/ParamValidator/SimpleCallbacks.php new file mode 100644 index 0000000000..77dab92619 --- /dev/null +++ b/includes/libs/ParamValidator/SimpleCallbacks.php @@ -0,0 +1,79 @@ +params = $params; + $this->files = $files; + } + + public function hasParam( $name, array $options ) { + return isset( $this->params[$name] ); + } + + public function getValue( $name, $default, array $options ) { + return $this->params[$name] ?? $default; + } + + public function hasUpload( $name, array $options ) { + return isset( $this->files[$name] ); + } + + public function getUploadedFile( $name, array $options ) { + $file = $this->files[$name] ?? null; + if ( $file && !$file instanceof UploadedFile ) { + $file = new UploadedFile( $file ); + $this->files[$name] = $file; + } + return $file; + } + + public function recordCondition( ValidationException $condition, array $options ) { + $this->conditions[] = $condition; + } + + /** + * Fetch any recorded conditions + * @return array[] + */ + public function getRecordedConditions() { + return $this->conditions; + } + + /** + * Clear any recorded conditions + */ + public function clearRecordedConditions() { + $this->conditions = []; + } + + public function useHighLimits( array $options ) { + return !empty( $options['useHighLimits'] ); + } + +} diff --git a/includes/libs/ParamValidator/TypeDef.php b/includes/libs/ParamValidator/TypeDef.php new file mode 100644 index 0000000000..0d54addc58 --- /dev/null +++ b/includes/libs/ParamValidator/TypeDef.php @@ -0,0 +1,148 @@ +callbacks = $callbacks; + } + + /** + * Get the value from the request + * + * @note Only override this if you need to use something other than + * $this->callbacks->getValue() to fetch the value. Reformatting from a + * string should typically be done by self::validate(). + * @note Handling of ParamValidator::PARAM_DEFAULT should be left to ParamValidator, + * as should PARAM_REQUIRED and the like. + * + * @param string $name Parameter name being fetched. + * @param array $settings Parameter settings array. + * @param array $options Options array. + * @return null|mixed Return null if the value wasn't present, otherwise a + * value to be passed to self::validate(). + */ + public function getValue( $name, array $settings, array $options ) { + return $this->callbacks->getValue( $name, null, $options ); + } + + /** + * Validate the value + * + * When ParamValidator is processing a multi-valued parameter, this will be + * called once for each of the supplied values. Which may mean zero calls. + * + * When getValue() returned null, this will not be called. + * + * @param string $name Parameter name being validated. + * @param mixed $value Value to validate, from getValue(). + * @param array $settings Parameter settings array. + * @param array $options Options array. Note the following values that may be set + * by ParamValidator: + * - values-list: (string[]) If defined, values of a multi-valued parameter are being processed + * (and this array holds the full set of values). + * @return mixed Validated value + * @throws ValidationException if the value is invalid + */ + abstract public function validate( $name, $value, array $settings, array $options ); + + /** + * Normalize a settings array + * @param array $settings + * @return array + */ + public function normalizeSettings( array $settings ) { + return $settings; + } + + /** + * Get the values for enum-like parameters + * + * This is primarily intended for documentation and implementation of + * PARAM_ALL; it is the responsibility of the TypeDef to ensure that validate() + * accepts the values returned here. + * + * @param string $name Parameter name being validated. + * @param array $settings Parameter settings array. + * @param array $options Options array. + * @return array|null All possible enumerated values, or null if this is + * not an enumeration. + */ + public function getEnumValues( $name, array $settings, array $options ) { + return null; + } + + /** + * Convert a value to a string representation. + * + * This is intended as the inverse of getValue() and validate(): this + * should accept anything returned by those methods or expected to be used + * as PARAM_DEFAULT, and if the string from this method is passed in as client + * input or PARAM_DEFAULT it should give equivalent output from validate(). + * + * @param string $name Parameter name being converted. + * @param mixed $value Parameter value being converted. Do not pass null. + * @param array $settings Parameter settings array. + * @param array $options Options array. + * @return string|null Return null if there is no representation of $value + * reasonably satisfying the description given. + */ + public function stringifyValue( $name, $value, array $settings, array $options ) { + return (string)$value; + } + + /** + * "Describe" a settings array + * + * This is intended to format data about a settings array using this type + * in a way that would be useful for automatically generated documentation + * or a machine-readable interface specification. + * + * Keys in the description array should follow the same guidelines as the + * code described for ValidationException. + * + * By default, each value in the description array is a single string, + * integer, or array. When `$options['compact']` is supplied, each value is + * instead an array of such and related values may be combined. For example, + * a non-compact description for an integer type might include + * `[ 'default' => 0, 'min' => 0, 'max' => 5 ]`, while in compact mode it might + * instead report `[ 'default' => [ 'value' => 0 ], 'minmax' => [ 'min' => 0, 'max' => 5 ] ]` + * to facilitate auto-generated documentation turning that 'minmax' into + * "Value must be between 0 and 5" rather than disconnected statements + * "Value must be >= 0" and "Value must be <= 5". + * + * @param string $name Parameter name being described. + * @param array $settings Parameter settings array. + * @param array $options Options array. Defined options for this base class are: + * - 'compact': (bool) Enable compact mode, as described above. + * @return array + */ + public function describeSettings( $name, array $settings, array $options ) { + $compact = !empty( $options['compact'] ); + + $ret = []; + + if ( isset( $settings[ParamValidator::PARAM_DEFAULT] ) ) { + $value = $this->stringifyValue( + $name, $settings[ParamValidator::PARAM_DEFAULT], $settings, $options + ); + $ret['default'] = $compact ? [ 'value' => $value ] : $value; + } + + return $ret; + } + +} diff --git a/includes/libs/ParamValidator/TypeDef/BooleanDef.php b/includes/libs/ParamValidator/TypeDef/BooleanDef.php new file mode 100644 index 0000000000..f77c930499 --- /dev/null +++ b/includes/libs/ParamValidator/TypeDef/BooleanDef.php @@ -0,0 +1,45 @@ + self::$TRUEVALS, + 'falsevals' => array_merge( self::$FALSEVALS, [ 'the empty string' ] ), + ] ); + } + + public function stringifyValue( $name, $value, array $settings, array $options ) { + return $value ? self::$TRUEVALS[0] : self::$FALSEVALS[0]; + } + +} diff --git a/includes/libs/ParamValidator/TypeDef/EnumDef.php b/includes/libs/ParamValidator/TypeDef/EnumDef.php new file mode 100644 index 0000000000..0f4f6908e5 --- /dev/null +++ b/includes/libs/ParamValidator/TypeDef/EnumDef.php @@ -0,0 +1,88 @@ +getEnumValues( $name, $settings, $options ); + + if ( in_array( $value, $values, true ) ) { + // Set a warning if a deprecated parameter value has been passed + if ( isset( $settings[self::PARAM_DEPRECATED_VALUES][$value] ) ) { + $this->callbacks->recordCondition( + new ValidationException( $name, $value, $settings, 'deprecated-value', [ + 'flag' => $settings[self::PARAM_DEPRECATED_VALUES][$value], + ] ), + $options + ); + } + + return $value; + } + + if ( !isset( $options['values-list'] ) && + count( ParamValidator::explodeMultiValue( $value, 2 ) ) > 1 + ) { + throw new ValidationException( $name, $value, $settings, 'notmulti', [] ); + } else { + throw new ValidationException( $name, $value, $settings, 'badvalue', [] ); + } + } + + public function getEnumValues( $name, array $settings, array $options ) { + return $settings[ParamValidator::PARAM_TYPE]; + } + + public function stringifyValue( $name, $value, array $settings, array $options ) { + if ( !is_array( $value ) ) { + return parent::stringifyValue( $name, $value, $settings, $options ); + } + + foreach ( $value as $v ) { + if ( strpos( $v, '|' ) !== false ) { + return "\x1f" . implode( "\x1f", $value ); + } + } + return implode( '|', $value ); + } + +} diff --git a/includes/libs/ParamValidator/TypeDef/FloatDef.php b/includes/libs/ParamValidator/TypeDef/FloatDef.php new file mode 100644 index 0000000000..0a204b3a88 --- /dev/null +++ b/includes/libs/ParamValidator/TypeDef/FloatDef.php @@ -0,0 +1,72 @@ + '.', + // PHP's number formatting currently uses only the first byte from 'decimal_point'. + // See upstream bug https://bugs.php.net/bug.php?id=78113 + $localeData['decimal_point'][0] => '.', + ] ); + } + return $value; + } + + public function stringifyValue( $name, $value, array $settings, array $options ) { + // Ensure sufficient precision for round-tripping. PHP_FLOAT_DIG was added in PHP 7.2. + $digits = defined( 'PHP_FLOAT_DIG' ) ? PHP_FLOAT_DIG : 15; + return $this->fixLocaleWeirdness( sprintf( "%.{$digits}g", $value ) ); + } + +} diff --git a/includes/libs/ParamValidator/TypeDef/IntegerDef.php b/includes/libs/ParamValidator/TypeDef/IntegerDef.php new file mode 100644 index 0000000000..556301b898 --- /dev/null +++ b/includes/libs/ParamValidator/TypeDef/IntegerDef.php @@ -0,0 +1,171 @@ + $max ) { + if ( $max2 !== null && $this->callbacks->useHighLimits( $options ) ) { + if ( $ret > $max2 ) { + $err = 'abovehighmaximum'; + $ret = $max2; + } + } else { + $err = 'abovemaximum'; + $ret = $max; + } + } + if ( $err !== null ) { + $ex = new ValidationException( $name, $value, $settings, $err, [ + 'min' => $min === null ? '' : $min, + 'max' => $max === null ? '' : $max, + 'max2' => $max2 === null ? '' : $max2, + ] ); + if ( empty( $settings[self::PARAM_IGNORE_RANGE] ) ) { + throw $ex; + } + $this->callbacks->recordCondition( $ex, $options ); + } + + return $ret; + } + + public function normalizeSettings( array $settings ) { + if ( !isset( $settings[self::PARAM_MAX] ) ) { + unset( $settings[self::PARAM_MAX2] ); + } + + if ( isset( $settings[self::PARAM_MAX2] ) && isset( $settings[self::PARAM_MAX] ) && + $settings[self::PARAM_MAX2] < $settings[self::PARAM_MAX] + ) { + $settings[self::PARAM_MAX2] = $settings[self::PARAM_MAX]; + } + + return parent::normalizeSettings( $settings ); + } + + public function describeSettings( $name, array $settings, array $options ) { + $info = parent::describeSettings( $name, $settings, $options ); + + $min = $settings[self::PARAM_MIN] ?? ''; + $max = $settings[self::PARAM_MAX] ?? ''; + $max2 = $settings[self::PARAM_MAX2] ?? ''; + if ( $max === '' || $max2 !== '' && $max2 <= $max ) { + $max2 = ''; + } + + if ( empty( $options['compact'] ) ) { + if ( $min !== '' ) { + $info['min'] = $min; + } + if ( $max !== '' ) { + $info['max'] = $max; + } + if ( $max2 !== '' ) { + $info['max2'] = $max2; + } + } else { + $key = ''; + if ( $min !== '' ) { + $key = 'min'; + } + if ( $max2 !== '' ) { + $key .= 'max2'; + } elseif ( $max !== '' ) { + $key .= 'max'; + } + if ( $key !== '' ) { + $info[$key] = [ 'min' => $min, 'max' => $max, 'max2' => $max2 ]; + } + } + + return $info; + } + +} diff --git a/includes/libs/ParamValidator/TypeDef/LimitDef.php b/includes/libs/ParamValidator/TypeDef/LimitDef.php new file mode 100644 index 0000000000..99780c4a0d --- /dev/null +++ b/includes/libs/ParamValidator/TypeDef/LimitDef.php @@ -0,0 +1,44 @@ +callbacks->useHighLimits( $options ) + ? $settings[self::PARAM_MAX2] ?? $settings[self::PARAM_MAX] ?? PHP_INT_MAX + : $settings[self::PARAM_MAX] ?? PHP_INT_MAX; + } + return $value; + } + + return parent::validate( $name, $value, $settings, $options ); + } + + public function normalizeSettings( array $settings ) { + $settings += [ + self::PARAM_MIN => 0, + ]; + + return parent::normalizeSettings( $settings ); + } + +} diff --git a/includes/libs/ParamValidator/TypeDef/PasswordDef.php b/includes/libs/ParamValidator/TypeDef/PasswordDef.php new file mode 100644 index 0000000000..289db54869 --- /dev/null +++ b/includes/libs/ParamValidator/TypeDef/PasswordDef.php @@ -0,0 +1,22 @@ +callbacks->hasParam( $name, $options ); + } + + public function validate( $name, $value, array $settings, array $options ) { + return (bool)$value; + } + + public function describeSettings( $name, array $settings, array $options ) { + $info = parent::describeSettings( $name, $settings, $options ); + unset( $info['default'] ); + return $info; + } + +} diff --git a/includes/libs/ParamValidator/TypeDef/StringDef.php b/includes/libs/ParamValidator/TypeDef/StringDef.php new file mode 100644 index 0000000000..0ed310b50f --- /dev/null +++ b/includes/libs/ParamValidator/TypeDef/StringDef.php @@ -0,0 +1,88 @@ +allowEmptyWhenRequired = !empty( $options['allowEmptyWhenRequired'] ); + } + + public function validate( $name, $value, array $settings, array $options ) { + if ( !$this->allowEmptyWhenRequired && $value === '' && + !empty( $settings[ParamValidator::PARAM_REQUIRED] ) + ) { + throw new ValidationException( $name, $value, $settings, 'missingparam', [] ); + } + + if ( isset( $settings[self::PARAM_MAX_BYTES] ) + && strlen( $value ) > $settings[self::PARAM_MAX_BYTES] + ) { + throw new ValidationException( $name, $value, $settings, 'maxbytes', [ + 'maxbytes' => $settings[self::PARAM_MAX_BYTES] ?? '', + 'maxchars' => $settings[self::PARAM_MAX_CHARS] ?? '', + ] ); + } + if ( isset( $settings[self::PARAM_MAX_CHARS] ) + && mb_strlen( $value, 'UTF-8' ) > $settings[self::PARAM_MAX_CHARS] + ) { + throw new ValidationException( $name, $value, $settings, 'maxchars', [ + 'maxbytes' => $settings[self::PARAM_MAX_BYTES] ?? '', + 'maxchars' => $settings[self::PARAM_MAX_CHARS] ?? '', + ] ); + } + + return $value; + } + +} diff --git a/includes/libs/ParamValidator/TypeDef/TimestampDef.php b/includes/libs/ParamValidator/TypeDef/TimestampDef.php new file mode 100644 index 0000000000..5d0bf4e951 --- /dev/null +++ b/includes/libs/ParamValidator/TypeDef/TimestampDef.php @@ -0,0 +1,100 @@ +defaultFormat = $options['defaultFormat'] ?? 'ConvertibleTimestamp'; + $this->stringifyFormat = $options['stringifyFormat'] ?? TS_ISO_8601; + } + + public function validate( $name, $value, array $settings, array $options ) { + // Confusing synonyms for the current time accepted by ConvertibleTimestamp + if ( !$value ) { + $this->callbacks->recordCondition( + new ValidationException( $name, $value, $settings, 'unclearnowtimestamp', [] ), + $options + ); + $value = 'now'; + } + + try { + $timestamp = new ConvertibleTimestamp( $value === 'now' ? false : $value ); + } catch ( TimestampException $ex ) { + throw new ValidationException( $name, $value, $settings, 'badtimestamp', [], $ex ); + } + + $format = $settings[self::PARAM_TIMESTAMP_FORMAT] ?? $this->defaultFormat; + switch ( $format ) { + case 'ConvertibleTimestamp': + return $timestamp; + + case 'DateTime': + // Eew, no getter. + return $timestamp->timestamp; + + default: + return $timestamp->getTimestamp( $format ); + } + } + + public function stringifyValue( $name, $value, array $settings, array $options ) { + if ( !$value instanceof ConvertibleTimestamp ) { + $value = new ConvertibleTimestamp( $value ); + } + return $value->getTimestamp( $this->stringifyFormat ); + } + +} diff --git a/includes/libs/ParamValidator/TypeDef/UploadDef.php b/includes/libs/ParamValidator/TypeDef/UploadDef.php new file mode 100644 index 0000000000..b436a6dc54 --- /dev/null +++ b/includes/libs/ParamValidator/TypeDef/UploadDef.php @@ -0,0 +1,116 @@ +callbacks->getUploadedFile( $name, $options ); + + if ( $ret && $ret->getError() === UPLOAD_ERR_NO_FILE && + !$this->callbacks->hasParam( $name, $options ) + ) { + // This seems to be that the client explicitly specified "no file" for the field + // instead of just omitting the field completely. DWTM. + $ret = null; + } elseif ( !$ret && $this->callbacks->hasParam( $name, $options ) ) { + // The client didn't format their upload properly so it came in as an ordinary + // field. Convert it to an error. + $ret = new UploadedFile( [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => -42, // PHP's UPLOAD_ERR_* are all positive numbers. + 'size' => 0, + ] ); + } + + return $ret; + } + + /** + * Fetch the value of PHP's upload_max_filesize ini setting + * + * This method exists so it can be mocked by unit tests that can't + * affect ini_get() directly. + * + * @codeCoverageIgnore + * @return string|false + */ + protected function getIniSize() { + return ini_get( 'upload_max_filesize' ); + } + + public function validate( $name, $value, array $settings, array $options ) { + static $codemap = [ + -42 => 'notupload', // Local from getValue() + UPLOAD_ERR_FORM_SIZE => 'formsize', + UPLOAD_ERR_PARTIAL => 'partial', + UPLOAD_ERR_NO_FILE => 'nofile', + UPLOAD_ERR_NO_TMP_DIR => 'notmpdir', + UPLOAD_ERR_CANT_WRITE => 'cantwrite', + UPLOAD_ERR_EXTENSION => 'phpext', + ]; + + if ( !$value instanceof UploadedFileInterface ) { + // Err? + throw new ValidationException( $name, $value, $settings, 'badupload', [] ); + } + + $err = $value->getError(); + if ( $err === UPLOAD_ERR_OK ) { + return $value; + } elseif ( $err === UPLOAD_ERR_INI_SIZE ) { + static $prefixes = [ + 'g' => 1024 ** 3, + 'm' => 1024 ** 2, + 'k' => 1024 ** 1, + ]; + $size = $this->getIniSize(); + $last = strtolower( substr( $size, -1 ) ); + $size = intval( $size, 10 ) * ( $prefixes[$last] ?? 1 ); + throw new ValidationException( $name, $value, $settings, 'badupload-inisize', [ + 'size' => $size, + ] ); + } elseif ( isset( $codemap[$err] ) ) { + throw new ValidationException( $name, $value, $settings, 'badupload-' . $codemap[$err], [] ); + } else { + throw new ValidationException( $name, $value, $settings, 'badupload-unknown', [ + 'code' => $err, + ] ); + } + } + + public function stringifyValue( $name, $value, array $settings, array $options ) { + // Not going to happen. + return null; + } + +} diff --git a/includes/libs/ParamValidator/Util/UploadedFile.php b/includes/libs/ParamValidator/Util/UploadedFile.php new file mode 100644 index 0000000000..2be9119d25 --- /dev/null +++ b/includes/libs/ParamValidator/Util/UploadedFile.php @@ -0,0 +1,141 @@ +data = $data; + $this->fromUpload = $fromUpload; + } + + /** + * Throw if there was an error + * @throws RuntimeException + */ + private function checkError() { + switch ( $this->data['error'] ) { + case UPLOAD_ERR_OK: + break; + + case UPLOAD_ERR_INI_SIZE: + throw new RuntimeException( 'Upload exceeded maximum size' ); + + case UPLOAD_ERR_FORM_SIZE: + throw new RuntimeException( 'Upload exceeded form-specified maximum size' ); + + case UPLOAD_ERR_PARTIAL: + throw new RuntimeException( 'File was only partially uploaded' ); + + case UPLOAD_ERR_NO_FILE: + throw new RuntimeException( 'No file was uploaded' ); + + case UPLOAD_ERR_NO_TMP_DIR: + throw new RuntimeException( 'PHP has no temporary folder for storing uploaded files' ); + + case UPLOAD_ERR_CANT_WRITE: + throw new RuntimeException( 'PHP was unable to save the uploaded file' ); + + case UPLOAD_ERR_EXTENSION: + throw new RuntimeException( 'A PHP extension stopped the file upload' ); + + default: + throw new RuntimeException( 'Unknown upload error code ' . $this->data['error'] ); + } + + if ( $this->moved ) { + throw new RuntimeException( 'File has already been moved' ); + } + if ( !isset( $this->data['tmp_name'] ) || !file_exists( $this->data['tmp_name'] ) ) { + throw new RuntimeException( 'Uploaded file is missing' ); + } + } + + public function getStream() { + if ( $this->stream ) { + return $this->stream; + } + + $this->checkError(); + $this->stream = new UploadedFileStream( $this->data['tmp_name'] ); + return $this->stream; + } + + public function moveTo( $targetPath ) { + $this->checkError(); + + if ( $this->fromUpload && !is_uploaded_file( $this->data['tmp_name'] ) ) { + throw new RuntimeException( 'Specified file is not an uploaded file' ); + } + + // TODO remove the function_exists check once we drop HHVM support + if ( function_exists( 'error_clear_last' ) ) { + error_clear_last(); + } + $ret = AtEase::quietCall( + $this->fromUpload ? 'move_uploaded_file' : 'rename', + $this->data['tmp_name'], + $targetPath + ); + if ( $ret === false ) { + $err = error_get_last(); + throw new RuntimeException( "Move failed: " . ( $err['message'] ?? 'Unknown error' ) ); + } + + $this->moved = true; + if ( $this->stream ) { + $this->stream->close(); + $this->stream = null; + } + } + + public function getSize() { + return $this->data['size'] ?? null; + } + + public function getError() { + return $this->data['error'] ?? UPLOAD_ERR_NO_FILE; + } + + public function getClientFilename() { + $ret = $this->data['name'] ?? null; + return $ret === '' ? null : $ret; + } + + public function getClientMediaType() { + $ret = $this->data['type'] ?? null; + return $ret === '' ? null : $ret; + } + +} diff --git a/includes/libs/ParamValidator/Util/UploadedFileStream.php b/includes/libs/ParamValidator/Util/UploadedFileStream.php new file mode 100644 index 0000000000..17eaaf4a01 --- /dev/null +++ b/includes/libs/ParamValidator/Util/UploadedFileStream.php @@ -0,0 +1,168 @@ +fp = self::quietCall( 'fopen', [ $filename, 'r' ], false, 'Failed to open file' ); + } + + /** + * Check if the stream is open + * @throws RuntimeException if closed + */ + private function checkOpen() { + if ( !$this->fp ) { + throw new RuntimeException( 'Stream is not open' ); + } + } + + public function __destruct() { + $this->close(); + } + + public function __toString() { + try { + $this->seek( 0 ); + return $this->getContents(); + } catch ( Exception $ex ) { + // Not allowed to throw + return ''; + } catch ( Throwable $ex ) { + // Not allowed to throw + return ''; + } + } + + public function close() { + if ( $this->fp ) { + // Spec doesn't care about close errors. + AtEase::quietCall( 'fclose', $this->fp ); + $this->fp = null; + } + } + + public function detach() { + $ret = $this->fp; + $this->fp = null; + return $ret; + } + + public function getSize() { + if ( $this->size === false ) { + $this->size = null; + + if ( $this->fp ) { + // Spec doesn't care about errors here. + $stat = AtEase::quietCall( 'fstat', $this->fp ); + $this->size = $stat['size'] ?? null; + } + } + + return $this->size; + } + + public function tell() { + $this->checkOpen(); + return self::quietCall( 'ftell', [ $this->fp ], -1, 'Cannot determine stream position' ); + } + + public function eof() { + // Spec doesn't care about errors here. + return !$this->fp || AtEase::quietCall( 'feof', $this->fp ); + } + + public function isSeekable() { + return (bool)$this->fp; + } + + public function seek( $offset, $whence = SEEK_SET ) { + $this->checkOpen(); + self::quietCall( 'fseek', [ $this->fp, $offset, $whence ], -1, 'Seek failed' ); + } + + public function rewind() { + $this->seek( 0 ); + } + + public function isWritable() { + return false; + } + + public function write( $string ) { + $this->checkOpen(); + throw new RuntimeException( 'Stream is read-only' ); + } + + public function isReadable() { + return (bool)$this->fp; + } + + public function read( $length ) { + $this->checkOpen(); + return self::quietCall( 'fread', [ $this->fp, $length ], false, 'Read failed' ); + } + + public function getContents() { + $this->checkOpen(); + return self::quietCall( 'stream_get_contents', [ $this->fp ], false, 'Read failed' ); + } + + public function getMetadata( $key = null ) { + $this->checkOpen(); + $ret = self::quietCall( 'stream_get_meta_data', [ $this->fp ], false, 'Metadata fetch failed' ); + if ( $key !== null ) { + $ret = $ret[$key] ?? null; + } + return $ret; + } + +} diff --git a/includes/libs/ParamValidator/ValidationException.php b/includes/libs/ParamValidator/ValidationException.php new file mode 100644 index 0000000000..c8d995e0b9 --- /dev/null +++ b/includes/libs/ParamValidator/ValidationException.php @@ -0,0 +1,128 @@ +paramName = $name; + $this->paramValue = $value; + $this->settings = $settings; + $this->failureCode = $code; + $this->failureData = $data; + } + + /** + * Make a simple English message for the exception + * @param string $name + * @param string $code + * @param array $data + * @return string + */ + private static function formatMessage( $name, $code, $data ) { + $ret = "Validation of `$name` failed: $code"; + foreach ( $data as $k => $v ) { + if ( is_array( $v ) ) { + $v = implode( ', ', $v ); + } + $ret .= "; $k => $v"; + } + return $ret; + } + + /** + * Fetch the parameter name that failed validation + * @return string + */ + public function getParamName() { + return $this->paramName; + } + + /** + * Fetch the parameter value that failed validation + * @return mixed + */ + public function getParamValue() { + return $this->paramValue; + } + + /** + * Fetch the settings array that failed validation + * @return array + */ + public function getSettings() { + return $this->settings; + } + + /** + * Fetch the validation failure code + * + * A validation failure code is a reasonably short string matching the regex + * `/^[a-z][a-z0-9-]*$/`. + * + * Users are encouraged to use this with a suitable i18n mechanism rather + * than relying on the limited English text returned by getMessage(). + * + * @return string + */ + public function getFailureCode() { + return $this->failureCode; + } + + /** + * Fetch the validation failure data + * + * This returns additional data relevant to the particular failure code. + * + * Keys in the array are short ASCII strings. Values are strings or + * integers, or arrays of strings intended to be displayed as a + * comma-separated list. For any particular code the same keys are always + * returned in the same order, making it safe to use array_values() and + * access them positionally if that is desired. + * + * For example, the data for a hypothetical "integer-out-of-range" code + * might have data `[ 'min' => 0, 'max' => 100 ]` indicating the range of + * allowed values. + * + * @return (string|int|string[])[] + */ + public function getFailureData() { + return $this->failureData; + } + +} diff --git a/includes/libs/filebackend/SwiftFileBackend.php b/includes/libs/filebackend/SwiftFileBackend.php index dc5aa22e78..a1b2460df6 100644 --- a/includes/libs/filebackend/SwiftFileBackend.php +++ b/includes/libs/filebackend/SwiftFileBackend.php @@ -297,7 +297,7 @@ class SwiftFileBackend extends FileBackendStore { $method = __METHOD__; $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) { list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response']; - if ( $rcode === 201 ) { + if ( $rcode === 201 || $rcode === 202 ) { // good } elseif ( $rcode === 412 ) { $status->fatal( 'backend-fail-contenttype', $params['dst'] ); @@ -360,7 +360,7 @@ class SwiftFileBackend extends FileBackendStore { $method = __METHOD__; $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) { list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response']; - if ( $rcode === 201 ) { + if ( $rcode === 201 || $rcode === 202 ) { // good } elseif ( $rcode === 412 ) { $status->fatal( 'backend-fail-contenttype', $params['dst'] ); diff --git a/includes/libs/rdbms/connectionmanager/ConnectionManager.php b/includes/libs/rdbms/connectionmanager/ConnectionManager.php index 27e6138102..50a0b0e1f8 100644 --- a/includes/libs/rdbms/connectionmanager/ConnectionManager.php +++ b/includes/libs/rdbms/connectionmanager/ConnectionManager.php @@ -73,7 +73,7 @@ class ConnectionManager { * @param int $i * @param string[]|null $groups * - * @return Database + * @return IDatabase */ private function getConnection( $i, array $groups = null ) { $groups = $groups === null ? $this->groups : $groups; @@ -97,7 +97,7 @@ class ConnectionManager { * * @since 1.29 * - * @return Database + * @return IDatabase */ public function getWriteConnection() { return $this->getConnection( DB_MASTER ); @@ -111,7 +111,7 @@ class ConnectionManager { * * @param string[]|null $groups * - * @return Database + * @return IDatabase */ public function getReadConnection( array $groups = null ) { $groups = $groups === null ? $this->groups : $groups; diff --git a/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php b/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php index aa3bea8fdc..ccb73d7256 100644 --- a/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php +++ b/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php @@ -64,7 +64,7 @@ class SessionConsistentConnectionManager extends ConnectionManager { * * @param string[]|null $groups * - * @return Database + * @return IDatabase */ public function getReadConnection( array $groups = null ) { if ( $this->forceWriteConnection ) { @@ -77,7 +77,7 @@ class SessionConsistentConnectionManager extends ConnectionManager { /** * @since 1.29 * - * @return Database + * @return IDatabase */ public function getWriteConnection() { $this->prepareForUpdates(); diff --git a/includes/libs/rdbms/database/DBConnRef.php b/includes/libs/rdbms/database/DBConnRef.php index 8af6bb3ff3..c8e31dfccf 100644 --- a/includes/libs/rdbms/database/DBConnRef.php +++ b/includes/libs/rdbms/database/DBConnRef.php @@ -598,6 +598,10 @@ class DBConnRef implements IDatabase { return $this->__call( __FUNCTION__, func_get_args() ); } + public function onAtomicSectionCancel( callable $callback, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + public function setTransactionListener( $name, callable $callback = null ) { return $this->__call( __FUNCTION__, func_get_args() ); } diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index bc8883c3ac..760d1373f1 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -38,6 +38,7 @@ use InvalidArgumentException; use UnexpectedValueException; use Exception; use RuntimeException; +use Throwable; /** * Relational database abstraction object @@ -74,7 +75,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware protected $delimiter = ';'; /** @var string|bool|null Stashed value of html_errors INI setting */ protected $htmlErrors; - /** @var int */ + /** @var int Row batch size to use for emulated INSERT SELECT queries */ protected $nonNativeInsertSelectBatchSize = 10000; /** @var BagOStuff APC cache */ @@ -93,22 +94,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware protected $trxProfiler; /** @var DatabaseDomain */ protected $currentDomain; + /** @var object|resource|null Database connection */ + protected $conn; + /** @var IDatabase|null Lazy handle to the master DB this server replicates from */ private $lazyMasterHandle; - /** @var object|resource|null Database connection */ - protected $conn = null; - /** @var bool Whether a connection handle is open (connection itself might be dead) */ - protected $opened = false; - /** @var array Map of (name => 1) for locks obtained via lock() */ protected $sessionNamedLocks = []; /** @var array Map of (table name => 1) for TEMPORARY tables */ protected $sessionTempTables = []; - /** @var int Whether there is an active transaction (1 or 0) */ - protected $trxLevel = 0; - /** @var string Hexidecimal string if a transaction is active or empty string otherwise */ + /** @var string ID of the active transaction or the empty string otherwise */ protected $trxShortId = ''; /** @var int Transaction status */ protected $trxStatus = self::STATUS_TRX_NONE; @@ -150,6 +147,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware private $trxPreCommitCallbacks = []; /** @var array[] List of (callable, method name, atomic section id) */ private $trxEndCallbacks = []; + /** @var array[] List of (callable, method name, atomic section id) */ + private $trxSectionCancelCallbacks = []; /** @var callable[] Map of (name => callable) */ private $trxRecurringCallbacks = []; /** @var bool Whether to suppress triggering of transaction end callbacks */ @@ -308,7 +307,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @param string $dbName Database name * @param string|null $schema Database schema name * @param string $tablePrefix Table prefix - * @return bool * @throws DBConnectionError */ abstract protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ); @@ -512,12 +510,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return $res; } - public function trxLevel() { - return $this->trxLevel; + final public function trxLevel() { + return ( $this->trxShortId != '' ) ? 1 : 0; } public function trxTimestamp() { - return $this->trxLevel ? $this->trxTimestamp : null; + return $this->trxLevel() ? $this->trxTimestamp : null; } /** @@ -620,20 +618,21 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function writesPending() { - return $this->trxLevel && $this->trxDoneWrites; + return $this->trxLevel() && $this->trxDoneWrites; } public function writesOrCallbacksPending() { - return $this->trxLevel && ( + return $this->trxLevel() && ( $this->trxDoneWrites || $this->trxIdleCallbacks || $this->trxPreCommitCallbacks || - $this->trxEndCallbacks + $this->trxEndCallbacks || + $this->trxSectionCancelCallbacks ); } public function preCommitCallbacksPending() { - return $this->trxLevel && $this->trxPreCommitCallbacks; + return $this->trxLevel() && $this->trxPreCommitCallbacks; } /** @@ -651,7 +650,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) { - if ( !$this->trxLevel ) { + if ( !$this->trxLevel() ) { return false; } elseif ( !$this->trxDoneWrites ) { return 0.0; @@ -681,7 +680,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function pendingWriteCallers() { - return $this->trxLevel ? $this->trxWriteCallers : []; + return $this->trxLevel() ? $this->trxWriteCallers : []; } public function pendingWriteRowsAffected() { @@ -701,7 +700,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware foreach ( [ $this->trxIdleCallbacks, $this->trxPreCommitCallbacks, - $this->trxEndCallbacks + $this->trxEndCallbacks, + $this->trxSectionCancelCallbacks ] as $callbacks ) { foreach ( $callbacks as $callback ) { $fnames[] = $callback[1]; @@ -721,7 +721,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function isOpen() { - return $this->opened; + return (bool)$this->conn; } public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) { @@ -865,11 +865,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware final public function close() { $exception = null; // error to throw after disconnecting - $wasOpen = $this->opened; + $wasOpen = (bool)$this->conn; // This should mostly do nothing if the connection is already closed if ( $this->conn ) { // Roll back any dangling transaction first - if ( $this->trxLevel ) { + if ( $this->trxLevel() ) { if ( $this->trxAtomicLevels ) { // Cannot let incomplete atomic sections be committed $levels = $this->flatAtomicSectionList(); @@ -914,7 +914,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } $this->conn = false; - $this->opened = false; // Throw any unexpected errors after having disconnected if ( $exception instanceof Exception ) { @@ -978,16 +977,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware */ abstract protected function closeConnection(); - /** - * @deprecated since 1.32 - * @param string $error Fallback message, if none is given by DB - * @throws DBConnectionError - */ - public function reportConnectionError( $error = 'Unknown error' ) { - call_user_func( $this->deprecationLogger, 'Use of ' . __METHOD__ . ' is deprecated.' ); - throw new DBConnectionError( $this, $this->lastError() ?: $error ); - } - /** * Run a query and return a DBMS-dependent wrapper or boolean * @@ -1167,7 +1156,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware final protected function executeQuery( $sql, $fname, $flags ) { $this->assertHasConnectionHandle(); - $priorTransaction = $this->trxLevel; + $priorTransaction = $this->trxLevel(); if ( $this->isWriteQuery( $sql ) ) { # In theory, non-persistent writes are allowed in read-only mode, but due to things @@ -1194,8 +1183,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware // Send the query to the server and fetch any corresponding errors list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) = $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ); + // Check if the query failed due to a recoverable connection loss - if ( $ret === false && $recoverableCL && $reconnected ) { + $allowRetry = !$this->hasFlags( $flags, self::QUERY_NO_RETRY ); + if ( $ret === false && $recoverableCL && $reconnected && $allowRetry ) { // Silently resend the query to the server since it is safe and possible list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) = $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ); @@ -1255,7 +1246,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware // Keep track of whether the transaction has write queries pending if ( $isPermWrite ) { $this->lastWriteTime = microtime( true ); - if ( $this->trxLevel && !$this->trxDoneWrites ) { + if ( $this->trxLevel() && !$this->trxDoneWrites ) { $this->trxDoneWrites = true; $this->trxProfiler->transactionWritingIn( $this->server, $this->getDomainID(), $this->trxShortId ); @@ -1285,7 +1276,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $ret !== false ) { $this->lastPing = $startTime; - if ( $isPermWrite && $this->trxLevel ) { + if ( $isPermWrite && $this->trxLevel() ) { $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() ); $this->trxWriteCallers[] = $fname; } @@ -1334,7 +1325,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware */ private function beginIfImplied( $sql, $fname ) { if ( - !$this->trxLevel && + !$this->trxLevel() && $this->getFlag( self::DBO_TRX ) && $this->isTransactableQuery( $sql ) ) { @@ -1464,7 +1455,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS $this->sessionNamedLocks = []; // Session loss implies transaction loss - $this->trxLevel = 0; + $oldTrxShortId = $this->consumeTrxShortId(); $this->trxAtomicCounter = 0; $this->trxIdleCallbacks = []; // T67263; transaction already lost $this->trxPreCommitCallbacks = []; // T67263; transaction already lost @@ -1473,7 +1464,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->trxProfiler->transactionWritingOut( $this->server, $this->getDomainID(), - $this->trxShortId, + $oldTrxShortId, $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ), $this->trxWriteAffectedRows ); @@ -1499,6 +1490,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } } + /** + * Reset the transaction ID and return the old one + * + * @return string The old transaction ID or the empty string if there wasn't one + */ + private function consumeTrxShortId() { + $old = $this->trxShortId; + $this->trxShortId = ''; + + return $old; + } + /** * Checks whether the cause of the error is detected to be a timeout. * @@ -1996,7 +1999,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware public function lockForUpdate( $table, $conds = '', $fname = __METHOD__, $options = [], $join_conds = [] ) { - if ( !$this->trxLevel && !$this->getFlag( self::DBO_TRX ) ) { + if ( !$this->trxLevel() && !$this->getFlag( self::DBO_TRX ) ) { throw new DBUnexpectedError( $this, __METHOD__ . ': no transaction is active nor is DBO_TRX set' @@ -3343,21 +3346,21 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) { - if ( !$this->trxLevel ) { + if ( !$this->trxLevel() ) { throw new DBUnexpectedError( $this, "No transaction is active." ); } $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ]; } final public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ ) { - if ( !$this->trxLevel && $this->getTransactionRoundId() ) { + if ( !$this->trxLevel() && $this->getTransactionRoundId() ) { // Start an implicit transaction similar to how query() does $this->begin( __METHOD__, self::TRANSACTION_INTERNAL ); $this->trxAutomatic = true; } $this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ]; - if ( !$this->trxLevel ) { + if ( !$this->trxLevel() ) { $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE ); } } @@ -3367,13 +3370,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) { - if ( !$this->trxLevel && $this->getTransactionRoundId() ) { + if ( !$this->trxLevel() && $this->getTransactionRoundId() ) { // Start an implicit transaction similar to how query() does $this->begin( __METHOD__, self::TRANSACTION_INTERNAL ); $this->trxAutomatic = true; } - if ( $this->trxLevel ) { + if ( $this->trxLevel() ) { $this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ]; } else { // No transaction is active nor will start implicitly, so make one for this callback @@ -3388,11 +3391,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } } + final public function onAtomicSectionCancel( callable $callback, $fname = __METHOD__ ) { + if ( !$this->trxLevel() || !$this->trxAtomicLevels ) { + throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." ); + } + $this->trxSectionCancelCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ]; + } + /** * @return AtomicSectionIdentifier|null ID of the topmost atomic section level */ private function currentAtomicSectionId() { - if ( $this->trxLevel && $this->trxAtomicLevels ) { + if ( $this->trxLevel() && $this->trxAtomicLevels ) { $levelInfo = end( $this->trxAtomicLevels ); return $levelInfo[1]; @@ -3402,6 +3412,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } /** + * Hoist callback ownership for callbacks in a section to a parent section. + * All callbacks should have an owner that is present in trxAtomicLevels. * @param AtomicSectionIdentifier $old * @param AtomicSectionIdentifier $new */ @@ -3423,13 +3435,35 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->trxEndCallbacks[$key][2] = $new; } } + foreach ( $this->trxSectionCancelCallbacks as $key => $info ) { + if ( $info[2] === $old ) { + $this->trxSectionCancelCallbacks[$key][2] = $new; + } + } } /** + * Update callbacks that were owned by cancelled atomic sections. + * + * Callbacks for "on commit" should never be run if they're owned by a + * section that won't be committed. + * + * Callbacks for "on resolution" need to reflect that the section was + * rolled back, even if the transaction as a whole commits successfully. + * + * Callbacks for "on section cancel" should already have been consumed, + * but errors during the cancellation itself can prevent that while still + * destroying the section. Hoist any such callbacks to the new top section, + * which we assume will itself have to be cancelled or rolled back to + * resolve the error. + * * @param AtomicSectionIdentifier[] $sectionIds ID of an actual savepoint + * @param AtomicSectionIdentifier|null $newSectionId New top section ID. * @throws UnexpectedValueException */ - private function modifyCallbacksForCancel( array $sectionIds ) { + private function modifyCallbacksForCancel( + array $sectionIds, AtomicSectionIdentifier $newSectionId = null + ) { // Cancel the "on commit" callbacks owned by this savepoint $this->trxIdleCallbacks = array_filter( $this->trxIdleCallbacks, @@ -3448,8 +3482,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( in_array( $entry[2], $sectionIds, true ) ) { $callback = $entry[0]; $this->trxEndCallbacks[$key][0] = function () use ( $callback ) { + // @phan-suppress-next-line PhanInfiniteRecursion No recursion at all here, phan is confused return $callback( self::TRIGGER_ROLLBACK, $this ); }; + // This "on resolution" callback no longer belongs to a section. + $this->trxEndCallbacks[$key][2] = null; + } + } + // Hoist callback ownership for section cancel callbacks to the new top section + foreach ( $this->trxSectionCancelCallbacks as $key => $entry ) { + if ( in_array( $entry[2], $sectionIds, true ) ) { + $this->trxSectionCancelCallbacks[$key][2] = $newSectionId; } } } @@ -3485,7 +3528,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @throws Exception */ public function runOnTransactionIdleCallbacks( $trigger ) { - if ( $this->trxLevel ) { // sanity + if ( $this->trxLevel() ) { // sanity throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open.' ); } @@ -3504,6 +3547,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware ); $this->trxIdleCallbacks = []; // consumed (and recursion guard) $this->trxEndCallbacks = []; // consumed (recursion guard) + + // Only run trxSectionCancelCallbacks on rollback, not commit. + // But always consume them. + if ( $trigger === self::TRIGGER_ROLLBACK ) { + $callbacks = array_merge( $callbacks, $this->trxSectionCancelCallbacks ); + } + $this->trxSectionCancelCallbacks = []; // consumed (recursion guard) + foreach ( $callbacks as $callback ) { ++$count; list( $phpCallback ) = $callback; @@ -3571,6 +3622,46 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return $count; } + /** + * Actually run any "atomic section cancel" callbacks. + * + * @param int $trigger IDatabase::TRIGGER_* constant + * @param AtomicSectionIdentifier[]|null $sectionId Section IDs to cancel, + * null on transaction rollback + */ + private function runOnAtomicSectionCancelCallbacks( + $trigger, array $sectionIds = null + ) { + /** @var Exception|Throwable $e */ + $e = null; // first exception + + $notCancelled = []; + do { + $callbacks = $this->trxSectionCancelCallbacks; + $this->trxSectionCancelCallbacks = []; // consumed (recursion guard) + foreach ( $callbacks as $entry ) { + if ( $sectionIds === null || in_array( $entry[2], $sectionIds, true ) ) { + try { + $entry[0]( $trigger, $this ); + } catch ( Exception $ex ) { + ( $this->errorLogger )( $ex ); + $e = $e ?: $ex; + } catch ( Throwable $ex ) { + // @todo: Log? + $e = $e ?: $ex; + } + } else { + $notCancelled[] = $entry; + } + } + } while ( count( $this->trxSectionCancelCallbacks ) ); + $this->trxSectionCancelCallbacks = $notCancelled; + + if ( $e !== null ) { + throw $e; // re-throw any first Exception/Throwable + } + } + /** * Actually run any "transaction listener" callbacks. * @@ -3668,7 +3759,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware ) { $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null; - if ( !$this->trxLevel ) { + if ( !$this->trxLevel() ) { $this->begin( $fname, self::TRANSACTION_INTERNAL ); // sets trxAutomatic // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result // in all changes being in one transaction to keep requests transactional. @@ -3694,7 +3785,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } final public function endAtomic( $fname = __METHOD__ ) { - if ( !$this->trxLevel || !$this->trxAtomicLevels ) { + if ( !$this->trxLevel() || !$this->trxAtomicLevels ) { throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." ); } @@ -3730,71 +3821,83 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware final public function cancelAtomic( $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null ) { - if ( !$this->trxLevel || !$this->trxAtomicLevels ) { + if ( !$this->trxLevel() || !$this->trxAtomicLevels ) { throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." ); } - $excisedFnames = []; - if ( $sectionId !== null ) { - // Find the (last) section with the given $sectionId - $pos = -1; - foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) { - if ( $asId === $sectionId ) { - $pos = $i; + $excisedIds = []; + $newTopSection = $this->currentAtomicSectionId(); + try { + $excisedFnames = []; + if ( $sectionId !== null ) { + // Find the (last) section with the given $sectionId + $pos = -1; + foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) { + if ( $asId === $sectionId ) { + $pos = $i; + } } + if ( $pos < 0 ) { + throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" ); + } + // Remove all descendant sections and re-index the array + $len = count( $this->trxAtomicLevels ); + for ( $i = $pos + 1; $i < $len; ++$i ) { + $excisedFnames[] = $this->trxAtomicLevels[$i][0]; + $excisedIds[] = $this->trxAtomicLevels[$i][1]; + } + $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 ); + $newTopSection = $this->currentAtomicSectionId(); } - if ( $pos < 0 ) { - throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" ); - } - // Remove all descendant sections and re-index the array - $excisedIds = []; - $len = count( $this->trxAtomicLevels ); - for ( $i = $pos + 1; $i < $len; ++$i ) { - $excisedFnames[] = $this->trxAtomicLevels[$i][0]; - $excisedIds[] = $this->trxAtomicLevels[$i][1]; - } - $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 ); - $this->modifyCallbacksForCancel( $excisedIds ); - } - // Check if the current section matches $fname - $pos = count( $this->trxAtomicLevels ) - 1; - list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos]; + // Check if the current section matches $fname + $pos = count( $this->trxAtomicLevels ) - 1; + list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos]; - if ( $excisedFnames ) { - $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname) " . - "and descendants " . implode( ', ', $excisedFnames ) ); - } else { - $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname)" ); - } + if ( $excisedFnames ) { + $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname) " . + "and descendants " . implode( ', ', $excisedFnames ) ); + } else { + $this->queryLogger->debug( "cancelAtomic: canceling level $pos ($savedFname)" ); + } - if ( $savedFname !== $fname ) { - throw new DBUnexpectedError( - $this, - "Invalid atomic section ended (got $fname but expected $savedFname)." - ); - } + if ( $savedFname !== $fname ) { + throw new DBUnexpectedError( + $this, + "Invalid atomic section ended (got $fname but expected $savedFname)." + ); + } - // Remove the last section (no need to re-index the array) - array_pop( $this->trxAtomicLevels ); - $this->modifyCallbacksForCancel( [ $savedSectionId ] ); + // Remove the last section (no need to re-index the array) + array_pop( $this->trxAtomicLevels ); + $excisedIds[] = $savedSectionId; + $newTopSection = $this->currentAtomicSectionId(); - if ( $savepointId !== null ) { - // Rollback the transaction to the state just before this atomic section - if ( $savepointId === self::$NOT_APPLICABLE ) { - $this->rollback( $fname, self::FLUSHING_INTERNAL ); - } else { - $this->doRollbackToSavepoint( $savepointId, $fname ); - $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered - $this->trxStatusIgnoredCause = null; + if ( $savepointId !== null ) { + // Rollback the transaction to the state just before this atomic section + if ( $savepointId === self::$NOT_APPLICABLE ) { + $this->rollback( $fname, self::FLUSHING_INTERNAL ); + // Note: rollback() will run trxSectionCancelCallbacks + } else { + $this->doRollbackToSavepoint( $savepointId, $fname ); + $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered + $this->trxStatusIgnoredCause = null; + + // Run trxSectionCancelCallbacks now. + $this->runOnAtomicSectionCancelCallbacks( self::TRIGGER_CANCEL, $excisedIds ); + } + } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) { + // Put the transaction into an error state if it's not already in one + $this->trxStatus = self::STATUS_TRX_ERROR; + $this->trxStatusCause = new DBUnexpectedError( + $this, + "Uncancelable atomic section canceled (got $fname)." + ); } - } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) { - // Put the transaction into an error state if it's not already in one - $this->trxStatus = self::STATUS_TRX_ERROR; - $this->trxStatusCause = new DBUnexpectedError( - $this, - "Uncancelable atomic section canceled (got $fname)." - ); + } finally { + // Fix up callbacks owned by the sections that were just cancelled. + // All callbacks should have an owner that is present in trxAtomicLevels. + $this->modifyCallbacksForCancel( $excisedIds, $newTopSection ); } $this->affectedRowCount = 0; // for the sake of consistency @@ -3823,7 +3926,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } // Protect against mismatched atomic section, transaction nesting, and snapshot loss - if ( $this->trxLevel ) { + if ( $this->trxLevel() ) { if ( $this->trxAtomicLevels ) { $levels = $this->flatAtomicSectionList(); $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open."; @@ -3843,6 +3946,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->assertHasConnectionHandle(); $this->doBegin( $fname ); + $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) ); $this->trxStatus = self::STATUS_TRX_OK; $this->trxStatusIgnoredCause = null; $this->trxAtomicCounter = 0; @@ -3851,7 +3955,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->trxDoneWrites = false; $this->trxAutomaticAtomic = false; $this->trxAtomicLevels = []; - $this->trxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) ); $this->trxWriteDuration = 0.0; $this->trxWriteQueryCount = 0; $this->trxWriteAffectedRows = 0; @@ -3873,10 +3976,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * * @see Database::begin() * @param string $fname + * @throws DBError */ protected function doBegin( $fname ) { $this->query( 'BEGIN', $fname ); - $this->trxLevel = 1; } final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) { @@ -3885,7 +3988,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." ); } - if ( $this->trxLevel && $this->trxAtomicLevels ) { + if ( $this->trxLevel() && $this->trxAtomicLevels ) { // There are still atomic sections open; this cannot be ignored $levels = $this->flatAtomicSectionList(); throw new DBUnexpectedError( @@ -3895,7 +3998,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) { - if ( !$this->trxLevel ) { + if ( !$this->trxLevel() ) { return; // nothing to do } elseif ( !$this->trxAutomatic ) { throw new DBUnexpectedError( @@ -3903,7 +4006,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware "$fname: Flushing an explicit transaction, getting out of sync." ); } - } elseif ( !$this->trxLevel ) { + } elseif ( !$this->trxLevel() ) { $this->queryLogger->error( "$fname: No transaction to commit, something got out of sync." ); return; // nothing to do @@ -3920,6 +4023,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY ); $this->doCommit( $fname ); + $oldTrxShortId = $this->consumeTrxShortId(); $this->trxStatus = self::STATUS_TRX_NONE; if ( $this->trxDoneWrites ) { @@ -3927,7 +4031,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->trxProfiler->transactionWritingOut( $this->server, $this->getDomainID(), - $this->trxShortId, + $oldTrxShortId, $writeTime, $this->trxWriteAffectedRows ); @@ -3945,16 +4049,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * * @see Database::commit() * @param string $fname + * @throws DBError */ protected function doCommit( $fname ) { - if ( $this->trxLevel ) { + if ( $this->trxLevel() ) { $this->query( 'COMMIT', $fname ); - $this->trxLevel = 0; } } final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) { - $trxActive = $this->trxLevel; + $trxActive = $this->trxLevel(); if ( $flush !== self::FLUSHING_INTERNAL && $flush !== self::FLUSHING_ALL_PEERS @@ -3970,6 +4074,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->assertHasConnectionHandle(); $this->doRollback( $fname ); + $oldTrxShortId = $this->consumeTrxShortId(); $this->trxStatus = self::STATUS_TRX_NONE; $this->trxAtomicLevels = []; // Estimate the RTT via a query now that trxStatus is OK @@ -3979,7 +4084,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->trxProfiler->transactionWritingOut( $this->server, $this->getDomainID(), - $this->trxShortId, + $oldTrxShortId, $writeTime, $this->trxWriteAffectedRows ); @@ -4013,13 +4118,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * * @see Database::rollback() * @param string $fname + * @throws DBError */ protected function doRollback( $fname ) { - if ( $this->trxLevel ) { + if ( $this->trxLevel() ) { # Disconnects cause rollback anyway, so ignore those errors $ignoreErrors = true; $this->query( 'ROLLBACK', $fname, $ignoreErrors ); - $this->trxLevel = 0; } } @@ -4037,7 +4142,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function explicitTrxActive() { - return $this->trxLevel && ( $this->trxAtomicLevels || !$this->trxAutomatic ); + return $this->trxLevel() && ( $this->trxAtomicLevels || !$this->trxAutomatic ); } public function duplicateTableStructure( @@ -4134,7 +4239,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware */ protected function replaceLostConnection( $fname ) { $this->closeConnection(); - $this->opened = false; $this->conn = false; $this->handleSessionLossPreconnect(); @@ -4190,7 +4294,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @since 1.27 */ final protected function getRecordedTransactionLagStatus() { - return ( $this->trxLevel && $this->trxReplicaLag !== null ) + return ( $this->trxLevel() && $this->trxReplicaLag !== null ) ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ] : null; } @@ -4708,9 +4812,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $this->isOpen() ) { // Open a new connection resource without messing with the old one - $this->opened = false; $this->conn = false; $this->trxEndCallbacks = []; // don't copy + $this->trxSectionCancelCallbacks = []; // don't copy $this->handleSessionLossPreconnect(); // no trx or locks anymore $this->open( $this->server, @@ -4738,7 +4842,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * Run a few simple sanity checks and close dangling connections */ public function __destruct() { - if ( $this->trxLevel && $this->trxDoneWrites ) { + if ( $this->trxLevel() && $this->trxDoneWrites ) { trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})." ); } @@ -4755,7 +4859,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->closeConnection(); Wikimedia\restoreWarnings(); $this->conn = false; - $this->opened = false; } } } diff --git a/includes/libs/rdbms/database/DatabaseMssql.php b/includes/libs/rdbms/database/DatabaseMssql.php index 56320273f6..50aaff28d4 100644 --- a/includes/libs/rdbms/database/DatabaseMssql.php +++ b/includes/libs/rdbms/database/DatabaseMssql.php @@ -27,9 +27,10 @@ namespace Wikimedia\Rdbms; -use Wikimedia; use Exception; +use RuntimeException; use stdClass; +use Wikimedia\AtEase\AtEase; /** * @ingroup Database @@ -78,7 +79,7 @@ class DatabaseMssql extends Database { } protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) { - # Test for driver support, to avoid suppressed fatal error + // Test for driver support, to avoid suppressed fatal error if ( !function_exists( 'sqlsrv_connect' ) ) { throw new DBConnectionError( $this, @@ -87,11 +88,6 @@ class DatabaseMssql extends Database { ); } - # e.g. the class is being loaded - if ( !strlen( $user ) ) { - return null; - } - $this->close(); $this->server = $server; $this->user = $user; @@ -110,15 +106,19 @@ class DatabaseMssql extends Database { $connectionInfo['PWD'] = $password; } - Wikimedia\suppressWarnings(); + AtEase::suppressWarnings(); $this->conn = sqlsrv_connect( $server, $connectionInfo ); - Wikimedia\restoreWarnings(); + AtEase::restoreWarnings(); if ( $this->conn === false ) { - throw new DBConnectionError( $this, $this->lastError() ); + $error = $this->lastError(); + $this->connLogger->error( + "Error connecting to {db_server}: {error}", + $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] ) + ); + throw new DBConnectionError( $this, $error ); } - $this->opened = true; $this->currentDomain = new DatabaseDomain( ( $dbName != '' ) ? $dbName : null, null, @@ -375,6 +375,17 @@ class DatabaseMssql extends Database { return $statementOnly; } + public function serverIsReadOnly() { + $encDatabase = $this->addQuotes( $this->getDBname() ); + $res = $this->query( + "SELECT IS_READ_ONLY FROM SYS.DATABASES WHERE NAME = $encDatabase", + __METHOD__ + ); + $row = $this->fetchObject( $res ); + + return $row ? (bool)$row->IS_READ_ONLY : false; + } + /** * @return int */ @@ -1072,13 +1083,10 @@ class DatabaseMssql extends Database { $this->query( 'ROLLBACK TRANSACTION ' . $this->addIdentifierQuotes( $identifier ), $fname ); } - /** - * Begin a transaction, committing any previously open transaction - * @param string $fname - */ protected function doBegin( $fname = __METHOD__ ) { - sqlsrv_begin_transaction( $this->conn ); - $this->trxLevel = 1; + if ( !sqlsrv_begin_transaction( $this->conn ) ) { + $this->reportQueryError( $this->lastError(), $this->lastErrno(), 'BEGIN', $fname ); + } } /** @@ -1086,8 +1094,9 @@ class DatabaseMssql extends Database { * @param string $fname */ protected function doCommit( $fname = __METHOD__ ) { - sqlsrv_commit( $this->conn ); - $this->trxLevel = 0; + if ( !sqlsrv_commit( $this->conn ) ) { + $this->reportQueryError( $this->lastError(), $this->lastErrno(), 'COMMIT', $fname ); + } } /** @@ -1096,8 +1105,17 @@ class DatabaseMssql extends Database { * @param string $fname */ protected function doRollback( $fname = __METHOD__ ) { - sqlsrv_rollback( $this->conn ); - $this->trxLevel = 0; + if ( !sqlsrv_rollback( $this->conn ) ) { + $this->queryLogger->error( + "{fname}\t{db_server}\t{errno}\t{error}\t", + $this->getLogContext( [ + 'errno' => $this->lastErrno(), + 'error' => $this->lastError(), + 'fname' => $fname, + 'trace' => ( new RuntimeException() )->getTraceAsString() + ] ) + ); + } } /** diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php index ef28f33ac6..e3c2268f91 100644 --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@ -122,9 +122,12 @@ abstract class DatabaseMysqlBase extends Database { } protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) { - # Close/unset connection handle $this->close(); + if ( $schema !== null ) { + throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." ); + } + $this->server = $server; $this->user = $user; $this->password = $password; @@ -143,88 +146,52 @@ abstract class DatabaseMysqlBase extends Database { $error = $error ?: $this->lastError(); $this->connLogger->error( "Error connecting to {db_server}: {error}", - $this->getLogContext( [ - 'method' => __METHOD__, - 'error' => $error, - ] ) + $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] ) ); $this->connLogger->debug( "DB connection error\n" . "Server: $server, User: $user, Password: " . substr( $password, 0, 3 ) . "..., error: " . $error . "\n" ); - throw new DBConnectionError( $this, $error ); } - if ( strlen( $dbName ) ) { - $this->selectDomain( new DatabaseDomain( $dbName, null, $tablePrefix ) ); - } else { - $this->currentDomain = new DatabaseDomain( null, null, $tablePrefix ); - } - - // Tell the server what we're communicating with - if ( !$this->connectInitCharset() ) { - $error = $this->lastError(); - $this->queryLogger->error( - "Error setting character set: {error}", - $this->getLogContext( [ - 'method' => __METHOD__, - 'error' => $this->lastError(), - ] ) + try { + $this->currentDomain = new DatabaseDomain( + strlen( $dbName ) ? $dbName : null, + null, + $tablePrefix ); - throw new DBConnectionError( $this, "Error setting character set: $error" ); - } - // Abstract over any insane MySQL defaults - $set = [ 'group_concat_max_len = 262144' ]; - // Set SQL mode, default is turning them all off, can be overridden or skipped with null - if ( is_string( $this->sqlMode ) ) { - $set[] = 'sql_mode = ' . $this->addQuotes( $this->sqlMode ); - } - // Set any custom settings defined by site config - // (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html) - foreach ( $this->connectionVariables as $var => $val ) { - // Escape strings but not numbers to avoid MySQL complaining - if ( !is_int( $val ) && !is_float( $val ) ) { - $val = $this->addQuotes( $val ); + // Abstract over any insane MySQL defaults + $set = [ 'group_concat_max_len = 262144' ]; + // Set SQL mode, default is turning them all off, can be overridden or skipped with null + if ( is_string( $this->sqlMode ) ) { + $set[] = 'sql_mode = ' . $this->addQuotes( $this->sqlMode ); + } + // Set any custom settings defined by site config + // (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html) + foreach ( $this->connectionVariables as $var => $val ) { + // Escape strings but not numbers to avoid MySQL complaining + if ( !is_int( $val ) && !is_float( $val ) ) { + $val = $this->addQuotes( $val ); + } + $set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val; } - $set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val; - } - if ( $set ) { - // Use doQuery() to avoid opening implicit transactions (DBO_TRX) - $success = $this->doQuery( 'SET ' . implode( ', ', $set ) ); - if ( !$success ) { - $error = $this->lastError(); - $this->queryLogger->error( - 'Error setting MySQL variables on server {db_server}: {error}', - $this->getLogContext( [ - 'method' => __METHOD__, - 'error' => $error, - ] ) + if ( $set ) { + $this->query( + 'SET ' . implode( ', ', $set ), + __METHOD__, + self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY ); - throw new DBConnectionError( $this, "Error setting MySQL variables: $error" ); } + } catch ( Exception $e ) { + // Connection was not fully initialized and is not safe for use + $this->conn = false; } - $this->opened = true; - return true; } - /** - * Set the character set information right after connection - * @return bool - */ - protected function connectInitCharset() { - if ( $this->utf8Mode ) { - // Tell the server we're communicating with it in UTF-8. - // This may engage various charset conversions. - return $this->mysqlSetCharset( 'utf8' ); - } else { - return $this->mysqlSetCharset( 'binary' ); - } - } - protected function doSelectDomain( DatabaseDomain $domain ) { if ( $domain->getSchema() !== null ) { throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." ); @@ -269,14 +236,6 @@ abstract class DatabaseMysqlBase extends Database { */ abstract protected function mysqlConnect( $realServer, $dbName ); - /** - * Set the character set of the MySQL link - * - * @param string $charset - * @return bool - */ - abstract protected function mysqlSetCharset( $charset ); - /** * @param IResultWrapper|resource $res * @throws DBUnexpectedError diff --git a/includes/libs/rdbms/database/DatabaseMysqli.php b/includes/libs/rdbms/database/DatabaseMysqli.php index 703c64d7c0..0f444cd210 100644 --- a/includes/libs/rdbms/database/DatabaseMysqli.php +++ b/includes/libs/rdbms/database/DatabaseMysqli.php @@ -125,21 +125,6 @@ class DatabaseMysqli extends DatabaseMysqlBase { return false; } - protected function connectInitCharset() { - // already done in mysqlConnect() - return true; - } - - /** - * @param string $charset - * @return bool - */ - protected function mysqlSetCharset( $charset ) { - $conn = $this->getBindingHandle(); - - return $conn->set_charset( $charset ); - } - /** * @return bool */ diff --git a/includes/libs/rdbms/database/DatabasePostgres.php b/includes/libs/rdbms/database/DatabasePostgres.php index d8be62f1f4..92eac90a16 100644 --- a/includes/libs/rdbms/database/DatabasePostgres.php +++ b/includes/libs/rdbms/database/DatabasePostgres.php @@ -87,7 +87,7 @@ class DatabasePostgres extends Database { } protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) { - # Test for Postgres support, to avoid suppressed fatal error + // Test for Postgres support, to avoid suppressed fatal error if ( !function_exists( 'pg_connect' ) ) { throw new DBConnectionError( $this, @@ -143,23 +143,36 @@ class DatabasePostgres extends Database { throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) ); } - $this->opened = true; + try { + // If called from the command-line (e.g. importDump), only show errors. + // No transaction should be open at this point, so the problem of the SET + // effects being rolled back should not be an issue. + // See https://www.postgresql.org/docs/8.3/sql-set.html + $variables = []; + if ( $this->cliMode ) { + $variables['client_min_messages'] = 'ERROR'; + } + $variables += [ + 'client_encoding' => 'UTF8', + 'datestyle' => 'ISO, YMD', + 'timezone' => 'GMT', + 'standard_conforming_strings' => 'on', + 'bytea_output' => 'escape' + ]; + foreach ( $variables as $var => $val ) { + $this->query( + 'SET ' . $this->addIdentifierQuotes( $var ) . ' = ' . $this->addQuotes( $val ), + __METHOD__, + self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY + ); + } - # If called from the command-line (e.g. importDump), only show errors - if ( $this->cliMode ) { - $this->doQuery( "SET client_min_messages = 'ERROR'" ); + $this->determineCoreSchema( $schema ); + $this->currentDomain = new DatabaseDomain( $dbName, $schema, $tablePrefix ); + } catch ( Exception $e ) { + // Connection was not fully initialized and is not safe for use + $this->conn = false; } - - $this->query( "SET client_encoding='UTF8'", __METHOD__ ); - $this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ ); - $this->query( "SET timezone = 'GMT'", __METHOD__ ); - $this->query( "SET standard_conforming_strings = on", __METHOD__ ); - $this->query( "SET bytea_output = 'escape'", __METHOD__ ); // PHP bug 53127 - - $this->determineCoreSchema( $schema ); - $this->currentDomain = new DatabaseDomain( $dbName, $schema, $tablePrefix ); - - return (bool)$this->conn; } protected function relationSchemaQualifier() { @@ -981,7 +994,7 @@ __INDEXATTR__; * @return string Default schema for the current session */ public function getCurrentSchema() { - $res = $this->query( "SELECT current_schema()", __METHOD__ ); + $res = $this->query( "SELECT current_schema()", __METHOD__, self::QUERY_IGNORE_DBO_TRX ); $row = $this->fetchRow( $res ); return $row[0]; @@ -998,7 +1011,11 @@ __INDEXATTR__; * @return array List of actual schemas for the current sesson */ public function getSchemas() { - $res = $this->query( "SELECT current_schemas(false)", __METHOD__ ); + $res = $this->query( + "SELECT current_schemas(false)", + __METHOD__, + self::QUERY_IGNORE_DBO_TRX + ); $row = $this->fetchRow( $res ); $schemas = []; @@ -1017,7 +1034,7 @@ __INDEXATTR__; * @return array How to search for table names schemas for the current user */ public function getSearchPath() { - $res = $this->query( "SHOW search_path", __METHOD__ ); + $res = $this->query( "SHOW search_path", __METHOD__, self::QUERY_IGNORE_DBO_TRX ); $row = $this->fetchRow( $res ); /* PostgreSQL returns SHOW values as strings */ @@ -1033,7 +1050,11 @@ __INDEXATTR__; * @param array $search_path List of schemas to be searched by default */ private function setSearchPath( $search_path ) { - $this->query( "SET search_path = " . implode( ", ", $search_path ) ); + $this->query( + "SET search_path = " . implode( ", ", $search_path ), + __METHOD__, + self::QUERY_IGNORE_DBO_TRX + ); } /** @@ -1051,7 +1072,15 @@ __INDEXATTR__; * @param string $desiredSchema */ public function determineCoreSchema( $desiredSchema ) { - $this->begin( __METHOD__, self::TRANSACTION_INTERNAL ); + if ( $this->trxLevel() ) { + // We do not want the schema selection to change on ROLLBACK or INSERT SELECT. + // See https://www.postgresql.org/docs/8.3/sql-set.html + throw new DBUnexpectedError( + $this, + __METHOD__ . ": a transaction is currently active." + ); + } + if ( $this->schemaExists( $desiredSchema ) ) { if ( in_array( $desiredSchema, $this->getSchemas() ) ) { $this->coreSchema = $desiredSchema; @@ -1064,8 +1093,7 @@ __INDEXATTR__; * Fixes T17816 */ $search_path = $this->getSearchPath(); - array_unshift( $search_path, - $this->addIdentifierQuotes( $desiredSchema ) ); + array_unshift( $search_path, $this->addIdentifierQuotes( $desiredSchema ) ); $this->setSearchPath( $search_path ); $this->coreSchema = $desiredSchema; $this->queryLogger->debug( @@ -1077,8 +1105,6 @@ __INDEXATTR__; "Schema \"" . $desiredSchema . "\" not found, using current \"" . $this->coreSchema . "\"\n" ); } - /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */ - $this->commit( __METHOD__, self::FLUSHING_INTERNAL ); } /** @@ -1243,10 +1269,14 @@ SQL; return false; // short-circuit } - $exists = $this->selectField( - '"pg_catalog"."pg_namespace"', 1, [ 'nspname' => $schema ], __METHOD__ ); + $res = $this->query( + "SELECT 1 FROM pg_catalog.pg_namespace " . + "WHERE nspname = " . $this->addQuotes( $schema ) . " LIMIT 1", + __METHOD__, + self::QUERY_IGNORE_DBO_TRX + ); - return (bool)$exists; + return ( $this->numRows( $res ) > 0 ); } /** diff --git a/includes/libs/rdbms/database/DatabaseSqlite.php b/includes/libs/rdbms/database/DatabaseSqlite.php index aff3774d7e..17f12d327f 100644 --- a/includes/libs/rdbms/database/DatabaseSqlite.php +++ b/includes/libs/rdbms/database/DatabaseSqlite.php @@ -168,15 +168,13 @@ class DatabaseSqlite extends Database { protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) { $this->close(); - $fileName = self::generateFileName( $this->dbDir, $dbName ); - if ( !is_readable( $fileName ) ) { - $this->conn = false; - throw new DBConnectionError( $this, "SQLite database not accessible" ); + + if ( $schema !== null ) { + throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." ); } - // Only $dbName is used, the other parameters are irrelevant for SQLite databases - $this->openFile( $fileName, $dbName, $tablePrefix ); - return (bool)$this->conn; + // Only $dbName is used, the other parameters are irrelevant for SQLite databases + $this->openFile( self::generateFileName( $this->dbDir, $dbName ), $dbName, $tablePrefix ); } /** @@ -186,45 +184,57 @@ class DatabaseSqlite extends Database { * @param string $dbName * @param string $tablePrefix * @throws DBConnectionError - * @return PDO|bool SQL connection or false if failed */ protected function openFile( $fileName, $dbName, $tablePrefix ) { - $err = false; + if ( !$this->hasMemoryPath() && !is_readable( $fileName ) ) { + $error = "SQLite database file not readable"; + $this->connLogger->error( + "Error connecting to {db_server}: {error}", + $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] ) + ); + throw new DBConnectionError( $this, $error ); + } $this->dbPath = $fileName; try { - if ( $this->flags & self::DBO_PERSISTENT ) { - $this->conn = new PDO( "sqlite:$fileName", '', '', - [ PDO::ATTR_PERSISTENT => true ] ); - } else { - $this->conn = new PDO( "sqlite:$fileName", '', '' ); - } + $this->conn = new PDO( + "sqlite:$fileName", + '', + '', + [ PDO::ATTR_PERSISTENT => (bool)( $this->flags & self::DBO_PERSISTENT ) ] + ); + $error = 'unknown error'; } catch ( PDOException $e ) { - $err = $e->getMessage(); + $error = $e->getMessage(); } if ( !$this->conn ) { - $this->queryLogger->debug( "DB connection error: $err\n" ); - throw new DBConnectionError( $this, $err ); + $this->connLogger->error( + "Error connecting to {db_server}: {error}", + $this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] ) + ); + throw new DBConnectionError( $this, $error ); } - $this->opened = is_object( $this->conn ); - if ( $this->opened ) { - $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix ); - # Set error codes only, don't raise exceptions + try { + // Set error codes only, don't raise exceptions $this->conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); - # Enforce LIKE to be case sensitive, just like MySQL - $this->query( 'PRAGMA case_sensitive_like = 1' ); + $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix ); + + $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY; + // Enforce LIKE to be case sensitive, just like MySQL + $this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags ); + // Apply an optimizations or requirements regarding fsync() usage $sync = $this->connectionVariables['synchronous'] ?? null; if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) { - $this->query( "PRAGMA synchronous = $sync" ); + $this->query( "PRAGMA synchronous = $sync", __METHOD__ ); } - - return $this->conn; + } catch ( Exception $e ) { + // Connection was not fully initialized and is not safe for use + $this->conn = false; + throw $e; } - - return false; } /** @@ -761,6 +771,17 @@ class DatabaseSqlite extends Database { return false; } + public function serverIsReadOnly() { + return ( !$this->hasMemoryPath() && !is_writable( $this->dbPath ) ); + } + + /** + * @return bool + */ + private function hasMemoryPath() { + return ( strpos( $this->dbPath, ':memory:' ) === 0 ); + } + /** * @return string Wikitext of a link to the server software's web site */ @@ -804,7 +825,6 @@ class DatabaseSqlite extends Database { } else { $this->query( 'BEGIN', $fname ); } - $this->trxLevel = 1; } /** diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index faed1bf3c4..fca2c005e3 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -42,6 +42,8 @@ interface IDatabase { const TRIGGER_COMMIT = 2; /** @var int Callback triggered by ROLLBACK */ const TRIGGER_ROLLBACK = 3; + /** @var int Callback triggered by atomic section cancel (ROLLBACK TO SAVEPOINT) */ + const TRIGGER_CANCEL = 4; /** @var string Transaction is requested by regular caller outside of the DB layer */ const TRANSACTION_EXPLICIT = ''; @@ -106,6 +108,8 @@ interface IDatabase { /** @var int Enable compression in connection protocol */ const DBO_COMPRESS = 512; + /** @var int Idiom for "no special flags" */ + const QUERY_NORMAL = 0; /** @var int Ignore query errors and return false when they happen */ const QUERY_SILENCE_ERRORS = 1; // b/c for 1.32 query() argument; note that (int)true = 1 /** @@ -117,6 +121,8 @@ interface IDatabase { const QUERY_REPLICA_ROLE = 4; /** @var int Ignore the current presence of any DBO_TRX flag */ const QUERY_IGNORE_DBO_TRX = 8; + /** @var int Do not try to retry the query if the connection was lost */ + const QUERY_NO_RETRY = 16; /** @var bool Parameter to unionQueries() for UNION ALL */ const UNION_ALL = true; @@ -1222,6 +1228,8 @@ interface IDatabase { * @since 1.16 * @param array[]|string|LikeMatch $param * @return string Fully built LIKE statement + * @phan-suppress-next-line PhanMismatchVariadicComment + * @phan-param array|string|LikeMatch ...$param T226223 */ public function buildLike( $param ); @@ -1574,6 +1582,9 @@ interface IDatabase { * * This is useful for combining cooperative locks and DB transactions. * + * Note this is called when the whole transaction is resolved. To take action immediately + * when an atomic section is cancelled, use onAtomicSectionCancel(). + * * @note do not assume that *other* IDatabase instances will be AUTOCOMMIT mode * * The callback takes the following arguments: @@ -1655,6 +1666,31 @@ interface IDatabase { */ public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ); + /** + * Run a callback when the atomic section is cancelled. + * + * The callback is run just after the current atomic section, any outer + * atomic section, or the whole transaction is rolled back. + * + * An error is thrown if no atomic section is pending. The atomic section + * need not have been created with the ATOMIC_CANCELABLE flag. + * + * Queries in the function may be running in the context of an outer + * transaction or may be running in AUTOCOMMIT mode. The callback should + * use atomic sections if necessary. + * + * @note do not assume that *other* IDatabase instances will be AUTOCOMMIT mode + * + * The callback takes the following arguments: + * - IDatabase::TRIGGER_CANCEL or IDatabase::TRIGGER_ROLLBACK + * - This IDatabase instance + * + * @param callable $callback + * @param string $fname Caller name + * @since 1.34 + */ + public function onAtomicSectionCancel( callable $callback, $fname = __METHOD__ ); + /** * Run a callback after each time any transaction commits or rolls back * diff --git a/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php index 3709de7de2..2ca3d7d51b 100644 --- a/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php +++ b/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php @@ -35,7 +35,7 @@ class FakeResultWrapper extends ResultWrapper { $this->next(); - return is_object( $row ) ? (array)$row : $row; + return is_object( $row ) ? get_object_vars( $row ) : $row; } function seek( $pos ) { diff --git a/includes/libs/rdbms/lbfactory/ILBFactory.php b/includes/libs/rdbms/lbfactory/ILBFactory.php index c5dbfc58a8..35c953912c 100644 --- a/includes/libs/rdbms/lbfactory/ILBFactory.php +++ b/includes/libs/rdbms/lbfactory/ILBFactory.php @@ -140,7 +140,7 @@ interface ILBFactory { /** * Get cached (tracked) load balancers for all main database clusters * - * @return LoadBalancer[] Map of (cluster name => LoadBalancer) + * @return ILoadBalancer[] Map of (cluster name => ILoadBalancer) * @since 1.29 */ public function getAllMainLBs(); @@ -148,7 +148,7 @@ interface ILBFactory { /** * Get cached (tracked) load balancers for all external database clusters * - * @return LoadBalancer[] Map of (cluster name => LoadBalancer) + * @return ILoadBalancer[] Map of (cluster name => ILoadBalancer) * @since 1.29 */ public function getAllExternalLBs(); diff --git a/includes/libs/rdbms/lbfactory/LBFactoryMulti.php b/includes/libs/rdbms/lbfactory/LBFactoryMulti.php index aec99f4ec7..f675b58778 100644 --- a/includes/libs/rdbms/lbfactory/LBFactoryMulti.php +++ b/includes/libs/rdbms/lbfactory/LBFactoryMulti.php @@ -34,55 +34,42 @@ use InvalidArgumentException; class LBFactoryMulti extends LBFactory { /** @var array A map of database names to section names */ private $sectionsByDB; - /** * @var array A 2-d map. For each section, gives a map of server names to * load ratios */ private $sectionLoads; - /** * @var array[] Server info associative array * @note The host, hostName and load entries will be overridden */ private $serverTemplate; - // Optional settings - /** @var array A 3-d map giving server load ratios for each section and group */ private $groupLoadsBySection = []; - /** @var array A 3-d map giving server load ratios by DB name */ private $groupLoadsByDB = []; - /** @var array A map of hostname to IP address */ private $hostsByName = []; - /** @var array A map of external storage cluster name to server load map */ private $externalLoads = []; - /** * @var array A set of server info keys overriding serverTemplate for * external storage */ private $externalTemplateOverrides; - /** * @var array A 2-d map overriding serverTemplate and * externalTemplateOverrides on a server-by-server basis. Applies to both * core and external storage */ private $templateOverridesByServer; - /** @var array A 2-d map overriding the server info by section */ private $templateOverridesBySection; - /** @var array A 2-d map overriding the server info by external storage cluster */ private $templateOverridesByCluster; - /** @var array An override array for all master servers */ private $masterTemplateOverrides; - /** * @var array|bool A map of section name to read-only message. Missing or * false for read/write @@ -91,16 +78,12 @@ class LBFactoryMulti extends LBFactory { /** @var LoadBalancer[] */ private $mainLBs = []; - /** @var LoadBalancer[] */ private $extLBs = []; - /** @var string */ private $loadMonitorClass = 'LoadMonitor'; - /** @var string */ private $lastDomain; - /** @var string */ private $lastSection; @@ -191,22 +174,19 @@ class LBFactoryMulti extends LBFactory { if ( $this->lastDomain === $domain ) { return $this->lastSection; } - list( $dbName, ) = $this->getDBNameAndPrefix( $domain ); - $section = $this->sectionsByDB[$dbName] ?? 'DEFAULT'; + + $database = $this->getDatabaseFromDomain( $domain ); + $section = $this->sectionsByDB[$database] ?? 'DEFAULT'; $this->lastSection = $section; $this->lastDomain = $domain; return $section; } - /** - * @param bool|string $domain - * @return LoadBalancer - */ public function newMainLB( $domain = false ) { - list( $dbName, ) = $this->getDBNameAndPrefix( $domain ); + $database = $this->getDatabaseFromDomain( $domain ); $section = $this->getSectionForDomain( $domain ); - $groupLoads = $this->groupLoadsByDB[$dbName] ?? []; + $groupLoads = $this->groupLoadsByDB[$database] ?? []; if ( isset( $this->groupLoadsBySection[$section] ) ) { $groupLoads = array_merge_recursive( @@ -232,10 +212,6 @@ class LBFactoryMulti extends LBFactory { ); } - /** - * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain - * @return LoadBalancer - */ public function getMainLB( $domain = false ) { $section = $this->getSectionForDomain( $domain ); if ( !isset( $this->mainLBs[$section] ) ) { @@ -379,23 +355,14 @@ class LBFactoryMulti extends LBFactory { /** * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain - * @return array [database name, table prefix] + * @return string */ - private function getDBNameAndPrefix( $domain = false ) { - $domain = ( $domain === false ) - ? $this->localDomain - : DatabaseDomain::newFromId( $domain ); - - return [ $domain->getDatabase(), $domain->getTablePrefix() ]; + private function getDatabaseFromDomain( $domain = false ) { + return ( $domain === false ) + ? $this->localDomain->getDatabase() + : DatabaseDomain::newFromId( $domain )->getDatabase(); } - /** - * 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 = [] ) { foreach ( $this->mainLBs as $lb ) { $callback( $lb, ...$params ); diff --git a/includes/libs/rdbms/lbfactory/LBFactorySimple.php b/includes/libs/rdbms/lbfactory/LBFactorySimple.php index 49054e083d..fd76d88dd0 100644 --- a/includes/libs/rdbms/lbfactory/LBFactorySimple.php +++ b/includes/libs/rdbms/lbfactory/LBFactorySimple.php @@ -70,20 +70,12 @@ class LBFactorySimple extends LBFactory { $this->loadMonitorClass = $conf['loadMonitorClass'] ?? 'LoadMonitor'; } - /** - * @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 ) ) { + if ( !$this->mainLB ) { $this->mainLB = $this->newMainLB( $domain ); } @@ -132,14 +124,6 @@ class LBFactorySimple extends LBFactory { 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 ) ) { $callback( $this->mainLB, ...$params ); diff --git a/includes/libs/rdbms/loadbalancer/ILoadBalancer.php b/includes/libs/rdbms/loadbalancer/ILoadBalancer.php index 4d148b4c90..b086beb31c 100644 --- a/includes/libs/rdbms/loadbalancer/ILoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/ILoadBalancer.php @@ -314,22 +314,6 @@ interface ILoadBalancer { */ public function getWriterIndex(); - /** - * Returns true if the specified index is a valid server index - * - * @param int $i - * @return bool - */ - public function haveIndex( $i ); - - /** - * Returns true if the specified index is valid and has non-zero load - * - * @param int $i - * @return bool - */ - public function isNonZeroLoad( $i ); - /** * Get the number of servers defined in configuration * diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancer.php b/includes/libs/rdbms/loadbalancer/LoadBalancer.php index c08655cb6a..44d526c2e2 100644 --- a/includes/libs/rdbms/loadbalancer/LoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/LoadBalancer.php @@ -117,8 +117,8 @@ class LoadBalancer implements ILoadBalancer { private $lastError = 'Unknown error'; /** @var string|bool Reason the LB is read-only or false if not */ private $readOnlyReason = false; - /** @var int Total connections opened */ - private $connsOpened = 0; + /** @var int Total number of new connections ever made with this instance */ + private $connectionCounter = 0; /** @var bool */ private $disabled = false; /** @var bool Whether any connection has been attempted yet */ @@ -199,7 +199,7 @@ class LoadBalancer implements ILoadBalancer { $this->waitTimeout = $params['waitTimeout'] ?? self::MAX_WAIT_DEFAULT; - $this->conns = self::newConnsArray(); + $this->conns = self::newTrackedConnectionsArray(); $this->waitForPos = false; $this->allowLagged = false; @@ -248,7 +248,7 @@ class LoadBalancer implements ILoadBalancer { $this->ownerId = $params['ownerId'] ?? null; } - private static function newConnsArray() { + private static function newTrackedConnectionsArray() { return [ // Connection were transaction rounds may be applied self::KEY_LOCAL => [], @@ -834,8 +834,7 @@ class LoadBalancer implements ILoadBalancer { $masterOnly = ( $i === self::DB_MASTER || $i === $this->getWriterIndex() ); // Number of connections made before getting the server index and handle - $priorConnectionsMade = $this->connsOpened; - + $priorConnectionsMade = $this->connectionCounter; // Choose a server if $i is DB_MASTER/DB_REPLICA (might trigger new connections) $serverIndex = $this->getConnectionIndex( $i, $groups, $domain ); // Get an open connection to that server (might trigger a new connection) @@ -852,7 +851,7 @@ class LoadBalancer implements ILoadBalancer { } // Profile any new connections caused by this method - if ( $this->connsOpened > $priorConnectionsMade ) { + if ( $this->connectionCounter > $priorConnectionsMade ) { $host = $conn->getServer(); $dbname = $conn->getDBname(); $this->trxProfiler->recordConnection( $host, $dbname, $masterOnly ); @@ -1001,11 +1000,8 @@ class LoadBalancer implements ILoadBalancer { if ( isset( $this->conns[$connKey][$i][0] ) ) { $conn = $this->conns[$connKey][$i][0]; } else { - if ( !isset( $this->servers[$i] ) || !is_array( $this->servers[$i] ) ) { - throw new InvalidArgumentException( "No server with index '$i'" ); - } // Open a new connection - $server = $this->servers[$i]; + $server = $this->getServerInfoStrict( $i ); $server['serverIndex'] = $i; $server['autoCommitOnly'] = $autoCommit; $conn = $this->reallyOpenConnection( $server, $this->localDomain ); @@ -1115,11 +1111,8 @@ class LoadBalancer implements ILoadBalancer { } if ( !$conn ) { - if ( !isset( $this->servers[$i] ) || !is_array( $this->servers[$i] ) ) { - throw new InvalidArgumentException( "No server with index '$i'" ); - } // Open a new connection - $server = $this->servers[$i]; + $server = $this->getServerInfoStrict( $i ); $server['serverIndex'] = $i; $server['foreignPoolRefCount'] = 0; $server['foreign'] = true; @@ -1213,12 +1206,6 @@ class LoadBalancer implements ILoadBalancer { $masterName = $this->getServerName( $this->getWriterIndex() ); $server['clusterMasterHost'] = $masterName; - // Log when many connection are made on requests - if ( ++$this->connsOpened >= self::CONN_HELD_WARN_THRESHOLD ) { - $this->perfLogger->warning( __METHOD__ . ": " . - "{$this->connsOpened}+ connections made (master=$masterName)" ); - } - $server['srvCache'] = $this->srvCache; // Set loggers and profilers $server['connLogger'] = $this->connLogger; @@ -1237,6 +1224,15 @@ class LoadBalancer implements ILoadBalancer { // Create a live connection object try { $db = Database::factory( $server['type'], $server ); + // Log when many connection are made on requests + ++$this->connectionCounter; + $currentConnCount = $this->getCurrentConnectionCount(); + if ( $currentConnCount >= self::CONN_HELD_WARN_THRESHOLD ) { + $this->perfLogger->warning( + __METHOD__ . ": {connections}+ connections made (master={masterdb})", + [ 'connections' => $currentConnCount, 'masterdb' => $masterName ] + ); + } } catch ( DBConnectionError $e ) { // FIXME: This is probably the ugliest thing I have ever done to // PHP. I'm half-expecting it to segfault, just out of disgust. -- TS @@ -1310,10 +1306,24 @@ class LoadBalancer implements ILoadBalancer { return 0; } + /** + * Returns true if the specified index is a valid server index + * + * @param int $i + * @return bool + * @deprecated Since 1.34 + */ public function haveIndex( $i ) { return array_key_exists( $i, $this->servers ); } + /** + * Returns true if the specified index is valid and has non-zero load + * + * @param int $i + * @return bool + * @deprecated Since 1.34 + */ public function isNonZeroLoad( $i ) { return array_key_exists( $i, $this->servers ) && $this->genericLoads[$i] != 0; } @@ -1337,7 +1347,7 @@ class LoadBalancer implements ILoadBalancer { } public function getServerName( $i ) { - $name = $this->servers[$i]['hostName'] ?? $this->servers[$i]['host'] ?? ''; + $name = $this->servers[$i]['hostName'] ?? ( $this->servers[$i]['host'] ?? '' ); return ( $name != '' ) ? $name : 'localhost'; } @@ -1383,8 +1393,7 @@ class LoadBalancer implements ILoadBalancer { $conn->close(); } ); - $this->conns = self::newConnsArray(); - $this->connsOpened = 0; + $this->conns = self::newTrackedConnectionsArray(); } public function closeConnection( IDatabase $conn ) { @@ -1405,7 +1414,6 @@ class LoadBalancer implements ILoadBalancer { $this->connLogger->debug( __METHOD__ . ": closing connection to database $i at '$host'." ); unset( $this->conns[$type][$serverIndex][$i] ); - --$this->connsOpened; break 2; } } @@ -1932,6 +1940,20 @@ class LoadBalancer implements ILoadBalancer { } } + /** + * @return int + */ + private function getCurrentConnectionCount() { + $count = 0; + foreach ( $this->conns as $connsByServer ) { + foreach ( $connsByServer as $serverConns ) { + $count += count( $serverConns ); + } + } + + return $count; + } + public function getMaxLag( $domain = false ) { $host = ''; $maxLag = -1; @@ -1942,7 +1964,7 @@ class LoadBalancer implements ILoadBalancer { foreach ( $lagTimes as $i => $lag ) { if ( $this->genericLoads[$i] > 0 && $lag > $maxLag ) { $maxLag = $lag; - $host = $this->servers[$i]['host']; + $host = $this->getServerInfoStrict( $i, 'host' ); $maxIndex = $i; } } @@ -2150,6 +2172,28 @@ class LoadBalancer implements ILoadBalancer { } } + /** + * @param int $i Server index + * @param string|null $field Server index field [optional] + * @return array|mixed + * @throws InvalidArgumentException + */ + private function getServerInfoStrict( $i, $field = null ) { + if ( !isset( $this->servers[$i] ) || !is_array( $this->servers[$i] ) ) { + throw new InvalidArgumentException( "No server with index '$i'" ); + } + + if ( $field !== null ) { + if ( !array_key_exists( $field, $this->servers[$i] ) ) { + throw new InvalidArgumentException( "No field '$field' in server index '$i'" ); + } + + return $this->servers[$i][$field]; + } + + return $this->servers[$i]; + } + function __destruct() { // Avoid connection leaks for sanity $this->disable(); diff --git a/includes/logging/BlockLogFormatter.php b/includes/logging/BlockLogFormatter.php index ddecf9ead5..ead290f062 100644 --- a/includes/logging/BlockLogFormatter.php +++ b/includes/logging/BlockLogFormatter.php @@ -127,7 +127,7 @@ class BlockLogFormatter extends LogFormatter { public function getPreloadTitles() { $title = $this->entry->getTarget(); // Preload user page for non-autoblocks - if ( substr( $title->getText(), 0, 1 ) !== '#' ) { + if ( substr( $title->getText(), 0, 1 ) !== '#' && $title->isValid() ) { return [ $title->getTalkPage() ]; } return []; diff --git a/includes/mail/EmailNotification.php b/includes/mail/EmailNotification.php index 0b77651bda..7361032937 100644 --- a/includes/mail/EmailNotification.php +++ b/includes/mail/EmailNotification.php @@ -407,7 +407,7 @@ class EmailNotification { * @param User $user * @param string $source */ - function compose( $user, $source ) { + private function compose( $user, $source ) { global $wgEnotifImpersonal; if ( !$this->composed_common ) { @@ -424,7 +424,7 @@ class EmailNotification { /** * Send any queued mails */ - function sendMails() { + private function sendMails() { global $wgEnotifImpersonal; if ( $wgEnotifImpersonal ) { $this->sendImpersonal( $this->mailTargets ); @@ -440,9 +440,8 @@ class EmailNotification { * @param User $watchingUser * @param string $source * @return Status - * @private */ - function sendPersonalised( $watchingUser, $source ) { + private function sendPersonalised( $watchingUser, $source ) { global $wgEnotifUseRealName; // From the PHP manual: // Note: The to parameter cannot be an address in the form of @@ -481,7 +480,7 @@ class EmailNotification { * @param MailAddress[] $addresses * @return Status|null */ - function sendImpersonal( $addresses ) { + private function sendImpersonal( $addresses ) { if ( empty( $addresses ) ) { return null; } diff --git a/includes/mail/MailAddress.php b/includes/mail/MailAddress.php index 63a114d2a9..1a5d08ac2a 100644 --- a/includes/mail/MailAddress.php +++ b/includes/mail/MailAddress.php @@ -71,7 +71,7 @@ class MailAddress { * Return formatted and quoted address to insert into SMTP headers * @return string */ - function toString() { + public function toString() { if ( !$this->address ) { return ''; } @@ -94,7 +94,7 @@ class MailAddress { return "$quoted <{$this->address}>"; } - function __toString() { + public function __toString() { return $this->toString(); } } diff --git a/includes/mail/UserMailer.php b/includes/mail/UserMailer.php index 5d7030bb62..47fa16f87f 100644 --- a/includes/mail/UserMailer.php +++ b/includes/mail/UserMailer.php @@ -64,7 +64,7 @@ class UserMailer { * * @return string */ - static function arrayToHeaderString( $headers, $endl = PHP_EOL ) { + private static function arrayToHeaderString( $headers, $endl = PHP_EOL ) { $strings = []; foreach ( $headers as $name => $value ) { // Prevent header injection by stripping newlines from value @@ -79,7 +79,7 @@ class UserMailer { * * @return string */ - static function makeMsgId() { + private static function makeMsgId() { global $wgSMTP, $wgServer; $domainId = WikiMap::getCurrentWikiDbDomain()->getId(); @@ -465,7 +465,7 @@ class UserMailer { * @param int $code Error number * @param string $string Error message */ - static function errorHandler( $code, $string ) { + private static function errorHandler( $code, $string ) { self::$mErrorString = preg_replace( '/^mail\(\)(\s*\[.*?\])?: /', '', $string ); } diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index d65d87b66a..9e80cf4936 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -1697,6 +1697,7 @@ class WikiPage implements Page, IDBAccessObject { MediaWikiServices::getInstance()->getDBLoadBalancerFactory() ); + $derivedDataUpdater->setLogger( LoggerFactory::getInstance( 'SaveParse' ) ); $derivedDataUpdater->setRcWatchCategoryMembership( $wgRCWatchCategoryMembership ); $derivedDataUpdater->setArticleCountMethod( $wgArticleCountMethod ); diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index 59f2db452f..4808cafe90 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -2775,7 +2775,7 @@ class Parser { # The vary-revision flag must be set, because the magic word # will have a different value once the page is saved. $this->mOutput->setFlag( 'vary-revision' ); - wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" ); + wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision" ); } $value = $pageid ?: null; break; @@ -2792,13 +2792,14 @@ class Parser { $value = '-'; } else { $this->mOutput->setFlag( 'vary-revision-exists' ); + wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-exists" ); $value = ''; } } else { # Inform the edit saving system that getting the canonical output after # revision insertion requires another parse using the actual revision ID $this->mOutput->setFlag( 'vary-revision-id' ); - wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" ); + wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id" ); $value = $this->getRevisionId(); if ( $value === 0 ) { $rev = $this->getRevisionObject(); @@ -2828,17 +2829,13 @@ class Parser { $value = $this->getRevisionTimestampSubstring( 0, 4, self::MAX_TTS, $index ); break; case 'revisiontimestamp': - # Let the edit saving system know we should parse the page - # *after* a revision ID has been assigned. This is for null edits. - $this->mOutput->setFlag( 'vary-revision' ); - wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" ); - $value = $this->getRevisionTimestamp(); + $value = $this->getRevisionTimestampSubstring( 0, 14, self::MAX_TTS, $index ); break; case 'revisionuser': - # Let the edit saving system know we should parse the page - # *after* a revision ID has been assigned for null edits. + # Inform the edit saving system that getting the canonical output after + # revision insertion requires a parse that used the actual user ID $this->mOutput->setFlag( 'vary-user' ); - wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" ); + wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user" ); $value = $this->getRevisionUser(); break; case 'revisionsize': @@ -2986,7 +2983,7 @@ class Parser { /** * @param int $start * @param int $len - * @param int $mtts Max time-till-save; sets vary-revision if result might change by then + * @param int $mtts Max time-till-save; sets vary-revision-timestamp if result changes by then * @param string $variable Parser variable name * @return string */ @@ -2995,7 +2992,10 @@ class Parser { $resNow = substr( $this->getRevisionTimestamp(), $start, $len ); # Possibly set vary-revision if there is not yet an associated revision if ( !$this->getRevisionObject() ) { - # Get the timezone-adjusted timestamp $mtts seconds in the future + # Get the timezone-adjusted timestamp $mtts seconds in the future. + # This future is relative to the current time and not that of the + # parser options. The rendered timestamp can be compared to that + # of the timestamp specified by the parser options. $resThen = substr( $this->contLang->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ), $start, @@ -3003,10 +3003,10 @@ class Parser { ); if ( $resNow !== $resThen ) { - # Let the edit saving system know we should parse the page - # *after* a revision ID has been assigned. This is for null edits. - $this->mOutput->setFlag( 'vary-revision' ); - wfDebug( __METHOD__ . ": $variable used, setting vary-revision...\n" ); + # Inform the edit saving system that getting the canonical output after + # revision insertion requires a parse that used an actual revision timestamp + $this->mOutput->setFlag( 'vary-revision-timestamp' ); + wfDebug( __METHOD__ . ": $variable used, setting vary-revision-timestamp" ); } } @@ -3728,6 +3728,7 @@ class Parser { // If we transclude ourselves, the final result // will change based on the new version of the page $this->mOutput->setFlag( 'vary-revision' ); + wfDebug( __METHOD__ . ": self transclusion, setting vary-revision" ); } } } @@ -5892,7 +5893,7 @@ class Parser { * * The return value will be either: * - a) Positive, indicating a specific revision ID (current or old) - * - b) Zero, meaning the revision ID specified by getCurrentRevisionCallback() + * - b) Zero, meaning the revision ID is specified by getCurrentRevisionCallback() * - c) Null, meaning the parse is for preview mode and there is no revision * * @return int|null @@ -5947,20 +5948,25 @@ class Parser { /** * Get the timestamp associated with the current revision, adjusted for * the default server-local timestamp - * @return string + * @return string TS_MW timestamp */ public function getRevisionTimestamp() { - if ( is_null( $this->mRevisionTimestamp ) ) { - $revObject = $this->getRevisionObject(); - $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow(); - - # The cryptic '' timezone parameter tells to use the site-default - # timezone offset instead of the user settings. - # Since this value will be saved into the parser cache, served - # to other users, and potentially even used inside links and such, - # it needs to be consistent for all visitors. - $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' ); + if ( $this->mRevisionTimestamp !== null ) { + return $this->mRevisionTimestamp; } + + # Use specified revision timestamp, falling back to the current timestamp + $revObject = $this->getRevisionObject(); + $timestamp = $revObject ? $revObject->getTimestamp() : $this->mOptions->getTimestamp(); + $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone + + # The cryptic '' timezone parameter tells to use the site-default + # timezone offset instead of the user settings. + # Since this value will be saved into the parser cache, served + # to other users, and potentially even used inside links and such, + # it needs to be consistent for all visitors. + $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' ); + return $this->mRevisionTimestamp; } diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index afd6b2d4c8..709f159bb0 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -895,7 +895,7 @@ class ParserOptions { /** * Timestamp used for {{CURRENTDAY}} etc. - * @return string + * @return string TS_MW timestamp */ public function getTimestamp() { if ( !isset( $this->mTimestamp ) ) { diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php index 282d6cefc8..c8113f38be 100644 --- a/includes/parser/ParserOutput.php +++ b/includes/parser/ParserOutput.php @@ -213,6 +213,9 @@ class ParserOutput extends CacheTime { /** @var int|null Assumed rev ID for {{REVISIONID}} if no revision is set */ private $mSpeculativeRevId; + /** @var int|null Assumed rev timestamp for {{REVISIONTIMESTAMP}} if no revision is set */ + private $revisionTimestampUsed; + /** string CSS classes to use for the wrapping div, stored in the array keys. * If no class is given, no wrapper is added. */ @@ -445,6 +448,22 @@ class ParserOutput extends CacheTime { return $this->mSpeculativeRevId; } + /** + * @param string $timestamp TS_MW timestamp + * @since 1.34 + */ + public function setRevisionTimestampUsed( $timestamp ) { + $this->revisionTimestampUsed = $timestamp; + } + + /** + * @return string|null TS_MW timestamp or null if not used + * @since 1.34 + */ + public function getRevisionTimestampUsed() { + return $this->revisionTimestampUsed; + } + public function &getLanguageLinks() { return $this->mLanguageLinks; } diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 8ce4ab9036..ec376e3a3c 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -118,11 +118,10 @@ class ResourceLoader implements LoggerAwareInterface { return; } $dbr = wfGetDB( DB_REPLICA ); - $skin = $context->getSkin(); $lang = $context->getLanguage(); // Batched version of ResourceLoaderModule::getFileDependencies - $vary = "$skin|$lang"; + $vary = ResourceLoaderModule::getVary( $context ); $res = $dbr->select( 'module_deps', [ 'md_module', 'md_deps' ], [ 'md_module' => $moduleNames, 'md_skin' => $vary, @@ -1048,9 +1047,6 @@ MESSAGE; $states[$name] = 'missing'; } - // Generate output - $isRaw = false; - $filter = $context->getOnly() === 'styles' ? 'minify-css' : 'minify-js'; foreach ( $modules as $name => $module ) { @@ -1129,12 +1125,11 @@ MESSAGE; $states[$name] = 'error'; unset( $modules[$name] ); } - $isRaw |= $module->isRaw(); } // Update module states - if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) { - if ( count( $modules ) && $context->getOnly() === 'scripts' ) { + if ( $context->shouldIncludeScripts() && !$context->getRaw() ) { + if ( $modules && $context->getOnly() === 'scripts' ) { // Set the state of modules loaded as only scripts to ready as // they don't have an mw.loader.implement wrapper that sets the state foreach ( $modules as $name => $module ) { @@ -1143,7 +1138,7 @@ MESSAGE; } // Set the state of modules we didn't respond to with mw.loader.implement - if ( count( $states ) ) { + if ( $states ) { $stateScript = self::makeLoaderStateScript( $states ); if ( !$context->getDebug() ) { $stateScript = self::filter( 'minify-js', $stateScript ); diff --git a/includes/resourceloader/ResourceLoaderClientHtml.php b/includes/resourceloader/ResourceLoaderClientHtml.php index 7f2f85fe14..e324d04423 100644 --- a/includes/resourceloader/ResourceLoaderClientHtml.php +++ b/includes/resourceloader/ResourceLoaderClientHtml.php @@ -348,7 +348,7 @@ JAVASCRIPT; private static function makeContext( ResourceLoaderContext $mainContext, $group, $type, array $extraQuery = [] ) { - // Create new ResourceLoaderContext so that $extraQuery may trigger isRaw(). + // Create new ResourceLoaderContext so that $extraQuery is supported (eg. for 'sync=1'). $req = new FauxRequest( array_merge( $mainContext->getRequest()->getValues(), $extraQuery ) ); // Set 'only' if not combined $req->setVal( 'only', $type === ResourceLoaderModule::TYPE_COMBINED ? null : $type ); @@ -434,12 +434,6 @@ JAVASCRIPT; ); } } else { - // See if we have one or more raw modules - $isRaw = false; - foreach ( $moduleSet as $key => $module ) { - $isRaw |= $module->isRaw(); - } - // Special handling for the user group; because users might change their stuff // on-wiki like user pages, or user preferences; we need to find the highest // timestamp of these user-changeable modules so we can ensure cache misses on change @@ -455,9 +449,15 @@ JAVASCRIPT; // Decide whether to use 'style' or 'script' element if ( $only === ResourceLoaderModule::TYPE_STYLES ) { $chunk = Html::linkedStyle( $url ); - } elseif ( $context->getRaw() || $isRaw ) { + } elseif ( $context->getRaw() ) { + // This request is asking for the module to be delivered standalone, + // (aka "raw") without communicating to any mw.loader client. + // Use cases: + // - startup (naturally because this is what will define mw.loader) + // - html5shiv (loads synchronously in old IE before the async startup module arrives) + // - QUnit (needed in SpecialJavaScriptTest before async startup) $chunk = Html::element( 'script', [ - // In SpecialJavaScriptTest, QUnit must load synchronous + // The 'sync' option is only supported in combination with 'raw'. 'async' => !isset( $extraQuery['sync'] ), 'src' => $url ] ); diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index 95a81e6d4b..1f06ede1b7 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -410,4 +410,24 @@ class ResourceLoaderContext implements MessageLocalizer { } return $this->hash; } + + /** + * Get the request base parameters, omitting any defaults. + * + * @internal For internal use by ResourceLoaderStartUpModule only + * @return array + */ + public function getReqBase() { + $reqBase = []; + if ( $this->getLanguage() !== self::DEFAULT_LANG ) { + $reqBase['lang'] = $this->getLanguage(); + } + if ( $this->getSkin() !== self::DEFAULT_SKIN ) { + $reqBase['skin'] = $this->getSkin(); + } + if ( $this->getDebug() ) { + $reqBase['debug'] = 'true'; + } + return $reqBase; + } } diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index 7093ab1dc0..017b39934e 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -140,9 +140,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** @var bool Link to raw files in debug mode */ protected $debugRaw = true; - /** @var bool Whether mw.loader.state() call should be omitted */ - protected $raw = false; - protected $targets = [ 'desktop' ]; /** @var bool Whether CSSJanus flipping should be skipped for this module */ @@ -305,7 +302,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { break; // Single booleans case 'debugRaw': - case 'raw': case 'noflip': $this->{$member} = (bool)$option; break; @@ -513,13 +509,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { return $contents; } - /** - * @return bool - */ - public function isRaw() { - return $this->raw; - } - /** * Disable module content versioning. * @@ -620,7 +609,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { 'templates', 'skipFunction', 'debugRaw', - 'raw', ] as $member ) { $options[$member] = $this->{$member}; } @@ -1004,7 +992,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { || $this->dependencies || $this->messages || $this->skipFunction - || $this->raw ); return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL; } diff --git a/includes/resourceloader/ResourceLoaderImage.php b/includes/resourceloader/ResourceLoaderImage.php index 2e2da70475..c1b3dc34ca 100644 --- a/includes/resourceloader/ResourceLoaderImage.php +++ b/includes/resourceloader/ResourceLoaderImage.php @@ -146,6 +146,7 @@ class ResourceLoaderImage { * * @param ResourceLoaderContext $context Any context * @return string + * @throws MWException If no matching path is found */ public function getPath( ResourceLoaderContext $context ) { $desc = $this->descriptor; @@ -167,7 +168,11 @@ class ResourceLoaderImage { if ( isset( $desc[$context->getDirection()] ) ) { return $this->basePath . '/' . $desc[$context->getDirection()]; } - return $this->basePath . '/' . $desc['default']; + if ( isset( $desc['default'] ) ) { + return $this->basePath . '/' . $desc['default']; + } else { + throw new MWException( 'No matching path found' ); + } } /** diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php index 0baed65fce..c376fa7362 100644 --- a/includes/resourceloader/ResourceLoaderModule.php +++ b/includes/resourceloader/ResourceLoaderModule.php @@ -335,17 +335,6 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { return 'local'; } - /** - * Whether this module's JS expects to work without the client-side ResourceLoader module. - * Returning true from this function will prevent mw.loader.state() call from being - * appended to the bottom of the script. - * - * @return bool - */ - public function isRaw() { - return false; - } - /** * Get a list of modules this module depends on. * @@ -409,7 +398,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { * @return array List of files */ protected function getFileDependencies( ResourceLoaderContext $context ) { - $vary = $context->getSkin() . '|' . $context->getLanguage(); + $vary = self::getVary( $context ); // Try in-object cache first if ( !isset( $this->fileDeps[$vary] ) ) { @@ -444,7 +433,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { * @param string[] $files Array of file names */ public function setFileDependencies( ResourceLoaderContext $context, $files ) { - $vary = $context->getSkin() . '|' . $context->getLanguage(); + $vary = self::getVary( $context ); $this->fileDeps[$vary] = $files; } @@ -481,7 +470,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { } // The file deps list has changed, we want to update it. - $vary = $context->getSkin() . '|' . $context->getLanguage(); + $vary = self::getVary( $context ); $cache = ObjectCache::getLocalClusterInstance(); $key = $cache->makeKey( __METHOD__, $this->getName(), $vary ); $scopeLock = $cache->getScopedLock( $key, 0 ); @@ -1017,4 +1006,18 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { protected static function safeFileHash( $filePath ) { return FileContentsHasher::getFileContentsHash( $filePath ); } + + /** + * Get vary string. + * + * @internal For internal use only. + * @param ResourceLoaderContext $context + * @return string Vary string + */ + public static function getVary( ResourceLoaderContext $context ) { + return implode( '|', [ + $context->getSkin(), + $context->getLanguage(), + ] ); + } } diff --git a/includes/resourceloader/ResourceLoaderOOUIModule.php b/includes/resourceloader/ResourceLoaderOOUIModule.php index 0395127c57..899fbbde72 100644 --- a/includes/resourceloader/ResourceLoaderOOUIModule.php +++ b/includes/resourceloader/ResourceLoaderOOUIModule.php @@ -27,7 +27,7 @@ trait ResourceLoaderOOUIModule { protected static $knownScriptsModules = [ 'core' ]; protected static $knownStylesModules = [ 'core', 'widgets', 'toolbars', 'windows' ]; protected static $knownImagesModules = [ - 'indicators', 'textures', + 'indicators', // Extra icons 'icons-accessibility', 'icons-alerts', diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index b90b618b7d..2959b22be0 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -254,10 +254,11 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { continue; } - if ( $module->isRaw() ) { - // Don't register "raw" modules (like 'startup') client-side because depending on them - // is illegal anyway and would only lead to them being loaded a second time, - // causing any state to be lost. + if ( $module instanceof ResourceLoaderStartUpModule ) { + // Don't register 'startup' to the client because loading it lazily or depending + // on it doesn't make sense, because the startup module *is* the client. + // Registering would be a waste of bandwidth and memory and risks somehow causing + // it to load a second time. // ATTENTION: Because of the line below, this is not going to cause infinite recursion. // Think carefully before making changes to this code! @@ -341,13 +342,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { return $out; } - /** - * @return bool - */ - public function isRaw() { - return true; - } - /** * @private For internal use by SpecialJavaScriptTest * @since 1.32 @@ -401,6 +395,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { // Perform replacements for mediawiki.js $mwLoaderPairs = [ + '$VARS.reqBase' => ResourceLoader::encodeJsonForScript( $context->getReqBase() ), '$VARS.baseModules' => ResourceLoader::encodeJsonForScript( $this->getBaseModules() ), '$VARS.maxQueryLength' => ResourceLoader::encodeJsonForScript( $conf->get( 'ResourceLoaderMaxQueryLength' ) diff --git a/includes/search/PrefixSearch.php b/includes/search/PrefixSearch.php index 3b7a0a9f99..3fff6c1b60 100644 --- a/includes/search/PrefixSearch.php +++ b/includes/search/PrefixSearch.php @@ -30,22 +30,6 @@ use MediaWiki\MediaWikiServices; * @ingroup Search */ abstract class PrefixSearch { - /** - * Do a prefix search of titles and return a list of matching page names. - * @deprecated Since 1.23, use TitlePrefixSearch or StringPrefixSearch classes - * - * @param string $search - * @param int $limit - * @param array $namespaces Used if query is not explicitly prefixed - * @param int $offset How many results to offset from the beginning - * @return array Array of strings - */ - public static function titleSearch( $search, $limit, $namespaces = [], $offset = 0 ) { - wfDeprecated( __METHOD__, '1.23' ); - $prefixSearch = new StringPrefixSearch; - return $prefixSearch->search( $search, $limit, $namespaces, $offset ); - } - /** * Do a prefix search of titles and return a list of matching page names. * diff --git a/includes/search/SearchDatabase.php b/includes/search/SearchDatabase.php index 230cdedd71..8ea356f77a 100644 --- a/includes/search/SearchDatabase.php +++ b/includes/search/SearchDatabase.php @@ -22,6 +22,7 @@ */ use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\ILoadBalancer; /** * Base search engine base class for database-backed searches @@ -29,16 +30,23 @@ use Wikimedia\Rdbms\IDatabase; * @since 1.23 */ abstract class SearchDatabase extends SearchEngine { + /** @var ILoadBalancer */ + protected $lb; + /** @var IDatabase (backwards compatibility) */ + protected $db; + /** - * @var IDatabase Replica database from which to read results + * @var string[] search terms */ - protected $db; + protected $searchTerms = []; /** - * @param IDatabase|null $db The database to search from + * @param ILoadBalancer $lb The load balancer for the DB cluster to search on */ - public function __construct( IDatabase $db = null ) { - $this->db = $db ?: wfGetDB( DB_REPLICA ); + public function __construct( ILoadBalancer $lb ) { + $this->lb = $lb; + // @TODO: remove this deprecated field in 1.35 + $this->db = $lb->getLazyConnectionRef( DB_REPLICA ); // b/c } /** diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php index fa6e7fd096..2fb4585c92 100644 --- a/includes/search/SearchEngine.php +++ b/includes/search/SearchEngine.php @@ -46,7 +46,10 @@ abstract class SearchEngine { /** @var int */ protected $offset = 0; - /** @var string[] */ + /** + * @var string[] + * @deprecated since 1.34 + */ protected $searchTerms = []; /** @var bool */ diff --git a/includes/search/SearchEngineFactory.php b/includes/search/SearchEngineFactory.php index ecb6f43e64..6a69cd4ee9 100644 --- a/includes/search/SearchEngineFactory.php +++ b/includes/search/SearchEngineFactory.php @@ -1,6 +1,8 @@ config->getSearchType(); + $alternativesClasses = $this->config->getSearchTypes(); - $configType = $this->config->getSearchType(); - $alternatives = $this->config->getSearchTypes(); - - if ( $type && in_array( $type, $alternatives ) ) { + $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); + if ( $type !== null && in_array( $type, $alternativesClasses ) ) { $class = $type; - } elseif ( $configType !== null ) { - $class = $configType; + } elseif ( $configuredClass !== null ) { + $class = $configuredClass; } else { - $dbr = wfGetDB( DB_REPLICA ); - $class = self::getSearchEngineClass( $dbr ); + $class = self::getSearchEngineClass( $lb ); } - $search = new $class( $dbr ); - return $search; + if ( is_subclass_of( $class, SearchDatabase::class ) ) { + return new $class( $lb ); + } else { + return new $class(); + } } /** - * @param IDatabase $db + * @param IDatabase|ILoadBalancer $dbOrLb * @return string SearchEngine subclass name * @since 1.28 */ - public static function getSearchEngineClass( IDatabase $db ) { - switch ( $db->getType() ) { + public static function getSearchEngineClass( $dbOrLb ) { + $type = ( $dbOrLb instanceof IDatabase ) + ? $dbOrLb->getType() + : $dbOrLb->getServerType( $dbOrLb->getWriterIndex() ); + + switch ( $type ) { case 'sqlite': return SearchSqlite::class; case 'mysql': diff --git a/includes/search/SearchMssql.php b/includes/search/SearchMssql.php index 0e85f9df2f..6a23bb344f 100644 --- a/includes/search/SearchMssql.php +++ b/includes/search/SearchMssql.php @@ -36,7 +36,9 @@ class SearchMssql extends SearchDatabase { * @return SqlSearchResultSet */ protected function doSearchTextInDB( $term ) { - $resultSet = $this->db->query( $this->getQuery( $this->filter( $term ), true ) ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $resultSet = $dbr->query( $this->getQuery( $this->filter( $term ), true ) ); + return new SqlSearchResultSet( $resultSet, $this->searchTerms ); } @@ -47,7 +49,9 @@ class SearchMssql extends SearchDatabase { * @return SqlSearchResultSet */ protected function doSearchTitleInDB( $term ) { - $resultSet = $this->db->query( $this->getQuery( $this->filter( $term ), false ) ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $resultSet = $dbr->query( $this->getQuery( $this->filter( $term ), false ) ); + return new SqlSearchResultSet( $resultSet, $this->searchTerms ); } @@ -72,7 +76,9 @@ class SearchMssql extends SearchDatabase { * @return string */ private function queryLimit( $sql ) { - return $this->db->limitResult( $sql, $this->limit, $this->offset ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + + return $dbr->limitResult( $sql, $this->limit, $this->offset ); } /** @@ -120,8 +126,9 @@ class SearchMssql extends SearchDatabase { */ private function queryMain( $filteredTerm, $fulltext ) { $match = $this->parseQuery( $filteredTerm, $fulltext ); - $page = $this->db->tableName( 'page' ); - $searchindex = $this->db->tableName( 'searchindex' ); + $dbr = $this->lb->getMaintenanceConnectionRef( DB_REPLICA ); + $page = $dbr->tableName( 'page' ); + $searchindex = $dbr->tableName( 'searchindex' ); return 'SELECT page_id, page_namespace, page_title, ftindex.[RANK]' . "FROM $page,FREETEXTTABLE($searchindex , $match, LANGUAGE 'English') as ftindex " . @@ -159,8 +166,10 @@ class SearchMssql extends SearchDatabase { } } - $searchon = $this->db->addQuotes( implode( ',', $q ) ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $searchon = $dbr->addQuotes( implode( ',', $q ) ); $field = $this->getIndexField( $fulltext ); + return "$field, $searchon"; } @@ -179,13 +188,14 @@ class SearchMssql extends SearchDatabase { // to properly decode the stream as UTF-8. SQL doesn't support UTF8 as a data type // but the indexer will correctly handle it by this method. Since all we are doing // is passing this data to the indexer and never retrieving it via PHP, this will save space - $table = $this->db->tableName( 'searchindex' ); + $dbr = $this->lb->getMaintenanceConnectionRef( DB_MASTER ); + $table = $dbr->tableName( 'searchindex' ); $utf8bom = '0xEFBBBF'; $si_title = $utf8bom . bin2hex( $title ); $si_text = $utf8bom . bin2hex( $text ); $sql = "DELETE FROM $table WHERE si_page = $id;"; $sql .= "INSERT INTO $table (si_page, si_title, si_text) VALUES ($id, $si_title, $si_text)"; - return $this->db->query( $sql, 'SearchMssql::update' ); + return $dbr->query( $sql, 'SearchMssql::update' ); } /** @@ -197,13 +207,14 @@ class SearchMssql extends SearchDatabase { * @return bool|IResultWrapper */ function updateTitle( $id, $title ) { - $table = $this->db->tableName( 'searchindex' ); + $dbr = $this->lb->getMaintenanceConnectionRef( DB_MASTER ); + $table = $dbr->tableName( 'searchindex' ); // see update for why we are using the utf8bom $utf8bom = '0xEFBBBF'; $si_title = $utf8bom . bin2hex( $title ); $sql = "DELETE FROM $table WHERE si_page = $id;"; $sql .= "INSERT INTO $table (si_page, si_title, si_text) VALUES ($id, $si_title, 0x00)"; - return $this->db->query( $sql, 'SearchMssql::updateTitle' ); + return $dbr->query( $sql, 'SearchMssql::updateTitle' ); } } diff --git a/includes/search/SearchMySQL.php b/includes/search/SearchMySQL.php index cae342670e..4a6b93b209 100644 --- a/includes/search/SearchMySQL.php +++ b/includes/search/SearchMySQL.php @@ -124,7 +124,8 @@ class SearchMySQL extends SearchDatabase { wfDebug( __METHOD__ . ": Can't understand search query '{$filteredText}'\n" ); } - $searchon = $this->db->addQuotes( $searchon ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $searchon = $dbr->addQuotes( $searchon ); $field = $this->getIndexField( $fulltext ); return [ " MATCH($field) AGAINST($searchon IN BOOLEAN MODE) ", @@ -186,14 +187,15 @@ class SearchMySQL extends SearchDatabase { $filteredTerm = $this->filter( $term ); $query = $this->getQuery( $filteredTerm, $fulltext ); - $resultSet = $this->db->select( + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $resultSet = $dbr->select( $query['tables'], $query['fields'], $query['conds'], __METHOD__, $query['options'], $query['joins'] ); $total = null; $query = $this->getCountQuery( $filteredTerm, $fulltext ); - $totalResult = $this->db->select( + $totalResult = $dbr->select( $query['tables'], $query['fields'], $query['conds'], __METHOD__, $query['options'], $query['joins'] ); @@ -224,7 +226,8 @@ class SearchMySQL extends SearchDatabase { protected function queryFeatures( &$query ) { foreach ( $this->features as $feature => $value ) { if ( $feature === 'title-suffix-filter' && $value ) { - $query['conds'][] = 'page_title' . $this->db->buildLike( $this->db->anyString(), $value ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $query['conds'][] = 'page_title' . $dbr->buildLike( $dbr->anyString(), $value ); } } } @@ -339,7 +342,7 @@ class SearchMySQL extends SearchDatabase { * @param string $text */ function update( $id, $title, $text ) { - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->lb->getConnectionRef( DB_MASTER ); $dbw->replace( 'searchindex', [ 'si_page' ], [ @@ -357,13 +360,12 @@ class SearchMySQL extends SearchDatabase { * @param string $title */ function updateTitle( $id, $title ) { - $dbw = wfGetDB( DB_MASTER ); - + $dbw = $this->lb->getConnectionRef( DB_MASTER ); $dbw->update( 'searchindex', [ 'si_title' => $this->normalizeText( $title ) ], [ 'si_page' => $id ], - __METHOD__, - [ $dbw->lowPriorityOption() ] ); + __METHOD__ + ); } /** @@ -374,8 +376,7 @@ class SearchMySQL extends SearchDatabase { * @param string $title Title of page that was deleted */ function delete( $id, $title ) { - $dbw = wfGetDB( DB_MASTER ); - + $dbw = $this->lb->getConnectionRef( DB_MASTER ); $dbw->delete( 'searchindex', [ 'si_page' => $id ], __METHOD__ ); } @@ -441,7 +442,7 @@ class SearchMySQL extends SearchDatabase { if ( is_null( self::$mMinSearchLength ) ) { $sql = "SHOW GLOBAL VARIABLES LIKE 'ft\\_min\\_word\\_len'"; - $dbr = wfGetDB( DB_REPLICA ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); $result = $dbr->query( $sql, __METHOD__ ); $row = $result->fetchObject(); $result->free(); diff --git a/includes/search/SearchOracle.php b/includes/search/SearchOracle.php index 6b2b4038dc..a5d351bcd7 100644 --- a/includes/search/SearchOracle.php +++ b/includes/search/SearchOracle.php @@ -71,7 +71,8 @@ class SearchOracle extends SearchDatabase { return new SqlSearchResultSet( false, '' ); } - $resultSet = $this->db->query( $this->getQuery( $this->filter( $term ), true ) ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $resultSet = $dbr->query( $this->getQuery( $this->filter( $term ), true ) ); return new SqlSearchResultSet( $resultSet, $this->searchTerms ); } @@ -86,7 +87,8 @@ class SearchOracle extends SearchDatabase { return new SqlSearchResultSet( false, '' ); } - $resultSet = $this->db->query( $this->getQuery( $this->filter( $term ), false ) ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $resultSet = $dbr->query( $this->getQuery( $this->filter( $term ), false ) ); return new SqlSearchResultSet( $resultSet, $this->searchTerms ); } @@ -101,7 +103,8 @@ class SearchOracle extends SearchDatabase { if ( $this->namespaces === [] ) { $namespaces = '0'; } else { - $namespaces = $this->db->makeList( $this->namespaces ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $namespaces = $dbr->makeList( $this->namespaces ); } return 'AND page_namespace IN (' . $namespaces . ')'; } @@ -114,7 +117,9 @@ class SearchOracle extends SearchDatabase { * @return string */ private function queryLimit( $sql ) { - return $this->db->limitResult( $sql, $this->limit, $this->offset ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + + return $dbr->limitResult( $sql, $this->limit, $this->offset ); } /** @@ -160,8 +165,11 @@ class SearchOracle extends SearchDatabase { */ function queryMain( $filteredTerm, $fulltext ) { $match = $this->parseQuery( $filteredTerm, $fulltext ); - $page = $this->db->tableName( 'page' ); - $searchindex = $this->db->tableName( 'searchindex' ); + + $dbr = $this->lb->getMaintenanceConnectionRef( DB_REPLICA ); + $page = $dbr->tableName( 'page' ); + $searchindex = $dbr->tableName( 'searchindex' ); + return 'SELECT page_id, page_namespace, page_title ' . "FROM $page,$searchindex " . 'WHERE page_id=si_page AND ' . $match; @@ -208,8 +216,10 @@ class SearchOracle extends SearchDatabase { } } - $searchon = $this->db->addQuotes( ltrim( $searchon, ' &' ) ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $searchon = $dbr->addQuotes( ltrim( $searchon, ' &' ) ); $field = $this->getIndexField( $fulltext ); + return " CONTAINS($field, $searchon, 1) > 0 "; } @@ -230,7 +240,7 @@ class SearchOracle extends SearchDatabase { * @param string $text */ function update( $id, $title, $text ) { - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->lb->getConnection( DB_MASTER ); $dbw->replace( 'searchindex', [ 'si_page' ], [ @@ -258,8 +268,7 @@ class SearchOracle extends SearchDatabase { * @param string $title */ function updateTitle( $id, $title ) { - $dbw = wfGetDB( DB_MASTER ); - + $dbw = $this->lb->getConnectionRef( DB_MASTER ); $dbw->update( 'searchindex', [ 'si_title' => $title ], [ 'si_page' => $id ], diff --git a/includes/search/SearchPostgres.php b/includes/search/SearchPostgres.php index 74ee552abb..63634cba7f 100644 --- a/includes/search/SearchPostgres.php +++ b/includes/search/SearchPostgres.php @@ -42,7 +42,8 @@ class SearchPostgres extends SearchDatabase { protected function doSearchTitleInDB( $term ) { $q = $this->searchQuery( $term, 'titlevector', 'page_title' ); $olderror = error_reporting( E_ERROR ); - $resultSet = $this->db->query( $q, 'SearchPostgres', true ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $resultSet = $dbr->query( $q, 'SearchPostgres', true ); error_reporting( $olderror ); return new SqlSearchResultSet( $resultSet, $this->searchTerms ); } @@ -50,7 +51,8 @@ class SearchPostgres extends SearchDatabase { protected function doSearchTextInDB( $term ) { $q = $this->searchQuery( $term, 'textvector', 'old_text' ); $olderror = error_reporting( E_ERROR ); - $resultSet = $this->db->query( $q, 'SearchPostgres', true ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $resultSet = $dbr->query( $q, 'SearchPostgres', true ); error_reporting( $olderror ); return new SqlSearchResultSet( $resultSet, $this->searchTerms ); } @@ -111,7 +113,8 @@ class SearchPostgres extends SearchDatabase { $searchstring = preg_replace( '/^[\'"](.*)[\'"]$/', "$1", $searchstring ); # # Quote the whole thing - $searchstring = $this->db->addQuotes( $searchstring ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $searchstring = $dbr->addQuotes( $searchstring ); wfDebug( "parseQuery returned: $searchstring \n" ); @@ -131,7 +134,8 @@ class SearchPostgres extends SearchDatabase { # # We need a separate query here so gin does not complain about empty searches $sql = "SELECT to_tsquery($searchstring)"; - $res = $this->db->query( $sql ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $res = $dbr->query( $sql ); if ( !$res ) { # # TODO: Better output (example to catch: one 'two) die( "Sorry, that was not a valid search string. Please go back and try again" ); @@ -172,14 +176,14 @@ class SearchPostgres extends SearchDatabase { if ( count( $this->namespaces ) < 1 ) { $query .= ' AND page_namespace = 0'; } else { - $namespaces = $this->db->makeList( $this->namespaces ); + $namespaces = $dbr->makeList( $this->namespaces ); $query .= " AND page_namespace IN ($namespaces)"; } } $query .= " ORDER BY score DESC, page_id DESC"; - $query .= $this->db->limitResult( '', $this->limit, $this->offset ); + $query .= $dbr->limitResult( '', $this->limit, $this->offset ); wfDebug( "searchQuery returned: $query \n" ); @@ -201,12 +205,14 @@ class SearchPostgres extends SearchDatabase { " AND s.slot_role_id = " . $slotRoleStore->getId( SlotRecord::MAIN ) . " " . " AND c.content_id = s.slot_content_id " . " ORDER BY old_rev_text_id DESC OFFSET 1)"; - $this->db->query( $sql ); + + $dbw = $this->lb->getConnectionRef( DB_MASTER ); + $dbw->query( $sql ); + return true; } function updateTitle( $id, $title ) { return true; } - } diff --git a/includes/search/SearchResult.php b/includes/search/SearchResult.php index a27d71933f..7703e38dd0 100644 --- a/includes/search/SearchResult.php +++ b/includes/search/SearchResult.php @@ -147,26 +147,11 @@ class SearchResult { } /** - * @param string[] $terms Terms to highlight + * @param string[] $terms Terms to highlight (this parameter is deprecated and ignored) * @return string Highlighted text snippet, null (and not '') if not supported */ - function getTextSnippet( $terms ) { - global $wgAdvancedSearchHighlighting; - $this->initText(); - - // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter. - list( $contextlines, $contextchars ) = $this->searchEngine->userHighlightPrefs(); - - $h = new SearchHighlighter(); - if ( count( $terms ) > 0 ) { - if ( $wgAdvancedSearchHighlighting ) { - return $h->highlightText( $this->mText, $terms, $contextlines, $contextchars ); - } else { - return $h->highlightSimple( $this->mText, $terms, $contextlines, $contextchars ); - } - } else { - return $h->highlightNone( $this->mText, $contextlines, $contextchars ); - } + function getTextSnippet( $terms = [] ) { + return ''; } /** diff --git a/includes/search/SearchResultSet.php b/includes/search/SearchResultSet.php index 92e2a17d6d..5ee96cba65 100644 --- a/includes/search/SearchResultSet.php +++ b/includes/search/SearchResultSet.php @@ -96,6 +96,7 @@ class SearchResultSet implements Countable, IteratorAggregate { * STUB * * @return string[] + * @deprecated since 1.34 (use SqlSearchResult) */ function termMatches() { return []; diff --git a/includes/search/SearchSqlite.php b/includes/search/SearchSqlite.php index c30479766e..3646b274ed 100644 --- a/includes/search/SearchSqlite.php +++ b/includes/search/SearchSqlite.php @@ -22,6 +22,7 @@ */ use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\DatabaseSqlite; /** * Search engine hook for SQLite @@ -33,7 +34,10 @@ class SearchSqlite extends SearchDatabase { * @return bool */ function fulltextSearchSupported() { - return $this->db->checkForEnabledSearch(); + /** @var DatabaseSqlite $dbr */ + $dbr = $this->lb->getConnection( DB_REPLICA ); + + return $dbr->checkForEnabledSearch(); } /** @@ -120,8 +124,10 @@ class SearchSqlite extends SearchDatabase { wfDebug( __METHOD__ . ": Can't understand search query '{$filteredText}'\n" ); } - $searchon = $this->db->addQuotes( $searchon ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $searchon = $dbr->addQuotes( $searchon ); $field = $this->getIndexField( $fulltext ); + return " $field MATCH $searchon "; } @@ -178,10 +184,11 @@ class SearchSqlite extends SearchDatabase { $filteredTerm = $this->filter( MediaWikiServices::getInstance()->getContentLanguage()->lc( $term ) ); - $resultSet = $this->db->query( $this->getQuery( $filteredTerm, $fulltext ) ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $resultSet = $dbr->query( $this->getQuery( $filteredTerm, $fulltext ) ); $total = null; - $totalResult = $this->db->query( $this->getCountQuery( $filteredTerm, $fulltext ) ); + $totalResult = $dbr->query( $this->getCountQuery( $filteredTerm, $fulltext ) ); $row = $totalResult->fetchObject(); if ( $row ) { $total = intval( $row->c ); @@ -202,7 +209,8 @@ class SearchSqlite extends SearchDatabase { if ( $this->namespaces === [] ) { $namespaces = '0'; } else { - $namespaces = $this->db->makeList( $this->namespaces ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + $namespaces = $dbr->makeList( $this->namespaces ); } return 'AND page_namespace IN (' . $namespaces . ')'; } @@ -213,7 +221,9 @@ class SearchSqlite extends SearchDatabase { * @return string */ private function limitResult( $sql ) { - return $this->db->limitResult( $sql, $this->limit, $this->offset ); + $dbr = $this->lb->getConnectionRef( DB_REPLICA ); + + return $dbr->limitResult( $sql, $this->limit, $this->offset ); } /** @@ -248,8 +258,9 @@ class SearchSqlite extends SearchDatabase { */ private function queryMain( $filteredTerm, $fulltext ) { $match = $this->parseQuery( $filteredTerm, $fulltext ); - $page = $this->db->tableName( 'page' ); - $searchindex = $this->db->tableName( 'searchindex' ); + $dbr = $this->lb->getMaintenanceConnectionRef( DB_REPLICA ); + $page = $dbr->tableName( 'page' ); + $searchindex = $dbr->tableName( 'searchindex' ); return "SELECT $searchindex.rowid, page_namespace, page_title " . "FROM $page,$searchindex " . "WHERE page_id=$searchindex.rowid AND $match"; @@ -257,8 +268,9 @@ class SearchSqlite extends SearchDatabase { private function getCountQuery( $filteredTerm, $fulltext ) { $match = $this->parseQuery( $filteredTerm, $fulltext ); - $page = $this->db->tableName( 'page' ); - $searchindex = $this->db->tableName( 'searchindex' ); + $dbr = $this->lb->getMaintenanceConnectionRef( DB_REPLICA ); + $page = $dbr->tableName( 'page' ); + $searchindex = $dbr->tableName( 'searchindex' ); return "SELECT COUNT(*) AS c " . "FROM $page,$searchindex " . "WHERE page_id=$searchindex.rowid AND $match " . @@ -279,10 +291,8 @@ class SearchSqlite extends SearchDatabase { } // @todo find a method to do it in a single request, // couldn't do it so far due to typelessness of FTS3 tables. - $dbw = wfGetDB( DB_MASTER ); - + $dbw = $this->lb->getConnectionRef( DB_MASTER ); $dbw->delete( 'searchindex', [ 'rowid' => $id ], __METHOD__ ); - $dbw->insert( 'searchindex', [ 'rowid' => $id, @@ -302,8 +312,8 @@ class SearchSqlite extends SearchDatabase { if ( !$this->fulltextSearchSupported() ) { return; } - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->lb->getConnectionRef( DB_MASTER ); $dbw->update( 'searchindex', [ 'si_title' => $title ], [ 'rowid' => $id ], diff --git a/includes/search/SqlSearchResult.php b/includes/search/SqlSearchResult.php new file mode 100644 index 0000000000..25e87e70ff --- /dev/null +++ b/includes/search/SqlSearchResult.php @@ -0,0 +1,69 @@ +initFromTitle( $title ); + $this->terms = $terms; + } + + /** + * return string[] + */ + public function getTermMatches(): array { + return $this->terms; + } + + /** + * @param array $terms Terms to highlight (this parameter is deprecated) + * @return string Highlighted text snippet, null (and not '') if not supported + */ + function getTextSnippet( $terms = [] ) { + global $wgAdvancedSearchHighlighting; + $this->initText(); + + // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter. + list( $contextlines, $contextchars ) = $this->searchEngine->userHighlightPrefs(); + + $h = new SearchHighlighter(); + if ( count( $this->terms ) > 0 ) { + if ( $wgAdvancedSearchHighlighting ) { + return $h->highlightText( $this->mText, $this->terms, $contextlines, $contextchars ); + } else { + return $h->highlightSimple( $this->mText, $this->terms, $contextlines, $contextchars ); + } + } else { + return $h->highlightNone( $this->mText, $contextlines, $contextchars ); + } + } + +} diff --git a/includes/search/SqlSearchResultSet.php b/includes/search/SqlSearchResultSet.php index f4e4a23abe..87068ca3d3 100644 --- a/includes/search/SqlSearchResultSet.php +++ b/includes/search/SqlSearchResultSet.php @@ -16,12 +16,22 @@ class SqlSearchResultSet extends SearchResultSet { /** @var int|null Total number of hits for $terms */ protected $totalHits; - function __construct( IResultWrapper $resultSet, $terms, $total = null ) { + /** + * @param IResultWrapper $resultSet + * @param string[] $terms + * @param int|null $total + */ + function __construct( IResultWrapper $resultSet, array $terms, $total = null ) { + parent::__construct(); $this->resultSet = $resultSet; $this->terms = $terms; $this->totalHits = $total; } + /** + * @return string[] + * @deprecated since 1.34 + */ function termMatches() { return $this->terms; } @@ -42,10 +52,15 @@ class SqlSearchResultSet extends SearchResultSet { if ( $this->results === null ) { $this->results = []; $this->resultSet->rewind(); + $terms = \MediaWiki\MediaWikiServices::getInstance()->getContentLanguage() + ->convertForSearchResult( $this->terms ); while ( ( $row = $this->resultSet->fetchObject() ) !== false ) { - $this->results[] = SearchResult::newFromTitle( - Title::makeTitle( $row->page_namespace, $row->page_title ), $this + $result = new SqlSearchResult( + Title::makeTitle( $row->page_namespace, $row->page_title ), + $terms ); + $this->augmentResult( $result ); + $this->results[] = $result; } } return $this->results; diff --git a/includes/session/SessionManager.php b/includes/session/SessionManager.php index 98c04995a3..3810565bcb 100644 --- a/includes/session/SessionManager.php +++ b/includes/session/SessionManager.php @@ -329,12 +329,9 @@ final class SessionManager implements SessionManagerInterface { $headers = []; foreach ( $this->getProviders() as $provider ) { foreach ( $provider->getVaryHeaders() as $header => $options ) { - if ( !isset( $headers[$header] ) ) { - $headers[$header] = []; - } - if ( is_array( $options ) ) { - $headers[$header] = array_unique( array_merge( $headers[$header], $options ) ); - } + # Note that the $options value returned has been deprecated + # and is ignored. + $headers[$header] = null; } } $this->varyHeaders = $headers; diff --git a/includes/session/SessionManagerInterface.php b/includes/session/SessionManagerInterface.php index c6990fefe7..7c05cfc6a6 100644 --- a/includes/session/SessionManagerInterface.php +++ b/includes/session/SessionManagerInterface.php @@ -96,6 +96,9 @@ interface SessionManagerInterface extends LoggerAwareInterface { * } * @endcode * + * Note that the $options argument to OutputPage::addVaryHeader() has + * been deprecated and should always be null. + * * @return array */ public function getVaryHeaders(); diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index f45596f3fd..918c761bca 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -389,9 +389,8 @@ abstract class Skin extends ContextSource { /** * Outputs the HTML generated by other functions. - * @param OutputPage|null $out */ - abstract function outputPage( OutputPage $out = null ); + abstract function outputPage(); /** * @param array $data @@ -814,9 +813,11 @@ abstract class Skin extends ContextSource { } /** + * @deprecated since 1.34, use getSearchLink() instead. * @return string */ function escapeSearchLink() { + wfDeprecated( __METHOD__, '1.34' ); return htmlspecialchars( $this->getSearchLink() ); } diff --git a/includes/skins/SkinFactory.php b/includes/skins/SkinFactory.php index eb71fe6f35..98d3456adc 100644 --- a/includes/skins/SkinFactory.php +++ b/includes/skins/SkinFactory.php @@ -21,8 +21,6 @@ * @file */ -use MediaWiki\MediaWikiServices; - /** * Factory class to create Skin objects * @@ -43,15 +41,6 @@ class SkinFactory { */ private $displayNames = []; - /** - * @deprecated in 1.27 - * @return SkinFactory - */ - public static function getDefaultInstance() { - wfDeprecated( __METHOD__, '1.27' ); - return MediaWikiServices::getInstance()->getSkinFactory(); - } - /** * Register a new Skin factory function. * diff --git a/includes/skins/SkinTemplate.php b/includes/skins/SkinTemplate.php index a7b7569f0e..5d6197e7ac 100644 --- a/includes/skins/SkinTemplate.php +++ b/includes/skins/SkinTemplate.php @@ -207,21 +207,10 @@ class SkinTemplate extends Skin { } /** - * initialize various variables and generate the template - * - * @param OutputPage|null $out + * Initialize various variables and generate the template */ - function outputPage( OutputPage $out = null ) { + function outputPage() { Profiler::instance()->setTemplated( true ); - - $oldContext = null; - if ( $out !== null ) { - // Deprecated since 1.20, note added in 1.25 - wfDeprecated( __METHOD__, '1.25' ); - $oldContext = $this->getContext(); - $this->setContext( $out->getContext() ); - } - $out = $this->getOutput(); $this->initPage( $out ); @@ -231,10 +220,6 @@ class SkinTemplate extends Skin { // result may be an error $this->printOrError( $res ); - - if ( $oldContext ) { - $this->setContext( $oldContext ); - } } /** @@ -332,7 +317,7 @@ class SkinTemplate extends Skin { $tpl->set( 'handheld', $request->getBool( 'handheld' ) ); $tpl->set( 'loggedin', $this->loggedin ); $tpl->set( 'notspecialpage', !$title->isSpecialPage() ); - $tpl->set( 'searchaction', $this->escapeSearchLink() ); + $tpl->set( 'searchaction', $this->getSearchLink() ); $tpl->set( 'searchtitle', SpecialPage::getTitleFor( 'Search' )->getPrefixedDBkey() ); $tpl->set( 'search', trim( $request->getVal( 'search' ) ) ); $tpl->set( 'stylepath', $wgStylePath ); diff --git a/includes/specialpage/QueryPage.php b/includes/specialpage/QueryPage.php index 700672f12f..eb179bf310 100644 --- a/includes/specialpage/QueryPage.php +++ b/includes/specialpage/QueryPage.php @@ -660,7 +660,7 @@ abstract class QueryPage extends SpecialPage { # an OutputPage, and let them get on with it $this->outputResults( $out, $this->getSkin(), - $dbr, # Should use a ResultWrapper for this + $dbr, # Should use IResultWrapper for this $res, min( $this->numRows, $this->limit ), # do not format the one extra row, if exist $this->offset ); @@ -738,13 +738,13 @@ abstract class QueryPage extends SpecialPage { } /** - * Creates a new LinkBatch object, adds all pages from the passed ResultWrapper (MUST include + * Creates a new LinkBatch object, adds all pages from the passed result wrapper (MUST include * title and optional the namespace field) and executes the batch. This operation will pre-cache * LinkCache information like page existence and information for stub color and redirect hints. * - * @param IResultWrapper $res The ResultWrapper object to process. Needs to include the title + * @param IResultWrapper $res The result wrapper to process. Needs to include the title * field and namespace field, if the $ns parameter isn't set. - * @param null $ns Use this namespace for the given titles in the ResultWrapper object, + * @param null $ns Use this namespace for the given titles in the result wrapper, * instead of the namespace value of $res. */ protected function executeLBFromResultWrapper( IResultWrapper $res, $ns = null ) { diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php index 9a793c3146..40172ab693 100644 --- a/includes/specialpage/SpecialPageFactory.php +++ b/includes/specialpage/SpecialPageFactory.php @@ -232,6 +232,7 @@ class SpecialPageFactory { 'EmailAuthentication', 'EnableEmail', 'EnableJavaScriptTest', + 'EnableSpecialMute', 'PageLanguageUseDB', 'SpecialPages', ]; @@ -282,9 +283,14 @@ class SpecialPageFactory { $this->list['JavaScriptTest'] = \SpecialJavaScriptTest::class; } + if ( $this->options->get( 'EnableSpecialMute' ) ) { + $this->list['Mute'] = \SpecialMute::class; + } + if ( $this->options->get( 'PageLanguageUseDB' ) ) { $this->list['PageLanguage'] = \SpecialPageLanguage::class; } + if ( $this->options->get( 'ContentHandlerUseDB' ) ) { $this->list['ChangeContentModel'] = \SpecialChangeContentModel::class; } diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index d83853af7a..4f5c15099c 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -625,8 +625,7 @@ class SpecialContributions extends IncludableSpecialPage { [], Xml::label( $this->msg( 'namespace' )->text(), - 'namespace', - '' + 'namespace' ) . "\u{00A0}" . Html::namespaceSelector( [ 'selected' => $this->opts['namespace'], 'all' => '', 'in-user-lang' => true ], diff --git a/includes/specials/SpecialEmailUser.php b/includes/specials/SpecialEmailUser.php index e1606b2561..b42cdea08a 100644 --- a/includes/specials/SpecialEmailUser.php +++ b/includes/specials/SpecialEmailUser.php @@ -375,6 +375,15 @@ class SpecialEmailUser extends UnlistedSpecialPage { $text .= $context->msg( 'emailuserfooter', $from->name, $to->name )->inContentLanguage()->text(); + if ( $config->get( 'EnableSpecialMute' ) ) { + $specialMutePage = SpecialPage::getTitleFor( 'Mute', $context->getUser()->getName() ); + $text .= "\n" . $context->msg( + 'specialmute-email-footer', + $specialMutePage->getCanonicalURL(), + $context->getUser()->getName() + )->inContentLanguage()->text(); + } + // Check and increment the rate limits if ( $context->getUser()->pingLimiter( 'emailuser' ) ) { throw new ThrottledError(); diff --git a/includes/specials/SpecialMute.php b/includes/specials/SpecialMute.php new file mode 100644 index 0000000000..4f34785115 --- /dev/null +++ b/includes/specials/SpecialMute.php @@ -0,0 +1,213 @@ +getConfig(); + $this->enableUserEmailBlacklist = $config->get( 'EnableUserEmailBlacklist' ); + $this->enableUserEmail = $config->get( 'EnableUserEmail' ); + + $this->centralIdLookup = CentralIdLookup::factory(); + + parent::__construct( 'Mute', '', false ); + } + + /** + * Entry point for special pages + * + * @param string $par + */ + public function execute( $par ) { + $this->requireLogin( 'specialmute-login-required' ); + $this->loadTarget( $par ); + + parent::execute( $par ); + + $out = $this->getOutput(); + $out->addModules( 'mediawiki.special.pageLanguage' ); + } + + /** + * @inheritDoc + */ + public function requiresUnblock() { + return false; + } + + /** + * @inheritDoc + */ + protected function getDisplayFormat() { + return 'ooui'; + } + + /** + * @inheritDoc + */ + public function onSuccess() { + $out = $this->getOutput(); + $out->addWikiMsg( 'specialmute-success' ); + } + + /** + * @param array $data + * @param HTMLForm|null $form + * @return bool + */ + public function onSubmit( array $data, HTMLForm $form = null ) { + if ( !empty( $data['MuteEmail'] ) ) { + $this->muteEmailsFromTarget(); + } else { + $this->unmuteEmailsFromTarget(); + } + + return true; + } + + /** + * @inheritDoc + */ + public function getDescription() { + return $this->msg( 'specialmute' )->text(); + } + + /** + * Un-mute emails from target + */ + private function unmuteEmailsFromTarget() { + $blacklist = $this->getBlacklist(); + + $key = array_search( $this->targetCentralId, $blacklist ); + if ( $key !== false ) { + unset( $blacklist[$key] ); + $blacklist = implode( "\n", $blacklist ); + + $user = $this->getUser(); + $user->setOption( 'email-blacklist', $blacklist ); + $user->saveSettings(); + } + } + + /** + * Mute emails from target + */ + private function muteEmailsFromTarget() { + // avoid duplicates just in case + if ( !$this->isTargetBlacklisted() ) { + $blacklist = $this->getBlacklist(); + + $blacklist[] = $this->targetCentralId; + $blacklist = implode( "\n", $blacklist ); + + $user = $this->getUser(); + $user->setOption( 'email-blacklist', $blacklist ); + $user->saveSettings(); + } + } + + /** + * @inheritDoc + */ + protected function alterForm( HTMLForm $form ) { + $form->setId( 'mw-specialmute-form' ); + $form->setHeaderText( $this->msg( 'specialmute-header', $this->target )->parse() ); + $form->setSubmitTextMsg( 'specialmute-submit' ); + $form->setSubmitID( 'save' ); + } + + /** + * @inheritDoc + */ + protected function getFormFields() { + if ( !$this->enableUserEmailBlacklist || !$this->enableUserEmail ) { + throw new ErrorPageError( 'specialmute', 'specialmute-error-email-blacklist-disabled' ); + } + + if ( !$this->getUser()->getEmailAuthenticationTimestamp() ) { + throw new ErrorPageError( 'specialmute', 'specialmute-error-email-preferences' ); + } + + $fields['MuteEmail'] = [ + 'type' => 'check', + 'label-message' => 'specialmute-label-mute-email', + 'default' => $this->isTargetBlacklisted(), + ]; + + return $fields; + } + + /** + * @param string $username + */ + private function loadTarget( $username ) { + $target = User::newFromName( $username ); + if ( !$target || !$target->getId() ) { + throw new ErrorPageError( 'specialmute', 'specialmute-error-invalid-user' ); + } else { + $this->target = $target; + $this->targetCentralId = $this->centralIdLookup->centralIdFromLocalUser( $target ); + } + } + + /** + * @return bool + */ + private function isTargetBlacklisted() { + $blacklist = $this->getBlacklist(); + return in_array( $this->targetCentralId, $blacklist ); + } + + /** + * @return array + */ + private function getBlacklist() { + $blacklist = $this->getUser()->getOption( 'email-blacklist' ); + if ( !$blacklist ) { + return []; + } + + return MultiUsernameFilter::splitIds( $blacklist ); + } +} diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php index fc54890563..1c87f7a395 100644 --- a/includes/specials/SpecialUserrights.php +++ b/includes/specials/SpecialUserrights.php @@ -405,8 +405,6 @@ class UserrightsPage extends SpecialPage { wfDebug( 'newGroups: ' . print_r( $newGroups, true ) . "\n" ); wfDebug( 'oldUGMs: ' . print_r( $oldUGMs, true ) . "\n" ); wfDebug( 'newUGMs: ' . print_r( $newUGMs, true ) . "\n" ); - // Deprecated in favor of UserGroupsChanged hook - Hooks::run( 'UserRights', [ &$user, $add, $remove ], '1.26' ); // Only add a log entry if something actually changed if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) { diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php index 5456ce7861..ec34db8611 100644 --- a/includes/specials/SpecialVersion.php +++ b/includes/specials/SpecialVersion.php @@ -219,23 +219,26 @@ class SpecialVersion extends SpecialPage { } /** - * Returns wiki text showing the third party software versions (apache, php, mysql). + * @since 1.34 * - * @return string + * @return array */ - public static function softwareInformation() { + public static function getSoftwareInformation() { $dbr = wfGetDB( DB_REPLICA ); // Put the software in an array of form 'name' => 'version'. All messages should // be loaded here, so feel free to use wfMessage in the 'name'. Raw HTML or // wikimarkup can be used. - $software = []; - $software['[https://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked(); + $software = [ + '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked() + ]; + if ( wfIsHHVM() ) { $software['[https://hhvm.com/ HHVM]'] = HHVM_VERSION . " (" . PHP_SAPI . ")"; } else { $software['[https://php.net/ PHP]'] = PHP_VERSION . " (" . PHP_SAPI . ")"; } + $software[$dbr->getSoftwareLink()] = $dbr->getServerInfo(); if ( defined( 'INTL_ICU_VERSION' ) ) { @@ -245,18 +248,27 @@ class SpecialVersion extends SpecialPage { // Allow a hook to add/remove items. Hooks::run( 'SoftwareInfo', [ &$software ] ); + return $software; + } + + /** + * Returns HTML showing the third party software versions (apache, php, mysql). + * + * @return string HTML table + */ + public static function softwareInformation() { $out = Xml::element( 'h2', [ 'id' => 'mw-version-software' ], wfMessage( 'version-software' )->text() ) . - Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] ) . - " - " . wfMessage( 'version-software-product' )->text() . " - " . wfMessage( 'version-software-version' )->text() . " - \n"; + Xml::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] ) . + " + " . wfMessage( 'version-software-product' )->text() . " + " . wfMessage( 'version-software-version' )->text() . " + \n"; - foreach ( $software as $name => $version ) { + foreach ( self::getSoftwareInformation() as $name => $version ) { $out .= " " . $name . " " . $version . " diff --git a/includes/specials/helpers/LoginHelper.php b/includes/specials/helpers/LoginHelper.php index 6c9bea598f..f66eccf7bc 100644 --- a/includes/specials/helpers/LoginHelper.php +++ b/includes/specials/helpers/LoginHelper.php @@ -25,6 +25,7 @@ class LoginHelper extends ContextSource { 'resetpass-no-info', 'confirmemail_needlogin', 'prefsnologintext2', + 'specialmute-login-required', ]; /** diff --git a/includes/user/User.php b/includes/user/User.php index 3a57c0b3eb..97d47023f7 100644 --- a/includes/user/User.php +++ b/includes/user/User.php @@ -111,95 +111,7 @@ class User implements IDBAccessObject, UserIdentity { ]; /** - * Array of Strings Core rights. - * Each of these should have a corresponding message of the form - * "right-$right". - * @showinitializer * @var string[] - */ - protected static $mCoreRights = [ - 'apihighlimits', - 'applychangetags', - 'autoconfirmed', - 'autocreateaccount', - 'autopatrol', - 'bigdelete', - 'block', - 'blockemail', - 'bot', - 'browsearchive', - 'changetags', - 'createaccount', - 'createpage', - 'createtalk', - 'delete', - 'deletechangetags', - 'deletedhistory', - 'deletedtext', - 'deletelogentry', - 'deleterevision', - 'edit', - 'editcontentmodel', - 'editinterface', - 'editprotected', - 'editmyoptions', - 'editmyprivateinfo', - 'editmyusercss', - 'editmyuserjson', - 'editmyuserjs', - 'editmywatchlist', - 'editsemiprotected', - 'editsitecss', - 'editsitejson', - 'editsitejs', - 'editusercss', - 'edituserjson', - 'edituserjs', - 'hideuser', - 'import', - 'importupload', - 'ipblock-exempt', - 'managechangetags', - 'markbotedits', - 'mergehistory', - 'minoredit', - 'move', - 'movefile', - 'move-categorypages', - 'move-rootuserpages', - 'move-subpages', - 'nominornewtalk', - 'noratelimit', - 'override-export-depth', - 'pagelang', - 'patrol', - 'patrolmarks', - 'protect', - 'purge', - 'read', - 'reupload', - 'reupload-own', - 'reupload-shared', - 'rollback', - 'sendemail', - 'siteadmin', - 'suppressionlog', - 'suppressredirect', - 'suppressrevision', - 'unblockself', - 'undelete', - 'unwatchedpages', - 'upload', - 'upload_by_url', - 'userrights', - 'userrights-interwiki', - 'viewmyprivateinfo', - 'viewmywatchlist', - 'viewsuppressed', - 'writeapi', - ]; - - /** * @var string[] Cached results of getAllRights() */ protected static $mAllRights = false; @@ -274,8 +186,6 @@ class User implements IDBAccessObject, UserIdentity { public $mBlockedby; /** @var string */ protected $mHash; - /** @var array */ - public $mRights; /** @var string */ protected $mBlockreason; /** @var array */ @@ -333,6 +243,24 @@ class User implements IDBAccessObject, UserIdentity { return (string)$this->getName(); } + public function __get( $name ) { + // A shortcut for $mRights deprecation phase + if ( $name === 'mRights' ) { + return $this->getRights(); + } + } + + public function __set( $name, $value ) { + // A shortcut for $mRights deprecation phase, only known legitimate use was for + // testing purposes, other uses seem bad in principle + if ( $name === 'mRights' ) { + MediaWikiServices::getInstance()->getPermissionManager()->overrideUserRightsForTesting( + $this, + is_null( $value ) ? [] : $value + ); + } + } + /** * Test if it's safe to load this User object. * @@ -1346,13 +1274,6 @@ class User implements IDBAccessObject, UserIdentity { * @return bool True if the user is logged in, false otherwise. */ private function loadFromSession() { - // Deprecated hook - $result = null; - Hooks::run( 'UserLoadFromSession', [ $this, &$result ], '1.27' ); - if ( $result !== null ) { - return $result; - } - // MediaWiki\Session\Session already did the necessary authentication of the user // returned here, so just use it if applicable. $session = $this->getRequest()->getSession(); @@ -1706,11 +1627,12 @@ class User implements IDBAccessObject, UserIdentity { * given source. May be "name", "id", "actor", "defaults", "session", or false for no reload. */ public function clearInstanceCache( $reloadFrom = false ) { + global $wgFullyInitialised; + $this->mNewtalk = -1; $this->mDatePreference = null; $this->mBlockedby = -1; # Unset $this->mHash = false; - $this->mRights = null; $this->mEffectiveGroups = null; $this->mImplicitGroups = null; $this->mGroupMemberships = null; @@ -1718,6 +1640,13 @@ class User implements IDBAccessObject, UserIdentity { $this->mOptionsLoaded = false; $this->mEditCount = null; + // Replacement of former `$this->mRights = null` line + if ( $wgFullyInitialised && $this->mFrom ) { + MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache( + $this + ); + } + if ( $reloadFrom ) { $this->mLoadedItems = []; $this->mFrom = $reloadFrom; @@ -2156,7 +2085,6 @@ class User implements IDBAccessObject, UserIdentity { * @param Title $title Title to check * @param bool $fromReplica Whether to check the replica DB instead of the master * @return bool - * @throws MWException * * @deprecated since 1.33, * use MediaWikiServices::getInstance()->getPermissionManager()->isBlockedFrom(..) @@ -3402,44 +3330,13 @@ class User implements IDBAccessObject, UserIdentity { /** * Get the permissions this user has. * @return string[] permission names + * + * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager() + * ->getUserPermissions(..) instead + * */ public function getRights() { - if ( is_null( $this->mRights ) ) { - $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() ); - Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] ); - - // Deny any rights denied by the user's session, unless this - // endpoint has no sessions. - if ( !defined( 'MW_NO_SESSION' ) ) { - $allowedRights = $this->getRequest()->getSession()->getAllowedUserRights(); - if ( $allowedRights !== null ) { - $this->mRights = array_intersect( $this->mRights, $allowedRights ); - } - } - - Hooks::run( 'UserGetRightsRemove', [ $this, &$this->mRights ] ); - // Force reindexation of rights when a hook has unset one of them - $this->mRights = array_values( array_unique( $this->mRights ) ); - - // If block disables login, we should also remove any - // extra rights blocked users might have, in case the - // blocked user has a pre-existing session (T129738). - // This is checked here for cases where people only call - // $user->isAllowed(). It is also checked in Title::checkUserBlock() - // to give a better error message in the common case. - $config = RequestContext::getMain()->getConfig(); - // @TODO Partial blocks should not prevent the user from logging in. - // see: https://phabricator.wikimedia.org/T208895 - if ( - $this->isLoggedIn() && - $config->get( 'BlockDisablesLogin' ) && - $this->getBlock() - ) { - $anon = new User; - $this->mRights = array_intersect( $this->mRights, $anon->getRights() ); - } - } - return $this->mRights; + return MediaWikiServices::getInstance()->getPermissionManager()->getUserPermissions( $this ); } /** @@ -3608,8 +3505,7 @@ class User implements IDBAccessObject, UserIdentity { // Refresh the groups caches, and clear the rights cache so it will be // refreshed on the next call to $this->getRights(). $this->getEffectiveGroups( true ); - $this->mRights = null; - + MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache( $this ); $this->invalidateCache(); return true; @@ -3640,8 +3536,7 @@ class User implements IDBAccessObject, UserIdentity { // Refresh the groups caches, and clear the rights cache so it will be // refreshed on the next call to $this->getRights(). $this->getEffectiveGroups( true ); - $this->mRights = null; - + MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache( $this ); $this->invalidateCache(); return true; @@ -3724,16 +3619,17 @@ class User implements IDBAccessObject, UserIdentity { /** * Internal mechanics of testing a permission + * + * @deprecated since 1.34, use MediaWikiServices::getInstance() + * ->getPermissionManager()->userHasRight(...) instead + * * @param string $action + * * @return bool */ public function isAllowed( $action = '' ) { - if ( $action === '' ) { - return true; // In the spirit of DWIM - } - // Use strict parameter to avoid matching numeric 0 accidentally inserted - // by misconfiguration: 0 == 'foo' - return in_array( $action, $this->getRights(), true ); + return MediaWikiServices::getInstance()->getPermissionManager() + ->userHasRight( $this, $action ); } /** @@ -4882,45 +4778,27 @@ class User implements IDBAccessObject, UserIdentity { /** * Get the permissions associated with a given list of groups * + * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager() + * ->getGroupPermissions() instead + * * @param array $groups Array of Strings List of internal group names * @return array Array of Strings List of permission key names for given groups combined */ public static function getGroupPermissions( $groups ) { - global $wgGroupPermissions, $wgRevokePermissions; - $rights = []; - // grant every granted permission first - foreach ( $groups as $group ) { - if ( isset( $wgGroupPermissions[$group] ) ) { - $rights = array_merge( $rights, - // array_filter removes empty items - array_keys( array_filter( $wgGroupPermissions[$group] ) ) ); - } - } - // now revoke the revoked permissions - foreach ( $groups as $group ) { - if ( isset( $wgRevokePermissions[$group] ) ) { - $rights = array_diff( $rights, - array_keys( array_filter( $wgRevokePermissions[$group] ) ) ); - } - } - return array_unique( $rights ); + return MediaWikiServices::getInstance()->getPermissionManager()->getGroupPermissions( $groups ); } /** * Get all the groups who have a given permission * + * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager() + * ->getGroupsWithPermission() instead + * * @param string $role Role to check * @return array Array of Strings List of internal group names with the given permission */ public static function getGroupsWithPermission( $role ) { - global $wgGroupPermissions; - $allowedGroups = []; - foreach ( array_keys( $wgGroupPermissions ) as $group ) { - if ( self::groupHasPermission( $group, $role ) ) { - $allowedGroups[] = $group; - } - } - return $allowedGroups; + return MediaWikiServices::getInstance()->getPermissionManager()->getGroupsWithPermission( $role ); } /** @@ -4930,15 +4808,17 @@ class User implements IDBAccessObject, UserIdentity { * User::isEveryoneAllowed() instead. That properly checks if it's revoked * from anyone. * + * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager() + * ->groupHasPermission(..) instead + * * @since 1.21 * @param string $group Group to check * @param string $role Role to check * @return bool */ public static function groupHasPermission( $group, $role ) { - global $wgGroupPermissions, $wgRevokePermissions; - return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role] - && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] ); + return MediaWikiServices::getInstance()->getPermissionManager() + ->groupHasPermission( $group, $role ); } /** @@ -4951,51 +4831,16 @@ class User implements IDBAccessObject, UserIdentity { * Specifically, session-based rights restrictions (such as OAuth or bot * passwords) are applied based on the current session. * - * @since 1.22 + * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager() + * ->isEveryoneAllowed() instead + * * @param string $right Right to check + * * @return bool + * @since 1.22 */ public static function isEveryoneAllowed( $right ) { - global $wgGroupPermissions, $wgRevokePermissions; - static $cache = []; - - // Use the cached results, except in unit tests which rely on - // being able change the permission mid-request - if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) { - return $cache[$right]; - } - - if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) { - $cache[$right] = false; - return false; - } - - // If it's revoked anywhere, then everyone doesn't have it - foreach ( $wgRevokePermissions as $rights ) { - if ( isset( $rights[$right] ) && $rights[$right] ) { - $cache[$right] = false; - return false; - } - } - - // Remove any rights that aren't allowed to the global-session user, - // unless there are no sessions for this endpoint. - if ( !defined( 'MW_NO_SESSION' ) ) { - $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights(); - if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) { - $cache[$right] = false; - return false; - } - } - - // Allow extensions to say false - if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) { - $cache[$right] = false; - return false; - } - - $cache[$right] = true; - return true; + return MediaWikiServices::getInstance()->getPermissionManager()->isEveryoneAllowed( $right ); } /** @@ -5014,19 +4859,14 @@ class User implements IDBAccessObject, UserIdentity { /** * Get a list of all available permissions. + * + * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager() + * ->getAllPermissions() instead + * * @return string[] Array of permission names */ public static function getAllRights() { - if ( self::$mAllRights === false ) { - global $wgAvailableRights; - if ( count( $wgAvailableRights ) ) { - self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) ); - } else { - self::$mAllRights = self::$mCoreRights; - } - Hooks::run( 'UserGetAllRights', [ &self::$mAllRights ] ); - } - return self::$mAllRights; + return MediaWikiServices::getInstance()->getPermissionManager()->getAllPermissions(); } /** diff --git a/includes/widget/search/BasicSearchResultSetWidget.php b/includes/widget/search/BasicSearchResultSetWidget.php index 1a885b0136..0e102878bc 100644 --- a/includes/widget/search/BasicSearchResultSetWidget.php +++ b/includes/widget/search/BasicSearchResultSetWidget.php @@ -117,12 +117,9 @@ class BasicSearchResultSetWidget { * @return string HTML */ protected function renderResultSet( SearchResultSet $resultSet, $offset ) { - $terms = MediaWikiServices::getInstance()->getContentLanguage()-> - convertForSearchResult( $resultSet->termMatches() ); - $hits = []; foreach ( $resultSet as $result ) { - $hits[] = $this->resultWidget->render( $result, $terms, $offset++ ); + $hits[] = $this->resultWidget->render( $result, $offset++ ); } return "
    " . implode( '', $hits ) . "
"; diff --git a/includes/widget/search/FullSearchResultWidget.php b/includes/widget/search/FullSearchResultWidget.php index f648535c01..7212dc0498 100644 --- a/includes/widget/search/FullSearchResultWidget.php +++ b/includes/widget/search/FullSearchResultWidget.php @@ -31,11 +31,10 @@ class FullSearchResultWidget implements SearchResultWidget { /** * @param SearchResult $result The result to render - * @param string[] $terms Terms to be highlighted (@see SearchResult::getTextSnippet) * @param int $position The result position, including offset * @return string HTML */ - public function render( SearchResult $result, $terms, $position ) { + public function render( SearchResult $result, $position ) { // If the page doesn't *exist*... our search index is out of date. // The least confusing at this point is to drop the result. // You may get less results, but... on well. :P @@ -43,7 +42,7 @@ class FullSearchResultWidget implements SearchResultWidget { return ''; } - $link = $this->generateMainLinkHtml( $result, $terms, $position ); + $link = $this->generateMainLinkHtml( $result, $position ); // If page content is not readable, just return ths title. // This is not quite safe, but better than showing excerpts from // non-readable pages. Note that hiding the entry entirely would @@ -63,7 +62,7 @@ class FullSearchResultWidget implements SearchResultWidget { $this->specialPage->getUser() ); list( $file, $desc, $thumb ) = $this->generateFileHtml( $result ); - $snippet = $result->getTextSnippet( $terms ); + $snippet = $result->getTextSnippet(); if ( $snippet ) { $extract = "
$snippet
"; } else { @@ -80,6 +79,9 @@ class FullSearchResultWidget implements SearchResultWidget { $html = null; $score = ''; $related = ''; + // TODO: remove this instanceof and always pass [], let implementors do the cast if + // they want to be SearchDatabase specific + $terms = $result instanceof \SqlSearchResult ? $result->getTermMatches() : []; if ( !Hooks::run( 'ShowSearchHit', [ $this->specialPage, $result, $terms, &$link, &$redirect, &$section, &$extract, @@ -121,11 +123,10 @@ class FullSearchResultWidget implements SearchResultWidget { * title with highlighted words). * * @param SearchResult $result - * @param string[] $terms * @param int $position * @return string HTML */ - protected function generateMainLinkHtml( SearchResult $result, $terms, $position ) { + protected function generateMainLinkHtml( SearchResult $result, $position ) { $snippet = $result->getTitleSnippet(); if ( $snippet === '' ) { $snippet = null; @@ -139,7 +140,9 @@ class FullSearchResultWidget implements SearchResultWidget { $attributes = [ 'data-serp-pos' => $position ]; Hooks::run( 'ShowSearchHitTitle', - [ &$title, &$snippet, $result, $terms, $this->specialPage, &$query, &$attributes ] ); + [ &$title, &$snippet, $result, + $result instanceof \SqlSearchResult ? $result->getTermMatches() : [], + $this->specialPage, &$query, &$attributes ] ); $link = $this->linkRenderer->makeLink( $title, diff --git a/includes/widget/search/InterwikiSearchResultWidget.php b/includes/widget/search/InterwikiSearchResultWidget.php index 745bc12c61..3f758db5bb 100644 --- a/includes/widget/search/InterwikiSearchResultWidget.php +++ b/includes/widget/search/InterwikiSearchResultWidget.php @@ -24,14 +24,13 @@ class InterwikiSearchResultWidget implements SearchResultWidget { /** * @param SearchResult $result The result to render - * @param string[] $terms Terms to be highlighted (@see SearchResult::getTextSnippet) * @param int $position The result position, including offset * @return string HTML */ - public function render( SearchResult $result, $terms, $position ) { + public function render( SearchResult $result, $position ) { $title = $result->getTitle(); $titleSnippet = $result->getTitleSnippet(); - $snippet = $result->getTextSnippet( $terms ); + $snippet = $result->getTextSnippet(); if ( $titleSnippet ) { $titleSnippet = new HtmlArmor( $titleSnippet ); diff --git a/includes/widget/search/SearchResultWidget.php b/includes/widget/search/SearchResultWidget.php index 4f0a271e5b..e001395541 100644 --- a/includes/widget/search/SearchResultWidget.php +++ b/includes/widget/search/SearchResultWidget.php @@ -10,9 +10,8 @@ use SearchResult; interface SearchResultWidget { /** * @param SearchResult $result The result to render - * @param string[] $terms Terms to be highlighted (@see SearchResult::getTextSnippet) * @param int $position The zero indexed result position, including offset * @return string HTML */ - public function render( SearchResult $result, $terms, $position ); + public function render( SearchResult $result, $position ); } diff --git a/includes/widget/search/SimpleSearchResultWidget.php b/includes/widget/search/SimpleSearchResultWidget.php index 86a04b1839..fe8b4d5092 100644 --- a/includes/widget/search/SimpleSearchResultWidget.php +++ b/includes/widget/search/SimpleSearchResultWidget.php @@ -26,11 +26,10 @@ class SimpleSearchResultWidget implements SearchResultWidget { /** * @param SearchResult $result The result to render - * @param string[] $terms Terms to be highlighted (@see SearchResult::getTextSnippet) * @param int $position The result position, including offset * @return string HTML */ - public function render( SearchResult $result, $terms, $position ) { + public function render( SearchResult $result, $position ) { $title = $result->getTitle(); $titleSnippet = $result->getTitleSnippet(); if ( $titleSnippet ) { diff --git a/languages/Language.php b/languages/Language.php index fd8aedff2b..bb256c9c99 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -4863,6 +4863,7 @@ class Language { public function viewPrevNext( Title $title, $offset, $limit, array $query = [], $atend = false ) { + wfDeprecated( __METHOD__, '1.34' ); // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip? # Make 'previous' link diff --git a/languages/i18n/ar.json b/languages/i18n/ar.json index 126f07cd55..2d2d3f1b17 100644 --- a/languages/i18n/ar.json +++ b/languages/i18n/ar.json @@ -242,7 +242,7 @@ "history": "تاريخ الصفحة", "history_short": "التاريخ", "history_small": "تاريخ", - "updatedmarker": "عُدلت منذ زيارتي الأخيرة", + "updatedmarker": "عُدِّلت منذ زيارتك الأخيرة", "printableversion": "نسخة للطباعة", "permalink": "وصلة دائمة", "print": "اطبع", @@ -3915,6 +3915,16 @@ "restrictionsfield-help": "عنوان أيبي أو نطاق CIDR واحد لكل سطر. لتفعيل كل شيء، استخدم:\n
0.0.0.0/0\n::/0
", "edit-error-short": "خطأ: $1", "edit-error-long": "الأخطاء:\n\n$1", + "specialmute": "كتم الصوت", + "specialmute-success": "تم تحديث تفضيلات كتم الصوت بنجاح، شاهد كل المستخدمين الصامتين في [[Special:Preferences]].", + "specialmute-submit": "تأكيد", + "specialmute-label-mute-email": "كتم رسائل البريد الإلكتروني من هذا المستخدم", + "specialmute-header": "يُرجَى تحديد تفضيلات كتم الصوت لـ{{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "لا يمكن العثور على اسم المستخدم المطلوب.", + "specialmute-error-email-blacklist-disabled": "لم يتم تمكين كتم المستخدمين من إرسال رسائل البريد الإلكتروني إليك.", + "specialmute-error-email-preferences": "يجب تأكيد عنوان بريدك الإلكتروني قبل أن تتمكن من كتم صوت المستخدم، يمكنك القيام بذلك من [[Special:Preferences]].", + "specialmute-email-footer": "لإدارة تفضيلات البريد الإلكتروني لـ{{BIDI:$2}}؛ تُرجَى زيارة <$1>", + "specialmute-login-required": "يُرجَى تسجيل الدخول لتغيير تفضيلات الصمت الخاصة بك.", "revid": "المراجعة $1", "pageid": "معرف الصفحة $1", "interfaceadmin-info": "$1\n\nتم فصل صلاحيات تحرير ملفات CSS/JS/JSON على مستوى الموقع مؤخرً من صلاحية editinterface، إذا لم تفهم سبب حصولك على هذا الخطأ، فراجع [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/as.json b/languages/i18n/as.json index db8b3ef2af..0a3d39e962 100644 --- a/languages/i18n/as.json +++ b/languages/i18n/as.json @@ -1618,6 +1618,7 @@ "pager-older-n": "{{PLURAL:$1|পুৰণতৰ ১|পুৰণতৰ $1}}", "suppress": "অমনোযোগ", "querypage-disabled": "কাৰ্য্যগত কাৰণত এই বিশেষ পৃষ্ঠাটো নিষ্ক্ৰিয় কৰা হৈছে।", + "apihelp-no-such-module": "\"$1\" মডিউল পোৱা নগ'ল।", "apisandbox-results": "ফলাফল", "apisandbox-continue": "অব্যাহত ৰাখক", "booksources": "গ্ৰন্থৰ উৎস সমূহ", diff --git a/languages/i18n/ast.json b/languages/i18n/ast.json index d8319f063e..b3707a6059 100644 --- a/languages/i18n/ast.json +++ b/languages/i18n/ast.json @@ -180,7 +180,7 @@ "history": "Historial de la páxina", "history_short": "Historial", "history_small": "historial", - "updatedmarker": "anovada dende la mio visita cabera", + "updatedmarker": "anovada dende la to visita cabera", "printableversion": "Versión pa imprentar", "permalink": "Enllaz permanente", "print": "Imprentar", diff --git a/languages/i18n/az.json b/languages/i18n/az.json index e6122fcd25..21151e5e6e 100644 --- a/languages/i18n/az.json +++ b/languages/i18n/az.json @@ -372,7 +372,7 @@ "perfcached": "Aşağıdakı məlumatlar keş yaddaşdan götürülmüşdür və bu səbəbdən aktual olmaya bilər. A maximum of {{PLURAL:$1|one result is|$1 results are}} available in the cache.", "perfcachedts": "Aşağıdakı məlumatlar keş yaddaşdan götürülmüşdür və sonuncu dəfə $1 tarixində yenilənmişdir. A maximum of {{PLURAL:$4|one result is|$4 results are}} available in the cache.", "querypage-no-updates": "Bu an üçün güncəlləmələr sıradan çıxdı. Buradakı məlumat dərhal yenilənməyəcək.", - "viewsource": "Mənbə göstər", + "viewsource": "Kodu göstər", "viewsource-title": "$1 üçün mənbəyə bax", "actionthrottled": "Sürət məhdudiyyəti", "actionthrottledtext": "Spamla mübarizə məqsədilə qısa vaxt kəsiyi ərzində bu hərəkətlərin təkrarlanma sayı məhdudlaşdırılıb və siz qoyulan həddi aşmısınız.\nLütfən bir neçə dəqiqə sonra yenidən yoxlayın.", @@ -1512,7 +1512,7 @@ "nmembers": "$1 {{PLURAL:$1|üzv|üzv}}", "nmemberschanged": "$1 → $2 {{PLURAL:$2|üzv|üzvlər}}", "nrevisions": "$1 dəyişiklik", - "nimagelinks": "$1 səhifədə istifadə olunmur", + "nimagelinks": "$1 səhifədə istifadə olunur", "ntransclusions": "$1 səhifədə istifadə olunur", "specialpage-empty": "Bu səhifə boşdur.", "lonelypages": "Yetim səhifələr", @@ -2480,7 +2480,7 @@ "feedback-subject": "Mövzu:", "feedback-submit": "Təsdiq et", "feedback-thanks-title": "Təşəkkür!", - "searchsuggest-search": "{{grammar:prepositional|{{SITENAME}}}} axtar", + "searchsuggest-search": "{{grammar:prepositional|{{SITENAME}}}} saytında axtar", "api-error-unknown-warning": "Naməlum xəbərdarlıq: \"$1\".", "api-error-unknownerror": "Naməlum xəta: \"$1\".", "duration-seconds": "$1 {{PLURAL:$1|saniyə|saniyə}}", diff --git a/languages/i18n/ban.json b/languages/i18n/ban.json index 91d0395b8a..1bc2f18915 100644 --- a/languages/i18n/ban.json +++ b/languages/i18n/ban.json @@ -222,7 +222,7 @@ "viewsourcelink": "cingak wit", "editsectionhint": "Uah pahan: $1", "toc": "Daging", - "showtoc": "edengang", + "showtoc": "sinahang", "hidetoc": "engkebang", "collapsible-expand": "buka", "confirmable-confirm": "{{GENDER:$1|Jero}} yakin?", @@ -277,7 +277,7 @@ "createacct-benefit-heading": "{{SITENAME}} kakaryanin olih anak sakadi jero.", "createacct-benefit-body1": "{{PLURAL:$1|uahan}}", "createacct-benefit-body2": "{{PLURAL:$1|kaca}}", - "createacct-benefit-body3": "{{PLURAL:$1|sang anuut}} anyar", + "createacct-benefit-body3": "{{PLURAL:$1|sang anuut}} sané mangkin", "mailmypassword": "nyumu ngaryanin kruna sandi", "loginlanguagelabel": "Basa: $1", "pt-login": "Manjing log", @@ -313,7 +313,7 @@ "savearticle-start": "Raksa kaca...", "publishpage-start": "Terbitang kaca…", "preview": "tayangan sadurungnyane", - "showpreview": "cingak sane lintang", + "showpreview": "Sinahang preview", "showdiff": "Cingak uahan", "anoneditwarning": "Pingetan: Ida dané nénten kacatet ngranjing. Alamat IP ida dané jagi kacatet ring sejarah (indik sané dumunan) ring lembar puniki. Yening ida dane [$1 log in] utawi [$2 create an account], your edits will be attributed to your username, along with other benefits.", "loginreqlink": "manjing log", @@ -359,6 +359,7 @@ "history-feed-description": "Babad uahan kaca puniki ring wiki", "history-feed-item-nocomment": "$1 ring $2", "rev-delundel": "gentos pangatonan", + "rev-showdeleted": "sinahang", "revdelete-hide-comment": "Uah ringkesan", "revdel-restore": "gentos pangatonan", "pagehist": "Babad kaca", @@ -376,7 +377,7 @@ "prev-page": "kaca sadurungnyané", "prevn-title": "$1 {{PLURAL:$1|asil}} sadurunge", "nextn-title": "$1 {{PLURAL:$1|asil}} selanturnyane", - "shown-title": "ngantenang $1{{PLURAL:$1|asil}} sabilang lembar", + "shown-title": "Sinahang $1 {{PLURAL:$1|asil}} per kaca", "viewprevnext": "Cingak ($1 {{int:pipe-separator}}$2)($3)", "searchmenu-exists": "wenten lembar sane mamurda \"[[:$1]]\" ring wiki puniki. {{PLURAL:$2|0=| cingakin taler asil rerehan lianan sane kapolihang}}", "searchmenu-new": " ngawi lembar \"[[:$1]] ring wiki puniki ! {{{{PLURAL:$2|}}| 0 = | cingak teler lembar sane kapolihang ring pangreregan | cingak taler asil pangrerehan sane kapolihang}}", @@ -384,7 +385,7 @@ "searchprofile-images": "multimedia", "searchprofile-everything": "Samian", "searchprofile-advanced": "lanturane", - "searchprofile-articles-tooltip": "ngarereh ring $1", + "searchprofile-articles-tooltip": "Rereh ring $1", "searchprofile-images-tooltip": "Rereh berkas", "searchprofile-everything-tooltip": "pangrereh ring samian isi (taler lembar wecana)", "searchprofile-advanced-tooltip": "pangrereh ring genah pesengan sane kasinahang", @@ -426,14 +427,17 @@ "action-editsemiprotected": "uah kaca sané kasaibin \"{{int:protect-level-autoconfirmed}}\"", "nchanges": "$1{{PLURAL:$1|panguwahan|uwah-uwahan}}", "enhancedrc-history": "babad", - "recentchanges": "Uahan anyar", - "recentchanges-legend": "pilihan panguwahan sane anyar", + "recentchanges": "Uahan sané mangkin", + "recentchanges-legend": "Opsi uahan sané mangkin", + "recentchanges-summary": "Track uahan sané mangkin ring wikiné indik kaca puniki.", "recentchanges-feed-description": "molihang pagentosan anyar ring wiki ring \"umpan\" puniki", "recentchanges-label-newpage": "Uahan puniki makarya kaca anyar", "recentchanges-label-minor": "Punika uahan alit", "recentchanges-label-bot": "penguwahan puniki kalaksanayang antuk bot", "recentchanges-label-unpatrolled": "Uahan puniki durung kapatroli", "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (taler cingak [[Special:NewPages|bacakan kaca anyar]])", + "recentchanges-submit": "Sinahang", + "rcfilters-activefilters-show": "Sinahang", "rcfilters-savedqueries-remove": "Usap", "rcfilters-filter-minor-label": "Uahan alit", "rcfilters-filter-major-label": "Uahan tan alit", @@ -441,26 +445,28 @@ "rcnotefrom": "Ring beten puniki inggih punika {{PLURAL:$5|panguwahan}} saking $3, $4 (kaedengang ngantos $1 panguwahan).", "rclistfrom": "edengang penguwahan sane anyar wit saking $3 $2", "rcshowhideminor": "$1 uahan alit", - "rcshowhideminor-show": "Edengang", + "rcshowhideminor-show": "Sinahang", "rcshowhideminor-hide": "Engkebang", "rcshowhidebots": "$1 bot", - "rcshowhidebots-show": "Edengang", + "rcshowhidebots-show": "Sinahang", "rcshowhidebots-hide": "Engkebang", "rcshowhideliu": "$1 sang anganggé madaptar", - "rcshowhideliu-show": "Edengang", + "rcshowhideliu-show": "Sinahang", "rcshowhideliu-hide": "engkebang", "rcshowhideanons": "$1 sang anganggé tan kauningin", - "rcshowhideanons-show": "Edengang", + "rcshowhideanons-show": "Sinahang", "rcshowhideanons-hide": "Engkebang", - "rcshowhidepatr": "$1 suntingan sane kapatroli", + "rcshowhidepatr": "$1 uahan sané kapatroli", + "rcshowhidepatr-show": "Sinahang", "rcshowhidemine": "$1 uahan titiang", - "rcshowhidemine-show": "Edengang", + "rcshowhidemine-show": "Sinahang", "rcshowhidemine-hide": "Engkebang", + "rcshowhidecategorization-show": "Sinahang", "rclinks": "Edengang untat $1 gentosan anyar $2 dina kaping untat", "diff": "bina", "hist": "bbd", "hide": "engkebang", - "show": "edengang", + "show": "Sinahang", "minoreditletter": "a", "newpageletter": "A", "boteditletter": "b", @@ -468,17 +474,18 @@ "rc-enhanced-expand": "edengang rerincian", "rc-enhanced-hide": "engkebang rerincian", "rc-old-title": "witnyané kakaryanin pinaka \"$1\"", - "recentchangeslinked": "pangentos sane wenten paiketane", - "recentchangeslinked-toolbox": "pangentos sane wenten paiketane", - "recentchangeslinked-title": "panguwahan sane mapaiketan ring $1", + "recentchangeslinked": "Uahan mapaiketan", + "recentchangeslinked-toolbox": "Uahan mapaiketan", + "recentchangeslinked-title": "Uahan sané mapaiketan $1", "recentchangeslinked-summary": "lembar kautamayang puniki ngicenin kepahan penguwahan kaping untat ring lembar-lembar sana mapaiket. Lembar sane [[Special:Watchlist|ida dane iwasin]] mapinget antuk sesuratan tebel", "recentchangeslinked-page": "Peséngan kaca:", - "recentchangeslinked-to": "edengang panguwahan sakin lembar-lembar sane mapaiket antuk lembar-lembar sane kaedengang", - "upload": "ngunggahang berkas", - "uploadlogpage": "Log pangunggahan", + "recentchangeslinked-to": "Sinahang uahan saking kaca-kaca sané linked kaca puniki", + "upload": "Unggahang berkas", + "uploadlogpage": "Log unggahan", "filedesc": "Ringkesan", "savefile": "Raksa berkas", "upload-dialog-button-save": "Raksa", + "backend-fail-delete": "Tan prasida ngusapin berkas \"$1\".", "license": "kepahan lugra", "license-header": "kepahan lugra", "listfiles-delete": "usap", @@ -496,7 +503,7 @@ "filehist-user": "Sang anganggé", "filehist-dimensions": "ukuran", "filehist-comment": "tureksa", - "imagelinks": "penganggen berkas", + "imagelinks": "Panganggén berkas", "linkstoimage": "nyarengin {{PLURAL:$1|pranala|$1pranala}} ring pupulan puniki", "nolinkstoimage": "Nénten wénten kaca sané nganggén berkas puniki.", "sharedupload-desc-here": "pupulan puniki mawit saking $1 lan minab kaanggen olih proyek-proyek sane lianan. Deskripsi saking [$2 lebar deskripsinyane] kaarahin ring ungkur puniki", @@ -504,15 +511,18 @@ "upload-disallowed-here": "Jero nénten dados numpuk berkas puniki.", "filedelete": "Usap $1", "filedelete-submit": "Usap", + "filedelete-success": "$1 sampun kausapin.", "filedelete-maintenance-title": "Nénten prasida ngusapin berkas", "randompage": "Kaca punapi kémanten", "statistics": "Statistik", "statistics-articles": "Kaca daging", "brokenredirects-edit": "uah", "brokenredirects-delete": "usap", + "withoutinterwiki-submit": "Sinahang", "nbytes": "$1{{PLURAL:$1|bita}}", "nmembers": "$1 {{PLURAL:$1|krama}}", "prefixindex": "Makasami kaca sané mapangater", + "prefixindex-submit": "Sinahang", "protectedpages": "Kaca sané kasaibin", "protectedpages-page": "Kaca", "protectedpages-performer": "Sang anganggé sané nyaibin", @@ -521,6 +531,7 @@ "usereditcount": "$1 {{PLURAL:$1|uahan}}", "usercreated": "{{GENDER:$3|kakaryanin}} ring $1 galah $2", "newpages": "Kaca anyar", + "newpages-submit": "Sinahang", "move": "Gingsirang", "pager-newer-n": "{{PLURAL:$1|1 lewih anyar|$1 lewih anyar}}", "pager-older-n": "{{PLURAL:$1|1 lewih suwe|$1 lewih anyar}}", @@ -528,13 +539,16 @@ "booksources-search-legend": "Rereh wit buku", "booksources-search": "Rereh", "log": "Log", + "logeventslist-submit": "Sinahang", "all-logs-page": "Makasami log publik", "allpages": "Makasami kaca", "allarticles": "Makasami kaca", "allpagessubmit": "lanturang", "categories": "Golongan", + "categories-submit": "Sinahang", "deletedcontributions": "Pituut sang anganggé sané kausapin", "linksearch-line": "$1 masambung saking $2", + "listusers-submit": "Sinahang", "listgrouprights-members": "kepahan krama", "emailuser": "email sane nganggo niki", "watchlist": "kepahan peninjoan", @@ -543,13 +557,15 @@ "watch": "cingak", "unwatch": "tan sida maninjo", "watchlist-details": "{{PLURAL:$1|$1 lembar}} ring paninjoan ida dane, nenten sareng lembar wacana.", - "wlshowlast": "Cingak $1 jam $2 rahina sané lintang", + "wlshowlast": "Sinahang $1 jam $2 rahina sané lintang", + "watchlist-submit": "Sinahang", "wlshowhideminor": "uahan alit", "watchlist-options": "milih kepahan peninjo", "enotif_subject_deleted": "Kaca {{SITENAME}} $1 sampun {{GENDER:$2|kausap}} $2", "enotif_body_intro_deleted": "Kaca{{SITENAME}} $1 sampun {{GENDER:$2|kausapin}} ring $PAGEEDITDATE olih $2, cingak $3.", "deletepage": "Usap kaca", "delete-confirm": "Usap \"$1\"", + "historyaction-submit": "Sinahang uahan", "actioncomplete": "pelaksanan sampun wusan", "actionfailed": "pelaksana luput", "dellogpage": "log pangapus", @@ -580,7 +596,7 @@ "sp-contributions-newbies": "Cingak pituut wantah saking akun anyar", "sp-contributions-blocklog": "log pemblokiran", "sp-contributions-deleted": "pituut {{GENDER:$1|sang anganggé}} sané kausapin", - "sp-contributions-uploads": "unggahang", + "sp-contributions-uploads": "unggahan", "sp-contributions-logs": "log", "sp-contributions-talk": "pabligbagan", "sp-contributions-search": "Rereh pituut", @@ -606,6 +622,7 @@ "whatlinkshere-filters": "Panyaring", "ipboptions": "2 jam:2 hours,1 dina:1 day,3 dina:3 days,1 minggu:1 week,2 minggu:2 weeks,1 sasih:1 month,3 sasih:3 months,6 sasih:6 months,1 taun:1 year,tanpa wates:infinite", "ipb-pages-label": "Kaca", + "block-prevent-edit": "Nguahin", "ipblocklist": "ngempetin sane nganggo", "blocklist-nousertalk": "tan prasida nguahin kaca pabligbagan praragan", "blocklist-editing-page": "kaca", @@ -632,7 +649,7 @@ "tooltip-pt-watchlist": "kepahan-kepahan lembar sane katinjo titiang", "tooltip-pt-mycontris": "Bacakan pituut {{GENDER:|jero}}", "tooltip-pt-login": "Jero kaaptiang mangda manjing log; yadiastun nénten wajib", - "tooltip-pt-logout": "medal saking Log", + "tooltip-pt-logout": "Medal log", "tooltip-pt-createaccount": "Jero kaaptiang mangda makarya akun miwah manjing log; yadiastun nénten wajib", "tooltip-ca-talk": "Pabligbagan indik kaca daging", "tooltip-ca-edit": "Uah kaca puniki", @@ -653,11 +670,11 @@ "tooltip-n-mainpage-description": "Cingak kaca utama", "tooltip-n-portal": "Indik proyék, sané prasida kalaksanayang, genah ngrereh wantuan", "tooltip-n-currentevents": "molihang warta indik kawentenan kawentenan sane pinih anyar", - "tooltip-n-recentchanges": "Bacakan uahan anyar ring wiki", + "tooltip-n-recentchanges": "Bacakan uahan sané mangkin ring wiki", "tooltip-n-randompage": "Cihnayang kaca napi kémanten", "tooltip-n-help": "Genah ngrereh wantuan", "tooltip-t-whatlinkshere": "Bacakan makasami kaca ring wiki sané nuju iriki", - "tooltip-t-recentchangeslinked": "Pagentosan anyar lembar sane maduwe pranala nuju lembar puniki", + "tooltip-t-recentchangeslinked": "Uahan sané mangkin saking kaca-kaca sané linked ring kaca puniki", "tooltip-feed-atom": "\"atom feed\" anggen lembar puniki", "tooltip-t-contributions": "Bacakan pituut olih {{GENDER:$1|sang anganggé puniki}}", "tooltip-t-emailuser": "Ngirim surel majeng ring {{GENDER:$1|penganggo puniki}}", @@ -676,7 +693,7 @@ "tooltip-minoredit": "pingetin puniki dados panguwahan kidik", "tooltip-save": "Raksa uahan jero", "tooltip-preview": "Pagentosan sane dumun duwen ida dane, mangda anggen niki sadurung jagi nyimpen!", - "tooltip-diff": "Cingak uahan sané karyanin jero ring suratannyané", + "tooltip-diff": "Sinahang uahan sané karyanin jero ring sesuratannyané", "tooltip-compareselectedversions": "cingak binane makekalih kepahan lembar sane kasudi", "tooltip-watch": "imbuhin lembar niki ring daftar paninjoan ida dane", "tooltip-rollback": "\"nguliang\" muwungan jagi ngabecikang ring lembar puniki nuju haturan sane untat ngangge apisan klik", @@ -733,12 +750,15 @@ "tags-active-no": "Nénten", "tags-edit": "uah", "tags-delete": "usap", + "tags-delete-title": "Usap tag", "compare-page2": "Kaca 2", "logentry-delete-delete": "$1 {{GENDER:$2|ngusapin}} kaca $3", "logentry-move-move": "$1 {{GENDER:$2|ngingsirang}} kaca $3 ring $4", "logentry-newusers-create": "Akun sang anganggé $1 {{GENDER:$2|kakaryanin}}", "logentry-newusers-autocreate": "Akun sang anganggé $1 {{GENDER:$2|kakaryanin}} otomatis", "logentry-protect-protect": "$1 {{GENDER:$2|nyaibin}} $3 $4", + "logentry-upload-upload": "$1 {{GENDER:$2|ngunggahang}} $3", + "logentry-upload-overwrite": "$1 {{GENDER:$2|ngunggahang}} vèrsi anyar saking $3", "searchsuggest-search": "Rereh ring {{SITENAME}}", "duration-days": "$1 {{PLURAL:$1|rahina}}", "pagelanguage": "Uah basa ring kaca", diff --git a/languages/i18n/bcc.json b/languages/i18n/bcc.json index 4b5ef43f84..d47aaec8c7 100644 --- a/languages/i18n/bcc.json +++ b/languages/i18n/bcc.json @@ -171,7 +171,7 @@ "searcharticle": "برا", "history": "دیمی تاریخ", "history_short": "دپتر", - "history_small": "تاریخچگ", + "history_small": "وھدگ", "updatedmarker": "په روچ بیتگین چه منی اهری اهری چارگ", "printableversion": "چاپی بھر", "permalink": "دایمی لینک", @@ -787,7 +787,7 @@ "difference-multipage": "(پرک مان تاک ان)", "lineno": "خط$1:", "compareselectedversions": "مقایسه انتخاب بوتگین نسخه یان", - "showhideselectedversions": "نمایش/پنهان کتن نسخ انتخابی", + "showhideselectedversions": "سۏج/جوان کنگ اے ورژنئی", "editundo": "خنثی کتن", "diff-empty": "(بئ پرک)", "diff-multi-sameuser": "({{PLURAL:$1|یک میانجیگین نسخگ|$1 میانجیگین نسخگ}} گون همجندیء کاربر که پیش دارگ نه بوتگ انت)", @@ -1097,7 +1097,7 @@ "action-editmyprivateinfo": "وتی پرایویت اینفارمیشنء ادیت بکن", "nchanges": "$1 {{PLURAL:$1|تغییر|تغییرات}}", "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|چه آهریگین چارگ}}", - "enhancedrc-history": "تاریخچگ", + "enhancedrc-history": "وھدگ", "recentchanges": "نوکین تغییرات", "recentchanges-legend": "گزینه ی نوکین تغییرات", "recentchanges-summary": "رندگر نوکترین تغییرات ته ویکی تی ای صفحه.", @@ -1144,8 +1144,8 @@ "rc-enhanced-hide": "پناه کتن جزییات", "rc-old-title": "اڈ بیتگ گون «$1»", "recentchangeslinked": "مربوطین تغییرات", - "recentchangeslinked-feed": "مربوطین تغییرات", - "recentchangeslinked-toolbox": "مربوطین تغییرات", + "recentchangeslinked-feed": "امبندݔں ٹگلاں", + "recentchangeslinked-toolbox": "امبندݔں ٹگلاں", "recentchangeslinked-title": "تغییراتی مربوط په \"$1\"", "recentchangeslinked-summary": "شی یک لیستی چه تغییراتی هستنت که نوکی اعمال بوتگنت په صفحاتی که چه یک صفحه خاصی لینک بوته( یا په اعضای یک خاصین دسته).\nصفحات ته [[Special:Watchlist| شمی لیست چارگ]] '''' پررنگنت''''", "recentchangeslinked-page": "تاکدیمِ نام:", @@ -1334,7 +1334,7 @@ "filehist-datetime": "تاریح/زمان", "filehist-thumb": "بند انگشت", "filehist-thumbtext": "بندانگشتی از نسخهٔ مورخ $1", - "filehist-nothumb": "فاقد بندانگشتی", + "filehist-nothumb": "بندلنکُتکی نے", "filehist-user": "کاربر", "filehist-dimensions": "جنبه یان", "filehist-filesize": "اندازه فایل", @@ -1589,7 +1589,7 @@ "watchnologin": "وارد نه بی تگیت", "addedwatchtext": "صفحه \"[[:$1]]\" په شمی [[Special:Watchlist|watchlist]] هور بیت.\nدیمگی تغییرات په ای صفحه و آیاء صفحه گپ ادان لیست بنت، و صفحه پررنگ جاه کیت ته [[Special:RecentChanges|لیست نوکیت تغییرات]] په راحتر کتن شی که آی زورگ بیت.", "removedwatchtext": "صفحه\"[[:$1]]\" چه [[Special:Watchlist|شمی لیست چارگ]]. دربیت.", - "watch": "به چار", + "watch": "چار", "watchthispage": "ای تاکدیما بگیند", "unwatch": "نه چارگ", "unwatchthispage": "چارگ بند کن", @@ -2194,9 +2194,9 @@ "watchlistedit-raw-done": "شمی لیست چارگ په روچ بیتگت", "watchlistedit-raw-added": "{{PLURAL:$1|1 عنوان انت|$1 عناوین ات}} اضافه بوت:", "watchlistedit-raw-removed": "{{PLURAL:$1|1 عنوان|$1 عناوین}} دور بوت:", - "watchlisttools-view": "مربوطین تغییرات بچار", - "watchlisttools-edit": "به چار و اصلاح کن لیست چارگ آ", - "watchlisttools-raw": "هامین لیست چارگ آ اصلاح کن", + "watchlisttools-view": "امبندݔں ٹگلاں چار", + "watchlisttools-edit": "چارگءِ لیست‌ئا چار ءُ ٹگلݔنی", + "watchlisttools-raw": "لیست چارگ‌ئا ٹگلݔن", "iranian-calendar-m1": "فروردین", "iranian-calendar-m2": "اردیبهشت", "iranian-calendar-m3": "خرداد", diff --git a/languages/i18n/be-tarask.json b/languages/i18n/be-tarask.json index 9ceaa6cdc9..c0467ccbb6 100644 --- a/languages/i18n/be-tarask.json +++ b/languages/i18n/be-tarask.json @@ -183,7 +183,7 @@ "history": "Гісторыя старонкі", "history_short": "Гісторыя", "history_small": "гісторыя", - "updatedmarker": "абноўлена з часу майго апошняга наведваньня", + "updatedmarker": "абноўлена з часу вашага апошняга наведваньня", "printableversion": "Вэрсія для друку", "permalink": "Сталая спасылка", "print": "Друкаваць", @@ -2243,8 +2243,8 @@ "enotif_subject_deleted": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была выдаленая {{GENDER:$2|ўдзельнікам|ўдзельніцай}} $2", "enotif_subject_created": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была створаная {{GENDER:$2|ўдзельнікам|ўдзельніцай}} $2", "enotif_subject_moved": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была перанесеная {{GENDER:$2|ўдзельнікам|ўдзельніцай}} $2", - "enotif_subject_restored": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была адноўленая {{GENDER:$2|удзельнікам|удзельніцай}} $2", - "enotif_subject_changed": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была зьмененая {{GENDER:$2|удзельнікам|удзельніцай}} $2", + "enotif_subject_restored": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была адноўленая {{GENDER:$2|ўдзельнікам|ўдзельніцай}} $2", + "enotif_subject_changed": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была зьмененая {{GENDER:$2|ўдзельнікам|ўдзельніцай}} $2", "enotif_body_intro_deleted": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была выдаленая $PAGEEDITDATE {{GENDER:$2|удзельнікам|удзельніцай}} $2, глядзіце $3.", "enotif_body_intro_created": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была створаная $PAGEEDITDATE {{GENDER:$2|удзельнікам|удзельніцай}} $2, па цяперашнюю вэрсію глядзіце $3.", "enotif_body_intro_moved": "Старонка {{GRAMMAR:родны|{{SITENAME}}}} «$1» была перанесеная $PAGEEDITDATE {{GENDER:$2|удзельнікам|удзельніцай}} $2, па цяперашнюю вэрсію глядзіце $3.", @@ -3775,6 +3775,11 @@ "restrictionsfield-help": "Адзін IP-адрас ці CIDR-дыяпазон на радок. Каб дазволіць усё, ужывайце:
0.0.0.0/0\n::/0
", "edit-error-short": "Памылка: $1", "edit-error-long": "Памылкі:\n\n$1", + "specialmute": "Заглушаныя ўдзельнікі", + "specialmute-success": "Вашыя налады заглушэньня былі пасьпяхова абноўленыя. Глядзіце ўсіх заглушаных удзельнікаў на старонцы [[Special:Preferences]].", + "specialmute-submit": "Пацьвердзіць", + "specialmute-label-mute-email": "Заглушыць лісты электроннай пошты ад гэтага ўдзельніка", + "specialmute-header": "Калі ласка, абярыце вашыя налады заглушэньня для {{BIDI:[[User:$1]]}}.", "revid": "вэрсія $1", "pageid": "Ідэнтыфікатар старонкі $1", "interfaceadmin-info": "$1\n\nДазволы на рэдагаваньне агульнасайтавых CSS/JS/JSON-файлаў былі нядаўна вылучаныя з права editinterface. Калі вы не разумееце, чаму атрымліваеце гэтую памылку, глядзіце [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/bn.json b/languages/i18n/bn.json index 2fc856d676..dbc65abe5a 100644 --- a/languages/i18n/bn.json +++ b/languages/i18n/bn.json @@ -204,7 +204,7 @@ "history": "পাতার ইতিহাস", "history_short": "ইতিহাস", "history_small": "ইতিহাস", - "updatedmarker": "আমার শেষ পরিদর্শনের পর থেকে হালনাগাদকৃত", + "updatedmarker": "আপনার শেষ পরিদর্শনের পর থেকে হালনাগাদকৃত", "printableversion": "ছাপার যোগ্য সংস্করণ", "permalink": "স্থায়ী সংযোগ", "print": "মুদ্রণ", @@ -3849,6 +3849,7 @@ "restrictionsfield-help": "লাইন প্রতি একটি আইপি ঠিকানা বা CIDR পরিসীমা। সবকিছু সক্রিয় করতে ব্যবহার করুন: :
0.0.0.0/0\n::/0
", "edit-error-short": "ত্রুটি: $1", "edit-error-long": "ত্রুটিসমূহ:\n\n$1", + "specialmute-submit": "নিশ্চিত করুন", "revid": "সংশোধন $1", "pageid": "পাতার আইডি $1", "rawhtml-notallowed": "<html> ট্যাগ স্বাভাবিক পৃষ্ঠাগুলির বাহিরে ব্যবহার করা যাবে না।", @@ -3872,5 +3873,5 @@ "passwordpolicies-policy-maximalpasswordlength": "পাসওয়ার্ড $1 {{PLURAL:$1|অক্ষরের}} চেয়ে কম দীর্ঘ হতে হবে", "passwordpolicies-policy-passwordnotinlargeblacklist": "পাসওয়ার্ড ১,০০,০০০ সর্বাধিক ব্যবহৃত পাসওয়ার্ডের তালিকায় থাকতে পারবে না।", "unprotected-js": "নিরাপত্তার কারণে জাভাস্ক্রিপ্ট অনিরাপদ পৃষ্ঠা থেকে লোড করা যাবে না। শুধুমাত্র মিডিয়াউইকি: নামস্থান বা ব্যবহারকারী উপপাতায় জাভাস্ক্রিপ্ট তৈরি করুন", - "userlogout-continue": "আপনি যদি প্রস্থান করতে চান দয়া করে [$1 প্রস্থান পাতায় যান]।" + "userlogout-continue": "আপনি কি প্রস্থান করতে চান?" } diff --git a/languages/i18n/ca.json b/languages/i18n/ca.json index 314ad7b3f0..765e5f023f 100644 --- a/languages/i18n/ca.json +++ b/languages/i18n/ca.json @@ -227,7 +227,7 @@ "history": "Historial de canvis", "history_short": "Historial", "history_small": "historial", - "updatedmarker": "actualitzat des de la darrera visita", + "updatedmarker": "actualitzat des de la vostra darrera visita", "printableversion": "Versió per a impressora", "permalink": "Enllaç permanent", "print": "Imprimir", @@ -2012,7 +2012,7 @@ "movethispage": "Trasllada la pàgina", "unusedimagestext": "Els següents fitxers existeixen però estan incorporats en cap altra pàgina.\nTingueu en compte que altres llocs web poden enllaçar un fitxer amb un URL directe i estar llistat ací tot i estar en ús actiu.", "unusedcategoriestext": "Les pàgines de categoria següents existeixen encara que cap altra pàgina o categoria les utilitza.", - "notargettitle": "No hi ha pàgina en blanc", + "notargettitle": "No hi ha cap objectiu", "notargettext": "No heu especificat a quina pàgina dur a terme aquesta funció.", "nopagetitle": "No existeix aquesta pàgina", "nopagetext": "La pàgina que heu especificat no existeix.", @@ -3727,6 +3727,8 @@ "restrictionsfield-label": "Intervals d'IP permesos:", "edit-error-short": "Error: $1", "edit-error-long": "Errors:\n\n$1", + "specialmute-submit": "Confirma", + "specialmute-error-invalid-user": "No s’ha trobat el nom d’usuari que heu indicat.", "revid": "revisió $1", "pageid": "ID de pàgina $1", "rawhtml-notallowed": "No és possible fer servir les etiquetes <html> fora de les pàgines normals.", diff --git a/languages/i18n/ckb.json b/languages/i18n/ckb.json index d1906f5d00..99edfcbd73 100644 --- a/languages/i18n/ckb.json +++ b/languages/i18n/ckb.json @@ -426,6 +426,7 @@ "badretype": "تێپەڕوشەکان لەیەک ناچن.", "usernameinprogress": "دروستکردنی ھەژمارێک بۆ ئەم ناوی بەکارھێنەرە لە پڕۆسەی بەرھەمھێناندایە. تکایە چاوەڕوان بە.", "userexists": "ئەو ناوەی تۆ داوتە پێشتر بەکارھێنراوە.\nناوێکی دیکە ھەڵبژێرە.", + "createacct-normalization": "بەھۆی بەستنەوە تەکنیکییەکان ناوە بەکارھێنەرییەکەت دەگۆڕدرێت بۆ \"$2\".", "loginerror": "ھەڵەی چوونەژوورەوە", "createacct-error": "ھەڵە لە دروستکردنی ھەژمار", "createaccounterror": "ناتوانیت هەژماری بەکارهێنەر دروست بکەیت: $1", @@ -2339,7 +2340,7 @@ "tooltip-t-contributions": "پێڕستی بەشدارییەکانی {{GENDER:$1|ئەم بەکارھێنەرە}}", "tooltip-t-emailuser": "ئیمەیڵێک بنێرە بۆ {{GENDER:$1|ئەم بەکارھێنەرە}}", "tooltip-t-info": "زانیاری زیاتر لەبارەی ئەم پەڕەیەوە", - "tooltip-t-upload": "پەڕگە بار بکە", + "tooltip-t-upload": "پەڕگەکان بار بکە", "tooltip-t-specialpages": "پێڕستی ھەموو پەڕە تایبەتەکان", "tooltip-t-print": "وەشانی چاپی ئەم پەڕەیە", "tooltip-t-permalink": "گرێدەری ھەمیشەیی بۆ ئەم وەشانەی ئەم پەڕەیە", diff --git a/languages/i18n/cs.json b/languages/i18n/cs.json index 9e18328283..a4b4b79fad 100644 --- a/languages/i18n/cs.json +++ b/languages/i18n/cs.json @@ -208,7 +208,7 @@ "history": "Historie stránky", "history_short": "Historie", "history_small": "historie", - "updatedmarker": "změněno od poslední návštěvy", + "updatedmarker": "změněno od vaší poslední návštěvy", "printableversion": "Verze k tisku", "permalink": "Trvalý odkaz", "print": "Vytisknout", diff --git a/languages/i18n/cy.json b/languages/i18n/cy.json index a55f7ebaee..54f14e9f4e 100644 --- a/languages/i18n/cy.json +++ b/languages/i18n/cy.json @@ -184,7 +184,7 @@ "history": "Hanes y dudalen", "history_short": "Hanes", "history_small": "hanes", - "updatedmarker": "diwygiwyd ers i mi ymweld ddiwethaf", + "updatedmarker": "diwygiwyd ers eich ymweliad ddiwethaf", "printableversion": "Fersiwn argraffu", "permalink": "Dolen barhaol", "print": "Argraffu", @@ -337,6 +337,7 @@ "badarticleerror": "Mae'n amhosib cyflawni'r weithred hon ar y dudalen hon.", "cannotdelete": "Mae'n amhosib dileu'r dudalen neu'r ddelwedd \"$1\".\nEfallai fod rhywun arall eisoes wedi'i dileu.", "cannotdelete-title": "Ni ellir dileu'r dudalen '$1'", + "delete-scheduled": "Bydd \"$1\" yn cael ei dileu.\nMam inni yw amynedd!", "delete-hook-aborted": "Terfynwyd y dilead cyn pryd gan fachyn.\nNi roddodd eglurhad.", "no-null-revision": "Ni lwyddwyd i wneud diwygiad newydd heb unrhyw newid ynddo, i'r dudalen \"$1\"", "badtitle": "Teitl gwael", @@ -366,8 +367,13 @@ "cascadeprotected": "Diogelwyd y ddalen hon rhag ei newid, oherwydd ei bod wedi ei chynnwys yn y {{PLURAL:$1|ddalen ganlynol|dalennau canlynol}}, a ddiogelwyd, gyda'r dewisiad hwn yn weithredol: $2", "namespaceprotected": "Nid oes caniatâd gennych i olygu tudalennau yn y parth '''$1'''.", "customcssprotected": "Nid oes caniatâd ganddoch i olygu'r dudalen CSS hon oherwydd bod gosodiadau personol defnyddiwr arall arno.", + "customjsonprotected": "Nid oes gennych yr hawl i olygu tudalen JASON, gan ei bod yn cynnwys dewisiadau (settings) defnyddiwr arall.", "customjsprotected": "Nid oes caniatâd ganddoch i olygu'r dudalen JavaScript hon oherwydd bod gosodiadau personol defnyddiwr arall arno.", + "sitecssprotected": "Nid oes gennych yr hawl i olygu tudalen CSS, gan y gall effeithio pob ymwelydd.", + "sitejsonprotected": "Nid oes gennych yr hawl i olygu tudalen JASON yma, gan y gall effeithio pob ymwelydd.", + "sitejsprotected": "Nid oes gennych yr hawl i olygu'r dudalen JASON yma gan y gall effeithio pob ymwelydd.", "mycustomcssprotected": "Does dim caniatad gennych i olygu'r dudalen CSS hon.", + "mycustomjsonprotected": "Nid oes gennych yr hawl i olygu tudalen JASON yma.", "mycustomjsprotected": "Does dim caniatad gennych i olygu'r dudalen JavaScript hon.", "myprivateinfoprotected": "Nid oes caniatad gennych i olygu eich manylion personol preifat.", "mypreferencesprotected": "Nid oes caniatad gennych i olygu eich dewisiadau eich hunan.", @@ -384,6 +390,8 @@ "virus-scanfailed": "methodd y sgan (côd $1)", "virus-unknownscanner": "gwrthfirysydd anhysbys:", "logouttext": "'''Rydych wedi allgofnodi.'''\n\nSylwer y bydd rhai tudalennau yn parhau i ymddangos fel ag yr oeddent pan oeddech wedi mewngofnodi hyd nes i chi glirio celc eich porwr.", + "logging-out-notify": "Rydych yn cael eich hallgofnodi, rhowswch funud...", + "logout-failed": "Methu allgofnodi ar hyn o bryd: $1", "cannotlogoutnow-title": "Ni ellir allgofnodi ar hyn o bryd", "cannotlogoutnow-text": "Ni ellir allgofnodi tra'n defnyddio $1.", "welcomeuser": "Croeso, $1!", @@ -445,6 +453,7 @@ "badretype": "Nid yw'r cyfrineiriau'n union yr un fath.", "usernameinprogress": "Mae creu cyfrif i'r enw-defnyddiwr hwn wrthi'n cael ei brosesu. Daliwch eich gafael!", "userexists": "Mae rhywun arall wedi dewis yr enw defnyddiwr hwn. \nDewiswch un arall os gwelwch yn dda.", + "createacct-normalization": "Oherwydd cyfyngiadau technegol, bydd eich enw defnyddiwr yn cael ei ailenwi yn \"$2\".", "loginerror": "Problem mewngofnodi", "createacct-error": "Nam wrth greu cyfrif", "createaccounterror": "Ni lwyddwyd i greu'r cyfrif: $1", @@ -464,6 +473,7 @@ "passwordtooshort": "Mae'n rhaid fod gan gyfrinair o leia $1 {{PLURAL:$1|nod}}.", "passwordtoolong": "Ni chaiff cyfrinair fod yn hirach na {{PLURAL:$1|1 llythyren|$1 llythyren}}.", "passwordtoopopular": "Chewch chi ddim defnyddio cyfrineiriau cyffredin. Dewisiwch un unigryw a gwahanol!", + "passwordinlargeblacklist": "Mae'r cyfrinair yma ar restr o rai sy'n rhy gyffredin o'r hanner. dewisiwch un mwy unigryw, gwahanol.", "password-name-match": "Rhaid i'ch cyfrinair a'ch enw defnyddiwr fod yn wahanol i'w gilydd.", "password-login-forbidden": "Gwaharddwyd defnyddio'r enw defnyddiwr a'r cyfrinair hwn.", "mailmypassword": "Ailosoder y cyfrinair", @@ -512,6 +522,7 @@ "changepassword-success": "Newidiwyd eich cyfrinair!", "changepassword-throttled": "Rydych wedi ceisio logio mewn yn rhy aml.\nArhoswch am $1 cyn trio eto.", "botpasswords": "Cyfrineiriau bots", + "botpasswords-disabled": "Ni ellir creu cyfrinair gyda bot.", "botpasswords-label-appid": "Enw bot:", "botpasswords-label-create": "Dechrau", "botpasswords-label-update": "Diweddaru", @@ -709,12 +720,14 @@ "editpage-invalidcontentmodel-text": "Nid yw'r model \"$1\" ar gael.", "editpage-notsupportedcontentformat-title": "Dydy fformat y cynnwys hwn ddim yn cael ei gefnogi gennym.", "editpage-notsupportedcontentformat-text": "Dydy'r fformat $1 ar y cynnwys ddim yn cael ei gefnogi gan y model $2.", + "slot-name-main": "Prif", "content-model-wikitext": "cystrawen wici", "content-model-text": "testun plaen", "content-model-javascript": "JavaScript", "content-model-css": "CSS", "content-json-empty-object": "Dim gwrthrych", "content-json-empty-array": "Rhesi gwag", + "deprecated-self-close-category": "Tudalennau gyda tagiau HTML annilys.", "duplicate-args-warning": "Rhybudd: Mae [[:$1]] yn galw [[:$2]] gyda mwy nag un gwerthrif (''value'') i baramedr \"$3\". Dim ond y gwerthrif diwethaf gaiff ei ddefnyddio.", "duplicate-args-category": "Tudalennau gyda meysydd deublyg yn y Nodion", "duplicate-args-category-desc": "Mae'r dudalen hon yn cynnwys meysydd yn y Nodion, ddwy waith e.e. {{foo|bar=1|bar=2}} neu {{foo|bar|1=baz}}.", @@ -759,7 +772,7 @@ "page_first": "cyntaf", "page_last": "olaf", "histlegend": "Cymharu dau fersiwn: marciwch y cylchoedd ar y ddau fersiwn i'w cymharu, yna pwyswch ar 'return' neu'r botwm 'Cymharer y fersiynau dewisedig'.
\nEglurhad: '''({{int:cur}})''' = gwahaniaethau rhyngddo a'r fersiwn cyfredol,\n'''({{int:last}})''' = gwahaniaethau rhyngddo a'r fersiwn cynt, '''({{int:minoreditletter}})''' = golygiad bychan", - "history-fieldset-title": "Chwilio drwy'r hanes", + "history-fieldset-title": "Hidlo'r adolygiadau", "history-show-deleted": "Dangos y rhai a ddilëwyd yn unig", "histfirst": "cynharaf", "histlast": "diweddaraf", @@ -881,6 +894,7 @@ "diff-multi-manyusers": "(Ni ddangosir {{PLURAL:$1|yr $1 diwygiad|yr $1 diwygiad|y $1 ddiwygiad|y $1 diwygiad|y $1 diwygiad|y $1 diwygiad}} rhyngol gan mwy na $2 {{PLURAL:$2|o ddefnyddwyr}}.)", "difference-missing-revision": "Ni chafwyd hyd i $1 {{PLURAL:$2|diwygiad|diwygiad|ddiwygiad|diwygiad}} o'r gwahaniaeth ($1) {{PLURAL:$2|hwn}}.\n\nFel arfer, fe ddigwydd hyn pan mae person wedi dilyn hen gyswllt gwahaniaeth i dudalen sydd erbyn hyn wedi cael ei ddileu.\nMae manylion pellach i'w cael yn [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} lòg y dileuon].", "searchresults": "Canlyniadau'r chwiliad", + "search-filter-title-prefix-reset": "Chwilio pob tudalen", "searchresults-title": "Canlyniadau chwilio am \"$1\"", "titlematches": "Teitlau erthygl yn cyfateb", "textmatches": "Testun erthygl yn cyfateb", @@ -1247,6 +1261,8 @@ "action-applychangetags": "rhowch y tagiau ar waith, gyda'ch newidiadau", "action-deletechangetags": "dilewch tagiau o'r gronfa ddata", "action-purge": "carthwch y ddalen", + "action-editinterface": "golygwch y rhyngwyneb", + "action-unblockself": "dadflocio eich hunan", "nchanges": "$1 {{PLURAL:$1|newid|newid|newid|newid|newid|o newidiadau}}", "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|ers eich ymweliad diwethaf}}", "enhancedrc-history": "hanes", @@ -1264,11 +1280,13 @@ "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (gweler hefyd [[Special:NewPages|restr y tudalennau newydd]])", "recentchanges-legend-plusminus": "(''±123'')", "recentchanges-submit": "Dangos", + "rcfilters-tag-remove": "Dileu '$1'", "rcfilters-legend-heading": "Rhestr o fyrfoddau:", "rcfilters-other-review-tools": "Teclynau adolygu eraill", "rcfilters-group-results-by-page": "Canlyniadau'r grwp bob yn ddalen", "rcfilters-activefilters": "Hidlau sydd ar waith", "rcfilters-activefilters-hide": "Cuddio", + "rcfilters-activefilters-show": "Dangos", "rcfilters-advancedfilters": "Ffiltrau ychwanegol", "rcfilters-limit-title": "Canlyniadau a ddangosir", "rcfilters-date-popup-title": "Cyfnod (i'w chwilio)", @@ -1283,17 +1301,18 @@ "rcfilters-savedqueries-rename": "Ailenwi", "rcfilters-savedqueries-setdefault": "Gosod yn ddiofyn (''Set as default'')", "rcfilters-savedqueries-unsetdefault": "Diddymu fel gweithred ddiofyn (''Remove as default'')", - "rcfilters-savedqueries-remove": "Cael gwared", + "rcfilters-savedqueries-remove": "Dileu", "rcfilters-savedqueries-new-name-label": "Enw", "rcfilters-savedqueries-new-name-placeholder": "Disgrifiwch bwrpas y ffiltr", "rcfilters-savedqueries-apply-label": "Crewch ffiltr", "rcfilters-restore-default-filters": "Ailosodwch y ffiltrau di-ofyn", "rcfilters-clear-all-filters": "Cliriwch yr holl hidlau (ffiltrau)", - "rcfilters-search-placeholder": "Ffiltrwch y newidiadau diweddaraf", + "rcfilters-search-placeholder": "FNewidiadau'r hidl (ffiltr) - defnyddiwch y blwch chwilio", "rcfilters-invalid-filter": "Hidl annilys", "rcfilters-empty-filter": "Dim hidlau ar waith", "rcfilters-filterlist-title": "Hidlau (ffiltrau)", - "rcfilters-filterlist-feedbacklink": "Rhowch adborth ar yr hidlau beta", + "rcfilters-filterlist-whatsthis": "Sut mae'r rhain yn gweithio?", + "rcfilters-filterlist-feedbacklink": "Rhowch adborth ar y teclynau hidlo", "rcfilters-highlightbutton-title": "Amlygwch y canlyniadau", "rcfilters-highlightmenu-title": "Dewisiwch liw", "rcfilters-highlightmenu-help": "Dewisiwch liw sy'n cyd-fynd gyda'r nodwedd hon", @@ -1308,7 +1327,7 @@ "rcfilters-filter-user-experience-level-unregistered-label": "Heb gofrestru", "rcfilters-filter-user-experience-level-unregistered-description": "Golygyddion nad ydynt wedi cofrestru.", "rcfilters-filter-user-experience-level-newcomer-label": "Newydd-ddyfodiaid", - "rcfilters-filter-user-experience-level-newcomer-description": "Defnyddwyr cofrestredig gyda llai na 10 golygiad a 4 diwrnod o weithgaredd.", + "rcfilters-filter-user-experience-level-newcomer-description": "Defnyddwyr cofrestredig gyda llai na 10 golygiad neu 4 diwrnod o weithgaredd.", "rcfilters-filter-user-experience-level-learner-label": "Dysgwyr", "rcfilters-filter-user-experience-level-learner-description": "Defnyddwyr cofrestredig ble mae eu profiad yn syrthio rhwng \"Newydd-ddyfodiaid\" a \"Defnyddwyr profiadol.\"", "rcfilters-filter-user-experience-level-experienced-label": "Defnyddwyr profiadol", @@ -1320,6 +1339,8 @@ "rcfilters-filter-humans-description": "Golygiadau a wnaed gan olygyddion go-iawn.", "rcfilters-filtergroup-reviewstatus": "Statws adolygu", "rcfilters-filter-reviewstatus-unpatrolled-label": "Heb ei gadarnhau (''Unpatrolled'')", + "rcfilters-filter-reviewstatus-manual-label": "Patrol gyda llaw a llygad", + "rcfilters-filter-reviewstatus-auto-label": "Patroliwyd yn otomatig", "rcfilters-filtergroup-significance": "Arwyddocaol", "rcfilters-filter-minor-label": "Golygiadau bach", "rcfilters-filter-minor-description": "Golygiadau a nodwyd gan y golygydd fel rhai bach.", @@ -1329,6 +1350,7 @@ "rcfilters-filter-watchlist-watched-label": "Ar y Rhestr Wylio", "rcfilters-filter-watchlist-watched-description": "Newidiadau i'r dalennau yn eich Rhestr Wylio.", "rcfilters-filter-watchlist-watchednew-label": "Newidiadau newydd i'ch Rhestr Wylio", + "rcfilters-filter-watchlist-watchednew-description": "Newidiadau yn y Rhestr wylio nad ydych wedi ymweld a nhw ers i'r newidiadau gael eu gwneud.", "rcfilters-filter-watchlist-notwatched-label": "Heb fod yn eich Rhestr Wylio", "rcfilters-filter-watchlist-notwatched-description": "Popeth ar wahan i'r newidiadau i'ch Rhestr Wylio.", "rcfilters-filter-watchlistactivity-unseen-label": "Newidiadau heb eu gweld gennych", @@ -1564,7 +1586,7 @@ "uploadstash-bad-path-invalid": "'Dyw'r llwybr ddim yn gywir.", "invalid-chunk-offset": "Atred annilys i'r talpiau", "img-auth-accessdenied": "Ni chaniatawyd mynediad", - "img-auth-nopathinfo": "PATH_INFO yn eisiau.\nNid yw'ch gweinydd wedi ei osod i fedru pasio'r wybodaeth hon.\nEfallai ei fod wedi ei seilio ar CGI, ac heb fod yn gallu cynnal img_auth.\nGweler https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.", + "img-auth-nopathinfo": "Gwybodaeth am y llwybr yn eisiau.\nNid yw'ch gweinydd wedi ei osod i fedru pasio'r REQUEST_URI a/neu PATH_INFO.\nOs ydyw yna trowch y $wgUsePathInfo ymlaen.\n\nGweler https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.", "img-auth-notindir": "Nid yw'r llwybr y gwneuthpwyd cais amdano yn y cyfeiriadur uwchlwytho ffurfweddedig.", "img-auth-badtitle": "Ddim yn gallu gwneud teitl dilys o \"$1\".", "img-auth-nofile": "Nid oes ffeil a'r enw \"$1\" ar gael.", @@ -1579,6 +1601,7 @@ "http-timed-out": "Goroedi wedi digwydd ar y cais HTTP.", "http-curl-error": "Cafwyd gwall wrth nôl yr URL: $1", "http-bad-status": "Cafwyd trafferth yn ystod y cais HTTP: $1 $2", + "http-internal-error": "Gwall mewnol HTTP.", "upload-curl-error6": "Wedi methu cyrraedd yr URL", "upload-curl-error6-text": "Ni chyrhaeddwyd yr URL a roddwyd.\nGwiriwch yr URL a sicrhau bod y wefan ar waith.", "upload-curl-error28": "Goroedi wrth uwchlwytho", @@ -1759,7 +1782,7 @@ "prefixindex": "Pob tudalen yn ôl parth", "prefixindex-namespace": "Pob tudalen â rhagddodiad penodol (y parth $1)", "prefixindex-submit": "Dangos", - "prefixindex-strip": "Diosg y rhagddodiad wrth restru", + "prefixindex-strip": "Cuddio'r rhagddodiad yn y canfyddiadau", "shortpages": "Erthyglau byr", "longpages": "Tudalennau hirion", "deadendpages": "Tudalennau heb gysylltiadau ynddynt", @@ -1815,6 +1838,7 @@ "apisandbox-dynamic-parameters": "Paramedrau ychwanegol", "apisandbox-dynamic-parameters-add-label": "Ychwanegu paramedrau", "apisandbox-dynamic-parameters-add-placeholder": "Enw'r paramedr", + "apisandbox-add-multi": "Ychwanegu", "apisandbox-results": "Canlyniadau", "apisandbox-continue": "Parhau", "apisandbox-continue-clear": "Clirio", @@ -1829,6 +1853,8 @@ "speciallogtitlelabel": "Targed (teitl neu {{ns:user}}:username ar gyfer y defnyddiwr):", "log": "Logiau", "logeventslist-submit": "Dangos", + "logeventslist-patrol-log": "Log patrolio", + "logeventslist-tag-log": "Log y tagiau", "all-logs-page": "Pob lòg cyhoeddus", "alllogstext": "Mae pob cofnod yn holl logiau {{SITENAME}} wedi cael eu rhestru yma.\nGallwch weld chwiliad mwy penodol trwy ddewis y math o lòg, enw'r defnyddiwr, neu'r dudalen benodedig.\nSylwer bod llythrennau mawr neu fach o bwys i'r chwiliad.", "logempty": "Does dim eitemau yn cyfateb yn y lòg.", @@ -2001,7 +2027,7 @@ "delete-confirm": "Dileu \"$1\"", "delete-legend": "Dileu", "historywarning": "Rhybudd: bu tua $1 {{PLURAL:$1|golygiad|golygiad|olygiad|golygiad|golygiad|o olygiadau}} yn hanes y dudalen rydych ar fin ei dileu:", - "historyaction-submit": "Dangos", + "historyaction-submit": "Dangos yr adolygiadau", "confirmdeletetext": "Rydych chi ar fin dileu tudalen neu ddelwedd, ynghŷd â'i hanes, o'r data-bas, a hynny'n barhaol.\nOs gwelwch yn dda, cadarnhewch eich bod chi wir yn bwriadu gwneud hyn, eich bod yn deall y canlyniadau, ac yn ei wneud yn ôl [[{{MediaWiki:Policy-url}}|polisïau {{SITENAME}}]].", "actioncomplete": "Wedi cwblhau'r weithred", "actionfailed": "Methodd y weithred", @@ -2009,6 +2035,9 @@ "dellogpage": "Lòg dileuon", "dellogpagetext": "Ceir rhestr isod o'r dileadau diweddaraf.", "deletionlog": "lòg dileuon", + "log-name-create": "Log creu tudalennau", + "log-description-create": "Nodir isod y rhestr o'r tudalennau newydd mwyaf diweddar.", + "logentry-create-create": "$1 {{GENDER:$2|created}} tudalen $3", "reverted": "Wedi gwrthdroi i'r golygiad cynt", "deletecomment": "Rheswm:", "deleteotherreason": "Rheswm arall:", @@ -2021,6 +2050,9 @@ "deleting-backlinks-warning": "'''Rhybudd:''' Mae [[Special:WhatLinksHere/{{FULLPAGENAME}}|tudalennau eraill]] yn cysylltu i'r ddalen rydych ar fin ei dileu.", "deleting-subpages-warning": "Rhybudd: Mae gan y ddalen rydych ar fin ei dileu [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|is-ddalen|$1 is-ddalennau|51=dros 50 o is-ddalennau}}]].", "rollback": "Gwrthdroi golygiadau", + "rollback-confirmation-confirm": "Cadarnhewch:", + "rollback-confirmation-yes": "Troi'n ol", + "rollback-confirmation-no": "Canslo", "rollbacklink": "gwrthdröer", "rollbacklinkcount": "gwrthdröer $1 {{PLURAL:$1||golygiad|olygiad|golygiad}}", "rollbacklinkcount-morethan": "gwrthdröer mwy na $1 {{PLURAL:$1||golygiad|olygiad|golygiad}}", @@ -2034,7 +2066,7 @@ "revertpage-nouser": "Wedi gwrthdroi golygiadau gan ddefnyddiwr cudd; wedi adfer y golygiad diweddaraf gan {{GENDER:$1|[[User:$1|$1]]}}", "rollback-success": "Gwrthdrowyd y golygiadau gan {{GENDER:$3|$1}};\nailosodwyd y golygiad olaf gan {{GENDER:$4|$2}}.", "sessionfailure-title": "Sesiwn wedi methu", - "sessionfailure": "Mae'n debyg fod yna broblem gyda'ch sesiwn mewngofnodi; diddymwyd y weithred er mwyn diogelu'r sustem rhag ddefnyddwyr maleisus. Gwasgwch botwm 'nôl' eich porwr ac ail-lwythwch y dudalen honno, yna ceisiwch eto.", + "sessionfailure": "Mae'n debyg fod yna broblem gyda'ch sesiwn mewngofnodi;\ndiddymwyd y weithred er mwyn diogelu'r sustem rhag ddefnyddwyr maleisus.\nAilgyflwynwch y ffurflen.", "changecontentmodel-title-label": "Teitl y ddalen", "changecontentmodel-reason-label": "Rheswm:", "changecontentmodel-submit": "Newid", @@ -2118,6 +2150,7 @@ "undelete-search-title": "Chwilio drwy'r tudalennau dilëedig", "undelete-search-box": "Chwilio tudalennau a ddilëwyd", "undelete-search-prefix": "Dangos tudalennau gan ddechrau gyda:", + "undelete-search-full": "Dangos tudalennau sy'n cynnwys:", "undelete-search-submit": "Chwilio", "undelete-no-results": "Ni chafwyd hyd i dudalennau cyfatebol yn archif y dileuon.", "undelete-filename-mismatch": "Nid oes modd dad-ddileu'r golygiad ffeil â'r stamp amser $1: nid oedd enw'r ffeil yn cydweddu", @@ -2201,6 +2234,8 @@ "ipb-disableusertalk": "Atal y defnyddiwr hwn rhag golygu ei dudalen/ei thudalen sgwrs ei hunan wrth i'r bloc fod yn weithredol", "ipb-change-block": "Ailflocio'r defnyddiwr hwn gyda'r gosodiadau hyn", "ipb-confirm": "Cadarnhau'r rhwystr", + "ipb-pages-label": "Tudalennau", + "ipb-namespaces-label": "Parthenwau", "badipaddress": "Cyfeiriad IP annilys.", "blockipsuccesssub": "Llwyddodd y rhwystr", "blockipsuccesstext": "Mae [[Special:Contributions/$1|$1]] wedi cael ei flocio.
\nGweler y [[Special:BlockList|rhestr blociau]] er mwyn arolygu blociau.", @@ -2213,6 +2248,9 @@ "ipb-blocklist": "Dangos y blociau cyfredol", "ipb-blocklist-contribs": "Cyfraniadau {{GENDER:$1|$1}}", "block-expiry": "Am gyfnod:", + "block-prevent-edit": "Golygu", + "block-reason": "Rhesymau:", + "block-target": "Cyfeiriad IP y Defnyddiwr", "unblockip": "Dadflocio defnyddiwr", "unblockiptext": "Defnyddiwch y ffurflen isod i ail-alluogi golygiadau gan ddefnyddiwr neu o gyfeiriad IP a fu gynt wedi'i flocio.", "ipusubmit": "Tynnu'r rhwystr hwn", @@ -2221,7 +2259,11 @@ "unblocked-id": "Tynnwyd rhwystr $1", "unblocked-ip": "Mae [[Special:Contributions/$1|$1]] wedi ei atal.", "blocklist": "Defnyddwyr a rwystrwyd", + "autoblocklist": "Rhwystrau otomatig", "autoblocklist-submit": "Chwilio", + "autoblocklist-legend": "Rhestr o rwystrau otomatig", + "autoblocklist-localblocks": "{{PLURAL:$1|autoblock|autoblocks}} lleol", + "autoblocklist-total-autoblocks": "Cyfanswm y rhwystrau otomatig: $1", "ipblocklist": "Defnyddwyr a rwystrwyd", "ipblocklist-legend": "Dod o hyd i ddefnyddiwr a rwystrwyd", "blocklist-userblocks": "Cuddio rhwystrau cyfrifon", @@ -2245,7 +2287,7 @@ "emailblock": "rhwystrwyd e-bostio", "blocklist-nousertalk": "ni all olygu ei dudalen/ei thudalen sgwrs ei hunan", "ipblocklist-empty": "Mae'r rhestr rwystrau'n wag.", - "ipblocklist-no-results": "Nid yw cyfeiriad IP neu enw defnyddiwr yr ymholiad wedi'i rwystro.", + "ipblocklist-no-results": "Ni chafwyd hyn i rwystrau o nanut yma ar gyfer cyfeiriad IP neu enw defnyddiwr.", "blocklink": "rhwystro", "unblocklink": "dadrwystro", "change-blocklink": "newid y rhwystr", @@ -2343,7 +2385,7 @@ "delete_and_move_text": "==Angen dileu==\n\nMae'r erthygl \"[[:$1]]\" yn bodoli'n barod. Ydych chi am ddileu'r erthygl er mwyn paratoi lle?", "delete_and_move_confirm": "Ie, dileu'r dudalen", "delete_and_move_reason": "Wedi'i dileu er mwyn gallu symud y dudalen \"[[$1]]\" i gymryd ei lle", - "selfmove": "Mae'r teitlau hen a newydd yn union yr un peth;\nnid yw'n bosib cyflawnu'r symud.", + "selfmove": "Mae'r teitlau yr un peth;\nnid yw'n symud tudalen iddi hi ei hun.", "immobile-source-namespace": "Ni ellir symud tudalennau yn y parth \"$1\".", "immobile-target-namespace": "Ni ellir symud tudalennau i'r parth \"$1\".", "immobile-target-namespace-iw": "Nid yw cyswllt rhyngwici yn nod dilys wrth symud tudalen.", @@ -2357,7 +2399,7 @@ "fix-double-redirects": "Yn diwygio unrhyw ailgyfeiriadau sy'n cysylltu i'r teitl gwreiddiol", "move-leave-redirect": "Creu tudalen ail-gyfeirio â'r teitl gwreiddiol", "protectedpagemovewarning": "'''Sylwer:''' Clowyd y dudalen ac felly dim ond defnyddwyr a galluoedd gweinyddu ganddynt sy'n gallu ei symud.\nDyma'r cofnod lòg diweddaraf, er gwybodaeth:", - "semiprotectedpagemovewarning": "'''Sylwer:''' Clowyd y dudalen ac felly dim ond defnyddwyr mewngofnodedig sy'n gallu ei symud.\nDyma'r cofnod lòg diweddaraf, er gwybodaeth:", + "semiprotectedpagemovewarning": "Sylwer: Clowyd y dudalen. Dim ond defnyddwyr mewngofnodedig sy'n gallu ei symud.\nDyma'r cofnod lòg diweddaraf isod, er gwybodaeth:", "move-over-sharedrepo": "Mae'r ffeil [[:$1]] ar gael mewn storfa gyfrannol. Pe byddech yn symud y ffeil i'r teitl hwn, yna byddai'r ffeil o'r storfa gyfrannol yn cael ei disodli.", "file-exists-sharedrepo": "Mae'r enw y dewisoch ar y ffeil yn cael ei ddefnyddio'n barod ar storfa gyfrannol.\nDewiswch enw arall os gwelwch yn dda.", "export": "Allforio tudalennau", @@ -2621,7 +2663,7 @@ "previousdiff": "← Y fersiwn gynt", "nextdiff": "Y fersiwn dilynol →", "mediawarning": "'''Rhybudd''': Gallasai'r math hwn o ffeil gynnwys côd maleisus.\nMae'n bosib y bydd eich cyfrifiadur yn cael ei danseilio wrth ddefnyddio'r ffeil.", - "imagemaxsize": "Maint mwyaf y delweddau:
''(ar y tudalennau disgrifiad)''", + "imagemaxsize": "Ceir cyfyngiad ar faint tudalennau disgrifio'r ffeil:", "thumbsize": "Maint mân-lun :", "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|tudalen|dudalen|dudalen|tudalen|thudalen|tudalen}}", "file-info": "maint y ffeil: $1, ffurf MIME: $2", @@ -2729,6 +2771,9 @@ "confirm-unwatch-top": "Tynner y dudalen hon oddi ar eich rhestr wylio?", "confirm-rollback-button": "Iawn", "confirm-rollback-top": "Dadwneud golygiadau'r ddalen hon?", + "confirm-mcrrestore-title": "Adfer diwygiadau", + "confirm-mcrundo-title": "Dadwneud y newidiadau", + "mcrundofailed": "Methwyd gwrthdroi", "quotation-marks": "'$1'", "imgmultipageprev": "← i'r dudalen gynt", "imgmultipagenext": "i'r dudalen nesaf →", @@ -2817,7 +2862,7 @@ "version-poweredby-others": "eraill", "version-poweredby-translators": "cyfieithwyr translatewiki.net", "version-credits-summary": "Hoffem gydnabod cyfraniad y bobl canlynol i [[Special:Version|MediaWiki]].", - "version-license-info": "Meddalwedd rhydd yw MediaWiki; gallwch ei ddefnyddio a'i addasu yn ôl termau'r GNU General Public License a gyhoeddir gan Free Software Foundation; naill ai fersiwn 2 o'r Drwydded, neu unrhyw fersiwn diweddarach o'ch dewis.\n\nCyhoeddir MediaWiki yn y gobaith y bydd o ddefnydd, ond HEB UNRHYW WARANT; heb hyd yn oed gwarant ymhlyg o FARCHNADWYEDD nag o FOD YN ADDAS AT RYW BWRPAS ARBENNIG. Gweler y GNU General Public License am fanylion pellach.\n\nDylech fod wedi derbyn [{{SERVER}}{{SCRIPTPATH}}/COPYING gopi o GNU General Public License] gyda'r rhaglen hon; os nad ydych, ysgrifennwch at Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, neu [//www.gnu.org/licenses/old-licenses/gpl-2.0.html gallwch ei ddarllen ar y we].", + "version-license-info": "Meddalwedd rhydd ac agored yw MediaWiki; gallwch ei ailddosbarthu a'i addasu yn ôl termau'r GNU General Public License a gyhoeddir gan Free Software Foundation; naill ai fersiwn 2 o'r Drwydded, neu unrhyw fersiwn diweddarach o'ch dewis.\n\nCyhoeddir MediaWiki yn y gobaith y bydd o ddefnydd, ond HEB UNRHYW WARANT; heb hyd yn oed gwarant ymhlyg o FARCHNADWYEDD nag o FOD YN ADDAS AT RYW BWRPAS ARBENNIG. Gweler y GNU General Public License am fanylion pellach.\n\nDylech fod wedi derbyn [{{SERVER}}{{SCRIPTPATH}}/COPYING copi o GNU General Public License] gyda'r rhaglen hon; os nad ydych, ysgrifennwch at Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, neu [//www.gnu.org/licenses/old-licenses/gpl-2.0.html gallwch ei ddarllen ar y we].", "version-software": "Meddalwedd gosodedig", "version-software-product": "Cynnyrch", "version-software-version": "Fersiwn", @@ -2841,6 +2886,7 @@ "redirect-file": "Enwau ffeiliau", "redirect-logid": "Log yr ID", "redirect-not-exists": "Heb lwyddo i'w ganfod", + "redirect-not-numeric": "Nid yw'r gwerth yn rhif", "fileduplicatesearch": "Chwilio am ffeiliau dyblyg", "fileduplicatesearch-summary": "Chwilier am ffeiliau dyblyg ar sail ei werth stwnsh.", "fileduplicatesearch-filename": "Enw'r ffeil:", @@ -2867,11 +2913,13 @@ "specialpages-group-developer": "Arfau ar gyfer y Datblygwr", "blankpage": "Tudalen wag", "intentionallyblankpage": "Gadawyd y dudalen hon yn wag o fwriad", + "disabledspecialpage-disabled": "Anallugwyd y dudalen gan weinyddwr y system.", "external_image_whitelist": " #Leave this line exactly as it is
\n#Gosodwch ddarnau o ymadroddion rheolaidd (y rhan sy'n cael ei osod rhwng y //) isod\n#Caiff y rhain eu cysefeillio gyda URL y delweddau allanol (a chyswllt poeth atynt)\n#Dangosir y rhai sy'n cysefeillio fel delweddau; dangosir cyswllt at y ddelwedd yn unig ar gyfer y lleill\n#Caiff y llinellau sy'n dechrau gyda # eu trin fel sylwadau\n#Nid yw'n gwahaniaethu rhwng llythrennau mawr a bach\n\n#Put all regex fragments above this line. Leave this line exactly as it is
", "tags": "Tagiau newidiadau", "tag-filter": "Hidl [[Special:Tags|tagiau]]:", "tag-filter-submit": "Hidlo", "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|Tag|Tagiau}}]]: $2", + "tag-mw-undo": "Dadwneud", "tags-title": "Tagiau", "tags-intro": "Dyma restr o'r tagiau y mae'r meddalwedd yn defnyddio i farcio golygiad, ynghyd â'r rhesymau dros eu defnyddio.", "tags-tag": "Enw'r tag", @@ -2966,7 +3014,7 @@ "logentry-rights-autopromote": "{{GENDER:$2|Dyrchafwyd}} $1 yn awtomatig o $4 i $5", "logentry-upload-upload": "Mae $1 {{GENDER:$2|wedi uwchlwytho}} $3", "logentry-upload-overwrite": "Mae $1 {{GENDER:$2|wedi uwchlwytho}} fersiwn newydd o $3", - "logentry-upload-revert": "Mae $1 {{GENDER:$2|wedi uwchlwytho}} $3", + "logentry-upload-revert": "Mae $1 {{GENDER:$2|wedi gwrthdroi}} $3 i fersiwn hyn", "rightsnone": "(dim)", "feedback-adding": "Wrthi'n ychwanegu adborth i'r dudalen...", "feedback-bugcheck": "Iawn! Gwnewch yn siwr yn gyntaf nag ydy hwn yn un o'r [$1 bygiau hysbys].", @@ -2989,7 +3037,7 @@ "api-error-emptypage": "Ni chaniateir dechrau tudalen newydd, a honno'n wag.", "api-error-publishfailed": "Gwall mewnol: methodd y gweinydd â chyhoeddi'r ffeil dros dro.", "api-error-stashfailed": "Gwall mewnol: methodd y gweinydd â rhoi'r ffeil dros dro ar gadw.", - "api-error-unknown-warning": "Rhybudd anhysbys: $1", + "api-error-unknown-warning": "Rhybudd anhysbys: \"$1\".", "api-error-unknownerror": "Gwall anhysbys: \"$1\".", "duration-seconds": "$1 {{PLURAL:$1|eiliad}}", "duration-minutes": "$1 {{PLURAL:$1|munud|munud|funud|munud|munud|munud}}", @@ -3016,7 +3064,7 @@ "limitreport-expensivefunctioncount": "Nifer y ffwythiannau dosrannu sy'n dreth ar adnoddau", "expandtemplates": "Ehangu'r nodynnau", "expand_templates_title": "Teitl y cyd-destun, ar gyfer {{FULLPAGENAME}}, etc.:", - "expand_templates_input": "Cynnwys y mewnbwn:", + "expand_templates_input": "Codwici'r mewnbwn:", "expand_templates_output": "Y canlyniad", "expand_templates_xml_output": "Yr allbwn XML", "expand_templates_html_output": "Allbwn HTML crai", diff --git a/languages/i18n/da.json b/languages/i18n/da.json index 169cda8e50..e7ae28158b 100644 --- a/languages/i18n/da.json +++ b/languages/i18n/da.json @@ -3504,6 +3504,7 @@ "credentialsform-account": "Kontonavn:", "edit-error-short": "Fejl: $1", "edit-error-long": "Fejl:\n\n$1", + "specialmute-submit": "Bekræft", "revid": "version $1", "pageid": "side id: $1", "gotointerwiki": "Forlader {{SITENAME}}", diff --git a/languages/i18n/diq.json b/languages/i18n/diq.json index 946502e4e5..7b58d71742 100644 --- a/languages/i18n/diq.json +++ b/languages/i18n/diq.json @@ -32,7 +32,8 @@ "Archaeodontosaurus", "Fitoschido", "ديفيد", - "Orbot707" + "Orbot707", + "Shirayuki" ] }, "tog-underline": "Bınê gırey de xete bance:", @@ -111,8 +112,8 @@ "november": "Tışrino Peyên", "december": "Kanun", "january-gen": "Çele", - "february-gen": "Gucige", - "march-gen": "Adar", + "february-gen": "Şıbat", + "march-gen": "Mert", "april-gen": "Nisane", "may-gen": "Gulane", "june-gen": "Heziran", @@ -123,7 +124,7 @@ "november-gen": "Tışrino Peyên", "december-gen": "Kanun", "jan": "Çel", - "feb": "Gcg", + "feb": "Şbt", "mar": "Adr", "apr": "Nsn", "may": "Gul", @@ -135,7 +136,7 @@ "nov": "Tşp", "dec": "Gğn", "january-date": "$1 Çele", - "february-date": "$1 Gucige", + "february-date": "$1 Şıbat", "march-date": "$1 Adar", "april-date": "$1 Nisane", "may-date": "$1 Gulane", @@ -194,7 +195,7 @@ "history": "Tarixê perrer", "history_short": "Veror", "history_small": "tarix", - "updatedmarker": "cı kewtena mına peyêne ra dıme biyo rocane", + "updatedmarker": "ziyaretê peyêni dıma biyo rocane", "printableversion": "Versiyonê çapkerdışi", "permalink": "Gıreyo daimi", "print": "Bınuşne", @@ -230,7 +231,7 @@ "redirectedfrom": "($1 ra kırışı yê)", "redirectpagesub": "Perra kırıştışi", "redirectto": "Kırışêno:", - "lastmodifiedat": "Ena perre roca $1 de, saete $2 de vırriye.", + "lastmodifiedat": "Ena pela roca $1 de, sehate $2 de vıriyaya", "viewcount": "Ena pele {{PLURAL:$1|rae|$1 rey}} vêniya.", "protectedpage": "Pera pawıyayi", "jumpto": "Şo be:", @@ -287,7 +288,7 @@ "hidetoc": "bınımne", "collapsible-collapse": "Teng ke", "collapsible-expand": "Hera kerê", - "confirmable-confirm": "{{GENDER:$1|Şıma}} pêbawerê?", + "confirmable-confirm": "{{GENDER:$1|Şıma}} bêgumanê?", "confirmable-yes": "Eya", "confirmable-no": "Nê", "thisisdeleted": "Bıvêne ya zi $1 peyser biya?", @@ -315,7 +316,7 @@ "nstab-template": "Şablon", "nstab-help": "Perra pasti", "nstab-category": "Kategoriye", - "mainpage-nstab": "Pela seri", + "mainpage-nstab": "Pera seri", "nosuchaction": "Fealiyeto wınasi çıniyo", "nosuchactiontext": "URL ra kar qebul nêbı.\nŞıma belka URL şaş nuşt, ya zi gıreyi şaş ra ameyi.\nKeyepelê {{SITENAME}} eşkeno xeta aşkera bıkero.", "nosuchspecialpage": "Pela hısusiya wınasiyên çıniya.", @@ -380,7 +381,7 @@ "mycustomjsprotected": "Desturê şıma çıniyo ke na pela JavaScripti bıvurnê.", "myprivateinfoprotected": "Ğısusi malumatana ğo timar kerdışire icazeta şıma çıniya.", "mypreferencesprotected": "Terciha timar kerdışire icazeta şıam çıniya.", - "ns-specialprotected": "Pelê xısusiyi nêşenê bıvurriyê.", + "ns-specialprotected": "Pelanê bağseya şıma nêşenê bıvurnê.", "titleprotected": "No sername terefê [[User:$1|$1]] ra, afernayene ra şevekiyayo.\nSebebê cı $2 de deya yo.", "filereadonlyerror": "Dosyay vurnayışê \"$1\" nê abêno lakin depoy dosya da \"$2\" mod dê salt wendi de yo.\n\nXızmetkarê kılit kerdışi wa bewniro enay wa çım ra ravyarn o: \"$3\".", "invalidtitle": "Sernuşteyo nêravêrde", @@ -393,6 +394,7 @@ "virus-scanfailed": "cıgerayiş tamam nêbı (kod $1)", "virus-unknownscanner": "antiviruso ke nêzanyeno:", "logouttext": "'''Henda şıma hesab ra veciyay.'''\n\nDiqat kerê ke tayê perri şenê hewna zey şıma kewtê ra cı bıasê, heta şıma ver-virê şanekerê (browserê) xo besterê.", + "logout-failed": "Enewke ronıştışo nêracneyêno:$1", "cannotlogoutnow-title": "Enewke ronıştışo nêracneyêno", "cannotlogoutnow-text": "Gurenayışê $1i de veciyayış mımkın niyo.", "welcomeuser": "Heyr amey, $1!", @@ -735,6 +737,7 @@ "post-expand-template-argument-warning": "Tembe: No per de tewr tay yew şablono herayi esto.Nê vurnayeni ser çebyay", "post-expand-template-argument-category": "Pelê ke şablonê eyi qebul niye", "parser-template-loop-warning": "Gıreyê şabloni ca biyo: [[$1]]", + "template-loop-category": "dordorekê şabloniya peri", "parser-template-recursion-depth-warning": "limitê şablonê newekerdışi biyo de ($1)", "language-converter-depth-warning": "xoritiya çarnekarê zıwanan viyarnê ra ($1)", "node-count-exceeded-category": "Pela ra hetê kotya amardışê cı ravêrya", @@ -753,10 +756,10 @@ "undo-summary": "[[Special:Contributions/$2|$2]]i ([[User talk:$2|werênayış]]) vurnayışê $1i peyser gırewt", "undo-summary-username-hidden": "Rewizyona veri $1'i hewada", "cantcreateaccount-text": "Hesabvıraştışê na IP adrese ('''$1''') terefê [[User:$3|$3]] kılit biyo.\n\nSebebo ke terefê $3 ra diyao ''$2''", - "viewpagelogs": "Qeydanê na pele bımocne", + "viewpagelogs": "Qandê ena pela roceka bıvinê", "nohistory": "Verorê vurnayışanê na perer çıni yo.", "currentrev": "Çımraviyarnayışo rocane", - "currentrev-asof": "Çımraviyarnayışê $1iyo peyên", + "currentrev-asof": "$1 ra tepiya weziyeta pela", "revisionasof": "Çımraviyarnayışê $1", "revision-info": "Vurnayışo ke $1 de terefê {{GENDER:$6|$2}}$7 ra biyo", "previousrevision": "← Çımraviyarnayışo kıhanêr", @@ -767,8 +770,8 @@ "last": "verên", "page_first": "verên", "page_last": "peyên", - "histlegend": "Ferqê weçinayışi: Qutiya versiyonan qandé têversanayış işaret ke u dest be ''enter''i ya zi gocega cêrêne rone.
\nCetwel: ({{int:ferq}}) = ferqê versiyonê peyêni, ({{int:peyên}}) = ferqê versiyonê verêni, {{int:q}} = vırnayışo werdiyo.", - "history-fieldset-title": "Çımraviyarnayışan parzûn ke", + "histlegend": "Ferqê weçinayışi: Qutiya versiyonan qandê têversanayış işaret kerê u dest be ''enter''i ya zi gocega cêrêne rone.
\nCetwel: ({{int:ferq}}) = ferqê versiyonê peyêni, ({{int:peyên}}) = ferqê versiyonê verêni, {{int:q}} = vırnayışo werdiyo.", + "history-fieldset-title": "Revizyona parzun kerê", "history-show-deleted": "Tenya çımraviyarnayışanê esterıteyan bımocne", "histfirst": "Verênêr", "histlast": "Peyênêr", @@ -803,6 +806,7 @@ "revdelete-no-file": "Dosya diyarkerdiye çıniya.", "revdelete-show-file-confirm": "Şıma eminê ke wazenê çımraviyarnayışê esterıtey na dosya \"$1\" $2 ra $3 de bıvênê?", "revdelete-show-file-submit": "Eya", + "revdelete-selected-text": "Qandê [[:$2]] {{PLURAL:$1|weçinaye revizyon|weçinaye revizyoni}}:", "logdelete-selected": "{{PLURAL:$1|Qeydbiyayışo weçinıte|Qeydbiyayışê weçinıtey}}:", "revdelete-confirm": "Ma rica keno testiq bike ti ena hereket keno u ti zano neticeyanê herketanê xo u ti ena hereket pê ena [[{{MediaWiki:Policy-url}}|polici]] ra keno.", "revdelete-suppress-text": "Wedardış gani '''tenya''' nê halanê cêrênan de bıxebıtiyo:\n* Melumatê kıfırio mıhtemel\n* Melumatê şexio bêmınasıb\n*: ''adresa keyey u numreyê têlefoni, numreyê siğorta sosyale, uêb.''", @@ -874,7 +878,7 @@ "difference-title": "Pela \"$1\" ferqê çım ra viyarnayışan", "difference-title-multipage": "Ferkê pelan dê \"$1\" u \"$2\"", "difference-multipage": "(Ferqê pelan)", - "lineno": "Xeta $1:", + "lineno": "Satır $1:", "compareselectedversions": "Rewizyonanê weçineyan pêver ke", "showhideselectedversions": "weçinaye revizyona bımotne/bınımne", "editundo": "peyser bıgê", @@ -1123,7 +1127,7 @@ "right-reupload-own": "Dosyeyê ke to bar kerdi, inan sero bınuse", "right-reupload-shared": "Dosyeyê ke ambarê medyao barekerde de, inan mehelli wedare", "right-upload_by_url": "Yew URL ra dosyeyan bar ke", - "right-purge": "Virê sita seba yew pele bêdestur bestere.", + "right-purge": "Qandê yew pela vervirê site bıesterne", "right-autoconfirmed": "Perê ke nême kılit biyê, inan bıvurne", "right-bot": "Zey yew karê otomatiki kar bıvêne", "right-nominornewtalk": "Pelanê werênayışan rê vurnayışê qıckeki çıniyê, qutiya mesacanê newiyan bıgurene", @@ -1181,6 +1185,7 @@ "right-sendemail": "Karberanê binî ra e-mail bişirav", "right-managechangetags": "[[Special:Tags|Etiketi]] vıraz u aktiv (me)ke", "right-applychangetags": "[[Special:Tags|Etiketa]] vurnayışana piya dezge fi.", + "right-deletechangetags": "Database ra [[Special:Tags|etiketa]] bıesternê", "grant-generic": "\"$1\" paketa heqan", "grant-group-page-interaction": "Peran na tesiri", "grant-group-file-interaction": "Medya na tesiri", @@ -1268,6 +1273,10 @@ "action-applychangetags": "Vurnayışana piya etiket kerdışi zi dezge fi", "action-deletechangetags": "etitikan danegeh ra bestere", "action-purge": "Ane perer newe ke", + "action-blockemail": "Yew karberi rıştena e-maili ra bloke bıke", + "action-bot": "Yew karo otomatik deyne muamele bıkerê", + "action-editprotected": "\"{{int:protect-level-sysop}}\" şeveknaye pêlan de vırnayış bıkerê", + "action-editsemiprotected": "\"{{int:protect-level-autoconfirmed}}\" deyne şeveknaye pelan dê vurnayış bıkerê", "action-editinterface": "miyanriyê karberi bıvurne", "action-editusercss": "dosyeyanê CSSyê karberanê binan bıvurne", "action-edituserjson": "dosyeyanê JSONiyê karberanê binan bıvurne", @@ -1278,6 +1287,10 @@ "action-editmyusercss": "dosyeyanê CSSyê karberiya xo bıvurne", "action-editmyuserjson": "dosyeyanê JSONiyê karberiya xo bıvurne", "action-editmyuserjs": "dosyeyanê JavaScriptiyê karberiya xo bıvurne", + "action-viewsuppressed": "Karberan ra nımneyayen revizyona bıvênê", + "action-hideuser": "Yew nameyê karberi şari ra miyanki bloke bıkerê", + "action-ipblock-exempt": "Blokanê IPi, oto-blokan u blokanê menzıli ra ravêre", + "action-unblockself": "Blpqey ho wedarne", "nchanges": "$1 {{PLURAL:$1|vurnayış|vurnayışi}}", "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|ziyaretê peyêni ra nata}}", "enhancedrc-history": "tarix", @@ -1312,6 +1325,7 @@ "rcfilters-hours-title": "Seatê peyêni", "rcfilters-days-show-days": "($1 {{PLURAL:$1|roce|roci}})", "rcfilters-days-show-hours": "($1 {{PLURAL:$1|saete|saeti}})", + "rcfilters-highlighted-filters-list": "Wesıbneyayeni:$1", "rcfilters-quickfilters": "Parzûnê qeydbiyayeyi", "rcfilters-quickfilters-placeholder-title": "Qet yew parzûn qeyd nêbiyo", "rcfilters-quickfilters-placeholder-description": "Eyaranê parzûni qeydkerdış u bahdo zi seba gurenayışi rê, cêr de simgeyanê cayanê parzûnanê aktifan bıtıknê.", @@ -1334,6 +1348,7 @@ "rcfilters-empty-filter": "Parzûnê aktifi çıniyê. İştırakê cı pêro mocniyenê.", "rcfilters-filterlist-title": "Parzûni", "rcfilters-filterlist-whatsthis": "Nê çıtewri guriyenê?", + "rcfilters-highlightbutton-title": "Neticeyê wesıbneyayeni", "rcfilters-highlightmenu-title": "Yew reng weçine", "rcfilters-filterlist-noresults": "Parzûni nêvêniyayi", "rcfilters-filtergroup-authorship": "Wayiriya iştırakan", @@ -1681,8 +1696,8 @@ "linkstoimage-redirect": "$1 (Dosya raçarnayış) $2", "duplicatesoffile": "a {{PLURAL:$1|dosya|$1 dosya}}, kopyayê na dosyayi ([[Special:FileDuplicateSearch/$2|teferruati]]):", "sharedupload": "Ena dosya $1 ra u belki projeyê binan dı hewitiyeno.", - "sharedupload-desc-there": "Na dosya depoyê $1 de esta u terefê proceyanê binan ra gureniyena. \nCêr dê [şınasiya dosyay pela $2] mocniyeno.", - "sharedupload-desc-here": "Na dosya depoyê $1 de esta u terefê proceyanê binan ra gureniyena. \nCêr dê [şınasiya dosyay pela $2] mocniyeno.", + "sharedupload-desc-there": "Na dosya depoyê $1 de esta u terefê proceyanê binan ra gureniyena. \nCêr dê [$2 şınasiya dosyay pela] mocniyeno.", + "sharedupload-desc-here": "Na dosya depoyê $1 de esta u terefê proceyanê binan ra gureniyena. \nCêr dê [$2 şınasiya dosyay pela] mocniyeno.", "sharedupload-desc-edit": "Na dosya $1 proceyan dê binandı ke şeno bıgurweyno.\nŞıma qayılê ke malumatê cı bıvurnê se şıre [pela da $2 ].", "sharedupload-desc-create": "Na dosya $1 proceyan dê binandı ke şeno bıgurweyno.\nŞıma qayılê ke malumatê cı bıvurnê se şıre [pela da $2 ].", "filepage-nofile": "Ena name de dosya çin o.", @@ -1856,7 +1871,7 @@ "suppress": "Fetesnayene", "querypage-disabled": "Na pelaya xısusi,sebeb de performansi ra qefılneyê.", "apihelp": "Peştiya APIyi", - "apihelp-no-such-module": "Modulê \"$1\" çıniyo.", + "apihelp-no-such-module": "Modulê \"$1\" nêvineya.", "apisandbox": "API qumdor", "apisandbox-api-disabled": "API na site de dewre ra veciyayo.", "apisandbox-submit": "Bıwazê", @@ -2089,6 +2104,7 @@ "delete-warning-toobig": "no pel wayirê tarixê vurnayiş ê derg o, $1 {{PLURAL:$1|revizyonê|revizyonê}} seri de.\nhewn a kerdışê ıney {{SITENAME}} şuxul bıne gırano;\nbı diqqet dewam kerê.", "deleteprotected": "Şıma nêşenê ena perer esternê, çıkı per starya ya.", "rollback": "vurnayişan tepiya bıger", + "rollback-confirmation-confirm": "Araşt Kerê :", "rollback-confirmation-yes": "Peyser biya", "rollback-confirmation-no": "Bıtexelne", "rollbacklink": "ageyrayış", @@ -2099,9 +2115,9 @@ "cantrollback": "karbero peyin têna paşt dayo, no semedi ra vuriyayiş tepiya nêgeriyeni.", "alreadyrolled": "[[User:$2|$2]] ([[User talk:$2|Talk]]{{int:pipe-separator}} hetê [[Special:Contributions/$2|{{int:contribslink}}]]) ra perrê ıney[[:$1]] de vırnayış biyo u no vırnayiş tepeya nêgêriyeno;\nyewna ten perre de vırnayiş kerdo u perre tepiya nêgeriyeno.\n\noyo ke vırnayışo peyên kerdo: [[User:$3|$3]] ([[User talk:$3|Talk]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).", "editcomment": "Xulasay vurnayışi: $1 bi", - "revertpage": "Hetê [[Special:Contributions/$2|$2]] ([[User talk:$2|Mesac]]) ra vurnayiş biyo u ney vurnayişi tepiya geriyayo u no [[User:$1|$1]] kes o ke cuwa ver revizyon kerdo revizyonê no kesi tepiya anciyayo.", + "revertpage": "Terefê [[Special:Contributions/$2|$2]] ([[User talk:$2|Mesac]]) ra vurnayışê [[User:$1|$1]] peyser gêriyayo.", "revertpage-nouser": "No keso ke vuriyayiş kerdo vuriyayişé{{GENDER:$1|[[User:$1|$1]]}} ker o", - "rollback-success": "Terefê {{GENDER:$3|$1}}i ra vuriyayışi peyser gêriyayi; peyser geyriya be revizyonê {{GENDER:$4|$2}}i.", + "rollback-success": "Terefê {{GENDER:$3|$1}}i ra vuriyayış peyser gêriya; revizyonê {{GENDER:$4|$2}} peyser ard.", "sessionfailure-title": "Seans xeripiya", "sessionfailure": "cıkewtışê hesabê şıma de yew problem aseno;\nno kar semedê dızdiyê hesabi ibtal biyo.\nkerem kerê \"tepiya\" şiyerê u pel o ke şıma tera ameyî u o pel newe ra bar kerê , newe ra tesel/cereb kerê.", "changecontentmodel": "Modelê zerrekê pele bıvurne", @@ -2219,6 +2235,7 @@ "mycontris": "İştıraki", "anoncontribs": "İştıraki", "contribsub2": "Qandê {{GENDER:$3|$1}} ($2)", + "contributions-subtitle": "Qandê {{GENDER:$3|$1}}", "contributions-userdoesnotexist": "Hesabê karberi \"$1\" qeyd nêbiyo.", "nocontribs": "Ena kriteriya de vurnayîş çini yo.", "uctop": "weziyet", @@ -2229,6 +2246,7 @@ "sp-contributions-newbies-sub": "Qe hesebê newe", "sp-contributions-newbies-title": "Hesabanê neweyan rê iştırakê karberi", "sp-contributions-blocklog": "qeydê kılitkerdışi", + "sp-contributions-suppresslog": "İştirakê {{GENDER:$1|karberiyê}} degusneyayey", "sp-contributions-deleted": "iştırakê {{GENDER:$1|karberi}} esterdi", "sp-contributions-uploads": "Barkerdışi", "sp-contributions-logs": "qeydi", @@ -2817,7 +2835,7 @@ "metadata-expand": "Detayan bımotné", "metadata-collapse": "melumati bınımne", "metadata-fields": "Resımê meydanê metadataê ke na pele de benê lista, pela resımmocnaene de ke tabloê metadata gına waro, gureniyenê.\nÊ bini zey sayekerdoğan nımiyenê.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude", - "metadata-langitem": "'''$2:''' $1", + "metadata-langitem": "$2: $1", "metadata-langitem-default": "$1", "namespacesall": "pêro", "monthsall": "pêro", @@ -2839,6 +2857,7 @@ "confirmemail_body_set": "Jew ten, muhtemelen şıma no IP-adresi $1 ra,\nkeye pelê {{SITENAME}}i de pê no $2 e-postayi hesab kerda.\n\nEke raşta no e-posta eyê şıma yo şıma gani tesdiq bıkerî,\nqey tesdiq kerdışi gani karê e-postayê keyepeli {{SITENAME}} aktif bıbo, qey aktif kerdışi gıreyê cêrıni bıtıkne:\n\n$3\n\neke şıma hesab *nêakerdo*, qey ibtalê tesdiq kerdışê adresa e-postayi gıreyê cêrêni bıtıknê:\n\n$5\n\nkodê tesdiqi heta ıney tarixi $4 meqbul o.", "confirmemail_invalidated": "Konfermasyonê adres ê emaîlî iptal biy", "invalidateemail": "confirmasyonê e-maili iptal bik", + "notificationemail_subject_changed": "Site da {{SITENAME}} dı qeydın adresê eposta vurneya", "scarytranscludedisabled": "[Transcludê înterwîkîyî nihebityeno]", "scarytranscludefailed": "[Qe $1 fetch kerdişî nihebitiyeno]", "scarytranscludefailed-httpstatus": "[Qande $1 şablon nêşa bıgêriyo: HTTP $2]", @@ -2994,7 +3013,7 @@ "version": "Versiyon", "version-extensions": "Ekstensiyonî ke ronaye", "version-skins": "Bar kerde bejni", - "version-specialpages": "Pelê xısusiyi", + "version-specialpages": "Pelê bağsey", "version-parserhooks": "Çengelê Parserî", "version-variables": "Vurnayeyî", "version-editors": "Vurnayoği", @@ -3059,20 +3078,20 @@ "fileduplicatesearch-result-1": "Dosyayê ''$1î'' de hem-kopya çini yo.", "fileduplicatesearch-result-n": "Dosyayê ''$1î'' de {{PLURAL:$2|1 hem-kopya|$2 hem-kopyayî'}} esto.", "fileduplicatesearch-noresults": "Ebe namey \"$1\" ra dosya nêdiyayê.", - "specialpages": "Pelê xısusiyi", + "specialpages": "Pelê bağsey", "specialpages-note-top": "Kıtabek", "specialpages-note-restricted": "* Pelê xasê normali.\n* Pelê xasê nımıtey.", - "specialpages-group-maintenance": "Rapora pawıtışi", - "specialpages-group-other": "Pelê xısusiyê bini", - "specialpages-group-login": "Cı kewe / hesab vıraze", - "specialpages-group-changes": "Vurnayışê peyêni û qeydi", - "specialpages-group-media": "Raporê medya û barkerdışi", - "specialpages-group-users": "Karberi u heqê inan", - "specialpages-group-highuse": "Pelê ke zêdêr gureniyenê", - "specialpages-group-pages": "Listeyê pelan", + "specialpages-group-maintenance": "Raporê weynayışi", + "specialpages-group-other": "Pelê bağseyê bini", + "specialpages-group-login": "Ronıştış akerê / hesab vıraze", + "specialpages-group-changes": "Vurnayışê peyêni û Roceki", + "specialpages-group-media": "Raporê medyay u barkerdışi", + "specialpages-group-users": "Karberi u Heqi", + "specialpages-group-highuse": "Pelê zaf karnıyayey", + "specialpages-group-pages": "Listey peleyan", "specialpages-group-pagetools": "Haletê pelan", "specialpages-group-wiki": "Melumat u haceti", - "specialpages-group-redirects": "Pelê serşıkıtışiyê xısusiyi", + "specialpages-group-redirects": "Pelê bağseyê serşıkıtışini", "specialpages-group-spam": "haletê spami", "specialpages-group-developer": "Xacetanê raverberdoğî", "blankpage": "Pela venge", @@ -3170,6 +3189,7 @@ "htmlform-datetime-placeholder": "SSSS-AA-RR SS:DD:SS", "logentry-delete-delete": "$1 perra $3 {{GENDER:$2|esterıte}}", "logentry-delete-restore": "$1 pela $3 ($4) {{GENDER:$2|peyser arde}}", + "logentry-delete-restore-nocount": "$1, pela $3 {{GENDER:$2|timar kerd }}", "restore-count-revisions": "{{PLURAL:$1|1 çımraviyarnayış|$1 çımraviyarnayışi}}", "restore-count-files": "{{PLURAL:$1|1 dosya|$1 dosyeyi}}", "logentry-delete-event": "$1 $3: $4 de asayışê {{PLURAL:$5|cıkerdışi|cıkerdışan}} {{GENDER:$2|vurna}}", @@ -3189,8 +3209,11 @@ "revdelete-uname-unhid": "nameyê karberi nênımıteyo", "revdelete-restricted": "vergırewtışê ke xızmekaran rê biye", "revdelete-unrestricted": "vergırewtışê ke xızmekaran rê dariyê we", + "logentry-block-block": "$1, karber {{GENDER:$4|$3}} $5 demi rê {{GENDER:$2|kerd men}} $6", + "logentry-block-unblock": "$1, {{GENDER:$4|$3}} {{GENDER:$2|men kerdış wedarna}}", "logentry-partialblock-block-page": "{{PLURAL:$1|pele|peli}} $2", "logentry-partialblock-block-ns": "{{PLURAL:$1|cayê nameyi|cayê nameyan}} $2", + "logentry-import-upload": "$1 {{GENDER:$2|zere kerdışa }} $3'i Dosya kerd bar.", "logentry-move-move": "$1, pela $3 ra {{GENDER:$2|kırışt}} pela $4", "logentry-move-move-noredirect": "$1, pera $3'i bêhetenayış {{GENDER:$2|kırışt}} pera $4`i", "logentry-move-move_redir": "$1 {{GENDER:$2|kırışna}} riperr $3 be $4 weçarnayış sera.", @@ -3326,7 +3349,7 @@ "mw-widgets-abandonedit": "Qeydkerdışi ra ravêr, şıma qayılê peyser şêrê asayışo vêrên?", "mw-widgets-abandonedit-discard": "Vurnayışan vece", "mw-widgets-abandonedit-keep": "Vurnayışi rê dewam ke", - "mw-widgets-abandonedit-title": "Vac welay?", + "mw-widgets-abandonedit-title": "Şıma bêgumanê?", "mw-widgets-copytextlayout-copy": "Kopya", "mw-widgets-dateinput-no-date": "Tarix nêweçiniya", "mw-widgets-dateinput-placeholder-day": "SSSS-AA-RR", @@ -3340,6 +3363,7 @@ "mw-widgets-titlesmultiselect-placeholder": "Tayêna cı ke...", "date-range-from": "Nê tarixi ra:", "date-range-to": "Heta nê tarixi:", + "sessionprovider-generic": "Ronıştışê $1", "randomrootpage": "Pela raştameya rıçıkıne", "log-action-filter-block": "Tewrê kılitkerdışi:", "log-action-filter-contentmodel": "Tewrê vurnayışê modelê zerreki:", @@ -3365,6 +3389,13 @@ "log-action-filter-delete-revision": "Esterıtışê çımraviyarnayışi", "log-action-filter-import-interwiki": "Zerrenayışê Transwikiyi", "log-action-filter-import-upload": "Ebe barkerdışê XMLi ra zerre ke", + "log-action-filter-managetags-create": "Etiket vıraştış", + "log-action-filter-managetags-delete": "Etiket esternayış", + "log-action-filter-managetags-activate": "Etiket raştkerdış", + "log-action-filter-managetags-deactivate": "Etiket hewadayış", + "log-action-filter-newusers-autocreate": "Otomatik vıraştış", + "log-action-filter-patrol-patrol": "Dewriyeyo menuel", + "log-action-filter-patrol-autopatrol": "Dewriyeyo otomatik", "log-action-filter-protect-protect": "Şeveknayış", "log-action-filter-protect-modify": "Vurnayışê şeveknayışi", "log-action-filter-protect-unprotect": "Şeveknayışi wedare", diff --git a/languages/i18n/el.json b/languages/i18n/el.json index 59d12bad31..e0ae02b0f1 100644 --- a/languages/i18n/el.json +++ b/languages/i18n/el.json @@ -2091,7 +2091,7 @@ "cachedspecial-refresh-now": "Προβολή τελευταίας.", "categories": "Κατηγορίες", "categories-submit": "Εμφάνιση", - "categoriespagetext": "{{PLURAL:$1|Η ακόλουθη κατηγορία υπάρχει|Οι ακόλουθες κατηγορίες υπάρχουν}} σε αυτό το wiki, και μπορεί ή μπορεί να μην είναι {{PLURAL:$1|αχρησιμοποίητη|αχρησιμοποίητες}}.\nΔείτε τις ενεργές Κατηγορίες στο [[:Κατηγορία:Βικιλεξικό|'''Βικιλεξικό''']]. Δείτε επίσης τις [[Special:WantedCategories|ζητούμενες κατηγορίες]].", + "categoriespagetext": "{{PLURAL:$1|Η ακόλουθη κατηγορία υπάρχει|Οι ακόλουθες κατηγορίες υπάρχουν}} σε αυτό το wiki, και μπορεί ή μπορεί να μην είναι {{PLURAL:$1|αχρησιμοποίητη|αχρησιμοποίητες}}. Δείτε επίσης τις [[Special:WantedCategories|ζητούμενες κατηγορίες]].", "categoriesfrom": "Εμφάνιση κατηγοριών που αρχίζουν από:", "deletedcontributions": "Διαγεγραμμένες συνεισφορές χρήστη", "deletedcontributions-title": "Διαγεγραμμένες συνεισφορές χρήστη", @@ -2254,7 +2254,7 @@ "deletionlog": "Καταγραφές διαγραφών", "log-name-create": "Αρχείο καταγραφών δημιουργίας σελίδων", "log-description-create": "Παρακάτω υπάρχει ένας κατάλογος των πιο πρόσφατων δημιουργιών σελίδας.", - "logentry-create-create": "$1 δημιούργησε τη σελίδα $3", + "logentry-create-create": "{{GENDER:$2|Ο|Η}} $1 δημιούργησε τη σελίδα $3", "reverted": "Επαναφορά σε προηγούμενη αναθεώρηση", "deletecomment": "Λόγος:", "deleteotherreason": "Άλλος/πρόσθετος λόγος:", diff --git a/languages/i18n/en-gb.json b/languages/i18n/en-gb.json index 6f81c4b95c..b42423f28f 100644 --- a/languages/i18n/en-gb.json +++ b/languages/i18n/en-gb.json @@ -196,7 +196,7 @@ "history": "Page history", "history_short": "History", "history_small": "history", - "updatedmarker": "updated since my last visit", + "updatedmarker": "updated since your last visit", "printableversion": "Printable version", "permalink": "Permanent link", "print": "Print", diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 425cf2ba37..e33e9bdc7c 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -4196,6 +4196,16 @@ "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use:
0.0.0.0/0\n::/0
", "edit-error-short": "Error: $1", "edit-error-long": "Errors:\n\n$1", + "specialmute": "Mute", + "specialmute-success": "Your mute preferences have been successfully updated. See all muted users in [[Special:Preferences]].", + "specialmute-submit": "Confirm", + "specialmute-label-mute-email": "Mute emails from this user", + "specialmute-header": "Please select your mute preferences for {{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "The username requested could not be found.", + "specialmute-error-email-blacklist-disabled": "Muting users from sending you emails is not enabled.", + "specialmute-error-email-preferences": "You must confirm your email address before you can mute a user. You may do so from [[Special:Preferences]].", + "specialmute-email-footer": "To manage email preferences for {{BIDI:$2}} please visit <$1>.", + "specialmute-login-required": "Please log in to change your mute preferences.", "revid": "revision $1", "pageid": "page ID $1", "interfaceadmin-info": "$1\n\nPermissions for editing of sitewide CSS/JS/JSON files were recently separated from the editinterface right. If you do not understand why you are getting this error, see [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/eo.json b/languages/i18n/eo.json index 760e8b80a4..b1bc55d662 100644 --- a/languages/i18n/eo.json +++ b/languages/i18n/eo.json @@ -222,7 +222,7 @@ "history": "Paĝa historio", "history_short": "Historio", "history_small": "historio", - "updatedmarker": "ĝisdatigita de post mia lasta vizito", + "updatedmarker": "ĝisdatigita de post via lasta vizito", "printableversion": "Presebla versio", "permalink": "Konstanta ligilo", "print": "Presi", @@ -3334,7 +3334,7 @@ "tag-mw-new-redirect-description": "Redaktoj kiuj kreas novajn alidirektigilojn aŭ ŝanĝas paĝojn al alidirektigiloj", "tag-mw-removed-redirect": "Forigis alidirektilon", "tag-mw-removed-redirect-description": "Redaktoj kiuj ŝanĝas ekzistintan alidirektigilon al ne-alidirektigilon", - "tag-mw-changed-redirect-target": "Ŝanĝis celon de alidirektilon", + "tag-mw-changed-redirect-target": "Ŝanĝis celon de alidirektilo", "tag-mw-changed-redirect-target-description": "Redaktoj kiuj ŝanĝas la celon de alidirektigilo", "tag-mw-blank": "Vakigo", "tag-mw-blank-description": "Redaktoj kiuj vakigis paĝon", @@ -3859,6 +3859,16 @@ "restrictionsfield-help": "Unu IP-adreso aŭ CIDR-intervalo per linio. Por permesigi ĉion, uzu:
0.0.0.0/0\n::/0
", "edit-error-short": "Eraro: $1", "edit-error-long": "Eraroj:\n\n$1", + "specialmute": "Silentigi", + "specialmute-success": "Sukcese ĝisdatiĝis viaj preferoj pri kaŝado de mesaĝoj. Vi povas vidi ĉiujn silentigitajn uzantojn ĉe [[Special:Preferences]].", + "specialmute-submit": "Konfirmi", + "specialmute-label-mute-email": "Kaŝi retmesaĝojn el ĉi tiu uzanto", + "specialmute-header": "Bonvolu elekti viajn preferojn pri kaŝado de mesaĝoj el {{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "La petita uzantnomo ne troviĝis.", + "specialmute-error-email-blacklist-disabled": "Malŝaltiĝis kaŝado de retmesaĝoj el specifaj uzantoj.", + "specialmute-error-email-preferences": "Vi povas konfirmi vian retpoŝtan adreson, antaŭ vi povas kaŝi mesaĝojn. Vi povas tion fari ĉe [[Special:Preferences]].", + "specialmute-email-footer": "[$1 Administri preferojn pri retpoŝto por {{BIDI:$2}}.]", + "specialmute-login-required": "Bonvolu ensaluti por konservi vian preferon pri kaŝado de mesaĝoj.", "revid": "revizio $1", "pageid": "Identigilo de paĝo $1", "interfaceadmin-info": "$1\n\nPermesoj pri redaktado de tut-retejaj CSS/JavaScript/JSON-dosieroj estis lastatempe disigitaj for de la rajto editinterface. Se vi ne komprenas kial vi ricevis ĉi tiun eraron, vidu la paĝon [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/es.json b/languages/i18n/es.json index 34a3ec00f8..87bee6d801 100644 --- a/languages/i18n/es.json +++ b/languages/i18n/es.json @@ -354,7 +354,7 @@ "history": "Historial", "history_short": "Historial", "history_small": "historial", - "updatedmarker": "actualizado desde mi última visita", + "updatedmarker": "actualizado desde tu última visita", "printableversion": "Versión para imprimir", "permalink": "Enlace permanente", "print": "Imprimir", @@ -3871,8 +3871,8 @@ "log-action-filter-managetags-deactivate": "Desactivación de etiquetas", "log-action-filter-move-move": "Traslado sin sobrescritura de redirecciones", "log-action-filter-move-move_redir": "Traslado con sobrescritura de redirecciones", - "log-action-filter-newusers-create": "La creación por usuario anónimo", - "log-action-filter-newusers-create2": "La creación por usuario registrado", + "log-action-filter-newusers-create": "Creación por usuario anónimo", + "log-action-filter-newusers-create2": "Creación por usuario registrado", "log-action-filter-newusers-autocreate": "Creación automática", "log-action-filter-newusers-byemail": "Creación con la contraseña enviada por correo", "log-action-filter-patrol-patrol": "Verificación manual", @@ -3968,6 +3968,11 @@ "restrictionsfield-help": "Una dirección IP o intervalo de CIDR por renglón. Para activarlo todo, utiliza
0.0.0.0/0\n::/0
", "edit-error-short": "Error: $1", "edit-error-long": "Errores:\n\n$1", + "specialmute": "Silenciar", + "specialmute-submit": "Confirmar", + "specialmute-label-mute-email": "Silenciar los correos electrónicos de este usuario", + "specialmute-error-invalid-user": "No se encontró el nombre de usuario solicitado.", + "specialmute-error-email-preferences": "Debes confirmar tu dirección de correo electrónico antes de que puedas silenciar a un usuario. Puedes hacerlo desde [[Special:Preferences|tus preferencias]].", "revid": "revisión $1", "pageid": "ID de página $1", "interfaceadmin-info": "$1\n\nLos permisos para editar los archivos con formato CSS, JS y JSON en todo el sitio han sido recientemente separados del permiso editinterface. Si no comprendes por qué recibes este error, por favor lee [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/exif/ml.json b/languages/i18n/exif/ml.json index 4e35fab695..26398ed902 100644 --- a/languages/i18n/exif/ml.json +++ b/languages/i18n/exif/ml.json @@ -31,7 +31,7 @@ "exif-make": "ഛായാഗ്രാഹി നിർമ്മാതാവ്", "exif-model": "ഛായാഗ്രാഹി മോഡൽ", "exif-software": "ഉപയോഗിച്ച സോഫ്റ്റ്‌വെയർ", - "exif-artist": "ഛായാഗ്രാഹകൻ", + "exif-artist": "ഛായാഗ്രാഹക(ൻ)", "exif-copyright": "പകർപ്പവകാശ ഉടമ", "exif-exifversion": "എക്സിഫ് (Exif) പതിപ്പ്", "exif-flashpixversion": "പിന്തുണയുള്ള ഫ്ലാഷ്‌‌പിക്സ് പതിപ്പ്", diff --git a/languages/i18n/exif/sr-ec.json b/languages/i18n/exif/sr-ec.json index cf2b244254..044703bf08 100644 --- a/languages/i18n/exif/sr-ec.json +++ b/languages/i18n/exif/sr-ec.json @@ -6,7 +6,8 @@ "Milicevic01", "Rancher", "Sasa Stefanovic", - "Сербијана" + "Сербијана", + "Zoranzoki21" ] }, "exif-imagewidth": "Ширина", @@ -208,8 +209,11 @@ "exif-photometricinterpretation-2": "RGB", "exif-photometricinterpretation-3": "Палета", "exif-photometricinterpretation-4": "Маска транспарентности", + "exif-photometricinterpretation-5": "Одвојено (вероватно CMYK)", "exif-photometricinterpretation-6": "YCbCr", "exif-photometricinterpretation-8": "CIE L*a*b*", + "exif-photometricinterpretation-9": "CIE L*a*b* (ICC кодирање)", + "exif-photometricinterpretation-10": "CIE L*a*b* (ITU кодирање)", "exif-unknowndate": "Непознат датум", "exif-orientation-1": "Нормално", "exif-orientation-2": "Обрнуто по хоризонтали", diff --git a/languages/i18n/exif/zh-hans.json b/languages/i18n/exif/zh-hans.json index 41ff061ad4..2a07679cee 100644 --- a/languages/i18n/exif/zh-hans.json +++ b/languages/i18n/exif/zh-hans.json @@ -8,7 +8,8 @@ "Liuxinyu970226", "PhiLiP", "Qiyue2001", - "Xiaomingyan" + "Xiaomingyan", + "神樂坂秀吉" ] }, "exif-imagewidth": "宽度", @@ -197,8 +198,10 @@ "exif-copyrighted-false": "版权状态未设定", "exif-photometricinterpretation-0": "黑白(白为0)", "exif-photometricinterpretation-1": "黑白(黑为0)", + "exif-photometricinterpretation-3": "主色调", "exif-photometricinterpretation-4": "透明遮罩", "exif-photometricinterpretation-5": "分隔(可能是CMYK)", + "exif-photometricinterpretation-8": "CIE L*a*b*", "exif-photometricinterpretation-9": "CIE L*a*b*(ICC编码)", "exif-photometricinterpretation-10": "CIE L*a*b*(ITU编码)", "exif-photometricinterpretation-32803": "色彩滤镜矩阵", diff --git a/languages/i18n/fa.json b/languages/i18n/fa.json index 80b83aea9e..5f0f2c96f7 100644 --- a/languages/i18n/fa.json +++ b/languages/i18n/fa.json @@ -238,7 +238,7 @@ "history": "تاریخچهٔ صفحه", "history_short": "تاریخچه", "history_small": "تاریخچه", - "updatedmarker": "به‌روزشده از آخرین باری که سرزده‌ام", + "updatedmarker": "به‌روزشده از آخرین باری که سرزده‌اید", "printableversion": "نسخهٔ قابل چاپ", "permalink": "پیوند پایدار", "print": "چاپ", @@ -410,7 +410,7 @@ "title-invalid-leading-colon": "عنوان صفحهٔ درخواستی دارای دونقطهٔ نامجاز در ابتدایش است.", "perfcached": "داده‌های زیر از حافظهٔ نهانی فراخوانی شده‌اند و ممکن است کاملاً به‌روز نباشند. حداکثر {{PLURAL:$1|یک نتیجه| $1 نتیجه}} در حافظهٔ نهانی قابل دسترس است.", "perfcachedts": "داده‌های زیر از حافظهٔ نهانی فراخوانی شده‌اند و آخرین بار در $1 به‌روزرسانی شدند. حداکثر {{PLURAL:$4|یک نتیجه|$4 نتیجه}} در حافظهٔ نهانی قابل دسترس است.", - "querypage-no-updates": "امکان به‌روزرسانی این صفحه فعلاً غیرفعال شده‌است.\nاطلاعات این صفحه ممکن است به‌روز نباشد.", + "querypage-no-updates": "روزآمدسازی این صفحه هم‌اکنون غیر فعال است.\nداده‌های این صفحه در حال حاضر، بازآوری نمی‌شود.", "viewsource": "نمایش مبدأ", "viewsource-title": "نمایش مبدأ برای $1", "actionthrottled": "جلوی عمل شما گرفته شد", @@ -602,7 +602,7 @@ "botpasswords-created-title": "گذرواژه ربات ایجاد شد", "botpasswords-created-body": "گذرواژهٔ رباتی برای ربات «$1» و {{GENDER:$2|کاربر}} «$2» ایجاد شد.", "botpasswords-updated-title": "گذرواژه ربات روزآمد شد", - "botpasswords-updated-body": "گذرواژهٔ رباتی برای ربات «$1» و {{GENDER:$2|کاربر}} «$2» به‌روز شد.", + "botpasswords-updated-body": "گذرواژهٔ رباتی برای ربات «$1» {{GENDER:$2|کاربر}} «$2» روزآمد شد.", "botpasswords-deleted-title": "گذرواژه ربات حذف شد", "botpasswords-deleted-body": "گذرواژهٔ رباتی برای ربات «$1» و {{GENDER:$2|کاربر}} «$2» حذف شد.", "botpasswords-newpassword": "$2 گذرواژهٔ جدید برای ورود با حساب $1 است. لطفاً آن را برای ارجاع در آینده ذخیره کنید.
(برای ربات‌های قدیمی که نیاز به نام کاربری مطابق با حساب کاربری‌شان دارد، شما می‌توانید از $3 به عنوان نام کاربری و از $4 به عنوان گذرواژه استفاده کنید.)", @@ -714,6 +714,8 @@ "autoblockedtext": "دسترسی نشانی آی‌پی شما قطع شده‌است، زیرا این نشانی آی‌پی توسط کاربر دیگری استفاده شده که دسترسی او توسط $1 قطع شده‌است.\nدلیل ارائه‌شده چنین است:\n\n:''$2''\n\n* شروع قطع دسترسی: $8\n* پایان قطع دسترسی: $6\n* کاربری هدف قطع دسترسی: $7\n\nشما می‌توانید با $1 یا [[{{MediaWiki:Grouppage-sysop}}|مدیری]] دیگر تماس بگیرید و در این باره صحبت کنید.\nتوجه کنید که شما نمی‌توانید از قابلیت «{{int:emailuser}}» استفاده کنید مگر آنکه نشانی ایمیل معتبری در [[Special:Preferences|ترجیحات کاربری]] خودتان ثبت کرده باشید و نیز باید امکان استفاده از این قابلیت برای شما قطع نشده باشد.\nنشانی آی‌پی فعلی شما $3 و شمارهٔ قطع دسترسی شما $5 است.\nلطفاً تمامی جزئیات فوق را در کلیهٔ درخواست‌هایی که در این باره مطرح می‌کنید ذکر کنید.", "systemblockedtext": "نام کاربری یا نشانی آی‌پی شما خودکار توسط مدیاویکی مسدود شده‌است.\nدلیل ارائه‌شده:\n\n:$2\n\n* آغاز بلاک: $8\n* پایان بلاک: $6\n* قطع دسترسی‌شده مورد نظر: $7\n\nنشانی آی‌پی کنونی شما $3 است.\nخواهشمند است تمام جزئیات بالا را در هر پرس‌وجویی که انجام می‌دهید قرار دهید.", "blockednoreason": "دلیلی مشخص نشده‌است", + "blockedtext-composite": "نام کاربری یا نشانی آی‌پی شما خودکار توسط مدیاویکی مسدود شده‌است.\nدلیل ارائه‌شده:\n\n:$2\n\n* آغاز بلاک: $8\n* پایان بلاک: $6\n\nنشانی آی‌پی کنونی شما $3 است.\nخواهشمند است تمام جزئیات بالا را در هر پرس‌وجویی که انجام می‌دهید قرار دهید.", + "blockedtext-composite-reason": "حساب/آی‌پی شما به چند طریق بسته شده‌است", "whitelistedittext": "برای ویرایش مقاله‌ها باید $1.", "confirmedittext": "شما باید، پیش از ویرایش صفحات، آدرس ایمیل خود را مشخص و تأیید کنید. لطفاً از طریق [[Special:Preferences|ترجیحات کاربر]] این کار را صورت دهید.", "nosuchsectiontitle": "چنین بخشی پیدا نشد", @@ -930,7 +932,7 @@ "revdelete-unsuppress": "حذف محدودیت‌ها در بازبینی‌های ترمیم‌شده", "revdelete-log": "دلیل:", "revdelete-submit": "اعمال بر {{PLURAL:$1|نسخهٔ|نسخه‌های}} انتخاب شده", - "revdelete-success": "'''پیدایی نسخه به روز شد.'''", + "revdelete-success": "پیدایی بازنگری، روزآمد شد.", "revdelete-failure": "'''پیدایی نسخه‌ها قابل به روز کردن نیست:'''\n$1", "logdelete-success": "تغییر پیدایی مورد انجام شد.", "logdelete-failure": "'''پیدایی سیاهه‌ها قابل تنظیم نیست:'''\n$1", @@ -1314,11 +1316,11 @@ "grant-createaccount": "ایجاد حساب‌های کاربری", "grant-createeditmovepage": "ایجاد، ویرایش و انتقال صفحات", "grant-delete": "حذف صفحات، نسخه‌های ویرایش و سیاهه ورودی", - "grant-editinterface": "ویرایش صفحه‌های جی‌سان کاربری یا سراسری و فضای نام مدیاویکی", + "grant-editinterface": "ویرایش فضای نام مدیاویکی و JSONهای کاربری/وب‌گاه‌مبنا", "grant-editmycssjs": "ویرایش CSS /جاوااسکریپت/JSON کاربری", "grant-editmyoptions": "اولویت‌های کاربری و پیکربندی JSON را ویرایش کنید", "grant-editmywatchlist": "ویرایش فهرست پی‌گیری‌هایتان", - "grant-editsiteconfig": "ویرایش گسترده CSS/JS کاربر", + "grant-editsiteconfig": "ویرایش CSS/JS کاربری و وب‌گاه‌مبنا", "grant-editpage": "ویرایش صفحات موجود", "grant-editprotected": "ویرایش صفحه محافظت شده", "grant-highvolume": "ویرایش با حجم بالا", @@ -1976,7 +1978,7 @@ "pageswithprop-prophidden-binary": "جزییات مقدار مخفی باینری ($1)", "doubleredirects": "تغییرمسیرهای دوتایی", "doubleredirectstext": "این صفحه فهرستی از صفحه‌های تغییرمسیری را ارائه می‌کند که به صفحهٔ تغییرمسیر دیگری اشاره می‌کنند.\nهر سطر دربردارندهٔ پیوندهایی به تغییرمسیر اول و دوم و همچنین مقصد تغییرمسیر دوم است، که معمولاً صفحهٔ مقصد واقعی است و نخستین تغییرمسیر باید به آن اشاره کند.\nموارد خط خورده درست شده‌اند.", - "double-redirect-fixed-move": "[[$1]] انتقال داده شده‌است.\n\nبه صورت خودکار به‌روز شده‌است و تغییرمسیری به [[$2]] داده شد.", + "double-redirect-fixed-move": "[[$1]] انتقال داده شده است.\nبه‌صورت خودکار روزآمد شده و هم‌اکنون به [[$2]] تغییر مسیر داده شده است.", "double-redirect-fixed-maintenance": "رفع خودکار تغییرمسیر دوتایی از [[$1]] به [[$2]] در روند نگهداری", "double-redirect-fixer": "تعمیرکار تغییرمسیرها", "brokenredirects": "تغییرمسیرهای خراب", @@ -3184,7 +3186,7 @@ "watchlistedit-raw-explain": "عنوان‌های موجود در فهرست پی‌گیری‌های شما در زیر نشان داده شده‌اند، و شما می‌توانید مواردی را حذف یا اضافه کنید؛ هر مورد در یک سطر جداگانه باید قرار بگیرد.\nدر پایان، دکمهٔ «{{int:Watchlistedit-raw-submit}}» را بفشارید.\nتوجه کنید که شما می‌توانید از [[Special:EditWatchlist|ویرایشگر استاندارد فهرست پی‌گیری‌ها]] هم استفاده کنید.", "watchlistedit-raw-titles": "عنوان‌ها:", "watchlistedit-raw-submit": "روزآمدسازی پی‌گیری‌ها", - "watchlistedit-raw-done": "فهرست پی‌گیری‌های شما به روز شد.", + "watchlistedit-raw-done": "فهرست پی‌گیری‌های شما روزآمد شد.", "watchlistedit-raw-added": "$1 عنوان به فهرست پی‌گیری‌ها اضافه {{PLURAL:$1|شد|شدند}}:", "watchlistedit-raw-removed": "$1 عنوان حذف {{PLURAL:$1|شد|شدند}}:", "watchlistedit-clear-title": "پاک کردن فهرست پی‌گیری‌ها", @@ -3256,7 +3258,7 @@ "timezone-local": "محلی", "duplicate-defaultsort": "هشدار: ترتیب پیش‌فرض «$2» ترتیب پیش‌فرض قبلی «$1» را باطل می‌کند.", "duplicate-displaytitle": "هشدار: نمایش عنوان \" $2 \"باعث ابطال پیش نمایش عنوان\" $1 \" می‌شود.", - "restricted-displaytitle": "هشدار: از آنجايي که عنوان نمایشی «$1» با عنوان اصلی صفحه یکی نبود، مورد اغماز قرار گرفت.", + "restricted-displaytitle": "هشدار: از آنجایی که عنوان نمایشی «$1» با عنوان اصلی صفحه یکی نبود، نادیده گرفته شد.", "invalid-indicator-name": "خطا:ویژگی های شاخص‌های وضعیت صفحهٔ name نباید خالی باشند.", "version": "نسخه", "version-extensions": "افزونه‌های نصب‌شده", @@ -3879,6 +3881,16 @@ "restrictionsfield-help": "یک نشانی آی‌پی یا بازهٔ سی‌آی‌دی‌ار در هر خط وارد کنید. برای فعال کردن همه‌چیز، این مقدار را استفاده کنید: 0.0.0.0/0
::/0", "edit-error-short": "خطا: $1", "edit-error-long": "خطاها:\n\n$1", + "specialmute": "بی‌صدا", + "specialmute-success": "تنظیمات بی‌صدا به روز شد. دیدن فهرست همهٔ کاربرانی که در [[Special:Preferences|ترجیحاتتان]] به عنوان بی‌صدا انتخاب کردید.", + "specialmute-submit": "تأیید", + "specialmute-label-mute-email": "بی‌صدا کردن ایمیل از این کاربر", + "specialmute-header": "لطفاً ترجیحات بی‌صدا برای {{BIDI:[[User:$1]]}} را انتخاب کنید.", + "specialmute-error-invalid-user": "نام کاربری درخواست شده یافت نشد.", + "specialmute-error-email-blacklist-disabled": "بی‌صدا کردن کاربران برای ارسال ایمیل فعال نشده‌است.", + "specialmute-error-email-preferences": "پیش از بی‌صدا کردن دیگر کاربران باید آدرس ایمیلیتان را تائید کنید. که از [[Special:Preferences|ترجیحاتتان]] مقدور است.", + "specialmute-email-footer": "[$1 مدیریت ترجیحات ایمیل برای {{BIDI:$2}}.]", + "specialmute-login-required": "لطفاً برای تغییر ترجیحات بی‌صدا به سامانه وارد شوید.", "revid": "نسخهٔ $1", "pageid": "شناسهٔ صفحهٔ $1", "interfaceadmin-info": "\n$1\n\nدسترسی‌ها برای ویرایش فایل‌های CSS/JS/JSON که اخیراً از دسترسی editinterface جدا شده‌اند. اگر نمی دانید که چرا این خطا رخ داده‌است [[mw:MediaWiki_1.32/interface-admin]] را مطالعه کنید.", diff --git a/languages/i18n/fr.json b/languages/i18n/fr.json index e2e55074d9..3a79359186 100644 --- a/languages/i18n/fr.json +++ b/languages/i18n/fr.json @@ -337,7 +337,7 @@ "history": "Historique de la page", "history_short": "Historique", "history_small": "historique", - "updatedmarker": "modifié depuis ma dernière visite", + "updatedmarker": "modifié depuis votre dernière visite", "printableversion": "Version imprimable", "permalink": "Lien permanent", "print": "Imprimer", @@ -1529,6 +1529,7 @@ "action-override-export-depth": "exporter les pages en incluant les pages liées jusqu’à une profondeur de 5 niveaux", "action-suppressredirect": "ne pas créer de redirections depuis les pages sources lors du renommage", "nchanges": "$1 modification{{PLURAL:$1||s}}", + "ntimes": "$1×", "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|depuis la dernière visite}}", "enhancedrc-history": "historique", "recentchanges": "Modifications récentes", @@ -2313,6 +2314,8 @@ "listgrouprights-rights": "Droits associés", "listgrouprights-helppage": "Help:Droits de groupes", "listgrouprights-members": "(liste des membres)", + "listgrouprights-right-display": "$1 ($2)", + "listgrouprights-right-revoked": "$1 ($2)", "listgrouprights-addgroup": "Ajouter des membres {{PLURAL:$2|au groupe|aux groupes}} : $1", "listgrouprights-removegroup": "Retirer des membres {{PLURAL:$2|du groupe|des groupes}} : $1", "listgrouprights-addgroup-all": "Ajouter des membres à tous les groupes", @@ -2529,6 +2532,7 @@ "protect-fallback": "Autoriser uniquement les utilisateurs avec le droit « $1 »", "protect-level-autoconfirmed": "Autoriser uniquement les utilisateurs autoconfirmés", "protect-level-sysop": "Autoriser uniquement les administrateurs", + "protect-summary-desc": "[$1=$2] ($3)", "protect-summary-cascade": "protection en cascade", "protect-expiring": "expire le $1 (UTC)", "protect-expiring-local": "expire le $1", @@ -2594,6 +2598,7 @@ "undelete-error-long": "Des erreurs ont été rencontrées lors de la restauration du fichier :\n\n$1", "undelete-show-file-confirm": "Êtes-vous sûr{{GENDER:||e}} de vouloir consulter une version supprimée du fichier « $1 » datant du $2 à $3 ?", "undelete-show-file-submit": "Oui", + "undelete-revision-row2": "$1 ($2) $3 . . $4 $5 $6 $7 $8", "namespace": "Espace de noms :", "invert": "Inverser la sélection", "tooltip-invert": "Cochez cette case pour cacher les modifications des pages dans l'espace de noms sélectionné (et l'espace de noms associé si coché)", @@ -2781,6 +2786,7 @@ "ip_range_toolow": "Les intervalles d'adresses IP ne sont effectivement pas autorisés.", "proxyblocker": "Bloqueur de serveurs mandataires", "proxyblockreason": "Votre adresse IP a été bloquée car c'est celle d’un serveur mandataire ouvert.\nVeuillez contacter votre fournisseur d’accès à Internet ou votre service d’assistance technique et l’informer de ce sérieux problème de sécurité.", + "sorbs": "DNSBL", "sorbsreason": "Votre adresse IP est listée comme mandataire ouvert dans le DNSBL utilisé par {{SITENAME}}.", "sorbs_create_account_reason": "Votre adresse IP est listée comme mandataire ouvert dans le DNSBL utilisé par {{SITENAME}}.\nVous ne pouvez pas créer un compte.", "softblockrangesreason": "Les contributions anonymes ne sont pas autorisées à partir de votre adresse IP ($1). Veuillez vous connecter.", @@ -3097,6 +3103,7 @@ "pageinfo-few-watchers": "Moins de $1 {{PLURAL:$1|observateur|observateurs}}", "pageinfo-few-visiting-watchers": "Il peut ou non y avoir un observateur regardant les modifications récentes", "pageinfo-redirects-name": "Nombre de redirections vers cette page", + "pageinfo-redirects-value": "$1", "pageinfo-subpages-name": "Nombre de sous-pages de cette page", "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|redirection|redirections}}; $3 {{PLURAL:$3|non-redirection|non-redirections}})", "pageinfo-firstuser": "Créateur de la page", @@ -3230,7 +3237,8 @@ "metadata-expand": "Afficher les informations détaillées", "metadata-collapse": "Masquer les informations détaillées", "metadata-fields": "Les champs de métadonnées d'image listés dans ce message seront inclus dans la page de description de l'image quand la table de métadonnées sera réduite. Les autres champs seront cachés par défaut.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude", - "metadata-langitem": "'''$2 :''' $1", + "metadata-langitem": "$2 : $1", + "metadata-langitem-default": "$1", "namespacesall": "Tous", "monthsall": "tous", "confirmemail": "Confirmer l’adresse de courriel", @@ -3263,6 +3271,7 @@ "confirmrecreate": "L’utilisat{{GENDER:$1|eur|rice}} [[User:$1|$1]] ([[User talk:$1|Discussion]]) a supprimé cette page, alors que vous aviez commencé à la modifier, pour le motif suivant :\n: $2\nVeuillez confirmer que vous désirez réellement recréer cette page.", "confirmrecreate-noreason": "L’utilisat{{GENDER:$1|eur|rice}} [[User:$1|$1]] ([[User talk:$1|Discussion]]) a supprimé cette page, alors que vous aviez commencé à la modifier. Veuillez confirmer que vous désirez réellement recréer cette page.", "recreate": "Recréer", + "unit-pixel": "px", "confirm-purge-title": "Purger cette page", "confirm_purge_button": "Confirmer", "confirm-purge-top": "Voulez-vous rafraîchir cette page (purger le cache) ?", @@ -3282,12 +3291,18 @@ "mcrundo-parse-failed": "Echec dans l'analyse de la nouvelle version : $1", "semicolon-separator": " ; ", "colon-separator": " : ", + "ellipsis": "...", "percent": "$1 %", + "parentheses": "($1)", + "parentheses-start": "(", + "parentheses-end": ")", + "brackets": "[$1]", "quotation-marks": "« $1 »", "imgmultipageprev": "← page précédente", "imgmultipagenext": "page suivante →", "imgmultigo": "Accéder !", "imgmultigoto": "Aller à la page $1", + "img-lang-opt": "$2 ($1)", "img-lang-default": "(langue par défaut)", "img-lang-info": "Afficher cette image en $1 $2.", "img-lang-go": "Lancer", @@ -3317,6 +3332,7 @@ "size-exabytes": "$1 Eio", "size-zetabytes": "$1 Zio", "size-yottabytes": "$1 Yio", + "size-pixel": "$1 {{PLURAL:$1|pixel|pixels}}", "bitrate-bits": "$1 bps", "bitrate-kilobits": "$1 kbps", "bitrate-megabits": "$1 Mbps", @@ -3420,6 +3436,7 @@ "version-variables": "Variables", "version-editors": "Éditeurs", "version-antispam": "Prévention du pollupostage", + "version-api": "API", "version-other": "Divers", "version-mediahandlers": "Manipulateurs de médias", "version-hooks": "Greffons", @@ -3784,13 +3801,17 @@ "limitreport-walltime": "Temps réel d’utilisation", "limitreport-walltime-value": "$1 {{PLURAL:$1|seconde|secondes}}", "limitreport-ppvisitednodes": "Nombre de nœuds de préprocesseur visités", + "limitreport-ppvisitednodes-value": "$1/$2", "limitreport-ppgeneratednodes": "Nombre de nœuds de préprocesseur générés", + "limitreport-ppgeneratednodes-value": "$1/$2", "limitreport-postexpandincludesize": "Taille d’inclusion après expansion", "limitreport-postexpandincludesize-value": "$1/$2 {{PLURAL:$2|octet|octets}}", "limitreport-templateargumentsize": "Taille de l’argument du modèle", "limitreport-templateargumentsize-value": "$1/$2 {{PLURAL:$2|octet|octets}}", "limitreport-expansiondepth": "Profondeur d’expansion maximale", + "limitreport-expansiondepth-value": "$1/$2", "limitreport-expensivefunctioncount": "Nombre de fonctions d’analyse coûteuses", + "limitreport-expensivefunctioncount-value": "$1/$2", "limitreport-unstrip-depth": "Profondeur de récursion de développement", "limitreport-unstrip-depth-value": "$1/$2", "limitreport-unstrip-size": "Taille de développement après expansion", @@ -3851,6 +3872,7 @@ "mediastatistics-header-text": "Textuel", "mediastatistics-header-executable": "Exécutables", "mediastatistics-header-archive": "Formats compressés", + "mediastatistics-header-3d": "3D", "mediastatistics-header-total": "Tous les fichiers", "json-warn-trailing-comma": "$1 {{PLURAL:$1|virgule finale a été supprimée|virgules finales ont été supprimées}} du JSON", "json-error-unknown": "Il y a eu un problème avec le JSON. Erreur : $1", @@ -3990,6 +4012,7 @@ "authmanager-provider-password-domain": "Authentification par mot de passe et domaine", "authmanager-provider-temporarypassword": "Mot de passe temporaire", "authprovider-confirmlink-message": "D’après vos dernières tentatives de connexion, les comptes suivants peuvent être liés à votre compte wiki. Les lier vous permettra de se connecter via ces comptes. Veuillez sélectionner lesquels doivent être liés.", + "authprovider-confirmlink-option": "$1 ($2)", "authprovider-confirmlink-request-label": "Comptes qui doivent être liés", "authprovider-confirmlink-success-line": "$1 : Liés avec succès.", "authprovider-confirmlink-failed-line": "$1 : $2", @@ -4038,6 +4061,16 @@ "restrictionsfield-help": "Une adresse IP ou une plage CIDR par ligne. Pour tout activer, utiliser :
0.0.0.0/0\n::/0
", "edit-error-short": "Erreur : $1", "edit-error-long": "Erreurs :\n\n$1", + "specialmute": "Muet", + "specialmute-success": "Vos préférences de mise en sourdine on bien été mises à jour. Voyez tous les utilisateurs impliqués dans [[Special:Preferences]].", + "specialmute-submit": "Confirmer", + "specialmute-label-mute-email": "Mettre en sourdine les courriels de cet utilisateur", + "specialmute-header": "Veuillez sélectionner vos préférences de mise en sourdine pour {{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "Le nom d'utilisateur demandé n'a pu être trouvé.", + "specialmute-error-email-blacklist-disabled": "Mise en sourdine des utilisateurs pour vous envoyer des courriels, non activée.", + "specialmute-error-email-preferences": "Vous devez confirmer votre adresse courriel avant de pouvoir mettre en sourdine un utilisateur. Vous pouvez le faire depuis [[Special:Preferences]].", + "specialmute-email-footer": "Pour gérer les préférences courriel pour {{BIDI:$2}} voir <$1>.", + "specialmute-login-required": "Veuillez vous connecter pour mettre à jour vos préférences de mise en sourdine d'utilisateurs.", "revid": "version $1", "pageid": "ID de page $1", "interfaceadmin-info": "$1\n\nLes droits pour modifier les fichiers CSS/JS/JSON globaux au site ont été récemment séparés du droit editinterface. Si vous ne comprenez pas pourquoi vous avez cette erreur, voyez [[mw:MediaWiki_1.32/interface-admin]].", @@ -4056,6 +4089,8 @@ "passwordpolicies-summary": "Voici une liste des politiques des mots de passe effectifs pour les groupes d'utilisateurs de ce wiki.", "passwordpolicies-group": "Groupe", "passwordpolicies-policies": "Politiques", + "passwordpolicies-policy-display": "$1 ($2)", + "passwordpolicies-policy-displaywithflags": "$1 ($2) ($3)", "passwordpolicies-policy-minimalpasswordlength": "Les mots de passe doivent avoir au moins $1 caractère{{PLURAL:$1||s}} de long", "passwordpolicies-policy-minimumpasswordlengthtologin": "Les mots de passe doivent avoir au moins $1 caractère{{PLURAL:$1||s}} de long pour autoriser la connextion", "passwordpolicies-policy-passwordcannotmatchusername": "Le mot de passe ne peut pas être le même que le nom d'utilisateur", diff --git a/languages/i18n/frp.json b/languages/i18n/frp.json index 6cfd81bf9e..2dd9452cea 100644 --- a/languages/i18n/frp.json +++ b/languages/i18n/frp.json @@ -10,7 +10,8 @@ "Macofe", "Matma Rex", "Fitoschido", - "Vlad5250" + "Vlad5250", + "Wladek92" ] }, "tog-underline": "Solegnér los lims :", @@ -2609,7 +2610,7 @@ "metadata-expand": "Montrar los dètalys de més", "metadata-collapse": "Cachiér los dètalys de més", "metadata-fields": "Los champs de mètabalyês d’émâge listâs dens cél mèssâjo seront rapondus dedens la pâge de dèscripcion de l’émâge quand la trâbla de mètabalyês serat rèduita.\nLos ôtros champs seront cachiês per dèfôt.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude", - "metadata-langitem": "'''$2 :''' $1", + "metadata-langitem": "$2 : $1", "namespacesall": "Tôs", "monthsall": "tôs", "confirmemail": "Confirmar l’adrèce èlèctronica", diff --git a/languages/i18n/fy.json b/languages/i18n/fy.json index cd3def4c03..2f625209c2 100644 --- a/languages/i18n/fy.json +++ b/languages/i18n/fy.json @@ -336,7 +336,7 @@ "protectedpagetext": "Dizze side is befeilige. Bewurkjen is net mooglik.", "viewsourcetext": "Jo kinne de boarnetekst fan dizze side besjen en kopiearje:", "protectedinterface": "Dizze side jout systeemteksten fan 'e software en is befeilige tsjin misbrûk. Asto oersettingen foar alle wiki's tafoegje of bewurkje wolst, kinsto [https://translatewiki.net/ translatewiki.net] brûke.", - "editinginterface": "Warskôging: Jo bewurkje in side dy't brûkt wurdt foar systeemteksten foar de software.\nBewurkings op dizze side beynfloedzje de meidoggersynterface fan elkenien.", + "editinginterface": "Warskôging: Jo bewurkje in side dy't brûkt wurdt foar systeemteksten fan de programmatuer.\nFeroarings oan dizze side beynfloedzje it oansjoch fan it meidoggersoerflak fan oaren op dizze wiki.", "cascadeprotected": "Dizze side is skoattele tsjin wizigjen, om't der in ûnderdiel útmakket fan de neikommende {{PLURAL:$1|side|siden}}, dy't skoattele {{PLURAL:$1|is|binne}} mei de \"ûnderlizzende siden\" opsje ynskeakele: $2", "namespaceprotected": "Jo hawwe gjin rjochten om siden yn'e nammeromte '''$1''' te bewurkjen.", "ns-specialprotected": "Siden yn'e nammerûmte {{ns:special}} kinne net bewurke wurde.", @@ -544,7 +544,7 @@ "editingsection": "Bewurkje $1 (seksje)", "editingcomment": "Bewurkjen fan $1 (nij mêd)", "editconflict": "Tagelyk bewurke: \"$1\"", - "explainconflict": "In oar hat de side feroare sûnt jo begûn binne mei it bewurkjen.\nIt earste bewurkingsfjild is hoe't de tekst wilens wurden is.\nJo feroarings stean yn it twadde fjild.\nDy wurde allinnich tapast safier as jo se yn it earste fjild ynpasse.\n'''Allinnich''' de tekst út it earste fjild kin fêstlein wurde.", + "explainconflict": "Immen oars hat dizze side feroare, nei't jo mei bewurkjen derfan begûn binne.\nIt bewurkingsfjild boppe befettet de sidetekst sa as it no is.\nJo wizigings wurde werjûn yn it tekstfjild ûnder.\nJo sille jo wizigings gearfoegje moatte mei de besteande tekst.\nAllinnich de tekst yn it bewurkingsfjild boppe wurdt bewarre at jo op \"$1\" drukke.", "yourtext": "Jo tekst", "storedversion": "Fêstleine ferzje", "editingold": "Warskôging: Jo binne dwaande mei in âldere ferzje fan dizze side.\nSoene jo dy fêstlizze, dan is alles wei wat sûnt dy tiid feroare is.", @@ -1099,6 +1099,7 @@ "rcfilters-filter-previousrevision-label": "Net de lêste ferzje", "rcfilters-filter-previousrevision-description": "Alle wizigings dy't net de \"lêste ferzje\" binne.", "rcfilters-filter-excluded": "Utsein", + "rcfilters-tag-prefix-namespace-inverted": ":net $1", "rcfilters-exclude-button-off": "Seleksje omkeare", "rcfilters-exclude-button-on": "Omkearde seleksje", "rcfilters-view-tags": "Lebele bewurkings", @@ -1557,14 +1558,14 @@ "deletecomment": "Reden:", "deleteotherreason": "Oare/eventuele reden:", "deletereasonotherlist": "Oare reden", - "deletereason-dropdown": "*Faak-brûkte redenen\n** Frege troch de skriuwer\n** Skeining fan auteursrjocht\n** Fandalisme", + "deletereason-dropdown": "* Gongbere wiskredens\n** Spam\n** Fandalisme\n** Skeining fan auteursrjochten\n** Frege troch de skriuwer\n** Misse trochferwizing", "rollback": "Wizigings weromdraaie", "rollback-confirmation-yes": "Weromdraaie", "rollbacklink": "weromdraaie", "rollbacklinkcount": "$1 {{PLURAL:$1|bewurking|bewurkings}} weromdraaie", "rollbacklinkcount-morethan": "mear as $1 {{PLURAL:$1|bewurking|bewurkings}} weromdraaie", "rollbackfailed": "Weromdraaien fan wizigings net slagge.", - "cantrollback": "Dizze feroaring kin net werom setten wurde, om't der mar ien skriuwer is.", + "cantrollback": "Kin de wiziging net ûngedien meitsje;\nde lêste bydrager is de iennichste bewurker fan dizze side.", "alreadyrolled": "Kin de wiziging fan [[:$1]] troch [[User:$2|$2]] ([[User talk:$2|oerlis]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]) net weromdraaie;\nin oar hat de wiziging weromdraaid, of oars wat oan de side feroare.\n\nDe lêste wiziging wie fan [[User:$3|$3]] ([[User talk:$3|oerlis]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).", "editcomment": "De gearfetting wie: $1.", "revertpage": "Bewurkings fan [[Special:Contributions/$2|$2]] ([[User talk:$2|oerlis]]) weromset ta de lêste ferzje fan [[User:$1|$1]]", @@ -1678,7 +1679,7 @@ "whatlinkshere-hidetrans": "$1 transklúzjes", "whatlinkshere-hidelinks": "$1 keppelings", "whatlinkshere-filters": "Filters", - "blockip": "Slút {{GENDER:$1|meidogger}} út", + "blockip": "{{GENDER:$1|Meidogger|Meidochster}} útslute", "blockiptext": "Brûk dizze fjilden om in beskaat IP-adres of meidochnamme fan skriuwtagong út te sluten.\nDat soe allinnich dien wurde moatte fanwegen fandalisme of oar ûnakseptabel hâlden en dragen, sa't de\n[[{{MediaWiki:Policy-url}}|útslút-rie]] it oanjout.\nMeld de krekte reden! Neam bygelyks de siden dy't oantaaste waarden.\nJo kinne IP-adresrigen útslute mei de syntaksis fan [https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR]; de grutst tastiene rige is /$1 foar IPv4 en /$2 foar IPv6.", "ipaddressorusername": "IP-adres of meidochnamme:", "ipbreason": "Reden:", @@ -1995,12 +1996,13 @@ "confirmemail_send": "Stjoer in befêstigingskoade", "confirmemail_sent": "Befêstiginskoade tastjoerd.", "confirmemail_sendfailed": "De befêstigingskoade koe net stjoerd wurde. Faaks stean der ferkearde tekens yn it e-postadres.\n\nBerjocht: $1", - "confirmemail_invalid": "Dizze befêstiginskoade jildt net (mear).\nFaaks is de koade ferrûn.", + "confirmemail_invalid": "Unjildige befêstigingskoade.\nDe koade soe ferrûn wêze kinne.", "confirmemail_needlogin": "Jo moatte $1 om jo e-mailadres befêstigje te kinnen.", "confirmemail_success": "Jo netpostadres is befêstige. Jo kinne jo no oanmelde en de wiki brûke.", "confirmemail_loggedin": "Jo e-mailadres is no befêstige.", "confirmemail_subject": "Befêstiging e-mailadres foar {{SITENAME}}", - "confirmemail_body": "Immen, nei gedachten jo, hat him by {{SITENAME}} oanmelde as \"$2\", mei dit netpostadres ($1).\n\nHjirtroch komme ek de netpostfunksjes fan {{SITENAME}} foar jo beskikber. Iepenje de neikommende keppeling om te befêstigjen dat jo wier josels by {{SITENAME}} mei dit netpostadres oanmelde hawwe:\n\n$3\n\nAt jo dat *net* wienen, brûk dy keppeling dan net, en klik hjir:\n\n$5\n\nDizze befêstigingskoade ferrint dan op $4.", + "confirmemail_body": "Immen, nei alle gedachten jo, mei it IP-adres $1,\nhat it akkount \"$2\" op {{SITENAME}} oanmakke mei dit e-mailadres.\n\nOm te befêstigjen dat dat akkount wier jowes is en de e-mailfunksjes\nop {{SITENAME}} beskikber makke wurde kinne, iepenje dan dizze\nferwizing yn jo webblêder:\n\n$3\n\nAt jo it akkount *net* oanmakke hawwe, folgje dan dizze ferwizing\nom it befêstigjen fan it e-mailadres ôf te sizzen:\n\n$5\n\nDe befêstigingskoade ferrint op $4.", + "invalidateemail": "Befêstigjen e-mail ôfsizze", "scarytranscludetoolong": "[URL-adres is te lang]", "confirmrecreate": "Sûnt jo begûn binne dizze side te bewurkjen, hat meidogger [[User:$1|$1]] ([[User talk:$1|oerlis]]) de side wiske. De reden dy't derfoar jûn waard wie:\n: ''$2''\nWolle jo de side wier op 'e nij skriuwe?", "unit-pixel": "px", diff --git a/languages/i18n/gcr.json b/languages/i18n/gcr.json index 6fd1e82464..db8f7389bf 100644 --- a/languages/i18n/gcr.json +++ b/languages/i18n/gcr.json @@ -5,7 +5,7 @@ "Léon973" ] }, - "tog-underline": "Soulignman dé lyannaj :", + "tog-underline": "Soulignman di yannaj :", "tog-hideminor": "Maské modifikasyon minò-ya annan modifikasyon résan-yan", "tog-hidepatrolled": "Maské modifikasyon-yan ki rouli annan modifikasyon résan-yan", "tog-newpageshidepatrolled": "Maské paj-ya ki rouli annan lis dé nouvèl paj", @@ -30,7 +30,7 @@ "tog-enotifrevealaddr": "Afiché mo adrès élègtronnik annan kourilèt di notifikasyon", "tog-shownumberswatching": "Afiché nonm-an di itilizatò an kour", "tog-oldsig": "Zòt signatir atchwèl :", - "tog-fancysig": "Trété signatir-a kou di wikitègs (san lyannaj otonmantik)", + "tog-fancysig": "Trété signatir-a kou wikitègs (san yannaj otonmantik)", "tog-uselivepreview": "Afiché apèrsou san roucharjé paj-a", "tog-forceeditsummary": "Avèrti mo lò mo pa èspésifyé di rézimen di modifikasyon", "tog-watchlisthideown": "Maské mo pròp modifikasyon annan lis di swivi", @@ -135,7 +135,7 @@ "listingcontinuesabbrev": "(swit)", "index-category": "Paj endèksé", "noindex-category": "Paj ki pa endèksé", - "broken-file-category": "Paj ké lyannaj di fiché brizé", + "broken-file-category": "Paj ké yannaj-ya di fiché ki brizé", "categoryviewer-pagedlinks": "($1) ($2)", "category-header-numerals": "$1–$2", "about": "Apropo", @@ -159,16 +159,16 @@ "tagline": "Di {{SITENAME}}", "help": "Lèd", "search": "Sasé", - "search-ignored-headings": " #
\n# Tit dé sègsyon ki ké fika ignoré pa sasé-a.\n# Chanjman-yan ki éfègtchwé isi ka pran léfè lò ki paj-a ké tit-a sa endègsé.\n# Zòt pouvé fòrsé réyendègsasyon di paj-a an éfègtchwan roun modifikasyon vid.\n# Sentags-a sa swivant-a :\n#   * Tousa ki ka swiv roun « # » jouk finisman-an di lign-an sa roun koumantèr.\n#   * Tout lign ki pa-vid sa tit ègzak-a pou ignoré, kas konprann osi.\nRéférans\nLyannaj ègstèrn\nWè osi\n #
", + "search-ignored-headings": " #
\n# Tit sègsyon-yan ki ké fika ignoré pa sasé-a.\n# Chanjman-yan ki éfègtchwé isi ka pran léfè lò ki paj-a ké tit-a sa endègsé.\n# Zòt pouvé fòrsé réyendègsasyon-an di paj-a an éfègtchwan roun modifikasyon ki vid.\n# Sentags-a sa swivant-a :\n#   * Tousa ki ka swiv roun « # » jouk finisman-an di lign-an sa roun koumantèr.\n#   * Tout lign ki pa-vid sa tit ègzak-a pou ignoré, kas konprann osi.\nRéférans\nYannaj èstèrn\nWè osi\n #
", "searchbutton": "Sasé", "go": "Konsilté", "searcharticle": "Kontinwé", "history": "Listorik di paj-a", "history_short": "Listorik", "history_small": "listorik", - "updatedmarker": "modifyé dipi mo dannyé vizit", - "printableversion": "Vèrsyon enprimab", - "permalink": "Lyannaj pèrmannan", + "updatedmarker": "modifyé dipi zòt dannyé vizit", + "printableversion": "Vèrsyon ki enprimab", + "permalink": "Yannaj ki pèrmannan", "print": "Enprimé", "view": "Lir", "view-foreign": "Wè asou $1", @@ -205,7 +205,7 @@ "lastmodifiedat": "Dannyé modifikasyon di sa paj té fè $1 à $2.", "viewcount": "Sa paj {{PLURAL:$1|0=pa té janmen konsilté|1=té konsilté roun sèl fwè|té konsilté $1 fwè}}.", "protectedpage": "Paj protéjé", - "jumpto": "Alé à", + "jumpto": "Alé bò :", "jumptonavigation": "navigasyon", "jumptosearch": "sasé", "view-pool-error": "Dézolé, sèrvò-ya sa sircharjé pou moman-an.\nTròp itilizatò ka sasé konsilté sa paj.\nSouplé, atann enpé anvan di éséyé òkò d’aksédé à sala.\n\n$1", @@ -289,7 +289,7 @@ "nstab-category": "Katégori", "mainpage-nstab": "Paj prensipal", "nosuchaction": "Agsyon enkonnèt", - "nosuchactiontext": "Lagsyon-an ki èspésifyé annan URL-a sa envalid.\nZòt pitèt mal rantré URL-a oben swivi roun lyannaj ki éronnen.\nLi pouvé égalman endiké roun annonmanli annan logisyèl-a ki itilizé pa {{SITENAME}}.", + "nosuchactiontext": "Lagsyon-an ki èspésifyé annan URL-a sa envalid.\nZòt pitèt mal rantré URL-a oben swivi roun yannaj ki éronnen.\nLi pouvé égalman endiké roun annonmanli annan logisyèl-a ki itilizé pa {{SITENAME}}.", "nosuchspecialpage": "Paj èspésyal inègzistant", "nospecialpagetext": "Zòt doumandé oun paj èspésyal ki pa ka ègzisté.\n\nOun lis dé paj èspésyal valid ka trouvé so kò asou [[Special:SpecialPages|{{int:specialpages}}]].", "error": "Lérò", @@ -304,7 +304,7 @@ "readonly": "Baz di data vérouyé", "enterlockreason": "Endiké rézon-an di vérouyaj ensi ki roun èstimasyon di so douré", "readonlytext": "Ajou-ya ké mizajou-ya di baz di data fika atchwèlman bloké, probabman pou pèrmèt mentnans-a di baz-a, apré sa, tout bagaj ké rantré annòrd.\n\nAdministratò sistenm-an ki vérouyé baz di data fourni lèsplikasyon-an ki ka swiv :
$1", - "missing-article": "Baz-a di data pa trouvé tègs-a di roun paj ki li té divèt trouvé, ki entitilé « $1 » $2.\n\nJénéralman, sala ka sirvini an swivan roun lyannaj bò'd roun dif ki périmen oben bò'd listorik-a di roun paj ki siprimen.\n\nSi a pa sa ki la, zòt pitèt trouvé roun annonmanli annan progranm-an.\nSouplé, signalé li à roun [[Special:ListUsers/sysop|administratò]] é pa bliyé di endiké li URL-a di paj-a.", + "missing-article": "Baz-a di data pa trouvé tègs-a di roun paj ki li té divèt trouvé, ki entitilé « $1 » $2.\n\nJénéralman, sala ka sirvini an swivan roun yannaj bò'd roun dif ki périmen oben bò'd listorik-a di roun paj ki siprimen.\n\nSi a pa sa, a pitèt roun annonmanli annan progranm-an.\nSouplé, signalé li bay roun [[Special:ListUsers/sysop|administratò]] é pa bliyé di endiké li URL-a di paj-a.", "missingarticle-rev": "(niméro di vèrsyon : $1)", "missingarticle-diff": "(diff : $1, $2)", "readonly_lag": "Baz-a di data té otonmatikman vérouyé pannan ki sèrvò-ya ségondèr ka réyaligné yé kò asou sèrvò prensipal-a", @@ -331,7 +331,7 @@ "badtitletext": "Tit di paj doumandé pa valid, vid, oben mal fòrmé si a roun tit entèr-lanng oben entèr-projè.\nI ka kontni pitèt oun oben plizyò karaktèr ki pa pouvé fika itilizé annan tit-ya.", "title-invalid-empty": "Tit di paj doumandé sa vid oben ka kontni sèlman non-an di roun lèspas di non.", "title-invalid-utf8": "Tit di paj doumandé ka kontni roun sékans UTF-8 envalid.", - "title-invalid-interwiki": "Paj siblé-a ka kontni roun lyannaj entèrwiki ki pa pouvé fika itilizé annan tit-ya.", + "title-invalid-interwiki": "Paj sib-a ka kontni roun yannaj entèrwiki ki pa pouvé fika itilizé annan tit-ya.", "title-invalid-talk-namespace": "Tit di paj doumandé ka fè référans à roun paj di diskisyon ki pa pouvé ègzisté.", "title-invalid-characters": "Tit di paj doumandé ka kontni dé karaktèr ki pa valid : « $1 ».", "title-invalid-relative": "Tit ka kontni oun chimen roulatif. Tit-ya ki ka référansé dé paj roulativ (./, ../) pa valid pas li sa souvan itilizé pa navigatò di itilizatò-a.", @@ -422,7 +422,7 @@ "createacct-email-ph": "Antré zòt adrès di kourilèt", "createacct-another-email-ph": "Antré adrès-a di kourilèt", "createaccountmail": "Itilizé roun modipas aléyatwè ki tanporèr é voyé li pou adrès-a di kourilèt ki èspésifyé", - "createaccountmail-help": "Pouvé fika itilizé pou kréyé roun kont pou rounòt moun san konèt mo di pas-a.", + "createaccountmail-help": "Pouvé fika itilizé pou kréyé roun kont pou rounòt moun san konnèt modipas-a.", "createacct-realname": "Non réyèl (fakiltatif)", "createacct-reason": "Motif", "createacct-reason-ph": "Poukisa zòt kréyé rounòt kont", @@ -455,10 +455,10 @@ "login-userblocked": "{{GENDER:$1|Sa itilizatò}} bloké. Konnègsyon-an pa otorizé.", "wrongpassword": "Non-an di itilizatò oben modipas enkorèk.\nSouplé, éséyé òkò.", "wrongpasswordempty": "Zòt pa rantré pyès modipas.\nSouplé, éséyé òkò.", - "passwordtooshort": "Zòt mo di pas divèt kontni omwen $1 karaktèr{{PLURAL:$1|}}.", + "passwordtooshort": "Zòt modipas divèt kontni onmwen $1 karaktèr{{PLURAL:$1|}}.", "passwordtoolong": "Modipas-ya pa pouvé dépasé $1 karagtèr{{PLURAL:$1|}}.", "passwordtoopopular": "Modipas ki tròp kouran pa pouvé fika itilizé. Souplé, chwézi roun modipas ki pi difisil pou sonjé.", - "password-name-match": "Zòt mo di pas divèt fika diféran di zòt non d'itilizatò.", + "password-name-match": "Zòt modipas divèt fika diféran di zòt non di itilizatò.", "password-login-forbidden": "Litilizasyon-an di sa non d'itilizatò oben di sa modipas sa entèrdi.", "mailmypassword": "Réynisyalizé modipas-a", "passwordremindertitle": "Nouvèl modipas tanporèr pou {{SITENAME}}", @@ -488,11 +488,11 @@ "loginlanguagelabel": "Lanng : $1", "suspicious-userlogout": "Zòt doumann di konnègsyon té roufizé pas i sanblé ki li té voyé pa roun navigatò défègtché oben dipi kach-a di roun sèrvis mandatèr.", "createacct-another-realname-tip": "Véritab non-an sa òpsyonnèl.\nSi zòt désidé di fourni li, i ké fika itilizé pou krédité lotò-a di so travay-ya.", - "pt-login": "Konnègté so kò", + "pt-login": "Konnègté sokò", "pt-login-button": "Konnègté so kò", "pt-login-continue-button": "Kontinwé konnègsyon-an", "pt-createaccount": "Kréyé roun kont", - "pt-userlogout": "Dékonnègté so kò", + "pt-userlogout": "Dékonnègté sokò", "php-mail-error-unknown": "Lérò enkonnèt annan fongsyon-an mail() di PHP.", "user-mail-no-addy": "Enposib di voyé roun kourilèt san adrès di kourilèt.", "user-mail-no-body": "Lésè di voyé di roun kourilèt ké roun kò vid oben anòrmalman kourt.", @@ -516,7 +516,7 @@ "botpasswords-label-needsreset": "(Modipas-a divèt fika réynisyalizé)", "botpasswords-label-appid": "Non di robo :", "botpasswords-label-create": "Kréyé", - "botpasswords-label-update": "Mété à jou", + "botpasswords-label-update": "Fè roun mizajou", "botpasswords-label-cancel": "Annilé", "botpasswords-label-delete": "Siprimen", "botpasswords-label-resetpassword": "Réynisyalizé modipas-a", @@ -527,16 +527,16 @@ "botpasswords-insert-failed": "Échèk di ajou-a di non di robo « $1 ». Ès i té ja ajouté ?", "botpasswords-update-failed": "Léchèk di mizajou di non di robo « $1 ». Ès i té ja siprimen ?", "botpasswords-created-title": "Modipas di robo kréyé", - "botpasswords-created-body": "Mo di pas pou robo-a « $1 » di {{GENDER:$2|itilizatò|itilizatris}}-a « $2 » té kréyé.", + "botpasswords-created-body": "Modipas-a pou robo-a « $1 » di {{GENDER:$2|itilizatò|itilizatris}}-a « $2 » fika kréyé.", "botpasswords-updated-title": "Modipas di robo mizajou", - "botpasswords-updated-body": "Mo di pas pou robo-a « $1 » di {{GENDER:$2|itilizatò|itilizatris}}-a « $2 » té mizajou.", + "botpasswords-updated-body": "Modipas-a pou robo-a « $1 » di {{GENDER:$2|itilizatò|itilizatris}}-a « $2 » fika mizajou.", "botpasswords-deleted-title": "Modipas di robo siprimen", "botpasswords-deleted-body": "Modipas-a pou robo-a « $1 » di {{GENDER:$2|itilizatò|itilizatris}}-a « $2 » té siprimen.", "botpasswords-newpassword": "Nouvèl modipas-a pou konnègté so kò à$1 sa $2. Souplé, anréjistré li pou fè référans asou li iltèryòrman.
(Pou ansyen robo-ya ki ka nésésité ki non-an ki fourni pou konnègsyon-an ka fika menm-an ki non-an di itilizatò évantchwèl, zòt pouvé osi itilizé $3 kou non di itilizatò é $4 kou modipas).", "botpasswords-no-provider": "BotPasswordsSessionProvider pa disponnib.", "botpasswords-restriction-failed": "Rèstrigsyon-yan di modipas di robo ka anpéché sa konnègsyon.", "botpasswords-invalid-name": "Non-an d'itilizatò ki èspésifyé pa ka kontni di séparatò di modipas di robo (« $1 »).", - "botpasswords-not-exist": "{{GENDER:$1|Itilizatò|Itilizatris}}-a « $1 » pa gen di mo di pas di robo nonmen « $2 ».", + "botpasswords-not-exist": "{{GENDER:$1|Itilizatò|Itilizatris}}-a « $1 » pa gen di modipas di robo ki nonmen « $2 ».", "botpasswords-needs-reset": "Modipas-a di robo di non « $2 » di itilizatò-a « $1 » divèt fika réynisyalizé.", "resetpass_forbidden": "Modipas-ya pa pouvé fika chanjé.", "resetpass_forbidden-reason": "Modipas-ya pa pouvé fika chanjé : $1", @@ -581,16 +581,16 @@ "bold_tip": "Tègs gra", "italic_sample": "Tègs italik", "italic_tip": "Tègs italik", - "link_sample": "Tit di lyannaj", - "link_tip": "Lyannaj entèrn", - "extlink_sample": "http://www.example.com/ tit di lyannaj", - "extlink_tip": "Lyannaj ègstèrn (pa bliyé préfigs-a http://)", + "link_sample": "Tit di yannaj", + "link_tip": "Yannaj entèrn", + "extlink_sample": "http://www.example.com/ tit di yannaj", + "extlink_tip": "Yannaj èstèrn (pa bliyé préfigs-a http://)", "headline_sample": "Tègs di tit", "headline_tip": "Soutit nivo 2", "nowiki_sample": "Rantré tègs-a ki pa fòrmaté isi", "nowiki_tip": "Ignoré sentags wiki-a", "image_tip": "Fiché enséré", - "media_tip": "Lyannaj bò'd roun fiché médja", + "media_tip": "Yannaj bò'd roun fiché médja", "sig_tip": "Zòt signatir ké dat", "hr_tip": "Lign orizontal (pa an abizé)", "summary": "Rézimen :", @@ -612,12 +612,12 @@ "blockedtitle": "Itilizatò-a bloké.", "blockedtext": "Zòt kont itilizatò oben zòt adrès IP fika bloké.\n\nBlokaj té éfègtchwé pa $1.\nRézon-an ki évoké ka swiv : $2.\n\n* Koumansman di blokaj : $8\n* Lèspirasyon di blokaj : $6\n* Kont bloké : $7.\n\nZòt pouvé kontagté $1 oben rounòt [[{{MediaWiki:Grouppage-sysop}}|administratò]] pou diskité apropo di sa.\nZòt pouvé itilizé fongsyon-an « {{int:emailuser}} » rounso si roun adrès di kourilèt valid sa èspésifyé annan zòt [[Special:Preferences|préférans]] é rounso si sa fongsyonnalité pa fika bloké ba zòt.\nZòt adrès IP atchwèl sa $3 é zòt idantifyan di blokaj sa $5.\nSouplé, enkli tout détay-ya lasou'l annan chak rékèt ki zòt ké fè.", "blockednoreason": "pyès rézon bay", - "loginreqlink": "konnègté so kò", + "loginreqlink": "konnègté sokò", "accmailtitle": "Modipas voyé.", "newarticle": "(Nòv)", - "newarticletext": "Zòt swiv roun lyannaj bò'd roun paj ki pa ka ègzisté òkò. \nAfen di kréyé sa paj, rantré zòt tègs annan bwèt-a ki apré (zòt pouvé konsilté [$1 paj di lèd-a] pou plis di lenfòrmasyon).\nSi zòt vini{{GENDER:|}} isi pa lérò, kliké asou bouton-an Viré di zòt navigatò.", + "newarticletext": "Zòt swiv roun yannaj bò'd roun paj ki pa ka ègzisté òkò. \nAfen di kréyé sa paj, rantré zòt tègs annan bwèt-a ki apré (zòt pouvé konsilté [$1 paj di lèd-a] pou plis lenfòrmasyon).\nSi zòt vini{{GENDER:|}} isi pa lérò, kliké asou bouton-an Viré di zòt navigatò.", "anontalkpagetext": "----\nZòt asou paj-q di diskisyon di roun itilizatò annonnim ki pa òkò kréyé di kont oben ki pa ka itilizé roun.\nPou sa rézon, nou divèt itilizé so adrès IP pou idantifyé li.\nOun adrès konran IP pouvé fika patajé pa plizyò itilizatò.\nSi zòt sa roun itiliza{{GENDER:|ò}} annonnim é si zòt ka kontasté ki dé koumantèr ki pa ka konsèrnen zòt, fika adrésé ba zòt, zòt pouvé [[Special:CreateAccount|kréyé roun kont]] oben [[Special:UserLogin|konnègté zòt kò]] pou évité tout konfizyon fitir ké ròt kontribitò annonnim.", - "noarticletext": "I pa gen atchwèlman pyès tègs asou sa paj.\nZòt pouvé [[Special:Search/{{PAGENAME}}|lansé oun sasé asou sa tit]] annan ròt paj-ya,\n[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} sasé annan lopérasyon-yan ki lyannen]\noben [{{fullurl:{{FULLPAGENAME}}|action=edit}} kréyé sa paj].", + "noarticletext": "I gen atchwèlman pyès tègs asou sa paj.\nZòt pouvé [[Special:Search/{{PAGENAME}}|lansé oun sasé asou sa tit]] annan ròt paj-ya,\n[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} sasé annan lopérasyon-yan ki yannen]\noben [{{fullurl:{{FULLPAGENAME}}|action=edit}} kréyé sa paj].", "noarticletext-nopermission": "I pa gen atchwèlman pyès tègs asou sa paj.\nZòt pouvé [[Special:Search/{{PAGENAME}}|fè roun sasé asou sa tit]] annan ròt paj-ya,\noben [{{fullurl:{{#Special:Log}}|paj={{FULLPAGENAMEE}}}} sasé annan journal-ya ki asosyé], mé zòt pa gen pèrmisyon-an di kréyé sa paj.", "userpage-userdoesnotexist-view": "Kont itilizatò-a « $1 » pa anréjistré.", "clearyourcache": "Nòt : apré zòt anréjistré zòt modifikasyon, zòt divèt fòrsé roucharjman konplè di kach di zòt navigatò pou wè chanjman-yan.\n* Firefox / Safari : mentni touch-a Maj (Shift) an klikan asou bouton-an Atchwalizé oben présé Ctrl-F5 oben Ctrl-R (⌘-R asou roun Mac) \n* Google Chrome : apiyé asou Ctrl-Maj-R (⌘-Shift-R asou roun Mac) \n* Internet Explorer : mentni touch-a Ctrl an klikan asou bouton-an Atchwalizé oben présé Ctrl-F5 \n* Opera : alé annan Menu → Settings (Opera → Préférences asou roun Mac) é answit à Konfidansyalité & sékrité → Éfasé data di lésplorasyon-yan → Zimaj ké fiché an kach.", @@ -663,13 +663,13 @@ "page_first": "pronmyé", "page_last": "dannyé", "histlegend": "Sélègsyon di diff : koché bouton radjo-ya dé vèrsyon ki à konparé é apiyé asou rantré oben asou bouton-an ki anba.
\nLéjann : ({{int:cur}}) = diférans ké dannyé vèrsyon-an, ({{int:last}}) = diférans ké vèrsyon présédan-an, {{int:minoreditletter}} = modifikasyon minò.", - "history-fieldset-title": "Sasé dé révizyon", + "history-fieldset-title": "Filtré vèrsyon-yan", "histfirst": "Pli ansyenn", "histlast": "Pli résan-yan", - "historyempty": "(vid)", + "historyempty": "vid", "history-feed-title": "Listorik dé vèrsyon", "history-feed-description": "Listorik dé vèrsyon pou sa paj asou wiki-a", - "history-feed-item-nocomment": "$1 à $2", + "history-feed-item-nocomment": "$1 bò $2", "rev-delundel": "afiché/maské", "rev-showdeleted": "afiché", "revdelete-show-file-submit": "Enren", @@ -765,13 +765,13 @@ "boteditletter": "b", "rc-change-size-new": "$1 {{PLURAL:$1|ògtè}} apré chanjman", "rc-old-title": "kréyé inisyalman ké tit « $1 »", - "recentchangeslinked": "Swivi dé paj ki lyannen", - "recentchangeslinked-feed": "Swivi dé paj ki lyannen", - "recentchangeslinked-toolbox": "Swivi dé paj ki lyannen", - "recentchangeslinked-title": "Swivi dé paj asosyé à « $1 »", - "recentchangeslinked-summary": "Rantré roun non di paj pou wè modifikasyon-yan ki fè résaman asou dé paj ki lyannen dipi oben bò'd sa paj (pou wè manm-yan di roun katégori, rantré {{ns:category}}:Non di katégori). Modifikasyon-yan dé paj di [[Special:Watchlist|zòt lis di swivi]] sa an gra.", + "recentchangeslinked": "Swivi di paj-ya ki yannen", + "recentchangeslinked-feed": "Swivi di paj-ya ki yannen", + "recentchangeslinked-toolbox": "Swivi di paj-ya ki yannen", + "recentchangeslinked-title": "Swivi di paj-ya ki asosyé ké « $1 »", + "recentchangeslinked-summary": "Rantré roun non di paj pou wè modifikasyon-yan ki fèt résaman asou paj-ya ki yannen dipi oben bò'd sa paj (pou wè manm-yan di roun katégori, rantré {{ns:category}}:Non di katégori). Modifikasyon-yan di paj-ya di [[Special:Watchlist|zòt lis di swivi]] sa an gra.", "recentchangeslinked-page": "Non di paj :", - "recentchangeslinked-to": "Afiché modifikasyon-yan dé paj ki ka konpòrté roun lyannaj bò'd paj-a ki bay plito ki lenvèrs-a", + "recentchangeslinked-to": "Afiché modifikasyon-yan di paj-ya ki ka konpòrté roun yannaj bò'd paj-a ki bay plito ki lenvèrs-a", "upload": "Enpòrté roun fiché", "uploadlogpage": "Journal di enpo di fiché", "filedesc": "Dèskripsyon", @@ -799,7 +799,7 @@ "sharedupload-desc-here": "Sa fiché ka provini di $1. Li pouvé fika itilizé pa ròt projè.\nSo dèskripsyon asou so [$2 paj di dèskripsyon] sa afiché anba.", "filepage-nofile": "Pyès fiché di sa non ka ègzisté.", "upload-disallowed-here": "Zòt pa pouvé ranplasé sa fiché.", - "randompage": "Paj an azò", + "randompage": "Paj ké azò", "statistics": "Èstatistik", "double-redirect-fixer": "Korègtò di roudirègsyon", "nbytes": "$1 {{PLURAL:$1|ògtè}}", @@ -860,8 +860,8 @@ "contribsub2": "Pou {{GENDER:$3|$1}} ($2)", "nocontribs": "Pyès modifikasyon korèspondan à sa kritèr trouvé.", "uctop": "atchwèl", - "month": "À partir di mwa (é présédan) :", - "year": "À partir di lannen (é présédant) :", + "month": "Apati di mwè (ké anvan) :", + "year": "Apati di lannen (ké anvan) :", "sp-contributions-newbies": "Montré ren ki kontribisyon-yan dé nouvèl itilizatò", "sp-contributions-blocklog": "journal dé blokaj", "sp-contributions-uploads": "enpòr", @@ -872,21 +872,21 @@ "sp-contributions-toponly": "Montré ki kontribisyon-yan ki sa dannyé-ya dé artik", "sp-contributions-newonly": "Afiché inikman modifikasyon-yan ki sa dé kréyasyon di paj", "sp-contributions-submit": "Sasé", - "whatlinkshere": "Paj ki lyannen", + "whatlinkshere": "Paj ki yannen", "whatlinkshere-title": "Paj ki ka pwenté bò'd « $1 »", "whatlinkshere-page": "Paj :", - "linkshere": "Paj-ya ki anba ka kontni roun lyannaj bò'd $2 :", - "nolinkshere": "Pyès paj ka kontni lyannaj bò'd $2.", + "linkshere": "Paj-ya ki anba ka kontni roun yannaj bò'd $2 :", + "nolinkshere": "Pyès paj yannen bò'd $2.", "isredirect": "paj di roudirègsyon", "istemplate": "enklizyon", - "isimage": "lyannaj bò'd fiché-a", + "isimage": "yannaj bò'd fiché-a", "whatlinkshere-prev": "{{PLURAL:$1|présédant|$1 présédant}}", "whatlinkshere-next": "{{PLURAL:$1|swivant|$1 swivant}}", - "whatlinkshere-links": "← lyannaj", + "whatlinkshere-links": "← yannaj", "whatlinkshere-hideredirs": "$1 roudirègsyon-yan", "whatlinkshere-hidetrans": "$1 enklizyon-yan", - "whatlinkshere-hidelinks": "$1 lyannaj-ya", - "whatlinkshere-hideimages": "$1 lyannaj-ya bò'd fiché-a", + "whatlinkshere-hidelinks": "$1 yannaj-ya", + "whatlinkshere-hideimages": "$1 yannaj-ya bò'd fiché-a", "whatlinkshere-filters": "Filt", "ipboptions": "2 lò:2 hours,1 jou:1 day,3 jou:3 days,1 simenn:1 week,2 simenn:2 weeks,1 mwa:1 month,3 mwa:3 months,6 mwa:6 months,1 lan:1 year,endéfiniman:infinite", "infiniteblock": "enfini", @@ -907,7 +907,7 @@ "tooltip-pt-watchlist": "Oun lis dé paj don zòt ka swiv modifikasyon", "tooltip-pt-mycontris": "Oun lis di {{GENDER:|zòt}} kontribisyon", "tooltip-pt-login": "Nou ka ankourajé zòt à konnègté zòt kò ; soupannan, zòt pa blijé di fè li", - "tooltip-pt-logout": "Dékonnègté so kò", + "tooltip-pt-logout": "Dékonnègté sokò", "tooltip-pt-createaccount": "Nou ka ankourajé zòt à kréyé roun kont itilizatò é konnègté zòt kò ; soupannan, zòt pa blijé di fè li", "tooltip-ca-talk": "Diskisyon o sijè di sa paj di kontni", "tooltip-ca-edit": "Modifyé wikikod-a", @@ -928,16 +928,16 @@ "tooltip-n-portal": "Apropo di projè, sa ki zòt pouvé fè, koté trouvé lenfòrmasyon-yan", "tooltip-n-currentevents": "Trouvé plis d'enfòrmasyon asou atchwalité an kour", "tooltip-n-recentchanges": "Lis dé modifikasyon résan asou wiki-a", - "tooltip-n-randompage": "Afiché roun paj an azò", - "tooltip-n-help": "Aksè à lèd", - "tooltip-t-whatlinkshere": "Lis dé paj ki lyannen ki ka pwenté asou sala", - "tooltip-t-recentchangeslinked": "Lis dé modifikasyon résan ki lyannen ké sa paj", + "tooltip-n-randompage": "Afiché roun paj ké azò", + "tooltip-n-help": "Laksè bò lèd-a", + "tooltip-t-whatlinkshere": "Lis di paj-ya ki yannen ki ka pwenté asou sala", + "tooltip-t-recentchangeslinked": "Lis di modifikasyon-yan ki résan ki yannen ké sa paj", "tooltip-feed-atom": "Flux Atom pou sa paj", "tooltip-t-contributions": "Wè lis dé kontribisyon di {{GENDER:$1|sa itilizatò|sa itilizatris}}", "tooltip-t-emailuser": "Voyé roun kourilèt pou {{GENDER:$1|sa itilizatò}}", "tooltip-t-upload": "Télévèrsé dé fiché", "tooltip-t-specialpages": "Lis di tout paj èspésyal", - "tooltip-t-print": "Vèrsyon enprimab di sa paj", + "tooltip-t-print": "Vèrsyon ki enprimab di sa paj", "tooltip-t-permalink": "Adrès pèrmanant di sa vèrsyon di paj-a", "tooltip-ca-nstab-main": "Wè kontni di paj-a", "tooltip-ca-nstab-user": "Wè paj di itilizatò", diff --git a/languages/i18n/gl.json b/languages/i18n/gl.json index 48b601991b..46b48e2e13 100644 --- a/languages/i18n/gl.json +++ b/languages/i18n/gl.json @@ -194,7 +194,7 @@ "history": "Historial da páxina", "history_short": "Historial", "history_small": "historial", - "updatedmarker": "actualizado desde a miña última visita", + "updatedmarker": "actualizado desde a súa última visita", "printableversion": "Versión para imprimir", "permalink": "Ligazón permanente", "print": "Imprimir", diff --git a/languages/i18n/gom-deva.json b/languages/i18n/gom-deva.json index dd7ba716d1..0062c0b2ca 100644 --- a/languages/i18n/gom-deva.json +++ b/languages/i18n/gom-deva.json @@ -12,7 +12,8 @@ "Vaishali Parab", "The Discoverer", "Cliffa fernandes", - "Rxy" + "Rxy", + "Isidore Dantas" ] }, "tog-hideminor": "हालींच बदल केल्ल्यांतले बारीक संपादन लिपय", @@ -222,6 +223,7 @@ "nstab-template": "सांचो", "nstab-help": "आदाराचें पान", "nstab-category": "वर्ग", + "mainpage-nstab": "मुखेल पान", "nosuchaction": "असले तरेचे कार्य ना", "nosuchspecialpage": "असले कांयच विशेश पान ना", "error": "चूक", diff --git a/languages/i18n/he.json b/languages/i18n/he.json index 86526a103f..a2573b7ea1 100644 --- a/languages/i18n/he.json +++ b/languages/i18n/he.json @@ -532,7 +532,7 @@ "pt-createaccount": "יצירת חשבון", "pt-userlogout": "יציאה מהחשבון", "php-mail-error-unknown": "שגיאה לא ידועה בפונקציה mail()‎ של PHP.", - "user-mail-no-addy": "ניסיון לשלוח דוא\"ל ללא כתובת דוא\"ל.", + "user-mail-no-addy": "התבצע ניסיון לשליחת הודעה ללא כתובת דוא״ל.", "user-mail-no-body": "ניסיון לשלוח דוא\"ל עם תוכן ריק או קצר מאוד.", "changepassword": "שינוי סיסמה", "resetpass_announce": "כדי לסיים את הכניסה לחשבון, יש להגדיר סיסמה חדשה.", @@ -1109,7 +1109,7 @@ "gender-male": "הוא עורך דפים בוויקי", "gender-female": "היא עורכת דפים בוויקי", "prefs-help-gender": "לא חובה למלא העדפה זו.\nהמערכת משתמשת במידע הזה כדי לפנות אליך/אלייך ולציין את שם המשתמש שלך במין הדקדוקי הנכון.\nהמידע יהיה ציבורי.", - "email": "דוא\"ל", + "email": "דוא״ל", "prefs-help-realname": "לא חובה למלא את השם האמיתי.\nאם סופק, הוא עשוי לשמש כדי לייחס לך את עבודתך.", "prefs-help-email": "כתובת דואר אלקטרוני היא אופציונלית, אבל היא חיונית לאיפוס הסיסמה במקרה ש{{GENDER:|תשכח|תשכחי}} אותה.", "prefs-help-email-others": "באפשרותך גם לאפשר למשתמשים ליצור איתך קשר באמצעות דוא\"ל דרך קישור בדף המשתמש או בדף השיחה שלך.\nכתובת הדוא\"ל שלך לא תיחשף כשמשתמשים יצרו איתך קשר.", @@ -3822,7 +3822,7 @@ "authmanager-password-help": "הסיסמה לאימות.", "authmanager-domain-help": "שם מתחם לאימות חיצוני.", "authmanager-retype-help": "חזרה על הסיסמה.", - "authmanager-email-label": "דוא\"ל", + "authmanager-email-label": "דוא״ל", "authmanager-email-help": "כתובת דוא\"ל", "authmanager-realname-label": "שם אמיתי", "authmanager-realname-help": "השם האמיתי של המשתמש", diff --git a/languages/i18n/hr.json b/languages/i18n/hr.json index d7eb11fdf1..3a4ed91580 100644 --- a/languages/i18n/hr.json +++ b/languages/i18n/hr.json @@ -41,7 +41,9 @@ "Hamster", "BadDog", "Vlad5250", - "Zeljko.filipin" + "Zeljko.filipin", + "Anarhistička Maca", + "Astrind" ] }, "tog-underline": "Podcrtavanje poveznica", @@ -205,7 +207,7 @@ "history": "Povijest stranice", "history_short": "Stare izmjene", "history_small": "povijest", - "updatedmarker": "Obnovljeno od posljednjeg posjeta", + "updatedmarker": "obnovljeno od posljednjeg posjeta", "printableversion": "Inačica za ispis", "permalink": "Trajna poveznica", "print": "IspiÅ¡i", @@ -409,7 +411,7 @@ "virus-badscanner": "LoÅ¡a konfiguracija: nepoznati skener za viruse: ''$1''", "virus-scanfailed": "skeniranje neuspjeÅ¡no (kod $1)", "virus-unknownscanner": "nepoznati antivirus:", - "logouttext": "'''Odjavili ste se.'''\n\nNeke se stranice mogu prikazivati kao da ste joÅ¡ uvijek prijavljeni, sve dok ne očistite međuspremnik svog preglednika.", + "logouttext": "Odjavljeni ste.\n\nNeke se stranice mogu prikazivati kao da ste joÅ¡ uvijek prijavljeni, sve dok ne očistite međuspremnik svog preglednika.", "logging-out-notify": "Odjavljujemo Vas, molimo pričekajte.", "cannotlogoutnow-title": "Odjava trenutno nije moguća", "cannotlogoutnow-text": "Odjava nije moguća tijekom uporabe $1.", @@ -3305,7 +3307,7 @@ "logentry-pagelang-pagelang": "$1 {{GENDER:$2|promijenio|promijenila}} je jezik stranice $3 iz $4 u $5.", "mediastatistics": "Statistika datoteka", "mediastatistics-summary": "Slijede statistike postavljenih datoteka koje pokazuju zadnju inačicu datoteke. Starije ili izbrisane inačice nisu prikazane.", - "mediastatistics-nfiles": "$1 ($2 %)", + "mediastatistics-nfiles": "$1 ($2%)", "mediastatistics-nbytes": "{{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2; $3 %)", "mediastatistics-bytespertype": "Ukupna veličina datoteka za ovaj odlomak: {{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2; $3%).", "mediastatistics-allbytes": "Ukupna veličina svih datoteka: {{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2).", @@ -3426,6 +3428,12 @@ "removecredentials-submit": "Ukloni vjerodajnice", "credentialsform-provider": "Vrsta vjerodajnica:", "credentialsform-account": "Suradnički račun:", + "specialmute": "Isključi zvuk", + "specialmute-success": "VaÅ¡e postavke utiÅ¡avanja su uspjeÅ¡no ažurirane. Vidite sve utiÅ¡ane korisnike ovdje: [[Special:Preferences]].", + "specialmute-submit": "Potvrdi", + "specialmute-error-invalid-user": "Korisničko ime koje ste tražili nije moguće pronaći.", + "specialmute-error-email-preferences": "Morate potvrditi svoju email adresu prije nego Å¡to možete utiÅ¡ati ovoga korisnika. To možete učiniti putem [[Special:Preferences]].", + "specialmute-login-required": "Molimo Vas prijavite se da biste promijenili postavke.", "gotointerwiki": "NapuÅ¡tate projekt {{SITENAME}}", "gotointerwiki-invalid": "Navedeni naslov nije valjan.", "gotointerwiki-external": "NapuÅ¡tate projekt {{SITENAME}} da biste posjetili zasebno mrežno mjesto [[$2]].\n\n[$1 Nastavljate na $1]", diff --git a/languages/i18n/ht.json b/languages/i18n/ht.json index 450d6b2666..d41c894fe5 100644 --- a/languages/i18n/ht.json +++ b/languages/i18n/ht.json @@ -11,7 +11,8 @@ "Bfpage", "Macofe", "Lucas", - "LeGuyanaisPure" + "LeGuyanaisPure", + "Schery19" ] }, "tog-underline": "Souliyen lyen yo :", @@ -48,12 +49,14 @@ "tog-watchlisthideliu": "Kache modifikasyon yo ki fèt pa itilizatè yo ki enskri nan lis swivi mwen", "tog-watchlisthideanons": "Kache modifikasyon anònim nan lis swivi mwen", "tog-watchlisthidepatrolled": "Kache modifikasyon ki siveye yo nan lis swivi mwen", + "tog-watchlisthidecategorization": "Kache kategorizasyon paj yo", "tog-ccmeonemails": "Voye yon kopi imèl mwen voye ba lòt ban mwen", "tog-diffonly": "Pa montre enfòmasyon yon paj ki anba chanjman yo montre nan konparezon", "tog-showhiddencats": "Montre kategori kache yo", "tog-norollbackdiff": "Pa montre chanjman yo lè mwen fè yon revokasyon", "tog-useeditwarning": "Avèti lè m ap kite yon paj chanjman san m pa sovgade", "tog-prefershttps": "Toujou sèvi ak yon koneksyon sekirize lè m ap konekte", + "tog-showrollbackconfirmation": "Montre yon demann konfimasyon lè gen klik sou yon lyen revokasyon", "underline-always": "Toujou", "underline-never": "Jamè", "underline-default": "Dekorasyon ou navigatè pa defo", @@ -123,6 +126,8 @@ "october-date": "$1 oktòb", "november-date": "$1 novanm", "december-date": "$1 desanm", + "period-am": "AM", + "period-pm": "PM", "pagecategories": "{{PLURAL:$1|Kategori|Kategori yo}}", "category_header": "Paj yo ki nan kategori « $1 »", "subcategories": "Soukategori yo", @@ -145,7 +150,7 @@ "newwindow": "(Ouvè nan yon lòt fenèt)", "cancel": "Anile", "moredotdotdot": "Pi plis …", - "morenotlisted": "Lis sa a pa konplè.", + "morenotlisted": "Lis sa ka pa konplè.", "mypage": "Paj", "mytalk": "Diskisyon", "anontalk": "Diskite", @@ -160,6 +165,7 @@ "returnto": "Ritounen nan paj $1.", "tagline": "Yon atik de {{SITENAME}}.", "help": "Èd", + "help-mediawiki": "Èd konsènan MediaWiki", "search": "Chache", "searchbutton": "Fouye", "go": "Ale", @@ -167,7 +173,7 @@ "history": "Istorik paj la", "history_short": "Istorik", "history_small": "Istwa", - "updatedmarker": "Aktyalize depi dènyè visit mwen", + "updatedmarker": "Aktyalize depi dènyè visit ou", "printableversion": "Vèsyon ou kapab enprime", "permalink": "Lyen pou tout tan", "print": "Enprime", @@ -190,6 +196,9 @@ "talk": "Diskisyon", "views": "Afichay yo", "toolbox": "Bwat zouti", + "tool-link-userrights": "Chanje {{GENDER:$1|itilizatè}} gwoup yo", + "tool-link-userrights-readonly": "Gade {{GENDER:$1|itilizatè}} gwoup yo", + "tool-link-emailuser": "Voye yon mail bay {{GENDER:$1|itilizatè }}", "imagepage": "Wè paj fichye", "mediawikipage": "Wè paj mesaj", "templatepage": "Wè paj modèl", @@ -238,7 +247,9 @@ "ok": "OK", "retrievedfrom": "Rekipere depi « $1 Â»", "youhavenewmessages": "Ou genyen $1 ($2).", - "youhavenewmessagesmanyusers": "Ou gen $2 de plizyè itilizatè $2.", + "youhavenewmessagesmanyusers": "Ou gen $1 de plizyè itilizatè $2.", + "newmessageslinkplural": "{{PLURAL:$1|yon nouvo mesaj|999=nouvo mesaj yo}}", + "newmessagesdifflinkplural": "Dènye {{PLURAL:$1|chanjman|999=chanjman yo}}", "youhavenewmessagesmulti": "Ou genyen nouvo mesaj sou $1.", "editsection": "modifye", "editold": "modifye", @@ -251,7 +262,7 @@ "hidetoc": "kache", "collapsible-collapse": "Redui", "collapsible-expand": "Etann", - "confirmable-confirm": "Eske w si?", + "confirmable-confirm": "Eske {{GENDER:$1|w}} si?", "confirmable-yes": "Wi", "confirmable-no": "Non", "thisisdeleted": "Ou vle wè oubyen restore $1 ?", @@ -284,6 +295,11 @@ "nospecialpagetext": "Paj espesial ou demande-a envalid.\n\nOu ka jwenn yon lis paj espesial ki valid yo la [[Special:SpecialPages|{{int:specialpages}}]].", "error": "Erè", "databaseerror": "Erè nan bazdone.", + "databaseerror-text": "Gen yon erè rekèt bazdone ki fèt.\nSa ka endike yon erè nan lojisyèl la", + "databaseerror-textcl": "Yon erè rekèt bazdone fèt.", + "databaseerror-query": "Rekèt: $1", + "databaseerror-function": "Fonksyon: $1", + "databaseerror-error": "Erè: $1", "laggedslavemode": "'''Atansyon:''' paj sa a kapab pa anrejistre modifikasyon ki fèk fèt yo.", "readonly": "Bazdone a fèmen toutbon.", "enterlockreason": "Bay yon rezon pou fème bazdone a ak yon estimasyon ki lè w ap ouvri l ankò", diff --git a/languages/i18n/hu.json b/languages/i18n/hu.json index dd290a31e1..9cd104b6c9 100644 --- a/languages/i18n/hu.json +++ b/languages/i18n/hu.json @@ -216,7 +216,7 @@ "history": "Laptörténet", "history_short": "Laptörténet", "history_small": "laptörténet", - "updatedmarker": "az utolsó látogatásom óta frissítették", + "updatedmarker": "utolsó látogatásod óta frissítve", "printableversion": "Nyomtatható változat", "permalink": "Hivatkozás erre a változatra", "print": "Nyomtatás", @@ -468,7 +468,7 @@ "userlogin-createanother": "Másik felhasználói fiók létrehozása", "createacct-emailrequired": "E-mail-cím", "createacct-emailoptional": "E-mail-cím (opcionális)", - "createacct-email-ph": "Add meg e-mail-címed", + "createacct-email-ph": "Add meg az e-mail-címed", "createacct-another-email-ph": "Add meg az e-mail-címet", "createaccountmail": "Átmeneti, véletlenszerű jelszó beállítása és kiküldése a megadott e-mail-címre", "createaccountmail-help": "A jelszó megismerése nélkül készíthető valaki másnak fiók.", @@ -3796,6 +3796,14 @@ "restrictionsfield-help": "Egy IP-cím vagy CIDR-tartomány soronként. Minden engedélyezéséhez használd a következő tartományokat:\n
\n0.0.0.0/0\n::/0\n
", "edit-error-short": "Hiba: $1", "edit-error-long": "Hibák:\n\n$1", + "specialmute": "Némítás", + "specialmute-submit": "Megerősítés", + "specialmute-label-mute-email": "E-mailek némítása ettől a felhasználótól", + "specialmute-error-invalid-user": "A kért felhasználónév nem található.", + "specialmute-error-email-blacklist-disabled": "Felhasználók e-mailküldési lehetőségének némítása nincs bekapcsolva.", + "specialmute-error-email-preferences": "Először meg kell erősítened az e-mail-címedet, mielőtt lenémíthatnál egy felhasználót. Ezt a [[Special:Preferences]] oldalon tudod megtenni.", + "specialmute-email-footer": "[$1 {{BIDI:$2}} e-mail beállításainak kezelése.]", + "specialmute-login-required": "Kérjük, jelentkezz be a némítási beállításaid módosításához.", "revid": "$1 változat", "pageid": "$1 lapazonosító", "interfaceadmin-info": "$1\n\nA CSS/JS/JSON lapok szerkesztéséhez szükséges jogosultság a közelmúltban elválasztásra került a editinterface jogtól. Amennyiben nem érted, miért látod ezt az üzenetet, [[mw:MediaWiki_1.32/interface-admin|itt tudhatsz meg többet]].", diff --git a/languages/i18n/hy.json b/languages/i18n/hy.json index 4cb740b77a..0ed8f56075 100644 --- a/languages/i18n/hy.json +++ b/languages/i18n/hy.json @@ -698,7 +698,7 @@ "histfirst": "ամենահին", "histlast": "ամենաթարմ", "historysize": "({{PLURAL:$1|1 բայթ|$1 բայթ}})", - "historyempty": "(դատարկ)", + "historyempty": "դատարկ", "history-feed-title": "Փոփոխությունների պատմություն", "history-feed-description": "Վիքիի այս էջի փոփոխումների պատմություն", "history-feed-item-nocomment": "$1՝ $2", @@ -734,9 +734,9 @@ "revdelete-unsuppress": "Հանել սահմանափակումները վերականգնված տարբերակներից", "revdelete-log": "Պատճառ.", "revdelete-submit": "Կիրառել ընտրված {{PLURAL:$1|տարբերակի|տարբերակների}} վրա", - "revdelete-success": "'''Տարբերակի տեսանելիությունը բարեհաջող թարմացված է։'''", + "revdelete-success": "Տարբերակի տեսանելիությունը թարմացված է։", "revdelete-failure": "Խմբագրման տեսանելիություն հնարավոր չէր փոփոխել՝\n$1", - "logdelete-success": "'''Իրադարձության տեսանելիությունը փոփոխված է։'''", + "logdelete-success": "Իրադարձության տեսանելիությունը փոփոխված է։", "revdel-restore": "Փոխել տեսանելիությունը", "pagehist": "Էջի պատմություն", "deletedhist": "Ջնջումների պատմություն", @@ -1601,7 +1601,7 @@ "contribsub2": "{{GENDER:$3|$1}}-ի ներդրումները ($2)", "contributions-subtitle": "{{GENDER:$3|$1}}-ի համար", "nocontribs": "Այս չափանիշներին համապատասխանող փոփոխություններ չեն գտնվել։", - "uctop": " վերջինը", + "uctop": "վերջինը", "month": "Սկսած ամսից (և վաղ)՝", "year": "Սկսած տարեթվից (և վաղ)՝", "sp-contributions-newbies": "Ցույց տալ միայն նորաստեղծ հաշիվներից կատարված ներդրումները", diff --git a/languages/i18n/ia.json b/languages/i18n/ia.json index 63969abf8e..851afbab66 100644 --- a/languages/i18n/ia.json +++ b/languages/i18n/ia.json @@ -181,7 +181,7 @@ "history": "Historia del pagina", "history_short": "Historia", "history_small": "historia", - "updatedmarker": "actualisate post mi ultime visita", + "updatedmarker": "actualisate post tu ultime visita", "printableversion": "Version pro imprimer", "permalink": "Ligamine permanente", "print": "Imprimer", @@ -2138,8 +2138,8 @@ "listgrouprights-rights": "Derectos", "listgrouprights-helppage": "Help:Derectos de gruppos", "listgrouprights-members": "(lista de membros)", - "listgrouprights-addgroup": "Pote adder {{PLURAL:$2|gruppo|gruppos}}: $1", - "listgrouprights-removegroup": "Pote remover {{PLURAL:$2|gruppo|gruppos}}: $1", + "listgrouprights-addgroup": "Pote adder membros al {{PLURAL:$2|gruppo|gruppos}}: $1", + "listgrouprights-removegroup": "Pote remover membros del {{PLURAL:$2|gruppo|gruppos}}: $1", "listgrouprights-addgroup-all": "Pote adder tote le gruppos", "listgrouprights-removegroup-all": "Pote eliminar tote le gruppos", "listgrouprights-addgroup-self": "Pote adder {{PLURAL:$2|gruppo|gruppos}} al proprie conto: $1", @@ -3759,6 +3759,16 @@ "restrictionsfield-help": "Un adresse IP o intervallo CIDR per linea. Pro activar toto, usa:
0.0.0.0/0\n::/0
", "edit-error-short": "Error: $1", "edit-error-long": "Errores:\n\n$1", + "specialmute": "Silentio", + "specialmute-success": "Tu preferentias de silentio ha essite actualisate. Vide tote le usatores silentiate in [[Special:Preferences]].", + "specialmute-submit": "Confirmar", + "specialmute-label-mute-email": "Silentiar e-mail de iste usator", + "specialmute-header": "Selige tu preferentias de silentio pro {{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "Le nomine de usator que tu requestava non pote esser trovate.", + "specialmute-error-email-blacklist-disabled": "Le silentiamento de usatores pro inviar te e-mail non ha essite activate.", + "specialmute-error-email-preferences": "Tu debe confirmar tu adresse de e-mail ante de poter silentiar un usator. Face isto in [[Special:Preferences]].", + "specialmute-email-footer": "Pro gerer le preferentias de e-mail pro {{BIDI:$2}}, visita <$1>.", + "specialmute-login-required": "Es necessari aperir session pro cambiar le preferentias de silentio.", "revid": "version $1", "pageid": "ID de pagina $1", "interfaceadmin-info": "$1\n\nLe permissiones pro modificar le files CSS/JS/JSON global del sito ha recentemente essite separate del privilegio editinterface. Si tu non comprende proque tu recipe iste error, vide [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/id.json b/languages/i18n/id.json index 9785ce205c..27d95694ab 100644 --- a/languages/i18n/id.json +++ b/languages/i18n/id.json @@ -64,7 +64,8 @@ "Bagas Chrisara", "Pebaryan", "Veracious", - "Mnam23" + "Mnam23", + "Shirayuki" ] }, "tog-underline": "Garis bawahi pranala:", @@ -3130,7 +3131,7 @@ "metadata-expand": "Tampilkan rincian tambahan", "metadata-collapse": "Sembunyikan rincian tambahan", "metadata-fields": "Bidang metadata gambar yang tercantum dalam pesan ini akan dimasukkan pada tampilan halaman gambar ketika tabel metadata diciutkan.\nData lain akan disembunyikan secara bawaan.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude", - "metadata-langitem": "'''$2:''' $1", + "metadata-langitem": "$2: $1", "metadata-langitem-default": "$1", "namespacesall": "semua", "monthsall": "semua", diff --git a/languages/i18n/io.json b/languages/i18n/io.json index 41e4c54d28..a965592866 100644 --- a/languages/i18n/io.json +++ b/languages/i18n/io.json @@ -1317,7 +1317,7 @@ "nolinkstoimage": "Nula pagino ligesas ad ica pagino.", "morelinkstoimage": "Videz [[Special:WhatLinksHere/$1|plusa ligili]] ad ica arkivo.", "linkstoimage-redirect": "$1 (arkivo ridirektita) $2", - "sharedupload": "Ca arkivo esas de $1 e posible esas uzata da altra projekti.", + "sharedupload": "Ca arkivo originis de $1 e posible esas uzata da altra projeti.", "sharedupload-desc-here": "Ca arkivo jacas en $1, e povas uzesar en altra projeti.\nLa deskriptado en lua [$2 pagino di deskriptado] montresas adinfre.", "sharedupload-desc-edit": "Ca arkivo venas de $1 e povas uzesar en altra projeti.\nPosible vu deziros redaktar ibe lua deskripto en [$2 lua deskripto-pagino].", "sharedupload-desc-create": "Ca arkivo venas de $1 e povas uzesar en altra projeti.\nPosible vu deziros redaktar ibe lua deskripto en [$2 lua deskripto-pagino].", diff --git a/languages/i18n/it.json b/languages/i18n/it.json index 5e3bc6527c..1dd959a5a7 100644 --- a/languages/i18n/it.json +++ b/languages/i18n/it.json @@ -288,7 +288,7 @@ "history": "Cronologia della pagina", "history_short": "Cronologia", "history_small": "cronologia", - "updatedmarker": "modificata dalla mia ultima visita", + "updatedmarker": "modificata dalla tua ultima visita", "printableversion": "Versione stampabile", "permalink": "Link permanente", "print": "Stampa", @@ -2608,6 +2608,8 @@ "createaccountblock": "registrazione bloccata", "emailblock": "e-mail bloccate", "blocklist-nousertalk": "non può modificare la propria pagina di discussione", + "blocklist-editing": "modifica", + "blocklist-editing-sitewide": "modifica (sito intero)", "blocklist-editing-page": "pagine", "blocklist-editing-ns": "namespace", "ipblocklist-empty": "L'elenco dei blocchi è vuoto.", @@ -3816,6 +3818,9 @@ "restrictionsfield-help": "Un indirizzo IP o intervallo CIDR per linea. Per consentire tutto, utilizza:
0.0.0.0/0\n::/0
", "edit-error-short": "Errore: $1", "edit-error-long": "Errori:\n\n$1", + "specialmute": "Muto", + "specialmute-submit": "Conferma", + "specialmute-error-invalid-user": "Impossibile trovare il nome utente richiesto.", "revid": "versione $1", "pageid": "ID della pagina $1", "rawhtml-notallowed": "I tag <html> non possono essere utilizzati al di fuori delle normali pagine.", diff --git a/languages/i18n/ja.json b/languages/i18n/ja.json index e27a85022f..02766453fe 100644 --- a/languages/i18n/ja.json +++ b/languages/i18n/ja.json @@ -95,7 +95,8 @@ "Suyama", "고솜", "Wat", - "Puntti ja" + "Puntti ja", + "マツムシ" ] }, "tog-underline": "リンクの下線:", @@ -740,6 +741,8 @@ "autoblockedtext": "このIPアドレスは、$1によりブロックされた利用者によって使用されたため、自動的にブロックされています。\n理由は次の通りです。\n\n:$2\n\n* ブロック開始日時: $8\n* ブロック解除予定: $6\n* ブロック対象: $7\n\n$1または他の[[{{MediaWiki:Grouppage-sysop}}|管理者]]にこのブロックについて問い合わせることができます。\n\nただし、[[Special:Preferences|個人設定]]に正しいメールアドレスが登録されていない場合、またはメール送信がブロックされている場合、「{{int:emailuser}}」機能を使用できないことに注意してください。\n\n現在ご使用中のIPアドレスは$3 、このブロックIDは#$5です。\nお問い合わせの際は、上記の情報を必ず書いてください。", "systemblockedtext": "あなたの利用者名またはIPアドレスはMediaWikiによって自動的にブロックされています。\n理由は次の通りです。\n\n:$2\n\n* ブロック開始日時: $8\n* ブロック解除予定: $6\n* ブロック対象: $7\n\nあなたの現在のIPアドレスは $3 です。\nお問い合わせの際は、上記の詳細情報をすべて含めてください。", "blockednoreason": "理由が設定されていません", + "blockedtext-composite": "あなたのアカウントまたはIPアドレスはブロックされています\n\n理由:\n\n:$2.\n\n* ブロック開始日: $8\n* ブロックの有効期限: $6\n\nあなたの現在のIPアドレスは$3です。\n上記の詳細は,ご質問にお答えください。", + "blockedtext-composite-reason": "アカウントまたはIPアドレスに対して複数のブロックが存在します", "whitelistedittext": "このページを編集するには$1してください。", "confirmedittext": "ページの編集を始める前にメールアドレスの確認をする必要があります。\n[[Special:Preferences|個人設定]]でメールアドレスを設定し、確認を行ってください。", "nosuchsectiontitle": "節が見つかりません", @@ -4022,6 +4025,9 @@ "restrictionsfield-help": "一行につき、単一の IP アドレス、もしくは CIDR による範囲。全帯域からの接続を許可する場合:
0.0.0.0/0\n::/0
", "edit-error-short": "エラー: $1", "edit-error-long": "エラー:\n\n\n\n$1", + "specialmute": "ミュート", + "specialmute-label-mute-email": "この利用者からのウィキメールをミュートする", + "specialmute-error-invalid-user": "あなたが要求した利用者名は見つかりませんでした。", "revid": "版 $1", "pageid": "ページID $1", "interfaceadmin-info": "$1\n\nサイト全体のCSS/JavaScriptの編集権限は、最近editinterface 権限から分離されました。なぜこのエラーが表示されたのかわからない場合は、[[mw:MediaWiki_1.32/interface-admin]]をご覧ください。", diff --git a/languages/i18n/jv.json b/languages/i18n/jv.json index 92f091d361..8bf1a308eb 100644 --- a/languages/i18n/jv.json +++ b/languages/i18n/jv.json @@ -652,7 +652,7 @@ "editingcomment": "Mbesut $1 (pérangan anyar)", "editconflict": "Cengkah besutan: $1", "explainconflict": "Wong liya wis mbesut kaca iki wiwit panjenengan lekas mbesut.\nBagian dhuwur tèks iki ngamot tèks kaca vèrsi saiki.\nPangowahan kang panjenengan lakoni dituduhaké ing bagian ngisor tèks.\nPanjenengan namung prelu nggabungaké pangowahan panjenengan karo tèks kang wis ana.\n'''Namung''' tèks ing bagian dhuwur kaca kang bakal kasimpen manawa panjenengan mencèt \"$1\".", - "yourtext": "Tèksé panjenengan", + "yourtext": "Tèksmu", "storedversion": "Owahan kasimpen", "editingold": "'''PÈNGET:''' Panjenengan mbesut revisi lawas saka siji kaca. Yèn versi iki panjenengan simpen, mengko pangowahan-pangowahan kang wis digawé wiwit revisi iki bakal ilang.", "yourdiff": "Béda", diff --git a/languages/i18n/ka.json b/languages/i18n/ka.json index e8c35204c9..264e0c9133 100644 --- a/languages/i18n/ka.json +++ b/languages/i18n/ka.json @@ -31,7 +31,8 @@ "OpusDEI", "Fitoschido", "Mehman97", - "Vlad5250" + "Vlad5250", + "Shirayuki" ] }, "tog-underline": "ბმულების ხაზგასმა:", @@ -2949,7 +2950,7 @@ "metadata-expand": "დამატებითი ინფორმაციის ჩვენება", "metadata-collapse": "დამატებითი ინფორმაციის დამალვა", "metadata-fields": "მეტამონაცემების ჩამონათვალი ამ შეტყობინებაში დამატებული იქნება სურათის გვერდზე, როცა მეტამონაცემების ცხრილი გახსნილია.\nსხვები უპირობოდ დამალული იქნება.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude", - "metadata-langitem": "'''$2:''' $1", + "metadata-langitem": "$2: $1", "metadata-langitem-default": "$1", "namespacesall": "ყველა", "monthsall": "ყველა", diff --git a/languages/i18n/ko.json b/languages/i18n/ko.json index 1a85840c84..da07dd65ac 100644 --- a/languages/i18n/ko.json +++ b/languages/i18n/ko.json @@ -3141,7 +3141,7 @@ "confirm-unwatch-top": "이 문서를 주시문서 목록에서 뺄까요?", "confirm-rollback-button": "확인", "confirm-rollback-top": "이 문서의 편집을 되돌리시겠습니까?", - "confirm-rollback-bottom": "이 작업은 선택된 변경 사항을 즉시 롤백합니다", + "confirm-rollback-bottom": "이 작업은 이 문서의 선택된 변경 사항을 즉시 되돌립니다.", "confirm-mcrrestore-title": "판 복구", "confirm-mcrundo-title": "변경사항 취소", "mcrundofailed": "실행 취소를 실패했습니다", @@ -3826,6 +3826,16 @@ "restrictionsfield-help": "줄 단위의 하나의 IP 주소 또는 CIDR 대역입니다. 모든 곳에 적용하려면, 다음을 사용하세요:
0.0.0.0/0\n::/0
", "edit-error-short": "오류: $1", "edit-error-long": "오류:\n\n$1", + "specialmute": "알림 미표시", + "specialmute-success": "알림 미표시 환경 설정이 성공적으로 업데이트되었습니다. [[Special:Preferences]]에서 알림이 표시되지 않는 모든 사용자를 확인하십시오.", + "specialmute-submit": "확인", + "specialmute-label-mute-email": "이 사용자의 이메일 알림을 표시하지 않습니다", + "specialmute-header": "{{BIDI:[[User:$1]]}}의 알림 미표시 환경 설정을 선택해 주십시오.", + "specialmute-error-invalid-user": "요청한 사용자 이름을 찾을 수 없습니다.", + "specialmute-error-email-blacklist-disabled": "이메일 보내기로부터 사용자 알림 미표시가 활성화되어 있지 않습니다.", + "specialmute-error-email-preferences": "사용자의 알림을 미표시 처리하기 전에 이메일 주소를 확인해야 합니다. [[Special:Preferences]]에서 이 작업을 할 수 있습니다.", + "specialmute-email-footer": "{{BIDI:$2}}의 이메일 환경 설정을 관리하려면 <$1>을(를) 방문해 주십시오.", + "specialmute-login-required": "알림 미표시 환경 설정을 변경하려면 로그인해 주십시오.", "revid": "$1 판", "pageid": "페이지 ID $1", "interfaceadmin-info": "$1\n\n사이트 전체에 쓰이는 CSS/JS/JSON 파일의 편집 권한이 최근 editinterface 권한에서 분리되었습니다. 왜 이 오류가 발생하는지 이해가 되지 않는다면, [[mw:MediaWiki_1.32/interface-admin]]을 참고하십시오.", diff --git a/languages/i18n/lb.json b/languages/i18n/lb.json index 4f97e29d1c..d61a2e68d4 100644 --- a/languages/i18n/lb.json +++ b/languages/i18n/lb.json @@ -180,7 +180,7 @@ "history": "Historique vun der Säit", "history_short": "Versiounen", "history_small": "Versiounen", - "updatedmarker": "geännert zanter ech d'Säit fir d'lescht gekuckt hunn", + "updatedmarker": "geännert zanter Ärem leschte Besuch", "printableversion": "Drockversioun", "permalink": "Zitéierfäege Link", "print": "Drécken", @@ -3414,6 +3414,9 @@ "restrictionsfield-label": "Zougeloossen IP-Beräicher:", "edit-error-short": "Feeler: $1", "edit-error-long": "Feeler:\n\n$1", + "specialmute": "Toun aus", + "specialmute-submit": "Confirméieren", + "specialmute-error-invalid-user": "De Gefrote Benotzernumm gouf net fonnt.", "revid": "Versioun $1", "gotointerwiki": "{{SITENAME}} verloossen", "gotointerwiki-invalid": "De spezifizéierten Titel ass net valabel.", diff --git a/languages/i18n/lrc.json b/languages/i18n/lrc.json index a93a0cb3e5..2a90040af9 100644 --- a/languages/i18n/lrc.json +++ b/languages/i18n/lrc.json @@ -36,7 +36,7 @@ "tog-enotifwatchlistpages": "ٱر یاٛ بٱلگٱ یا جانؽا د ساٛلٛ بٱرگ ماْ آلشت بۊئٱ ماْ ناْ ڤا ٱنجومانامٱ خڤٱر کو", "tog-enotifusertalkpages": "ڤٱختؽ کاْ بٱلگٱ سالفٱ کاریاریم آلشت کاری بی ماْ ناْ ڤا ٱنجومانامٱ خڤٱر کو", "tog-enotifminoredits": "ڤٱختؽ کاْ ڤیرایشؽا کوچکؽ د بٱلگٱیایا جانؽایا ٱنجوم بۊئٱ ماْ ناْ ڤارٱسیاری کو", - "tog-enotifrevealaddr": "تیر نشوݩ ٱنجومانامٱ ماْ ناْ د ٱنجومانامٱ دؽارکو دؽاری کو", + "tog-enotifrevealaddr": "تیرنشوݩ ٱنجومانامٱ ماْ ناْ د ٱنجومانامٱ دؽارکو دؽاری کو", "tog-shownumberswatching": "ٱندازٱ کاریارؽایی کاْ د هال ۉ بال دیئن هؽسن دؽاری کو", "tog-oldsig": "اْمزا ایسنی شما:", "tog-fancysig": "ڤا اْمزا چی یاٛ ڤیکی نیسسٱ رٱفتار کو", @@ -312,7 +312,7 @@ "filecopyerror": "نمۊئٱ جانؽا $1 د $2 ڤرداشتٱ بۊئٱ", "filerenameerror": "نمۊئٱ نوم جانؽا $1 د $2 آلشت کاری بۊئٱ.", "filedeleteerror": "نمۊئٱ جانؽا $1 پاکسا بۊئٱ.", - "directorycreateerror": "نمۊئٱ تیرنشونگٱاٛ$1 دۏرس بۊئٱ.", + "directorycreateerror": "نمۊئٱ تیرنشونگٱ$1 دۏرس بۊئٱ.", "directoryreadonlyerror": "فقٱت مۊئٱ تیرنشونگٱ \"$1\" ناْ بونی.", "directorynotreadableerror": "تیرنشونگٱ \"$1\" ڤٱننی نؽ.", "filenotfound": "نمؽ تونؽت جانؽا $1 ناْ بٱجۊرؽت.", @@ -395,7 +395,7 @@ "createaccount": "هساو دۏرس بٱکؽت", "userlogin-resetpassword-link": "رازینٱ گوئارسن تو د ڤیرتو رٱتٱ؟", "userlogin-helplink2": "هومیاری کردن د تٱریق ڤامؽن اوماین", - "userlogin-loggedin": "شما ایساْ چی یاٛ {{GENDER:$1|$1}} اومایتٱ ڤامؽن.نوم بٱلگٱ هاری ناْ سی ڤامؽن اوماین چی یاٛ کاریار هنی بٱلگٱ هاری سی ڤا مؽن اومابن چی یاٛ کاریار هنی ڤ کار باٛیرؽت.", + "userlogin-loggedin": "شما ایساْ چی یاٛ {{GENDER:$1|$1}} اومایتٱ ڤامؽن.نوم بٱلگٱ هاری ناْ سی ڤامؽن اوماین چی یاٛ کاریار هنی بٱلگٱ هاری سی ڤا مؽن اوماین چی یاٛ کاریار هنی ڤ کار باٛیرؽت.", "userlogin-createanother": "یاٛ هساو هنی دۏرس بٱکؽت", "createacct-emailrequired": "تیرنشوݩ ٱنجومانامٱ", "createacct-emailoptional": "تیرنشوݩ ٱنجومانامٱ", @@ -662,28 +662,28 @@ "post-expand-template-inclusion-warning": "زٱنڳیار چۊئٱ د ڤٱر گرتٱ ٱندازاٛ یٱ کاْ فرٱ گٱپٱ.پاراٛیؽ د چۊئٱیا ناْ د ڤٱر نماٛیرٱ.", "post-expand-template-inclusion-category": "بٱلگٱیا د ڤٱر گرتٱ چۊئٱ هؽسن کاْ ٱندازٱش د هٱد اومایٱ ڤ دٱر", "post-expand-template-argument-warning": "زٱنڳیار اؽ بٱلگٱ د ڤٱر گرتٱ هٱدٱقٱل یاٛ چۊئٱ سی چٱک چنٱ یٱ کاْ ٱندازٱ فرٱ گٱپٱ.\nگٱپسنؽا پاک بینٱ.", - "post-expand-template-argument-category": "بلگه د ور گرته چوئه چک چنیا د بین رئته", - "parser-template-loop-warning": "حلقه چوئه دیاری کرده:[[$1]]", - "parser-template-recursion-depth-warning": "محدودیت پی یا ورئشتن چوئه رد بی($1)", - "language-converter-depth-warning": "محدودیت پی یا زون والرن رد بی($1)", - "node-count-exceeded-category": "بلگه یا که د بیشرونه شماره گرو فره پئشکرد کردنه", - "node-count-exceeded-category-desc": "زیردسه سی بلگه یایی که د ونو اشمارنه فره پئشکرد کرده.", - "node-count-exceeded-warning": "بلگه د بیشترونه شماره گرو فره پئشکرد کرد", - "expansion-depth-exceeded-category": "بلگه یایی که د بیشترونه پی یا ووله کردن فره پئشکرد کردنه", - "expansion-depth-exceeded-category-desc": "زیر دسه سی بلگه یایی که د ونو پی یا ووله بیین فره پئشکرد کرده.", - "expansion-depth-exceeded-warning": "بلگه د پی یا ووله بیین پئشکرد کرد", - "parser-unstrip-loop-warning": "گردوله د فرمونه Unstrip پیدا بیه", - "unstrip-depth-warning": "د بیشترونه د سرچشمه رئتن د دستور Unstrip واروتر رئتیته($1)", - "converter-manual-rule-error": "خطا د قانون والرشتن دسی زون", - "undo-success": "نبوئه ویرایشت نه انجومشیو بکیت.\nلطفا ای فرخی که ها د هار نه وارسی بکیت تا یه کاریه که میهات انجوم بئیت، و اوسه آلشتیا هار نه اماییه بکیت سی یه که خمثی کردن ویرایشت نه انجوم بئیت.", + "post-expand-template-argument-category": "بٱلگٱ د ڤٱر گرتٱ چۊئٱ چٱک چنٱ د باٛن رٱتٱ", + "parser-template-loop-warning": "هٱلقٱ چۊئٱ دؽاری کردٱ:[[$1]]", + "parser-template-recursion-depth-warning": "مٱهدۊدیٱت پی یا ڤرگٱشتن چۊئٱ رٱد بی($1)", + "language-converter-depth-warning": "مٱهدۊدیٱت پی یا زڤوݩ ڤالٛرن رٱد بی($1)", + "node-count-exceeded-category": "بٱلگٱیا کاْ د بؽشرونٱ شمارٱ گرۊ فرٱ پیشکرد کردنٱ", + "node-count-exceeded-category-desc": "زؽردٱسٱ سی بٱلگٱیایؽ کاْ د ڤنو اْشمارنٱ فرٱ پیشکرد کردٱ.", + "node-count-exceeded-warning": "بٱلگٱ د بؽشترونٱ شمارٱ گرۊ فرٱ پیشکرد کرد", + "expansion-depth-exceeded-category": "بٱلگٱیایؽ کاْ د بؽشترونٱ پی یا ڤلٱ کردن فرٱ پیشکرد کردنٱ", + "expansion-depth-exceeded-category-desc": "زؽر دٱسٱ سی بٱلگٱیایؽ کاْ د ڤنو پی یا ڤلٱ بیئن فرٱ پیشکرد کردٱ.", + "expansion-depth-exceeded-warning": "بٱلگٱ د پی یا ڤلٱ بیئن پیشکرد کرد", + "parser-unstrip-loop-warning": "گردۊلٱ د فرمونٱ Unstrip پاٛدا بیٱ", + "unstrip-depth-warning": "د بؽشترونٱ د سرچشمٱ رٱتن د دٱسدۊر Unstrip ڤارۉتر رٱتؽتٱ($1)", + "converter-manual-rule-error": "خٱتا د قانۊن ڤالٛرشتن دٱسی زڤوݩ", + "undo-success": "نمۊئٱ ڤیرایش ناْ ٱنجومشیو بٱکؽت.\nلوتفٱن اؽ فٱرخؽ کاْ ها د هار ناْ ڤارسی بٱکؽت تا یاٛ کارؽ کاْ مؽهایت ٱنجوم باٛیؽت،ۉ اۊساْ آلشتؽا هار ناْآمادٱ بٱکؽت سی یٱ کاْ خونسا کردن ڤیرایش ناْ ٱنجوم باٛیؽت.", "undo-failure": "سی ری ڤ ری بیئن اؽ ڤیرایش ڤا ڤیرایشؽا مؽنجایی، نمۊئٱ اؽ ڤیرایش ناْ خونسا بٱکؽت.", - "undo-norev": "نبوئه ای ویرایشت نه خومثی بکیت سی یه که یا وجود ناره یا پاکسا بیه.", - "undo-nochange": "وه نظر میا که ای ویرایشت د ایسنیا خومثی بیه.", - "undo-summary": "خومثی بیئن وانئری وا $1 [[Special:Contributions/$2|$2]] ([[User talk:$2|چک چنه]])", - "undo-summary-username-hidden": "خومثی بیئن وانئری $1 وا یه گل کاریار قام بیه", - "cantcreateaccount-text": "حساو دروس بیه و ا ای تیرنشون آی پی($1) وه دس ای [[کاریار:$3|$3]] قلف بیه.\n\n\nدلیل دئه بیه وا $3 ها د$2", + "undo-norev": "نمۊئٱ اؽ ڤیرایش ناْ خونسا بٱکؽت سی یٱ کاْ یا ڤوجۊد نارٱ یا پاکسا بیٱ.", + "undo-nochange": "ڤ نٱزٱر مؽا کاْ اؽ ڤیرایش د ایسنیا خونسی بیٱ.", + "undo-summary": "خونسا بیئن ڤانری ڤا $1 [[Special:Contributions/$2|$2]] ([[User talk:$2|چٱک چنٱ]])", + "undo-summary-username-hidden": "خونسا بیئن ڤانری $1 ڤا یاٛ کاریار قایم بیٱ", + "cantcreateaccount-text": "هساو دۏرس بیٱ ڤا اؽ تیرنشوݩ آی پی($1) ڤ دٱس اؽ [[کاریار:$3|$3]] قلف بیٱ.\n\n\nدلیل دئه بیه وا $3 ها د$2", "cantcreateaccount-range-text": "حساو دروس بیه وا تیرنشون آی پی که د پوشینه $1 ، که وه ئم مینونه دار تیرنشون آی پی شما ئم هئ($4)، وه دس [[کاریار:$3|$3]]قلف بیه.\n\nدلیل دئه بیه وا $3، \"$2\" ئه.", - "viewpagelogs": "ساٛلٛ پهرستنومٱیا اب بٱلگٱ بٱکؽت", + "viewpagelogs": "ساٛلٛ پهرستنومٱیا اؽ بٱلگٱ بٱکؽت", "nohistory": "هیچ ویرگار ویرایشتی د ای بلگه نئ.", "currentrev": "آخرین دوواره دیئن", "currentrev-asof": "آخری ڤانری چی $1", @@ -852,8 +852,8 @@ "search-relatedarticle": "مرتوط", "searchrelated": "مرتوط", "searchall": "همٱ", - "showingresults": "نمائشت بیشترونه {{PLURAL:$1|'''۱''' نتیجه|'''$1''' نتیجه}} د هار، شرو د شماره'''$2'''.", - "showingresultsinrange": "نمائشت بیشترونه {{PLURAL:$1|'''۱''' نتیجه|'''$1''' نتیجه}} د هار، شرو د شماره'''$2''' تا شماره '''$3'''.", + "showingresults": "نمایش بؽشترونٱ {{PLURAL:$1|'''۱''' نتیجٱ|'''$1''' نتیجٱ}} د هار، شرۊ د شمارٱ'''$2'''.", + "showingresultsinrange": "نمایش بؽشترونٱ {{PLURAL:$1|'''۱''' نتیجٱ|'''$1''' نتیجٱ}} د هار، شرۊ د شمارٱ'''$2''' تا شمارٱ '''$3'''.", "search-showingresults": "{{PLURAL:$4|نٱتیجٱیا$1 د $3|نٱتیجٱیا$1 - $2$3
}}", "search-nonefound": "هیچ نتیجاٛیؽ ڤا پاٛجۊری تو یٱکؽ نؽ.", "powersearch-legend": "پی جوری پیشکرده", @@ -940,8 +940,8 @@ "prefs-registration-date-time": "$1", "yourrealname": "نوم راستكی:", "yourlanguage": "زوٙن:", - "yourvariant": "مینونه آلشتگر زون:", - "prefs-help-variant": "قسه وری انتخاوی شما سی نمائشت مینونه بلگه یا د ای ویکی.", + "yourvariant": "مینونٱ آلشتگٱر زڤوݩ:", + "prefs-help-variant": "قسٱ ڤری اْنتخاویی شما سی نمایش مینونٱ بٱلگٱیا د اؽ ڤیکی.", "yournick": "امضا تازه:", "prefs-help-signature": "ویر و باوریا نیسسه بیه د بلگه چک چنه باید وا«~~~~» امضا بان؛ ای نشون وه شکل خودانجومی وه امضا شما و مؤر ویرگار تبدیل بوئه.", "badsig": "ئمضا خوم بی ئتئڤار.\nسأردیسیا ئچ تی ئم ئل نە ڤارئسی بأکیت.", @@ -1019,11 +1019,11 @@ "right-createtalk": "بلگه یا چک چنه نه راس بکید", "right-createaccount": "یه گل حساو کاروری تازه راس بکیت", "right-minoredit": "نشودار کردن همه ویرایشتیا چی حیرده", - "right-move": "بلگه یا جا وه جا کو", - "right-move-subpages": "بلگه یا و زیر بلگه یا شونه جا وه جا کو", - "right-move-rootuserpages": "بلگه یا ریشه ای کارور نه جا وه جا کو", + "right-move": "بٱلگٱیا ناْ جا ڤ جا کو", + "right-move-subpages": "بٱلگٱیا ۉ زؽر بٱلگٱیا شوناْ جا ڤ جا کو", + "right-move-rootuserpages": "بٱلگٱیا ریشاٛیی کاریار ناْ جا ڤ جا کو", "right-move-categorypages": "دسه بلگه یا نه جا وه جا بکیت", - "right-movefile": "جانیایا نه جا وه جا کو", + "right-movefile": "جانؽایا ناْ جا ڤ جا کو", "right-suppressredirect": "اوسه که بلگه یا د بین رئتنه هیچ واگردونی سی بلگه یا سرچشمه دروس نبیه", "right-upload": "سوار کردن جانیایا", "right-reupload": "سوارکرد هنی جانیایی که دماتر بئیشه", @@ -1095,7 +1095,7 @@ "action-createaccount": "هساو اؽ کاریار ناْ دۏرس بٱکؽت", "action-history": "ویرگار ای بلگه نه بوینیت", "action-minoredit": "ای ویرایشت نه چی یه حیرده ویرایشت نشو بیئت", - "action-move": "لی بلگه جا وه جا کو", + "action-move": "اؽ بٱلگٱ ناْ جا ڤ جا کو", "action-move-subpages": "ای بلگه و زیر بلگه یاشه جا وه جا بکید", "action-move-rootuserpages": "بلگه یا ریشه ای کاریار نه جا وه جا بکید", "action-move-categorypages": "جا وه جا کردن دسه بلگه یا", @@ -1214,7 +1214,7 @@ "upload-preferred": "جوٙرا حاستئنی جانیا {{PLURAL:$2|جوٙر|جوٙرا}}:$1 .", "upload-prohibited": "جورا جانیا صلادار:$1{{PLURAL:$2|.}}", "uploadlogpage": "سڤارکرد", - "uploadlogpagetext": "نومگه هاری یه گل نومگه د آخری سوارکرد جانیایا هئ.\nسی د نو سیل کردن[[Special:NewFiles|عسگدونی جانیایا تازه نه]] به ونیت.", + "uploadlogpagetext": "نومگٱ هاری یاٛ نومگٱ د آخری سڤارکرد جانؽایا هؽ.\nسی د نۊ ساٛلٛ کردن[[Special:NewFiles|عٱسگدونی جانؽایا تازٱ ناْ]] بڤٱنؽت.", "filename": "نوم جانیا", "filedesc": "چکسٱ", "fileuploadsummary": "چکسه", @@ -1413,7 +1413,7 @@ "filehist-comment": "ڤیر ۉ باڤٱر", "imagelinks": "ڤ کار گرتن جانؽا", "linkstoimage": "دۏنبال بيٱ {{PLURAL:$1|ديس ڤنؽا بٱلگٱ|$1 ديس ڤنؽا بٱلگٱيا}} د اؽ فایلٛ:", - "linkstoimage-more": "بؽشتر د $1 بٱلگٱ د اؽ جانؽا هوم پاٛڤٱن {{PLURAL:$1|بٱ|بینٱ}}.\nنومگٱ هاری تٱنڳؽا{{PLURAL:$1|ٱڤلی هوم پاٛڤٱن|ٱڤلی $1 هوم پاٛڤٱن}} د ؽ بٱلگٱ ناْ نشوݩ مؽ یٱ.\n[[Special:WhatLinksHere/$2|نومگٱ کامل]] ٱم هؽسش.", + "linkstoimage-more": "بؽشتر د $1 بٱلگٱ د اؽ جانؽا هوم پاٛڤٱن {{PLURAL:$1|بٱ|بینٱ}}.\nنومگٱ هاری تٱنڳؽا{{PLURAL:$1|ٱڤلی هوم پاٛڤٱن|ٱڤلی $1 هوم پاٛڤٱن}} د ؽ بٱلگٱ ناْ نشوݩ ماٛیٱ.\n[[Special:WhatLinksHere/$2|نومگٱ کامل]] ٱم هؽسش.", "nolinkstoimage": "ایچاْ هیچ بٱلگاٛیی سی هوم پیاٛڤٱن بیئن ڤا اؽ جانؽا نؽ", "morelinkstoimage": " [[ویجه:چه هوم پیوندی ها ایچه/$1|هوم پیوندیا هنی]]سی ای جانیا نه بونیت.", "linkstoimage-redirect": "$1 (ڤاگٱردونی جانؽا) $2", @@ -1505,8 +1505,8 @@ "brokenredirectstext": "واگردونیا نهاتر د بلگه یایی که وجود نارن هوم پیوند بینه.", "brokenredirects-edit": "ڤیرایئشت", "brokenredirects-delete": "پاكسا كردن", - "withoutinterwiki": "بلگه یایی که هوم پیوند زون نارن", - "withoutinterwiki-summary": "بلگه یا هاری وه زون نسقه یا زونا تر هوم پیوند نبیه.", + "withoutinterwiki": "بٱلگٱیایؽ کاْ هوم پاٛڤٱن زڤوݩ نارٱن", + "withoutinterwiki-summary": "بٱلگٱیا هاری ڤ زڤوݩ نۏسخٱیا زڤونؽا تر هوم پاٛڤٱن ناٛئینٱ.", "withoutinterwiki-legend": "پیشون", "withoutinterwiki-submit": "نشون دائن", "fewestrevisions": "بلگه یایی که کمتری وانئری نه دارن", @@ -1521,7 +1521,7 @@ "ntransclusions": "$1 {{PLURAL:$1|بلگه|بلگيا}} استفاده بیه", "specialpage-empty": "نتیجه ای د ای گزارشت نئ.", "lonelypages": "بلگه یا تک منه", - "lonelypagestext": "د بلگه یا هاری هیچ بلگه هنی د {{SITENAME}} هوم پیوند نبیه و د هیچ بلگه هنی مین چین نبیه.", + "lonelypagestext": "د بٱلگٱیا هاری هیژ بٱلگاٛ هنی د {{SITENAME}} هوم پاٛڤٱن ناٛئیٱۉ د هیژ بٱلگاٛ هنی مؽن چین ناٛئیٱ.", "uncategorizedpages": "بلگه یا دسه بنی نبیه", "uncategorizedcategories": "دسه یا دسه بنی نبیه", "uncategorizedimages": "فایلیا دسه بنی نبیه", @@ -1551,7 +1551,7 @@ "shortpages": "بلگه یا کؤچک", "longpages": "بلگه یا گپ", "deadendpages": "بلگه یا نابود بیئنی", - "deadendpagestext": "بلگه یا هاری وه هیچ بلگه هنی د {{SITENAME}} هوم پیوند نبینه.", + "deadendpagestext": "بٱلگٱیا هاری ڤ هیژ بٱلگاٛ هنی د {{SITENAME}} هوم پاٛڤٱن ناٛئینٱ.", "protectedpages": "بلگه یا حفاظت بيه", "protectedpages-indef": "فقط پر و پیم بیین یا بی زمون", "protectedpages-summary": "د ای بلگه نومگه بلگه یایی هیئن که د ایسنی پر و پیم بینه. سی نومگه سرونیا که نبوئه دروس بان، سیل[[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]] بکیت.", @@ -1767,7 +1767,7 @@ "exbeforeblank": "مینونه حالی دمایی:\"$1\" بی", "delete-confirm": "پاکسا کئردئن \"$1\"", "delete-legend": "پاکسا کئردئن", - "historywarning": "هشدار: بلگه یی که شما میهایت پاکساش بکیت دش یه گل ویرگارچه واگرد $1 {{PLURAL:$1|وانئری|وانئریا}} ئه:", + "historywarning": "هوشدار: بٱلگاٛیؽ کاْ شما مؽهایت پاکساش بٱکؽت دش یاٛ ڤیرگارچٱ ڤاگرد $1 {{PLURAL:$1|ڤانری|ڤانریا}} ئٱ:", "confirmdeletetext": "شما د حال و بار پاکسا کردن یه گل بلگه یا عسگ د رسینه جا واگرد همه ویرگارچه ونیت.\nلطف بکیت ای کنشتکاری نه پشت راسکاری بکیت و یه دل بوئیت که سرانجوم ای کار نه دونیت و ای کار نه مطابق وا [[{{MediaWiki:Policy-url}}|سیاستیا]] انجوم دئیته.", "actioncomplete": "عملكرد كامل بيه", "actionfailed": "عملكرد شكست حرده", @@ -1872,7 +1872,7 @@ "undeleterevisions": "$1 نسقه مال دیاری{{PLURAL:$1|بیه|بینه}}", "undeletehistory": "ار ای بلگه نه د نو زنه بکیت، همه نسقه یا وه د ویرگارچه ش د نو زنه بوئن.\nار بلگه تازه یی وا نوم هومبراوری د گات پاکسا بیین دروس بیه با، نسقه یا د نو زنه بیه د ویرگارچه ره وندیاری می کن.", "undeleterevdel": "ناپاکسا کردن بلگه یا د حال و باری که باعث پاکسا بیین بهرجایی د آخری نسقه بلگه یا جانیا با امکانش نئ.\nد ای حال و بار شما واس تازه تری نسقه پاکساگری بینه ئم د نو زنه بکیت.", - "undeletehistorynoadmin": "ای بلگه پاکسا بیه.\nدلیل پاکسا بیین ای بلگه واگرد مشخصات کاریاریایی که دما د پاکسا کردن ای بلگه نه ویرایشت دئنه ها د چکسته هاری.\nنیسسه راستیکی ای ویرایشت پاکسا بیه و فقط ها د دسرس دیوونداریا.", + "undeletehistorynoadmin": "اؽ بٱلگٱ پاکسا بیٱ.\nدلٛیلٛ پاکسا بیئن اؽ بٱلگٱ ڤاگرد موشٱخٱسؽا کاریارؽایؽ کاْ دما د پاکسا کردن اؽ بٱلگٱ ناْ ڤیرایش کردنٱ ها د چکسٱ هاری.\nنیسسٱ راسٱکی اؽ ڤیرایش پاکسا بیٱ ۉ فقٱت ها د دٱسرس دیڤوندارؽا.", "undelete-revision": "نسقه پاکسا بیه $1 (د ویرگار$4 ساعت $5) وه دس $3:", "undeleterevision-missing": "وانئری یا گم بیه یا نامعتوره.\nشایت هوم پیوند شما دروس نبوئه یا یه که ای وانئری د اماییه جا پاکسا بیه یا بازجست بیه.", "undelete-nodiff": "وانئری دماتری پیدا نبیه.", @@ -2071,8 +2071,8 @@ "lockfilenotwritable": "نبوئه قلف رسینه جا نه بنیسیت. سی یه بتونیت رسینه جا قلف بکیت یا قلفش وا بکیت، واس ای جانیا نیسسه یی بوئه.", "databasenotlocked": "رسینه گا وازه.", "lockedbyandtime": "(وا{{GENDER:$1|$1}} د $2 د$3)", - "move-page": "$1 جا وه جا کو", - "move-page-legend": "بلگه نه جا وه جا کو", + "move-page": "$1 جا ڤ جا کو", + "move-page-legend": "بٱلگٱ ناْ جا ڤ جا کو", "movepagetext": "وا وه کار گرتن نوم بلگه های نوم بلگه آلشت موئه، و همه ویرگارچه وه روئه وه نوم تازه ش.\nشما می تونیت آلشتکاری مسیریایی که وه داسون اصلی خوشو اشاره می کن نه وه هنگوم سازی بکیت.\nهوم پیوندیایی که چی بلگه دماترین، آلشتکاری نموئن؛ حتمن آلشت کاری مسیریا [[Special:DoubleRedirects|دوتایی]] یا [[Special:BrokenRedirects|خروا]] نه وارسی بکیت.\n'''شما''' مسئول یه دل بیین ده یه نیت که هوم پیوندیا هنی هان د هموچه که قراره روئن.\n\nد ویر داشوئیت که ار د دما یه گل بلگه د داسون تازه با بلگه\nجا وه جا '''نبوئه'''،\nمر یه آخری ویرایشت آلشتکاری مسیر با و د ویرگارچه ویرایشتی نبوئه.\nوه یئنی که ار اشتوا کردیته می تونیت بلگه نه د هموچه که جا وه جا بیه ورگردونیت و یه که نمی تونیت ری بلگه یا ایسنی بنیسیت. \n\n'''هشدار!'''\nجاوه جا کاری بلگه د نوم تازه شایت یه گل آلشتکاری پایه یی و ناحاستنی سی بلگه یا حاستنی با؛\nلطف بکیت یه دل بوئیت که دما د جا وه جا کاری بلگه، عاقوت ای کار نه دونیت.", "movepagetext-noredirectfixer": "وا وه کار گرتن نوم بلگه های نوم بلگه آلشت موئه، و همه ویرگارچه وه روئه وه نوم تازه ش.\nشما می تونیت آلشتکاری مسیریایی که وه داسون اصلی خوشو اشاره می کن نه وه هنگوم سازی بکیت.\nهوم پیوندیایی که چی بلگه دماترین، آلشتکاری نموئن؛ حتمن آلشت کاری مسیریا [[Special:DoubleRedirects|دوتایی]] یا [[Special:BrokenRedirects|خروا]] نه وارسی بکیت.\n'''شما''' مسئول یه دل بیین ده یه نیت که هوم پیوندیا هنی هان د هموچه که قراره روئن.\n\nد ویر داشوئیت که ار د دما یه گل بلگه د داسون تازه با بلگه\nجا وه جا '''نبوئه'''،\nمر یه آخری ویرایشت آلشتکاری مسیر با و د ویرگارچه ویرایشتی نبوئه.\nوه یئنی که ار اشتوا کردیته می تونیت بلگه نه د هموچه که جا وه جا بیه ورگردونیت و یه که نمی تونیت ری بلگه یا ایسنی بنیسیت. \n\n'''هشدار!'''\nجاوه جا کاری بلگه د نوم تازه شایت یه گل آلشتکاری پایه یی و ناحاستنی سی بلگه یا حاستنی با؛\nلطف بکیت یه دل بوئیت که دما د جا وه جا کاری بلگه، عاقوت ای کار نه دونیت.", "movepagetalktext": "بلگه چک چنه مربوطه، ار با، وه حال و بار خودانجوم واگرد گوتار اصلی جا وه جا کاری بوئهمر یه که:\n* شما د حال و بار جا وه جاکاری بلگه د ای نوم جا وه یه گل نوم جا هنی بوئیت.\n* یه گل بلگه چک چنه حال نبیه نه وا ای نوم با، یا \n* جعوه هاری نه نشودار نکردیته.\n\nد ای حال و باریا، واس بلگه نه دسی جا وه جاکاری بکیت یا مینونه یا دو بلگه نه وا ویرایشت یکی بکیت.", @@ -2087,7 +2087,7 @@ "cant-move-to-category-page": "شما صلا ینه که یه بلگه نه بوریت وه بلگه دسه ناریت.", "newtitle": "سی سرون هنی:", "move-watch": "دیئن بلگه سرچشمه و بلگه حاستنی", - "movepagebtn": "بلگه جا وه جا کو", + "movepagebtn": "بٱلگٱ جا ڤ جا کو", "pagemovedsub": "د خوئی جا وه جا بیه", "movepage-moved": "\"$1\" جا وه جا بیه سی \"$2\"", "movepage-moved-redirect": "یه گل واگردونی دروس بیه.", diff --git a/languages/i18n/lt.json b/languages/i18n/lt.json index d404d89dab..2e5a6baaab 100644 --- a/languages/i18n/lt.json +++ b/languages/i18n/lt.json @@ -2768,7 +2768,7 @@ "pageinfo-authors": "Skirtingų autorių skaičius", "pageinfo-recent-edits": "Paskutinųjų keitimų skaičius (per $1 laikotarpį)", "pageinfo-recent-authors": "Pastarųjų skirtingų redaguotojų skaičius", - "pageinfo-magic-words": "Magiškas(-i) {{PLURAL:$1|žodis|žodžiai}} ($1)", + "pageinfo-magic-words": "Magiški {{PLURAL:$1|žodis|žodžiai}} ($1)", "pageinfo-hidden-categories": "{{PLURAL:$1|Paslėpta kategorija|Paslėptos kategorijos|Paslėptų kategorijų}} ($1)", "pageinfo-templates": "{{PLURAL:$1|Įtrauktas šablonas|Įtraukti šablonai|Įtrauktų šablonų}} ($1)", "pageinfo-transclusions": "{{PLURAL:$1|Įtrauktas puslapis|Įtraukti puslapiai|Įtrauktų puslapių}} ($1)", diff --git a/languages/i18n/luz.json b/languages/i18n/luz.json index 9b6542441e..8a3784435b 100644 --- a/languages/i18n/luz.json +++ b/languages/i18n/luz.json @@ -158,7 +158,7 @@ "searcharticle": "رۉ", "history": "ڤیرگار ھ بألگە", "history_short": "ڤیرگار", - "updatedmarker": "بروز وابی تا موقع آخرین سیل کردن مو", + "updatedmarker": "بهروز وابی تا موقع آخرین سیل کردن مو", "printableversion": "ڤیرژین سی چاپ", "permalink": "لینکل دائمی", "print": "چاپ", @@ -652,5 +652,6 @@ "logentry-newusers-create": "حسآۉ کارڤأر $1 ڤابیە {{GENDER:$2|راس ڤیدھ }}", "logentry-upload-upload": "$1 {{GENDER:$2|بلم گیر کردھ ۉابی}} $3", "searchsuggest-search": "جۉستأن", + "specialmute": "بی‌صدا", "userlogout-continue": "ایخیت برِیِتو وَدَر" } diff --git a/languages/i18n/mai.json b/languages/i18n/mai.json index 640d2aee36..1120141523 100644 --- a/languages/i18n/mai.json +++ b/languages/i18n/mai.json @@ -25,7 +25,8 @@ "Macofe", "राम प्रसाद जोशी", "Fitoschido", - "Haribanshi" + "Haribanshi", + "Shirayuki" ] }, "tog-underline": "लिङ्कके रेखाङ्कित करी:", @@ -2716,7 +2717,7 @@ "metadata-expand": "बढ़ाओल विवरण देखाउ।", "metadata-collapse": "विस्तृत विवरण नुकाउ", "metadata-fields": "चित्र प्रदत्तांश क्षेत्र सभ जे ई सन्देशमे सङ्कलित अछि चित्र पन्ना प्रदर्शनमे लेल जाएत जखन प्रदत्तांश सारणी क्षतिग्रस्त हएत। \nआन सभ पूर्वनिधारित रूपेँ नुका जाएत।\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude", - "metadata-langitem": "'''$2:''' $1", + "metadata-langitem": "$2: $1", "metadata-langitem-default": "$1", "namespacesall": "सभटा", "monthsall": "सभ", diff --git a/languages/i18n/mk.json b/languages/i18n/mk.json index 82c78a0b93..eb80f11d72 100644 --- a/languages/i18n/mk.json +++ b/languages/i18n/mk.json @@ -188,7 +188,7 @@ "history": "историја", "history_short": "Историја", "history_small": "историја", - "updatedmarker": "подновено од мојата последна посета", + "updatedmarker": "подновено од вашата последна посета", "printableversion": "Верзија за печатење", "permalink": "Постојана врска", "print": "Печати", @@ -667,7 +667,7 @@ "systemblockedtext": "Вашето корисничко име или IP-адреса е автоматски блокирано од МедијаВики.\nНаведената причина гласи:\n\n:$2\n\n* Почеток на блокот: $8\n* Истек на блокот: $6\n* Блокот е наменет за: $7\n\nВашата тековна IP-адреса гласи $3.\nПрепишете ги сите горенаведени поединости доколку сакате да се распрашате кај надлежните во врска со блокот.", "blockednoreason": "не е наведена причина", "blockedtext-composite": "Вашето корисничко име или IP-адреса е блокирано.\n\nНаведената причина гласи:\n\n:$2.\n\n* Почеток на блокот: $8\n* Истек на најдолгиот блок: $6\n\nВашата тековна IP-адреса гласи $3.\nПрепишете ги сите горенаведени поединости доколку сакате да се распрашате кај надлежните во врска со блокот.", - "blockedtext-composite-reason": "Вашата сметка или IP-адреса има неколку блокови", + "blockedtext-composite-reason": "Вашата сметка и/или IP-адреса има неколку блокови", "whitelistedittext": "Мора да сте $1 за да уредувате страници.", "confirmedittext": "Морате да ја потврдите вашата е-поштенска адреса пред да уредувате страници.\nПоставете ја и валидирајте ја вашата е-поштенска адреса преку вашите [[Special:Preferences|нагодувања]].", "nosuchsectiontitle": "Не можам да го пронајдам заглавието", @@ -679,7 +679,7 @@ "accmailtext": "На $2 е спратена е случајно создадена лозинка за [[User talk:$1|$1]] е испратена. Истата може да се смени на страницата ''[[Special:ChangePassword|Менување на лозинка]]'' откако ќе се најавите.", "newarticle": "(нова)", "newarticletext": "Дојдовте на врска до страница која сѐ уште не постои.\nЗа да ја создадете страницата, напишете текст во полето подолу ([$1 помош]). Ако сте овде по грешка, само систнете на копчето '''назад''' во вашиот прелистувач.", - "anontalkpagetext": "----\nОва е разговорна страница со анонимен корисник кој сè уште не регистрирал корисничка сметка или не ја користи.\nЗатоа мораме да ја користиме неговата бројчена IP-адреса за да го препознаеме.\nЕдна ваква IP-адреса може да ја делат повеќе корисници.\nАко сте анонимен корисник и сметате дека кон вас се упатени нерелевантни коментари, тогаш [[Special:CreateAccount|создајте корисничка сметка]] или [[Special:UserLogin|најавете се]] за да избегнете поистоветување со други анонимни корисници во иднина.''", + "anontalkpagetext": "----\nОва е разговорна страница со анонимен корисник кој сè уште не регистрирал корисничка сметка или не ја користи.\nЗатоа мораме да ја користиме неговата бројчена IP-адреса за да го препознаеме.\nЕдна ваква IP-адреса може да ја делат повеќе корисници.\nАко сте анонимен корисник и сметате дека кон вас се упатени нерелевантни коментари, тогаш [[Special:CreateAccount|создајте корисничка сметка]] или [[Special:UserLogin|најавете се]] за да избегнете поистоветување со други анонимни корисници во иднина.", "noarticletext": "Таква страница сè уште не постои.\nМожете да проверите [[Special:Search/{{PAGENAME}}|дали насловот се споменува]] во други статии,\nда ги [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пребарате дневниците],\nили да [{{fullurl:{{FULLPAGENAME}}|action=edit}} ја создадете].", "noarticletext-nopermission": "Таква страница сè уште не постои.\nМожете да проверите [[Special:Search/{{PAGENAME}}|дали насловот се споменува]] во други статии или пак да [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пребарате поврзаните дневници], но немате дозвола да ја создадете страницата.", "missing-revision": "Не ја пронајдов преработката бр. $1 на страницата со наслов „{{FULLPAGENAME}}“.\n\nОва обично се должи на застарена врска за разлики што води кон избришана страница.\nПовеќе подробности ќе најдете во [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} дневникот на бришења].", @@ -3911,6 +3911,16 @@ "restrictionsfield-help": "Една IP-адреса или CIDR-опсег по ред. За да овозможите сè, користете
0.0.0.0/0
::/0", "edit-error-short": "Грешка: $1", "edit-error-long": "Грешки:\n\n$1", + "specialmute": "Искл. известувања", + "specialmute-success": "Промените се успешно направени. Погледајте ги сите исклучени корисници на [[Special:Preferences]].", + "specialmute-submit": "Потврди", + "specialmute-label-mute-email": "Исклучи е-пошта од корисников", + "specialmute-header": "Изберете поставки за известувања од {{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "Не можев да го најдам корисничкото име.", + "specialmute-error-email-blacklist-disabled": "Исклучувањето на е-пошта од корисници не е овозможено.", + "specialmute-error-email-preferences": "Ќе мора да ја потврдите вашата е-пошта пред да исклучите известувања од други. Тоа се прави на страницата [[Special:Preferences]].", + "specialmute-email-footer": "[$1 Раководење со поставки за е-пошта од {{BIDI:$2}}.]", + "specialmute-login-required": "Најавете се за да ги направите промените.", "revid": "преработка $1", "pageid": "назнака на страницата $1", "interfaceadmin-info": "$1\n\nДозволите за уредување на CSS/JS/JSON податотеки низ цело вики неодамна се одвоени од правото editinterface. Ако не разбирате зошто ја добивате оваа грешка, погл. [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/ml.json b/languages/i18n/ml.json index ac5d598419..565a91d21e 100644 --- a/languages/i18n/ml.json +++ b/languages/i18n/ml.json @@ -36,7 +36,7 @@ ] }, "tog-underline": "കണ്ണികൾക്ക് അടിവരയിടുക:", - "tog-hideminor": "പുതിയ മാറ്റങ്ങളുടെ പട്ടികയിൽ ചെറിയ തിരുത്തുകൾ മറയ്ക്കുക", + "tog-hideminor": "സമീപകാല മാറ്റങ്ങളുടെ പട്ടികയിൽ ചെറുതിരുത്തുകൾ മറയ്ക്കുക", "tog-hidepatrolled": "റോന്തുചുറ്റിയ തിരുത്തുകൾ പുതിയമാറ്റങ്ങളിൽ മറയ്ക്കുക", "tog-newpageshidepatrolled": "റോന്തുചുറ്റപ്പെട്ട താളുകൾ പുതിയതാളുകളുടെ പട്ടികയിൽ മറയ്ക്കുക", "tog-hidecategorization": "താളുകളുടെ വർഗ്ഗീകരണം മറയ്ക്കുക", @@ -196,7 +196,7 @@ "history": "നാൾവഴി", "history_short": "നാൾവഴി", "history_small": "നാൾവഴി", - "updatedmarker": "കഴിഞ്ഞ സന്ദർശനത്തിന് ശേഷം മാറ്റം വന്നത്", + "updatedmarker": "കഴിഞ്ഞ സന്ദർശനത്തിന് ശേഷം പുതുക്കപ്പെട്ടത്", "printableversion": "അച്ചടിരൂപം", "permalink": "സ്ഥിരംകണ്ണി", "print": "അച്ചടിയ്ക്കുക", @@ -364,7 +364,7 @@ "title-invalid-magic-tilde": "ആവശ്യപ്പെട്ട താൾ തലക്കെട്ടിൽ അസാധുവായ മാന്ത്രിക ടിൽഡേ പരമ്പര ഉൾപ്പെടുന്നു (~~~).", "title-invalid-too-long": "ഈ തലക്കെട്ടിന്റെ നീളം കൂടുതലാണു്. UTF-8 എൻകോഡിങ്ങിൽ തലക്കെട്ടുകൾക്ക് $1 {{PLURAL:$1|ബൈറ്റിലധികം|ബൈറ്റുകളിലധികം}} നീളമുണ്ടാകാൻ പാടില്ല.", "title-invalid-leading-colon": "ആവശ്യപ്പെട്ട താൾ തലക്കെട്ടിന്റെയാദ്യം അസാധുവായ അപൂർണ്ണവിരാമം ഉൾപ്പെടുന്നു.", - "perfcached": "താഴെ കൊടുത്തിരിക്കുന്ന വിവരം ശേഖരിച്ചു വെച്ചിരിക്കുന്നതാണ്, അതുകൊണ്ട് ചിലപ്പോൾ പുതിയതായിരിക്കണമെന്നില്ല. ശേഖരിച്ചുവെച്ചിരിക്കുന്നവയിൽ പരമാവധി {{PLURAL:$4|ഒരു ഫലം|$4 ഫലങ്ങൾ}} ആണ് ഉണ്ടാവുക.", + "perfcached": "താഴെ കൊടുത്തിരിക്കുന്ന വിവരം ശേഖരിച്ചു വെച്ചിരിക്കുന്നതാണ്, അതുകൊണ്ട് ചിലപ്പോൾ പുതിയതായിരിക്കണമെന്നില്ല. ശേഖരിച്ചുവെച്ചിരിക്കുന്നവയിൽ പരമാവധി {{PLURAL:$1|ഒരു ഫലം|$1 ഫലങ്ങൾ}} ആണ് ഉണ്ടാവുക.", "perfcachedts": "താഴെയുള്ള വിവരങ്ങൾ ശേഖരിച്ചുവെച്ചവയിൽ പെടുന്നു, അവസാനം പുതുക്കിയത് $1-നു ആണ്‌. ശേഖരിച്ചുവെച്ചിരിക്കുന്നവയിൽ പരമാവധി {{PLURAL:$4|ഒരു ഫലം|$4 ഫലങ്ങൾ}} ആണ് ഉണ്ടാവുക.", "querypage-no-updates": "ഈ താളിന്റെ പുതുക്കൽ തൽക്കാലം നടക്കുന്നില്ല. ഇവിടുള്ള വിവരങ്ങൾ ഏറ്റവും പുതിയതാവണമെന്നില്ല.", "viewsource": "മൂലരൂപം കാണുക", @@ -3662,5 +3662,5 @@ "passwordpolicies-policyflag-forcechange": "ലോഗിൻ മാറ്റിയിരിക്കണം", "passwordpolicies-policyflag-suggestchangeonlogin": "ലോഗിൻ മാറ്റാൻ നിർദ്ദേശിക്കുന്നു", "unprotected-js": "സുരക്ഷാകാരണങ്ങളാൽ സംരക്ഷണമില്ലാത്ത താളുകളിൽ നിന്നും ജാവാസ്ക്രിപ്റ്റ് എടുത്തുപയോഗിക്കാൻ കഴിയില്ല. ജാവാസ്ക്രിപ്റ്റ് താളുകൾ മീഡിയവിക്കി: നാമമേഖലയിലോ ഉപയോക്തൃ ഉപതാളായോ മാത്രം സൃഷ്ടിക്കുക", - "userlogout-continue": "താങ്കൾ പുറത്ത് കടക്കാൻ ആഗ്രഹിക്കുന്നുവെങ്കിൽ [$1 ലോഗ് ഔട്ട് താളിലേക്ക് തുടരുക]." + "userlogout-continue": "പുറത്തുകടക്കണോ?" } diff --git a/languages/i18n/my.json b/languages/i18n/my.json index f5cb6fface..094937d0ed 100644 --- a/languages/i18n/my.json +++ b/languages/i18n/my.json @@ -189,7 +189,7 @@ "history": "စာမျက်နှာ ရာဇဝင်", "history_short": "ရာဇဝင်", "history_small": "ရာဇဝင်", - "updatedmarker": "နောက်ဆုံးကြည့်ပြီးသည့်နောက်ပိုင်း တည်းဖြတ်ထားသည်။", + "updatedmarker": "နောက်ဆုံးကြည့်ပြီးသည့်နောက်ပိုင်း တည်းဖြတ်ထားသည်", "printableversion": "ပရင့်ထုတ်နိုင်သော ဗားရှင်း", "permalink": "ပုံ​သေ​လိပ်​စာ​", "print": "ပရင့်ထုတ်", @@ -317,11 +317,15 @@ "filerenameerror": "ဖိုင် \"$1\" ကို \"$2\" သို့ အမည်ပြောင်းမရပါ။", "filedeleteerror": "ဖိုင် \"$1\" ကို ဖျက်မရပါ။", "directorycreateerror": "လမ်းညွှန် \"$1\" ကို ဖန်တီးမရနိုင်ပါ။", + "directoryreadonlyerror": "လမ်းညွှန် \"$1\" သည် ဖတ်ရှုရန်သာဖြစ်သည်။", + "directorynotreadableerror": "လမ်းညွှန် \"$1\" သည် ဖတ်ရှု၍မရနိုင်ပါ။", "filenotfound": "ဖိုင် \"$1\" ကို ရှာမတွေ့ပါ။", + "unexpected": "မမျော်လင့်ထားသောတန်ဖိုး: \"$1\"=\"$2\"", "formerror": "အမှား - ဖောင်သွင်းနိုင်ခြင်းမရှိပါ", "badarticleerror": "ဤလုပ်ဆောင်မှုအား ဤစာမျက်နှာတွင် လုပ်ဆောင်၍ မရနိုင်ပါ။", "cannotdelete": "\"$1\" စာမျက်နှာ သို့မဟုတ် ဖိုင်ကို ဖျက်၍ မရပါ။\nတစ်စုံတစ်ဦးမှ ဖျက်နှင့်ပြီး ဖြစ်နိုင်ပါသည်။", "cannotdelete-title": "\"$1\" စာမျက်နှာကို ဖျက်၍ မရပါ", + "delete-scheduled": "စာမျက်နှာ \"$1\" ကို ဖျက်ပစ်ရန် ရက်သတ်မှတ်ထားသည်။ ကျေးဇူးပြု၍ စိတ်ရှည်ပါ။", "delete-hook-aborted": "ရှင်းလင်းပြချက် မပေးထားပါ။", "badtitle": "ညံ့ဖျင်းသော ခေါင်းစဉ်", "badtitletext": "တောင်းဆိုထားသော စာမျက်နှာ ခေါင်းစဉ်သည် တရားမဝင်ပါ (သို့) ဗလာဖြစ်နေသည် (သို့) အခြားဘာသာများ(inter-language or inter-wiki title)သို့ မှားယွင်းစွာ လင့်ချိတ်ထားသည်။", @@ -607,6 +611,7 @@ "explainconflict": "သင် စတင်တည်းဖြတ်ကတည်းက တစ်စုံတစ်ယောက်မှ ဤစာမျက်နှာကို ပြောင်းလဲခဲ့သည်။ အပေါ်ပိုင်းဧရိယာတွင် လက်ရှိတည်ရှိနေသော စာမျက်နှာစာသား ပါဝင်သည်။ သင်၏ပြောင်းလဲချက်များကို အောက်ပိုင်းစာသားဧရိယာတွင် ပြသပေးထားသည်။ သင်၏ပြောင်းလဲချက်များကို ရှိနှင့်ပြီးသားစာသားတွင် ပေါင်းစပ်ရမည်ဖြစ်ပါသည်။ \"$1\" ကို သင်နှိပ်လိုက်ပါက အပေါ်ပိုင်းဧရိယာရှိ စာသားသာလျင် သိမ်းဆည်းသွားမည်ဖြစ်ပါသည်။", "yourtext": "သင့်စာသား", "storedversion": "သိမ်းဆည်းထားသောမူ", + "editingold": "သတိပေးချက်: သင်သည် ဤစာမျက်နှာ၏ ခေတ်နောက်ကျသောမူကို တည်းဖြတ်နေခြင်းဖြစ်သည်။\nသိမ်းဆည်းလိုက်ပါက ယခင်မူဟောင်းမှ မည်သည့်ပြောင်းလဲချက်များမဆို ပျောက်ဆုံးသွားမည်ဖြစ်သည်။", "yourdiff": "ကွဲပြားချက်များ", "copyrightwarning": "{{SITENAME}} တွင် ရေးသားမှုအားလုံးကို $2 အောက်တွင် ဖြန့်ဝေရန် ဆုံးဖြတ်ပြီး ဖြစ်သည်ကို ကျေးဇူးပြု၍ သတိပြုပါ။။ (အသေးစိတ်ကို $1 တွင်ကြည့်ပါ။)\nအကယ်၍ သင့်ရေးသားချက်များကို အညှာအတာမရှိ တည်းဖြတ်ခံရခြင်း၊ စိတ်တိုင်းကျ ဖြန့်ဝေခံရခြင်းတို့ကို အလိုမရှိပါက ဤနေရာတွင် မတင်ပါနှင့်။
\nသင်သည် ဤဆောင်းပါးကို သင်ကိုယ်တိုင်ရေးသားခြင်း၊ သို့မဟုတ် အများပြည်သူဆိုင်ရာဒိုမိန်းများ၊ ယင်းကဲ့သို့ လွတ်လပ်သည့် ရင်းမြစ်မှ ကူးယူထားခြင်း ဖြစ်ကြောင်းလည်း ဝန်ခံ ကတိပြုပါသည်။\nမူပိုင်ခွင့်ရှိသော စာ၊ပုံများကို ခွင့်ပြုချက်မရှိဘဲ မတင်ပါနှင့်။", "copyrightwarning2": "{{SITENAME}} တွင် ရေးသားမှုအားလုံးသည် အခြားပုံပိုးသူများ၏ တည်းဖြတ်၊ ပြောင်းလဲ၊ ဖယ်ရှားခံရနိုင်သည်ကို သတိပြုပါ။\nအကယ်၍ သင့်ရေးသားချက်များကို အညှာအတာမရှိ တည်းဖြတ်ခံရခြင်း၊ စိတ်တိုင်းကျ ဖြန့်ဝေခံရခြင်းတို့ကို အလိုမရှိပါက ဤနေရာတွင် မတင်ပါနှင့်။
\nသင်သည် ဤဆောင်းပါးကို သင်ကိုယ်တိုင်ရေးသားခြင်း၊ သို့မဟုတ် အများပြည်သူဆိုင်ရာဒိုမိန်းများ၊ ယင်းကဲ့သို့ လွတ်လပ်သည့် ရင်းမြစ်မှ ကူးယူထားခြင်း ဖြစ်ကြောင်းလည်း ဝန်ခံ ကတိပြုပါသည် (အသေးစိတ်ကို $1 တွင်ကြည့်ပါ)။\nမူပိုင်ခွင့်ရှိသော စာ၊ပုံများကို ခွင့်ပြုချက်မရှိဘဲ မတင်ပါနှင့်။", @@ -1011,7 +1016,7 @@ "grant-blockusers": "အသုံးပြုသူများအား ပိတ်ပင်ခြင်းနှင့် ပိတ်ပင်မှု ဖယ်ရှားခြင်း", "grant-createaccount": "အကောင့်များ ဖန်တီးရန်", "grant-createeditmovepage": "စာမျက်နှာများကို ဖန်တီး၊ တည်းဖြတ်၊ ရွေ့ပြောင်းရန်", - "grant-editmyoptions": "သင်၏အသုံးပြုသူ အပြင်အဆင်များကို ပြင်ရန်", + "grant-editmyoptions": "သင်၏အသုံးပြုသူ ရွေးချယ်စရာများနှင့် JSON အပြင်အဆင်ကို ပြင်ရန်", "grant-editmywatchlist": "သင့် စောင့်ကြည့်စာရင်းကို တည်းဖြတ်ရန်", "grant-editpage": "ရှိပြီးသား စာမျက်နှာများကို တည်းဖြတ်ရန်", "grant-editprotected": "ကာကွယ်ထားသော စာမျက်နှာများကို တည်းဖြတ်ရန်", @@ -1714,7 +1719,7 @@ "delete-confirm": "\"$1\"ကို ဖျက်ပါ", "delete-legend": "ဖျက်", "historywarning": "သတိပေးချက်။ သင်ဖျက်ပစ်တော့မည့် စာမျက်နှာတွင် {{PLURAL:$1|တည်းဖြတ်မူ|တည်းဖြတ်မူများ}} $1 ခု ရှိနေသည်-", - "historyaction-submit": "ပြသရန်", + "historyaction-submit": "ပြန်လည်ပြင်ဆင်မှုများကို ပြသရန်", "confirmdeletetext": "သင်သည် စာမျက်နှာတစ်ခုကို ယင်း၏ မှတ်တမ်းများနှင့်တကွ ဖျက်ပစ်တော့မည် ဖြစ်သည်။\nဤသို့ ဖျက်ပစ်ရန် သင် အမှန်တကယ် ရည်ရွယ်လျက် နောက်ဆက်တွဲ အကျိုးဆက်များကို သိရှိနားလည်ပြီး [[{{MediaWiki:Policy-url}}|မူဝါဒ]] အတိုင်းလုပ်ဆောင်နေခြင်းဖြစ်ကြောင်းကို အတည်ပြုပေးပါ။", "actioncomplete": "လုပ်ဆောင်ချက် ပြီးပြီ", "actionfailed": "ဆောင်ရွက်မှုမအောင်မြင်ပါ", @@ -1966,7 +1971,7 @@ "blocklist-editing-page": "စာမျက်နှာများ", "blocklist-editing-ns": "အမည်ညွှန်းများ", "ipblocklist-empty": "ပိတ်ပင်ထားမှုစာရင်းသည် ဗလာဖြစ်နေသည်။", - "ipblocklist-no-results": "တောင်းဆိုလိုက်သော အိုင်ပီလိပ်စာ သို့မဟုတ် အသုံးပြုသူအမည်ကို မပိတ်ပင်ထားပါ။", + "ipblocklist-no-results": "တောင်းဆိုလိုက်သော အိုင်ပီလိပ်စာ သို့မဟုတ် အသုံးပြုသူအမည်တွင် ကိုက်ညီသောပိတ်ပင်ထားဆီးမှုကို မတွေ့ရှိပါ။", "blocklink": "ပိတ်ပင်", "unblocklink": "မပိတ်ပင်တော့ရန်", "change-blocklink": "စာကြောင်းအမည် ပြောင်းရန်", @@ -2045,12 +2050,14 @@ "protectedpagemovewarning": "သတိပေးချက်။ ဤစာမျက်နှာအား စီမံခန့်ခွဲသူအဆင့်ရှိသူများသာ ရွှေ့ပြောင်းနိုင်ရန် ကာကွယ်ထားသည်။\nနောက်ဆုံးမှတ်တမ်းအား ကိုးကားနိုင်ရန် အောက်တွင် ဖော်ပြထားသည်။", "semiprotectedpagemovewarning": "မှတ်ချက်။ ဤစာမျက်နှာအား အလိုအလျောက် အတည်ပြုထားသော အသုံးပြုသူအဆင့်ရှိသူများသာ ရွှေ့ပြောင်းနိုင်ရန် ကာကွယ်ထားသည်။\nနောက်ဆုံးမှတ်တမ်းအား ကိုးကားနိုင်ရန် အောက်တွင် ဖော်ပြထားသည်။", "export": "စာမျက်နှာများကို တင်ပို့ရန်", + "exportall": "စာမျက်နှာများအားလုံးကို တင်ပို့ရန်", "export-submit": "တင်ပို့ရန်", "export-addcattext": "ကဏ္ဍမှ စာမျက်နှာများကို ပေါင်းထည့်ရန် -", "export-addcat": "ပေါင်းထည့်ရန်", "export-addnstext": "အမည်ညွှန်းမှ စာမျက်နှာများကို ပေါင်းထည့်ရန်", "export-addns": "ပေါင်းထည့်ရန်", "export-download": "ဖိုင်အဖြစ် သိမ်းရန်", + "export-templates": "တမ်းပလိတ်များ ပါဝင်မည်", "allmessages": "စနစ်၏ သတင်းများ", "allmessagesname": "အမည်", "allmessagesdefault": "ပုံမှန် အသိပေးချက် စာသား", @@ -2070,6 +2077,7 @@ "importinterwiki": "အခြားဝီကီမှ တင်သွင်းရန်", "import-interwiki-sourcewiki": "ရင်းမြစ် ဝီကီ:", "import-interwiki-sourcepage": "ရင်းမြစ် စာမျက်နှာ:", + "import-interwiki-templates": "တမ်းပလိတ်များအားလုံး ပါဝင်မည်", "import-interwiki-submit": "တင်သွင်းရန်", "import-upload-filename": "ဖိုင်အမည် -", "import-comment": "မှတ်ချက် -", @@ -2081,6 +2089,8 @@ "import-token-mismatch": "session data ဆုံးရှုံးမှု ဖြစ်ပါသည်။\n\nသင်သည် အကောင့်မှ ထွက်လိုက်တာဖြစ်နိုင်သည်။ အကောင့်ထဲသို့ ဝင်ထားနေခြင်းဖြစ်အောင် အတည်ပြုပြီး ထပ်မံကြိုးစားကြည့်ပါ။\nအကယ်၍ အလုပ်မဖြစ်သေးပါက [[Special:UserLogout|အကောင့်မှထွက်]]ပြီးနောက် ထပ်မံလော့ဂ်အင်ဝင်ရောက်ပါ။ သင်၏ဘရောက်ဆာက ဤဝဘ်ဆိုဒ်မှ cookie ကို ခွင့်ပြုထားကြောင့် စစ်ဆေးပေးပါ။", "importlogpage": "ထည့်သွင်းသည့် မှတ်တမ်း", "importlogpagetext": "အခြားဝီကီများမှ အက်ဒမင်ဆိုင်ရာ တည်းဖြတ်မှုရာဇဝင်နှင့် စာမျက်နှာ တင်သွင်းမှုများ", + "javascripttest-pagetext-unknownaction": "အမည်မသိ လုပ်ဆောင်ချက် \"$1\"။", + "javascripttest-qunit-intro": "[$1 စမ်းသပ်မှုစာရွက်စာတမ်း] ကို mediawiki.org ပေါ်တွင်ကြည့်ပါ။", "tooltip-pt-userpage": "{{GENDER:|သင်၏ အသုံးပြုသူ}} စာမျက်နှာ", "tooltip-pt-mytalk": "{{GENDER:|သင်၏}} ဆွေးနွေးချက်စာမျက်နှာ", "tooltip-pt-anontalk": "ဤအိုင်ပီလိပ်စာမှ တည်းဖြတ်မှုများအကြောင်း ဆွေးနွေးချက်", @@ -2485,6 +2495,7 @@ "logentry-block-block": "$1 က {{GENDER:$4|$3}} ကို သက်တမ်းကုန်လွန်ချိန် $5 $6 ဖြင့် {{GENDER:$2|ပိတ်ပင်ခဲ့သည်}}", "logentry-block-unblock": "$1 က {{GENDER:$4|$3}} ကို {{GENDER:$2|ပိတ်ပင်မှုမှ ပြန်ဖြေခဲ့သည်}}", "logentry-block-reblock": "$1 က {{GENDER:$4|$3}} အတွက် ပိတ်ပင်မှုအပြင်အဆင်များကို သက်တမ်းကုန်လွန်ချိန် $5 $6 ဖြင့် {{GENDER:$2|ပြောင်းလဲခဲ့သည်}}", + "logentry-partialblock-block-page": "{{PLURAL:$1|စာမျက်နှာ|စာမျက်နှာများ}} $2", "logentry-suppress-block": "{{GENDER:$4|$3}} အား $5 ကြာအောင် $1 က {{GENDER:$2|ပိတ်ပင်ခဲ့သည်}} $6", "logentry-suppress-reblock": "$1 က {{GENDER:$4|$3}} အတွက် ပိတ်ပင်မှုအပြင်အဆင်များကို သက်တမ်းကုန်လွန်ချိန် $5 $6 ဖြင့် {{GENDER:$2|ပြောင်းလဲခဲ့သည်}}", "logentry-move-move": "$3 စာမျက်နှာကို $4 သို့ $1က {{GENDER:$2|ရွှေ့ခဲ့သည်}}", @@ -2514,6 +2525,7 @@ "feedback-cancel": "မလုပ်တော့ပါ", "feedback-close": "ပြီးပြီ", "feedback-dialog-title": "အကြံပေး ပေါင်းထည့်ရန်", + "feedback-error2": "အမှား- တည်းဖြတ်မှု မအောင်မြင်ပါ", "feedback-message": "မက်ဆေ့:", "feedback-subject": "အကြောင်းအရာ:", "feedback-submit": "ထည့်သွင်းရန်", @@ -2576,6 +2588,7 @@ "special-characters-group-telugu": "တီလူဂု", "special-characters-group-sinhala": "ရှင်ဟာလာ", "special-characters-group-gujarati": "ဂူဂျာရတီ", + "special-characters-group-devanagari": "ဒီဗနာဂရီ", "special-characters-group-thai": "ထိုင်း", "special-characters-group-lao": "လာအို", "special-characters-group-khmer": "ခမာ", @@ -2639,6 +2652,7 @@ "specialpage-securitylevel-not-allowed-title": "ခွင့်မပြုပါ", "cannotauth-not-allowed-title": "ခွင့်ပြုချက် ငြင်းပယ်လိုက်သည်", "cannotauth-not-allowed": "သင်သည် ဤစာမျက်နှာကို အသုံးပြုခွင့်မရှိပါ", + "credentialsform-account": "အကောင့်နာမည်-", "userjsispublic": "ကျေးဇူးပြု၍ မှတ်သားပါ- JavaScript စာမျက်နှာခွဲများတွင် အခြားအသုံးပြုသူများ ကြည့်ရှုနိုင်သော လျို့ဝှက်အပ်သည့်အချက်အလက် မပါဝင်သင့်ပါ။", "edit-error-short": "အမှား - $1", "edit-error-long": "အမှားများ:\n\n$1", diff --git a/languages/i18n/nan.json b/languages/i18n/nan.json index dead74a430..c60f4b4f4b 100644 --- a/languages/i18n/nan.json +++ b/languages/i18n/nan.json @@ -12,7 +12,8 @@ "Liuxinyu970226", "Yoxem", "Matěj Suchánek", - "Reke" + "Reke", + "LNDDYL" ] }, "tog-underline": "Liân-kiat oē té-sûn:", @@ -27,7 +28,7 @@ "tog-editsectiononrightclick": "Chiàⁿ ji̍h toāⁿ-lo̍h phiau-tê to̍h ē-tàng pian-chi̍p toāⁿ-lo̍h", "tog-watchcreations": "Kā goá khui ê ia̍h kah chiūⁿ-chái ê tóng-àn ka-ji̍p kàm-sī-toaⁿ lāi-té", "tog-watchdefault": "Kā goá pian-chi̍p kòe ê ia̍h kah tóng-àn ka-ji̍p kàm-sī-toaⁿ lāi-té", - "tog-watchmoves": "Kā goá soá ê ia̍h kah tóng-àn ka-ji̍p kàm-sī-toaⁿ", + "tog-watchmoves": "Kā góa sóa ê ia̍h kah tóng-àn ka-ji̍p kàm-sī-toaⁿ", "tog-watchdeletion": "Kā goá thâi tiāu ê ia̍h kah tóng-àn ka-ji̍p kàm-sī-toaⁿ", "tog-watchuploads": "Chiong góa ap-ló͘ ê tóng-àn ka-ji̍p kam-sī-toaⁿ", "tog-watchrollback": "Chiong góa í-keng ká--tńg-khì ê ia̍h-bīn ka-ji̍p góa-ê kam-sī-toaⁿ", @@ -924,6 +925,7 @@ "prot_1movedto2": "[[$1]] sóa khì tī [[$2]]", "protect-legend": "Khak-tēng beh pó-hō·", "protectcomment": "Lí-iû:", + "protect-default": "Ún-chún só͘-ū iōng-chiá", "protect-level-autoconfirmed": "Ta ín-chún chū-tōng khak-jīn iōng-chiá", "protect-level-sysop": "Ta ín-chún koán-lí jîn-oân", "protect-expiring": "chì $1 (UTC) kòe-kî", @@ -997,10 +999,10 @@ "block-log-flags-anononly": "Kaⁿ-taⁿ bô-miâ iōng-chiá", "block-log-flags-nocreate": "Khui kháu-chō thêng-iōng ah", "locknoconfirm": "Lí bô kau \"khak-tēng\" ê keh-á.", - "move-page": "徙$1", + "move-page": "Sóa $1", "move-page-legend": "Sóa ia̍h", "movepagetext": "Ē-kha chit ê form> iōng lâi kái 1 ê ia̍h ê piau-tê (miâ-chheng); só·-ū siong-koan ê le̍k-sú ē tòe leh sóa khì sin piau-tê.\nKū piau-tê ē chiâⁿ-chò 1 ia̍h choán khì sin piau-tê ê choán-ia̍h.\nLiân khì kū piau-tê ê liân-kiat (link) bē khì tāng--tio̍h; ē-kì-tit chhiau-chhōe siang-thâu (double) ê a̍h-sī kò·-chiòng ê choán-ia̍h.\nLí ū chek-jīm khak-tēng liân-kiat kè-sio̍k liân tio̍h ūi.\n\nSin piau-tê nā í-keng tī leh (bô phian-chi̍p koè ê khang ia̍h, choán-ia̍h bô chún-sǹg), tō bô-hoat-tō· soá khì hia.\nChe piaú-sī nā ū têng-tâⁿ, ē-sái kā sin ia̍h soà tńg-khì goân-lâi ê kū ia̍h.\n\n'''SÈ-JĪ!'''\nTùi chē lâng tha̍k ê ia̍h lâi kóng, soá-ūi sī toā tiâu tāi-chì.\nLiâu--lo̍h-khì chìn-chêng, chhiáⁿ seng khak-tēng lí ū liáu-kái chiah-ê hiō-kó.", - "movepagetalktext": "Siong-koan ê thó-lūn-ia̍h (chún ū) oân-nâ ē chū-tōng tòe leh sóa-ūi. Í-hā ê chêng-hêng '''bô chún-sǹg''': *Beh kā chit ia̍h tùi 1 ê miâ-khong-kan (namespace) soá khì lēng-gōa 1 ê miâ-khong-kan, *Sin piau-tê í-keng ū iōng--kòe ê thó-lūn-ia̍h, he̍k-chiá *Ē-kha ê sió-keh-á bô phah-kau. Í-siōng ê chêng-hêng nā-chún tī leh, lí chí-hó iōng jîn-kang ê hong-sek sóa ia̍h a̍h-sī kā ha̍p-pèng (nā ū su-iàu).", + "movepagetalktext": "Siong-koan ê thó-lūn-ia̍h (chún ū) oân-nâ ē chū-tōng tòe leh sóa-ūi. Í-hā ê chêng-hêng '''bô chún-sǹg''': *Beh kā chit ia̍h tùi 1 ê miâ-khong-kan (namespace) sóa khì lēng-gōa 1 ê miâ-khong-kan, *Sin piau-tê í-keng ū iōng--kòe ê thó-lūn-ia̍h, he̍k-chiá *Ē-kha ê sió-keh-á bô phah-kau. Í-siōng ê chêng-hêng nā-chún tī leh, lí chí-hó iōng jîn-kang ê hong-sek sóa ia̍h a̍h-sī kā ha̍p-pèng (nā ū su-iàu).", "movenologintext": "Lí it-tēng ài sī chù-chheh ê iōng-chiá jī-chhiáⁿ ū [[Special:UserLogin|teng-ji̍p]] chiah ē-tàng sóa ia̍h.", "newtitle": "Khì sin piau-tê:", "move-watch": "Kàm-sī chit ia̍h", @@ -1011,12 +1013,12 @@ "movetalk": "Sūn-sòa sóa thó-lūn-ia̍h", "movepage-page-moved": "$1 í-keng sóa khì tī $2.", "movepage-page-unmoved": "$1 chit ia̍h hô hoat-tō͘ sóa khì $2.", - "movelogpagetext": "Ē-kha lia̍t-chhut hông soá-ūi ê ia̍h.", + "movelogpagetext": "Ē-kha lia̍t-chhut hông sóa-ūi ê ia̍h.", "movereason": "Lí-iû:", "revertmove": "hôe-tńg", "delete_and_move_reason": "Thâi-ia̍h hō͘ \"[[$1]]\" thang sóa-ia̍h kòe--lâi", "selfmove": "Goân piau-tê kap sin piau-tê sio-siâng; bô hoat-tō· sóa.", - "protectedpagemovewarning": "'''KÉNG-KÒ: Pún ia̍h só tiâu leh. Kan-taⁿ ū hêng-chèng te̍k-koân ê iōng-chiá (sysop) ē-sái soá tín-tāng.'''\nĒ-kha ū choè-kīn ê kì-lio̍k thang chham-khó:", + "protectedpagemovewarning": "'''KÉNG-KÒ: Pún ia̍h só tiâu leh. Kan-taⁿ ū hêng-chèng te̍k-koân ê iōng-chiá (sysop) ē-sái sóa tín-tāng.'''\nĒ-kha ū choè-kīn ê kì-lio̍k thang chham-khó:", "export": "Su-chhut ia̍h", "exportcuronly": "Hān hiān-chhú-sî ê siu-téng-pún, mài pau-koat kui-ê le̍k-sú", "allmessages": "Hē-thóng sìn-sit", @@ -1043,7 +1045,7 @@ "tooltip-ca-delete": "Thâi chit ia̍h", "tooltip-ca-move": "Sóa chit ia̍h", "tooltip-ca-watch": "共這頁加入去你的監視單", - "tooltip-ca-unwatch": "Lí ê kàm-sī-toaⁿ soá tiàu chit ia̍h.", + "tooltip-ca-unwatch": "Lí ê kàm-sī-toaⁿ sóa tiàu chit ia̍h.", "tooltip-search": "Chhoé {{SITENAME}}", "tooltip-search-go": "Nā ū kāng-miâ--ê, tō khì hit-ia̍h.", "tooltip-search-fulltext": "Chhoé ū chia-ê jī ê ia̍h", @@ -1134,7 +1136,7 @@ "autosumm-changed-redirect-target": "Choán-ia̍h bo̍k-phiau kái [[$1]] kòe [[$2]] oân-sêng", "autosumm-new": "$1 ê ia̍h í-keng kiàn-li̍p", "watchlistedit-normal-submit": "Mài kàm-sī", - "watchlistedit-normal-done": "Í-keng uì lí ê kám-sī-toaⁿ soá {{PLURAL:$1|ia̍h}} cháu:", + "watchlistedit-normal-done": "Í-keng uì lí ê kám-sī-toaⁿ sóa {{PLURAL:$1|ia̍h}} cháu:", "watchlisttools-edit": "Khoàⁿ koh kái kàm-sī-toaⁿ", "watchlisttools-raw": "Kái chhiⁿ ê kàm-sī-toaⁿ", "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|thó-lūn]])", @@ -1151,6 +1153,7 @@ "logentry-move-move": "$1 {{GENDER:$2|sóa}} $3 chit ia̍h khì $4", "logentry-move-move_redir": "$1 iōng choán-ia̍h {{GENDER:$2|sóa}} ia̍h-bīn $3 kòe $4", "logentry-newusers-create": "已經{{GENDER:$2|開好}}用者口座 $1", + "logentry-protect-protect": "$1 {{GENDER:$2|pó-hō͘ liáu}} $3 $4", "searchsuggest-search": "Chhoē {{SITENAME}}", "expandtemplates": "Khok-chhiong pang-bô͘", "expand_templates_input": "Su-ji̍p bûn-jī:", diff --git a/languages/i18n/nl.json b/languages/i18n/nl.json index 09d06dd3aa..8d3c9be89e 100644 --- a/languages/i18n/nl.json +++ b/languages/i18n/nl.json @@ -261,7 +261,7 @@ "history": "Geschiedenis", "history_short": "Geschiedenis", "history_small": "geschiedenis", - "updatedmarker": "bewerkt sinds mijn laatste bezoek", + "updatedmarker": "bewerkt sinds uw laatste bezoek", "printableversion": "Printvriendelijke versie", "permalink": "Permanente koppeling", "print": "Afdrukken", @@ -3866,6 +3866,16 @@ "restrictionsfield-help": "Een IP-adres of CIDR bereik per lijn. Om alles toe te staan, gebruik:
0.0.0.0/0\n::/0
", "edit-error-short": "Fout: $1", "edit-error-long": "Fouten:\n\n$1", + "specialmute": "Negeren", + "specialmute-success": "Het bijwerken van uw voorkeur voor het negeren is geslaagd. Bekijk een lijst met alle genegeerde gebruikers in [[Special:Preferences|uw voorkeuren]].", + "specialmute-submit": "Bevestig", + "specialmute-label-mute-email": "Negeer e-mails van deze gebruiker", + "specialmute-header": "Selecteer uw voorkeur voor het negeren van {{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "De ingevoerde gebruikersnaam kon niet worden gevonden.", + "specialmute-error-email-blacklist-disabled": "Het negeren van e-mails verstuurd door andere gebruikers is niet ingeschakeld.", + "specialmute-error-email-preferences": "U moet uw e-mailadres bevestigen voordat u een gebruiker kunt negeren. U kunt dit doen in [[Special:Preferences|uw voorkeuren]].", + "specialmute-email-footer": "[$1 E-mail voorkeuren beheren voor {{BIDI:$2}}.]", + "specialmute-login-required": "U moet aanmelden om voorkeuren voor het negeren van gebruikers in te stellen.", "revid": "versie $1", "pageid": "Pagina-ID $1", "interfaceadmin-info": "$1\n\nRechten voor het bewerken van wikibrede CSS/JS/JSON-bestanden zijn recentelijk gescheiden van het editinterface recht. Als u niet begrijpt waarom u deze foutmelding te zien krijgt, ga dan naar [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/nqo.json b/languages/i18n/nqo.json index bbb42b481c..3dad188411 100644 --- a/languages/i18n/nqo.json +++ b/languages/i18n/nqo.json @@ -166,7 +166,7 @@ "history": "ߞߐߜߍ ߟߊ߫ ߘߐ߬ߝߐ", "history_short": "ߘߐ߬ߝߐ", "history_small": "ߕߊ߬ߡߌ߲߬ߣߍ߲", - "updatedmarker": "ߊ߬ ߟߏ߲ߘߐߦߊ ߞߊ߬ߦߌ߯ ߒ ߠߊ߫ ߞߐߟߊ߫ ߓߐߒߡߊߟߌ ߟߎ߬ ߡߊ߬", + "updatedmarker": "ߊ߬ ߟߏ߲ߘߐߦߊ ߞߊ߬ߦߌ߯ ߌ ߟߊ߫ ߞߐߟߊ߫ ߓߐߒߡߊߟߌ ߟߎ߬ ߡߊ߬", "printableversion": "ߓߐߞߏߣߊ߲߫ ߜߌ߬ߙߌ߲߬ߘߌ߬ߕߊ", "permalink": "ߛߘߌ߬ߜߋ߲߬ ߓߟߏߕߍ߰ߓߊߟߌ", "print": "ߜߌ߬ߙߌ߲߬ߘߌ߬ߟߌ", @@ -322,6 +322,12 @@ "viewsource": "ߊ߬ ߛߎ߲ ߘߐߜߍ߫", "viewsource-title": "ߣߌ߲߬ $1 ߛߎ߲ ߘߐߜߍ߫", "viewsourcetext": "ߌ ߘߌ߫ ߛߋ߫ ߞߐߜߍ ߣߌ߲߬ ߛߎ߲ ߦߋ߫ ߟߊ߫߸ ߞߵߊ߬ ߓߊߓߌ߬ߟߊ߬", + "customcssprotected": "CSS ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߊߏ߬ ߊ߬ ߘߐߞߍߣߍ߲߫ ߦߋ߫ ߡߐ߰ ߜߘߍ߫ ߟߊ߫ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߟߊ߫.", + "customjsonprotected": "JSON ߞߐߜߍ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߊߏ߬ ߊ߬ ߘߐߞߍߣߍ߲߫ ߦߋ߫ ߡߐ߰ ߜߘߍ߫ ߟߊ߫ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߟߊ߫.", + "customjsprotected": "JavaScript ߞߐߜߍ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߊߏ߬ ߊ߬ ߘߐߞߍߣߍ߲߫ ߦߋ߫ ߡߐ߰ ߜߘߍ߫ ߟߊ߫ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߟߊ߫.", + "sitecssprotected": "CSS ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߴߊ߬ ߘߌ߫ ߛߋ߫ ߓߐߒߡߊߟߌߟߊ ߟߎ߬ ߓߍ߯ ߕߙߐ߫ ߟߊ߫.", + "sitejsonprotected": "JSON ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߴߊ߬ ߘߌ߫ ߛߋ߫ ߓߐߒߡߊߟߌߟߊ ߟߎ߬ ߓߍ߯ ߕߙߐ߫ ߟߊ߫.", + "sitejsprotected": "JavaScript ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߴߌ ߦߋ߫߸ ߓߴߊ߬ ߘߌ߫ ߛߋ߫ ߘߋ߬ߦߌ߬ ߟߊ߫ ߓߐߒߡߊߟߌߟߊ ߟߎ߬ ߓߍ߯ ߡߊ߬.", "mycustomcssprotected": "ߌ ߟߊߘߌ߬ߢߍ߬ ߣߍ߲߬ ߕߍ߫ ߞߊ߬ CCS ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߫.", "mycustomjsonprotected": "ߌ ߟߊߘߌ߬ߢߍ߬ ߣߍ߲߬ ߕߍ߫ ߞߊ߬ JSON ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߫.", "mycustomjsprotected": "ߌ ߟߊߘߌ߬ߢߍ߬ ߣߍ߲߬ ߕߍ߫ ߞߊ߬ JavaScript ߞߐߜߍ ߡߊߦߟߍ߬ߡߊ߲߫.", @@ -331,7 +337,10 @@ "titleprotected": "ߞߎ߲߬ߕߐ߮ ߣߌ߲߬ ߓߘߊ߫ ߟߊߞߊ߲ߘߊ߫ ߛߌ߲ߘߟߌ ߡߊ߬ [[User:$1|$1]] ߓߟߏ߫.\nߞߎ߲߭ ߡߍ߲ ߦߴߏ߬ ߟߊ߫߸ ߏ߬ ߦߋ߫ $2.", "filereadonlyerror": "ߞߐߕߐ߮ \"$1\" ߕߍ߫ ߛߐ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫߸ ߞߵߊ߬ ߡߊߛߐ߬ߘߐ߲߬ ߞߐߕߐ߮ ߟߊߡߙߊ߬ ߦߙߐ \"$2\" ߦߋ߫ ߞߊ߬ߙߊ߲ ߘߐߙߐ߲߫ ߝߊ߬ߘߌ ߟߋ߬ ߘߐ߫.\n\nߞߊ߲ߞߋ ߟߊߓߊ߯ߙߟߊ ߡߍ߲ ߣߵߊ߬ ߛߐ߰ ߟߊ߫߸ ߏ߬ ߓߘߊ߫ ߘߊ߲߬ߕߍ߰ߟߌ ߘߏ߫ ߞߍ߫: \"$3\".", "invalidtitle": "ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߓߊߟߌ", + "invalidtitle-knownnamespace": "ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߓߊߟߌ ߕߐ߮ ߛߓߍ ߞߣߍ ߡߊ߬ \"$2\" ߊ߬ ߣߌ߫ ߛߓߍߟߌ \"$3\"", + "invalidtitle-unknownnamespace": "ߞߎ߲߬ߕߐ߮ ߓߍ߲߬ߓߊߟߌ ߞߊ߬ ߓߍ߲߬ ߕߐ߯ߛߓߍ ߞߣߍ߫ ߡߊߟߐ߲ߓߊߟߌ ߝߙߍߕߍ ߡߊ߬ $1 ߊ߬ ߣߌ߫ ߛߓߍߟߌ \"$2\"", "exception-nologin": "ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߕߍ߫", + "exception-nologin-text": "ߌ ߜߊ߲߬ߞߎ߲߫ ߖߊ߰ߣߌ߲߬߸ ߛߴߌ ߘߌ߫ ߛߋ߫ ߞߐߜߍ ߣߌ߲߬ ߡߊߛߐ߬ߘߐ߲߬ ߠߊ߫ ߥߟߊ߫ ߝߏ߲߬ߝߏ߲.", "virus-unknownscanner": "ߢߐߛߌߙߋ߲ߞߟߊ߬ ߡߊߟߐ߲ߓߊߟߌ", "logouttext": "ߌ ߜߊ߲߬ߞߎ߲߬ߓߐ߬ߣߍ߲߬ ߕߍ߫.\n\nߞߐߜߍ ߘߏ߫ ߟߎ߫ ߕߘߍ߬ ߘߌ߫ ߞߍ߫ ߓߊ߯ߙߊ߫ ߟߊ߫ ߞߵߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲ ߕߏ߫߸ ߝߏ߫ ߣߴߌ ߞߵߌ ߟߊ߫ ߛߏ߲߯ߓߊߟߊ߲ ߢߡߊߘߏ߲߰ߣߍ߲ ߠߎ߬ ߖߏ߬ߛߌ߬.", "logging-out-notify": "ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲ ߓߐ ߦߴߌ ߘߐ߫߸ ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬.", @@ -478,6 +487,9 @@ "botpasswords-updated-body": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߏߕ ߕߐ߮ ߦߋ߫ \"$1\" {{GENDER:$2|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߦߋ߫ \"$2\" ߕߎ߲߬ ߓߘߊ߫ ߟߏ߲ߘߐߦߊ߫.", "botpasswords-deleted-title": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߘߊ߫ ߖߏ߬ߛߌ߬", "botpasswords-deleted-body": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߓߏߕ ߕߐ߮ ߦߋ߫ \"$1\" {{GENDER:$2|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߦߋ߫ \"$2\" ߕߎ߲߬ ߓߘߊ߫ ߖߏ߬ߛߌ߬.", + "botpasswords-not-exist": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߕߐ߯ߟߊߣߍ߲߫ \"$2\" ߕߍ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ \"$1\" ߓߟߏ߫", + "botpasswords-needs-reset": "ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ \"$1\" ߓߏߕ ߕߐ߯ \"$2\" ߦߋ߫ {{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} \"$1\" ߦߋ߫ ߡߊߦߟߍ߬ߡߊ߲߫.", + "botpasswords-locked": "ߌ ߕߍߣߊ߬ ߛߋߟߴߌ ߜߊ߲߬ߞߎ߲߬ ߠߊ߫ ߓߏߕ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߘߌ߫ ߓߊ ߌ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ ߛߐ߰ߣߍ߲߫ ߠߋ߫.", "resetpass_forbidden": "ߕߊ߬ߡߌ߲߬ߞߊ߲ ߕߴߛߋ߫ ߡߊߝߊ߬ߘߋ߲߬ ߠߊ߫.", "resetpass_forbidden-reason": "ߕߊ߬ߡߌ߲߬ߞߊ߲ ߕߴߛߋ߫ ߡߊߝߊ߬ߟߋ߲߬ ߠߊ߫: $1", "resetpass-no-info": "ߌ ߦߴߌ ߜߊ߲߬ߞߎ߲߬ ߡߎߣߎ߲߬ ߞߣߊ߬ ߕߏ߫ ߞߐߜߍ ߣߌ߲߬ ߡߊߛߐ߬ߘߐ߲߬ ߠߊ߫.", @@ -585,8 +597,11 @@ "template-protected": "(ߊ߬ ߡߊߞߊ߲ߞߊ߲ߣߍ߲߫ ߠߋ߬)", "template-semiprotected": "(ߟߊ߬ߞߊ߲߬ߘߊ߬ߟߌ-ߝߊ߲߬ߞߋ߬ߟߋ߲߬ߡߊ)", "hiddencategories": "ߞߐߜߍ ߣߌ߲߬ ߦߋ߫ ߢߌ߲߬ ߠߎ߫ ߛߌ߲߬ߝߏ߲ ߠߋ߬ ߘߌ߫{{PLURAL:$1|}}", + "sectioneditnotsupported-text": "ߛߌ߰ߘߊ ߡߊߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߊߘߤߊ߬ߣߍ߲߬ ߕߍ߫ ߞߐߜߍ ߣߌ߲߬ ߠߊ߫ ߕߊ߲߬.", "permissionserrors": "ߝߌ߬ߟߌ߫ ߘߌ߬ߢߍ߬ߒߧߋ", + "permissionserrorstext": "ߌ ߟߊߘߌ߬ߢߍ߬ߣߍ߲߬ ߕߍ߫ ߞߵߏ߬ ߞߍ߫߸ ߣߌ߲߬ ߠߊ߫ {{PLURAL:$1|ߛߊߓߎ|ߛߊߓߎ ߟߎ߬}}:", "permissionserrorstext-withaction": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ߬ ߛߌ߫ ߕߴߌ ߦߋ߫ ߞߊ߬ $2߸ {{PLURAL:$1|ߞߏߛߐ߲߬|ߟߎ߬ ߞߏߛߐ߲߬}}", + "contentmodelediterror": "ߌ ߕߍߣߊ߬ ߛߋ߫ ߟߊ߫ ߛߌ߰ߘߊ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫߸ ߓߊߏ߬ ߞߣߐߘߐ ߛߎ߯ߦߊ ߦߋ߫ $1 ߟߋ߬ ߘߌ߫߸ ߡߍ߲ ߦߋ߫ ߕߋ߲߬ߕߋ߲߬ ߞߣߐߘߐ ߛߎ߯ߦߊ ߝߘߏ߬ ߟߊ߫ ߞߐߜߍ $2 ߘߐ߫.", "recreate-moveddeleted-warn": "ߌ ߖߊ߲߬ߕߏ߫: ߌ ߦߋ߫ ߞߐߜߍ ߘߏ߫ ߟߋ߬ ߟߊߘߊ߲߫ ߞߏ ߘߐ߫ ߣߌ߲߬߸ ߡߍ߲ ߖߏ߬ߛߌ߬ߣߍ߲߬ ߡߎߣߎ߲߬. \nߌ ߓߛߌ߬ߞߌ߬ ߕߐ߫ ߟߋ߬ ߛߍ߲߸ ߣߴߌ ߘߌ߫ ߛߋ߫ ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲ ߘߊߓߊ߲߫ ߠߊ߫. \nߞߐߜߍ ߣߌ߲߬ ߦߟߌߣߐ ߖߏ߬ߛߌ߬ߣߍ߲ ߣߴߊ߬ ߛߋ߲߬ߓߐ߬ߣߍ߲ ߠߎ߬ ߡߊߘߊ߲ߣߍ߲߫ ߦߊ߲߬ ߠߋ ߟߊ߬ߣߐ߰ߦߊ߬ߟߌ ߘߌ߫:", "moveddeleted-notice": "ߞߐߜߍ ߣߌ߲߬ ߓߘߊ߫ ߖߏ߬ߛߌ߬.\nߖߏ߬ߛߌ߬ߟߌ߸ ߟߊ߬ߞߊ߲߬ߘߊ߬ߟߌ߸ ߊ߬ ߣߌ߫ ߞߐߜߍ ߛߓߍߟߌ ߟߎ߬ ߛߋ߲߬ߓߐ߸ ߏ߬ ߟߎ߫ ߓߍ߯ ߡߊߛߐߣߍ߲߫ ߦߋ߫ ߘߎ߰ߟߊ ߘߐ߫.", "log-fulllog": "ߘߎ߲ߛߓߍ ߘߝߊߣߍ߲ ߦߋ߫", @@ -597,6 +612,7 @@ "postedit-confirmation-saved": "ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߟߊߞߎ߲߬ߘߎ߬.", "postedit-confirmation-published": "ߌ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߘߊ߫ ߟߊߥߊ߲߬ߞߊ߫.", "edit-already-exists": "ߌ ߕߴߛߋ߫ ߞߐߜߍ߫ ߞߎߘߊ߫ ߛߌ߲ߘߌ߫ ߟߊ߫.\nߊ߬ ߦߋ߫ ߦߋ߲߬ ߞߘߐ߬ߡߊ߲߬.", + "defaultmessagetext": "ߓߐߛߎ߲ ߗߋߛߓߍ ߛߓߍߟߌ", "invalid-content-data": "ߞߣߐߘߐ ߓߟߏߡߟߊ ߓߍ߲߬ߓߊߟߌ", "content-not-allowed-here": "\"$1\" ߞߣߐߘߐ ߟߊߘߤߊ߬ߣߍ߲߬ ߕߍ߫ ߞߐߜߍ ߘߐ߫ [[:$2]] ߛߍ߲ߞߍߘߊ ߘߐ߫ \"$3\"", "editwarning-warning": "ߣߴߌ ߓߐ߫ ߘߊ߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬߸ ߌ ߘߌ߫ ߓߣߐ߬ ߌ ߟߊ߫ ߡߊ߬ߝߊ߬ߟߋ߲߬ߠߌ߲߬ ߞߍߣߍ߲ ߠߎ߬ ߓߍ߯ ߘߐ߫.\nߣߴߌ ߘߏ߲߬ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߞߍ߫ ߘߊ߫߸ ߌ ߘߌ߫ ߛߋ߫ ߖߊ߬ߛߙߋ߬ߡߊ߬ߟߊ ߣߌ߲߬ ߓߐ߫ ߟߴߊ߬ ߟߊ߫ \"{{int:prefs-editing}}\" ߘߐ߫߸ ߌ ߟߊ߫ ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߥߟߊ߬ߘߊ ߘߐ߫.", @@ -879,6 +895,7 @@ "saveusergroups": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬}} ߞߙߎ ߟߊߞߎ߲߬ߘߎ߬", "userrights-groupsmember": "ߛߌ߲߬ߝߏ߲ ߠߎ߬:", "userrights-reason": "ߊ߬ ߛߊߓߎ:", + "userrights-nodatabase": "ߓߟߏߡߟߊ ߝߊ߲ $1 ߕߴߦߋ߲߬ ߥߟߴߊ߬ ߕߍ߫ ߕߌ߲߬ߞߎ߬ߘߎ߲߬ߡߊ߬ ߘߌ߫.", "userrights-changeable-col": "ߌ ߘߌ߫ ߛߋ߫ ߞߙߎ ߡߍ߲ ߠߎ߬ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫", "userrights-unchangeable-col": "ߌ ߕߴߛߋ߫ ߞߙߎ ߡߍ߲ ߠߎ߬ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫", "userrights-expiry-current": "ߊ߬ ߛߕߊ ߓߘߊ߫ ߝߊ߫ $1", @@ -887,13 +904,21 @@ "userrights-expiry-existing": "ߕߋ߲߭ߕߋ߲߭ ߛߕߊߝߊ߫ ߕߎߡߊ: $3߸ $2", "userrights-expiry-othertime": "ߕߎ߬ߡߊ߬ ߜߘߍ:", "userrights-expiry-options": "ߕߟߋ߬ ߁: ߕߟߋ߬ ߁߸ ߞߎ߲߬ߢߐ߰ ߁: ߞߎ߲߬ߢߐ߰ ߁߸ ߞߊߙߏ߫ ߁: ߞߊߙߏ߫ ߁߸ ߞߊߙߏ߫ ߃: ߞߊߙߏ߫ ߃߸ ߞߊߙߏ߫ ߆: ߞߊߙߏ߫ ߆߸ ߛߊ߲߬ ߁: ߛߊ߲߬ ߁", + "userrights-invalid-expiry": "ߞߙߎ \"$1\" ߛߕߊ ߝߊ߫ ߕߎߡߊ ߓߍ߲߬ߣߍ߲߫ ߕߍ߫.", + "userrights-expiry-in-past": "ߞߙߎ \"$1\" ߛߕߊ ߝߊ߫ ߕߎߡߊ ߓߘߊ߫ ߕߊ߬ߡߌ߲߬.", "group": "ߞߙߎ:", "group-user": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ", "group-autoconfirmed": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ߬ ߞߍߒߖߘߍߦߋ߫ ߟߊߛߙߋߦߊߣߍ߲", "group-bot": "ߓߏߕ", "group-sysop": "ߞߎ߲߬ߠߊ߬ߛߌ߰ߟߊ", + "group-bureaucrat": "ߛߓߍߘߟߊߡߐ߮", + "group-suppress": "ߛߎ߬ߔߙߋߛߐ߬", "group-all": "(ߊ߬ ߓߍ߯)", "group-user-member": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}}", + "group-autoconfirmed-member": "{{GENDER:$1|ߞߍߒߖߘߍߦߋ߫ ߟߊߛߙߋߦߊߟߌ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}}", + "group-bot-member": "{{GENDER:$1|ߓߏߕ}}", + "group-sysop-member": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}}", + "group-bureaucrat-member": "{{GENDER:$1|ߛߓߍߘߟߊߡߐ߮}}", "grouppage-user": "{{ns:project}}: ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ", "grouppage-bot": "{{ns:project}}:ߓߏߕ", "grouppage-sysop": "{{ns:project}}:ߡߊ߬ߡߙߊ߬ߟߌ߬ߟߊ", @@ -902,12 +927,26 @@ "right-createpage": "ߞߐߜߍ ߘߏ߫ ߛߌ߲ߘߌ߫ (ߡߍ߲ ߕߍ߫ ߓߊ߬ߘߏ߬ߓߊ߬ߘߌ߬ߦߊ߬ ߞߐߜߍ ߝߋ߲߫ ߘߌ߫)", "right-createtalk": "ߓߊ߬ߘߏ߬ߓߊ߬ߘߌ߬ߦߊ߬ ߞߐߜߍ ߛߌ߲ߘߌ߫", "right-createaccount": "ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ߫ ߞߎߘߊ߫ ߛߌ߲ߘߌ߫", + "right-minoredit": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߐ߬ߣߐ߬ ߡߌ߬ߛߍ߬ߡߊ߲ ߘߌ߫", "right-move": "ߞߐߜߍ ߟߎ߬ ߛߋ߲߬ߓߐ߫", "right-move-subpages": "ߞߐߜߍ ߛߋ߲߬ߓߐ߫ ߊ߬ߟߎ߬ ߟߊ߫ ߞߐߜߍߙߋ߲ ߠߎ߬ ߘߐ߫", "right-move-categorypages": "ߦߌߟߡߊ߫ ߞߐߜߍ ߟߎ߬ ߛߋ߲߬ߓߐ߫", "right-movefile": "ߞߐߕߐ߮ ߟߎ߬ ߛߋ߲߬ߓߐ߫", "right-upload": "ߞߐߕߐ߮ ߟߎ߬ ߟߊߦߟߍ߬", + "right-reupload": "ߛߋ߲߬ߠߊ߬ ߞߐߕߐ߮ ߖߏ߬ߛߌ߬", + "right-reupload-own": "ߌ ߖߍ߬ߘߍ ߟߊ߫ ߞߐߕߐ߯ ߟߊߦߟߍ߬ߣߍ߲ ߠߎ߬ ߖߏ߰ߛߌ߬", + "right-upload_by_url": "ߞߐߕߐ߮ ߘߏ߫ ߟߊߦߟߍ߬ ߞߊ߬ ߓߐ߫ URL ߘߐ߫", "right-writeapi": "ߛߓߍߟߌ API ߟߊߓߊ߯ߙߊ߫", + "right-delete": "ߞߐߜߍ ߟߎ߬ ߖߏ߰ߛߌ߬", + "right-bigdelete": "ߞߐߜߍ߫ ߘߝߐ߬ ߓߟߋ߬ߓߟߋ߬ߡߊ ߟߎ߬ ߖߏ߰ߛߌ߬", + "right-browsearchive": "ߞߐߜߍ߫ ߖߏ߰ߛߌ߬ߣߍ߲ ߠߎ߬ ߢߌߣߌ߲߫", + "right-undelete": "ߞߐߜߍ ߖߏ߰ߛߌ߬ߣߍ߲ ߓߐ߫", + "right-suppressionlog": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߘߎ߲ߛߓߍ ߟߎ߬ ߦߋ߫", + "right-block": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߓߊ߬ߟߌ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߊ߬", + "right-blockemail": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߓߊ߬ߟߌ߬ ߢߎߡߍߙߋ߲ ߗߋߟߌ ߡߊ߬", + "right-hideuser": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߓߊ߬ߟߌ߬߸ ߊ߬ ߢߡߊߘߏ߲߰ ߖߊ߬ߡߊ ߡߊ߬.", + "right-unblockself": "ߴ ߖߍ߬ߘߍ ߓߊ߬ߟߌ߬ߣߍ߲ ߓߐ߫", + "right-editcontentmodel": "ߞߐߜߍ ߣߌ߲߬ ߞߣߐߘߐ ߛߎ߮ߦߊ ߡߊߝߊ߬ߟߋ߲߬", "right-editusercss": "CSS ߞߐߕߐ߮ ߘߏ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫", "right-edituserjson": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ CSS ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫", "right-edituserjs": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ JavaScript ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫", @@ -916,15 +955,97 @@ "right-editsitejs": "ߞߍߦߙߐ ߞߣߍ JavaScript ߡߊߦߟߍ߬ߡߊ߲߫", "right-editmyusercss": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ CSS ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߞߐߕߐ߮ ߡߊߦߟߍ߬ߡߊ߲߫", "right-editmyuserjson": "ߌ ߖߍ߬ߘߍ ߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ JSON ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫", + "right-editmyuserjs": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ JavaScript ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫", + "right-viewmywatchlist": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߦߋ߫", + "right-editmyoptions": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߟߊߝߌߛߦߊߟߌ ߡߊߦߟߍ߬ߡߊ߲߫", + "right-unwatchedpages": "ߞߐߜߍ߫ ߜߋ߬ߟߎ߲߬ߓߊߟߌ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫", + "right-mergehistory": "ߞߐߜߍ ߟߊ߫ ߘߐ߬ߝߐ ߟߎ߬ ߞߍߢߐ߲߮ߞߊ߲߬", + "right-userrights": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߤߊߞߍ ߓߍ߯ ߡߊߦߟߍ߬ߡߊ߲߫", + "right-userrights-interwiki": "ߥߞߌ ߘߏ ߟߎ߬ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߎ߬ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߤߊߞߍ ߡߊߦߟߍ߬ߡߊ߲߫", + "right-siteadmin": "ߓߟߏߡߟߊ ߝߊ߲ ߣߍ߰ ߊ߬ ߣߌ߫ ߞߵߊ߬ ߟߊߞߊ߬", + "right-sendemail": "ߢߎߡߍߙߋ߲ ߗߋ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߡߊ߬", + "grant-group-email": "ߢߎߡߍߙߋ߲ ߗߋ߫", + "grant-createaccount": "ߖߊ߬ߕߋ߬ߘߊ ߘߏ߫ ߛߌ߲ߘߌ߫", + "grant-createeditmovepage": "ߞߐߜߍ ߛߌ߲ߘߌ߫߸ ߡߊߦߟߍ߬ߡߊ߲߫߸ ߊ߬ ߣߌ߫ ߞߵߊ߬ ߛߋ߲߬ߓߐ߫", + "grant-editinterface": "MediaWiki ߕߐ߯ߛߓߍ ߞߣߍ ߡߊߦߟߍ߬ߡߊ߲߫ ߊ߬ ߣߌ߫ ߞߍߦߙߐ ߞߣߍ/ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ JSON", + "grant-editmycssjs": "ߌ ߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ CSS/JSON/JavaScript ߡߊߦߟߍ߬ߡߊ߲߫", + "grant-editmyoptions": "ߌ ߟߊ߫ ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ ߣߌ߫ JSON ߛߏ߯ߙߏߟߌ ߡߊߦߟߍ߬ߡߊ߲߫", + "grant-editmywatchlist": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߡߊߦߟߍ߬ߡߊ߲߫", + "grant-editsiteconfig": "ߞߍߦߙߐ ߞߣߍ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ CSS/JS ߡߊߦߟߍ߬ߡߊ߲߫", + "grant-editpage": "ߞߐߜߍ߫ ߓߍߓߊ߮ ߡߊߦߟߍ߬ߡߊ߲߫", + "grant-editprotected": "ߞߐߜߍ߫ ߟߊߞߊ߲ߘߊߣߍ߲ ߡߊߦߟߍ߬ߡߊ߲߫", + "grant-highvolume": "ߢߊ߲ߞߊ߲-ߛߊ߲ߘߐߕߊ ߡߊߦߟߍߡߊ߲ ߦߴߌ ߘߐ߫", + "grant-privateinfo": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߞߌߓߊߙߏߦߊ ߟߊߛߐ߬ߘߐ߲߬", + "grant-protect": "ߞߐߜߍ ߟߎ߬ ߟߊߞߊ߲ߘߊ߫ ߊ߬ ߣߌ߫ ߞߵߊ߬ߟߎ߬ ߟߊߞߊ߲ߘߊߣߍ߲ ߓߐ߫", + "grant-sendemail": "ߢߎߡߍߙߋ߲ ߗߋ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߡߊ߬", + "grant-uploadeditmovefile": "ߞߐߕߐ߮ ߟߊߦߟߍ߬߸ ߣߐ߬ߘߐߓߌ߬ߟߊ߬߸ ߊ߬ ߣߌ߫ ߞߵߊ߬ ߛߋ߲߬ߓߐ߫", + "grant-uploadfile": "ߞߐߕߐ߮ ߞߎߘߊ߫ ߟߊߦߟߍ߬", + "grant-basic": "ߤߊߞߍ ߓߊߖߎߟߞߊ", + "grant-viewdeleted": "ߞߐߜߍ ߣߌ߫ ߞߐߕߐ߮ ߖߏ߰ߛߌ߬ߣߍ߲ ߠߎ߬ ߦߋ߫", + "grant-viewmywatchlist": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߦߋ߫", "newuserlogpage": "ߖߊ߬ߕߋ߬ߘߊ߬ ߓߘߊ߫ ߟߊߞߊ߬ ߌ ߜߊ߲߬ߞߎ߲߬", + "newuserlogpagetext": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߊ߫ ߘߎ߲ߛߓߍ߫ ߛߌ߲ߘߌߣߍ߲ ߘߏ߫ ߟߋ߬ ߦߋ߫ ߣߌ߲߬.", "rightslog": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߜߊ߲߬ߞߎ߲߬ ߢߊ߬ ߓߘߍ", + "rightslogtext": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߤߊߞߍ ߡߊߦߟߍ߬ߡߊ߲߫ ߘߎ߲ߛߓߍ ߟߋ߬ ߦߋ߫ ߣߌ߲߬", + "action-read": "ߞߐߜߍ ߣߌ߲߬ ߘߐߞߊ߬ߙߊ߲߬", "action-edit": "ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬", + "action-createpage": "ߞߐߜߍ ߣߌ߲߬ ߛߌ߲ߘߌ߫", + "action-createtalk": "ߘߊߘߐߖߊߥߏ߫ ߞߐߜߍ ߣߌ߲߬ ߛߌ߲ߘߌ߫", "action-createaccount": "ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ ߣߌ߲߬ ߠߊߘߊ߲߫", + "action-autocreateaccount": "ߞߐߞߊ߲ߝߊ߲ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߖߊߕߋߘߊ ߣߌ߲߬ ߛߌ߲ߘߌ߫ ߞߍߒߖߘߍߦߋ߫ ߓߟߏߡߊ߬", + "action-history": "ߞߐߜߍ ߣߌ߲߬ ߠߊ߫ ߘߐ߬ߝߐ ߦߋ߫", + "action-minoredit": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߌ߲߬ ߣߐ߬ߣߐ߬ ߢߟߋߢߟߋ ߘߌ߫", + "action-move": "ߞߐߜߍ ߣߌ߲߬ ߛߋ߲߬ߓߐ߫", + "action-move-subpages": "ߞߐߜߍ ߣߌ߲߬ ߛߋ߲߬ߓߐ߫߸ ߊ߬ ߣߴߊ߬ ߞߐߜߍߙߋ߲ ߠߎ߬", + "action-move-categorypages": "ߦߌߟߡߊ߫ ߞߐߜߍ ߟߎ߬ ߛߋ߲߬ߓߐ߫", + "action-movefile": "ߞߐߕߐ߮ ߣߌ߲߬ ߛߋ߲߬ߓߐ߫", + "action-upload": "ߞߐߕߐ߮ ߣߌ߲߬ ߠߊߦߟߍ߬", + "action-reupload": "ߞߐߕߐ߯ ߓߍߓߊ߮ ߣߌ߲߬ ߥߦߊ߬", + "action-upload_by_url": "ߞߐߕߐ߮ ߣߌ߲߬ ߠߊߦߟߍ߬ ߞߊ߬ ߓߐ߫ URL ߘߐ߫", + "action-writeapi": "ߛߓߍߟߌ API ߟߊߓߊ߯ߙߊ߫", + "action-delete": "ߞߐߜߍ ߣߌ߲߬ ߖߏ߰ߛߌ߬", + "action-deleterevision": "ߟߢߊ߬ߟߌ ߟߎ߬ ߖߏ߬ߛߌ߬", + "action-deletedhistory": "ߞߐߜߍ ߟߎ߬ ߖߏ߰ߛߌ߬ߟߌ ߘߐ߬ߝߐ ߦߋ߫", + "action-browsearchive": "ߞߐߜߍ߬ ߖߏ߰ߛߌ߬ߣߍ߲ ߠߎ߬ ߢߌߣߌ߲߫", + "action-undelete": "ߞߐߜߍ ߖߏ߰ߛߌ߬ߓߊߟߌ ߟߎ߬", + "action-suppressionlog": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߘߎ߲ߛߓߍ ߣߌ߲߬ ߦߋ߫", + "action-block": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߣߌ߲߬ ߓߊ߬ߟߌ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߊ߬", + "action-protect": "ߞߐߜߍ ߣߌ߲߬ ߟߊ߬ߞߊ߲߬ߘߊ߬ߟߌ߬ ߞߛߊߞߊ ߡߊߝߊ߬ߟߋ߲߬", + "action-import": "ߞߐߜߍ ߟߎ߬ ߟߊߛߣߍ߫ ߞߊ߬ ߓߐ߫ ߥߞߌ ߕߐ߭ ߟߎ߬ ߘߐ߫", + "action-importupload": "ߞߐߜߍ ߟߎ߬ ߟߊߛߣߍ߫ ߞߊ߬ ߓߐ߫ ߞߐߕߐ߯ ߟߊߦߟߍ߬ߣߍ߲ ߠߎ߬ ߘߐ߫", + "action-unwatchedpages": "ߞߐߜߍ߫ ߜߋ߬ߟߎ߲߬ߓߊߟߌ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫", + "action-mergehistory": "ߞߐߜߍ ߣߌ߲߬ ߠߊ߫ ߘߐ߬ߝߐ ߟߎ߬ ߞߍߢߐ߲߮ߞߊ߲߬", + "action-userrights": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߤߊߞߍ ߓߍ߯ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-userrights-interwiki": "ߥߞߌ ߘߏ ߟߎ߬ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߎ߬ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߤߊߞߍ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-siteadmin": "ߓߟߏߡߟߊ ߝߊ߲ ߣߍ߰ ߥߟߊ߫ ߞߵߊ߬ ߣߍ߰ߣߍ߲ ߓߐ߫", + "action-sendemail": "ߢߎߡߍߙߋ߲ ߗߋ߫", + "action-editmyoptions": "ߌ ߟߊ߫ ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-editmywatchlist": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-viewmywatchlist": "ߌ ߟߊ߫ ߜߋ߬ߟߎ߬ߠߌ߲߬ ߛߙߍߘߍ ߦߋ߫", + "action-viewmyprivateinfo": "ߌ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߞߌߓߊߙߏߦߊ ߟߎ߬ ߦߋ߫", + "action-editmyprivateinfo": "ߌ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߞߌߓߊߙߏߦߊ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-editcontentmodel": "ߞߐߜߍ ߣߌ߲߬ ߞߣߐߘߐ ߛߎ߮ߦߊ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-editusercss": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ CSS ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߞߐߕߐ߮ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-edituserjson": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ JSON ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-edituserjs": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ JavaScript ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-editsitecss": "ߞߍߦߙߐ ߞߣߍ CSS ߡߊߦߟߍ߬ߡߊ߲߫", + "action-editsitejson": "ߞߍߦߙߐ ߞߣߍ JSON ߡߊߦߟߍ߬ߡߊ߲߫", + "action-editsitejs": "ߞߍߦߙߐ ߞߣߍ JavaScript ߡߊߦߟߍ߬ߡߊ߲߫", + "action-editmyusercss": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ CSS ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬ ߞߐߕߐ߮ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-editmyuserjson": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ JSON ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-editmyuserjs": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ JavaScript ߞߐߕߐ߮ ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߫", + "action-viewsuppressed": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߘߏ ߟߎ߬ ߟߊ߫ ߟߢߊ߬ߟߌ߬ ߢߡߊߘߏ߲߰ߣߍ߲ ߠߎ߬ ߦߋ߫", + "action-unblockself": "ߌ ߖߍ߬ߘߍ ߓߊ߬ߟߌ߬ߣߍ߲ ߓߐ߫", + "nchanges": "$1 {{PLURAL:$1|ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲|ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬}}", + "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|ߞߊ߬ߦߌ߯ ߓߐߒߡߊߟߌ ߟߊߓߊ߲}}", "enhancedrc-history": "ߕߊ߬ߡߌ߲߬ߣߍ߲", "recentchanges": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߬", "recentchanges-legend": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߫ ߟߊ߬ߓߍ߲߬ߢߐ߰ߡߦߊ߬ߘߊ", "recentchanges-summary": "ߥߞߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎ߲ߓߊ ߡߍ߲ ߠߎ߬ ߞߍߣߍ߲߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬߸ ߏ߬ ߟߎ߫ ߣߐ߬ߣߐ߬.", "recentchanges-noresult": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߛߌ߫ ߓߍ߲߬ߢߐ߲߰ߦߊ߬ߣߍ߲߬ ߕߍ߫ ߛߎߡߊ߲ߡߕߊ ߢߌ߲߬ ߠߎ߫ ߡߊ߬ ߕߎ߬ߡߊ߬ ߟߊߕߍ߰ߣߍ߲ ߦߌ߬ߘߊ ߘߐ߫.", + "recentchanges-timeout": "ߢߌߣߌ߲ߣߌ߲ ߣߌ߲߬ ߕߎ߬ߡߊ ߓߘߊ߫ ߕߊ߬ߡߌ߲߬. ߌ ߞߊߞߊ߲߫ ߞߊ߬ ߢߊߢߌߣߌ߲߫ ߜߘߍ߫ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߞߍ߫.", + "recentchanges-network": "ߞߊ߬ ߓߍ߲߬ ߛߋߒߞߏߟߦߊ ߝߎ߬ߕߎ߲߬ߕߌ ߡߊ߬߸ ߞߐߝߟߌ߫ ߛߌ߫ ߕߍ߫ ߣߊ߬ ߛߋ߫ ߟߊ߫ ߟߊߢߎ߲߫ ߠߊ߫. ߌ ߞߊߘߊ߲߫ ߞߊ߬ ߞߐߜߍ ߣߌ߲߬ ߠߊߛߎߡߦߊ߫ ߖߊ߰ߣߌ߲߫.", + "recentchanges-notargetpage": "ߞߐߜߍ ߕߐ߮ ߟߊߘߏ߲߬ ߛߊ߲ߝߍ߬߸ ߦߟߍ߬ߡߊ߲ ߡߍ߲ ߦߋ߫ ߞߐߜߍ ߘߐ߫߸ ߞߵߏ߬ ߦߋ߫.", "recentchanges-label-newpage": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߌ߲߬ ߓߘߊ߫ ߘߐߜߍ߫ ߞߎߘߊ ߟߊߘߊ߲߫", "recentchanges-label-minor": "ߢߟߊߞߎߘߦߊ߫ ߝߕߌߣߍ߲ ߠߋ߬", "recentchanges-label-bot": "ߡߐ߰ߡߐ߮ ߟߋ߫ ߣߐ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ ߣߌ߲߬ ߞߍ߫ ߟߊ߫", @@ -932,7 +1053,65 @@ "recentchanges-label-plusminus": "ߞߐߜߍ ߢߊ߲ߞߊ߲ ߓߘߊ߫ ߡߊߦߟߍ߬ߡߊ߲߫ ߞߵߊ߬ ߝߌ߬ߘߊ߲ ߦߙߌߞߊ ߣߌ߲߬ ߘߌ߫", "recentchanges-legend-heading": "ߡߊ߬ߛߙߋ:", "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (ߣߌ߲߬ ߝߣߊ߫ ߦߋ߫ \n[[Special:NewPages|list of new pages]])", + "recentchanges-submit": "ߊ߬ ߦߌ߬ߘߊ߬", + "rcfilters-tag-remove": "$1 ߛߋ߲߬ߓߐ߫", + "rcfilters-legend-heading": "ߟߊ߬ߘߛߏ߬ߟߌ ߛߙߍߘߍ", + "rcfilters-other-review-tools": "ߡߊ߬ߛߊ߬ߦߌ߲߬ߠߌ߲߬ ߖߐ߯ߙߊ߲ ߘߏ ߟߎ߬", + "rcfilters-group-results-by-page": "ߞߙߎ ߞߐߝߟߌ ߞߐߜߍ ߡߊ߬", + "rcfilters-activefilters-hide": "ߊ߬ ߢߡߊߘߏ߲߰", + "rcfilters-activefilters-show": "ߊ߬ ߦߌ߬ߘߊ߬", + "rcfilters-limit-title": "ߞߐߝߟߌ ߡߍ߲ ߠߎ߬ ߦߌ߬ߘߊ߬ߕߊ ߦߋ߫", + "rcfilters-limit-and-date-label": "$1{{PLURAL:$1|ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲|ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬}}߸ $2", + "rcfilters-date-popup-title": "ߕߎ߬ߡߊ ߣߌ߫ ߥߎ߬ߛߎ ߡߍ߲ ߠߎ߬ ߢߌߣߌ߲ߕߊ ߦߋ߫", + "rcfilters-days-title": "ߟߏ߲ ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߬", + "rcfilters-hours-title": "ߕߎ߬ߡߊ߬ߙߋ߲߫ ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߬", + "rcfilters-days-show-days": "$1 {{PLURAL:$1|ߟߏ߲|ߟߏ߲ ߠߎ߬}}", + "rcfilters-days-show-hours": "$1 {{PLURAL:$1|ߕߎ߬ߡߊ߬ߙߋ߲|ߕߎ߬ߡߊ߬ߙߋ߲ ߠߎ߬}}", + "rcfilters-highlighted-filters-list": "ߡߊߦߋߙߋ߲ߣߍ߲:$1", + "rcfilters-quickfilters": "ߞߎ߲߬ߕߐ߰ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲ ߠߎ߬", + "rcfilters-quickfilters-placeholder-title": "ߛߌ߲ߘߌߣߍ߲ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲߬ ߕߍ߫ ߡߎߣߎ߲߬", + "rcfilters-savedqueries-defaultlabel": "ߞߎ߲߬ߕߐ߰ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲ ߠߎ߬", + "rcfilters-savedqueries-rename": "ߊ߬ ߕߐ߯ߟߊ߫", + "rcfilters-savedqueries-setdefault": "ߊ߬ ߞߍ߫ ߓߐߛߎ߲ ߘߌ߫", + "rcfilters-savedqueries-unsetdefault": "ߊ߬ ߓߐ߫ ߓߐߛߎ߲ ߘߐ߫", + "rcfilters-savedqueries-remove": "ߊ߬ ߖߏ߰ߛߌ߬", + "rcfilters-savedqueries-new-name-label": "ߕߐ߮", + "rcfilters-savedqueries-apply-label": "ߛߍ߲ߛߍ߲ߟߊ߲ ߛߌ߲ߘߌ߫", + "rcfilters-savedqueries-apply-and-setdefault-label": "ߓߐߛߎ߲ ߛߍ߲ߛߍ߲ߟߊ߲ ߛߌ߲ߘߌ߫", + "rcfilters-savedqueries-cancel-label": "ߊ߬ ߘߐߛߊ߬", + "rcfilters-savedqueries-add-new-title": "ߕߋ߲߬ߕߋ߲߬ ߛߍ߲ߛߍ߲ߟߊ߲ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߟߊߞߎ߲߬ߘߎ߬", + "rcfilters-savedqueries-already-saved": "ߛߍ߲ߛߍ߲ߟߊ߲ ߣߌ߲߬ ߓߘߊ߫ ߓߊ߲߫ ߠߊߞߎ߲߬ߘߎ߬ ߟߊ߫.ߌ ߟߊ߫ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߊ ߡߊߝߊ߬ߟߋ߲߬ ߞߊ߬ ߛߍ߲ߛߍ߲ߟߊ߲߫ ߟߊߞߎ߲߬ߘߎ߬ߣߍ߲ ߘߏ߫ ߛߌ߲ߘߌ߫.", + "rcfilters-clear-all-filters": "ߛߍ߲ߛߍ߲ߟߊ߲ ߓߍ߯ ߛߊߣߌ߲ߧߊ߫", + "rcfilters-show-new-changes": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߬ ߦߋ߫ ߞߊ߬ߦߌ߯ $1", + "rcfilters-filterlist-whatsthis": "ߣߌ߲߬ ߦߋ߫ ߓߊ߯ߙߊ߫ ߟߊ߫ ߘߌ߬؟", + "rcfilters-highlightbutton-title": "ߞߐߝߟߌ߫ ߡߊߦߋߙߋ߲ߣߍ߲ ߠߎ߬", + "rcfilters-highlightmenu-title": "ߞߐ߬ߟߐ ߘߏ߫ ߓߊߓߌ߬ߟߊ߬", + "rcfilters-filter-editsbyself-label": "ߡߍ߲ ߠߎ߬ ߡߊߦߟߍ߬ߡߊ߲߬ߣߍ߲߬ ߌ ߓߟߏ߫", + "rcfilters-filter-editsbyself-description": "ߌ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲.", + "rcfilters-filter-editsbyother-label": "ߘߏ ߟߎ߬ ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬", + "rcfilters-filter-editsbyother-description": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߍ߯ ߝߴߌ ߕߊ ߟߎ߬.", + "rcfilters-filter-user-experience-level-registered-label": "ߕߐ߯ߛߓߍߣߍ߲", + "rcfilters-filter-user-experience-level-registered-description": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߊ߫ ߜߊ߲߬ߞߎ߲߬ߣߍ߲ ߠߎ߬", + "rcfilters-filter-user-experience-level-unregistered-label": "ߕߐ߯ߛߓߍߓߊߟߌ", + "rcfilters-filter-user-experience-level-unregistered-description": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߊ ߡߍ߲ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߕߍ߫.", + "rcfilters-filter-user-experience-level-learner-label": "ߞߊ߬ߙߊ߲߬ߠߊ ߟߎ߬", + "rcfilters-filter-bots-label": "ߓߏߕ", + "rcfilters-filter-bots-description": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߍ߲ ߠߎ߬ ߛߌ߲ߘߌߣߍ߲߫ ߞߍߒߖߘߍߦߋ߫ ߖߐ߯ߙߊ߲ ߠߎ߬ ߘߐ߫.", + "rcfilters-filter-humans-label": "ߡߐ߱ (ߓߏߕ ߕߍ߫)", + "rcfilters-filter-humans-description": "ߡߐ߱ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߊ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ ߣߐ.", + "rcfilters-filter-reviewstatus-unpatrolled-label": "ߓߍ߬ߙߍ߲߬ߓߍ߬ߙߍ߲߬ߓߊߟߌ", + "rcfilters-filter-minor-label": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߘߋ߬ߣߍ߲ ߠߎ߬", + "rcfilters-filtergroup-watchlist": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߞߐߜߍ ߟߎ߬", + "rcfilters-filter-watchlist-watched-label": "ߜߋ߬ߟߎ߲߬ߠߌ߲߬ ߛߙߍߘߍ ߘߐ߫", + "rcfilters-filtergroup-changetype": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߛߎ߯ߦߊ", + "rcfilters-filter-pageedits-label": "ߞߐߜߐ ߡߊߦߟߍ߬ߡߊ߲߫", + "rcfilters-filter-pageedits-description": "ߞߐߜߍ ߛߌ߲ߘߟߌ", + "rcfilters-filter-newpages-label": "ߞߐߜߍ ߛߌ߲ߘߟߌ", + "rcfilters-filter-newpages-description": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߍ߲ ߠߎ߬ ߦߋ߫ ߞߐߜߍ߫ ߞߎߘߊ߫ ߟߊߘߊ߲߫ ߠߊ߫.", + "rcfilters-filter-categorization-label": "ߦߌߟߡߊ߫ ߡߊߦߟߍߡߊ߲", + "rcfilters-target-page-placeholder": "ߞߐߜߍ ߕߐ߮ ߟߊߘߏ߲߬ (ߥߟߊ߫ ߦߌߟߡߊ)", "rcnotefrom": "ߘߎ߰ߟߊ ߘߐ߫ {{PLURAL:$5|is the change|are the changes}} ߞߊ߬ߦߌ߯ $3, $4 (up to $1 shown).", + "rclistfromreset": "ߞߐߜߍ ߓߊߕߐߡߐ߲ߠߌ߲ ߡߊߦߟߍ߬ߡߊ߲߫", "rclistfrom": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߫ ߦߌ߬ߘߊ ߘߊߡߌ߬ߣߊ߬ ߣߌ߲߭ ߡߊ߬ $2, $3", "rcshowhideminor": "$1 ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߘߋ߬ߣߍ߲", "rcshowhideminor-show": "ߊ߬ ߦߌ߬ߘߊ߬", @@ -947,9 +1126,14 @@ "rcshowhideanons-show": "ߦߌ߬ߘߊ߬ߟߌ", "rcshowhideanons-hide": "ߊ߬ ߢߡߊߘߏ߲߰", "rcshowhidepatr": "$1 ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ߣߍ߲߫ ߞߣߐ߬ߜߍ߲߬ߣߍ߲ ߠߎ߬", + "rcshowhidepatr-show": "ߊ߬ ߦߌ߬ߘߊ߬", + "rcshowhidepatr-hide": "ߊ߬ ߢߡߊߘߏ߲߰", "rcshowhidemine": "ߒ ߠߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߣߍ߲ ߠߎ߬ $1", "rcshowhidemine-show": "ߊ߬ ߦߌ߬ߘߊ߬", "rcshowhidemine-hide": "ߊ߬ ߦߡߊߘߏ߲߰", + "rcshowhidecategorization": "$1 ߞߐߜߍ ߦߌߟߡߊߦߊߟߌ", + "rcshowhidecategorization-show": "ߊ߬ ߦߌ߬ߘߊ߬", + "rcshowhidecategorization-hide": "ߊ߬ ߢߡߊߘߏ߲߰", "rclinks": "ߕߋ߬ߟߋ $2 ߕߊ߬ߡߌ߲߬ߣߍ߲ ߣߌ߲߬ ߡߝߊ߬ߟߋ߲߬ߠߌ߲߬ ߟߊ߬ߓߊ߲ $1 ߦߌ߬ߘߊ߬", "diff": "ߝߘߏ߬ߢߐ߲߰ߡߊ", "hist": "ߞߊ߬ߞߘߐ", @@ -959,6 +1143,9 @@ "newpageletter": "ߞ", "boteditletter": "ߓ", "rc-change-size-new": "$1 {{PLURAL:$1|ߝߌ߬ߘߊ߲|ߝߌ߬ߘߊ߲ ߠߎ߬}} ߢߟߊߞߎߘߦߊ ߞߐ߫", + "newsectionsummary": "/* $1 */ ߞߣߐߘߐ߫ ߞߎߘߊ߫", + "rc-enhanced-expand": "ߝߊߙߊ߲ߝߊ߯ߛߌ ߟߎ߬ ߦߌ߬ߘߊ߬", + "rc-enhanced-hide": "ߝߊߙߊ߲ߝߊ߯ߛߌ ߟߎ߬ ߢߡߊߘߏ߲߰", "rc-old-title": "ߊ߬ ߓߊߞߘߐ ߟߊߘߊ߲߫ ߣߍ߲߫ ߦߋ߫ ߕߊ߲߬ ߠߋ߫ \"$1\"", "recentchangeslinked": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߜߋ߲߬ߞߘߎ߬ߢߐ߲߰ߡߊ ߟߎ߬", "recentchangeslinked-toolbox": "ߢߟߊߞߎߘߦߊߟߌ߫ ߜߋ߲߬ߞߘߎ߬ߡߊ ߟߎ߬", @@ -966,9 +1153,63 @@ "recentchangeslinked-summary": "ߞߐߜߍ ߕߐ߮ ߟߊߘߏ߲߬߸ ߞߊ߬ ߞߐߜߍ ߛߘߌ߬ߜߋ߲ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߦߋ߫߸ ߥߟߊ߫ \nߞߊ߬ ߝߘߊ߫ ߞߐߜߍ ߣߌ߲߬ ߠߊ߫. (ߖߐ߲߬ߛߊ߬ ߌ ߘߌ߫ ߦߌߟߡߊ ߛߌ߲߬ߝߏ߲ ߠߎ߬ ߦߋ߫߸ ߣߌ߲߬ ߠߊߘߏ߲߬ {{ns:category}}: ߦߌߟߡߊ ߕߐ߮). ߦߟߍ߬ߡߊ߲߬ ߡߍ߲ ߦߋ߫ ߞߐߜߍ ߣߌ߲߬ [[Special:Watchlist|your Watchlist]] ߘߐ߫߸ ߏ߬ ߦߋ߫ ߛߓߍߘߋ߲߫ ߞߎ߲ߓߊ ߟߋ߬ ߘߐ߫.", "recentchangeslinked-page": "ߞߐߜߍ ߕߐ߮:", "recentchangeslinked-to": "ߞߐߜߍ ߛߘߌ߬ߜߋ߲ ߠߎ߬ ߦߌ߬ߘߊ߬߸ ߞߊ߬ ߞߐߜߍ ߣߌ߬ ߞߋߟߋ߲ߘߌ߫", + "recentchanges-page-added-to-category": "[[:$1]] ߓߘߊ߫ ߟߊߘߏ߲߬ ߦߌߟߡߊ ߘߐ߫", + "recentchanges-page-removed-from-category": "[[:$1]] ߛߋ߲߬ߓߐ߫ ߦߌߟߡߊ ߘߐ߫", + "autochange-username": "ߡߋߘߌߦߊ߫-ߥߞߌ ߞߍߒߖߘߍߦߋ߫ ߡߊߦߟߍߡߊ߲ߠߌ߲", "upload": "ߞߐߕߐ߮ ߟߊߦߟߍ", + "uploadbtn": "ߞߐߕߐ߮ ߟߊߦߟߍ߬", + "reuploaddesc": "ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߬ ߊ߬ ߣߌ߫ ߞߵߌ ߞߐߛߊ߬ߦߌ߬ ߟߊ߬ߦߟߍ߬ߟߌ ߖߙߎߡߎ߲ ߘߐ߫", + "uploadnologin": "ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲߬ ߕߍ߫", + "uploadnologintext": "ߖߊ߰ߣߌ߲߫ $1 ߞߊ߬ ߞߐߕߐ߮ ߟߎ߬ ߟߊߦߟߍ߬.", "uploadlogpage": "ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߘߏ߫ ߟߊߦߟߍ߬", + "filename": "ߞߐߕߐ߮ ߕߐ߮", "filedesc": "ߟߊߘߛߏߣߍ߲", + "fileuploadsummary": "ߟߊ߬ߘߛߏ߬ߟߌ:", + "filereuploadsummary": "ߞߐߕߐ߮ ߡߊߦߟߍ߬ߡߊ߲:", + "filesource": "ߛߎ߲:", + "empty-file": "ߌ ߣߊ߬ ߞߐߕߐ߮ ߡߍ߲ ߞߙߊߓߊ߫ ߟߊ߫߸ ߊ߬ ߘߐߞߏߟߏ߲ ߠߋ߬ ߕߘߍ߬.", + "file-too-large": "ߌ ߟߊ߫ ߞߐߕߐ߮ ߞߙߊߓߊߣߍ߲ ߓߏ߲߬ߓߊ߫ ߕߘߍ߬ ߞߏߖߎ߰߹", + "filename-tooshort": "ߞߐߕߐ߮ ߕߐ߮ ߛߘߎ߬ߡߊ߲߬ ߞߏߖߎ߰.", + "filetype-banned": "ߞߐߕߐ߮ ߛߎ߯ߦߊ ߟߊߕߐ߲ߣߍ߲߫ ߠߋ߬.", + "verification-error": "ߞߐߕߐ߮ ߣߌ߲߬ ߡߊ߫ ߕߊ߬ߡߌ߲߬ ߞߐߕߐ߮ ߝߛߍ߬ߝߛߍ߬ ߦߌߟߊ.", + "hookaborted": "ߌ ߕߘߍ߬ ߦߋ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߍ߲ ߞߍ߫ ߞߏ ߘߐ߫߸ ߊ߬ ߘߐߛߊ߬ߣߍ߲߬ ߦߋ߫ ߘߐ߬ߥߙߊ߬ߟߌ ߟߋ߬ ߓߟߏ߫.", + "illegal-filename": "ߞߐߕߐ߮ ߕߐ߮ ߟߊߘߌ߬ߢߍ߬ߣߍ߲߬ ߕߍ߫.", + "uploadwarning": "ߟߊ߬ߦߟߍ߬ߟߌ ߖߊ߬ߛߙߋ߬ߡߊ߬ߟߊ", + "uploadwarning-text": "ߞߐߕߐ߮ ߘߎ߰ߟߊ߬ߘߐ߫ ߞߊ߲ߛߓߍߟߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߫ ߖߊ߰ߣߌ߲߬߸ ߞߵߊ߬ ߡߊߝߍߣߍ߲߫ ߕߎ߲߯.", + "savefile": "ߞߐߕߐ߮ ߟߊߞߎ߲߬ߘߎ߬", + "upload-source": "ߞߐߕߐ߮ ߛߎ߲", + "sourceurl": "URL ߛߎ߲:", + "destfilename": "ߞߐߕߐ߮ ߕߐ߮ ߞߎ߲߬ߕߋߟߋ߲:", + "upload-maxfilesize": "ߞߐߕߐ߮ ߢߊ߲ߞߊ߲ ߞߐߘߊ߲: $1", + "upload-description": "ߞߐߕߐ߮ ߞߊ߲߬ߛߓߍߟߌ", + "upload-options": "ߟߊ߬ߦߟߍ߬ߟߌ ߢߣߊߕߊߟߌ", + "watchthisupload": "ߞߐߕߐ߮ ߣߌ߲߬ ߘߐߜߍ߫", + "upload-dialog-title": "ߞߐߕߐ߮ ߟߊߦߟߍ߬", + "upload-dialog-button-cancel": "ߊ߬ ߘߐߛߊ߬", + "upload-dialog-button-back": "ߌ ߞߐߛߊ߬ߦߌ߬", + "upload-dialog-button-done": "ߊ߬ ߓߘߊ߫ ߞߍ߫", + "upload-dialog-button-save": "ߊ߬ ߟߊߞߎ߲߬ߘߎ߬", + "upload-dialog-button-upload": "ߟߊ߬ߦߟߍ߬ߟߌ", + "upload-form-label-infoform-title": "ߝߊߙߊ߲ߝߊ߯ߛߟߌ", + "upload-form-label-infoform-name": "ߕߐ߮", + "upload-form-label-usage-title": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ", + "upload-form-label-usage-filename": "ߞߐߕߐ߮ ߕߐ߮", + "upload-form-label-own-work": "ߒ ߖߘߍ߬ߞߊ߬ߣߌ߲߬ ߓߊ߯ߙߊ ߟߋ߬", + "upload-form-label-infoform-categories": "ߦߌߟߡߊ ߟߎ߬", + "upload-form-label-infoform-date": "ߕߎ߬ߡߊ߬ߘߊ", + "upload-form-label-own-work-message-generic-local": "ߒ ߧߴߊ߬ ߟߊߛߙߋߦߊ߫ ߟߊ߫ ߞߏ߫ ߒ ߧߋ߫ ߞߐߕߐ߮ ߣߌ߲߬ ߠߊߦߟߍ߬ ߞߊ߲߬ ߞߊ߬ ߓߍ߲߬ ߗߋߘߊ ߛߙߊߕߌ ߣߌ߫ ߕߌ߰ߦߊ ߤߊߞߍ ߡߊ߬ {{SITENAME}} ߞߊ߲߬", + "backend-fail-delete": "ߞߐߕߐ߮ ߕߴߛߋ߫ ߖߏ߰ߛߌ߬ ߟߊ߫ \"$1\".", + "backend-fail-describe": "ߡߋߕߊߘߕߊ ߞߐߕߐ߮ ߕߴߛߋ߫ ߡߊߦߟߍ߬ߡߊ߲߫ ߠߊ߫ \"$1\".", + "backend-fail-store": "ߞߐߕߐ߮ \"$1\" ߕߍ߫ ߛߐ߲߬ ߟߊߡߙߊ߬ ߟߊ߫ ߦߊ߲߬ \"$2\"", + "backend-fail-copy": "ߊ߬ ߕߍ߫ ߣߊ߬ ߛߐ߲߬ ߠߊ߫ ߞߐߕߐ߮ \"$1\" ߓߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫ ߦߊ߲߬ \"$2\".", + "img-auth-nofile": "ߞߐߕߐ߮ \"$1\" ߕߍ߫ ߦߋ߲߬.", + "http-request-error": "HTTP ߡߊ߬ߢߌ߬ߣߌ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫ ߝߎ߬ߕߎ߲߬ߕߌ߬ ߡߊߟߐ߲ߓߊߟߌ ߘߏ߫ ߞߏߛߐ߲߬.", + "http-read-error": "HTTP ߘߐ߬ߞߊ߬ߙߊ߲߬ߠߌ߲ ߝߎ߬ߕߎ߲߬ߕߌ.", + "http-timed-out": "HTTP ߡߊ߬ߢߌ߬ߣߌ߲߬ߠߌ߲ ߕߎ߬ߡߊ ߓߘߊ߫ ߕߊ߬ߡߌ߲߬.", + "http-curl-error": "URL: $1 ߕߌߙߌ߲ߠߌ߲ ߝߎ߬ߕߎ߲߬ߕߌ", + "http-bad-status": "ߝߙߋߞߋ ߕߘߍ߬ ߦߋ߫ ߦߋ߲߬ HTTP ߡߊߢߌߣߌ߲ߠߌ߲: $1 $2 ߘߐ߫", + "http-internal-error": "HTTP ߞߣߐߟߊ ߘߐ߫ ߝߎߕߎ߲ߕߌ.", + "upload-curl-error6": "ߌ ߕߍ߫ ߣߊ߬ URL ߡߊߛߐ߬ߘߐ߲߬ ߠߊ߫", "license": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߴߌ ߘߐ߫:", "license-header": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߴߌ ߘߐ߫", "imgfile": "ߞߐߕߐ߮", diff --git a/languages/i18n/pl.json b/languages/i18n/pl.json index 2e9a11de41..6cac408062 100644 --- a/languages/i18n/pl.json +++ b/languages/i18n/pl.json @@ -265,7 +265,7 @@ "history": "Historia strony", "history_short": "historia", "history_small": "historia", - "updatedmarker": "zmienione od ostatniej wizyty", + "updatedmarker": "zmienione od twojej ostatniej wizyty", "printableversion": "Wersja do druku", "permalink": "Link do tej wersji", "print": "Drukuj", @@ -3894,6 +3894,16 @@ "restrictionsfield-help": "Jeden adres IP lub zakres CIDR w wierszu. Aby zaznaczyć wszystkie, użyj:
0.0.0.0/0\n::/0
", "edit-error-short": "Błąd: $1", "edit-error-long": "Błędy:\n\n$1", + "specialmute": "Ignoruj", + "specialmute-success": "Twoje preferencje ignorowania zostały pomyślnie zaktualizowane. Zobacz wszystkich ignorowanych użytkowników w [[Special:Preferences|preferencjach]].", + "specialmute-submit": "Potwierdź", + "specialmute-label-mute-email": "Ignoruj e-maile od tego użytkownika", + "specialmute-header": "Wybierz swoje preferencje ignorowania dla {{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "Pożądana nazwa użytkownika nie została odnaleziona.", + "specialmute-error-email-blacklist-disabled": "Ignorowanie e-maili od użytkowników nie jest włączone.", + "specialmute-error-email-preferences": "Musisz potwierdzić swój adres e-mail zanim będziesz {{GENDER:|mógł|mogła}} ignorować użytkownika. Możesz to zrobić w [[Special:Preferences|preferencjach]].", + "specialmute-email-footer": "[$1 Zarządzaj preferencjami ignorowania dla {{BIDI:$2}}.]", + "specialmute-login-required": "Zaloguj się, aby zmienić swoje preferencje wyignorowania.", "revid": "wersja $1", "pageid": "ID strony: $1", "interfaceadmin-info": "$1\n\nUprawnienia do edycji plików CSS/JS/JSON całej witryny zostały wydzielone z dotychczasowego uprawnienia editinterface. Jeżeli nie rozumiesz, dlaczego otrzymujesz ten komunikat, przeczytaj [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/pt-br.json b/languages/i18n/pt-br.json index dff37f4ceb..175e0b4020 100644 --- a/languages/i18n/pt-br.json +++ b/languages/i18n/pt-br.json @@ -285,7 +285,7 @@ "history": "Histórico da página", "history_short": "Histórico", "history_small": "histórico", - "updatedmarker": "atualizado desde a minha última visita", + "updatedmarker": "atualizado desde sua última visita", "printableversion": "Versão para impressão", "permalink": "Ligação permanente", "print": "Imprimir", @@ -3959,6 +3959,16 @@ "restrictionsfield-help": "Um endereço IP ou intervalo CIDR por linha. Para ativar tudo, use\n
0.0.0.0/0\n::/0
", "edit-error-short": "Erro: $1", "edit-error-long": "Erros:\n$1", + "specialmute": "Silenciar", + "specialmute-success": "Suas preferências de silêncio foram atualizadas com sucesso. Ver todos os usuários silenciados em [[Special:Preferences]].", + "specialmute-submit": "Confirmar", + "specialmute-label-mute-email": "Silenciar e-mails deste usuário", + "specialmute-header": "Por favor, selecione suas preferências de mudo para {{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "O nome de usuário solicitado não foi encontrado.", + "specialmute-error-email-blacklist-disabled": "O silenciamento de usuários do envio de e-mails não está ativado.", + "specialmute-error-email-preferences": "Você deve confirmar seu endereço de e-mail antes de poder silenciar um usuário. Você pode fazer isso de [[Special:Preferences]].", + "specialmute-email-footer": "[$1 Gerenciar preferências de email para {{BIDI:$2}}.]", + "specialmute-login-required": "Por favor, entre para alterar suas preferências de mudo.", "revid": "revisão $1", "pageid": "ID da página $1", "interfaceadmin-info": "$1\n\nAs permissões para edição de arquivos CSS/JS/JSON em todo o site foram separadas recentemente do direito editinterface. Se você não entende porque está recebendo este erro, veja [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/pt.json b/languages/i18n/pt.json index 4c7b336ceb..f765ef41a4 100644 --- a/languages/i18n/pt.json +++ b/languages/i18n/pt.json @@ -245,7 +245,7 @@ "history": "Histórico", "history_short": "Histórico", "history_small": "histórico", - "updatedmarker": "atualizado desde a minha última visita", + "updatedmarker": "atualizado desde a sua última visita", "printableversion": "Versão para impressão", "permalink": "Hiperligação permanente", "print": "Imprimir", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 507bbfd042..63ff3759d6 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -4404,6 +4404,16 @@ "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword).", "edit-error-short": "Error message. Parameters:\n* $1 - the error details\nSee also:\n* {{msg-mw|edit-error-long}}\n{{Identical|Error}}", "edit-error-long": "Error message. Parameters:\n* $1 - the error details\nSee also:\n* {{msg-mw|edit-error-short}}\n{{Identical|Error}}", + "specialmute": "The name of the special page [[Special:Mute]].", + "specialmute-success": "The content of [[Special:Mute]] with a successful message indicating that your mute preferences have been updated after the form has been submitted.", + "specialmute-submit": "Submit button on [[Special:Mute]] form.\n{{Identical|Confirm}}", + "specialmute-label-mute-email": "Label for the checkbox that mutes/unmutes emails from the specified user.", + "specialmute-header": "Used as header text on [[Special:Mute]]. Shown before the form with the muting options.\n* $1 - User selected for muting", + "specialmute-error-invalid-user": "Error displayed when the username cannot be found.", + "specialmute-error-email-blacklist-disabled": "Error displayed when email blacklist is not enabled.", + "specialmute-error-email-preferences": "Error displayed when the user has not confirmed their email address.", + "specialmute-email-footer": "Email footer linking to [[Special:Mute]] preselecting the sender to manage muting options.\n* $1 - Url linking to [[Special:Mute]].\n* $2 - The user sending the email.", + "specialmute-login-required": "Error displayed when a user tries to access [[Special:Mute]] before logging in.", "revid": "Used to format a revision ID number in text. Parameters:\n* $1 - Revision ID number.\n{{Identical|Revision}}", "pageid": "Used to format a page ID number in text. Parameters:\n* $1 - Page ID number.", "interfaceadmin-info": "Part of the error message shown when someone with the editinterface right but without the appropriate editsite* right tries to edit a sitewide CSS/JSON/JS page.", diff --git a/languages/i18n/roa-tara.json b/languages/i18n/roa-tara.json index fe264b6563..cdf5c9c814 100644 --- a/languages/i18n/roa-tara.json +++ b/languages/i18n/roa-tara.json @@ -177,7 +177,7 @@ "history": "Storie d'a pàgene", "history_short": "Cunde", "history_small": "cunde", - "updatedmarker": "aggiornate da l'urtema visita meje", + "updatedmarker": "aggiornate da l'urtema visita toje", "printableversion": "Versione ca se stambe", "permalink": "Collegamende ca remane pe sembre", "print": "Stambe", @@ -388,6 +388,7 @@ "virus-scanfailed": "condrolle fallite (codece $1)", "virus-unknownscanner": "antivirus scanusciute:", "logouttext": "'''Tu tè scollegate.'''\n\nNote Bbuene ca certe pàggene ponne condinuà a essere viste cumme ce tu ste angore collegate, fine a quanne a cache d'u browser no se sdevache.", + "logging-out-notify": "Ste isse, aspitte.", "logout-failed": "Non ge puè assè mò: $1", "cannotlogoutnow-title": "Non ge puè assè mò", "cannotlogoutnow-text": "Non ge puè assè quanne ste ause $1.", @@ -1657,6 +1658,10 @@ "uploadstash-bad-path-unknown-type": "Tipe scanusciute \"$1\".", "uploadstash-bad-path-unrecognized-thumb-name": "Nome d'a miniature non acchiate.", "uploadstash-bad-path-bad-format": "'A chiave \"$1\" non ge ste jndr'à 'nu formate appropriate.", + "uploadstash-file-not-found-no-thumb": "No ge se pò avè 'a miniature.", + "uploadstash-file-not-found-no-local-path": "Nisciune percorse locale pa vôsce in scale.", + "uploadstash-file-not-found-no-object": "Non ge pozze ccrejà 'nu oggette file locale pa miniature.", + "uploadstash-file-not-found-no-remote-thumb": "Recupere d'a miniature schiute a male: $1\nURL = $2", "uploadstash-no-extension": "L'estenzione jè vacande.", "uploadstash-zero-length": "'U file tène lunghezze zero.", "invalid-chunk-offset": "distanze d'u chunk invalide", @@ -2374,6 +2379,10 @@ "blocklist-userblocks": "Scunne le blocche sus a le cunde de l'utinde", "blocklist-tempblocks": "Scunne le blocche temboranèe", "blocklist-addressblocks": "Scunne le blocche de le IP singole", + "blocklist-type": "Tipe:", + "blocklist-type-opt-all": "Tutte", + "blocklist-type-opt-sitewide": "Tutte 'u site", + "blocklist-type-opt-partial": "Parziale", "blocklist-rangeblocks": "Scunne le indervalle de blocche", "blocklist-timestamp": "Orarie de stambe", "blocklist-target": "Destinazione", @@ -2400,6 +2409,7 @@ "blocklink": "blocche", "unblocklink": "sblocche", "change-blocklink": "cange 'u blocche", + "empty-username": "(nisciune nome utende disponibbele)", "contribslink": "condrebbute", "emaillink": "manne 'n'e-mail", "autoblocker": "Autobloccate purcè l'indirizze IP tune ha state ausate urtemamende da \"[[User:$1|$1]]\".\n'U mutive date pu blocche de $1 ète \"$2\"", @@ -2419,6 +2429,7 @@ "block-log-flags-hiddenname": "nome de l'utende scunnute", "range_block_disabled": "L'abbilità de le amministrature de ccrejà blocche a indervalle jè disabbilitate.", "ipb_expiry_invalid": "L'orarije de scadenze non g'è valide.", + "ipb_expiry_old": "L'ore de scadenza jè jndr'à 'u passate.", "ipb_expiry_temp": "Le blocche sus a le nome de l'utinde scunnute onna essere permanende.", "ipb_hide_invalid": "Non ge se pò scangellà stu cunde utende; tène cchiù de troppe {{PLURAL:$1|'nu cangiamede|$1 cangiaminde}}.", "ipb_already_blocked": "\"$1\" jè ggià blocchete", @@ -3576,6 +3587,9 @@ "linkaccounts-submit": "Colleghe le cunde", "unlinkaccounts": "Scolleghe le cunde", "unlinkaccounts-success": "'U cunde ha state scollegate.", + "specialmute": "Citte", + "specialmute-submit": "Conferme", + "specialmute-error-invalid-user": "'U nome utende rechieste non g'è state acchiate.", "revid": "revisione $1", "pageid": "ID d'a pàgene $1", "rawhtml-notallowed": "Le tag <html> non ge ponne essere ausate fore da le pàggene normale.", @@ -3583,5 +3597,6 @@ "gotointerwiki-invalid": "'U titole specificate non g'è valide.", "pagedata-bad-title": "Titole invalide: $1.", "passwordpolicies-group": "Gruppe", - "passwordpolicies-policies": "Politeche" + "passwordpolicies-policies": "Politeche", + "userlogout-continue": "Vue ccù isse?" } diff --git a/languages/i18n/ru.json b/languages/i18n/ru.json index 43629e37de..7136fb7924 100644 --- a/languages/i18n/ru.json +++ b/languages/i18n/ru.json @@ -308,7 +308,7 @@ "history": "История", "history_short": "История", "history_small": "история", - "updatedmarker": "обновлено после моего последнего посещения", + "updatedmarker": "обновлено после вашего последнего посещения", "printableversion": "Версия для печати", "permalink": "Постоянная ссылка", "print": "Печать", @@ -784,7 +784,7 @@ "autoblockedtext": "Ваш IP-адрес автоматически заблокирован в связи с тем, что он ранее использовался кем-то из участников, заблокированных администратором $1. \nБыла указана следующая причина блокировки:\n\n: «$2».\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n* Цель блокировки: $7\n\nВы можете связаться с $1 или любым другим [[{{MediaWiki:Grouppage-sysop}}|администратором]], чтобы обсудить блокировку.\n\nОбратите внимание, что вы не сможете использовать функцию «{{int:emailuser}}», если в своих [[Special:Preferences|персональных настройках]] не задали или не подтвердили корректный адрес электронной почты, или если ваша блокировка включает запрет отправки писем подобным образом.\n\nВаш IP-адрес — $3, идентификатор блокировки — #$5.\nПожалуйста, указывайте эти сведения в любых своих обращениях.", "systemblockedtext": "Ваше имя участника или IP-адрес были автоматически заблокированы MediaWiki.\nУказана следующая причина:\n\n:$2\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n* Цель блокировки: $7\n\nВаш текущий IP-адрес $3.\nПожалуйста, указывайте все эти сведения в любых своих обращениях.", "blockednoreason": "причина не указана", - "blockedtext-composite": "Ваше имя участника или IP-адрес были заблокированы.\nУказана следующая причина:\n\n:$2\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n\nВаш текущий IP-адрес $3.\nПожалуйста, указывайте все эти сведения в любых своих обращениях.", + "blockedtext-composite": "Ваше имя участника или IP-адрес были заблокированы.\n\nУказана следующая причина:\n\n:$2\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n\nВаш текущий IP-адрес $3.\nПожалуйста, указывайте все эти сведения в любых своих обращениях.", "blockedtext-composite-reason": "Есть несколько блокировок вашей учётной записи и/или IP-адреса", "whitelistedittext": "Вы должны $1 для изменения страниц.", "confirmedittext": "Вы должны подтвердить свой адрес электронной почты перед правкой страниц.\nПожалуйста, введите и подтвердите свой адрес электронной почты в своих [[Special:Preferences|персональных настройках]].", @@ -4005,6 +4005,15 @@ "restrictionsfield-help": "По одному IP-адресу или CIDR-диапазону в строке. Чтобы разрешить всё, используйте:
0.0.0.0/0\n::/0
", "edit-error-short": "Ошибка: $1", "edit-error-long": "Ошибки:\n\n$1", + "specialmute": "Откл. уведомления", + "specialmute-success": "Изменения были успешно сделаны. Просмотрите всех отключённых участников на [[Special:Preferences]].", + "specialmute-submit": "Подтвердить", + "specialmute-label-mute-email": "Отключить эл. почту от этого участника", + "specialmute-header": "Пожалуйста, выберите настройки уведомлений от {{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "Указанное вами имя участника не может быть найдено.", + "specialmute-error-email-preferences": "Вы должны подтвердить вашу электронную почту, прежде чем отключить уведомление от других. Это можно сделать на странице [[Special:Preferences]].", + "specialmute-email-footer": "[$1 Управление настройками эл. почты для {{BIDI:$2}}.]", + "specialmute-login-required": "Пожалуйста, войдите, чтобы совершить изменения.", "revid": "версия $1", "pageid": "ID страницы $1", "interfaceadmin-info": "$1\n\nПрава на редактирование общесайтных CSS/JS/JSON-файлов были недавно вынесены из права editinterface. Если вы не понимаете, почему вы наткнулись на эту ошибку, см. [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/rue.json b/languages/i18n/rue.json index 0171ad6094..7cc15a530c 100644 --- a/languages/i18n/rue.json +++ b/languages/i18n/rue.json @@ -329,7 +329,7 @@ "protectedinterface": "Тота сторінка є частёв інтрефейсу проґрамового забеспечіня той вікі і єй можуть едітовати лем адміністраторы проєкту.\nЖебы придати або змінити переклады, просиме хоснуйте [https://translatewiki.net/ translatewiki.net], локалізачный проєкт MediaWiki.", "editinginterface": "Позірь: Едітуєте сторінку, котра є частинов текстового інтерфейсу.\nЗміны той сторінкы выкличуть зміну інтерфейсу про іншых хоснователїв той вікі.", "translateinterface": "Додати ці змінити переклады на вшыткых вікі просиме хоснуйте [https://translatewiki.net/ translatewiki.net] — проєкт, што ся занимать локалізаціов MediaWiki.", - "cascadeprotected": "Сторінка є замнкута, бо є вложена до {{PLURAL:$1|наслїдуючой сторінкы замкнуты|наслїдуючіх сторінок замнкнутых|наслїдуючіх сторінок замнкнутых}} каскадовым замком:\n$2", + "cascadeprotected": "Сторінка є замкнута, бо є вложена до {{PLURAL:$1|наслїдуючой сторінкы замкнуты|наслїдуючіх сторінок замкнутых}} „каскадовым“ замкнутям:\n$2", "namespaceprotected": "Не маєте права едітовати сторінкы в просторї назв «$1».", "customcssprotected": "Не маєте права едітовати тоту сторінку з CSS, бо обсягує персоналны наставлїна іншого хоснователя.", "customjsonprotected": "Не маєте права едітовати тоту сторінку з JSON, бо обсягує персоналны наставлїна іншого хоснователя.", @@ -341,7 +341,7 @@ "mypreferencesprotected": "Не мате дозволїня мінити свої наставлїня.", "ns-specialprotected": "Шпеціалны сторінкы не є можне едітовати.", "titleprotected": "Створїня сторінкы з таков назвов было заборонене хоснователём [[User:$1|$1]] з причінов: $2.", - "filereadonlyerror": "Не годно змінити файл „$1“, бо архів файлів „$2“ є теперь лем на чітаня.\n\nАдміністратор сервера, котрый архів заблоковав, додав тото пояснїня: „''$3''“.", + "filereadonlyerror": "Не было можно змінити файл „$1“, бо архів файлів „$2“ є теперь лем на чітаня.\n\nАдміністратор сервера, котрый архів заблоковав, додав тото пояснїня: „$3“.", "invalidtitle": "Неприпустна назва", "invalidtitle-knownnamespace": "Непряавилна назва в просторї назв „$2“ і текстом „$3“", "invalidtitle-unknownnamespace": "Неправилна назва з незнамым чіслом простору назв $1 і текстом „$2“", @@ -404,7 +404,7 @@ "nocookieslogin": "{{SITENAME}} хоснує cookies про приголошіня хоснователїв. Вы маєте cookies выпнуты. Просиме Вас, повольте їх і спобуйте знова.", "nocookiesfornew": "Конто хоснователя не было створене, бо сьме не были годны потвердити ёго походжіня.\nУтвердите ся, же маєте дозволены cookies, обновте тоту сторінку і спробуйте то знову.", "noname": "Мусите увести мено свого конта.", - "loginsuccesstitle": "Успішне приголошіня", + "loginsuccesstitle": "Приголошеный(а)", "loginsuccess": "'''Теперь працуєте {{grammar:locative|{{SITENAME}}}} під меном $1.'''", "nosuchuser": "Не екзістує хоснователь з меном «$1».\nУ хосновательскых мен ся розлишують малы/великы писмена.\nСконтролюйте запис, або собі [[Special:CreateAccount|зареґіструйте нове конто]].", "nosuchusershort": "Хоснователь з меном $1 не екзістує.\nПеревірте правилность написаня мена.", diff --git a/languages/i18n/sh.json b/languages/i18n/sh.json index 48bf8f8ca9..48fcb81d57 100644 --- a/languages/i18n/sh.json +++ b/languages/i18n/sh.json @@ -316,7 +316,7 @@ "databaseerror-query": "Upit: $1", "databaseerror-function": "Funkcija: $1", "databaseerror-error": "Greška: $1", - "transaction-duration-limit-exceeded": "Da se izbjegne veliko zaostajanje replikacije, transakcija je prekinuta zato što je trajanje zapisivanja ($1) prekoračilo ograničenje od {{PLURAL:$2|jedne sekunde|$2 sekunde|$2 sekundi}}.\nAko mijenjate mnogo stavki odjednom, uradite to u više navrata.", + "transaction-duration-limit-exceeded": "Da se izbjegne veliko zaostajanje odgovorâ, transakcija je prekinuta zato što prekoračeno je trajanje zapisivanja ($1), ograničeno {{PLURAL:$2|jednom sekundom|$2 sekunde|$2 sekundi}}.\nAko mijenjate više stavki odjednom, uradite ovo na više navrata umjesto zajednički.", "laggedslavemode": "'''Upozorenje''': Stranica ne mora sadržavati posljednja ažuriranja.", "readonly": "Baza podataka je zaključana", "enterlockreason": "Unesite razlog za zaključavanje, uključujući procjenu vremena otključavanja", @@ -658,6 +658,8 @@ "autoblockedtext": "Vaša IP adresa je automatski blokirana jer je korištena od strane drugog korisnika, a blokirao ju je $1.\nNaveden je slijedeći razlog:\n\n:$2\n\n* Početak blokade: $8\n* Kraj blokade: $6\n* Blokirani korisnik: $7\n\nMožete kontaktirati $1 ili nekog drugog iz grupe [[{{MediaWiki:Grouppage-sysop}}|administratora]] i zahtijevati da Vas deblokira.\n\nZapamtite da ne možete koristiti opciju \"{{int:emailuser}}\" ukoliko nije unesena validna e-mail adresa u [[Special:Preferences|Vašim postavkama]] te Vas ne spriječava ga je koristite.\n\nVaša trenutna IP adresa je $3, a ID blokade je $5.\nMolimo da navedete sve gore navedene detalje u zahtjevu za deblokadu.", "systemblockedtext": "MediaWiki je automatski blokirao Vaše korisničko ime ili IP-adresu.\nDat je sljedeći razlog:\n\n:$2\n\n* Početak bloka: $8\n* Istek bloka: $6\n* Blok je namijenjen za: $7\n\nVaša trenutna IP-adresa je $3.\nPrepišite sve gorenavedene pojedinosti ukoliko želite da vlasti pitaju za blok.", "blockednoreason": "razlog nije naveden", + "blockedtext-composite": "Vaše korisničko ime ili IP-adresa je blokirano.\n\nDat je sljedeći razlog:\n\n:$2.\n\n* Početak bloka: $8\n* Istek najdužeg bloka: $6\n\nVaša trenutna IP-adresa je $3.\nPrepišite sve gorenavedene pojedinosti ukoliko želite da vlasti pitaju za blok.", + "blockedtext-composite-reason": "Vaš račun i/ili IP adresa ima nekoliko blokova", "whitelistedittext": "Da bi ste uređivali stranice, morate se $1.", "confirmedittext": "Morate potvrditi Vašu e-mail adresu prije nego počnete mijenjati stranice.\nMolimo da postavite i verifikujete Vašu e-mail adresu putem Vaših [[Special:Preferences|korisničkih opcija]].", "nosuchsectiontitle": "Ne mogu pronaći sekciju", @@ -669,7 +671,7 @@ "accmailtext": "Nasumično odabrana šifra za [[User talk:$1|$1]] je poslata na adresu $2.\n\nŠifra/lozinka za ovaj novi račun može biti promijenjena na stranici ''[[Special:ChangePassword|izmjene šifre]]'' nakon prijave.", "newarticle": "(Novi)", "newarticletext": "Preko linka ste došli na stranicu koja još uvijek ne postoji.\n* Ako želite stvoriti stranicu, počnite tipkati u okviru dolje (v. [$1 stranicu za pomoć] za više informacija).\n* Ukoliko ste došli greškom, pritisnike dugme '''Nazad''' ('''back''') na vašem pregledniku.", - "anontalkpagetext": "----''Ovo je stranica za razgovor za anonimnog korisnika koji još nije napravio račun ili ga ne koristi.\nZbog toga moramo da koristimo brojčanu IP adresu kako bismo identifikovali njega ili nju.\nTakvu adresu može dijeliti više korisnika.\nAko ste anonimni korisnik i mislite da su vam upućene nebitne primjedbe, molimo Vas da [[Special:CreateAccount|napravite račun]] ili se [[Special:UserLogin|prijavite]] da biste izbjegli buduću zabunu sa ostalim anonimnim korisnicima.''", + "anontalkpagetext": "----\nOvo je razgovorna stranica s anonimnim korisnikom koji još nije napravio račun ili ga ne koristi.\nZbog toga moramo da koristimo brojčanu IP adresu kako bismo identifikovali njega ili nju.\nTakvu adresu može dijeliti više korisnika.\nAko ste anonimni korisnik i mislite da su vam upućene nebitne primjedbe, molimo Vas da [[Special:CreateAccount|napravite račun]] ili se [[Special:UserLogin|prijavite]] da biste izbjegli buduću zabunu sa ostalim anonimnim korisnicima.", "noarticletext": "Na ovoj stranici trenutno nema teksta.\nMožete [[Special:Search/{{PAGENAME}}|tražiti naslov ove stranice]] u drugim stranicama,\n[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretraživati srodne registre],\nili [{{fullurl:{{FULLPAGENAME}}|action=edit}} napraviti ovu stranicu].", "noarticletext-nopermission": "Trenutno nema teksta na ovoj stranici.\nMožete [[Special:Search/{{PAGENAME}}|tražiti ovaj naslov stranice]] na drugim stranicama ili [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} pretražiti povezane registre]. alio nemate dozvolu za stvaranje ove stranice.", "missing-revision": "Ne mogu da pronađem izmenu br. $1 na stranici pod nazivom „{{FULLPAGENAME}}“.\n\nOvo se obično dešava kada pratite zastarjelu vezu do stranice koja je obrisana.\nViše informacija možete pronaći u [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} evidenciji brisanja].", @@ -711,7 +713,7 @@ "copyrightwarning2": "Zapamtite da svaki doprinos na stranici {{SITENAME}} može biti izmijenjen, promijenjen ili uklonjen od strane ostalih korisnika. Ako ne želite da ovo desi sa Vašim tekstom, onda ga nemojte slati ovdje.
\nTakođer nam garantujete da ste ovo Vi napisali, ili da ste ga kopirali iz javne domene ili sličnog slobodnog izvora informacija (pogledajte $1 za više detalja).\n'''NE ŠALJITE DJELA ZAŠTIĆENA AUTORSKIM PRAVOM BEZ DOZVOLE!'''", "editpage-cannot-use-custom-model": "Model sadržaja ove stranice se ne može promijeniti.", "longpageerror": "'''Greška: tekst koji ste uneli je veličine {{PLURAL:$1|jedan kilobajt|$1 kilobajta|$1 kilobajta}}, što je veće od {{PLURAL:$2|dozvoljenog jednog kilobajta|dozvoljena $2 kilobajta|dozvoljenih $2 kilobajta}}.'''\nStranica ne može biti sačuvana.", - "readonlywarning": "Upozorenje: baza podataka je zaključana radi održavanja, tako da trenutno nećete moći da sačuvate izmene.\nMožda biste želeli sačuvati tekst za kasnije u nekoj tekstualnoj datoteci.\n\nAdministrator koji je zaključao bazu dao je sledeće objašnjenje: $1", + "readonlywarning": "Upozorenje: baza podataka je zaključana radi održavanja, i stoga trenutno nećete moći da sačuvate izmjene.\n\nPreporučujemo Vam prekopirati tekst na strani i sačuvati ga za kasnije.\n\nAdministrator koji je zaključao bazu dao je sledeće objašnjenje: $1", "protectedpagewarning": "'''PAŽNJA: Ova stranica je zaključana tako da samo korisnici sa administratorskim privilegijama mogu da je mijenjaju.'''\nPosljednja stavka u registru je prikazana ispod kao referenca:", "semiprotectedpagewarning": "Pažnja: Ova stranica je zaključana tako da je samo automatski potvrđeni korisnici mogu uređivati.\nPosljednja stavka registra je prikazana ispod kao referenca:", "cascadeprotectedwarning": "Upozorenje: Ova stranica je zaključana tako da je samo korisnici sa [[Special:ListGroupRights|određenim pravima]] mogu mijenjati, jer je ona uključena u {{PLURAL:$1|sljedeću, prenosivo zaštićenu stranicu|sljedeće, prenosivo zaštićene stranice}}:", @@ -738,6 +740,9 @@ "edit-gone-missing": "Stranica se nije mogla osvježiti.\nIzgleda da je obrisana.", "edit-conflict": "Sukob izmjena.", "edit-no-change": "Vaša izmjena je ignorirana, jer nije bilo promjena teksta stranice.", + "edit-slots-cannot-add": "{{PLURAL:$1|Sljedeći slot ovdje nije podržan|Sljedeći slotovi ovdje nisu podržani}}: $2.", + "edit-slots-cannot-remove": "{{PLURAL:$1|Sljedeći slot je obavezan i ne može da se ukloni|Sljedeći slotovi su obavezni i ne mogu da se uklone}}: $2.", + "edit-slots-missing": "{{PLURAL:$1|Sljedeći slot nedostaje|Sljedeći slotovi nedostaju}}: $2.", "postedit-confirmation-created": "Stranica je stvorena.", "postedit-confirmation-restored": "Stranica je obnovljena.", "postedit-confirmation-saved": "Vaša izmjena je snimljena.", @@ -2857,7 +2862,7 @@ "anonymous": "{{PLURAL:$1|Anonimni korisnik|$1 anonimna korisnika|$1 anonimnih korisnika}} projekta {{SITENAME}}", "siteuser": "{{SITENAME}} korisnik $1", "anonuser": "{{SITENAME}} anonimni korisnik $1", - "lastmodifiedatby": "Ovu stranicu je posljednji put promjenio $3, u $2, $1", + "lastmodifiedatby": "Ovu stranicu je posljednji put {{GENDER:$4|uredio|uredila}} $3 u $2 na datum $1.", "othercontribs": "Bazirano na radu od strane korisnika $1.", "others": "ostali", "siteusers": "{{SITENAME}} {{PLURAL:$2|{{GENDER:$1|korisnik}}|korisnika}} $1", @@ -3156,7 +3161,7 @@ "version-poweredby-others": "ostali", "version-poweredby-translators": "translatewiki.net prevodioci", "version-credits-summary": "Htjeli bismo da zahvalimo sljedećim osobama na njihovom doprinosu [[Special:Version|MediaWiki]].", - "version-license-info": "Mediawiki je slobodni softver, možete ga redistribuirati i/ili mijenjati pod uslovima GNU opće javne licence kao što je objavljeno od strane Fondacije Slobodnog Softvera, bilo u verziji 2 licence, ili (po vašoj volji) nekoj od kasniji verzija.\n\nMediawiki se distriburia u nadi da će biti korisna, ali BEZ IKAKVIH GARANCIJA, čak i bez ikakvih posrednih garancija o KOMERCIJALNOSTI ili DOSTUPNOSTI ZA ODREĐENU SVRHU. Pogledajte GNU opću javnu licencu za više detalja.\n\nTrebali biste dobiti [{{SERVER}}{{SCRIPTPATH}}/KOPIJU GNU opće javne licence] zajedno s ovim programom, ako niste, pišite Fondaciji Slobodnog Softvera na adresu Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ili je pročitajte [//www.gnu.org/licenses/old-licenses/gpl-2.0.html online].", + "version-license-info": "Mediawiki je slobodni softver; možete ga redistribuirati i/ili mijenjati pod uslovima GNU-ove opće javne licence Fondacije slobodnog softvera; ili u verziji 2 Licence, ili nekoj od kasniji verzija (po vašoj volji).\n\nMediaWiki se nudi u nadi da će biti korisna, ali BEZ IKAKVIH GARANCIJA; čak i bez podrazumjevane garancije o KOMERCIJALNOSTI ili PRIKLADNOSTI ZA ODREĐENU SVRHU. Za više informacija, pogledajte GNU-ovu opću javnu licencu.\n\nZajedno s ovim programom trebali biste dobiti [{{SERVER}}{{SCRIPTPATH}}/COPYING primjerak GNU-ove opće javne licence]; ako niste dobili primjerak, pišite na Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA ili je [//www.gnu.org/licenses/old-licenses/gpl-2.0.html pročitajte ovdje].", "version-software": "Instalirani softver", "version-software-product": "Proizvod", "version-software-version": "Verzija", @@ -3739,6 +3744,16 @@ "restrictionsfield-help": "Jedna IP-adresa ili CIDR-opseg po redu. Da omogućite sve, koristite
0.0.0.0/0
::/0", "edit-error-short": "Greška: $1", "edit-error-long": "Greške:\n\n$1", + "specialmute": "Iskl. obavještenja", + "specialmute-success": "Promjene su uspješno napravljene. Pogledajte sve isključene korisnike na [[Special:Preferences]].", + "specialmute-submit": "Potvrdi / Потврди", + "specialmute-label-mute-email": "Isključi e-poštu od ovog korisnika", + "specialmute-header": "Izaberite postavke za obavještenja od {{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "Nisam mogao naći korisničko ime.", + "specialmute-error-email-blacklist-disabled": "Isključavanje e-pošte od korisnika nije omogućeno.", + "specialmute-error-email-preferences": "Morat ćete potvrditi svoju e-poštu prije isključivanja obavijesti od drugih. To je učinjeno na stranici [[Special:Preferences]].", + "specialmute-email-footer": "[$1 Upravljanje postavkama e-pošte od {{BIDI:$2}}.]", + "specialmute-login-required": "Molimo Vas prijavite se da biste napravili promjene.", "revid": "izmjena $1", "pageid": "ID stranice $1", "interfaceadmin-info": "$1\n\nDozvole za uređivanje CSS/JS/JSON datoteka preko cijelog wikija nedavno su odvojene od prava editinterface. Ako ne razumijete zašto ste dobili ovu grešku, pogl. [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/sk.json b/languages/i18n/sk.json index 6b462ae33b..1606205c94 100644 --- a/languages/i18n/sk.json +++ b/languages/i18n/sk.json @@ -651,7 +651,7 @@ "subject-preview": "Náhľad predmetu:", "previewerrortext": "Pri pokuse o zobrazenie náhľadu došlo k chybe.", "blockedtitle": "Používateľ je zablokovaný", - "blockedtext": "'''Vaše používateľské meno alebo IP adresa bola zablokovaná.'''\n\nZablokoval vás správca $1. Udáva tento dôvod:
''$2''\n\n* Blokovanie začalo: $8\n* Blokovanie vyprší: $6\n* Kto mal byť zablokovaný: $7\n\nMôžete kontaktovať $1 alebo s jedného z ďalších [[{{MediaWiki:Grouppage-sysop}}|správcov]] a prediskutovať blokovanie.\nUvedomte si, že nemôžete použiť funkciu „{{int:Emailuser}}“, pokiaľ nemáte registrovanú platnú e-mailovú adresu vo svojich [[Special:Preferences|nastaveniach]].\nVaša IP adresa je $3 a ID blokovania je #$5.\nProsím, uveďte oba tieto údaje do každej správy, ktorú posielate.", + "blockedtext": "Vaše používateľské meno alebo IP adresa bola zablokovaná.\n\nZablokoval vás správca $1.\nUdáva tento dôvod: $2.\n\n* Blokovanie začalo: $8\n* Blokovanie vyprší: $6\n* Kto mal byť zablokovaný: $7\n\nAk chcete prediskutovať blokovanie, kontaktujte $1 alebo iného [[{{MediaWiki:Grouppage-sysop}}|správcu]].\nFunkciu „{{int:emailuser}}“ môžete použiť, iba ak máte registrovanú platnú e-mailovú adresu vo svojich [[Special:Preferences|nastaveniach]] a jej použitie nebolo zablokované.\nVaša súčasná IP adresa je $3 a ID blokovania je #$5.\nProsím, uveďte všetky tieto údaje do každej správy, ktorú posielate.", "autoblockedtext": "Vaša IP adresa bola automaticky zablokovaná, pretože ju používa iný používateľ, ktorého zablokoval $1.\nUdaný dôvod zablokovania:\n\n:''$2''\n\n* Blokovanie začalo: $8\n* Blokovanie vyprší: $6\n* Blokovanie sa týka: $7\n\nAk potrebujete informácie o blokovaní, môžete kontaktovať $1 alebo niektorého iného\n[[{{MediaWiki:Grouppage-sysop}}|správcu]].\n\nPozn.: Nemôžete použiť funkciu „{{int:emailuser}}“, ak ste si vo svojich\n[[Special:Preferences|používateľských nastaveniach]] nezaregistrovali platnú e-mailovú adresu.\n\nVaša aktuálna IP adresa je $3. ID vášho blokovania je $5.\nProsím, uveďte tieto podrobnosti v akýchkoľvek otázkach, ktoré sa opýtate.", "systemblockedtext": "Vaša IP adresa bola automaticky zablokovaná.\nUdaný dôvod zablokovania:\n\n:$2\n\n* Blokovanie začalo: $8\n* Blokovanie vyprší: $6\n* Blokovanie sa týka: $7\n\nVaša aktuálna IP adresa je $3.\nProsím, uveďte tieto podrobnosti v akýchkoľvek otázkach, ktoré sa opýtate.", "blockednoreason": "nebol uvedený dôvod", @@ -666,7 +666,7 @@ "accmailtext": "Náhodne vytvorené heslo pre používateľa [[User talk:$1|$1]] bolo poslané na $2. Je možné ho zmeniť na stránke ''[[Special:ChangePassword|zmena hesla]]'' po prihlásení.", "newarticle": "(Nový)", "newarticletext": "Nasledovali ste odkaz, vedúci na stránku, ktorá zatiaľ neexistuje.\nStránku vytvoríte tak, že začnete písať do políčka nižšie (viac informácií nájdete na stránkach [$1 nápovedy]).\nAk ste sa sem dostali nechtiac, kliknite na tlačidlo späť vo svojom prehliadači.", - "anontalkpagetext": "----\nToto je diskusná stránka anonymného používateľa, ktorý nemá vytvorené svoje konto alebo ho nepoužíva.\nPreto musíme na jeho identifikáciu použiť numerickú IP adresu. Je možné, že takúto IP adresu používajú viacerí používatelia.\nAk ste anonymný používateľ a máte pocit, že vám boli adresované irelevantné diskusné príspevky, [[Special:CreateAccount|vytvorte si konto]] alebo sa [[Special:UserLogin|prihláste]], aby sa zamedzilo budúcim zámenám s inými anonymnými používateľmi.", + "anontalkpagetext": "----\nToto je diskusná stránka anonymného používateľa, ktorý ešte nemá vytvorené svoje konto alebo ho nepoužíva.\nPreto musíme na jeho identifikáciu použiť numerickú IP adresu.\nJe možné, že túto IP adresu používajú viacerí používatelia.\nAk ste anonymný používateľ a máte pocit, že vám boli adresované irelevantné diskusné príspevky, [[Special:CreateAccount|vytvorte si konto]] alebo sa [[Special:UserLogin|prihláste]], aby sa zamedzilo budúcim zámenám s inými anonymnými používateľmi.", "noarticletext": "Na tejto stránke sa momentálne nenachádza žiadny text.\nMôžete [[Special:Search/{{PAGENAME}}|vyhľadávať názov tejto stránky]] v obsahu iných stránok,\n[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} vyhľadávať v súvisiacich záznamoch] alebo [{{fullurl:{{FULLPAGENAME}}|action=edit}} vytvoriť túto stránku].", "noarticletext-nopermission": "Táto stránka momentálne neobsahuje žiadny text.\nMôžete [[Special:Search/{{PAGENAME}}|hľadať názov tejto stránky]] v texte iných stránok\nalebo [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} hľadať v súvisiacich záznamoch], ale nemáte oprávnenie túto stránku vytvoriť.", "missing-revision": "Revízia #$1 stránky s názvom „{{FULLPAGENAME}}“ neexistuje.\n\nPravdepodobne ste nasledovali zastaraný odkaz na historickú verziu stránky, ktorá bola medzičasom odstránená.\nPodrobnosti nájdete v [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} zázname zmazaní].", @@ -792,7 +792,7 @@ "page_first": "prvá", "page_last": "posledná", "histlegend": "Porovnanie zmien: označte výberové políčka revízií, ktoré sa majú porovnať a kliknite na tlačidlo dolu.
\nLegenda: (aktuálna) = rozdiel oproti aktuálnej verzii,\n(posledná) = rozdiel oproti predchádzajúcej verzii, D = drobná úprava", - "history-fieldset-title": "Prechádzať históriou", + "history-fieldset-title": "Filtrovať revízie", "history-show-deleted": "Iba zmazané", "histfirst": "najstaršie", "histlast": "najnovšie", @@ -1702,7 +1702,7 @@ "filehist-comment": "komentár", "imagelinks": "Použitie súboru", "linkstoimage": "Na tento súbor {{PLURAL:$1|odkazuje nasledujúca stránka|odkazujú nasledujúce $1 stránky|odkazuje nasledujúcich $1 stránok}}:", - "linkstoimage-more": "Viac ako $1 {{PLURAL:$1|stránka odkazuje|stránky odkazujú|stránok odkazuje}} na tento súbor.\nNasledovný zoznam zobrazuje {{PLURAL:$1|prvú stránku odkazujúcu|prvé $1 stránky odkazujúce|prvých $1 stránok odkazujúcich}} iba na tento súbor.\nMôžete si pozrieť [[Special:WhatLinksHere/$2|úplný zoznam]].", + "linkstoimage-more": "Viac ako $1 {{PLURAL:$1|stránka odkazuje|stránky odkazujú|stránok odkazuje}} na tento súbor.\nNasledovný zoznam zobrazuje {{PLURAL:$1|prvú stránku odkazujúcu|prvé $1 stránky odkazujúce|prvých $1 stránok odkazujúcich}} iba na tento súbor.\nK dispozícii je aj [[Special:WhatLinksHere/$2|úplný zoznam]].", "nolinkstoimage": "Žiadne stránky neobsahujú odkazy na tento súbor.", "morelinkstoimage": "Zobraziť [[Special:WhatLinksHere/$1|ďalšie odkazy]] na tento súbor.", "linkstoimage-redirect": "$1 (presmerovanie súboru) $2", diff --git a/languages/i18n/sl.json b/languages/i18n/sl.json index fad8fb838e..da11bf7c2e 100644 --- a/languages/i18n/sl.json +++ b/languages/i18n/sl.json @@ -181,7 +181,7 @@ "history": "Zgodovina strani", "history_short": "Zgodovina", "history_small": "zgodovina", - "updatedmarker": "Posodobljeno od mojega zadnjega obiska", + "updatedmarker": "posodobljeno od vašega zadnjega obiska", "printableversion": "Različica za tisk", "permalink": "Trajna povezava", "print": "Tisk", diff --git a/languages/i18n/sq.json b/languages/i18n/sq.json index add19763e9..fc0d936009 100644 --- a/languages/i18n/sq.json +++ b/languages/i18n/sq.json @@ -85,6 +85,7 @@ "tog-norollbackdiff": "Mos trego ndrysh pas kryerjes së një rikthkimi", "tog-useeditwarning": "Më paralajmëro kur lë një redaktim faqeje me ndryshime të paruajtura", "tog-prefershttps": "Gjithmonë përdorni një lidhje të sigurt kur të kyçur", + "tog-showrollbackconfirmation": "Shfaq një komandë konfirmimi kur shtyp mbi butonin e rikthimit të përgjithshëm.", "underline-always": "Gjithmonë", "underline-never": "Asnjëherë", "underline-default": "Parapërcaktuar nga shfletuesi", @@ -193,6 +194,7 @@ "returnto": "Kthehu tek $1", "tagline": "Nga {{SITENAME}}", "help": "Ndihmë", + "help-mediawiki": "Ndihmë rreth MediaWiki-t", "search": "Kërko", "searchbutton": "Kërko", "go": "Shko", @@ -200,7 +202,7 @@ "history": "Historiku i faqes", "history_short": "Historiku", "history_small": "historiku", - "updatedmarker": "përditësuar që nga vizita ime e fundit", + "updatedmarker": "përditësuar që nga vizita e fundit", "printableversion": "Versioni i printueshëm", "permalink": "Lidhje e përhershme", "print": "Printo", @@ -352,6 +354,7 @@ "badarticleerror": "Ky veprim nuk mund të bëhet në këtë faqe.", "cannotdelete": "Faqja ose skeda $1 nuk mund të fshihej.\nMund të jetë fshirë nga dikush tjetër.", "cannotdelete-title": "Faqja \"$1\" nuk mund të fshihet", + "delete-scheduled": "Faqja \"$1\" është përcaktuar për fshirje.\n\nJu lutemi, kini durim.", "delete-hook-aborted": "Fshirja u anulua nga togëza.\nNuk jipet shpjegim.", "no-null-revision": "I pamundur krijimi rishikimi i ri për faqen bosh \"$ 1\"", "badtitle": "Titull i pasaktë", @@ -381,14 +384,20 @@ "cascadeprotected": "Kjo faqe është mbrojtur nga redaktimi pasi që është përfshirë në {{PLURAL:$1|faqen|faqet}} e mëposhtme që {{PLURAL:$1|është|janë}} mbrojtur sipas metodës \"cascading\":\n$2", "namespaceprotected": "Nuk ju lejohet redaktimi i faqeve në hapsirën '''$1'''.", "customcssprotected": "Ju nuk keni leje për të redaktuar këtë faqe CSS, sepse ai përmban cilësimet personale tjetër user's.", + "customjsonprotected": "Ju nuk keni leje për ta redaktuar këtë faqe JSON sepse përmban të dhënat personale të një përdoruesi tjetër.", "customjsprotected": "Ju nuk keni leje për të redaktuar këtë faqe JavaScript, sepse ai përmban cilësimet personale tjetër user's.", + "sitecssprotected": "Ju nuk keni leje ta redaktoni këtë faqe CSS sepse ndryshimi mund të prekë të gjithë vizitorët.", + "sitejsonprotected": "Ju nuk keni leje ta redaktoni këtë faqe JSON sepse ndryshimi mund të prekë të gjithë vizitorët.", + "sitejsprotected": "Ju nuk keni leje ta redaktoni këtë faqe JavaScript sepse ndryshimi mund të prekë të gjithë vizitorët.", "mycustomcssprotected": "Ju nuk keni leje për të redaktuar këtë faqe CSS.", + "mycustomjsonprotected": "Ju nuk keni leje ta redaktoni këtë faqe JSON.", "mycustomjsprotected": "Ju nuk keni leje për të redaktuar këtë faqe JavaScript .", "myprivateinfoprotected": "Ti nuk ke leje për të redaktuar të dhënat e tua private.", "mypreferencesprotected": "Ti nuk ke leje për të ndryshuar preferencat e tua.", "ns-specialprotected": "Faqet speciale nuk mund të redaktohen.", "titleprotected": "Ky titull është mbrojtur nga [[User:$1|$1]] dhe nuk mund të krijohet.\nArsyeja e dhënë është $2.", "filereadonlyerror": "Nuk është në gjendje që të ndryshojë skedarin \"$1\" sepse depoja e skedarit \"$2\" është në formën vetëm-lexim.\n\nAdministratori sistemit i cili e mbylli atë e dha këtë shpjegim: \"$3\".", + "invalidtitle": "Titull i pavlefshëm", "invalidtitle-knownnamespace": "Titull jo i vlefshëm me hapësirën \"$2\" dhe teksti \"$3\"", "invalidtitle-unknownnamespace": "Titull jo i vlefshëm me numrin e panjohur të hapësirës së emrit $1 dhe tekstit \"$2\"", "exception-nologin": "I paqasur", @@ -660,7 +669,7 @@ "userpage-userdoesnotexist": "Llogaria e përdoruesit \"$1\" nuk është e regjistruar. \nJu lutem kontrolloni nëse dëshironi të krijoni/redaktoni këtë faqe.", "userpage-userdoesnotexist-view": "Llogaria i përdoruesit \"$1\" nuk është e regjistruar.", "blocked-notice-logextract": "Ky përdorues është aktualisht i bllokuar.\nMë poshtë mund t'i referoheni shënimit të regjistruar për bllokimin e fundit:", - "clearyourcache": "Shënim: Pas ruajtjes, juve mund t'iu duhet të anashkaloni \"cache-in\" e shfletuesit tuaj për të parë ndryshimet. \n* Firefox/Safari: Mbaj të shtypur Shift ndërkohë që klikon Reload, ose shtyp Ctrl-F5 ose Ctrl-R (⌘-R në Mac)\n* Google Chrome: Shtyp Ctrl-Shift-R ('⌘-R' në Mac)\n* Internet Explorer: Mbaj të shtypur Ctrl ndërkohë që klikon Refresh, ose shtyp Ctrl-F5 \n* Opera: Shkoni në Menu → Settings (Opera → Preferences në Mac) dhe pastaj në Privacy & security → Clear browsing data → Cached images and files.", + "clearyourcache": "Shënim: Pas ruajtjes, mund t'iu duhet të pastroni kashenë e shfletuesit tuaj për të parë ndryshimet. \n* Firefox/Safari: Mbaj të shtypur Shift ndërkohë që shtyp Reload, ose shtyp Ctrl-F5 ose Ctrl-R (⌘-R në Mac).\n* Google Chrome: Shtyp Ctrl-Shift-R ('⌘-R' në Mac).\n* Internet Explorer: Mbaj të shtypur Ctrl ndërkohë që shtyp Refresh, ose shtyp Ctrl-F5. \n* Opera: Shkoni në Menu → Settings (Opera → Preferences në Mac) dhe pastaj në Privacy & security → Clear browsing data → Cached images and files.", "usercssyoucanpreview": "'''Këshillë:''' Përdorni butonin '{{int:showpreview}}' për të testuar CSS-në e re para se të ruani ndryshimet e kryera.", "userjsyoucanpreview": "'''Këshillë:''' Përdorni butonin '{{int:showpreview}}' për të testuar JavaScripting e ri para se të ruani ndryshimet e kryera.", "usercsspreview": "Vini re! Ju jeni duke inspektuar CSS-në si përdorues!\nNuk është ruajtur ende!", diff --git a/languages/i18n/sr-ec.json b/languages/i18n/sr-ec.json index f30aa1ccdc..6872a0ff95 100644 --- a/languages/i18n/sr-ec.json +++ b/languages/i18n/sr-ec.json @@ -206,7 +206,7 @@ "history": "Историја странице", "history_short": "Историја", "history_small": "историја", - "updatedmarker": "ажурирано од моје последње посете", + "updatedmarker": "ажурирано од ваше последње посете", "printableversion": "Верзија за штампање", "permalink": "Трајна веза", "print": "Штампање", @@ -339,6 +339,7 @@ "databaseerror-query": "Упит: $1", "databaseerror-function": "Функција: $1", "databaseerror-error": "Грешка: $1", + "transaction-duration-limit-exceeded": "Због избегавања великих заостајања репликације, ова трансакција је прекинута због тога што је трајање записивања ($1) премашило $2 секунди ограничења. \nУколико мењате више ставки одједном, правите ово на више мањих операција.", "laggedslavemode": "Упозорење: страница можда не садржи недавна ажурирања.", "readonly": "База података је закључана", "enterlockreason": "Унесите разлог за закључавање, укључујући и време откључавања", @@ -450,7 +451,7 @@ "userlogout": "Одјава", "notloggedin": "Нисте пријављени", "userlogin-noaccount": "Немате налог?", - "userlogin-joinproject": "Придружите се пројекту {{SITENAME}}", + "userlogin-joinproject": "Придружите се пројекту", "createaccount": "Отварање налога", "userlogin-resetpassword-link": "Заборавили сте лозинку?", "userlogin-helplink2": "Помоћ при пријављивању", @@ -1428,7 +1429,7 @@ "rcfilters-restore-default-filters": "Врати подразумеване филтере", "rcfilters-clear-all-filters": "Обришите све филтере", "rcfilters-show-new-changes": "Нове промене од $1", - "rcfilters-search-placeholder": "Филтрирајте промене (користите мени или претрагу за име филтера)", + "rcfilters-search-placeholder": "Филтрирајте промене (користите мени или претражите име филтера)", "rcfilters-invalid-filter": "Неважећи филтер", "rcfilters-empty-filter": "Нема активних филтера. Сви доприноси су приказани.", "rcfilters-filterlist-title": "Филтери", diff --git a/languages/i18n/sv.json b/languages/i18n/sv.json index 695f57d2ee..af4bd1eca3 100644 --- a/languages/i18n/sv.json +++ b/languages/i18n/sv.json @@ -82,7 +82,8 @@ "Nirmos (Wikimedia)", "Psl85", "Sturban", - "Taylor" + "Taylor", + "Mjälten" ] }, "tog-underline": "Stryk under länkar:", @@ -246,7 +247,7 @@ "history": "Sidhistorik", "history_short": "Historik", "history_small": "historik", - "updatedmarker": "uppdaterad sedan senaste besöket", + "updatedmarker": "uppdaterad sedan ditt senaste besök", "printableversion": "Utskriftsvänlig version", "permalink": "Permanent länk", "print": "Skriv ut", @@ -1565,8 +1566,8 @@ "rcfilters-view-tags-tooltip": "Filtrera resultat med redigeringsmärken", "rcfilters-view-return-to-default-tooltip": "Återvänd till huvudfiltreringsmenyn", "rcfilters-view-tags-help-icon-tooltip": "Läs mer om taggade redigeringar", - "rcfilters-liveupdates-button": "Liveuppdateringar", - "rcfilters-liveupdates-button-title-on": "Stäng av liveuppdateringar", + "rcfilters-liveupdates-button": "Realtidsuppdateringar", + "rcfilters-liveupdates-button-title-on": "Stäng av uppdateringar i realtid", "rcfilters-liveupdates-button-title-off": "Visa nya ändringar när de händer", "rcfilters-watchlist-markseen-button": "Markera alla ändringar som sedda", "rcfilters-watchlist-edit-watchlist-button": "Redigera din lista över bevakade sidor", @@ -1712,7 +1713,7 @@ "uploaded-setting-href-svg": "Användning av taggen \"set\" för att lägga till attributen \"href\" till överordnade element blockeras.", "uploaded-wrong-setting-svg": "Användning av \"set\"-taggen för att lägga till ett remote-/data-/skriptmål till något attribut är blokerat. Hittade <set to=\"$1\"> i den uppladdade SVG-filen.", "uploaded-setting-handler-svg": "SVG som anger \"handler\"-attributet med remote/data/skript är blockerat. Hittade $1=\"$2\" i den uppladdade SVG-filen.", - "uploaded-remote-url-svg": "SVG som anger style-attributet med en fjärr-URL är blockerat. Hittade $1=\"$2\" i den uppladdade SVG-filen.", + "uploaded-remote-url-svg": "SVG som anger stilattributet med en fjärr-URL är blockerat. Hittade $1=\"$2\" i den uppladdade SVG-filen.", "uploaded-image-filter-svg": "Hittade bildfilter med URL: <$1 $2=\"$3\"> i den uppladdade SVG-filen.", "uploadscriptednamespace": "Denna SVG-fil innehåller den ogiltiga namnrymden \"$1\".", "uploadinvalidxml": "XML-koden i den uppladdade filen kunde inte tolkas.", @@ -3844,6 +3845,16 @@ "restrictionsfield-help": "En IP-adress eller CIDR-intervall per rad. För att aktivera allting, använd
0.0.0.0/0
::/0", "edit-error-short": "Fel: $1", "edit-error-long": "Fel:\n\n$1", + "specialmute": "Tyst", + "specialmute-success": "Dina tystnadsinställningar har uppdateras. Se alla tystade användare i [[Special:Preferences|inställningarna]].", + "specialmute-submit": "Bekräfta", + "specialmute-label-mute-email": "Tysta e-post från denna användare", + "specialmute-header": "Välj dina tystnadsinställningar för {{BIDI:[[User:$1]]}}.", + "specialmute-error-invalid-user": "Det begärda användarnamnet kunde inte hittas.", + "specialmute-error-email-blacklist-disabled": "Att förhindra användare från att skicka e-post till dig har inte aktiverats.", + "specialmute-error-email-preferences": "Du måste bekräfta din e-postadress innan du kan tysta en användare. Du kan göra det i [[Special:Preferences|inställningarna]].", + "specialmute-email-footer": "[$1 Hantera e-postinställningar för {{BIDI:$2}}.]", + "specialmute-login-required": "Logga in för att ändra dina tystnadsinställningar.", "revid": "sidversion $1", "pageid": "sid-ID $1", "interfaceadmin-info": "$1\n\nBehörigheter för att redigera CSS/JS/JSON-filer för hela webbplatsen separerades nyligen från rättigheten editinterface. Om du inte förstår varför du får detta felmeddelande, se [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/tr.json b/languages/i18n/tr.json index ffce23c0a7..b89b619cda 100644 --- a/languages/i18n/tr.json +++ b/languages/i18n/tr.json @@ -266,7 +266,7 @@ "history": "Sayfa geçmişi", "history_short": "Geçmiş", "history_small": "geçmiş", - "updatedmarker": "son ziyaretimden sonra güncellenmiş", + "updatedmarker": "son ziyaretinizden bu yana güncellendi", "printableversion": "Yazdırılabilir sürüm", "permalink": "Kalıcı bağlantı", "print": "Yazdır", @@ -743,6 +743,8 @@ "autoblockedtext": "IP adresiniz otomatik olarak engellendi, çünkü $1 tarafından engellenmiş başka bir kullanıcı tarafından kullanılmaktaydı.\nBelirtilen sebep şudur:\n\n:$2\n\n* Engellemenin başlangıcı: $8\n* Engellemenin bitişi: $6\n* Bloke edilmesi istenen: $7\n\nEngelleme hakkında tartışmak için $1 ile veya diğer [[{{MediaWiki:Grouppage-sysop}}|hizmetlilerden]] biriyle irtibata geçebilirsiniz.\n\nNot, [[Special:Preferences|kullanıcı tercihlerinize]] geçerli bir e-posta adresi kaydetmediyseniz \"{{int:emailuser}}\" özelliğinden faydalanamayabilirsiniz ve bu özelliği kullanmaktan engellenmediniz.\n\nŞu anki IP numaranız $3 ve engellenme ID'niz #$5.\nLütfen yapacağınız herhangi bir sorguda yukarıdaki bütün detayları bulundurun.", "systemblockedtext": "Kullanıcı adınız veya IP adresiniz MediaWiki tarafından otomatik olarak engellendi.\nSebebi:\n\n:$2\n\n* Engelin başlangıcı: $8\n* Engelin süresi: $6\n* Engellenmesi istenen: $7\n\nMevcut IP adresiniz $3.\nLütfen yukarıdaki tüm ayrıntıları, yaptığınız sorgularda belirtin.", "blockednoreason": "sebep verilmedi", + "blockedtext-composite": "Kullanıcı adınız veya IP adresiniz engellendi.\n\nSebebi:\n\n:$2.\n\n* Engel başlama tarihi: $8\n* Engelin süresi: $6\n\nGeçerli IP adresiniz $3.\nLütfen yukarıdaki tüm detayları yaptığınız tüm sorgulara dahil ediniz.", + "blockedtext-composite-reason": "Hesabınızda ve/veya IP adresinizde birden fazla engel mevcut.", "whitelistedittext": "Değişiklik yapabilmek için $1.", "confirmedittext": "Sayfa değiştirmeden önce e-posta adresinizi onaylamalısınız. Lütfen [[Special:Preferences|tercihler]] kısmından e-postanızı ekleyin ve onaylayın.", "nosuchsectiontitle": "Bölüm bulunamadı", @@ -894,7 +896,7 @@ "revision-info": "$2 tarafından oluşturulmuş $1 tarihli sürüm $7", "previousrevision": "← Önceki hâli", "nextrevision": "Sonraki hâli →", - "currentrevisionlink": "En güncel hâli", + "currentrevisionlink": "Güncel sürüm", "cur": "gün", "next": "sonraki", "last": "son", @@ -998,6 +1000,7 @@ "mergehistory-fail-no-change": "Geçmiş birleştirme hiçbir sürümü birleştirmedi. Lütfen sayfa ve zaman parametrelerini bir kez daha kontrol edin.", "mergehistory-fail-permission": "Geçmiş birleştirmek için gerekli izinler yok.", "mergehistory-fail-self-merge": "Kaynak ve hedef sayfa aynı.", + "mergehistory-fail-timestamps-overlap": "Kaynak revizyonları çakışıyor veya hedef revizyonlarından sonra geliyor.", "mergehistory-fail-toobig": "Limit olarak belirlenen $1 {{PLURAL:$1|sürümden|sürümden}} daha fazlasını taşımak gerekeceği için geçmiş birleştirme gerçekleştirilemiyor.", "mergehistory-no-source": "Kaynak sayfa $1 bulunmamaktadır.", "mergehistory-no-destination": "Hedef sayfa $1 bulunmamaktadır.", @@ -1421,11 +1424,34 @@ "action-changetags": "tekil sürümlere veya günlük kayıtlarına etiket ekleme veya çıkarma", "action-deletechangetags": "etiketleri veritabanından sil", "action-purge": "bu sayfayı temizle", + "action-apihighlimits": "API sorgularında daha yüksek sınır kullan", + "action-autoconfirmed": "IP-tabanlı hız limitleri etkilenmez", + "action-bigdelete": "uzun tarihli sayfaları sil", + "action-blockemail": "bir kullanıcının e-posta göndermesini engelle", + "action-bot": "otomatik bir işlem gibi muamele gör", + "action-editprotected": "\"{{int:protect-level-sysop}}\" olarak korunan sayfalarda değişiklik yap", + "action-editsemiprotected": "\"{{int:protect-level-autoconfirmed}}\" olarak korunan sayfalarda değişiklik yap", "action-editinterface": "Kullanıcı arayüzünü değiştir", "action-editusercss": "Diğer kullanıcıların CSS sayfalarını değiştir", "action-edituserjson": "Diğer kullanıcıların JSON sayfalarını değiştir", "action-edituserjs": "Diğer kullanıcıların JavaScript sayfalarını değiştir", + "action-editsitecss": "sitewide CSS düzenle", + "action-editsitejson": "sitewide JSON'u düzenle", + "action-editsitejs": "sitewide JavaScript'i düzenle", + "action-editmyusercss": "kendi kullanıcı CSS dosyaları düzenle", + "action-editmyuserjson": "kendi kullanıcı JSON dosyalarını düzenle", + "action-editmyuserjs": "kendi kullanıcı JavaScript dosyalarını düzenle", + "action-viewsuppressed": "herhangi bir kullanıcıdan saklanan sürümleri göster", "action-hideuser": "Herkesten gizleyerek bir kullanıcı adını engelle", + "action-ipblock-exempt": "IP engellemelerini, otomatik engellemelerini ve aralık engellemelerini atla", + "action-unblockself": "kendi engellini kaldır", + "action-noratelimit": "derecelendirme sınırlamalarından etkilenme", + "action-reupload-own": "kendisi tarafından yüklenen mevcut dosyaların üzerine yaz", + "action-nominornewtalk": "kullanıcı tartışma sayfalarında yaptığı küçük değişiklikler kullanıcıya yeni mesaj bildirimiyle bildirilmez", + "action-markbotedits": "geri döndürülen değişikliklerini bot değişiklikleri olarak işaretle", + "action-patrolmarks": "son değişiklikler gözleme işaretlerini gör", + "action-override-export-depth": "sayfaları, derinlik 5'e kadar bağlantılı sayfalarla beraber dışa aktar", + "action-suppressredirect": "sayfaları taşırken kaynak sayfalardan yönlendirmeler oluşturma", "nchanges": "$1 {{PLURAL:$1|değişiklik|değişiklik}}", "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|son ziyaretten bu yana}}", "enhancedrc-history": "geçmiş", @@ -1548,11 +1574,16 @@ "rcfilters-filter-categorization-description": "Kategorilere eklenen veya kaldırılan sayfaların kayıtları.", "rcfilters-filter-logactions-label": "Günlüğü tutulan işlemler", "rcfilters-filter-logactions-description": "Hizmetli işlemleri, hesap oluşturmalar, sayfa silmeler, yüklemeler...", - "rcfilters-filtergroup-lastrevision": "En son sürümler", + "rcfilters-hideminor-conflicts-typeofchange-global": "\"Küçük değişiklikler\" filtresi, bir veya daha fazla değişiklik türü filtresiyle çakışıyor çünkü belirli değişiklik türleri \"küçük\" olarak belirlenemez. Çakışan filtreler yukarıdaki Aktif filtreler alanında işaretlenmiştir.", + "rcfilters-hideminor-conflicts-typeofchange": "Bazı değişiklik türleri \"küçük\" olarak belirlenemez, bu nedenle bu filtre şu değişiklik filtreleriyle çakışıyor: $1", + "rcfilters-typeofchange-conflicts-hideminor": "Bu değişiklik filtresi \"Küçük değişiklikler\" filtresiyle çakışıyor. Bazı değişiklik türleri \"küçük\" olarak tanımlanamaz.", + "rcfilters-filtergroup-lastrevision": "Güncel sürümler", "rcfilters-filter-lastrevision-label": "Son revizyon", "rcfilters-filter-lastrevision-description": "Bir sayfadaki en yeni değişiklik.", - "rcfilters-filter-previousrevision-label": "Son revizyon değil", + "rcfilters-filter-previousrevision-label": "Güncel sürüm değil", + "rcfilters-filter-previousrevision-description": "\"Güncel sürüm\" olmayan tüm değişiklikler", "rcfilters-filter-excluded": "Hariç", + "rcfilters-tag-prefix-namespace-inverted": ":not $1", "rcfilters-exclude-button-off": "Seçileni hariç tut", "rcfilters-exclude-button-on": "Seçilen hariç", "rcfilters-view-tags": "Etiketli düzenlemeler", @@ -1616,12 +1647,15 @@ "recentchangeslinked-page": "Sayfa adı:", "recentchangeslinked-to": "Belirtilen sayfadan verilenler yerine, sayfaya verilen bağlantıları göster.", "recentchanges-page-added-to-category": "[[:$1]] kategoriye eklendi", + "recentchanges-page-added-to-category-bundled": "[[:$1]] kategoriye eklendi, [[Special:WhatLinksHere/$1|bu sayfa diğer sayfalara dahil edildi]]", "recentchanges-page-removed-from-category": "[[:$1]] kategoriden çıkarıldı", + "recentchanges-page-removed-from-category-bundled": "[[:$1]] kategoriden kaldırıldı, [[Special:WhatLinksHere/$1|bu sayfa diğer sayfalara dahil edildi]]", "autochange-username": "MediaWiki otomatik değişimi", "upload": "Dosya yükle", "uploadbtn": "Dosya yükle", "reuploaddesc": "Yükleme formuna geri dön.", "upload-tryagain": "Değiştirilmiş dosya açıklamasını gönder", + "upload-tryagain-nostash": "Yeniden yüklenen dosyayı ve değiştirilen açıklamayı gönder", "uploadnologin": "Oturum açık değil", "uploadnologintext": "Dosya yükleyebilmek için $1manız gerekiyor.", "upload_directory_missing": "Yükleme dizini ($1) kayıp ve websunucusu tarafından oluşturulamıyor.", @@ -1674,11 +1708,14 @@ "file-thumbnail-no": "Bu dosyanın adı $1 ile başlıyor.\nBu başka bir resim küçültülmüş sürümüne benziyor ''(thumbnail)''\nEğer sizde bu resmin tam çöznürlükteki sürümü varsa onu yükleyin, aksi takdirde lütfen dosya adını değiştirin.", "fileexists-forbidden": "Bu isimde bir dosya zaten var, ve üzerine yazılamıyor.\nDosyanızı yinede yüklemek istiyorsanız, lütfen geri dönüp yeni bir isim kullanın. [[File:$1|thumb|center|$1]]", "fileexists-shared-forbidden": "Bu isimde bir dosya ortak havuzda zaten mevcut.\nDosyanızı yinede yüklemek istiyorsanız, lütfen geri gidip yeni bir isim kullanın. [[File:$1|thumb|center|$1]]", + "fileexists-no-change": "Yükleme, mevcut [[:$1]] sürümünün tam bir kopyasıdır.", + "fileexists-duplicate-version": "Yükleme, [[:$1]] dosyasının {{PLURAL:$2|eski bir sürümünün|eski sürümlerinin}} tam bir kopyasıdır.", "file-exists-duplicate": "Bu dosya aşağıdaki {{PLURAL:$1|dosyanın|dosyaların}} kopyasıdır:", "file-deleted-duplicate": "Bu dosyanın özdeşi olan başka bir dosya ([[:$1]]) daha önceden silindi. Bu dosyayı yeniden yüklemeden önce diğer dosyanın silme kayıtlarını kontrol etmelisiniz.", "file-deleted-duplicate-notitle": "Bu dosyaya eş bir dosya daha önceden silinmiş, ve başlık bastırılmış.\nDosyayı tekrar yüklemeye devam etmeden önce, bastırılmış dosya verisini görme yetkisine sahip birisine durumu gözden geçirmesini istemelisiniz.", "uploadwarning": "Yükleme uyarısı", "uploadwarning-text": "Lütfen aşağıdaki dosya açıklamasını değiştirin ve tekrar deneyin.", + "uploadwarning-text-nostash": "Lütfen dosyayı yeniden yükleyin, aşağıdaki açıklamayı değiştirin ve tekrar deneyin.", "savefile": "Dosyayı kaydet", "uploaddisabled": "Geçici olarak şu anda bu wiki'ye herhangi bir dosya yüklenemez. Lütfen daha sonra bir daha deneyiniz.", "copyuploaddisabled": "URL ile yükleme devre dışı.", @@ -1686,11 +1723,14 @@ "php-uploaddisabledtext": "PHP dosyası yüklemeleri devre dışıdır. Lütfen file_uploads ayarını kontrol edin.", "uploadscripted": "Bu dosya bir internet tarayıcısı tarafından hatalı çevrilebilecek bir HTML veya script kodu içermektedir.", "upload-scripted-pi-callback": "XML-stylesheet işleme talimatları içeren bir dosyalar yüklenemez.", + "upload-scripted-dtd": "Standart olmayan bir DTD bildirimi içeren SVG dosyaları yüklenemiyor.", "uploaded-script-svg": "Yüklenen SVG dosyasında komutlanabilir (scriptable) öğe bulundu: \"$1\"", "uploaded-hostile-svg": "Yüklenen SVG dosyasının \"style\" öğesinde güvensiz CSS bulundu.", "uploaded-event-handler-on-svg": "SVG dosyalarında event-handler özniteliğini $1=\"$2\" şeklinde ayarlanmasına izin verilmiyor.", "uploaded-href-unsafe-target-svg": "Yüklenen SVG dosyasında <$1 $2=\"$3\"> güvensiz hedefine href veri: URI bulundu.", "uploaded-animate-svg": "\"animate\" etiketi bulundu, href'i değiştiriyor olabilir. Yüklenen SVG dosyasındaki \"from\" özniteliği kullanılıyor <$1 $2=\"$3\">", + "uploaded-setting-event-handler-svg": "Olay işleyicisi özniteliklerini ayarlama engellenir, yüklenen SVG dosyasında <$1 $2=\"$3\"> bulundu.", + "uploaded-setting-href-svg": "Üst ögeye \"href\" özelliğini eklemek için \"set\" etiketinin kullanılması engellenir.", "uploadscriptednamespace": "Bu SVG dosyası geçersiz \"$1\" alan adını içermektedir.", "uploadinvalidxml": "Yüklenen dosyadaki XML işlenemedi.", "uploadvirus": "Bu dosya virüslüdür! Detayları: $1", @@ -2280,10 +2320,14 @@ "deleteprotected": "Bu sayfayı silemezsiniz çünkü sayfa korumaya alınmış.", "deleting-backlinks-warning": "'''Uyarı:''' Silmek üzere olduğunuz sayfaya [[Özel:SayfayaBağlantılar/{{FULLPAGENAME}}|başka sayfalardan]] bağlantılar var veya sayfanın bazı bölümleri başka sayfalar tarafından alıntı olarak kullanılıyor.", "rollback": "değişiklikleri geri al", + "rollback-confirmation-confirm": "Lütfen onaylayın:", + "rollback-confirmation-yes": "Geri döndürme", + "rollback-confirmation-no": "İptal", "rollbacklink": "geri döndür", "rollbacklinkcount": "$1 {{PLURAL:$1|değişikliği|değişikliği}} geri döndür", "rollbacklinkcount-morethan": "$1 {{PLURAL:$1|değişiklikten|değişiklikten}} daha fazlasını geri döndür", "rollbackfailed": "geri alma işlemi başarısız", + "rollback-missingparam": "İstek üzerine gerekli parametreler eksik.", "rollback-missingrevision": "Sürüm verisi yüklenemedi.", "cantrollback": "Sayfaya son katkıda bulunan kullanıcı, sayfaya katkıda bulunmuş tek kişi olduğu için, değişiklikler geri alınamıyor.", "alreadyrolled": "[[User:$2|$2]] ([[User talk:$2|Tartışma]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]) tarafından [[:$1]] sayfasında yapılmış son değişiklik geri döndürülemiyor;\nbaşka birisi sayfada değişiklik yaptı ya da sayfayı geri döndürdü.\n\nSon değişikliği yapan: [[User:$3|$3]] ([[User talk:$3|Tartışma]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).", @@ -2430,7 +2474,7 @@ "sp-contributions-blocked-notice-anon": "Bu IP adresi şu anda engellenmiş.\nSon engelleme günlüğü girdisi kaynak amacıyla aşağıda verilmiştir:", "sp-contributions-search": "Katkıları ara", "sp-contributions-username": "IP adresi veya kullanıcı adı:", - "sp-contributions-toponly": "Sadece son revizyon olan değişiklikleri göster", + "sp-contributions-toponly": "Sadece güncel sürüm olan değişiklikleri göster", "sp-contributions-newonly": "Yalnızca yeni sayfa oluşturan değişiklikleri görüntüle", "sp-contributions-hideminor": "Küçük değişiklikleri gizle", "sp-contributions-submit": "Ara", @@ -2505,6 +2549,10 @@ "blocklist-userblocks": "Hesap engellemelerini gizle", "blocklist-tempblocks": "Geçici engellemeleri gizle", "blocklist-addressblocks": "Tek IP engellemelerini gizle", + "blocklist-type": "Tür:", + "blocklist-type-opt-all": "Hepsi", + "blocklist-type-opt-sitewide": "Site genelinde", + "blocklist-type-opt-partial": "Kısmi", "blocklist-rangeblocks": "Dizi bloklarını gizle", "blocklist-timestamp": "Tarih", "blocklist-target": "Hedef", @@ -2892,7 +2940,7 @@ "markedaspatrollednotify": "$1 için bu değişiklik kontrol edildi olarak işaretlendi.", "markedaspatrollederrornotify": "Kontrol edildi olarak işaretleme başarısız oldu.", "patrol-log-page": "Devriye günlüğü", - "patrol-log-header": "Bu gözlenmiş revizyonların günlüğüdür.", + "patrol-log-header": "Bu onaylanmış sürümlerin günlüğüdür.", "confirm-markpatrolled-button": "TAMAM", "deletedrevision": "$1 sayılı eski sürüm silindi.", "filedeleteerror-short": "$1 dosyanın silinmesinde hata oldu", @@ -3509,7 +3557,7 @@ "mw-widgets-categoryselector-add-category-placeholder": "Bir kategori ekle...", "mw-widgets-usersmultiselect-placeholder": "Daha fazla ekle...", "date-range-from": "Şu tarihten:", - "date-range-to": "Bu güne kadar:", + "date-range-to": "Şu güne kadar:", "sessionprovider-mediawiki-session-cookiesessionprovider": "çerez tabanlı oturumlar", "sessionprovider-nocookies": "Çerezler devre dışı olabilir. Çerkezlerin aktif olduğuna emin olun ve yeniden başlatin.", "randomrootpage": "Rasgele kök sayfa", diff --git a/languages/i18n/uk.json b/languages/i18n/uk.json index 3537faac4d..dc556f2776 100644 --- a/languages/i18n/uk.json +++ b/languages/i18n/uk.json @@ -244,7 +244,7 @@ "history": "Історія сторінки", "history_short": "Історія", "history_small": "історія", - "updatedmarker": "оновлено після мого останнього перегляду", + "updatedmarker": "оновлено після Вашого останнього перегляду", "printableversion": "Версія до друку", "permalink": "Постійне посилання", "print": "Друк", @@ -722,6 +722,8 @@ "autoblockedtext": "Ваша IP-адреса автоматично заблокована у зв'язку з тим, що вона раніше використовувалася кимось із користувачів, якого заблокував $1.\nПричина блокування блокування:\n\n:$2\n\n* Початок блокування: $8\n* Закінчення блокування: $6\n* Блокування виконав: $7\n\nВи можете надіслати листа користувачеві $1 або будь-якому іншому [[{{MediaWiki:Grouppage-sysop}}|адміністратору]], щоб обговорити блокування.\n\nЗверніть увагу, що ви не зможете скористатися функцією \"{{int:emailuser}}\", так як не маєте дійсної електронної пошти, зареєстрованої в [[Special:Preferences|особистих налаштуваннях]], а також якщо вам було заборонено надсилати листи при блокуванні.\n\nВаша поточна IP-адреса — $3, ідентифікатор блокування — #$5. Будь ласка, зазначайте ці дані у своїх запитах.", "systemblockedtext": "Ваше ім'я користувача або IP-адресу було автоматично заблоковано MediaWiki.\nВказана причина:\n\n:$2\n\n* Початок блокування: $8\n* Закінчення блокування: $6\n* Ціль блокування: $7\n\nВаша поточна IP-адреса — $3.\nБудь ласка, додайте всі вказані подробиці до будь-яких запитів, які Ви будете робити.", "blockednoreason": "не вказано причини", + "blockedtext-composite": "Ваше ім'я користувача або IP-адресу було заблоковано.\n\nВказана причина:\n\n:$2.\n\n* Початок блокування: $8\n* Закінчення найдовшого блокування: $6\n\nВаша поточна IP-адреса — $3.\nБудь ласка, додайте всі вказані подробиці до будь-яких запитів, які Ви будете робити.", + "blockedtext-composite-reason": "Встановлено кілька блокувань для Вашого облікового запису та/або IP-адреси", "whitelistedittext": "Ви повинні $1, щоб редагувати сторінки.", "confirmedittext": "Ви повинні підтвердити вашу адресу електронної пошти перед редагуванням сторінок.\nБудь-ласка вкажіть і підтвердіть вашу електронну адресу на [[Special:Preferences|сторінці налаштувань]].", "nosuchsectiontitle": "Не вдається знайти розділ", diff --git a/languages/i18n/yue.json b/languages/i18n/yue.json index 0b3b932f85..1b0da3911b 100644 --- a/languages/i18n/yue.json +++ b/languages/i18n/yue.json @@ -666,7 +666,7 @@ "accmailtext": "「[[User talk:$1|$1]]」嘅隨機產生密碼已經寄咗去 $2。\n\n呢個密碼可以響簽到咗之後嘅[[Special:ChangePassword|改密碼]] 版度改佢。", "newarticle": "(新)", "newarticletext": "你連連過嚟嘅頁面重未存在。\n要起版新嘅,請你喺下面嗰格度輸入。(睇睇[$1 自助版]拎多啲資料。)\n如果你係唔覺意嚟到呢度,撳一次你個瀏覽器'''返轉頭'''個掣。", - "anontalkpagetext": "----\n呢度係匿名用戶嘅討論頁,佢可能係重未開戶口,或者佢重唔識開戶口。\n我哋會用數字表示嘅IP地址嚟代表佢。\n一個IP地址係可以由幾個用戶夾來用。\n如果你係匿名用戶,同覺得呢啲留言係同你冇關係嘅話,唔該去[[Special:CreateAccount|開一個新戶口]]或[[Special:UserLogin|登入]],避免喺以後嘅留言會同埋其它用戶混淆。", + "anontalkpagetext": "----\n呢度係位匿名用戶嘅討論頁,佢可能係重未開戶口,或者佢唔想開戶口。\n因此,我哋會用數字表示嘅IP地址嚟代表佢。\n一個IP地址係可以由幾個用戶夾來用。\n如果你係匿名用戶,同覺得呢啲留言係同你冇關係嘅話,唔該去[[Special:CreateAccount|開一個新戶口]]或[[Special:UserLogin|登入]],避免喺以後嘅留言會同埋其它用戶混淆。", "noarticletext": "喺呢一頁而家並冇任何嘅文字,你可以喺其它嘅頁面中[[Special:Search/{{PAGENAME}}|搵呢一頁嘅標題]],\n[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搵有關嘅日誌],\n或者[{{fullurl:{{FULLPAGENAME}}|action=edit}} 編輯呢一版]。", "noarticletext-nopermission": "呢一頁而家冇任何文字,你可以喺其它嘅頁面中[[Special:Search/{{PAGENAME}}|搵呢一頁嘅標題]],或者[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 搵有關嘅日誌]。", "missing-revision": "The revision #$1 of the page named \"{{FULLPAGENAME}}\" does not exist.\n\nThis is usually caused by following an outdated history link to a page that has been deleted.\nDetails can be found in the [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log].\n\n《{{FULLPAGENAME}}》嘅編輯#$1唔存在。\n\n恁通常係因為一條過徂時嘅鏈接帶徂閣下去一個已經刪除徂嘅版。\n詳情請查閱[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} 刪文紀錄]。", @@ -1347,7 +1347,7 @@ "rcfilters-savedqueries-add-new-title": "儲存而家個篩選條件設定", "rcfilters-restore-default-filters": "恢復返預設篩選條件", "rcfilters-clear-all-filters": "清走全部篩選條件", - "rcfilters-show-new-changes": "睇吓最新嘅改動", + "rcfilters-show-new-changes": "睇下自$1以來新嘅改動", "rcfilters-search-placeholder": "篩選條件最近改動(瀏覽或者開始輸入)", "rcfilters-invalid-filter": "無效嘅篩選條件", "rcfilters-empty-filter": "無用到篩選條件。顯示晒全部貢獻。", @@ -2285,8 +2285,8 @@ "blockip": "封鎖{{GENDER:$1|用戶}}", "blockiptext": "使用以下嘅表格嚟去阻止指定嘅IP地址或用戶名嘅寫權限。\n僅當僅當為咗避免有版畀人惡意破壞嘅時候先可以使用,而且唔可以違反[[{{MediaWiki:Policy-url}}|政策]]。\n喺下面填寫阻止嘅確切原因(比如:引用咗某啲已經破壞咗嘅頁面)。\n你可以用[https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR]語法格式封鎖 IP 範圍,IPv4最大容許範圍係 /$1,IPv6就係 /$2。", "ipaddressorusername": "IP地址或用戶名:", - "ipbreason": "原因:", - "ipbreason-dropdown": "*共用封鎖原因\n** 插入錯嘅資料\n** 響頁面度拎走\n** 亂加入外部連結\n** 響頁度加入冇意義嘅嘢\n** 嚇人/騷擾\n** 濫用多個戶口\n** 唔能夠接受嘅用戶名", + "ipbreason": "原因:", + "ipbreason-dropdown": "*常用封鎖原因\n** 加入錯嘅資料\n** 響頁面度拎走内容\n** 亂加入外部連結\n** 響頁度加入冇意義嘅嘢\n** 嚇人/騷擾\n** 濫用多個戶口\n** 唔能夠接受嘅用戶名", "ipb-hardblock": "唔畀簽到用戶用呢個IP位址去改文", "ipbcreateaccount": "防止開新戶口", "ipbemailban": "防止用戶傳送電郵", diff --git a/languages/i18n/zh-hans.json b/languages/i18n/zh-hans.json index a7183f32be..c872c4b6a8 100644 --- a/languages/i18n/zh-hans.json +++ b/languages/i18n/zh-hans.json @@ -115,7 +115,8 @@ "94rain", "Viztor", "Ps2049", - "Suchichi02" + "Suchichi02", + "神樂坂秀吉" ] }, "tog-underline": "链接下划线:", @@ -280,7 +281,7 @@ "history": "页面历史", "history_short": "历史", "history_small": "历史", - "updatedmarker": "更新于我上次访问后", + "updatedmarker": "更新于您上次访问后", "printableversion": "可打印版本", "permalink": "固定链接", "print": "打印", @@ -754,6 +755,7 @@ "autoblockedtext": "您的IP地址因曾被一位被$1封禁的用户使用而被自动封禁。封禁原因:\n\n:$2\n\n* 开始时间:$8\n* 到期时间:$6\n* 目标用户:$7\n\n您可以联系$1或其他[[{{MediaWiki:Grouppage-sysop}}|管理员]]申诉该封禁。\n\n请注意,只有当您已在[[Special:Preferences|系统设置]]确认了电子邮件地址且未被禁止使用“{{int:emailuser}}”功能时,才能发送电子邮件联系管理员。\n\n您当前的IP地址为$3,该封禁ID为#$5。请在您做出的任何查询中包含所有上述详情。", "systemblockedtext": "您的用户名或IP地址已被MediaWiki自动封禁。封禁原因:\n\n:$2\n\n* 开始时间:$8\n* 到期时间:$6\n* 目标用户:$7\n\n您当前的IP地址是$3。请在您做出的任何查询中包含所有上述详情。", "blockednoreason": "未给出原因", + "blockedtext-composite": "您的用户名或IP地址已被封禁。封禁原因:\n\n:$2\n\n* 开始时间:$8\n* 到期时间:$6\n\n您当前的IP地址是$3。请在您做出的任何查询中包含所有上述详情。", "whitelistedittext": "请$1以编辑页面。", "confirmedittext": "您必须确认您的电子邮件地址才能编辑页面。请通过[[Special:Preferences|系统设置]]设置并确认您的电子邮件地址。", "nosuchsectiontitle": "没有这个段落", @@ -3932,6 +3934,8 @@ "restrictionsfield-help": "每行一个IP地址或CIDR段。要启用任何地址或地址段,可使用:
0.0.0.0/0\n::/0
", "edit-error-short": "错误:$1", "edit-error-long": "错误:\n\n$1", + "specialmute": "屏蔽", + "specialmute-submit": "确认", "revid": "修订版本$1", "pageid": "页面ID$1", "interfaceadmin-info": "$1\n\n编辑全站CSS/JS/JSON文件的权限刚刚从editinterface权限中拆分。如果您不知道为何收到此错误,请参见[[mw:MediaWiki_1.32/interface-admin]]。", @@ -3961,5 +3965,5 @@ "passwordpolicies-policyflag-suggestchangeonlogin": "建议在登录时更改", "easydeflate-invaliddeflate": "提供的内容未被适当缩小", "unprotected-js": "基于安全原因,JavaScript不能在未保护页面中载入。请在“MediaWiki:”名字空间或者用户子页面中添加JavaScript。", - "userlogout-continue": "如果你希望登出请[$1 点这里]。" + "userlogout-continue": "您确定要登出吗?" } diff --git a/languages/i18n/zh-hant.json b/languages/i18n/zh-hant.json index c01c98a3fb..39d9be727a 100644 --- a/languages/i18n/zh-hant.json +++ b/languages/i18n/zh-hant.json @@ -740,6 +740,8 @@ "autoblockedtext": "因先前的另一位使用者被 $1 封鎖,您的 IP 位址已被自動封鎖。\n原因是:\n\n:$2\n\n* 封鎖開始時間:$8\n* 封鎖結束時間:$6\n* 相關封鎖對象:$7\n\n您可以聯絡 $1 或其他的 [[{{MediaWiki:Grouppage-sysop}}|管理員]] 討論封鎖的相關問題。\n若您已在 [[Special:Preferences|偏好設定]] 中設定了一個有效的電子郵件地址,且尚未被封鎖郵件功能,則您可透過 \"{{int:emailuser}}\" 的功能來聯絡相關管理員。\n您目前的 IP 位址是 $3,此次封鎖的 ID 為 #$5。\n請您在詢問時附註以上詳細資料。", "systemblockedtext": "您的使用者名稱或 IP 位址已被 MediaWiki 自動封鎖,原因如下:\n\n:$2\n\n* 封鎖開始時間:$8\n* 封鎖結束時間:$6\n* 被封鎖的使用者:$7\n\n您目前的 IP 位址為 $3。\n請在做詢問時附上以上資訊。", "blockednoreason": "未說明原因", + "blockedtext-composite": "您的使用者名稱或 IP 位址已被封鎖。\n\n原因如下:\n\n:$2\n\n* 封鎖開始時間:$8\n* 最長的封鎖結束時間:$6\n\n您目前的 IP 位址為 $3。\n請在做詢問時附上以上資訊。", + "blockedtext-composite-reason": "有多個封鎖目標為您的帳號和/或IP位址", "whitelistedittext": "請先 $1 才可編輯頁面。", "confirmedittext": "在編輯此頁之前您必須確認您的電子郵件地址。\n請透過 [[Special:Preferences|偏好設定]] 設定並驗證您的電子郵件地址。", "nosuchsectiontitle": "找不到章節", @@ -3904,5 +3906,5 @@ "passwordpolicies-policyflag-suggestchangeonlogin": "建議在登入時更改", "easydeflate-invaliddeflate": "提供的內容未被正常的壓縮", "unprotected-js": "基於安全因素,JavaScript 不能從未保護的頁面來載入。請僅在 MediaWiki:命名空間或使用者子頁面中建立 JavaScript。", - "userlogout-continue": "若您想要登出請[$1 繼續前至登出頁面]。" + "userlogout-continue": "您想要登出嗎?" } diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 666b28f82e..22313a439f 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -447,6 +447,7 @@ $specialPageAliases = [ 'Mostlinkedtemplates' => [ 'MostTranscludedPages', 'MostLinkedTemplates', 'MostUsedTemplates' ], 'Mostrevisions' => [ 'MostRevisions' ], 'Movepage' => [ 'MovePage' ], + 'Mute' => [ 'Mute' ], 'Mycontributions' => [ 'MyContributions' ], 'MyLanguage' => [ 'MyLanguage' ], 'Mypage' => [ 'MyPage' ], diff --git a/maintenance/findHooks.php b/maintenance/findHooks.php index a902397391..6d5dda1bfc 100644 --- a/maintenance/findHooks.php +++ b/maintenance/findHooks.php @@ -82,7 +82,7 @@ class FindHooks extends Maintenance { "$IP/", ]; $extraFiles = [ - "$IP/tests/phpunit/MediaWikiTestCase.php", + "$IP/tests/phpunit/MediaWikiIntegrationTestCase.php", ]; foreach ( $recurseDirs as $dir ) { diff --git a/maintenance/includes/TextPassDumper.php b/maintenance/includes/TextPassDumper.php index eaed7ed2fa..b37fec188e 100644 --- a/maintenance/includes/TextPassDumper.php +++ b/maintenance/includes/TextPassDumper.php @@ -281,7 +281,7 @@ TEXT $this->finalOptionCheck(); // we only want this so we know how to close a stream :-P - $this->xmlwriterobj = new XmlDumpWriter(); + $this->xmlwriterobj = new XmlDumpWriter( XmlDumpWriter::WRITE_CONTENT, $this->schemaVersion ); $input = fopen( $this->input, "rt" ); $this->readDump( $input ); diff --git a/maintenance/sqlite/archives/patch-pagelinks-fix-pk.sql b/maintenance/sqlite/archives/patch-pagelinks-fix-pk.sql index 0e845865dc..40fd51fad5 100644 --- a/maintenance/sqlite/archives/patch-pagelinks-fix-pk.sql +++ b/maintenance/sqlite/archives/patch-pagelinks-fix-pk.sql @@ -13,8 +13,8 @@ CREATE TABLE /*_*/pagelinks_tmp ( PRIMARY KEY (pl_from,pl_namespace,pl_title) ) /*$wgDBTableOptions*/; -INSERT INTO /*_*/pagelinks_tmp - SELECT * FROM /*_*/pagelinks; +INSERT INTO /*_*/pagelinks_tmp (pl_from, pl_from_namespace, pl_namespace, pl_title) + SELECT pl_from, pl_from_namespace, pl_namespace, pl_title FROM /*_*/pagelinks; DROP TABLE /*_*/pagelinks; diff --git a/maintenance/sqlite/archives/patch-templatelinks-fix-pk.sql b/maintenance/sqlite/archives/patch-templatelinks-fix-pk.sql index 5f09f60d3a..e9bbab8e9c 100644 --- a/maintenance/sqlite/archives/patch-templatelinks-fix-pk.sql +++ b/maintenance/sqlite/archives/patch-templatelinks-fix-pk.sql @@ -13,8 +13,8 @@ CREATE TABLE /*_*/templatelinks_tmp ( PRIMARY KEY (tl_from,tl_namespace,tl_title) ) /*$wgDBTableOptions*/; -INSERT INTO /*_*/templatelinks_tmp - SELECT * FROM /*_*/templatelinks; +INSERT INTO /*_*/templatelinks_tmp (tl_from, tl_from_namespace, tl_namespace, tl_title) + SELECT tl_from, tl_from_namespace, tl_namespace, tl_title FROM /*_*/templatelinks; DROP TABLE /*_*/templatelinks; diff --git a/maintenance/storage/checkStorage.php b/maintenance/storage/checkStorage.php index 173d741be8..c2fa687432 100644 --- a/maintenance/storage/checkStorage.php +++ b/maintenance/storage/checkStorage.php @@ -45,6 +45,7 @@ if ( !defined( 'MEDIAWIKI' ) ) { class CheckStorage { const CONCAT_HEADER = 'O:27:"concatenatedgziphistoryblob"'; public $oldIdMap, $errors; + /** @var ExternalStoreDB */ public $dbStore = null; public $errorDescriptions = [ @@ -223,7 +224,8 @@ class CheckStorage { // Check external normal blobs for existence if ( count( $externalNormalBlobs ) ) { if ( is_null( $this->dbStore ) ) { - $this->dbStore = new ExternalStoreDB; + $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory(); + $this->dbStore = $esFactory->getStore( 'DB' ); } foreach ( $externalConcatBlobs as $cluster => $xBlobIds ) { $blobIds = array_keys( $xBlobIds ); @@ -422,7 +424,8 @@ class CheckStorage { } if ( is_null( $this->dbStore ) ) { - $this->dbStore = new ExternalStoreDB; + $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory(); + $this->dbStore = $esFactory->getStore( 'DB' ); } foreach ( $externalConcatBlobs as $cluster => $oldIds ) { diff --git a/maintenance/storage/compressOld.php b/maintenance/storage/compressOld.php index d3e9ce2cf6..beb1975fad 100644 --- a/maintenance/storage/compressOld.php +++ b/maintenance/storage/compressOld.php @@ -188,7 +188,9 @@ class CompressOld extends Maintenance { # Store in external storage if required if ( $extdb !== '' ) { - $storeObj = new ExternalStoreDB; + $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory(); + /** @var ExternalStoreDB $storeObj */ + $storeObj = $esFactory->getStore( 'DB' ); $compress = $storeObj->store( $extdb, $compress ); if ( $compress === false ) { $this->error( "Unable to store object" ); @@ -232,7 +234,9 @@ class CompressOld extends Maintenance { # Set up external storage if ( $extdb != '' ) { - $storeObj = new ExternalStoreDB; + $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory(); + /** @var ExternalStoreDB $storeObj */ + $storeObj = $esFactory->getStore( 'DB' ); } # Get all articles by page_id diff --git a/maintenance/storage/moveToExternal.php b/maintenance/storage/moveToExternal.php index 0b95ba5e68..9554797f44 100644 --- a/maintenance/storage/moveToExternal.php +++ b/maintenance/storage/moveToExternal.php @@ -21,6 +21,8 @@ * @ingroup Maintenance ExternalStorage */ +use MediaWiki\MediaWikiServices; + define( 'REPORTING_INTERVAL', 1 ); if ( !defined( 'MEDIAWIKI' ) ) { @@ -30,21 +32,22 @@ if ( !defined( 'MEDIAWIKI' ) ) { $fname = 'moveToExternal'; - if ( !isset( $args[0] ) ) { - print "Usage: php moveToExternal.php [-s ] [-e ] \n"; + if ( !isset( $args[1] ) ) { + print "Usage: php moveToExternal.php [-s ] [-e ] \n"; exit; } - $cluster = $args[0]; + $type = $args[0]; // e.g. "DB" or "mwstore" + $location = $args[1]; // e.g. "cluster12" or "global-swift" $dbw = wfGetDB( DB_MASTER ); $maxID = $options['e'] ?? $dbw->selectField( 'text', 'MAX(old_id)', '', $fname ); $minID = $options['s'] ?? 1; - moveToExternal( $cluster, $maxID, $minID ); + moveToExternal( $type, $location, $maxID, $minID ); } -function moveToExternal( $cluster, $maxID, $minID = 1 ) { +function moveToExternal( $type, $location, $maxID, $minID = 1 ) { $fname = 'moveToExternal'; $dbw = wfGetDB( DB_MASTER ); $dbr = wfGetDB( DB_REPLICA ); @@ -53,7 +56,9 @@ function moveToExternal( $cluster, $maxID, $minID = 1 ) { $blockSize = 1000; $numBlocks = ceil( $count / $blockSize ); print "Moving text rows from $minID to $maxID to external storage\n"; - $ext = new ExternalStoreDB; + + $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory(); + $extStore = $esFactory->getStore( $type ); $numMoved = 0; for ( $block = 0; $block < $numBlocks; $block++ ) { @@ -108,7 +113,7 @@ function moveToExternal( $cluster, $maxID, $minID = 1 ) { # print "Storing " . strlen( $text ) . " bytes to $url\n"; # print "old_id=$id\n"; - $url = $ext->store( $cluster, $text ); + $url = $extStore->store( $location, $text ); if ( !$url ) { print "Error writing to external storage\n"; exit; diff --git a/maintenance/storage/recompressTracked.php b/maintenance/storage/recompressTracked.php index f17b00c1c8..e6733a184b 100644 --- a/maintenance/storage/recompressTracked.php +++ b/maintenance/storage/recompressTracked.php @@ -69,6 +69,7 @@ class RecompressTracked { public $replicaId = false; public $noCount = false; public $debugLog, $infoLog, $criticalLog; + /** @var ExternalStoreDB */ public $store; private static $optionsWithArgs = [ @@ -109,7 +110,8 @@ class RecompressTracked { foreach ( $options as $name => $value ) { $this->$name = $value; } - $this->store = new ExternalStoreDB; + $esFactory = MediaWikiServices::getInstance()->getExternalStoreFactory(); + $this->store = $esFactory->getStore( 'DB' ); if ( !$this->isChild ) { $GLOBALS['wgDebugLogPrefix'] = "RCT M: "; } elseif ( $this->replicaId !== false ) { diff --git a/maintenance/update.php b/maintenance/update.php index b6c7ae473b..fe405364c9 100755 --- a/maintenance/update.php +++ b/maintenance/update.php @@ -123,6 +123,10 @@ class UpdateMediaWiki extends Maintenance { $this->output( "MediaWiki {$wgVersion} Updater\n\n" ); + foreach ( SpecialVersion::getSoftwareInformation() as $name => $version ) { + $this->output( "{$name}: {$version}\n" ); + } + wfWaitForSlaves(); if ( !$this->hasOption( 'skip-compat-checks' ) ) { diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000000..e160f3b2ac --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,62 @@ + + + + + + + + tests/phpunit/unit + + + tests/phpunit/integration + + + + + Broken + + + + + includes + languages + maintenance + + languages/messages + languages/data/normalize-ar.php + languages/data/normalize-ml.php + + + + + + + + + 50 + + + 50 + + + + + + diff --git a/resources/Resources.php b/resources/Resources.php index b90ead4c45..b228b96134 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -2150,7 +2150,10 @@ return [ ], ], 'mediawiki.special.pageLanguage' => [ - 'scripts' => 'resources/src/mediawiki.special.pageLanguage.js', + 'scripts' => [ + 'resources/src/mediawiki.special.mute.js', + 'resources/src/mediawiki.special.pageLanguage.js' + ], 'dependencies' => [ 'oojs-ui-core', ], @@ -2822,7 +2825,6 @@ return [ 'scripts' => [ 'resources/lib/html5shiv/html5shiv.js' ], - 'raw' => true, ], /* EasyDeflate */ @@ -2899,7 +2901,6 @@ return [ 'oojs-ui-core.styles', 'oojs-ui-core.icons', 'oojs-ui.styles.indicators', - 'oojs-ui.styles.textures', 'mediawiki.language', ], 'messages' => [ @@ -3009,10 +3010,6 @@ return [ 'class' => ResourceLoaderOOUIImageModule::class, 'themeImages' => 'indicators', ], - 'oojs-ui.styles.textures' => [ - 'class' => ResourceLoaderOOUIImageModule::class, - 'themeImages' => 'textures', - ], 'oojs-ui.styles.icons-accessibility' => [ 'class' => ResourceLoaderOOUIImageModule::class, 'themeImages' => 'icons-accessibility', diff --git a/resources/lib/foreign-resources.yaml b/resources/lib/foreign-resources.yaml index d737cbe2ba..3e14b4835f 100644 --- a/resources/lib/foreign-resources.yaml +++ b/resources/lib/foreign-resources.yaml @@ -249,8 +249,8 @@ oojs-router: ooui: type: tar - src: https://registry.npmjs.org/oojs-ui/-/oojs-ui-0.32.1.tgz - integrity: sha384-p2Y1dRD73TIYZFFhvRu1GtOrklANhT/DBJavX9YIN5aystf5hL5KYj4i1QDY/gCW + src: https://registry.npmjs.org/oojs-ui/-/oojs-ui-0.33.0.tgz + integrity: sha384-/oGS1QAz6AStnSlOzdjntrAvhqPkMQMDBCzG403tEo+ufJ6BUTTW8NElUb2RXd5z dest: # Main stuff diff --git a/resources/lib/ooui/History.md b/resources/lib/ooui/History.md index f5e80cccd1..d5b8b5935d 100644 --- a/resources/lib/ooui/History.md +++ b/resources/lib/ooui/History.md @@ -1,4 +1,54 @@ # OOUI Release History +## v0.33.0 / 2019-06-26 +### Breaking changes +* [BREAKING CHANGE] Element: Drop `getJQuery`, unused, useless since approximately 2015 (Ed Sanders) +* [BREAKING CHANGE] Element: Drop support for `$`, deprecated since 2015 (James D. Forrester) +* [BREAKING CHANGE] Make OO.ui.throttle always work asynchronously (David Chan) +* [BREAKING CHANGE] Toolbar: Drop support for unnamed groups, deprecated since v0.27.1 (James D. Forrester) +* [BREAKING CHANGE] core: Drop OO.ui.now(), deprecated since 0.31.1 (James D. Forrester) +* [BREAKING CHANGE] {Icon,Iindicator}Element: Drop get$1Title, deprecated in 0.30.0 (James D. Forrester) +* [BREAKING CHANGE] Drop textures, deprecated since 0.31.1 (James D. Forrester) + +### Features +* Add 'close' action flag and use close icon on mobile (Ed Sanders) +* Add a MessageWidget (Moriel Schottlender) + +### Styles +* Fix positioning of TabSelectWidget gradient (Ed Sanders) +* MessageWidget: Add `box-sizing` rule (Moriel Schottlender) +* ProcessDialog: Increase title size, and align to left on mobile (Volker E.) +* ProcessDialog: Use frameless actions and icons on desktop (Volker E.) +* WikimediaUI theme: Apply primary flag to ButtonWidget (frameless) (Volker E.) +* WikimediaUI theme: Converge appearance of mobile & desktop ProcessDialog (Volker E.) +* WikimediaUI theme: Make ProcessDialog action icon buttons square (Volker E.) +* WikimediaUI theme: Use `bold` for primary tools (Volker E.) +* icons: Create 'unLink' icon (Ed Sanders) +* icons: Use square dot in 'infoFilled' icon (Bartosz Dziewoński) + +### Code +* ActionFieldLayout: Fix `z-index` hack for invalid input element (Bartosz Dziewoński) +* FieldLayout: Use the newly created MessageWidget in notices (Moriel Schottlender) +* Hide tool shortcuts on mobile (Ed Sanders) +* PHP FlaggedElement: Fix `clearFlags()` method (Bartosz Dziewoński) +* ProcessDialog: Keep labels for screen readers on mobile (Volker E.) +* TextInputWidget: Fix Firefox proprietary appearance (Volker E.) +* build: Remove outdated comment (Bartosz Dziewoński) +* build: Update 'WikimediaUI-Base' to latest v0.14.0 and amend variables (Volker E.) +* build: Updating 'mediawiki/mediawiki-codesniffer' to 26.0.0 (libraryupgrader) +* demos: Add matomo/piwik tracking code for page views (Francisco Dans) +* demos: Create Demo.LinkedFieldsetLayout to provide links to demo sections (Ed Sanders) +* demos: Don't add top margin at first child paragraph (Volker E.) +* demos: Don't load Piwik analytics when testing locally (Bartosz Dziewoński) +* demos: Fix Piwik analytics tracking using the wrong URL (Bartosz Dziewoński) +* demos: Fix RTL issues and link/show code positions (Volker E.) +* demos: Fix appearance of TagMultiselect- & NumberInputWidget combo (Volker E.) +* demos: Fix links to sections on mobile (Bartosz Dziewoński) +* demos: Load 'demo.css' early on (Volker E.) +* demos: Style the MessageWidget to fit a smaller width (Moriel Schottlender) +* package-lock.json: npm audit bump (James D. Forrester) +* package.json: Hard-code jsduck fewer times (James D. Forrester) + + ## v0.32.1 / 2019-06-04 ### Features * Add 'helpInline' support to FieldsetLayout (Ed Sanders) diff --git a/resources/lib/ooui/i18n/sc.json b/resources/lib/ooui/i18n/sc.json new file mode 100644 index 0000000000..30221da39a --- /dev/null +++ b/resources/lib/ooui/i18n/sc.json @@ -0,0 +1,20 @@ +{ + "@metadata": { + "authors": [ + "L2212" + ] + }, + "ooui-outline-control-move-down": "Move s'elementu a suta", + "ooui-outline-control-move-up": "Move s'elementu a suta", + "ooui-outline-control-remove": "Boga s'elementu", + "ooui-toolbar-more": "De prus", + "ooui-toolgroup-collapse": "De mancu", + "ooui-item-remove": "Boga", + "ooui-dialog-message-accept": "AB", + "ooui-dialog-message-reject": "Annulla", + "ooui-dialog-process-error": "B'at àpidu carchi problema", + "ooui-dialog-process-retry": "Torra a proare", + "ooui-dialog-process-continue": "Sighi", + "ooui-selectfile-button-select": "Ischerta unu documentu", + "ooui-field-help": "Agiudu" +} diff --git a/resources/lib/ooui/oojs-ui-apex.js b/resources/lib/ooui/oojs-ui-apex.js index 4d24a502fb..52e63df182 100644 --- a/resources/lib/ooui/oojs-ui-apex.js +++ b/resources/lib/ooui/oojs-ui-apex.js @@ -1,12 +1,12 @@ /*! - * OOUI v0.32.1 + * OOUI v0.33.0 * https://www.mediawiki.org/wiki/OOUI * * Copyright 2011–2019 OOUI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2019-06-05T16:24:08Z + * Date: 2019-06-27T03:27:26Z */ ( function ( OO ) { diff --git a/resources/lib/ooui/oojs-ui-core-apex.css b/resources/lib/ooui/oojs-ui-core-apex.css index 7cc8f6754e..f3b05d384b 100644 --- a/resources/lib/ooui/oojs-ui-core-apex.css +++ b/resources/lib/ooui/oojs-ui-core-apex.css @@ -1,12 +1,12 @@ /*! - * OOUI v0.32.1 + * OOUI v0.33.0 * https://www.mediawiki.org/wiki/OOUI * * Copyright 2011–2019 OOUI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2019-06-05T16:24:16Z + * Date: 2019-06-27T03:27:33Z */ .oo-ui-element-hidden { display: none !important; @@ -541,10 +541,11 @@ margin-left: -1px; } .oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget > .oo-ui-inputWidget-input:hover, -.oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget > .oo-ui-inputWidget-input:hover ~ *, .oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget > .oo-ui-inputWidget-input:focus, +.oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget.oo-ui-flaggedElement-invalid > .oo-ui-inputWidget-input, +.oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget > .oo-ui-inputWidget-input:hover ~ *, .oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget > .oo-ui-inputWidget-input:focus ~ *, -.oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget.oo-ui-flaggedElement-invalid > .oo-ui-inputWidget-input { +.oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget.oo-ui-flaggedElement-invalid > .oo-ui-inputWidget-input ~ * { z-index: 1; } .oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-button > .oo-ui-buttonElement > .oo-ui-buttonElement-button:hover, @@ -766,6 +767,54 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { font-size: 0.9375em; } +.oo-ui-messageWidget { + position: relative; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0.3125em 0.78125em; + font-weight: bold; +} +.oo-ui-messageWidget .oo-ui-labelElement-label { + display: block; +} +.oo-ui-messageWidget.oo-ui-messageWidget-block { + border: 1px solid; + padding: 1.5625em 1.875em; +} +.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-error { + background-color: #ffdcdc; + border-color: #d45353; +} +.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-warning { + background-color: #fff8c6; + border-color: #b85c00; +} +.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-success { + background-color: #d5fdd6; + border-color: #34782b; +} +.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-notice { + background-color: #fafafa; + border-color: #ccc; +} +.oo-ui-messageWidget.oo-ui-flaggedElement-error { + color: #d45353; +} +.oo-ui-messageWidget.oo-ui-flaggedElement-success:not( .oo-ui-messageWidget-block ) { + color: #34782b; +} +.oo-ui-messageWidget .oo-ui-iconElement-icon { + display: block; + float: left; + margin: 0; +} +.oo-ui-messageWidget .oo-ui-labelElement-label { + margin-top: 0.15625em; + margin-left: 2.5em; + line-height: 1.4; +} + .oo-ui-iconWidget { vertical-align: middle; -webkit-touch-callout: none; @@ -1172,15 +1221,13 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { } .oo-ui-textInputWidget .oo-ui-inputWidget-input { -webkit-appearance: none; + -moz-appearance: textfield; display: block; width: 100%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } -.oo-ui-textInputWidget input { - -moz-appearance: textfield; -} .oo-ui-textInputWidget input::-ms-clear { display: none; } diff --git a/resources/lib/ooui/oojs-ui-core-wikimediaui.css b/resources/lib/ooui/oojs-ui-core-wikimediaui.css index 6f2c914df0..cfe6f8d209 100644 --- a/resources/lib/ooui/oojs-ui-core-wikimediaui.css +++ b/resources/lib/ooui/oojs-ui-core-wikimediaui.css @@ -1,12 +1,12 @@ /*! - * OOUI v0.32.1 + * OOUI v0.33.0 * https://www.mediawiki.org/wiki/OOUI * * Copyright 2011–2019 OOUI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2019-06-05T16:24:16Z + * Date: 2019-06-27T03:27:33Z */ .oo-ui-element-hidden { display: none !important; @@ -212,6 +212,52 @@ color: #b32424; box-shadow: none; } +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button { + color: #fff; + background-color: #36c; + border-color: #36c; +} +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:hover { + background-color: #447ff5; + border-color: #447ff5; +} +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:active, +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:active:focus, +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button, +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive.oo-ui-buttonElement-active > .oo-ui-buttonElement-button, +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive.oo-ui-popupToolGroup-active > .oo-ui-buttonElement-button { + color: #fff; + background-color: #2a4b8d; + border-color: #2a4b8d; + box-shadow: none; +} +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button:focus { + border-color: #36c; + box-shadow: inset 0 0 0 1px #36c, inset 0 0 0 2px #fff; +} +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button { + color: #fff; + background-color: #d33; + border-color: #d33; +} +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover { + background-color: #ff4242; + border-color: #ff4242; +} +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active, +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active:focus, +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button, +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-active > .oo-ui-buttonElement-button, +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-popupToolGroup-active > .oo-ui-buttonElement-button { + color: #fff; + background-color: #b32424; + border-color: #b32424; + box-shadow: none; +} +.oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:focus { + border-color: #d33; + box-shadow: inset 0 0 0 1px #d33, inset 0 0 0 2px #fff; +} .oo-ui-buttonElement-frameless.oo-ui-widget-enabled[class*='oo-ui-flaggedElement'] > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon, .oo-ui-buttonElement-frameless.oo-ui-widget-enabled[class*='oo-ui-flaggedElement'] > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator { opacity: 1; @@ -618,7 +664,7 @@ box-sizing: border-box; max-width: 50em; margin: 0; - padding: 0.28571429em 0.85714286em; + padding: 0.28571429em 0; } .oo-ui-fieldLayout-messages > [class|='oo-ui-fieldLayout-messages'] { color: #000; @@ -675,10 +721,11 @@ margin-left: 0.14285714em; } .oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget > .oo-ui-inputWidget-input:hover, -.oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget > .oo-ui-inputWidget-input:hover ~ *, .oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget > .oo-ui-inputWidget-input:focus, +.oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget.oo-ui-flaggedElement-invalid > .oo-ui-inputWidget-input, +.oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget > .oo-ui-inputWidget-input:hover ~ *, .oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget > .oo-ui-inputWidget-input:focus ~ *, -.oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget.oo-ui-flaggedElement-invalid > .oo-ui-inputWidget-input { +.oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-input > .oo-ui-textInputWidget.oo-ui-flaggedElement-invalid > .oo-ui-inputWidget-input ~ * { z-index: 1; } .oo-ui-actionFieldLayout .oo-ui-actionFieldLayout-button > .oo-ui-buttonElement > .oo-ui-buttonElement-button:hover, @@ -918,6 +965,47 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { font-size: 0.92857143em; } +.oo-ui-messageWidget { + position: relative; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0.28571429em 0.85714286em; + font-weight: bold; +} +.oo-ui-messageWidget .oo-ui-labelElement-label { + display: block; +} +.oo-ui-messageWidget.oo-ui-messageWidget-block { + border: 1px solid; + padding: 1.42857143em 1.71428571em; +} +.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-error { + background-color: #fee7e6; + border-color: #d33; +} +.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-warning { + background-color: #fef6e7; + border-color: #fc3; +} +.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-success { + background-color: #d5fdf4; + border-color: #14866d; +} +.oo-ui-messageWidget.oo-ui-messageWidget-block.oo-ui-flaggedElement-notice { + background-color: #eaecf0; + border-color: #a2a9b1; +} +.oo-ui-messageWidget.oo-ui-flaggedElement-error { + color: #d33; +} +.oo-ui-messageWidget.oo-ui-flaggedElement-success:not( .oo-ui-messageWidget-block ) { + color: #14866d; +} +.oo-ui-messageWidget .oo-ui-labelElement-label { + margin-left: 2em; +} + .oo-ui-iconWidget { vertical-align: middle; -webkit-touch-callout: none; @@ -1550,15 +1638,13 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { } .oo-ui-textInputWidget .oo-ui-inputWidget-input { -webkit-appearance: none; + -moz-appearance: textfield; display: block; width: 100%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } -.oo-ui-textInputWidget input { - -moz-appearance: textfield; -} .oo-ui-textInputWidget input::-ms-clear { display: none; } diff --git a/resources/lib/ooui/oojs-ui-core.js b/resources/lib/ooui/oojs-ui-core.js index f78f4e7cb1..b7f83a3a63 100644 --- a/resources/lib/ooui/oojs-ui-core.js +++ b/resources/lib/ooui/oojs-ui-core.js @@ -1,12 +1,12 @@ /*! - * OOUI v0.32.1 + * OOUI v0.33.0 * https://www.mediawiki.org/wiki/OOUI * * Copyright 2011–2019 OOUI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2019-06-05T16:24:08Z + * Date: 2019-06-27T03:27:26Z */ ( function ( OO ) { @@ -292,7 +292,7 @@ OO.ui.warnDeprecation = function ( message ) { */ OO.ui.throttle = function ( func, wait ) { var context, args, timeout, - previous = 0, + previous = Date.now() - wait, run = function () { timeout = null; previous = Date.now(); @@ -304,33 +304,17 @@ OO.ui.throttle = function ( func, wait ) { // period. If it's less, run the function immediately. If it's more, // set a timeout for the remaining time -- but don't replace an // existing timeout, since that'd indefinitely prolong the wait. - var remaining = wait - ( Date.now() - previous ); + var remaining = Math.max( wait - ( Date.now() - previous ), 0 ); context = this; args = arguments; - if ( remaining <= 0 ) { - // Note: unless wait was ridiculously large, this means we'll - // automatically run the first time the function was called in a - // given period. (If you provide a wait period larger than the - // current Unix timestamp, you *deserve* unexpected behavior.) - clearTimeout( timeout ); - run(); - } else if ( !timeout ) { + if ( !timeout ) { + // If time is up, do setTimeout( run, 0 ) so the function + // always runs asynchronously, just like Promise#then . timeout = setTimeout( run, remaining ); } }; }; -/** - * A (possibly faster) way to get the current timestamp as an integer. - * - * @deprecated Since 0.31.1; use `Date.now()` instead. - * @return {number} Current timestamp, in milliseconds since the Unix epoch - */ -OO.ui.now = function () { - OO.ui.warnDeprecation( 'OO.ui.now() is deprecated, use Date.now() instead' ); - return Date.now(); -}; - /** * Reconstitute a JavaScript object corresponding to a widget created by * the PHP implementation. @@ -613,10 +597,6 @@ OO.ui.Element = function OoUiElement( config ) { config = config || {}; // Properties - this.$ = function () { - OO.ui.warnDeprecation( 'this.$ is deprecated, use global $ instead' ); - return $.apply( this, arguments ); - }; this.elementId = null; this.visible = true; this.data = config.data; @@ -893,29 +873,6 @@ OO.ui.Element.static.gatherPreInfuseState = function () { return {}; }; -/** - * Get a jQuery function within a specific document. - * - * @static - * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to - * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is - * not in an iframe - * @return {Function} Bound jQuery function - */ -OO.ui.Element.static.getJQuery = function ( context, $iframe ) { - function wrapper( selector ) { - return $( selector, wrapper.context ); - } - - wrapper.context = this.getDocument( context ); - - if ( $iframe ) { - wrapper.$iframe = $iframe; - } - - return wrapper; -}; - /** * Get the document of an element. * @@ -3052,16 +3009,6 @@ OO.ui.mixin.IconElement.prototype.getIcon = function () { return this.icon; }; -/** - * Get the icon title. The title text is displayed when a user moves the mouse over the icon. - * - * @return {string} Icon title text - * @deprecated - */ -OO.ui.mixin.IconElement.prototype.getIconTitle = function () { - return this.iconTitle; -}; - /** * IndicatorElement is often mixed into other classes to generate an indicator. * Indicators are small graphics that are generally used in two ways: @@ -3196,18 +3143,6 @@ OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () { return this.indicator; }; -/** - * Get the indicator title. - * - * The title is displayed when a user moves the mouse over the indicator. - * - * @return {string} Indicator title text - * @deprecated - */ -OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () { - return this.indicatorTitle; -}; - /** * The FlaggedElement class is an attribute mixin, meaning that it is used to add * additional functionality to an element created by another class. The class provides @@ -4274,6 +4209,125 @@ OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement ); */ OO.ui.LabelWidget.static.tagName = 'label'; +/** + * MessageWidget produces a visual component for sending a notice to the user + * with an icon and distinct design noting its purpose. The MessageWidget changes + * its visual presentation based on the type chosen, which also denotes its UX + * purpose. + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.mixin.IconElement + * @mixins OO.ui.mixin.LabelElement + * @mixins OO.ui.mixin.TitledElement + * @mixins OO.ui.mixin.FlaggedElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [type='notice'] The type of the notice widget. This will also + * impact the flags that the widget receives (and hence its CSS design) as well + * as the icon that appears. Available types: + * 'notice', 'error', 'warning', 'success' + * @cfg {boolean} [inline] Set the notice as an inline notice. The default + * is not inline, or 'boxed' style. + */ +OO.ui.MessageWidget = function OoUiMessageWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.MessageWidget.parent.call( this, config ); + + // Mixin constructors + OO.ui.mixin.IconElement.call( this, config ); + OO.ui.mixin.LabelElement.call( this, config ); + OO.ui.mixin.TitledElement.call( this, config ); + OO.ui.mixin.FlaggedElement.call( this, config ); + + // Set type + this.setType( config.type ); + this.setInline( config.inline ); + + // Build the widget + this.$element + .append( this.$icon, this.$label ) + .addClass( 'oo-ui-messageWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.MessageWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.MessageWidget, OO.ui.mixin.IconElement ); +OO.mixinClass( OO.ui.MessageWidget, OO.ui.mixin.LabelElement ); +OO.mixinClass( OO.ui.MessageWidget, OO.ui.mixin.TitledElement ); +OO.mixinClass( OO.ui.MessageWidget, OO.ui.mixin.FlaggedElement ); + +/* Static Properties */ + +/** + * An object defining the icon name per defined type. + * + * @static + * @property {Object} + */ +OO.ui.MessageWidget.static.iconMap = { + notice: 'infoFilled', + error: 'error', + warning: 'alert', + success: 'check' +}; + +/* Methods */ + +/** + * Set the inline state of the widget. + * + * @param {boolean} inline Widget is inline + */ +OO.ui.MessageWidget.prototype.setInline = function ( inline ) { + inline = !!inline; + + if ( this.inline !== inline ) { + this.inline = inline; + this.$element + .toggleClass( 'oo-ui-messageWidget-block', !this.inline ); + } +}; +/** + * Set the widget type. The given type must belong to the list of + * legal types set by OO.ui.MessageWidget.static.iconMap + * + * @param {string} [type] Given type. Defaults to 'notice' + */ +OO.ui.MessageWidget.prototype.setType = function ( type ) { + // Validate type + if ( Object.keys( this.constructor.static.iconMap ).indexOf( type ) === -1 ) { + type = 'notice'; // Default + } + + if ( this.type !== type ) { + + // Flags + this.clearFlags(); + this.setFlags( type ); + + // Set the icon and its variant + this.setIcon( this.constructor.static.iconMap[ type ] ); + this.$icon.removeClass( 'oo-ui-image-' + this.type ); + this.$icon.addClass( 'oo-ui-image-' + type ); + + if ( type === 'error' ) { + this.$element.attr( 'role', 'alert' ); + this.$element.removeAttr( 'aria-live' ); + } else { + this.$element.removeAttr( 'role' ); + this.$element.attr( 'aria-live', 'polite' ); + } + + this.type = type; + } +}; + /** * PendingElement is a mixin that is used to create elements that notify users that something is * happening and that they should wait before proceeding. The pending state is visually represented @@ -11986,7 +12040,7 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) { this.successMessages = []; this.notices = []; this.$field = this.isFieldInline() ? $( '' ) : $( '
' ); - this.$messages = $( '